{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slot-planner",
  "type": "registry:ui",
  "title": "SlotPlanner",
  "description": "Bookable time-slot inventory family: manage-mode week/day planner with editor dialog, recurrence and per-occurrence overrides, declarative constraints with cap meters, copy-day/copy-week/clear-day batch operations, per-surface custom renderers, a headless useSlotPlanner hook, configurable taxonomy wording, Motion choreography with reduced-motion collapse, and a book-mode SlotPicker that projects occurrences into the viewer's time zone.",
  "dependencies": [
    "motion@^12.43.0",
    "@internationalized/date@^3.12.4"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://components.dethink.co.uk/r/dethink-base.json",
    "https://components.dethink.co.uk/r/button.json",
    "https://components.dethink.co.uk/r/checkbox.json",
    "https://components.dethink.co.uk/r/dialog.json",
    "https://components.dethink.co.uk/r/dropdown-menu.json",
    "https://components.dethink.co.uk/r/form-field.json",
    "https://components.dethink.co.uk/r/icon-button.json",
    "https://components.dethink.co.uk/r/input.json",
    "https://components.dethink.co.uk/r/number-input.json",
    "https://components.dethink.co.uk/r/radio-group.json",
    "https://components.dethink.co.uk/r/select.json",
    "https://components.dethink.co.uk/r/tag-input.json",
    "https://components.dethink.co.uk/r/textarea.json"
  ],
  "files": [
    {
      "path": "packages/components/src/components/slot-planner/slot-planner-calendar.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/slot-planner/slot-planner-calendar.tsx",
      "content": "import { useMemo } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Button } from \"../button\";\nimport type {\n  SlotPlannerSlotData,\n  SlotPlannerSlotPayload,\n  SlotPlannerTaxonomy,\n} from \"./slot-planner-contract\";\nimport {\n  CalendarIcon,\n  getConventionalSlotData,\n  PlusIcon,\n  slotPlannerStatusDotClasses,\n  toUtcDate,\n} from \"./slot-planner-dom-shared\";\nimport {\n  formatSlotPlannerCountTemplate,\n  type SlotPlannerOccurrence,\n} from \"./slot-planner-utils\";\nimport {\n  calendarMinimumMinutes,\n  getSlotPlannerCalendarEntries,\n} from \"./slot-planner-calendar-layout\";\n\nconst statusStyles = {\n  requestable: \"border-s-success bg-success/5\",\n  requested: \"border-s-warning bg-warning/5\",\n  booked: \"border-s-info bg-info/5\",\n  blocked: \"border-s-destructive bg-destructive/5\",\n  draft: \"border-s-muted-foreground bg-muted/40\",\n  expired: \"border-s-muted-foreground bg-muted/40\",\n  cancelled: \"border-s-muted-foreground bg-muted/40\",\n};\n\nexport function SlotPlannerCalendar<TData extends SlotPlannerSlotPayload>({\n  slots,\n  dates,\n  focusedDate,\n  today,\n  now,\n  timeZone,\n  locale,\n  taxonomy,\n  onSelectDay,\n  onOpenSlot,\n  onAdd,\n}: {\n  slots: SlotPlannerSlotData<TData>[];\n  dates: string[];\n  focusedDate: string;\n  today: string;\n  now: string;\n  timeZone: string;\n  locale: string;\n  taxonomy: SlotPlannerTaxonomy;\n  onSelectDay: (date: string) => void;\n  onOpenSlot: (occurrence: SlotPlannerOccurrence<TData>) => void;\n  onAdd: (date: string) => void;\n}) {\n  const entries = useMemo(\n    () => getSlotPlannerCalendarEntries(slots, dates, now, timeZone),\n    [slots, dates, now, timeZone],\n  );\n  const weekend = dates.indexOf(focusedDate) > 4;\n  const visibleDates = weekend ? dates.slice(5) : dates.slice(0, 5);\n  const visibleEntries = entries.filter((entry) =>\n    visibleDates.includes(entry.date),\n  );\n  const startHour = Math.floor(\n    Math.min(480, ...visibleEntries.map((entry) => entry.start)) / 60,\n  );\n  const endHour = Math.min(\n    24,\n    Math.ceil(\n      Math.max(\n        1080,\n        ...visibleEntries.map((entry) =>\n          Math.max(entry.end, entry.start + calendarMinimumMinutes),\n        ),\n      ) / 60,\n    ),\n  );\n  const hours = Array.from(\n    { length: endHour - startHour + 1 },\n    (_, i) => startHour + i,\n  );\n  const dateFormatter = new Intl.DateTimeFormat(locale, {\n    weekday: \"short\",\n    day: \"numeric\",\n    month: \"short\",\n    timeZone: \"UTC\",\n  });\n  const pixelsPerMinute = 1.2;\n  // Late short slots need room for their labels below midnight, without\n  // adding misleading next-day hours to the selected day's time axis.\n  const bottomPadding = visibleEntries.some(\n    (entry) => entry.start + calendarMinimumMinutes > 1440,\n  )\n    ? calendarMinimumMinutes * pixelsPerMinute\n    : 0;\n  const height = (endHour - startHour) * 60 * pixelsPerMinute + bottomPadding;\n  return (\n    <div data-slot=\"slot-planner-calendar\" className=\"min-w-0\">\n      <div className=\"mb-4 flex flex-wrap items-center justify-end gap-3\">\n        <div className=\"flex flex-wrap gap-2\">\n          {weekend ? (\n            <Button\n              size=\"sm\"\n              variant=\"outline\"\n              onClick={() => onSelectDay(dates[0]!)}\n            >\n              {taxonomy.weekView}\n            </Button>\n          ) : null}\n          {dates.slice(5).map((date) => (\n            <Button\n              key={date}\n              size=\"sm\"\n              variant={date === focusedDate ? \"soft\" : \"ghost\"}\n              onClick={() => onSelectDay(date)}\n            >\n              {`${dateFormatter.format(toUtcDate(date))} · ${formatSlotPlannerCountTemplate(taxonomy.calendarSlotCount, entries.filter((e) => e.date === date).length, {}, locale)}`}\n            </Button>\n          ))}\n        </div>\n      </div>\n      <div\n        role=\"region\"\n        aria-label={`${taxonomy.weekView} · ${timeZone}`}\n        // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- Scrollable regions need keyboard focus for scrolling.\n        tabIndex={0}\n        className=\"border-border bg-background focus-visible:outline-ring max-h-[56rem] overflow-auto rounded-lg border focus-visible:outline-2\"\n      >\n        <div\n          className=\"relative grid min-w-[36rem]\"\n          style={{\n            gridTemplateColumns: `3.25rem repeat(${visibleDates.length}, minmax(0, 1fr))`,\n          }}\n        >\n          <div className=\"bg-background border-border sticky top-0 z-20 border-b\" />\n          {visibleDates.map((date) => (\n            <button\n              key={date}\n              type=\"button\"\n              aria-pressed={date === focusedDate}\n              aria-current={date === today ? \"date\" : undefined}\n              onClick={() => onSelectDay(date)}\n              className={cn(\n                \"border-border bg-background focus-visible:outline-ring sticky top-0 z-20 min-h-20 border-s border-b p-3 text-start text-sm font-medium focus-visible:z-30 focus-visible:outline-2\",\n                date === focusedDate &&\n                  \"bg-muted text-primary shadow-[inset_0_-2px_var(--dt-color-primary)]\",\n              )}\n            >\n              {dateFormatter.format(toUtcDate(date))}\n              <span className=\"text-muted-foreground mt-1 block text-xs\">\n                {formatSlotPlannerCountTemplate(\n                  taxonomy.calendarSlotCount,\n                  entries.filter((e) => e.date === date).length,\n                  {},\n                  locale,\n                )}\n              </span>\n            </button>\n          ))}\n          <div\n            aria-hidden=\"true\"\n            className=\"text-muted-foreground relative text-[11px]\"\n            style={{ height }}\n          >\n            {hours.slice(0, -1).map((hour) => (\n              <span\n                key={hour}\n                className=\"absolute end-2 top-0\"\n                style={{ top: (hour - startHour) * 60 * pixelsPerMinute + 4 }}\n              >\n                {String(hour % 24).padStart(2, \"0\")}:00\n              </span>\n            ))}\n          </div>\n          {visibleDates.map((date) => {\n            const dayEntries = visibleEntries.filter(\n              (entry) => entry.date === date,\n            );\n            return (\n              <div\n                key={date}\n                role=\"group\"\n                aria-label={dateFormatter.format(toUtcDate(date))}\n                className={cn(\n                  \"border-border relative border-s\",\n                  date === focusedDate && \"bg-primary/5\",\n                )}\n                style={{ height }}\n              >\n                {hours.slice(0, -1).map((hour) => (\n                  <div\n                    key={hour}\n                    aria-hidden=\"true\"\n                    className=\"border-border/60 pointer-events-none absolute inset-x-0 border-t border-dashed\"\n                    style={{ top: (hour - startHour) * 60 * pixelsPerMinute }}\n                  />\n                ))}\n                {dayEntries.map((entry) => {\n                  const remaining = Math.max(\n                    0,\n                    entry.occurrence.capacity -\n                      entry.occurrence.bookedCount -\n                      entry.occurrence.requestedCount,\n                  );\n                  const { tags } = getConventionalSlotData(\n                    entry.occurrence.slot.data,\n                  );\n                  const blockHeight = Math.max(\n                    calendarMinimumMinutes * pixelsPerMinute - 4,\n                    (entry.end - entry.start) * pixelsPerMinute - 4,\n                  );\n                  return (\n                    <button\n                      key={`${entry.occurrence.slotId}:${entry.occurrence.occurrenceDate}:${entry.date}`}\n                      type=\"button\"\n                      data-slot=\"slot-planner-calendar-event\"\n                      data-status={entry.occurrence.status}\n                      title={`${entry.timeLabel} · ${taxonomy.statusLabels[entry.occurrence.status]}`}\n                      aria-label={`${dateFormatter.format(toUtcDate(date))}, ${entry.timeLabel}, ${taxonomy.statusLabels[entry.occurrence.status]}, ${formatSlotPlannerCountTemplate(taxonomy.remainingSeats, remaining, { remaining }, locale)}`}\n                      onClick={() => onOpenSlot(entry.occurrence)}\n                      className={cn(\n                        \"border-border focus-visible:outline-ring absolute flex min-w-0 flex-col gap-1 overflow-hidden rounded-md border border-s-[3px] p-2 text-start text-xs shadow-sm hover:brightness-95 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2\",\n                        statusStyles[entry.occurrence.status],\n                      )}\n                      style={{\n                        top:\n                          (entry.start - startHour * 60) * pixelsPerMinute + 2,\n                        height: blockHeight,\n                        insetInlineStart: `calc(${(entry.lane * 100) / entry.lanes}% + 4px)`,\n                        width: `calc(${100 / entry.lanes}% - 8px)`,\n                      }}\n                    >\n                      <span className=\"w-full truncate text-sm font-semibold tabular-nums\">\n                        {entry.timeLabel}\n                      </span>\n                      <span className=\"flex min-w-0 items-center gap-1.5\">\n                        <span\n                          aria-hidden=\"true\"\n                          className={cn(\n                            \"size-1.5 shrink-0 rounded-full\",\n                            slotPlannerStatusDotClasses[\n                              entry.occurrence.status\n                            ],\n                          )}\n                        />\n                        <span className=\"truncate\">\n                          {taxonomy.statusLabels[entry.occurrence.status]}\n                        </span>\n                      </span>\n                      {blockHeight >= 86 ? (\n                        <span className=\"text-muted-foreground truncate\">\n                          {formatSlotPlannerCountTemplate(\n                            taxonomy.remainingSeats,\n                            remaining,\n                            { remaining },\n                            locale,\n                          )}\n                        </span>\n                      ) : null}\n                      {tags.length ? (\n                        <span className=\"text-muted-foreground truncate\">\n                          {tags.join(\" · \")}\n                        </span>\n                      ) : null}\n                    </button>\n                  );\n                })}\n                {dayEntries.length === 0 ? (\n                  <div className=\"border-border bg-background text-muted-foreground absolute inset-x-2 top-40 flex flex-col items-center gap-3 rounded-md border border-dashed p-3 text-center text-sm\">\n                    <CalendarIcon />\n                    <span>{taxonomy.emptyDay}</span>\n                    {date >= today ? (\n                      <Button\n                        size=\"sm\"\n                        variant=\"outline\"\n                        leftIcon={<PlusIcon />}\n                        onClick={() => onAdd(date)}\n                        aria-label={`${taxonomy.addSlot} · ${dateFormatter.format(toUtcDate(date))}`}\n                      >\n                        {taxonomy.calendarAdd}\n                      </Button>\n                    ) : null}\n                  </div>\n                ) : null}\n              </div>\n            );\n          })}\n        </div>\n      </div>\n      <p className=\"text-muted-foreground mt-3 text-xs\">\n        {taxonomy.calendarHint}\n      </p>\n    </div>\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/slot-planner/slot-planner-calendar-layout.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/slot-planner/slot-planner-calendar-layout.ts",
      "content": "import { fromAbsolute, parseDate } from \"@internationalized/date\";\nimport type {\n  SlotPlannerSlotData,\n  SlotPlannerSlotPayload,\n} from \"./slot-planner-contract\";\nimport {\n  expandSlotOccurrences,\n  projectSlotPlannerOccurrenceToZone,\n  type SlotPlannerOccurrence,\n} from \"./slot-planner-utils\";\n\nexport interface CalendarEntry<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> {\n  occurrence: SlotPlannerOccurrence<TData>;\n  date: string;\n  start: number;\n  end: number;\n  timeLabel: string;\n  lane: number;\n  lanes: number;\n}\n\nexport const calendarMinimumMinutes = 80;\n\n/** Calendar geometry is in display-zone wall-clock minutes; payload identities stay in the provider zone. */\nexport function getSlotPlannerCalendarEntries<\n  TData extends SlotPlannerSlotPayload,\n>(\n  slots: SlotPlannerSlotData<TData>[],\n  dates: string[],\n  now: string,\n  timeZone: string,\n): CalendarEntry<TData>[] {\n  if (!dates.length) return [];\n  const first = dates[0]!;\n  const last = dates[dates.length - 1]!;\n  const padding =\n    Math.ceil(\n      Math.max(0, ...slots.map((slot) => slot.durationMinutes)) / 1440,\n    ) + 2;\n  const entries: CalendarEntry<TData>[] = [];\n  for (const slot of slots) {\n    for (const occurrence of expandSlotOccurrences(\n      slot,\n      parseDate(first).subtract({ days: padding }).toString(),\n      parseDate(last).add({ days: 2 }).toString(),\n      now,\n    )) {\n      const projected = projectSlotPlannerOccurrenceToZone(\n        occurrence,\n        timeZone,\n      );\n      const finish = fromAbsolute(\n        projected.startEpochMs + occurrence.durationMinutes * 60_000,\n        timeZone,\n      );\n      const endDate = finish.toString().slice(0, 10);\n      for (const date of dates) {\n        if (date < projected.viewerDate || date > endDate) continue;\n        const [hour = 0, minute = 0] = projected.viewerStartTime\n          .split(\":\")\n          .map(Number);\n        const start = date === projected.viewerDate ? hour * 60 + minute : 0;\n        const end = date === endDate ? finish.hour * 60 + finish.minute : 1440;\n        if (date === endDate && end === 0 && date !== projected.viewerDate)\n          continue;\n        entries.push({\n          occurrence,\n          date,\n          start,\n          end: Math.max(start + 1, end),\n          timeLabel: `${projected.viewerStartTime} – ${projected.viewerEndTime}${endDate !== projected.viewerDate ? ` (${endDate})` : \"\"}`,\n          lane: 0,\n          lanes: 1,\n        });\n      }\n    }\n  }\n  // Reserve enough room for time, status and capacity. Assign overlap lanes\n  // using that visual footprint, so short adjacent events cannot cover one another.\n  for (const date of dates) {\n    const day = entries\n      .filter((entry) => entry.date === date)\n      .sort(\n        (a, b) =>\n          a.start - b.start ||\n          a.occurrence.slotId.localeCompare(b.occurrence.slotId),\n      );\n    let cluster: CalendarEntry<TData>[] = [];\n    let laneEnds: number[] = [];\n    let clusterEnd = -1;\n    const finishCluster = () => {\n      for (const entry of cluster) entry.lanes = laneEnds.length;\n    };\n    for (const entry of day) {\n      if (entry.start >= clusterEnd) {\n        finishCluster();\n        cluster = [];\n        laneEnds = [];\n      }\n      const end = Math.max(entry.end, entry.start + calendarMinimumMinutes);\n      let lane = laneEnds.findIndex((value) => value <= entry.start);\n      if (lane < 0) lane = laneEnds.length;\n      laneEnds[lane] = end;\n      entry.lane = lane;\n      cluster.push(entry);\n      clusterEnd = Math.max(...laneEnds);\n    }\n    finishCluster();\n  }\n  return entries.sort(\n    (a, b) =>\n      a.date.localeCompare(b.date) ||\n      a.start - b.start ||\n      a.occurrence.slotId.localeCompare(b.occurrence.slotId),\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/slot-planner/slot-planner.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/slot-planner/slot-planner.tsx",
      "content": "import { fromAbsolute, parseDate } from \"@internationalized/date\";\nimport {\n  Fragment,\n  forwardRef,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ForwardedRef,\n  type HTMLAttributes,\n  type KeyboardEvent,\n  type ReactElement,\n  type ReactNode,\n  type RefAttributes,\n} from \"react\";\nimport { AnimatePresence, MotionConfig, useReducedMotion } from \"motion/react\";\nimport { cn } from \"../../utils/cn\";\nimport { Button, buttonClassNames } from \"../button\";\nimport { IconButton } from \"../icon-button\";\nimport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n} from \"../dropdown-menu\";\nimport { SlotPlannerCalendar } from \"./slot-planner-calendar\";\nimport {\n  slotPlannerOccurrenceStatuses,\n  type SlotPlannerBatchChangePayload,\n  type SlotPlannerConstraints,\n  type SlotPlannerCreatePayload,\n  type SlotPlannerDeleteOccurrencePayload,\n  type SlotPlannerDeleteSeriesPayload,\n  type SlotPlannerOccurrenceStatus,\n  type SlotPlannerSlotData,\n  type SlotPlannerSlotPayload,\n  type SlotPlannerTaxonomy,\n  type SlotPlannerTaxonomyInput,\n  type SlotPlannerUpdatePayload,\n  type SlotPlannerViolation,\n} from \"./slot-planner-contract\";\nimport type {\n  SlotPlannerEditorResult,\n  SlotPlannerEditorSeriesValues,\n} from \"./slot-planner-crud\";\nimport {\n  CalendarIcon,\n  ChevronIcon,\n  CopyIcon,\n  EditIcon,\n  getConventionalSlotData,\n  getSlotPlannerDirection,\n  PlusIcon,\n  slotPlannerChipClasses,\n  slotPlannerDayPanelClasses,\n  slotPlannerDayRailClasses,\n  slotPlannerDayTabClasses,\n  slotPlannerDayTabContentClasses,\n  slotPlannerRootClasses,\n  slotPlannerSaveErrorClasses,\n  slotPlannerSavePendingClasses,\n  slotPlannerSlotActionButtonClasses,\n  slotPlannerSlotCardClasses,\n  slotPlannerStatusBadgeClasses,\n  slotPlannerStatusDotClasses,\n  slotPlannerToolbarClasses,\n  slotPlannerWeekPanelContentClasses,\n  TrashIcon,\n  toUtcDate,\n  useSlotPlannerRailOrientation,\n} from \"./slot-planner-dom-shared\";\nimport {\n  SlotPlannerBatchConfirmDialog,\n  SlotPlannerCopyDayDialog,\n  SlotPlannerDeleteDialog,\n  SlotPlannerEditorDialog,\n} from \"./slot-planner-editor\";\nimport {\n  SlotPlannerCapMeterFill,\n  SlotPlannerDayTabContent,\n  SlotPlannerDayTabIndicator,\n  SlotPlannerSlotListItem,\n  SlotPlannerWeekSlide,\n} from \"./slot-planner-motion\";\nimport type {\n  SlotPlannerRenderers,\n  SlotPlannerTagRenderContext,\n} from \"./slot-planner-renderers\";\nimport {\n  formatSlotPlannerCountTemplate,\n  formatSlotPlannerTemplate,\n  type SlotPlannerOccurrence,\n} from \"./slot-planner-utils\";\nimport {\n  getSlotPlannerBatchKey,\n  getSlotPlannerCreateKey,\n  useSlotPlanner,\n  type SlotPlannerView,\n} from \"./use-slot-planner\";\n\nconst useIsomorphicLayoutEffect =\n  typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nexport interface SlotPlannerProps<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> extends Omit<HTMLAttributes<HTMLDivElement>, \"children\" | \"title\"> {\n  /** Controlled slot collection. */\n  slots?: SlotPlannerSlotData<TData>[];\n  /** Initial slot collection for uncontrolled usage. */\n  defaultSlots?: SlotPlannerSlotData<TData>[];\n  /** Controlled focused ISO date (`YYYY-MM-DD`). */\n  focusedDate?: string;\n  /** Initial focused ISO date for uncontrolled usage. Defaults to today. */\n  defaultFocusedDate?: string;\n  onFocusedDateChange?: (dateIso: string) => void;\n  onCreateSlot?: (\n    payload: SlotPlannerCreatePayload<TData>,\n  ) => void | Promise<void>;\n  onUpdateSlot?: (\n    payload: SlotPlannerUpdatePayload<TData>,\n  ) => void | Promise<void>;\n  onDeleteOccurrence?: (\n    payload: SlotPlannerDeleteOccurrencePayload,\n  ) => void | Promise<void>;\n  onDeleteSeries?: (\n    payload: SlotPlannerDeleteSeriesPayload,\n  ) => void | Promise<void>;\n  /**\n   * Declarative rules validated on editor saves and batch operations, and\n   * surfaced by the daily cap meter. Enforcement authority stays with the\n   * app; the planner blocks its own affordances only.\n   */\n  constraints?: SlotPlannerConstraints;\n  /** Fires for copy-day, copy-week, and clear-day batch operations. */\n  onBatchChange?: (\n    payload: SlotPlannerBatchChangePayload<TData>,\n  ) => void | Promise<void>;\n  /** Ids for slots created through the editor. Defaults to crypto.randomUUID. */\n  generateSlotId?: () => string;\n  taxonomy?: SlotPlannerTaxonomyInput;\n  /** Controlled view projection. Pair with `onViewChange` for the default switcher. */\n  view?: SlotPlannerView;\n  /** Initial view for uncontrolled usage. */\n  defaultView?: SlotPlannerView;\n  /** Wide week presentation. Agenda preserves the original renderer layout. */\n  weekLayout?: \"calendar\" | \"agenda\";\n  onViewChange?: (view: SlotPlannerView) => void;\n  /** ISO date-time treated as \"now\"; injectable for deterministic renders. */\n  now?: string;\n  /** IANA zone used for manage-mode \"today\", past-day state, and new slots. */\n  timeZone?: string;\n  loading?: boolean;\n  error?: ReactNode;\n  title?: ReactNode;\n  locale?: string;\n  /**\n   * Forces the reduced-motion rendering path. Defaults to the viewer's\n   * `prefers-reduced-motion` preference. When reduced, every animation\n   * collapses to an instant state change; all state stays communicated by\n   * text and data attributes either way.\n   */\n  reducedMotion?: boolean;\n  /**\n   * Optional render props, one per surface, each with a `renderDefault()`\n   * escape hatch. Structural invariants (day-rail tab semantics, the\n   * slot-card `<li>` wrapper, the live region, dialog focus containment,\n   * post-delete focus recovery) stay outside the render slots.\n   */\n  renderers?: SlotPlannerRenderers<TData>;\n}\n\nconst slotPlannerAddSlotButtonClasses = buttonClassNames({\n  className: \"self-stretch shadow-sm sm:self-start\",\n  size: \"sm\",\n  variant: \"solid\",\n});\n\nconst lockedOccurrenceStatuses = new Set<SlotPlannerOccurrenceStatus>([\n  \"booked\",\n  \"blocked\",\n  \"expired\",\n  \"cancelled\",\n]);\n\nfunction getWallClockEndTime(startTime: string, durationMinutes: number) {\n  const [hours = 0, minutes = 0] = startTime.split(\":\").map(Number);\n  const total = (hours * 60 + minutes + durationMinutes) % (24 * 60);\n  const endHours = String(Math.floor(total / 60)).padStart(2, \"0\");\n  const endMinutes = String(total % 60).padStart(2, \"0\");\n\n  return `${endHours}:${endMinutes}`;\n}\n\nfunction getSeriesEditorValues(\n  slot: SlotPlannerSlotData,\n): SlotPlannerEditorSeriesValues {\n  const { note, tags } = getConventionalSlotData(slot.data);\n\n  return {\n    startTime: slot.startTime,\n    durationMinutes: slot.durationMinutes,\n    capacity: slot.capacity ?? 1,\n    bufferBeforeMinutes: slot.bufferBeforeMinutes ?? 0,\n    bufferAfterMinutes: slot.bufferAfterMinutes ?? 0,\n    timeZone: slot.timeZone,\n    tags,\n    note,\n    recurrence: slot.recurrence?.frequency ?? \"none\",\n    recurrenceUntil: slot.recurrence?.until ?? \"\",\n  };\n}\n\nfunction getCreateEditorValues(\n  timeZone: string,\n  date: string,\n  now: string,\n  constraints?: SlotPlannerConstraints,\n): SlotPlannerEditorSeriesValues {\n  const earliest = fromAbsolute(\n    Date.parse(now) + (constraints?.minNoticeMinutes ?? 0) * 60_000,\n    timeZone,\n  );\n  const earliestDate = earliest.toString().slice(0, 10);\n  const minute =\n    earliestDate === date\n      ? Math.min(\n          1439,\n          Math.max(\n            540,\n            Math.ceil((earliest.hour * 60 + earliest.minute + 1) / 15) * 15,\n          ),\n        )\n      : 540;\n  return {\n    startTime: `${String(Math.floor(minute / 60)).padStart(2, \"0\")}:${String(minute % 60).padStart(2, \"0\")}`,\n    durationMinutes: 60,\n    capacity: 1,\n    bufferBeforeMinutes: 0,\n    bufferAfterMinutes: 0,\n    timeZone,\n    tags: [],\n    note: \"\",\n    recurrence: \"none\",\n    recurrenceUntil: \"\",\n  };\n}\n\n// Let the menu's focus scope restore its trigger before mounting a dialog's\n// focus scope. Otherwise the closing menu can move focus out of the dialog.\nfunction openAfterMenu(action: () => void) {\n  requestAnimationFrame(() => requestAnimationFrame(action));\n}\n\n/** Default content inside the structural slot list item, also available to custom renderers. */\nfunction SlotPlannerSlotCardContent<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>({\n  occurrence,\n  taxonomy,\n  pending = false,\n  onRetry,\n  onEdit,\n  onDelete,\n  editButtonRef,\n  renderTag,\n  compact = false,\n}: {\n  occurrence: SlotPlannerOccurrence<TData>;\n  taxonomy: SlotPlannerTaxonomy;\n  pending?: boolean;\n  /** Present only while the last mutation for this occurrence failed. */\n  onRetry?: () => void;\n  /** Absent on locked occurrences: no edit affordance renders at all. */\n  onEdit?: () => void;\n  onDelete?: () => void;\n  editButtonRef?: (node: HTMLButtonElement | null) => void;\n  /** Custom renderer for one tag chip. */\n  renderTag?: (context: SlotPlannerTagRenderContext<TData>) => ReactNode;\n  compact?: boolean;\n}) {\n  const { note, tags } = getConventionalSlotData(occurrence.slot.data);\n  const recurrenceLabel = occurrence.isRecurring\n    ? occurrence.slot.recurrence?.frequency === \"weekly\"\n      ? taxonomy.recurringWeekly\n      : taxonomy.recurringBiweekly\n    : null;\n  const bufferBeforeMinutes = occurrence.slot.bufferBeforeMinutes ?? 0;\n  const metaLine =\n    bufferBeforeMinutes > 0\n      ? `${formatSlotPlannerCountTemplate(\n          taxonomy.bufferSummary,\n          bufferBeforeMinutes,\n          {},\n        )} · ${occurrence.timeZone}`\n      : occurrence.timeZone;\n  const renderTagChip = (tag: string) => (\n    <span\n      data-slot=\"slot-planner-tag-chip\"\n      className={\n        compact ? \"text-muted-foreground text-xs\" : slotPlannerChipClasses\n      }\n    >\n      {tag}\n    </span>\n  );\n\n  return (\n    <>\n      <div\n        className={\n          compact\n            ? \"grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-2 gap-y-1\"\n            : \"flex flex-wrap items-center justify-between gap-[var(--dt-space-2)]\"\n        }\n      >\n        <span\n          data-slot=\"slot-planner-slot-time\"\n          className={\n            compact\n              ? \"text-base font-semibold tabular-nums\"\n              : \"text-sm font-semibold\"\n          }\n        >\n          {`${occurrence.startTime} – ${getWallClockEndTime(\n            occurrence.startTime,\n            occurrence.durationMinutes,\n          )}`}\n        </span>\n        <span\n          data-slot=\"slot-planner-status-badge\"\n          data-status={occurrence.status}\n          className={\n            compact\n              ? \"col-start-1 row-start-2 inline-flex items-center gap-1 text-xs\"\n              : slotPlannerStatusBadgeClasses\n          }\n        >\n          <span\n            aria-hidden=\"true\"\n            className={cn(\n              \"size-1.5 rounded-full\",\n              slotPlannerStatusDotClasses[occurrence.status],\n            )}\n          />\n          {taxonomy.statusLabels[occurrence.status]}\n        </span>\n        {compact && (onEdit || onDelete) ? (\n          <DropdownMenu\n            motionPreset=\"none\"\n            className=\"col-start-2 row-span-2 row-start-1\"\n          >\n            <DropdownMenuTrigger\n              ref={editButtonRef}\n              size=\"sm\"\n              variant=\"ghost\"\n              disabled={pending}\n              aria-label={`${taxonomy.slotActions} · ${occurrence.startTime}`}\n              className=\"col-start-2 row-span-2 row-start-1 ms-auto min-h-10 min-w-10 px-2\"\n            >\n              <span aria-hidden=\"true\">•••</span>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent>\n              {onEdit ? (\n                <DropdownMenuItem onAction={() => openAfterMenu(onEdit)}>\n                  {taxonomy.editSlot}\n                </DropdownMenuItem>\n              ) : null}\n              {onDelete ? (\n                <DropdownMenuItem\n                  destructive\n                  onAction={() => openAfterMenu(onDelete)}\n                >\n                  {taxonomy.deleteSlot}\n                </DropdownMenuItem>\n              ) : null}\n            </DropdownMenuContent>\n          </DropdownMenu>\n        ) : null}\n      </div>\n      {compact ? (\n        <p className=\"text-muted-foreground text-sm\">\n          {formatSlotPlannerCountTemplate(\n            taxonomy.remainingSeats,\n            Math.max(\n              0,\n              occurrence.capacity -\n                occurrence.bookedCount -\n                occurrence.requestedCount,\n            ),\n            {\n              remaining: Math.max(\n                0,\n                occurrence.capacity -\n                  occurrence.bookedCount -\n                  occurrence.requestedCount,\n              ),\n            },\n          )}\n        </p>\n      ) : null}\n      <div className=\"flex flex-wrap items-center gap-[var(--dt-space-1)]\">\n        <span\n          data-slot=\"slot-planner-duration-chip\"\n          className={\n            compact ? \"text-muted-foreground text-xs\" : slotPlannerChipClasses\n          }\n        >\n          {formatSlotPlannerCountTemplate(\n            taxonomy.durationSummary,\n            occurrence.durationMinutes,\n          )}\n        </span>\n        {recurrenceLabel ? (\n          <span\n            data-slot=\"slot-planner-recurrence-chip\"\n            className={\n              compact ? \"text-muted-foreground text-xs\" : slotPlannerChipClasses\n            }\n          >\n            {recurrenceLabel}\n          </span>\n        ) : null}\n        {tags.map((tag) => (\n          <Fragment key={tag}>\n            {renderTag\n              ? renderTag({\n                  occurrence,\n                  renderDefault: () => renderTagChip(tag),\n                  tag,\n                  taxonomy,\n                })\n              : renderTagChip(tag)}\n          </Fragment>\n        ))}\n      </div>\n      {note ? (\n        <p\n          data-slot=\"slot-planner-slot-note\"\n          className=\"text-muted-foreground text-sm leading-6\"\n        >\n          {note}\n        </p>\n      ) : null}\n      <p\n        data-slot=\"slot-planner-slot-meta\"\n        className=\"text-muted-foreground text-xs\"\n      >\n        {metaLine}\n      </p>\n      {!compact && (onEdit || onDelete) ? (\n        <div\n          data-slot=\"slot-planner-slot-actions\"\n          className=\"flex flex-wrap items-center gap-[var(--dt-space-2)]\"\n        >\n          {onEdit ? (\n            <button\n              ref={editButtonRef}\n              type=\"button\"\n              data-slot=\"slot-planner-edit-slot\"\n              className={slotPlannerSlotActionButtonClasses}\n              disabled={pending}\n              onClick={onEdit}\n            >\n              <span aria-hidden=\"true\">\n                <EditIcon />\n              </span>\n              {taxonomy.editSlot}\n            </button>\n          ) : null}\n          {onDelete ? (\n            <button\n              type=\"button\"\n              data-slot=\"slot-planner-delete-slot\"\n              className={slotPlannerSlotActionButtonClasses}\n              disabled={pending}\n              onClick={onDelete}\n            >\n              <span aria-hidden=\"true\">\n                <TrashIcon />\n              </span>\n              {taxonomy.deleteSlot}\n            </button>\n          ) : null}\n        </div>\n      ) : null}\n      {pending ? (\n        <p\n          data-slot=\"slot-planner-save-pending\"\n          className={slotPlannerSavePendingClasses}\n        >\n          {taxonomy.savePending}\n        </p>\n      ) : null}\n      {onRetry ? (\n        <div\n          data-slot=\"slot-planner-save-error\"\n          className={slotPlannerSaveErrorClasses}\n        >\n          <span>{taxonomy.saveError}</span>\n          <button\n            type=\"button\"\n            data-slot=\"slot-planner-retry\"\n            className={slotPlannerSlotActionButtonClasses}\n            onClick={onRetry}\n          >\n            {taxonomy.retry}\n          </button>\n        </div>\n      ) : null}\n    </>\n  );\n}\n\nfunction SlotPlannerInner<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  {\n    className,\n    slots,\n    defaultSlots,\n    focusedDate,\n    defaultFocusedDate,\n    onFocusedDateChange,\n    onCreateSlot,\n    onUpdateSlot,\n    onDeleteOccurrence,\n    onDeleteSeries,\n    constraints,\n    onBatchChange,\n    generateSlotId,\n    taxonomy,\n    view: controlledView,\n    defaultView = \"week\",\n    weekLayout = \"calendar\",\n    onViewChange,\n    now,\n    timeZone,\n    loading = false,\n    error,\n    title,\n    locale = \"en\",\n    reducedMotion,\n    renderers,\n    ...props\n  }: SlotPlannerProps<TData>,\n  ref: ForwardedRef<HTMLDivElement>,\n) {\n  const baseId = useId();\n  const panelId = `${baseId}-panel`;\n  const prefersReducedMotion = useReducedMotion();\n  const resolvedReducedMotion = reducedMotion ?? prefersReducedMotion === true;\n  const motionEnabled = !resolvedReducedMotion;\n  const [environmentTimeZone] = useState(\n    () => new Intl.DateTimeFormat().resolvedOptions().timeZone,\n  );\n  const resolvedPlannerTimeZone = timeZone ?? environmentTimeZone;\n  const [internalView, setInternalView] = useState(defaultView);\n  const currentView = controlledView ?? internalView;\n  const setView = useCallback(\n    (nextView: SlotPlannerView) => {\n      if (controlledView === undefined) {\n        setInternalView(nextView);\n      }\n\n      if (nextView !== currentView) {\n        onViewChange?.(nextView);\n      }\n    },\n    [controlledView, currentView, onViewChange],\n  );\n\n  // Mutations recorded at dispatch time (create/edit) or from batch payloads\n  // (copy day/week), keyed `slotId::occurrenceDate`. Once the mutation has\n  // settled successfully and the occurrence renders on the focused day, the\n  // entry is promoted to a highlight pulse; batch entries also carry the\n  // stagger index of their entrance.\n  const [recentMutations, setRecentMutations] = useState(\n    () =>\n      new Map<\n        string,\n        {\n          slotId: string;\n          occurrenceDate: string;\n          staggerIndex: number;\n          /** The actual pending/error key of the CRUD op that created it. */\n          crudKey: string;\n        }\n      >(),\n  );\n  const recordRecentMutation = useCallback(\n    (\n      slotId: string,\n      occurrenceDate: string,\n      crudKey: string,\n      staggerIndex = 0,\n    ) => {\n      setRecentMutations((previous) =>\n        new Map(previous).set(`${slotId}::${occurrenceDate}`, {\n          crudKey,\n          occurrenceDate,\n          slotId,\n          staggerIndex,\n        }),\n      );\n    },\n    [],\n  );\n  // Focused date at dispatch time, so batch (copy) highlights can be guarded\n  // against the batch's `batch:<focusedDate>` pending/error key.\n  const focusedDateRef = useRef(focusedDate ?? defaultFocusedDate ?? \"\");\n  const handleCreateSlotWithHighlight = (\n    payload: SlotPlannerCreatePayload<TData>,\n  ) => {\n    // Creates are keyed `create::<date>`, not `<slotId>::<date>`.\n    recordRecentMutation(\n      payload.slot.id,\n      payload.slot.date,\n      getSlotPlannerCreateKey(payload.slot.date),\n    );\n\n    return onCreateSlot?.(payload);\n  };\n  const handleBatchChangeWithHighlight = (\n    payload: SlotPlannerBatchChangePayload<TData>,\n  ) => {\n    const countsByDate = new Map<string, number>();\n    // Copy batches share one `batch:<focusedDate>` key for the whole payload.\n    const batchCrudKey = getSlotPlannerBatchKey(focusedDateRef.current);\n\n    for (const slot of payload.createdSlots) {\n      const staggerIndex = countsByDate.get(slot.date) ?? 0;\n\n      countsByDate.set(slot.date, staggerIndex + 1);\n      recordRecentMutation(slot.id, slot.date, batchCrudKey, staggerIndex);\n    }\n\n    return onBatchChange?.(payload);\n  };\n\n  // All non-visual state and dispatch lives in the headless hook; this\n  // component owns DOM structure, refs, focus, and the motion layer only.\n  const planner = useSlotPlanner<TData>({\n    constraints,\n    defaultFocusedDate,\n    defaultSlots,\n    focusedDate,\n    generateSlotId,\n    locale,\n    now,\n    onBatchChange: handleBatchChangeWithHighlight,\n    onCreateSlot: handleCreateSlotWithHighlight,\n    onDeleteOccurrence,\n    onDeleteSeries,\n    onFocusedDateChange,\n    onUpdateSlot,\n    slots,\n    taxonomy,\n    timeZone: resolvedPlannerTimeZone,\n    view: currentView,\n  });\n  const {\n    announcement,\n    announcementNonce,\n    batchKey,\n    clearDay,\n    clearError,\n    copyDay,\n    copyWeek,\n    createKey,\n    createSlot,\n    dailyCap,\n    deleteOccurrence,\n    deleteSeries,\n    focusedDate: currentFocusedDate,\n    goToNextWeek,\n    goToPreviousWeek,\n    goToThisWeek,\n    occurrenceKey,\n    occurrencesByDate,\n    pendingKeys,\n    retryByKey,\n    selectedOccurrences,\n    setFocusedDate,\n    summarizeDay,\n    taxonomy: resolvedTaxonomy,\n    todayIso,\n    updateSlot,\n    weekDays,\n    weeklyCap,\n  } = planner;\n  useIsomorphicLayoutEffect(() => {\n    focusedDateRef.current = currentFocusedDate;\n  }, [currentFocusedDate]);\n  const nounTemplateTokens = {\n    slot: resolvedTaxonomy.slot,\n    slotPlural: resolvedTaxonomy.slotPlural,\n  };\n  const isPastDay = currentFocusedDate < todayIso;\n\n  const rootRef = useRef<HTMLDivElement | null>(null);\n  const [calendarWidth, setCalendarWidth] = useState(false);\n  useEffect(() => {\n    const root = rootRef.current;\n    if (!root || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(([entry]) => {\n      setCalendarWidth((entry?.contentRect.width ?? 0) >= 620);\n    });\n    observer.observe(root);\n    return () => observer.disconnect();\n  }, []);\n  // Legacy renderers own their original structural surfaces; opt them into the\n  // agenda automatically rather than silently bypassing consumer content.\n  const calendarPresentation = weekLayout === \"calendar\" && !renderers;\n  const showCalendar =\n    calendarPresentation && calendarWidth && currentView === \"week\";\n  const mergedRef = useCallback(\n    (node: HTMLDivElement | null) => {\n      rootRef.current = node;\n\n      if (typeof ref === \"function\") {\n        ref(node);\n      } else if (ref) {\n        ref.current = node;\n      }\n    },\n    [ref],\n  );\n\n  // Logical week-navigation direction, derived from the week-start change so\n  // controlled and uncontrolled focus updates both animate. The RTL factor\n  // inverts the slide axis; ISO dates compare lexicographically.\n  const weekStart = weekDays[0]!;\n  const [weekNavigation, setWeekNavigation] = useState<{\n    start: string;\n    direction: -1 | 0 | 1;\n  }>({ start: weekStart, direction: 0 });\n  const weekDirection =\n    weekNavigation.start === weekStart\n      ? weekNavigation.direction\n      : weekStart > weekNavigation.start\n        ? 1\n        : -1;\n  if (weekNavigation.start !== weekStart) {\n    setWeekNavigation({ start: weekStart, direction: weekDirection });\n  }\n  const [isRtl, setIsRtl] = useState(false);\n\n  useEffect(() => {\n    if (rootRef.current) {\n      setIsRtl(getSlotPlannerDirection(rootRef.current) === \"rtl\");\n    }\n  }, []);\n\n  const weekSlideFactor = weekDirection * (isRtl ? -1 : 1);\n\n  // Highlight pulses per occurrence key; the nonce re-pulses repeat edits.\n  const [highlightNonces, setHighlightNonces] = useState<\n    ReadonlyMap<string, number>\n  >(() => new Map());\n  const clearHighlight = useCallback((key: string) => {\n    setHighlightNonces((previous) => {\n      if (!previous.has(key)) {\n        return previous;\n      }\n\n      const next = new Map(previous);\n\n      next.delete(key);\n\n      return next;\n    });\n  }, []);\n\n  // Promotes recorded mutations to highlight pulses once the mutation has\n  // settled successfully (not pending, not failed) and its occurrence is\n  // rendered on the focused day — including slots from copy batches, which\n  // are consumed when navigation makes them visible.\n  useEffect(() => {\n    const recent = new Map(recentMutations);\n\n    if (recent.size === 0) {\n      return;\n    }\n\n    // Prune entries whose slot has since disappeared (e.g. created then\n    // deleted, or a rejected copy) so they never linger or mis-promote.\n    const liveSlotIds = new Set(planner.slots.map((slot) => slot.id));\n\n    for (const [key, entry] of recent) {\n      if (!liveSlotIds.has(entry.slotId)) {\n        recent.delete(key);\n      }\n    }\n\n    const visibleKeys = new Set(\n      selectedOccurrences.map(\n        (occurrence) => `${occurrence.slotId}::${occurrence.occurrenceDate}`,\n      ),\n    );\n    const consumed: string[] = [];\n\n    for (const [key, entry] of recent) {\n      // Guard against the op's actual CRUD key (create::<date> /\n      // batch:<date> / <slotId>::<date>), not a reconstructed occurrence key.\n      if (pendingKeys.has(entry.crudKey) || retryByKey.has(entry.crudKey)) {\n        continue;\n      }\n\n      if (visibleKeys.has(key)) {\n        consumed.push(key);\n      }\n    }\n\n    if (consumed.length === 0 && recent.size === recentMutations.size) {\n      return;\n    }\n\n    for (const key of consumed) {\n      recent.delete(key);\n    }\n\n    // Consume only after the entrance has committed, then pulse settled CRUD results.\n    // eslint-disable-next-line react-hooks/set-state-in-effect -- Consume queued CRUD entrances only after they commit, then highlight successfully settled visible occurrences.\n    setRecentMutations(recent);\n    if (motionEnabled && consumed.length > 0) {\n      setHighlightNonces((previous) => {\n        const next = new Map(previous);\n\n        for (const key of consumed) {\n          next.set(key, (previous.get(key) ?? 0) + 1);\n        }\n\n        return next;\n      });\n    }\n  }, [\n    recentMutations,\n    motionEnabled,\n    pendingKeys,\n    planner.slots,\n    retryByKey,\n    selectedOccurrences,\n  ]);\n\n  const tabRefs = useRef(new Map<string, HTMLButtonElement>());\n  const [editorState, setEditorState] = useState<\n    | { mode: \"create\"; date: string }\n    | { mode: \"edit\"; occurrence: SlotPlannerOccurrence<TData> }\n    | null\n  >(null);\n  const [editorViolations, setEditorViolations] = useState<\n    SlotPlannerViolation[] | null\n  >(null);\n  const [batchDialog, setBatchDialog] = useState<\n    \"copy-day\" | \"copy-week\" | \"clear-day\" | null\n  >(null);\n  const [deleteTarget, setDeleteTarget] =\n    useState<SlotPlannerOccurrence<TData> | null>(null);\n  const editButtonRefs = useRef(new Map<string, HTMLButtonElement>());\n  const cardRefs = useRef(new Map<string, HTMLLIElement>());\n  const addSlotButtonRef = useRef<HTMLButtonElement | null>(null);\n  const panelRef = useRef<HTMLDivElement | null>(null);\n\n  // Deletion moves focus to the nearest remaining card. Candidates are\n  // captured before the mutation and resolved against the DOM after it\n  // commits: the default renderer's edit button wins; custom slot cards\n  // without one fall back to the structural <li> wrapper, then the add-slot\n  // affordance, then the day panel.\n  const scheduleFocusAfterDelete = useCallback((candidateKeys: string[]) => {\n    if (typeof window === \"undefined\") {\n      return;\n    }\n\n    window.setTimeout(() => {\n      for (const candidateKey of candidateKeys) {\n        const button = editButtonRefs.current.get(candidateKey);\n\n        if (button?.isConnected) {\n          button.focus();\n\n          return;\n        }\n      }\n\n      for (const candidateKey of candidateKeys) {\n        const card = cardRefs.current.get(candidateKey);\n\n        if (card?.isConnected) {\n          card.focus();\n\n          return;\n        }\n      }\n\n      if (addSlotButtonRef.current?.isConnected) {\n        addSlotButtonRef.current.focus();\n\n        return;\n      }\n\n      panelRef.current?.focus();\n    }, 0);\n  }, []);\n\n  const computeDeleteFocusCandidates = (\n    deleted: SlotPlannerOccurrence<TData>,\n    scope: \"occurrence\" | \"series\",\n  ) => {\n    const deletedKey = occurrenceKey(deleted);\n    const index = selectedOccurrences.findIndex(\n      (occurrence) => occurrenceKey(occurrence) === deletedKey,\n    );\n\n    if (index === -1) {\n      return [];\n    }\n\n    return [\n      ...selectedOccurrences.slice(index + 1),\n      ...selectedOccurrences.slice(0, index).reverse(),\n    ]\n      .filter((occurrence) =>\n        scope === \"series\" ? occurrence.slotId !== deleted.slotId : true,\n      )\n      .map((occurrence) => occurrenceKey(occurrence));\n  };\n\n  // Validation happens in the hook's dispatchers; on violations the editor\n  // stays open and renders them, and saving again revalidates.\n  const handleEditorSubmit = (result: SlotPlannerEditorResult) => {\n    const state = editorState;\n\n    if (!state) {\n      return;\n    }\n\n    let violations: SlotPlannerViolation[];\n\n    if (state.mode === \"create\") {\n      if (result.scope !== \"series\") {\n        return;\n      }\n\n      violations = createSlot(result.values, { date: state.date });\n    } else {\n      violations = updateSlot(state.occurrence, result);\n\n      if (violations.length === 0) {\n        // Created slots are recorded in the wrapped onCreateSlot (the\n        // generated id is only known there); edits are recorded here. An edit\n        // is keyed by its occurrence key (`<slotId>::<date>`).\n        recordRecentMutation(\n          state.occurrence.slotId,\n          state.occurrence.occurrenceDate,\n          occurrenceKey(state.occurrence),\n        );\n      }\n    }\n\n    if (violations.length > 0) {\n      setEditorViolations(violations);\n\n      return;\n    }\n\n    setEditorViolations(null);\n    setEditorState(null);\n  };\n\n  const handleConfirmDeleteOccurrence = () => {\n    const occurrence = deleteTarget;\n\n    if (!occurrence) {\n      return;\n    }\n\n    setDeleteTarget(null);\n\n    const candidates = computeDeleteFocusCandidates(occurrence, \"occurrence\");\n\n    deleteOccurrence(occurrence, {\n      onApplied: () => scheduleFocusAfterDelete(candidates),\n    });\n  };\n\n  const handleConfirmDeleteSeries = () => {\n    const occurrence = deleteTarget;\n\n    if (!occurrence) {\n      return;\n    }\n\n    setDeleteTarget(null);\n\n    const candidates = computeDeleteFocusCandidates(occurrence, \"series\");\n\n    deleteSeries(occurrence, {\n      onApplied: () => scheduleFocusAfterDelete(candidates),\n    });\n  };\n\n  const openCopyDay = () => {\n    clearError(batchKey);\n    setBatchDialog(\"copy-day\");\n  };\n  const openCopyWeek = () => {\n    clearError(batchKey);\n    setBatchDialog(\"copy-week\");\n  };\n  const openClearDay = () => {\n    clearError(batchKey);\n    setBatchDialog(\"clear-day\");\n  };\n\n  const createPending = pendingKeys.has(createKey);\n  const createRetry = retryByKey.get(createKey);\n  const batchPending = pendingKeys.has(batchKey);\n  const batchRetry = retryByKey.get(batchKey);\n\n  const weekdayFormatter = useMemo(\n    () =>\n      new Intl.DateTimeFormat(locale, { weekday: \"short\", timeZone: \"UTC\" }),\n    [locale],\n  );\n  const dayMonthFormatter = useMemo(\n    () =>\n      new Intl.DateTimeFormat(locale, {\n        day: \"numeric\",\n        month: \"short\",\n        timeZone: \"UTC\",\n      }),\n    [locale],\n  );\n  const longDateFormatter = useMemo(\n    () =>\n      new Intl.DateTimeFormat(locale, { dateStyle: \"full\", timeZone: \"UTC\" }),\n    [locale],\n  );\n\n  const getTabId = (date: string) => `${baseId}-tab-${date}`;\n  const railOrientation = useSlotPlannerRailOrientation();\n  const handleRailKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    const direction = getSlotPlannerDirection(event.currentTarget);\n    const forwardKey = direction === \"rtl\" ? \"ArrowLeft\" : \"ArrowRight\";\n    const backwardKey = direction === \"rtl\" ? \"ArrowRight\" : \"ArrowLeft\";\n    const currentIndex = weekDays.indexOf(currentFocusedDate);\n    let nextIndex: number;\n\n    // ArrowUp/ArrowDown mirror the vertical (`md:` and up) rail layout, and\n    // stay accepted in the horizontal layout so navigation never depends on\n    // which axis the tablist happens to render along.\n    if (event.key === forwardKey || event.key === \"ArrowDown\") {\n      nextIndex = (currentIndex + 1) % weekDays.length;\n    } else if (event.key === backwardKey || event.key === \"ArrowUp\") {\n      nextIndex = (currentIndex + weekDays.length - 1) % weekDays.length;\n    } else if (event.key === \"Home\") {\n      nextIndex = 0;\n    } else if (event.key === \"End\") {\n      nextIndex = weekDays.length - 1;\n    } else {\n      return;\n    }\n\n    event.preventDefault();\n\n    const nextDate = weekDays[nextIndex];\n\n    setFocusedDate(nextDate);\n    tabRefs.current.get(nextDate)?.focus();\n  };\n\n  const formattedFocusedDate = longDateFormatter.format(\n    toUtcDate(currentFocusedDate),\n  );\n  const defaultDayHeader = (\n    <h3\n      data-slot=\"slot-planner-day-heading\"\n      className=\"text-base font-semibold\"\n    >\n      {formattedFocusedDate}\n    </h3>\n  );\n\n  const dailyCapText = dailyCap\n    ? formatSlotPlannerCountTemplate(\n        resolvedTaxonomy.dailyCapSummary,\n        dailyCap.cap,\n        {\n          cap: dailyCap.cap,\n          requestableLabel: resolvedTaxonomy.statusLabels.requestable,\n          slot: resolvedTaxonomy.slot,\n          slotPlural: resolvedTaxonomy.slotPlural,\n          used: dailyCap.used,\n        },\n        locale,\n      )\n    : \"\";\n  const weeklyCapText = weeklyCap\n    ? formatSlotPlannerCountTemplate(\n        resolvedTaxonomy.weeklyCapSummary,\n        weeklyCap.cap,\n        {\n          cap: weeklyCap.cap,\n          requestableLabel: resolvedTaxonomy.statusLabels.requestable,\n          slot: resolvedTaxonomy.slot,\n          slotPlural: resolvedTaxonomy.slotPlural,\n          used: weeklyCap.used,\n        },\n        locale,\n      )\n    : \"\";\n  const dailyCapReachedMessage = dailyCap?.reached\n    ? formatSlotPlannerTemplate(\n        resolvedTaxonomy.violationMessages[\"daily-cap\"],\n        {\n          cap: dailyCap.cap,\n          date: currentFocusedDate,\n          slot: resolvedTaxonomy.slot,\n          slotPlural: resolvedTaxonomy.slotPlural,\n        },\n      )\n    : undefined;\n  const weeklyCapReachedMessage = weeklyCap?.reached\n    ? formatSlotPlannerTemplate(\n        resolvedTaxonomy.violationMessages[\"weekly-cap\"],\n        {\n          cap: weeklyCap.cap,\n          slot: resolvedTaxonomy.slot,\n          slotPlural: resolvedTaxonomy.slotPlural,\n          weekStart,\n        },\n      )\n    : undefined;\n  const capEntries = [\n    dailyCap\n      ? {\n          cap: dailyCap.cap,\n          key: \"daily\",\n          reached: dailyCap.reached,\n          reachedMessage: dailyCapReachedMessage,\n          text: dailyCapText,\n          used: dailyCap.used,\n        }\n      : undefined,\n    weeklyCap\n      ? {\n          cap: weeklyCap.cap,\n          key: \"weekly\",\n          reached: weeklyCap.reached,\n          reachedMessage: weeklyCapReachedMessage,\n          text: weeklyCapText,\n          used: weeklyCap.used,\n        }\n      : undefined,\n  ].filter((entry) => entry !== undefined);\n  const primaryCap = capEntries[0];\n  const defaultCapMeter =\n    capEntries.length > 0 ? (\n      <div\n        data-slot=\"slot-planner-cap-meter\"\n        data-cap-reached={\n          capEntries.some((entry) => entry.reached) ? \"true\" : undefined\n        }\n        className=\"text-muted-foreground flex flex-col gap-[var(--dt-space-1)] text-xs\"\n      >\n        {capEntries.map((entry) => (\n          <div\n            key={entry.key}\n            data-slot={`slot-planner-${entry.key}-cap`}\n            className=\"flex flex-col gap-[var(--dt-space-1)]\"\n          >\n            <p className=\"flex flex-wrap items-center gap-x-[var(--dt-space-2)]\">\n              <span>{entry.text}</span>\n              {entry.reachedMessage ? (\n                // Cap-reached is stated in text, never by color alone.\n                <span\n                  data-slot=\"slot-planner-cap-meter-message\"\n                  className=\"text-destructive font-medium\"\n                >\n                  {entry.reachedMessage}\n                </span>\n              ) : null}\n            </p>\n            {/* Decorative fill bar; the text above stays canonical. */}\n            <div\n              aria-hidden=\"true\"\n              data-slot=\"slot-planner-cap-meter-track\"\n              className=\"bg-muted h-1 w-full overflow-hidden rounded-full\"\n            >\n              <SlotPlannerCapMeterFill\n                motionEnabled={motionEnabled}\n                ratio={entry.cap > 0 ? entry.used / entry.cap : 1}\n              />\n            </div>\n          </div>\n        ))}\n      </div>\n    ) : null;\n\n  const defaultEmptyDay = (\n    <p\n      data-slot=\"slot-planner-empty-day\"\n      className=\"text-muted-foreground text-sm\"\n    >\n      {resolvedTaxonomy.emptyDay}\n    </p>\n  );\n  const hasExternalError =\n    error !== undefined && error !== null && error !== false;\n  const externalErrorContent =\n    typeof error === \"boolean\" ? resolvedTaxonomy.error : error;\n  const defaultLoading = (\n    <p\n      role=\"status\"\n      data-slot=\"slot-planner-loading\"\n      className=\"text-muted-foreground text-sm\"\n    >\n      {resolvedTaxonomy.loading}\n    </p>\n  );\n  const defaultError = (\n    <div\n      role=\"alert\"\n      data-slot=\"slot-planner-error\"\n      className={slotPlannerSaveErrorClasses}\n    >\n      {externalErrorContent ?? resolvedTaxonomy.error}\n    </div>\n  );\n\n  const dayPanel = (\n    <div\n      ref={panelRef}\n      role={currentView === \"week\" && !showCalendar ? \"tabpanel\" : undefined}\n      id={currentView === \"week\" ? panelId : undefined}\n      aria-labelledby={\n        currentView === \"week\" ? getTabId(currentFocusedDate) : undefined\n      }\n      tabIndex={currentView === \"week\" ? 0 : -1}\n      aria-busy={loading || undefined}\n      data-slot=\"slot-planner-day-panel\"\n      data-past={isPastDay ? \"true\" : undefined}\n      data-loading={loading ? \"true\" : undefined}\n      data-error={hasExternalError ? \"true\" : undefined}\n      className={cn(\n        slotPlannerDayPanelClasses,\n        calendarPresentation && \"border-0 bg-transparent p-0 sm:p-0\",\n      )}\n    >\n      <SlotPlannerWeekSlide\n        motionEnabled={motionEnabled}\n        slideFactor={weekSlideFactor}\n        weekKey={weekStart}\n        className={slotPlannerWeekPanelContentClasses}\n      >\n        {renderers?.dayHeader\n          ? renderers.dayHeader({\n              date: currentFocusedDate,\n              formattedDate: formattedFocusedDate,\n              isPast: isPastDay,\n              isToday: currentFocusedDate === todayIso,\n              occurrences: selectedOccurrences,\n              renderDefault: () => defaultDayHeader,\n              taxonomy: resolvedTaxonomy,\n            })\n          : defaultDayHeader}\n        {primaryCap\n          ? renderers?.capMeter\n            ? renderers.capMeter({\n                cap: primaryCap.cap,\n                date: currentFocusedDate,\n                ...(dailyCap\n                  ? {\n                      daily: {\n                        cap: dailyCap.cap,\n                        reached: dailyCap.reached,\n                        ...(dailyCapReachedMessage !== undefined\n                          ? { reachedMessage: dailyCapReachedMessage }\n                          : {}),\n                        text: dailyCapText,\n                        used: dailyCap.used,\n                      },\n                    }\n                  : {}),\n                reached: primaryCap.reached,\n                ...(primaryCap.reachedMessage !== undefined\n                  ? { reachedMessage: primaryCap.reachedMessage }\n                  : {}),\n                renderDefault: () => defaultCapMeter,\n                taxonomy: resolvedTaxonomy,\n                text: primaryCap.text,\n                used: primaryCap.used,\n                ...(weeklyCap\n                  ? {\n                      weekly: {\n                        cap: weeklyCap.cap,\n                        reached: weeklyCap.reached,\n                        ...(weeklyCapReachedMessage !== undefined\n                          ? { reachedMessage: weeklyCapReachedMessage }\n                          : {}),\n                        text: weeklyCapText,\n                        used: weeklyCap.used,\n                      },\n                    }\n                  : {}),\n              })\n            : defaultCapMeter\n          : null}\n        {!isPastDay && !loading && !hasExternalError ? (\n          <div\n            data-slot=\"slot-planner-day-actions\"\n            data-pending={batchPending ? \"true\" : undefined}\n            data-error={batchRetry ? \"true\" : undefined}\n            className=\"flex flex-wrap items-center gap-[var(--dt-space-2)]\"\n          >\n            {calendarPresentation ? (\n              <DropdownMenu motionPreset=\"none\">\n                <DropdownMenuTrigger\n                  size=\"sm\"\n                  variant=\"ghost\"\n                  disabled={batchPending}\n                >\n                  {resolvedTaxonomy.dayActions}\n                </DropdownMenuTrigger>\n                <DropdownMenuContent>\n                  <DropdownMenuItem onAction={() => openAfterMenu(openCopyDay)}>\n                    {resolvedTaxonomy.copyDay}\n                  </DropdownMenuItem>\n                  <DropdownMenuItem\n                    onAction={() => openAfterMenu(openCopyWeek)}\n                  >\n                    {resolvedTaxonomy.copyWeek}\n                  </DropdownMenuItem>\n                  <DropdownMenuItem\n                    destructive\n                    onAction={() => openAfterMenu(openClearDay)}\n                  >\n                    {resolvedTaxonomy.clearDay}\n                  </DropdownMenuItem>\n                </DropdownMenuContent>\n              </DropdownMenu>\n            ) : (\n              <>\n                <button\n                  type=\"button\"\n                  data-slot=\"slot-planner-copy-day\"\n                  className={slotPlannerSlotActionButtonClasses}\n                  disabled={batchPending}\n                  onClick={openCopyDay}\n                >\n                  <span aria-hidden=\"true\">\n                    <CopyIcon />\n                  </span>\n                  {resolvedTaxonomy.copyDay}\n                </button>\n                <button\n                  type=\"button\"\n                  data-slot=\"slot-planner-copy-week\"\n                  className={slotPlannerSlotActionButtonClasses}\n                  disabled={batchPending}\n                  onClick={openCopyWeek}\n                >\n                  <span aria-hidden=\"true\">\n                    <CopyIcon />\n                  </span>\n                  {resolvedTaxonomy.copyWeek}\n                </button>\n                <button\n                  type=\"button\"\n                  data-slot=\"slot-planner-clear-day\"\n                  className={slotPlannerSlotActionButtonClasses}\n                  disabled={batchPending}\n                  onClick={openClearDay}\n                >\n                  <span aria-hidden=\"true\">\n                    <TrashIcon />\n                  </span>\n                  {resolvedTaxonomy.clearDay}\n                </button>\n              </>\n            )}\n            {batchPending ? (\n              <span\n                data-slot=\"slot-planner-save-pending\"\n                className={slotPlannerSavePendingClasses}\n              >\n                {resolvedTaxonomy.savePending}\n              </span>\n            ) : null}\n            {batchRetry ? (\n              <span\n                data-slot=\"slot-planner-save-error\"\n                className={slotPlannerSaveErrorClasses}\n              >\n                <span>{resolvedTaxonomy.saveError}</span>\n                <button\n                  type=\"button\"\n                  data-slot=\"slot-planner-retry\"\n                  className={slotPlannerSlotActionButtonClasses}\n                  onClick={batchRetry}\n                >\n                  {resolvedTaxonomy.retry}\n                </button>\n              </span>\n            ) : null}\n          </div>\n        ) : null}\n        {isPastDay ? (\n          <p\n            data-slot=\"slot-planner-past-day\"\n            className=\"text-muted-foreground text-sm\"\n          >\n            {resolvedTaxonomy.pastDay}\n          </p>\n        ) : null}\n        {loading ? (\n          defaultLoading\n        ) : hasExternalError ? (\n          defaultError\n        ) : selectedOccurrences.length === 0 ? (\n          renderers?.emptyDay ? (\n            renderers.emptyDay({\n              date: currentFocusedDate,\n              isPast: isPastDay,\n              renderDefault: () => defaultEmptyDay,\n              taxonomy: resolvedTaxonomy,\n            })\n          ) : (\n            defaultEmptyDay\n          )\n        ) : (\n          // The list is keyed by the focused date so switching days swaps the\n          // whole list instantly; enter/exit animations only run for slots\n          // appearing or disappearing within the visible day.\n          // eslint-disable-next-line jsx-a11y/no-redundant-roles -- Explicit list semantics preserve VoiceOver support when CSS removes list styling.\n          <ul\n            key={currentFocusedDate}\n            role=\"list\"\n            data-slot=\"slot-planner-slot-list\"\n            className=\"m-[var(--dt-space-0)] flex list-none flex-col gap-[var(--dt-space-2)] p-[var(--dt-space-0)]\"\n          >\n            <AnimatePresence>\n              {selectedOccurrences.map((occurrence) => {\n                const key = occurrenceKey(occurrence);\n                const locked = lockedOccurrenceStatuses.has(occurrence.status);\n                const pending = pendingKeys.has(key);\n                const retry = retryByKey.get(key);\n                const edit = locked\n                  ? undefined\n                  : () => {\n                      clearError(key);\n                      setEditorViolations(null);\n                      setEditorState({ mode: \"edit\", occurrence });\n                    };\n                const remove = locked\n                  ? undefined\n                  : () => {\n                      clearError(key);\n                      setDeleteTarget(occurrence);\n                    };\n                const defaultCardContent = (\n                  <SlotPlannerSlotCardContent\n                    occurrence={occurrence}\n                    taxonomy={resolvedTaxonomy}\n                    pending={pending}\n                    onRetry={retry}\n                    onEdit={edit}\n                    onDelete={remove}\n                    editButtonRef={(node) => {\n                      if (node) {\n                        editButtonRefs.current.set(key, node);\n                      } else {\n                        editButtonRefs.current.delete(key);\n                      }\n                    }}\n                    renderTag={renderers?.tag}\n                    compact={calendarPresentation}\n                  />\n                );\n\n                const recentEntry = recentMutations.get(key);\n\n                return (\n                  // The <li> wrapper and its data attributes are structural; it\n                  // is programmatically focusable so post-delete focus recovery\n                  // works with custom slot cards that render no edit button.\n                  // The motion layer animates the wrapper itself, so custom\n                  // slot cards are animated identically to the default.\n                  <SlotPlannerSlotListItem\n                    key={key}\n                    ref={(node) => {\n                      if (node) {\n                        cardRefs.current.set(key, node);\n                      } else {\n                        cardRefs.current.delete(key);\n                      }\n                    }}\n                    motionEnabled={motionEnabled}\n                    entrance={recentEntry !== undefined}\n                    layoutDependency={selectedOccurrences.length}\n                    staggerIndex={recentEntry?.staggerIndex ?? 0}\n                    highlightNonce={highlightNonces.get(key) ?? 0}\n                    onHighlightComplete={() => clearHighlight(key)}\n                    tabIndex={-1}\n                    data-status={occurrence.status}\n                    data-locked={locked ? \"true\" : undefined}\n                    data-pending={pending ? \"true\" : undefined}\n                    data-error={retry ? \"true\" : undefined}\n                    className={cn(\n                      slotPlannerSlotCardClasses,\n                      calendarPresentation &&\n                        \"data-[status=requestable]:border-s-success data-[status=requested]:border-s-warning data-[status=booked]:border-s-info border-s-[3px]\",\n                    )}\n                  >\n                    {renderers?.slotCard\n                      ? renderers.slotCard({\n                          ...(edit ? { edit } : {}),\n                          error: retry !== undefined,\n                          locked,\n                          occurrence,\n                          pending,\n                          ...(remove ? { remove } : {}),\n                          renderDefault: () => defaultCardContent,\n                          ...(retry ? { retry } : {}),\n                          taxonomy: resolvedTaxonomy,\n                        })\n                      : defaultCardContent}\n                  </SlotPlannerSlotListItem>\n                );\n              })}\n            </AnimatePresence>\n          </ul>\n        )}\n        {!isPastDay && !loading && !hasExternalError ? (\n          <>\n            <button\n              ref={addSlotButtonRef}\n              type=\"button\"\n              data-slot=\"slot-planner-add-slot\"\n              data-pending={createPending ? \"true\" : undefined}\n              data-error={createRetry ? \"true\" : undefined}\n              className={slotPlannerAddSlotButtonClasses}\n              disabled={createPending}\n              onClick={() => {\n                clearError(createKey);\n                setEditorViolations(null);\n                setEditorState({ mode: \"create\", date: currentFocusedDate });\n              }}\n            >\n              <span aria-hidden=\"true\">\n                <PlusIcon />\n              </span>\n              {resolvedTaxonomy.addSlot}\n            </button>\n            {createPending ? (\n              <p\n                data-slot=\"slot-planner-save-pending\"\n                className={slotPlannerSavePendingClasses}\n              >\n                {resolvedTaxonomy.savePending}\n              </p>\n            ) : null}\n            {createRetry ? (\n              <div\n                data-slot=\"slot-planner-save-error\"\n                className={slotPlannerSaveErrorClasses}\n              >\n                <span>{resolvedTaxonomy.saveError}</span>\n                <button\n                  type=\"button\"\n                  data-slot=\"slot-planner-retry\"\n                  className={slotPlannerSlotActionButtonClasses}\n                  onClick={createRetry}\n                >\n                  {resolvedTaxonomy.retry}\n                </button>\n              </div>\n            ) : null}\n          </>\n        ) : null}\n      </SlotPlannerWeekSlide>\n    </div>\n  );\n\n  const returnToCurrentLabel =\n    currentView === \"day\" ? resolvedTaxonomy.today : resolvedTaxonomy.thisWeek;\n  const returnToCurrentDisabled =\n    currentView === \"day\" && currentFocusedDate === todayIso;\n  const moveDay = (days: number) => {\n    setFocusedDate(parseDate(currentFocusedDate).add({ days }).toString());\n  };\n  const goToPreviousPeriod =\n    currentView === \"day\" ? () => moveDay(-1) : goToPreviousWeek;\n  const goToNextPeriod =\n    currentView === \"day\" ? () => moveDay(1) : goToNextWeek;\n  const previousPeriodLabel =\n    currentView === \"day\"\n      ? resolvedTaxonomy.previousDay\n      : resolvedTaxonomy.previousWeek;\n  const nextPeriodLabel =\n    currentView === \"day\"\n      ? resolvedTaxonomy.nextDay\n      : resolvedTaxonomy.nextWeek;\n\n  const defaultToolbar = (\n    <>\n      <div\n        data-slot=\"slot-planner-title\"\n        className=\"min-w-0 text-base font-semibold sm:text-lg\"\n      >\n        {title}\n        {calendarPresentation ? (\n          <p className=\"text-muted-foreground mt-1 text-xs font-normal\">\n            {resolvedPlannerTimeZone.replaceAll(\"_\", \" \")}\n          </p>\n        ) : null}\n      </div>\n      <div className=\"col-span-2 row-start-2 flex min-w-0 items-center justify-start gap-[var(--dt-space-1)] sm:col-span-1 sm:col-start-auto sm:row-auto sm:justify-center\">\n        <IconButton\n          aria-label={previousPeriodLabel}\n          size=\"sm\"\n          variant=\"outline\"\n          onClick={goToPreviousPeriod}\n        >\n          <ChevronIcon direction=\"backward\" />\n        </IconButton>\n        {calendarPresentation ? (\n          <span className=\"px-1 text-sm font-medium tabular-nums\">\n            {currentView === \"week\"\n              ? dayMonthFormatter\n                  .formatRange(toUtcDate(weekDays[0]!), toUtcDate(weekDays[6]!))\n                  .replace(/\\s+/gu, \" \")\n              : dayMonthFormatter.format(toUtcDate(currentFocusedDate))}\n          </span>\n        ) : null}\n        <Button\n          size=\"sm\"\n          variant=\"outline\"\n          leftIcon={<CalendarIcon />}\n          className=\"shadow-sm\"\n          disabled={returnToCurrentDisabled}\n          onClick={goToThisWeek}\n        >\n          {calendarPresentation ? resolvedTaxonomy.today : returnToCurrentLabel}\n        </Button>\n        <IconButton\n          aria-label={nextPeriodLabel}\n          size=\"sm\"\n          variant=\"outline\"\n          onClick={goToNextPeriod}\n        >\n          <ChevronIcon direction=\"forward\" />\n        </IconButton>\n      </div>\n      <div\n        role=\"group\"\n        aria-label={resolvedTaxonomy.viewSwitcherLabel}\n        data-slot=\"slot-planner-view-switch\"\n        className=\"border-border bg-background col-start-2 row-start-1 flex items-center gap-[var(--dt-space-1)] justify-self-end rounded-md border p-0.5 sm:col-auto sm:row-auto\"\n      >\n        {([\"week\", \"day\"] as const).map((option) => (\n          <button\n            key={option}\n            type=\"button\"\n            aria-pressed={currentView === option}\n            data-slot=\"slot-planner-view-switch-option\"\n            data-view={option}\n            className={cn(\n              \"text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-ring focus-visible:ring-offset-background inline-flex h-7 items-center rounded px-[var(--dt-space-2)] text-xs font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none motion-safe:transition-colors motion-safe:duration-150\",\n              currentView === option &&\n                \"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground\",\n            )}\n            onClick={() => setView(option)}\n          >\n            {option === \"week\"\n              ? resolvedTaxonomy.weekView\n              : resolvedTaxonomy.dayView}\n          </button>\n        ))}\n      </div>\n      {showCalendar ? (\n        <Button\n          size=\"sm\"\n          leftIcon={<PlusIcon />}\n          disabled={createPending || isPastDay || loading || hasExternalError}\n          onClick={() => {\n            clearError(createKey);\n            setEditorViolations(null);\n            setEditorState({ mode: \"create\", date: currentFocusedDate });\n          }}\n        >\n          {resolvedTaxonomy.calendarAdd}\n        </Button>\n      ) : null}\n    </>\n  );\n\n  return (\n    // reducedMotion=\"always\" hard-disables transform animation inside Motion\n    // as defense in depth; the wrappers above already collapse to plain\n    // elements when motionEnabled is false.\n    <MotionConfig reducedMotion={resolvedReducedMotion ? \"always\" : \"user\"}>\n      <div\n        {...props}\n        ref={mergedRef}\n        data-slot=\"slot-planner\"\n        data-view={currentView}\n        data-reduced-motion={resolvedReducedMotion ? \"true\" : undefined}\n        data-week-direction={\n          weekDirection === 1\n            ? \"forward\"\n            : weekDirection === -1\n              ? \"backward\"\n              : \"none\"\n        }\n        className={cn(slotPlannerRootClasses, className)}\n      >\n        <div\n          data-slot=\"slot-planner-toolbar\"\n          className={\n            showCalendar\n              ? \"flex flex-wrap items-center justify-between gap-3\"\n              : slotPlannerToolbarClasses\n          }\n        >\n          {renderers?.toolbar\n            ? renderers.toolbar({\n                clearDay: openClearDay,\n                copyDay: openCopyDay,\n                copyWeek: openCopyWeek,\n                focusedDate: currentFocusedDate,\n                goToNextWeek,\n                goToPreviousWeek,\n                goToThisWeek,\n                renderDefault: () => defaultToolbar,\n                setFocusedDate,\n                setView,\n                taxonomy: resolvedTaxonomy,\n                title,\n                todayIso,\n                view: currentView,\n                weekDays,\n              })\n            : defaultToolbar}\n        </div>\n        {showCalendar ? (\n          <>\n            {!loading && !hasExternalError && primaryCap ? (\n              <div className=\"flex flex-wrap items-center justify-between gap-3\">\n                <div className=\"min-w-0 flex-1\">{defaultCapMeter}</div>\n              </div>\n            ) : null}\n            {loading ? (\n              defaultLoading\n            ) : hasExternalError ? (\n              defaultError\n            ) : (\n              <SlotPlannerCalendar\n                slots={planner.slots}\n                dates={weekDays}\n                focusedDate={currentFocusedDate}\n                today={todayIso}\n                now={now ?? new Date().toISOString()}\n                timeZone={resolvedPlannerTimeZone}\n                locale={locale}\n                taxonomy={resolvedTaxonomy}\n                onSelectDay={setFocusedDate}\n                onAdd={(date) => {\n                  setFocusedDate(date);\n                  setEditorViolations(null);\n                  setEditorState({ mode: \"create\", date });\n                }}\n                onOpenSlot={(occurrence) => {\n                  setFocusedDate(occurrence.occurrenceDate);\n                  setView(\"day\");\n                  window.setTimeout(() => panelRef.current?.focus(), 0);\n                }}\n              />\n            )}\n            {createPending ? (\n              <p role=\"status\" className={slotPlannerSavePendingClasses}>\n                {resolvedTaxonomy.savePending}\n              </p>\n            ) : null}\n            {createRetry ? (\n              <div role=\"alert\" className={slotPlannerSaveErrorClasses}>\n                {resolvedTaxonomy.saveError}\n                <Button size=\"sm\" variant=\"outline\" onClick={createRetry}>\n                  {resolvedTaxonomy.retry}\n                </Button>\n              </div>\n            ) : null}\n          </>\n        ) : currentView === \"week\" ? (\n          <div\n            data-slot=\"slot-planner-week-layout\"\n            className={cn(\n              \"grid min-w-0 gap-4\",\n              !calendarPresentation && \"md:grid-cols-[10rem_minmax(0,1fr)]\",\n            )}\n          >\n            {/* eslint-disable-next-line jsx-a11y/interactive-supports-focus -- The tablist delegates keyboard events to its roving, focusable tab buttons. */}\n            <div\n              role=\"tablist\"\n              aria-label={resolvedTaxonomy.weekRailLabel}\n              aria-orientation={\n                calendarPresentation ? \"horizontal\" : railOrientation\n              }\n              data-slot=\"slot-planner-day-rail\"\n              className={\n                calendarPresentation\n                  ? \"border-border flex min-w-0 gap-1 overflow-x-auto border-b pb-2\"\n                  : slotPlannerDayRailClasses\n              }\n              onKeyDown={handleRailKeyDown}\n            >\n              {weekDays.map((date) => {\n                const selected = date === currentFocusedDate;\n                const summary = summarizeDay(date);\n                const defaultDayCard = (\n                  <>\n                    <span className=\"text-muted-foreground text-xs font-medium\">\n                      {weekdayFormatter.format(toUtcDate(date))}\n                    </span>\n                    <span className=\"text-sm font-semibold\">\n                      {calendarPresentation\n                        ? Number(date.slice(-2))\n                        : dayMonthFormatter.format(toUtcDate(date))}\n                    </span>\n                    {calendarPresentation ? (\n                      <span\n                        className=\"text-muted-foreground text-xs\"\n                        aria-label={formatSlotPlannerCountTemplate(\n                          resolvedTaxonomy.calendarSlotCount,\n                          (occurrencesByDate[date] ?? []).length,\n                          {},\n                          locale,\n                        )}\n                      >\n                        {(occurrencesByDate[date] ?? []).length ? \"•\" : \"–\"}\n                      </span>\n                    ) : (\n                      slotPlannerOccurrenceStatuses\n                        .filter((status) => summary[status] > 0)\n                        .map((status) => {\n                          const summaryText = formatSlotPlannerCountTemplate(\n                            resolvedTaxonomy.statusCountSummary,\n                            summary[status],\n                            {\n                              statusLabel:\n                                resolvedTaxonomy.statusLabels[status],\n                            },\n                            locale,\n                          );\n\n                          return (\n                            <span\n                              key={status}\n                              title={summaryText}\n                              data-slot=\"slot-planner-day-summary\"\n                              data-status={status}\n                              className=\"text-muted-foreground flex w-full min-w-0 items-center gap-[var(--dt-space-1)] overflow-hidden text-xs whitespace-nowrap\"\n                            >\n                              <span\n                                aria-hidden=\"true\"\n                                className={cn(\n                                  \"size-1.5 shrink-0 rounded-full\",\n                                  slotPlannerStatusDotClasses[status],\n                                )}\n                              />\n                              <span\n                                data-slot=\"slot-planner-day-summary-text\"\n                                className=\"min-w-0 truncate\"\n                              >\n                                {summaryText}\n                              </span>\n                            </span>\n                          );\n                        })\n                    )}\n                  </>\n                );\n\n                return (\n                  // The tab element, its ARIA wiring, and the roving tabindex\n                  // are structural; a custom dayCard renders only its children.\n                  <button\n                    key={date}\n                    ref={(node) => {\n                      if (node) {\n                        tabRefs.current.set(date, node);\n                      } else {\n                        tabRefs.current.delete(date);\n                      }\n                    }}\n                    type=\"button\"\n                    role=\"tab\"\n                    id={getTabId(date)}\n                    aria-selected={selected}\n                    aria-controls={panelId}\n                    aria-current={date === todayIso ? \"date\" : undefined}\n                    tabIndex={selected ? 0 : -1}\n                    data-slot=\"slot-planner-day-tab\"\n                    data-selected={selected ? \"true\" : undefined}\n                    data-today={date === todayIso ? \"true\" : undefined}\n                    data-past={date < todayIso ? \"true\" : undefined}\n                    className={\n                      calendarPresentation\n                        ? \"focus-visible:outline-ring data-[selected=true]:border-primary data-[selected=true]:bg-primary/5 relative flex min-h-20 min-w-10 flex-1 flex-col rounded-md border-b-2 border-transparent p-2 text-start focus-visible:outline-2\"\n                        : slotPlannerDayTabClasses\n                    }\n                    onClick={() => setFocusedDate(date)}\n                  >\n                    {selected && !calendarPresentation ? (\n                      // Decorative shared-layout indicator; the non-motion\n                      // selection state stays on aria-selected/data-selected.\n                      <SlotPlannerDayTabIndicator\n                        layoutId={`${baseId}-day-tab-indicator`}\n                        motionEnabled={motionEnabled}\n                      />\n                    ) : null}\n                    <SlotPlannerDayTabContent\n                      motionEnabled={motionEnabled}\n                      slideFactor={weekSlideFactor}\n                      className={slotPlannerDayTabContentClasses}\n                    >\n                      {renderers?.dayCard\n                        ? renderers.dayCard({\n                            date,\n                            isPast: date < todayIso,\n                            isToday: date === todayIso,\n                            occurrences: occurrencesByDate[date] ?? [],\n                            renderDefault: () => defaultDayCard,\n                            selected,\n                            summary,\n                            taxonomy: resolvedTaxonomy,\n                          })\n                        : defaultDayCard}\n                    </SlotPlannerDayTabContent>\n                  </button>\n                );\n              })}\n            </div>\n            {dayPanel}\n          </div>\n        ) : (\n          dayPanel\n        )}\n        {editorState ? (\n          <SlotPlannerEditorDialog\n            mode={editorState.mode}\n            date={\n              editorState.mode === \"create\"\n                ? editorState.date\n                : editorState.occurrence.occurrenceDate\n            }\n            isRecurring={\n              editorState.mode === \"edit\" && editorState.occurrence.isRecurring\n            }\n            seriesValues={\n              editorState.mode === \"create\"\n                ? getCreateEditorValues(\n                    resolvedPlannerTimeZone,\n                    editorState.date,\n                    now ?? new Date().toISOString(),\n                    constraints,\n                  )\n                : getSeriesEditorValues(editorState.occurrence.slot)\n            }\n            occurrenceValues={\n              editorState.mode === \"create\"\n                ? { startTime: \"09:00\", durationMinutes: 60 }\n                : {\n                    startTime: editorState.occurrence.startTime,\n                    durationMinutes: editorState.occurrence.durationMinutes,\n                  }\n            }\n            taxonomy={resolvedTaxonomy}\n            constraints={constraints}\n            violations={editorViolations ?? undefined}\n            onDismiss={() => {\n              setEditorViolations(null);\n              setEditorState(null);\n            }}\n            onSubmit={handleEditorSubmit}\n            renderContent={renderers?.slotEditor}\n          />\n        ) : null}\n        {batchDialog === \"copy-day\" ? (\n          <SlotPlannerCopyDayDialog\n            targets={weekDays\n              .filter((date) => date !== currentFocusedDate)\n              .map((date) => ({\n                date,\n                label: `${weekdayFormatter.format(\n                  toUtcDate(date),\n                )} ${dayMonthFormatter.format(toUtcDate(date))}`,\n              }))}\n            taxonomy={resolvedTaxonomy}\n            onDismiss={() => setBatchDialog(null)}\n            onApply={(targetDates) => {\n              setBatchDialog(null);\n              copyDay(targetDates);\n            }}\n          />\n        ) : null}\n        {batchDialog === \"copy-week\" ? (\n          <SlotPlannerBatchConfirmDialog\n            title={formatSlotPlannerTemplate(\n              resolvedTaxonomy.copyWeekConfirmTitle,\n              nounTemplateTokens,\n            )}\n            body={formatSlotPlannerTemplate(\n              resolvedTaxonomy.copyWeekConfirmBody,\n              nounTemplateTokens,\n            )}\n            confirmLabel={resolvedTaxonomy.apply}\n            taxonomy={resolvedTaxonomy}\n            onDismiss={() => setBatchDialog(null)}\n            onConfirm={() => {\n              setBatchDialog(null);\n              copyWeek();\n            }}\n          />\n        ) : null}\n        {batchDialog === \"clear-day\" ? (\n          <SlotPlannerBatchConfirmDialog\n            title={formatSlotPlannerTemplate(\n              resolvedTaxonomy.clearDayConfirmTitle,\n              nounTemplateTokens,\n            )}\n            body={formatSlotPlannerTemplate(\n              resolvedTaxonomy.clearDayConfirmBody,\n              nounTemplateTokens,\n            )}\n            confirmLabel={resolvedTaxonomy.clearDay}\n            destructive\n            taxonomy={resolvedTaxonomy}\n            onDismiss={() => setBatchDialog(null)}\n            onConfirm={() => {\n              setBatchDialog(null);\n              clearDay();\n            }}\n          />\n        ) : null}\n        {deleteTarget ? (\n          <SlotPlannerDeleteDialog\n            isRecurring={deleteTarget.isRecurring}\n            taxonomy={resolvedTaxonomy}\n            onDismiss={() => setDeleteTarget(null)}\n            onDeleteOccurrence={handleConfirmDeleteOccurrence}\n            onDeleteSeries={handleConfirmDeleteSeries}\n          />\n        ) : null}\n        <div\n          aria-live=\"polite\"\n          data-slot=\"slot-planner-live-region\"\n          className=\"sr-only\"\n        >\n          {/* Keyed on the nonce so a repeated identical message still swaps the\n              text node and gets announced instead of being a silent no-op. */}\n          <span key={announcementNonce}>{announcement}</span>\n        </div>\n      </div>\n    </MotionConfig>\n  );\n}\n\nexport const SlotPlanner = forwardRef(SlotPlannerInner) as (<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  props: SlotPlannerProps<TData> & RefAttributes<HTMLDivElement>,\n) => ReactElement | null) & { displayName?: string };\n\nSlotPlanner.displayName = \"SlotPlanner\";\n"
    },
    {
      "path": "packages/components/src/components/slot-planner/slot-picker.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/slot-planner/slot-picker.tsx",
      "content": "import { parseDate } from \"@internationalized/date\";\nimport {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ForwardedRef,\n  type HTMLAttributes,\n  type KeyboardEvent,\n  type ReactElement,\n  type ReactNode,\n  type RefAttributes,\n} from \"react\";\nimport { MotionConfig, useReducedMotion } from \"motion/react\";\nimport { cn } from \"../../utils/cn\";\nimport { Button } from \"../button\";\nimport { IconButton } from \"../icon-button\";\nimport {\n  slotPlannerOccurrenceStatuses,\n  type SlotPlannerBookRequestPayload,\n  type SlotPlannerSlotData,\n  type SlotPlannerSlotPayload,\n  type SlotPlannerTaxonomy,\n  type SlotPlannerTaxonomyInput,\n} from \"./slot-planner-contract\";\nimport {\n  CalendarIcon,\n  ChevronIcon,\n  getConventionalSlotData,\n  getSlotPlannerDirection,\n  slotPlannerChipClasses,\n  slotPlannerDayPanelClasses,\n  slotPlannerDayRailClasses,\n  slotPlannerDayTabClasses,\n  slotPlannerDayTabContentClasses,\n  slotPlannerRootClasses,\n  slotPlannerSaveErrorClasses,\n  slotPlannerSavePendingClasses,\n  slotPlannerSlotActionButtonClasses,\n  slotPlannerSlotCardClasses,\n  slotPlannerStatusBadgeClasses,\n  slotPlannerStatusDotClasses,\n  slotPlannerToolbarClasses,\n  slotPlannerWeekPanelContentClasses,\n  toUtcDate,\n  useSlotPlannerRailOrientation,\n} from \"./slot-planner-dom-shared\";\nimport {\n  SlotPlannerDayTabContent,\n  SlotPlannerDayTabIndicator,\n  SlotPlannerWeekSlide,\n} from \"./slot-planner-motion\";\nimport type { SlotPlannerEmptyDayRenderContext } from \"./slot-planner-renderers\";\nimport {\n  expandSlotsForViewerZone,\n  formatSlotPlannerCountTemplate,\n  formatSlotPlannerTemplate,\n  getSlotPlannerIsoDateInZone,\n  getSlotPlannerWeekDays,\n  resolveSlotPlannerTaxonomy,\n  summarizeSlotPlannerDay,\n  type SlotPickerOccurrence,\n} from \"./slot-planner-utils\";\nimport { useSlotPlannerCrud, type SlotPlannerView } from \"./use-slot-planner\";\n\n/**\n * Context of one book-mode slot card. The renderer output becomes the\n * children of the structural `<li data-slot=\"slot-picker-slot-card\">`\n * wrapper; the wrapper keeps its `data-status` / `data-available` /\n * `data-pending` / `data-error` attributes.\n */\nexport type SlotPickerSlotCardRenderContext<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> = {\n  /** Renders the shipped default card content for decoration. */\n  renderDefault: () => ReactNode;\n  taxonomy: SlotPlannerTaxonomy;\n  /** The occurrence, projected into the viewer's time zone. */\n  occurrence: SlotPickerOccurrence<TData>;\n  /** True when the occurrence is requestable by this viewer. */\n  available: boolean;\n  /** True while this occurrence's book-request promise is pending. */\n  pending: boolean;\n  /** Request acknowledged during this mounted picker session; not server-owned identity. */\n  requestSent?: boolean;\n  /** True while the last book request for this occurrence failed. */\n  error: boolean;\n  /** Re-fires the failed request's identical payload. Present on error. */\n  retry?: () => void;\n  /** Fires the book request. Present only on available occurrences. */\n  request?: () => void;\n};\n\n/**\n * Optional render props for SlotPicker surfaces. Omitted surfaces keep their\n * shipped defaults; every context exposes `renderDefault()` so a renderer\n * can decorate the default instead of rebuilding it. Structural invariants\n * (day-rail tab semantics, the slot-card `<li>` wrapper, the live region)\n * stay outside the render slots.\n */\nexport interface SlotPickerRenderers<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> {\n  slotCard?: (context: SlotPickerSlotCardRenderContext<TData>) => ReactNode;\n  emptyDay?: (context: SlotPlannerEmptyDayRenderContext) => ReactNode;\n}\n\nexport interface SlotPickerProps<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> extends Omit<HTMLAttributes<HTMLDivElement>, \"children\" | \"title\"> {\n  /**\n   * The slot collection to browse. Read-only in book mode: SlotPicker never\n   * mutates it — a request only fires `onBookRequest` and the app updates\n   * the collection (e.g. bumps `requestedCount`) after handling it.\n   */\n  slots: SlotPlannerSlotData<TData>[];\n  /**\n   * IANA zone the occurrences are projected into for display and day\n   * grouping. Defaults to the environment zone\n   * (`Intl.DateTimeFormat().resolvedOptions().timeZone`); pass explicitly\n   * for deterministic renders and SSR.\n   */\n  viewerTimeZone?: string;\n  /**\n   * Fires when the viewer requests an available occurrence. The payload's\n   * `occurrenceDate` stays the PROVIDER-zone occurrence date — it identifies\n   * the occurrence in the shared slot model; the viewer-zone date is\n   * presentation only. A returned promise drives the per-occurrence\n   * pending/error/retry affordances.\n   */\n  onBookRequest?: (\n    payload: SlotPlannerBookRequestPayload,\n  ) => void | Promise<void>;\n  taxonomy?: SlotPlannerTaxonomyInput;\n  /** Controlled view projection. Pair with `onViewChange` for the default switcher. */\n  view?: SlotPlannerView;\n  /** Initial view for uncontrolled usage. */\n  defaultView?: SlotPlannerView;\n  onViewChange?: (view: SlotPlannerView) => void;\n  /** Controlled focused VIEWER-zone ISO date (`YYYY-MM-DD`). */\n  focusedDate?: string;\n  /**\n   * Initial focused viewer-zone ISO date for uncontrolled usage. Defaults\n   * to the viewer-zone \"today\" derived from `now`.\n   */\n  defaultFocusedDate?: string;\n  onFocusedDateChange?: (dateIso: string) => void;\n  /** ISO date-time treated as \"now\"; injectable for deterministic renders. */\n  now?: string;\n  locale?: string;\n  title?: ReactNode;\n  loading?: boolean;\n  error?: ReactNode;\n  /**\n   * Forces the reduced-motion rendering path. Defaults to the viewer's\n   * `prefers-reduced-motion` preference.\n   */\n  reducedMotion?: boolean;\n  renderers?: SlotPickerRenderers<TData>;\n}\n\n/** Statuses a book-mode viewer can see (draft/cancelled are never shown). */\nconst slotPickerVisibleStatuses = slotPlannerOccurrenceStatuses.filter(\n  (status) => status !== \"draft\" && status !== \"cancelled\",\n);\n\nfunction getSlotPickerOccurrenceKey(slotId: string, occurrenceDate: string) {\n  return `${slotId}::${occurrenceDate}`;\n}\n\nfunction getSlotPickerRemainingSeats(occurrence: SlotPickerOccurrence) {\n  return Math.max(\n    0,\n    occurrence.capacity - occurrence.bookedCount - occurrence.requestedCount,\n  );\n}\n\nfunction isSlotPickerOccurrenceAvailable(occurrence: SlotPickerOccurrence) {\n  return (\n    occurrence.status === \"requestable\" ||\n    (occurrence.status === \"requested\" &&\n      getSlotPickerRemainingSeats(occurrence) > 0)\n  );\n}\n\n/**\n * Default book-mode slot-card content. Rendered inside the structural `<li>`\n * wrapper — directly, or through a custom `slotCard` renderer's\n * `renderDefault()`.\n */\nfunction SlotPickerSlotCardContent<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>({\n  occurrence,\n  taxonomy,\n  locale,\n  pending,\n  requestSent = false,\n  onRetry,\n  onRequest,\n}: {\n  occurrence: SlotPickerOccurrence<TData>;\n  taxonomy: SlotPlannerTaxonomy;\n  locale: string;\n  pending: boolean;\n  requestSent?: boolean;\n  /** Present only while the last book request for this occurrence failed. */\n  onRetry?: (() => void) | undefined;\n  /** Present only on available (requestable) occurrences. */\n  onRequest?: (() => void) | undefined;\n}) {\n  const { note, tags } = getConventionalSlotData(occurrence.slot.data);\n  const showProviderContext = occurrence.timeZone !== occurrence.viewerTimeZone;\n  const remainingSeats = getSlotPickerRemainingSeats(occurrence);\n\n  return (\n    <>\n      <div className=\"flex flex-wrap items-center justify-between gap-[var(--dt-space-2)]\">\n        <span\n          data-slot=\"slot-picker-slot-time\"\n          className=\"text-sm font-semibold\"\n        >\n          {`${occurrence.viewerStartTime} – ${occurrence.viewerEndTime}`}\n        </span>\n        <span\n          data-slot=\"slot-picker-status-badge\"\n          data-status={occurrence.status}\n          className={slotPlannerStatusBadgeClasses}\n        >\n          <span\n            aria-hidden=\"true\"\n            className={cn(\n              \"size-1.5 rounded-full\",\n              slotPlannerStatusDotClasses[occurrence.status],\n            )}\n          />\n          {taxonomy.statusLabels[occurrence.status]}\n        </span>\n      </div>\n      {showProviderContext ? (\n        <p\n          data-slot=\"slot-picker-provider-time\"\n          className=\"text-muted-foreground text-xs\"\n        >\n          {formatSlotPlannerTemplate(taxonomy.providerTimeContext, {\n            time: occurrence.startTime,\n            timeZone: occurrence.timeZone,\n          })}\n        </p>\n      ) : null}\n      {tags.length > 0 ? (\n        <div className=\"flex flex-wrap items-center gap-[var(--dt-space-1)]\">\n          {tags.map((tag) => (\n            <span\n              key={tag}\n              data-slot=\"slot-picker-tag-chip\"\n              className={slotPlannerChipClasses}\n            >\n              {tag}\n            </span>\n          ))}\n        </div>\n      ) : null}\n      {note ? (\n        <p\n          data-slot=\"slot-picker-slot-note\"\n          className=\"text-muted-foreground text-sm leading-6\"\n        >\n          {note}\n        </p>\n      ) : null}\n      {onRequest && occurrence.capacity > 1 ? (\n        <p\n          data-slot=\"slot-picker-remaining-seats\"\n          className=\"text-muted-foreground text-xs\"\n        >\n          {formatSlotPlannerCountTemplate(\n            taxonomy.remainingSeats,\n            remainingSeats,\n            { remaining: remainingSeats },\n            locale,\n          )}\n        </p>\n      ) : null}\n      {occurrence.status === \"booked\" ? (\n        <p\n          data-slot=\"slot-picker-slot-full\"\n          className=\"text-muted-foreground text-xs font-medium\"\n        >\n          {taxonomy.slotFull}\n        </p>\n      ) : null}\n      {onRequest ? (\n        <button\n          type=\"button\"\n          data-slot=\"slot-picker-request\"\n          className={slotPlannerSlotActionButtonClasses}\n          aria-disabled={pending || requestSent || undefined}\n          data-disabled={pending || requestSent || undefined}\n          onClick={() => {\n            // `aria-disabled` keeps the button focusable across the request\n            // (native `disabled` would drop keyboard focus to the body); the\n            // guard stops a second activation from firing mid-request.\n            if (pending || requestSent) {\n              return;\n            }\n\n            onRequest();\n          }}\n        >\n          {requestSent\n            ? taxonomy.bookRequestSent\n            : formatSlotPlannerTemplate(taxonomy.requestSlot, {\n                slot: taxonomy.slot,\n                slotPlural: taxonomy.slotPlural,\n              })}\n        </button>\n      ) : null}\n      {pending ? (\n        <p\n          data-slot=\"slot-picker-request-pending\"\n          className={slotPlannerSavePendingClasses}\n        >\n          {taxonomy.bookRequestPending}\n        </p>\n      ) : null}\n      {onRetry ? (\n        <div\n          data-slot=\"slot-picker-request-error\"\n          className={slotPlannerSaveErrorClasses}\n        >\n          <span>{taxonomy.bookRequestError}</span>\n          <button\n            type=\"button\"\n            data-slot=\"slot-picker-retry\"\n            className={slotPlannerSlotActionButtonClasses}\n            onClick={onRetry}\n          >\n            {taxonomy.retry}\n          </button>\n        </div>\n      ) : null}\n    </>\n  );\n}\n\nfunction SlotPickerInner<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  {\n    className,\n    slots,\n    viewerTimeZone,\n    onBookRequest,\n    taxonomy,\n    view: controlledView,\n    defaultView = \"week\",\n    onViewChange,\n    focusedDate,\n    defaultFocusedDate,\n    onFocusedDateChange,\n    now,\n    locale = \"en\",\n    title,\n    loading = false,\n    error,\n    reducedMotion,\n    renderers,\n    ...props\n  }: SlotPickerProps<TData>,\n  ref: ForwardedRef<HTMLDivElement>,\n) {\n  const baseId = useId();\n  const panelId = `${baseId}-panel`;\n  const prefersReducedMotion = useReducedMotion();\n  const resolvedReducedMotion = reducedMotion ?? prefersReducedMotion === true;\n  const motionEnabled = !resolvedReducedMotion;\n  const [internalView, setInternalView] = useState(defaultView);\n  const currentView = controlledView ?? internalView;\n  const setView = useCallback(\n    (nextView: SlotPlannerView) => {\n      if (controlledView === undefined) {\n        setInternalView(nextView);\n      }\n\n      if (nextView !== currentView) {\n        onViewChange?.(nextView);\n      }\n    },\n    [controlledView, currentView, onViewChange],\n  );\n\n  // Both fallbacks are captured once so an unmounted-and-remounted render\n  // stays internally consistent; pass the props for deterministic renders.\n  const [environmentTimeZone] = useState(\n    () => new Intl.DateTimeFormat().resolvedOptions().timeZone,\n  );\n  const resolvedViewerTimeZone = viewerTimeZone ?? environmentTimeZone;\n  const [fallbackNow] = useState(() => new Date().toISOString());\n  const resolvedNow = now ?? fallbackNow;\n  const todayIso = getSlotPlannerIsoDateInZone(\n    Date.parse(resolvedNow),\n    resolvedViewerTimeZone,\n  );\n\n  const resolvedTaxonomy = useMemo(\n    () => resolveSlotPlannerTaxonomy(taxonomy),\n    [taxonomy],\n  );\n  const nounTemplateTokens = {\n    slot: resolvedTaxonomy.slot,\n    slotPlural: resolvedTaxonomy.slotPlural,\n  };\n\n  const [internalFocusedDate, setInternalFocusedDate] = useState(\n    () => defaultFocusedDate ?? todayIso,\n  );\n  const currentFocusedDate = focusedDate ?? internalFocusedDate;\n  const setFocusedDate = useCallback(\n    (nextDate: string) => {\n      if (focusedDate === undefined) {\n        setInternalFocusedDate(nextDate);\n      }\n\n      if (nextDate !== currentFocusedDate) {\n        onFocusedDateChange?.(nextDate);\n      }\n    },\n    [currentFocusedDate, focusedDate, onFocusedDateChange],\n  );\n\n  const weekDays = useMemo(\n    () => getSlotPlannerWeekDays(currentFocusedDate),\n    [currentFocusedDate],\n  );\n  const rangeStart = currentView === \"week\" ? weekDays[0]! : currentFocusedDate;\n  const rangeEnd = currentView === \"week\" ? weekDays[6]! : currentFocusedDate;\n  // Occurrences are expanded in the provider zone and bucketed by the\n  // VIEWER-zone date of their start instant; draft/cancelled never surface.\n  const occurrencesByViewerDate = useMemo(\n    () =>\n      expandSlotsForViewerZone(\n        slots,\n        rangeStart,\n        rangeEnd,\n        resolvedNow,\n        resolvedViewerTimeZone,\n      ),\n    [rangeEnd, rangeStart, resolvedNow, resolvedViewerTimeZone, slots],\n  );\n  const selectedOccurrences = occurrencesByViewerDate[currentFocusedDate] ?? [];\n  const isPastDay = currentFocusedDate < todayIso;\n\n  // Book requests reuse the CRUD pending/error/retry machinery, keyed per\n  // provider-zone occurrence identity.\n  const { pendingKeys, retryByKey, run } = useSlotPlannerCrud();\n  const [sentRequests, setSentRequests] = useState<ReadonlySet<string>>(\n    () => new Set(),\n  );\n  // The nonce advances on every announcement so identical consecutive\n  // messages still mutate the live region's DOM (via the keyed span) instead\n  // of being dropped by React's bail-out.\n  const [announcementState, setAnnouncementState] = useState<{\n    text: string;\n    nonce: number;\n  }>(() => ({ nonce: 0, text: \"\" }));\n  const setAnnouncement = useCallback((text: string) => {\n    setAnnouncementState((previous) => ({\n      nonce: previous.nonce + 1,\n      text,\n    }));\n  }, []);\n  const announcement = announcementState.text;\n  const announcementNonce = announcementState.nonce;\n\n  // Announces failures politely: a key newly appearing in retryByKey means\n  // its book-request promise rejected.\n  const previousRetryKeysRef = useRef<ReadonlySet<string>>(new Set());\n\n  useEffect(() => {\n    const previous = previousRetryKeysRef.current;\n    const currentKeys = new Set(retryByKey.keys());\n\n    previousRetryKeysRef.current = currentKeys;\n\n    for (const key of currentKeys) {\n      if (!previous.has(key)) {\n        setAnnouncement(\n          formatSlotPlannerTemplate(resolvedTaxonomy.bookRequestError, {\n            slot: resolvedTaxonomy.slot,\n            slotPlural: resolvedTaxonomy.slotPlural,\n          }),\n        );\n\n        return;\n      }\n    }\n  }, [resolvedTaxonomy, retryByKey, setAnnouncement]);\n\n  const requestOccurrence = (occurrence: SlotPickerOccurrence<TData>) => {\n    const key = getSlotPickerOccurrenceKey(\n      occurrence.slotId,\n      occurrence.occurrenceDate,\n    );\n    if (sentRequests.has(key)) return;\n    const payload: SlotPlannerBookRequestPayload = {\n      slotId: occurrence.slotId,\n      occurrenceDate: occurrence.occurrenceDate,\n      seats: 1,\n      viewerTimeZone: resolvedViewerTimeZone,\n    };\n\n    // Pending is announced immediately; a synchronous (or resolved) callback\n    // overwrites it with the success announcement in the same pass.\n    setAnnouncement(\n      formatSlotPlannerTemplate(\n        resolvedTaxonomy.bookRequestPending,\n        nounTemplateTokens,\n      ),\n    );\n    run({\n      key: getSlotPickerOccurrenceKey(\n        occurrence.slotId,\n        occurrence.occurrenceDate,\n      ),\n      execute: () => onBookRequest?.(payload),\n      onSuccess: () => {\n        setSentRequests((previous) => new Set(previous).add(key));\n        setAnnouncement(\n          formatSlotPlannerTemplate(\n            resolvedTaxonomy.announceBookRequested,\n            nounTemplateTokens,\n          ),\n        );\n      },\n    });\n  };\n\n  const rootRef = useRef<HTMLDivElement | null>(null);\n  const mergedRef = useCallback(\n    (node: HTMLDivElement | null) => {\n      rootRef.current = node;\n\n      if (typeof ref === \"function\") {\n        ref(node);\n      } else if (ref) {\n        ref.current = node;\n      }\n    },\n    [ref],\n  );\n\n  // Logical week-navigation direction, mirrored from the manage-mode\n  // SlotPlanner so both modes animate week changes identically.\n  const weekStart = weekDays[0]!;\n  const [weekNavigation, setWeekNavigation] = useState<{\n    start: string;\n    direction: -1 | 0 | 1;\n  }>({ start: weekStart, direction: 0 });\n  const weekDirection =\n    weekNavigation.start === weekStart\n      ? weekNavigation.direction\n      : weekStart > weekNavigation.start\n        ? 1\n        : -1;\n  if (weekNavigation.start !== weekStart) {\n    setWeekNavigation({ start: weekStart, direction: weekDirection });\n  }\n  const [isRtl, setIsRtl] = useState(false);\n\n  useEffect(() => {\n    if (rootRef.current) {\n      setIsRtl(getSlotPlannerDirection(rootRef.current) === \"rtl\");\n    }\n  }, []);\n\n  const weekSlideFactor = weekDirection * (isRtl ? -1 : 1);\n\n  const moveWeek = (weeks: number) => {\n    setFocusedDate(parseDate(currentFocusedDate).add({ weeks }).toString());\n  };\n  const goToPreviousWeek = () => moveWeek(-1);\n  const goToNextWeek = () => moveWeek(1);\n  const goToThisWeek = () => setFocusedDate(todayIso);\n\n  const tabRefs = useRef(new Map<string, HTMLButtonElement>());\n  const getTabId = (date: string) => `${baseId}-tab-${date}`;\n  const railOrientation = useSlotPlannerRailOrientation();\n  const handleRailKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    const direction = getSlotPlannerDirection(event.currentTarget);\n    const forwardKey = direction === \"rtl\" ? \"ArrowLeft\" : \"ArrowRight\";\n    const backwardKey = direction === \"rtl\" ? \"ArrowRight\" : \"ArrowLeft\";\n    const currentIndex = weekDays.indexOf(currentFocusedDate);\n    let nextIndex: number;\n\n    // ArrowUp/ArrowDown mirror the vertical (`md:` and up) rail layout, and\n    // stay accepted in the horizontal layout so navigation never depends on\n    // which axis the tablist happens to render along.\n    if (event.key === forwardKey || event.key === \"ArrowDown\") {\n      nextIndex = (currentIndex + 1) % weekDays.length;\n    } else if (event.key === backwardKey || event.key === \"ArrowUp\") {\n      nextIndex = (currentIndex + weekDays.length - 1) % weekDays.length;\n    } else if (event.key === \"Home\") {\n      nextIndex = 0;\n    } else if (event.key === \"End\") {\n      nextIndex = weekDays.length - 1;\n    } else {\n      return;\n    }\n\n    event.preventDefault();\n\n    const nextDate = weekDays[nextIndex]!;\n\n    setFocusedDate(nextDate);\n    tabRefs.current.get(nextDate)?.focus();\n  };\n\n  const weekdayFormatter = useMemo(\n    () =>\n      new Intl.DateTimeFormat(locale, { weekday: \"short\", timeZone: \"UTC\" }),\n    [locale],\n  );\n  const dayMonthFormatter = useMemo(\n    () =>\n      new Intl.DateTimeFormat(locale, {\n        day: \"numeric\",\n        month: \"short\",\n        timeZone: \"UTC\",\n      }),\n    [locale],\n  );\n  const longDateFormatter = useMemo(\n    () =>\n      new Intl.DateTimeFormat(locale, { dateStyle: \"full\", timeZone: \"UTC\" }),\n    [locale],\n  );\n\n  const formattedFocusedDate = longDateFormatter.format(\n    toUtcDate(currentFocusedDate),\n  );\n\n  // Mirrors the manage-mode planner's week-change announcement: navigating to\n  // a new week (week view) or a new day (day view) is announced politely.\n  // Pure view toggles and the first mount stay silent, and switching to a\n  // week already announced by the other view never re-fires.\n  const announcedNavRef = useRef<{\n    view: SlotPlannerView;\n    anchor: string;\n  } | null>(null);\n\n  useEffect(() => {\n    const anchor = currentView === \"day\" ? currentFocusedDate : weekStart;\n    const previous = announcedNavRef.current;\n\n    announcedNavRef.current = { anchor, view: currentView };\n\n    if (!previous || previous.view !== currentView) {\n      return;\n    }\n\n    if (previous.anchor === anchor) {\n      return;\n    }\n\n    setAnnouncement(\n      currentView === \"day\"\n        ? formattedFocusedDate\n        : formatSlotPlannerTemplate(resolvedTaxonomy.announceWeekChanged, {\n            slot: resolvedTaxonomy.slot,\n            slotPlural: resolvedTaxonomy.slotPlural,\n            weekStart,\n          }),\n    );\n  }, [\n    currentFocusedDate,\n    currentView,\n    formattedFocusedDate,\n    resolvedTaxonomy,\n    setAnnouncement,\n    weekStart,\n  ]);\n\n  const defaultEmptyDay = (\n    <p\n      data-slot=\"slot-picker-empty-day\"\n      className=\"text-muted-foreground text-sm\"\n    >\n      {resolvedTaxonomy.emptyDay}\n    </p>\n  );\n  const hasExternalError =\n    error !== undefined && error !== null && error !== false;\n  const externalErrorContent =\n    typeof error === \"boolean\" ? resolvedTaxonomy.error : error;\n  const defaultLoading = (\n    <p\n      role=\"status\"\n      data-slot=\"slot-picker-loading\"\n      className=\"text-muted-foreground text-sm\"\n    >\n      {resolvedTaxonomy.loading}\n    </p>\n  );\n  const defaultError = (\n    <div\n      role=\"alert\"\n      data-slot=\"slot-picker-error\"\n      className={slotPlannerSaveErrorClasses}\n    >\n      {externalErrorContent ?? resolvedTaxonomy.error}\n    </div>\n  );\n  const returnToCurrentLabel =\n    currentView === \"day\" ? resolvedTaxonomy.today : resolvedTaxonomy.thisWeek;\n  const returnToCurrentDisabled =\n    currentView === \"day\" && currentFocusedDate === todayIso;\n  const moveDay = (days: number) => {\n    setFocusedDate(parseDate(currentFocusedDate).add({ days }).toString());\n  };\n  const goToPreviousPeriod =\n    currentView === \"day\" ? () => moveDay(-1) : goToPreviousWeek;\n  const goToNextPeriod =\n    currentView === \"day\" ? () => moveDay(1) : goToNextWeek;\n  const previousPeriodLabel =\n    currentView === \"day\"\n      ? resolvedTaxonomy.previousDay\n      : resolvedTaxonomy.previousWeek;\n  const nextPeriodLabel =\n    currentView === \"day\"\n      ? resolvedTaxonomy.nextDay\n      : resolvedTaxonomy.nextWeek;\n\n  const availableCount = selectedOccurrences.filter((occurrence) =>\n    isSlotPickerOccurrenceAvailable(occurrence),\n  ).length;\n\n  const dayPanel = (\n    <div\n      role={currentView === \"week\" ? \"tabpanel\" : undefined}\n      id={currentView === \"week\" ? panelId : undefined}\n      aria-labelledby={\n        currentView === \"week\" ? getTabId(currentFocusedDate) : undefined\n      }\n      tabIndex={currentView === \"week\" ? 0 : -1}\n      data-slot=\"slot-picker-day-panel\"\n      data-past={isPastDay ? \"true\" : undefined}\n      data-loading={loading ? \"true\" : undefined}\n      data-error={hasExternalError ? \"true\" : undefined}\n      className={slotPlannerDayPanelClasses}\n    >\n      <SlotPlannerWeekSlide\n        motionEnabled={motionEnabled}\n        slideFactor={weekSlideFactor}\n        weekKey={weekStart}\n        className={slotPlannerWeekPanelContentClasses}\n      >\n        <h3\n          data-slot=\"slot-picker-day-heading\"\n          className=\"text-base font-semibold\"\n        >\n          {formattedFocusedDate}\n        </h3>\n        {isPastDay ? (\n          <p\n            data-slot=\"slot-picker-past-day\"\n            className=\"text-muted-foreground text-sm\"\n          >\n            {resolvedTaxonomy.pastDay}\n          </p>\n        ) : null}\n        {loading ? (\n          defaultLoading\n        ) : hasExternalError ? (\n          defaultError\n        ) : selectedOccurrences.length === 0 ? (\n          renderers?.emptyDay ? (\n            renderers.emptyDay({\n              date: currentFocusedDate,\n              isPast: isPastDay,\n              renderDefault: () => defaultEmptyDay,\n              taxonomy: resolvedTaxonomy,\n            })\n          ) : (\n            defaultEmptyDay\n          )\n        ) : (\n          <>\n            {availableCount === 0 ? (\n              <p\n                data-slot=\"slot-picker-all-unavailable\"\n                className=\"text-muted-foreground text-sm\"\n              >\n                {formatSlotPlannerTemplate(\n                  resolvedTaxonomy.noAvailableSlots,\n                  nounTemplateTokens,\n                )}\n              </p>\n            ) : null}\n            {/* eslint-disable-next-line jsx-a11y/no-redundant-roles -- Explicit list semantics preserve VoiceOver support when CSS removes list styling. */}\n            <ul\n              role=\"list\"\n              data-slot=\"slot-picker-slot-list\"\n              className=\"m-[var(--dt-space-0)] flex list-none flex-col gap-[var(--dt-space-2)] p-[var(--dt-space-0)]\"\n            >\n              {selectedOccurrences.map((occurrence) => {\n                const key = getSlotPickerOccurrenceKey(\n                  occurrence.slotId,\n                  occurrence.occurrenceDate,\n                );\n                const available = isSlotPickerOccurrenceAvailable(occurrence);\n                const requestSent = sentRequests.has(key);\n                const pending = pendingKeys.has(key);\n                const storedRetry = retryByKey.get(key);\n                const retry = storedRetry\n                  ? () => {\n                      setAnnouncement(\n                        formatSlotPlannerTemplate(\n                          resolvedTaxonomy.bookRequestPending,\n                          nounTemplateTokens,\n                        ),\n                      );\n                      storedRetry();\n                    }\n                  : undefined;\n                const request =\n                  available && !requestSent\n                    ? () => requestOccurrence(occurrence)\n                    : undefined;\n                const defaultCardContent = (\n                  <SlotPickerSlotCardContent\n                    occurrence={occurrence}\n                    taxonomy={resolvedTaxonomy}\n                    locale={locale}\n                    pending={pending}\n                    requestSent={requestSent}\n                    onRetry={retry}\n                    onRequest={requestSent ? () => {} : request}\n                  />\n                );\n\n                return (\n                  // The <li> wrapper and its data attributes are structural;\n                  // unavailable occurrences render no interactive control at\n                  // all — availability is conveyed by the status text.\n                  <li\n                    key={key}\n                    data-slot=\"slot-picker-slot-card\"\n                    data-status={occurrence.status}\n                    data-available={available ? \"true\" : undefined}\n                    data-pending={pending ? \"true\" : undefined}\n                    data-error={storedRetry ? \"true\" : undefined}\n                    className={cn(\n                      slotPlannerSlotCardClasses,\n                      !available && \"bg-muted/40 text-muted-foreground\",\n                    )}\n                  >\n                    {renderers?.slotCard\n                      ? renderers.slotCard({\n                          available,\n                          error: storedRetry !== undefined,\n                          occurrence,\n                          pending,\n                          requestSent,\n                          renderDefault: () => defaultCardContent,\n                          ...(request ? { request } : {}),\n                          ...(retry ? { retry } : {}),\n                          taxonomy: resolvedTaxonomy,\n                        })\n                      : defaultCardContent}\n                  </li>\n                );\n              })}\n            </ul>\n          </>\n        )}\n      </SlotPlannerWeekSlide>\n    </div>\n  );\n\n  return (\n    <MotionConfig reducedMotion={resolvedReducedMotion ? \"always\" : \"user\"}>\n      <div\n        {...props}\n        ref={mergedRef}\n        data-slot=\"slot-picker\"\n        data-view={currentView}\n        data-viewer-time-zone={resolvedViewerTimeZone}\n        data-reduced-motion={resolvedReducedMotion ? \"true\" : undefined}\n        data-week-direction={\n          weekDirection === 1\n            ? \"forward\"\n            : weekDirection === -1\n              ? \"backward\"\n              : \"none\"\n        }\n        className={cn(slotPlannerRootClasses, className)}\n      >\n        <div\n          data-slot=\"slot-picker-toolbar\"\n          className={slotPlannerToolbarClasses}\n        >\n          <div\n            data-slot=\"slot-picker-title\"\n            className=\"min-w-0 truncate text-base font-semibold sm:text-lg\"\n          >\n            {title}\n            <p className=\"text-muted-foreground mt-1 text-xs font-normal\">\n              {resolvedViewerTimeZone.replaceAll(\"_\", \" \")}\n            </p>\n          </div>\n          <div className=\"col-span-2 row-start-2 flex min-w-0 items-center justify-start gap-[var(--dt-space-1)] sm:col-span-1 sm:col-start-auto sm:row-auto sm:justify-center\">\n            <IconButton\n              aria-label={previousPeriodLabel}\n              size=\"sm\"\n              variant=\"outline\"\n              onClick={goToPreviousPeriod}\n            >\n              <ChevronIcon direction=\"backward\" />\n            </IconButton>\n            <Button\n              size=\"sm\"\n              variant=\"outline\"\n              leftIcon={<CalendarIcon />}\n              className=\"shadow-sm\"\n              disabled={returnToCurrentDisabled}\n              onClick={goToThisWeek}\n            >\n              {returnToCurrentLabel}\n            </Button>\n            <IconButton\n              aria-label={nextPeriodLabel}\n              size=\"sm\"\n              variant=\"outline\"\n              onClick={goToNextPeriod}\n            >\n              <ChevronIcon direction=\"forward\" />\n            </IconButton>\n          </div>\n          <div\n            role=\"group\"\n            aria-label={resolvedTaxonomy.viewSwitcherLabel}\n            data-slot=\"slot-picker-view-switch\"\n            className=\"border-border bg-background col-start-2 row-start-1 flex items-center gap-[var(--dt-space-1)] justify-self-end rounded-md border p-0.5 sm:col-auto sm:row-auto\"\n          >\n            {([\"week\", \"day\"] as const).map((option) => (\n              <button\n                key={option}\n                type=\"button\"\n                aria-pressed={currentView === option}\n                data-slot=\"slot-picker-view-switch-option\"\n                data-view={option}\n                className={cn(\n                  \"text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-ring focus-visible:ring-offset-background inline-flex h-7 items-center rounded px-[var(--dt-space-2)] text-xs font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none motion-safe:transition-colors motion-safe:duration-150\",\n                  currentView === option &&\n                    \"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground\",\n                )}\n                onClick={() => setView(option)}\n              >\n                {option === \"week\"\n                  ? resolvedTaxonomy.weekView\n                  : resolvedTaxonomy.dayView}\n              </button>\n            ))}\n          </div>\n        </div>\n        {currentView === \"week\" ? (\n          <div\n            data-slot=\"slot-picker-week-layout\"\n            className=\"grid min-w-0 gap-[var(--dt-space-3)] md:grid-cols-[10rem_minmax(0,1fr)]\"\n          >\n            {/* eslint-disable-next-line jsx-a11y/interactive-supports-focus -- The tablist delegates keyboard events to its roving, focusable tab buttons. */}\n            <div\n              role=\"tablist\"\n              aria-label={resolvedTaxonomy.weekRailLabel}\n              aria-orientation={railOrientation}\n              data-slot=\"slot-picker-day-rail\"\n              className={slotPlannerDayRailClasses}\n              onKeyDown={handleRailKeyDown}\n            >\n              {weekDays.map((date) => {\n                const selected = date === currentFocusedDate;\n                const summary = summarizeSlotPlannerDay(\n                  occurrencesByViewerDate[date] ?? [],\n                );\n\n                return (\n                  <button\n                    key={date}\n                    ref={(node) => {\n                      if (node) {\n                        tabRefs.current.set(date, node);\n                      } else {\n                        tabRefs.current.delete(date);\n                      }\n                    }}\n                    type=\"button\"\n                    role=\"tab\"\n                    id={getTabId(date)}\n                    aria-selected={selected}\n                    aria-controls={panelId}\n                    aria-current={date === todayIso ? \"date\" : undefined}\n                    tabIndex={selected ? 0 : -1}\n                    data-slot=\"slot-picker-day-tab\"\n                    data-selected={selected ? \"true\" : undefined}\n                    data-today={date === todayIso ? \"true\" : undefined}\n                    data-past={date < todayIso ? \"true\" : undefined}\n                    className={slotPlannerDayTabClasses}\n                    onClick={() => setFocusedDate(date)}\n                  >\n                    {selected ? (\n                      <SlotPlannerDayTabIndicator\n                        layoutId={`${baseId}-day-tab-indicator`}\n                        motionEnabled={motionEnabled}\n                      />\n                    ) : null}\n                    <SlotPlannerDayTabContent\n                      motionEnabled={motionEnabled}\n                      slideFactor={weekSlideFactor}\n                      className={slotPlannerDayTabContentClasses}\n                    >\n                      <span className=\"text-muted-foreground text-xs font-medium\">\n                        {weekdayFormatter.format(toUtcDate(date))}\n                      </span>\n                      <span className=\"text-sm font-semibold\">\n                        {dayMonthFormatter.format(toUtcDate(date))}\n                      </span>\n                      {slotPickerVisibleStatuses\n                        .filter((status) => summary[status] > 0)\n                        .map((status) => {\n                          const summaryText = formatSlotPlannerCountTemplate(\n                            resolvedTaxonomy.statusCountSummary,\n                            summary[status],\n                            {\n                              statusLabel:\n                                resolvedTaxonomy.statusLabels[status],\n                            },\n                            locale,\n                          );\n\n                          return (\n                            <span\n                              key={status}\n                              title={summaryText}\n                              data-slot=\"slot-picker-day-summary\"\n                              data-status={status}\n                              className=\"text-muted-foreground flex w-full min-w-0 items-center gap-[var(--dt-space-1)] overflow-hidden text-xs whitespace-nowrap\"\n                            >\n                              <span\n                                aria-hidden=\"true\"\n                                className={cn(\n                                  \"size-1.5 shrink-0 rounded-full\",\n                                  slotPlannerStatusDotClasses[status],\n                                )}\n                              />\n                              <span\n                                data-slot=\"slot-picker-day-summary-text\"\n                                className=\"min-w-0 truncate\"\n                              >\n                                {summaryText}\n                              </span>\n                            </span>\n                          );\n                        })}\n                    </SlotPlannerDayTabContent>\n                  </button>\n                );\n              })}\n            </div>\n            {dayPanel}\n          </div>\n        ) : (\n          dayPanel\n        )}\n        <div\n          aria-live=\"polite\"\n          data-slot=\"slot-picker-live-region\"\n          className=\"sr-only\"\n        >\n          {/* Keyed on the nonce so a repeated identical message still swaps the\n              text node and gets announced instead of being a silent no-op. */}\n          <span key={announcementNonce}>{announcement}</span>\n        </div>\n      </div>\n    </MotionConfig>\n  );\n}\n\nexport const SlotPicker = forwardRef(SlotPickerInner) as (<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  props: SlotPickerProps<TData> & RefAttributes<HTMLDivElement>,\n) => ReactElement | null) & { displayName?: string };\n\nSlotPicker.displayName = \"SlotPicker\";\n"
    },
    {
      "path": "packages/components/src/components/slot-planner/slot-planner-editor.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/slot-planner/slot-planner-editor.tsx",
      "content": "import {\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type FormEvent,\n  type ReactNode,\n} from \"react\";\nimport { parseDateTime, toZoned } from \"@internationalized/date\";\nimport { Button } from \"../button\";\nimport { Checkbox } from \"../checkbox\";\nimport {\n  AlertDialog,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"../dialog\";\nimport {\n  Field,\n  FieldControl,\n  FieldLabel,\n  FieldSet,\n  FieldLegend,\n} from \"../form-field\";\nimport { Input } from \"../input\";\nimport { NumberInput } from \"../number-input\";\nimport { RadioGroup, RadioGroupItem } from \"../radio-group\";\nimport { Select, SelectItem, type SelectItemData } from \"../select\";\nimport { TagInput } from \"../tag-input\";\nimport { Textarea } from \"../textarea\";\nimport type {\n  SlotPlannerTaxonomy,\n  SlotPlannerViolation,\n  SlotPlannerConstraints,\n} from \"./slot-planner-contract\";\nimport type {\n  SlotPlannerEditorRecurrence,\n  SlotPlannerEditorResult,\n  SlotPlannerEditorSeriesValues,\n} from \"./slot-planner-crud\";\nimport type { SlotPlannerSlotEditorRenderContext } from \"./slot-planner-renderers\";\nimport {\n  formatSlotPlannerTemplate,\n  formatSlotPlannerCountTemplate,\n} from \"./slot-planner-utils\";\n\ntype SlotPlannerEditorScope = SlotPlannerEditorResult[\"scope\"];\n\nconst editorFormClasses = \"grid gap-[var(--dt-space-2)]\";\n\nconst editorFieldsClasses =\n  \"grid gap-[var(--dt-space-4)] px-[var(--dt-space-6)]\";\n\nconst editorFieldRowClasses = \"grid gap-[var(--dt-space-4)] sm:grid-cols-2\";\n\nconst editorRadioOptionClasses = \"flex items-center gap-[var(--dt-space-2)]\";\n\nconst editorRadioLabelClasses = \"text-sm leading-none text-foreground\";\n\nconst editorViolationsClasses =\n  \"mx-[var(--dt-space-6)] flex flex-col gap-[var(--dt-space-1)] rounded-md border border-destructive/40 bg-destructive/10 p-[var(--dt-space-3)] text-sm text-destructive\";\n\n/**\n * Renders structured violations as taxonomy-phrased text inside a dialog.\n * `role=\"alert\"` so assistive tech announces a blocked save immediately;\n * text-only communication, never color alone.\n */\nexport function SlotPlannerViolationList({\n  taxonomy,\n  violations,\n}: {\n  taxonomy: SlotPlannerTaxonomy;\n  violations: SlotPlannerViolation[];\n}) {\n  return (\n    <div\n      role=\"alert\"\n      data-slot=\"slot-planner-violations\"\n      className={editorViolationsClasses}\n    >\n      <p className=\"font-medium\">{taxonomy.violationsHeading}</p>\n      <ul\n        data-slot=\"slot-planner-violation-list\"\n        className=\"m-[var(--dt-space-0)] flex list-none flex-col gap-[var(--dt-space-1)] p-[var(--dt-space-0)]\"\n      >\n        {violations.map((violation, index) => (\n          <li\n            key={`${violation.code}-${index}`}\n            data-violation={violation.code}\n          >\n            {formatSlotPlannerTemplate(\n              taxonomy.violationMessages[violation.code],\n              {\n                slot: taxonomy.slot,\n                slotPlural: taxonomy.slotPlural,\n                ...violation.params,\n              },\n            )}\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n\n// Cached because Intl.supportedValuesOf builds a fresh ~400-entry array per\n// call; null means the platform lacks the API and a free-text zone input is\n// rendered instead.\nlet cachedTimeZoneOptions: string[] | null | undefined;\n\nfunction getTimeZoneOptions(): string[] | null {\n  if (cachedTimeZoneOptions === undefined) {\n    cachedTimeZoneOptions =\n      typeof Intl.supportedValuesOf === \"function\"\n        ? Intl.supportedValuesOf(\"timeZone\")\n        : null;\n  }\n\n  return cachedTimeZoneOptions;\n}\n\nfunction parseMinutes(text: string, fallback: number, minimum: number) {\n  const parsed = Number.parseInt(text, 10);\n\n  return Number.isFinite(parsed) && parsed >= minimum ? parsed : fallback;\n}\n\nexport interface SlotPlannerEditorDialogProps {\n  mode: \"create\" | \"edit\";\n  /** Fixed ISO date: the focused day (create) or the occurrence (edit). */\n  date: string;\n  /** Whether the edited slot recurs; enables the scope choice. */\n  isRecurring: boolean;\n  /** Series-level initial values (create defaults or the slot definition). */\n  seriesValues: SlotPlannerEditorSeriesValues;\n  /** Override-resolved values of the edited occurrence. */\n  occurrenceValues: { startTime: string; durationMinutes: number };\n  taxonomy: SlotPlannerTaxonomy;\n  /** Violations from the last blocked submit; saving again revalidates. */\n  violations?: SlotPlannerViolation[];\n  onDismiss: () => void;\n  onSubmit: (result: SlotPlannerEditorResult) => void;\n  /**\n   * Custom editor content (the `slotEditor` render prop). Replaces the form\n   * area only; the Dialog shell, its title, and focus containment stay\n   * structural.\n   */\n  renderContent?: (context: SlotPlannerSlotEditorRenderContext) => ReactNode;\n  constraints?: SlotPlannerConstraints;\n}\n\nexport function SlotPlannerEditorDialog({\n  mode,\n  date,\n  isRecurring,\n  seriesValues,\n  occurrenceValues,\n  taxonomy,\n  violations,\n  onDismiss,\n  onSubmit,\n  renderContent,\n  constraints,\n}: SlotPlannerEditorDialogProps) {\n  const baseId = useId();\n  const advancedRef = useRef<HTMLDetailsElement>(null);\n  const asksScope = mode === \"edit\" && isRecurring;\n  const [scope, setScope] = useState<SlotPlannerEditorScope>(\n    asksScope ? \"occurrence\" : \"series\",\n  );\n  const scopedInitial = asksScope ? occurrenceValues : seriesValues;\n  const [startTime, setStartTime] = useState(scopedInitial.startTime);\n  const [durationText, setDurationText] = useState(\n    String(scopedInitial.durationMinutes),\n  );\n  const [capacityText, setCapacityText] = useState(\n    String(seriesValues.capacity),\n  );\n  const [bufferBeforeText, setBufferBeforeText] = useState(\n    String(seriesValues.bufferBeforeMinutes),\n  );\n  const [bufferAfterText, setBufferAfterText] = useState(\n    String(seriesValues.bufferAfterMinutes),\n  );\n  const [timeZone, setTimeZone] = useState(seriesValues.timeZone);\n  const [tags, setTags] = useState(seriesValues.tags);\n  const [note, setNote] = useState(seriesValues.note);\n  const [recurrence, setRecurrence] = useState(seriesValues.recurrence);\n  const [recurrenceUntil, setRecurrenceUntil] = useState(\n    seriesValues.recurrenceUntil,\n  );\n  const timeZoneItems = useMemo<SelectItemData[] | null>(() => {\n    const options = getTimeZoneOptions();\n\n    if (!options) {\n      return null;\n    }\n\n    const withCurrent = options.includes(timeZone)\n      ? options\n      : [timeZone, ...options];\n\n    return withCurrent.map((zone) => ({\n      label: zone,\n      textValue: zone,\n      value: zone,\n    }));\n  }, [timeZone]);\n\n  // Per-scope drafts of the two shared fields (start time and duration) so\n  // toggling occurrence → series → occurrence restores what the user typed\n  // instead of resetting to the untouched base values.\n  const scopeDraftsRef = useRef<\n    Record<SlotPlannerEditorScope, { startTime: string; durationText: string }>\n  >({\n    occurrence: {\n      durationText: String(occurrenceValues.durationMinutes),\n      startTime: occurrenceValues.startTime,\n    },\n    series: {\n      durationText: String(seriesValues.durationMinutes),\n      startTime: seriesValues.startTime,\n    },\n  });\n\n  const handleScopeChange = (value: string) => {\n    const nextScope: SlotPlannerEditorScope =\n      value === \"series\" ? \"series\" : \"occurrence\";\n\n    if (nextScope === scope) {\n      return;\n    }\n\n    // Stash the current scope's in-progress edits before restoring the\n    // target scope's stashed draft.\n    scopeDraftsRef.current[scope] = { durationText, startTime };\n\n    const restored = scopeDraftsRef.current[nextScope];\n\n    setScope(nextScope);\n    setStartTime(restored.startTime);\n    setDurationText(restored.durationText);\n  };\n\n  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n\n    // Cleared or out-of-range numeric fields (duration, capacity, buffers)\n    // must not silently coerce to fallback values; the native `required`/`min`\n    // constraints surface the problem and block the save instead.\n    const form = event.currentTarget;\n\n    if (!form.checkValidity()) {\n      if (advancedRef.current) advancedRef.current.open = true;\n      form.reportValidity();\n\n      return;\n    }\n\n    if (scope === \"occurrence\") {\n      onSubmit({\n        scope: \"occurrence\",\n        startTime,\n        durationMinutes: parseMinutes(\n          durationText,\n          occurrenceValues.durationMinutes,\n          1,\n        ),\n      });\n\n      return;\n    }\n\n    onSubmit({\n      scope: \"series\",\n      values: {\n        startTime,\n        durationMinutes: parseMinutes(\n          durationText,\n          seriesValues.durationMinutes,\n          1,\n        ),\n        capacity: parseMinutes(capacityText, seriesValues.capacity, 1),\n        bufferBeforeMinutes: parseMinutes(bufferBeforeText, 0, 0),\n        bufferAfterMinutes: parseMinutes(bufferAfterText, 0, 0),\n        timeZone,\n        tags,\n        note,\n        recurrence,\n        recurrenceUntil,\n      },\n    });\n  };\n\n  const title = formatSlotPlannerTemplate(\n    mode === \"create\" ? taxonomy.editorCreateTitle : taxonomy.editorEditTitle,\n    { slot: taxonomy.slot },\n  );\n  const recurrenceOptions: Array<{\n    label: string;\n    value: SlotPlannerEditorRecurrence;\n  }> = [\n    { label: taxonomy.recurrenceNone, value: \"none\" },\n    { label: taxonomy.recurringWeekly, value: \"weekly\" },\n    { label: taxonomy.recurringBiweekly, value: \"biweekly\" },\n  ];\n  let endTime = \"\";\n  try {\n    if (startTime && Number(durationText) > 0) {\n      const end = toZoned(parseDateTime(`${date}T${startTime}`), timeZone).add({\n        minutes: Number(durationText),\n      });\n      endTime = `${String(end.hour).padStart(2, \"0\")}:${String(end.minute).padStart(2, \"0\")}${end.toString().slice(0, 10) !== date ? ` (${end.toString().slice(0, 10)})` : \"\"}`;\n    }\n  } catch {\n    /* The constraint engine reports invalid wall-clock values on submit. */\n  }\n\n  const defaultContent = (\n    <form\n      data-slot=\"slot-planner-editor-form\"\n      className={editorFormClasses}\n      noValidate\n      onSubmit={handleSubmit}\n    >\n      <div className={editorFieldsClasses}>\n        {asksScope ? (\n          <FieldSet data-slot=\"slot-planner-edit-scope\">\n            <FieldLegend>{taxonomy.editScopeLegend}</FieldLegend>\n            <RadioGroup\n              aria-label={taxonomy.editScopeLegend}\n              value={scope}\n              onValueChange={handleScopeChange}\n            >\n              <div className={editorRadioOptionClasses}>\n                <RadioGroupItem\n                  id={`${baseId}-scope-occurrence`}\n                  value=\"occurrence\"\n                />\n                <label\n                  htmlFor={`${baseId}-scope-occurrence`}\n                  className={editorRadioLabelClasses}\n                >\n                  {taxonomy.editScopeOccurrence}\n                </label>\n              </div>\n              <div className={editorRadioOptionClasses}>\n                <RadioGroupItem id={`${baseId}-scope-series`} value=\"series\" />\n                <label\n                  htmlFor={`${baseId}-scope-series`}\n                  className={editorRadioLabelClasses}\n                >\n                  {taxonomy.editScopeSeries}\n                </label>\n              </div>\n            </RadioGroup>\n          </FieldSet>\n        ) : null}\n        <Field>\n          <FieldLabel>{taxonomy.fieldDate}</FieldLabel>\n          <FieldControl asChild>\n            <Input readOnly value={date} />\n          </FieldControl>\n        </Field>\n        <div className={editorFieldRowClasses}>\n          <Field>\n            <FieldLabel>{taxonomy.fieldStartTime}</FieldLabel>\n            <FieldControl asChild>\n              <Input\n                type=\"time\"\n                required\n                value={startTime}\n                onChange={(event) => setStartTime(event.target.value)}\n              />\n            </FieldControl>\n          </Field>\n          <Field>\n            <FieldLabel>{taxonomy.fieldDurationMinutes}</FieldLabel>\n            <FieldControl asChild>\n              <NumberInput\n                type=\"number\"\n                numberMode=\"numeric\"\n                min={1}\n                required\n                value={durationText}\n                onChange={(event) => setDurationText(event.target.value)}\n              />\n            </FieldControl>\n          </Field>\n        </div>\n        <div className=\"flex flex-wrap gap-2\">\n          {[30, 45, 60].map((minutes) => (\n            <Button\n              key={minutes}\n              type=\"button\"\n              size=\"sm\"\n              variant={Number(durationText) === minutes ? \"soft\" : \"outline\"}\n              aria-pressed={Number(durationText) === minutes}\n              disabled={\n                minutes < (constraints?.minDurationMinutes ?? 1) ||\n                minutes > (constraints?.maxDurationMinutes ?? Infinity) ||\n                minutes % (constraints?.durationIncrementMinutes ?? 1) !== 0\n              }\n              onClick={() => setDurationText(String(minutes))}\n            >\n              {formatSlotPlannerCountTemplate(\n                taxonomy.durationSummary,\n                minutes,\n              )}\n            </Button>\n          ))}\n        </div>\n        {endTime ? (\n          <p\n            className=\"text-muted-foreground text-sm\"\n            data-slot=\"slot-planner-end-preview\"\n          >\n            {formatSlotPlannerTemplate(taxonomy.endTimePreview, {\n              time: endTime,\n              timeZone,\n            })}\n          </p>\n        ) : null}\n        {scope === \"series\" ? (\n          <>\n            <Select\n              data-slot=\"slot-planner-recurrence\"\n              label={taxonomy.fieldRecurrence}\n              value={recurrence}\n              onValueChange={(value) =>\n                setRecurrence(value as SlotPlannerEditorRecurrence)\n              }\n            >\n              {recurrenceOptions.map((option) => (\n                <SelectItem key={option.value} value={option.value}>\n                  {option.label}\n                </SelectItem>\n              ))}\n            </Select>\n            <details\n              ref={advancedRef}\n              className=\"border-border rounded-md border p-3\"\n              open={violations?.length ? true : undefined}\n            >\n              <summary className=\"min-h-6 cursor-pointer text-sm font-medium\">\n                {taxonomy.moreOptions}\n              </summary>\n              <div className=\"mt-4 grid gap-4\">\n                <Field>\n                  <FieldLabel>{taxonomy.fieldCapacity}</FieldLabel>\n                  <FieldControl asChild>\n                    <NumberInput\n                      type=\"number\"\n                      numberMode=\"numeric\"\n                      min={1}\n                      required\n                      value={capacityText}\n                      onChange={(event) => setCapacityText(event.target.value)}\n                    />\n                  </FieldControl>\n                </Field>\n                <div className={editorFieldRowClasses}>\n                  <Field>\n                    <FieldLabel>{taxonomy.fieldBufferBeforeMinutes}</FieldLabel>\n                    <FieldControl asChild>\n                      <NumberInput\n                        type=\"number\"\n                        numberMode=\"numeric\"\n                        min={0}\n                        required\n                        value={bufferBeforeText}\n                        onChange={(event) =>\n                          setBufferBeforeText(event.target.value)\n                        }\n                      />\n                    </FieldControl>\n                  </Field>\n                  <Field>\n                    <FieldLabel>{taxonomy.fieldBufferAfterMinutes}</FieldLabel>\n                    <FieldControl asChild>\n                      <NumberInput\n                        type=\"number\"\n                        numberMode=\"numeric\"\n                        min={0}\n                        required\n                        value={bufferAfterText}\n                        onChange={(event) =>\n                          setBufferAfterText(event.target.value)\n                        }\n                      />\n                    </FieldControl>\n                  </Field>\n                </div>\n                {timeZoneItems ? (\n                  <Select\n                    data-slot=\"slot-planner-time-zone-select\"\n                    label={taxonomy.fieldTimeZone}\n                    items={timeZoneItems}\n                    value={timeZone}\n                    onValueChange={setTimeZone}\n                  >\n                    {(item) => (\n                      <SelectItem value={item.value}>{item.label}</SelectItem>\n                    )}\n                  </Select>\n                ) : (\n                  <Field>\n                    <FieldLabel>{taxonomy.fieldTimeZone}</FieldLabel>\n                    <FieldControl asChild>\n                      <Input\n                        value={timeZone}\n                        onChange={(event) => setTimeZone(event.target.value)}\n                      />\n                    </FieldControl>\n                  </Field>\n                )}\n                <TagInput\n                  label={taxonomy.fieldTags}\n                  value={tags}\n                  onValueChange={setTags}\n                />\n                <Field>\n                  <FieldLabel>{taxonomy.fieldNote}</FieldLabel>\n                  <FieldControl asChild>\n                    <Textarea\n                      rows={2}\n                      value={note}\n                      onChange={(event) => setNote(event.target.value)}\n                    />\n                  </FieldControl>\n                </Field>\n              </div>\n            </details>\n            {recurrence !== \"none\" ? (\n              <Field>\n                <FieldLabel>{taxonomy.recurrenceUntil}</FieldLabel>\n                <FieldControl asChild>\n                  <Input\n                    type=\"date\"\n                    value={recurrenceUntil}\n                    onChange={(event) => setRecurrenceUntil(event.target.value)}\n                  />\n                </FieldControl>\n              </Field>\n            ) : null}\n          </>\n        ) : null}\n      </div>\n      {violations && violations.length > 0 ? (\n        <SlotPlannerViolationList taxonomy={taxonomy} violations={violations} />\n      ) : null}\n      <DialogFooter className=\"border-border bg-background sticky bottom-0 z-10 border-t pt-4\">\n        <Button type=\"button\" variant=\"outline\" onClick={onDismiss}>\n          {taxonomy.cancel}\n        </Button>\n        <Button type=\"submit\">{taxonomy.save}</Button>\n      </DialogFooter>\n    </form>\n  );\n\n  return (\n    <Dialog\n      data-slot=\"slot-planner-editor\"\n      open\n      onOpenChange={(open) => {\n        if (!open) {\n          onDismiss();\n        }\n      }}\n    >\n      <DialogContent size=\"md\">\n        <DialogHeader>\n          <DialogTitle>{title}</DialogTitle>\n        </DialogHeader>\n        {renderContent\n          ? renderContent({\n              date,\n              dismiss: onDismiss,\n              isRecurring,\n              mode,\n              occurrenceValues,\n              renderDefault: () => defaultContent,\n              seriesValues,\n              submit: onSubmit,\n              taxonomy,\n              violations: violations ?? [],\n            })\n          : defaultContent}\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport interface SlotPlannerDeleteDialogProps {\n  /** Recurring targets choose occurrence-versus-series before confirming. */\n  isRecurring: boolean;\n  taxonomy: SlotPlannerTaxonomy;\n  onDismiss: () => void;\n  onDeleteOccurrence: () => void;\n  onDeleteSeries: () => void;\n}\n\nexport function SlotPlannerDeleteDialog({\n  isRecurring,\n  taxonomy,\n  onDismiss,\n  onDeleteOccurrence,\n  onDeleteSeries,\n}: SlotPlannerDeleteDialogProps) {\n  const [confirmingSeries, setConfirmingSeries] = useState(false);\n  const tokens = { slot: taxonomy.slot, slotPlural: taxonomy.slotPlural };\n  // Advancing to the series-confirm step unmounts the button that triggered\n  // it, so focus is moved onto the destructive confirm button instead of\n  // dropping to the document body.\n  const confirmSeriesRef = useRef<HTMLElement>(null);\n\n  useEffect(() => {\n    if (confirmingSeries) {\n      confirmSeriesRef.current?.focus();\n    }\n  }, [confirmingSeries]);\n\n  return (\n    <AlertDialog\n      data-slot=\"slot-planner-delete-dialog\"\n      open\n      onOpenChange={(open) => {\n        if (!open) {\n          onDismiss();\n        }\n      }}\n    >\n      <AlertDialogContent size=\"sm\">\n        <AlertDialogHeader>\n          <AlertDialogTitle>\n            {formatSlotPlannerTemplate(\n              confirmingSeries\n                ? taxonomy.deleteSeriesConfirmTitle\n                : taxonomy.deleteOccurrenceConfirmTitle,\n              tokens,\n            )}\n          </AlertDialogTitle>\n          {confirmingSeries ? (\n            <AlertDialogDescription>\n              {formatSlotPlannerTemplate(\n                taxonomy.deleteSeriesConfirmBody,\n                tokens,\n              )}\n            </AlertDialogDescription>\n          ) : null}\n        </AlertDialogHeader>\n        <AlertDialogFooter>\n          <Button type=\"button\" variant=\"outline\" onClick={onDismiss}>\n            {taxonomy.cancel}\n          </Button>\n          {confirmingSeries ? (\n            <Button\n              ref={confirmSeriesRef}\n              type=\"button\"\n              variant=\"destructive\"\n              onClick={onDeleteSeries}\n            >\n              {taxonomy.confirmDelete}\n            </Button>\n          ) : isRecurring ? (\n            <>\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                onClick={() => setConfirmingSeries(true)}\n              >\n                {taxonomy.deleteSeries}\n              </Button>\n              <Button\n                type=\"button\"\n                variant=\"destructive\"\n                onClick={onDeleteOccurrence}\n              >\n                {taxonomy.deleteOccurrence}\n              </Button>\n            </>\n          ) : (\n            <Button\n              type=\"button\"\n              variant=\"destructive\"\n              onClick={onDeleteOccurrence}\n            >\n              {taxonomy.confirmDelete}\n            </Button>\n          )}\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  );\n}\n\n/** One selectable copy-day target: an ISO date plus its rail-style label. */\nexport type SlotPlannerCopyTarget = {\n  /** ISO date (`YYYY-MM-DD`). */\n  date: string;\n  /** Visible label, e.g. \"Tue 7 Jul\" (weekday + date, like the day rail). */\n  label: string;\n};\n\nexport interface SlotPlannerCopyDayDialogProps {\n  /** The focused week's other days, in rail order. */\n  targets: SlotPlannerCopyTarget[];\n  taxonomy: SlotPlannerTaxonomy;\n  onDismiss: () => void;\n  /** Fires with the checked ISO dates, in rail order. */\n  onApply: (targetDates: string[]) => void;\n}\n\nexport function SlotPlannerCopyDayDialog({\n  targets,\n  taxonomy,\n  onDismiss,\n  onApply,\n}: SlotPlannerCopyDayDialogProps) {\n  const baseId = useId();\n  const [selected, setSelected] = useState<ReadonlySet<string>>(\n    () => new Set(),\n  );\n\n  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n\n    // Applying with no targets is a no-op batch; the disabled Apply button is\n    // the primary guard, and this early return backs it up.\n    if (selected.size === 0) {\n      return;\n    }\n\n    onApply(\n      targets.map(({ date }) => date).filter((date) => selected.has(date)),\n    );\n  };\n\n  return (\n    <Dialog\n      data-slot=\"slot-planner-copy-day-dialog\"\n      open\n      onOpenChange={(open) => {\n        if (!open) {\n          onDismiss();\n        }\n      }}\n    >\n      <DialogContent size=\"sm\">\n        <DialogHeader>\n          <DialogTitle>{taxonomy.copyDay}</DialogTitle>\n        </DialogHeader>\n        <form className={editorFormClasses} onSubmit={handleSubmit}>\n          <div className={editorFieldsClasses}>\n            <FieldSet data-slot=\"slot-planner-copy-day-targets\">\n              <FieldLegend>{taxonomy.copyDayTargetsLegend}</FieldLegend>\n              {targets.map((target) => (\n                <div key={target.date} className={editorRadioOptionClasses}>\n                  <Checkbox\n                    id={`${baseId}-target-${target.date}`}\n                    checked={selected.has(target.date)}\n                    onCheckedChange={(checked) => {\n                      setSelected((previous) => {\n                        const next = new Set(previous);\n\n                        if (checked === true) {\n                          next.add(target.date);\n                        } else {\n                          next.delete(target.date);\n                        }\n\n                        return next;\n                      });\n                    }}\n                  />\n                  <label\n                    htmlFor={`${baseId}-target-${target.date}`}\n                    className={editorRadioLabelClasses}\n                  >\n                    {target.label}\n                  </label>\n                </div>\n              ))}\n            </FieldSet>\n          </div>\n          <DialogFooter>\n            <Button type=\"button\" variant=\"outline\" onClick={onDismiss}>\n              {taxonomy.cancel}\n            </Button>\n            <Button type=\"submit\" disabled={selected.size === 0}>\n              {taxonomy.apply}\n            </Button>\n          </DialogFooter>\n        </form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport interface SlotPlannerBatchConfirmDialogProps {\n  title: string;\n  body: ReactNode;\n  confirmLabel: string;\n  /** Renders the confirm action as destructive (clear day). */\n  destructive?: boolean;\n  taxonomy: SlotPlannerTaxonomy;\n  onDismiss: () => void;\n  onConfirm: () => void;\n}\n\n/** Shared confirm step for copy-week and clear-day batch operations. */\nexport function SlotPlannerBatchConfirmDialog({\n  title,\n  body,\n  confirmLabel,\n  destructive = false,\n  taxonomy,\n  onDismiss,\n  onConfirm,\n}: SlotPlannerBatchConfirmDialogProps) {\n  return (\n    <AlertDialog\n      data-slot=\"slot-planner-batch-confirm-dialog\"\n      open\n      onOpenChange={(open) => {\n        if (!open) {\n          onDismiss();\n        }\n      }}\n    >\n      <AlertDialogContent size=\"sm\">\n        <AlertDialogHeader>\n          <AlertDialogTitle>{title}</AlertDialogTitle>\n          <AlertDialogDescription>{body}</AlertDialogDescription>\n        </AlertDialogHeader>\n        <AlertDialogFooter>\n          <Button type=\"button\" variant=\"outline\" onClick={onDismiss}>\n            {taxonomy.cancel}\n          </Button>\n          <Button\n            type=\"button\"\n            variant={destructive ? \"destructive\" : \"solid\"}\n            onClick={onConfirm}\n          >\n            {confirmLabel}\n          </Button>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/slot-planner/slot-planner-motion.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/slot-planner/slot-planner-motion.tsx",
      "content": "import { forwardRef, useState, type ReactNode } from \"react\";\nimport {\n  AnimatePresence,\n  motion,\n  useIsPresent,\n  type Transition,\n  type Variants,\n} from \"motion/react\";\n\n/**\n * SlotPlanner motion layer: shared transition constants and the motion\n * wrapper components the DOM layer composes around its structural elements.\n *\n * Everything here animates transform and opacity only, and every component\n * takes a `motionEnabled` flag so the DOM layer can collapse the whole layer\n * to plain elements under reduced motion. This module is the only\n * SlotPlanner module allowed to import from Motion — the headless hook,\n * constraints, CRUD, and contract modules stay Motion-free.\n *\n * The wrappers are structural: they sit outside/around the render-slot\n * output, so custom renderers are animated identically to the defaults and\n * cannot break the animation invariants.\n */\n\nconst slotPlannerMotionEase: [number, number, number, number] = [0.2, 0, 0, 1];\n\n/** Directional week slide of the day panel and day-tab contents. */\nexport const slotPlannerWeekTransition: Transition = {\n  duration: 0.22,\n  ease: slotPlannerMotionEase,\n};\n\n/** Slot-card entrance (also delayed per item for copy staggering). */\nexport const slotPlannerSlotEnterTransition: Transition = {\n  duration: 0.18,\n  ease: slotPlannerMotionEase,\n};\n\n/** Slot-card exit after deletion. */\nexport const slotPlannerSlotExitTransition: Transition = {\n  duration: 0.16,\n  ease: slotPlannerMotionEase,\n};\n\n/** Layout settling of the remaining cards after a deletion. */\nexport const slotPlannerLayoutTransition: Transition = {\n  duration: 0.2,\n  ease: slotPlannerMotionEase,\n};\n\n/** Shared-layout travel of the day-tab selection indicator. */\nexport const slotPlannerIndicatorTransition: Transition = {\n  duration: 0.2,\n  ease: slotPlannerMotionEase,\n};\n\n/** Cap-meter fill scaleX. */\nexport const slotPlannerCapFillTransition: Transition = {\n  duration: 0.3,\n  ease: slotPlannerMotionEase,\n};\n\n/** Opacity pulse on just-created/just-edited slot cards. */\nexport const slotPlannerHighlightTransition: Transition = {\n  duration: 1.2,\n  ease: \"easeInOut\",\n  times: [0, 0.2, 1],\n};\n\n/** Entrance delay per item for slots created by one copy batch. */\nexport const slotPlannerStaggerSeconds = 0.04;\n\nconst WEEK_SLIDE_OFFSET = 24;\nconst DAY_TAB_SLIDE_OFFSET = 12;\nconst SLOT_SLIDE_OFFSET = 8;\n\n// `custom` is the RTL-resolved slide factor: +1 slides in from the reading\n// end (next week), -1 from the opposite end, 0 falls back to a pure fade.\nconst slotPlannerWeekSlideVariants: Variants = {\n  enter: (slideFactor: number) => ({\n    opacity: 0,\n    x: slideFactor * WEEK_SLIDE_OFFSET,\n  }),\n  center: { opacity: 1, x: 0 },\n  exit: (slideFactor: number) => ({\n    opacity: 0,\n    x: slideFactor * -WEEK_SLIDE_OFFSET,\n  }),\n};\n\n// `custom` is the per-item stagger index of one copy batch.\nconst slotPlannerSlotItemVariants: Variants = {\n  enter: { opacity: 0, y: SLOT_SLIDE_OFFSET },\n  visible: (staggerIndex: number) => ({\n    opacity: 1,\n    y: 0,\n    transition: {\n      ...slotPlannerSlotEnterTransition,\n      delay: staggerIndex * slotPlannerStaggerSeconds,\n    },\n  }),\n  exit: {\n    opacity: 0,\n    y: -SLOT_SLIDE_OFFSET,\n    transition: slotPlannerSlotExitTransition,\n  },\n};\n\ntype SlotPlannerWeekSlideItemProps = {\n  children: ReactNode;\n  className: string;\n  slideFactor: number;\n};\n\nconst SlotPlannerWeekSlideItem = forwardRef<\n  HTMLDivElement,\n  SlotPlannerWeekSlideItemProps\n>(function SlotPlannerWeekSlideItem({ children, className, slideFactor }, ref) {\n  const isPresent = useIsPresent();\n\n  return (\n    <motion.div\n      ref={ref}\n      // Exiting week content is decorative while it fades out.\n      aria-hidden={isPresent ? undefined : true}\n      data-slot=\"slot-planner-week-panel\"\n      data-exiting={isPresent ? undefined : \"true\"}\n      className={className}\n      custom={slideFactor}\n      variants={slotPlannerWeekSlideVariants}\n      initial=\"enter\"\n      animate=\"center\"\n      exit=\"exit\"\n      transition={slotPlannerWeekTransition}\n    >\n      {children}\n    </motion.div>\n  );\n});\n\n/**\n * Direction-aware slide of the day-panel content, keyed by the focused week\n * start. `mode=\"popLayout\"` pops exiting content out of the flow, so rapid\n * repeated navigation never stacks panels in layout, and `custom` keeps\n * updating in-flight exits when the direction flips mid-animation.\n */\nexport function SlotPlannerWeekSlide({\n  children,\n  className,\n  motionEnabled,\n  slideFactor,\n  weekKey,\n}: {\n  children: ReactNode;\n  className: string;\n  motionEnabled: boolean;\n  slideFactor: number;\n  weekKey: string;\n}) {\n  if (!motionEnabled) {\n    return (\n      <div data-slot=\"slot-planner-week-panel\" className={className}>\n        {children}\n      </div>\n    );\n  }\n\n  return (\n    <motion.div\n      data-slot=\"slot-planner-week-viewport\"\n      className=\"relative min-w-0 overflow-hidden\"\n      layout=\"size\"\n      transition={{ layout: slotPlannerLayoutTransition }}\n    >\n      <AnimatePresence custom={slideFactor} initial={false} mode=\"popLayout\">\n        <SlotPlannerWeekSlideItem\n          key={weekKey}\n          className={className}\n          slideFactor={slideFactor}\n        >\n          {children}\n        </SlotPlannerWeekSlideItem>\n      </AnimatePresence>\n    </motion.div>\n  );\n}\n\n/**\n * Entrance slide of one day-tab's content. The structural tab buttons are\n * keyed by date and remount on week change, so a mount-time entrance is the\n * week transition for the rail. `slideFactor === 0` (initial render, and any\n * render before the first navigation) renders a plain span.\n */\nexport function SlotPlannerDayTabContent({\n  children,\n  className,\n  motionEnabled,\n  slideFactor,\n}: {\n  children: ReactNode;\n  className: string;\n  motionEnabled: boolean;\n  slideFactor: number;\n}) {\n  if (!motionEnabled || slideFactor === 0) {\n    return (\n      <span data-slot=\"slot-planner-day-tab-content\" className={className}>\n        {children}\n      </span>\n    );\n  }\n\n  return (\n    <motion.span\n      data-slot=\"slot-planner-day-tab-content\"\n      className={className}\n      initial={{ opacity: 0, x: slideFactor * DAY_TAB_SLIDE_OFFSET }}\n      animate={{ opacity: 1, x: 0 }}\n      transition={slotPlannerWeekTransition}\n    >\n      {children}\n    </motion.span>\n  );\n}\n\nconst slotPlannerDayTabIndicatorClasses =\n  \"pointer-events-none absolute inset-0 rounded-md border-2 border-ring\";\n\n/**\n * Shared-layout selection indicator rendered inside the selected day tab\n * only. The non-motion selection state stays on the tab itself\n * (`aria-selected`, `data-selected` styling); this border overlay is the\n * decorative layer that travels between tabs.\n */\nexport function SlotPlannerDayTabIndicator({\n  layoutId,\n  motionEnabled,\n}: {\n  layoutId: string;\n  motionEnabled: boolean;\n}) {\n  if (!motionEnabled) {\n    return (\n      <span\n        aria-hidden=\"true\"\n        data-slot=\"slot-planner-day-tab-indicator\"\n        className={slotPlannerDayTabIndicatorClasses}\n      />\n    );\n  }\n\n  return (\n    <motion.span\n      aria-hidden=\"true\"\n      data-slot=\"slot-planner-day-tab-indicator\"\n      className={slotPlannerDayTabIndicatorClasses}\n      layoutId={layoutId}\n      transition={slotPlannerIndicatorTransition}\n    />\n  );\n}\n\n/**\n * Token-colored opacity pulse over a just-created or just-edited slot card.\n * Opacity only — no background-color animation — over a `bg-primary` layer.\n */\nfunction SlotPlannerSlotHighlight({ onComplete }: { onComplete: () => void }) {\n  return (\n    <motion.span\n      aria-hidden=\"true\"\n      data-slot=\"slot-planner-slot-highlight\"\n      className=\"bg-primary pointer-events-none absolute inset-0 rounded-md\"\n      initial={{ opacity: 0 }}\n      animate={{ opacity: [0, 0.14, 0] }}\n      transition={slotPlannerHighlightTransition}\n      onAnimationComplete={onComplete}\n    />\n  );\n}\n\nexport interface SlotPlannerSlotListItemProps {\n  children: ReactNode;\n  className: string;\n  /** Animate in on mount (just created, or arriving from a copy batch). */\n  entrance: boolean;\n  /** Non-zero mounts a highlight pulse; bump it to pulse again. */\n  highlightNonce: number;\n  /** State that should trigger position-settling layout animation. */\n  layoutDependency: number;\n  motionEnabled: boolean;\n  onHighlightComplete: () => void;\n  /** Entrance delay index within one copy batch. */\n  staggerIndex: number;\n  tabIndex: number;\n  \"data-error\"?: \"true\";\n  \"data-locked\"?: \"true\";\n  \"data-pending\"?: \"true\";\n  \"data-status\": string;\n}\n\n/**\n * The structural slot-card `<li>` wrapper, animated. Enter/exit run under\n * the parent `AnimatePresence` with the stable `slotId::occurrenceDate` key;\n * `layout=\"position\"` settles the remaining cards after a deletion. The\n * dependency scopes layout measurement to list-count changes so switching\n * week/day views does not animate cards from their old rail-offset position.\n * Exiting cards turn `aria-hidden` so assistive tech ignores the fade-out.\n */\nexport const SlotPlannerSlotListItem = forwardRef<\n  HTMLLIElement,\n  SlotPlannerSlotListItemProps\n>(\n  (\n    {\n      children,\n      className,\n      entrance,\n      highlightNonce,\n      layoutDependency,\n      motionEnabled,\n      onHighlightComplete,\n      staggerIndex,\n      ...liProps\n    },\n    ref,\n  ) => {\n    const isPresent = useIsPresent();\n    // Entrance and stagger only apply at mount; capturing them keeps the\n    // rendered stagger state stable once the parent clears its bookkeeping.\n    const [{ entrance: mountEntrance, staggerIndex: mountStaggerIndex }] =\n      useState(() => ({ entrance, staggerIndex }));\n\n    if (!motionEnabled) {\n      return (\n        <li\n          {...liProps}\n          ref={ref}\n          data-slot=\"slot-planner-slot-card\"\n          className={className}\n        >\n          {children}\n        </li>\n      );\n    }\n\n    return (\n      <motion.li\n        {...liProps}\n        ref={ref}\n        aria-hidden={isPresent ? undefined : true}\n        data-slot=\"slot-planner-slot-card\"\n        data-exiting={isPresent ? undefined : \"true\"}\n        data-motion-stagger={\n          mountEntrance && mountStaggerIndex > 0 ? \"true\" : undefined\n        }\n        className={className}\n        layout=\"position\"\n        layoutDependency={layoutDependency}\n        custom={mountStaggerIndex}\n        variants={slotPlannerSlotItemVariants}\n        initial={mountEntrance ? \"enter\" : false}\n        animate=\"visible\"\n        exit=\"exit\"\n        transition={{ layout: slotPlannerLayoutTransition }}\n      >\n        {highlightNonce > 0 ? (\n          <SlotPlannerSlotHighlight\n            key={highlightNonce}\n            onComplete={onHighlightComplete}\n          />\n        ) : null}\n        {children}\n      </motion.li>\n    );\n  },\n);\n\nSlotPlannerSlotListItem.displayName = \"SlotPlannerSlotListItem\";\n\nconst slotPlannerCapMeterFillClasses =\n  \"h-full w-full origin-left rounded-full bg-primary rtl:origin-right\";\n\n/**\n * Cap-meter fill, animated via `scaleX` with a start-aligned, RTL-aware\n * transform origin. The meter's text stays the canonical communication; the\n * whole track is `aria-hidden` in the DOM layer.\n */\nexport function SlotPlannerCapMeterFill({\n  motionEnabled,\n  ratio,\n}: {\n  motionEnabled: boolean;\n  ratio: number;\n}) {\n  const clamped = Math.min(1, Math.max(0, ratio));\n\n  if (!motionEnabled) {\n    return (\n      <div\n        data-slot=\"slot-planner-cap-meter-fill\"\n        className={slotPlannerCapMeterFillClasses}\n        style={{ transform: `scaleX(${clamped})` }}\n      />\n    );\n  }\n\n  return (\n    <motion.div\n      data-slot=\"slot-planner-cap-meter-fill\"\n      className={slotPlannerCapMeterFillClasses}\n      initial={false}\n      animate={{ scaleX: clamped }}\n      transition={slotPlannerCapFillTransition}\n    />\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/slot-planner/slot-planner-dom-shared.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/slot-planner/slot-planner-dom-shared.tsx",
      "content": "import { parseDate } from \"@internationalized/date\";\nimport { useEffect, useState } from \"react\";\nimport { buttonClassNames } from \"../button\";\nimport type {\n  SlotPlannerConventionalSlotData,\n  SlotPlannerOccurrenceStatus,\n  SlotPlannerSlotPayload,\n} from \"./slot-planner-contract\";\n\n/**\n * Internal DOM-layer constants and helpers shared by the manage-mode\n * `SlotPlanner` and the book-mode `SlotPicker`. Both components render the\n * same tokenized visual language (toolbar, day rail, day panel, slot cards),\n * so the class recipes live here once. Not part of the public package API.\n */\n\nexport const slotPlannerRootClasses =\n  \"flex w-full min-w-0 flex-col gap-[var(--dt-space-3)] text-foreground\";\n\nexport const slotPlannerToolbarClasses =\n  \"grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-[var(--dt-space-2)] sm:grid-cols-[auto_1fr_auto]\";\n\nexport const slotPlannerToolbarButtonClasses = buttonClassNames({\n  className: \"shadow-sm\",\n  size: \"sm\",\n  variant: \"outline\",\n});\n\nexport const slotPlannerDayRailClasses =\n  \"flex min-w-0 gap-[var(--dt-space-2)] overflow-x-auto pb-[var(--dt-space-1)] md:min-h-full md:flex-col md:gap-0 md:overflow-visible md:rounded-md md:border md:border-border md:bg-background md:p-[var(--dt-space-1)] md:pb-[var(--dt-space-1)]\";\n\nexport const slotPlannerDayTabClasses =\n  \"relative flex h-24 w-28 shrink-0 flex-col items-start gap-[var(--dt-space-1)] overflow-hidden rounded-md border border-border bg-background p-[var(--dt-space-2)] text-left motion-safe:transition-colors motion-safe:duration-150 hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background data-[past=true]:text-muted-foreground data-[selected=true]:border-ring data-[selected=true]:bg-muted/40 data-[today=true]:border-primary/60 md:h-auto md:min-h-16 md:w-full md:border-transparent md:bg-transparent md:shadow-none md:after:absolute md:after:inset-x-[var(--dt-space-2)] md:after:bottom-0 md:after:h-px md:after:bg-border/50 md:after:content-[''] md:last:after:hidden md:data-[selected=true]:border-ring md:data-[selected=true]:bg-muted/50 md:data-[selected=true]:after:hidden\";\n\n// Inner day-tab content wrapper; the motion layer slides it in on week\n// changes while the structural tab button keeps the ARIA wiring.\nexport const slotPlannerDayTabContentClasses =\n  \"flex w-full min-w-0 flex-col items-start gap-[var(--dt-space-1)]\";\n\n// `relative` contains the popLayout-exiting week content during transitions.\nexport const slotPlannerDayPanelClasses =\n  \"relative flex min-w-0 flex-col rounded-md border border-border bg-background p-[var(--dt-space-3)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background sm:p-[var(--dt-space-4)]\";\n\n// Inner day-panel content wrapper; the motion layer slides it directionally\n// when the focused week changes.\nexport const slotPlannerWeekPanelContentClasses =\n  \"flex min-w-0 flex-col gap-[var(--dt-space-3)]\";\n\n// `relative` anchors the created/edited highlight overlay.\nexport const slotPlannerSlotCardClasses =\n  \"relative flex flex-col gap-[var(--dt-space-2)] rounded-md border border-border bg-background p-[var(--dt-space-3)] motion-safe:transition-colors motion-safe:duration-150 data-[locked=true]:border-border/70 data-[locked=true]:bg-muted/40 data-[locked=true]:text-muted-foreground\";\n\nexport const slotPlannerChipClasses =\n  \"inline-flex items-center rounded-full border border-border bg-muted px-[var(--dt-space-2)] py-[var(--dt-space-0-5)] text-xs font-medium text-muted-foreground\";\n\nexport const slotPlannerStatusBadgeClasses =\n  \"inline-flex items-center gap-[var(--dt-space-1)] rounded-full border border-border bg-background px-[var(--dt-space-2)] py-[var(--dt-space-0-5)] text-xs font-medium text-foreground\";\n\nexport const slotPlannerStatusDotClasses: Record<\n  SlotPlannerOccurrenceStatus,\n  string\n> = {\n  draft: \"bg-muted-foreground\",\n  requestable: \"bg-success\",\n  requested: \"bg-warning\",\n  booked: \"bg-primary\",\n  blocked: \"bg-destructive\",\n  expired: \"bg-muted-foreground\",\n  cancelled: \"bg-destructive\",\n};\n\nexport const slotPlannerSlotActionButtonClasses = buttonClassNames({\n  className: \"min-h-10 shadow-none\",\n  size: \"sm\",\n  variant: \"outline\",\n});\n\nexport const slotPlannerSavePendingClasses = \"text-xs text-muted-foreground\";\n\nexport const slotPlannerSaveErrorClasses =\n  \"flex flex-wrap items-center gap-[var(--dt-space-2)] text-xs font-medium text-destructive\";\n\nexport function ChevronIcon({\n  direction,\n}: {\n  direction: \"backward\" | \"forward\";\n}) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"size-4 rtl:-scale-x-100\"\n      fill=\"none\"\n      viewBox=\"0 0 24 24\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n    >\n      <path\n        d={direction === \"backward\" ? \"M15 6l-6 6 6 6\" : \"M9 6l6 6-6 6\"}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nexport function CalendarIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"size-4\"\n      fill=\"none\"\n      viewBox=\"0 0 24 24\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n    >\n      <path d=\"M8 2v4M16 2v4M3 10h18\" strokeLinecap=\"round\" />\n      <rect\n        x=\"3\"\n        y=\"4\"\n        width=\"18\"\n        height=\"18\"\n        rx=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nexport function CopyIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"size-4\"\n      fill=\"none\"\n      viewBox=\"0 0 24 24\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n    >\n      <rect x=\"8\" y=\"8\" width=\"12\" height=\"12\" rx=\"2\" />\n      <path d=\"M4 16V6a2 2 0 0 1 2-2h10\" strokeLinecap=\"round\" />\n    </svg>\n  );\n}\n\nexport function EditIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"size-4\"\n      fill=\"none\"\n      viewBox=\"0 0 24 24\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n    >\n      <path\n        d=\"M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nexport function PlusIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"size-4\"\n      fill=\"none\"\n      viewBox=\"0 0 24 24\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n    >\n      <path d=\"M12 5v14M5 12h14\" strokeLinecap=\"round\" />\n    </svg>\n  );\n}\n\nexport function TrashIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"size-4\"\n      fill=\"none\"\n      viewBox=\"0 0 24 24\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n    >\n      <path d=\"M3 6h18M8 6V4h8v2M10 11v6M14 11v6\" strokeLinecap=\"round\" />\n      <path\n        d=\"M6 6l1 16h10l1-16\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\n// The day rail stacks vertically from the `md` breakpoint up (see\n// `slotPlannerDayRailClasses`). Tailwind v4's default `md` is 48rem.\nconst SLOT_PLANNER_RAIL_VERTICAL_QUERY = \"(min-width: 48rem)\";\n\n/**\n * Tracks whether the day rail is laid out vertically (the `md:` and up layout)\n * so the tablist can advertise the matching `aria-orientation`. SSR-safe: the\n * horizontal (default) orientation is assumed until the client can read\n * `matchMedia`.\n */\nexport function useSlotPlannerRailOrientation(): \"horizontal\" | \"vertical\" {\n  const [vertical, setVertical] = useState(false);\n\n  useEffect(() => {\n    if (\n      typeof window === \"undefined\" ||\n      typeof window.matchMedia !== \"function\"\n    ) {\n      return;\n    }\n\n    const query = window.matchMedia(SLOT_PLANNER_RAIL_VERTICAL_QUERY);\n    const update = () => setVertical(query.matches);\n\n    update();\n    query.addEventListener(\"change\", update);\n\n    return () => query.removeEventListener(\"change\", update);\n  }, []);\n\n  return vertical ? \"vertical\" : \"horizontal\";\n}\n\nexport function getSlotPlannerDirection(element: HTMLElement) {\n  const explicitDir = element.closest(\"[dir]\")?.getAttribute(\"dir\");\n\n  if (explicitDir === \"rtl\" || explicitDir === \"ltr\") {\n    return explicitDir;\n  }\n\n  return window.getComputedStyle(element).direction === \"rtl\" ? \"rtl\" : \"ltr\";\n}\n\nexport function toUtcDate(dateIso: string) {\n  return parseDate(dateIso).toDate(\"UTC\");\n}\n\nexport function getConventionalSlotData(\n  data: SlotPlannerSlotPayload | undefined,\n) {\n  const conventional = (data ?? {}) as SlotPlannerConventionalSlotData;\n  const tags = Array.isArray(conventional.tags)\n    ? conventional.tags.filter((tag): tag is string => typeof tag === \"string\")\n    : [];\n  const note = typeof conventional.note === \"string\" ? conventional.note : \"\";\n\n  return { note, tags };\n}\n"
    },
    {
      "path": "packages/components/src/components/slot-planner/slot-planner-renderers.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/slot-planner/slot-planner-renderers.ts",
      "content": "import type { ReactNode } from \"react\";\nimport type {\n  SlotPlannerOccurrenceStatus,\n  SlotPlannerSlotPayload,\n  SlotPlannerTaxonomy,\n  SlotPlannerViolation,\n} from \"./slot-planner-contract\";\nimport type { SlotPlannerView } from \"./use-slot-planner\";\nimport type {\n  SlotPlannerEditorResult,\n  SlotPlannerEditorSeriesValues,\n} from \"./slot-planner-crud\";\nimport type { SlotPlannerOccurrence } from \"./slot-planner-utils\";\n\n/**\n * SlotPlanner render-prop contexts.\n *\n * Every context is serializable except for its functions: the data fields\n * are the same plain JSON shapes the public contract uses, and each context\n * carries the resolved taxonomy plus a `renderDefault()` escape hatch that\n * returns the shipped default for the surface — so a renderer can decorate\n * (wrap or augment) the default instead of rebuilding it.\n *\n * Structural invariants stay outside the render slots: the component keeps\n * ownership of the day-rail tablist/tab/tabpanel semantics (roving tabindex,\n * arrow keys), the slot-card `<li>` wrapper and its data attributes, the\n * polite live region, dialog focus containment and labelling, and\n * post-delete focus recovery. Renderers fill the content inside those\n * structural elements.\n */\n\n/**\n * Context of one slot card. The renderer output becomes the children of the\n * structural `<li data-slot=\"slot-planner-slot-card\">` wrapper; the wrapper\n * keeps its `data-status` / `data-locked` / `data-pending` / `data-error`\n * attributes. `occurrence.slot.data` is the raw payload bag with unknown\n * keys untouched.\n */\nexport type SlotPlannerSlotCardRenderContext<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> = {\n  /** Renders the shipped default card content for decoration. */\n  renderDefault: () => ReactNode;\n  taxonomy: SlotPlannerTaxonomy;\n  /** The fully resolved occurrence, including the raw slot definition. */\n  occurrence: SlotPlannerOccurrence<TData>;\n  /** Locked occurrences expose no edit/remove dispatchers. */\n  locked: boolean;\n  /** True while this occurrence's mutation callback promise is pending. */\n  pending: boolean;\n  /** True while the last mutation for this occurrence failed. */\n  error: boolean;\n  /** Re-fires the failed mutation's identical payload. Present on error. */\n  retry?: () => void;\n  /** Opens the structural editor dialog. Absent on locked occurrences. */\n  edit?: () => void;\n  /** Opens the structural delete confirm. Absent on locked occurrences. */\n  remove?: () => void;\n};\n\n/**\n * Context of one day-rail card. The renderer output becomes the children of\n * the structural `role=\"tab\"` button; selection, roving tabindex, and\n * arrow-key navigation stay with the component.\n */\nexport type SlotPlannerDayCardRenderContext<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> = {\n  /** Renders the shipped default tab content (weekday, date, summaries). */\n  renderDefault: () => ReactNode;\n  taxonomy: SlotPlannerTaxonomy;\n  /** ISO date (`YYYY-MM-DD`) of this day. */\n  date: string;\n  /** Whether this day is the focused (selected) day. */\n  selected: boolean;\n  isToday: boolean;\n  isPast: boolean;\n  /** Occurrence counts per derived status for this day. */\n  summary: Record<SlotPlannerOccurrenceStatus, number>;\n  /** This day's occurrences, sorted by wall-clock start. */\n  occurrences: SlotPlannerOccurrence<TData>[];\n};\n\n/**\n * Context of the day-panel heading area. The renderer replaces the default\n * `<h3>` region; the panel's accessible name comes from the selected tab,\n * so a custom heading cannot break the tabpanel labelling.\n */\nexport type SlotPlannerDayHeaderRenderContext<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> = {\n  /** Renders the shipped default heading. */\n  renderDefault: () => ReactNode;\n  taxonomy: SlotPlannerTaxonomy;\n  /** ISO date (`YYYY-MM-DD`) of the focused day. */\n  date: string;\n  /** The default long-format label, e.g. \"Monday, July 6, 2026\". */\n  formattedDate: string;\n  isToday: boolean;\n  isPast: boolean;\n  /** The focused day's occurrences, sorted by wall-clock start. */\n  occurrences: SlotPlannerOccurrence<TData>[];\n};\n\n/**\n * Context of the toolbar region (week view only). The renderer output\n * becomes the children of the structural toolbar wrapper. Besides the week\n * navigation the context exposes the day-level batch dispatchers, which\n * open the same structural confirm dialogs as the default day-action row,\n * so a custom toolbar can host copy/clear controls.\n */\nexport type SlotPlannerToolbarRenderContext = {\n  /** Renders the shipped default title and week-navigation controls. */\n  renderDefault: () => ReactNode;\n  taxonomy: SlotPlannerTaxonomy;\n  /** The `title` prop, untouched. */\n  title?: ReactNode;\n  /** Local ISO date (`YYYY-MM-DD`) derived from `now`. */\n  todayIso: string;\n  /** ISO date of the focused day. */\n  focusedDate: string;\n  /** The focused week's 7 ISO dates, Monday first. */\n  weekDays: string[];\n  /** Current projection of the same slot collection. */\n  view: SlotPlannerView;\n  setView: (view: SlotPlannerView) => void;\n  setFocusedDate: (dateIso: string) => void;\n  goToPreviousWeek: () => void;\n  goToNextWeek: () => void;\n  goToThisWeek: () => void;\n  /** Opens the structural copy-day target dialog for the focused day. */\n  copyDay: () => void;\n  /** Opens the structural copy-week confirm dialog. */\n  copyWeek: () => void;\n  /** Opens the structural clear-day confirm dialog for the focused day. */\n  clearDay: () => void;\n};\n\n/**\n * Context of the empty-day message, rendered when the focused day has no\n * occurrences. The renderer replaces the default paragraph; the structural\n * add-slot affordance stays outside this slot.\n */\nexport type SlotPlannerEmptyDayRenderContext = {\n  /** Renders the shipped default empty-day message. */\n  renderDefault: () => ReactNode;\n  taxonomy: SlotPlannerTaxonomy;\n  /** ISO date (`YYYY-MM-DD`) of the focused day. */\n  date: string;\n  isPast: boolean;\n};\n\n/**\n * Context of the daily cap meter, rendered only when\n * `constraints.dailyRequestableCap` is set. The renderer replaces the\n * default meter region. Cap-reached must stay communicated in text, never\n * by color alone.\n */\nexport type SlotPlannerCapMeterRenderContext = {\n  /** Renders the shipped default cap meter. */\n  renderDefault: () => ReactNode;\n  taxonomy: SlotPlannerTaxonomy;\n  /** ISO date (`YYYY-MM-DD`) of the focused day. */\n  date: string;\n  /** Published (requestable/requested/booked) occurrences for the primary cap. */\n  used: number;\n  cap: number;\n  reached: boolean;\n  /** The formatted primary cap text, e.g. \"Daily cap: 2 / 3 …\". */\n  text: string;\n  /** The formatted primary cap-reached violation message; set only when reached. */\n  reachedMessage?: string;\n  /** Focused-day cap info, present when `dailyRequestableCap` is configured. */\n  daily?: {\n    used: number;\n    cap: number;\n    reached: boolean;\n    text: string;\n    reachedMessage?: string;\n  };\n  /** Focused-week cap info, present when `weeklyRequestableCap` is configured. */\n  weekly?: {\n    used: number;\n    cap: number;\n    reached: boolean;\n    text: string;\n    reachedMessage?: string;\n  };\n};\n\n/**\n * Context of one tag chip on the default slot card. The renderer replaces\n * the default chip for each conventional `data.tags` entry; it is invoked by\n * the default card content, including via a custom slot card's\n * `renderDefault()`.\n */\nexport type SlotPlannerTagRenderContext<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> = {\n  /** Renders the shipped default chip. */\n  renderDefault: () => ReactNode;\n  taxonomy: SlotPlannerTaxonomy;\n  /** The tag string from the occurrence's conventional `data.tags`. */\n  tag: string;\n  /** The occurrence the chip belongs to. */\n  occurrence: SlotPlannerOccurrence<TData>;\n};\n\n/**\n * Context of the slot editor dialog content. The Dialog shell, its\n * labelling (title), and focus containment stay structural; the renderer\n * replaces the form area inside the dialog. Custom content owns its own\n * field state and submits a `SlotPlannerEditorResult` through `submit`,\n * which runs the same validate → callback → apply → announce pipeline as\n * the default form (violations returned by a blocked submit re-render\n * through this context on the next open state).\n */\nexport type SlotPlannerSlotEditorRenderContext = {\n  /** Renders the shipped default editor form. */\n  renderDefault: () => ReactNode;\n  taxonomy: SlotPlannerTaxonomy;\n  mode: \"create\" | \"edit\";\n  /** Fixed ISO date: the focused day (create) or the occurrence (edit). */\n  date: string;\n  /** Whether the edited slot recurs; enables occurrence/series scope. */\n  isRecurring: boolean;\n  /** Series-level initial values (create defaults or the slot definition). */\n  seriesValues: SlotPlannerEditorSeriesValues;\n  /** Override-resolved values of the edited occurrence. */\n  occurrenceValues: { startTime: string; durationMinutes: number };\n  /** Violations from the last blocked submit; empty otherwise. */\n  violations: SlotPlannerViolation[];\n  /** Validates and dispatches the result; keeps the dialog open on block. */\n  submit: (result: SlotPlannerEditorResult) => void;\n  /** Closes the dialog without saving. */\n  dismiss: () => void;\n};\n\n/**\n * Optional render props, one per SlotPlanner surface. Omitted surfaces keep\n * their shipped defaults; every context exposes `renderDefault()` so a\n * renderer can decorate the default instead of rebuilding it.\n */\nexport interface SlotPlannerRenderers<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> {\n  slotCard?: (context: SlotPlannerSlotCardRenderContext<TData>) => ReactNode;\n  dayCard?: (context: SlotPlannerDayCardRenderContext<TData>) => ReactNode;\n  dayHeader?: (context: SlotPlannerDayHeaderRenderContext<TData>) => ReactNode;\n  toolbar?: (context: SlotPlannerToolbarRenderContext) => ReactNode;\n  emptyDay?: (context: SlotPlannerEmptyDayRenderContext) => ReactNode;\n  capMeter?: (context: SlotPlannerCapMeterRenderContext) => ReactNode;\n  tag?: (context: SlotPlannerTagRenderContext<TData>) => ReactNode;\n  slotEditor?: (context: SlotPlannerSlotEditorRenderContext) => ReactNode;\n}\n"
    },
    {
      "path": "packages/components/src/components/slot-planner/use-slot-planner.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/slot-planner/use-slot-planner.ts",
      "content": "import { parseDate, parseDateTime, toZoned } from \"@internationalized/date\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n  countSlotPlannerPublishedOccurrences,\n  validateSlotPlannerSlot,\n  validateSlotPlannerSlots,\n} from \"./slot-planner-constraints\";\nimport type {\n  SlotPlannerBatchChangePayload,\n  SlotPlannerConstraints,\n  SlotPlannerCreatePayload,\n  SlotPlannerDeleteOccurrencePayload,\n  SlotPlannerDeleteSeriesPayload,\n  SlotPlannerOccurrenceRef,\n  SlotPlannerOccurrenceStatus,\n  SlotPlannerSlotData,\n  SlotPlannerSlotPayload,\n  SlotPlannerTaxonomy,\n  SlotPlannerTaxonomyInput,\n  SlotPlannerUpdatePayload,\n  SlotPlannerViolation,\n} from \"./slot-planner-contract\";\nimport {\n  applySlotPlannerBatch,\n  applySlotPlannerMutation,\n  createSlotFromEditorValues,\n  updateSlotFromEditorValues,\n  upsertSlotPlannerOccurrenceOverride,\n  type SlotPlannerEditorResult,\n  type SlotPlannerEditorSeriesValues,\n  type SlotPlannerMutation,\n} from \"./slot-planner-crud\";\nimport {\n  expandSlotsForRange,\n  formatSlotPlannerCountTemplate,\n  formatSlotPlannerTemplate,\n  getSlotPlannerIsoDateInZone,\n  getSlotPlannerWeekDays,\n  resolveSlotPlannerTaxonomy,\n  summarizeSlotPlannerDay,\n  type SlotPlannerOccurrence,\n  type SlotPlannerOccurrencesByDate,\n} from \"./slot-planner-utils\";\n\nexport type SlotPlannerView = \"week\" | \"day\";\n\n/** One in-flight or failed mutation, keyed per slot id + occurrence date. */\nexport type SlotPlannerCrudOperation = {\n  key: string;\n  /** Fires the app callback with its (identical-on-retry) payload. */\n  execute: () => void | Promise<void>;\n  /** Applies internal state, announces, and moves focus after success. */\n  onSuccess: () => void;\n};\n\n/**\n * Tracks per-key pending and error state for CRUD callbacks. Callbacks that\n * return promises drive pending affordances and settle into success (apply)\n * or a stored retry that re-fires the identical payload. Non-promise returns\n * succeed synchronously. The thrown error / rejection reason for the latest\n * failure of each key is captured in `errorByKey` and forwarded to the\n * optional `onError` callback.\n */\nexport function useSlotPlannerCrud(\n  onError?: (key: string, error: unknown) => void,\n) {\n  const [pendingKeys, setPendingKeys] = useState<ReadonlySet<string>>(\n    () => new Set(),\n  );\n  const [retryByKey, setRetryByKey] = useState<ReadonlyMap<string, () => void>>(\n    () => new Map(),\n  );\n  const [errorByKey, setErrorByKey] = useState<ReadonlyMap<string, unknown>>(\n    () => new Map(),\n  );\n  // Latest-ref so `run` can stay stable while still calling the freshest\n  // `onError` supplied by the consumer.\n  const onErrorRef = useRef(onError);\n  onErrorRef.current = onError;\n\n  const clearError = useCallback((key: string) => {\n    setRetryByKey((previous) => {\n      if (!previous.has(key)) {\n        return previous;\n      }\n\n      const next = new Map(previous);\n\n      next.delete(key);\n\n      return next;\n    });\n    setErrorByKey((previous) => {\n      if (!previous.has(key)) {\n        return previous;\n      }\n\n      const next = new Map(previous);\n\n      next.delete(key);\n\n      return next;\n    });\n  }, []);\n\n  const run = useCallback(\n    (operation: SlotPlannerCrudOperation) => {\n      const { key } = operation;\n      const removePending = () => {\n        setPendingKeys((previous) => {\n          const next = new Set(previous);\n\n          next.delete(key);\n\n          return next;\n        });\n      };\n      const recordFailure = (error: unknown) => {\n        setRetryByKey((previous) => {\n          const next = new Map(previous);\n\n          next.set(key, () => run(operation));\n\n          return next;\n        });\n        setErrorByKey((previous) => {\n          const next = new Map(previous);\n\n          next.set(key, error);\n\n          return next;\n        });\n        onErrorRef.current?.(key, error);\n      };\n\n      clearError(key);\n\n      let result: void | Promise<void>;\n\n      try {\n        result = operation.execute();\n      } catch (error) {\n        recordFailure(error);\n\n        return;\n      }\n\n      if (result && typeof result.then === \"function\") {\n        setPendingKeys((previous) => new Set(previous).add(key));\n        result.then(\n          () => {\n            removePending();\n            operation.onSuccess();\n          },\n          (reason: unknown) => {\n            removePending();\n            recordFailure(reason);\n          },\n        );\n\n        return;\n      }\n\n      operation.onSuccess();\n    },\n    [clearError],\n  );\n\n  return { clearError, errorByKey, pendingKeys, retryByKey, run };\n}\n\nfunction getEnvironmentTimeZone() {\n  return new Intl.DateTimeFormat().resolvedOptions().timeZone;\n}\n\nfunction getSlotPlannerOccurrenceKey(slotId: string, occurrenceDate: string) {\n  return `${slotId}::${occurrenceDate}`;\n}\n\n/** Pending/error key of create operations targeting a given ISO date. */\nexport function getSlotPlannerCreateKey(dateIso: string) {\n  return `create::${dateIso}`;\n}\n\n/** Pending/error key of batch (copy/clear) operations fired from an ISO date. */\nexport function getSlotPlannerBatchKey(dateIso: string) {\n  return `batch:${dateIso}`;\n}\n\n/**\n * Builds the non-recurring copy of one draft/requestable occurrence for\n * copy-day and copy-week. The resolved wall-clock start and duration (after\n * overrides), buffers, zone, and `data` are copied; recurrence and\n * booked/requested counts are dropped and the copy is always requestable.\n */\nfunction buildCopiedSlot<TData extends SlotPlannerSlotPayload>(\n  occurrence: SlotPlannerOccurrence<TData>,\n  targetDate: string,\n  id: string,\n): SlotPlannerSlotData<TData> {\n  const source = occurrence.slot;\n\n  return {\n    id,\n    date: targetDate,\n    startTime: occurrence.startTime,\n    durationMinutes: occurrence.durationMinutes,\n    timeZone: source.timeZone,\n    state: \"requestable\",\n    ...(source.capacity !== undefined ? { capacity: source.capacity } : {}),\n    ...(source.bufferBeforeMinutes !== undefined\n      ? { bufferBeforeMinutes: source.bufferBeforeMinutes }\n      : {}),\n    ...(source.bufferAfterMinutes !== undefined\n      ? { bufferAfterMinutes: source.bufferAfterMinutes }\n      : {}),\n    ...(source.data !== undefined ? { data: source.data } : {}),\n  };\n}\n\nfunction defaultGenerateSlotId() {\n  return crypto.randomUUID();\n}\n\nfunction isMutableAvailabilityStatus(status: SlotPlannerOccurrenceStatus) {\n  return (\n    status === \"draft\" || status === \"requestable\" || status === \"requested\"\n  );\n}\n\n/**\n * Options accepted by `useSlotPlanner`. A strict subset of\n * `SlotPlannerProps`: everything non-visual the planner needs — the slot\n * collection, focus, taxonomy, constraints, and the mutation callbacks.\n */\nexport interface UseSlotPlannerOptions<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> {\n  /** Controlled slot collection. */\n  slots?: SlotPlannerSlotData<TData>[];\n  /** Initial slot collection for uncontrolled usage. */\n  defaultSlots?: SlotPlannerSlotData<TData>[];\n  /** Controlled focused ISO date (`YYYY-MM-DD`). */\n  focusedDate?: string;\n  /** Initial focused ISO date for uncontrolled usage. Defaults to today. */\n  defaultFocusedDate?: string;\n  onFocusedDateChange?: (dateIso: string) => void;\n  onCreateSlot?: (\n    payload: SlotPlannerCreatePayload<TData>,\n  ) => void | Promise<void>;\n  onUpdateSlot?: (\n    payload: SlotPlannerUpdatePayload<TData>,\n  ) => void | Promise<void>;\n  onDeleteOccurrence?: (\n    payload: SlotPlannerDeleteOccurrencePayload,\n  ) => void | Promise<void>;\n  onDeleteSeries?: (\n    payload: SlotPlannerDeleteSeriesPayload,\n  ) => void | Promise<void>;\n  /** Declarative rules; see `SlotPlannerProps.constraints`. */\n  constraints?: SlotPlannerConstraints;\n  /** Fires for copy-day, copy-week, and clear-day batch operations. */\n  onBatchChange?: (\n    payload: SlotPlannerBatchChangePayload<TData>,\n  ) => void | Promise<void>;\n  /**\n   * Fires when a mutation callback throws or its promise rejects, with the\n   * failing operation's key and the thrown error / rejection reason. The retry\n   * affordance (`retryByKey`) still surfaces regardless; this is an additive\n   * hook for logging or toast surfaces.\n   */\n  onMutationError?: (key: string, error: unknown) => void;\n  /** Ids for slots created through the editor. Defaults to crypto.randomUUID. */\n  generateSlotId?: () => string;\n  taxonomy?: SlotPlannerTaxonomyInput;\n  /** Expansion range: the focused week or the focused day only. */\n  view?: SlotPlannerView;\n  /** ISO date-time treated as \"now\"; injectable for deterministic renders. */\n  now?: string;\n  /** IANA zone used for manage-mode \"today\" and past-day state. */\n  timeZone?: string;\n  locale?: string;\n}\n\n/** Per-action options accepted by the delete dispatchers. */\nexport interface UseSlotPlannerActionOptions {\n  /**\n   * Fires after the callback succeeds and internal state (uncontrolled) has\n   * applied — the component layer uses it to schedule focus recovery.\n   */\n  onApplied?: () => void;\n}\n\n/** Focused-day daily-cap meter data. */\nexport type SlotPlannerDailyCapInfo = {\n  /** Published (requestable/requested/booked) occurrences on the day. */\n  used: number;\n  cap: number;\n  reached: boolean;\n};\n\n/** Focused-week weekly-cap meter data. */\nexport type SlotPlannerWeeklyCapInfo = {\n  /** Published (requestable/requested/booked) occurrences in the focused week. */\n  used: number;\n  cap: number;\n  reached: boolean;\n};\n\n/**\n * Everything `useSlotPlanner` returns: resolved state and data (all\n * JSON-friendly) plus action dispatchers. The dispatchers wrap the same\n * validate → callback → apply → announce machinery the shipped component\n * uses, so a fully custom layout keeps identical payload and controlled /\n * uncontrolled semantics.\n */\nexport interface UseSlotPlannerReturn<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> {\n  /** Taxonomy after merging the partial input over the defaults. */\n  taxonomy: SlotPlannerTaxonomy;\n  /** Local ISO date (`YYYY-MM-DD`) derived from `now`. */\n  todayIso: string;\n  /** Currently focused ISO date. */\n  focusedDate: string;\n  setFocusedDate: (dateIso: string) => void;\n  /** The focused week's 7 ISO dates, Monday first. */\n  weekDays: string[];\n  goToPreviousWeek: () => void;\n  goToNextWeek: () => void;\n  /** Moves the focus back to today. */\n  goToThisWeek: () => void;\n  /** Resolved slot collection (the controlled prop or internal state). */\n  slots: SlotPlannerSlotData<TData>[];\n  /** Expanded occurrences for the visible range, grouped by ISO date. */\n  occurrencesByDate: SlotPlannerOccurrencesByDate<TData>;\n  /** The focused day's occurrences, sorted by wall-clock start. */\n  selectedOccurrences: SlotPlannerOccurrence<TData>[];\n  /** Counts one visible day's occurrences per derived status. */\n  summarizeDay: (\n    dateIso: string,\n  ) => Record<SlotPlannerOccurrenceStatus, number>;\n  /** Focused-day cap info; undefined without a `dailyRequestableCap`. */\n  dailyCap: SlotPlannerDailyCapInfo | undefined;\n  /** Focused-week cap info; undefined without a `weeklyRequestableCap`. */\n  weeklyCap: SlotPlannerWeeklyCapInfo | undefined;\n  /** Validates a candidate against the current collection and \"now\". */\n  validate: (candidate: SlotPlannerSlotData<TData>) => SlotPlannerViolation[];\n  /**\n   * Validates and dispatches a create from editor values. Returns the\n   * violations that blocked it, or `[]` once the callback has been fired.\n   * Targets the focused date unless `target.date` overrides it.\n   */\n  createSlot: (\n    values: SlotPlannerEditorSeriesValues,\n    target?: { date?: string },\n  ) => SlotPlannerViolation[];\n  /**\n   * Validates and dispatches an occurrence- or series-scope update built\n   * from an editor result. Returns the blocking violations, or `[]`.\n   */\n  updateSlot: (\n    occurrence: SlotPlannerOccurrence<TData>,\n    result: SlotPlannerEditorResult,\n  ) => SlotPlannerViolation[];\n  deleteOccurrence: (\n    occurrence: SlotPlannerOccurrence<TData>,\n    options?: UseSlotPlannerActionOptions,\n  ) => void;\n  deleteSeries: (\n    occurrence: SlotPlannerOccurrence<TData>,\n    options?: UseSlotPlannerActionOptions,\n  ) => void;\n  /** Copies the focused day's copyable occurrences to the target dates. */\n  copyDay: (targetDates: string[]) => void;\n  /** Copies the focused week's copyable occurrences one week forward. */\n  copyWeek: () => void;\n  /** Clears the focused day, keeping locked occurrences. */\n  clearDay: () => void;\n  /** Keys with an in-flight callback promise. */\n  pendingKeys: ReadonlySet<string>;\n  /** Retry dispatchers stored per failed key; re-fire identical payloads. */\n  retryByKey: ReadonlyMap<string, () => void>;\n  clearError: (key: string) => void;\n  /**\n   * Thrown error / rejection reason of the latest failure per key. An entry is\n   * present exactly while its `retryByKey` retry is; cleared on retry/success.\n   */\n  errors: ReadonlyMap<string, unknown>;\n  /** Stable pending/error key of one occurrence. */\n  occurrenceKey: (ref: SlotPlannerOccurrenceRef) => string;\n  /** Pending/error key of create operations targeting the focused date. */\n  createKey: string;\n  /** Pending/error key of batch operations fired from the focused date. */\n  batchKey: string;\n  /** Latest polite live-region message, phrased through the taxonomy. */\n  announcement: string;\n  /**\n   * Advances on every announcement, including repeats of an identical\n   * message. Renderers key the live region on it so repeated announcements\n   * still mutate the DOM and are read out.\n   */\n  announcementNonce: number;\n}\n\n/**\n * Headless SlotPlanner state: week navigation, occurrence expansion,\n * constraint validation, CRUD/batch dispatch with pending/error/retry\n * tracking, and taxonomy-phrased announcements. No JSX, no DOM, no Motion —\n * the shipped `SlotPlanner` component is a renderer over this hook, and a\n * fully custom layout can consume it directly.\n */\nexport function useSlotPlanner<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(options: UseSlotPlannerOptions<TData>): UseSlotPlannerReturn<TData> {\n  const {\n    constraints,\n    defaultFocusedDate,\n    defaultSlots,\n    focusedDate,\n    generateSlotId,\n    locale = \"en\",\n    now,\n    onBatchChange,\n    onCreateSlot,\n    onDeleteOccurrence,\n    onDeleteSeries,\n    onFocusedDateChange,\n    onMutationError,\n    onUpdateSlot,\n    slots,\n    taxonomy,\n    timeZone,\n    view = \"week\",\n  } = options;\n\n  const [fallbackNow] = useState(() => new Date().toISOString());\n  const [fallbackTimeZone] = useState(getEnvironmentTimeZone);\n  const resolvedTimeZone = timeZone ?? fallbackTimeZone;\n  const resolvedNow = now ?? fallbackNow;\n  const todayIso = getSlotPlannerIsoDateInZone(\n    Date.parse(resolvedNow),\n    resolvedTimeZone,\n  );\n  const resolvedTaxonomy = useMemo(\n    () => resolveSlotPlannerTaxonomy(taxonomy),\n    [taxonomy],\n  );\n  const [internalSlots, setInternalSlots] = useState<\n    SlotPlannerSlotData<TData>[]\n  >(() => defaultSlots ?? []);\n  const resolvedSlots = slots ?? internalSlots;\n  // Ref so promise callbacks resolve controlled-ness at settle time, not at\n  // the render that fired the mutation.\n  const slotsControlledRef = useRef(slots !== undefined);\n  slotsControlledRef.current = slots !== undefined;\n  const [internalFocusedDate, setInternalFocusedDate] = useState(\n    () => defaultFocusedDate ?? todayIso,\n  );\n  const currentFocusedDate = focusedDate ?? internalFocusedDate;\n  const setFocusedDate = useCallback(\n    (nextDate: string) => {\n      if (focusedDate === undefined) {\n        setInternalFocusedDate(nextDate);\n      }\n\n      if (nextDate !== currentFocusedDate) {\n        onFocusedDateChange?.(nextDate);\n      }\n    },\n    [currentFocusedDate, focusedDate, onFocusedDateChange],\n  );\n\n  const weekDays = useMemo(\n    () => getSlotPlannerWeekDays(currentFocusedDate),\n    [currentFocusedDate],\n  );\n  const rangeStart = view === \"week\" ? weekDays[0]! : currentFocusedDate;\n  const rangeEnd = view === \"week\" ? weekDays[6]! : currentFocusedDate;\n  const occurrencesByDate = useMemo(\n    () => expandSlotsForRange(resolvedSlots, rangeStart, rangeEnd, resolvedNow),\n    [rangeEnd, rangeStart, resolvedNow, resolvedSlots],\n  );\n  const selectedOccurrences = occurrencesByDate[currentFocusedDate] ?? [];\n\n  const { clearError, errorByKey, pendingKeys, retryByKey, run } =\n    useSlotPlannerCrud(onMutationError);\n  // The nonce advances on every announcement so identical consecutive\n  // messages still change state (and, via `announcementNonce`, still mutate\n  // the live region's DOM) instead of being dropped by React's bail-out.\n  const [announcementState, setAnnouncementState] = useState<{\n    text: string;\n    nonce: number;\n  }>(() => ({ nonce: 0, text: \"\" }));\n  const setAnnouncement = useCallback(\n    (next: string | ((current: string) => string)) => {\n      setAnnouncementState((previous) => ({\n        nonce: previous.nonce + 1,\n        text: typeof next === \"function\" ? next(previous.text) : next,\n      }));\n    },\n    [],\n  );\n  const announcement = announcementState.text;\n\n  const applyMutation = useCallback((mutation: SlotPlannerMutation<TData>) => {\n    // Controlled collections are never self-mutated: the app owns them and\n    // updates the `slots` prop after handling the callback.\n    if (!slotsControlledRef.current) {\n      setInternalSlots((previous) =>\n        applySlotPlannerMutation(previous, mutation),\n      );\n    }\n  }, []);\n\n  const announce = useCallback(\n    (template: string) => {\n      setAnnouncement(\n        formatSlotPlannerTemplate(template, {\n          slot: resolvedTaxonomy.slot,\n          slotPlural: resolvedTaxonomy.slotPlural,\n        }),\n      );\n    },\n    [resolvedTaxonomy],\n  );\n\n  const capLimit = constraints?.dailyRequestableCap;\n  const dailyCapUsed = useMemo(\n    () =>\n      capLimit === undefined\n        ? 0\n        : countSlotPlannerPublishedOccurrences(\n            resolvedSlots,\n            currentFocusedDate,\n            resolvedNow,\n          ),\n    [capLimit, currentFocusedDate, resolvedNow, resolvedSlots],\n  );\n  const dailyCapReached = capLimit !== undefined && dailyCapUsed >= capLimit;\n  const dailyCap: SlotPlannerDailyCapInfo | undefined =\n    capLimit === undefined\n      ? undefined\n      : { cap: capLimit, reached: dailyCapReached, used: dailyCapUsed };\n  const weeklyCapLimit = constraints?.weeklyRequestableCap;\n  const weeklyCapUsed = useMemo(\n    () =>\n      weeklyCapLimit === undefined\n        ? 0\n        : weekDays.reduce(\n            (total, date) =>\n              total +\n              countSlotPlannerPublishedOccurrences(\n                resolvedSlots,\n                date,\n                resolvedNow,\n              ),\n            0,\n          ),\n    [resolvedNow, resolvedSlots, weekDays, weeklyCapLimit],\n  );\n  const weeklyCapReached =\n    weeklyCapLimit !== undefined && weeklyCapUsed >= weeklyCapLimit;\n  const weeklyCap: SlotPlannerWeeklyCapInfo | undefined =\n    weeklyCapLimit === undefined\n      ? undefined\n      : { cap: weeklyCapLimit, reached: weeklyCapReached, used: weeklyCapUsed };\n  // Announces caps only when a mutation flips the same focused range from\n  // under-cap to at-cap; navigating onto an already-full range stays silent.\n  const capStateRef = useRef<{\n    date: string;\n    dailyReached: boolean;\n    weekStart: string;\n    weeklyReached: boolean;\n  } | null>(null);\n\n  useEffect(() => {\n    const previous = capStateRef.current;\n    const weekStart = weekDays[0]!;\n\n    capStateRef.current = {\n      dailyReached: dailyCapReached,\n      date: currentFocusedDate,\n      weeklyReached: weeklyCapReached,\n      weekStart,\n    };\n\n    if (!previous) {\n      return;\n    }\n\n    const messages: string[] = [];\n    const nounTokens = {\n      slot: resolvedTaxonomy.slot,\n      slotPlural: resolvedTaxonomy.slotPlural,\n    };\n\n    if (\n      previous.date === currentFocusedDate &&\n      !previous.dailyReached &&\n      dailyCapReached\n    ) {\n      messages.push(\n        formatSlotPlannerTemplate(\n          resolvedTaxonomy.announceDailyCapReached,\n          nounTokens,\n        ),\n      );\n    }\n\n    if (\n      previous.weekStart === weekStart &&\n      !previous.weeklyReached &&\n      weeklyCapReached\n    ) {\n      messages.push(\n        formatSlotPlannerTemplate(\n          resolvedTaxonomy.announceWeeklyCapReached,\n          nounTokens,\n        ),\n      );\n    }\n\n    if (messages.length > 0) {\n      setAnnouncement((current) =>\n        current ? `${current}. ${messages.join(\". \")}` : messages.join(\". \"),\n      );\n    }\n  }, [\n    currentFocusedDate,\n    dailyCapReached,\n    resolvedTaxonomy,\n    weekDays,\n    weeklyCapReached,\n  ]);\n\n  const summarizeDay = useCallback(\n    (dateIso: string) =>\n      summarizeSlotPlannerDay(occurrencesByDate[dateIso] ?? []),\n    [occurrencesByDate],\n  );\n\n  const moveWeek = (weeks: number) => {\n    setFocusedDate(parseDate(currentFocusedDate).add({ weeks }).toString());\n  };\n\n  const announcedWeekStartRef = useRef(weekDays[0]!);\n\n  useEffect(() => {\n    const weekStart = weekDays[0]!;\n\n    if (announcedWeekStartRef.current === weekStart) {\n      return;\n    }\n\n    announcedWeekStartRef.current = weekStart;\n    setAnnouncement(\n      formatSlotPlannerTemplate(resolvedTaxonomy.announceWeekChanged, {\n        slot: resolvedTaxonomy.slot,\n        slotPlural: resolvedTaxonomy.slotPlural,\n        weekStart,\n      }),\n    );\n  }, [resolvedTaxonomy, weekDays]);\n\n  const generateId = generateSlotId ?? defaultGenerateSlotId;\n\n  // Dispatcher bodies are re-created each render as closures over the current\n  // values, then routed through `latestRef` below so the *returned* dispatchers\n  // can be stable (empty-dependency `useCallback`) without going stale.\n  const runValidation = (candidate: SlotPlannerSlotData<TData>) =>\n    validateSlotPlannerSlot(candidate, {\n      constraints,\n      now: resolvedNow,\n      slots: resolvedSlots,\n    });\n\n  const createSlotImpl = (\n    values: SlotPlannerEditorSeriesValues,\n    target?: { date?: string },\n  ): SlotPlannerViolation[] => {\n    const date = target?.date ?? currentFocusedDate;\n    const slot = createSlotFromEditorValues<TData>(values, {\n      id: generateId(),\n      date,\n    });\n    const violations = runValidation(slot);\n\n    // Creating inventory is different from editing a historical series: its\n    // first occurrence must still be usable, even when no notice rule is set.\n    if (violations.length === 0) {\n      const start = toZoned(\n        parseDateTime(`${date}T${slot.startTime}`),\n        slot.timeZone,\n      )\n        .toDate()\n        .getTime();\n      const minNoticeMinutes = constraints?.minNoticeMinutes ?? 0;\n      if (start < Date.parse(resolvedNow) + minNoticeMinutes * 60_000) {\n        return [\n          {\n            code: \"min-notice\",\n            params: { date, startTime: slot.startTime, minNoticeMinutes },\n          },\n        ];\n      }\n    }\n\n    if (violations.length > 0) {\n      return violations;\n    }\n\n    const payload: SlotPlannerCreatePayload<TData> = { slot };\n\n    run({\n      key: getSlotPlannerCreateKey(date),\n      execute: () => onCreateSlot?.(payload),\n      onSuccess: () => {\n        applyMutation({ type: \"create\", slot });\n        announce(resolvedTaxonomy.announceCreated);\n      },\n    });\n\n    return [];\n  };\n\n  const updateSlotImpl = (\n    occurrence: SlotPlannerOccurrence<TData>,\n    result: SlotPlannerEditorResult,\n  ): SlotPlannerViolation[] => {\n    const previous = occurrence.slot;\n    const nextSlot =\n      result.scope === \"occurrence\"\n        ? previous.recurrence\n          ? upsertSlotPlannerOccurrenceOverride(previous, {\n              occurrenceDate: occurrence.occurrenceDate,\n              startTime: result.startTime,\n              durationMinutes: result.durationMinutes,\n            })\n          : // Non-recurring slots have no per-occurrence overrides, so the\n            // occurrence edit applies directly to the single slot instead of\n            // writing an override that would be a silent no-op.\n            {\n              ...previous,\n              startTime: result.startTime,\n              durationMinutes: result.durationMinutes,\n            }\n        : updateSlotFromEditorValues(previous, result.values);\n    const violations = runValidation(nextSlot);\n\n    if (violations.length > 0) {\n      return violations;\n    }\n\n    const payload: SlotPlannerUpdatePayload<TData> = {\n      slot: nextSlot,\n      previous,\n    };\n\n    run({\n      key: getSlotPlannerOccurrenceKey(\n        occurrence.slotId,\n        occurrence.occurrenceDate,\n      ),\n      execute: () => onUpdateSlot?.(payload),\n      onSuccess: () => {\n        applyMutation({ type: \"update\", slot: nextSlot });\n        announce(resolvedTaxonomy.announceUpdated);\n      },\n    });\n\n    return [];\n  };\n\n  const deleteOccurrenceImpl = (\n    occurrence: SlotPlannerOccurrence<TData>,\n    actionOptions?: UseSlotPlannerActionOptions,\n  ) => {\n    const payload: SlotPlannerDeleteOccurrencePayload = {\n      slotId: occurrence.slotId,\n      occurrenceDate: occurrence.occurrenceDate,\n    };\n\n    run({\n      key: getSlotPlannerOccurrenceKey(\n        occurrence.slotId,\n        occurrence.occurrenceDate,\n      ),\n      execute: () => onDeleteOccurrence?.(payload),\n      onSuccess: () => {\n        applyMutation({ type: \"delete-occurrence\", ...payload });\n        announce(resolvedTaxonomy.announceDeleted);\n        actionOptions?.onApplied?.();\n      },\n    });\n  };\n\n  const deleteSeriesImpl = (\n    occurrence: SlotPlannerOccurrence<TData>,\n    actionOptions?: UseSlotPlannerActionOptions,\n  ) => {\n    const payload: SlotPlannerDeleteSeriesPayload = {\n      slotId: occurrence.slotId,\n    };\n\n    run({\n      key: getSlotPlannerOccurrenceKey(\n        occurrence.slotId,\n        occurrence.occurrenceDate,\n      ),\n      execute: () => onDeleteSeries?.(payload),\n      onSuccess: () => {\n        applyMutation({ type: \"delete-series\", slotId: payload.slotId });\n        announce(resolvedTaxonomy.announceSeriesDeleted);\n        actionOptions?.onApplied?.();\n      },\n    });\n  };\n\n  const announceBatchResult = (\n    payload: SlotPlannerBatchChangePayload<TData>,\n  ) => {\n    const nounTokens = {\n      slot: resolvedTaxonomy.slot,\n      slotPlural: resolvedTaxonomy.slotPlural,\n    };\n    const applied = formatSlotPlannerCountTemplate(\n      resolvedTaxonomy.announceBatchApplied,\n      payload.createdSlots.length,\n      nounTokens,\n      locale,\n    );\n    const rejectedCount = Object.keys(payload.violations).length;\n\n    setAnnouncement(\n      rejectedCount > 0\n        ? `${applied}. ${formatSlotPlannerCountTemplate(\n            resolvedTaxonomy.announceBatchRejected,\n            rejectedCount,\n            nounTokens,\n            locale,\n          )}`\n        : applied,\n    );\n  };\n\n  // Copy/clear operations share one run() key per focused date, so the\n  // pending/error affordance renders on the day-action row that fired it.\n  const runBatch = (\n    payload: SlotPlannerBatchChangePayload<TData>,\n    onApplied: (payload: SlotPlannerBatchChangePayload<TData>) => void,\n  ) => {\n    run({\n      key: getSlotPlannerBatchKey(currentFocusedDate),\n      execute: () => onBatchChange?.(payload),\n      onSuccess: () => {\n        // Controlled collections fire the callback but never self-mutate.\n        if (!slotsControlledRef.current) {\n          setInternalSlots((previous) =>\n            applySlotPlannerBatch(previous, payload),\n          );\n        }\n\n        onApplied(payload);\n      },\n    });\n  };\n\n  const buildCopyPayload = (\n    candidates: SlotPlannerSlotData<TData>[],\n  ): SlotPlannerBatchChangePayload<TData> => {\n    const violations = validateSlotPlannerSlots(candidates, {\n      constraints,\n      now: resolvedNow,\n      slots: resolvedSlots,\n    });\n\n    return {\n      createdSlots: candidates.filter(\n        (candidate) => !(candidate.id in violations),\n      ),\n      deletedSlotIds: [],\n      violations,\n    };\n  };\n\n  const copyDayImpl = (targetDates: string[]) => {\n    const sources = selectedOccurrences.filter((occurrence) =>\n      isMutableAvailabilityStatus(occurrence.status),\n    );\n    const candidates = [...targetDates]\n      .sort()\n      .flatMap((targetDate) =>\n        sources.map((occurrence) =>\n          buildCopiedSlot(occurrence, targetDate, generateId()),\n        ),\n      );\n\n    runBatch(buildCopyPayload(candidates), announceBatchResult);\n  };\n\n  const copyWeekImpl = () => {\n    // Day view still copies the whole focused week, so the week's\n    // occurrences are expanded on demand rather than reusing the view range.\n    const weekOccurrences = expandSlotsForRange(\n      resolvedSlots,\n      weekDays[0]!,\n      weekDays[6]!,\n      resolvedNow,\n    );\n    const candidates = weekDays.flatMap((day) =>\n      (weekOccurrences[day] ?? [])\n        .filter((occurrence) => isMutableAvailabilityStatus(occurrence.status))\n        .map((occurrence) =>\n          buildCopiedSlot(\n            occurrence,\n            parseDate(day).add({ days: 7 }).toString(),\n            generateId(),\n          ),\n        ),\n    );\n\n    runBatch(buildCopyPayload(candidates), announceBatchResult);\n  };\n\n  const clearDayImpl = () => {\n    const deletedSlotIds: string[] = [];\n    const updatedSlots: SlotPlannerSlotData<TData>[] = [];\n\n    for (const occurrence of selectedOccurrences) {\n      // Locked statuses (booked, blocked, expired, cancelled) are skipped\n      // and kept.\n      if (!isMutableAvailabilityStatus(occurrence.status)) {\n        continue;\n      }\n\n      if (occurrence.isRecurring) {\n        updatedSlots.push(\n          upsertSlotPlannerOccurrenceOverride(occurrence.slot, {\n            cancelled: true,\n            occurrenceDate: occurrence.occurrenceDate,\n          }),\n        );\n      } else {\n        deletedSlotIds.push(occurrence.slotId);\n      }\n    }\n\n    const payload: SlotPlannerBatchChangePayload<TData> = {\n      createdSlots: [],\n      deletedSlotIds,\n      ...(updatedSlots.length > 0 ? { updatedSlots } : {}),\n      violations: {},\n    };\n\n    runBatch(payload, () => {\n      announce(resolvedTaxonomy.announceDayCleared);\n    });\n  };\n\n  // Latest-ref of the freshly-closed-over dispatcher bodies, refreshed every\n  // render. The returned dispatchers below read through it, so their own\n  // identities stay stable while still running against current state.\n  const latestRef = useRef<{\n    clearDayImpl: () => void;\n    copyDayImpl: (targetDates: string[]) => void;\n    copyWeekImpl: () => void;\n    createSlotImpl: (\n      values: SlotPlannerEditorSeriesValues,\n      target?: { date?: string },\n    ) => SlotPlannerViolation[];\n    deleteOccurrenceImpl: (\n      occurrence: SlotPlannerOccurrence<TData>,\n      options?: UseSlotPlannerActionOptions,\n    ) => void;\n    deleteSeriesImpl: (\n      occurrence: SlotPlannerOccurrence<TData>,\n      options?: UseSlotPlannerActionOptions,\n    ) => void;\n    moveWeek: (weeks: number) => void;\n    runValidation: (\n      candidate: SlotPlannerSlotData<TData>,\n    ) => SlotPlannerViolation[];\n    setFocusedDate: (dateIso: string) => void;\n    todayIso: string;\n    updateSlotImpl: (\n      occurrence: SlotPlannerOccurrence<TData>,\n      result: SlotPlannerEditorResult,\n    ) => SlotPlannerViolation[];\n  }>(null!);\n  latestRef.current = {\n    clearDayImpl,\n    copyDayImpl,\n    copyWeekImpl,\n    createSlotImpl,\n    deleteOccurrenceImpl,\n    deleteSeriesImpl,\n    moveWeek,\n    runValidation,\n    setFocusedDate,\n    todayIso,\n    updateSlotImpl,\n  };\n\n  const validate = useCallback(\n    (candidate: SlotPlannerSlotData<TData>) =>\n      latestRef.current.runValidation(candidate),\n    [],\n  );\n  const createSlot = useCallback(\n    (values: SlotPlannerEditorSeriesValues, target?: { date?: string }) =>\n      latestRef.current.createSlotImpl(values, target),\n    [],\n  );\n  const updateSlot = useCallback(\n    (\n      occurrence: SlotPlannerOccurrence<TData>,\n      result: SlotPlannerEditorResult,\n    ) => latestRef.current.updateSlotImpl(occurrence, result),\n    [],\n  );\n  const deleteOccurrence = useCallback(\n    (\n      occurrence: SlotPlannerOccurrence<TData>,\n      actionOptions?: UseSlotPlannerActionOptions,\n    ) => latestRef.current.deleteOccurrenceImpl(occurrence, actionOptions),\n    [],\n  );\n  const deleteSeries = useCallback(\n    (\n      occurrence: SlotPlannerOccurrence<TData>,\n      actionOptions?: UseSlotPlannerActionOptions,\n    ) => latestRef.current.deleteSeriesImpl(occurrence, actionOptions),\n    [],\n  );\n  const copyDay = useCallback(\n    (targetDates: string[]) => latestRef.current.copyDayImpl(targetDates),\n    [],\n  );\n  const copyWeek = useCallback(() => latestRef.current.copyWeekImpl(), []);\n  const clearDay = useCallback(() => latestRef.current.clearDayImpl(), []);\n  const goToNextWeek = useCallback(() => latestRef.current.moveWeek(1), []);\n  const goToPreviousWeek = useCallback(\n    () => latestRef.current.moveWeek(-1),\n    [],\n  );\n  const goToThisWeek = useCallback(\n    () => latestRef.current.setFocusedDate(latestRef.current.todayIso),\n    [],\n  );\n  const occurrenceKey = useCallback(\n    (occurrenceRef: SlotPlannerOccurrenceRef) =>\n      getSlotPlannerOccurrenceKey(\n        occurrenceRef.slotId,\n        occurrenceRef.occurrenceDate,\n      ),\n    [],\n  );\n\n  const batchKey = getSlotPlannerBatchKey(currentFocusedDate);\n  const createKey = getSlotPlannerCreateKey(currentFocusedDate);\n\n  return useMemo<UseSlotPlannerReturn<TData>>(\n    () => ({\n      announcement,\n      announcementNonce: announcementState.nonce,\n      batchKey,\n      clearDay,\n      clearError,\n      copyDay,\n      copyWeek,\n      createKey,\n      createSlot,\n      dailyCap,\n      deleteOccurrence,\n      deleteSeries,\n      errors: errorByKey,\n      focusedDate: currentFocusedDate,\n      goToNextWeek,\n      goToPreviousWeek,\n      goToThisWeek,\n      occurrenceKey,\n      occurrencesByDate,\n      pendingKeys,\n      retryByKey,\n      selectedOccurrences,\n      setFocusedDate,\n      slots: resolvedSlots,\n      summarizeDay,\n      taxonomy: resolvedTaxonomy,\n      todayIso,\n      updateSlot,\n      validate,\n      weekDays,\n      weeklyCap,\n    }),\n    [\n      announcement,\n      announcementState.nonce,\n      batchKey,\n      clearDay,\n      clearError,\n      copyDay,\n      copyWeek,\n      createKey,\n      createSlot,\n      dailyCap,\n      deleteOccurrence,\n      deleteSeries,\n      errorByKey,\n      currentFocusedDate,\n      goToNextWeek,\n      goToPreviousWeek,\n      goToThisWeek,\n      occurrenceKey,\n      occurrencesByDate,\n      pendingKeys,\n      retryByKey,\n      selectedOccurrences,\n      setFocusedDate,\n      resolvedSlots,\n      summarizeDay,\n      resolvedTaxonomy,\n      todayIso,\n      updateSlot,\n      validate,\n      weekDays,\n      weeklyCap,\n    ],\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/slot-planner/slot-planner-utils.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/slot-planner/slot-planner-utils.ts",
      "content": "import {\n  fromAbsolute,\n  parseDate,\n  parseDateTime,\n  toZoned,\n  type ZonedDateTime,\n} from \"@internationalized/date\";\nimport {\n  defaultSlotPlannerTaxonomy,\n  slotPlannerOccurrenceStatuses,\n  type SlotPlannerCountTemplate,\n  type SlotPlannerOccurrenceStatus,\n  type SlotPlannerSlotData,\n  type SlotPlannerSlotPayload,\n  type SlotPlannerTaxonomy,\n  type SlotPlannerTaxonomyInput,\n} from \"./slot-planner-contract\";\n\n/** One concrete, fully resolved occurrence of a (possibly recurring) slot. */\nexport type SlotPlannerOccurrence<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> = {\n  slotId: string;\n  /** ISO date (`YYYY-MM-DD`) of the occurrence in the slot's time zone. */\n  occurrenceDate: string;\n  /** Wall-clock start time (`HH:mm`) after applying any override. */\n  startTime: string;\n  durationMinutes: number;\n  timeZone: string;\n  capacity: number;\n  bookedCount: number;\n  requestedCount: number;\n  status: SlotPlannerOccurrenceStatus;\n  isRecurring: boolean;\n  slot: SlotPlannerSlotData<TData>;\n};\n\n/** Occurrences grouped by ISO date, each day sorted by wall-clock start. */\nexport type SlotPlannerOccurrencesByDate<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> = Record<string, SlotPlannerOccurrence<TData>[]>;\n\ntype ResolvedOccurrenceFields = {\n  occurrenceDate: string;\n  startTime: string;\n  durationMinutes: number;\n  bookedCount: number;\n  requestedCount: number;\n  capacity: number;\n};\n\nconst MS_PER_DAY = 86_400_000;\n\n/** Merges a partial taxonomy over the defaults, nesting the record fields. */\nexport function resolveSlotPlannerTaxonomy(\n  input?: SlotPlannerTaxonomyInput,\n): SlotPlannerTaxonomy {\n  if (!input) {\n    return defaultSlotPlannerTaxonomy;\n  }\n\n  return {\n    ...defaultSlotPlannerTaxonomy,\n    ...input,\n    statusLabels: {\n      ...defaultSlotPlannerTaxonomy.statusLabels,\n      ...input.statusLabels,\n    },\n    violationMessages: {\n      ...defaultSlotPlannerTaxonomy.violationMessages,\n      ...input.violationMessages,\n    },\n  };\n}\n\n/** Replaces every known `{token}`; unknown tokens are left verbatim. */\nexport function formatSlotPlannerTemplate(\n  template: string,\n  tokens: Record<string, string | number>,\n): string {\n  return template.replace(/\\{(\\w+)\\}/g, (match, token: string) =>\n    token in tokens ? String(tokens[token]) : match,\n  );\n}\n\n/**\n * Picks the CLDR plural category for `count` (falling back to `other`) and\n * interpolates `{tokens}`, with `{count}` always available.\n */\nexport function formatSlotPlannerCountTemplate(\n  template: SlotPlannerCountTemplate,\n  count: number,\n  tokens: Record<string, string | number> = {},\n  locale?: string,\n): string {\n  const category = new Intl.PluralRules(locale ?? \"en\").select(count);\n\n  return formatSlotPlannerTemplate(template[category] ?? template.other, {\n    count,\n    ...tokens,\n  });\n}\n\nfunction getOccurrenceEndEpoch(\n  timeZone: string,\n  resolved: Pick<\n    ResolvedOccurrenceFields,\n    \"occurrenceDate\" | \"startTime\" | \"durationMinutes\"\n  >,\n) {\n  const start = toZoned(\n    parseDateTime(`${resolved.occurrenceDate}T${resolved.startTime}`),\n    timeZone,\n  );\n\n  return start.add({ minutes: resolved.durationMinutes }).toDate().getTime();\n}\n\n/**\n * Derives the render-time status of one resolved occurrence per the contract\n * precedence: cancelled → blocked → draft → expired → booked → requested →\n * requestable. Override-cancelled occurrences never reach this function —\n * expansion excludes them entirely.\n */\nexport function deriveOccurrenceStatus(\n  slot: SlotPlannerSlotData,\n  resolved: ResolvedOccurrenceFields,\n  nowIso: string,\n): SlotPlannerOccurrenceStatus {\n  if (slot.state === \"cancelled\") {\n    return \"cancelled\";\n  }\n\n  if (slot.state === \"blocked\") {\n    return \"blocked\";\n  }\n\n  if (slot.state === \"draft\") {\n    return \"draft\";\n  }\n\n  if (getOccurrenceEndEpoch(slot.timeZone, resolved) < Date.parse(nowIso)) {\n    return \"expired\";\n  }\n\n  if (resolved.bookedCount >= resolved.capacity) {\n    return \"booked\";\n  }\n\n  if (resolved.requestedCount > 0) {\n    return \"requested\";\n  }\n\n  return \"requestable\";\n}\n\n/**\n * Expands one slot into its occurrences within `[rangeStart, rangeEnd]`\n * (inclusive ISO dates). Recurring slots step from `slot.date` by the series\n * frequency; per-occurrence overrides replace fields, and `cancelled: true`\n * excludes the occurrence entirely. Booking counts resolve per occurrence:\n * override counts win, slot-level counts apply only on `slot.date`, and every\n * other occurrence defaults to 0.\n */\nexport function expandSlotOccurrences<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  slot: SlotPlannerSlotData<TData>,\n  rangeStartIso: string,\n  rangeEndIso: string,\n  nowIso: string,\n): SlotPlannerOccurrence<TData>[] {\n  const rangeStart = parseDate(rangeStartIso);\n  const rangeEnd = parseDate(rangeEndIso);\n  const seriesStart = parseDate(slot.date);\n  const recurrence = slot.recurrence;\n  const occurrenceDates: string[] = [];\n\n  if (recurrence) {\n    const seriesEnd = recurrence.until\n      ? parseDate(recurrence.until)\n      : undefined;\n    const lastDate =\n      seriesEnd && seriesEnd.compare(rangeEnd) < 0 ? seriesEnd : rangeEnd;\n    const stepDays = recurrence.frequency === \"weekly\" ? 7 : 14;\n    // Fast-forward to the first occurrence on/after rangeStart with modular\n    // date arithmetic instead of stepping week-by-week from a possibly-distant\n    // series start. Whole-day UTC midnights make the day delta exact.\n    const startEpochDay = Math.round(\n      seriesStart.toDate(\"UTC\").getTime() / MS_PER_DAY,\n    );\n    const rangeStartEpochDay = Math.round(\n      rangeStart.toDate(\"UTC\").getTime() / MS_PER_DAY,\n    );\n    const daysAhead = rangeStartEpochDay - startEpochDay;\n    const skipSteps = daysAhead > 0 ? Math.ceil(daysAhead / stepDays) : 0;\n\n    for (\n      let current = seriesStart.add({ days: skipSteps * stepDays });\n      current.compare(lastDate) <= 0;\n      current = current.add({ days: stepDays })\n    ) {\n      occurrenceDates.push(current.toString());\n    }\n  } else if (\n    seriesStart.compare(rangeStart) >= 0 &&\n    seriesStart.compare(rangeEnd) <= 0\n  ) {\n    occurrenceDates.push(slot.date);\n  }\n\n  const occurrences: SlotPlannerOccurrence<TData>[] = [];\n\n  for (const occurrenceDate of occurrenceDates) {\n    const override = recurrence?.overrides?.find(\n      (candidate) => candidate.occurrenceDate === occurrenceDate,\n    );\n\n    if (override?.cancelled) {\n      continue;\n    }\n\n    const onSlotDate = occurrenceDate === slot.date;\n    const resolved: ResolvedOccurrenceFields = {\n      occurrenceDate,\n      startTime: override?.startTime ?? slot.startTime,\n      durationMinutes: override?.durationMinutes ?? slot.durationMinutes,\n      bookedCount:\n        override?.bookedCount ?? (onSlotDate ? (slot.bookedCount ?? 0) : 0),\n      requestedCount:\n        override?.requestedCount ??\n        (onSlotDate ? (slot.requestedCount ?? 0) : 0),\n      capacity: slot.capacity ?? 1,\n    };\n\n    occurrences.push({\n      slotId: slot.id,\n      ...resolved,\n      timeZone: slot.timeZone,\n      status: deriveOccurrenceStatus(slot, resolved, nowIso),\n      isRecurring: recurrence !== undefined,\n      slot,\n    });\n  }\n\n  return occurrences;\n}\n\n/**\n * The 7 ISO dates of the Monday-start week containing `focusedDateIso`,\n * Monday first.\n */\nexport function getSlotPlannerWeekDays(focusedDateIso: string): string[] {\n  const focusedDate = parseDate(focusedDateIso);\n  const mondayOffset = (focusedDate.toDate(\"UTC\").getUTCDay() + 6) % 7;\n  const monday = focusedDate.subtract({ days: mondayOffset });\n\n  return Array.from({ length: 7 }, (_, index) =>\n    monday.add({ days: index }).toString(),\n  );\n}\n\n/** Counts one day's occurrences per derived status. */\nexport function summarizeSlotPlannerDay(\n  occurrences: SlotPlannerOccurrence[],\n): Record<SlotPlannerOccurrenceStatus, number> {\n  const summary = Object.fromEntries(\n    slotPlannerOccurrenceStatuses.map((status) => [status, 0]),\n  ) as Record<SlotPlannerOccurrenceStatus, number>;\n\n  for (const occurrence of occurrences) {\n    summary[occurrence.status] += 1;\n  }\n\n  return summary;\n}\n\n/**\n * One occurrence projected into a viewer's time zone for book mode. The\n * underlying occurrence fields keep their provider-zone meaning —\n * `occurrenceDate`, `startTime`, and `timeZone` still identify and describe\n * the occurrence in the shared slot model — while the `viewer*` fields carry\n * the presentation values in the viewer's zone. The projection is computed\n * on the occurrence's epoch instant, so it lands on a different viewer-zone\n * calendar date when the zone offset crosses midnight.\n */\nexport type SlotPickerOccurrence<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> = SlotPlannerOccurrence<TData> & {\n  /** IANA zone the viewer fields are projected into. */\n  viewerTimeZone: string;\n  /** ISO date (`YYYY-MM-DD`) of the occurrence start in the viewer's zone. */\n  viewerDate: string;\n  /** Wall-clock start (`HH:mm`) in the viewer's zone. */\n  viewerStartTime: string;\n  /** Wall-clock end (`HH:mm`) in the viewer's zone. */\n  viewerEndTime: string;\n  /** Epoch milliseconds of the occurrence start instant. */\n  startEpochMs: number;\n};\n\n/** Projected occurrences grouped by viewer-zone ISO date, sorted by instant. */\nexport type SlotPickerOccurrencesByDate<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> = Record<string, SlotPickerOccurrence<TData>[]>;\n\nfunction zonedIsoDate(zoned: ZonedDateTime) {\n  const month = String(zoned.month).padStart(2, \"0\");\n  const day = String(zoned.day).padStart(2, \"0\");\n\n  return `${zoned.year}-${month}-${day}`;\n}\n\nfunction zonedWallTime(zoned: ZonedDateTime) {\n  const hour = String(zoned.hour).padStart(2, \"0\");\n  const minute = String(zoned.minute).padStart(2, \"0\");\n\n  return `${hour}:${minute}`;\n}\n\n/** ISO date (`YYYY-MM-DD`) of an epoch instant in an IANA zone. */\nexport function getSlotPlannerIsoDateInZone(\n  epochMs: number,\n  timeZone: string,\n): string {\n  return zonedIsoDate(fromAbsolute(epochMs, timeZone));\n}\n\n/**\n * Projects one resolved occurrence into a viewer's zone. The provider-zone\n * wall clock resolves to an epoch instant first, so the viewer date and\n * times are exact across DST transitions on either side.\n */\nexport function projectSlotPlannerOccurrenceToZone<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  occurrence: SlotPlannerOccurrence<TData>,\n  viewerTimeZone: string,\n): SlotPickerOccurrence<TData> {\n  const start = toZoned(\n    parseDateTime(`${occurrence.occurrenceDate}T${occurrence.startTime}`),\n    occurrence.timeZone,\n  );\n  const startEpochMs = start.toDate().getTime();\n  const endEpochMs = start\n    .add({ minutes: occurrence.durationMinutes })\n    .toDate()\n    .getTime();\n  const viewerStart = fromAbsolute(startEpochMs, viewerTimeZone);\n  const viewerEnd = fromAbsolute(endEpochMs, viewerTimeZone);\n\n  return {\n    ...occurrence,\n    startEpochMs,\n    viewerDate: zonedIsoDate(viewerStart),\n    viewerEndTime: zonedWallTime(viewerEnd),\n    viewerStartTime: zonedWallTime(viewerStart),\n    viewerTimeZone,\n  };\n}\n\n/**\n * A viewer-zone calendar date can differ from the provider-zone occurrence\n * date by at most this many days: real IANA offsets span UTC-12 to UTC+14,\n * so an instant can shift up to two calendar dates between zones. The same\n * bound governs how far the overlap check must look at neighboring days, so\n * it is shared with the constraints engine.\n */\nexport const VIEWER_RANGE_PAD_DAYS = 2;\n\n/**\n * Book-mode expansion: expands every slot, projects each occurrence into the\n * viewer's zone, and keeps the occurrences whose VIEWER date falls within\n * `[rangeStart, rangeEnd]` (inclusive viewer-zone ISO dates). The provider\n * expansion range is padded so occurrences that cross a date boundary into\n * the viewer range are not missed. Draft and cancelled occurrences are\n * excluded entirely — consumers never see them. Days are sorted by instant.\n */\nexport function expandSlotsForViewerZone<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  slots: SlotPlannerSlotData<TData>[],\n  rangeStartIso: string,\n  rangeEndIso: string,\n  nowIso: string,\n  viewerTimeZone: string,\n): SlotPickerOccurrencesByDate<TData> {\n  const providerRangeStart = parseDate(rangeStartIso)\n    .subtract({ days: VIEWER_RANGE_PAD_DAYS })\n    .toString();\n  const providerRangeEnd = parseDate(rangeEndIso)\n    .add({ days: VIEWER_RANGE_PAD_DAYS })\n    .toString();\n  const byViewerDate: SlotPickerOccurrencesByDate<TData> = {};\n\n  for (const slot of slots) {\n    for (const occurrence of expandSlotOccurrences(\n      slot,\n      providerRangeStart,\n      providerRangeEnd,\n      nowIso,\n    )) {\n      if (occurrence.status === \"draft\" || occurrence.status === \"cancelled\") {\n        continue;\n      }\n\n      const projected = projectSlotPlannerOccurrenceToZone(\n        occurrence,\n        viewerTimeZone,\n      );\n\n      if (\n        projected.viewerDate < rangeStartIso ||\n        projected.viewerDate > rangeEndIso\n      ) {\n        continue;\n      }\n\n      (byViewerDate[projected.viewerDate] ??= []).push(projected);\n    }\n  }\n\n  for (const occurrences of Object.values(byViewerDate)) {\n    occurrences.sort(\n      (a, b) =>\n        a.startEpochMs - b.startEpochMs || a.slotId.localeCompare(b.slotId),\n    );\n  }\n\n  return byViewerDate;\n}\n\n/**\n * Expands every slot into `[rangeStart, rangeEnd]` and groups the occurrences\n * by ISO date, each day sorted by wall-clock start time.\n */\nexport function expandSlotsForRange<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  slots: SlotPlannerSlotData<TData>[],\n  rangeStartIso: string,\n  rangeEndIso: string,\n  nowIso: string,\n): SlotPlannerOccurrencesByDate<TData> {\n  const byDate: SlotPlannerOccurrencesByDate<TData> = {};\n\n  for (const slot of slots) {\n    for (const occurrence of expandSlotOccurrences(\n      slot,\n      rangeStartIso,\n      rangeEndIso,\n      nowIso,\n    )) {\n      (byDate[occurrence.occurrenceDate] ??= []).push(occurrence);\n    }\n  }\n\n  for (const occurrences of Object.values(byDate)) {\n    occurrences.sort(\n      (a, b) =>\n        a.startTime.localeCompare(b.startTime) ||\n        a.slotId.localeCompare(b.slotId),\n    );\n  }\n\n  return byDate;\n}\n"
    },
    {
      "path": "packages/components/src/components/slot-planner/slot-planner-crud.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/slot-planner/slot-planner-crud.ts",
      "content": "import type {\n  SlotPlannerBatchChangePayload,\n  SlotPlannerOccurrenceOverride,\n  SlotPlannerRecurrenceFrequency,\n  SlotPlannerSlotData,\n  SlotPlannerSlotPayload,\n} from \"./slot-planner-contract\";\n\n/**\n * Internal mutation applied to an uncontrolled slot collection after the\n * matching callback succeeds. Mirrors the callback payloads: create appends,\n * update replaces by id, delete-occurrence cancels one occurrence of a\n * recurring slot (or removes a single slot entirely), delete-series removes\n * the slot.\n */\nexport type SlotPlannerMutation<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> =\n  | { type: \"create\"; slot: SlotPlannerSlotData<TData> }\n  | { type: \"update\"; slot: SlotPlannerSlotData<TData> }\n  | { type: \"delete-occurrence\"; slotId: string; occurrenceDate: string }\n  | { type: \"delete-series\"; slotId: string };\n\n/** Editor recurrence choice; `\"none\"` maps to an omitted `recurrence`. */\nexport type SlotPlannerEditorRecurrence =\n  \"none\" | SlotPlannerRecurrenceFrequency;\n\n/**\n * Normalized values submitted by the slot editor when creating a slot or\n * editing an entire series. Empty strings and empty arrays mean \"unset\" and\n * are omitted from the built slot.\n */\nexport type SlotPlannerEditorSeriesValues = {\n  startTime: string;\n  durationMinutes: number;\n  capacity: number;\n  bufferBeforeMinutes: number;\n  bufferAfterMinutes: number;\n  timeZone: string;\n  tags: string[];\n  note: string;\n  recurrence: SlotPlannerEditorRecurrence;\n  /** ISO date (`YYYY-MM-DD`); ignored when `recurrence` is `\"none\"`. */\n  recurrenceUntil: string;\n};\n\n/**\n * Result of a successful editor submit; the scope decides payload shape.\n * Series results carry the full editor values; occurrence results carry only\n * the per-occurrence override fields.\n */\nexport type SlotPlannerEditorResult =\n  | { scope: \"series\"; values: SlotPlannerEditorSeriesValues }\n  | { scope: \"occurrence\"; startTime: string; durationMinutes: number };\n\n/**\n * Returns a copy of `slot` with `override` merged over any existing override\n * for the same occurrence date, or appended when none exists. Never mutates\n * its inputs. Slots without recurrence are returned unchanged — single slots\n * have no per-occurrence overrides.\n */\nexport function upsertSlotPlannerOccurrenceOverride<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  slot: SlotPlannerSlotData<TData>,\n  override: SlotPlannerOccurrenceOverride,\n): SlotPlannerSlotData<TData> {\n  const recurrence = slot.recurrence;\n\n  if (!recurrence) {\n    return slot;\n  }\n\n  const overrides = recurrence.overrides ?? [];\n  const existingIndex = overrides.findIndex(\n    (candidate) => candidate.occurrenceDate === override.occurrenceDate,\n  );\n  const nextOverrides =\n    existingIndex === -1\n      ? [...overrides, override]\n      : overrides.map((candidate, index) =>\n          index === existingIndex ? { ...candidate, ...override } : candidate,\n        );\n\n  return { ...slot, recurrence: { ...recurrence, overrides: nextOverrides } };\n}\n\n/**\n * Applies one mutation to a slot collection, returning a new array without\n * mutating the input. Deleting an occurrence of a recurring slot upserts a\n * `cancelled: true` override; deleting a single slot's occurrence removes\n * the slot.\n */\nexport function applySlotPlannerMutation<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  slots: SlotPlannerSlotData<TData>[],\n  mutation: SlotPlannerMutation<TData>,\n): SlotPlannerSlotData<TData>[] {\n  switch (mutation.type) {\n    case \"create\":\n      return [...slots, mutation.slot];\n    case \"update\":\n      return slots.map((slot) =>\n        slot.id === mutation.slot.id ? mutation.slot : slot,\n      );\n    case \"delete-occurrence\":\n      return slots.flatMap((slot) => {\n        if (slot.id !== mutation.slotId) {\n          return [slot];\n        }\n\n        if (!slot.recurrence) {\n          return [];\n        }\n\n        return [\n          upsertSlotPlannerOccurrenceOverride(slot, {\n            occurrenceDate: mutation.occurrenceDate,\n            cancelled: true,\n          }),\n        ];\n      });\n    case \"delete-series\":\n      return slots.filter((slot) => slot.id !== mutation.slotId);\n  }\n}\n\n/**\n * Applies one batch payload to a slot collection, returning a new array\n * without mutating the input: `updatedSlots` replace by id, `deletedSlotIds`\n * are removed, and `createdSlots` are appended. Rejected slots (the\n * `violations` map) never touch the collection.\n */\nexport function applySlotPlannerBatch<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  slots: SlotPlannerSlotData<TData>[],\n  batch: Pick<\n    SlotPlannerBatchChangePayload<TData>,\n    \"createdSlots\" | \"deletedSlotIds\" | \"updatedSlots\"\n  >,\n): SlotPlannerSlotData<TData>[] {\n  const updatedById = new Map(\n    (batch.updatedSlots ?? []).map((slot) => [slot.id, slot]),\n  );\n  const deletedIds = new Set(batch.deletedSlotIds);\n\n  return [\n    ...slots\n      .filter((slot) => !deletedIds.has(slot.id))\n      .map((slot) => updatedById.get(slot.id) ?? slot),\n    ...batch.createdSlots,\n  ];\n}\n\nfunction buildEditorRecurrence(values: SlotPlannerEditorSeriesValues) {\n  if (values.recurrence === \"none\") {\n    return undefined;\n  }\n\n  return {\n    frequency: values.recurrence,\n    ...(values.recurrenceUntil ? { until: values.recurrenceUntil } : {}),\n  };\n}\n\nfunction buildEditorData<TData extends SlotPlannerSlotPayload>(\n  values: SlotPlannerEditorSeriesValues,\n  previousData?: TData,\n): TData | undefined {\n  const data: Record<string, unknown> = { ...previousData };\n\n  if (values.tags.length > 0) {\n    data.tags = values.tags;\n  } else {\n    delete data.tags;\n  }\n\n  if (values.note.trim() !== \"\") {\n    data.note = values.note;\n  } else {\n    delete data.note;\n  }\n\n  // The editor only writes the conventional keys, so the cast is sound for\n  // any payload type that follows the conventional-data contract.\n  return Object.keys(data).length > 0 ? (data as TData) : undefined;\n}\n\n/**\n * Builds the slot for a `SlotPlannerCreatePayload` from editor values. Keys\n * the user left empty (buffers, recurrence, tags, note) are omitted so the\n * payload stays minimal.\n */\nexport function createSlotFromEditorValues<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  values: SlotPlannerEditorSeriesValues,\n  target: { id: string; date: string },\n): SlotPlannerSlotData<TData> {\n  const recurrence = buildEditorRecurrence(values);\n  const data = buildEditorData<TData>(values);\n\n  return {\n    id: target.id,\n    date: target.date,\n    startTime: values.startTime,\n    durationMinutes: values.durationMinutes,\n    timeZone: values.timeZone,\n    state: \"requestable\",\n    ...(recurrence ? { recurrence } : {}),\n    ...(values.capacity > 1 ? { capacity: values.capacity } : {}),\n    ...(values.bufferBeforeMinutes > 0\n      ? { bufferBeforeMinutes: values.bufferBeforeMinutes }\n      : {}),\n    ...(values.bufferAfterMinutes > 0\n      ? { bufferAfterMinutes: values.bufferAfterMinutes }\n      : {}),\n    ...(data ? { data } : {}),\n  };\n}\n\n/**\n * Builds the updated slot for a series-scope edit. Unknown `data` keys and\n * existing per-occurrence overrides are preserved; cleared buffers, tags,\n * and note drop their keys; switching recurrence to `\"none\"` removes the\n * series definition (and its overrides). Never mutates `previous`.\n */\nexport function updateSlotFromEditorValues<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  previous: SlotPlannerSlotData<TData>,\n  values: SlotPlannerEditorSeriesValues,\n): SlotPlannerSlotData<TData> {\n  const next: SlotPlannerSlotData<TData> = {\n    ...previous,\n    startTime: values.startTime,\n    durationMinutes: values.durationMinutes,\n    timeZone: values.timeZone,\n  };\n  const recurrence = buildEditorRecurrence(values);\n  const previousOverrides = previous.recurrence?.overrides;\n\n  if (recurrence) {\n    next.recurrence = {\n      ...recurrence,\n      ...(previousOverrides ? { overrides: previousOverrides } : {}),\n    };\n  } else {\n    delete next.recurrence;\n  }\n\n  if (values.capacity > 1) {\n    next.capacity = values.capacity;\n  } else {\n    delete next.capacity;\n  }\n\n  if (values.bufferBeforeMinutes > 0) {\n    next.bufferBeforeMinutes = values.bufferBeforeMinutes;\n  } else {\n    delete next.bufferBeforeMinutes;\n  }\n\n  if (values.bufferAfterMinutes > 0) {\n    next.bufferAfterMinutes = values.bufferAfterMinutes;\n  } else {\n    delete next.bufferAfterMinutes;\n  }\n\n  const data = buildEditorData(values, previous.data);\n\n  if (data) {\n    next.data = data;\n  } else {\n    delete next.data;\n  }\n\n  return next;\n}\n"
    },
    {
      "path": "packages/components/src/components/slot-planner/slot-planner-constraints.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/slot-planner/slot-planner-constraints.ts",
      "content": "import { parseDate, parseDateTime, toZoned } from \"@internationalized/date\";\nimport type {\n  SlotPlannerConstraints,\n  SlotPlannerIsoWeekday,\n  SlotPlannerOccurrenceStatus,\n  SlotPlannerSlotData,\n  SlotPlannerSlotPayload,\n  SlotPlannerViolation,\n  SlotPlannerViolationCode,\n} from \"./slot-planner-contract\";\nimport {\n  expandSlotOccurrences,\n  getSlotPlannerIsoDateInZone,\n  getSlotPlannerWeekDays,\n  VIEWER_RANGE_PAD_DAYS,\n  type SlotPlannerOccurrence,\n} from \"./slot-planner-utils\";\n\n/**\n * Pure, JSON-in/JSON-out validation context for the constraints engine. No\n * React, no Motion, no component state — apps can run the same checks on a\n * server before persisting.\n */\nexport type SlotPlannerValidationContext<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> = {\n  /** Existing collection; any slot with the candidate's id is ignored. */\n  slots: SlotPlannerSlotData<TData>[];\n  constraints?: SlotPlannerConstraints;\n  /** ISO date-time treated as \"now\". */\n  now: string;\n};\n\n/** Occurrence statuses that count against daily/weekly requestable caps. */\nconst publishedOccurrenceStatuses = new Set<SlotPlannerOccurrenceStatus>([\n  \"requestable\",\n  \"requested\",\n  \"booked\",\n]);\n\nconst MS_PER_MINUTE = 60_000;\nconst MS_PER_DAY = 86_400_000;\n\n/** Longest expansion window, in days, for open-ended recurring candidates. */\nconst MAX_VALIDATION_WINDOW_DAYS = 366;\n\n/** ISO weekday number (1 = Monday … 7 = Sunday) of an ISO date. */\nfunction getIsoWeekday(dateIso: string): SlotPlannerIsoWeekday {\n  return (((parseDate(dateIso).toDate(\"UTC\").getUTCDay() + 6) % 7) +\n    1) as SlotPlannerIsoWeekday;\n}\n\nfunction getOccurrenceStartEpoch(occurrence: {\n  occurrenceDate: string;\n  startTime: string;\n  timeZone: string;\n}): number {\n  return toZoned(\n    parseDateTime(`${occurrence.occurrenceDate}T${occurrence.startTime}`),\n    occurrence.timeZone,\n  )\n    .toDate()\n    .getTime();\n}\n\ntype BufferedInterval = { start: number; end: number };\n\n/**\n * Epoch interval `[start − bufferBefore, end + bufferAfter]` of one\n * occurrence, so overlap comparisons work across time zones.\n */\nfunction getBufferedInterval(\n  occurrence: SlotPlannerOccurrence,\n): BufferedInterval {\n  const start = getOccurrenceStartEpoch(occurrence);\n  const slot = occurrence.slot;\n\n  return {\n    start: start - (slot.bufferBeforeMinutes ?? 0) * MS_PER_MINUTE,\n    end:\n      start +\n      (occurrence.durationMinutes + (slot.bufferAfterMinutes ?? 0)) *\n        MS_PER_MINUTE,\n  };\n}\n\n/** Half-open intersection: back-to-back buffered intervals do not overlap. */\nfunction intervalsOverlap(a: BufferedInterval, b: BufferedInterval): boolean {\n  return a.start < b.end && b.start < a.end;\n}\n\n/**\n * Expands the candidate into the occurrences to validate. Non-recurring\n * candidates validate their single occurrence on `date` (wherever it falls,\n * so horizon/notice checks still see it). Recurring candidates validate a\n * bounded window: from max(slot date, today) to the earliest of\n * `recurrence.until`, the booking horizon end, or today + 366 days.\n */\nfunction getCandidateOccurrences<TData extends SlotPlannerSlotPayload>(\n  candidate: SlotPlannerSlotData<TData>,\n  constraints: SlotPlannerConstraints | undefined,\n  nowIso: string,\n): SlotPlannerOccurrence<TData>[] {\n  if (!candidate.recurrence) {\n    return expandSlotOccurrences(\n      candidate,\n      candidate.date,\n      candidate.date,\n      nowIso,\n    );\n  }\n\n  const today = parseDate(\n    getSlotPlannerIsoDateInZone(Date.parse(nowIso), candidate.timeZone),\n  );\n  const seriesStart = parseDate(candidate.date);\n  const windowStart = seriesStart.compare(today) > 0 ? seriesStart : today;\n  let windowEnd = today.add({ days: MAX_VALIDATION_WINDOW_DAYS });\n\n  if (constraints?.bookingHorizonDays !== undefined) {\n    const horizonEnd = today.add({ days: constraints.bookingHorizonDays });\n\n    if (horizonEnd.compare(windowEnd) < 0) {\n      windowEnd = horizonEnd;\n    }\n  }\n\n  if (candidate.recurrence.until) {\n    const until = parseDate(candidate.recurrence.until);\n\n    if (until.compare(windowEnd) < 0) {\n      windowEnd = until;\n    }\n  }\n\n  if (windowEnd.compare(windowStart) < 0) {\n    return [];\n  }\n\n  return expandSlotOccurrences(\n    candidate,\n    windowStart.toString(),\n    windowEnd.toString(),\n    nowIso,\n  );\n}\n\n/**\n * Counts one day's \"published\" occurrences — derived status `requestable`,\n * `requested`, or `booked` — across the whole collection. Used by the daily\n * cap meter and the cap checks.\n */\nexport function countSlotPlannerPublishedOccurrences<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  slots: SlotPlannerSlotData<TData>[],\n  dateIso: string,\n  nowIso: string,\n): number {\n  let count = 0;\n\n  for (const slot of slots) {\n    for (const occurrence of expandSlotOccurrences(\n      slot,\n      dateIso,\n      dateIso,\n      nowIso,\n    )) {\n      if (publishedOccurrenceStatuses.has(occurrence.status)) {\n        count += 1;\n      }\n    }\n  }\n\n  return count;\n}\n\n/**\n * Validates one candidate slot against the collection, constraints, and\n * \"now\". Reports violations for every failed check (not first-failure-only),\n * with each violation code reported at most once per candidate (the first\n * offending occurrence's params win). Slots in the context that share the\n * candidate's id are ignored, so updates validate against the rest of the\n * collection.\n *\n * Checks without a `SlotPlannerConstraints` field — `overlap` and\n * `invalid-wall-clock-time` — always run; the rest run only when their\n * constraint field is set.\n *\n * DST note: non-existent wall-clock times (spring-forward gaps) and ambiguous\n * times (fall-back repeated hours) violate `invalid-wall-clock-time`; the UI\n * should surface them rather than silently accepting a resolved instant.\n */\nexport function validateSlotPlannerSlot<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  candidate: SlotPlannerSlotData<TData>,\n  context: SlotPlannerValidationContext<TData>,\n): SlotPlannerViolation[] {\n  const { constraints, now } = context;\n  const otherSlots = context.slots.filter((slot) => slot.id !== candidate.id);\n  const nowEpoch = Date.parse(now);\n  const byCode = new Map<SlotPlannerViolationCode, SlotPlannerViolation>();\n  const addViolation = (\n    code: SlotPlannerViolationCode,\n    params: Record<string, string | number>,\n  ) => {\n    if (!byCode.has(code)) {\n      byCode.set(code, { code, params });\n    }\n  };\n\n  const occurrences = getCandidateOccurrences(candidate, constraints, now);\n  const durations = new Set<number>([candidate.durationMinutes]);\n\n  for (const occurrence of occurrences) {\n    durations.add(occurrence.durationMinutes);\n  }\n\n  for (const durationMinutes of durations) {\n    if (\n      constraints?.minDurationMinutes !== undefined &&\n      durationMinutes < constraints.minDurationMinutes\n    ) {\n      addViolation(\"min-duration\", {\n        durationMinutes,\n        minDurationMinutes: constraints.minDurationMinutes,\n      });\n    }\n\n    if (\n      constraints?.maxDurationMinutes !== undefined &&\n      durationMinutes > constraints.maxDurationMinutes\n    ) {\n      addViolation(\"max-duration\", {\n        durationMinutes,\n        maxDurationMinutes: constraints.maxDurationMinutes,\n      });\n    }\n\n    if (\n      constraints?.durationIncrementMinutes !== undefined &&\n      durationMinutes % constraints.durationIncrementMinutes !== 0\n    ) {\n      addViolation(\"duration-increment\", {\n        durationMinutes,\n        increment: constraints.durationIncrementMinutes,\n      });\n    }\n  }\n\n  const blackoutDates = new Set(constraints?.blackoutDates ?? []);\n  const workingDays = constraints?.workingDays;\n\n  for (const occurrence of occurrences) {\n    const date = occurrence.occurrenceDate;\n\n    if (blackoutDates.has(date)) {\n      addViolation(\"blackout-date\", { date });\n    }\n\n    if (workingDays !== undefined) {\n      const weekday = getIsoWeekday(date);\n\n      if (!workingDays.includes(weekday)) {\n        addViolation(\"non-working-day\", { date, weekday });\n      }\n    }\n\n    // `reject` catches both skipped and repeated wall-clock times around DST\n    // transitions, matching the PRD's \"surface, don't silently shift\" rule.\n    try {\n      toZoned(\n        parseDateTime(`${date}T${occurrence.startTime}`),\n        occurrence.timeZone,\n        \"reject\",\n      );\n    } catch {\n      addViolation(\"invalid-wall-clock-time\", {\n        date,\n        startTime: occurrence.startTime,\n        timeZone: occurrence.timeZone,\n      });\n    }\n\n    if (constraints?.bookingHorizonDays !== undefined) {\n      const horizonEpoch =\n        nowEpoch + constraints.bookingHorizonDays * MS_PER_DAY;\n\n      if (getOccurrenceStartEpoch(occurrence) > horizonEpoch) {\n        addViolation(\"booking-horizon\", {\n          bookingHorizonDays: constraints.bookingHorizonDays,\n          date,\n          startTime: occurrence.startTime,\n        });\n      }\n    }\n  }\n\n  if (constraints?.minNoticeMinutes !== undefined) {\n    // Expired occurrences are in the past; a series whose first occurrence has\n    // already lapsed must not be blocked by min-notice, so the check applies\n    // to the earliest still-upcoming occurrence.\n    const first = occurrences.find(\n      (occurrence) => occurrence.status !== \"expired\",\n    );\n\n    if (\n      first &&\n      getOccurrenceStartEpoch(first) <\n        nowEpoch + constraints.minNoticeMinutes * MS_PER_MINUTE\n    ) {\n      addViolation(\"min-notice\", {\n        date: first.occurrenceDate,\n        minNoticeMinutes: constraints.minNoticeMinutes,\n        startTime: first.startTime,\n      });\n    }\n  }\n\n  // Buffer-aware overlap, epoch-based so it works across time zones. Other\n  // slots expand over [date − pad, date + pad] because an occurrence in\n  // another zone can overlap in real time while carrying an ISO date up to\n  // VIEWER_RANGE_PAD_DAYS away — the same cross-zone bound the viewer\n  // expansion uses.\n  for (const occurrence of occurrences) {\n    if (byCode.has(\"overlap\")) {\n      break;\n    }\n\n    const candidateInterval = getBufferedInterval(occurrence);\n    const neighborStart = parseDate(occurrence.occurrenceDate)\n      .subtract({ days: VIEWER_RANGE_PAD_DAYS })\n      .toString();\n    const neighborEnd = parseDate(occurrence.occurrenceDate)\n      .add({ days: VIEWER_RANGE_PAD_DAYS })\n      .toString();\n\n    for (const other of otherSlots) {\n      const otherOccurrences = expandSlotOccurrences(\n        other,\n        neighborStart,\n        neighborEnd,\n        now,\n      );\n      const overlapping = otherOccurrences.find(\n        (otherOccurrence) =>\n          otherOccurrence.status !== \"cancelled\" &&\n          otherOccurrence.status !== \"expired\" &&\n          intervalsOverlap(\n            candidateInterval,\n            getBufferedInterval(otherOccurrence),\n          ),\n      );\n\n      if (overlapping) {\n        addViolation(\"overlap\", {\n          date: occurrence.occurrenceDate,\n          otherSlotId: overlapping.slotId,\n        });\n        break;\n      }\n    }\n  }\n\n  const publishedOccurrences = occurrences.filter((occurrence) =>\n    publishedOccurrenceStatuses.has(occurrence.status),\n  );\n\n  if (constraints?.dailyRequestableCap !== undefined) {\n    const cap = constraints.dailyRequestableCap;\n    const candidateCountByDate = new Map<string, number>();\n\n    for (const occurrence of publishedOccurrences) {\n      candidateCountByDate.set(\n        occurrence.occurrenceDate,\n        (candidateCountByDate.get(occurrence.occurrenceDate) ?? 0) + 1,\n      );\n    }\n\n    for (const [date, candidateCount] of candidateCountByDate) {\n      const used = countSlotPlannerPublishedOccurrences(otherSlots, date, now);\n\n      if (used + candidateCount > cap) {\n        addViolation(\"daily-cap\", { cap, date });\n        break;\n      }\n    }\n  }\n\n  if (constraints?.weeklyRequestableCap !== undefined) {\n    const cap = constraints.weeklyRequestableCap;\n    const candidateCountByWeek = new Map<string, number>();\n\n    for (const occurrence of publishedOccurrences) {\n      const weekStart = getSlotPlannerWeekDays(occurrence.occurrenceDate)[0]!;\n\n      candidateCountByWeek.set(\n        weekStart,\n        (candidateCountByWeek.get(weekStart) ?? 0) + 1,\n      );\n    }\n\n    for (const [weekStart, candidateCount] of candidateCountByWeek) {\n      const used = getSlotPlannerWeekDays(weekStart).reduce(\n        (total, date) =>\n          total + countSlotPlannerPublishedOccurrences(otherSlots, date, now),\n        0,\n      );\n\n      if (used + candidateCount > cap) {\n        addViolation(\"weekly-cap\", { cap, weekStart });\n        break;\n      }\n    }\n  }\n\n  return [...byCode.values()];\n}\n\n/**\n * Validates candidates in order. A candidate that passes joins the context\n * for later candidates, so intra-batch overlaps and cap totals are caught.\n * Only ids with at least one violation appear in the result.\n */\nexport function validateSlotPlannerSlots<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n>(\n  candidates: SlotPlannerSlotData<TData>[],\n  context: SlotPlannerValidationContext<TData>,\n): Record<string, SlotPlannerViolation[]> {\n  const result: Record<string, SlotPlannerViolation[]> = {};\n  const workingSlots = [...context.slots];\n\n  for (const candidate of candidates) {\n    const violations = validateSlotPlannerSlot(candidate, {\n      ...context,\n      slots: workingSlots,\n    });\n\n    if (violations.length > 0) {\n      result[candidate.id] = violations;\n    } else {\n      workingSlots.push(candidate);\n    }\n  }\n\n  return result;\n}\n"
    },
    {
      "path": "packages/components/src/components/slot-planner/slot-planner-contract.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/slot-planner/slot-planner-contract.ts",
      "content": "/**\n * SlotPlanner public data contract.\n *\n * Every type in this module is JSON-serializable by convention: values are\n * plain in-memory objects (typically deserialized API payloads) built from\n * ISO 8601 date/time strings, IANA time-zone identifiers, numbers, strings,\n * booleans, arrays, and plain nested objects. SlotPlanner never parses or\n * emits JSON text itself, and callback payloads round-trip losslessly\n * through `JSON.stringify`/`JSON.parse`.\n *\n * Recurring slots are modeled as wall-clock time bound to an IANA zone so\n * series survive DST transitions predictably.\n *\n * Stored versus derived state: `SlotPlannerSlotState` is the stored\n * definition lifecycle of a slot. Time- and booking-dependent conditions\n * (requested, booked, expired) are never stored — they are derived per\n * occurrence at render time as `SlotPlannerOccurrenceStatus`.\n */\n\n/** Stored definition lifecycle of a slot. */\nexport const slotPlannerSlotStates = [\n  \"draft\",\n  \"requestable\",\n  \"blocked\",\n  \"cancelled\",\n] as const;\n\nexport type SlotPlannerSlotState = (typeof slotPlannerSlotStates)[number];\n\n/**\n * Render-time status of one occurrence. Derived — never stored — by this\n * precedence:\n *\n * 1. `cancelled` — the slot state is `cancelled`, or the occurrence's\n *    override has `cancelled: true`\n * 2. `blocked` — the slot state is `blocked`\n * 3. `draft` — the slot state is `draft`\n * 4. `expired` — the occurrence's end is in the past\n * 5. `booked` — the occurrence's booked count has reached `capacity`\n * 6. `requested` — the occurrence's requested count is greater than zero\n * 7. `requestable`\n *\n * Booking counts resolve per occurrence: an override's `bookedCount` /\n * `requestedCount` wins; otherwise the slot-level counts apply only to the\n * occurrence on the slot's own `date`; every other occurrence defaults to 0.\n */\nexport const slotPlannerOccurrenceStatuses = [\n  \"draft\",\n  \"requestable\",\n  \"requested\",\n  \"booked\",\n  \"blocked\",\n  \"expired\",\n  \"cancelled\",\n] as const;\n\nexport type SlotPlannerOccurrenceStatus =\n  (typeof slotPlannerOccurrenceStatuses)[number];\n\nexport type SlotPlannerRecurrenceFrequency = \"weekly\" | \"biweekly\";\n\n/**\n * Open payload bag attached to a slot. Opaque to SlotPlanner itself: default\n * renderers read only the conventional keys in\n * `SlotPlannerConventionalSlotData` and ignore everything else, while custom\n * renderers receive the full bag untouched. Must stay JSON-serializable.\n */\nexport type SlotPlannerSlotPayload = Record<string, unknown>;\n\n/**\n * Conventional `data` keys understood by the default renderers. Consumers\n * are free to use any other keys alongside these.\n */\nexport type SlotPlannerConventionalSlotData = {\n  /** Session-type tags rendered as chips on the default slot card. */\n  tags?: string[];\n  /** Free-form note rendered on the default slot card when present. */\n  note?: string;\n};\n\n/**\n * Override for a single occurrence of a recurring slot, keyed by the\n * occurrence's ISO date (`YYYY-MM-DD`) in the slot's time zone.\n *\n * `cancelled: true` is the only occurrence-removal mechanism; whole-series\n * cancellation uses the slot-level `state: \"cancelled\"` instead.\n */\nexport type SlotPlannerOccurrenceOverride = {\n  /** ISO date (`YYYY-MM-DD`) of the occurrence being overridden. */\n  occurrenceDate: string;\n  /** Removes this occurrence from the series when true. */\n  cancelled?: boolean;\n  /** Wall-clock start (`HH:mm`) replacing the series start for this date. */\n  startTime?: string;\n  durationMinutes?: number;\n  /** Seats booked for this occurrence, replacing the 0 default. */\n  bookedCount?: number;\n  /** Seats requested (pending) for this occurrence, replacing the 0 default. */\n  requestedCount?: number;\n};\n\nexport type SlotPlannerRecurrence = {\n  frequency: SlotPlannerRecurrenceFrequency;\n  /**\n   * ISO date (`YYYY-MM-DD`) of the last day, inclusive, on which an\n   * occurrence may fall. The series is open-ended when omitted.\n   */\n  until?: string;\n  overrides?: SlotPlannerOccurrenceOverride[];\n};\n\n/**\n * A bookable slot definition. Single slots occur once on `date`; recurring\n * slots treat `date` as the first occurrence and repeat per `recurrence`.\n * Times are wall-clock values interpreted in `timeZone`.\n */\nexport type SlotPlannerSlotData<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> = {\n  id: string;\n  /** ISO date (`YYYY-MM-DD`) of the first or only occurrence. */\n  date: string;\n  /** Wall-clock start time (`HH:mm`, 24-hour). */\n  startTime: string;\n  durationMinutes: number;\n  /** IANA zone (e.g. `Europe/London`) the wall-clock times are bound to. */\n  timeZone: string;\n  /**\n   * Stored definition lifecycle. Occurrence availability (requested,\n   * booked, expired) is derived per `SlotPlannerOccurrenceStatus`, never\n   * stored here.\n   */\n  state: SlotPlannerSlotState;\n  recurrence?: SlotPlannerRecurrence;\n  /** Seats available per occurrence. Defaults to 1. */\n  capacity?: number;\n  /**\n   * Seats booked for the occurrence on this slot's own `date`. Other\n   * occurrences of a series carry their counts on overrides. Defaults to 0.\n   */\n  bookedCount?: number;\n  /**\n   * Seats requested (pending confirmation) for the occurrence on this\n   * slot's own `date`. Same per-occurrence rule as `bookedCount`.\n   * Defaults to 0.\n   */\n  requestedCount?: number;\n  bufferBeforeMinutes?: number;\n  bufferAfterMinutes?: number;\n  /** Open, JSON-serializable payload for custom renderers. */\n  data?: TData;\n};\n\n/** Identity of one concrete occurrence of a (possibly recurring) slot. */\nexport type SlotPlannerOccurrenceRef = {\n  slotId: string;\n  /** ISO date (`YYYY-MM-DD`) of the occurrence in the slot's time zone. */\n  occurrenceDate: string;\n};\n\n/**\n * Declarative rules validated by the exported constraints utility and\n * surfaced inline by SlotPlanner. Enforcement authority stays with the app.\n */\nexport type SlotPlannerConstraints = {\n  /** Maximum requestable occurrences per day. */\n  dailyRequestableCap?: number;\n  /** Maximum requestable occurrences per week. */\n  weeklyRequestableCap?: number;\n  minDurationMinutes?: number;\n  maxDurationMinutes?: number;\n  /** Durations must be a multiple of this increment (e.g. 15). */\n  durationIncrementMinutes?: number;\n  /** Minimum lead time between \"now\" and a bookable occurrence start. */\n  minNoticeMinutes?: number;\n  /** How far ahead, in days, occurrences may be booked. */\n  bookingHorizonDays?: number;\n  /** ISO dates (`YYYY-MM-DD`) on which no slot may occur. */\n  blackoutDates?: string[];\n  /**\n   * ISO weekday numbers (1 = Monday … 7 = Sunday) on which slots may occur.\n   * All days are working days when omitted.\n   */\n  workingDays?: SlotPlannerIsoWeekday[];\n};\n\n/** ISO weekday number: 1 = Monday … 7 = Sunday. */\nexport type SlotPlannerIsoWeekday = 1 | 2 | 3 | 4 | 5 | 6 | 7;\n\n/** Machine-readable constraint violation kinds. */\nexport const slotPlannerViolationCodes = [\n  \"daily-cap\",\n  \"weekly-cap\",\n  \"overlap\",\n  \"min-duration\",\n  \"max-duration\",\n  \"duration-increment\",\n  \"min-notice\",\n  \"booking-horizon\",\n  \"blackout-date\",\n  \"non-working-day\",\n  \"invalid-wall-clock-time\",\n] as const;\n\nexport type SlotPlannerViolationCode =\n  (typeof slotPlannerViolationCodes)[number];\n\n/**\n * Structured constraint violation. Apps filter and log by `code`; the\n * taxonomy's `violationMessages` render codes to human-readable text for the\n * inline UI, with `params` available as `{tokens}`.\n */\nexport type SlotPlannerViolation = {\n  code: SlotPlannerViolationCode;\n  params?: Record<string, string | number>;\n};\n\n/**\n * Count-aware message template keyed by CLDR plural categories (as produced\n * by `Intl.PluralRules`). `other` is the required fallback; the remaining\n * categories are optional refinements. `{tokens}` are replaced with runtime\n * values.\n */\nexport type SlotPlannerCountTemplate = {\n  zero?: string;\n  one?: string;\n  two?: string;\n  few?: string;\n  many?: string;\n  other: string;\n};\n\n/**\n * Every noun, verb, and announcement SlotPlanner renders, across manage and\n * book modes. Values are plain strings or count templates; `{tokens}` are\n * replaced with runtime values, including `{slot}`/`{slotPlural}` (the\n * configured nouns) and `{statusLabel}`/`{requestableLabel}` (resolved\n * entries from `statusLabels`, so summaries can never drift from the\n * labels). Override any subset via the `taxonomy` prop; the defaults speak\n * neutral slot language.\n */\nexport type SlotPlannerTaxonomy = {\n  calendarAdd: string;\n  dayActions: string;\n  slotActions: string;\n  calendarHint: string;\n  calendarSlotCount: SlotPlannerCountTemplate;\n  moreOptions: string;\n  endTimePreview: string;\n  bookRequestSent: string;\n  /** Singular noun for a slot, e.g. \"slot\", \"session\", \"appointment\". */\n  slot: string;\n  slotPlural: string;\n  /** Labels for derived occurrence statuses (chips, summaries, filters). */\n  statusLabels: Record<SlotPlannerOccurrenceStatus, string>;\n  addSlot: string;\n  editSlot: string;\n  deleteSlot: string;\n  deleteSeries: string;\n  recurringWeekly: string;\n  recurringBiweekly: string;\n  copyDay: string;\n  copyWeek: string;\n  clearDay: string;\n  viewSwitcherLabel: string;\n  weekView: string;\n  dayView: string;\n  /** Legend for the copy-day target-date checkbox group. */\n  copyDayTargetsLegend: string;\n  copyWeekConfirmTitle: string;\n  /** Tokens: `{slot}`, `{slotPlural}`. */\n  copyWeekConfirmBody: string;\n  clearDayConfirmTitle: string;\n  /** Tokens: `{slot}`, `{slotPlural}`. */\n  clearDayConfirmBody: string;\n  /** Confirm action for copy operations. */\n  apply: string;\n  previousWeek: string;\n  nextWeek: string;\n  previousDay: string;\n  nextDay: string;\n  thisWeek: string;\n  today: string;\n  /** Persistent accessible label for the day-rail tablist. */\n  weekRailLabel: string;\n  /**\n   * Day-card summary for one status count, e.g. \"2 requestable\". Tokens:\n   * `{count}`, `{statusLabel}`. Plural category keyed on `{count}`.\n   */\n  statusCountSummary: SlotPlannerCountTemplate;\n  /**\n   * Daily cap meter. Tokens: `{used}`, `{cap}`, `{requestableLabel}`,\n   * `{slot}`, `{slotPlural}`. Plural category keyed on `{cap}`.\n   */\n  dailyCapSummary: SlotPlannerCountTemplate;\n  /** Weekly cap meter; same tokens as `dailyCapSummary`. */\n  weeklyCapSummary: SlotPlannerCountTemplate;\n  /** Book-mode remaining seats. Tokens: `{remaining}`. */\n  remainingSeats: SlotPlannerCountTemplate;\n  /** Duration chip text. Tokens: `{count}`. */\n  durationSummary: SlotPlannerCountTemplate;\n  /** Buffer metadata text. Tokens: `{count}`. */\n  bufferSummary: SlotPlannerCountTemplate;\n  emptyDay: string;\n  pastDay: string;\n  loading: string;\n  error: string;\n  /** Book-mode request action label. Tokens: `{slot}`. */\n  requestSlot: string;\n  /** Book-mode label for an occurrence with no seats left. */\n  slotFull: string;\n  /**\n   * Book-mode secondary line giving the provider-zone wall-clock context of\n   * an occurrence, shown when the viewer's zone differs from the slot's\n   * zone. Tokens: `{time}` (provider wall-clock start), `{timeZone}` (the\n   * slot's IANA zone).\n   */\n  providerTimeContext: string;\n  /**\n   * Book-mode message shown when the focused day has occurrences but none\n   * of them are requestable. Tokens: `{slot}`, `{slotPlural}`.\n   */\n  noAvailableSlots: string;\n  /** Affordance shown while a mutation callback promise is pending. */\n  savePending: string;\n  /** Affordance shown when a mutation callback promise rejects. */\n  saveError: string;\n  retry: string;\n  /** Affordance shown while a book-request callback promise is pending. */\n  bookRequestPending: string;\n  /** Affordance shown when a book-request callback promise rejects. */\n  bookRequestError: string;\n  /** Editor dialog title when creating. Tokens: `{slot}`. */\n  editorCreateTitle: string;\n  /** Editor dialog title when editing. Tokens: `{slot}`. */\n  editorEditTitle: string;\n  /** Legend for the occurrence-versus-series edit scope choice. */\n  editScopeLegend: string;\n  editScopeOccurrence: string;\n  editScopeSeries: string;\n  fieldDate: string;\n  fieldStartTime: string;\n  fieldDurationMinutes: string;\n  fieldCapacity: string;\n  fieldTimeZone: string;\n  fieldBufferBeforeMinutes: string;\n  fieldBufferAfterMinutes: string;\n  fieldTags: string;\n  fieldNote: string;\n  fieldRecurrence: string;\n  recurrenceNone: string;\n  /** Label for the repeat-until date, shown only for recurring slots. */\n  recurrenceUntil: string;\n  save: string;\n  cancel: string;\n  confirmDelete: string;\n  /** Choice deleting a single occurrence of a recurring {slot}. */\n  deleteOccurrence: string;\n  /** Tokens: `{slot}`. */\n  deleteOccurrenceConfirmTitle: string;\n  /** Tokens: `{slot}`. */\n  deleteSeriesConfirmTitle: string;\n  /** Tokens: `{slot}`. */\n  deleteSeriesConfirmBody: string;\n  /**\n   * Human-readable rendering of structured constraint violations. Violation\n   * `params` are available as `{tokens}`.\n   */\n  violationMessages: Record<SlotPlannerViolationCode, string>;\n  /** Heading for the editor's inline violation list. */\n  violationsHeading: string;\n  /** Template announced after creation, e.g. \"{slot} added\". */\n  announceCreated: string;\n  announceUpdated: string;\n  announceDeleted: string;\n  announceSeriesDeleted: string;\n  /** Template announced on week change, e.g. \"Showing week of {weekStart}\". */\n  announceWeekChanged: string;\n  announceDailyCapReached: string;\n  announceWeeklyCapReached: string;\n  /**\n   * Accepted half of a batch announcement, e.g. \"2 slots added\". Plural\n   * category keyed on the accepted count.\n   */\n  announceBatchApplied: SlotPlannerCountTemplate;\n  /**\n   * Rejected half of a batch announcement, e.g. \"1 slot rejected\". Announced\n   * after `announceBatchApplied` (joined with \". \") when a batch has\n   * rejections. Plural category keyed on the rejected count.\n   */\n  announceBatchRejected: SlotPlannerCountTemplate;\n  announceDayCleared: string;\n  /** Announced after a book request is submitted. Tokens: `{slot}`. */\n  announceBookRequested: string;\n};\n\n/** Partial taxonomy accepted by the `taxonomy` prop and merged over defaults. */\nexport type SlotPlannerTaxonomyInput = Partial<\n  Omit<SlotPlannerTaxonomy, \"statusLabels\" | \"violationMessages\">\n> & {\n  statusLabels?: Partial<Record<SlotPlannerOccurrenceStatus, string>>;\n  violationMessages?: Partial<Record<SlotPlannerViolationCode, string>>;\n};\n\nexport const defaultSlotPlannerTaxonomy: SlotPlannerTaxonomy = {\n  calendarAdd: \"Add slot\",\n  dayActions: \"Day actions\",\n  slotActions: \"Slot actions\",\n  calendarHint:\n    \"Select a slot to view its details and manage it. Times are shown in the zone above.\",\n  calendarSlotCount: { one: \"{count} slot\", other: \"{count} slots\" },\n  moreOptions: \"More options\",\n  endTimePreview: \"Ends at {time} · {timeZone}\",\n  bookRequestSent: \"Request sent\",\n  slot: \"slot\",\n  slotPlural: \"slots\",\n  statusLabels: {\n    draft: \"draft\",\n    requestable: \"requestable\",\n    requested: \"requested\",\n    booked: \"booked\",\n    blocked: \"blocked\",\n    expired: \"expired\",\n    cancelled: \"cancelled\",\n  },\n  addSlot: \"Add slot to this day\",\n  editSlot: \"Edit slot\",\n  deleteSlot: \"Delete slot\",\n  deleteSeries: \"Delete series\",\n  recurringWeekly: \"Recurring weekly\",\n  recurringBiweekly: \"Recurring biweekly\",\n  copyDay: \"Copy day\",\n  copyWeek: \"Copy week\",\n  clearDay: \"Clear day\",\n  viewSwitcherLabel: \"Planner view\",\n  weekView: \"Week\",\n  dayView: \"Day\",\n  copyDayTargetsLegend: \"Copy to\",\n  copyWeekConfirmTitle: \"Copy this week forward?\",\n  copyWeekConfirmBody: \"Copies every {slot} from this week to next week.\",\n  clearDayConfirmTitle: \"Clear this day?\",\n  clearDayConfirmBody:\n    \"This removes every {slot} on this day. Locked {slotPlural} are kept.\",\n  apply: \"Apply\",\n  previousWeek: \"Previous week\",\n  nextWeek: \"Next week\",\n  previousDay: \"Previous day\",\n  nextDay: \"Next day\",\n  thisWeek: \"This week\",\n  today: \"Today\",\n  weekRailLabel: \"Days of the week\",\n  statusCountSummary: {\n    other: \"{count} {statusLabel}\",\n  },\n  dailyCapSummary: {\n    one: \"Daily cap: {used} / {cap} {requestableLabel} {slot}\",\n    other: \"Daily cap: {used} / {cap} {requestableLabel} {slotPlural}\",\n  },\n  weeklyCapSummary: {\n    one: \"Weekly cap: {used} / {cap} {requestableLabel} {slot}\",\n    other: \"Weekly cap: {used} / {cap} {requestableLabel} {slotPlural}\",\n  },\n  remainingSeats: {\n    one: \"{remaining} seat left\",\n    other: \"{remaining} seats left\",\n  },\n  durationSummary: {\n    one: \"{count} min\",\n    other: \"{count} min\",\n  },\n  bufferSummary: {\n    one: \"{count} min buffer\",\n    other: \"{count} min buffer\",\n  },\n  emptyDay: \"No slots on this day\",\n  pastDay: \"This day is in the past\",\n  loading: \"Loading slots\",\n  error: \"Unable to load slots\",\n  requestSlot: \"Request {slot}\",\n  slotFull: \"Full\",\n  providerTimeContext: \"{time} {timeZone}\",\n  noAvailableSlots: \"No {slotPlural} available on this day\",\n  savePending: \"Saving\",\n  saveError: \"Save failed\",\n  retry: \"Retry\",\n  bookRequestPending: \"Requesting\",\n  bookRequestError: \"Request failed\",\n  editorCreateTitle: \"Add {slot}\",\n  editorEditTitle: \"Edit {slot}\",\n  editScopeLegend: \"Apply changes to\",\n  editScopeOccurrence: \"This occurrence only\",\n  editScopeSeries: \"Entire series\",\n  fieldDate: \"Date\",\n  fieldStartTime: \"Start time\",\n  fieldDurationMinutes: \"Duration (minutes)\",\n  fieldCapacity: \"Capacity\",\n  fieldTimeZone: \"Time zone\",\n  fieldBufferBeforeMinutes: \"Buffer before (minutes)\",\n  fieldBufferAfterMinutes: \"Buffer after (minutes)\",\n  fieldTags: \"Tags\",\n  fieldNote: \"Note\",\n  fieldRecurrence: \"Repeats\",\n  recurrenceNone: \"Does not repeat\",\n  recurrenceUntil: \"Repeat until\",\n  save: \"Save\",\n  cancel: \"Cancel\",\n  confirmDelete: \"Delete\",\n  deleteOccurrence: \"Delete this occurrence\",\n  deleteOccurrenceConfirmTitle: \"Delete this {slot}?\",\n  deleteSeriesConfirmTitle: \"Delete this {slot} series?\",\n  deleteSeriesConfirmBody: \"This deletes every occurrence of this {slot}.\",\n  violationMessages: {\n    \"daily-cap\": \"Daily cap reached\",\n    \"weekly-cap\": \"Weekly cap reached\",\n    overlap: \"Overlaps another {slot}\",\n    \"min-duration\": \"Shorter than the minimum duration\",\n    \"max-duration\": \"Longer than the maximum duration\",\n    \"duration-increment\": \"Duration must be a multiple of {increment} minutes\",\n    \"min-notice\": \"Starts sooner than the minimum notice period\",\n    \"booking-horizon\": \"Starts beyond the booking horizon\",\n    \"blackout-date\": \"Falls on a blackout date\",\n    \"non-working-day\": \"Falls on a non-working day\",\n    \"invalid-wall-clock-time\": \"Time does not exist on this date in {timeZone}\",\n  },\n  violationsHeading: \"Fix before saving\",\n  announceCreated: \"{slot} added\",\n  announceUpdated: \"{slot} updated\",\n  announceDeleted: \"{slot} deleted\",\n  announceSeriesDeleted: \"{slot} series deleted\",\n  announceWeekChanged: \"Showing week of {weekStart}\",\n  announceDailyCapReached: \"Daily cap reached\",\n  announceWeeklyCapReached: \"Weekly cap reached\",\n  announceBatchApplied: {\n    one: \"{count} {slot} added\",\n    other: \"{count} {slotPlural} added\",\n  },\n  announceBatchRejected: {\n    one: \"{count} {slot} rejected\",\n    other: \"{count} {slotPlural} rejected\",\n  },\n  announceDayCleared: \"Day cleared\",\n  announceBookRequested: \"{slot} requested\",\n};\n\nexport type SlotPlannerCreatePayload<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> = {\n  slot: SlotPlannerSlotData<TData>;\n};\n\nexport type SlotPlannerUpdatePayload<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> = {\n  slot: SlotPlannerSlotData<TData>;\n  previous: SlotPlannerSlotData<TData>;\n};\n\nexport type SlotPlannerDeleteOccurrencePayload = SlotPlannerOccurrenceRef;\n\nexport type SlotPlannerDeleteSeriesPayload = {\n  slotId: string;\n};\n\nexport type SlotPlannerBookRequestPayload = SlotPlannerOccurrenceRef & {\n  /** Seats requested. Defaults to 1. */\n  seats?: number;\n  /** IANA zone the requester was viewing when selecting the occurrence. */\n  viewerTimeZone?: string;\n};\n\n/**\n * Batch payload emitted by copy-day, copy-week, and clear-day operations.\n * `createdSlots` and the ids keyed in `violations` are disjoint: a slot is\n * either created or rejected with its violations, never both.\n */\nexport type SlotPlannerBatchChangePayload<\n  TData extends SlotPlannerSlotPayload = SlotPlannerSlotPayload,\n> = {\n  createdSlots: SlotPlannerSlotData<TData>[];\n  deletedSlotIds: string[];\n  /**\n   * Full replacements for existing slots, keyed by id. Recurring occurrences\n   * cleared by clear-day land here as `cancelled: true` override upserts —\n   * a series is never deleted wholesale by a batch operation.\n   */\n  updatedSlots?: SlotPlannerSlotData<TData>[];\n  /** Structured violations keyed by rejected slot id. */\n  violations: Record<string, SlotPlannerViolation[]>;\n};\n"
    },
    {
      "path": "packages/components/src/components/slot-planner/slot-planner-fixtures.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/slot-planner/slot-planner-fixtures.ts",
      "content": "import type {\n  SlotPlannerConstraints,\n  SlotPlannerSlotData,\n  SlotPlannerTaxonomyInput,\n} from \"./slot-planner-contract\";\n\n/**\n * Canonical SlotPlanner sample data shared by Storybook, playground smoke\n * checks, and consumer demos. Not part of the package's public entry —\n * import this module directly (or receive it via the registry item). The\n * dataset is anchored to the week of Monday 2026-07-06 (the reference\n * design's week) and intentionally covers:\n *\n * - recurring weekly and biweekly series, one with per-occurrence overrides\n *   including a cancelled occurrence and a fully booked occurrence\n * - every stored slot state, plus booking/requested/past data that derives\n *   every occurrence status\n * - multiple IANA zones (Europe/London, America/New_York, Asia/Kolkata)\n * - a weekly series that spans the Europe/London DST end on 2026-10-25,\n *   so wall-clock expansion across a transition is exercised\n * - capacity > 1 with partial booking\n * - conventional `data` keys (tags, note) plus a custom key that default\n *   renderers must ignore\n */\nexport const slotPlannerSampleSlots: SlotPlannerSlotData[] = [\n  {\n    id: \"mon-morning-architecture\",\n    date: \"2026-07-06\",\n    startTime: \"14:15\",\n    durationMinutes: 60,\n    timeZone: \"Europe/London\",\n    state: \"requestable\",\n    recurrence: { frequency: \"weekly\" },\n    bufferBeforeMinutes: 0,\n    bufferAfterMinutes: 0,\n    data: {\n      tags: [\n        \"Frontend Architecture Review\",\n        \"Data structure and algorithms\",\n        \"Staff Full-Stack Engineer Mock Interview\",\n        \"Playwright\",\n      ],\n    },\n  },\n  {\n    id: \"mon-evening-architecture\",\n    date: \"2026-07-06\",\n    startTime: \"18:00\",\n    durationMinutes: 60,\n    timeZone: \"Europe/London\",\n    state: \"requestable\",\n    recurrence: { frequency: \"weekly\" },\n    data: {\n      tags: [\"Frontend Architecture Review\", \"Playwright\"],\n      note: \"Runs until the autumn cohort ends.\",\n    },\n  },\n  {\n    // requestedCount derives occurrence status \"requested\".\n    id: \"tue-pairing\",\n    date: \"2026-07-07\",\n    startTime: \"09:30\",\n    durationMinutes: 45,\n    timeZone: \"Europe/London\",\n    state: \"requestable\",\n    requestedCount: 1,\n    bufferBeforeMinutes: 10,\n    bufferAfterMinutes: 5,\n    data: { tags: [\"Pair Programming\"] },\n  },\n  {\n    id: \"wed-group-systems\",\n    date: \"2026-07-08\",\n    startTime: \"17:00\",\n    durationMinutes: 90,\n    timeZone: \"America/New_York\",\n    state: \"requestable\",\n    recurrence: {\n      frequency: \"biweekly\",\n      until: \"2026-12-16\",\n      overrides: [\n        { occurrenceDate: \"2026-08-05\", cancelled: true },\n        { occurrenceDate: \"2026-09-02\", startTime: \"18:00\" },\n        // Fully booked occurrence: derives status \"booked\" for this date.\n        { occurrenceDate: \"2026-07-22\", bookedCount: 4 },\n      ],\n    },\n    capacity: 4,\n    bookedCount: 2,\n    data: {\n      tags: [\"System Design\", \"Group Session\"],\n      // Custom key outside the conventional contract; default renderers\n      // must ignore it while custom renderers receive it untouched.\n      priceUsd: 40,\n    },\n  },\n  {\n    // bookedCount at capacity (default 1) derives occurrence status \"booked\".\n    id: \"thu-booked-review\",\n    date: \"2026-07-09\",\n    startTime: \"11:00\",\n    durationMinutes: 60,\n    timeZone: \"Europe/London\",\n    state: \"requestable\",\n    bookedCount: 1,\n    data: { tags: [\"Frontend Architecture Review\"] },\n  },\n  {\n    id: \"fri-blocked-admin\",\n    date: \"2026-07-10\",\n    startTime: \"13:00\",\n    durationMinutes: 120,\n    timeZone: \"Europe/London\",\n    state: \"blocked\",\n    data: { note: \"Held for internal planning.\" },\n  },\n  {\n    // Past date derives occurrence status \"expired\" at render time.\n    id: \"past-intro-call\",\n    date: \"2026-06-29\",\n    startTime: \"10:00\",\n    durationMinutes: 30,\n    timeZone: \"Europe/London\",\n    state: \"requestable\",\n  },\n  {\n    id: \"cancelled-office-hours\",\n    date: \"2026-07-15\",\n    startTime: \"16:00\",\n    durationMinutes: 60,\n    timeZone: \"Asia/Kolkata\",\n    state: \"cancelled\",\n    data: { tags: [\"Office Hours\"] },\n  },\n  {\n    id: \"autumn-dst-spanning-clinic\",\n    // Weekly Sunday-morning series whose occurrences straddle the\n    // Europe/London DST end (2026-10-25): wall-clock 09:00 stays 09:00\n    // across the transition while the UTC offset changes.\n    date: \"2026-10-18\",\n    startTime: \"09:00\",\n    durationMinutes: 60,\n    timeZone: \"Europe/London\",\n    state: \"requestable\",\n    recurrence: { frequency: \"weekly\", until: \"2026-11-08\" },\n    data: { tags: [\"Career Clinic\"] },\n  },\n  {\n    id: \"draft-new-offering\",\n    date: \"2026-07-13\",\n    startTime: \"08:00\",\n    durationMinutes: 30,\n    timeZone: \"Europe/London\",\n    state: \"draft\",\n    data: { tags: [\"Speed Mentoring\"], note: \"Not published yet.\" },\n  },\n];\n\n/** Constraints matching the reference design's \"2 / 3 requestable\" cap. */\nexport const slotPlannerSampleConstraints: SlotPlannerConstraints = {\n  dailyRequestableCap: 3,\n  weeklyRequestableCap: 12,\n  minDurationMinutes: 15,\n  maxDurationMinutes: 180,\n  durationIncrementMinutes: 15,\n  minNoticeMinutes: 720,\n  bookingHorizonDays: 90,\n  blackoutDates: [\"2026-12-25\", \"2026-12-26\"],\n};\n\n/** Mentoring vocabulary matching the reference design. */\nexport const slotPlannerMentoringTaxonomy: SlotPlannerTaxonomyInput = {\n  slot: \"session\",\n  slotPlural: \"sessions\",\n  addSlot: \"Add session to this day\",\n  announceCreated: \"Session added\",\n  announceUpdated: \"Session updated\",\n  announceDeleted: \"Session deleted\",\n};\n\n/** Clinic vocabulary example used in docs and stories. */\nexport const slotPlannerClinicTaxonomy: SlotPlannerTaxonomyInput = {\n  slot: \"appointment\",\n  slotPlural: \"appointments\",\n  statusLabels: { requestable: \"open\", booked: \"scheduled\" },\n  addSlot: \"Add appointment\",\n};\n"
    },
    {
      "path": "packages/components/src/components/slot-planner/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/slot-planner/index.ts",
      "content": "// Canonical sample data lives in ./slot-planner-fixtures and is deliberately\n// not re-exported here: it is shared by tests, Storybook, and the registry\n// item without becoming public package API.\nexport {\n  defaultSlotPlannerTaxonomy,\n  slotPlannerOccurrenceStatuses,\n  slotPlannerSlotStates,\n  slotPlannerViolationCodes,\n  type SlotPlannerBatchChangePayload,\n  type SlotPlannerBookRequestPayload,\n  type SlotPlannerConstraints,\n  type SlotPlannerConventionalSlotData,\n  type SlotPlannerCountTemplate,\n  type SlotPlannerCreatePayload,\n  type SlotPlannerDeleteOccurrencePayload,\n  type SlotPlannerDeleteSeriesPayload,\n  type SlotPlannerIsoWeekday,\n  type SlotPlannerOccurrenceOverride,\n  type SlotPlannerOccurrenceRef,\n  type SlotPlannerOccurrenceStatus,\n  type SlotPlannerRecurrence,\n  type SlotPlannerRecurrenceFrequency,\n  type SlotPlannerSlotData,\n  type SlotPlannerSlotPayload,\n  type SlotPlannerSlotState,\n  type SlotPlannerTaxonomy,\n  type SlotPlannerTaxonomyInput,\n  type SlotPlannerUpdatePayload,\n  type SlotPlannerViolation,\n  type SlotPlannerViolationCode,\n} from \"./slot-planner-contract\";\n// The constraints engine is pure and React-free so apps can run the exact\n// same checks on a server before persisting.\nexport {\n  countSlotPlannerPublishedOccurrences,\n  validateSlotPlannerSlot,\n  validateSlotPlannerSlots,\n  type SlotPlannerValidationContext,\n} from \"./slot-planner-constraints\";\n// The editor and delete dialogs are internal building blocks of SlotPlanner\n// and deliberately not exported; the pure CRUD helpers are public so apps can\n// mirror the uncontrolled mutation rules on their own state.\nexport {\n  applySlotPlannerBatch,\n  applySlotPlannerMutation,\n  createSlotFromEditorValues,\n  updateSlotFromEditorValues,\n  upsertSlotPlannerOccurrenceOverride,\n  type SlotPlannerEditorRecurrence,\n  type SlotPlannerEditorResult,\n  type SlotPlannerEditorSeriesValues,\n  type SlotPlannerMutation,\n} from \"./slot-planner-crud\";\n// Render-prop contexts: one per surface, each with a `renderDefault()`\n// escape hatch so consumers can decorate the shipped default.\nexport type {\n  SlotPlannerCapMeterRenderContext,\n  SlotPlannerDayCardRenderContext,\n  SlotPlannerDayHeaderRenderContext,\n  SlotPlannerEmptyDayRenderContext,\n  SlotPlannerRenderers,\n  SlotPlannerSlotCardRenderContext,\n  SlotPlannerSlotEditorRenderContext,\n  SlotPlannerTagRenderContext,\n  SlotPlannerToolbarRenderContext,\n} from \"./slot-planner-renderers\";\nexport { SlotPlanner, type SlotPlannerProps } from \"./slot-planner\";\n// Pure, contract-typed reader for the conventional `tags`/`note` slot data,\n// exported so custom `slotCard` renderers can normalize payloads exactly like\n// the shipped default cards do.\nexport { getConventionalSlotData } from \"./slot-planner-dom-shared\";\n// Book mode: the consumer-facing picker over the same slot collection model,\n// projecting occurrences into the viewer's time zone.\nexport {\n  SlotPicker,\n  type SlotPickerProps,\n  type SlotPickerRenderers,\n  type SlotPickerSlotCardRenderContext,\n} from \"./slot-picker\";\n// The headless hook: all non-visual SlotPlanner state and dispatch, no JSX\n// and no Motion, for fully custom layouts.\nexport {\n  useSlotPlanner,\n  type SlotPlannerDailyCapInfo,\n  type SlotPlannerWeeklyCapInfo,\n  type SlotPlannerView,\n  type UseSlotPlannerActionOptions,\n  type UseSlotPlannerOptions,\n  type UseSlotPlannerReturn,\n} from \"./use-slot-planner\";\nexport {\n  deriveOccurrenceStatus,\n  expandSlotOccurrences,\n  expandSlotsForRange,\n  expandSlotsForViewerZone,\n  formatSlotPlannerCountTemplate,\n  formatSlotPlannerTemplate,\n  getSlotPlannerIsoDateInZone,\n  getSlotPlannerWeekDays,\n  projectSlotPlannerOccurrenceToZone,\n  resolveSlotPlannerTaxonomy,\n  summarizeSlotPlannerDay,\n  type SlotPickerOccurrence,\n  type SlotPickerOccurrencesByDate,\n  type SlotPlannerOccurrence,\n  type SlotPlannerOccurrencesByDate,\n} from \"./slot-planner-utils\";\n"
    },
    {
      "path": "packages/components/src/utils/cn.ts",
      "type": "registry:file",
      "target": "~/components/dethink/utils/cn.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
    }
  ]
}
