{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calendar-view",
  "title": "Calendar View",
  "description": "A props-driven event calendar with month, week, day and agenda views — capped event chips with an overflow popover, overlap-aware hour-gridded time columns, all-day and multi-day spanning bars, a live current-time line and optional pointer drag-to-move.",
  "dependencies": [
    "@untitledui/icons@^0.0.22",
    "date-fns@^4.4.0"
  ],
  "registryDependencies": [
    "@nx-ui/button",
    "@nx-ui/popover",
    "@nx-ui/select",
    "@nx-ui/toggle-group",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/calendar-view/agenda-view.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  addDays,\n  endOfDay,\n  format,\n  isSameDay,\n  isToday,\n  startOfDay,\n} from \"date-fns\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport type { CalendarEvent, CalendarViewProps } from \"./calendar-types\"\nimport {\n  compareEvents,\n  eventToneClasses,\n  isSpanning,\n  toneOf,\n} from \"./calendar-utils\"\nimport type { CalendarController } from \"./use-calendar-view\"\n\n/** Grouped, scrollable list of every day in range that has events. */\nexport function AgendaView({\n  controller,\n  events,\n  onEventClick,\n}: {\n  controller: CalendarController\n  events: CalendarEvent[]\n} & Pick<CalendarViewProps, \"onEventClick\">) {\n  const { range } = controller\n\n  const groups = React.useMemo(() => {\n    const dayCount =\n      Math.round(\n        (startOfDay(range.end).getTime() - startOfDay(range.start).getTime()) /\n          86_400_000\n      ) + 1\n    return Array.from({ length: dayCount }, (_, i) => {\n      const day = addDays(range.start, i)\n      const dayEvents = events\n        .filter((e) => e.start <= endOfDay(day) && e.end >= startOfDay(day))\n        .sort(compareEvents)\n      return { day, events: dayEvents }\n    }).filter((g) => g.events.length > 0)\n  }, [events, range.start, range.end])\n\n  if (groups.length === 0) {\n    return (\n      <div className=\"flex min-h-40 items-center justify-center p-10 text-center text-sm text-muted-foreground\">\n        No events scheduled in this period.\n      </div>\n    )\n  }\n\n  return (\n    <div className=\"max-h-[34rem] divide-y divide-border overflow-y-auto\">\n      {groups.map(({ day, events: dayEvents }) => (\n        <div key={day.toISOString()} className=\"flex gap-4 p-4 sm:gap-6\">\n          <div className=\"w-16 shrink-0 sm:w-24\">\n            <div\n              className={cn(\n                \"text-2xl font-semibold tabular-nums\",\n                isToday(day) ? \"text-text-brand-secondary\" : \"text-foreground\"\n              )}\n            >\n              {format(day, \"d\")}\n            </div>\n            <div className=\"text-xs font-medium text-muted-foreground\">\n              {format(day, \"EEE\")}\n            </div>\n            <div className=\"text-xs text-muted-foreground\">\n              {format(day, \"MMM yyyy\")}\n            </div>\n          </div>\n\n          <div className=\"flex min-w-0 flex-1 flex-col gap-1.5\">\n            {dayEvents.map((event) => (\n              <AgendaRow\n                key={event.id}\n                event={event}\n                day={day}\n                onClick={(e) => onEventClick?.(event, e)}\n              />\n            ))}\n          </div>\n        </div>\n      ))}\n    </div>\n  )\n}\n\nfunction AgendaRow({\n  event,\n  day,\n  onClick,\n}: {\n  event: CalendarEvent\n  day: Date\n  onClick: (e: React.MouseEvent) => void\n}) {\n  const tone = eventToneClasses[toneOf(event)]\n  const time = React.useMemo(() => {\n    if (event.allDay) return \"All day\"\n    if (isSpanning(event)) {\n      const startsToday = isSameDay(event.start, day)\n      const endsToday = isSameDay(event.end, day)\n      if (startsToday) return `From ${format(event.start, \"HH:mm\")}`\n      if (endsToday) return `Until ${format(event.end, \"HH:mm\")}`\n      return \"All day\"\n    }\n    return `${format(event.start, \"HH:mm\")} – ${format(event.end, \"HH:mm\")}`\n  }, [event, day])\n\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      className=\"flex cursor-pointer items-center gap-3 rounded-lg border border-transparent px-2 py-2 text-left transition-colors hover:border-border hover:bg-muted/60\"\n    >\n      <span className={cn(\"h-8 w-1 shrink-0 rounded-full\", tone.dot)} />\n      <span className=\"w-24 shrink-0 text-xs font-medium tabular-nums text-muted-foreground\">\n        {time}\n      </span>\n      <span className=\"min-w-0 flex-1 truncate text-sm font-medium text-foreground\">\n        {event.title}\n      </span>\n    </button>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/calendar-view/agenda-view.tsx"
    },
    {
      "path": "components/blocks/calendar-view/calendar-toolbar.tsx",
      "content": "\"use client\"\n\nimport { ChevronLeft, ChevronRight } from \"@untitledui/icons\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\nimport { ToggleGroup, ToggleGroupItem } from \"@/components/ui/toggle-group\"\n\nimport type { CalendarView } from \"./calendar-types\"\nimport type { CalendarController } from \"./use-calendar-view\"\n\nconst VIEW_LABELS: Record<CalendarView, string> = {\n  month: \"Month\",\n  week: \"Week\",\n  day: \"Day\",\n  agenda: \"Agenda\",\n}\n\n/**\n * Header toolbar: prev / today / next, the current title, and a view switcher\n * (a segmented ToggleGroup on wider screens, a Select on mobile).\n */\nexport function CalendarToolbar({\n  controller,\n  views,\n}: {\n  controller: CalendarController\n  views: CalendarView[]\n}) {\n  const { title, step, goToToday, view, setView } = controller\n\n  return (\n    <div className=\"flex flex-wrap items-center justify-between gap-3 border-b border-border px-3 py-3 sm:px-4\">\n      <div className=\"flex items-center gap-2\">\n        <div className=\"flex items-center gap-0.5\">\n          <Button\n            variant=\"ghost\"\n            size=\"icon-sm\"\n            aria-label=\"Previous\"\n            onClick={() => step(-1)}\n          >\n            <ChevronLeft />\n          </Button>\n          <Button\n            variant=\"ghost\"\n            size=\"icon-sm\"\n            aria-label=\"Next\"\n            onClick={() => step(1)}\n          >\n            <ChevronRight />\n          </Button>\n        </div>\n        <Button variant=\"secondary\" size=\"sm\" onClick={goToToday}>\n          Today\n        </Button>\n      </div>\n\n      <h2 className=\"order-last w-full text-center text-sm font-semibold text-foreground tabular-nums sm:order-none sm:w-auto sm:text-left sm:text-base\">\n        {title}\n      </h2>\n\n      {views.length > 1 ? (\n        <>\n          {/* Segmented switcher — wide screens. */}\n          <ToggleGroup\n            className=\"hidden bg-muted p-0.5 sm:flex\"\n            spacing={0}\n            value={[view]}\n            onValueChange={(value: string[]) => {\n              const next = value[0] as CalendarView | undefined\n              if (next) setView(next)\n            }}\n          >\n            {views.map((v) => (\n              <ToggleGroupItem\n                key={v}\n                value={v}\n                size=\"sm\"\n                className={cn(\n                  \"rounded-md! px-3 text-sm font-medium text-muted-foreground\",\n                  \"data-[state=on]:bg-background data-[state=on]:text-foreground data-[state=on]:shadow-xs\"\n                )}\n              >\n                {VIEW_LABELS[v]}\n              </ToggleGroupItem>\n            ))}\n          </ToggleGroup>\n\n          {/* Compact switcher — mobile. */}\n          <Select\n            value={view}\n            onValueChange={(value: string | null) => {\n              if (value) setView(value as CalendarView)\n            }}\n          >\n            <SelectTrigger size=\"sm\" className=\"w-32 sm:hidden\">\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent>\n              {views.map((v) => (\n                <SelectItem key={v} value={v}>\n                  {VIEW_LABELS[v]}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        </>\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/calendar-view/calendar-toolbar.tsx"
    },
    {
      "path": "components/blocks/calendar-view/calendar-types.ts",
      "content": "/**\n * calendar-view — shared contract.\n *\n * The event model is deliberately small and framework-free: give the calendar an\n * array of {@link CalendarEvent}s and wire the callbacks. Everything else (which\n * view is showing, which date is anchored) can be controlled or left to the\n * internal state in {@link useCalendarView}.\n */\n\n/** The four surfaces the calendar can render. */\nexport type CalendarView = \"month\" | \"week\" | \"day\" | \"agenda\"\n\n/**\n * Colour tone for an event, mapped onto the shared `utility-*` token ramps (the\n * same palette the Badge uses), so chips read correctly in light and dark mode.\n */\nexport type EventTone =\n  | \"brand\"\n  | \"blue\"\n  | \"green\"\n  | \"amber\"\n  | \"red\"\n  | \"purple\"\n  | \"pink\"\n  | \"indigo\"\n  | \"gray\"\n\n/**\n * A single calendar event. `start`/`end` are absolute `Date`s; for an `allDay`\n * event the time components are ignored and the event spans whole days from\n * `start` to `end` (inclusive of the start day, exclusive treatment handled\n * internally so a same-day all-day event covers exactly one cell).\n */\nexport interface CalendarEvent<TMeta = unknown> {\n  id: string\n  title: string\n  start: Date\n  end: Date\n  allDay?: boolean\n  tone?: EventTone\n  /** Optional caller payload — surfaced back through the click/drop callbacks. */\n  meta?: TMeta\n}\n\n/** An empty slot the user clicked to create a new event. */\nexport interface CalendarSlot {\n  start: Date\n  end: Date\n  allDay: boolean\n}\n\n/** Payload for a completed drag-to-move gesture. */\nexport interface CalendarEventDrop<TMeta = unknown> {\n  event: CalendarEvent<TMeta>\n  start: Date\n  end: Date\n  allDay: boolean\n}\n\nexport interface CalendarViewProps<TMeta = unknown> {\n  /** Events to render. */\n  events: CalendarEvent<TMeta>[]\n\n  /** Controlled active view. Omit to run uncontrolled from {@link defaultView}. */\n  view?: CalendarView\n  /** Initial view when uncontrolled. Defaults to `\"month\"`. */\n  defaultView?: CalendarView\n  onViewChange?: (view: CalendarView) => void\n\n  /** Controlled anchor date. Omit to run uncontrolled from {@link defaultDate}. */\n  date?: Date\n  /** Initial anchor date when uncontrolled. Defaults to today. */\n  defaultDate?: Date\n  onDateChange?: (date: Date) => void\n\n  /** Which views the switcher offers, in order. Defaults to all four. */\n  views?: CalendarView[]\n\n  onEventClick?: (event: CalendarEvent<TMeta>, e: React.MouseEvent) => void\n  onSlotClick?: (slot: CalendarSlot) => void\n  /** Enables drag-to-move in the month grid and time columns when provided. */\n  onEventDrop?: (drop: CalendarEventDrop<TMeta>) => void\n\n  /** First hour shown in the week/day time grid (0–23). Defaults to 7. */\n  dayStartHour?: number\n  /** Last hour shown in the week/day time grid (1–24). Defaults to 21. */\n  dayEndHour?: number\n\n  /** Override \"now\" (for the current-time indicator + today ring). Defaults to a live clock. */\n  now?: Date\n\n  className?: string\n}\n",
      "type": "registry:component",
      "target": "components/blocks/calendar-view/calendar-types.ts"
    },
    {
      "path": "components/blocks/calendar-view/calendar-utils.ts",
      "content": "import {\n  areIntervalsOverlapping,\n  differenceInCalendarDays,\n  endOfDay,\n  isSameDay,\n  startOfDay,\n} from \"date-fns\"\n\nimport type { CalendarEvent, EventTone } from \"./calendar-types\"\n\n/**\n * Tone → class map. Written out in full (never interpolated) so Tailwind's\n * source scanner sees every literal class name. `gray` maps to the neutral\n * ramp; every other tone maps to its like-named `utility-*` ramp.\n */\nexport interface ToneClasses {\n  /** Month-grid chip: subtle fill + readable text. */\n  chip: string\n  /** Small leading dot. */\n  dot: string\n  /** Time-grid block: fill, border and text. */\n  block: string\n  /** Spanning bar (multi-day / all-day row). */\n  bar: string\n}\n\nexport const eventToneClasses: Record<EventTone, ToneClasses> = {\n  brand: {\n    chip: \"bg-utility-brand-50 text-utility-brand-700 hover:bg-utility-brand-100\",\n    dot: \"bg-utility-brand-500\",\n    block:\n      \"bg-utility-brand-50 text-utility-brand-700 border-utility-brand-200 hover:bg-utility-brand-100\",\n    bar: \"bg-utility-brand-100 text-utility-brand-700 ring-utility-brand-200 hover:bg-utility-brand-200\",\n  },\n  blue: {\n    chip: \"bg-utility-blue-50 text-utility-blue-700 hover:bg-utility-blue-100\",\n    dot: \"bg-utility-blue-500\",\n    block:\n      \"bg-utility-blue-50 text-utility-blue-700 border-utility-blue-200 hover:bg-utility-blue-100\",\n    bar: \"bg-utility-blue-100 text-utility-blue-700 ring-utility-blue-200 hover:bg-utility-blue-200\",\n  },\n  green: {\n    chip: \"bg-utility-green-50 text-utility-green-700 hover:bg-utility-green-100\",\n    dot: \"bg-utility-green-500\",\n    block:\n      \"bg-utility-green-50 text-utility-green-700 border-utility-green-200 hover:bg-utility-green-100\",\n    bar: \"bg-utility-green-100 text-utility-green-700 ring-utility-green-200 hover:bg-utility-green-200\",\n  },\n  amber: {\n    chip: \"bg-utility-amber-50 text-utility-amber-700 hover:bg-utility-amber-100\",\n    dot: \"bg-utility-amber-500\",\n    block:\n      \"bg-utility-amber-50 text-utility-amber-700 border-utility-amber-200 hover:bg-utility-amber-100\",\n    bar: \"bg-utility-amber-100 text-utility-amber-700 ring-utility-amber-200 hover:bg-utility-amber-200\",\n  },\n  red: {\n    chip: \"bg-utility-red-50 text-utility-red-700 hover:bg-utility-red-100\",\n    dot: \"bg-utility-red-500\",\n    block:\n      \"bg-utility-red-50 text-utility-red-700 border-utility-red-200 hover:bg-utility-red-100\",\n    bar: \"bg-utility-red-100 text-utility-red-700 ring-utility-red-200 hover:bg-utility-red-200\",\n  },\n  purple: {\n    chip: \"bg-utility-purple-50 text-utility-purple-700 hover:bg-utility-purple-100\",\n    dot: \"bg-utility-purple-500\",\n    block:\n      \"bg-utility-purple-50 text-utility-purple-700 border-utility-purple-200 hover:bg-utility-purple-100\",\n    bar: \"bg-utility-purple-100 text-utility-purple-700 ring-utility-purple-200 hover:bg-utility-purple-200\",\n  },\n  pink: {\n    chip: \"bg-utility-pink-50 text-utility-pink-700 hover:bg-utility-pink-100\",\n    dot: \"bg-utility-pink-500\",\n    block:\n      \"bg-utility-pink-50 text-utility-pink-700 border-utility-pink-200 hover:bg-utility-pink-100\",\n    bar: \"bg-utility-pink-100 text-utility-pink-700 ring-utility-pink-200 hover:bg-utility-pink-200\",\n  },\n  indigo: {\n    chip: \"bg-utility-indigo-50 text-utility-indigo-700 hover:bg-utility-indigo-100\",\n    dot: \"bg-utility-indigo-500\",\n    block:\n      \"bg-utility-indigo-50 text-utility-indigo-700 border-utility-indigo-200 hover:bg-utility-indigo-100\",\n    bar: \"bg-utility-indigo-100 text-utility-indigo-700 ring-utility-indigo-200 hover:bg-utility-indigo-200\",\n  },\n  gray: {\n    chip: \"bg-utility-neutral-50 text-utility-neutral-700 hover:bg-utility-neutral-100\",\n    dot: \"bg-utility-neutral-500\",\n    block:\n      \"bg-utility-neutral-50 text-utility-neutral-700 border-utility-neutral-200 hover:bg-utility-neutral-100\",\n    bar: \"bg-utility-neutral-100 text-utility-neutral-700 ring-utility-neutral-200 hover:bg-utility-neutral-200\",\n  },\n}\n\nexport function toneOf(event: CalendarEvent): EventTone {\n  return event.tone ?? \"brand\"\n}\n\n/** True when an event covers more than one calendar day (or is a multi-day all-day). */\nexport function isMultiDay(event: CalendarEvent): boolean {\n  return !isSameDay(event.start, event.end)\n}\n\n/** True when an event should live in the all-day / spanning lane of a view. */\nexport function isSpanning(event: CalendarEvent): boolean {\n  return Boolean(event.allDay) || isMultiDay(event)\n}\n\n/** Events that intersect a given day, ordered for stable stacking. */\nexport function eventsForDay(\n  events: CalendarEvent[],\n  day: Date\n): CalendarEvent[] {\n  const dayStart = startOfDay(day)\n  const dayEnd = endOfDay(day)\n  return events\n    .filter((e) =>\n      areIntervalsOverlapping(\n        { start: e.start, end: e.end },\n        { start: dayStart, end: dayEnd },\n        { inclusive: true }\n      )\n    )\n    .sort(compareEvents)\n}\n\n/** Spanning bars first (longest, earliest), then timed events by start. */\nexport function compareEvents(a: CalendarEvent, b: CalendarEvent): number {\n  const aSpan = isSpanning(a)\n  const bSpan = isSpanning(b)\n  if (aSpan !== bSpan) return aSpan ? -1 : 1\n  if (aSpan && bSpan) {\n    const lenA = differenceInCalendarDays(b.end, b.start)\n    const lenB = differenceInCalendarDays(a.end, a.start)\n    if (lenA !== lenB) return lenA - lenB\n  }\n  return a.start.getTime() - b.start.getTime()\n}\n\n/**\n * Overlap layout for a single day's timed events — the classic column-packing\n * algorithm. Returns each event with the column it occupies and the number of\n * columns in its overlap cluster, so a caller can position side-by-side.\n */\nexport interface PositionedEvent {\n  event: CalendarEvent\n  column: number\n  columns: number\n}\n\nexport function layoutTimedEvents(events: CalendarEvent[]): PositionedEvent[] {\n  const sorted = [...events].sort(\n    (a, b) => a.start.getTime() - b.start.getTime() || b.end.getTime() - a.end.getTime()\n  )\n\n  const positioned: PositionedEvent[] = []\n  let cluster: PositionedEvent[] = []\n  let clusterEnd = -Infinity\n\n  const flush = () => {\n    const columns = cluster.reduce((max, p) => Math.max(max, p.column + 1), 0)\n    for (const p of cluster) p.columns = columns\n    positioned.push(...cluster)\n    cluster = []\n    clusterEnd = -Infinity\n  }\n\n  for (const event of sorted) {\n    if (cluster.length && event.start.getTime() >= clusterEnd) flush()\n\n    // First free column not overlapping an existing member.\n    const taken = new Set(\n      cluster\n        .filter((p) => p.event.end.getTime() > event.start.getTime())\n        .map((p) => p.column)\n    )\n    let column = 0\n    while (taken.has(column)) column += 1\n\n    cluster.push({ event, column, columns: 1 })\n    clusterEnd = Math.max(clusterEnd, event.end.getTime())\n  }\n  if (cluster.length) flush()\n\n  return positioned\n}\n\n/** Minutes since midnight, clamped so an event that starts before the grid still lays out. */\nexport function minutesIntoDay(date: Date, day: Date): number {\n  return (\n    Math.max(0, date.getTime() - startOfDay(day).getTime()) / 60000\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/calendar-view/calendar-utils.ts"
    },
    {
      "path": "components/blocks/calendar-view/calendar-view.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport type { CalendarView, CalendarViewProps } from \"./calendar-types\"\nimport { AgendaView } from \"./agenda-view\"\nimport { CalendarToolbar } from \"./calendar-toolbar\"\nimport { MonthView } from \"./month-view\"\nimport { TimeGridView, weekDays } from \"./time-grid-view\"\nimport { useCalendarView } from \"./use-calendar-view\"\n\nconst ALL_VIEWS: CalendarView[] = [\"month\", \"week\", \"day\", \"agenda\"]\n\n/**\n * calendar-view — a props-driven event calendar with Month, Week, Day and\n * Agenda surfaces.\n *\n * Feed it an array of {@link CalendarEvent}s; control `view` and `date` or let\n * them run uncontrolled. Wire `onEventClick` / `onSlotClick` to open your own\n * editor, and pass `onEventDrop` to enable pointer-based drag-to-move in the\n * month grid and the week/day time columns.\n */\nexport function CalendarView<TMeta = unknown>(props: CalendarViewProps<TMeta>) {\n  // Erase the caller's meta type at the internal boundary: the surfaces work on\n  // the base event shape, and the callbacks are invariant in `meta` from here on.\n  const p = props as CalendarViewProps\n  const {\n    events,\n    views = ALL_VIEWS,\n    className,\n    dayStartHour,\n    dayEndHour,\n    onEventClick,\n    onSlotClick,\n    onEventDrop,\n  } = p\n\n  const controller = useCalendarView(p)\n  const { view, range } = controller\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-col overflow-hidden rounded-xl border border-border bg-background text-sm shadow-xs\",\n        className\n      )}\n    >\n      <CalendarToolbar controller={controller} views={views} />\n\n      {view === \"month\" ? (\n        <MonthView\n          controller={controller}\n          events={events}\n          onEventClick={onEventClick}\n          onSlotClick={onSlotClick}\n          onEventDrop={onEventDrop}\n        />\n      ) : null}\n\n      {view === \"week\" ? (\n        <TimeGridView\n          controller={controller}\n          events={events}\n          days={weekDays(range.start)}\n          dayStartHour={dayStartHour}\n          dayEndHour={dayEndHour}\n          onEventClick={onEventClick}\n          onSlotClick={onSlotClick}\n          onEventDrop={onEventDrop}\n        />\n      ) : null}\n\n      {view === \"day\" ? (\n        <TimeGridView\n          controller={controller}\n          events={events}\n          days={[range.start]}\n          dayStartHour={dayStartHour}\n          dayEndHour={dayEndHour}\n          onEventClick={onEventClick}\n          onSlotClick={onSlotClick}\n          onEventDrop={onEventDrop}\n        />\n      ) : null}\n\n      {view === \"agenda\" ? (\n        <AgendaView\n          controller={controller}\n          events={events}\n          onEventClick={onEventClick}\n        />\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/calendar-view/calendar-view.tsx"
    },
    {
      "path": "components/blocks/calendar-view/index.ts",
      "content": "\"use client\"\n\n/**\n * calendar-view — a props-driven event calendar with Month, Week, Day and\n * Agenda views, styled to the Untitled UI look.\n *\n * Month renders a six-week grid with event chips (capped, with a \"+N more\"\n * popover), a today ring, weekend tint, other-month muting and spanning bars for\n * all-day / multi-day events. Week and Day are hour-gridded time columns with\n * side-by-side overlap layout, an all-day lane and a live current-time line.\n * Agenda groups events into a day-by-day list. All four share one headless\n * controller ({@link useCalendarView}); events carry a badge-palette colour tone.\n *\n * Drive it entirely through props: pass `events`, control `view`/`date` (or run\n * uncontrolled), and wire `onEventClick`, `onSlotClick` and the optional\n * `onEventDrop` (pointer drag-to-move in the month grid and time columns).\n */\n\nexport { CalendarView } from \"./calendar-view\"\nexport { useCalendarView } from \"./use-calendar-view\"\nexport {\n  eventToneClasses,\n  eventsForDay,\n  layoutTimedEvents,\n  isSpanning,\n  isMultiDay,\n} from \"./calendar-utils\"\nexport type {\n  CalendarView as CalendarViewName,\n  CalendarViewProps,\n  CalendarEvent,\n  CalendarSlot,\n  CalendarEventDrop,\n  EventTone,\n} from \"./calendar-types\"\n",
      "type": "registry:component",
      "target": "components/blocks/calendar-view/index.ts"
    },
    {
      "path": "components/blocks/calendar-view/month-view.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  addDays,\n  differenceInCalendarDays,\n  endOfDay,\n  format,\n  isSameDay,\n  isSameMonth,\n  isWeekend,\n  max as maxDate,\n  min as minDate,\n  startOfDay,\n} from \"date-fns\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\n\nimport type {\n  CalendarEvent,\n  CalendarSlot,\n  CalendarViewProps,\n} from \"./calendar-types\"\nimport {\n  compareEvents,\n  eventsForDay,\n  eventToneClasses,\n  isSpanning,\n  toneOf,\n} from \"./calendar-utils\"\nimport { useCalendarDrag } from \"./use-calendar-drag\"\nimport type { CalendarController } from \"./use-calendar-view\"\nimport { WEEKDAY_LABELS } from \"./use-calendar-view\"\n\nconst MAX_ROWS = 3\nconst DATE_H = 28\nconst ROW_H = 22\nconst ROW_GAP = 2\n\nconst dayKey = (d: Date) => format(d, \"yyyy-MM-dd\")\n\ninterface WeekBar {\n  event: CalendarEvent\n  lane: number\n  startCol: number\n  span: number\n  clipStart: boolean\n  clipEnd: boolean\n}\n\n/** Greedy lane assignment for the spanning events intersecting one week. */\nfunction layoutWeekBars(\n  spanning: CalendarEvent[],\n  weekStart: Date,\n  weekEnd: Date\n): { bars: WeekBar[]; laneCount: number } {\n  const lanes: Date[] = [] // per lane: the end of the last-placed bar\n  const bars: WeekBar[] = []\n\n  for (const event of [...spanning].sort(compareEvents)) {\n    const from = maxDate([startOfDay(event.start), weekStart])\n    const to = minDate([endOfDay(event.end), weekEnd])\n    const startCol = differenceInCalendarDays(from, weekStart)\n    const span = differenceInCalendarDays(to, from) + 1\n\n    let lane = lanes.findIndex((laneEnd) => from > laneEnd)\n    if (lane === -1) {\n      lane = lanes.length\n      lanes.push(to)\n    } else {\n      lanes[lane] = to\n    }\n\n    bars.push({\n      event,\n      lane,\n      startCol,\n      span,\n      clipStart: startOfDay(event.start) < weekStart,\n      clipEnd: endOfDay(event.end) > weekEnd,\n    })\n  }\n\n  return { bars, laneCount: lanes.length }\n}\n\nexport function MonthView({\n  controller,\n  events,\n  onEventClick,\n  onSlotClick,\n  onEventDrop,\n}: {\n  controller: CalendarController\n  events: CalendarEvent[]\n} & Pick<\n  CalendarViewProps,\n  \"onEventClick\" | \"onSlotClick\" | \"onEventDrop\"\n>) {\n  const { range, date, now } = controller\n\n  // Always render a full six-week grid for a stable height.\n  const days = React.useMemo(\n    () => Array.from({ length: 42 }, (_, i) => addDays(range.start, i)),\n    [range.start]\n  )\n  const weeks = React.useMemo(\n    () => Array.from({ length: 6 }, (_, w) => days.slice(w * 7, w * 7 + 7)),\n    [days]\n  )\n\n  const eventById = React.useMemo(() => {\n    const map = new Map<string, CalendarEvent>()\n    for (const e of events) map.set(e.id, e)\n    return map\n  }, [events])\n\n  const { dragging, target, onPointerDown, shouldSuppressClick } =\n    useCalendarDrag<string>({\n      enabled: Boolean(onEventDrop),\n      onResolve: (e) => {\n        const el = document\n          .elementFromPoint(e.clientX, e.clientY)\n          ?.closest<HTMLElement>(\"[data-calendar-day]\")\n        return el?.dataset.calendarDay ?? null\n      },\n      onCommit: (eventId, key) => {\n        const event = eventById.get(eventId)\n        if (!event) return\n        const targetDay = new Date(`${key}T00:00:00`)\n        const delta = differenceInCalendarDays(\n          targetDay,\n          startOfDay(event.start)\n        )\n        if (delta === 0) return\n        onEventDrop?.({\n          event,\n          start: addDays(event.start, delta),\n          end: addDays(event.end, delta),\n          allDay: Boolean(event.allDay),\n        })\n      },\n    })\n\n  return (\n    <div className=\"flex flex-col\">\n      {/* Weekday header. */}\n      <div className=\"grid grid-cols-7 border-b border-border\">\n        {WEEKDAY_LABELS.map((label) => (\n          <div\n            key={label}\n            className=\"px-2 py-2 text-center text-xs font-semibold text-muted-foreground\"\n          >\n            <span className=\"hidden sm:inline\">{label}</span>\n            <span className=\"sm:hidden\">{label.charAt(0)}</span>\n          </div>\n        ))}\n      </div>\n\n      <div className=\"grid grid-rows-6\">\n        {weeks.map((week, wi) => {\n          const weekStart = startOfDay(week[0])\n          const weekEnd = endOfDay(week[6])\n          const spanning = events.filter(\n            (e) => isSpanning(e) && e.end >= weekStart && e.start <= weekEnd\n          )\n          const { bars, laneCount } = layoutWeekBars(\n            spanning,\n            weekStart,\n            weekEnd\n          )\n          const shownLanes = Math.min(laneCount, MAX_ROWS)\n          const spacerH = shownLanes * (ROW_H + ROW_GAP)\n\n          return (\n            <div\n              key={wi}\n              className=\"relative grid grid-cols-7 border-b border-border last:border-b-0\"\n            >\n              {week.map((day) => {\n                const inMonth = isSameMonth(day, date)\n                const isToday = isSameDay(day, now)\n                const weekend = isWeekend(day)\n                const all = eventsForDay(events, day)\n                const timed = all.filter((e) => !isSpanning(e))\n\n                const timedSlots = Math.max(0, MAX_ROWS - shownLanes)\n                const shownTimed = timed.slice(0, timedSlots)\n\n                // Everything the cell is not already showing as a visible chip/bar.\n                const shownSpanIds = new Set(\n                  bars\n                    .filter(\n                      (b) =>\n                        b.lane < MAX_ROWS &&\n                        day >= startOfDay(b.event.start) &&\n                        day <= endOfDay(b.event.end)\n                    )\n                    .map((b) => b.event.id)\n                )\n                const shownTimedIds = new Set(shownTimed.map((e) => e.id))\n                const hidden = all.filter(\n                  (e) => !shownSpanIds.has(e.id) && !shownTimedIds.has(e.id)\n                )\n\n                return (\n                  <div\n                    key={day.toISOString()}\n                    data-calendar-day={dayKey(day)}\n                    onClick={() =>\n                      onSlotClick?.({\n                        start: startOfDay(day),\n                        end: endOfDay(day),\n                        allDay: true,\n                      })\n                    }\n                    className={cn(\n                      \"group/cell relative min-h-24 cursor-pointer border-r border-border px-1 pb-1 transition-colors last:border-r-0\",\n                      weekend && \"bg-muted/40\",\n                      !inMonth && \"bg-muted/20\",\n                      target === dayKey(day) &&\n                        dragging &&\n                        \"bg-utility-brand-50 ring-1 ring-inset ring-utility-brand-200\"\n                    )}\n                  >\n                    <div\n                      className=\"flex items-center justify-end\"\n                      style={{ height: DATE_H }}\n                    >\n                      <span\n                        className={cn(\n                          \"flex size-6 items-center justify-center rounded-full text-xs font-medium tabular-nums\",\n                          !inMonth && \"text-muted-foreground/70\",\n                          inMonth && \"text-foreground\",\n                          isToday &&\n                            \"bg-primary font-semibold text-primary-foreground\"\n                        )}\n                      >\n                        {format(day, \"d\")}\n                      </span>\n                    </div>\n\n                    {/* Reserve vertical space for the spanning-bar overlay. */}\n                    <div style={{ height: spacerH }} aria-hidden />\n\n                    <div className=\"flex flex-col gap-0.5\">\n                      {shownTimed.map((event) => (\n                        <MonthChip\n                          key={event.id}\n                          event={event}\n                          dragging={dragging === event.id}\n                          onPointerDown={onPointerDown(event.id)}\n                          onClick={(e) => {\n                            e.stopPropagation()\n                            if (shouldSuppressClick()) return\n                            onEventClick?.(event, e)\n                          }}\n                        />\n                      ))}\n\n                      {hidden.length > 0 ? (\n                        <MorePopover\n                          day={day}\n                          events={all}\n                          onEventClick={onEventClick}\n                          count={hidden.length}\n                        />\n                      ) : null}\n                    </div>\n                  </div>\n                )\n              })}\n\n              {/* Spanning-bar overlay, aligned to the reserved spacer. */}\n              <div\n                className=\"pointer-events-none absolute inset-x-0 grid grid-cols-7\"\n                style={{ top: DATE_H }}\n              >\n                {bars\n                  .filter((b) => b.lane < MAX_ROWS)\n                  .map((b) => (\n                    <button\n                      key={b.event.id}\n                      type=\"button\"\n                      onPointerDown={onPointerDown(b.event.id)}\n                      onClick={(e) => {\n                        e.stopPropagation()\n                        if (shouldSuppressClick()) return\n                        onEventClick?.(b.event, e)\n                      }}\n                      style={{\n                        gridColumnStart: b.startCol + 1,\n                        gridColumnEnd: `span ${b.span}`,\n                        transform: `translateY(${b.lane * (ROW_H + ROW_GAP)}px)`,\n                        height: ROW_H,\n                      }}\n                      className={cn(\n                        \"pointer-events-auto mx-1 flex cursor-pointer items-center gap-1 overflow-hidden rounded-md px-1.5 text-left text-xs font-medium ring-1 ring-inset transition-colors\",\n                        eventToneClasses[toneOf(b.event)].bar,\n                        b.clipStart ? \"rounded-l-none\" : \"\",\n                        b.clipEnd ? \"rounded-r-none\" : \"\",\n                        dragging === b.event.id && \"opacity-60\"\n                      )}\n                    >\n                      {!b.event.allDay && !b.clipStart ? (\n                        <span className=\"tabular-nums opacity-70\">\n                          {format(b.event.start, \"HH:mm\")}\n                        </span>\n                      ) : null}\n                      <span className=\"truncate\">{b.event.title}</span>\n                    </button>\n                  ))}\n              </div>\n            </div>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n\nfunction MonthChip({\n  event,\n  dragging,\n  onPointerDown,\n  onClick,\n}: {\n  event: CalendarEvent\n  dragging: boolean\n  onPointerDown: (e: React.PointerEvent) => void\n  onClick: (e: React.MouseEvent) => void\n}) {\n  const tone = eventToneClasses[toneOf(event)]\n  return (\n    <button\n      type=\"button\"\n      onPointerDown={onPointerDown}\n      onClick={onClick}\n      className={cn(\n        \"flex h-[22px] w-full cursor-pointer items-center gap-1.5 overflow-hidden rounded-md px-1.5 text-left text-xs font-medium transition-colors\",\n        tone.chip,\n        dragging && \"opacity-60\"\n      )}\n    >\n      <span className={cn(\"size-1.5 shrink-0 rounded-full\", tone.dot)} />\n      <span className=\"tabular-nums opacity-70\">\n        {format(event.start, \"HH:mm\")}\n      </span>\n      <span className=\"truncate\">{event.title}</span>\n    </button>\n  )\n}\n\nfunction MorePopover({\n  day,\n  events,\n  count,\n  onEventClick,\n}: {\n  day: Date\n  events: CalendarEvent[]\n  count: number\n  onEventClick?: (event: CalendarEvent, e: React.MouseEvent) => void\n}) {\n  return (\n    <Popover>\n      <PopoverTrigger\n        onClick={(e) => e.stopPropagation()}\n        className=\"cursor-pointer rounded-md px-1.5 py-0.5 text-left text-xs font-semibold text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n      >\n        +{count} more\n      </PopoverTrigger>\n      <PopoverContent align=\"start\" className=\"w-64 gap-2 p-2\">\n        <p className=\"px-1 pb-1 text-xs font-semibold text-muted-foreground\">\n          {format(day, \"EEEE d MMMM\")}\n        </p>\n        <div className=\"flex flex-col gap-0.5\">\n          {events.map((event) => {\n            const tone = eventToneClasses[toneOf(event)]\n            return (\n              <button\n                key={event.id}\n                type=\"button\"\n                onClick={(e) => onEventClick?.(event, e)}\n                className=\"flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted\"\n              >\n                <span className={cn(\"size-2 shrink-0 rounded-full\", tone.dot)} />\n                <span className=\"w-12 shrink-0 text-xs tabular-nums text-muted-foreground\">\n                  {event.allDay ? \"All day\" : format(event.start, \"HH:mm\")}\n                </span>\n                <span className=\"truncate font-medium text-foreground\">\n                  {event.title}\n                </span>\n              </button>\n            )\n          })}\n        </div>\n      </PopoverContent>\n    </Popover>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/calendar-view/month-view.tsx"
    },
    {
      "path": "components/blocks/calendar-view/time-grid-view.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  addDays,\n  addMinutes,\n  differenceInMinutes,\n  endOfDay,\n  format,\n  isSameDay,\n  isWeekend,\n  startOfDay,\n} from \"date-fns\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport type {\n  CalendarEvent,\n  CalendarViewProps,\n} from \"./calendar-types\"\nimport {\n  eventsForDay,\n  eventToneClasses,\n  isSpanning,\n  layoutTimedEvents,\n  minutesIntoDay,\n  toneOf,\n} from \"./calendar-utils\"\nimport { useCalendarDrag } from \"./use-calendar-drag\"\nimport type { CalendarController } from \"./use-calendar-view\"\n\nconst HOUR_H = 48 // px per hour\nconst SNAP = 15 // minutes\nconst GUTTER = \"w-14\"\nconst dayKey = (d: Date) => format(d, \"yyyy-MM-dd\")\n\ninterface DropTarget {\n  key: string\n  minutes: number\n}\n\nexport function TimeGridView({\n  controller,\n  events,\n  days,\n  dayStartHour = 7,\n  dayEndHour = 21,\n  onEventClick,\n  onSlotClick,\n  onEventDrop,\n}: {\n  controller: CalendarController\n  events: CalendarEvent[]\n  days: Date[]\n} & Pick<\n  CalendarViewProps,\n  \"dayStartHour\" | \"dayEndHour\" | \"onEventClick\" | \"onSlotClick\" | \"onEventDrop\"\n>) {\n  const { now } = controller\n  const hours = React.useMemo(\n    () =>\n      Array.from(\n        { length: dayEndHour - dayStartHour },\n        (_, i) => dayStartHour + i\n      ),\n    [dayStartHour, dayEndHour]\n  )\n  const gridStartMin = dayStartHour * 60\n  const gridHeight = (dayEndHour - dayStartHour) * HOUR_H\n\n  const eventById = React.useMemo(() => {\n    const map = new Map<string, CalendarEvent>()\n    for (const e of events) map.set(e.id, e)\n    return map\n  }, [events])\n\n  const resolveTarget = (e: PointerEvent): DropTarget | null => {\n    const col = document\n      .elementFromPoint(e.clientX, e.clientY)\n      ?.closest<HTMLElement>(\"[data-calendar-col]\")\n    if (!col?.dataset.calendarCol) return null\n    const rect = col.getBoundingClientRect()\n    const raw = ((e.clientY - rect.top) / HOUR_H) * 60 + gridStartMin\n    const minutes = Math.round(raw / SNAP) * SNAP\n    return { key: col.dataset.calendarCol, minutes }\n  }\n\n  const { dragging, target, onPointerDown, shouldSuppressClick } =\n    useCalendarDrag<DropTarget>({\n      enabled: Boolean(onEventDrop),\n      onResolve: resolveTarget,\n      onCommit: (eventId, t) => {\n        const event = eventById.get(eventId)\n        if (!event) return\n        const duration = differenceInMinutes(event.end, event.start)\n        const targetDay = new Date(`${t.key}T00:00:00`)\n        const start = addMinutes(targetDay, t.minutes)\n        onEventDrop?.({\n          event,\n          start,\n          end: addMinutes(start, duration),\n          allDay: false,\n        })\n      },\n    })\n\n  // Spanning / all-day events per day, for the top lane.\n  const hasAllDay = days.some((d) =>\n    events.some(\n      (e) =>\n        isSpanning(e) && e.end >= startOfDay(d) && e.start <= endOfDay(d)\n    )\n  )\n\n  const nowTop =\n    (minutesIntoDay(now, startOfDay(now)) - gridStartMin) / 60 * HOUR_H\n  const nowVisible =\n    minutesIntoDay(now, startOfDay(now)) >= gridStartMin &&\n    minutesIntoDay(now, startOfDay(now)) <= dayEndHour * 60\n\n  return (\n    <div className=\"flex flex-col\">\n      {/* Column header. */}\n      <div className=\"flex border-b border-border\">\n        <div className={cn(GUTTER, \"shrink-0\")} />\n        <div\n          className=\"grid flex-1\"\n          style={{ gridTemplateColumns: `repeat(${days.length}, 1fr)` }}\n        >\n          {days.map((day) => {\n            const isToday = isSameDay(day, now)\n            return (\n              <div\n                key={day.toISOString()}\n                className={cn(\n                  \"flex flex-col items-center gap-0.5 border-l border-border py-2 first:border-l-0\",\n                  isWeekend(day) && \"bg-muted/40\"\n                )}\n              >\n                <span className=\"text-xs font-medium text-muted-foreground\">\n                  {format(day, \"EEE\")}\n                </span>\n                <span\n                  className={cn(\n                    \"flex size-7 items-center justify-center rounded-full text-sm font-semibold tabular-nums\",\n                    isToday\n                      ? \"bg-primary text-primary-foreground\"\n                      : \"text-foreground\"\n                  )}\n                >\n                  {format(day, \"d\")}\n                </span>\n              </div>\n            )\n          })}\n        </div>\n      </div>\n\n      {/* All-day lane. */}\n      {hasAllDay ? (\n        <div className=\"flex border-b border-border\">\n          <div\n            className={cn(\n              GUTTER,\n              \"flex shrink-0 items-start justify-end pr-2 pt-1.5 text-[11px] font-medium text-muted-foreground\"\n            )}\n          >\n            All day\n          </div>\n          <div\n            className=\"grid flex-1\"\n            style={{ gridTemplateColumns: `repeat(${days.length}, 1fr)` }}\n          >\n            {days.map((day) => {\n              const dayAllDay = eventsForDay(events, day).filter(isSpanning)\n              return (\n                <div\n                  key={day.toISOString()}\n                  className={cn(\n                    \"min-h-8 space-y-0.5 border-l border-border p-1 first:border-l-0\",\n                    isWeekend(day) && \"bg-muted/40\"\n                  )}\n                >\n                  {dayAllDay.map((event) => {\n                    const tone = eventToneClasses[toneOf(event)]\n                    return (\n                      <button\n                        key={event.id}\n                        type=\"button\"\n                        onClick={(e) => onEventClick?.(event, e)}\n                        className={cn(\n                          \"flex h-5 w-full cursor-pointer items-center gap-1 overflow-hidden rounded px-1.5 text-left text-xs font-medium ring-1 ring-inset\",\n                          tone.bar\n                        )}\n                      >\n                        <span className=\"truncate\">{event.title}</span>\n                      </button>\n                    )\n                  })}\n                </div>\n              )\n            })}\n          </div>\n        </div>\n      ) : null}\n\n      {/* Scrollable time grid. */}\n      <div className=\"max-h-[32rem] overflow-y-auto\">\n        <div className=\"flex\">\n          {/* Hour gutter. */}\n          <div className={cn(GUTTER, \"shrink-0\")}>\n            {hours.map((h) => (\n              <div\n                key={h}\n                className=\"relative text-right\"\n                style={{ height: HOUR_H }}\n              >\n                <span className=\"absolute -top-2 right-2 text-[11px] font-medium text-muted-foreground tabular-nums\">\n                  {h.toString().padStart(2, \"0\")}:00\n                </span>\n              </div>\n            ))}\n          </div>\n\n          {/* Day columns. */}\n          <div\n            className=\"grid flex-1\"\n            style={{ gridTemplateColumns: `repeat(${days.length}, 1fr)` }}\n          >\n            {days.map((day) => {\n              const timed = eventsForDay(events, day).filter(\n                (e) => !isSpanning(e)\n              )\n              const positioned = layoutTimedEvents(timed)\n              const isToday = isSameDay(day, now)\n              const showNow =\n                isToday && nowVisible && days.length >= 1\n\n              return (\n                <div\n                  key={day.toISOString()}\n                  data-calendar-col={dayKey(day)}\n                  onClick={(e) => {\n                    if (shouldSuppressClick()) return\n                    const rect = e.currentTarget.getBoundingClientRect()\n                    const raw =\n                      ((e.clientY - rect.top) / HOUR_H) * 60 + gridStartMin\n                    const minutes = Math.floor(raw / SNAP) * SNAP\n                    const start = addMinutes(startOfDay(day), minutes)\n                    onSlotClick?.({\n                      start,\n                      end: addMinutes(start, 60),\n                      allDay: false,\n                    })\n                  }}\n                  className={cn(\n                    \"relative border-l border-border first:border-l-0\",\n                    isWeekend(day) && \"bg-muted/40\",\n                    dragging &&\n                      target?.key === dayKey(day) &&\n                      \"bg-utility-brand-50/60\"\n                  )}\n                  style={{ height: gridHeight }}\n                >\n                  {/* Hour lines. */}\n                  {hours.map((h, i) => (\n                    <div\n                      key={h}\n                      className=\"absolute inset-x-0 border-t border-border/70\"\n                      style={{ top: i * HOUR_H }}\n                    />\n                  ))}\n\n                  {/* Drop indicator. */}\n                  {dragging && target?.key === dayKey(day) ? (\n                    <div\n                      className=\"pointer-events-none absolute inset-x-1 z-20 h-0.5 rounded-full bg-utility-brand-500\"\n                      style={{\n                        top:\n                          ((target.minutes - gridStartMin) / 60) * HOUR_H,\n                      }}\n                    >\n                      <span className=\"absolute -top-2 left-0 rounded bg-utility-brand-500 px-1 text-[10px] font-semibold text-white tabular-nums\">\n                        {format(\n                          addMinutes(startOfDay(day), target.minutes),\n                          \"HH:mm\"\n                        )}\n                      </span>\n                    </div>\n                  ) : null}\n\n                  {/* Positioned events. */}\n                  {positioned.map(({ event, column, columns }) => {\n                    const top =\n                      ((minutesIntoDay(event.start, day) - gridStartMin) /\n                        60) *\n                      HOUR_H\n                    const rawEnd =\n                      (minutesIntoDay(event.end, day) - gridStartMin) / 60 *\n                      HOUR_H\n                    const height = Math.max(14, rawEnd - top)\n                    const tone = eventToneClasses[toneOf(event)]\n                    const widthPct = 100 / columns\n                    return (\n                      <button\n                        key={event.id}\n                        type=\"button\"\n                        onPointerDown={onPointerDown(event.id)}\n                        onClick={(e) => {\n                          e.stopPropagation()\n                          if (shouldSuppressClick()) return\n                          onEventClick?.(event, e)\n                        }}\n                        style={{\n                          top: Math.max(0, top),\n                          height,\n                          left: `calc(${column * widthPct}% + 2px)`,\n                          width: `calc(${widthPct}% - 4px)`,\n                        }}\n                        className={cn(\n                          \"absolute z-10 flex cursor-pointer flex-col overflow-hidden rounded-md border-l-2 px-1.5 py-1 text-left text-xs transition-colors\",\n                          tone.block,\n                          dragging === event.id && \"z-30 opacity-70 shadow-lg\"\n                        )}\n                      >\n                        <span className=\"truncate font-semibold leading-tight\">\n                          {event.title}\n                        </span>\n                        {height > 28 ? (\n                          <span className=\"truncate tabular-nums opacity-80\">\n                            {format(event.start, \"HH:mm\")} –{\" \"}\n                            {format(event.end, \"HH:mm\")}\n                          </span>\n                        ) : null}\n                      </button>\n                    )\n                  })}\n\n                  {/* Current-time indicator. */}\n                  {showNow ? (\n                    <div\n                      className=\"pointer-events-none absolute inset-x-0 z-20 flex items-center\"\n                      style={{ top: nowTop }}\n                    >\n                      <span className=\"-ml-1 size-2 rounded-full bg-destructive\" />\n                      <span className=\"h-px flex-1 bg-destructive\" />\n                    </div>\n                  ) : null}\n                </div>\n              )\n            })}\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\n/** Build the seven Monday-first days for a week view. */\nexport function weekDays(rangeStart: Date): Date[] {\n  return Array.from({ length: 7 }, (_, i) => addDays(rangeStart, i))\n}\n",
      "type": "registry:component",
      "target": "components/blocks/calendar-view/time-grid-view.tsx"
    },
    {
      "path": "components/blocks/calendar-view/use-calendar-drag.ts",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\n/**\n * Pointer-driven drag-to-move for calendar events. View-agnostic: the caller\n * supplies `onResolve` (turn a pointer position into a drop target of type `T`,\n * e.g. a day or a time slot) and `onCommit` (apply the move). A small movement\n * threshold means a plain click on an event is never treated as a drag, and the\n * synthetic click that follows a drag is suppressed via {@link shouldSuppressClick}.\n */\nexport function useCalendarDrag<T>({\n  enabled,\n  onResolve,\n  onCommit,\n}: {\n  enabled: boolean\n  onResolve: (e: PointerEvent) => T | null\n  onCommit: (eventId: string, target: T) => void\n}) {\n  const [dragging, setDragging] = React.useState<string | null>(null)\n  const [target, setTarget] = React.useState<T | null>(null)\n  const stateRef = React.useRef<{\n    id: string\n    startX: number\n    startY: number\n    moved: boolean\n  } | null>(null)\n  const suppressRef = React.useRef(false)\n\n  // Keep the latest callbacks without re-binding listeners mid-drag.\n  const resolveRef = React.useRef(onResolve)\n  const commitRef = React.useRef(onCommit)\n  resolveRef.current = onResolve\n  commitRef.current = onCommit\n\n  const onPointerDown = React.useCallback(\n    (eventId: string) => (e: React.PointerEvent) => {\n      if (!enabled || e.button !== 0) return\n      stateRef.current = {\n        id: eventId,\n        startX: e.clientX,\n        startY: e.clientY,\n        moved: false,\n      }\n\n      const move = (ev: PointerEvent) => {\n        const s = stateRef.current\n        if (!s) return\n        if (\n          !s.moved &&\n          Math.hypot(ev.clientX - s.startX, ev.clientY - s.startY) < 4\n        )\n          return\n        if (!s.moved) {\n          s.moved = true\n          setDragging(s.id)\n          document.body.style.userSelect = \"none\"\n        }\n        setTarget(resolveRef.current(ev))\n      }\n\n      const up = (ev: PointerEvent) => {\n        const s = stateRef.current\n        window.removeEventListener(\"pointermove\", move)\n        window.removeEventListener(\"pointerup\", up)\n        document.body.style.userSelect = \"\"\n        if (s?.moved) {\n          suppressRef.current = true\n          setTimeout(() => (suppressRef.current = false), 0)\n          const t = resolveRef.current(ev)\n          if (t != null) commitRef.current(s.id, t)\n        }\n        stateRef.current = null\n        setDragging(null)\n        setTarget(null)\n      }\n\n      window.addEventListener(\"pointermove\", move)\n      window.addEventListener(\"pointerup\", up)\n    },\n    [enabled]\n  )\n\n  const shouldSuppressClick = React.useCallback(\n    () => suppressRef.current,\n    []\n  )\n\n  return { dragging, target, onPointerDown, shouldSuppressClick }\n}\n",
      "type": "registry:component",
      "target": "components/blocks/calendar-view/use-calendar-drag.ts"
    },
    {
      "path": "components/blocks/calendar-view/use-calendar-view.ts",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  addDays,\n  addMonths,\n  addWeeks,\n  endOfMonth,\n  endOfWeek,\n  format,\n  startOfMonth,\n  startOfWeek,\n} from \"date-fns\"\n\nimport type { CalendarView, CalendarViewProps } from \"./calendar-types\"\n\n/** en-GB conventions: weeks start on Monday. */\nexport const WEEK_OPTS = { weekStartsOn: 1 as const }\n\n/** Ordered weekday labels, Monday-first. */\nexport const WEEKDAY_LABELS = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"]\n\n/**\n * Headless state for the calendar: resolves controlled/uncontrolled `view` and\n * `date`, exposes prev/today/next stepping (view-aware), the current title, and\n * a live \"now\" clock (unless a fixed `now` was supplied).\n */\nexport function useCalendarView(props: CalendarViewProps) {\n  const {\n    view: viewProp,\n    defaultView = \"month\",\n    onViewChange,\n    date: dateProp,\n    defaultDate,\n    onDateChange,\n    now: nowProp,\n  } = props\n\n  const [viewState, setViewState] = React.useState<CalendarView>(defaultView)\n  const view = viewProp ?? viewState\n  const setView = React.useCallback(\n    (next: CalendarView) => {\n      if (viewProp === undefined) setViewState(next)\n      onViewChange?.(next)\n    },\n    [viewProp, onViewChange]\n  )\n\n  const [dateState, setDateState] = React.useState<Date>(\n    () => defaultDate ?? new Date()\n  )\n  const date = dateProp ?? dateState\n  const setDate = React.useCallback(\n    (next: Date) => {\n      if (dateProp === undefined) setDateState(next)\n      onDateChange?.(next)\n    },\n    [dateProp, onDateChange]\n  )\n\n  // Live clock for the current-time indicator + today ring. A fixed `now` (for\n  // tests / snapshots) disables the ticker.\n  const [liveNow, setLiveNow] = React.useState<Date>(() => nowProp ?? new Date())\n  React.useEffect(() => {\n    if (nowProp) return\n    const id = setInterval(() => setLiveNow(new Date()), 60_000)\n    return () => clearInterval(id)\n  }, [nowProp])\n  const now = nowProp ?? liveNow\n\n  const step = React.useCallback(\n    (dir: -1 | 1) => {\n      if (view === \"month\") setDate(addMonths(date, dir))\n      else if (view === \"week\") setDate(addWeeks(date, dir))\n      else setDate(addDays(date, dir))\n    },\n    [view, date, setDate]\n  )\n\n  const goToToday = React.useCallback(() => setDate(new Date()), [setDate])\n\n  const title = React.useMemo(() => {\n    if (view === \"month\") return format(date, \"MMMM yyyy\")\n    if (view === \"day\") return format(date, \"EEEE d MMMM yyyy\")\n    if (view === \"week\") {\n      const start = startOfWeek(date, WEEK_OPTS)\n      const end = endOfWeek(date, WEEK_OPTS)\n      const sameMonth = start.getMonth() === end.getMonth()\n      const sameYear = start.getFullYear() === end.getFullYear()\n      if (sameMonth) return `${format(start, \"d\")} – ${format(end, \"d MMMM yyyy\")}`\n      if (sameYear) return `${format(start, \"d MMM\")} – ${format(end, \"d MMM yyyy\")}`\n      return `${format(start, \"d MMM yyyy\")} – ${format(end, \"d MMM yyyy\")}`\n    }\n    // agenda: the month the anchor sits in.\n    return format(date, \"MMMM yyyy\")\n  }, [view, date])\n\n  const range = React.useMemo(() => {\n    if (view === \"month\") {\n      return {\n        start: startOfWeek(startOfMonth(date), WEEK_OPTS),\n        end: endOfWeek(endOfMonth(date), WEEK_OPTS),\n      }\n    }\n    if (view === \"week\") {\n      return {\n        start: startOfWeek(date, WEEK_OPTS),\n        end: endOfWeek(date, WEEK_OPTS),\n      }\n    }\n    if (view === \"day\") {\n      return { start: date, end: date }\n    }\n    // agenda: 28 days from the anchor.\n    return { start: date, end: addDays(date, 27) }\n  }, [view, date])\n\n  return { view, setView, date, setDate, now, step, goToToday, title, range }\n}\n\nexport type CalendarController = ReturnType<typeof useCalendarView>\n",
      "type": "registry:component",
      "target": "components/blocks/calendar-view/use-calendar-view.ts"
    }
  ],
  "type": "registry:block"
}