A day agenda with native date entry, adjacent-day controls, and accessible event times.
Agenda
Wednesday, September 30
Team check-in
Review priorities for the week
Design review
Dashboard and mobile flows
ProductRelease planning
Milestone
import { CalendarAgenda } from "@/components/ballmac/calendar-agenda"
const events = [
{
id: "a",
date: "2026-09-30",
time: "09:30",
title: "Team check-in",
description: "Review priorities for the week",
},
{
id: "b",
date: "2026-09-30",
time: "13:00",
title: "Design review",
description: "Dashboard and mobile flows",
label: "Product",
},
{
id: "c",
date: "2026-09-30",
time: "16:15",
title: "Release planning",
label: "Milestone",
},
{ id: "d", date: "2026-10-01", time: "11:00", title: "Customer call" },
]
export default function CalendarAgendaDemo() {
return (
<CalendarAgenda
className="w-full max-w-sm"
events={events}
defaultValue="2026-09-30"
/>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/calendar-agendaInstall the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/calendar-agenda.tsx// Ballmac UI: Calendar Agenda. https://ui.ballmac.com/components/calendar-agenda "use client" import * as React from "react" import { ChevronLeft, ChevronRight } from "lucide-react" import { cn } from "@/lib/utils" import { useLocale, useMessages } from "@/lib/ballmac/i18n" type AgendaEvent = { /** Stable event ID. */ id: string /** ISO date (YYYY-MM-DD). */ date: string /** 24-hour time (HH:mm), or omitted for all-day. */ time?: string /** Event title. */ title: string /** Optional supporting detail. */ description?: string /** Optional category. */ label?: string } type CalendarAgendaProps = Omit<React.ComponentProps<"div">, "defaultValue"> & { /** Agenda events to display. */ events: AgendaEvent[] /** Controlled selected ISO date. */ value?: string /** Initial selected ISO date. */ defaultValue?: string /** Called when the selected date changes. */ onValueChange?: (date: string) => void /** Fixed locale for date and time labels. */ locale?: string /** Empty-day message. */ emptyMessage?: string } function shiftDay(date: string, days: number) { const parsed = new Date(`${date}T00:00:00Z`) parsed.setUTCDate(parsed.getUTCDate() + days) return parsed.toISOString().slice(0, 10) } function CalendarAgenda({ className, events, value, defaultValue, onValueChange, locale, emptyMessage, ...props }: CalendarAgendaProps) { const defaultLocale = useLocale() locale ??= defaultLocale const msg = useMessages() emptyMessage ??= msg("calendar-agenda.emptyMessage", "No events scheduled") const [internal, setInternal] = React.useState( defaultValue ?? events[0]?.date ?? "", ) const selected = value ?? internal const day = selected ? new Date(`${selected}T00:00:00Z`) : null const heading = day && !Number.isNaN(day.getTime()) ? new Intl.DateTimeFormat(locale, { weekday: "long", month: "long", day: "numeric", timeZone: "UTC", }).format(day) : "Choose a date" const daily = events .filter((event) => event.date === selected) .sort((a, b) => (a.time ?? "").localeCompare(b.time ?? "")) function choose(date: string) { if (value === undefined) setInternal(date) onValueChange?.(date) } return ( <div data-slot="calendar-agenda" className={cn( "bg-card min-w-0 rounded-xl border border-border p-4 shadow-sm", className, )} {...props} > <div className="flex flex-wrap items-center justify-between gap-3 border-b border-border pb-4"> <div> <p className="text-muted-foreground text-xs font-medium uppercase tracking-wide"> {msg("calendar-agenda.agenda", "Agenda")} </p> <h3 className="mt-1 text-base font-semibold">{heading}</h3> </div> <div className="flex items-center gap-1"> <button type="button" aria-label={msg("calendar-agenda.previousDay", "Previous day")} disabled={!selected} onClick={() => choose(shiftDay(selected, -1))} className="hover:bg-accent flex size-8 items-center justify-center rounded-md outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-40" > <ChevronLeft aria-hidden="true" className="size-4 rtl:rotate-180" /> </button> <input type="date" aria-label={msg("calendar-agenda.chooseDate", "Choose date")} value={selected} onChange={(event) => choose(event.target.value)} className="border-input bg-background h-8 max-w-32 rounded-md border px-1 text-xs outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50" /> <button type="button" aria-label={msg("calendar-agenda.nextDay", "Next day")} disabled={!selected} onClick={() => choose(shiftDay(selected, 1))} className="hover:bg-accent flex size-8 items-center justify-center rounded-md outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-40" > <ChevronRight aria-hidden="true" className="size-4 rtl:rotate-180" /> </button> </div> </div> <ol className="mt-3 flex flex-col divide-y divide-border"> {daily.length ? ( daily.map((event) => ( <li key={event.id} data-slot="calendar-agenda-event" className="grid grid-cols-[4rem_minmax(0,1fr)] gap-3 py-3" > <time dateTime={ event.time ? `${event.date}T${event.time}` : event.date } className="text-muted-foreground pt-0.5 text-xs tabular-nums" > {event.time ? new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit", timeZone: "UTC", }).format(new Date(`${event.date}T${event.time}:00Z`)) : "All day"} </time> <div className="min-w-0 border-s-2 border-primary ps-3"> <p className="text-sm font-medium">{event.title}</p> {event.description && ( <p className="text-muted-foreground mt-1 text-xs"> {event.description} </p> )} {event.label && ( <span className="text-primary mt-1 block text-xs font-medium"> {event.label} </span> )} </div> </li> )) ) : ( <li data-slot="calendar-agenda-empty" className="text-muted-foreground py-8 text-center text-sm" > {emptyMessage} </li> )} </ol> </div> ) } export { CalendarAgenda, type CalendarAgendaProps, type AgendaEvent }Update the import paths to match your project setup.
Usage
import { CalendarAgenda } from "@/components/ballmac/calendar-agenda"The full example is in the Code tab above.
Examples
States and variants
Agenda
Friday, October 2
- A clear day to focus on deep work.
import { CalendarAgenda } from "@/components/ballmac/calendar-agenda"
export default function CalendarAgendaStates() {
return (
<CalendarAgenda
className="w-full max-w-sm"
events={[]}
defaultValue="2026-10-02"
emptyMessage="A clear day to focus on deep work."
/>
)
}API reference
| Prop | Type | Default |
|---|---|---|
events*Agenda events to display. | AgendaEvent[] | — |
valueControlled selected ISO date. | string | — |
defaultValueInitial selected ISO date. | string | — |
onValueChangeCalled when the selected date changes. | (date: string) => void | — |
localeFixed locale for date and time labels. | string | — |
emptyMessageEmpty-day message. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab / Enter / Space | Changes the day through native date input and buttons |
Use with AI
A day agenda with native date entry, adjacent-day controls, and accessible event times. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Calendar Agenda (@ballmac/calendar-agenda) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Show scheduled events for one day
Not for
- Use a full calendar for month-level date picking
Registry JSON: https://ui.ballmac.com/r/calendar-agenda.json
Credits
Free to use in personal and commercial projects.
- npm
- lucide-react
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Timeline
A composable sequence of milestones with current and complete states, dates, and connectors.
Activity Feed
A compact actor and action feed with timestamps and an empty state.
Audio Player
A podcast-style audio player: play and pause, skip back and forward, a scrubber with chapter ticks, a named current chapter and a speed control. Works with a real file or as a silent demo.
Avatar Circles
Overlapping avatars that spread on hover, lift individually with a name and role tooltip, show presence, fall back to initials and end in a +N count that can be a button.