{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "event-calendar",
  "type": "registry:ui",
  "title": "Event calendar",
  "description": "A week and day event scheduler with overlap layout, an all-day row, a current time line and click to create.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://ui.minidev.pro/r/button.json",
    "https://ui.minidev.pro/r/segmented-control.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/event-calendar.tsx",
      "type": "registry:ui",
      "target": "components/ui/event-calendar.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { SegmentedControl } from \"@/components/ui/segmented-control\"\n\n/** A week and day event scheduler with overlap layout, an all-day row, a current time line and click to create. */\ntype CalendarTone = \"accent\" | \"info\" | \"success\" | \"warning\" | \"neutral\"\n\ntype CalendarSource = { id: string; label: string; tone: CalendarTone }\n\ntype CalendarEvent = {\n  id: string\n  title: string\n  start: Date\n  end: Date\n  /** Id of a calendar in `calendars`. Sets the colour. */\n  calendar?: string\n  allDay?: boolean\n  location?: string\n}\n\ntype CalendarView = \"week\" | \"day\"\n\ntype EventCalendarProps = {\n  events?: CalendarEvent[]\n  calendars?: CalendarSource[]\n  /** Controlled view. */\n  view?: CalendarView\n  /** \"auto\" picks Day under 600px of width and Week above. */\n  defaultView?: CalendarView | \"auto\"\n  onViewChange?: (view: CalendarView) => void\n  /** Any date inside the first week or day shown. Defaults to today. */\n  defaultDate?: Date\n  onDateChange?: (date: Date) => void\n  /** 0 for Sunday, 1 for Monday. */\n  weekStartsOn?: 0 | 1\n  /** Hour the grid scrolls to on mount. */\n  scrollToHour?: number\n  /** Pixel height of one hour row. */\n  hourHeight?: number\n  /** Pixel height of the scrolling time grid. */\n  height?: number\n  /** Minutes a click on an empty slot snaps to. */\n  slotMinutes?: number\n  /** Called when an empty slot is clicked or chosen with the keyboard. */\n  onCreate?: (range: { start: Date; end: Date }) => void\n  onEventClick?: (event: CalendarEvent) => void\n  locale?: string\n  className?: string\n}\n\nconst TONE: Record<CalendarTone, { block: string; bar: string; dot: string }> = {\n  accent: { block: \"bg-accent-soft hover:bg-accent/15\", bar: \"bg-accent\", dot: \"bg-accent\" },\n  info: { block: \"bg-info/10 hover:bg-info/15\", bar: \"bg-info\", dot: \"bg-info\" },\n  success: { block: \"bg-success/10 hover:bg-success/15\", bar: \"bg-success\", dot: \"bg-success\" },\n  warning: { block: \"bg-warning/12 hover:bg-warning/18\", bar: \"bg-warning\", dot: \"bg-warning\" },\n  neutral: { block: \"bg-fg/[0.06] hover:bg-fg/10\", bar: \"bg-fg-subtle\", dot: \"bg-fg-subtle\" },\n}\n\nconst CALENDARS: CalendarSource[] = [\n  { id: \"northwind\", label: \"Northwind\", tone: \"accent\" },\n  { id: \"eng\", label: \"Engineering\", tone: \"info\" },\n  { id: \"customers\", label: \"Customers\", tone: \"success\" },\n  { id: \"releases\", label: \"Releases\", tone: \"warning\" },\n  { id: \"personal\", label: \"Personal\", tone: \"neutral\" },\n]\n\nconst DAY_MS = 86_400_000\nconst startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate())\nconst addDays = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n, d.getHours(), d.getMinutes())\nconst sameDay = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()\nconst startOfWeek = (d: Date, ws: number) => addDays(startOfDay(d), -((d.getDay() - ws + 7) % 7))\nconst at = (day: Date, h: number, m = 0) => new Date(day.getFullYear(), day.getMonth(), day.getDate(), h, m)\n\nfunction sampleEvents(): CalendarEvent[] {\n  const mon = startOfWeek(new Date(), 1)\n  const d = (n: number) => addDays(mon, n)\n  const ev: CalendarEvent[] = []\n  for (let i = 0; i < 5; i++) ev.push({ id: `standup-${i}`, title: \"Daily standup\", start: at(d(i), 9, 30), end: at(d(i), 9, 45), calendar: \"eng\", location: \"Zoom\" })\n  ev.push(\n    { id: \"planning\", title: \"Sprint planning\", start: at(d(0), 11), end: at(d(0), 12), calendar: \"eng\", location: \"Room Atlas\" },\n    { id: \"acme\", title: \"Customer call: Acme Corp\", start: at(d(0), 14), end: at(d(0), 14, 45), calendar: \"customers\", location: \"Google Meet\" },\n    { id: \"focus\", title: \"Focus: Lumen invoices v2\", start: at(d(0), 15, 30), end: at(d(0), 17, 30), calendar: \"personal\" },\n    { id: \"design\", title: \"Design review\", start: at(d(1), 10), end: at(d(1), 11, 30), calendar: \"northwind\", location: \"Room Atlas\" },\n    { id: \"globex\", title: \"Globex call\", start: at(d(1), 10, 30), end: at(d(1), 11, 15), calendar: \"customers\", location: \"Zoom\" },\n    { id: \"lunch\", title: \"Lunch with Priya\", start: at(d(1), 12, 30), end: at(d(1), 13, 30), calendar: \"personal\", location: \"Pier 9 Cafe\" },\n    { id: \"hiring\", title: \"Hiring loop: frontend\", start: at(d(1), 15), end: at(d(1), 16), calendar: \"northwind\" },\n    { id: \"allhands\", title: \"Northwind all hands\", start: at(d(2), 12), end: at(d(2), 13), calendar: \"northwind\", location: \"Main hall\" },\n    { id: \"initech\", title: \"Initech onboarding\", start: at(d(2), 14), end: at(d(2), 15), calendar: \"customers\", location: \"Zoom\" },\n    { id: \"maya\", title: \"1:1 Maya\", start: at(d(2), 14, 30), end: at(d(2), 15, 15), calendar: \"northwind\" },\n    { id: \"deploy-wed\", title: \"Deploy window: Lumen 2.4\", start: at(d(2), 16), end: at(d(2), 18), calendar: \"releases\", location: \"#deploys\" },\n    { id: \"design-2\", title: \"Design review: billing flow\", start: at(d(3), 10), end: at(d(3), 11), calendar: \"northwind\", location: \"Room Atlas\" },\n    { id: \"umbrella\", title: \"Customer call: Umbrella\", start: at(d(3), 11), end: at(d(3), 11, 30), calendar: \"customers\", location: \"Google Meet\" },\n    { id: \"postmortem\", title: \"Postmortem: API latency\", start: at(d(3), 15), end: at(d(3), 16), calendar: \"eng\", location: \"Zoom\" },\n    { id: \"demo\", title: \"Demo day\", start: at(d(4), 11), end: at(d(4), 12), calendar: \"northwind\", location: \"Main hall\" },\n    { id: \"deploy-fri\", title: \"Deploy window: hotfixes\", start: at(d(4), 14), end: at(d(4), 15, 30), calendar: \"releases\", location: \"#deploys\" },\n    { id: \"social\", title: \"Team social\", start: at(d(4), 17), end: at(d(4), 19), calendar: \"personal\", location: \"Rooftop\" },\n    { id: \"run\", title: \"Run club\", start: at(d(5), 8), end: at(d(5), 9, 30), calendar: \"personal\" },\n    { id: \"release\", title: \"Lumen 2.4 release\", start: d(2), end: d(3), calendar: \"releases\", allDay: true },\n    { id: \"q4\", title: \"Q4 planning offsite\", start: d(3), end: d(5), calendar: \"northwind\", allDay: true }\n  )\n  return ev\n}\n\ntype Placed = { ev: CalendarEvent; top: number; bottom: number; col: number; span: number; cols: number }\n\n/** Side by side columns for overlapping events, Google Calendar style. */\nfunction layoutDay(items: { ev: CalendarEvent; top: number; bottom: number }[]): Placed[] {\n  const sorted = [...items].sort((a, b) => a.top - b.top || b.bottom - a.bottom)\n  const out: Placed[] = []\n  let cluster: Placed[] = []\n  let clusterEnd = -1\n  let colEnds: number[] = []\n  const flush = () => {\n    const cols = colEnds.length\n    for (const p of cluster) {\n      p.cols = cols\n      // Grow into free columns to the right.\n      let span = 1\n      for (let c = p.col + 1; c < cols; c++) {\n        if (cluster.some((o) => o.col === c && o.top < p.bottom && o.bottom > p.top)) break\n        span++\n      }\n      p.span = span\n      out.push(p)\n    }\n    cluster = []\n    colEnds = []\n  }\n  for (const it of sorted) {\n    if (cluster.length && it.top >= clusterEnd) flush()\n    let col = colEnds.findIndex((end) => end <= it.top)\n    if (col < 0) {\n      col = colEnds.length\n      colEnds.push(it.bottom)\n    } else colEnds[col] = it.bottom\n    cluster.push({ ...it, col, span: 1, cols: 1 })\n    clusterEnd = cluster.length === 1 ? it.bottom : Math.max(clusterEnd, it.bottom)\n  }\n  if (cluster.length) flush()\n  return out\n}\n\n/**\n * A week and day scheduler in the style of Google Calendar. Events sit on an\n * hour grid by start and end, overlapping events share the column side by\n * side, a live line marks the current time, and all-day events span a row\n * above. Click an empty slot to create, or focus a day and pick a time with\n * the arrow keys.\n */\nfunction EventCalendar({\n  events: eventsProp,\n  calendars = CALENDARS,\n  view: viewProp,\n  defaultView = \"auto\",\n  onViewChange,\n  defaultDate,\n  onDateChange,\n  weekStartsOn = 1,\n  scrollToHour = 8,\n  hourHeight = 48,\n  height = 520,\n  slotMinutes = 30,\n  onCreate,\n  onEventClick,\n  locale = \"en-US\",\n  className,\n}: EventCalendarProps) {\n  const events = React.useMemo(() => eventsProp ?? sampleEvents(), [eventsProp])\n  const [now, setNow] = React.useState(() => new Date())\n  const [cursor, setCursor] = React.useState(() => startOfDay(defaultDate ?? new Date()))\n  const [chosen, setChosen] = React.useState<CalendarView | null>(defaultView === \"auto\" ? null : defaultView)\n  const [width, setWidth] = React.useState(0)\n  const narrow = width > 0 && width < 600\n  const view: CalendarView = viewProp ?? chosen ?? (narrow ? \"day\" : \"week\")\n  const [draft, setDraft] = React.useState<{ start: Date; end: Date } | null>(null)\n  const [hover, setHover] = React.useState<{ day: number; min: number } | null>(null)\n  const [keySlot, setKeySlot] = React.useState<{ day: number; min: number } | null>(null)\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const scrollRef = React.useRef<HTMLDivElement>(null)\n\n  React.useEffect(() => {\n    const t = setInterval(() => setNow(new Date()), 60_000)\n    return () => clearInterval(t)\n  }, [])\n\n  React.useEffect(() => {\n    const el = rootRef.current\n    if (!el || typeof ResizeObserver === \"undefined\") return\n    const ro = new ResizeObserver(([e]) => setWidth(e.contentRect.width))\n    ro.observe(el)\n    return () => ro.disconnect()\n  }, [])\n\n  React.useLayoutEffect(() => {\n    if (scrollRef.current) scrollRef.current.scrollTop = Math.max(0, scrollToHour * hourHeight - 8)\n    // Only on mount.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  const days = React.useMemo(() => {\n    const first = view === \"week\" ? startOfWeek(cursor, weekStartsOn) : cursor\n    return Array.from({ length: view === \"week\" ? 7 : 1 }, (_, i) => addDays(first, i))\n  }, [cursor, view, weekStartsOn])\n\n  const cal = React.useMemo(() => new Map(calendars.map((c) => [c.id, c])), [calendars])\n  const toneOf = (ev: CalendarEvent) => TONE[cal.get(ev.calendar ?? \"\")?.tone ?? \"accent\"]\n\n  const fmt = React.useMemo(\n    () => ({\n      time: new Intl.DateTimeFormat(locale, { hour: \"numeric\", minute: \"2-digit\" }),\n      hour: new Intl.DateTimeFormat(locale, { hour: \"numeric\" }),\n      weekday: new Intl.DateTimeFormat(locale, { weekday: \"short\" }),\n      long: new Intl.DateTimeFormat(locale, { weekday: \"long\", month: \"long\", day: \"numeric\" }),\n      month: new Intl.DateTimeFormat(locale, { month: \"long\", year: \"numeric\" }),\n      monthShort: new Intl.DateTimeFormat(locale, { month: \"short\" }),\n    }),\n    [locale]\n  )\n\n  const titleText = (() => {\n    const a = days[0]\n    const b = days[days.length - 1]\n    if (a.getMonth() === b.getMonth()) return fmt.month.format(a)\n    if (a.getFullYear() === b.getFullYear()) return `${fmt.monthShort.format(a)} – ${fmt.monthShort.format(b)} ${b.getFullYear()}`\n    return `${fmt.monthShort.format(a)} ${a.getFullYear()} – ${fmt.monthShort.format(b)} ${b.getFullYear()}`\n  })()\n\n  const setView = (v: CalendarView) => {\n    if (viewProp === undefined) setChosen(v)\n    onViewChange?.(v)\n  }\n  const go = (d: Date) => {\n    setCursor(startOfDay(d))\n    onDateChange?.(d)\n  }\n  const step = view === \"week\" ? 7 : 1\n\n  const timeRange = (s: Date, e: Date) => `${fmt.time.format(s)} to ${fmt.time.format(e)}`\n\n  // All-day events, placed into the visible day span.\n  const allDay = events\n    .filter((e) => e.allDay)\n    .map((e) => {\n      const s = startOfDay(e.start)\n      const last = addDays(startOfDay(new Date(e.end.getTime() - 1)), 0)\n      const from = days.findIndex((d) => d >= s)\n      let to = -1\n      for (let i = days.length - 1; i >= 0; i--) if (days[i] <= last) { to = i; break }\n      return { ev: e, from, to }\n    })\n    .filter((x) => x.from >= 0 && x.to >= x.from)\n\n  const timed = days.map((day) => {\n    const dayStart = day.getTime()\n    const dayEnd = dayStart + DAY_MS\n    const items = events\n      .filter((e) => !e.allDay && e.start.getTime() < dayEnd && e.end.getTime() > dayStart)\n      .map((ev) => ({\n        ev,\n        top: Math.max(0, (ev.start.getTime() - dayStart) / 60_000),\n        bottom: Math.min(1440, (ev.end.getTime() - dayStart) / 60_000),\n      }))\n    return layoutDay(items)\n  })\n\n  const px = (min: number) => (min / 60) * hourHeight\n  const snap = (min: number) => Math.max(0, Math.min(1440 - slotMinutes, Math.floor(min / slotMinutes) * slotMinutes))\n  const minFromPointer = (e: React.PointerEvent<HTMLElement> | React.MouseEvent<HTMLElement>) => {\n    const r = e.currentTarget.getBoundingClientRect()\n    return snap(((e.clientY - r.top) / hourHeight) * 60)\n  }\n  const create = (dayIdx: number, min: number) => {\n    const start = new Date(days[dayIdx].getTime() + min * 60_000)\n    const end = new Date(start.getTime() + 60 * 60_000)\n    setDraft({ start, end })\n    onCreate?.({ start, end })\n  }\n\n  const nowMin = now.getHours() * 60 + now.getMinutes()\n  const cols = view === \"week\" ? \"grid-cols-7\" : \"grid-cols-1\"\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"event-calendar\"\n      className={cn(\"flex w-full max-w-4xl min-w-0 flex-col overflow-hidden rounded-xl border border-border bg-surface shadow-raised\", className)}\n    >\n      {/* Toolbar */}\n      <div className=\"flex flex-wrap items-center gap-2 border-b border-border px-3 py-2.5 sm:px-4\">\n        <h2 aria-live=\"polite\" className=\"mr-auto min-w-0 truncate text-[0.9375rem] font-medium tracking-[-0.01em] text-fg\">\n          {view === \"day\" ? fmt.long.format(days[0]) : titleText}\n        </h2>\n        <Button variant=\"outline\" size=\"sm\" onClick={() => go(new Date())}>\n          Today\n        </Button>\n        <div className=\"flex items-center\">\n          <Button variant=\"ghost\" size=\"icon-sm\" aria-label={view === \"week\" ? \"Previous week\" : \"Previous day\"} onClick={() => go(addDays(cursor, -step))}>\n            <ChevronLeftIcon />\n          </Button>\n          <Button variant=\"ghost\" size=\"icon-sm\" aria-label={view === \"week\" ? \"Next week\" : \"Next day\"} onClick={() => go(addDays(cursor, step))}>\n            <ChevronRightIcon />\n          </Button>\n        </div>\n        <SegmentedControl\n          size=\"sm\"\n          aria-label=\"Calendar view\"\n          value={view}\n          onChange={(v) => setView(v as CalendarView)}\n          options={[\n            { value: \"week\", label: \"Week\" },\n            { value: \"day\", label: \"Day\" },\n          ]}\n        />\n      </div>\n\n      <div className={cn(\"min-w-0\", view === \"week\" && \"overflow-x-auto\")}>\n        <div className={cn(view === \"week\" && \"min-w-[640px]\")}>\n          {/* Day headers */}\n          <div className=\"flex border-b border-border\">\n            <div className=\"w-14 shrink-0\" />\n            <div className={cn(\"grid flex-1\", cols)}>\n              {days.map((d) => {\n                const today = sameDay(d, now)\n                return (\n                  <button\n                    key={d.toISOString()}\n                    type=\"button\"\n                    disabled={view === \"day\"}\n                    onClick={() => {\n                      setCursor(d)\n                      setView(\"day\")\n                    }}\n                    aria-label={`${fmt.long.format(d)}${today ? \", today\" : \"\"}${view === \"week\" ? \", open day view\" : \"\"}`}\n                    className={cn(\n                      \"flex items-center justify-center gap-1.5 border-l border-border py-2 outline-none first:border-l-0\",\n                      \"transition-colors duration-[70ms] enabled:hover:bg-sunken focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-inset\",\n                      view === \"day\" && \"justify-start px-3\"\n                    )}\n                  >\n                    <span className={cn(\"text-[11px] font-medium tracking-[0.04em] uppercase\", today ? \"text-accent-fg\" : \"text-fg-subtle\")}>\n                      {fmt.weekday.format(d)}\n                    </span>\n                    <span\n                      className={cn(\n                        \"grid size-7 place-items-center rounded-full text-[0.8125rem] font-medium tabular-nums\",\n                        today ? \"bg-accent text-on-accent\" : \"text-fg\"\n                      )}\n                    >\n                      {d.getDate()}\n                    </span>\n                  </button>\n                )\n              })}\n            </div>\n          </div>\n\n          {/* All-day row */}\n          <div className=\"flex border-b border-border\">\n            <div className=\"flex w-14 shrink-0 items-start justify-end pt-1.5 pr-2 text-[10px] text-fg-subtle\">All day</div>\n            <div className={cn(\"relative grid min-h-8 flex-1 auto-rows-[22px] gap-y-0.5 py-1\", cols)} style={{ gridAutoFlow: \"row dense\" }}>\n              <div aria-hidden className={cn(\"pointer-events-none absolute inset-0 grid\", cols)}>\n                {days.map((d) => (\n                  <div key={d.toISOString()} className=\"border-l border-border first:border-l-0\" />\n                ))}\n              </div>\n              {allDay.map(({ ev, from, to }) => {\n                const t = toneOf(ev)\n                return (\n                  <button\n                    key={ev.id}\n                    type=\"button\"\n                    onClick={() => onEventClick?.(ev)}\n                    aria-label={`${ev.title}, all day, ${fmt.long.format(ev.start)}${to > from ? ` to ${fmt.long.format(days[to])}` : \"\"}${cal.get(ev.calendar ?? \"\") ? `, ${cal.get(ev.calendar ?? \"\")!.label}` : \"\"}`}\n                    className={cn(\n                      \"relative mx-1 flex min-w-0 items-center gap-1.5 overflow-hidden rounded-md px-2 text-left text-[11px] font-medium text-fg outline-none\",\n                      \"transition-colors duration-[70ms] focus-visible:ring-2 focus-visible:ring-accent\",\n                      t.block\n                    )}\n                    style={{ gridColumn: `${from + 1} / ${to + 2}` }}\n                  >\n                    <span aria-hidden className={cn(\"size-1.5 shrink-0 rounded-full\", t.dot)} />\n                    <span className=\"truncate\">{ev.title}</span>\n                  </button>\n                )\n              })}\n            </div>\n          </div>\n\n          {/* Time grid */}\n          <div ref={scrollRef} className=\"overflow-y-auto overscroll-contain\" style={{ height }}>\n            <div className=\"relative flex\" style={{ height: hourHeight * 24 }}>\n              <div aria-hidden className=\"relative w-14 shrink-0\">\n                {Array.from({ length: 23 }, (_, i) => i + 1).map((h) => (\n                  <span\n                    key={h}\n                    className=\"absolute right-2 -translate-y-1/2 text-[10px] text-fg-subtle tabular-nums\"\n                    style={{ top: h * hourHeight }}\n                  >\n                    {fmt.hour.format(at(days[0], h))}\n                  </span>\n                ))}\n              </div>\n              <div className={cn(\"relative grid flex-1\", cols)}>\n                {/* Hour lines */}\n                <div aria-hidden className=\"pointer-events-none absolute inset-0\">\n                  {Array.from({ length: 23 }, (_, i) => i + 1).map((h) => (\n                    <div key={h} className=\"absolute inset-x-0 border-t border-border\" style={{ top: h * hourHeight }} />\n                  ))}\n                  {Array.from({ length: 24 }, (_, h) => (\n                    <div key={h} className=\"absolute inset-x-0 border-t border-dashed border-border/50\" style={{ top: h * hourHeight + hourHeight / 2 }} />\n                  ))}\n                </div>\n                {days.map((day, di) => {\n                  const today = sameDay(day, now)\n                  const slot = keySlot?.day === di ? keySlot : hover?.day === di ? hover : null\n                  const dayDraft = draft && sameDay(draft.start, day) ? draft : null\n                  return (\n                    <div key={day.toISOString()} className={cn(\"relative border-l border-border first:border-l-0\", today && view === \"week\" && \"bg-accent-soft/50\")}>\n                      {/* Empty-slot target: one tab stop per day, arrows pick the time. */}\n                      <div\n                        role=\"button\"\n                        tabIndex={0}\n                        aria-label={`Create event on ${fmt.long.format(day)} at ${fmt.time.format(at(day, 0, keySlot?.day === di ? keySlot.min : scrollToHour * 60))}. Up and down arrows change the time.`}\n                        className=\"absolute inset-0 cursor-cell outline-none\"\n                        onPointerMove={(e) => {\n                          if (e.pointerType !== \"mouse\") return\n                          const min = minFromPointer(e)\n                          if (hover?.day !== di || hover.min !== min) setHover({ day: di, min })\n                        }}\n                        onPointerLeave={() => setHover(null)}\n                        onClick={(e) => create(di, minFromPointer(e))}\n                        onFocus={(e) => {\n                          if (!e.currentTarget.matches(\":focus-visible\")) return\n                          setKeySlot((k) => (k?.day === di ? k : { day: di, min: snap(scrollToHour * 60) }))\n                        }}\n                        onBlur={() => setKeySlot(null)}\n                        onKeyDown={(e) => {\n                          const k = keySlot ?? { day: di, min: snap(scrollToHour * 60) }\n                          let min = k.min\n                          if (e.key === \"ArrowDown\") min = snap(min + slotMinutes)\n                          else if (e.key === \"ArrowUp\") min = snap(min - slotMinutes)\n                          else if (e.key === \"Enter\" || e.key === \" \") {\n                            e.preventDefault()\n                            create(di, k.min)\n                            return\n                          } else return\n                          e.preventDefault()\n                          setKeySlot({ day: di, min })\n                          const sc = scrollRef.current\n                          if (sc) {\n                            const y = px(min)\n                            if (y < sc.scrollTop + 8) sc.scrollTop = y - 8\n                            else if (y + hourHeight > sc.scrollTop + sc.clientHeight) sc.scrollTop = y + hourHeight - sc.clientHeight + 8\n                          }\n                        }}\n                      />\n                      {slot && !dayDraft ? (\n                        <div\n                          aria-hidden\n                          className={cn(\n                            \"pointer-events-none absolute inset-x-1 rounded-md border border-dashed\",\n                            keySlot?.day === di ? \"border-solid border-accent bg-accent-soft ring-2 ring-accent-soft\" : \"border-fg/20\"\n                          )}\n                          style={{ top: px(slot.min) + 1, height: px(slotMinutes) - 2 }}\n                        >\n                          <span className={cn(\"block truncate px-1.5 pt-0.5 text-[10px] whitespace-nowrap tabular-nums\", keySlot?.day === di ? \"font-medium text-accent-fg\" : \"text-fg-muted\")}>\n                            {fmt.time.format(at(day, 0, slot.min))}\n                          </span>\n                        </div>\n                      ) : null}\n                      {dayDraft ? (\n                        <div\n                          aria-hidden\n                          className=\"pointer-events-none absolute inset-x-1 z-10 overflow-hidden rounded-md bg-accent px-2 py-1 text-on-accent shadow-md\"\n                          style={{\n                            top: px(dayDraft.start.getHours() * 60 + dayDraft.start.getMinutes()) + 1,\n                            height: px((dayDraft.end.getTime() - dayDraft.start.getTime()) / 60_000) - 2,\n                          }}\n                        >\n                          <span className=\"block truncate text-[11px] font-medium\">New event</span>\n                          <span className=\"block truncate text-[10px] whitespace-nowrap opacity-80 tabular-nums\">{fmt.time.format(dayDraft.start)}</span>\n                        </div>\n                      ) : null}\n                      {timed[di].map((p) => {\n                        const t = toneOf(p.ev)\n                        const h = px(p.bottom - p.top)\n                        const compact = h < 40\n                        // Rendered width of this block, for choosing how much text fits.\n                        const colPx = width ? ((Math.max(width, view === \"week\" ? 640 : 0) - 56) / days.length) * (p.span / p.cols) : 120\n                        const tight = colPx < 110\n                        const c = cal.get(p.ev.calendar ?? \"\")\n                        return (\n                          <button\n                            key={p.ev.id}\n                            type=\"button\"\n                            onClick={() => onEventClick?.(p.ev)}\n                            aria-label={`${p.ev.title}, ${fmt.long.format(p.ev.start)}, ${timeRange(p.ev.start, p.ev.end)}${p.ev.location ? `, ${p.ev.location}` : \"\"}${c ? `, ${c.label}` : \"\"}`}\n                            className={cn(\n                              \"absolute z-[1] flex flex-col overflow-hidden rounded-md py-0.5 pr-1 pl-2.5 text-left outline-none\",\n                              \"shadow-[0_0_0_1px_var(--surface)] transition-colors duration-[70ms] focus-visible:z-20 focus-visible:ring-2 focus-visible:ring-accent\",\n                              t.block\n                            )}\n                            style={{\n                              top: px(p.top) + 1,\n                              height: Math.max(18, h - 2),\n                              left: `calc(${(p.col / p.cols) * 100}% + 2px)`,\n                              width: `calc(${(p.span / p.cols) * 100}% - 4px)`,\n                            }}\n                          >\n                            <span aria-hidden className={cn(\"absolute top-1 bottom-1 left-1 w-0.5 rounded-full\", t.bar)} />\n                            {compact ? (\n                              <span className=\"truncate text-[11px] leading-4 text-fg\">\n                                <span className=\"font-medium\">{p.ev.title}</span>\n                                <span className=\"text-fg-muted tabular-nums\">, {fmt.time.format(p.ev.start)}</span>\n                              </span>\n                            ) : (\n                              <>\n                                <span className={cn(\"text-[11px] leading-4 font-medium break-words text-fg\", h >= 68 && colPx >= 64 ? \"line-clamp-2\" : \"truncate\")}>{p.ev.title}</span>\n                                <span className={cn(\"truncate text-[10px] leading-4 text-fg-muted tabular-nums\", colPx < 60 && \"hidden\")}>\n                                  {tight ? fmt.time.format(p.ev.start) : timeRange(p.ev.start, p.ev.end).replace(\" to \", \" – \")}\n                                </span>\n                                {h >= (colPx >= 64 ? 84 : 52) && colPx >= 60 && p.ev.location ? <span className=\"truncate text-[10px] leading-4 text-fg-subtle\">{p.ev.location}</span> : null}\n                              </>\n                            )}\n                          </button>\n                        )\n                      })}\n                      {today ? (\n                        <div aria-hidden className=\"pointer-events-none absolute inset-x-0 z-10\" style={{ top: px(nowMin) }}>\n                          <div className=\"absolute inset-x-0 h-0.5 -translate-y-1/2 bg-danger\" />\n                          <div className=\"absolute -left-1.5 size-3 -translate-y-1/2 rounded-full bg-danger\" />\n                        </div>\n                      ) : null}\n                    </div>\n                  )\n                })}\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <p aria-live=\"polite\" className=\"sr-only\">\n        {keySlot ? `${fmt.long.format(days[keySlot.day] ?? days[0])}, ${fmt.time.format(at(days[keySlot.day] ?? days[0], 0, keySlot.min))}` : \"\"}\n      </p>\n\n      {/* Legend */}\n      <ul aria-label=\"Calendars\" className=\"flex flex-wrap items-center gap-x-4 gap-y-1.5 border-t border-border px-4 py-2.5\">\n        {calendars.map((c) => (\n          <li key={c.id} className=\"inline-flex items-center gap-1.5 text-[11px] text-fg-muted\">\n            <span aria-hidden className={cn(\"size-2 rounded-full\", TONE[c.tone].dot)} />\n            {c.label}\n          </li>\n        ))}\n      </ul>\n    </div>\n  )\n}\n\nexport { EventCalendar }\nexport type { EventCalendarProps, CalendarEvent, CalendarSource, CalendarTone, CalendarView }\n"
    }
  ]
}
