{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "event-calendar",
  "title": "Event Calendar",
  "author": "ilinxa",
  "description": "Editable event calendar with month, week, day, and agenda views — multi-day spans, drag and resize editing, clipboard support, and keyboard navigation.",
  "dependencies": [
    "date-fns",
    "lucide-react"
  ],
  "registryDependencies": [
    "avatar",
    "badge",
    "button",
    "calendar",
    "popover",
    "skeleton",
    "@ilinxa/task-card"
  ],
  "files": [
    {
      "path": "src/registry/components/data/event-calendar/event-calendar.tsx",
      "content": "\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { useCalendar } from \"./hooks/use-calendar-context\";\nimport { useCalendarEditOptional } from \"./hooks/use-calendar-edit-extension\";\nimport { EventCalendarRoot } from \"./parts/calendar-root\";\nimport { CalendarToolbar } from \"./parts/calendar-toolbar\";\nimport { CalendarMiniNav } from \"./parts/calendar-mini-nav\";\nimport { CalendarMonthView } from \"./parts/calendar-month-view\";\nimport { CalendarWeekView } from \"./parts/calendar-week-view\";\nimport { CalendarDayView } from \"./parts/calendar-day-view\";\nimport { CalendarAgendaView } from \"./parts/calendar-agenda-view\";\nimport { CalendarEventInspector } from \"./parts/calendar-event-inspector\";\nimport type { CalendarHandle, CalendarProps } from \"./types\";\n\n/** Renders the active view (must live inside the Root to read context). */\nfunction ActiveView() {\n  const { view, availableViews } = useCalendar();\n  const v = availableViews.includes(view) ? view : availableViews[0];\n  switch (v) {\n    case \"week\":\n      return <CalendarWeekView />;\n    case \"day\":\n      return <CalendarDayView />;\n    case \"agenda\":\n      return <CalendarAgendaView />;\n    case \"month\":\n    default:\n      return <CalendarMonthView />;\n  }\n}\n\n/**\n * Renders the quick-composer / rename field / detail-editor overlays — but\n * ONLY when the editing extension is wired (`useCalendarEditOptional()` is\n * non-null). A tiny inner component so it can read that context itself\n * (must live inside the Root, same as `ActiveView`); the assembly never\n * statically imports any of these — they resolve through `edit.components`.\n */\nfunction EditOverlaysSlot({ showInspector }: { showInspector: boolean }) {\n  const edit = useCalendarEditOptional();\n  if (!edit) return null;\n  return (\n    <>\n      <edit.components.QuickComposer />\n      {/* Inline rename popup (the inspector never hosted rename). */}\n      <edit.components.RenameField />\n      {/* Detail editor — only when the inspector isn't mounted (it hosts the\n          editor inline; mounting both would double-render on `editingId`). */}\n      {showInspector ? null : <edit.components.EditOverlays />}\n    </>\n  );\n}\n\n/**\n * Calendar 01 — batteries-included assembly (Tier A). Root + toolbar + optional\n * mini-nav + the active view, gated by `show*` toggles. Contains no logic the\n * parts lack; hand-assemble `EventCalendarRoot` + the parts you want for a lighter\n * build.\n */\nexport const EventCalendar = forwardRef<CalendarHandle, CalendarProps>(\n  function EventCalendar(props, ref) {\n    const {\n      showToolbar = true,\n      showMiniNav = false,\n      showInspector = false,\n      className,\n      ...rootProps\n    } = props;\n\n    return (\n      <EventCalendarRoot ref={ref} className={className} {...rootProps}>\n        {showToolbar ? <CalendarToolbar /> : null}\n        <div className=\"flex min-h-0 flex-1\">\n          {showInspector ? (\n            <CalendarEventInspector className=\"hidden w-64 shrink-0 border-r border-border sm:flex\" />\n          ) : null}\n          {showMiniNav ? (\n            <CalendarMiniNav className=\"hidden border-r border-border p-2 sm:block\" />\n          ) : null}\n          <div className=\"min-w-0 flex-1 overflow-hidden\">\n            <ActiveView />\n          </div>\n        </div>\n        <EditOverlaysSlot showInspector={showInspector} />\n      </EventCalendarRoot>\n    );\n  },\n);\n",
      "type": "registry:component",
      "target": "components/event-calendar/event-calendar.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/index.ts",
      "content": "// Assembly (Tier A)\nexport { EventCalendar } from \"./event-calendar\";\n\n// Headless provider + context parts (Tier B) — flat exports, never a namespace\nexport { EventCalendarRoot } from \"./parts/calendar-root\";\nexport { CalendarToolbar } from \"./parts/calendar-toolbar\";\nexport { CalendarMonthView } from \"./parts/calendar-month-view\";\nexport { CalendarWeekView } from \"./parts/calendar-week-view\";\nexport { CalendarDayView } from \"./parts/calendar-day-view\";\nexport { CalendarAgendaView } from \"./parts/calendar-agenda-view\";\nexport { CalendarMiniNav } from \"./parts/calendar-mini-nav\";\nexport { CalendarEventInspector } from \"./parts/calendar-event-inspector\";\n\n// Standalone primitives (Tier C)\nexport {\n  CalendarEventChip,\n  CalendarEventBar,\n  CalendarTimeBlock,\n  NowIndicator,\n  EventTooltip,\n} from \"./parts/calendar-event\";\nexport { MonthDayCell } from \"./parts/calendar-month-view\";\nexport { TimeGrid, TimeGutter } from \"./parts/calendar-time-grid\";\nexport { AgendaRow } from \"./parts/calendar-agenda-view\";\nexport { CalendarSkeleton } from \"./parts/calendar-skeleton\";\nexport { CalendarFullCardTooltip } from \"./parts/event-tooltip-full\";\n\n// Hook\nexport { useCalendar } from \"./hooks/use-calendar-context\";\n\n// Editing injection seam (P3 feature-slicing) — base owns the extension\n// point; the editing feature (`@ilinxa/event-calendar-editing`) supplies the\n// `CalendarEditExtension` value passed as `CalendarProps.editing`. The edit\n// components / dispatchers / gestures themselves live in the feature's own\n// barrel (`features/editing`), NOT here — this package never statically\n// imports feature code.\nexport { useCalendarEditOptional } from \"./hooks/use-calendar-edit-extension\";\n\n// Public types (+ the consumed task-card data language, re-exported)\nexport type {\n  CalendarProps,\n  CalendarRootProps,\n  CalendarHandle,\n  CalendarView,\n  WeekStart,\n  EventKind,\n  CalendarOccurrence,\n  CalendarEventColor,\n  CalendarStatusTone,\n  CalendarTooltipRenderer,\n  TaskItem,\n} from \"./types\";\nexport type {\n  TaskPerson,\n  TaskStatusOption,\n  TaskPriorityOption,\n  TaskLabelOption,\n  TaskColorRamp,\n  TaskPermissions,\n  TaskPermissionRule,\n  TaskPermissionReason,\n} from \"./types\";\nexport type {\n  CalendarEditExtension,\n  CalendarEditProps,\n  CalendarEditContextValue,\n} from \"./hooks/use-calendar-edit-extension\";\n\n// Type-only re-exports for the base prop surface (R4 finding, 2026-08-11):\n// `CalendarProps` references these in its own signatures (onItemAdded…,\n// snap, quickCompose, renderQuickComposer), so a BASE-only consumer must be\n// able to name them from this barrel. Type-only — erased at compile, adds no\n// runtime dependency on the editing feature or task-card values.\nexport type {\n  CalendarSnap,\n  CalendarEditAction,\n  CalendarComposerTarget,\n  CalendarQuickComposerRenderer,\n  TaskItemAddedEvent,\n  TaskItemRemovedEvent,\n  TaskItemMovedEvent,\n  TaskFieldEditedEvent,\n  TaskStatusChangedEvent,\n} from \"./types\";\n",
      "type": "registry:component",
      "target": "components/event-calendar/index.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/types.ts",
      "content": "import type { ReactNode } from \"react\";\nimport type { CalendarEditExtension } from \"./hooks/use-calendar-edit-extension\";\n\n/*\n * event-calendar — the date-grid sibling of gantt-timeline.\n *\n * It consumes the SAME canonical TaskItem[] as the rest of the task family\n * (task-card / task-tree / kanban-board / gantt-timeline) and lays\n * the items onto a calendar grid (month / week / day / agenda) instead of a\n * continuous time axis. v1 is read-only; the editing surface is declared below\n * the `Editing (v0.2.0)` fence but inert in v1 (so v2 is purely additive).\n *\n * Cross-procomp reuse (mirrors gantt): the data + permission + event language\n * is IMPORTED from task-card via the same-category relative barrel and\n * RE-EXPORTED here, so a consumer importing the calendar gets the whole\n * vocabulary from one module. Rewriter-safe (same-category relative import).\n */\nimport type {\n  TaskItem,\n  TaskPerson,\n  TaskStatusOption,\n  TaskPriorityOption,\n  TaskLabelOption,\n  TaskColorRamp,\n  TaskPermissions,\n  TaskPermissionRule,\n  TaskPermissionReason,\n  TaskEditableField,\n  TaskItemAddedEvent,\n  TaskItemRemovedEvent,\n  TaskItemMovedEvent,\n  TaskFieldEditedEvent,\n  TaskStatusChangedEvent,\n} from \"../task-card\";\n\n// Re-export the consumed data + editing language so a consumer importing the\n// calendar gets item, option, permission, and event types without a second\n// import (same-category barrel import; rewriter-safe).\nexport type {\n  TaskItem,\n  TaskPerson,\n  TaskStatusOption,\n  TaskPriorityOption,\n  TaskLabelOption,\n  TaskColorRamp,\n  TaskPermissions,\n  TaskPermissionRule,\n  TaskPermissionReason,\n  TaskEditableField,\n  TaskItemAddedEvent,\n  TaskItemRemovedEvent,\n  TaskItemMovedEvent,\n  TaskFieldEditedEvent,\n  TaskStatusChangedEvent,\n};\n\n/* ───────── calendar enums + occurrence ───────── */\n\nexport type CalendarView = \"month\" | \"week\" | \"day\" | \"agenda\";\n\n/** 0 = Sunday … 6 = Saturday (matches date-fns `weekStartsOn`). */\nexport type WeekStart = 0 | 1 | 2 | 3 | 4 | 5 | 6;\n\n/** Semantic tone, mirrored from TaskStatusOption.tone. */\nexport type CalendarStatusTone = \"active\" | \"done\" | \"blocked\";\n\n/** How an event is laid out. Derived (never stored on TaskItem) — see lib/classify.ts. */\nexport type EventKind = \"all-day\" | \"timed\" | \"milestone\";\n\nexport type CalendarEventColor = {\n  fill: string;\n  foreground: string;\n  border?: string;\n};\n\n/**\n * Normalized, render-ready event — the output of lib/occurrences.ts. Exported\n * for advanced Tier-C use (like gantt's GanttRow / GanttBarGeometry).\n */\nexport type CalendarOccurrence = {\n  /** The source item (the calendar never mutates it). */\n  item: TaskItem;\n  /** = item.id */\n  id: string;\n  kind: EventKind;\n  /** Effective start, epoch ms (floating-local for date-only all-day). */\n  startMs: number;\n  /** Effective end, epoch ms (= startMs for a milestone). */\n  endMs: number;\n  /** kind !== \"timed\". */\n  allDay: boolean;\n  tone: CalendarStatusTone;\n  color: CalendarEventColor;\n  /** endMs < now && tone !== \"done\". */\n  overdue: boolean;\n  /** item.active === false. */\n  inactive: boolean;\n  /** Priority-flag color when flagged (see CalendarProps.flagPriority); else absent. */\n  flagColor?: string;\n  /** Unparseable date → finite-guard; rendered label-only, no geometry. */\n  invalid?: boolean;\n};\n\nexport type CalendarTooltipRenderer = (\n  item: TaskItem,\n  occ: CalendarOccurrence,\n) => ReactNode;\n\n/* ───────── editing (v0.2.0) enums + renderers ───────── */\n\n/** Drag/resize snap granularity. Time grid snaps to the minute increment;\n *  Month always snaps to the day regardless of this value. Default \"15min\". */\nexport type CalendarSnap =\n  | \"minute\"\n  | \"5min\"\n  | \"15min\"\n  | \"30min\"\n  | \"hour\"\n  | \"day\"\n  | \"off\"\n  | number;\n\n/** Edit actions; mapped onto task-card's `TaskPermissionRule` keys by the\n *  editing feature's `lib/edit-permissions.ts` (move/resize→drag,\n *  delete→remove, create→addChildren, editDetails→edit). Mirrors gantt's\n *  `GanttEditAction`. */\nexport type CalendarEditAction =\n  | \"move\"\n  | \"resize\"\n  | \"delete\"\n  | \"create\"\n  | \"editDetails\";\n\n/** Where the quick-composer is anchored + the seeded window. `x`/`y` are the\n *  pointer coords for floating placement (omitted for programmatic opens). */\nexport type CalendarComposerTarget = {\n  date: Date;\n  allDay: boolean;\n  defaultEnd: Date;\n  x?: number;\n  y?: number;\n};\n\n/** Override the default quick mini-composer (title + time + \"More options\"). */\nexport type CalendarQuickComposerRenderer = (args: {\n  date: Date;\n  allDay: boolean;\n  defaultEnd: Date;\n  commit: (seed: Partial<TaskItem>) => void;\n  cancel: () => void;\n  openFull: () => void;\n}) => ReactNode;\n\n/* ───────── public component props ───────── */\n\nexport type CalendarProps = {\n  // ── Data (identical surface to gantt / card / tree) ──\n  data: TaskItem[];\n  statusOptions?: TaskStatusOption[];\n  priorityOptions?: TaskPriorityOption[];\n  labelOptions?: TaskLabelOption[];\n  /** Urgency ramp; RAMPS imported from task-card. Used only when `colorBy` is \"urgency\". */\n  colorRamp?: TaskColorRamp;\n  /** Per-status accent color (status value → CSS color). When an item's status\n   *  has an entry it drives the event color — so changing status changes color. */\n  statusColors?: Record<string, string>;\n  /** What drives the event accent: \"status\" (default — by status/tone) or\n   *  \"urgency\" (the v1 time-elapsed ramp that matches task-card + gantt). */\n  colorBy?: \"status\" | \"urgency\";\n\n  // ── Cursor: view + focus date (each controlled OR uncontrolled) ──\n  defaultView?: CalendarView; // default \"month\"\n  view?: CalendarView; // controlled\n  onViewChange?: (view: CalendarView) => void;\n  defaultDate?: Date; // default = now\n  date?: Date; // controlled focus date\n  onDateChange?: (date: Date) => void;\n  /** Fires on every cursor move with the newly-visible window (lazy data fetch). */\n  onRangeChange?: (range: {\n    view: CalendarView;\n    start: Date;\n    end: Date;\n  }) => void;\n\n  // ── Calendar config ──\n  weekStartsOn?: WeekStart; // default 1 (Mon)\n  now?: Date | string; // SSR-stable now; client interval refreshes\n  colorRefreshIntervalMs?: number; // urgency tick; default 60_000\n  agendaRangeDays?: number; // default 30\n  maxEventsPerCell?: number; // month overflow cap; default = height-responsive\n  scrollToHour?: number; // time-grid initial scroll; default 8\n  /** Classification escape hatch (layer 1 of the 3-layer rule). */\n  classifyEvent?: (item: TaskItem) => EventKind | undefined;\n  /** Show a small priority flag on events where this returns true (flag color =\n   *  the item's `priorityOptions` color). Opt-in; e.g. `(i) => i.priority === \"high\"`. */\n  flagPriority?: (item: TaskItem) => boolean;\n\n  // ── Assembly toggles + layout ──\n  showToolbar?: boolean; // default true (assembly only)\n  showMiniNav?: boolean; // default false (assembly only)\n  /** Render the selected-event inspector panel as a side column. Default false (assembly only). */\n  showInspector?: boolean;\n  /** Trim the toolbar view switch + assembly's mountable views. Default all four. */\n  views?: CalendarView[];\n  className?: string;\n  \"aria-label\"?: string;\n\n  // ── Read-only interactions ──\n  selectedId?: string | null;\n  onSelect?: (itemId: string | null) => void;\n  onTaskClick?: (item: TaskItem) => void;\n  /** Fires on a day click. NOTE: when `editable`, month day single-click is taken\n   *  over by editing (double-click / Enter composes), so this does not fire there. */\n  onDateClick?: (date: Date) => void;\n  onShowMore?: (date: Date, items: TaskItem[]) => void;\n  /** Override the hover tooltip; default = lightweight summary. */\n  renderTooltip?: CalendarTooltipRenderer;\n\n  // ══ Editing (v0.2.0) — ALL opt-in; default surface is the v1 read-only calendar ══\n  /** Master switch. Default false → byte-identical v1 read-only behavior. */\n  editable?: boolean;\n  /** Wires the editing feature slice (P3 — `@ilinxa/event-calendar-editing`).\n   *  Import `calendarEditing` from the feature's barrel and pass it here\n   *  alongside `editable` to enable drag/resize/create/clipboard/keyboard\n   *  editing. `editable` without `editing` stays read-only (one dev\n   *  `console.warn`) — the base package never statically imports the feature. */\n  editing?: CalendarEditExtension;\n  /** Full mutated forest after ANY edit; controlled consumer echoes into `data`. */\n  onChange?: (data: TaskItem[]) => void;\n  /** Reschedule sugar — fires alongside onChange/onFieldEdited (kept from gantt). */\n  onTaskReschedule?: (next: {\n    itemId: string;\n    startAt: string;\n    expireAt?: string;\n  }) => void;\n  // CRUD + field events (shapes reused verbatim from task-card)\n  onItemAdded?: (event: TaskItemAddedEvent) => void;\n  onItemRemoved?: (event: TaskItemRemovedEvent) => void;\n  onItemMoved?: (event: TaskItemMovedEvent) => void;\n  /** Granular per-field edit event. Fires for name / description / status /\n   *  active / setAt / startAt / expireAt / duration from drag-reschedule,\n   *  inline rename, context-menu status, AND the inspector / modal detail editor\n   *  (v0.2.2). NOTE: `priority` is intentionally absent — it is not a\n   *  `TaskEditableField`, so it cannot be carried by `TaskFieldEditedEvent`;\n   *  priority changes persist via `onChange` only. */\n  onFieldEdited?: (event: TaskFieldEditedEvent) => void;\n  onStatusChanged?: (event: TaskStatusChangedEvent) => void;\n  // Permissions (reused from task-card; mirrors gantt + tree)\n  permissions?: TaskPermissions;\n  canMoveItem?: (id: string) => boolean;\n  canResizeItem?: (id: string) => boolean;\n  canDeleteItem?: (id: string) => boolean;\n  canCreateChild?: (id: string) => boolean;\n  canEditItem?: (id: string) => boolean;\n  onPermissionDenied?: (\n    action: keyof TaskPermissionRule,\n    itemId: string,\n    reason: TaskPermissionReason,\n  ) => void;\n  /** Drag/resize snap granularity (time grid). Default \"15min\". */\n  snap?: CalendarSnap;\n  /** Create opens the Google-style quick mini-composer (default true when\n   *  editable); false → create opens the full detail card directly. */\n  quickCompose?: boolean;\n  /** RESERVED — not wired by any gesture in v0.2.0. Cross-surface task transfer\n   *  ships as copy/paste (the `ilinxa/task` clipboard envelope), so native HTML5\n   *  external drop targets are deferred; this callback stays declared for the\n   *  future opt-in but never fires today. */\n  onExternalDrop?: (date: Date, allDay: boolean, data: DataTransfer) => void;\n  /** Override the default quick-composer body. */\n  renderQuickComposer?: CalendarQuickComposerRenderer;\n};\n\n/** Headless provider props = assembly props minus the assembly-only toggles. */\nexport type CalendarRootProps = Omit<\n  CalendarProps,\n  \"showToolbar\" | \"showMiniNav\"\n> & {\n  children: ReactNode;\n};\n\n/* ───────── imperative handle ───────── */\n\nexport type CalendarHandle = {\n  goToDate(date: Date): void;\n  goToToday(): void;\n  setView(view: CalendarView): void;\n  next(): void;\n  prev(): void;\n  getVisibleRange(): { start: Date; end: Date };\n  // Editing (v0.2.0) — no-ops when `editable` is false / permission denied.\n  addTask(date: Date, item?: Partial<TaskItem>): void;\n  deleteTask(itemId: string): void;\n  editTask(itemId: string): void;\n  beginRename(itemId: string): void;\n  openQuickComposer(date: Date, allDay?: boolean): void;\n};\n\n/* ───────── context (internal; constructed in the Root) ─────────\n *\n * Split at P3 feature-slicing (v0.3.0): the base Root builds ONLY\n * `CalendarBaseContextValue` (read-only surface — cursor/data/config/\n * selection/read-only callbacks). The full edit surface (dispatchers +\n * transient UI + gestures + components) lives in `CalendarEditContextValue`,\n * defined in the injection seam (`hooks/use-calendar-edit-extension.ts`) and\n * supplied by the editing feature's Provider — base never references it by\n * static import, only through `useCalendarEditOptional()`.\n */\n\nexport type CalendarBaseContextValue = {\n  // cursor\n  view: CalendarView;\n  focusDate: Date;\n  visibleRange: { start: Date; end: Date };\n  weekStartsOn: WeekStart;\n  availableViews: CalendarView[];\n\n  // data\n  occurrences: CalendarOccurrence[];\n  nowMs: number;\n\n  // config\n  agendaRangeDays: number;\n  maxEventsPerCell?: number;\n  scrollToHour: number;\n  statusOptions?: TaskStatusOption[];\n  priorityOptions?: TaskPriorityOption[];\n  labelOptions?: TaskLabelOption[];\n\n  // selection\n  selectedId: string | null;\n\n  // cursor actions\n  setView(view: CalendarView): void;\n  goToDate(date: Date): void;\n  goToToday(): void;\n  next(): void;\n  prev(): void;\n  select(id: string | null): void;\n\n  // read-only interaction callbacks\n  onTaskClick?: (item: TaskItem) => void;\n  onDateClick?: (date: Date) => void;\n  onShowMore?: (date: Date, items: TaskItem[]) => void;\n  renderTooltip?: CalendarTooltipRenderer;\n};\n\n/** @deprecated internal compat alias — pre-split code referenced the combined\n *  (base + edit) shape under this name. Never part of the public API (not\n *  re-exported from `index.ts`); use `CalendarBaseContextValue` for the\n *  read-only context, or the editing feature's `CalendarEditContextValue`\n *  for the edit surface. */\nexport type CalendarContextValue = CalendarBaseContextValue;\n",
      "type": "registry:component",
      "target": "components/event-calendar/types.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/lib/classify.ts",
      "content": "/**\n * Date parsing + the §7-D5 three-layer all-day/timed/milestone classification.\n * Pure; framework-free.\n */\nimport type { EventKind, TaskItem } from \"../types\";\n\nconst DATE_ONLY_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst MS_PER_DAY = 86_400_000;\n\nexport type ParsedDate = {\n  /** Epoch ms, or NaN if unparseable. */\n  ms: number;\n  /** True when the source was a bare YYYY-MM-DD (no time component). */\n  dateOnly: boolean;\n};\n\n/**\n * Parse a TaskItem ISO date value. A bare calendar date (YYYY-MM-DD, no `T`)\n * is treated as an ALL-DAY, timezone-independent date and parsed as a FLOATING\n * LOCAL date — NOT via `Date.parse`, which per spec reads \"2026-06-22\" as UTC\n * midnight and renders a day early in negative-UTC offsets. Full timestamps go\n * through `Date.parse` unchanged (matching the rest of the family).\n */\nexport function parseDateValue(value: string | undefined): ParsedDate {\n  if (!value) return { ms: NaN, dateOnly: false };\n  if (DATE_ONLY_RE.test(value)) {\n    const [y, m, d] = value.split(\"-\").map(Number);\n    return { ms: new Date(y, m - 1, d).getTime(), dateOnly: true };\n  }\n  return { ms: Date.parse(value), dateOnly: false };\n}\n\n/** Effective start (floating-local for date-only). */\nexport function effectiveStart(item: TaskItem): ParsedDate {\n  return parseDateValue(item.startAt ?? item.setAt);\n}\n\n/**\n * Effective end. `expireAt` wins; else `start + duration`; else null (no end).\n * `ms: NaN` signals an unparseable `expireAt`.\n */\nexport function effectiveEnd(\n  item: TaskItem,\n  startMs: number,\n): { ms: number | null; dateOnly: boolean } {\n  if (item.expireAt) {\n    const p = parseDateValue(item.expireAt);\n    return { ms: p.ms, dateOnly: p.dateOnly };\n  }\n  if (item.duration != null) {\n    // Inherit the start's date-only-ness: a whole-day duration off a bare\n    // YYYY-MM-DD is conceptually all-day, so the derived end keeps the flag (C3).\n    return { ms: startMs + item.duration, dateOnly: effectiveStart(item).dateOnly };\n  }\n  return { ms: null, dateOnly: false };\n}\n\n/**\n * Three-layer rule, first match wins (§7-D5):\n *   1. consumer `classifyEvent` predicate (if it returns a kind)\n *   2. no end:  date-only start ⇒ all-day (a bare date = an all-day event);\n *               otherwise ⇒ milestone (a precise instant / deadline)\n *   3. date-only start or end ⇒ all-day (Google's mechanism)\n *   4. span ≥ 1 full day ⇒ all-day\n *   5. otherwise ⇒ timed\n */\nexport function classify(\n  item: TaskItem,\n  classifyEvent?: (item: TaskItem) => EventKind | undefined,\n): EventKind {\n  const override = classifyEvent?.(item);\n  if (override) return override;\n\n  const start = effectiveStart(item);\n  const end = effectiveEnd(item, start.ms);\n\n  if (end.ms == null) return start.dateOnly ? \"all-day\" : \"milestone\";\n  if (start.dateOnly || end.dateOnly) return \"all-day\";\n\n  const span = end.ms - start.ms;\n  if (Number.isFinite(span) && span >= MS_PER_DAY) return \"all-day\";\n  return \"timed\";\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/lib/classify.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/lib/color.ts",
      "content": "/**\n * Event color resolution. The OKLCH urgency ramp is IMPORTED from task-card\n * (`RAMPS`) — not re-derived — so the calendar matches the card + gantt exactly.\n * Only the done/blocked tones are adapted (muted / destructive).\n *\n * `RAMPS` must be the FIRST `from`-import so the meta-deps audit registers the\n * task-card dependency (blackboard / content-composer lesson).\n */\nimport { RAMPS } from \"../../task-card\";\nimport type {\n  CalendarEventColor,\n  CalendarStatusTone,\n  TaskColorRamp,\n  TaskItem,\n  TaskStatusOption,\n} from \"../types\";\n\nexport function toneFor(\n  item: TaskItem,\n  statusOptions?: TaskStatusOption[],\n): CalendarStatusTone {\n  const opt = statusOptions?.find((o) => o.value === item.status);\n  return opt?.tone ?? \"active\";\n}\n\n/** Resolve a `TaskColorRamp` (preset name | custom fn) into a callable. */\nexport function resolveRamp(\n  ramp: TaskColorRamp | undefined,\n): (t: number) => string {\n  if (ramp == null) return RAMPS.default;\n  if (typeof ramp === \"function\") return ramp;\n  return RAMPS[ramp] ?? RAMPS.default;\n}\n\n/** Fraction 0..1 of the window elapsed at `nowMs`. */\nexport function elapsedFraction(\n  startMs: number,\n  endMs: number,\n  nowMs: number,\n): number {\n  if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) {\n    return 0;\n  }\n  const f = (nowMs - startMs) / (endMs - startMs);\n  return f < 0 ? 0 : f > 1 ? 1 : f;\n}\n\n/**\n * Resolve the accent color for an event. Precedence:\n *   item.borderColor override → that color (skips the engine)\n *   statusColor (statusColors[item.status]) → that color (status-driven)\n *   tone \"done\"    → muted-foreground\n *   tone \"blocked\" → destructive\n *   colorBy \"urgency\" → time-elapsed ramp (the v1 behavior, matches card + gantt)\n *   otherwise (status mode, active, no explicit color) → primary\n *\n * The accent is the chip/block border + text; the surface tints it (color-mix)\n * for the background — same accent, light fill. Default `colorBy` is \"status\",\n * so changing an item's status changes its color (use \"urgency\" for the ramp).\n */\nexport function eventColor(\n  item: TaskItem,\n  tone: CalendarStatusTone,\n  startMs: number,\n  endMs: number,\n  nowMs: number,\n  ramp: (t: number) => string,\n  statusColor?: string,\n  colorBy: \"status\" | \"urgency\" = \"status\",\n): CalendarEventColor {\n  let accent: string;\n  if (item.borderColor) accent = item.borderColor;\n  else if (statusColor) accent = statusColor;\n  else if (tone === \"done\") accent = \"var(--muted-foreground)\";\n  else if (tone === \"blocked\") accent = \"var(--destructive)\";\n  else if (colorBy === \"urgency\") accent = ramp(elapsedFraction(startMs, endMs, nowMs));\n  else accent = \"var(--primary)\";\n  return { fill: accent, foreground: accent, border: accent };\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/lib/color.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/lib/date-range.ts",
      "content": "/**\n * Calendar date math — pure, framework-free, all via date-fns (DST-correct by\n * construction). Maps the cursor (view + focus date) to the visible range and\n * the per-view day grids.\n */\nimport {\n  addDays,\n  addMonths,\n  addWeeks,\n  eachDayOfInterval,\n  endOfMonth,\n  endOfWeek,\n  startOfDay,\n  startOfMonth,\n  startOfWeek,\n} from \"date-fns\";\nimport type { CalendarView, WeekStart } from \"../types\";\n\n/** The inclusive-start / exclusive-ish-end window the active view covers. */\nexport function visibleRange(\n  view: CalendarView,\n  focusDate: Date,\n  weekStartsOn: WeekStart,\n  agendaRangeDays: number,\n): { start: Date; end: Date } {\n  switch (view) {\n    case \"month\": {\n      return {\n        start: startOfWeek(startOfMonth(focusDate), { weekStartsOn }),\n        end: endOfWeek(endOfMonth(focusDate), { weekStartsOn }),\n      };\n    }\n    case \"week\": {\n      return {\n        start: startOfWeek(focusDate, { weekStartsOn }),\n        end: endOfWeek(focusDate, { weekStartsOn }),\n      };\n    }\n    case \"day\": {\n      const s = startOfDay(focusDate);\n      return { start: s, end: addDays(s, 1) };\n    }\n    case \"agenda\": {\n      const s = startOfDay(focusDate);\n      return { start: s, end: addDays(s, Math.max(1, agendaRangeDays)) };\n    }\n  }\n}\n\n/** Full weeks (5–6 rows × 7 days) covering the focus month. */\nexport function monthGrid(focusDate: Date, weekStartsOn: WeekStart): Date[][] {\n  const { start, end } = visibleRange(\"month\", focusDate, weekStartsOn, 0);\n  const days = eachDayOfInterval({ start, end });\n  const weeks: Date[][] = [];\n  for (let i = 0; i < days.length; i += 7) weeks.push(days.slice(i, i + 7));\n  return weeks;\n}\n\n/** The 7 day columns of the focus week. */\nexport function weekColumns(focusDate: Date, weekStartsOn: WeekStart): Date[] {\n  const s = startOfWeek(focusDate, { weekStartsOn });\n  return Array.from({ length: 7 }, (_, i) => addDays(s, i));\n}\n\n/** The consecutive days an agenda window spans (for day grouping). */\nexport function agendaDays(focusDate: Date, agendaRangeDays: number): Date[] {\n  const s = startOfDay(focusDate);\n  return Array.from({ length: Math.max(1, agendaRangeDays) }, (_, i) =>\n    addDays(s, i),\n  );\n}\n\n/** Step the focus date by one view-relative period. `dir` = +1 / −1. */\nexport function stepDate(\n  view: CalendarView,\n  focusDate: Date,\n  dir: number,\n  agendaRangeDays: number,\n): Date {\n  switch (view) {\n    case \"month\":\n      return addMonths(focusDate, dir);\n    case \"week\":\n      return addWeeks(focusDate, dir);\n    case \"day\":\n      return addDays(focusDate, dir);\n    case \"agenda\":\n      return addDays(focusDate, dir * Math.max(1, agendaRangeDays));\n  }\n}\n\n/** 0..23 — the hour rows of a time-grid. */\nexport const HOURS: number[] = Array.from({ length: 24 }, (_, h) => h);\n",
      "type": "registry:component",
      "target": "components/event-calendar/lib/date-range.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/lib/occurrences.ts",
      "content": "/**\n * TaskItem[] → CalendarOccurrence[] — the single normalization pass.\n * Pure; framework-free. Flattens `children` (every dated item renders; no WBS\n * rollup — D10), computes the effective window with finite guards (never a NaN\n * geometry / `toISOString` throw — gantt v0.3.1 G2 lesson), classifies, and\n * resolves color via the shared engine.\n */\nimport { classify, effectiveEnd, effectiveStart } from \"./classify\";\nimport { eventColor, resolveRamp, toneFor } from \"./color\";\nimport type {\n  CalendarOccurrence,\n  EventKind,\n  TaskColorRamp,\n  TaskItem,\n  TaskPriorityOption,\n  TaskStatusOption,\n} from \"../types\";\n\nexport type OccurrenceContext = {\n  nowMs: number;\n  classifyEvent?: (item: TaskItem) => EventKind | undefined;\n  statusOptions?: TaskStatusOption[];\n  priorityOptions?: TaskPriorityOption[];\n  colorRamp?: TaskColorRamp;\n  /** Per-status accent color; drives the event color in \"status\" mode. */\n  statusColors?: Record<string, string>;\n  /** \"status\" (default) or \"urgency\" (time ramp). */\n  colorBy?: \"status\" | \"urgency\";\n  /** Predicate → events that show a priority flag. */\n  flagPriority?: (item: TaskItem) => boolean;\n};\n\nexport function toOccurrences(\n  data: TaskItem[],\n  ctx: OccurrenceContext,\n): CalendarOccurrence[] {\n  const ramp = resolveRamp(ctx.colorRamp);\n  const out: CalendarOccurrence[] = [];\n\n  const walk = (item: TaskItem) => {\n    const kind = classify(item, ctx.classifyEvent);\n    const start = effectiveStart(item);\n    const end = effectiveEnd(item, start.ms);\n\n    const startMs = start.ms;\n    const rawEnd = end.ms == null ? startMs : end.ms; // no end ⇒ point\n    const invalid =\n      !Number.isFinite(startMs) || (end.ms != null && !Number.isFinite(end.ms));\n\n    // Finite guard: never emit a NaN geometry.\n    let endMs = Number.isFinite(rawEnd) ? rawEnd : startMs;\n    if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs < startMs) {\n      endMs = startMs;\n    }\n\n    const tone = toneFor(item, ctx.statusOptions);\n    const overdue =\n      Number.isFinite(endMs) && endMs < ctx.nowMs && tone !== \"done\";\n    const statusColor = ctx.statusColors?.[item.status];\n    const color = eventColor(\n      item,\n      tone,\n      startMs,\n      endMs,\n      ctx.nowMs,\n      ramp,\n      statusColor,\n      ctx.colorBy,\n    );\n    const flagColor = ctx.flagPriority?.(item)\n      ? (ctx.priorityOptions?.find((o) => o.value === item.priority)?.color ??\n        \"var(--destructive)\")\n      : undefined;\n\n    out.push({\n      item,\n      id: item.id,\n      kind,\n      startMs,\n      endMs,\n      allDay: kind !== \"timed\",\n      tone,\n      color,\n      overdue,\n      inactive: item.active === false,\n      flagColor,\n      invalid: invalid || undefined,\n    });\n\n    item.children?.forEach(walk);\n  };\n\n  data.forEach(walk);\n  return out;\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/lib/occurrences.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/lib/segments.ts",
      "content": "/**\n * Month-grid layout — pure. Lays a week's occurrences into horizontal lanes:\n * multi-day events become spanning bar segments (clipped per week row, with\n * continuation flags), single-day events occupy one column. Overflow past the\n * lane cap becomes a per-day \"+N more\" count.\n */\nimport { addDays, differenceInCalendarDays, startOfDay } from \"date-fns\";\nimport type { CalendarOccurrence } from \"../types\";\n\n/**\n * First/last covered calendar day (local). All-day end is EXCLUSIVE at local\n * midnight (iCal / Google semantics): an event 00:00 Mon → 00:00 Wed covers\n * Mon + Tue.\n */\nexport function coveredDays(occ: CalendarOccurrence): {\n  firstMs: number;\n  lastMs: number;\n} {\n  const first = startOfDay(occ.startMs).getTime();\n  let lastDay = startOfDay(occ.endMs);\n  if (occ.endMs === lastDay.getTime() && lastDay.getTime() > first) {\n    lastDay = addDays(lastDay, -1); // exclusive midnight end\n  }\n  return { firstMs: first, lastMs: Math.max(first, lastDay.getTime()) };\n}\n\nexport type MonthSegment = {\n  occ: CalendarOccurrence;\n  lane: number;\n  startCol: number; // 0..6 within the week\n  endCol: number; // 0..6\n  continuesLeft: boolean; // clipped at the week's left edge\n  continuesRight: boolean; // clipped at the week's right edge\n  spanning: boolean; // endCol > startCol → bar, else chip\n};\n\nexport type MonthWeekLayout = {\n  segments: MonthSegment[]; // lane < cap\n  overflow: number[]; // hidden-count per day-column (length 7)\n  laneCount: number; // lanes actually rendered (≤ cap)\n};\n\n/**\n * Lay a run of `startOfDay` dates into ≤ `cap` lanes. Used by the month grid\n * (7-day weeks) AND the week/day all-day band (1–7 columns) — hence generalized\n * to `weekDays.length` rather than a hardcoded 7 (F-03).\n */\nexport function layoutMonthWeek(\n  weekDays: Date[],\n  occurrences: CalendarOccurrence[],\n  cap: number,\n): MonthWeekLayout {\n  const lastCol = weekDays.length - 1;\n  const weekStart = startOfDay(weekDays[0]).getTime();\n  const weekEnd = startOfDay(weekDays[lastCol]).getTime();\n\n  type Placed = { occ: CalendarOccurrence; startCol: number; endCol: number };\n  const placed: Placed[] = [];\n  for (const occ of occurrences) {\n    if (occ.invalid) continue;\n    const { firstMs, lastMs } = coveredDays(occ);\n    if (lastMs < weekStart || firstMs > weekEnd) continue;\n    const startCol = Math.max(\n      0,\n      differenceInCalendarDays(new Date(Math.max(firstMs, weekStart)), weekDays[0]),\n    );\n    const endCol = Math.min(\n      lastCol,\n      differenceInCalendarDays(new Date(Math.min(lastMs, weekEnd)), weekDays[0]),\n    );\n    placed.push({ occ, startCol, endCol });\n  }\n\n  // All-day / multi-day first (earlier start, then longer span), timed after.\n  placed.sort((a, b) => {\n    const am = a.occ.allDay ? 0 : 1;\n    const bm = b.occ.allDay ? 0 : 1;\n    if (am !== bm) return am - bm;\n    if (a.startCol !== b.startCol) return a.startCol - b.startCol;\n    return b.endCol - b.startCol - (a.endCol - a.startCol);\n  });\n\n  const laneOccupancy: boolean[][] = []; // [lane][col]\n  const overflow = new Array(weekDays.length).fill(0);\n  const segments: MonthSegment[] = [];\n  let laneCount = 0;\n\n  for (const p of placed) {\n    let lane = 0;\n    for (;;) {\n      if (!laneOccupancy[lane])\n        laneOccupancy[lane] = new Array(weekDays.length).fill(false);\n      let free = true;\n      for (let c = p.startCol; c <= p.endCol; c++) {\n        if (laneOccupancy[lane][c]) {\n          free = false;\n          break;\n        }\n      }\n      if (free) break;\n      lane++;\n    }\n    for (let c = p.startCol; c <= p.endCol; c++) laneOccupancy[lane][c] = true;\n\n    if (lane >= cap) {\n      for (let c = p.startCol; c <= p.endCol; c++) overflow[c]++;\n      continue;\n    }\n    laneCount = Math.max(laneCount, lane + 1);\n    const { firstMs, lastMs } = coveredDays(p.occ);\n    segments.push({\n      occ: p.occ,\n      lane,\n      startCol: p.startCol,\n      endCol: p.endCol,\n      continuesLeft: firstMs < weekStart,\n      continuesRight: lastMs > weekEnd,\n      spanning: p.endCol > p.startCol,\n    });\n  }\n\n  return { segments, overflow, laneCount };\n}\n\n/** Every occurrence covering a given day (for the \"+N more\" popover list). */\nexport function occurrencesOnDay(\n  occ: CalendarOccurrence[],\n  day: Date,\n): CalendarOccurrence[] {\n  const dayStart = startOfDay(day).getTime();\n  return occ.filter((o) => {\n    if (o.invalid) return false;\n    const { firstMs, lastMs } = coveredDays(o);\n    return dayStart >= firstMs && dayStart <= lastMs;\n  });\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/lib/segments.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/lib/lane-pack.ts",
      "content": "/**\n * Week/Day time-grid overlap packing — pure. Classic greedy interval-graph\n * column assignment: overlapping timed events split into side-by-side lanes;\n * each cluster reports its lane count so blocks can size to `1 / laneCount`.\n */\nimport type { CalendarOccurrence } from \"../types\";\n\nexport type PackedBlock = {\n  occ: CalendarOccurrence;\n  lane: number; // 0-based column within its cluster\n  laneCount: number; // columns in the cluster (block width = 1 / laneCount)\n};\n\nexport function packLanes(timed: CalendarOccurrence[]): PackedBlock[] {\n  const sorted = [...timed].sort(\n    (a, b) => a.startMs - b.startMs || a.endMs - b.endMs,\n  );\n\n  const out: PackedBlock[] = [];\n  let cluster: { occ: CalendarOccurrence; lane: number }[] = [];\n  let clusterEnd = -Infinity;\n  const laneEnds: number[] = []; // end ms per active lane\n\n  const flush = () => {\n    if (!cluster.length) return;\n    const laneCount = Math.max(1, ...cluster.map((c) => c.lane + 1));\n    for (const c of cluster) out.push({ occ: c.occ, lane: c.lane, laneCount });\n    cluster = [];\n    laneEnds.length = 0;\n  };\n\n  for (const occ of sorted) {\n    // A gap with no overlap closes the current cluster.\n    if (cluster.length && occ.startMs >= clusterEnd) flush();\n\n    let lane = laneEnds.findIndex((end) => end <= occ.startMs);\n    if (lane === -1) {\n      lane = laneEnds.length;\n      laneEnds.push(occ.endMs);\n    } else {\n      laneEnds[lane] = occ.endMs;\n    }\n    cluster.push({ occ, lane });\n    clusterEnd = Math.max(clusterEnd, occ.endMs);\n  }\n  flush();\n  return out;\n}\n\n/** Vertical placement of a timed block within a day column, as 0..1 fractions. */\nexport function blockOffsets(\n  occ: CalendarOccurrence,\n  dayStartMs: number,\n): { top: number; height: number } {\n  const dayMs = 86_400_000;\n  const top = Math.max(0, (occ.startMs - dayStartMs) / dayMs);\n  const rawH = (occ.endMs - occ.startMs) / dayMs;\n  // Minimum height so a very short event is still tappable.\n  const height = Math.min(1 - top, Math.max(rawH, 0.02));\n  return { top, height };\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/lib/lane-pack.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/hooks/use-calendar-context.ts",
      "content": "\"use client\";\n\nimport { createContext, useContext } from \"react\";\nimport type { CalendarBaseContextValue } from \"../types\";\n\nexport const CalendarContext = createContext<CalendarBaseContextValue | null>(\n  null,\n);\n\n/** Read the base (read-only) calendar context. Throws if used outside\n *  `<EventCalendarRoot>`. For the edit surface, see the editing feature's\n *  `useCalendarEditOptional()` / `useCalendarEditContext()`. */\nexport function useCalendar(): CalendarBaseContextValue {\n  const ctx = useContext(CalendarContext);\n  if (!ctx) {\n    throw new Error(\"useCalendar must be used within <EventCalendarRoot>\");\n  }\n  return ctx;\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/hooks/use-calendar-context.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/hooks/use-calendar-cursor.ts",
      "content": "\"use client\";\n\nimport { useCallback, useState } from \"react\";\nimport { stepDate } from \"../lib/date-range\";\nimport type { CalendarView } from \"../types\";\n\ntype CursorArgs = {\n  defaultView?: CalendarView;\n  view?: CalendarView;\n  onViewChange?: (v: CalendarView) => void;\n  defaultDate?: Date;\n  date?: Date;\n  onDateChange?: (d: Date) => void;\n  now?: Date | string;\n  agendaRangeDays: number;\n};\n\n/**\n * Resolves the cursor — `{ view, focusDate }` — in either controlled or\n * uncontrolled mode, and the navigation actions. Controlled when `view` /\n * `date` props are passed; otherwise internal state seeded by `default*`.\n */\nexport function useCalendarCursor(args: CursorArgs) {\n  const [internalView, setInternalView] = useState<CalendarView>(\n    args.defaultView ?? \"month\",\n  );\n  const view = args.view ?? internalView;\n\n  const [internalDate, setInternalDate] = useState<Date>(\n    () => args.defaultDate ?? (args.now != null ? new Date(args.now) : new Date()),\n  );\n  const focusDate = args.date ?? internalDate;\n\n  const viewControlled = args.view !== undefined;\n  const dateControlled = args.date !== undefined;\n  const { onViewChange, onDateChange, now, agendaRangeDays } = args;\n\n  const setView = useCallback(\n    (v: CalendarView) => {\n      if (!viewControlled) setInternalView(v);\n      onViewChange?.(v);\n    },\n    [viewControlled, onViewChange],\n  );\n\n  const goToDate = useCallback(\n    (d: Date) => {\n      if (!dateControlled) setInternalDate(d);\n      onDateChange?.(d);\n    },\n    [dateControlled, onDateChange],\n  );\n\n  const goToToday = useCallback(() => {\n    goToDate(now != null ? new Date(now) : new Date());\n  }, [goToDate, now]);\n\n  const next = useCallback(() => {\n    goToDate(stepDate(view, focusDate, 1, agendaRangeDays));\n  }, [goToDate, view, focusDate, agendaRangeDays]);\n\n  const prev = useCallback(() => {\n    goToDate(stepDate(view, focusDate, -1, agendaRangeDays));\n  }, [goToDate, view, focusDate, agendaRangeDays]);\n\n  return { view, focusDate, setView, goToDate, goToToday, next, prev };\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/hooks/use-calendar-cursor.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/hooks/use-calendar-edit-extension.ts",
      "content": "\"use client\";\n\n/**\n * The editing injection seam (P3 feature-slicing, strategy-b). BASE owns this\n * file — the extension TYPE, the context, and the optional accessor — and\n * NEVER statically imports anything under `../features/editing/`. The editing\n * feature (`@ilinxa/event-calendar-editing`) supplies the `CalendarEditExtension`\n * value (a `Provider`) that a consumer passes via `CalendarProps.editing`;\n * `EventCalendarRoot` mounts that Provider (only when `editable` too) around\n * its children, which is what makes `CalendarEditContext` resolve non-null\n * for every base part below it. Base parts read the surface via\n * `useCalendarEditOptional()` and render `edit ? <editable-ui> : <read-only-ui>`\n * — so a base-only install (no `editing` prop, or the feature package absent)\n * compiles and renders fully read-only, byte-identical to v1.\n *\n * Dependency-light by design (react + local `../types` only) — this file ships\n * with the BASE artifact, so it must never pull `@dnd-kit/*` or any feature code.\n */\n\nimport { createContext, useContext } from \"react\";\nimport type {\n  ComponentType,\n  KeyboardEvent,\n  MouseEvent as ReactMouseEvent,\n  PointerEvent as ReactPointerEvent,\n  ReactNode,\n  RefObject,\n} from \"react\";\nimport type {\n  CalendarComposerTarget,\n  CalendarEditAction,\n  CalendarOccurrence,\n  CalendarQuickComposerRenderer,\n  CalendarSnap,\n  TaskFieldEditedEvent,\n  TaskItem,\n  TaskItemAddedEvent,\n  TaskItemMovedEvent,\n  TaskItemRemovedEvent,\n  TaskLabelOption,\n  TaskPermissionReason,\n  TaskPermissionRule,\n  TaskPermissions,\n  TaskPriorityOption,\n  TaskStatusChangedEvent,\n  TaskStatusOption,\n} from \"../types\";\n\n/* ───────── component prop contracts (structural — the feature's real\n   implementations must conform; base call sites typecheck against these) ───────── */\n\nexport type DraggableEventWrapProps = {\n  occ: CalendarOccurrence;\n  canDrag: boolean;\n  resizable: boolean;\n  containerRef: RefObject<HTMLDivElement | null>;\n  cols: Date[];\n  children: ReactNode;\n};\n\nexport type DroppableDayCellProps = {\n  day: Date;\n  outside: boolean;\n  today: boolean;\n  hidden: number;\n  hiddenItems: CalendarOccurrence[];\n  onShowMore?: (d: Date, items: TaskItem[]) => void;\n};\n\nexport type EventEditorPanelProps = {\n  item: TaskItem;\n  statusOptions?: TaskStatusOption[];\n  priorityOptions?: TaskPriorityOption[];\n  labelOptions?: TaskLabelOption[];\n  permissions?: TaskPermissions;\n  onChange: (next: TaskItem) => void;\n  onDone: () => void;\n  className?: string;\n};\n\n/** Native-pointer time-grid gestures, pre-parameterized by the Provider (snap +\n *  the live dispatchers are closed over) — base call sites pass only the\n *  per-invocation event + refs. */\nexport type CalendarGestures = {\n  /** Pointer-down on a timed block → drag-move (continuous time grid). */\n  startTimedMove: (\n    e: ReactPointerEvent,\n    occ: CalendarOccurrence,\n    gridRef: RefObject<HTMLDivElement | null>,\n    columns: Date[],\n    suppressClick: RefObject<boolean>,\n    gestureCleanup: RefObject<(() => void) | null>,\n  ) => void;\n  /** Pointer-down on a timed block's edge grip → resize. */\n  startTimedResize: (\n    e: ReactPointerEvent,\n    occ: CalendarOccurrence,\n    edge: \"start\" | \"end\",\n    colRef: RefObject<HTMLDivElement | null>,\n    dayStartMs: number,\n    gestureCleanup: RefObject<(() => void) | null>,\n  ) => void;\n  /** Pointer-down + drag across empty time-grid space → create-by-drag. */\n  startDraw: (\n    e: ReactPointerEvent,\n    colRef: RefObject<HTMLDivElement | null>,\n    dayStartMs: number,\n    gestureCleanup: RefObject<(() => void) | null>,\n  ) => void;\n  /** Double-click empty time-grid space → create a default 1h event. */\n  createAtDoubleClick: (\n    e: ReactMouseEvent,\n    colRef: RefObject<HTMLDivElement | null>,\n    dayStartMs: number,\n  ) => void;\n};\n\n/* ───────── the full edit-surface context value (feature-supplied) ───────── */\n\nexport type CalendarEditContextValue = {\n  editable: boolean;\n  snap: CalendarSnap;\n  quickCompose: boolean;\n  permissions?: TaskPermissions;\n\n  getItem: (id: string) => TaskItem | undefined;\n  can: (action: CalendarEditAction, item: TaskItem) => boolean;\n\n  // dispatchers (mirror the pre-split `use-calendar-edit` surface)\n  rescheduleItem: (\n    id: string,\n    patch: { startMs?: number; endMs?: number; allDay: boolean },\n    kind: \"move\" | \"resize\",\n  ) => void;\n  createItem: (\n    parentId: string | null,\n    seed: Partial<TaskItem> | undefined,\n    window: { startMs: number; endMs?: number; allDay: boolean },\n    opts?: { openEditor?: boolean },\n  ) => void;\n  deleteItem: (id: string) => void;\n  renameItemAction: (id: string, name: string) => void;\n  changeStatus: (id: string, status: string) => void;\n  changePriority: (id: string, priority: string) => void;\n  applyEditedSubtree: (next: TaskItem) => void;\n\n  // transient edit UI\n  editingId: string | null;\n  openEditor: (id: string) => void;\n  closeEditor: () => void;\n  renamingId: string | null;\n  beginRename: (id: string) => void;\n  endRename: () => void;\n  composerTarget: CalendarComposerTarget | null;\n  openComposer: (target: CalendarComposerTarget) => void;\n  closeComposer: () => void;\n\n  /** Live drag/resize preview geometry (mid-gesture); commit happens on release. */\n  resizePreview: { id: string; startMs: number; endMs: number } | null;\n  setResizePreview: (\n    p: { id: string; startMs: number; endMs: number } | null,\n  ) => void;\n\n  // external drop-in + composer override (pass-through props)\n  onExternalDrop?: (date: Date, allDay: boolean, data: DataTransfer) => void;\n  renderQuickComposer?: CalendarQuickComposerRenderer;\n\n  /** Component refs base parts render through instead of static imports. */\n  components: {\n    DraggableEventWrap: ComponentType<DraggableEventWrapProps>;\n    DroppableDayCell: ComponentType<DroppableDayCellProps>;\n    BandDropCell: ComponentType<{ day: Date }>;\n    EventContextMenu: ComponentType<{ item: TaskItem; children: ReactNode }>;\n    EditOverlays: ComponentType<Record<string, never>>;\n    RenameField: ComponentType<Record<string, never>>;\n    QuickComposer: ComponentType<Record<string, never>>;\n    EventEditorPanel: ComponentType<EventEditorPanelProps>;\n  };\n\n  /** Pre-parameterized native-pointer time-grid gestures. */\n  gestures: CalendarGestures;\n\n  /** Event-focused keyboard editing (arrows move/resize, Enter edit, F2\n   *  rename, Delete). Returns true when it consumed the key. */\n  handleEventKey: (e: KeyboardEvent<HTMLDivElement>, id: string) => boolean;\n  /** Enter on a focused, empty day cell → open the quick-composer there. */\n  handleDayEnterKey: (dayMs: number) => void;\n\n  /** The `CalendarHandle` editing methods (imperative ref surface). */\n  handleMethods: {\n    addTask: (date: Date, item?: Partial<TaskItem>) => void;\n    deleteTask: (itemId: string) => void;\n    editTask: (itemId: string) => void;\n    beginRename: (itemId: string) => void;\n    openQuickComposer: (date: Date, allDay?: boolean) => void;\n  };\n};\n\n/** The edit-prop subset `EventCalendarRoot` collects from `CalendarProps` and\n *  hands to the extension's `Provider` — plus the shared root DOM ref (the\n *  clipboard + focus-restore effects need to know when focus is inside the\n *  calendar). `data` travels here too: the base context only exposes derived\n *  `occurrences`, never the raw forest the edit mutations operate on. */\nexport type CalendarEditProps = {\n  data: TaskItem[];\n  editable: boolean;\n  onChange?: (data: TaskItem[]) => void;\n  onTaskReschedule?: (next: {\n    itemId: string;\n    startAt: string;\n    expireAt?: string;\n  }) => void;\n  onItemAdded?: (event: TaskItemAddedEvent) => void;\n  onItemRemoved?: (event: TaskItemRemovedEvent) => void;\n  onItemMoved?: (event: TaskItemMovedEvent) => void;\n  onFieldEdited?: (event: TaskFieldEditedEvent) => void;\n  onStatusChanged?: (event: TaskStatusChangedEvent) => void;\n  permissions?: TaskPermissions;\n  canMoveItem?: (id: string) => boolean;\n  canResizeItem?: (id: string) => boolean;\n  canDeleteItem?: (id: string) => boolean;\n  canCreateChild?: (id: string) => boolean;\n  canEditItem?: (id: string) => boolean;\n  onPermissionDenied?: (\n    action: keyof TaskPermissionRule,\n    itemId: string,\n    reason: TaskPermissionReason,\n  ) => void;\n  snap: CalendarSnap;\n  quickCompose: boolean;\n  onExternalDrop?: (date: Date, allDay: boolean, data: DataTransfer) => void;\n  renderQuickComposer?: CalendarQuickComposerRenderer;\n  /** The calendar's root DOM node (clipboard-owns / focus-restore target). */\n  rootRef: RefObject<HTMLDivElement | null>;\n};\n\n/** The injection surface itself — what a consumer passes as `CalendarProps.editing`. */\nexport type CalendarEditExtension = {\n  Provider: ComponentType<{ editProps: CalendarEditProps; children: ReactNode }>;\n};\n\n/* ───────── context + accessors ───────── */\n\nexport const CalendarEditContext =\n  createContext<CalendarEditContextValue | null>(null);\n\n/** Read the edit surface, or `null` when no editing extension is wired (base-\n *  only install, or `editable` is false). Base parts branch on this — never\n *  throw, so a base-only bundle renders fully read-only. */\nexport function useCalendarEditOptional(): CalendarEditContextValue | null {\n  return useContext(CalendarEditContext);\n}\n\n/** Required variant for editing-feature-internal files — they only ever\n *  render inside the extension's own `Provider`, so a null context here is a\n *  wiring bug, not a valid read-only state. */\nexport function useCalendarEditContext(): CalendarEditContextValue {\n  const ctx = useContext(CalendarEditContext);\n  if (!ctx) {\n    throw new Error(\n      \"useCalendarEditContext must be used within the event-calendar editing extension's <Provider>\",\n    );\n  }\n  return ctx;\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/hooks/use-calendar-edit-extension.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/hooks/use-now-tick.ts",
      "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * SSR-safe \"now\" in epoch ms. On the server + first client render it returns\n * the seed (the `now` prop, if any) so server + client markup match; after\n * mount it switches to the real clock (on the next animation frame, so we never\n * setState synchronously inside the effect) and refreshes every `intervalMs`\n * (0 disables the refresh). Drives the now-line + urgency color.\n *\n * If no seed is given, the pre-mount value is `0` (epoch) — deterministic, so\n * no hydration mismatch; the real clock takes over on mount (a one-frame\n * settle). Pass `now` for an exact SSR-stable first paint.\n */\nexport function useNowTick(\n  seed: Date | string | undefined,\n  intervalMs: number,\n): number {\n  const seedMs =\n    seed == null\n      ? null\n      : typeof seed === \"string\"\n        ? Date.parse(seed)\n        : seed.getTime();\n\n  const [nowMs, setNowMs] = useState<number>(() =>\n    seedMs != null && Number.isFinite(seedMs) ? seedMs : 0,\n  );\n\n  useEffect(() => {\n    const tick = () => setNowMs(Date.now());\n    // First real-clock read on the next frame (not synchronous in the effect).\n    const raf = requestAnimationFrame(tick);\n    if (!intervalMs || intervalMs <= 0) {\n      return () => cancelAnimationFrame(raf);\n    }\n    const id = setInterval(tick, intervalMs);\n    return () => {\n      cancelAnimationFrame(raf);\n      clearInterval(id);\n    };\n  }, [intervalMs]);\n\n  return nowMs;\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/hooks/use-now-tick.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/parts/calendar-root.tsx",
      "content": "\"use client\";\n\nimport {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport type { KeyboardEvent, ReactNode, RefObject } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { CalendarContext } from \"../hooks/use-calendar-context\";\nimport { useCalendarCursor } from \"../hooks/use-calendar-cursor\";\nimport { useNowTick } from \"../hooks/use-now-tick\";\nimport { useCalendarEditOptional } from \"../hooks/use-calendar-edit-extension\";\nimport type { CalendarEditProps } from \"../hooks/use-calendar-edit-extension\";\nimport { visibleRange as computeVisibleRange } from \"../lib/date-range\";\nimport { toOccurrences } from \"../lib/occurrences\";\nimport type {\n  CalendarBaseContextValue,\n  CalendarHandle,\n  CalendarRootProps,\n  CalendarView,\n} from \"../types\";\n\nconst ALL_VIEWS: CalendarView[] = [\"month\", \"week\", \"day\", \"agenda\"];\n\n// Per-instance dedup (ref passed in) — two calendars on one page each get\n// their own diagnostic, matching the sibling `warnedEditingRef` pattern.\nfunction warnNoEditingMethod(warnedRef: { current: boolean }) {\n  if (process.env.NODE_ENV === \"production\" || warnedRef.current) return;\n  warnedRef.current = true;\n  console.warn(\n    \"[event-calendar] Called an editing method on the imperative handle, but no editing extension is wired — pass `editing={calendarEditing}` from @ilinxa/event-calendar-editing (and `editable`). No-op.\",\n  );\n}\n\ntype RootShellProps = {\n  className?: string;\n  ariaLabel?: string;\n  rootRef: RefObject<HTMLDivElement | null>;\n  rangeStartMs: number;\n  rangeEndMs: number;\n  goToDate: (date: Date) => void;\n  goToToday: () => void;\n  setView: (view: CalendarView) => void;\n  next: () => void;\n  prev: () => void;\n  availableViews: CalendarView[];\n  children: ReactNode;\n};\n\n/**\n * Inner shell (base, always rendered) — owns the root DOM node, the keyboard\n * router, and the imperative handle. Split out from `EventCalendarRoot` so it\n * can be mounted INSIDE the editing extension's `Provider` (when wired): the\n * Provider establishes `CalendarEditContext` above this component, so\n * `useCalendarEditOptional()` here resolves non-null exactly when editing is\n * active — letting the imperative handle + keyboard router delegate to the\n * edit surface without the base Root itself ever importing feature code.\n */\nconst RootShell = forwardRef<CalendarHandle, RootShellProps>(\n  function RootShell(\n    {\n      className,\n      ariaLabel,\n      rootRef,\n      rangeStartMs,\n      rangeEndMs,\n      goToDate,\n      goToToday,\n      setView,\n      next,\n      prev,\n      availableViews,\n      children,\n    },\n    ref,\n  ) {\n    const edit = useCalendarEditOptional();\n    const warnedNoEditingRef = useRef(false);\n\n    useImperativeHandle(\n      ref,\n      (): CalendarHandle => ({\n        goToDate,\n        goToToday,\n        setView,\n        next,\n        prev,\n        getVisibleRange: () => ({\n          start: new Date(rangeStartMs),\n          end: new Date(rangeEndMs),\n        }),\n        // Editing (v0.2.0+) — delegates to the wired extension; a dev-only\n        // console.warn (once) + silent no-op when no extension is wired.\n        addTask: (date, item) => {\n          if (!edit) return warnNoEditingMethod(warnedNoEditingRef);\n          edit.handleMethods.addTask(date, item);\n        },\n        deleteTask: (id) => {\n          if (!edit) return warnNoEditingMethod(warnedNoEditingRef);\n          edit.handleMethods.deleteTask(id);\n        },\n        editTask: (id) => {\n          if (!edit) return warnNoEditingMethod(warnedNoEditingRef);\n          edit.handleMethods.editTask(id);\n        },\n        beginRename: (id) => {\n          if (!edit) return warnNoEditingMethod(warnedNoEditingRef);\n          edit.handleMethods.beginRename(id);\n        },\n        openQuickComposer: (date, allDay) => {\n          if (!edit) return warnNoEditingMethod(warnedNoEditingRef);\n          edit.handleMethods.openQuickComposer(date, allDay);\n        },\n      }),\n      [goToDate, goToToday, setView, next, prev, rangeStartMs, rangeEndMs, edit],\n    );\n\n    // View/period keys (always) + delegated event/day-cell editing keys (only\n    // when an editing extension is wired — `edit` is null otherwise).\n    const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n      const targetEl = e.target as HTMLElement;\n      const tag = targetEl.tagName;\n      if (tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\") return;\n      if (edit) {\n        const occId = targetEl\n          .closest?.(\"[data-occ-id]\")\n          ?.getAttribute(\"data-occ-id\");\n        if (occId) {\n          if (edit.handleEventKey(e, occId)) return;\n        } else if (e.key === \"Enter\") {\n          const dayMsAttr = targetEl\n            .closest?.(\"[data-day-ms]\")\n            ?.getAttribute(\"data-day-ms\");\n          if (dayMsAttr) {\n            edit.handleDayEnterKey(Number(dayMsAttr));\n            e.preventDefault();\n            return;\n          }\n        }\n      }\n      switch (e.key) {\n        case \"ArrowLeft\":\n        case \"PageUp\":\n          e.preventDefault();\n          prev();\n          break;\n        case \"ArrowRight\":\n        case \"PageDown\":\n          e.preventDefault();\n          next();\n          break;\n        case \"t\":\n        case \"T\":\n          e.preventDefault();\n          goToToday();\n          break;\n        case \"m\":\n        case \"M\":\n          if (availableViews.includes(\"month\")) setView(\"month\");\n          break;\n        case \"w\":\n        case \"W\":\n          if (availableViews.includes(\"week\")) setView(\"week\");\n          break;\n        case \"d\":\n        case \"D\":\n          if (availableViews.includes(\"day\")) setView(\"day\");\n          break;\n        case \"a\":\n        case \"A\":\n          if (availableViews.includes(\"agenda\")) setView(\"agenda\");\n          break;\n      }\n    };\n\n    return (\n      <div\n        ref={rootRef}\n        tabIndex={0}\n        onKeyDown={handleKeyDown}\n        aria-label={ariaLabel ?? \"Calendar\"}\n        className={cn(\n          \"flex flex-col overflow-hidden rounded-lg border border-border bg-card text-card-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          className,\n        )}\n      >\n        {children}\n      </div>\n    );\n  },\n);\n\n/**\n * Headless provider (Tier B). Owns ALL base state — the cursor (view + focus\n * date), selection, the memoized occurrences, the now-tick — plus the\n * read-only context. No data state of its own (controlled, family invariant).\n *\n * Editing (P3 feature-slicing, v0.3.0): when BOTH `editable` and `editing`\n * (the `CalendarEditExtension`) are set, the Root mounts the extension's\n * `Provider` around its children — which is what makes `CalendarEditContext`\n * resolve for every part below. This file never statically imports anything\n * under `../features/editing/` (strategy-b injection); a base-only install\n * compiles and renders fully read-only.\n */\nexport const EventCalendarRoot = forwardRef<CalendarHandle, CalendarRootProps>(\n  function EventCalendarRoot(props, ref) {\n    const {\n      data,\n      statusOptions,\n      priorityOptions,\n      labelOptions,\n      colorRamp,\n      statusColors,\n      colorBy,\n      flagPriority,\n      classifyEvent,\n      now,\n      colorRefreshIntervalMs = 60_000,\n      agendaRangeDays = 30,\n      maxEventsPerCell,\n      scrollToHour = 8,\n      weekStartsOn = 1,\n      views,\n      selectedId: selectedIdProp,\n      onSelect,\n      onTaskClick,\n      onDateClick,\n      onShowMore,\n      onRangeChange,\n      renderTooltip,\n      // editing (v0.2.0+) — collected into `editProps` for the extension; not\n      // read directly by this component beyond `editable`/`editing` gating.\n      editable = false,\n      snap = \"15min\",\n      quickCompose = true,\n      onChange,\n      onTaskReschedule,\n      onItemAdded,\n      onItemRemoved,\n      onItemMoved,\n      onFieldEdited,\n      onStatusChanged,\n      permissions,\n      canMoveItem,\n      canResizeItem,\n      canDeleteItem,\n      canCreateChild,\n      canEditItem,\n      onPermissionDenied,\n      onExternalDrop,\n      renderQuickComposer,\n      editing,\n      className,\n      children,\n    } = props;\n\n    const availableViews = useMemo(\n      () => (views && views.length ? views : ALL_VIEWS),\n      [views],\n    );\n\n    const cursor = useCalendarCursor({\n      defaultView: props.defaultView,\n      view: props.view,\n      onViewChange: props.onViewChange,\n      defaultDate: props.defaultDate,\n      date: props.date,\n      onDateChange: props.onDateChange,\n      now,\n      agendaRangeDays,\n    });\n    const { view, focusDate, setView, goToDate, goToToday, next, prev } = cursor;\n\n    const nowMs = useNowTick(now, colorRefreshIntervalMs);\n\n    const occurrences = useMemo(\n      () =>\n        toOccurrences(data, {\n          nowMs,\n          classifyEvent,\n          statusOptions,\n          priorityOptions,\n          colorRamp,\n          statusColors,\n          colorBy,\n          flagPriority,\n        }),\n      [\n        data,\n        nowMs,\n        classifyEvent,\n        statusOptions,\n        priorityOptions,\n        colorRamp,\n        statusColors,\n        colorBy,\n        flagPriority,\n      ],\n    );\n\n    const visibleRange = useMemo(\n      () => computeVisibleRange(view, focusDate, weekStartsOn, agendaRangeDays),\n      [view, focusDate, weekStartsOn, agendaRangeDays],\n    );\n\n    // Selection — controlled (incl. null) or internal.\n    const [internalSelected, setInternalSelected] = useState<string | null>(\n      null,\n    );\n    const selectionControlled = selectedIdProp !== undefined;\n    const selectedId = selectionControlled\n      ? (selectedIdProp ?? null)\n      : internalSelected;\n    const select = useCallback(\n      (id: string | null) => {\n        if (!selectionControlled) setInternalSelected(id);\n        onSelect?.(id);\n      },\n      [selectionControlled, onSelect],\n    );\n\n    // onRangeChange — fire on mount + when the visible window changes. The\n    // callback is read through a ref updated in an effect (never during render)\n    // so an inline consumer callback doesn't retrigger the notify effect.\n    const rangeStartMs = visibleRange.start.getTime();\n    const rangeEndMs = visibleRange.end.getTime();\n    const rangeCbRef = useRef(onRangeChange);\n    useEffect(() => {\n      rangeCbRef.current = onRangeChange;\n    }, [onRangeChange]);\n    useEffect(() => {\n      rangeCbRef.current?.({\n        view,\n        start: new Date(rangeStartMs),\n        end: new Date(rangeEndMs),\n      });\n    }, [view, rangeStartMs, rangeEndMs]);\n\n    // Shared root DOM node — the editing extension's clipboard + focus-restore\n    // effects need to know when focus is inside the calendar; created here so\n    // the same ref object reaches both the extension Provider (via `editProps`)\n    // and `RootShell` (which attaches it to the actual div).\n    const rootRef = useRef<HTMLDivElement>(null);\n\n    // Dev-only, once: `editable` set but no extension wired → read-only fallback.\n    const warnedEditingRef = useRef(false);\n    useEffect(() => {\n      if (\n        process.env.NODE_ENV !== \"production\" &&\n        editable &&\n        !editing &&\n        !warnedEditingRef.current\n      ) {\n        warnedEditingRef.current = true;\n        console.warn(\n          \"[event-calendar] `editable` is set but no editing extension is wired — pass `editing={calendarEditing}` from @ilinxa/event-calendar-editing. Falling back to read-only.\",\n        );\n      }\n    }, [editable, editing]);\n\n    const ctx = useMemo<CalendarBaseContextValue>(\n      () => ({\n        view,\n        focusDate,\n        visibleRange,\n        weekStartsOn,\n        availableViews,\n        occurrences,\n        nowMs,\n        agendaRangeDays,\n        maxEventsPerCell,\n        scrollToHour,\n        statusOptions,\n        priorityOptions,\n        labelOptions,\n        selectedId,\n        setView,\n        goToDate,\n        goToToday,\n        next,\n        prev,\n        select,\n        onTaskClick,\n        onDateClick,\n        onShowMore,\n        renderTooltip,\n      }),\n      [\n        view,\n        focusDate,\n        visibleRange,\n        weekStartsOn,\n        availableViews,\n        occurrences,\n        nowMs,\n        agendaRangeDays,\n        maxEventsPerCell,\n        scrollToHour,\n        statusOptions,\n        priorityOptions,\n        labelOptions,\n        selectedId,\n        setView,\n        goToDate,\n        goToToday,\n        next,\n        prev,\n        select,\n        onTaskClick,\n        onDateClick,\n        onShowMore,\n        renderTooltip,\n      ],\n    );\n\n    const editProps = useMemo<CalendarEditProps>(\n      () => ({\n        data,\n        editable: true,\n        onChange,\n        onTaskReschedule,\n        onItemAdded,\n        onItemRemoved,\n        onItemMoved,\n        onFieldEdited,\n        onStatusChanged,\n        permissions,\n        canMoveItem,\n        canResizeItem,\n        canDeleteItem,\n        canCreateChild,\n        canEditItem,\n        onPermissionDenied,\n        snap,\n        quickCompose,\n        onExternalDrop,\n        renderQuickComposer,\n        rootRef,\n      }),\n      [\n        data,\n        onChange,\n        onTaskReschedule,\n        onItemAdded,\n        onItemRemoved,\n        onItemMoved,\n        onFieldEdited,\n        onStatusChanged,\n        permissions,\n        canMoveItem,\n        canResizeItem,\n        canDeleteItem,\n        canCreateChild,\n        canEditItem,\n        onPermissionDenied,\n        snap,\n        quickCompose,\n        onExternalDrop,\n        renderQuickComposer,\n      ],\n    );\n\n    // Mount the extension only when BOTH `editable` and `editing` are set —\n    // matches the v1 invariant exactly (editable=false ⇒ byte-identical\n    // read-only calendar, no DnD/clipboard machinery mounted at all).\n    const mountEditing = editable && !!editing;\n\n    const shell = (\n      <RootShell\n        ref={ref}\n        className={className}\n        ariaLabel={props[\"aria-label\"]}\n        rootRef={rootRef}\n        rangeStartMs={rangeStartMs}\n        rangeEndMs={rangeEndMs}\n        goToDate={goToDate}\n        goToToday={goToToday}\n        setView={setView}\n        next={next}\n        prev={prev}\n        availableViews={availableViews}\n      >\n        {children}\n      </RootShell>\n    );\n\n    return (\n      <CalendarContext.Provider value={ctx}>\n        {mountEditing && editing ? (\n          <editing.Provider editProps={editProps}>{shell}</editing.Provider>\n        ) : (\n          shell\n        )}\n      </CalendarContext.Provider>\n    );\n  },\n);\n",
      "type": "registry:component",
      "target": "components/event-calendar/parts/calendar-root.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/parts/calendar-toolbar.tsx",
      "content": "\"use client\";\n\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { format } from \"date-fns\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { useCalendar } from \"../hooks/use-calendar-context\";\nimport type { CalendarView } from \"../types\";\n\nconst VIEW_LABELS: Record<CalendarView, string> = {\n  month: \"Month\",\n  week: \"Week\",\n  day: \"Day\",\n  agenda: \"Agenda\",\n};\n\nfunction periodLabel(\n  view: CalendarView,\n  focusDate: Date,\n  range: { start: Date; end: Date },\n): string {\n  if (view === \"month\") return format(focusDate, \"MMMM yyyy\");\n  if (view === \"day\") return format(focusDate, \"EEEE, MMMM d, yyyy\");\n  // week + agenda → a range. Same year: show the year once, on the end\n  // (\"Jun 1 – Jun 7, 2026\"). Cross-year: show it on both (\"Dec 28, 2025 – Jan 3, 2026\").\n  const sameYear = range.start.getFullYear() === range.end.getFullYear();\n  return `${format(range.start, sameYear ? \"MMM d\" : \"MMM d, yyyy\")} – ${format(\n    range.end,\n    \"MMM d, yyyy\",\n  )}`;\n}\n\n/** Toolbar (Tier B): period nav + label + view switch. */\nexport function CalendarToolbar({ className }: { className?: string }) {\n  const {\n    view,\n    focusDate,\n    visibleRange,\n    availableViews,\n    setView,\n    next,\n    prev,\n    goToToday,\n  } = useCalendar();\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-wrap items-center justify-between gap-2 border-b border-border p-2\",\n        className,\n      )}\n    >\n      <div className=\"flex items-center gap-1\">\n        <Button variant=\"outline\" size=\"sm\" onClick={goToToday}>\n          Today\n        </Button>\n        <Button\n          variant=\"ghost\"\n          size=\"icon\"\n          aria-label=\"Previous period\"\n          onClick={prev}\n        >\n          <ChevronLeft className=\"size-4\" />\n        </Button>\n        <Button\n          variant=\"ghost\"\n          size=\"icon\"\n          aria-label=\"Next period\"\n          onClick={next}\n        >\n          <ChevronRight className=\"size-4\" />\n        </Button>\n        <span className=\"ml-1 text-sm font-semibold text-foreground\">\n          {periodLabel(view, focusDate, visibleRange)}\n        </span>\n      </div>\n\n      {availableViews.length > 1 ? (\n        // Plain-button segmented control (NOT shadcn ToggleGroup): Base UI's\n        // ToggleGroup is a multi-value model (`value: string[]`, `onValueChange:\n        // (string[], details) => void`) while Radix's `type=\"single\"` is a string\n        // — so the single-select view switcher fails consumer-tsc on Base UI\n        // (F-cross-13). Mirrors gantt-timeline's zoom switcher; drops the\n        // `toggle-group` dep entirely. (v0.2.1)\n        <div\n          role=\"group\"\n          aria-label=\"Calendar view\"\n          className=\"inline-flex overflow-hidden rounded-md border border-border\"\n        >\n          {availableViews.map((v) => (\n            <button\n              key={v}\n              type=\"button\"\n              aria-pressed={view === v}\n              aria-label={VIEW_LABELS[v]}\n              onClick={() => setView(v)}\n              className={cn(\n                \"h-8 px-3 text-sm font-medium transition-colors\",\n                view === v\n                  ? \"bg-primary text-primary-foreground\"\n                  : \"bg-card text-muted-foreground hover:bg-muted hover:text-foreground\",\n              )}\n            >\n              {VIEW_LABELS[v]}\n            </button>\n          ))}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/parts/calendar-toolbar.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/parts/calendar-month-view.tsx",
      "content": "\"use client\";\n\nimport type { CSSProperties, MouseEvent as ReactMouseEvent } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { format, isSameDay, isSameMonth, startOfDay } from \"date-fns\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { cn } from \"@/lib/utils\";\nimport { useCalendar } from \"../hooks/use-calendar-context\";\nimport { useCalendarEditOptional } from \"../hooks/use-calendar-edit-extension\";\nimport type { CalendarEditContextValue } from \"../hooks/use-calendar-edit-extension\";\nimport { layoutMonthWeek, occurrencesOnDay } from \"../lib/segments\";\nimport { monthGrid } from \"../lib/date-range\";\nimport {\n  CalendarEventBar,\n  CalendarEventChip,\n  EventHoverWrap,\n} from \"./calendar-event\";\nimport type { CalendarOccurrence, TaskItem } from \"../types\";\n\nconst DEFAULT_CAP = 3;\nconst LANE_H = \"1.3rem\";\n\n/** One month-grid day cell (Tier C). Renders the cell chrome + day number +\n *  the \"+N more\" affordance; spanning events are overlaid by the view. The\n *  editing feature's `DroppableDayCell` passes `dropRef`/`isOver` (a\n *  `@dnd-kit` droppable) + a create click; read-only consumers omit them and\n *  the cell stays context-free. */\nexport function MonthDayCell({\n  day,\n  outside,\n  today,\n  hidden,\n  hiddenItems,\n  onDayClick,\n  onDayDoubleClick,\n  onShowMore,\n  dropRef,\n  isOver,\n  creatable,\n}: {\n  day: Date;\n  outside: boolean;\n  today: boolean;\n  hidden: number;\n  hiddenItems: CalendarOccurrence[];\n  onDayClick?: (d: Date) => void;\n  onDayDoubleClick?: (d: Date, e: ReactMouseEvent) => void;\n  onShowMore?: (d: Date, items: TaskItem[]) => void;\n  dropRef?: (el: HTMLElement | null) => void;\n  isOver?: boolean;\n  creatable?: boolean;\n}) {\n  return (\n    <div\n      ref={dropRef}\n      role=\"gridcell\"\n      // Editable cells are keyboard-focusable; the root's delegated handler reads\n      // `data-day-ms` so Enter on a focused empty day opens the quick-composer.\n      tabIndex={creatable ? 0 : undefined}\n      data-day-ms={creatable ? startOfDay(day).getTime() : undefined}\n      onClick={onDayClick ? () => onDayClick(day) : undefined}\n      onDoubleClick={onDayDoubleClick ? (e) => onDayDoubleClick(day, e) : undefined}\n      className={cn(\n        \"relative min-h-27 border-r border-border outline-none last:border-r-0\",\n        (onDayClick || creatable) && \"cursor-pointer\",\n        creatable &&\n          \"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\",\n        outside && \"bg-muted/30\",\n        isOver && \"bg-primary/10 ring-1 ring-inset ring-primary/40\",\n      )}\n    >\n      <div className=\"flex items-center justify-end p-1\">\n        <span\n          className={cn(\n            \"flex size-6 items-center justify-center rounded-full text-xs tabular-nums\",\n            outside ? \"text-muted-foreground\" : \"text-foreground\",\n            today && \"bg-primary font-semibold text-primary-foreground\",\n          )}\n        >\n          {format(day, \"d\")}\n        </span>\n      </div>\n      {hidden > 0 ? (\n        <div className=\"absolute inset-x-1 bottom-1\">\n          {onShowMore ? (\n            <button\n              type=\"button\"\n              onClick={(e) => {\n                e.stopPropagation();\n                onShowMore(day, hiddenItems.map((o) => o.item));\n              }}\n              className=\"w-full rounded-sm px-1 text-left text-xs text-muted-foreground hover:bg-muted hover:text-foreground\"\n            >\n              +{hidden} more\n            </button>\n          ) : (\n            <Popover>\n              {/* F-cross-13 path-b: the trigger IS the button (native <button>\n                  in both backends) — `asChild` is Radix-only. (v0.2.5) */}\n              <PopoverTrigger\n                type=\"button\"\n                onClick={(e) => e.stopPropagation()}\n                className=\"w-full rounded-sm px-1 text-left text-xs text-muted-foreground hover:bg-muted hover:text-foreground\"\n              >\n                +{hidden} more\n              </PopoverTrigger>\n              <PopoverContent align=\"start\" className=\"w-56 p-2\">\n                <p className=\"mb-1.5 px-1 text-xs font-medium text-muted-foreground\">\n                  {format(day, \"EEEE, MMM d\")}\n                </p>\n                <DayList day={day} items={hiddenItems} />\n              </PopoverContent>\n            </Popover>\n          )}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nfunction DayList({\n  items,\n}: {\n  day: Date;\n  items: CalendarOccurrence[];\n}) {\n  const { select, onTaskClick } = useCalendar();\n  return (\n    <div className=\"flex flex-col gap-1\">\n      {items.map((occ) => (\n        <CalendarEventChip\n          key={occ.id}\n          occ={occ}\n          onClick={() => {\n            select(occ.id);\n            onTaskClick?.(occ.item);\n          }}\n        />\n      ))}\n    </div>\n  );\n}\n\n/** Month view (Tier B). 7-column weekday grid; multi-day spanning bars + chips\n *  in lanes, \"+N more\" overflow per day. Editing (when the extension is\n *  wired): drag-to-reschedule (whole days), all-day bar resize, click-to-create. */\nexport function CalendarMonthView({ className }: { className?: string }) {\n  const ctx = useCalendar();\n  const {\n    focusDate,\n    weekStartsOn,\n    occurrences,\n    nowMs,\n    maxEventsPerCell,\n    selectedId,\n    select,\n    onTaskClick,\n    onDateClick,\n    onShowMore,\n    renderTooltip,\n  } = ctx;\n  const edit = useCalendarEditOptional();\n\n  const weeks = monthGrid(focusDate, weekStartsOn);\n  const weeksCount = weeks.length;\n  const nowDate = new Date(nowMs);\n\n  // F-04 — height-responsive overflow cap: derive how many event lanes fit in a\n  // measured week-row, so a taller calendar shows more events before \"+N more\".\n  // `maxEventsPerCell` overrides (no measuring). rAF-coalesced ResizeObserver\n  // (gantt G8); state is set only inside the rAF (never directly in the effect).\n  const rowsRef = useRef<HTMLDivElement>(null);\n  const [responsiveCap, setResponsiveCap] = useState(DEFAULT_CAP);\n  useEffect(() => {\n    const el = rowsRef.current;\n    if (!el || maxEventsPerCell != null) return;\n    let raf = 0;\n    const ro = new ResizeObserver(() => {\n      if (raf) return;\n      raf = requestAnimationFrame(() => {\n        raf = 0;\n        const rowH = el.clientHeight / Math.max(1, weeksCount);\n        const root =\n          parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;\n        const laneH = root * 1.3 + 2; // LANE_H (1.3rem) + the grid's gap-y-0.5\n        const next = Math.max(1, Math.floor((rowH - 34) / laneH)); // 34 ≈ day-number row\n        setResponsiveCap((prev) => (prev === next ? prev : next));\n      });\n    });\n    ro.observe(el);\n    return () => {\n      if (raf) cancelAnimationFrame(raf);\n      ro.disconnect();\n    };\n  }, [maxEventsPerCell, weeksCount]);\n\n  const cap = maxEventsPerCell ?? responsiveCap;\n  // Apply the live resize preview (edit-only) to the occurrence being dragged.\n  const resizePreview = edit?.resizePreview ?? null;\n  const occs = resizePreview\n    ? occurrences.map((o) =>\n        o.id === resizePreview.id\n          ? { ...o, startMs: resizePreview.startMs, endMs: resizePreview.endMs }\n          : o,\n      )\n    : occurrences;\n\n  const activate = (occ: CalendarOccurrence) => {\n    select(occ.id);\n    onTaskClick?.(occ.item);\n  };\n\n  return (\n    <div role=\"grid\" className={cn(\"flex h-full flex-col\", className)}>\n      {/* weekday header */}\n      <div role=\"row\" className=\"grid grid-cols-7 border-b border-border\">\n        {weeks[0].map((d) => (\n          <div\n            key={d.toISOString()}\n            role=\"columnheader\"\n            className=\"border-r border-border px-2 py-1.5 text-xs font-medium text-muted-foreground last:border-r-0\"\n          >\n            {format(d, \"EEE\")}\n          </div>\n        ))}\n      </div>\n\n      {/* week rows — share the available height so the overflow cap is responsive */}\n      <div ref={rowsRef} className=\"flex min-h-0 flex-1 flex-col\">\n        {weeks.map((week) => (\n          <MonthWeekRow\n            key={week[0].toISOString()}\n            week={week}\n            cap={cap}\n            occurrences={occs}\n            focusDate={focusDate}\n            nowDate={nowDate}\n            selectedId={selectedId}\n            edit={edit}\n            onDateClick={onDateClick}\n            onShowMore={onShowMore}\n            renderTooltip={renderTooltip}\n            activate={activate}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n\n/** One week row — split out so it can own a stable ref for resize geometry. */\nfunction MonthWeekRow({\n  week,\n  cap,\n  occurrences,\n  focusDate,\n  nowDate,\n  selectedId,\n  edit,\n  onDateClick,\n  onShowMore,\n  renderTooltip,\n  activate,\n}: {\n  week: Date[];\n  cap: number;\n  occurrences: CalendarOccurrence[];\n  focusDate: Date;\n  nowDate: Date;\n  selectedId: string | null;\n  edit: CalendarEditContextValue | null;\n  onDateClick?: (d: Date) => void;\n  onShowMore?: (d: Date, items: TaskItem[]) => void;\n  renderTooltip?: ReturnType<typeof useCalendar>[\"renderTooltip\"];\n  activate: (occ: CalendarOccurrence) => void;\n}) {\n  const weekRef = useRef<HTMLDivElement | null>(null);\n  const layout = layoutMonthWeek(week, occurrences, cap);\n\n  return (\n    <div\n      ref={weekRef}\n      role=\"row\"\n      className=\"relative grid min-h-0 flex-1 grid-cols-7 border-b border-border last:border-b-0\"\n    >\n      {week.map((day, col) => {\n        const onDay = occurrencesOnDay(occurrences, day);\n        const hiddenItems = onDay.filter(\n          (o) => !layout.segments.some((s) => s.occ.id === o.id),\n        );\n        const cellProps = {\n          day,\n          outside: !isSameMonth(day, focusDate),\n          today: isSameDay(day, nowDate),\n          hidden: layout.overflow[col],\n          hiddenItems,\n          onShowMore,\n        };\n        return edit ? (\n          <edit.components.DroppableDayCell key={day.toISOString()} {...cellProps} />\n        ) : (\n          <MonthDayCell\n            key={day.toISOString()}\n            {...cellProps}\n            onDayClick={onDateClick}\n          />\n        );\n      })}\n\n      {/* events overlay (bars + chips), placed by lane */}\n      <div\n        className=\"pointer-events-none absolute inset-x-0 top-8 grid grid-cols-7 gap-x-px gap-y-0.5 px-px\"\n        style={\n          {\n            gridTemplateRows: `repeat(${cap}, minmax(0, ${LANE_H}))`,\n          } as CSSProperties\n        }\n      >\n        {layout.segments.map((seg) => {\n          const tooltip = renderTooltip\n            ? renderTooltip(seg.occ.item, seg.occ)\n            : undefined;\n          const isBar = seg.spanning || seg.occ.allDay;\n          const Event = isBar ? (\n            <CalendarEventBar\n              occ={seg.occ}\n              selected={selectedId === seg.occ.id}\n              continuesLeft={seg.continuesLeft}\n              continuesRight={seg.continuesRight}\n              onClick={() => activate(seg.occ)}\n            />\n          ) : (\n            <CalendarEventChip\n              occ={seg.occ}\n              selected={selectedId === seg.occ.id}\n              onClick={() => activate(seg.occ)}\n            />\n          );\n          const canDrag = !!edit && edit.can(\"move\", seg.occ.item);\n          // All-day bars (incl. single-day) get day-resize grips → drag an edge\n          // to extend across days. Clipped (continues-left/right) bars can't.\n          const resizable =\n            !!edit &&\n            seg.occ.allDay &&\n            !seg.continuesLeft &&\n            !seg.continuesRight &&\n            edit.can(\"resize\", seg.occ.item);\n          return (\n            <div\n              key={seg.occ.id}\n              className=\"pointer-events-auto min-w-0\"\n              style={{\n                gridColumn: `${seg.startCol + 1} / ${seg.endCol + 2}`,\n                gridRow: seg.lane + 1,\n              }}\n            >\n              {edit ? (\n                <edit.components.DraggableEventWrap\n                  occ={seg.occ}\n                  canDrag={canDrag}\n                  resizable={resizable}\n                  containerRef={weekRef}\n                  cols={week}\n                >\n                  {Event}\n                </edit.components.DraggableEventWrap>\n              ) : (\n                <EventHoverWrap tooltip={tooltip}>{Event}</EventHoverWrap>\n              )}\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/parts/calendar-month-view.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/parts/calendar-time-grid.tsx",
      "content": "\"use client\";\n\nimport type { RefObject } from \"react\";\nimport { useEffect, useRef } from \"react\";\nimport { format, isSameDay, startOfDay } from \"date-fns\";\nimport { cn } from \"@/lib/utils\";\nimport { useCalendar } from \"../hooks/use-calendar-context\";\nimport { useCalendarEditOptional } from \"../hooks/use-calendar-edit-extension\";\nimport { HOURS } from \"../lib/date-range\";\nimport { coveredDays, layoutMonthWeek } from \"../lib/segments\";\nimport { packLanes } from \"../lib/lane-pack\";\nimport {\n  CalendarEventBar,\n  CalendarTimeBlock,\n  EventHoverWrap,\n  NowIndicator,\n} from \"./calendar-event\";\nimport type { CalendarOccurrence } from \"../types\";\n\nconst HOUR_PX = 48;\nconst DAY_PX = HOUR_PX * 24;\nconst MS_PER_DAY = 86_400_000;\nconst BAND_CAP = 99; // the all-day band grows to fit its lanes\nconst LANE_H = \"1.4rem\";\n\n/** Left hour-label rail (Tier C). */\nexport function TimeGutter() {\n  return (\n    <div className=\"w-14 shrink-0 select-none\" aria-hidden>\n      {HOURS.map((h) => (\n        <div key={h} style={{ height: HOUR_PX }} className=\"relative\">\n          {h > 0 ? (\n            <span className=\"absolute -top-2 right-1.5 text-[0.65rem] tabular-nums text-muted-foreground\">\n              {format(new Date(2000, 0, 1, h), \"h a\")}\n            </span>\n          ) : null}\n        </div>\n      ))}\n    </div>\n  );\n}\n\n/* ───────── all-day band (F-03: column-spanning bars) ───────── */\n\n/** The all-day band — spanning bars laid out like the month grid (F-03).\n *  Editing (when wired): `edit.components.BandDropCell` supplies the droppable\n *  + click-create cell beneath the bars; `DraggableEventWrap` wraps each bar. */\nfunction AllDayBand({ columns }: { columns: Date[] }) {\n  const { occurrences, selectedId, select, onTaskClick, renderTooltip } =\n    useCalendar();\n  const edit = useCalendarEditOptional();\n  const overlayRef = useRef<HTMLDivElement>(null);\n  const n = columns.length;\n\n  // Apply the live resize preview, then take all-day occurrences in range.\n  const resizePreview = edit?.resizePreview ?? null;\n  const occs = resizePreview\n    ? occurrences.map((o) =>\n        o.id === resizePreview.id\n          ? { ...o, startMs: resizePreview.startMs, endMs: resizePreview.endMs }\n          : o,\n      )\n    : occurrences;\n  const firstMs = startOfDay(columns[0]).getTime();\n  const lastDayMs = startOfDay(columns[n - 1]).getTime();\n  // Inclusive coveredDays test (mirrors `layoutMonthWeek`'s own range check) —\n  // a bare `endMs > firstMs` drops zero-length point events sitting exactly on\n  // the first visible midnight, i.e. every single-day all-day event in Day\n  // view and on Week's first column (v0.2.4).\n  const allDay = occs.filter((o) => {\n    if (o.invalid || !o.allDay) return false;\n    const cov = coveredDays(o);\n    return cov.lastMs >= firstMs && cov.firstMs <= lastDayMs;\n  });\n  const layout = layoutMonthWeek(columns, allDay, BAND_CAP);\n  const laneCount = Math.max(1, layout.laneCount);\n\n  const activate = (occ: CalendarOccurrence) => {\n    select(occ.id);\n    onTaskClick?.(occ.item);\n  };\n\n  return (\n    <div className=\"flex border-b border-border\">\n      <div className=\"flex w-14 shrink-0 items-start justify-end pr-1.5 pt-1 text-[0.6rem] uppercase text-muted-foreground\">\n        all day\n      </div>\n      <div className=\"relative flex-1\">\n        {/* base cells (droppable + click-create when editing is wired) */}\n        <div className=\"grid\" style={{ gridTemplateColumns: `repeat(${n}, 1fr)` }}>\n          {columns.map((day) =>\n            edit ? (\n              <edit.components.BandDropCell key={day.toISOString()} day={day} />\n            ) : (\n              <div\n                key={day.toISOString()}\n                className=\"min-h-7 border-l border-border first:border-l-0\"\n              />\n            ),\n          )}\n        </div>\n        {/* spanning-bar overlay */}\n        <div\n          ref={overlayRef}\n          className=\"pointer-events-none absolute inset-0 grid gap-x-px gap-y-0.5 p-0.5\"\n          style={{\n            gridTemplateColumns: `repeat(${n}, 1fr)`,\n            gridTemplateRows: `repeat(${laneCount}, ${LANE_H})`,\n          }}\n        >\n          {layout.segments.map((seg) => {\n            const Bar = (\n              <CalendarEventBar\n                occ={seg.occ}\n                selected={selectedId === seg.occ.id}\n                continuesLeft={seg.continuesLeft}\n                continuesRight={seg.continuesRight}\n                onClick={() => activate(seg.occ)}\n              />\n            );\n            const canDrag = !!edit && edit.can(\"move\", seg.occ.item);\n            const resizable =\n              !!edit &&\n              !seg.continuesLeft &&\n              !seg.continuesRight &&\n              edit.can(\"resize\", seg.occ.item);\n            return (\n              <div\n                key={seg.occ.id}\n                className=\"pointer-events-auto min-w-0\"\n                style={{\n                  gridColumn: `${seg.startCol + 1} / ${seg.endCol + 2}`,\n                  gridRow: seg.lane + 1,\n                }}\n              >\n                {edit ? (\n                  <edit.components.DraggableEventWrap\n                    occ={seg.occ}\n                    canDrag={canDrag}\n                    resizable={resizable}\n                    containerRef={overlayRef}\n                    cols={columns}\n                  >\n                    {Bar}\n                  </edit.components.DraggableEventWrap>\n                ) : (\n                  <EventHoverWrap tooltip={renderTooltip?.(seg.occ.item, seg.occ)}>\n                    {Bar}\n                  </EventHoverWrap>\n                )}\n              </div>\n            );\n          })}\n        </div>\n      </div>\n    </div>\n  );\n}\n\n/* ───────── timed day column ───────── */\n\nfunction DayColumn({\n  day,\n  columns,\n  gridRef,\n}: {\n  day: Date;\n  columns: Date[];\n  gridRef: RefObject<HTMLDivElement | null>;\n}) {\n  const { occurrences, nowMs, selectedId, select, onTaskClick, renderTooltip } =\n    useCalendar();\n  const edit = useCalendarEditOptional();\n  const colRef = useRef<HTMLDivElement>(null);\n  const suppressClick = useRef(false);\n  // Active native-pointer gesture teardown — run on unmount so a mid-gesture\n  // view switch can't leak window listeners or let a later, unrelated\n  // pointerup commit a stale reschedule (v0.2.4).\n  const gestureCleanup = useRef<(() => void) | null>(null);\n  useEffect(() => () => gestureCleanup.current?.(), []);\n\n  const dayStartMs = startOfDay(day).getTime();\n  const dayEndMs = dayStartMs + MS_PER_DAY;\n  const timed = occurrences.filter(\n    (o) => !o.invalid && !o.allDay && o.endMs > dayStartMs && o.startMs < dayEndMs,\n  );\n  const blocks = packLanes(timed);\n  const showNow = isSameDay(day, new Date(nowMs));\n  const nowFrac = (nowMs - dayStartMs) / MS_PER_DAY;\n\n  const activate = (occ: CalendarOccurrence) => {\n    if (suppressClick.current) {\n      suppressClick.current = false;\n      return;\n    }\n    select(occ.id);\n    onTaskClick?.(occ.item);\n  };\n\n  const resizePreview = edit?.resizePreview ?? null;\n\n  return (\n    <div\n      ref={colRef}\n      className=\"relative flex-1 border-l border-border\"\n      style={{ height: DAY_PX }}\n      onPointerDown={\n        edit\n          ? (e) => {\n              // Presses that start on a block must not start a draw. Guarded\n              // here (not via stopPropagation on the block) so pointerdown\n              // still bubbles to the box-less ContextMenuTrigger span — its\n              // touch long-press timer depends on it (F-cross-13 path-b).\n              if ((e.target as Element).closest(\"[data-occ-id]\")) return;\n              edit.gestures.startDraw(e, colRef, dayStartMs, gestureCleanup);\n            }\n          : undefined\n      }\n      onDoubleClick={\n        edit\n          ? (e) => edit.gestures.createAtDoubleClick(e, colRef, dayStartMs)\n          : undefined\n      }\n    >\n      {HOURS.map((h) => (\n        <div\n          key={h}\n          style={{ height: HOUR_PX }}\n          className=\"border-t border-border/50 first:border-t-0\"\n        />\n      ))}\n      {showNow && nowFrac >= 0 && nowFrac <= 1 ? (\n        <NowIndicator topFraction={nowFrac} />\n      ) : null}\n      {blocks.map((b) => {\n        // Apply the live resize preview to THIS block's geometry mid-gesture.\n        const pv = resizePreview?.id === b.occ.id ? resizePreview : null;\n        const sMs = pv ? pv.startMs : b.occ.startMs;\n        const eMs = pv ? pv.endMs : b.occ.endMs;\n        const top = Math.max(0, (sMs - dayStartMs) / MS_PER_DAY);\n        const height = Math.min(1 - top, Math.max((eMs - sMs) / MS_PER_DAY, 0.02));\n        const canMove = !!edit && edit.can(\"move\", b.occ.item);\n        const canResize = !!edit && edit.can(\"resize\", b.occ.item);\n        const Block = (\n          <CalendarTimeBlock\n            occ={b.occ}\n            selected={selectedId === b.occ.id}\n            onClick={() => activate(b.occ)}\n            onPointerDown={\n              edit\n                ? (e) => {\n                    // No stopPropagation: the column's draw handler ignores\n                    // block-origin presses itself, and the context-menu span\n                    // needs the bubble for touch long-press.\n                    if (canMove)\n                      edit.gestures.startTimedMove(\n                        e,\n                        b.occ,\n                        gridRef,\n                        columns,\n                        suppressClick,\n                        gestureCleanup,\n                      );\n                  }\n                : undefined\n            }\n            top={top}\n            height={height}\n            left={b.lane / b.laneCount}\n            width={1 / b.laneCount}\n          />\n        );\n        return (\n          <div key={b.occ.id} className=\"contents\">\n            {edit ? (\n              <edit.components.EventContextMenu item={b.occ.item}>\n                {Block}\n              </edit.components.EventContextMenu>\n            ) : (\n              <EventHoverWrap tooltip={renderTooltip?.(b.occ.item, b.occ)}>\n                {Block}\n              </EventHoverWrap>\n            )}\n            {edit && canResize ? (\n              <div\n                className=\"pointer-events-none absolute z-20\"\n                style={{\n                  top: `${top * 100}%`,\n                  height: `${height * 100}%`,\n                  left: `calc(${(b.lane / b.laneCount) * 100}% + 1px)`,\n                  width: `calc(${(1 / b.laneCount) * 100}% - 2px)`,\n                }}\n              >\n                <div\n                  role=\"button\"\n                  aria-label=\"Resize start\"\n                  onPointerDown={(e) =>\n                    edit.gestures.startTimedResize(e, b.occ, \"start\", colRef, dayStartMs, gestureCleanup)\n                  }\n                  className=\"pointer-events-auto absolute inset-x-0 top-0 flex h-2 cursor-ns-resize items-center justify-center\"\n                >\n                  <span className=\"h-0.5 w-5 rounded-full bg-current opacity-40\" aria-hidden />\n                </div>\n                <div\n                  role=\"button\"\n                  aria-label=\"Resize end\"\n                  onPointerDown={(e) =>\n                    edit.gestures.startTimedResize(e, b.occ, \"end\", colRef, dayStartMs, gestureCleanup)\n                  }\n                  className=\"pointer-events-auto absolute inset-x-0 bottom-0 flex h-2 cursor-ns-resize items-center justify-center\"\n                >\n                  <span className=\"h-0.5 w-5 rounded-full bg-current opacity-40\" aria-hidden />\n                </div>\n              </div>\n            ) : null}\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\n/**\n * The hour time-grid (Tier C), shared by Week (7 columns) and Day (1 column):\n * column headers + all-day band (spanning bars) + a scrollable 24h grid with\n * lane-packed timed blocks and a now-line on today. Editing (when the\n * `editing` extension is wired): native-pointer timed move/resize + draw-to-\n * create via `edit.gestures`; the all-day band drags by whole days.\n */\nexport function TimeGrid({\n  columns,\n  className,\n}: {\n  columns: Date[];\n  className?: string;\n}) {\n  const { nowMs, scrollToHour } = useCalendar();\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const gridRef = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    const el = scrollRef.current;\n    if (!el) return;\n    const hour = Math.max(0, Math.min(23, scrollToHour));\n    el.scrollTop = (hour / 24) * el.scrollHeight;\n  }, [scrollToHour]);\n\n  const nowDate = new Date(nowMs);\n\n  return (\n    <div className={cn(\"flex flex-col\", className)}>\n      {/* column headers */}\n      <div className=\"flex border-b border-border\">\n        <div className=\"w-14 shrink-0\" />\n        {columns.map((day) => (\n          <div\n            key={day.toISOString()}\n            className=\"flex-1 border-l border-border px-2 py-1.5 text-center\"\n          >\n            <div className=\"text-xs text-muted-foreground\">{format(day, \"EEE\")}</div>\n            <div\n              className={cn(\n                \"text-sm font-semibold tabular-nums\",\n                isSameDay(day, nowDate) ? \"text-primary\" : \"text-foreground\",\n              )}\n            >\n              {format(day, \"d\")}\n            </div>\n          </div>\n        ))}\n      </div>\n\n      {/* all-day band */}\n      <AllDayBand columns={columns} />\n\n      {/* scrollable hour grid */}\n      <div ref={scrollRef} className=\"flex max-h-128 overflow-y-auto\">\n        <TimeGutter />\n        <div ref={gridRef} className=\"flex flex-1\">\n          {columns.map((day) => (\n            <DayColumn\n              key={day.toISOString()}\n              day={day}\n              columns={columns}\n              gridRef={gridRef}\n            />\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/parts/calendar-time-grid.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/parts/calendar-week-view.tsx",
      "content": "\"use client\";\n\nimport { useCalendar } from \"../hooks/use-calendar-context\";\nimport { weekColumns } from \"../lib/date-range\";\nimport { TimeGrid } from \"./calendar-time-grid\";\n\n/** Week view (Tier B) — the 7-day time-grid. */\nexport function CalendarWeekView({ className }: { className?: string }) {\n  const { focusDate, weekStartsOn } = useCalendar();\n  return <TimeGrid columns={weekColumns(focusDate, weekStartsOn)} className={className} />;\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/parts/calendar-week-view.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/parts/calendar-day-view.tsx",
      "content": "\"use client\";\n\nimport { startOfDay } from \"date-fns\";\nimport { useCalendar } from \"../hooks/use-calendar-context\";\nimport { TimeGrid } from \"./calendar-time-grid\";\n\n/** Day view (Tier B) — the single-column time-grid. */\nexport function CalendarDayView({ className }: { className?: string }) {\n  const { focusDate } = useCalendar();\n  return <TimeGrid columns={[startOfDay(focusDate)]} className={className} />;\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/parts/calendar-day-view.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/parts/calendar-agenda-view.tsx",
      "content": "\"use client\";\n\nimport { Fragment, forwardRef } from \"react\";\nimport type { ComponentPropsWithoutRef } from \"react\";\nimport { format, isSameDay } from \"date-fns\";\nimport { Flag } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { cn } from \"@/lib/utils\";\nimport { useCalendar } from \"../hooks/use-calendar-context\";\nimport { useCalendarEditOptional } from \"../hooks/use-calendar-edit-extension\";\nimport { agendaDays } from \"../lib/date-range\";\nimport { occurrencesOnDay } from \"../lib/segments\";\nimport type { CalendarOccurrence, TaskStatusOption } from \"../types\";\n\nfunction timeLabel(occ: CalendarOccurrence): string {\n  // Milestones are precise instants (deadlines) and carry allDay=true, so check\n  // them BEFORE the all-day branch — otherwise they'd mislabel as \"All day\" and\n  // lose their time. A ◇ cue mirrors the diamond glyph the chips use.\n  if (occ.kind === \"milestone\") return `◇ ${format(new Date(occ.startMs), \"p\")}`;\n  if (occ.allDay) return \"All day\";\n  return format(new Date(occ.startMs), \"p\");\n}\n\nfunction initials(name: string): string {\n  const out = name\n    .trim()\n    .split(/\\s+/)\n    .filter(Boolean)\n    .slice(0, 2)\n    .map((w) => w[0]?.toUpperCase() ?? \"\")\n    .join(\"\");\n  return out || \"?\"; // never an empty avatar fallback (whitespace-only name)\n}\n\n/** One agenda row (Tier C). Spreads `...rest` so the context menu can inject\n *  `onContextMenu` (parity with the other event primitives). */\nexport const AgendaRow = forwardRef<\n  HTMLButtonElement,\n  {\n    occ: CalendarOccurrence;\n    selected?: boolean;\n    statusOptions?: TaskStatusOption[];\n  } & Omit<ComponentPropsWithoutRef<\"button\">, \"type\">\n>(function AgendaRow({ occ, selected, statusOptions, className, ...rest }, ref) {\n  const status = statusOptions?.find((o) => o.value === occ.item.status);\n  const person = occ.item.targetPerson;\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      {...rest}\n      data-selected={selected || undefined}\n      data-occ-id={occ.id}\n      className={cn(\n        \"flex w-full items-center gap-3 rounded-md px-2 py-2 text-left hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring data-[selected=true]:bg-muted\",\n        occ.inactive && \"opacity-50\",\n        className,\n      )}\n    >\n      <span className=\"w-20 shrink-0 text-xs tabular-nums text-muted-foreground\">\n        {timeLabel(occ)}\n      </span>\n      <span\n        className=\"size-2.5 shrink-0 rounded-full\"\n        style={{ backgroundColor: occ.color.fill }}\n        aria-hidden\n      />\n      {occ.flagColor ? (\n        <Flag\n          className=\"size-3 shrink-0 fill-current\"\n          style={{ color: occ.flagColor }}\n          aria-label=\"High priority\"\n        />\n      ) : null}\n      <span className=\"flex-1 truncate text-sm text-foreground\">\n        {occ.item.name}\n      </span>\n      {status ? (\n        <Badge variant={status.variant ?? \"secondary\"} className=\"shrink-0\">\n          {status.label}\n        </Badge>\n      ) : null}\n      {person ? (\n        <Avatar className=\"size-6 shrink-0\">\n          {person.avatar ? <AvatarImage src={person.avatar} alt={person.name} /> : null}\n          <AvatarFallback className=\"text-[0.6rem]\">\n            {initials(person.name)}\n          </AvatarFallback>\n        </Avatar>\n      ) : null}\n    </button>\n  );\n});\n\n/** Agenda view (Tier B). Day-grouped chronological list over `agendaRangeDays`. */\nexport function CalendarAgendaView({ className }: { className?: string }) {\n  const {\n    focusDate,\n    agendaRangeDays,\n    occurrences,\n    nowMs,\n    statusOptions,\n    selectedId,\n    select,\n    onTaskClick,\n  } = useCalendar();\n  const edit = useCalendarEditOptional();\n\n  const days = agendaDays(focusDate, agendaRangeDays);\n  const nowDate = new Date(nowMs);\n\n  const groups = days\n    .map((day) => ({\n      day,\n      items: occurrencesOnDay(occurrences, day).sort(\n        (a, b) =>\n          Number(b.allDay) - Number(a.allDay) || a.startMs - b.startMs,\n      ),\n    }))\n    .filter((g) => g.items.length > 0);\n\n  if (groups.length === 0) {\n    return (\n      <div\n        className={cn(\n          \"flex min-h-40 items-center justify-center p-8 text-sm text-muted-foreground\",\n          className,\n        )}\n      >\n        No events in the next {agendaRangeDays} days.\n      </div>\n    );\n  }\n\n  return (\n    <div className={cn(\"flex flex-col gap-4 overflow-y-auto p-3\", className)}>\n      {groups.map(({ day, items }) => (\n        <section key={day.toISOString()} className=\"flex flex-col gap-1\">\n          <h3\n            className={cn(\n              \"sticky top-0 z-10 bg-card px-2 py-1 text-xs font-semibold uppercase tracking-wide\",\n              isSameDay(day, nowDate) ? \"text-primary\" : \"text-muted-foreground\",\n            )}\n          >\n            {format(day, \"EEEE, MMMM d\")}\n            {isSameDay(day, nowDate) ? \" · Today\" : \"\"}\n          </h3>\n          {items.map((occ) => {\n            const row = (\n              <AgendaRow\n                occ={occ}\n                selected={selectedId === occ.id}\n                statusOptions={statusOptions}\n                onClick={() => {\n                  select(occ.id);\n                  onTaskClick?.(occ.item);\n                }}\n              />\n            );\n            return edit ? (\n              <edit.components.EventContextMenu key={occ.id} item={occ.item}>\n                {row}\n              </edit.components.EventContextMenu>\n            ) : (\n              <Fragment key={occ.id}>{row}</Fragment>\n            );\n          })}\n        </section>\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/parts/calendar-agenda-view.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/parts/calendar-mini-nav.tsx",
      "content": "\"use client\";\n\nimport { Calendar } from \"@/components/ui/calendar\";\nimport { cn } from \"@/lib/utils\";\nimport { useCalendar } from \"../hooks/use-calendar-context\";\n\n/**\n * Jump-to-date mini month (Tier B), built on the shadcn `calendar` primitive\n * (react-day-picker). The ONLY place the date-picker is used — the main grids\n * are bespoke.\n */\nexport function CalendarMiniNav({ className }: { className?: string }) {\n  const { focusDate, goToDate, weekStartsOn } = useCalendar();\n  return (\n    <Calendar\n      mode=\"single\"\n      selected={focusDate}\n      month={focusDate}\n      onMonthChange={goToDate}\n      onSelect={(d) => {\n        if (d) goToDate(d);\n      }}\n      weekStartsOn={weekStartsOn}\n      className={cn(\"shrink-0\", className)}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/parts/calendar-mini-nav.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/parts/calendar-event.tsx",
      "content": "\"use client\";\n\nimport { cloneElement, forwardRef, useEffect, useId, useState } from \"react\";\nimport type {\n  ComponentPropsWithoutRef,\n  CSSProperties,\n  ReactElement,\n  ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { format } from \"date-fns\";\nimport { Flag } from \"lucide-react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { cn } from \"@/lib/utils\";\nimport type { CalendarOccurrence, TaskStatusOption } from \"../types\";\n\n/** A small solid priority flag (color = the item's priority color). */\nfunction PriorityFlag({ color }: { color?: string }) {\n  if (!color) return null;\n  return (\n    <Flag\n      className=\"size-3 shrink-0 fill-current\"\n      style={{ color }}\n      aria-label=\"High priority\"\n    />\n  );\n}\n\n/**\n * Inline CSS var carrying the resolved accent. The tint / border / text / ring\n * are Tailwind classes referencing this var; only the var (and time-block\n * geometry) is inline, so there's no inline-vs-class specificity conflict.\n */\nfunction accentStyle(\n  occ: CalendarOccurrence,\n  extra?: CSSProperties,\n): CSSProperties {\n  return {\n    ...extra,\n    [\"--cal-accent\" as string]: occ.color.fill,\n  } as CSSProperties;\n}\n\nconst SURFACE =\n  \"bg-[color-mix(in_oklch,var(--cal-accent)_16%,transparent)] text-(--cal-accent) hover:bg-[color-mix(in_oklch,var(--cal-accent)_30%,transparent)]\";\nconst FOCUS =\n  \"focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--cal-accent)\";\nconst SELECTED =\n  \"data-[selected=true]:ring-2 data-[selected=true]:ring-(--cal-accent)\";\n\nfunction compactTime(ms: number): string {\n  const d = new Date(ms);\n  return d.getMinutes() === 0 ? format(d, \"h a\") : format(d, \"h:mm a\");\n}\n\n/** Build the native-title summary (the lightweight default tooltip). */\nexport function eventTitle(occ: CalendarOccurrence): string {\n  const name = occ.item.name;\n  if (occ.invalid) return `${name} — (unscheduled)`;\n  if (occ.kind === \"milestone\")\n    return `${name} — ${format(new Date(occ.startMs), \"PP\")}`;\n  if (occ.allDay)\n    return `${name} — ${format(new Date(occ.startMs), \"PP\")} (all day)`;\n  return `${name} — ${compactTime(occ.startMs)}–${compactTime(occ.endMs)}`;\n}\n\n/** Wrap an event trigger in a rich hover/focus tooltip when a `renderTooltip`\n *  node is given. Local implementation (F-cross-13 path-b, v0.2.5): shadcn\n *  `TooltipTrigger` renders a native <button> in BOTH backends and `asChild`\n *  is Radix-only — wrapping the chip (itself a <button>) is a compile error on\n *  Base UI and invalid button-in-button nesting besides. So: clone hover/focus\n *  handlers onto the chip and portal a fixed-position card to <body> (kanban\n *  drag-overlay precedent). No wrapper box → absolutely-positioned time blocks\n *  keep their day-column geometry; the portal dodges grid clipping. */\nexport function EventHoverWrap({\n  tooltip,\n  children,\n}: {\n  tooltip?: ReactNode;\n  children: ReactElement;\n}) {\n  const id = useId();\n  const [pos, setPos] = useState<{ x: number; y: number; below: boolean } | null>(\n    null,\n  );\n  // The fixed-position card can't track its anchor — hide on any scroll\n  // (capture phase catches the inner grid's scroller) like Radix's dismiss.\n  useEffect(() => {\n    if (!pos) return;\n    const dismiss = () => setPos(null);\n    window.addEventListener(\"scroll\", dismiss, { capture: true, passive: true });\n    return () => window.removeEventListener(\"scroll\", dismiss, { capture: true });\n  }, [pos]);\n  if (!tooltip) return children;\n\n  const child = children as ReactElement<ComponentPropsWithoutRef<\"button\">>;\n  const props = child.props;\n  const show = (e: { currentTarget: Element }) => {\n    const r = e.currentTarget.getBoundingClientRect();\n    // Near the viewport top there's no room above — flip below (Radix parity).\n    const below = r.top < 120;\n    setPos({\n      x: r.left + r.width / 2,\n      y: below ? r.bottom + 6 : r.top - 6,\n      below,\n    });\n  };\n  const hide = () => setPos(null);\n  return (\n    <>\n      {cloneElement(child, {\n        \"aria-describedby\": pos ? id : props[\"aria-describedby\"],\n        onPointerEnter: (e) => {\n          props.onPointerEnter?.(e);\n          // Radix parity: no hover tooltips for touch (tap would flash one).\n          if (e.pointerType !== \"touch\") show(e);\n        },\n        onPointerLeave: (e) => {\n          props.onPointerLeave?.(e);\n          hide();\n        },\n        // Radix parity: keyboard focus shows; click-focus + presses dismiss.\n        onFocus: (e) => {\n          props.onFocus?.(e);\n          if (e.currentTarget.matches(\":focus-visible\")) show(e);\n        },\n        onBlur: (e) => {\n          props.onBlur?.(e);\n          hide();\n        },\n        onPointerDown: (e) => {\n          props.onPointerDown?.(e);\n          hide();\n        },\n        onKeyDown: (e) => {\n          props.onKeyDown?.(e);\n          if (e.key === \"Escape\") hide(); // WAI-ARIA tooltip dismiss\n        },\n      })}\n      {pos\n        ? createPortal(\n            <span\n              id={id}\n              role=\"tooltip\"\n              style={{ left: pos.x, top: pos.y }}\n              className={cn(\n                \"pointer-events-none fixed z-50 max-w-xs -translate-x-1/2 rounded-md border border-border bg-popover text-popover-foreground shadow-md\",\n                !pos.below && \"-translate-y-full\",\n              )}\n            >\n              {tooltip}\n            </span>,\n            document.body,\n          )\n        : null}\n    </>\n  );\n}\n\ntype ChipProps = {\n  occ: CalendarOccurrence;\n  selected?: boolean;\n  className?: string;\n} & Omit<ComponentPropsWithoutRef<\"button\">, \"style\" | \"title\" | \"type\" | \"children\">;\n\n/** Month single-day / timed chip (Tier C). Spreads `...rest` so wrappers can\n *  inject handlers/aria (EventHoverWrap clones hover + focus props on). The\n *  context menu injects nothing: right-click bubbles from this <button> to its\n *  box-less `ContextMenuTrigger` span (F-cross-13 path-b, v0.2.5). */\nexport const CalendarEventChip = forwardRef<HTMLButtonElement, ChipProps>(\n  function CalendarEventChip({ occ, selected, className, ...rest }, ref) {\n    const milestone = occ.kind === \"milestone\";\n    return (\n      <button\n        ref={ref}\n        type=\"button\"\n        {...rest}\n        title={eventTitle(occ)}\n        data-selected={selected || undefined}\n        data-occ-id={occ.id}\n        style={accentStyle(occ)}\n        className={cn(\n          \"flex w-full items-center gap-1 truncate rounded-sm border-l-2 border-(--cal-accent) px-1.5 py-0.5 text-left text-xs leading-tight\",\n          SURFACE,\n          FOCUS,\n          SELECTED,\n          occ.inactive && \"opacity-50\",\n          className,\n        )}\n      >\n        <PriorityFlag color={occ.flagColor} />\n        {milestone ? (\n          <span\n            className=\"size-1.5 shrink-0 rotate-45 bg-(--cal-accent)\"\n            aria-hidden\n          />\n        ) : !occ.allDay ? (\n          <span className=\"shrink-0 tabular-nums opacity-80\">\n            {compactTime(occ.startMs)}\n          </span>\n        ) : null}\n        <span className=\"truncate\">{occ.item.name}</span>\n      </button>\n    );\n  },\n);\n\ntype BarProps = ChipProps & {\n  continuesLeft?: boolean;\n  continuesRight?: boolean;\n};\n\n/** Month multi-day spanning bar segment (Tier C). */\nexport const CalendarEventBar = forwardRef<HTMLButtonElement, BarProps>(\n  function CalendarEventBar(\n    { occ, selected, continuesLeft, continuesRight, className, ...rest },\n    ref,\n  ) {\n    return (\n      <button\n        ref={ref}\n        type=\"button\"\n        {...rest}\n        title={eventTitle(occ)}\n        data-selected={selected || undefined}\n        data-occ-id={occ.id}\n        style={accentStyle(occ)}\n        className={cn(\n          \"flex h-5 w-full items-center truncate px-1.5 text-left text-xs font-medium leading-none\",\n          SURFACE,\n          FOCUS,\n          SELECTED,\n          continuesLeft\n            ? \"rounded-l-none\"\n            : \"rounded-l-sm border-l-2 border-(--cal-accent)\",\n          continuesRight ? \"rounded-r-none\" : \"rounded-r-sm\",\n          occ.inactive && \"opacity-50\",\n          className,\n        )}\n      >\n        {!continuesLeft ? (\n          <span className=\"mr-1 inline-flex shrink-0\">\n            <PriorityFlag color={occ.flagColor} />\n          </span>\n        ) : null}\n        <span className=\"truncate\">\n          {continuesLeft ? \"◀ \" : \"\"}\n          {occ.item.name}\n          {continuesRight ? \" ▶\" : \"\"}\n        </span>\n      </button>\n    );\n  },\n);\n\ntype TimeBlockProps = ChipProps & {\n  /** top / height as 0..1 fractions of the day; left / width as 0..1 of the column. */\n  top: number;\n  height: number;\n  left: number;\n  width: number;\n};\n\n/** Week/Day positioned timed block (Tier C). */\nexport const CalendarTimeBlock = forwardRef<HTMLButtonElement, TimeBlockProps>(\n  function CalendarTimeBlock(\n    { occ, selected, top, height, left, width, className, ...rest },\n    ref,\n  ) {\n    const milestone = occ.kind === \"milestone\";\n    return (\n      <button\n        ref={ref}\n        type=\"button\"\n        {...rest}\n        title={eventTitle(occ)}\n        data-selected={selected || undefined}\n        data-occ-id={occ.id}\n        style={accentStyle(occ, {\n          top: `${top * 100}%`,\n          height: milestone ? \"0.5rem\" : `${height * 100}%`,\n          left: `calc(${left * 100}% + 1px)`,\n          width: `calc(${width * 100}% - 2px)`,\n        })}\n        className={cn(\n          \"absolute z-10 flex flex-col overflow-hidden rounded-sm border-l-2 border-(--cal-accent) px-1.5 py-0.5 text-left text-xs leading-tight\",\n          SURFACE,\n          FOCUS,\n          \"hover:z-20 focus-visible:z-20 data-[selected=true]:z-20\",\n          SELECTED,\n          occ.inactive && \"opacity-50\",\n          className,\n        )}\n      >\n        <span className=\"flex items-center gap-1\">\n          <PriorityFlag color={occ.flagColor} />\n          <span className=\"truncate font-medium\">{occ.item.name}</span>\n        </span>\n        {!milestone && height > 0.04 ? (\n          <span className=\"truncate tabular-nums opacity-75\">\n            {compactTime(occ.startMs)}–{compactTime(occ.endMs)}\n          </span>\n        ) : null}\n      </button>\n    );\n  },\n);\n\n/** A horizontal \"now\" line crossing a day column (Tier C). */\nexport function NowIndicator({\n  topFraction,\n  className,\n}: {\n  topFraction: number;\n  className?: string;\n}) {\n  return (\n    <div\n      aria-hidden\n      style={{ top: `${topFraction * 100}%` }}\n      className={cn(\n        \"pointer-events-none absolute inset-x-0 z-30 flex items-center\",\n        className,\n      )}\n    >\n      <span className=\"size-2 -translate-x-1/2 rounded-full bg-destructive\" />\n      <span className=\"h-px w-full bg-destructive\" />\n    </div>\n  );\n}\n\n/** The default lightweight tooltip content (Tier C). Exported for composition. */\nexport function EventTooltip({\n  occ,\n  statusOptions,\n}: {\n  occ: CalendarOccurrence;\n  statusOptions?: TaskStatusOption[];\n}) {\n  const status = statusOptions?.find((o) => o.value === occ.item.status);\n  return (\n    <div className=\"flex flex-col gap-1 p-3\">\n      <span className=\"text-sm font-semibold text-foreground\">\n        {occ.item.name}\n      </span>\n      <span className=\"text-xs text-muted-foreground\">{eventTitle(occ)}</span>\n      {status ? (\n        <Badge variant={status.variant ?? \"secondary\"} className=\"w-fit\">\n          {status.label}\n        </Badge>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/parts/calendar-event.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/parts/event-tooltip-full.tsx",
      "content": "\"use client\";\n\nimport { lazy, Suspense } from \"react\";\nimport type { TaskColorRamp, TaskItem, TaskStatusOption } from \"../types\";\n\n/**\n * Lazy-embeds the full `<TaskCard>` (Tier C). Pass this to `renderTooltip`\n * for a rich hover card. `React.lazy` keeps task-card OUT of the bundle\n * unless a consumer actually wires this in — the default lightweight tooltip\n * pulls nothing. Same-category relative dynamic import (rewriter-safe).\n */\nconst LazyTaskCard = lazy(() =>\n  import(\"../../task-card\").then((m) => ({ default: m.TaskCard })),\n);\n\nexport function CalendarFullCardTooltip({\n  item,\n  statusOptions,\n  colorRamp,\n}: {\n  item: TaskItem;\n  statusOptions?: TaskStatusOption[];\n  colorRamp?: TaskColorRamp;\n}) {\n  return (\n    <div className=\"w-72\">\n      <Suspense\n        fallback={\n          <div className=\"p-3 text-xs text-muted-foreground\">Loading…</div>\n        }\n      >\n        <LazyTaskCard\n          value={item}\n          statusOptions={statusOptions}\n          colorRamp={colorRamp}\n        />\n      </Suspense>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/parts/event-tooltip-full.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/parts/calendar-event-inspector.tsx",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { format } from \"date-fns\";\nimport { Pencil, Trash2, X } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { cn } from \"@/lib/utils\";\nimport { useCalendar } from \"../hooks/use-calendar-context\";\nimport { useCalendarEditOptional } from \"../hooks/use-calendar-edit-extension\";\nimport { coveredDays } from \"../lib/segments\";\nimport type { CalendarOccurrence } from \"../types\";\n\n/**\n * Selected-event inspector (Tier B). Shows a read-only preview of the selected\n * occurrence; when the editing extension is wired, an Edit button reveals the\n * shared `edit.components.EventEditorPanel` (the full lazy `<TaskCard editable>`\n * — title, status, priority, dates, description) whose edits splice back via\n * `applyEditedSubtree`, plus Delete. Lives inside `EventCalendarRoot` (reads\n * context). Place it beside the views for a persistent details panel, or omit\n * it (the editor also surfaces via the `EditOverlays` component the assembly\n * mounts when there's no inspector).\n */\n\nfunction whenLabel(occ: CalendarOccurrence): string {\n  const s = new Date(occ.startMs);\n  if (occ.kind === \"milestone\") return format(s, \"PPP 'at' p\");\n  if (occ.allDay) {\n    const last = new Date(coveredDays(occ).lastMs);\n    return format(s, \"yyyy-MM-dd\") === format(last, \"yyyy-MM-dd\")\n      ? `${format(s, \"PPP\")} (all day)`\n      : `${format(s, \"PPP\")} – ${format(last, \"PPP\")}`;\n  }\n  return `${format(s, \"PPP\")} · ${format(s, \"p\")} – ${format(new Date(occ.endMs), \"p\")}`;\n}\n\nexport function CalendarEventInspector({ className }: { className?: string }) {\n  const { occurrences, selectedId, select, statusOptions, priorityOptions, labelOptions } =\n    useCalendar();\n  const edit = useCalendarEditOptional();\n\n  const occ = useMemo(\n    () => occurrences.find((o) => o.id === selectedId) ?? null,\n    [occurrences, selectedId],\n  );\n\n  if (!occ) {\n    return (\n      <div\n        className={cn(\n          \"flex flex-col items-center justify-center gap-1 p-6 text-center\",\n          className,\n        )}\n      >\n        <p className=\"text-sm font-medium text-muted-foreground\">\n          No event selected\n        </p>\n        <p className=\"text-xs text-muted-foreground\">\n          Click an event to see its details{edit ? \" and edit it\" : \"\"}.\n        </p>\n      </div>\n    );\n  }\n\n  const item = occ.item;\n  const editingThis = edit?.editingId === item.id;\n  const status = statusOptions?.find((o) => o.value === item.status);\n  const priority = priorityOptions?.find((o) => o.value === item.priority);\n  const canEdit = !!edit && edit.can(\"editDetails\", item);\n  const canDelete = !!edit && edit.can(\"delete\", item);\n\n  return (\n    <div className={cn(\"flex flex-col gap-3 p-3\", className)}>\n      <div className=\"flex items-center justify-between gap-2\">\n        <span className=\"inline-flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n          <span\n            className=\"size-2 rounded-full\"\n            style={{ background: occ.color.fill }}\n            aria-hidden\n          />\n          Event details\n        </span>\n        <button\n          type=\"button\"\n          aria-label=\"Clear selection\"\n          onClick={() => select(null)}\n          className=\"rounded-sm p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground\"\n        >\n          <X className=\"size-4\" />\n        </button>\n      </div>\n\n      {editingThis && edit ? (\n        <edit.components.EventEditorPanel\n          item={item}\n          statusOptions={statusOptions}\n          priorityOptions={priorityOptions}\n          labelOptions={labelOptions}\n          permissions={edit.permissions}\n          onChange={edit.applyEditedSubtree}\n          onDone={edit.closeEditor}\n        />\n      ) : (\n        <>\n          <div className=\"space-y-1\">\n            <h3 className=\"text-base font-semibold leading-tight text-foreground\">\n              {item.name}\n            </h3>\n            <p className=\"text-xs text-muted-foreground\">{whenLabel(occ)}</p>\n          </div>\n\n          <div className=\"flex flex-wrap gap-1.5\">\n            {status ? (\n              <Badge variant={status.variant ?? \"secondary\"}>\n                {status.label}\n              </Badge>\n            ) : null}\n            {priority ? (\n              <Badge\n                variant=\"outline\"\n                style={\n                  priority.color\n                    ? { color: priority.color, borderColor: priority.color }\n                    : undefined\n                }\n              >\n                {priority.label}\n              </Badge>\n            ) : null}\n            {occ.overdue ? <Badge variant=\"destructive\">Overdue</Badge> : null}\n          </div>\n\n          {item.description ? (\n            <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-foreground/80\">\n              {item.description}\n            </p>\n          ) : null}\n\n          {edit ? (\n            <div className=\"mt-1 flex gap-2\">\n              <Button\n                size=\"sm\"\n                className=\"gap-1.5\"\n                disabled={!canEdit}\n                onClick={() => edit.openEditor(item.id)}\n              >\n                <Pencil className=\"size-3.5\" /> Edit\n              </Button>\n              <Button\n                size=\"sm\"\n                variant=\"outline\"\n                className=\"gap-1.5\"\n                disabled={!canDelete}\n                onClick={() => {\n                  edit.deleteItem(item.id);\n                  select(null);\n                }}\n              >\n                <Trash2 className=\"size-3.5\" /> Delete\n              </Button>\n            </div>\n          ) : null}\n        </>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/parts/calendar-event-inspector.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/parts/calendar-skeleton.tsx",
      "content": "\"use client\";\n\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { cn } from \"@/lib/utils\";\n\n/** Loading skeleton (Tier C). A month-grid placeholder. */\nexport function CalendarSkeleton({ className }: { className?: string }) {\n  return (\n    <div className={cn(\"flex flex-col gap-3 p-3\", className)}>\n      <div className=\"flex items-center justify-between\">\n        <Skeleton className=\"h-8 w-44\" />\n        <Skeleton className=\"h-8 w-52\" />\n      </div>\n      <div className=\"grid grid-cols-7 gap-1\">\n        {/* 42 = 6 weeks × 7 — a month grid can return 6 rows, so reserve the max\n            to avoid a load→loaded layout jump (monthGrid is 5 or 6 weeks). */}\n        {Array.from({ length: 42 }).map((_, i) => (\n          <Skeleton key={i} className=\"h-20 w-full\" />\n        ))}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/parts/calendar-skeleton.tsx"
    }
  ],
  "categories": [
    "data",
    "task-management"
  ],
  "type": "registry:block"
}