npx shadcn@latest add https://sdk-components.thesqd.com/r/event-calendar.jsonimport { EventCalendar } from "@/components/ui/event-calendar";import { EventCalendar } from "@/components/ui/event-calendar";
const events = [
{
id: "1",
title: "Standup",
description: "Daily sync with the design pod.",
draftDate: "2026-09-01",
estimateMinutes: 30,
createdAt: "2026-08-28T14:02:00",
color: "blue",
user: { id: "1", name: "Avery Diaz" },
},
{
id: "2",
title: "Concept review",
draftDate: "2026-09-01",
estimateMinutes: 90,
createdAt: "2026-08-29T09:14:00",
color: "purple",
user: { id: "2", name: "Jordan Lee" },
},
{
id: "3",
title: "Offsite",
draftDate: "2026-09-03",
estimateMinutes: 2 * 24 * 60,
allDay: true,
color: "yellow",
user: { id: "2", name: "Jordan Lee" },
},
];
export function Example() {
return <EventCalendar events={events} defaultView="month" />;
}EventCalendar (single component — pass everything as props)
├── variant ("calendar" | "workload")
├── events={CalendarEvent[]} (id, title, draftDate | startDate, endDate,
│ estimateMinutes,
│ createdAt, color, allDay, user,
│ statusText, statusColor, url,
│ blocked, blockedBy)
├── view / defaultView ("month" | "week" | "day" | "agenda")
├── views={[…]} (which views the toolbar switcher offers)
├── title / icon / countLabel (the toolbar band's identity — the same band
│ data grid v3 and the board view wear)
├── date / defaultDate (focused date — controlled or uncontrolled)
├── badgeVariant ("dot" | "colored" | "mixed")
├── visibleHours={{ start, end }} (hour range drawn by the week/day grid)
├── workingHours={{ start, end }} (unshaded hours inside the grid)
├── showUserFilter (builds the filter from event.user)
├── onEventClick / onDayClick (omit onEventClick to get the detail popover)
├── eventAction={{ label, icon, onSelect }}
│ (one action per event, as a button in the
│ detail popover)
├── eventActions={[{ label, icon, onSelect, available? }]}
│ (several, in order: the first outlined,
│ the rest quieter; wins over eventAction)
├── weekStartsOn (0 = Sunday, 1 = Monday)
└── workload only ───────────────
├── assignees={WorkloadAssignee[]} (id, name, role, capacityMinutes, capacityByDate)
├── capacityMinutes (default daily capacity, in minutes)
├── tasksPerCell (cards per cell before "+N more")
├── workloadTaskStatusStyle ("tint" | "bar" — status color treatment)
├── collapsedAssignees (collapsed rows, by assignee id)
├── defaultCollapsedAssignees (rows that start collapsed)
├── onCollapsedAssigneesChange (fires with the whole collapsed set)
└── onCapacityChange ({ assignee, date, minutes, scope })| Prop | Type | Default | Description |
|---|---|---|---|
variant | "calendar" | "workload" | "calendar" | `calendar` renders the month/week/day/agenda views. `workload` renders the capacity grid — one row per assignee, one column per day of the week. |
assignees | WorkloadAssignee[] | — | Workload rows — `{ id, name, role?, src?, capacityMinutes?, capacityByDate? }`. Omitted, the rows are derived from `event.user`. |
capacityMinutes | number | 420 | Default daily capacity for the workload grid. Overridden per person by `assignee.capacityMinutes`, and per day by `assignee.capacityByDate["yyyy-MM-dd"]`. |
tasksPerCell | number | 2 | Task cards shown in a workload cell before the rest collapse into “+N more”. |
workloadTaskStatusStyle | "tint" | "bar" | "tint" | How a workload task card carries its status color — `"tint"` washes the card in it, `"bar"` is the alert-v2 left accent stripe (which replaces the status dot). |
collapsedAssignees | string[] | — | Controlled collapsed workload rows, by assignee id. A collapsed row drops its task cards and span bars and keeps one line per person: name, role, and each day's load pill with that day's task count under it. Click a row's caret to toggle. |
defaultCollapsedAssignees | string[] | — | Rows that start collapsed when `collapsedAssignees` is not passed — the grid owns the state from there. |
onCollapsedAssigneesChange | (assigneeIds: string[]) => void | — | Fires with the full set of collapsed assignee ids whenever a row's caret is clicked. |
onCapacityChange | (change: CapacityChange) => void | — | Fires when a capacity edit is saved — `{ assignee, date, minutes, scope }` where scope is `"day"`, `"weekday"` (every same weekday from that date on), or `"all"` (every day from that date on). The grid also applies the edit to its own state, so this is persistence-only. |
events | CalendarEvent[] | — | Events to render — each is a `draftDate` plus an `estimateMinutes` duration. |
view / defaultView | "month" | "week" | "day" | "agenda" | "month" | Active view. Pass `view` + `onViewChange` to control it, or `defaultView` to let the toolbar own it. |
views | EventCalendarView[] | ["month","week","day","agenda"] | Views offered by the toolbar switcher. A single entry hides the switcher. |
title | ReactNode | false | "Calendar" | The toolbar band's identity, left of the controls — the same band data grid v3 and the board view wear. `false` drops the left side and the controls take the width. |
icon | ReactNode | false | <CalendarDots /> (workload: <UsersThree />) | Leading glyph beside the title. `false` drops it. |
countLabel | (count: number) => ReactNode | `${n} events` | The count pill beside the title, given the events the calendar holds after the user filter. Return `null` for no pill. Defaults to "N events", and to "N people" (the rows on the grid) in the workload variant. |
date / defaultDate | Date | new Date() | Focused date. Pair `date` with `onDateChange` for a controlled calendar. |
badgeVariant | "dot" | "colored" | "mixed" | "colored" | `dot` = colored dot + plain label, `colored` = tinted chip, `mixed` = dots in month/agenda and tinted blocks in week/day. |
visibleHours | { start: number; end: number } | { start: 0, end: 24 } | Hour range rendered by the week and day grids. |
workingHours | { start: number; end: number } | — | Hours drawn on the plain background; everything outside is shaded. |
showUserFilter | boolean | false | Adds an all-users Select built from `event.user`, plus an avatar group footer. |
weekStartsOn | 0 | 1 | 0 | 0 = Sunday, 1 = Monday. |
onEventClick | (event: CalendarEvent) => void | — | Given, chips call this instead of opening their built-in detail popover. |
eventAction | { label, icon?, onSelect(event) } | — | One action offered on every event, as a button at the foot of the chip's detail popover. Ignored when onEventClick is given — there is no popover then. Shapeless on purpose: what the action IS belongs to the app. |
eventActions | { label, icon?, onSelect(event), available?(event) }[] | — | Several actions on every event, in order — the first drawn as the main outlined button, the rest as quieter ghost buttons under it. `available` limits one to the events it returns true for (a whole-series action on repeating events only). Wins over eventAction; ignored when onEventClick is given. |
onDayClick | (date: Date) => void | — | Fires when a month cell or week column header is clicked. |
actions | ReactNode | — | The page's own controls (Filters, a ⋯ menu), drawn at the end of the toolbar after the date and view controls, so a page with one header does not draw a second. |
className | string | — | Extends the card surface wrapper. |
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique key. |
title | string | — | Chip label. |
description | string | — | Shown in the detail popover. |
draftDate | Date | string | — | The day the task is drafted on — `"2026-09-01"` or a Date. Date-only: any time component is ignored. (A bare `"yyyy-MM-dd"` string is parsed in local time, so it never slips a day.) |
estimateMinutes | number | 30 | Duration. In the workload grid these sum per assignee per day — that total is what runs against the person's capacity. On a multi-day event it is what the event books on *each* covered day. |
startDate | Date | string | — | First day the event covers — the blockout-shaped alias of `draftDate` (blockouts arrive as a start/end pair). One of the two is required; `startDate` wins if both are given. |
endDate | Date | string | — | Last day the event covers — a week-long block, PTO, a multi-day task. The workload grid repeats the card on every day from `draftDate` through `endDate` and books `estimateMinutes` against each of them; the month/week views draw it as one spanning bar. |
color | "brand" | "blue" | "green" | "red" | "yellow" | "purple" | "orange" | "brand" | Chip tint / dot color. |
allDay | boolean | — | Pins the event to the all-day strip above the week/day grid. Multi-day events land there too. |
createdAt | Date | string | — | Orders tasks within a workload cell, oldest first. Without it the cell falls back to alphabetical by title. |
statusText | string | — | Status label — renders a dot on the workload task card and a pill in its preview. |
statusColor | BadgeColor | `#${string}` | — | Status color: a Badge palette name (`"sky"`, `"coral"`, …) or a literal hex (`"#ffcf00"`), e.g. a ClickUp status color straight off the API. |
blocked | boolean | false | Marks the entry as blocked-out time rather than a real task — a placeholder for PTO, meetings or admin time. Renders as a dashed card labelled with the hours it holds, and still counts against the day's capacity. |
blockedBy | string | — | Who put the block on the calendar. Shown in the placeholder's preview popover. |
url | string | — | Target for the task preview's “Open task” link. Omitted, the link is hidden. External links pick up the global `a[target="_blank"]` ↗ affordance. |
user | { id: string; name: string; src?: string } | — | Owner — powers the user filter and the avatar footer. |