{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "event-calendar",
  "title": "Event Calendar",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-react/blocks/event-calendar/EventCalendar.tsx",
      "content": "'use client'\n\n/**\n * EventCalendar -- a full month-view event calendar with stats strip,\n * drag/shift range select, side rail (selected day or range summary),\n * upcoming list, and per-event/per-cell context menus.\n *\n * Configurable surface:\n *   - events / onEventsChange  CalendarEvent[]   the source of truth\n *   - eventTypes    Record<typeKey, EventTypeMeta>  visual config per type\n *   - initialDate   YYYY-MM-DD        which month opens first\n *   - weekStartsOn  0 | 1             Sunday vs Monday\n *   - loading                          skeleton state during fetch\n *   - title / description / show* toggles per pane\n *\n * Render-prop slots:\n *   - headerActions       extra controls to the right of search/view/new\n *   - eventDialog         full override of the detail dialog body\n *   - emptyDay            node shown when a selected day has no events\n *\n * Callbacks:\n *   - onEventClick(event)\n *   - onEventCreate(dateKey)            requested from \"New event\" or\n *                                       the per-cell context menu\n *   - onEventEdit / onEventDuplicate / onEventDelete(event)\n *   - onRangeChange({ lo, hi })         fires when the selection range moves\n */\nimport * as React from 'react'\nimport {\n  ChevronLeft,\n  ChevronRight,\n  Plus,\n  Clock,\n  MapPin,\n  Users,\n  Search,\n  CalendarDays,\n  ListFilter,\n  Sparkles,\n  Copy,\n  X,\n  CalendarPlus,\n  ArrowRight,\n  MousePointer2,\n  Pencil,\n  Trash2,\n  Eye,\n  CopyPlus,\n} from 'lucide-react'\nimport { useMonthGrid, dateFromKey } from '@/lib/use-month-grid'\nimport type { CalendarEvent, EventTypeMeta } from './types'\nimport { defaultEventTypes } from './defaults'\n\nexport type { CalendarEvent, EventTypeMeta } from './types'\nexport { defaultEventTypes } from './defaults'\nimport { Button } from '@/components/ui/button'\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'\nimport { Avatar, AvatarFallback } from '@/components/ui/avatar'\nimport { Separator } from '@/components/ui/separator'\nimport { Skeleton } from '@/components/ui/skeleton'\nimport { Input } from '@/components/ui/input'\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuTrigger,\n  ContextMenuLabel,\n} from '@/components/ui/context-menu'\n\nexport interface EventCalendarProps {\n  /** Source of truth. Pair with onEventsChange. */\n  events?: CalendarEvent[]\n  onEventsChange?: (value: CalendarEvent[]) => void\n  /** Visual + semantic theme per event type. Falls back to defaultEventTypes. */\n  eventTypes?: Record<string, EventTypeMeta>\n  /** YYYY-MM-DD -- month to open first. Defaults to today. */\n  initialDate?: string\n  /** 0 = Sunday (default), 1 = Monday. */\n  weekStartsOn?: 0 | 1\n  /** Show the skeleton placeholders instead of cells / rail rows. */\n  loading?: boolean\n  title?: string\n  description?: string\n  /** Default = true. Stats strip across the top. */\n  showStats?: boolean\n  /** Default = true. Side rail to the right of the grid. */\n  showSideRail?: boolean\n  /** Default = true. Search input in the header. */\n  showSearch?: boolean\n  /** Default = true. Month/Week/Day select. Week/Day not implemented yet\n   *  -- the select is purely visual until you wire up the modes. */\n  showViewSelect?: boolean\n  /** Default = true. Legend strip under the grid. */\n  showLegend?: boolean\n  /** Default = true. \"Up next\" pane under the side rail. */\n  showUpcoming?: boolean\n  onEventClick?: (event: CalendarEvent) => void\n  onEventCreate?: (dateKey: string) => void\n  onEventEdit?: (event: CalendarEvent) => void\n  onEventDuplicate?: (event: CalendarEvent) => void\n  onEventDelete?: (event: CalendarEvent) => void\n  onRangeChange?: (bounds: { lo: string; hi: string }) => void\n  /** Render-prop override for the controls to the right of search/view. */\n  headerActions?: React.ReactNode\n  /** Render-prop override for the dialog body. */\n  eventDialog?: (event: CalendarEvent) => React.ReactNode\n  /** Render-prop override for the empty selected-day state. */\n  emptyDay?: React.ReactNode\n}\n\nexport function EventCalendar({\n  events = [],\n  onEventsChange: _onEventsChange,\n  eventTypes = defaultEventTypes,\n  initialDate,\n  weekStartsOn = 0,\n  loading = false,\n  title = 'Calendar',\n  description = 'Schedule, meetings, and deadlines. Drag or shift-click to select a range.',\n  showStats = true,\n  showSideRail = true,\n  showSearch = true,\n  showViewSelect = true,\n  showLegend = true,\n  showUpcoming = true,\n  onEventClick,\n  onEventCreate,\n  onEventEdit,\n  onEventDuplicate,\n  onEventDelete,\n  onRangeChange,\n  headerActions,\n  eventDialog,\n  emptyDay,\n}: EventCalendarProps) {\n  const [view, setView] = React.useState<'month' | 'week' | 'day'>('month')\n  const [eventOpen, setEventOpen] = React.useState(false)\n  const [selectedEvent, setSelectedEvent] = React.useState<CalendarEvent | null>(null)\n  const [search, setSearch] = React.useState('')\n\n  const {\n    todayKey,\n    cursor,\n    monthLabel,\n    gridDays,\n    weekdays,\n    rangeStart,\n    rangeBounds,\n    rangeDayCount,\n    isRange,\n    inRange,\n    prevMonth,\n    nextMonth,\n    goToToday,\n    selectWeekOf,\n    clearRange,\n    onCellMouseDown,\n    onCellMouseEnter,\n    endDrag,\n  } = useMonthGrid({ initialDate, weekStartsOn })\n\n  React.useEffect(() => {\n    onRangeChange?.(rangeBounds)\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [rangeBounds.lo, rangeBounds.hi])\n\n  // Fall through to a neutral style for unknown event types so the calendar\n  // doesn't crash when a consumer passes a type that's missing from the map.\n  const neutralMeta: EventTypeMeta = {\n    label: 'Event',\n    icon: CalendarDays,\n    chip: 'bg-muted text-muted-foreground ring-muted-foreground/20',\n    bar: 'bg-muted-foreground',\n    dot: 'bg-muted-foreground',\n    glow: 'bg-muted-foreground/10',\n    text: 'text-muted-foreground',\n    ring: 'hover:ring-muted-foreground/30',\n  }\n  function metaFor(type: string): EventTypeMeta {\n    return eventTypes[type] ?? neutralMeta\n  }\n\n  const typeKeys = React.useMemo(() => Object.keys(eventTypes), [eventTypes])\n\n  const filtered = React.useMemo(() => {\n    const q = search.trim().toLowerCase()\n    if (!q) return events\n    return events.filter(\n      (e) =>\n        e.title.toLowerCase().includes(q) ||\n        (e.description ?? '').toLowerCase().includes(q) ||\n        (e.location ?? '').toLowerCase().includes(q),\n    )\n  }, [events, search])\n\n  const eventsByDate = React.useMemo(() => {\n    const map = new Map<string, CalendarEvent[]>()\n    for (const e of filtered) {\n      if (!map.has(e.date)) map.set(e.date, [])\n      map.get(e.date)!.push(e)\n    }\n    for (const arr of map.values()) arr.sort((a, b) => a.start.localeCompare(b.start))\n    return map\n  }, [filtered])\n\n  const monthCounts = React.useMemo(() => {\n    const y = cursor.getFullYear()\n    const m = cursor.getMonth()\n    const out: Record<string, number> & { total: number } = { total: 0 }\n    for (const k of typeKeys) out[k] = 0\n    for (const e of events) {\n      const d = new Date(e.date)\n      if (d.getFullYear() === y && d.getMonth() === m) {\n        if (out[e.type] !== undefined) out[e.type]!++\n        out.total++\n      }\n    }\n    return out\n  }, [cursor, typeKeys, events])\n\n  const typeStats = React.useMemo(() => {\n    const y = cursor.getFullYear()\n    const m = cursor.getMonth()\n    const out: Record<string, { weeks: number[]; next: CalendarEvent | null }> = {}\n    for (const k of typeKeys) out[k] = { weeks: [0, 0, 0, 0, 0, 0], next: null }\n    const firstOffset = new Date(y, m, 1).getDay()\n\n    for (const e of events) {\n      const d = new Date(e.date)\n      if (d.getFullYear() === y && d.getMonth() === m && out[e.type]) {\n        const week = Math.floor((d.getDate() - 1 + firstOffset) / 7)\n        out[e.type]!.weeks[week]!++\n      }\n    }\n    for (const t of typeKeys) {\n      const next = events\n        .filter((e) => e.type === t && e.date >= todayKey)\n        .sort((a, b) => a.date.localeCompare(b.date) || a.start.localeCompare(b.start))[0]\n      out[t]!.next = next ?? null\n    }\n    return out\n  }, [cursor, typeKeys, events, todayKey])\n\n  function fmtNextDate(key: string) {\n    if (key === todayKey) return 'Today'\n    return dateFromKey(key).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })\n  }\n\n  const rangeEvents = React.useMemo(() => {\n    const { lo, hi } = rangeBounds\n    return filtered\n      .filter((e) => e.date >= lo && e.date <= hi)\n      .sort((a, b) => a.date.localeCompare(b.date) || a.start.localeCompare(b.start))\n  }, [filtered, rangeBounds])\n\n  const rangeTypeCounts = React.useMemo(() => {\n    const init: Record<string, number> = {}\n    for (const k of typeKeys) init[k] = 0\n    for (const e of rangeEvents) {\n      if (init[e.type] !== undefined) init[e.type]!++\n    }\n    return init\n  }, [typeKeys, rangeEvents])\n\n  const selectedDayEvents = eventsByDate.get(rangeStart) ?? []\n\n  const upcoming = React.useMemo(\n    () =>\n      filtered\n        .filter((e) => e.date > todayKey)\n        .sort((a, b) => a.date.localeCompare(b.date) || a.start.localeCompare(b.start))\n        .slice(0, 4),\n    [filtered, todayKey],\n  )\n\n  function openEvent(e: CalendarEvent) {\n    setSelectedEvent(e)\n    setEventOpen(true)\n    onEventClick?.(e)\n  }\n\n  function copyDate(key: string) {\n    if (typeof navigator !== 'undefined' && navigator.clipboard) {\n      navigator.clipboard.writeText(key).catch(() => {\n        /* clipboard may be denied; nothing to do */\n      })\n    }\n  }\n\n  function fmtDayLong(key: string) {\n    return dateFromKey(key).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })\n  }\n  function fmtDayShort(key: string) {\n    return dateFromKey(key).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })\n  }\n\n  function initials(name: string) {\n    return name\n      .split(' ')\n      .map((n) => n[0])\n      .join('')\n      .slice(0, 2)\n  }\n\n  function cellRangeClass(key: string, inMonth: boolean) {\n    const { lo, hi } = rangeBounds\n    if (!inRange(key)) return inMonth ? 'bg-background hover:bg-accent/40' : 'bg-muted/20 hover:bg-muted/30'\n    if (key === lo && key === hi) return 'bg-accent/30 ring-1 ring-inset ring-primary/60'\n    let cls = 'bg-primary/10 hover:bg-primary/15'\n    if (key === lo) cls += ' ring-1 ring-inset ring-primary/60'\n    if (key === hi) cls += ' ring-1 ring-inset ring-primary/60'\n    return cls\n  }\n\n  return (\n    <div className=\"flex flex-col gap-5\" onMouseUp={endDrag}>\n      {/* Header */}\n      <header className=\"flex flex-wrap items-end justify-between gap-4\">\n        <div>\n          <h1 className=\"text-2xl font-semibold tracking-tight\">{title}</h1>\n          {description && <p className=\"text-muted-foreground mt-1 text-sm\">{description}</p>}\n        </div>\n        <div className=\"flex items-center gap-2\">\n          {showSearch && (\n            <div className=\"relative\">\n              <Search className=\"text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2\" />\n              <Input\n                value={search}\n                onChange={(e) => setSearch(e.target.value)}\n                placeholder=\"Search events\"\n                className=\"h-8 w-56 pl-8 text-xs\"\n              />\n            </div>\n          )}\n          {showViewSelect && (\n            <Select value={view} onValueChange={(v) => setView(v as 'month' | 'week' | 'day')}>\n              <SelectTrigger className=\"h-8 w-24 text-xs\">\n                <SelectValue />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem value=\"month\">Month</SelectItem>\n                <SelectItem value=\"week\">Week</SelectItem>\n                <SelectItem value=\"day\">Day</SelectItem>\n              </SelectContent>\n            </Select>\n          )}\n          {headerActions ?? (\n            <Button size=\"sm\" className=\"gap-1.5\" onClick={() => onEventCreate?.(rangeStart)}>\n              <Plus className=\"size-3.5\" />\n              New event\n            </Button>\n          )}\n        </div>\n      </header>\n\n      {/* Stats strip */}\n      {showStats && (\n        <div className={`grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-${Math.min(typeKeys.length, 4)}`}>\n          {typeKeys.map((key) => {\n            const Icon = eventTypes[key]!.icon\n            return (\n              <button\n                key={key}\n                type=\"button\"\n                className={[\n                  'group bg-card/40 hover:bg-card relative isolate flex flex-col overflow-hidden rounded-xl border p-4 text-left ring-1 ring-transparent backdrop-blur transition-all ring-inset hover:-translate-y-px hover:shadow-lg',\n                  eventTypes[key]!.ring,\n                ].join(' ')}\n                onClick={() =>\n                  setSearch(\n                    eventTypes[key]!.label.toLowerCase() === search.toLowerCase()\n                      ? ''\n                      : eventTypes[key]!.label.toLowerCase(),\n                  )\n                }\n              >\n                <Icon\n                  className={[\n                    'pointer-events-none absolute -top-4 -right-4 size-24 opacity-[0.06] transition-opacity group-hover:opacity-[0.12]',\n                    eventTypes[key]!.text,\n                  ].join(' ')}\n                />\n                <div\n                  className={[\n                    'pointer-events-none absolute -right-12 -bottom-12 size-32 rounded-full blur-2xl',\n                    eventTypes[key]!.glow,\n                  ].join(' ')}\n                />\n\n                <div className=\"flex items-center justify-between gap-2\">\n                  <div className=\"flex items-center gap-1.5\">\n                    <span className={['size-1.5 rounded-full', eventTypes[key]!.dot].join(' ')} />\n                    <p className=\"text-muted-foreground text-[10px] font-medium tracking-[0.14em] uppercase\">\n                      {eventTypes[key]!.label}\n                    </p>\n                  </div>\n                  <div\n                    className={[\n                      'flex size-7 items-center justify-center rounded-md ring-1 ring-inset',\n                      eventTypes[key]!.chip,\n                    ].join(' ')}\n                  >\n                    <Icon className=\"size-3.5\" />\n                  </div>\n                </div>\n\n                <div className=\"mt-2 flex items-baseline gap-1.5\">\n                  <p className=\"text-3xl leading-none font-semibold tabular-nums\">{monthCounts[key] ?? 0}</p>\n                  <p className=\"text-muted-foreground text-[10px] tracking-wider uppercase\">this month</p>\n                </div>\n\n                <div className=\"mt-3 flex h-6 items-end gap-1\">\n                  {typeStats[key]!.weeks.slice(0, 6).map((n, i) => (\n                    <span\n                      key={i}\n                      className={[\n                        'flex-1 rounded-sm transition-colors',\n                        n > 0 ? eventTypes[key]!.bar : 'bg-muted/40',\n                        n > 0 ? 'opacity-70 group-hover:opacity-100' : '',\n                      ].join(' ')}\n                      style={{ height: n > 0 ? `${Math.min(100, 30 + n * 35)}%` : '20%' }}\n                      title={`Week ${i + 1}: ${n} ${eventTypes[key]!.label.toLowerCase()}${n === 1 ? '' : 's'}`}\n                    />\n                  ))}\n                </div>\n\n                <div className=\"border-border/50 mt-3 min-h-[2.25rem] border-t pt-2\">\n                  {typeStats[key]!.next ? (\n                    <>\n                      <p className=\"text-muted-foreground text-[10px] tracking-wider uppercase\">\n                        Next <span className={eventTypes[key]!.text}>{fmtNextDate(typeStats[key]!.next!.date)}</span>\n                      </p>\n                      <p className=\"mt-0.5 truncate text-xs font-medium\">{typeStats[key]!.next!.title}</p>\n                    </>\n                  ) : (\n                    <>\n                      <p className=\"text-muted-foreground text-[10px] tracking-wider uppercase\">No upcoming</p>\n                      <p className=\"text-muted-foreground/70 mt-0.5 text-xs\">All clear</p>\n                    </>\n                  )}\n                </div>\n              </button>\n            )\n          })}\n        </div>\n      )}\n\n      {/* Main: calendar + side rail */}\n      <div className={['grid gap-4', showSideRail ? 'lg:grid-cols-[minmax(0,1fr)_320px]' : ''].join(' ')}>\n        {/* Month grid */}\n        <div className=\"bg-card/40 overflow-hidden rounded-xl border\">\n          <div className=\"bg-muted/30 flex flex-wrap items-center justify-between gap-3 border-b px-4 py-2.5\">\n            <div className=\"flex items-center gap-2\">\n              <Button variant=\"outline\" size=\"icon\" className=\"size-7\" onClick={prevMonth}>\n                <ChevronLeft className=\"size-4\" />\n              </Button>\n              <Button variant=\"outline\" size=\"icon\" className=\"size-7\" onClick={nextMonth}>\n                <ChevronRight className=\"size-4\" />\n              </Button>\n              <Button variant=\"ghost\" size=\"sm\" className=\"h-7 text-xs\" onClick={goToToday}>\n                Today\n              </Button>\n              <h2 className=\"ml-2 text-sm font-semibold\">{monthLabel}</h2>\n            </div>\n            <div className=\"text-muted-foreground flex items-center gap-3 text-[11px]\">\n              {isRange && (\n                <div className=\"bg-primary/10 text-primary ring-primary/20 flex items-center gap-1.5 rounded-full px-2 py-0.5 ring-1 ring-inset\">\n                  <MousePointer2 className=\"size-3\" />\n                  <span>\n                    {rangeDayCount} days, {rangeEvents.length} events\n                  </span>\n                  <button className=\"hover:text-foreground ml-0.5\" onClick={clearRange}>\n                    <X className=\"size-3\" />\n                  </button>\n                </div>\n              )}\n              <div className=\"flex items-center gap-1.5\">\n                <Sparkles className=\"size-3\" />\n                <span>\n                  {monthCounts.total} event{monthCounts.total === 1 ? '' : 's'} this month\n                </span>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"bg-muted/10 text-muted-foreground grid grid-cols-7 border-b text-[10px] tracking-wider uppercase\">\n            {weekdays.map((w) => (\n              <div key={w} className=\"px-2 py-2 font-medium\">\n                {w}\n              </div>\n            ))}\n          </div>\n\n          <div className=\"grid grid-cols-7 select-none\">\n            {loading ? (\n              Array.from({ length: 42 }).map((_, i) => (\n                <div key={i} className=\"h-24 border-r border-b p-1.5 last:border-r-0\">\n                  <Skeleton className=\"h-3 w-6\" />\n                  <Skeleton className=\"mt-2 h-3 w-full\" />\n                </div>\n              ))\n            ) : (\n              gridDays.map((d, i) => (\n                <ContextMenu key={d.key}>\n                  <ContextMenuTrigger asChild>\n                    <button\n                      type=\"button\"\n                      className={[\n                        'group focus-visible:ring-ring relative flex h-24 flex-col gap-1 border-r border-b p-1.5 text-left transition-colors focus-visible:z-10 focus-visible:ring-2 focus-visible:outline-none',\n                        (i + 1) % 7 === 0 ? 'border-r-0' : '',\n                        i >= 35 ? 'border-b-0' : '',\n                        cellRangeClass(d.key, d.inMonth),\n                        !d.inMonth && !inRange(d.key) ? 'text-muted-foreground/60' : '',\n                      ].join(' ')}\n                      onMouseDown={(e) => onCellMouseDown(d.key, e)}\n                      onMouseEnter={() => onCellMouseEnter(d.key)}\n                    >\n                      <div className=\"flex items-center justify-between\">\n                        <span\n                          className={[\n                            'inline-flex size-5 items-center justify-center rounded-full text-[11px] tabular-nums',\n                            d.key === todayKey ? 'bg-primary text-primary-foreground font-semibold' : '',\n                            d.key !== todayKey && d.inMonth ? 'text-foreground' : '',\n                            !d.inMonth ? 'text-muted-foreground/60' : '',\n                          ].join(' ')}\n                        >\n                          {d.date.getDate()}\n                        </span>\n                        {(eventsByDate.get(d.key)?.length ?? 0) > 0 && (\n                          <span className=\"text-muted-foreground text-[9px] tabular-nums\">\n                            {eventsByDate.get(d.key)!.length}\n                          </span>\n                        )}\n                      </div>\n                      <div className=\"flex flex-col gap-0.5\">\n                        {(eventsByDate.get(d.key) ?? []).slice(0, 2).map((e) => (\n                          <ContextMenu key={e.id}>\n                            <ContextMenuTrigger asChild>\n                              <span\n                                className={[\n                                  'flex cursor-pointer items-center gap-1 truncate rounded px-1 py-0.5 text-[10px] ring-1 ring-inset',\n                                  metaFor(e.type).chip,\n                                ].join(' ')}\n                                onMouseDown={(ev) => ev.stopPropagation()}\n                                onClick={(ev) => {\n                                  ev.stopPropagation()\n                                  openEvent(e)\n                                }}\n                                onContextMenu={(ev) => ev.stopPropagation()}\n                              >\n                                <span className=\"tabular-nums opacity-70\">{e.start}</span>\n                                <span className=\"truncate\">{e.title}</span>\n                              </span>\n                            </ContextMenuTrigger>\n                            <ContextMenuContent className=\"w-52\">\n                              <ContextMenuLabel className=\"text-muted-foreground flex items-center gap-1.5 text-[11px]\">\n                                <span className={['size-1.5 rounded-full', metaFor(e.type).dot].join(' ')} />\n                                <span className=\"truncate\">{e.title}</span>\n                              </ContextMenuLabel>\n                              <ContextMenuSeparator />\n                              <ContextMenuItem className=\"gap-2\" onSelect={() => openEvent(e)}>\n                                <Eye className=\"size-3.5\" /> View details\n                                <ContextMenuShortcut>Enter</ContextMenuShortcut>\n                              </ContextMenuItem>\n                              <ContextMenuItem className=\"gap-2\" onSelect={() => onEventEdit?.(e)}>\n                                <Pencil className=\"size-3.5\" /> Edit\n                              </ContextMenuItem>\n                              <ContextMenuItem className=\"gap-2\" onSelect={() => onEventDuplicate?.(e)}>\n                                <CopyPlus className=\"size-3.5\" /> Duplicate\n                              </ContextMenuItem>\n                              <ContextMenuItem className=\"gap-2\" onSelect={() => copyDate(e.date)}>\n                                <Copy className=\"size-3.5\" /> Copy date\n                              </ContextMenuItem>\n                              <ContextMenuSeparator />\n                              <ContextMenuItem\n                                className=\"text-destructive focus:text-destructive gap-2\"\n                                onSelect={() => onEventDelete?.(e)}\n                              >\n                                <Trash2 className=\"size-3.5\" /> Cancel event\n                              </ContextMenuItem>\n                            </ContextMenuContent>\n                          </ContextMenu>\n                        ))}\n                        {(eventsByDate.get(d.key)?.length ?? 0) > 2 && (\n                          <span className=\"text-muted-foreground px-1 text-[10px]\">\n                            +{eventsByDate.get(d.key)!.length - 2} more\n                          </span>\n                        )}\n                      </div>\n                    </button>\n                  </ContextMenuTrigger>\n                  <ContextMenuContent className=\"w-56\">\n                    <ContextMenuLabel className=\"text-muted-foreground text-[11px]\">{fmtDayLong(d.key)}</ContextMenuLabel>\n                    <ContextMenuSeparator />\n                    <ContextMenuItem className=\"gap-2\" onSelect={() => onEventCreate?.(d.key)}>\n                      <CalendarPlus className=\"size-3.5\" /> New event\n                      <ContextMenuShortcut>N</ContextMenuShortcut>\n                    </ContextMenuItem>\n                    <ContextMenuItem className=\"gap-2\" onSelect={() => selectWeekOf(d.key)}>\n                      <CalendarDays className=\"size-3.5\" /> Select this week\n                    </ContextMenuItem>\n                    <ContextMenuSeparator />\n                    <ContextMenuItem className=\"gap-2\" onSelect={() => copyDate(d.key)}>\n                      <Copy className=\"size-3.5\" /> Copy date\n                      <ContextMenuShortcut className=\"tabular-nums\">{d.key}</ContextMenuShortcut>\n                    </ContextMenuItem>\n                    <ContextMenuItem className=\"gap-2\" onSelect={goToToday}>\n                      <ArrowRight className=\"size-3.5\" /> Go to today\n                    </ContextMenuItem>\n                    {isRange && (\n                      <ContextMenuItem className=\"text-destructive focus:text-destructive gap-2\" onSelect={clearRange}>\n                        <X className=\"size-3.5\" /> Clear range\n                      </ContextMenuItem>\n                    )}\n                  </ContextMenuContent>\n                </ContextMenu>\n              ))\n            )}\n          </div>\n\n          {showLegend && (\n            <div className=\"bg-muted/20 text-muted-foreground flex flex-wrap items-center gap-3 border-t px-4 py-2 text-[10px]\">\n              <ListFilter className=\"size-3\" />\n              {typeKeys.map((key) => (\n                <div key={key} className=\"flex items-center gap-1.5\">\n                  <span className={['size-2 rounded-full', eventTypes[key]!.dot].join(' ')} />\n                  {eventTypes[key]!.label}\n                </div>\n              ))}\n              <span className=\"ml-auto text-[10px]\">Tip: drag or shift-click to range. Right-click for actions.</span>\n            </div>\n          )}\n        </div>\n\n        {/* Side rail */}\n        {showSideRail && (\n          <aside className=\"flex flex-col gap-4\">\n            {!isRange ? (\n              <div className=\"bg-card/40 overflow-hidden rounded-xl border\">\n                <div className=\"bg-muted/30 border-b px-4 py-3\">\n                  <div className=\"flex items-center justify-between\">\n                    <div>\n                      <p className=\"text-muted-foreground text-[10px] tracking-wider uppercase\">\n                        {rangeStart === todayKey ? 'Today' : 'Selected'}\n                      </p>\n                      <p className=\"mt-0.5 text-sm font-semibold\">{fmtDayLong(rangeStart)}</p>\n                    </div>\n                    <div className=\"text-right\">\n                      <p className=\"text-muted-foreground text-[10px] tracking-wider uppercase\">Events</p>\n                      <p className=\"text-sm font-semibold tabular-nums\">{selectedDayEvents.length}</p>\n                    </div>\n                  </div>\n                </div>\n                <div className=\"max-h-[420px] overflow-y-auto p-3\">\n                  {loading ? (\n                    Array.from({ length: 3 }).map((_, i) => (\n                      <div key={i} className=\"mb-2 space-y-2 rounded-lg border p-3\">\n                        <Skeleton className=\"h-3 w-32\" />\n                        <Skeleton className=\"h-2 w-20\" />\n                      </div>\n                    ))\n                  ) : selectedDayEvents.length === 0 ? (\n                    emptyDay ?? (\n                      <div className=\"flex flex-col items-center justify-center gap-2 py-8 text-center\">\n                        <div className=\"bg-muted flex size-10 items-center justify-center rounded-full\">\n                          <CalendarDays className=\"text-muted-foreground size-5\" />\n                        </div>\n                        <p className=\"text-sm font-medium\">Nothing scheduled</p>\n                        <p className=\"text-muted-foreground text-xs\">Click a date or add a new event.</p>\n                        <Button\n                          size=\"sm\"\n                          variant=\"outline\"\n                          className=\"mt-1 h-7 gap-1.5 text-xs\"\n                          onClick={() => onEventCreate?.(rangeStart)}\n                        >\n                          <Plus className=\"size-3\" /> New event\n                        </Button>\n                      </div>\n                    )\n                  ) : (\n                    selectedDayEvents.map((e) => (\n                      <ContextMenu key={e.id}>\n                        <ContextMenuTrigger asChild>\n                          <div\n                            className=\"group hover:bg-accent/50 relative flex cursor-pointer gap-3 rounded-lg p-2.5 transition-colors\"\n                            onClick={() => openEvent(e)}\n                          >\n                            <div className={['w-0.5 shrink-0 rounded-full', metaFor(e.type).bar].join(' ')} />\n                            <div className=\"min-w-0 flex-1 space-y-1\">\n                              <div className=\"flex items-start justify-between gap-2\">\n                                <p className=\"text-sm leading-tight font-medium\">{e.title}</p>\n                                <span\n                                  className={[\n                                    'shrink-0 rounded px-1.5 py-0.5 text-[9px] tracking-wider uppercase ring-1 ring-inset',\n                                    metaFor(e.type).chip,\n                                  ].join(' ')}\n                                >\n                                  {metaFor(e.type).label}\n                                </span>\n                              </div>\n                              <div className=\"text-muted-foreground flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[11px]\">\n                                <span className=\"inline-flex items-center gap-1\">\n                                  <Clock className=\"size-3\" />\n                                  {e.start}-{e.end}\n                                </span>\n                                {e.location && (\n                                  <span className=\"inline-flex items-center gap-1\">\n                                    <MapPin className=\"size-3\" />\n                                    {e.location}\n                                  </span>\n                                )}\n                              </div>\n                              {e.attendees?.length ? (\n                                <div className=\"flex items-center -space-x-1.5 pt-0.5\">\n                                  {e.attendees.slice(0, 4).map((a, i) => (\n                                    <Avatar key={i} className=\"border-background size-5 border-2\">\n                                      <AvatarFallback className=\"bg-primary/10 text-primary text-[8px]\">\n                                        {initials(a)}\n                                      </AvatarFallback>\n                                    </Avatar>\n                                  ))}\n                                  {e.attendees.length > 4 && (\n                                    <span className=\"text-muted-foreground pl-2 text-[10px]\">\n                                      +{e.attendees.length - 4}\n                                    </span>\n                                  )}\n                                </div>\n                              ) : null}\n                            </div>\n                          </div>\n                        </ContextMenuTrigger>\n                        <ContextMenuContent className=\"w-48\">\n                          <ContextMenuLabel className=\"text-muted-foreground truncate text-[11px]\">\n                            {e.title}\n                          </ContextMenuLabel>\n                          <ContextMenuSeparator />\n                          <ContextMenuItem className=\"gap-2\" onSelect={() => openEvent(e)}>\n                            <Eye className=\"size-3.5\" /> View details\n                          </ContextMenuItem>\n                          <ContextMenuItem className=\"gap-2\" onSelect={() => onEventEdit?.(e)}>\n                            <Pencil className=\"size-3.5\" /> Edit\n                          </ContextMenuItem>\n                          <ContextMenuItem className=\"gap-2\" onSelect={() => onEventDuplicate?.(e)}>\n                            <CopyPlus className=\"size-3.5\" /> Duplicate\n                          </ContextMenuItem>\n                          <ContextMenuSeparator />\n                          <ContextMenuItem\n                            className=\"text-destructive focus:text-destructive gap-2\"\n                            onSelect={() => onEventDelete?.(e)}\n                          >\n                            <Trash2 className=\"size-3.5\" /> Cancel event\n                          </ContextMenuItem>\n                        </ContextMenuContent>\n                      </ContextMenu>\n                    ))\n                  )}\n                </div>\n              </div>\n            ) : (\n              <div className=\"bg-card/40 overflow-hidden rounded-xl border\">\n                <div className=\"from-primary/10 border-b bg-gradient-to-r to-transparent px-4 py-3\">\n                  <div className=\"flex items-center justify-between gap-3\">\n                    <div className=\"min-w-0\">\n                      <p className=\"text-muted-foreground text-[10px] tracking-wider uppercase\">\n                        Range, {rangeDayCount} days\n                      </p>\n                      <p className=\"mt-0.5 truncate text-sm font-semibold\">\n                        {fmtDayShort(rangeBounds.lo)} -&gt; {fmtDayShort(rangeBounds.hi)}\n                      </p>\n                    </div>\n                    <Button variant=\"ghost\" size=\"icon\" className=\"size-7\" onClick={clearRange}>\n                      <X className=\"size-3.5\" />\n                    </Button>\n                  </div>\n                  <div className={`mt-3 grid gap-2 grid-cols-${Math.min(typeKeys.length, 4)}`}>\n                    {typeKeys.map((key) => (\n                      <div key={key} className=\"bg-background/40 rounded-md border px-2 py-1.5\">\n                        <div className=\"text-muted-foreground flex items-center gap-1 text-[9px] tracking-wider uppercase\">\n                          <span className={['size-1.5 rounded-full', eventTypes[key]!.dot].join(' ')} />\n                          {eventTypes[key]!.label}\n                        </div>\n                        <p className=\"mt-0.5 text-sm font-semibold tabular-nums\">{rangeTypeCounts[key] ?? 0}</p>\n                      </div>\n                    ))}\n                  </div>\n                </div>\n                <div className=\"max-h-[420px] overflow-y-auto\">\n                  {rangeEvents.length === 0 ? (\n                    <p className=\"text-muted-foreground px-4 py-8 text-center text-xs\">No events in range.</p>\n                  ) : (\n                    rangeEvents.map((e) => (\n                      <div\n                        key={e.id}\n                        className=\"hover:bg-accent/40 flex cursor-pointer items-start gap-3 border-b px-3 py-2.5 transition-colors last:border-b-0\"\n                        onClick={() => openEvent(e)}\n                      >\n                        <div className=\"bg-background/60 flex w-10 shrink-0 flex-col items-center rounded-md border px-1 py-1 text-center\">\n                          <span className=\"text-muted-foreground text-[8px] uppercase\">\n                            {new Date(e.date).toLocaleDateString('en-US', { month: 'short' })}\n                          </span>\n                          <span className=\"text-sm leading-none font-semibold tabular-nums\">\n                            {new Date(e.date).getDate()}\n                          </span>\n                        </div>\n                        <div className={['w-0.5 shrink-0 self-stretch rounded-full', metaFor(e.type).bar].join(' ')} />\n                        <div className=\"min-w-0 flex-1\">\n                          <p className=\"truncate text-xs font-medium\">{e.title}</p>\n                          <p className=\"text-muted-foreground mt-0.5 text-[11px] tabular-nums\">\n                            {e.start}-{e.end}\n                            {e.location && <span> {e.location}</span>}\n                          </p>\n                        </div>\n                        <span\n                          className={[\n                            'shrink-0 rounded px-1.5 py-0.5 text-[9px] tracking-wider uppercase ring-1 ring-inset',\n                            metaFor(e.type).chip,\n                          ].join(' ')}\n                        >\n                          {metaFor(e.type).label}\n                        </span>\n                      </div>\n                    ))\n                  )}\n                </div>\n              </div>\n            )}\n\n            {showUpcoming && (\n              <div className=\"bg-card/40 overflow-hidden rounded-xl border\">\n                <div className=\"bg-muted/30 border-b px-4 py-2.5\">\n                  <p className=\"text-sm font-semibold\">Up next</p>\n                  <p className=\"text-muted-foreground text-[11px]\">After today</p>\n                </div>\n                <div className=\"divide-y\">\n                  {loading ? (\n                    Array.from({ length: 3 }).map((_, i) => (\n                      <div key={i} className=\"flex items-center gap-3 p-3\">\n                        <Skeleton className=\"size-9 rounded-md\" />\n                        <div className=\"flex-1 space-y-1\">\n                          <Skeleton className=\"h-3 w-32\" />\n                          <Skeleton className=\"h-2 w-20\" />\n                        </div>\n                      </div>\n                    ))\n                  ) : upcoming.length === 0 ? (\n                    <p className=\"text-muted-foreground px-4 py-6 text-center text-xs\">Nothing on the horizon.</p>\n                  ) : (\n                    upcoming.map((e) => (\n                      <button\n                        key={e.id}\n                        type=\"button\"\n                        className=\"group hover:bg-accent/40 flex w-full items-center gap-3 p-3 text-left transition-colors\"\n                        onClick={() => openEvent(e)}\n                      >\n                        <div className=\"bg-background flex size-10 shrink-0 flex-col items-center justify-center rounded-md border text-center\">\n                          <span className=\"text-muted-foreground text-[9px] uppercase\">\n                            {new Date(e.date).toLocaleDateString('en-US', { month: 'short' })}\n                          </span>\n                          <span className=\"text-sm leading-none font-semibold tabular-nums\">\n                            {new Date(e.date).getDate()}\n                          </span>\n                        </div>\n                        <div className=\"min-w-0 flex-1\">\n                          <div className=\"flex items-center gap-1.5\">\n                            <span className={['size-1.5 rounded-full', metaFor(e.type).dot].join(' ')} />\n                            <p className=\"truncate text-xs font-medium\">{e.title}</p>\n                          </div>\n                          <p className=\"text-muted-foreground mt-0.5 text-[11px] tabular-nums\">\n                            {e.start}-{e.end}\n                            {e.location && <span> {e.location}</span>}\n                          </p>\n                        </div>\n                      </button>\n                    ))\n                  )}\n                </div>\n              </div>\n            )}\n          </aside>\n        )}\n      </div>\n\n      {/* Event Detail Dialog */}\n      <Dialog open={eventOpen} onOpenChange={setEventOpen}>\n        {selectedEvent && (\n          <DialogContent className=\"sm:max-w-md\">\n            {eventDialog ? (\n              eventDialog(selectedEvent)\n            ) : (\n              <>\n                <DialogHeader>\n                  <div className=\"flex items-center gap-2\">\n                    <div\n                      className={[\n                        'flex size-8 items-center justify-center rounded-md ring-1 ring-inset',\n                        metaFor(selectedEvent.type).chip,\n                      ].join(' ')}\n                    >\n                      {React.createElement(metaFor(selectedEvent.type).icon, { className: 'size-4' })}\n                    </div>\n                    <div>\n                      <DialogTitle className=\"text-base\">{selectedEvent.title}</DialogTitle>\n                      <DialogDescription className=\"text-xs\">\n                        {metaFor(selectedEvent.type).label}\n                        {selectedEvent.status && <span> {selectedEvent.status}</span>}\n                      </DialogDescription>\n                    </div>\n                  </div>\n                </DialogHeader>\n                <div className=\"space-y-3 py-2\">\n                  {selectedEvent.description && (\n                    <p className=\"text-muted-foreground text-sm\">{selectedEvent.description}</p>\n                  )}\n                  <Separator />\n                  <div className=\"grid gap-2 text-sm\">\n                    <div className=\"flex items-center gap-2\">\n                      <Clock className=\"text-muted-foreground size-4\" />\n                      <span>\n                        {selectedEvent.date} {selectedEvent.start} - {selectedEvent.end}\n                      </span>\n                    </div>\n                    {selectedEvent.location && (\n                      <div className=\"flex items-center gap-2\">\n                        <MapPin className=\"text-muted-foreground size-4\" />\n                        <span>{selectedEvent.location}</span>\n                      </div>\n                    )}\n                    {selectedEvent.attendees?.length ? (\n                      <div className=\"flex items-start gap-2\">\n                        <Users className=\"text-muted-foreground mt-0.5 size-4\" />\n                        <div className=\"flex flex-wrap items-center gap-1.5\">\n                          {selectedEvent.attendees.map((a) => (\n                            <div\n                              key={a}\n                              className=\"bg-muted flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs\"\n                            >\n                              <Avatar className=\"size-4\">\n                                <AvatarFallback className=\"bg-primary/10 text-primary text-[8px]\">\n                                  {initials(a)}\n                                </AvatarFallback>\n                              </Avatar>\n                              <span>{a}</span>\n                            </div>\n                          ))}\n                        </div>\n                      </div>\n                    ) : null}\n                  </div>\n                </div>\n                <DialogFooter>\n                  <Button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    onClick={() => {\n                      onEventEdit?.(selectedEvent)\n                      setEventOpen(false)\n                    }}\n                  >\n                    Edit\n                  </Button>\n                  <Button\n                    size=\"sm\"\n                    variant=\"destructive\"\n                    onClick={() => {\n                      onEventDelete?.(selectedEvent)\n                      setEventOpen(false)\n                    }}\n                  >\n                    Cancel event\n                  </Button>\n                </DialogFooter>\n              </>\n            )}\n          </DialogContent>\n        )}\n      </Dialog>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/event-calendar/EventCalendar.tsx"
    },
    {
      "path": "packages/registry-react/blocks/event-calendar/defaults.ts",
      "content": "import { Video, CheckCircle2, AlertCircle, Plane } from 'lucide-react'\nimport type { EventTypeMeta } from './types'\n\n/**\n * Default event-type theme. Mirrors the boilerplate dashboard but routes\n * every color through OKLCH tokens (chart-1..4, success, warning) so it\n * follows light/dark mode and any consumer theme override.\n *\n * Override per-type or replace the whole map via `<EventCalendar eventTypes={...} />`.\n */\nexport const defaultEventTypes: Record<string, EventTypeMeta> = {\n  meeting: {\n    label: 'Meeting',\n    icon: Video,\n    chip: 'bg-chart-1/10 text-chart-1 ring-chart-1/20',\n    bar: 'bg-chart-1',\n    dot: 'bg-chart-1',\n    glow: 'bg-chart-1/10',\n    text: 'text-chart-1',\n    ring: 'hover:ring-chart-1/30',\n  },\n  task: {\n    label: 'Task',\n    icon: CheckCircle2,\n    chip: 'bg-success/10 text-success ring-success/20',\n    bar: 'bg-success',\n    dot: 'bg-success',\n    glow: 'bg-success/10',\n    text: 'text-success',\n    ring: 'hover:ring-success/30',\n  },\n  reminder: {\n    label: 'Reminder',\n    icon: AlertCircle,\n    chip: 'bg-warning/10 text-warning ring-warning/20',\n    bar: 'bg-warning',\n    dot: 'bg-warning',\n    glow: 'bg-warning/10',\n    text: 'text-warning',\n    ring: 'hover:ring-warning/30',\n  },\n  travel: {\n    label: 'Travel',\n    icon: Plane,\n    chip: 'bg-chart-3/10 text-chart-3 ring-chart-3/20',\n    bar: 'bg-chart-3',\n    dot: 'bg-chart-3',\n    glow: 'bg-chart-3/10',\n    text: 'text-chart-3',\n    ring: 'hover:ring-chart-3/30',\n  },\n}",
      "type": "registry:block",
      "target": "~/components/blocks/event-calendar/defaults.ts"
    },
    {
      "path": "packages/registry-react/blocks/event-calendar/types.ts",
      "content": "import type { LucideIcon } from 'lucide-react'\n\n/**\n * Single event on the calendar. `type` is a free-form string (e.g. `'meeting'`,\n * `'task'`, anything) -- the matching key must exist in the `eventTypes` map\n * passed to <EventCalendar>. Unknown types fall through to a neutral style.\n */\nexport interface CalendarEvent {\n  id: string\n  title: string\n  /** YYYY-MM-DD */\n  date: string\n  /** HH:mm */\n  start: string\n  /** HH:mm */\n  end: string\n  type: string\n  description?: string\n  location?: string\n  attendees?: string[]\n  /** Free-form status tag rendered next to the type label in the dialog. */\n  status?: string\n}\n\n/**\n * Visual + semantic config per event type. The default theme uses OKLCH\n * tokens (chart-1..4, success, warning) so consumers get sensible colors\n * out of the box, but you can override per-type to wire any palette.\n */\nexport interface EventTypeMeta {\n  label: string\n  /** Lucide icon component for the stat tile + chip + dialog header. */\n  icon: LucideIcon\n  /** Tailwind classes for the colored chip on event rows + dialog header.\n   *  Shape: `bg-X/10 text-X ring-X/20` is what the default theme uses. */\n  chip: string\n  /** Tailwind class for the vertical bar to the left of side-rail rows. */\n  bar: string\n  /** Tailwind class for the legend / stat-tile dot. */\n  dot: string\n  /** Tailwind class for the soft watermark glow behind stat tiles. */\n  glow: string\n  /** Tailwind class for the small text accent on \"Next: ...\" date labels. */\n  text: string\n  /** Tailwind class for the stat-tile hover ring. */\n  ring: string\n}",
      "type": "registry:block",
      "target": "~/components/blocks/event-calendar/types.ts"
    },
    {
      "path": "packages/registry-react/blocks/event-calendar/index.ts",
      "content": "export { EventCalendar, type EventCalendarProps } from './EventCalendar'\nexport type { CalendarEvent, EventTypeMeta } from './types'\nexport { defaultEventTypes } from './defaults'",
      "type": "registry:block",
      "target": "~/components/blocks/event-calendar/index.ts"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/use-month-grid.json",
    "https://uipkge.dev/r/react/button.json",
    "https://uipkge.dev/r/react/dialog.json",
    "https://uipkge.dev/r/react/select.json",
    "https://uipkge.dev/r/react/avatar.json",
    "https://uipkge.dev/r/react/separator.json",
    "https://uipkge.dev/r/react/skeleton.json",
    "https://uipkge.dev/r/react/input.json",
    "https://uipkge.dev/r/react/context-menu.json"
  ],
  "description": "Month-view event calendar with stats strip, drag/shift range select, side rail (selected day or range summary), upcoming list, and per-cell/per-event context menus. events / onEventsChange binds the source of truth; eventTypes config maps each type key to its label/icon/color theme so swapping the visual palette is one prop. Built on the use-month-grid headless hook.",
  "categories": [
    "dashboard"
  ]
}