{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calendar",
  "title": "Calendar",
  "description": "Month grid with event chips, overflow popovers, and a month switcher.",
  "dependencies": [
    "@internationalized/date@^3.12.3",
    "lucide-react@^1.33.0"
  ],
  "registryDependencies": [
    "https://components.exe.xyz/r/avatar.json",
    "https://components.exe.xyz/r/button.json",
    "https://components.exe.xyz/r/icon-button.json",
    "https://components.exe.xyz/r/popover.json",
    "https://components.exe.xyz/r/utils.json"
  ],
  "files": [
    {
      "path": "src/components/blocks/calendar.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  CalendarDate,\n  getLocalTimeZone,\n  getWeeksInMonth,\n  isSameDay,\n  isSameMonth,\n  startOfMonth,\n  startOfWeek,\n  today,\n} from \"@internationalized/date\"\nimport { ChevronLeft, ChevronRight, Plus } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Avatar, AvatarFallback } from \"@/components/ui/avatar\"\nimport { Button } from \"@/components/ui/button\"\nimport { IconButton } from \"@/components/ui/icon-button\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverDescription,\n  PopoverHeader,\n  PopoverTitle,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\n\nexport type { CalendarDate }\n\nexport type CalendarEvent = {\n  id: string\n  title: string\n  start: Date\n  end?: Date\n  allDay?: boolean\n  color?: string\n  location?: string\n}\n\nconst MAX_VISIBLE = 2\nconst WEEKDAYS = 7\nconst tz = () => getLocalTimeZone()\n\nfunction dateToCalendar(date: Date) {\n  return new CalendarDate(\n    date.getFullYear(),\n    date.getMonth() + 1,\n    date.getDate()\n  )\n}\n\nfunction formatTime(date: Date) {\n  return date.toLocaleTimeString(\"en-GB\", {\n    hour: \"2-digit\",\n    minute: \"2-digit\",\n    hour12: false,\n  })\n}\n\nfunction occursOn(event: CalendarEvent, day: CalendarDate) {\n  const start = dateToCalendar(event.start)\n  const end = event.end ? dateToCalendar(event.end) : start\n  return start.compare(day) <= 0 && end.compare(day) >= 0\n}\n\nfunction monthGrid(month: CalendarDate, locale: string) {\n  const start = startOfWeek(startOfMonth(month), locale)\n  const weeks = getWeeksInMonth(month, locale)\n  const days: CalendarDate[][] = []\n  let cursor = start\n  for (let week = 0; week < weeks; week += 1) {\n    const row: CalendarDate[] = []\n    for (let day = 0; day < WEEKDAYS; day += 1) {\n      row.push(cursor)\n      cursor = cursor.add({ days: 1 })\n    }\n    days.push(row)\n  }\n  return days\n}\n\nexport const calendarDemoEvents: CalendarEvent[] = [\n  {\n    id: \"coffee\",\n    title: \"Coffee\",\n    start: new Date(2026, 7, 3, 9, 30),\n    color: \"var(--primary)\",\n    location: \"North kiln\",\n  },\n  {\n    id: \"payday\",\n    title: \"Payday\",\n    start: new Date(2026, 7, 7),\n    allDay: true,\n    color: \"var(--chart-2)\",\n  },\n  {\n    id: \"brunch\",\n    title: \"Brunch\",\n    start: new Date(2026, 7, 8, 11, 0),\n    color: \"var(--chart-4)\",\n    location: \"Quay hall\",\n  },\n  {\n    id: \"standup-10\",\n    title: \"Stand-up\",\n    start: new Date(2026, 7, 10, 11, 30),\n    color: \"var(--primary)\",\n  },\n  {\n    id: \"sync-10\",\n    title: \"1:1 sync\",\n    start: new Date(2026, 7, 10, 16, 30),\n    color: \"var(--chart-2)\",\n  },\n  {\n    id: \"gym\",\n    title: \"Gym\",\n    start: new Date(2026, 7, 11, 7, 0),\n    color: \"var(--chart-5)\",\n  },\n  {\n    id: \"game\",\n    title: \"Game night\",\n    start: new Date(2026, 7, 14, 19, 0),\n    color: \"var(--chart-4)\",\n    location: \"Studio loft\",\n  },\n  {\n    id: \"holidays\",\n    title: \"Holidays\",\n    start: new Date(2026, 7, 15),\n    allDay: true,\n    color: \"var(--chart-3)\",\n  },\n  {\n    id: \"birthday\",\n    title: \"Birthday night at Bacalar’s\",\n    start: new Date(2026, 7, 15, 20, 30),\n    color: \"var(--primary)\",\n    location: \"Bacalar’s\",\n  },\n  {\n    id: \"retro\",\n    title: \"Retro\",\n    start: new Date(2026, 7, 18, 15, 0),\n    color: \"var(--chart-2)\",\n  },\n  {\n    id: \"planning\",\n    title: \"Planning\",\n    start: new Date(2026, 7, 19, 10, 0),\n    color: \"var(--primary)\",\n  },\n  {\n    id: \"haircut\",\n    title: \"Haircut\",\n    start: new Date(2026, 7, 20, 13, 30),\n    color: \"var(--chart-4)\",\n  },\n  {\n    id: \"standup-21\",\n    title: \"Stand-up\",\n    start: new Date(2026, 7, 21, 11, 30),\n    color: \"var(--primary)\",\n  },\n  {\n    id: \"lunch-21\",\n    title: \"Team lunch\",\n    start: new Date(2026, 7, 21, 12, 30),\n    color: \"var(--chart-2)\",\n    location: \"Plotter canteen\",\n  },\n  {\n    id: \"portfolio-21\",\n    title: \"Portfolio review\",\n    start: new Date(2026, 7, 21, 14, 0),\n    color: \"var(--chart-4)\",\n  },\n  {\n    id: \"sync-21\",\n    title: \"1:1\",\n    start: new Date(2026, 7, 21, 16, 0),\n    color: \"var(--chart-5)\",\n  },\n  {\n    id: \"dinner\",\n    title: \"Dinner with friends\",\n    start: new Date(2026, 7, 22, 19, 30),\n    color: \"var(--chart-4)\",\n    location: \"East market\",\n  },\n  {\n    id: \"yoga\",\n    title: \"Yoga\",\n    start: new Date(2026, 7, 24, 7, 30),\n    color: \"var(--chart-2)\",\n  },\n  {\n    id: \"kickoff\",\n    title: \"Kickoff\",\n    start: new Date(2026, 7, 26, 9, 0),\n    color: \"var(--primary)\",\n  },\n  {\n    id: \"dentist\",\n    title: \"Dentist\",\n    start: new Date(2026, 7, 28, 8, 45),\n    color: \"var(--chart-5)\",\n    location: \"Harbour clinic\",\n  },\n]\n\nexport function Calendar({\n  month,\n  onMonthChange,\n  events,\n  onSelectEvent,\n  onCreate,\n  locale = \"en-US\",\n  className,\n}: {\n  month: CalendarDate\n  onMonthChange?: (month: CalendarDate) => void\n  events: CalendarEvent[]\n  onSelectEvent?: (id: string) => void\n  onCreate?: (date: CalendarDate) => void\n  locale?: string\n  className?: string\n}) {\n  const timeZone = tz()\n  const todayDate = today(timeZone)\n  const monthStart = startOfMonth(month)\n  const [focused, setFocused] = React.useState(monthStart)\n  const [selected, setSelected] = React.useState<CalendarDate | null>(null)\n  const gridRef = React.useRef<HTMLDivElement>(null)\n\n  React.useEffect(() => {\n    if (!isSameMonth(focused, monthStart)) {\n      setFocused(isSameMonth(todayDate, monthStart) ? todayDate : monthStart)\n    }\n  }, [monthStart, focused, todayDate])\n\n  const grid = monthGrid(monthStart, locale)\n  const weekdayStart = startOfWeek(monthStart, locale)\n  const weekdays = Array.from({ length: WEEKDAYS }, (_, index) =>\n    weekdayStart\n      .add({ days: index })\n      .toDate(timeZone)\n      .toLocaleDateString(locale, {\n        weekday: \"short\",\n      })\n  )\n  const monthLabel = monthStart.toDate(timeZone).toLocaleDateString(locale, {\n    month: \"long\",\n    year: \"numeric\",\n  })\n\n  function moveMonth(delta: number) {\n    onMonthChange?.(monthStart.add({ months: delta }))\n  }\n\n  function moveFocus(next: CalendarDate) {\n    setFocused(next)\n    if (!isSameMonth(next, monthStart)) {\n      onMonthChange?.(startOfMonth(next))\n    }\n    requestAnimationFrame(() => {\n      const root = gridRef.current\n      if (!root || !root.contains(document.activeElement)) return\n      root.querySelector<HTMLElement>(\"[data-focused='true']\")?.focus()\n    })\n  }\n\n  function onGridKeyDown(event: React.KeyboardEvent) {\n    let next: CalendarDate | null = null\n    switch (event.key) {\n      case \"ArrowLeft\":\n        next = focused.add({ days: -1 })\n        break\n      case \"ArrowRight\":\n        next = focused.add({ days: 1 })\n        break\n      case \"ArrowUp\":\n        next = focused.add({ days: -7 })\n        break\n      case \"ArrowDown\":\n        next = focused.add({ days: 7 })\n        break\n      case \"Home\":\n        next = startOfWeek(focused, locale)\n        break\n      case \"End\":\n        next = startOfWeek(focused, locale).add({ days: 6 })\n        break\n      case \"PageUp\":\n        next = focused.add({ months: -1 })\n        break\n      case \"PageDown\":\n        next = focused.add({ months: 1 })\n        break\n      default:\n        return\n    }\n    event.preventDefault()\n    moveFocus(next)\n  }\n\n  return (\n    <section\n      data-slot=\"calendar\"\n      className={cn(\n        \"w-full min-w-0 overflow-x-auto rounded-[2px] border border-border bg-card\",\n        className\n      )}\n    >\n      <header className=\"flex items-center justify-between gap-2 border-b border-border px-3 py-2\">\n        <div className=\"flex items-center gap-1\">\n          <IconButton\n            icon={ChevronLeft}\n            size=\"small\"\n            variant=\"ghost\"\n            aria-label=\"Previous month\"\n            onClick={() => moveMonth(-1)}\n          />\n          <h2 className=\"min-w-40 text-center font-heading text-sm font-medium\">\n            {monthLabel}\n          </h2>\n          <IconButton\n            icon={ChevronRight}\n            size=\"small\"\n            variant=\"ghost\"\n            aria-label=\"Next month\"\n            onClick={() => moveMonth(1)}\n          />\n        </div>\n        <Button\n          type=\"button\"\n          size=\"sm\"\n          onClick={() => onCreate?.(selected ?? focused)}\n        >\n          <Plus className=\"size-3.5\" />\n          New event\n        </Button>\n      </header>\n      <div\n        ref={gridRef}\n        role=\"grid\"\n        aria-label={monthLabel}\n        onKeyDown={onGridKeyDown}\n        className=\"p-2\"\n      >\n        <div role=\"row\" className=\"grid grid-cols-7 gap-px\">\n          {weekdays.map((day) => (\n            <div\n              key={day}\n              role=\"columnheader\"\n              className=\"px-1 py-1 text-center font-mono text-[11px] text-muted-foreground\"\n            >\n              {day}\n            </div>\n          ))}\n        </div>\n        <div className=\"flex flex-col gap-px\">\n          {grid.map((week, weekIndex) => (\n            <div key={weekIndex} role=\"row\" className=\"grid grid-cols-7 gap-px\">\n              {week.map((day) => {\n                const inMonth = isSameMonth(day, monthStart)\n                const isToday = isSameDay(day, todayDate)\n                const isFocused = isSameDay(day, focused)\n                const isSelected = selected ? isSameDay(day, selected) : false\n                const dayEvents = events.filter((event) => occursOn(event, day))\n                const titles = dayEvents.map((event) => event.title).join(\", \")\n                const visible = dayEvents.slice(0, MAX_VISIBLE)\n                const overflow = dayEvents.slice(MAX_VISIBLE)\n                const label = [\n                  day.toDate(timeZone).toLocaleDateString(locale, {\n                    weekday: \"long\",\n                    month: \"long\",\n                    day: \"numeric\",\n                  }),\n                  titles,\n                ]\n                  .filter(Boolean)\n                  .join(\". \")\n                return (\n                  <div\n                    key={day.toString()}\n                    role=\"gridcell\"\n                    aria-selected={isSelected || undefined}\n                    className={cn(\n                      \"min-h-[6.25rem] rounded-[2px] border border-transparent p-1\",\n                      !inMonth && \"opacity-40\",\n                      isToday && \"border-primary\",\n                      isSelected && \"bg-muted\"\n                    )}\n                  >\n                    <button\n                      type=\"button\"\n                      data-focused={isFocused ? \"true\" : undefined}\n                      tabIndex={isFocused ? 0 : -1}\n                      aria-label={label}\n                      aria-current={isToday ? \"date\" : undefined}\n                      className={cn(\n                        \"mb-1 flex size-6 items-center justify-center rounded-[2px] font-mono text-[11px] outline-none focus-visible:ring-3 focus-visible:ring-ring/50\",\n                        isSelected && \"bg-primary text-primary-foreground\",\n                        !isSelected && isToday && \"text-primary\"\n                      )}\n                      onClick={() => {\n                        setSelected(day)\n                        setFocused(day)\n                      }}\n                    >\n                      {day.day}\n                    </button>\n                    <div className=\"flex flex-col gap-0.5\">\n                      {visible.map((event) => (\n                        <EventChip\n                          key={event.id}\n                          event={event}\n                          onSelect={() => onSelectEvent?.(event.id)}\n                        />\n                      ))}\n                      {overflow.length > 0 ? (\n                        <OverflowChip\n                          events={overflow}\n                          onSelect={(id) => onSelectEvent?.(id)}\n                        />\n                      ) : null}\n                    </div>\n                  </div>\n                )\n              })}\n            </div>\n          ))}\n        </div>\n      </div>\n    </section>\n  )\n}\n\nfunction EventChip({\n  event,\n  onSelect,\n}: {\n  event: CalendarEvent\n  onSelect: () => void\n}) {\n  const color = event.color ?? \"var(--primary)\"\n  const time = event.allDay ? null : formatTime(event.start)\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <button\n          type=\"button\"\n          onClick={onSelect}\n          className=\"flex w-full min-w-0 items-center gap-1 rounded-[2px] border border-border bg-background px-1 py-0.5 text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50\"\n        >\n          <span\n            className=\"size-1.5 shrink-0 rounded-full\"\n            style={{ background: color }}\n            aria-hidden\n          />\n          {time ? (\n            <span className=\"font-mono text-[10px] text-muted-foreground\">\n              {time}\n            </span>\n          ) : null}\n          <span className=\"min-w-0 truncate text-[11px]\">{event.title}</span>\n        </button>\n      </PopoverTrigger>\n      <EventDetails event={event} time={time} />\n    </Popover>\n  )\n}\n\nfunction OverflowChip({\n  events,\n  onSelect,\n}: {\n  events: CalendarEvent[]\n  onSelect: (id: string) => void\n}) {\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <button\n          type=\"button\"\n          className=\"w-full rounded-[2px] px-1 py-0.5 text-left font-mono text-[10px] text-muted-foreground outline-none hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50\"\n        >\n          +{events.length} more\n        </button>\n      </PopoverTrigger>\n      <PopoverContent align=\"start\" className=\"w-56 gap-1 p-2\">\n        {events.map((event) => (\n          <EventChip\n            key={event.id}\n            event={event}\n            onSelect={() => onSelect(event.id)}\n          />\n        ))}\n      </PopoverContent>\n    </Popover>\n  )\n}\n\nfunction EventDetails({\n  event,\n  time,\n}: {\n  event: CalendarEvent\n  time: string | null\n}) {\n  const range = event.allDay\n    ? \"All day\"\n    : [time, event.end ? formatTime(event.end) : null]\n        .filter(Boolean)\n        .join(\" – \")\n  return (\n    <PopoverContent align=\"start\" className=\"w-64\">\n      <PopoverHeader>\n        <PopoverTitle>{event.title}</PopoverTitle>\n        <PopoverDescription className=\"font-mono text-[11px]\">\n          {range}\n          {event.location ? ` · ${event.location}` : \"\"}\n        </PopoverDescription>\n      </PopoverHeader>\n      <div className=\"flex items-center gap-2\">\n        <Avatar size=\"sm\">\n          <AvatarFallback>EV</AvatarFallback>\n        </Avatar>\n        <span className=\"text-xs text-muted-foreground\">Host</span>\n      </div>\n      <div className=\"flex gap-1.5\">\n        <Button size=\"sm\">Join</Button>\n        <Button size=\"sm\" variant=\"outline\">\n          Edit\n        </Button>\n      </div>\n    </PopoverContent>\n  )\n}\n",
      "type": "registry:block"
    }
  ],
  "type": "registry:block"
}