{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "event-calendar-editing",
  "title": "Event Calendar — Editing",
  "author": "ilinxa",
  "description": "Drag-and-resize editing, quick-compose creation, keyboard mutations, cross-surface clipboard, and permission-gated actions for the event calendar.",
  "dependencies": [
    "date-fns",
    "lucide-react",
    "@dnd-kit/core",
    "@dnd-kit/utilities"
  ],
  "registryDependencies": [
    "@ilinxa/event-calendar",
    "@ilinxa/task-card",
    "context-menu",
    "input",
    "button"
  ],
  "files": [
    {
      "path": "src/registry/components/data/event-calendar/features/editing/index.ts",
      "content": "// The editing feature slice for event-calendar (P3 feature-slicing —\n// `@ilinxa/event-calendar-editing`). Import `calendarEditing` and pass it to\n// `<EventCalendar editable editing={calendarEditing} />` (or\n// `<EventCalendarRoot editable editing={calendarEditing}>`) to enable\n// drag/resize/create/clipboard/keyboard editing. See the base package's\n// `editing?: CalendarEditExtension` prop (`@ilinxa/event-calendar`).\n\nimport { CalendarEditProvider } from \"./provider\";\nimport type { CalendarEditExtension } from \"../../hooks/use-calendar-edit-extension\";\n\nexport const calendarEditing: CalendarEditExtension = {\n  Provider: CalendarEditProvider,\n};\n\n// Edit-surface components (Tier B/C) — for advanced à-la-carte composition;\n// the assembly + default parts wire these in automatically via `edit.components`.\nexport { CalendarQuickComposer } from \"./parts/calendar-quick-composer\";\nexport { CalendarEventContextMenu } from \"./parts/calendar-context-menu\";\nexport {\n  CalendarEventEditorOverlay,\n  CalendarRenameField,\n  EventEditorPanel,\n} from \"./parts/calendar-edit-overlays\";\n\n// Cross-surface task clipboard (the unified family envelope, hosted in\n// task-card) — moved off the base barrel with the rest of the editing\n// surface (v0.3); a read-only base install has no copy/paste UI to serve.\nexport {\n  TASK_CLIPBOARD_KIND,\n  TASK_CLIPBOARD_VERSION,\n  serializeTasks,\n  parseTasks,\n  reassignTaskIds,\n  writeTasksToClipboardEvent,\n  readTasksFromClipboardEvent,\n} from \"../../../task-card/lib/clipboard\";\nexport type { TaskClipboardEnvelope } from \"../../../task-card/lib/clipboard\";\n\n// Edit-only public types (still DEFINED in the base package's `types.ts` —\n// `CalendarProps`'s edit-prop fields reference them — but re-exported from\n// here instead of the base barrel, since a read-only base consumer never\n// needs to name them).\nexport type {\n  CalendarSnap,\n  CalendarEditAction,\n  CalendarComposerTarget,\n  CalendarQuickComposerRenderer,\n} from \"../../types\";\nexport type {\n  TaskItemAddedEvent,\n  TaskItemRemovedEvent,\n  TaskItemMovedEvent,\n  TaskFieldEditedEvent,\n  TaskStatusChangedEvent,\n} from \"../../types\";\n\n// The injection seam's types, re-exported for convenience (also available\n// from the base barrel).\nexport type {\n  CalendarEditExtension,\n  CalendarEditProps,\n  CalendarEditContextValue,\n} from \"../../hooks/use-calendar-edit-extension\";\n",
      "type": "registry:component",
      "target": "components/event-calendar/features/editing/index.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/features/editing/provider.tsx",
      "content": "\"use client\";\n\n/**\n * The editing feature's Provider — the implementation `event-calendar`'s\n * `editing?: CalendarEditExtension` prop wires in. Mounted by\n * `EventCalendarRoot` (base) only when BOTH `editable` and `editing` are set.\n *\n * Reads the base calendar context (`useCalendar()`) for the fields the edit\n * state machine and gestures need (occurrences / view / focusDate /\n * scrollToHour / selectedId / select / statusOptions) — it is rendered INSIDE\n * `CalendarContext.Provider`, so this resolves. Runs the `useCalendarEdit`\n * state machine, builds the `@dnd-kit` sensors + `onDragEnd` (day-granular\n * drag-to-reschedule), the cross-surface clipboard effect, the focus-restore\n * effect (F-11), and the `handleEventKey` / `handleDayEnterKey` /\n * `handleMethods` glue the base Root's keyboard router + imperative handle\n * delegate to — then supplies the whole `CalendarEditContextValue`\n * (dispatchers + transient UI state + the `components`/`gestures` records the\n * base parts render through) via `CalendarEditContext`, wrapped in the\n * `DndContext`. Mounts nothing visual itself.\n */\n\nimport { useCallback, useEffect, useMemo, useRef } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { DndContext, PointerSensor, useSensor, useSensors } from \"@dnd-kit/core\";\nimport type { DragEndEvent } from \"@dnd-kit/core\";\nimport { addDays, differenceInCalendarDays, startOfDay } from \"date-fns\";\nimport { useCalendar } from \"../../hooks/use-calendar-context\";\nimport { CalendarEditContext } from \"../../hooks/use-calendar-edit-extension\";\nimport type {\n  CalendarEditContextValue,\n  CalendarEditProps,\n} from \"../../hooks/use-calendar-edit-extension\";\nimport { coveredDays } from \"../../lib/segments\";\nimport { snapStepMs } from \"./lib/edit-mutations\";\nimport { useCalendarEdit } from \"./hooks/use-calendar-edit\";\nimport { useCalendarClipboard } from \"./lib/clipboard-effect\";\nimport { createTimeGridGestures, BandDropCell } from \"./lib/time-grid-gestures\";\nimport { DraggableEventWrap } from \"./parts/calendar-edit-affordances\";\nimport { DroppableDayCell } from \"./parts/droppable-day-cell\";\nimport { CalendarEventContextMenu } from \"./parts/calendar-context-menu\";\nimport {\n  CalendarEventEditorOverlay,\n  CalendarRenameField,\n  EventEditorPanel,\n} from \"./parts/calendar-edit-overlays\";\nimport { CalendarQuickComposer } from \"./parts/calendar-quick-composer\";\nimport type { CalendarOccurrence, TaskItem } from \"../../types\";\n\nconst COMPONENTS: CalendarEditContextValue[\"components\"] = {\n  DraggableEventWrap,\n  DroppableDayCell,\n  BandDropCell,\n  EventContextMenu: CalendarEventContextMenu,\n  EditOverlays: CalendarEventEditorOverlay,\n  RenameField: CalendarRenameField,\n  QuickComposer: CalendarQuickComposer,\n  EventEditorPanel,\n};\n\nexport function CalendarEditProvider({\n  editProps,\n  children,\n}: {\n  editProps: CalendarEditProps;\n  children: ReactNode;\n}) {\n  const base = useCalendar();\n  const {\n    data,\n    onChange,\n    onTaskReschedule,\n    onItemAdded,\n    onItemRemoved,\n    onItemMoved,\n    onFieldEdited,\n    onStatusChanged,\n    permissions,\n    canMoveItem,\n    canResizeItem,\n    canDeleteItem,\n    canCreateChild,\n    canEditItem,\n    onPermissionDenied,\n    snap,\n    quickCompose,\n    onExternalDrop,\n    renderQuickComposer,\n    rootRef,\n  } = editProps;\n\n  const edit = useCalendarEdit({\n    data,\n    editable: true,\n    snap,\n    statusOptions: base.statusOptions,\n    select: base.select,\n    onChange,\n    onTaskReschedule,\n    onItemAdded,\n    onItemRemoved,\n    onItemMoved,\n    onFieldEdited,\n    onStatusChanged,\n    permissions,\n    canMoveItem,\n    canResizeItem,\n    canDeleteItem,\n    canCreateChild,\n    canEditItem,\n    onPermissionDenied,\n  });\n  const {\n    can,\n    getItem,\n    rescheduleItem,\n    createItem,\n    pasteTasks,\n    deleteItem,\n    renameItemAction,\n    changeStatus,\n    changePriority,\n    applyEditedSubtree,\n    editingId,\n    openEditor,\n    closeEditor,\n    renamingId,\n    beginRename,\n    endRename,\n    composerTarget,\n    openComposer,\n    closeComposer,\n    resizePreview,\n    setResizePreview,\n  } = edit;\n\n  // Restore-focus target for keyboard-opened transients (editor / rename /\n  // composer) — F-11.\n  const restoreFocusRef = useRef<string | null>(null);\n\n  // ── @dnd-kit sensors + day-granular drag-to-reschedule ──\n  const sensors = useSensors(\n    useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n  );\n  const onDragEnd = useCallback(\n    (e: DragEndEvent) => {\n      const occ = e.active.data.current?.occ as CalendarOccurrence | undefined;\n      const targetDayMs = e.over?.data.current?.dayMs as number | undefined;\n      if (!occ || targetDayMs == null) return;\n      const sourceDayMs = startOfDay(occ.startMs).getTime();\n      const deltaDays = differenceInCalendarDays(\n        new Date(targetDayMs),\n        new Date(sourceDayMs),\n      );\n      if (deltaDays === 0) return;\n      const newStart = addDays(new Date(occ.startMs), deltaDays).getTime();\n      const hasSpan = occ.endMs > occ.startMs;\n      const newEnd = hasSpan\n        ? addDays(new Date(occ.endMs), deltaDays).getTime()\n        : undefined;\n      rescheduleItem(\n        occ.id,\n        { startMs: newStart, endMs: newEnd, allDay: occ.allDay },\n        \"move\",\n      );\n    },\n    [rescheduleItem],\n  );\n\n  // ── cross-surface clipboard (copy/cut/paste) ──\n  useCalendarClipboard({\n    rootRef,\n    editable: true,\n    selectedId: base.selectedId,\n    getItem,\n    deleteItem,\n    pasteTasks,\n    occurrences: base.occurrences,\n    view: base.view,\n    focusDate: base.focusDate,\n    scrollToHour: base.scrollToHour,\n    editingId,\n    renamingId,\n    composerTarget,\n  });\n\n  // ── focus restore (F-11): after a keyboard-opened transient closes ──\n  useEffect(() => {\n    if (editingId || renamingId || composerTarget) return;\n    const target = restoreFocusRef.current;\n    if (!target) return;\n    restoreFocusRef.current = null;\n    const raf = requestAnimationFrame(() => {\n      const sel = target.startsWith(\"day:\")\n        ? `[data-day-ms=\"${target.slice(4)}\"]`\n        : `[data-occ-id=\"${target}\"]`;\n      (rootRef.current?.querySelector(sel) as HTMLElement | null)?.focus();\n    });\n    return () => cancelAnimationFrame(raf);\n  }, [editingId, renamingId, composerTarget, rootRef]);\n\n  // ── quick-create routing (drag/dblclick over empty time-grid space) ──\n  const requestCreate = useCallback(\n    (win: { startMs: number; endMs?: number; allDay: boolean }, x: number, y: number) => {\n      if (quickCompose) {\n        openComposer({\n          date: new Date(win.startMs),\n          allDay: win.allDay,\n          defaultEnd: new Date(win.endMs ?? win.startMs),\n          x,\n          y,\n        });\n      } else {\n        createItem(null, undefined, win);\n      }\n    },\n    [quickCompose, openComposer, createItem],\n  );\n\n  const gestures = useMemo(\n    () =>\n      createTimeGridGestures({\n        snap,\n        reschedule: rescheduleItem,\n        setPreview: setResizePreview,\n        requestCreate,\n      }),\n    [snap, rescheduleItem, setResizePreview, requestCreate],\n  );\n\n  // ── event-focused keyboard editing (moves/resizes/delete/edit/rename) ──\n  const handleEventKey = useCallback(\n    (e: KeyboardEvent<HTMLDivElement>, id: string): boolean => {\n      const occ = base.occurrences.find((o) => o.id === id);\n      if (!occ) return false;\n      const timed = (base.view === \"week\" || base.view === \"day\") && !occ.allDay;\n      const stepMs = snapStepMs(snap) || 900_000;\n      const hasSpan = occ.endMs > occ.startMs;\n      // Day-granular ←/→ moves shift by CALENDAR days via `addDays` — the same\n      // math as the pointer drop path, DST-safe where a raw MS_PER_DAY add is a\n      // silent no-op across the fall-back day (v0.2.4). Timed moves in the time\n      // grid shift by the snap step.\n      const shiftMs = (ms: number, dir: number) =>\n        timed ? ms + dir * stepMs : addDays(new Date(ms), dir).getTime();\n\n      // Resize the END by one unit. All-day events resize in whole days against\n      // the exclusive-end convention (same `addDays` midnight math as the drag\n      // grip, so keyboard and pointer agree — no off-by-one, no DST drift) and\n      // never collapse past their start; milestones are points in time and\n      // aren't resizable.\n      const resizeEnd = (dir: number) => {\n        if (occ.kind === \"milestone\") return;\n        if (occ.allDay) {\n          const cov = coveredDays(occ);\n          const curEnd = startOfDay(addDays(new Date(cov.lastMs), 1)).getTime();\n          const minEnd = startOfDay(addDays(new Date(cov.firstMs), 1)).getTime();\n          rescheduleItem(\n            id,\n            {\n              endMs: Math.max(minEnd, addDays(new Date(curEnd), dir).getTime()),\n              allDay: true,\n            },\n            \"resize\",\n          );\n        } else {\n          const base2 = hasSpan ? occ.endMs : occ.startMs;\n          rescheduleItem(\n            id,\n            { endMs: base2 + dir * stepMs, allDay: false },\n            \"resize\",\n          );\n        }\n      };\n\n      switch (e.key) {\n        case \"ArrowLeft\":\n        case \"ArrowRight\": {\n          const dir = e.key === \"ArrowRight\" ? 1 : -1;\n          if (e.shiftKey) {\n            resizeEnd(dir);\n          } else {\n            rescheduleItem(\n              id,\n              {\n                startMs: shiftMs(occ.startMs, dir),\n                endMs: hasSpan ? shiftMs(occ.endMs, dir) : undefined,\n                allDay: occ.allDay,\n              },\n              \"move\",\n            );\n          }\n          e.preventDefault();\n          return true;\n        }\n        case \"ArrowUp\":\n        case \"ArrowDown\": {\n          if (!timed || !e.shiftKey) return false; // resize-only, time grid only\n          resizeEnd(e.key === \"ArrowDown\" ? 1 : -1);\n          e.preventDefault();\n          return true;\n        }\n        case \"Delete\":\n        case \"Backspace\":\n          deleteItem(id);\n          e.preventDefault();\n          return true;\n        case \"Enter\":\n          restoreFocusRef.current = id;\n          openEditor(id);\n          e.preventDefault();\n          return true;\n        case \"F2\":\n          restoreFocusRef.current = id;\n          beginRename(id);\n          e.preventDefault();\n          return true;\n        default:\n          return false;\n      }\n    },\n    [base.occurrences, base.view, snap, rescheduleItem, deleteItem, openEditor, beginRename],\n  );\n\n  const handleDayEnterKey = useCallback(\n    (dayMs: number) => {\n      const d = startOfDay(new Date(dayMs));\n      restoreFocusRef.current = `day:${dayMs}`;\n      openComposer({ date: d, allDay: true, defaultEnd: d });\n    },\n    [openComposer],\n  );\n\n  const handleMethods = useMemo<CalendarEditContextValue[\"handleMethods\"]>(\n    () => ({\n      addTask: (date: Date, item?: Partial<TaskItem>) =>\n        createItem(null, item, { startMs: startOfDay(date).getTime(), allDay: true }),\n      deleteTask: (id: string) => deleteItem(id),\n      editTask: (id: string) => openEditor(id),\n      beginRename: (id: string) => beginRename(id),\n      openQuickComposer: (date: Date, allDay = true) =>\n        openComposer({\n          date: startOfDay(date),\n          allDay,\n          defaultEnd: allDay ? startOfDay(date) : new Date(date.getTime() + 3_600_000),\n        }),\n    }),\n    [createItem, deleteItem, openEditor, beginRename, openComposer],\n  );\n\n  const value = useMemo<CalendarEditContextValue>(\n    () => ({\n      editable: true,\n      snap,\n      quickCompose,\n      permissions,\n      can,\n      getItem,\n      rescheduleItem,\n      createItem,\n      deleteItem,\n      renameItemAction,\n      changeStatus,\n      changePriority,\n      applyEditedSubtree,\n      editingId,\n      openEditor,\n      closeEditor,\n      renamingId,\n      beginRename,\n      endRename,\n      composerTarget,\n      openComposer,\n      closeComposer,\n      resizePreview,\n      setResizePreview,\n      onExternalDrop,\n      renderQuickComposer,\n      components: COMPONENTS,\n      gestures,\n      handleEventKey,\n      handleDayEnterKey,\n      handleMethods,\n    }),\n    [\n      snap,\n      quickCompose,\n      permissions,\n      can,\n      getItem,\n      rescheduleItem,\n      createItem,\n      deleteItem,\n      renameItemAction,\n      changeStatus,\n      changePriority,\n      applyEditedSubtree,\n      editingId,\n      openEditor,\n      closeEditor,\n      renamingId,\n      beginRename,\n      endRename,\n      composerTarget,\n      openComposer,\n      closeComposer,\n      resizePreview,\n      setResizePreview,\n      onExternalDrop,\n      renderQuickComposer,\n      gestures,\n      handleEventKey,\n      handleDayEnterKey,\n      handleMethods,\n    ],\n  );\n\n  return (\n    <CalendarEditContext.Provider value={value}>\n      <DndContext sensors={sensors} onDragEnd={onDragEnd}>\n        {children}\n      </DndContext>\n    </CalendarEditContext.Provider>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/features/editing/provider.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/features/editing/hooks/use-calendar-edit.ts",
      "content": "\"use client\";\n\n/**\n * The calendar editing state machine (v0.2.0) — the date-grid sibling of gantt's\n * `use-gantt-edit.ts`. Owns the transient edit-UI targets (detail-editor +\n * inline-rename + quick-composer) and the mutation dispatchers. Every dispatcher\n * is a single chokepoint: re-check permission (veto-before-dispatch), compute the\n * NEXT forest from `data` via the pure `edit-mutations`, fire the typed\n * task-card-shaped event, then `onChange(next)`. No internal copy of `data`\n * — the controlled consumer echoes. When `editable` is false, `can()` returns\n * false so every dispatcher no-ops (the v1 read-only guarantee).\n *\n * Deltas vs gantt: no group-move (calendar renders a parent's own window, so a\n * parent moves alone via `setWindow`); reschedule is all-day aware (ms + allDay,\n * not ISO strings); create seeds `startAt`/`expireAt` from the gesture window;\n * `changePriority` is added (mutate + onChange only — \"priority\" is not a\n * `TaskEditableField`, so no typed field event, matching the card).\n */\n\nimport { useCallback, useMemo, useState } from \"react\";\nimport type {\n  CalendarComposerTarget,\n  CalendarEditAction,\n  CalendarSnap,\n  TaskEditableField,\n  TaskFieldEditedEvent,\n  TaskItem,\n  TaskItemAddedEvent,\n  TaskItemMovedEvent,\n  TaskItemRemovedEvent,\n  TaskPermissionReason,\n  TaskPermissionRule,\n  TaskPermissions,\n  TaskStatusChangedEvent,\n  TaskStatusOption,\n} from \"../../../types\";\nimport {\n  addItem,\n  buildIndex,\n  formatDateValue,\n  isAncestor,\n  moveItem,\n  removeItem,\n  renameItem,\n  setWindow,\n} from \"../lib/edit-mutations\";\nimport { evalCalendarPermission } from \"../lib/edit-permissions\";\nimport { reassignTaskIds } from \"../../../../task-card/lib/clipboard\";\nimport { parseDateValue } from \"../../../lib/classify\";\n\ntype Args = {\n  data: TaskItem[];\n  editable: boolean;\n  snap: CalendarSnap;\n  statusOptions?: TaskStatusOption[];\n  /** Select a node (newly created events auto-select). */\n  select?: (id: string | null) => void;\n  onChange?: (data: TaskItem[]) => void;\n  onTaskReschedule?: (next: {\n    itemId: string;\n    startAt: string;\n    expireAt?: string;\n  }) => void;\n  onItemAdded?: (e: TaskItemAddedEvent) => void;\n  onItemRemoved?: (e: TaskItemRemovedEvent) => void;\n  onItemMoved?: (e: TaskItemMovedEvent) => void;\n  onFieldEdited?: (e: TaskFieldEditedEvent) => void;\n  onStatusChanged?: (e: TaskStatusChangedEvent) => void;\n  permissions?: TaskPermissions;\n  canMoveItem?: (id: string) => boolean;\n  canResizeItem?: (id: string) => boolean;\n  canDeleteItem?: (id: string) => boolean;\n  canCreateChild?: (id: string) => boolean;\n  canEditItem?: (id: string) => boolean;\n  onPermissionDenied?: (\n    action: keyof TaskPermissionRule,\n    itemId: string,\n    reason: TaskPermissionReason,\n  ) => void;\n};\n\nconst ACTION_RULE: Record<CalendarEditAction, keyof TaskPermissionRule> = {\n  move: \"drag\",\n  resize: \"drag\",\n  delete: \"remove\",\n  create: \"addChildren\",\n  editDetails: \"edit\",\n};\n\nfunction freshId(): string {\n  if (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto) {\n    return `cal-${crypto.randomUUID()}`;\n  }\n  return `cal-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;\n}\n\nexport function useCalendarEdit(args: Args) {\n  const {\n    data,\n    editable,\n    snap,\n    statusOptions,\n    select,\n    onChange,\n    onTaskReschedule,\n    onItemAdded,\n    onItemRemoved,\n    onItemMoved,\n    onFieldEdited,\n    onStatusChanged,\n    permissions,\n    canMoveItem,\n    canResizeItem,\n    canDeleteItem,\n    canCreateChild,\n    canEditItem,\n    onPermissionDenied,\n  } = args;\n\n  const index = useMemo(() => buildIndex(data), [data]);\n  const [editingId, setEditingId] = useState<string | null>(null);\n  const [renamingId, setRenamingId] = useState<string | null>(null);\n  const [composerTarget, setComposerTarget] =\n    useState<CalendarComposerTarget | null>(null);\n  // Live drag/resize preview — the renderers show this geometry mid-gesture; the\n  // mutation commits only on release. Cleared when null.\n  const [resizePreview, setResizePreview] = useState<{\n    id: string;\n    startMs: number;\n    endMs: number;\n  } | null>(null);\n\n  const getItem = useCallback(\n    (id: string): TaskItem | undefined => index.get(id)?.item,\n    [index],\n  );\n\n  const predicateFor = useCallback(\n    (action: CalendarEditAction): ((id: string) => boolean) | undefined => {\n      switch (action) {\n        case \"move\":\n          return canMoveItem;\n        case \"resize\":\n          return canResizeItem;\n        case \"delete\":\n          return canDeleteItem;\n        case \"create\":\n          return canCreateChild;\n        case \"editDetails\":\n          return canEditItem;\n      }\n    },\n    [canMoveItem, canResizeItem, canDeleteItem, canCreateChild, canEditItem],\n  );\n\n  const can = useCallback(\n    (action: CalendarEditAction, item: TaskItem): boolean => {\n      if (!editable) return false;\n      const level = index.get(item.id)?.level ?? 1;\n      if (!evalCalendarPermission(permissions, action, item, level))\n        return false;\n      const pred = predicateFor(action);\n      if (pred && !pred(item.id)) return false;\n      return true;\n    },\n    [editable, index, permissions, predicateFor],\n  );\n\n  /** Guard a dispatch: return the item if allowed, else report denial + null. */\n  const guard = useCallback(\n    (action: CalendarEditAction, id: string): TaskItem | null => {\n      const item = index.get(id)?.item;\n      if (!item) return null;\n      if (can(action, item)) return item;\n      const reason: TaskPermissionReason = item.locked\n        ? \"locked\"\n        : predicateFor(action)?.(id) === false\n          ? \"predicate\"\n          : \"default\";\n      onPermissionDenied?.(ACTION_RULE[action], id, reason);\n      return null;\n    },\n    [index, can, predicateFor, onPermissionDenied],\n  );\n\n  const rescheduleItem = useCallback(\n    (\n      id: string,\n      patch: { startMs?: number; endMs?: number; allDay: boolean },\n      kind: \"move\" | \"resize\",\n    ) => {\n      const item = guard(kind, id);\n      if (!item) return;\n      const next = setWindow(data, id, patch);\n      if (next === data) return;\n      const updated = buildIndex(next).get(id)?.item;\n      if (!updated) return;\n      // Fire field events gated on the patch touching the key AND the instant\n      // really changing — compare by parsed ms (date-only goes floating-local),\n      // so a format-only rewrite (e.g. `+03:00` → `Z`) doesn't emit a phantom.\n      if (patch.startMs != null) {\n        const oldMs = parseDateValue(item.startAt ?? item.setAt).ms;\n        const newMs = parseDateValue(updated.startAt ?? updated.setAt).ms;\n        if (Number.isFinite(newMs) && newMs !== oldMs) {\n          onFieldEdited?.({\n            itemId: id,\n            key: \"startAt\",\n            oldValue: item.startAt,\n            newValue: updated.startAt,\n          });\n        }\n      }\n      if (patch.endMs != null) {\n        const oldMs = parseDateValue(item.expireAt ?? \"\").ms;\n        const newMs = parseDateValue(updated.expireAt ?? \"\").ms;\n        if (Number.isFinite(newMs) && newMs !== oldMs) {\n          onFieldEdited?.({\n            itemId: id,\n            key: \"expireAt\",\n            oldValue: item.expireAt,\n            newValue: updated.expireAt,\n          });\n        }\n      }\n      onTaskReschedule?.({\n        itemId: id,\n        startAt: updated.startAt ?? updated.setAt,\n        expireAt: updated.expireAt,\n      });\n      onChange?.(next);\n    },\n    [data, guard, onFieldEdited, onTaskReschedule, onChange],\n  );\n\n  const createItem = useCallback(\n    (\n      parentId: string | null,\n      seed: Partial<TaskItem> | undefined,\n      window: { startMs: number; endMs?: number; allDay: boolean },\n      opts?: { openEditor?: boolean },\n    ) => {\n      // Root create gates on `editable` only; child create gates the PARENT's\n      // addChildren rule.\n      if (!editable) return;\n      if (parentId != null && !guard(\"create\", parentId)) return;\n      const nowIso = new Date().toISOString();\n      const item: TaskItem = {\n        id: seed?.id ?? freshId(),\n        name: seed?.name ?? \"New event\",\n        status: seed?.status ?? statusOptions?.[0]?.value ?? \"todo\",\n        active: seed?.active ?? true,\n        setAt: seed?.setAt ?? nowIso,\n        ...seed,\n        startAt: formatDateValue(window.startMs, window.allDay),\n      };\n      if (window.endMs != null && Number.isFinite(window.endMs)) {\n        item.expireAt = formatDateValue(window.endMs, window.allDay);\n      }\n      const next = addItem(data, parentId, item);\n      onItemAdded?.({ parentId: parentId ?? \"\", item });\n      onChange?.(next);\n      select?.(item.id); // auto-select the new event (opens the inspector on it)\n      setComposerTarget(null);\n      if (opts?.openEditor) setEditingId(item.id); // \"More options\" → full editor\n    },\n    [editable, guard, statusOptions, data, onItemAdded, onChange, select],\n  );\n\n  /**\n   * Paste tasks from the cross-surface clipboard at a target window. Each root is\n   * re-id'd (no collision with the source), its dates are rewritten from the\n   * TARGET window (so paste lands where focus is AND converts all-day⇄timed by\n   * the target), then inserted. One `onChange` for the whole paste (one undo\n   * step); root-create gates on `editable`, child-paste on the parent's rule.\n   */\n  const pasteTasks = useCallback(\n    (\n      items: TaskItem[],\n      window: { startMs: number; endMs?: number; allDay: boolean },\n      parentId: string | null = null,\n    ) => {\n      if (!editable || items.length === 0) return;\n      if (parentId != null && !guard(\"create\", parentId)) return;\n      let next = data;\n      const created: TaskItem[] = [];\n      for (const raw of items) {\n        // Re-id the (typed `TaskItem`) source, then the TARGET window overrides the\n        // dates — so the paste lands where focus is AND converts all-day⇄timed.\n        const root: TaskItem = {\n          ...reassignTaskIds(raw),\n          startAt: formatDateValue(window.startMs, window.allDay),\n        };\n        if (window.endMs != null && Number.isFinite(window.endMs)) {\n          root.expireAt = formatDateValue(window.endMs, window.allDay);\n        } else {\n          delete root.expireAt;\n        }\n        if (root.duration != null) delete root.duration; // canonicalize to expireAt-driven\n        next = addItem(next, parentId, root);\n        created.push(root);\n      }\n      if (next === data) return;\n      created.forEach((item) =>\n        onItemAdded?.({ parentId: parentId ?? \"\", item }),\n      );\n      onChange?.(next);\n      select?.(created[created.length - 1]?.id ?? null);\n    },\n    [editable, guard, data, onItemAdded, onChange, select],\n  );\n\n  const deleteItem = useCallback(\n    (id: string) => {\n      if (!guard(\"delete\", id)) return;\n      const { next, removed, parentId } = removeItem(data, id);\n      if (!removed) return;\n      onItemRemoved?.({ itemId: id, removed, parentId: parentId ?? \"\" });\n      onChange?.(next);\n      if (editingId === id) setEditingId(null);\n      if (renamingId === id) setRenamingId(null);\n    },\n    [data, guard, onItemRemoved, onChange, editingId, renamingId],\n  );\n\n  const renameItemAction = useCallback(\n    (id: string, name: string) => {\n      const item = guard(\"editDetails\", id);\n      if (!item || name === item.name) return;\n      const next = renameItem(data, id, name);\n      onFieldEdited?.({\n        itemId: id,\n        key: \"name\",\n        oldValue: item.name,\n        newValue: name,\n      });\n      onChange?.(next);\n    },\n    [data, guard, onFieldEdited, onChange],\n  );\n\n  /** Reserved reparent path (not wired by any v0.2.0 gesture; see plan §1). */\n  const moveItemAction = useCallback(\n    (id: string, newParentId: string | null, newIndex: number) => {\n      const item = guard(\"move\", id);\n      if (!item) return;\n      if (\n        newParentId != null &&\n        (newParentId === id || isAncestor(data, id, newParentId))\n      )\n        return;\n      const from = index.get(id);\n      let targetIndex = newIndex;\n      if (\n        from &&\n        (from.parentId ?? null) === newParentId &&\n        from.index < newIndex\n      ) {\n        targetIndex = newIndex - 1;\n      }\n      const next = moveItem(data, id, newParentId, targetIndex);\n      if (next === data) return;\n      onItemMoved?.({\n        itemId: id,\n        oldParentId: from?.parentId ?? \"\",\n        newParentId: newParentId ?? \"\",\n        oldIndex: from?.index ?? 0,\n        newIndex: targetIndex,\n      });\n      onChange?.(next);\n    },\n    [data, guard, index, onItemMoved, onChange],\n  );\n\n  const changeStatus = useCallback(\n    (id: string, status: string) => {\n      const item = guard(\"editDetails\", id);\n      if (!item || status === item.status) return;\n      const apply = (items: TaskItem[]): TaskItem[] =>\n        items.map((it) =>\n          it.id === id\n            ? { ...it, status }\n            : it.children?.length\n              ? { ...it, children: apply(it.children) }\n              : it,\n        );\n      onStatusChanged?.({\n        itemId: id,\n        oldStatus: item.status,\n        newStatus: status,\n      });\n      onFieldEdited?.({\n        itemId: id,\n        key: \"status\",\n        oldValue: item.status,\n        newValue: status,\n      });\n      onChange?.(apply(data));\n    },\n    [data, guard, onStatusChanged, onFieldEdited, onChange],\n  );\n\n  const changePriority = useCallback(\n    (id: string, priority: string) => {\n      const item = guard(\"editDetails\", id);\n      if (!item || priority === item.priority) return;\n      // \"priority\" is not a `TaskEditableField`, so no typed field event — the\n      // forest echo carries it (parity with the card, which doesn't emit one).\n      const apply = (items: TaskItem[]): TaskItem[] =>\n        items.map((it) =>\n          it.id === id\n            ? { ...it, priority }\n            : it.children?.length\n              ? { ...it, children: apply(it.children) }\n              : it,\n        );\n      onChange?.(apply(data));\n    },\n    [data, guard, onChange],\n  );\n\n  /** Splice an edited subtree (same root id) from the embedded card back in. */\n  const applyEditedSubtree = useCallback(\n    (nextItem: TaskItem) => {\n      const prev = index.get(nextItem.id)?.item;\n      if (!prev) {\n        // Deleted (another path) while its editor was open — splicing an absent\n        // id would echo the UNCHANGED forest, silently dropping the edit. Drop\n        // the stale edit and close instead.\n        setEditingId(null);\n        return;\n      }\n      const apply = (items: TaskItem[]): TaskItem[] =>\n        items.map((it) =>\n          it.id === nextItem.id\n            ? nextItem\n            : it.children?.length\n              ? { ...it, children: apply(it.children) }\n              : it,\n        );\n      const forest = apply(data);\n      // Emit a granular field event per changed editable field, so a consumer\n      // persisting via `onFieldEdited` sees modal / inspector detail-editor edits\n      // too — not only the consolidated forest echo (A1, v0.2.2). The embedded\n      // card's own granular events are not wired (EventEditorPanel passes only\n      // `onChange`), so there is no double-fire. `priority` is intentionally\n      // excluded — it is not a `TaskEditableField`, so it persists via `onChange`\n      // only (parity with `changePriority`).\n      const FIELDS: TaskEditableField[] = [\n        \"name\",\n        \"description\",\n        \"status\",\n        \"active\",\n        \"setAt\",\n        \"startAt\",\n        \"expireAt\",\n        \"duration\",\n      ];\n      for (const key of FIELDS) {\n        const oldValue = prev[key];\n        const newValue = nextItem[key];\n        if (!Object.is(oldValue, newValue)) {\n          onFieldEdited?.({ itemId: nextItem.id, key, oldValue, newValue });\n        }\n      }\n      if (prev.status !== nextItem.status) {\n        onStatusChanged?.({\n          itemId: nextItem.id,\n          oldStatus: prev.status,\n          newStatus: nextItem.status,\n        });\n      }\n      onChange?.(forest);\n    },\n    [data, index, onFieldEdited, onStatusChanged, onChange],\n  );\n\n  const openEditor = useCallback(\n    (id: string) => {\n      if (!guard(\"editDetails\", id)) return;\n      setRenamingId(null); // editor + rename are mutually exclusive transients\n      // Select the item too: the inspector hosts the editor keyed on selection,\n      // so opening the editor (context-menu / Enter) without selecting first\n      // would otherwise be a no-op when the inspector is mounted.\n      select?.(id);\n      setEditingId(id);\n    },\n    [guard, select],\n  );\n  const closeEditor = useCallback(() => setEditingId(null), []);\n  const beginRename = useCallback(\n    (id: string) => {\n      if (!guard(\"editDetails\", id)) return;\n      setEditingId(null); // mutually exclusive with the editor\n      setRenamingId(id);\n    },\n    [guard],\n  );\n  const endRename = useCallback(() => setRenamingId(null), []);\n  const openComposer = useCallback(\n    (target: CalendarComposerTarget) => {\n      if (editable) setComposerTarget(target);\n    },\n    [editable],\n  );\n  const closeComposer = useCallback(() => setComposerTarget(null), []);\n\n  return {\n    editable,\n    snap,\n    can,\n    getItem,\n    rescheduleItem,\n    createItem,\n    pasteTasks,\n    deleteItem,\n    renameItemAction,\n    moveItemAction,\n    changeStatus,\n    changePriority,\n    applyEditedSubtree,\n    editingId,\n    openEditor,\n    closeEditor,\n    renamingId,\n    beginRename,\n    endRename,\n    composerTarget,\n    openComposer,\n    closeComposer,\n    resizePreview,\n    setResizePreview,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/features/editing/hooks/use-calendar-edit.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/features/editing/lib/edit-mutations.ts",
      "content": "/**\n * Pure forest mutations over `TaskItem[]` — the editing layer (v0.2.0).\n *\n * The array analogue of task-card's reducer mutations and the date-grid\n * sibling of gantt's `lib/edit-mutations.ts`: every function returns a NEW forest\n * with structural sharing and NEVER mutates in place, so the controlled consumer\n * can diff/echo and stack undo for free. Framework-free; Vitest-ready. Hierarchy\n * is nested `children` (no parentId on TaskItem), so the index here is what\n * callers use to locate a splice point.\n *\n * Calendar-specific delta vs gantt: `setWindow` is **all-day aware**. Where gantt\n * always serializes via `new Date(ms).toISOString()`, the calendar must write a\n * bare `YYYY-MM-DD` (from LOCAL fields) for all-day items so the value round-trips\n * through `classify.ts`'s floating-local `parseDateValue` without the UTC\n * off-by-one. `formatDateValue` is the inverse of `parseDateValue`.\n */\n\nimport type { CalendarSnap, TaskItem } from \"../../../types\";\nimport { parseDateValue } from \"../../../lib/classify\";\n\n/** Minimum timed span (ms) so a resize/drag can't collapse a block to zero. */\nexport const MIN_DURATION_MS = 60_000;\n\n/** Snap increment (ms) for a `CalendarSnap`. `\"off\"` → 0 (no snap). */\nexport function snapStepMs(snap: CalendarSnap): number {\n  if (typeof snap === \"number\") return Math.max(0, snap);\n  switch (snap) {\n    case \"off\":\n      return 0;\n    case \"minute\":\n      return 60_000;\n    case \"5min\":\n      return 300_000;\n    case \"15min\":\n      return 900_000;\n    case \"30min\":\n      return 1_800_000;\n    case \"hour\":\n      return 3_600_000;\n    case \"day\":\n      return 86_400_000;\n    default:\n      return 900_000; // \"15min\" fallback\n  }\n}\n\n/** Round `ms` to the nearest `step` relative to `originMs` (e.g. the day start). */\nexport function snapToStep(ms: number, originMs: number, step: number): number {\n  if (step <= 0 || !Number.isFinite(ms)) return ms;\n  return originMs + Math.round((ms - originMs) / step) * step;\n}\n\n/**\n * Inverse of `classify.ts`'s `parseDateValue`:\n *   - all-day  → bare `YYYY-MM-DD` built from LOCAL Y/M/D (NEVER `toISOString`,\n *                which is UTC and renders a day early in negative-UTC offsets).\n *   - timed    → full ISO timestamp.\n * Returns \"\" for a non-finite ms so callers never emit `new Date(NaN).toISOString()`.\n */\nexport function formatDateValue(ms: number, allDay: boolean): string {\n  if (!Number.isFinite(ms)) return \"\";\n  if (allDay) {\n    const d = new Date(ms);\n    const p = (n: number) => String(n).padStart(2, \"0\");\n    return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;\n  }\n  return new Date(ms).toISOString();\n}\n\nexport type ForestNodeInfo = {\n  item: TaskItem;\n  parentId: string | null;\n  index: number;\n  /** 1 = root. */\n  level: number;\n};\n\n/** Flat `id → { item, parentId, index, level }` over the whole forest. */\nexport function buildIndex(data: TaskItem[]): Map<string, ForestNodeInfo> {\n  const map = new Map<string, ForestNodeInfo>();\n  const walk = (items: TaskItem[], parentId: string | null, level: number) => {\n    items.forEach((item, index) => {\n      map.set(item.id, { item, parentId, index, level });\n      if (item.children?.length) walk(item.children, item.id, level + 1);\n    });\n  };\n  walk(data, null, 1);\n  return map;\n}\n\n/** Replace the matching item via `fn`; returns the same ref when nothing changed. */\nfunction replace(\n  data: TaskItem[],\n  id: string,\n  fn: (item: TaskItem) => TaskItem,\n): TaskItem[] {\n  let changed = false;\n  const next = data.map((item) => {\n    if (item.id === id) {\n      changed = true;\n      return fn(item);\n    }\n    if (item.children?.length) {\n      const kids = replace(item.children, id, fn);\n      if (kids !== item.children) {\n        changed = true;\n        return { ...item, children: kids };\n      }\n    }\n    return item;\n  });\n  return changed ? next : data;\n}\n\nexport function renameItem(\n  data: TaskItem[],\n  id: string,\n  name: string,\n): TaskItem[] {\n  return replace(data, id, (item) => ({ ...item, name }));\n}\n\n/**\n * Apply a calendar window patch (reschedule / resize / all-day⇄timed convert).\n *\n * Takes epoch **ms + an `allDay` flag** (not ISO strings — the geometry layer\n * hands it numbers and the formatter decides the serialization). The calendar\n * canonicalizes to a `startAt` + `expireAt` pair: when an `endMs` is written, a\n * pre-existing `duration` is dropped (an explicit end is clearer than a duration\n * for a calendar event, and avoids a stale duration shadowing the new expireAt).\n * Enforces `end ≥ start + MIN_DURATION_MS` for timed only (all-day spans are\n * day-quantized and need no ms floor). Non-finite ms leave the field untouched\n * rather than emitting `new Date(NaN).toISOString()`.\n */\nexport function setWindow(\n  data: TaskItem[],\n  id: string,\n  patch: { startMs?: number; endMs?: number; allDay: boolean },\n): TaskItem[] {\n  return replace(data, id, (item) => {\n    const next: TaskItem = { ...item };\n    if (patch.startMs != null && Number.isFinite(patch.startMs)) {\n      let startMs = patch.startMs;\n      // Start-side mirror of the end clamp below (defense in depth — the\n      // gesture layer clamps its commit too, v0.2.4): a timed start-only patch\n      // may not cross the item's existing end, else the persisted window\n      // inverts (startAt > expireAt). When the patch also carries an endMs the\n      // end clamp below owns the invariant instead.\n      if (!patch.allDay && patch.endMs == null) {\n        const endMs = parseDateValue(next.expireAt ?? \"\").ms;\n        if (Number.isFinite(endMs) && startMs > endMs - MIN_DURATION_MS) {\n          startMs = endMs - MIN_DURATION_MS;\n        }\n      }\n      next.startAt = formatDateValue(startMs, patch.allDay);\n    }\n    if (patch.endMs != null) {\n      const startMs =\n        patch.startMs != null && Number.isFinite(patch.startMs)\n          ? patch.startMs\n          : parseDateValue(next.startAt ?? next.setAt).ms;\n      let endMs = patch.endMs;\n      if (\n        !patch.allDay &&\n        Number.isFinite(startMs) &&\n        Number.isFinite(endMs) &&\n        endMs < startMs + MIN_DURATION_MS\n      ) {\n        endMs = startMs + MIN_DURATION_MS;\n      }\n      if (Number.isFinite(endMs)) {\n        next.expireAt = formatDateValue(endMs, patch.allDay);\n        if (next.duration != null) delete next.duration; // canonicalize to expireAt-driven\n      }\n    }\n    return next;\n  });\n}\n\n/** True when `maybeAncestorId` is an ancestor of `id` (circular-drop guard). */\nexport function isAncestor(\n  data: TaskItem[],\n  maybeAncestorId: string,\n  id: string,\n): boolean {\n  const idx = buildIndex(data);\n  let cur = idx.get(id)?.parentId ?? null;\n  while (cur != null) {\n    if (cur === maybeAncestorId) return true;\n    cur = idx.get(cur)?.parentId ?? null;\n  }\n  return false;\n}\n\n/** Insert `item` under `parentId` (null = root) at `index` (default = append). */\nexport function addItem(\n  data: TaskItem[],\n  parentId: string | null,\n  item: TaskItem,\n  index?: number,\n): TaskItem[] {\n  if (parentId == null) {\n    const next = [...data];\n    next.splice(index ?? next.length, 0, item);\n    return next;\n  }\n  return replace(data, parentId, (parent) => {\n    const kids = parent.children ? [...parent.children] : [];\n    kids.splice(index ?? kids.length, 0, item);\n    return { ...parent, children: kids };\n  });\n}\n\n/** Remove an item; returns the new forest + the removed subtree + its old parent. */\nexport function removeItem(\n  data: TaskItem[],\n  id: string,\n): { next: TaskItem[]; removed: TaskItem | null; parentId: string | null } {\n  const info = buildIndex(data).get(id);\n  if (!info) return { next: data, removed: null, parentId: null };\n  const prune = (items: TaskItem[]): TaskItem[] =>\n    items\n      .filter((it) => it.id !== id)\n      .map((it) =>\n        it.children?.length ? { ...it, children: prune(it.children) } : it,\n      );\n  return { next: prune(data), removed: info.item, parentId: info.parentId };\n}\n\n/**\n * Move an item to `newParentId` (null = root) at `newIndex`. The caller guards\n * circular drops (`isAncestor`); `newIndex` is interpreted against the target\n * parent's children AFTER the source has been removed (task-tree semantics).\n * Reserved for the v0.2.0 \"drop-onto-an-event-to-nest\" path (not wired by any\n * gesture this version — calendar's \"move\" is reschedule, not reparent).\n */\nexport function moveItem(\n  data: TaskItem[],\n  id: string,\n  newParentId: string | null,\n  newIndex: number,\n): TaskItem[] {\n  const { next: pruned, removed } = removeItem(data, id);\n  if (!removed) return data;\n  return addItem(pruned, newParentId, removed, newIndex);\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/features/editing/lib/edit-mutations.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/features/editing/lib/edit-permissions.ts",
      "content": "/**\n * Calendar edit-permission resolver. Mirrors gantt's `lib/edit-permissions.ts`\n * (itself mirroring task-tree): maps the calendar's edit actions onto\n * task-card's `TaskPermissionRule` keys and resolves against the shared\n * `TaskPermissions` matrix (+ `item.locked`). One matrix shape across card / tree\n * / gantt / calendar = one mental model for consumers. Framework-free; Vitest-ready.\n */\n\nimport type {\n  CalendarEditAction,\n  TaskItem,\n  TaskPermissionRule,\n  TaskPermissions,\n} from \"../../../types\";\n\n/** Calendar actions piggy-back on the most natural existing rule key. */\nconst ACTION_TO_RULE: Record<CalendarEditAction, keyof TaskPermissionRule> = {\n  move: \"drag\",\n  resize: \"drag\",\n  delete: \"remove\",\n  create: \"addChildren\",\n  editDetails: \"edit\",\n};\n\n/**\n * Effective allow/deny for one action on `item` at `level` (1 = root). Hard lock\n * (`item.locked`) denies everything. Lookup order byItem → byLevel → default,\n * then a `true` fallback; `inherit !== false` ascends on undefined keys.\n */\nexport function evalCalendarPermission(\n  permissions: TaskPermissions | undefined,\n  action: CalendarEditAction,\n  item: TaskItem,\n  level: number,\n): boolean {\n  if (item.locked) return false;\n  if (!permissions) return true;\n\n  const key = ACTION_TO_RULE[action];\n  const inherit = permissions.inherit !== false;\n\n  const byItem = permissions.byItem?.[item.id]?.[key];\n  if (byItem !== undefined) return byItem;\n  if (!inherit && permissions.byItem?.[item.id]) return true;\n\n  const byLevel = permissions.byLevel?.[level]?.[key];\n  if (byLevel !== undefined) return byLevel;\n  if (!inherit && permissions.byLevel?.[level]) return true;\n\n  const def = permissions.default?.[key];\n  if (def !== undefined) return def;\n\n  return true;\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/features/editing/lib/edit-permissions.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/features/editing/lib/paste-window.ts",
      "content": "/**\n * Paste-target resolution — extracted from the pre-split `calendar-root.tsx`\n * (features-editing split, v0.3). Pure; framework-free.\n */\nimport { startOfDay } from \"date-fns\";\nimport { parseDateValue } from \"../../../lib/classify\";\nimport type { CalendarOccurrence, CalendarView, TaskItem } from \"../../../types\";\n\n/** Duration (ms) of a task from its dates; 0 when it has no real span. */\nexport function itemDurationMs(it: TaskItem): number {\n  const startMs = parseDateValue(it.startAt ?? it.setAt).ms;\n  const endMs = it.expireAt ? parseDateValue(it.expireAt).ms : NaN;\n  return Number.isFinite(startMs) && Number.isFinite(endMs) && endMs > startMs\n    ? endMs - startMs\n    : 0;\n}\n\n/**\n * Where a paste lands. The TARGET decides all-day vs timed (so paste also\n * converts): a focused day-cell → that day, all-day; a focused event → its\n * day + all-day-ness; otherwise the current focus date (timed in week/day at\n * `scrollToHour`, else all-day). Duration is carried from the copied item.\n */\nexport function resolvePasteWindow(\n  items: TaskItem[],\n  opts: {\n    activeEl: Element | null;\n    occurrences: CalendarOccurrence[];\n    view: CalendarView;\n    focusDate: Date;\n    scrollToHour: number;\n  },\n): { startMs: number; endMs?: number; allDay: boolean } {\n  const { activeEl, occurrences, view, focusDate, scrollToHour } = opts;\n  const dur = itemDurationMs(items[0]);\n  const dayEl = activeEl?.closest?.(\"[data-day-ms]\") as HTMLElement | null;\n  if (dayEl) {\n    const startMs = Number(dayEl.getAttribute(\"data-day-ms\"));\n    return { startMs, endMs: dur > 0 ? startMs + dur : undefined, allDay: true };\n  }\n  const occEl = activeEl?.closest?.(\"[data-occ-id]\") as HTMLElement | null;\n  if (occEl) {\n    const occ = occurrences.find(\n      (o) => o.id === occEl.getAttribute(\"data-occ-id\"),\n    );\n    if (occ) {\n      const span = occ.endMs > occ.startMs ? occ.endMs - occ.startMs : 0;\n      const length = dur > 0 ? dur : span;\n      return {\n        startMs: occ.startMs,\n        endMs: length > 0 ? occ.startMs + length : undefined,\n        allDay: occ.allDay,\n      };\n    }\n  }\n  const base = startOfDay(focusDate).getTime();\n  if (view === \"week\" || view === \"day\") {\n    const startMs = base + Math.max(0, Math.min(23, scrollToHour)) * 3_600_000;\n    return {\n      startMs,\n      endMs: dur > 0 ? startMs + dur : startMs + 3_600_000,\n      allDay: false,\n    };\n  }\n  return { startMs: base, endMs: dur > 0 ? base + dur : undefined, allDay: true };\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/features/editing/lib/paste-window.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/features/editing/lib/clipboard-effect.ts",
      "content": "\"use client\";\n\n/**\n * Cross-surface clipboard (copy/cut/paste `TaskItem`s through the OS\n * clipboard — the shared `ilinxa/task` envelope; foreign text is ignored).\n * Extracted from the pre-split `calendar-root.tsx` MIXED host (features-editing\n * split, v0.3) as a hook. Document-level so it fires regardless of which\n * focused element the UA targets; gated on the calendar containing focus,\n * skipped over text inputs (so the composer / rename keep native text\n * copy/paste). An open editor / rename / composer owns the clipboard (it may\n * hold non-input controls), so this defers to native copy/paste while one is up.\n */\n\nimport { useEffect } from \"react\";\nimport type { RefObject } from \"react\";\nimport {\n  readTasksFromClipboardEvent,\n  writeTasksToClipboardEvent,\n} from \"../../../../task-card/lib/clipboard\";\nimport { resolvePasteWindow } from \"./paste-window\";\nimport type {\n  CalendarComposerTarget,\n  CalendarOccurrence,\n  CalendarView,\n  TaskItem,\n} from \"../../../types\";\n\ntype Args = {\n  rootRef: RefObject<HTMLDivElement | null>;\n  editable: boolean;\n  selectedId: string | null;\n  getItem: (id: string) => TaskItem | undefined;\n  deleteItem: (id: string) => void;\n  pasteTasks: (\n    items: TaskItem[],\n    window: { startMs: number; endMs?: number; allDay: boolean },\n  ) => void;\n  occurrences: CalendarOccurrence[];\n  view: CalendarView;\n  focusDate: Date;\n  scrollToHour: number;\n  editingId: string | null;\n  renamingId: string | null;\n  composerTarget: CalendarComposerTarget | null;\n};\n\nexport function useCalendarClipboard(args: Args) {\n  const {\n    rootRef,\n    editable,\n    selectedId,\n    getItem,\n    deleteItem,\n    pasteTasks,\n    occurrences,\n    view,\n    focusDate,\n    scrollToHour,\n    editingId,\n    renamingId,\n    composerTarget,\n  } = args;\n\n  useEffect(() => {\n    if (!editable) return;\n    const owns = () => {\n      const active = document.activeElement;\n      return !!rootRef.current && !!active && rootRef.current.contains(active);\n    };\n    const overText = () => {\n      const el = document.activeElement as HTMLElement | null;\n      return (\n        el?.tagName === \"INPUT\" ||\n        el?.tagName === \"TEXTAREA\" ||\n        el?.isContentEditable === true\n      );\n    };\n    const busy = () =>\n      editingId != null || renamingId != null || composerTarget != null;\n    const targetId = (): string | null => {\n      const active = document.activeElement as HTMLElement | null;\n      const occId = active\n        ?.closest?.(\"[data-occ-id]\")\n        ?.getAttribute(\"data-occ-id\");\n      return occId ?? selectedId ?? null;\n    };\n    const onCopy = (e: ClipboardEvent) => {\n      if (!owns() || overText() || busy()) return;\n      const id = targetId();\n      const item = id ? getItem(id) : undefined;\n      if (!item) return;\n      writeTasksToClipboardEvent(e, [item], \"event-calendar\");\n      e.preventDefault();\n    };\n    const onCut = (e: ClipboardEvent) => {\n      if (!owns() || overText() || busy()) return;\n      const id = targetId();\n      const item = id ? getItem(id) : undefined;\n      if (!item) return;\n      writeTasksToClipboardEvent(e, [item], \"event-calendar\");\n      deleteItem(item.id);\n      e.preventDefault();\n    };\n    const onPaste = (e: ClipboardEvent) => {\n      if (!owns() || overText() || busy()) return;\n      const items = readTasksFromClipboardEvent(e);\n      if (!items) return;\n      pasteTasks(\n        items,\n        resolvePasteWindow(items, {\n          activeEl: document.activeElement,\n          occurrences,\n          view,\n          focusDate,\n          scrollToHour,\n        }),\n      );\n      e.preventDefault();\n    };\n    document.addEventListener(\"copy\", onCopy);\n    document.addEventListener(\"cut\", onCut);\n    document.addEventListener(\"paste\", onPaste);\n    return () => {\n      document.removeEventListener(\"copy\", onCopy);\n      document.removeEventListener(\"cut\", onCut);\n      document.removeEventListener(\"paste\", onPaste);\n    };\n  }, [\n    rootRef,\n    editable,\n    selectedId,\n    getItem,\n    deleteItem,\n    pasteTasks,\n    occurrences,\n    view,\n    focusDate,\n    scrollToHour,\n    editingId,\n    renamingId,\n    composerTarget,\n  ]);\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/features/editing/lib/clipboard-effect.ts"
    },
    {
      "path": "src/registry/components/data/event-calendar/features/editing/lib/time-grid-gestures.tsx",
      "content": "\"use client\";\n\n/**\n * Time-grid native-pointer edit gestures (drag-move / drag-resize / draw-to-\n * create / double-click-create) + the all-day band's droppable create cell.\n * Extracted from the pre-split `calendar-time-grid.tsx` MIXED host\n * (features-editing split, v0.3) — moved verbatim except `snap` / the live\n * dispatchers / `quickCompose` routing are now closed over via\n * `createTimeGridGestures(deps)` (called once by the Provider) instead of\n * threaded through every call site, so the base time-grid's call sites shrink\n * to `edit.gestures.<fn>(event, refs…)`.\n */\n\nimport { startOfDay } from \"date-fns\";\nimport { useDroppable } from \"@dnd-kit/core\";\nimport { cn } from \"@/lib/utils\";\nimport { snapStepMs, snapToStep } from \"./edit-mutations\";\nimport { useCalendarEditContext } from \"../../../hooks/use-calendar-edit-extension\";\nimport type { CalendarGestures } from \"../../../hooks/use-calendar-edit-extension\";\nimport type { CalendarSnap } from \"../../../types\";\n\nconst HOUR_PX = 48;\nconst DAY_PX = HOUR_PX * 24;\nconst MS_PER_DAY = 86_400_000;\n\nconst COL_MS = (clientY: number, rect: DOMRect, dayStartMs: number, step: number) =>\n  snapToStep(dayStartMs + ((clientY - rect.top) / rect.height) * MS_PER_DAY, dayStartMs, step);\n\ntype RescheduleFn = (\n  id: string,\n  patch: { startMs?: number; endMs?: number; allDay: boolean },\n  kind: \"move\" | \"resize\",\n) => void;\ntype SetPreviewFn = (\n  p: { id: string; startMs: number; endMs: number } | null,\n) => void;\ntype RequestCreateFn = (\n  win: { startMs: number; endMs?: number; allDay: boolean },\n  x: number,\n  y: number,\n) => void;\n\n/** Build the time-grid gesture handlers, closing over the live edit\n *  dispatchers — base call sites pass only the per-gesture event + refs. */\nexport function createTimeGridGestures(deps: {\n  snap: CalendarSnap;\n  reschedule: RescheduleFn;\n  setPreview: SetPreviewFn;\n  requestCreate: RequestCreateFn;\n}): CalendarGestures {\n  const { snap, reschedule, setPreview, requestCreate } = deps;\n\n  /** Drag a timed block: vertical = time (delta), horizontal = day (target column). */\n  const startTimedMove: CalendarGestures[\"startTimedMove\"] = (\n    e,\n    occ,\n    gridRef,\n    columns,\n    suppressClick,\n    gestureCleanup,\n  ) => {\n    if (e.button !== 0) return;\n    // No stopPropagation: the column's draw handler ignores block-origin\n    // presses, and the context-menu trigger's long-press timer needs the\n    // bubble (Radix cancels it on pointermove, so drags don't open the menu).\n    const grid = gridRef.current;\n    if (!grid) return;\n    // Capture on the source block (gantt's pattern) so the gesture keeps\n    // receiving events off-element and gets a proper pointercancel on\n    // touch scroll-takeover (v0.2.4).\n    e.currentTarget.setPointerCapture(e.pointerId);\n    const pointerId = e.pointerId;\n    const startX = e.clientX;\n    const startY = e.clientY;\n    // Wall-clock time-of-day (H/M/…), NOT an ms offset from local midnight — the\n    // ms delta shifts the hour when the drop column crosses a DST transition (v0.2.4).\n    const startDate = new Date(occ.startMs);\n    const timeOfDay =\n      startDate.getHours() * 3_600_000 +\n      startDate.getMinutes() * 60_000 +\n      startDate.getSeconds() * 1_000 +\n      startDate.getMilliseconds();\n    const dur = occ.endMs - occ.startMs;\n    const step = snapStepMs(snap);\n    let moved = false;\n    const onMove = (ev: PointerEvent) => {\n      if (\n        !moved &&\n        (Math.abs(ev.clientX - startX) > 4 || Math.abs(ev.clientY - startY) > 4)\n      )\n        moved = true;\n    };\n    const onUp = (ev: PointerEvent) => {\n      dispose();\n      if (!moved) return; // a click selects; only a real drag reschedules\n      suppressClick.current = true;\n      const rect = grid.getBoundingClientRect();\n      const n = columns.length;\n      const col = Math.max(\n        0,\n        Math.min(n - 1, Math.floor((ev.clientX - rect.left) / (rect.width / n))),\n      );\n      const dyMs = ((ev.clientY - startY) / DAY_PX) * MS_PER_DAY;\n      const targetDayStart = startOfDay(columns[col]).getTime();\n      const newStart = snapToStep(targetDayStart + timeOfDay + dyMs, targetDayStart, step);\n      reschedule(occ.id, { startMs: newStart, endMs: newStart + dur, allDay: false }, \"move\");\n    };\n    // Abort (browser gesture takeover): tear down, commit nothing (v0.2.4).\n    const onCancel = (ev: PointerEvent) => {\n      if (ev.pointerId !== pointerId) return;\n      dispose();\n    };\n    const dispose = () => {\n      window.removeEventListener(\"pointermove\", onMove);\n      window.removeEventListener(\"pointerup\", onUp);\n      window.removeEventListener(\"pointercancel\", onCancel);\n      gestureCleanup.current = null;\n    };\n    window.addEventListener(\"pointermove\", onMove);\n    window.addEventListener(\"pointerup\", onUp);\n    window.addEventListener(\"pointercancel\", onCancel);\n    gestureCleanup.current = dispose;\n  };\n\n  /** Drag a block's top/bottom edge: set start/end time (snapped). Updates the\n   *  live preview each move; commits on release. */\n  const startTimedResize: CalendarGestures[\"startTimedResize\"] = (\n    e,\n    occ,\n    edge,\n    colRef,\n    dayStartMs,\n    gestureCleanup,\n  ) => {\n    e.stopPropagation();\n    e.preventDefault();\n    const col = colRef.current;\n    if (!col) return;\n    e.currentTarget.setPointerCapture(e.pointerId);\n    const pointerId = e.pointerId;\n    const step = snapStepMs(snap);\n    const MIN = 60_000;\n    const tAt = (clientY: number) => {\n      const rect = col.getBoundingClientRect();\n      return snapToStep(\n        dayStartMs + ((clientY - rect.top) / rect.height) * MS_PER_DAY,\n        dayStartMs,\n        step,\n      );\n    };\n    const onMove = (ev: PointerEvent) => {\n      const t = tAt(ev.clientY);\n      if (edge === \"start\") {\n        setPreview({ id: occ.id, startMs: Math.min(t, occ.endMs - MIN), endMs: occ.endMs });\n      } else {\n        setPreview({ id: occ.id, startMs: occ.startMs, endMs: Math.max(t, occ.startMs + MIN) });\n      }\n    };\n    const onUp = (ev: PointerEvent) => {\n      dispose();\n      const t = tAt(ev.clientY);\n      // Commit the SAME clamped window the preview showed — a raw `t` dragged\n      // past the opposite edge would persist an inverted startAt > expireAt (v0.2.4).\n      if (edge === \"start\")\n        reschedule(occ.id, { startMs: Math.min(t, occ.endMs - MIN), allDay: false }, \"resize\");\n      else\n        reschedule(occ.id, { endMs: Math.max(t, occ.startMs + MIN), allDay: false }, \"resize\");\n    };\n    // Abort (browser gesture takeover): drop the preview, commit nothing (v0.2.4).\n    const onCancel = (ev: PointerEvent) => {\n      if (ev.pointerId !== pointerId) return;\n      dispose();\n    };\n    const dispose = () => {\n      window.removeEventListener(\"pointermove\", onMove);\n      window.removeEventListener(\"pointerup\", onUp);\n      window.removeEventListener(\"pointercancel\", onCancel);\n      setPreview(null);\n      gestureCleanup.current = null;\n    };\n    window.addEventListener(\"pointermove\", onMove);\n    window.addEventListener(\"pointerup\", onUp);\n    window.addEventListener(\"pointercancel\", onCancel);\n    gestureCleanup.current = dispose;\n  };\n\n  /** Drag across an empty column → create a timed event spanning the range. A\n   *  single click does NOT create (that's a double-click); only a real drag does. */\n  const startDraw: CalendarGestures[\"startDraw\"] = (\n    e,\n    colRef,\n    dayStartMs,\n    gestureCleanup,\n  ) => {\n    if (e.button !== 0) return;\n    const col = colRef.current;\n    if (!col) return;\n    e.currentTarget.setPointerCapture(e.pointerId);\n    const pointerId = e.pointerId;\n    const step = snapStepMs(snap) || 900_000;\n    const startMs = COL_MS(e.clientY, col.getBoundingClientRect(), dayStartMs, step);\n    const startY = e.clientY;\n    let moved = false;\n    const onMove = (ev: PointerEvent) => {\n      if (!moved && Math.abs(ev.clientY - startY) > 4) moved = true;\n    };\n    const onUp = (ev: PointerEvent) => {\n      dispose();\n      if (!moved) return; // a click selects/deselects; only a drag creates\n      const endMs = COL_MS(ev.clientY, col.getBoundingClientRect(), dayStartMs, step);\n      const lo = Math.min(startMs, endMs);\n      let hi = Math.max(startMs, endMs);\n      if (hi <= lo) hi = lo + (step || 900_000);\n      requestCreate({ startMs: lo, endMs: hi, allDay: false }, ev.clientX, ev.clientY);\n    };\n    // Abort (browser gesture takeover): tear down, create nothing (v0.2.4).\n    const onCancel = (ev: PointerEvent) => {\n      if (ev.pointerId !== pointerId) return;\n      dispose();\n    };\n    const dispose = () => {\n      window.removeEventListener(\"pointermove\", onMove);\n      window.removeEventListener(\"pointerup\", onUp);\n      window.removeEventListener(\"pointercancel\", onCancel);\n      gestureCleanup.current = null;\n    };\n    window.addEventListener(\"pointermove\", onMove);\n    window.addEventListener(\"pointerup\", onUp);\n    window.addEventListener(\"pointercancel\", onCancel);\n    gestureCleanup.current = dispose;\n  };\n\n  /** Double-click an empty slot → create a default 1-hour timed event there. */\n  const createAtDoubleClick: CalendarGestures[\"createAtDoubleClick\"] = (\n    e,\n    colRef,\n    dayStartMs,\n  ) => {\n    if ((e.target as HTMLElement).closest(\"button\")) return; // not on an event\n    const col = colRef.current;\n    if (!col) return;\n    const step = snapStepMs(snap) || 900_000;\n    const startMs = COL_MS(e.clientY, col.getBoundingClientRect(), dayStartMs, step);\n    requestCreate(\n      { startMs, endMs: startMs + 3_600_000, allDay: false },\n      e.clientX,\n      e.clientY,\n    );\n  };\n\n  return { startTimedMove, startTimedResize, startDraw, createAtDoubleClick };\n}\n\n/** Editable droppable + click-create cell beneath the all-day band overlay\n *  (Tier B — editing feature). Reads the edit context directly: only ever\n *  mounted via `edit.components.BandDropCell`, i.e. inside the extension's\n *  own Provider. */\nexport function BandDropCell({ day }: { day: Date }) {\n  const edit = useCalendarEditContext();\n  const dayMs = startOfDay(day).getTime();\n  const { setNodeRef, isOver } = useDroppable({\n    id: `band:${dayMs}`,\n    data: { dayMs },\n  });\n  return (\n    <div\n      ref={setNodeRef}\n      // Double-click to create (parity with month/time-grid; honors quickCompose).\n      onDoubleClick={(e) => {\n        if (edit.quickCompose) {\n          edit.openComposer({\n            date: new Date(dayMs),\n            allDay: true,\n            defaultEnd: new Date(dayMs),\n            x: e.clientX,\n            y: e.clientY,\n          });\n        } else {\n          edit.createItem(null, undefined, { startMs: dayMs, allDay: true });\n        }\n      }}\n      className={cn(\n        \"min-h-7 cursor-pointer border-l border-border first:border-l-0\",\n        isOver && \"bg-primary/10\",\n      )}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/features/editing/lib/time-grid-gestures.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/features/editing/parts/calendar-edit-affordances.tsx",
      "content": "\"use client\";\n\n/**\n * Shared editable affordances for the DAY-GRANULAR surfaces — the month grid and\n * the week/day all-day band. Both make events @dnd-kit draggables (drops resolve\n * in the editing Provider's `onDragEnd` by whole calendar days) and add edge-resize\n * grips (native pointer, clientX → column → day) + a hover-delete. The continuous\n * TIME grid uses its own native-pointer layer (see `lib/time-grid-gestures.tsx`).\n */\n\nimport type {\n  PointerEvent as ReactPointerEvent,\n  ReactNode,\n  RefObject,\n} from \"react\";\nimport { useEffect, useRef } from \"react\";\nimport { addDays, startOfDay } from \"date-fns\";\nimport { useDraggable } from \"@dnd-kit/core\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { cn } from \"@/lib/utils\";\nimport { useCalendarEditContext } from \"../../../hooks/use-calendar-edit-extension\";\nimport { coveredDays } from \"../../../lib/segments\";\nimport { CalendarEventContextMenu } from \"./calendar-context-menu\";\nimport type { CalendarOccurrence } from \"../../../types\";\n\n/** Edge grip on an all-day bar → native-pointer DAY resize (clientX → column). */\nexport function DayResizeGrip({\n  edge,\n  occ,\n  containerRef,\n  cols,\n}: {\n  edge: \"start\" | \"end\";\n  occ: CalendarOccurrence;\n  containerRef: RefObject<HTMLDivElement | null>;\n  cols: Date[];\n}) {\n  const { rescheduleItem, setResizePreview } = useCalendarEditContext();\n  // Active gesture teardown — run on unmount so a mid-gesture removal can't\n  // leak window listeners or let a later, unrelated pointerup commit (v0.2.4).\n  const gestureCleanup = useRef<(() => void) | null>(null);\n  useEffect(() => () => gestureCleanup.current?.(), []);\n  const colAt = (clientX: number): number => {\n    const el = containerRef.current;\n    if (!el) return 0;\n    const r = el.getBoundingClientRect();\n    return Math.max(\n      0,\n      Math.min(cols.length - 1, Math.floor((clientX - r.left) / (r.width / cols.length))),\n    );\n  };\n  const onPointerDown = (e: ReactPointerEvent) => {\n    e.stopPropagation(); // keep the @dnd-kit drag sensor + draw out of it\n    e.preventDefault();\n    // Capture on the grip (gantt's pattern) so the gesture keeps receiving\n    // events off-element and gets a proper pointercancel on touch\n    // scroll-takeover (v0.2.4).\n    e.currentTarget.setPointerCapture(e.pointerId);\n    const pointerId = e.pointerId;\n    const span = coveredDays(occ);\n    // Resolve the edit window (start ms or exclusive-end ms) for a target column.\n    const windowAt = (clientX: number) => {\n      const dayMs = startOfDay(cols[colAt(clientX)]).getTime();\n      if (edge === \"start\") {\n        const startMs = Math.min(dayMs, startOfDay(span.lastMs).getTime());\n        return { startMs, endMs: occ.endMs };\n      }\n      const lastDay = Math.max(dayMs, startOfDay(span.firstMs).getTime());\n      return {\n        startMs: occ.startMs,\n        endMs: startOfDay(addDays(new Date(lastDay), 1)).getTime(),\n      };\n    };\n    const onMove = (ev: PointerEvent) => {\n      const w = windowAt(ev.clientX);\n      setResizePreview({ id: occ.id, startMs: w.startMs, endMs: w.endMs });\n    };\n    const onUp = (ev: PointerEvent) => {\n      dispose();\n      const w = windowAt(ev.clientX);\n      rescheduleItem(\n        occ.id,\n        edge === \"start\"\n          ? { startMs: w.startMs, allDay: true }\n          : { endMs: w.endMs, allDay: true },\n        \"resize\",\n      );\n    };\n    // Abort (browser gesture takeover): drop the preview, commit nothing (v0.2.4).\n    const onCancel = (ev: PointerEvent) => {\n      if (ev.pointerId !== pointerId) return;\n      dispose();\n    };\n    const dispose = () => {\n      window.removeEventListener(\"pointermove\", onMove);\n      window.removeEventListener(\"pointerup\", onUp);\n      window.removeEventListener(\"pointercancel\", onCancel);\n      setResizePreview(null);\n      gestureCleanup.current = null;\n    };\n    window.addEventListener(\"pointermove\", onMove);\n    window.addEventListener(\"pointerup\", onUp);\n    window.addEventListener(\"pointercancel\", onCancel);\n    gestureCleanup.current = dispose;\n  };\n  return (\n    <button\n      type=\"button\"\n      aria-label={edge === \"start\" ? \"Resize start\" : \"Resize end\"}\n      onPointerDown={onPointerDown}\n      onClick={(e) => e.stopPropagation()}\n      className={cn(\n        \"absolute inset-y-0 z-10 hidden w-2.5 cursor-ew-resize items-center justify-center group-hover:flex\",\n        edge === \"start\" ? \"left-0\" : \"right-0\",\n      )}\n    >\n      <span\n        className=\"h-1/2 w-0.5 rounded-full bg-current opacity-60\"\n        aria-hidden\n      />\n    </button>\n  );\n}\n\n/** Editable wrapper around an all-day segment: @dnd-kit draggable + edge resize\n *  (when `resizable`) + hover-delete. Used by the month grid and the all-day band. */\nexport function DraggableEventWrap({\n  occ,\n  canDrag,\n  resizable,\n  containerRef,\n  cols,\n  children,\n}: {\n  occ: CalendarOccurrence;\n  canDrag: boolean;\n  resizable: boolean;\n  containerRef: RefObject<HTMLDivElement | null>;\n  cols: Date[];\n  children: ReactNode;\n}) {\n  const { attributes, listeners, setNodeRef, transform, isDragging } =\n    useDraggable({ id: occ.id, data: { occ }, disabled: !canDrag });\n  return (\n    <div\n      ref={setNodeRef}\n      {...(canDrag ? listeners : {})}\n      {...attributes}\n      style={transform ? { transform: CSS.Translate.toString(transform) } : undefined}\n      className={cn(\n        \"group relative\",\n        canDrag && \"cursor-grab active:cursor-grabbing\",\n        isDragging && \"z-30 opacity-60\",\n      )}\n    >\n      <CalendarEventContextMenu item={occ.item}>{children}</CalendarEventContextMenu>\n      {resizable ? (\n        <>\n          <DayResizeGrip edge=\"start\" occ={occ} containerRef={containerRef} cols={cols} />\n          <DayResizeGrip edge=\"end\" occ={occ} containerRef={containerRef} cols={cols} />\n        </>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/features/editing/parts/calendar-edit-affordances.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/features/editing/parts/calendar-edit-overlays.tsx",
      "content": "\"use client\";\n\n/**\n * Editing overlays (Tier B — editing feature) mounted by the assembly via\n * `edit.components.EditOverlays` / `edit.components.RenameField`:\n *\n *  - `CalendarEventEditorOverlay` — the detail editor on `editingId`. Surfaces\n *    the same editor the inspector hosts inline, so the context-menu \"Edit…\" +\n *    keyboard Enter work even without the inspector mounted. The assembly\n *    mounts it only when `showInspector` is false (the inspector hosts it\n *    inline → no double).\n *  - `CalendarRenameField` — inline rename on `renamingId`.\n *\n * Both use the same centered modal shell as gantt's `GanttEditPopover` — a plain\n * backdrop button + panel (NOT the shadcn `dialog`/`popover` primitive), so the\n * task family stays consistent and it sidesteps the Radix/Base-UI `asChild`\n * divergence the v1 tooltip avoided the same way. Backdrop click + Esc close.\n * `EventEditorPanel` is the shared, prop-driven editor block reused by the\n * inspector so the lazy `<TaskCard>` boundary is defined once.\n */\n\nimport { lazy, Suspense, useEffect, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport { useCalendar } from \"../../../hooks/use-calendar-context\";\nimport { useCalendarEditContext } from \"../../../hooks/use-calendar-edit-extension\";\nimport type { EventEditorPanelProps } from \"../../../hooks/use-calendar-edit-extension\";\n\nconst LazyTaskCard = lazy(() =>\n  import(\"../../../../task-card\").then((m) => ({ default: m.TaskCard })),\n);\n\n/**\n * Shared editor block (Tier C) — the lazy `<TaskCard editable>` + a Done\n * button. Prop-driven (zero context); used inline by the inspector AND by the\n * centered editor overlay.\n */\nexport function EventEditorPanel({\n  item,\n  statusOptions,\n  priorityOptions,\n  labelOptions,\n  permissions,\n  onChange,\n  onDone,\n  className,\n}: EventEditorPanelProps) {\n  return (\n    <div className={cn(\"flex flex-col gap-2\", className)}>\n      <Suspense\n        fallback={\n          <div className=\"rounded-lg border border-border bg-card p-6 text-center text-sm text-muted-foreground\">\n            Loading editor…\n          </div>\n        }\n      >\n        <LazyTaskCard\n          value={item}\n          editable\n          statusOptions={statusOptions}\n          priorityOptions={priorityOptions}\n          labelOptions={labelOptions}\n          permissions={permissions}\n          onChange={(tree) => onChange(tree)}\n        />\n      </Suspense>\n      <Button\n        size=\"sm\"\n        variant=\"secondary\"\n        className=\"gap-1.5 self-end\"\n        onClick={onDone}\n      >\n        <Check className=\"size-3.5\" /> Done editing\n      </Button>\n    </div>\n  );\n}\n\n/**\n * Centered modal shell — a labelled backdrop button + a panel on top. Mirrors\n * `GanttEditPopover` (no shadcn `dialog`/`popover`; backdrop + Esc close).\n */\nfunction OverlayShell({\n  label,\n  onClose,\n  className,\n  children,\n}: {\n  label: string;\n  onClose: () => void;\n  className?: string;\n  children: ReactNode;\n}) {\n  const contentRef = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") onClose();\n    };\n    window.addEventListener(\"keydown\", onKey);\n    // Move focus into the dialog on open so keyboard / screen-reader users land\n    // inside it (a more specific control, e.g. the rename input, may refocus after).\n    const raf = requestAnimationFrame(() => contentRef.current?.focus());\n    return () => {\n      window.removeEventListener(\"keydown\", onKey);\n      cancelAnimationFrame(raf);\n    };\n  }, [onClose]);\n  return (\n    <div\n      role=\"dialog\"\n      aria-modal=\"true\"\n      aria-label={label}\n      className=\"fixed inset-0 z-50 flex items-center justify-center p-4\"\n    >\n      <button\n        type=\"button\"\n        aria-label=\"Close\"\n        onClick={onClose}\n        className=\"absolute inset-0 cursor-default bg-background/70 backdrop-blur-sm\"\n      />\n      <div\n        ref={contentRef}\n        tabIndex={-1}\n        className={cn(\"relative z-10 w-full max-w-sm outline-none\", className)}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n\n/**\n * Centered detail-editor overlay (Tier B). Renders the shared editor on\n * `editingId` without requiring the inspector. Mounted by the assembly only when\n * `showInspector` is false.\n */\nexport function CalendarEventEditorOverlay() {\n  const { occurrences, statusOptions, priorityOptions, labelOptions } =\n    useCalendar();\n  const { permissions, editingId, applyEditedSubtree, closeEditor } =\n    useCalendarEditContext();\n  const item = editingId\n    ? (occurrences.find((o) => o.id === editingId)?.item ?? null)\n    : null;\n\n  if (!editingId || !item) return null;\n\n  return (\n    <OverlayShell\n      label={`Edit ${item.name}`}\n      onClose={closeEditor}\n      className=\"max-h-[85vh] overflow-auto\"\n    >\n      <EventEditorPanel\n        item={item}\n        statusOptions={statusOptions}\n        priorityOptions={priorityOptions}\n        labelOptions={labelOptions}\n        permissions={permissions}\n        onChange={applyEditedSubtree}\n        onDone={closeEditor}\n      />\n    </OverlayShell>\n  );\n}\n\n/**\n * Inline-rename popup (Tier B). Renders on `renamingId` — a title input prefilled\n * from the item. Enter commits, Esc / Cancel / backdrop aborts.\n */\nexport function CalendarRenameField() {\n  const { occurrences } = useCalendar();\n  const { renamingId, renameItemAction, endRename } = useCalendarEditContext();\n  const item = renamingId\n    ? (occurrences.find((o) => o.id === renamingId)?.item ?? null)\n    : null;\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  // Focus + select on open (rAF so the input is mounted; not a render-time read).\n  useEffect(() => {\n    if (!renamingId) return;\n    const raf = requestAnimationFrame(() => {\n      inputRef.current?.focus();\n      inputRef.current?.select();\n    });\n    return () => cancelAnimationFrame(raf);\n  }, [renamingId]);\n\n  if (!renamingId || !item) return null;\n\n  const commit = () => {\n    const value = inputRef.current?.value.trim();\n    if (value) renameItemAction(renamingId, value);\n    endRename();\n  };\n\n  return (\n    <OverlayShell label={`Rename ${item.name}`} onClose={endRename} className=\"max-w-xs\">\n      <div className=\"rounded-lg border border-border bg-card p-3 shadow-lg\">\n        <label\n          htmlFor=\"cal-rename-input\"\n          className=\"mb-1.5 block text-xs font-medium text-muted-foreground\"\n        >\n          Rename event\n        </label>\n        <Input\n          id=\"cal-rename-input\"\n          key={renamingId}\n          ref={inputRef}\n          defaultValue={item.name}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\") {\n              e.preventDefault();\n              commit();\n            } else if (e.key === \"Escape\") {\n              e.preventDefault();\n              endRename();\n            }\n          }}\n        />\n        <div className=\"mt-2 flex justify-end gap-2\">\n          <Button size=\"sm\" variant=\"ghost\" onClick={() => endRename()}>\n            Cancel\n          </Button>\n          <Button size=\"sm\" onClick={commit}>\n            Save\n          </Button>\n        </div>\n      </div>\n    </OverlayShell>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/features/editing/parts/calendar-edit-overlays.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/features/editing/parts/calendar-quick-composer.tsx",
      "content": "\"use client\";\n\n/**\n * Quick-create composer (Tier B + Tier C — editing feature). When a create\n * gesture sets `composerTarget`, this floats a small title-entry popup at the\n * pointer: type a title + Enter to create, or \"More options\" to open the full\n * editor. Deliberately a plain fixed-position panel (NOT a Radix Popover) to\n * dodge the Base-UI `PopoverAnchor`/`asChild` divergence (the engagement-bar\n * lesson). Mounted via `edit.components.QuickComposer`; reads the edit context.\n */\n\nimport { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { format } from \"date-fns\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { useCalendarEditContext } from \"../../../hooks/use-calendar-edit-extension\";\nimport type { CalendarEditContextValue } from \"../../../hooks/use-calendar-edit-extension\";\nimport type { CalendarComposerTarget, TaskItem } from \"../../../types\";\n\nfunction whenText(t: CalendarComposerTarget): string {\n  if (t.allDay) return `${format(t.date, \"EEE, MMM d\")} · all day`;\n  return `${format(t.date, \"EEE, MMM d\")} · ${format(t.date, \"p\")} – ${format(\n    t.defaultEnd,\n    \"p\",\n  )}`;\n}\n\nfunction windowOf(t: CalendarComposerTarget) {\n  const endMs =\n    t.allDay && t.defaultEnd.getTime() === t.date.getTime()\n      ? undefined\n      : t.defaultEnd.getTime();\n  return { startMs: t.date.getTime(), endMs, allDay: t.allDay };\n}\n\nfunction Floating({\n  x,\n  y,\n  onClose,\n  children,\n}: {\n  x: number;\n  y: number;\n  onClose: () => void;\n  children: ReactNode;\n}) {\n  return (\n    <>\n      <div className=\"fixed inset-0 z-40\" onClick={onClose} aria-hidden />\n      <div\n        role=\"dialog\"\n        aria-label=\"Create event\"\n        className=\"fixed z-50 w-64 rounded-lg border border-border bg-popover p-3 text-popover-foreground shadow-lg\"\n        style={{ left: x, top: y }}\n      >\n        {children}\n      </div>\n    </>\n  );\n}\n\nfunction ComposerPopup({\n  target,\n  onClose,\n  onCreate,\n  render,\n}: {\n  target: CalendarComposerTarget;\n  onClose: () => void;\n  onCreate: CalendarEditContextValue[\"createItem\"];\n  render?: CalendarEditContextValue[\"renderQuickComposer\"];\n}) {\n  const [title, setTitle] = useState(\"\");\n  const commit = (seed?: Partial<TaskItem>) =>\n    onCreate(\n      null,\n      { name: title.trim() || \"New event\", ...seed },\n      windowOf(target),\n    );\n  const openFull = () =>\n    onCreate(null, { name: title.trim() || \"New event\" }, windowOf(target), {\n      openEditor: true,\n    });\n\n  const vw = typeof window !== \"undefined\" ? window.innerWidth : 1024;\n  const vh = typeof window !== \"undefined\" ? window.innerHeight : 768;\n  const x = Math.max(8, Math.min(target.x ?? 80, vw - 272));\n  const y = Math.max(8, Math.min(target.y ?? 80, vh - 180));\n\n  if (render) {\n    return (\n      <Floating x={x} y={y} onClose={onClose}>\n        {render({\n          date: target.date,\n          allDay: target.allDay,\n          defaultEnd: target.defaultEnd,\n          commit,\n          cancel: onClose,\n          openFull,\n        })}\n      </Floating>\n    );\n  }\n\n  return (\n    <Floating x={x} y={y} onClose={onClose}>\n      <form\n        onSubmit={(e) => {\n          e.preventDefault();\n          commit();\n        }}\n        className=\"flex flex-col gap-2\"\n      >\n        <p className=\"text-xs text-muted-foreground\">{whenText(target)}</p>\n        <Input\n          autoFocus\n          value={title}\n          onChange={(e) => setTitle(e.target.value)}\n          onKeyDown={(e) => {\n            if (e.key === \"Escape\") onClose();\n          }}\n          placeholder=\"Add title\"\n          className=\"h-8\"\n        />\n        <div className=\"flex items-center justify-between\">\n          <button\n            type=\"button\"\n            onClick={openFull}\n            className=\"text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline\"\n          >\n            More options\n          </button>\n          <div className=\"flex gap-1.5\">\n            <Button type=\"button\" size=\"sm\" variant=\"ghost\" onClick={onClose}>\n              Cancel\n            </Button>\n            <Button type=\"submit\" size=\"sm\">\n              Create\n            </Button>\n          </div>\n        </div>\n      </form>\n    </Floating>\n  );\n}\n\nexport function CalendarQuickComposer() {\n  const { composerTarget, closeComposer, createItem, renderQuickComposer } =\n    useCalendarEditContext();\n  if (!composerTarget) return null;\n  return (\n    <ComposerPopup\n      key={`${composerTarget.date.getTime()}:${composerTarget.x ?? 0}:${composerTarget.y ?? 0}`}\n      target={composerTarget}\n      onClose={closeComposer}\n      onCreate={createItem}\n      render={renderQuickComposer}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/features/editing/parts/calendar-quick-composer.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/features/editing/parts/calendar-context-menu.tsx",
      "content": "\"use client\";\n\n/**\n * Right-click action menu (Tier B — editing feature). Wraps an event and\n * offers permission-gated Edit / Status / Priority / Delete. Every action\n * routes through the same `use-calendar-edit` dispatchers as the pointer\n * paths (single chokepoint). Only ever mounted via `edit.components.\n * EventContextMenu` — i.e. inside the extension's own Provider — so it needs\n * no `editable`-guard early-return (the read-only path never reaches this\n * component at all). Mirrors gantt's `GanttContextMenu` (same F-cross-13\n * path-b trigger shape — see the trigger comment below).\n */\n\nimport type { ReactNode } from \"react\";\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuSub,\n  ContextMenuSubContent,\n  ContextMenuSubTrigger,\n  ContextMenuTrigger,\n} from \"@/components/ui/context-menu\";\nimport { useCalendar } from \"../../../hooks/use-calendar-context\";\nimport { useCalendarEditContext } from \"../../../hooks/use-calendar-edit-extension\";\nimport { serializeTasks } from \"../../../../task-card/lib/clipboard\";\nimport type { TaskItem } from \"../../../types\";\n\nexport function CalendarEventContextMenu({\n  item,\n  children,\n}: {\n  item: TaskItem;\n  children: ReactNode;\n}) {\n  const base = useCalendar();\n  const edit = useCalendarEditContext();\n  const canEdit = edit.can(\"editDetails\", item);\n  const canDelete = edit.can(\"delete\", item);\n  const statusOptions = base.statusOptions ?? [];\n  const priorityOptions = base.priorityOptions ?? [];\n\n  const copy = () => {\n    void navigator.clipboard\n      ?.writeText(serializeTasks([item], \"event-calendar\"))\n      .catch((err) => {\n        if (process.env.NODE_ENV !== \"production\") {\n          console.warn(\"[event-calendar] copy to clipboard failed:\", err);\n        }\n      });\n  };\n  const cut = () => {\n    // Don't delete-without-copy: if the clipboard is unavailable (insecure\n    // context) the menu cut is a no-op (the keyboard Cut, which uses the sync\n    // clipboard event, still works). The write is async — dispatch the delete\n    // ONLY on fulfillment, so a denied/failed write never destroys the item\n    // with nothing on the clipboard (v0.2.4).\n    if (!navigator.clipboard) return;\n    void navigator.clipboard\n      .writeText(serializeTasks([item], \"event-calendar\"))\n      .then(() => edit.deleteItem(item.id))\n      .catch((err: unknown) => {\n        if (process.env.NODE_ENV !== \"production\") {\n          console.warn(\n            \"[event-calendar] Cut aborted — clipboard write failed; item NOT deleted.\",\n            err,\n          );\n        }\n      });\n  };\n\n  return (\n    <ContextMenu>\n      {/* F-cross-13 path-b: `asChild` is Radix-only (Base UI rejects it). The\n          trigger renders its own wrapper element (a <span> — non-button in both\n          backends); `contents` makes it box-less so layout is untouched, and\n          right-click / long-press bubble up from the child to the trigger's\n          listeners — no prop injection into `children` needed. (v0.2.5) */}\n      <ContextMenuTrigger className=\"contents\">{children}</ContextMenuTrigger>\n      <ContextMenuContent className=\"w-44\">\n        {canEdit ? (\n          <ContextMenuItem onSelect={() => edit.openEditor(item.id)}>\n            Edit…\n          </ContextMenuItem>\n        ) : null}\n\n        {canEdit ? (\n          <ContextMenuItem onSelect={() => edit.beginRename(item.id)}>\n            Rename\n          </ContextMenuItem>\n        ) : null}\n\n        {canEdit && statusOptions.length > 0 ? (\n          <ContextMenuSub>\n            <ContextMenuSubTrigger>Status</ContextMenuSubTrigger>\n            <ContextMenuSubContent>\n              {statusOptions.map((o) => (\n                <ContextMenuItem\n                  key={o.value}\n                  onSelect={() => edit.changeStatus(item.id, o.value)}\n                >\n                  {o.label}\n                </ContextMenuItem>\n              ))}\n            </ContextMenuSubContent>\n          </ContextMenuSub>\n        ) : null}\n\n        {canEdit && priorityOptions.length > 0 ? (\n          <ContextMenuSub>\n            <ContextMenuSubTrigger>Priority</ContextMenuSubTrigger>\n            <ContextMenuSubContent>\n              {priorityOptions.map((o) => (\n                <ContextMenuItem\n                  key={o.value}\n                  onSelect={() => edit.changePriority(item.id, o.value)}\n                >\n                  {o.label}\n                </ContextMenuItem>\n              ))}\n            </ContextMenuSubContent>\n          </ContextMenuSub>\n        ) : null}\n\n        {canEdit ? <ContextMenuSeparator /> : null}\n        <ContextMenuItem onSelect={copy}>Copy</ContextMenuItem>\n        {canDelete ? (\n          <ContextMenuItem onSelect={cut}>Cut</ContextMenuItem>\n        ) : null}\n\n        {canDelete ? (\n          <>\n            <ContextMenuSeparator />\n            <ContextMenuItem\n              variant=\"destructive\"\n              onSelect={() => edit.deleteItem(item.id)}\n            >\n              Delete\n            </ContextMenuItem>\n          </>\n        ) : null}\n      </ContextMenuContent>\n    </ContextMenu>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/features/editing/parts/calendar-context-menu.tsx"
    },
    {
      "path": "src/registry/components/data/event-calendar/features/editing/parts/droppable-day-cell.tsx",
      "content": "\"use client\";\n\n/** Editable droppable wrapper around a month day cell (Tier B — editing\n *  feature). Extracted from the pre-split `calendar-month-view.tsx` MIXED\n *  host (features-editing split, v0.3). */\n\nimport { startOfDay } from \"date-fns\";\nimport { useDroppable } from \"@dnd-kit/core\";\nimport { useCalendarEditContext } from \"../../../hooks/use-calendar-edit-extension\";\nimport type { DroppableDayCellProps } from \"../../../hooks/use-calendar-edit-extension\";\nimport { MonthDayCell } from \"../../../parts/calendar-month-view\";\n\nexport function DroppableDayCell(props: DroppableDayCellProps) {\n  const edit = useCalendarEditContext();\n  const dayMs = startOfDay(props.day).getTime();\n  const { setNodeRef, isOver } = useDroppable({\n    id: `day:${dayMs}`,\n    data: { dayMs },\n  });\n  return (\n    <MonthDayCell\n      {...props}\n      dropRef={setNodeRef}\n      isOver={isOver}\n      creatable\n      onDayDoubleClick={(d, e) => {\n        const startMs = startOfDay(d).getTime();\n        if (edit.quickCompose) {\n          edit.openComposer({\n            date: new Date(startMs),\n            allDay: true,\n            defaultEnd: new Date(startMs),\n            x: e.clientX,\n            y: e.clientY,\n          });\n        } else {\n          edit.createItem(null, undefined, { startMs, allDay: true });\n        }\n      }}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/event-calendar/features/editing/parts/droppable-day-cell.tsx"
    }
  ],
  "meta": {
    "featureOf": "event-calendar",
    "budgetKB": 110
  },
  "categories": [
    "data",
    "task-management"
  ],
  "type": "registry:block"
}