{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-grid",
  "title": "Data Grid",
  "author": "gridcn",
  "description": "An editable data grid: range selection, spreadsheet clipboard, editing engine, validation, sorting, filtering, pinning, streaming updates. Real DOM cells, shadcn styling.",
  "dependencies": [
    "zustand"
  ],
  "registryDependencies": [
    "button",
    "input",
    "select",
    "checkbox",
    "popover",
    "calendar",
    "dropdown-menu",
    "context-menu",
    "separator",
    "tooltip"
  ],
  "files": [
    {
      "path": "registry/default/blocks/data-grid/body.tsx",
      "content": "\"use client\";\r\n\r\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from \"react\";\r\nimport { createPortal } from \"react-dom\";\r\nimport {\r\n  useDataGridActions,\r\n  useDataGridActiveCell,\r\n  useDataGridLabels,\r\n  useDataGridRowCount,\r\n  useDataGridRowIds,\r\n  useDataGridRowMarkers,\r\n  useDataGridRowReorderEnabled,\r\n  type AnyColumnDef,\r\n} from \"./store\";\r\nimport { useRowWindow } from \"./windowing/use-row-window\";\r\nimport { useDataGridRootContext, type WindowedColumn } from \"./layout-context\";\r\nimport { DataGridRow } from \"./row\";\r\nimport { DataGridOverlays } from \"./overlays\";\r\nimport { predictReorderTarget, useRowReorder } from \"./rows/use-row-reorder\";\r\nimport { isReorderMarkerMode } from \"./rows/marker-width\";\r\nimport { FLASH_KEYFRAMES } from \"./cell\";\r\nimport { GRID_LAYER } from \"./layers\";\r\nimport { gridAttrSelector } from \"./data-attributes\";\r\nimport { isDev } from \"./is-dev\";\r\n\r\n/** Real-index `[min, max+1)` bounds of the currently rendered UNPINNED columns.\r\n * Overlay segmentation clamps its unpinned segment to this, so it never reaches off-screen track\r\n * positions the virtualized window did not render. An empty window collapses to a zero-width range. */\r\nfunction unpinnedWindowRange(\r\n  windowedColumns: readonly WindowedColumn[],\r\n  pins: readonly (AnyColumnDef[\"pin\"] | undefined)[],\r\n): { start: number; end: number } {\r\n  let min = Infinity;\r\n  let max = -Infinity;\r\n  for (const { index } of windowedColumns) {\r\n    if (pins[index] !== undefined) continue;\r\n    if (index < min) min = index;\r\n    if (index > max) max = index;\r\n  }\r\n  return Number.isFinite(min) ? { start: min, end: max + 1 } : { start: 0, end: 0 };\r\n}\r\n\r\nlet warnedRowCountMismatch = false;\r\n\r\n/** Renders only the windowed rows for the current scroll position, plus the active row so focus survives scrolling it out of view. */\r\nexport function DataGridBody(): ReactNode {\r\n  const {\r\n    scrollRef,\r\n    windowedColumns,\r\n    rowHeight,\r\n    headerHeight,\r\n    pinnedTopHeight,\r\n    pinnedBottomHeight,\r\n    pinnedTopCount,\r\n    template,\r\n    layout,\r\n    readOnly,\r\n    interaction,\r\n    getRowClassName,\r\n    getCellClassName,\r\n    onCellClick,\r\n    onRowClick,\r\n    onRowWindowChange,\r\n  } = useDataGridRootContext();\r\n  const rowMarkers = useDataGridRowMarkers();\r\n  const rowCount = useDataGridRowCount();\r\n  const activeCell = useDataGridActiveCell();\r\n  // Data row 0 starts below the header AND any pinned-top band (shrunken effective viewport);\r\n  // the pinned-bottom band shrinks the window's bottom edge the same way.\r\n  const effectiveHeaderHeight = headerHeight + pinnedTopHeight;\r\n  const { start, end, windowTop, measured } = useRowWindow(scrollRef, {\r\n    rowCount,\r\n    rowHeight,\r\n    dataRowTop: effectiveHeaderHeight,\r\n    bottomInset: pinnedBottomHeight,\r\n  });\r\n\r\n  // Fires post-commit, never during render — DataGridBody already re-renders every window tick\r\n  // regardless (it calls useRowWindow directly), so this adds no extra subscription and can't\r\n  // affect the single-flushSync-per-tick scroll commit (the effect runs after that commit, on its\r\n  // own microtask-free schedule). Gated on [start, end] identity so it fires once on mount and\r\n  // again only when the committed range actually moved, never on a same-window scroll tick.\r\n  //\r\n  // `!measured` skips the one render before the scroll element is attached: useRowWindow's mount\r\n  // effect measures the real element and force-updates to the real window in the SAME effect flush\r\n  // this component's own effect runs in, so without this guard the fixed {0, min(rowCount,30)}\r\n  // fallback and the real window both looked like genuine, distinct committed ranges and this fired\r\n  // twice on every mount (root cause of the double-fire bug) — the fallback was never a range a\r\n  // consumer (e.g. data-grid-lazy's fetch-on-window) should act on in the first place.\r\n  const lastFiredRangeRef = useRef<{ start: number; end: number } | null>(null);\r\n  useEffect(() => {\r\n    if (!measured) return;\r\n    const last = lastFiredRangeRef.current;\r\n    if (last && last.start === start && last.end === end) return;\r\n    lastFiredRangeRef.current = { start, end };\r\n    onRowWindowChange?.({ start, end });\r\n  }, [start, end, measured, onRowWindowChange]);\r\n\r\n  // A naive \"clear then repopulate\" would blank the body for a frame on every window shift.\r\n  // Rows are keyed by getRowId and mapped straight from viewRowIndices, so keyed reconciliation\r\n  // unmounts only the rows that left the range and mounts only the rows that entered it; the\r\n  // rows common to both windows never unmount.\r\n  const viewRowIndices = useMemo(() => {\r\n    const indices: number[] = [];\r\n    for (let i = start; i < end; i++) indices.push(i);\r\n    // the active row always renders, even off-window, so focus survives scroll\r\n    const activeRowOffWindow = activeCell !== null && (activeCell.row < start || activeCell.row >= end) && activeCell.row < rowCount;\r\n    if (activeRowOffWindow) indices.push(activeCell.row);\r\n\r\n    // Dev-only invariant: the rendered set must be exactly computeWindow's [start,end) plus the\r\n    // off-window active row, never more/fewer — catches a regression that silently drops or\r\n    // duplicates rows.\r\n    if (isDev()) {\r\n      const expected = end - start + (activeRowOffWindow ? 1 : 0);\r\n      if (indices.length !== expected && !warnedRowCountMismatch) {\r\n        warnedRowCountMismatch = true;\r\n        console.warn(\r\n          `gridcn: rendered row count (${indices.length}) does not match the expected window size (${expected}) — start=${start} end=${end} activeRowOffWindow=${activeRowOffWindow}.`,\r\n        );\r\n      }\r\n    }\r\n    return indices;\r\n  }, [start, end, activeCell, rowCount]);\r\n\r\n  // Rows are memoized by getRowId, not viewRowIndex, so sort/filter/insert/delete reuses the right DOM row.\r\n  const rowIds = useDataGridRowIds(viewRowIndices);\r\n\r\n  // Keyed by getRowId (matches the row's own React key) so a row kept across a shift is found by\r\n  // identity, not array position — viewRowIndices order can change (e.g. the off-window active row\r\n  // entry moving) independent of which DOM nodes actually moved. React calls the ref callback with\r\n  // null on unmount, so a scrolled-out row removes its own rowElements entry — no pruning needed here.\r\n  const rowElements = useRef(new Map<string | number, HTMLDivElement>());\r\n  // A fresh inline ref callback every render would itself defeat DataGridRow's memo (ref is a plain\r\n  // prop in React 19, included in memo's shallow compare) — cache one stable callback per row key so\r\n  // reused rows (same key across a shift) get the same function reference every render.\r\n  const rowRefCallbacks = useRef(new Map<string | number, (el: HTMLDivElement | null) => void>());\r\n  // Rows churn through unique getRowId keys during a long scroll session (e.g. 100k rows) — the\r\n  // callback cache would otherwise grow unbounded. React unmounts stale rows (calling their ref\r\n  // callback with null) before this component's next render, so pruning on that same rAF-driven\r\n  // cadence is redundant work on the full-swap hot path (a Map scan for cols*rows keys every tick);\r\n  // amortize it instead, sweeping only once the cache has grown well past a live window's size.\r\n  const PRUNE_THRESHOLD = 500;\r\n  const getRowRefCallback = (key: string | number) => {\r\n    let cb = rowRefCallbacks.current.get(key);\r\n    if (!cb) {\r\n      cb = (el) => {\r\n        if (el) rowElements.current.set(key, el);\r\n        else {\r\n          rowElements.current.delete(key);\r\n          rowRefCallbacks.current.delete(key);\r\n        }\r\n      };\r\n      rowRefCallbacks.current.set(key, cb);\r\n      if (rowRefCallbacks.current.size > PRUNE_THRESHOLD) {\r\n        // defensive sweep for any key whose unmount ref-callback somehow didn't fire (should not\r\n        // happen under normal React unmounts, but keeps the cache bounded even so).\r\n        for (const k of rowRefCallbacks.current.keys()) {\r\n          if (!rowElements.current.has(k)) rowRefCallbacks.current.delete(k);\r\n        }\r\n      }\r\n    }\r\n    return cb;\r\n  };\r\n\r\n  // When the active row sits ABOVE the window, gridRowStart (viewRowIndex - windowStart + 1) would\r\n  // be <= 0 — an invalid <integer> (dropped, falls back to auto placement) or a negative index\r\n  // (counts from the end of the explicit grid) — either way it corrupts canvas layout. Extend the\r\n  // effective window start down to the active row and shift the canvas transform by the same\r\n  // amount, so every rendered row keeps a valid (>=1) gridRowStart with no visual change.\r\n  const effectiveStart = activeCell ? Math.min(start, activeCell.row) : start;\r\n  const effectiveWindowTop = windowTop - (start - effectiveStart) * rowHeight;\r\n\r\n  // Drag-to-reorder rows: one shared instance for the whole body (one indicator, one gesture),\r\n  // mirroring how DataGridHeader owns useColumnReorder for its headers.\r\n  const actions = useDataGridActions();\r\n  const labels = useDataGridLabels();\r\n  const rowReorderEnabled = useDataGridRowReorderEnabled();\r\n  // the gesture lives on the marker's reorder family only — plain number/checkbox/both markers\r\n  // stay pure row-select surfaces (see RowMarkersMode), so a selection drag never reorders.\r\n  const rowReorderArmed = rowReorderEnabled && isReorderMarkerMode(rowMarkers);\r\n  const [reorderAnnouncement, setReorderAnnouncement] = useState(\"\");\r\n  // A `role=\"status\"` region may not live inside role=\"grid\" (only row/rowgroup are allowed\r\n  // children — axe's aria-required-children), so the announcement portals to <body> after mount;\r\n  // the gate keeps SSR output free of the portal node.\r\n  const [announcementTarget, setAnnouncementTarget] = useState<HTMLElement | null>(null);\r\n  useEffect(() => {\r\n    setAnnouncementTarget(document.body);\r\n  }, []);\r\n\r\n  /** Resolves the data row (and its above/below half) under a client point, for the reorder drag's live drop target. */\r\n  const hitTestRow = useCallback((clientX: number, clientY: number) => {\r\n    const el = document.elementFromPoint(clientX, clientY)?.closest<HTMLElement>(`[role=\"row\"]${gridAttrSelector(\"rowIndex\")}`);\r\n    if (!el) return null;\r\n    const row = Number(el.dataset[\"gridRowIndex\"]);\r\n    if (!Number.isFinite(row)) return null;\r\n    const rect = el.getBoundingClientRect();\r\n    const position: \"before\" | \"after\" = clientY < rect.top + rect.height / 2 ? \"before\" : \"after\";\r\n    return { row, position };\r\n  }, []);\r\n\r\n  const onRowReorder = useCallback(\r\n    (from: number, over: number, position: \"before\" | \"after\") => {\r\n      const to = predictReorderTarget(from, over, position);\r\n      if (!actions.reorderRows(from, to)) return;\r\n      setReorderAnnouncement(labels.markers.reorderAnnouncement(from + 1, to + 1, rowCount));\r\n    },\r\n    [actions, labels, rowCount],\r\n  );\r\n\r\n  const rowReorder = useRowReorder({\r\n    enabled: rowReorderArmed,\r\n    hitTestRow,\r\n    onReorder: onRowReorder,\r\n    getScrollElement: () => scrollRef.current,\r\n  });\r\n\r\n  // The single row-reorder drop-indicator line, grid-placed on the boundary row's track in the\r\n  // canvas (same coordinate math as the rows' own imperative gridRowStart writes).\r\n  const rowDropIndicator = useMemo<CSSProperties | null>(() => {\r\n    const drag = rowReorder.dragState;\r\n    if (!drag || drag.overRow === null) return null;\r\n    const boundary = drag.position === \"before\" ? drag.overRow : drag.overRow + 1;\r\n    return {\r\n      gridRowStart: boundary - effectiveStart + 1,\r\n      gridColumn: \"1 / -1\",\r\n      position: \"relative\",\r\n      alignSelf: \"start\",\r\n      height: 2,\r\n      zIndex: GRID_LAYER.overlay,\r\n      pointerEvents: \"none\",\r\n      backgroundColor: \"var(--color-primary)\",\r\n    };\r\n  }, [rowReorder.dragState, effectiveStart]);\r\n\r\n  // Writes gridRowStart straight into each row's DOM node, bypassing React for this value.\r\n  // The body re-renders every window tick anyway, so the write costs nothing extra, and\r\n  // DataGridRow stays memoized without windowStart in its props.\r\n  useLayoutEffect(() => {\r\n    for (let i = 0; i < viewRowIndices.length; i++) {\r\n      // i < viewRowIndices.length by loop condition\r\n      const viewRowIndex = viewRowIndices[i]!;\r\n      const key = rowIds[i] ?? viewRowIndex;\r\n      const el = rowElements.current.get(key);\r\n      if (el) el.style.gridRowStart = String(viewRowIndex - effectiveStart + 1);\r\n    }\r\n  });\r\n\r\n  // Drop flash keys whose rows left the rendered window — a remounted cell would otherwise replay\r\n  // its one-shot pulse on scroll-back while the key is still live (the pulse is viewport-scoped by design).\r\n  useEffect(() => {\r\n    if (!measured) return;\r\n    actions._pruneFlashingCells(viewRowIndices);\r\n  }, [measured, viewRowIndices, actions]);\r\n\r\n  const canvasStyle: CSSProperties & Record<string, string | number> = {\r\n    position: \"absolute\",\r\n    insetBlockStart: 0,\r\n    insetInlineStart: 0,\r\n    display: \"grid\",\r\n    gridTemplateColumns: template,\r\n    gridAutoRows: `${rowHeight}px`,\r\n    \"--grid-window-top\": `${effectiveWindowTop}px`,\r\n    // X: cancel native scroll like the header. Y: place the canvas at effectiveHeaderHeight +\r\n    // windowTop (below the header AND any pinned-top band), then cancel native scroll's vertical\r\n    // component too — one transform, both axes, every tick.\r\n    // --grid-dir (-1 in LTR, 1 in RTL) signs the horizontal term: transforms are always physical\r\n    // while the grid tracks under them mirror themselves, so the sign is the only difference.\r\n    transform: `translate3d(calc(var(--grid-dir, -1) * var(--grid-scroll-left, 0px)), calc(var(--grid-window-top) - var(--grid-scroll-top, 0px) + ${effectiveHeaderHeight}px), 0)`,\r\n  };\r\n\r\n  return (\r\n    <>\r\n      <div style={canvasStyle} data-grid-rows-canvas=\"\">\r\n        {/* self-scoped keyframes for the flashCells write-pulse (loading-skeleton's own <style> pattern — registry item, no global.css edit) */}\r\n        <style>{FLASH_KEYFRAMES}</style>\r\n        {viewRowIndices.map((viewRowIndex, i) => {\r\n          const key = rowIds[i] ?? viewRowIndex;\r\n          return (\r\n            <DataGridRow\r\n              key={key}\r\n              viewRowIndex={viewRowIndex}\r\n              windowedColumns={windowedColumns}\r\n              layout={layout}\r\n              readOnly={readOnly}\r\n              rowMarkers={rowMarkers}\r\n              onMarkerPointerDown={interaction.onMarkerPointerDown}\r\n              onMarkerGripPointerDown={interaction.onMarkerGripPointerDown}\r\n              onMarkerCheckboxPointerDown={interaction.onMarkerCheckboxPointerDown}\r\n              onMarkerReorderPointerDown={rowReorder.onMarkerDragPointerDown}\r\n              isRowReorderDragging={rowReorder.dragState?.draggingRow === viewRowIndex}\r\n              rowRef={getRowRefCallback(key)}\r\n              ariaRowIndexOffset={pinnedTopCount}\r\n              getRowClassName={getRowClassName}\r\n              getCellClassName={getCellClassName}\r\n              onCellClick={onCellClick}\r\n              onRowClick={onRowClick}\r\n            />\r\n          );\r\n        })}\r\n        <DataGridOverlays\r\n          windowStart={effectiveStart}\r\n          rowCount={viewRowIndices.length}\r\n          // contiguous rendered span for clamping: [effectiveStart, effectiveStart + length) is\r\n          // disjoint whenever the active row was appended off-window\r\n          clampRowStart={start}\r\n          clampRowEnd={end}\r\n          // total visible columns (real index space), not the rendered window size — bands and\r\n          // pin-zone segmentation must span the FULL grid width\r\n          colCount={layout.pins.length}\r\n          colOffset={layout.markerWidth > 0 ? 2 : 1}\r\n          pinTrack={{\r\n            pins: layout.pins,\r\n            trackLefts: layout.trackLefts,\r\n            trackRights: layout.trackRights,\r\n            renderedUnpinnedRange: unpinnedWindowRange(windowedColumns, layout.pins),\r\n          }}\r\n        />\r\n        {rowDropIndicator && <div data-grid-drop-indicator=\"\" aria-hidden=\"true\" style={rowDropIndicator} />}\r\n      </div>\r\n      {announcementTarget &&\r\n        createPortal(\r\n          <div aria-live=\"polite\" role=\"status\" className=\"sr-only\">\r\n            {reorderAnnouncement}\r\n          </div>,\r\n          announcementTarget,\r\n        )}\r\n    </>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/body.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/cell-types/cell-span.tsx",
      "content": "import { cn } from \"@/lib/utils\";\r\n\r\n/**\r\n * Shared display span: truncates and honors the type's text alignment. `align` names are reading\r\n * relative, not physical — `\"right\"` is the END of the reading direction, so a right-aligned\r\n * numeric column hugs the inline end in both LTR and RTL.\r\n */\r\nexport function CellSpan({ text, align }: { text: string; align?: \"left\" | \"right\" | \"center\" }) {\r\n  return (\r\n    <span\r\n      className={cn(\r\n        \"block w-full truncate\",\r\n        align === \"right\" && \"text-end\",\r\n        align === \"center\" && \"text-center\",\r\n      )}\r\n    >\r\n      {/* <bdi> isolates the value's own bidi run so mixed-script data shapes correctly, without the\r\n          box taking that run's direction — `dir=\"auto\"` here would resolve a Latin value to `ltr`\r\n          and re-resolve this span's `text-align: start` to the physical LEFT, stranding cell text\r\n          on the wrong side of an RTL grid while the header mirrored correctly. */}\r\n      <bdi>{text}</bdi>\r\n    </span>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/cell-types/cell-span.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/cell-types/cell-types.ts",
      "content": "import type { CellOptionsOf, CellType, CellValueOf } from \"../types\";\r\nimport { textCellType } from \"./text\";\r\nimport { numberCellType } from \"./number\";\r\nimport { checkboxCellType } from \"./checkbox\";\r\nimport { selectCellType } from \"./select\";\r\nimport { dateCellType } from \"./date\";\r\n\r\nexport { textCellType } from \"./text\";\r\nexport { numberCellType } from \"./number\";\r\nexport { checkboxCellType } from \"./checkbox\";\r\nexport { selectCellType } from \"./select\";\r\nexport { dateCellType } from \"./date\";\r\n\r\n/**\r\n * The five built-in keys, deliberately DECOUPLED from `CellTypeKey` (which is\r\n * `keyof GridCellTypes` and grows with consumer augmentation). Keeping this\r\n * union literal means the `satisfies` below only ever checks these five\r\n * built-in entries — a consumer's `declare module` augmentation adds a key to\r\n * `GridCellTypes`/`CellTypeKey` without requiring the shipped `cellTypes`\r\n * object to provide an implementation for it.\r\n */\r\ntype BuiltinCellTypeKey = \"text\" | \"number\" | \"checkbox\" | \"select\" | \"date\";\r\n\r\n/** A `CellType` for one {@link BuiltinCellTypeKey}, with its exact value/options shape. */\r\ntype CellTypeFor<K extends BuiltinCellTypeKey> = CellType<unknown, CellValueOf<K>, CellOptionsOf<K>>;\r\n\r\n/** Built-in cell-type registry keyed by {@link BuiltinCellTypeKey}; consumers merge in custom types alongside these. */\r\nexport const cellTypes = {\r\n  text: textCellType,\r\n  number: numberCellType,\r\n  checkbox: checkboxCellType,\r\n  select: selectCellType,\r\n  date: dateCellType,\r\n} satisfies { [K in BuiltinCellTypeKey]: CellTypeFor<K> };\r\n",
      "type": "registry:component",
      "target": "components/data-grid/cell-types/cell-types.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/cell-types/checkbox.tsx",
      "content": "import { useEffect } from \"react\";\r\nimport { Checkbox } from \"@/components/ui/checkbox\";\r\nimport type { CellEditorProps, CellRenderProps, CellType, GridCellTypes } from \"../types\";\r\nimport { useCommitGuard } from \"../interaction/use-commit-guard\";\r\nimport { GRID_ATTR } from \"../data-attributes\";\r\n\r\nfunction CheckboxCell({ value }: CellRenderProps<unknown, boolean>) {\r\n  if (value == null) return null;\r\n  // display-only: the interaction layer toggles via commitCellValue, so the control never receives events\r\n  return (\r\n    <span className=\"flex size-full items-center justify-center\">\r\n      <Checkbox checked={value} tabIndex={-1} className=\"pointer-events-none\" {...{ [GRID_ATTR.checkboxBox]: \"\" }} />\r\n    </span>\r\n  );\r\n}\r\n\r\n/** Checkbox has no edit mode (interaction layer toggles+commits directly via `commitCellValue`); this only guards a stray `startEditing` call by bailing out immediately. */\r\nfunction CheckboxEditor({ cancel }: CellEditorProps<unknown, boolean>) {\r\n  const committed = useCommitGuard();\r\n  useEffect(() => {\r\n    if (!committed.tryCommit()) return;\r\n    cancel();\r\n    // oxlint-disable-next-line react-hooks/exhaustive-deps\r\n  }, []);\r\n  return null;\r\n}\r\n\r\n/** Truthy tokens accepted by the checkbox type's `fromText` (paste/import), case-insensitive. */\r\nconst CHECKBOX_TRUE_TOKENS = new Set([\"true\", \"1\", \"yes\", \"x\", \"on\"]);\r\n\r\n/** `checkbox`: fromText accepts a small truthy-token set; clipboard round-trips as \"true\"/\"false\". */\r\nexport const checkboxCellType: CellType<unknown, boolean, GridCellTypes[\"checkbox\"][\"options\"]> = {\r\n  Cell: CheckboxCell,\r\n  Editor: CheckboxEditor,\r\n  toText: (value) => (value ? \"true\" : \"false\"),\r\n  fromText: (text) => CHECKBOX_TRUE_TOKENS.has(text.trim().toLowerCase()),\r\n  clearValue: () => false,\r\n  isEmpty: (value) => value == null,\r\n  compare: (a, b) => Number(a) - Number(b),\r\n  align: \"center\",\r\n};\r\n",
      "type": "registry:component",
      "target": "components/data-grid/cell-types/checkbox.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/cell-types/date.tsx",
      "content": "import { useEffect, useRef, useState, type KeyboardEvent } from \"react\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\r\nimport { Calendar } from \"@/components/ui/calendar\";\r\nimport type { CellEditorProps, CellRenderProps, CellType, GridCellTypes } from \"../types\";\r\nimport { CellSpan } from \"./cell-span\";\r\nimport { displayText } from \"./display-text\";\r\nimport { useCommitGuard } from \"../interaction/use-commit-guard\";\r\nimport { useDataGridLabels } from \"../store\";\r\nimport { columnLabelText } from \"../columns/column-format-helpers\";\r\n\r\n/** Normalizes any Date-parseable input to its ISO `yyyy-mm-dd` date part; null when unparseable or out of range. */\r\nfunction toIsoDatePart(input: string, options?: GridCellTypes[\"date\"][\"options\"]): string | null {\r\n  const trimmed = input.trim();\r\n  if (trimmed === \"\") return null;\r\n  let isoDate: string;\r\n  if (/^\\d{4}-\\d{2}-\\d{2}$/.test(trimmed)) {\r\n    isoDate = trimmed;\r\n  } else {\r\n    const parsed = new Date(trimmed);\r\n    if (Number.isNaN(parsed.getTime())) return null;\r\n    // use local getters, not toISOString(), so a locale-parsed date (local midnight) doesn't shift a day across UTC\r\n    const year = String(parsed.getFullYear()).padStart(4, \"0\");\r\n    const month = String(parsed.getMonth() + 1).padStart(2, \"0\");\r\n    const day = String(parsed.getDate()).padStart(2, \"0\");\r\n    isoDate = `${year}-${month}-${day}`;\r\n  }\r\n  if (options?.min !== undefined && isoDate < options.min) return null;\r\n  if (options?.max !== undefined && isoDate > options.max) return null;\r\n  return isoDate;\r\n}\r\n\r\n/** Parses an ISO `yyyy-mm-dd` date part as a local-midnight `Date` for the Calendar; null for anything else. */\r\nfunction isoDatePartToLocalDate(isoDate: string | null): Date | undefined {\r\n  if (isoDate == null || !/^\\d{4}-\\d{2}-\\d{2}$/.test(isoDate)) return undefined;\r\n  // regex above guarantees exactly 3 numeric \"-\"-separated parts\r\n  const [year, month, day] = isoDate.split(\"-\").map(Number) as [number, number, number];\r\n  return new Date(year, month - 1, day);\r\n}\r\n\r\n/** Local-midnight `Date` back to its ISO `yyyy-mm-dd` part (mirrors `toIsoDatePart`'s local-getter approach). */\r\nfunction localDateToIsoDatePart(date: Date): string {\r\n  const year = String(date.getFullYear()).padStart(4, \"0\");\r\n  const month = String(date.getMonth() + 1).padStart(2, \"0\");\r\n  const day = String(date.getDate()).padStart(2, \"0\");\r\n  return `${year}-${month}-${day}`;\r\n}\r\n\r\n/** Constructing an `Intl.DateTimeFormat` costs ~38us against ~0.7us to reuse one (measured) — at\r\n * fling speed that ran once per date cell per newly-mounted row, so formatters are cached by their\r\n * resolved locale + format options instead. Keyed on the serialized options rather than the options\r\n * object's identity: a column literal re-created each render is the common case and must still hit. */\r\nconst dateFormatterCache = new Map<string, Intl.DateTimeFormat>();\r\n\r\nfunction getDateFormatter(locale: string, format: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\r\n  const key = `${locale}|${JSON.stringify(format)}`;\r\n  let formatter = dateFormatterCache.get(key);\r\n  if (!formatter) {\r\n    formatter = new Intl.DateTimeFormat(locale, format);\r\n    dateFormatterCache.set(key, formatter);\r\n  }\r\n  return formatter;\r\n}\r\n\r\n/** Formats an ISO date part per the column's `displayFormat`/`locale`, falling back to the raw ISO text. */\r\nfunction formatDateDisplay(value: string | null, options?: GridCellTypes[\"date\"][\"options\"]): string {\r\n  if (value == null) return \"\";\r\n  if (!options?.displayFormat) return value;\r\n  const date = isoDatePartToLocalDate(value);\r\n  if (!date) return value;\r\n  // pinned default: server and browser Intl defaults can differ -> SSR hydration mismatch\r\n  return getDateFormatter(options.locale ?? \"en-US\", options.displayFormat).format(date);\r\n}\r\n\r\nfunction DateCell({ value, column }: CellRenderProps<unknown, string | null>) {\r\n  const options = column.options as GridCellTypes[\"date\"][\"options\"] | undefined;\r\n  return <CellSpan text={displayText(dateCellType, value, options)} align=\"right\" />;\r\n}\r\n\r\n/** Popover + Calendar (shadcn date-picker pattern): opens on edit start; picking a date commits and stays; a small typed-input above the calendar accepts ISO or Date.parse-able text with Enter to commit. */\r\nfunction DateEditor({\r\n  value,\r\n  onChange,\r\n  commit,\r\n  cancel,\r\n  column,\r\n  pending,\r\n  rejectionCount,\r\n}: CellEditorProps<unknown, string | null>) {\r\n  const options = column.options as GridCellTypes[\"date\"][\"options\"] | undefined;\r\n  const labels = useDataGridLabels();\r\n  const inputRef = useRef<HTMLInputElement>(null);\r\n  const [text, setText] = useState(dateCellType.toText(value));\r\n  const [open, setOpen] = useState(true);\r\n  const committed = useCommitGuard();\r\n\r\n  // Re-arm on each rejection, not on pending->false (that also fires on the cancel right before unmount); see CellEditorProps.rejectionCount.\r\n  const lastRejectionCount = useRef(rejectionCount ?? 0);\r\n  useEffect(() => {\r\n    if ((rejectionCount ?? 0) !== lastRejectionCount.current) committed.reset();\r\n    lastRejectionCount.current = rejectionCount ?? 0;\r\n  }, [rejectionCount, committed]);\r\n\r\n  useEffect(() => {\r\n    const el = inputRef.current;\r\n    if (!el) return;\r\n    el.focus();\r\n    const end = el.value.length;\r\n    el.setSelectionRange(end, end);\r\n  }, []);\r\n\r\n  const commitValue = (nextValue: string | null, movement?: { dx: number; dy: number }) => {\r\n    if (!committed.tryCommit()) return;\r\n    onChange(nextValue);\r\n    commit(movement);\r\n  };\r\n\r\n  return (\r\n    <Popover\r\n      open={open}\r\n      onOpenChange={(next) => {\r\n        setOpen(next);\r\n        if (!next && committed.tryCommit()) cancel();\r\n      }}\r\n    >\r\n      <PopoverTrigger\r\n        nativeButton={false}\r\n        render={<span className=\"block size-full truncate text-end\" />}\r\n      >\r\n        {displayText(dateCellType, value, options)}\r\n      </PopoverTrigger>\r\n      <PopoverContent\r\n        data-grid-cell-editor=\"\"\r\n        align=\"start\"\r\n        className=\"w-auto p-0\"\r\n        onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {\r\n          if (e.key === \"Escape\") {\r\n            if (committed.tryCommit()) cancel();\r\n          }\r\n        }}\r\n      >\r\n        <div className=\"p-2 pb-0\">\r\n          <Input\r\n            ref={inputRef}\r\n            value={text}\r\n            // readOnly, not disabled: a disabled input can't receive focus/keyboard events at all, which\r\n            // would silently swallow Escape while an async validation is pending — readOnly blocks typing\r\n            // but keeps Escape/Tab/blur working.\r\n            readOnly={pending}\r\n            // ISO date text is direction-neutral; see the number editor for why `auto` beats the\r\n            // grid's inherited layout direction on a value-carrying input.\r\n            dir=\"auto\"\r\n            placeholder={labels.grid.datePlaceholder}\r\n            onChange={(e) => setText(e.target.value)}\r\n            onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {\r\n              if (e.key === \"Enter\") {\r\n                commitValue(dateCellType.fromText(text, options), { dx: 0, dy: 1 });\r\n              } else if (e.key === \"Escape\") {\r\n                if (committed.tryCommit()) cancel();\r\n              }\r\n            }}\r\n            aria-label={columnLabelText(column)}\r\n            className=\"h-8 w-full border-none bg-transparent px-2 shadow-none outline-none focus-visible:ring-0\"\r\n          />\r\n        </div>\r\n        <Calendar\r\n          mode=\"single\"\r\n          selected={isoDatePartToLocalDate(value)}\r\n          defaultMonth={isoDatePartToLocalDate(value)}\r\n          onSelect={(date) => {\r\n            if (!date) return;\r\n            commitValue(localDateToIsoDatePart(date), { dx: 0, dy: 0 });\r\n          }}\r\n        />\r\n      </PopoverContent>\r\n    </Popover>\r\n  );\r\n}\r\n\r\n/** `date`: ISO `yyyy-mm-dd` storage; fromText accepts ISO or any Date.parse-able string, normalized and range-checked against min/max. */\r\nexport const dateCellType: CellType<unknown, string | null, GridCellTypes[\"date\"][\"options\"]> = {\r\n  Cell: DateCell,\r\n  Editor: DateEditor,\r\n  toText: (value) => value ?? \"\",\r\n  toDisplayText: (value, options) => formatDateDisplay(value, options),\r\n  fromText: (text, options) => toIsoDatePart(text, options),\r\n  clearValue: () => null,\r\n  isEmpty: (value) => value == null,\r\n  compare: (a, b) => {\r\n    if (a == null && b == null) return 0;\r\n    if (a == null) return -1;\r\n    if (b == null) return 1;\r\n    return a < b ? -1 : a > b ? 1 : 0;\r\n  },\r\n  align: \"right\",\r\n};\r\n",
      "type": "registry:component",
      "target": "components/data-grid/cell-types/date.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/cell-types/display-text.ts",
      "content": "import type { CellType } from \"../types\";\r\n\r\n/** Generic display-text resolution: any {@link CellType} may define `toDisplayText` for a formatted read view; falls back to `toText`. Clipboard/export always call `toText` directly, never this. */\r\nexport function displayText<TValue, TOptions>(\r\n  cellType: Pick<CellType<unknown, TValue, TOptions>, \"toText\" | \"toDisplayText\">,\r\n  value: TValue,\r\n  options?: TOptions,\r\n): string {\r\n  return (cellType.toDisplayText ?? cellType.toText)(value, options);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/cell-types/display-text.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/cell-types/index.ts",
      "content": "/** Domain barrel — built-in cell type registry and shared editor pieces. */\r\nexport {\r\n  cellTypes,\r\n  textCellType,\r\n  numberCellType,\r\n  checkboxCellType,\r\n  selectCellType,\r\n  dateCellType,\r\n} from \"./cell-types\";\r\nexport { displayText } from \"./display-text\";\r\nexport { CellSpan } from \"./cell-span\";\r\n",
      "type": "registry:component",
      "target": "components/data-grid/cell-types/index.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/cell-types/number.tsx",
      "content": "import { useEffect, useRef, useState, type KeyboardEvent } from \"react\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport type { CellEditorProps, CellRenderProps, CellType, GridCellTypes } from \"../types\";\r\nimport { CellSpan } from \"./cell-span\";\r\nimport { useSeedFocus } from \"../interaction/use-seed-focus\";\r\nimport { useCommitGuard } from \"../interaction/use-commit-guard\";\r\nimport { columnLabelText } from \"../columns/column-format-helpers\";\r\n\r\n/** Normalizes a comma decimal separator to a dot so `Number()` can parse locales pasted verbatim. */\r\nfunction normalizeNumberText(text: string): string {\r\n  return text.trim().replace(\",\", \".\");\r\n}\r\n\r\n/** Clamps a parsed number into the column's configured min/max, if any. */\r\nfunction clampNumber(value: number, options?: GridCellTypes[\"number\"][\"options\"]): number {\r\n  let clamped = value;\r\n  if (options?.min !== undefined) clamped = Math.max(clamped, options.min);\r\n  if (options?.max !== undefined) clamped = Math.min(clamped, options.max);\r\n  return clamped;\r\n}\r\n\r\n/** Rounds to the column's configured decimal precision, if any. */\r\nfunction roundToDecimals(value: number, options?: GridCellTypes[\"number\"][\"options\"]): number {\r\n  if (options?.decimals === undefined) return value;\r\n  const factor = 10 ** options.decimals;\r\n  return Math.round(value * factor) / factor;\r\n}\r\n\r\nfunction NumberCell({ value, column }: CellRenderProps<unknown, number | null>) {\r\n  const options = column.options as GridCellTypes[\"number\"][\"options\"] | undefined;\r\n  return <CellSpan text={numberCellType.toText(value, options)} align=\"right\" />;\r\n}\r\n\r\nfunction NumberEditor({\r\n  value,\r\n  initialText,\r\n  onChange,\r\n  commit,\r\n  cancel,\r\n  column,\r\n  pending,\r\n  rejectionCount,\r\n}: CellEditorProps<unknown, number | null>) {\r\n  const ref = useRef<HTMLInputElement>(null);\r\n  // seed with the same options the cell display formats with, so the draft matches what the cell showed\r\n  const [text, setText] = useState(initialText ?? numberCellType.toText(value, column.options as GridCellTypes[\"number\"][\"options\"]));\r\n  useSeedFocus(ref, initialText);\r\n  const committed = useCommitGuard();\r\n\r\n  // An async schema resolving with ISSUES leaves editing open — re-arm the guard so the user's\r\n  // next Enter/blur after fixing the value isn't silently dropped. Keyed off rejectionCount\r\n  // specifically (not \"pending went from true to false\", which also fires on Escape/cancel right\r\n  // before unmount — resetting there let the unmount's own blur commit a second, stale time).\r\n  const lastRejectionCount = useRef(rejectionCount ?? 0);\r\n  useEffect(() => {\r\n    if ((rejectionCount ?? 0) !== lastRejectionCount.current) committed.reset();\r\n    lastRejectionCount.current = rejectionCount ?? 0;\r\n  }, [rejectionCount, committed]);\r\n\r\n  const commitText = (movement?: { dx: number; dy: number }) => {\r\n    if (!committed.tryCommit()) return;\r\n    onChange(numberCellType.fromText(text, column.options as GridCellTypes[\"number\"][\"options\"]));\r\n    commit(movement);\r\n  };\r\n\r\n  return (\r\n    <Input\r\n      ref={ref}\r\n      value={text}\r\n      // Numerals are direction-neutral, so under an inherited `dir=rtl` a leading minus sign and the\r\n      // caret land on the wrong end. `auto` has no strong character to find here and resolves to\r\n      // ltr — the correct reading order for a number in either layout direction.\r\n      dir=\"auto\"\r\n      // readOnly, not disabled: a disabled input can't receive focus/keyboard events at all, which\r\n      // would silently swallow Escape while an async validation is pending — readOnly blocks typing\r\n      // but keeps Escape/Tab/blur working.\r\n      readOnly={pending}\r\n      onChange={(e) => setText(e.target.value)}\r\n      onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {\r\n        if (e.key === \"Enter\") {\r\n          commitText({ dx: 0, dy: 1 });\r\n        } else if (e.key === \"Escape\") {\r\n          cancel();\r\n        }\r\n      }}\r\n      onBlur={() => commitText({ dx: 0, dy: 0 })}\r\n      aria-label={columnLabelText(column)}\r\n      aria-busy={pending || undefined}\r\n      className=\"h-full w-full border-none bg-transparent p-0 text-end shadow-none outline-none [appearance:textfield] focus-visible:ring-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none\"\r\n    />\r\n  );\r\n}\r\n\r\n/** `number`: comma-decimal-tolerant parse, min/max clamp, decimals rounding, plain `String()` serialization (no locale formatting in v1). */\r\nexport const numberCellType: CellType<unknown, number | null, GridCellTypes[\"number\"][\"options\"]> = {\r\n  Cell: NumberCell,\r\n  Editor: NumberEditor,\r\n  toText: (value, options) => {\r\n    if (value == null) return \"\";\r\n    return String(options?.decimals !== undefined ? roundToDecimals(value, options) : value);\r\n  },\r\n  fromText: (text, options) => {\r\n    const normalized = normalizeNumberText(text);\r\n    if (normalized === \"\") return null;\r\n    const parsed = Number(normalized);\r\n    if (Number.isNaN(parsed)) return null;\r\n    return roundToDecimals(clampNumber(parsed, options), options);\r\n  },\r\n  clearValue: () => null,\r\n  isEmpty: (value) => value == null,\r\n  compare: (a, b) => {\r\n    if (a == null && b == null) return 0;\r\n    if (a == null) return -1;\r\n    if (b == null) return 1;\r\n    return a - b;\r\n  },\r\n  align: \"right\",\r\n};\r\n",
      "type": "registry:component",
      "target": "components/data-grid/cell-types/number.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/cell-types/select.tsx",
      "content": "import { useEffect, useRef, useState } from \"react\";\r\nimport {\r\n  Select,\r\n  SelectContent,\r\n  SelectItem,\r\n  SelectTrigger,\r\n  SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport type { CellEditorProps, CellRenderProps, CellType, GridCellTypes } from \"../types\";\r\nimport { CellSpan } from \"./cell-span\";\r\nimport { useCommitGuard } from \"../interaction/use-commit-guard\";\r\nimport { columnLabelText } from \"../columns/column-format-helpers\";\r\n\r\nfunction findChoice(\r\n  choices: readonly { value: string; label: string }[] | undefined,\r\n  raw: string,\r\n): { value: string; label: string } | undefined {\r\n  if (!choices) return undefined;\r\n  const byValue = choices.find((c) => c.value === raw);\r\n  if (byValue) return byValue;\r\n  const lower = raw.toLowerCase();\r\n  return choices.find((c) => c.label.toLowerCase() === lower);\r\n}\r\n\r\nfunction SelectCell({ value, column }: CellRenderProps<unknown, string | null>) {\r\n  const options = column.options as GridCellTypes[\"select\"][\"options\"] | undefined;\r\n  const choice = value == null ? undefined : findChoice(options?.choices, value);\r\n  return <CellSpan text={choice?.label ?? value ?? \"\"} />;\r\n}\r\n\r\n/** Opens on mount; picking an option commits immediately (no separate confirm step). */\r\nfunction SelectEditor({ value, onChange, commit, cancel, column, rejectionCount }: CellEditorProps<unknown, string | null>) {\r\n  const options = column.options as GridCellTypes[\"select\"][\"options\"] | undefined;\r\n  const [open, setOpen] = useState(true);\r\n  const committed = useCommitGuard();\r\n\r\n  // Re-arm on each rejection, not on pending->false (that also fires on the cancel right before unmount); see CellEditorProps.rejectionCount.\r\n  const lastRejectionCount = useRef(rejectionCount ?? 0);\r\n  useEffect(() => {\r\n    if ((rejectionCount ?? 0) !== lastRejectionCount.current) committed.reset();\r\n    lastRejectionCount.current = rejectionCount ?? 0;\r\n  }, [rejectionCount, committed]);\r\n\r\n  return (\r\n    <Select\r\n      value={value ?? \"\"}\r\n      open={open}\r\n      onOpenChange={(next) => {\r\n        setOpen(next);\r\n        // picking an option also closes the select, firing this with `next: false` after commit already ran\r\n        if (!next && committed.tryCommit()) cancel();\r\n      }}\r\n      onValueChange={(next) => {\r\n        if (!committed.tryCommit()) return;\r\n        onChange(next === \"\" || next == null ? null : next);\r\n        commit({ dx: 0, dy: 0 });\r\n      }}\r\n    >\r\n      <SelectTrigger aria-label={columnLabelText(column)} className=\"size-full justify-start rounded-none border-none p-0 text-sm shadow-none outline-none focus-visible:ring-0 [&_svg]:hidden\">\r\n        <SelectValue placeholder=\"\" />\r\n      </SelectTrigger>\r\n      <SelectContent data-grid-cell-editor=\"\">\r\n        {options?.choices.map((choice) => (\r\n          <SelectItem key={choice.value} value={choice.value}>\r\n            {choice.label}\r\n          </SelectItem>\r\n        ))}\r\n      </SelectContent>\r\n    </Select>\r\n  );\r\n}\r\n\r\n/** `select`: fromText resolves pasted option values first, then labels (case-insensitive) so pasted labels round-trip. */\r\nexport const selectCellType: CellType<unknown, string | null, GridCellTypes[\"select\"][\"options\"]> = {\r\n  Cell: SelectCell,\r\n  Editor: SelectEditor,\r\n  toText: (value, options) => {\r\n    if (value == null) return \"\";\r\n    return findChoice(options?.choices, value)?.label ?? value;\r\n  },\r\n  fromText: (text, options) => {\r\n    const trimmed = text.trim();\r\n    if (trimmed === \"\") return null;\r\n    const choice = findChoice(options?.choices, trimmed);\r\n    return choice?.value ?? null;\r\n  },\r\n  clearValue: () => null,\r\n  isEmpty: (value) => value == null,\r\n  compare: (a, b) => {\r\n    if (a == null && b == null) return 0;\r\n    if (a == null) return -1;\r\n    if (b == null) return 1;\r\n    return a.localeCompare(b);\r\n  },\r\n  align: \"left\",\r\n};\r\n",
      "type": "registry:component",
      "target": "components/data-grid/cell-types/select.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/cell-types/text.tsx",
      "content": "import { useEffect, useRef, useState, type KeyboardEvent } from \"react\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport type { CellEditorProps, CellRenderProps, CellType, GridCellTypes } from \"../types\";\r\nimport { CellSpan } from \"./cell-span\";\r\nimport { useSeedFocus } from \"../interaction/use-seed-focus\";\r\nimport { useCommitGuard } from \"../interaction/use-commit-guard\";\r\nimport { columnLabelText } from \"../columns/column-format-helpers\";\r\n\r\nfunction TextCell({ value }: CellRenderProps<unknown, string>) {\r\n  return <CellSpan text={value} />;\r\n}\r\n\r\nfunction TextEditor({ value, initialText, onChange, commit, cancel, column, pending, rejectionCount }: CellEditorProps<unknown, string>) {\r\n  const ref = useRef<HTMLInputElement>(null);\r\n  const [text, setText] = useState(initialText ?? textCellType.toText(value));\r\n  useSeedFocus(ref, initialText);\r\n  const committed = useCommitGuard();\r\n\r\n  // An async schema resolving with ISSUES leaves editing open — re-arm the guard so the user's\r\n  // next Enter/blur after fixing the value isn't silently dropped. Keyed off rejectionCount\r\n  // specifically (not \"pending went from true to false\", which also fires on Escape/cancel right\r\n  // before unmount — resetting there let the unmount's own blur commit a second, stale time).\r\n  const lastRejectionCount = useRef(rejectionCount ?? 0);\r\n  useEffect(() => {\r\n    if ((rejectionCount ?? 0) !== lastRejectionCount.current) committed.reset();\r\n    lastRejectionCount.current = rejectionCount ?? 0;\r\n  }, [rejectionCount, committed]);\r\n\r\n  const commitText = (movement?: { dx: number; dy: number }) => {\r\n    if (!committed.tryCommit()) return;\r\n    onChange(textCellType.fromText(text));\r\n    commit(movement);\r\n  };\r\n\r\n  return (\r\n    <Input\r\n      ref={ref}\r\n      value={text}\r\n      // Caret and typing order must follow the VALUE's script, not the grid's layout direction:\r\n      // inheriting `dir=rtl` for a Latin value renders the caret at one end while edits land at the\r\n      // other. An input is its own box, so `auto` here is safe (unlike CellSpan, see cell-span.tsx).\r\n      dir=\"auto\"\r\n      // readOnly, not disabled: a disabled input can't receive focus/keyboard events at all, which\r\n      // would silently swallow Escape while an async validation is pending — readOnly blocks typing\r\n      // but keeps Escape/Tab/blur working.\r\n      readOnly={pending}\r\n      onChange={(e) => setText(e.target.value)}\r\n      onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {\r\n        if (e.key === \"Enter\") {\r\n          commitText({ dx: 0, dy: 1 });\r\n        } else if (e.key === \"Escape\") {\r\n          cancel();\r\n        }\r\n      }}\r\n      onBlur={() => commitText({ dx: 0, dy: 0 })}\r\n      aria-label={columnLabelText(column)}\r\n      aria-busy={pending || undefined}\r\n      className=\"h-full w-full border-none bg-transparent p-0 shadow-none outline-none focus-visible:ring-0\"\r\n    />\r\n  );\r\n}\r\n\r\n/** `text`: identity value pipeline — clipboard/paste text is the value verbatim. */\r\nexport const textCellType: CellType<unknown, string, GridCellTypes[\"text\"][\"options\"]> = {\r\n  Cell: TextCell,\r\n  Editor: TextEditor,\r\n  toText: (value) => value,\r\n  fromText: (text) => text,\r\n  clearValue: () => \"\",\r\n  isEmpty: (value) => value == null || value === \"\",\r\n  compare: (a, b) => a.localeCompare(b),\r\n  align: \"left\",\r\n};\r\n",
      "type": "registry:component",
      "target": "components/data-grid/cell-types/text.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/cell.tsx",
      "content": "\"use client\";\r\n\r\nimport {\r\n  memo,\r\n  useCallback,\r\n  useLayoutEffect,\r\n  useMemo,\r\n  useRef,\r\n  type CSSProperties,\r\n  type MouseEvent as ReactMouseEvent,\r\n  type PointerEvent as ReactPointerEvent,\r\n  type ReactNode,\r\n} from \"react\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { COLUMN_BORDER } from \"./columns/column-border\";\r\nimport type { GetCellClassName, OnCellClick, OnRowClick } from \"./types\";\r\nimport { getCellValue } from \"./columns/column-helpers\";\r\nimport { useDataGridActions, useDataGridCellTypes, useDataGridCellEditingError, useDataGridCellRejectionCount, type AnyColumnDef } from \"./store\";\r\nimport { useDataGridRootContext } from \"./layout-context\";\r\nimport { pinnedInsetStyle } from \"./columns/pinned-inset-style\";\r\nimport { cellLayer } from \"./layers\";\r\nimport { cellTypes as defaultCellTypes } from \"./cell-types/cell-types\";\r\nimport { useAsyncValidate } from \"./interaction/use-async-validate\";\r\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\";\r\n\r\n/** The \"highlight-what-changed\" fade pulse `flashCells` plays on the cells it just wrote; the keyframes `DataGridBody` injects once per grid (self-contained, no app global.css step). Duration reads `--grid-flash-duration` (default 1.2s; `0s` disables the pulse). */\r\nexport const FLASH_ANIMATION = \"grid-cell-flash var(--grid-flash-duration, 1.2s) ease-out\";\r\n/** One-shot pulse: a background fading to transparent, tinted by `--grid-flash-color` (default `--color-primary`) at `--grid-flash-intensity` (default 24%) — the exact visual the `flashCells` consumers (fill, paste, move) promise. */\r\nexport const FLASH_KEYFRAMES =\r\n  \"@keyframes grid-cell-flash{0%{background-color:color-mix(in oklab, var(--grid-flash-color, var(--color-primary)) var(--grid-flash-intensity, 24%), transparent)}100%{background-color:transparent}}\";\r\n\r\n// This component is generic-erased (row: unknown, column: AnyColumnDef) since it renders arbitrary\r\n// consumer row shapes through one shared runtime — see store.tsx's InternalSyncProps comment for why\r\n// TData=unknown makes every ColumnDef-API call below directly callable with no cast. The two\r\n// `as unknown as` casts further down are a separate, narrower need: a cell type's own\r\n// `FC<CellRenderProps<TData, TValue>>`/`FC<CellEditorProps<TData, TValue>>` is per-cell-type generic,\r\n// widened once here to this erased prop shape (same invariant, different call surface).\r\n\r\nexport type DataGridCellProps = {\r\n  row: unknown;\r\n  rowIndex: number;\r\n  column: AnyColumnDef;\r\n  /** Pure data-column index — drives `CellCoord.col`, `aria-colindex`, and pin-var lookup. Never touched by the marker column (it lives outside the data index space). */\r\n  columnIndex: number;\r\n  /** Grid-level readOnly (from `DataGridRoot`), independent of the column's own `readOnly`. */\r\n  gridReadOnly?: boolean;\r\n  /** 1-based `gridColumnStart` offset added to `columnIndex`; 2 when a marker column occupies track 1, else 1. Presentation-only — never affects `coord`/`aria-colindex`. */\r\n  gridColOffset?: number;\r\n  /** Forwarded from row.tsx (stable identity, root's dev guardrail) rather than read from context here, so this memoized cell's props stay shallow-equal across ticks. */\r\n  getCellClassName?: GetCellClassName<unknown>;\r\n  /** Forwarded from row.tsx, same stable-identity guidance as `getCellClassName`. Fired from this cell's own existing click handler — see {@link OnCellClick}. */\r\n  onCellClick?: OnCellClick<unknown>;\r\n  /** Forwarded alongside `onCellClick`, fired from the same click — see {@link OnRowClick}. */\r\n  onRowClick?: OnRowClick<unknown>;\r\n  /**\r\n   * True for a pinned top/bottom row cell (the `data-grid-pinned-rows` add-on's row-bands):\r\n   * display-only, so this skips every pointer/focus handler — pinned rows aren't part of\r\n   * `data`/`viewIndex` and aren't navigable in v1 (documented). A prop on the same component\r\n   * (not a fork) keeps cell rendering single-sourced.\r\n   */\r\n   pinnedRow?: boolean;\r\n  /**\r\n   * Interactive cell state, derived by the row (`useDataGridRowCellState`) from ONE per-row store\r\n   * subscription. Primitives (not the row's whole derived object) keep `memo`'s shallow compare\r\n   * effective: a cell re-renders only when its own flags change.\r\n   */\r\n  isActive?: boolean;\r\n  isSelected?: boolean;\r\n  isEditing?: boolean;\r\n  /** Only meaningful when `isEditing` is true. */\r\n  initialText?: string;\r\n  isSearchMatch?: boolean;\r\n  /** True when `row` is a hole in `data` (lazy loading) — renders a static shimmer block instead of cell content and skips every pointer/focus handler, same as a pinned-row cell. */\r\n  isSkeleton?: boolean;\r\n  /**\r\n   * True while this cell is inside the transient `flashCells` write-pulse (a fill/paste/move just\r\n   * committed a new value here) — plays the one-shot fade pulse {@link FLASH_ANIMATION} (keyframes\r\n   * injected once per grid by `DataGridBody`).\r\n   */\r\n  isFlashing?: boolean;\r\n  /**\r\n   * This cell's post-commit server-error message (`cellErrors`), or null/undefined\r\n   * when it has none. Painted with the SAME ring/tint/`aria-invalid` treatment as a live `editingError`\r\n   * rejection below — one visual language for \"this value is wrong\", regardless of who said so.\r\n   */\r\n  cellError?: string | null;\r\n};\r\n\r\n/** Renders via the column's cell type (`Cell`/`Editor`), or `column.renderCell` for a display-only override. */\r\nfunction DataGridCellImpl(props: DataGridCellProps): ReactNode {\r\n  const {\r\n    row,\r\n    rowIndex,\r\n    column,\r\n    columnIndex,\r\n    gridReadOnly,\r\n    gridColOffset = 1,\r\n    getCellClassName,\r\n    onCellClick,\r\n    onRowClick,\r\n    pinnedRow: isPinnedRow,\r\n    isActive = false,\r\n    isSelected = false,\r\n    isEditing = false,\r\n    initialText,\r\n    isSearchMatch = false,\r\n    isSkeleton = false,\r\n    isFlashing = false,\r\n    cellError = null,\r\n  } = props;\r\n  const { interaction } = useDataGridRootContext();\r\n  const coord = useMemo(() => ({ col: columnIndex, row: rowIndex }), [columnIndex, rowIndex]);\r\n  const actions = useDataGridActions();\r\n  const cellRef = useRef<HTMLDivElement | null>(null);\r\n  // Atomic per-cell read (see its doc comment) — a live sync/async `validate` rejection on THIS\r\n  // cell's in-progress edit takes priority over its persisted `cellError` (more current: the user\r\n  // is actively looking at why the fresh value they typed was rejected). Opening the editor on an\r\n  // ALREADY-errored cell falls back to `cellError` until a rejection of its own supersedes it, so\r\n  // the message stays visible the instant the editor opens (spec: \"editor open on an errored cell\r\n  // shows the message like editingError\"). A non-editing cell's `liveEditingError` is always null,\r\n  // so `errorMessage` there is exactly `cellError`, unaffected by any OTHER cell's edit session.\r\n  const liveEditingError = useDataGridCellEditingError(coord);\r\n  const rejectionCount = useDataGridCellRejectionCount(coord);\r\n  const errorMessage = isEditing ? (liveEditingError ?? cellError) : cellError;\r\n\r\n  const pinned = column.pin;\r\n  // no setValue/accessorKey = no write path (accessorFn-only, display computed) → readOnly by construction, matching the store's write-skip rule.\r\n  // row === undefined marks a lazy-loading hole (skeleton row): never passed to consumer callbacks, which may read row fields.\r\n  const columnReadOnly =\r\n    (!column.setValue && !column.accessorKey) ||\r\n    (typeof column.readOnly === \"function\"\r\n      ? row === undefined\r\n        ? undefined\r\n        : column.readOnly(row)\r\n      : column.readOnly);\r\n  // Pinned top/bottom rows are readOnly by default — they're usually derived aggregates,\r\n  // not editable data — unless the column's own readOnly explicitly says otherwise.\r\n  const readOnly = isPinnedRow ? (columnReadOnly ?? true) : Boolean(gridReadOnly || columnReadOnly);\r\n  const isNumberOrDate = column.type === \"number\" || column.type === \"date\";\r\n\r\n  const style: CSSProperties = {\r\n    gridColumnStart: columnIndex + gridColOffset,\r\n    ...pinnedInsetStyle(pinned, columnIndex),\r\n  };\r\n  // pinned outranks active-unpinned: pinning is a spatial guarantee, active is a focus state.\r\n  style.zIndex = cellLayer(Boolean(pinned), isActive);\r\n  // One-shot pulse on a freshly written cell; the animation overrides `background-color` only while\r\n  // running, so the cell's own bg/hover classes take over again the moment the key lifts.\r\n  if (isFlashing) style.animation = FLASH_ANIMATION;\r\n\r\n  // Focus the active cell imperatively (never scrollIntoView — cells live in the transformed\r\n  // canvas layer) once it becomes active, unless the editor owns focus. errorMessage re-triggers\r\n  // because the tooltip mount remounts this div, dropping DOM focus.\r\n  useLayoutEffect(() => {\r\n    if (isActive && !isEditing) cellRef.current?.focus({ preventScroll: true });\r\n  }, [isActive, isEditing, errorMessage]);\r\n\r\n  // Pinned rows are not navigable in v1 (documented) — no selection/edit gestures, so no handlers.\r\n  // Skeleton cells (row is a hole, lazy loading) are click/dblclick-inert too: the\r\n  // store's own edit/commit entry points (startEditing, applyCellUpdates, commitCellValue) already\r\n  // no-op on an unresolvable row (resolveEditTarget/computeRowEditsBatch both bail on `row ===\r\n  // undefined`), so pointerdown-driven selection is harmless to leave wired — only click/dblclick\r\n  // (which can open an editor with nothing to edit) are skipped here.\r\n  // explicit TData=unknown: row's `undefined`-narrowed type ({} | null) would otherwise drive inference instead of column's own already-unknown TData.\r\n  const value = row === undefined ? undefined : getCellValue<unknown, typeof column>(row, column);\r\n\r\n  const onPointerDown = useCallback(\r\n    (event: ReactPointerEvent<HTMLDivElement>) => interaction.onCellPointerDown(coord, event),\r\n    [interaction, coord],\r\n  );\r\n  const onClick = useCallback(\r\n    (event: ReactMouseEvent<HTMLDivElement>) => {\r\n      interaction.onCellClick(coord, event);\r\n      // Pure notification, fired alongside whatever the click already resolved to (select/toggle) —\r\n      // never gated on it. Skeleton/pinned-row cells don't reach here (this handler isn't attached\r\n      // for them below), so `row`/`value` are always real by construction.\r\n      if (onCellClick || onRowClick) {\r\n        onCellClick?.({ value, row, column, rowIndex, columnIndex }, event.nativeEvent);\r\n        onRowClick?.({ row, rowIndex }, event.nativeEvent);\r\n      }\r\n    },\r\n    [interaction, coord, onCellClick, onRowClick, value, row, column, rowIndex, columnIndex],\r\n  );\r\n  const onDoubleClick = useCallback(\r\n    (event: ReactMouseEvent<HTMLDivElement>) => interaction.onCellDoubleClick(coord, event),\r\n    [interaction, coord],\r\n  );\r\n\r\n  const cellTypesRegistry = useDataGridCellTypes();\r\n  const cellType = cellTypesRegistry[column.type ?? \"text\"] ?? cellTypesRegistry[\"text\"] ?? defaultCellTypes.text;\r\n  const align = cellType.align ?? \"left\";\r\n\r\n  // Grid-level hook first, then the column's own override — later cn() args win on conflicting\r\n  // utilities, so a per-column override can still beat a grid-wide default.\r\n  const classNameCtx = { value, row, column, viewRowIndex: rowIndex };\r\n  const gridCellClassName = row === undefined ? undefined : getCellClassName?.(classNameCtx);\r\n  const columnCellClassName =\r\n    typeof column.cellClassName === \"function\"\r\n      ? row === undefined\r\n        ? undefined\r\n        : column.cellClassName(classNameCtx)\r\n      : column.cellClassName;\r\n\r\n  // The editor contract calls onChange(nextValue) then commit(movement) synchronously (see\r\n  // cell-types/index.ts); stash the pending value in a ref for commit, re-seeding only while\r\n  // not editing so a mid-edit re-render (stream tick, cellError) never resets the typed draft.\r\n  const pendingValueRef = useRef<unknown>(value);\r\n  if (!isEditing) pendingValueRef.current = value;\r\n  const onChange = useCallback((nextValue: unknown) => {\r\n    pendingValueRef.current = nextValue;\r\n  }, []);\r\n  // Async Standard Schema interception (see use-async-validate.ts doc) — a sync validate/schema\r\n  // forwards to commitCellEdit immediately, zero behavior change from before this hook existed.\r\n  const asyncValidate = useAsyncValidate(actions, column);\r\n  const commit = useCallback(\r\n    (movement?: { dx: number; dy: number }) => asyncValidate.commit(pendingValueRef.current, movement),\r\n    [asyncValidate],\r\n  );\r\n  const cancel = useCallback(() => {\r\n    asyncValidate.cancelPending();\r\n    actions.cancelEditing();\r\n  }, [actions, asyncValidate]);\r\n\r\n  let content: ReactNode;\r\n  if (isSkeleton) {\r\n    // Static shimmer block (CSS animate-pulse only, no per-frame JS) sized to roughly a text line;\r\n    // never rendered for checkbox/select cells specially — one shape covers every column type since\r\n    // there's no real value to shape it around yet.\r\n    content = <div className=\"h-4 w-3/4 motion-safe:animate-pulse rounded bg-muted\" />;\r\n  } else if (isEditing) {\r\n    const Editor = cellType.Editor as unknown as (p: {\r\n      value: unknown;\r\n      initialText?: string;\r\n      row: unknown;\r\n      column: AnyColumnDef;\r\n      onChange: (v: unknown) => void;\r\n      commit: (movement?: { dx: number; dy: number }) => void;\r\n      cancel: () => void;\r\n      pending?: boolean;\r\n      rejectionCount?: number;\r\n    }) => ReactNode;\r\n    content = (\r\n      <Editor\r\n        value={value}\r\n        initialText={initialText}\r\n        row={row}\r\n        column={column}\r\n        onChange={onChange}\r\n        commit={commit}\r\n        cancel={cancel}\r\n        pending={asyncValidate.pending}\r\n        rejectionCount={rejectionCount}\r\n      />\r\n    );\r\n  } else if (column.renderCell) {\r\n    content = column.renderCell({ value, row, rowIndex, column, isActive });\r\n  } else {\r\n    const Cell = cellType.Cell as unknown as (p: {\r\n      value: unknown;\r\n      row: unknown;\r\n      rowIndex: number;\r\n      column: AnyColumnDef;\r\n      isActive: boolean;\r\n    }) => ReactNode;\r\n    content = <Cell value={value} row={row} rowIndex={rowIndex} column={column} isActive={isActive} />;\r\n  }\r\n\r\n  const cell = (\r\n    <div\r\n      ref={cellRef}\r\n      role=\"gridcell\"\r\n      aria-colindex={columnIndex + 1}\r\n      aria-selected={isSelected || undefined}\r\n      aria-busy={isSkeleton || undefined}\r\n      aria-invalid={Boolean(errorMessage) || undefined}\r\n      tabIndex={isPinnedRow || isSkeleton ? undefined : isActive ? 0 : -1}\r\n      data-column-id={column.id}\r\n      data-pinned={pinned || undefined}\r\n      data-grid-pinned-row={isPinnedRow || undefined}\r\n      data-skeleton={isSkeleton || undefined}\r\n      data-readonly={readOnly || undefined}\r\n      data-type={column.type ?? \"text\"}\r\n      data-active={isActive || undefined}\r\n      data-editing={isEditing || undefined}\r\n      data-search-match={isSearchMatch || undefined}\r\n      data-flash={isFlashing || undefined}\r\n      data-invalid={Boolean(errorMessage) || undefined}\r\n      className={cn(\r\n        \"relative flex items-center overflow-hidden border-b border-border bg-background px-2 outline-none group-hover/row:bg-muted/50 transition-colors group-hover/row:transition-none\",\r\n        COLUMN_BORDER,\r\n        // Pinned-column cells and pinned-row cells both sit opaque above the scrolled canvas\r\n        // (z-index 1 / 3) — a translucent bg-muted/50 hover tint would let the scrolled content\r\n        // underneath show through. color-mix() pre-composites the same tint as an OPAQUE color (same\r\n        // visual result, no transparency) so both stay fully opaque on hover in both themes;\r\n        // unpinned, non-banded cells keep the plain translucent utility.\r\n        \"data-pinned:group-hover/row:bg-[color-mix(in_oklch,var(--color-muted)_50%,var(--color-background))]\",\r\n        \"data-grid-pinned-row:group-hover/row:bg-[color-mix(in_oklch,var(--color-muted)_50%,var(--color-background))]\",\r\n        // own token, not accent/muted: search-highlight is a distinct convention from selection/active colors.\r\n        \"data-[search-match]:bg-search-highlight/60 dark:data-[search-match]:bg-search-highlight/25\",\r\n        // Same reasoning as the pinned-cell hover tint above: pinned-row bands sit opaque (z-index 3)\r\n        // above the scrolled rows canvas, so a translucent bg-muted/30 would let scrolled content\r\n        // bleed through — color-mix() pre-composites the same tint as an opaque color instead.\r\n        isPinnedRow && \"bg-[color-mix(in_oklch,var(--color-muted)_30%,var(--color-background))] font-medium\",\r\n        align === \"left\" && \"justify-start\",\r\n        align === \"center\" && \"justify-center\",\r\n        align === \"right\" && \"justify-end\",\r\n        isNumberOrDate && !isEditing && \"tabular-nums\",\r\n        // One visual language for \"this value is wrong\" (design spec), whether a live sync/async\r\n        // `validate` rejection or a persisted post-commit `cellErrors` entry set the message — same\r\n        // aria-invalid:ring-destructive/20 convention every shadcn form control in this repo already\r\n        // uses (input.tsx/button.tsx/select.tsx), inset + z-1 so it wins over the search-match tint.\r\n        errorMessage && \"z-1 aria-invalid:bg-destructive/10 aria-invalid:ring-3 aria-invalid:ring-inset aria-invalid:ring-destructive/20 dark:aria-invalid:bg-destructive/15 dark:aria-invalid:ring-destructive/40\",\r\n        gridCellClassName,\r\n        columnCellClassName,\r\n      )}\r\n      style={style}\r\n      onPointerDown={isPinnedRow ? undefined : onPointerDown}\r\n      onClick={isPinnedRow ? undefined : onClick}\r\n      // dblclick opens the editor (startEditing) — pointless on a row with nothing to edit yet,\r\n      // and skipping it avoids the guaranteed no-op round-trip through resolveEditTarget.\r\n      onDoubleClick={isPinnedRow || isSkeleton ? undefined : onDoubleClick}\r\n    >\r\n      {isEditing ? (\r\n        content\r\n      ) : (\r\n        <span\r\n          className={cn(\r\n            \"w-full min-w-0 overflow-hidden text-ellipsis whitespace-nowrap\",\r\n            align === \"center\" && \"text-center\",\r\n            align === \"right\" && \"text-end\",\r\n          )}\r\n        >\r\n          {content}\r\n        </span>\r\n      )}\r\n      {errorMessage && isEditing && (\r\n        // Visible without a hover (editing needs the message evident immediately, not just on\r\n        // the error tooltip) — positioned below the cell so it never clips the editor input.\r\n        <span\r\n          role=\"alert\"\r\n          className=\"absolute inset-x-0 top-full z-10 mt-0.5 truncate rounded-sm border border-destructive/20 bg-destructive/10 px-1.5 py-0.5 text-xs text-destructive dark:bg-destructive/20\"\r\n        >\r\n          {errorMessage}\r\n        </span>\r\n      )}\r\n    </div>\r\n  );\r\n\r\n  // While editing the message is already visible without a hover (the inline role=\"alert\" below),\r\n  // and wrapping mid-edit would remount the editor when a live rejection lands.\r\n  if (errorMessage && !isEditing) {\r\n    return (\r\n      <Tooltip>\r\n        <TooltipTrigger render={cell} />\r\n        <TooltipContent>{errorMessage}</TooltipContent>\r\n      </Tooltip>\r\n    );\r\n  }\r\n  return cell;\r\n}\r\n\r\n// Default shallow compare suffices: `row` and `column` keep stable identity, and the state flags\r\n// are primitives the row derives from its own per-row subscription — so only a cell whose own\r\n// flags changed re-renders, even though the whole row re-rendered to recompute them.\r\nexport const DataGridCell = memo(DataGridCellImpl);\r\n",
      "type": "registry:component",
      "target": "components/data-grid/cell.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/clipboard/index.ts",
      "content": "/** Domain barrel — clipboard parse/serialize + the grid/data-grid clipboard hooks. */\r\nexport { serializeCells } from \"./serialize-cells\";\r\nexport { parseClipboardHtml, parseClipboardText, parseClipboard } from \"./parse-clipboard\";\r\nexport {\r\n  useGridClipboard,\r\n  resolveCopyScope,\r\n  serializeRect,\r\n  serializeCopyScope,\r\n  tileToHeight,\r\n  resolvePasteTarget,\r\n  targetHeight,\r\n  buildPasteCandidates,\r\n  buildPasteWrites,\r\n  pasteText,\r\n  type UseGridClipboardOptions,\r\n  type CopyScope,\r\n} from \"./use-grid-clipboard\";\r\nexport {\r\n  useDataGridClipboard,\r\n  type UseDataGridClipboardResult,\r\n  type PasteFromClipboardResult,\r\n} from \"./use-data-grid-clipboard\";\r\n",
      "type": "registry:component",
      "target": "components/data-grid/clipboard/index.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/clipboard/parse-clipboard.ts",
      "content": "const BLOCK_TAGS = new Set([\"P\", \"DIV\", \"LI\"]);\r\n\r\n/** Converts a table cell's DOM content to text, treating `<br>` as a newline and separating block-level children (e.g. Apple Numbers' `<p>` per line) with one. */\r\nfunction cellNodeToText(cell: Element): string {\r\n  const raw = cell.getAttribute(\"data-gridcn-raw\");\r\n  if (raw !== null) return raw;\r\n\r\n  let out = \"\";\r\n  for (const node of Array.from(cell.childNodes)) {\r\n    if (node.nodeType === Node.TEXT_NODE) {\r\n      out += node.textContent ?? \"\";\r\n    } else if (node.nodeType === Node.ELEMENT_NODE) {\r\n      const el = node as Element;\r\n      if (el.tagName === \"BR\") {\r\n        out += \"\\n\";\r\n      } else if (BLOCK_TAGS.has(el.tagName) && out !== \"\") {\r\n        out += \"\\n\" + cellNodeToText(el);\r\n      } else {\r\n        out += cellNodeToText(el);\r\n      }\r\n    }\r\n  }\r\n  return out;\r\n}\r\n\r\n/**\r\n * Parses a clipboard `text/html` payload into a 2D string grid using the first `<table>` found.\r\n * Prefers each cell's `data-gridcn-raw` attribute, falling back to text content with `<br>`\r\n * treated as a newline. Returns null when no table is present.\r\n */\r\nexport function parseClipboardHtml(html: string): string[][] | null {\r\n  const doc = new DOMParser().parseFromString(html, \"text/html\");\r\n  const table = doc.querySelector(\"table\");\r\n  if (!table) return null;\r\n\r\n  return Array.from(table.rows).map((row) =>\r\n    Array.from(row.cells).map((cell) => cellNodeToText(cell)),\r\n  );\r\n}\r\n\r\n/**\r\n * Parses a clipboard `text/plain` TSV payload with Excel-style quoting: a field starting with\r\n * `\"` is quoted, `\"\"` is an escaped quote, and tabs/newlines inside quotes are literal.\r\n * Handles both `\\r\\n` and `\\n` row separators; a trailing empty row from a final newline is dropped.\r\n */\r\nexport function parseClipboardText(text: string): string[][] {\r\n  const rows: string[][] = [];\r\n  let row: string[] = [];\r\n  let field = \"\";\r\n  let inQuotes = false;\r\n  // tracks whether the current field has any content (incl. a closed \"\") so it isn't mistaken for \"nothing pending\"\r\n  let fieldPending = false;\r\n  let i = 0;\r\n  const len = text.length;\r\n  // start of the current unflushed run; -1 means nothing pending (avoids per-char += allocation)\r\n  let start = -1;\r\n\r\n  const flush = () => {\r\n    if (start !== -1) {\r\n      field += text.slice(start, i);\r\n      start = -1;\r\n    }\r\n  };\r\n  const endField = () => {\r\n    flush();\r\n    row.push(field);\r\n    field = \"\";\r\n    fieldPending = false;\r\n  };\r\n  const endRow = () => {\r\n    endField();\r\n    rows.push(row);\r\n    row = [];\r\n  };\r\n\r\n  while (i < len) {\r\n    const ch = text[i];\r\n\r\n    if (inQuotes) {\r\n      if (ch === '\"') {\r\n        flush();\r\n        if (text[i + 1] === '\"') {\r\n          field += '\"';\r\n          i += 2;\r\n          continue;\r\n        }\r\n        inQuotes = false;\r\n        i += 1;\r\n        continue;\r\n      }\r\n      if (start === -1) start = i;\r\n      i += 1;\r\n      continue;\r\n    }\r\n\r\n    if (ch === '\"' && field === \"\") {\r\n      inQuotes = true;\r\n      fieldPending = true;\r\n      i += 1;\r\n      continue;\r\n    }\r\n    if (ch === \"\\t\") {\r\n      endField();\r\n      i += 1;\r\n      continue;\r\n    }\r\n    if (ch === \"\\r\") {\r\n      // lookahead for \\r\\n; a lone \\r is treated as a row separator too\r\n      endRow();\r\n      i += text[i + 1] === \"\\n\" ? 2 : 1;\r\n      continue;\r\n    }\r\n    if (ch === \"\\n\") {\r\n      endRow();\r\n      i += 1;\r\n      continue;\r\n    }\r\n    if (start === -1) start = i;\r\n    fieldPending = true;\r\n    i += 1;\r\n  }\r\n\r\n  // final field/row, unless the input ended exactly on a row separator (trailing newline)\r\n  if (fieldPending || row.length > 0) {\r\n    endRow();\r\n  }\r\n\r\n  return rows;\r\n}\r\n\r\n/**\r\n * Produces a 2D string grid from clipboard data, preferring the HTML table representation\r\n * and falling back to TSV plain text. Returns an empty array when neither is usable.\r\n */\r\nexport function parseClipboard(data: { html?: string; text?: string }): string[][] {\r\n  if (data.html) {\r\n    const fromHtml = parseClipboardHtml(data.html);\r\n    if (fromHtml && fromHtml.length > 0) return fromHtml;\r\n  }\r\n  if (data.text !== undefined) {\r\n    return parseClipboardText(data.text);\r\n  }\r\n  return [];\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/clipboard/parse-clipboard.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/clipboard/serialize-cells.ts",
      "content": "const NEEDS_QUOTING = /[\\t\\n\\r\"]/;\r\n\r\n/** Escapes a single TSV field per Excel quoting rules (quote wrap + doubled internal quotes). */\r\nfunction quoteTsvField(cell: string): string {\r\n  if (!NEEDS_QUOTING.test(cell)) return cell;\r\n  return `\"${cell.replace(/\"/g, '\"\"')}\"`;\r\n}\r\n\r\n/** Escapes text and preserves whitespace fidelity for Excel/Sheets: tabs -> 4 spaces, runs of >=2 spaces wrapped in a span. */\r\nfunction escapeHtmlText(value: string): string {\r\n  return value\r\n    .replace(/&/g, \"&amp;\")\r\n    .replace(/</g, \"&lt;\")\r\n    .replace(/>/g, \"&gt;\")\r\n    .replace(/\\t/g, \"    \")\r\n    .replace(/  +/g, (run) => `<span>${\" \".repeat(run.length)}</span>`)\r\n    .replace(/\\n/g, \"<br>\");\r\n}\r\n\r\nfunction escapeHtmlAttr(value: string): string {\r\n  return value\r\n    .replace(/&/g, \"&amp;\")\r\n    .replace(/</g, \"&lt;\")\r\n    .replace(/>/g, \"&gt;\")\r\n    .replace(/\"/g, \"&quot;\");\r\n}\r\n\r\n/**\r\n * Serializes a rectangular block of display strings into clipboard `text/plain` (quoted TSV)\r\n * and `text/html` (a `<table>` carrying the raw value in `data-gridcn-raw` per cell).\r\n */\r\nexport function serializeCells(cells: string[][]): { text: string; html: string } {\r\n  const text = cells.map((row) => row.map(quoteTsvField).join(\"\\t\")).join(\"\\n\");\r\n\r\n  const rowsHtml = cells\r\n    .map((row) => {\r\n      const cellsHtml = row\r\n        .map((cell) => `<td data-gridcn-raw=\"${escapeHtmlAttr(cell)}\">${escapeHtmlText(cell)}</td>`)\r\n        .join(\"\");\r\n      return `<tr>${cellsHtml}</tr>`;\r\n    })\r\n    .join(\"\");\r\n\r\n  return { text, html: `<table><tbody>${rowsHtml}</tbody></table>` };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/clipboard/serialize-cells.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/clipboard/use-data-grid-clipboard.ts",
      "content": "\"use client\";\r\n\r\nimport { useCallback, useMemo } from \"react\";\r\nimport { serializeCells } from \"./serialize-cells\";\r\nimport { useDataGridActions, useDataGridStoreApi } from \"../store\";\r\nimport { useBulkGeneration } from \"../validation/bulk-generation\";\r\nimport { pasteText, resolveCopyScope, serializeCopyScope } from \"./use-grid-clipboard\";\r\n\r\n/** Result of {@link useDataGridClipboard.prototype.pasteFromClipboard}'s permission/content outcome. */\r\nexport type PasteFromClipboardResult = \"ok\" | \"permission-denied\" | \"empty\";\r\n\r\n/** Public clipboard triggers, usable outside native browser clipboard events. */\r\nexport type UseDataGridClipboardResult = {\r\n  /**\r\n   * Serializes the current copy scope and writes it to the OS clipboard (`navigator.clipboard.write`,\r\n   * falling back to `document.execCommand(\"copy\")`); resolves `'ok'` when the async Clipboard API\r\n   * accepted the write, `'fallback'` when the legacy text/plain-only path ran instead, and\r\n   * `'no-selection'` when there was no copy scope.\r\n   */\r\n  copy: () => Promise<\"ok\" | \"fallback\" | \"no-selection\">;\r\n  /** Same as {@link copy}, then clears the copied selection via `deleteSelection`. */\r\n  cut: () => Promise<\"ok\" | \"fallback\" | \"no-selection\">;\r\n  /**\r\n   * Reads `navigator.clipboard.readText()` and applies it through the same parse -> writes ->\r\n   * applyCellUpdates pipeline as a native paste. Resolves `'permission-denied'` when the read is\r\n   * rejected (no `clipboard-read` permission — the context-menu add-on falls back to a\r\n   * \"press Ctrl+V\" hint), `'empty'` when the clipboard held no usable text, else `'ok'`.\r\n   */\r\n  pasteFromClipboard: () => Promise<PasteFromClipboardResult>;\r\n};\r\n\r\n/** Guards {@link execCommandCopyFallback} against re-entrancy — a second copy fired while the async `clipboard.write` rejection from the first is still in flight must not double-run the legacy fallback. */\r\nlet execCommandFallbackInFlight = false;\r\n\r\n/** Writes `text`/`html` to the OS clipboard via the async Clipboard API, falling back to the guarded legacy `execCommand(\"copy\")` path in environments without it (or without the `clipboard-write` permission); resolves which path ran. */\r\nasync function writeToClipboard(text: string, html: string): Promise<\"ok\" | \"fallback\"> {\r\n  if (navigator.clipboard?.write && typeof ClipboardItem !== \"undefined\") {\r\n    const item = new ClipboardItem({\r\n      \"text/plain\": new Blob([text], { type: \"text/plain\" }),\r\n      \"text/html\": new Blob([html], { type: \"text/html\" }),\r\n    });\r\n    try {\r\n      await navigator.clipboard.write([item]);\r\n      return \"ok\";\r\n    } catch {\r\n      execCommandCopyFallback(text);\r\n      return \"fallback\";\r\n    }\r\n  }\r\n  execCommandCopyFallback(text);\r\n  return \"fallback\";\r\n}\r\n\r\n/** Deprecated legacy fallback ONLY (guarded, last resort): a throwaway selected textarea + `document.execCommand(\"copy\")`, used only when the async Clipboard API is unavailable or rejected. */\r\nfunction execCommandCopyFallback(text: string): void {\r\n  if (execCommandFallbackInFlight) return;\r\n  execCommandFallbackInFlight = true;\r\n  const textarea = document.createElement(\"textarea\");\r\n  textarea.value = text;\r\n  textarea.style.position = \"fixed\";\r\n  textarea.style.opacity = \"0\";\r\n  document.body.appendChild(textarea);\r\n  textarea.select();\r\n  try {\r\n    document.execCommand(\"copy\");\r\n  } finally {\r\n    document.body.removeChild(textarea);\r\n    execCommandFallbackInFlight = false;\r\n  }\r\n}\r\n\r\n/**\r\n * Public clipboard triggers for use outside native clipboard events (e.g. a context menu's\r\n * Copy/Cut/Paste items). Mirrors {@link useGridClipboard}'s native-event pipeline exactly, so\r\n * behavior stays identical whether the user presses Ctrl+C or clicks a menu item.\r\n */\r\nexport function useDataGridClipboard(): UseDataGridClipboardResult {\r\n  const actions = useDataGridActions();\r\n  const storeApi = useDataGridStoreApi();\r\n  const guard = useBulkGeneration();\r\n\r\n  const copy = useCallback((): Promise<\"ok\" | \"fallback\" | \"no-selection\"> => {\r\n    const s = storeApi.getState();\r\n    const scope = resolveCopyScope(s);\r\n    if (!scope) return Promise.resolve(\"no-selection\");\r\n    const { text, html } = serializeCells(serializeCopyScope(s, scope));\r\n    return writeToClipboard(text, html);\r\n  }, [storeApi]);\r\n\r\n  const cut = useCallback((): Promise<\"ok\" | \"fallback\" | \"no-selection\"> => {\r\n    const s = storeApi.getState();\r\n    const scope = resolveCopyScope(s);\r\n    if (!scope) return Promise.resolve(\"no-selection\");\r\n    const { text, html } = serializeCells(serializeCopyScope(s, scope));\r\n    const result = writeToClipboard(text, html);\r\n    actions.deleteSelection();\r\n    return result;\r\n  }, [storeApi, actions]);\r\n\r\n  const pasteFromClipboard = useCallback(async (): Promise<PasteFromClipboardResult> => {\r\n    let text: string;\r\n    try {\r\n      text = await navigator.clipboard.readText();\r\n    } catch {\r\n      return \"permission-denied\";\r\n    }\r\n    if (!text) return \"empty\";\r\n    const s = storeApi.getState();\r\n    const applied = pasteText(s, actions, text, guard, storeApi);\r\n    return applied ? \"ok\" : \"empty\";\r\n  }, [storeApi, actions, guard]);\r\n\r\n  return useMemo(() => ({ copy, cut, pasteFromClipboard }), [copy, cut, pasteFromClipboard]);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/clipboard/use-data-grid-clipboard.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/clipboard/use-grid-clipboard.ts",
      "content": "\"use client\";\r\n\r\nimport { useCallback, useEffect, type RefObject } from \"react\";\r\nimport type { CellCoord, GridRect } from \"../types\";\r\nimport { getCellValue } from \"../columns/column-helpers\";\r\nimport { isDev } from \"../is-dev\";\r\nimport { serializeCells } from \"./serialize-cells\";\r\nimport { parseClipboard } from \"./parse-clipboard\";\r\nimport { useDataGridActions, useDataGridStoreApi, type DataGridActions, type DataGridStoreState } from \"../store\";\r\nimport { resolveBulkWrites, type BulkCandidate, type BulkWrite } from \"../validation/validate-batch\";\r\nimport { candidateRowIds, reresolveBulkWrites, snapshotBulkBatch, useBulkGeneration, type BulkGeneration } from \"../validation/bulk-generation\";\r\n\r\n/** Options for {@link useGridClipboard}. */\r\nexport type UseGridClipboardOptions = {\r\n  rootRef: RefObject<HTMLElement | null>;\r\n  readOnly?: boolean;\r\n};\r\n\r\n/**\r\n * Copy scope per glide-behavior-spec.md §4: the primary range if one exists (ignoring the range\r\n * stack — multi-rect copy is document-scoped to the primary range only), else the selected rows\r\n * (full width) or selected columns (full height) as their disjoint member lists — a CompactSelection\r\n * from Ctrl-click is RLE and may hold non-contiguous members, so this must not collapse to a\r\n * min..max bounding rect (that would silently include unselected rows/columns in between).\r\n */\r\nexport type CopyScope = { kind: \"rect\"; rect: GridRect } | { kind: \"rows\"; rows: number[] } | { kind: \"columns\"; columns: number[] };\r\n\r\n/** Resolves the copy scope; returns null when nothing is selected. Exported for direct unit testing; not part of the public hook surface. */\r\nexport function resolveCopyScope(s: DataGridStoreState): CopyScope | null {\r\n  if (s.selection.current) return { kind: \"rect\", rect: s.selection.current.range };\r\n  const rows = s.selection.rows.toArray();\r\n  if (rows.length > 0) return { kind: \"rows\", rows };\r\n  const cols = s.selection.columns.toArray();\r\n  if (cols.length > 0) return { kind: \"columns\", columns: cols };\r\n  return null;\r\n}\r\n\r\n/** Serializes one view row's `[colStart, colStart + colCount)` slice via each cell type's `toText`. */\r\nfunction serializeRowSlice(s: DataGridStoreState, viewRow: number, colStart: number, colCount: number): string[] {\r\n  const dataRowIndex = s.viewIndex[viewRow];\r\n  const row = dataRowIndex === undefined ? undefined : s.data[dataRowIndex];\r\n  const cells: string[] = [];\r\n  for (let col = colStart; col < colStart + colCount; col++) {\r\n    const column = s.visibleColumns[col];\r\n    if (!column || row === undefined) {\r\n      cells.push(\"\");\r\n      continue;\r\n    }\r\n    const cellType = s.cellTypes[column.type ?? \"text\"];\r\n    // explicit TData=unknown: row's `undefined`-narrowed type ({} | null) would otherwise drive inference instead of column's own already-unknown TData.\r\n    const value = getCellValue<unknown, typeof column>(row, column);\r\n    const text = s.processCellForClipboard\r\n      ? s.processCellForClipboard(value, { row, column })\r\n      : (cellType?.toText(value, column.options) ?? \"\");\r\n    cells.push(text);\r\n  }\r\n  return cells;\r\n}\r\n\r\n/** Serializes `rect` to a 2D string grid via each cell type's `toText`, honoring `processCellForClipboard` when provided. */\r\nexport function serializeRect(s: DataGridStoreState, rect: GridRect): string[][] {\r\n  const out: string[][] = [];\r\n  for (let viewRow = rect.y; viewRow < rect.y + rect.height; viewRow++) {\r\n    out.push(serializeRowSlice(s, viewRow, rect.x, rect.width));\r\n  }\r\n  return out;\r\n}\r\n\r\n/**\r\n * A copy above this many cells is truncated to the rows that fit — no spreadsheet accepts a\r\n * clipboard payload this large anyway, and below this cap output is byte-identical to the\r\n * uncapped path (2026-08-02 optimization audit, confirmed medium; same precedent as\r\n * `MAX_SEARCH_MATCHES` in store/compute.ts). 200k cells is generously above any realistic manual\r\n * copy while still bounding the worst case (a Ctrl+A copy on a 100k-row grid) to a few seconds.\r\n */\r\nexport const MAX_COPY_CELLS = 200_000;\r\n\r\n/**\r\n * Serializes an explicit, disjoint list of view rows against an explicit, disjoint list of view\r\n * columns — the shared engine for the \"rows\" (all visible columns) and \"columns\" (explicit column\r\n * indices) copy-scope branches. Column + cellType resolution happens once per selected column,\r\n * hoisted OUTSIDE the row loop, instead of `serializeRowSlice`'s per-(row,col) re-resolution — the\r\n * columns branch used to call `serializeRowSlice(s, viewRow, col, 1)` once per cell, re-reading\r\n * `visibleColumns`/`cellTypes` and allocating a throwaway 1-element array every time (2026-08-02\r\n * optimization audit, confirmed medium). Truncates at {@link MAX_COPY_CELLS}.\r\n */\r\nfunction serializeDisjointRows(s: DataGridStoreState, rows: readonly number[], cols: readonly number[]): string[][] {\r\n  const resolved = cols.map((col) => {\r\n    const column = s.visibleColumns[col];\r\n    return column ? { column, cellType: s.cellTypes[column.type ?? \"text\"] } : null;\r\n  });\r\n\r\n  const maxRows = cols.length === 0 ? rows.length : Math.max(1, Math.floor(MAX_COPY_CELLS / cols.length));\r\n  const rowCount = Math.min(rows.length, maxRows);\r\n  // parity with the paste cap's dev warn (applyParsedPaste): a truncated copy must be observable\r\n  if (rowCount < rows.length && isDev()) {\r\n    console.warn(`gridcn: copy truncated to ${rowCount} of ${rows.length} rows (MAX_COPY_CELLS)`);\r\n  }\r\n\r\n  const out: string[][] = [];\r\n  for (let r = 0; r < rowCount; r++) {\r\n    const viewRow = rows[r]!; // r < rowCount <= rows.length\r\n    const dataRowIndex = s.viewIndex[viewRow];\r\n    const row = dataRowIndex === undefined ? undefined : s.data[dataRowIndex];\r\n    const cells: string[] = [];\r\n    for (const entry of resolved) {\r\n      if (!entry || row === undefined) {\r\n        cells.push(\"\");\r\n        continue;\r\n      }\r\n      const { column, cellType } = entry;\r\n      // explicit TData=unknown: row's `undefined`-narrowed type ({} | null) would otherwise drive inference instead of column's own already-unknown TData.\r\n      const value = getCellValue<unknown, typeof column>(row, column);\r\n      const text = s.processCellForClipboard\r\n        ? s.processCellForClipboard(value, { row, column })\r\n        : (cellType?.toText(value, column.options) ?? \"\");\r\n      cells.push(text);\r\n    }\r\n    out.push(cells);\r\n  }\r\n  return out;\r\n}\r\n\r\n/** All view-row indices `[0, rowCount)`, as a plain array — the \"columns\" scope's implicit row list. */\r\nfunction allViewRows(s: DataGridStoreState): number[] {\r\n  return Array.from({ length: s.viewIndex.length }, (_, i) => i);\r\n}\r\n\r\n/**\r\n * Serializes a copy scope, preserving disjoint row/column membership (a Ctrl-click multi-select\r\n * copies exactly the selected rows/columns, never the rows/columns in between). All three scopes\r\n * are capped at {@link MAX_COPY_CELLS}. The rect scope needs the cap too because two-stage Ctrl+A\r\n * produces a whole-grid rect (selection/select-all-progression.ts), so \"visually bounded by what\r\n * the user dragged\" does not bound a select-all copy.\r\n */\r\nexport function serializeCopyScope(s: DataGridStoreState, scope: CopyScope): string[][] {\r\n  if (scope.kind === \"rect\") {\r\n    const maxRows = Math.max(1, Math.floor(MAX_COPY_CELLS / Math.max(1, scope.rect.width)));\r\n    if (scope.rect.height > maxRows && isDev()) {\r\n      console.warn(`gridcn: copy truncated to ${maxRows} of ${scope.rect.height} rows (MAX_COPY_CELLS)`);\r\n    }\r\n    return serializeRect(s, { ...scope.rect, height: Math.min(scope.rect.height, maxRows) });\r\n  }\r\n  if (scope.kind === \"rows\") {\r\n    return serializeDisjointRows(\r\n      s,\r\n      scope.rows,\r\n      Array.from({ length: s.visibleColumns.length }, (_, i) => i),\r\n    );\r\n  }\r\n  return serializeDisjointRows(s, allViewRows(s), scope.columns);\r\n}\r\n\r\n/** Writes both `text/plain` (TSV) and `text/html` (table) to a clipboard event's DataTransfer. */\r\nfunction writeClipboardEvent(event: ClipboardEvent, cells: string[][]): void {\r\n  const { text, html } = serializeCells(cells);\r\n  event.clipboardData?.setData(\"text/plain\", text);\r\n  event.clipboardData?.setData(\"text/html\", html);\r\n}\r\n\r\n/**\r\n * Single pasted row tiles down to fill a taller target selection height (react-datasheet-grid\r\n * nicety, research/react-datasheet-grid-study.md §4); otherwise the parsed grid is used as-is.\r\n * Exported for direct unit testing; not part of the public hook surface.\r\n */\r\nexport function tileToHeight(cells: string[][], height: number): string[][] {\r\n  if (cells.length !== 1 || height <= 1) return cells;\r\n  const onlyRow = cells[0]!; // cells.length === 1 checked above\r\n  return Array.from({ length: height }, () => onlyRow);\r\n}\r\n\r\n/** Max cells a single paste may touch — same rationale/precedent as {@link MAX_COPY_CELLS}. */\r\nexport const MAX_PASTE_CELLS = 200_000;\r\n\r\n/**\r\n * Truncates a paste grid to the first rows that fit under `maxCells` (row-summed). The paste anchor\r\n * is the target's top-left, so the HEAD rows are kept — dropping them would silently skip the rows\r\n * the user intended to paste first. Below the cap the input is returned unchanged.\r\n */\r\nexport function truncatePasteGrid(cells: string[][], maxCells: number): string[][] {\r\n  let total = 0;\r\n  for (let i = 0; i < cells.length; i++) {\r\n    const row = cells[i]!; // i < cells.length by loop condition\r\n    if (total + row.length > maxCells) return cells.slice(0, i);\r\n    total += row.length;\r\n  }\r\n  return cells;\r\n}\r\n\r\n/**\r\n * Resolves the paste target per glide-behavior-spec.md §4: top-left of `current.range`, else the\r\n * lowest selected column at row 0, else the lowest selected row at col 0. `activeCell` alone isn't\r\n * enough — selectRow/selectColumn (row-marker/header click) never set it, so a column- or\r\n * row-only selection needs its own fallback here rather than silently no-op'ing the paste.\r\n */\r\nexport function resolvePasteTarget(s: DataGridStoreState): CellCoord | null {\r\n  if (s.selection.current) return { col: s.selection.current.range.x, row: s.selection.current.range.y };\r\n  const cols = s.selection.columns.toArray();\r\n  if (cols.length > 0) return { col: Math.min(...cols), row: 0 };\r\n  const rows = s.selection.rows.toArray();\r\n  if (rows.length > 0) return { col: 0, row: Math.min(...rows) };\r\n  return s.activeCell;\r\n}\r\n\r\n/** Height (in view rows) of the paste target: current range's height if >1 row, else 1 (anchored expand). */\r\nexport function targetHeight(s: DataGridStoreState): number {\r\n  const range = s.selection.current?.range;\r\n  return range && range.height > 1 ? range.height : 1;\r\n}\r\n\r\n/**\r\n * Builds the unvalidated candidate cells for an anchored-expand paste: `cells` written at `target`\r\n * top-left, clipped at grid bounds, skipping readOnly columns and rows that don't resolve. Values are\r\n * parsed via `processCellFromClipboard` when provided, else the cell type's `fromText`. Validation is\r\n * the caller's next step ({@link buildPasteWrites} for the sync answer, `resolveBulkWrites` when a\r\n * column's schema is async). Exported for direct unit testing; not part of the public hook surface.\r\n */\r\nexport function buildPasteCandidates(s: DataGridStoreState, cells: string[][], target: CellCoord): BulkCandidate[] {\r\n  const candidates: BulkCandidate[] = [];\r\n  const rowCount = s.viewIndex.length;\r\n  const colCount = s.visibleColumns.length;\r\n\r\n  for (let r = 0; r < cells.length; r++) {\r\n    const viewRow = target.row + r;\r\n    if (viewRow >= rowCount) break;\r\n    const dataRowIndex = s.viewIndex[viewRow];\r\n    if (dataRowIndex === undefined) continue;\r\n    const row = s.data[dataRowIndex];\r\n    if (row === undefined) continue;\r\n    const rowId = s.getRowId(row, dataRowIndex);\r\n\r\n    const sourceRow = cells[r]!; // r < cells.length by loop condition\r\n    for (let c = 0; c < sourceRow.length; c++) {\r\n      const col = target.col + c;\r\n      if (col >= colCount) break;\r\n      const column = s.visibleColumns[col];\r\n      if (!column) continue;\r\n      if (typeof column.readOnly === \"function\" ? column.readOnly(row) : Boolean(column.readOnly)) continue;\r\n\r\n      const cellType = s.cellTypes[column.type ?? \"text\"];\r\n      if (!cellType) continue;\r\n\r\n      const text = sourceRow[c]!; // c < sourceRow.length by loop condition\r\n      const value = s.processCellFromClipboard\r\n        ? s.processCellFromClipboard(text, { row, column })\r\n        : cellType.fromText(text, column.options);\r\n\r\n      candidates.push({ viewRow, columnId: column.id, value, validate: column.validate, row, rowId });\r\n    }\r\n  }\r\n  return candidates;\r\n}\r\n\r\n/**\r\n * The paste-write list: {@link buildPasteCandidates} validated through {@link resolveBulkWrites}, so\r\n * an array when every touched column validates synchronously and a Promise for it when one does not.\r\n * Kept as a named export for consumers and tests that want the pure answer without the store.\r\n */\r\nexport function buildPasteWrites(\r\n  s: DataGridStoreState,\r\n  cells: string[][],\r\n  target: CellCoord,\r\n): BulkWrite[] | Promise<BulkWrite[]> {\r\n  return resolveBulkWrites(buildPasteCandidates(s, cells, target));\r\n}\r\n\r\n/**\r\n * The paste application core (parse -> tile -> processPaste -> writes -> applyCellUpdates),\r\n * shared by the native `paste` event handler and {@link pasteText} so\r\n * both funnel through one pipeline. `cells` is already parsed (HTML-table or TSV); returns `false`\r\n * when there was nothing to paste (empty parse, no target, or `processPaste` vetoed it) so callers\r\n * can distinguish \"nothing happened\" from a successful write.\r\n *\r\n * A touched column with an ASYNC schema makes the write list a Promise: the batch is held, and the\r\n * single `applyCellUpdates` runs on resolution — still one commit, one DataChange, still dropping\r\n * failing cells silently. `guard` then decides whether that resolution may land at all; without one\r\n * (a direct call in a test) a resolved batch always applies. `true` here means \"a paste started\",\r\n * which for an async batch is not yet \"cells changed\".\r\n */\r\nfunction applyParsedPaste(\r\n  s: DataGridStoreState,\r\n  actions: DataGridActions,\r\n  cells: string[][],\r\n  guard?: BulkGeneration,\r\n  storeApi?: { getState: () => DataGridStoreState },\r\n): boolean {\r\n  if (cells.length === 0) return false;\r\n  const target = resolvePasteTarget(s);\r\n  if (!target) return false;\r\n\r\n  const tiled = tileToHeight(cells, targetHeight(s));\r\n  const finalCells = s.processPaste ? s.processPaste(tiled, target) : tiled;\r\n  if (finalCells === false) return false;\r\n\r\n  // Cap AFTER processPaste so a consumer hook cannot re-expand a truncated grid past the bound.\r\n  const cappedCells = truncatePasteGrid(finalCells, MAX_PASTE_CELLS);\r\n  if (cappedCells.length < finalCells.length && isDev()) {\r\n    console.warn(`gridcn: paste truncated to ${cappedCells.length} of ${finalCells.length} rows (MAX_PASTE_CELLS)`);\r\n  }\r\n\r\n  const candidates = buildPasteCandidates(s, cappedCells, target);\r\n  const writes = resolveBulkWrites(candidates);\r\n  if (!(writes instanceof Promise)) {\r\n    actions.applyCellUpdates(writes, \"paste\");\r\n    return true;\r\n  }\r\n\r\n  const token = guard?.begin();\r\n  const snapshot = snapshotBulkBatch(s, candidateRowIds(s, candidates));\r\n  void writes.then((resolved) => {\r\n    const current = storeApi?.getState() ?? s;\r\n    if (guard && token !== undefined && !guard.isCurrent(token, current, snapshot)) return;\r\n    actions.applyCellUpdates(reresolveBulkWrites(current, resolved), \"paste\");\r\n  });\r\n  return true;\r\n}\r\n\r\n/**\r\n * Applies pasted plain text through the same pipeline the native `paste` event uses (parse -> tile\r\n * -> processPaste -> writes -> applyCellUpdates), for triggers outside a native ClipboardEvent\r\n * (context-menu Paste, programmatic paste). Text-only — no HTML-table fidelity, since there's no\r\n * `text/html` payload to prefer; parity with a plain-text native paste. Returns whether a paste\r\n * started (false when there's no paste target or `processPaste` vetoed it); with an async schema on\r\n * a touched column the cells commit once it resolves.\r\n */\r\nexport function pasteText(\r\n  s: DataGridStoreState,\r\n  actions: DataGridActions,\r\n  text: string,\r\n  guard?: BulkGeneration,\r\n  storeApi?: { getState: () => DataGridStoreState },\r\n): boolean {\r\n  if (s.editing || s.readOnly) return false;\r\n  return applyParsedPaste(s, actions, parseClipboard({ text }), guard, storeApi);\r\n}\r\n\r\n/**\r\n * Wires native `copy`/`cut`/`paste` events to the grid root so OS-native shortcuts and menu\r\n * actions work (glide-behavior-spec.md §2, §4). Attached to the root element (not `document`) so\r\n * multiple grids on one page each own their own clipboard traffic, gated on focus being inside the\r\n * grid via roving tabindex; inactive while a cell is being edited (the editor owns its own\r\n * clipboard then).\r\n */\r\nexport function useGridClipboard(options: UseGridClipboardOptions): void {\r\n  const { rootRef, readOnly } = options;\r\n  const actions = useDataGridActions();\r\n  const storeApi = useDataGridStoreApi();\r\n  // One counter for this grid's paste surface: a second paste supersedes a still-validating first.\r\n  const guard = useBulkGeneration();\r\n\r\n  const onCopy = useCallback(\r\n    (event: ClipboardEvent) => {\r\n      const s = storeApi.getState();\r\n      if (s.editing) return;\r\n      const scope = resolveCopyScope(s);\r\n      if (!scope) return;\r\n      event.preventDefault();\r\n      writeClipboardEvent(event, serializeCopyScope(s, scope));\r\n    },\r\n    [storeApi],\r\n  );\r\n\r\n  const onCut = useCallback(\r\n    (event: ClipboardEvent) => {\r\n      const s = storeApi.getState();\r\n      if (s.editing) return;\r\n      const scope = resolveCopyScope(s);\r\n      if (!scope) return;\r\n      event.preventDefault();\r\n      writeClipboardEvent(event, serializeCopyScope(s, scope));\r\n      if (!readOnly) actions.deleteSelection();\r\n    },\r\n    [storeApi, actions, readOnly],\r\n  );\r\n\r\n  const onPaste = useCallback(\r\n    (event: ClipboardEvent) => {\r\n      const s = storeApi.getState();\r\n      if (s.editing || readOnly) return;\r\n      // resolvePasteTarget gates BEFORE parsing/preventDefault, so a no-target paste (nothing\r\n      // selected/active) leaves the native paste behavior (e.g. into an unrelated focused input) intact.\r\n      if (!resolvePasteTarget(s)) return;\r\n      const html = event.clipboardData?.getData(\"text/html\");\r\n      const text = event.clipboardData?.getData(\"text/plain\");\r\n      const parsed = parseClipboard({ html: html || undefined, text });\r\n      if (parsed.length === 0) return;\r\n      event.preventDefault();\r\n      applyParsedPaste(s, actions, parsed, guard, storeApi);\r\n    },\r\n    [storeApi, actions, readOnly, guard],\r\n  );\r\n\r\n  useEffect(() => {\r\n    const root = rootRef.current;\r\n    if (!root) return;\r\n    root.addEventListener(\"copy\", onCopy);\r\n    root.addEventListener(\"cut\", onCut);\r\n    root.addEventListener(\"paste\", onPaste);\r\n    return () => {\r\n      root.removeEventListener(\"copy\", onCopy);\r\n      root.removeEventListener(\"cut\", onCut);\r\n      root.removeEventListener(\"paste\", onPaste);\r\n    };\r\n  }, [rootRef, onCopy, onCut, onPaste]);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/clipboard/use-grid-clipboard.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/columns/column-format-helpers.ts",
      "content": "import type { ColumnDef } from \"../types\";\r\n\r\n/** Column label for a11y strings: `headerText`, else the string `header`, else the id. Generic over TData/TValue so callers with a concrete `ColumnDef<TData, TValue>` (e.g. a cell editor's `column` prop) don't need a cast to `ColumnDef<unknown, unknown>`. */\r\nexport function columnLabelText<TData = never, TValue = unknown>(column: ColumnDef<TData, TValue>): string {\r\n  return column.headerText ?? (typeof column.header === \"string\" ? column.header : column.id);\r\n}\r\n\r\n/** Run-length-encodes consecutive equal widths into `repeat(n, wpx)` grid-template segments. */\r\nexport function encodeTemplate(widths: number[]): string {\r\n  const segments: string[] = [];\r\n  let i = 0;\r\n  while (i < widths.length) {\r\n    let j = i + 1;\r\n    while (j < widths.length && widths[j] === widths[i]) j++;\r\n    const count = j - i;\r\n    segments.push(count > 1 ? `repeat(${count}, ${widths[i]}px)` : `${widths[i]}px`);\r\n    i = j;\r\n  }\r\n  return segments.join(\" \");\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/columns/column-format-helpers.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/columns/column-helpers.ts",
      "content": "import type { AccessorKeyOf, CellOptionsOf, CellTypeKey, CellValueOf, ColumnDef } from \"../types\";\r\nimport { isDev } from \"../is-dev\";\r\n\r\n/** Keys of `T` whose value extends `V` (nullable `V` also matches the non-null key type). */\r\nexport type KeysMatching<T, V> = {\r\n  [K in keyof T]-?: T[K] extends V\r\n    ? K\r\n    : null extends V\r\n      ? T[K] extends NonNullable<V>\r\n        ? K\r\n        : never\r\n      : never;\r\n}[keyof T] &\r\n  string;\r\n\r\n/**\r\n * A column definition whose `type` selects a {@link CellTypeKey}, narrowing\r\n * `options` and the inferred value type (and thus `accessorKey`) through\r\n * the `GridCellTypes` map. Omitting `type` behaves as `\"text\"`.\r\n */\r\nexport type TypedColumnDef<TData, K extends CellTypeKey> = Omit<\r\n  ColumnDef<TData, CellValueOf<K>>,\r\n  \"type\" | \"options\" | \"accessorKey\"\r\n> & {\r\n  // `type` must be required for non-text branches, else an untyped column literal\r\n  // structurally matches every branch of AnyTypedColumn (and defaults to \"text\" at runtime).\r\n  type: K;\r\n  options?: CellOptionsOf<K>;\r\n  accessorKey?: KeysMatching<TData, CellValueOf<K>>;\r\n};\r\n\r\n/** `TypedColumnDef` for the default \"text\" branch, where `type` may be omitted. */\r\nexport type TypedTextColumnDef<TData> = Omit<TypedColumnDef<TData, \"text\">, \"type\"> & {\r\n  type?: \"text\";\r\n};\r\n\r\n/** A `TypedColumnDef` for some cell-type key, used where the specific `K` need not be named. */\r\nexport type AnyTypedColumn<TData> =\r\n  | TypedTextColumnDef<TData>\r\n  | { [K in Exclude<CellTypeKey, \"text\">]: TypedColumnDef<TData, K> }[Exclude<CellTypeKey, \"text\">];\r\n\r\n/** All keys any `AnyTypedColumn<TData>` branch may carry (for excess-property rejection). */\r\ntype KnownColumnKey<TData> = keyof AnyTypedColumn<TData>;\r\n\r\n/** Every key of a column literal in `T` that lies outside the known column shape. */\r\ntype ExcessColumnKeys<TData, T extends readonly unknown[]> = {\r\n  [I in keyof T]: Exclude<keyof T[I], KnownColumnKey<TData>>;\r\n}[number];\r\n\r\n/**\r\n * Rejects excess properties: generic inference against a union constraint (as `defineColumns`\r\n * does against `AnyTypedColumn`) skips TypeScript's normal excess-property check on object\r\n * literals. An unknown key turns this into a required argument, so the call fails to compile and\r\n * the missing argument's type names the key.\r\n */\r\ntype RejectExcessColumnKeys<TData, T extends readonly unknown[]> = [ExcessColumnKeys<TData, T>] extends [never]\r\n  ? []\r\n  : [unknownColumnKey: `unknown column key: ${ExcessColumnKeys<TData, T> & string}`];\r\n\r\n/**\r\n * Identity helper that gives each column literal in the array its own\r\n * `TValue`/`options` inference (via `type`) while returning `columns`\r\n * unchanged at runtime. In dev mode it warns on duplicate column ids and on\r\n * columns missing both `accessorKey` and `accessorFn`.\r\n */\r\nexport function defineColumns<TData>(): <const T extends readonly AnyTypedColumn<TData>[]>(\r\n  columns: T,\r\n  // A check on `columns` itself (`T & ...`) drops callback contextual typing on TypeScript 5.x.\r\n  ...excessKeyCheck: RejectExcessColumnKeys<TData, T>\r\n) => T {\r\n  return (columns, ..._excessKeyCheck) => {\r\n    if (isDev()) {\r\n      const seen = new Set<string>();\r\n      for (const column of columns) {\r\n        if (seen.has(column.id)) {\r\n          console.warn(`[data-grid] duplicate column id \"${column.id}\"`);\r\n        }\r\n        seen.add(column.id);\r\n        if (!column.accessorKey && !column.accessorFn) {\r\n          console.warn(`[data-grid] column \"${column.id}\" has neither accessorKey nor accessorFn`);\r\n        }\r\n      }\r\n    }\r\n    return columns;\r\n  };\r\n}\r\n\r\n/**\r\n * A minimal column shape covering both plain `ColumnDef` and the narrower\r\n * per-literal type `defineColumns`'s `const` inference produces (which keeps\r\n * only the accessor fields actually present in the literal, no TValue-bearing\r\n * member of its own).\r\n */\r\nexport type AccessorLike<TData> = {\r\n  id: string;\r\n  accessorKey?: AccessorKeyOf<TData>;\r\n  accessorFn?: (row: TData) => unknown;\r\n  setValue?: (row: TData, value: never) => TData;\r\n};\r\n\r\n/**\r\n * Resolves the value type a column reads/writes: `accessorFn`'s return type\r\n * if present, else the field type at `accessorKey`. Mirrors `getCellValue`'s\r\n * runtime precedence so it works on the narrow literal types `defineColumns`\r\n * produces (which carry no explicit TValue-bearing member).\r\n */\r\nexport type InferredValue<TData, TCol> = TCol extends { accessorFn: (row: TData) => infer TValue }\r\n  ? TValue\r\n  : TCol extends { accessorKey: infer K extends keyof TData }\r\n    ? TData[K]\r\n    : unknown;\r\n\r\n/** Reads a column's value from `row` via `accessorFn`/`accessorKey`. */\r\nexport function getCellValue<TData, const TCol extends AccessorLike<TData>>(\r\n  row: TData,\r\n  column: TCol,\r\n): InferredValue<TData, TCol> {\r\n  if (column.accessorFn) return column.accessorFn(row) as InferredValue<TData, TCol>;\r\n  // row as Record: AccessorKeyOf<TData> degrades to a bare `string` when TData=unknown (the\r\n  // erasure-boundary case, see types.ts), which TS can no longer prove indexes `row` — sound because\r\n  // a real (non-unknown) TData's accessorKey is still `keyof TData & string`, indexable directly.\r\n  if (column.accessorKey) return (row as Record<string, unknown>)[column.accessorKey] as InferredValue<TData, TCol>;\r\n  throw new Error(`[data-grid] column \"${column.id}\" has no accessorKey or accessorFn to read`);\r\n}\r\n\r\n/**\r\n * Writes `value` back onto `row` immutably: `setValue` if provided, else a\r\n * spread on `accessorKey`. Throws for accessorFn-only columns with no `setValue`.\r\n */\r\nexport function setCellValue<TData, const TCol extends AccessorLike<TData>>(\r\n  row: TData,\r\n  column: TCol,\r\n  value: InferredValue<TData, TCol>,\r\n): TData {\r\n  // value as never: TCol's setValue is typed per-column, but TS can't unify it with InferredValue<TData, TCol> at this generic call site — safe by TCol's own constraint.\r\n  if (column.setValue) return column.setValue(row, value as never);\r\n  if (column.accessorKey) return { ...row, [column.accessorKey]: value };\r\n  throw new Error(`[data-grid] column \"${column.id}\" is not writable (no setValue or accessorKey)`);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/columns/column-helpers.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/columns/index.ts",
      "content": "/** Domain barrel — column sizing/pinning/reorder/label helpers. */\r\nexport {\r\n  defineColumns,\r\n  getCellValue,\r\n  setCellValue,\r\n  type KeysMatching,\r\n  type TypedColumnDef,\r\n  type TypedTextColumnDef,\r\n  type AnyTypedColumn,\r\n  type AccessorLike,\r\n  type InferredValue,\r\n} from \"./column-helpers\";\r\nexport { columnLabelText, encodeTemplate } from \"./column-format-helpers\";\r\nexport { measureTextWidths, measureColumnAutosizeWidth } from \"./measure-column-text\";\r\nexport { pinLeftOffsets, pinRightOffsets } from \"./pin-offsets\";\r\nexport { pinnedInsetStyle } from \"./pinned-inset-style\";\r\nexport { resolveColumnWidth, distributeFlexWidths } from \"./resolve-column-width\";\r\nexport {\r\n  useColumnReorder,\r\n  type ColumnReorderState,\r\n  type ColumnReorderHandlers,\r\n} from \"./use-column-reorder\";\r\nexport {\r\n  useColumnResize,\r\n  readRenderedCellTexts,\r\n  type ColumnResizeHandlers,\r\n} from \"./use-column-resize\";\r\nexport {\r\n  DataGridSortIndicator,\r\n  ariaSortFor,\r\n  type DataGridSortIndicatorProps,\r\n} from \"./sort-indicator\";\r\n",
      "type": "registry:component",
      "target": "components/data-grid/columns/index.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/columns/measure-column-text.ts",
      "content": "/** Extra horizontal padding (px) added around measured text: cell's own inline padding (px-2 = 8px each side) plus a small buffer. */\r\nconst CELL_PADDING = 20;\r\n\r\nlet sharedCanvas: HTMLCanvasElement | null = null;\r\n\r\n/** Lazily creates (and caches) the offscreen canvas used for `measureText`; returns null when canvas is unavailable (SSR/jsdom without a 2d context). */\r\nfunction getMeasureContext(): CanvasRenderingContext2D | null {\r\n  if (typeof document === \"undefined\") return null;\r\n  if (!sharedCanvas) sharedCanvas = document.createElement(\"canvas\");\r\n  return sharedCanvas.getContext(\"2d\");\r\n}\r\n\r\n/**\r\n * Measures the widest of `texts` (in px) using `font` via canvas `measureText` — far cheaper than\r\n * a DOM layout pass per candidate string. Returns 0 when no canvas context is available (SSR).\r\n */\r\nexport function measureTextWidths(texts: readonly string[], font: string): number {\r\n  const ctx = getMeasureContext();\r\n  if (!ctx) return 0;\r\n  ctx.font = font;\r\n  let max = 0;\r\n  for (const text of texts) {\r\n    const width = ctx.measureText(text).width;\r\n    if (width > max) max = width;\r\n  }\r\n  return max;\r\n}\r\n\r\n/**\r\n * Autosize measurement for a column's double-click-to-fit gesture: measures the\r\n * header text plus every CURRENTLY RENDERED cell's text in that column (not the whole dataset —\r\n * only what's actually mounted), using the grid's own computed font so the measurement matches\r\n * what's on screen, then clamps to `[minWidth, maxWidth]` and adds cell padding.\r\n */\r\nexport function measureColumnAutosizeWidth(args: {\r\n  headerText: string;\r\n  cellTexts: readonly string[];\r\n  font: string;\r\n  minWidth?: number;\r\n  maxWidth?: number;\r\n}): number {\r\n  const { headerText, cellTexts, font, minWidth = 0, maxWidth = Number.POSITIVE_INFINITY } = args;\r\n  const widest = measureTextWidths([headerText, ...cellTexts], font);\r\n  const target = Math.ceil(widest) + CELL_PADDING;\r\n  return Math.min(Math.max(target, minWidth), maxWidth);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/columns/measure-column-text.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/columns/pin-offsets.ts",
      "content": "import type { AnyColumnDef } from \"../store\";\r\n\r\n/** Cumulative inset-inline-start offsets (px) for pinned-left columns, keyed by column index. */\r\nexport function pinLeftOffsets(widths: number[], pins: (AnyColumnDef[\"pin\"] | undefined)[]): number[] {\r\n  const offsets: number[] = [];\r\n  let acc = 0;\r\n  for (let i = 0; i < widths.length; i++) {\r\n    offsets.push(acc);\r\n    if (pins[i] === \"left\") acc += widths[i]!; // i < widths.length by loop condition\r\n  }\r\n  return offsets;\r\n}\r\n\r\n/** Cumulative inset-inline-end offsets (px) for pinned-right columns, keyed by column index. */\r\nexport function pinRightOffsets(widths: number[], pins: (AnyColumnDef[\"pin\"] | undefined)[]): number[] {\r\n  const offsets = new Array<number>(widths.length).fill(0);\r\n  let acc = 0;\r\n  for (let i = widths.length - 1; i >= 0; i--) {\r\n    offsets[i] = acc;\r\n    if (pins[i] === \"right\") acc += widths[i]!; // 0 <= i < widths.length by loop condition\r\n  }\r\n  return offsets;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/columns/pin-offsets.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/columns/column-border.ts",
      "content": "// Right-pinned cells draw their divider on the start side, so it marks the band edge while columns scroll underneath.\r\nexport const COLUMN_BORDER =\r\n  \"border-e data-[pinned=right]:border-e-0 data-[pinned=right]:border-s border-x-[color:var(--grid-column-border,var(--color-border))]\";\r\n",
      "type": "registry:component",
      "target": "components/data-grid/columns/column-border.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/columns/pinned-inset-style.ts",
      "content": "import type { CSSProperties } from \"react\";\r\nimport type { AnyColumnDef } from \"../store\";\r\n\r\n/** Pinned-cell inset style: position:relative works under a transformed ancestor, `position:sticky` doesn't. */\r\nexport function pinnedInsetStyle(\r\n  pinned: AnyColumnDef[\"pin\"] | undefined,\r\n  index: number,\r\n): CSSProperties {\r\n  if (!pinned) return {};\r\n  if (pinned === \"left\") {\r\n    // Cell's flow position is trackLeft, then the ancestor canvas applies -scrollLeft; to land the\r\n    // cell at the static offset regardless of scroll, add scrollLeft back and cancel trackLeft.\r\n    return {\r\n      position: \"relative\",\r\n      insetInlineStart: `calc(var(--grid-scroll-left, 0px) + var(--grid-pin-left-${index}) - var(--grid-track-left-${index}))`,\r\n    };\r\n  }\r\n  // Pin-right: the cell's right edge must sit `staticRightOffset` from the viewport's right edge,\r\n  // but only floats to that edge once content actually overflows it — diceui's rule — via\r\n  // min(viewportWidth, contentWidth), else it hugs the content's own right edge (no dead gap).\r\n  // rendered_left = trackLeft - scrollLeft + insetInlineStart (canvas transform + this cell's own inset).\r\n  // Solve rendered_left = min(viewportWidth, contentWidth) - staticRightOffset - cellWidth for\r\n  // insetInlineStart, and simplify trackLeft + cellWidth = trackRight:\r\n  //   insetInlineStart = scrollLeft + min(viewportWidth, contentWidth) - staticRightOffset - trackRight\r\n  return {\r\n    position: \"relative\",\r\n    insetInlineStart: `calc(var(--grid-scroll-left, 0px) + min(var(--grid-viewport-width), var(--grid-content-width)) - var(--grid-pin-right-${index}) - var(--grid-track-right-${index}))`,\r\n  };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/columns/pinned-inset-style.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/columns/resolve-column-width.ts",
      "content": "import type { AnyColumnDef } from \"../store\";\r\n\r\n/** Absolute floor for any column width: a runaway drag or a zero/negative `minWidth` can never collapse a column to unusable. */\r\nexport const MIN_COLUMN_WIDTH = 32;\r\n\r\n/**\r\n * Resolves a column's rendered width in px: live override, else def `width`, else the 150px\r\n * default, clamped by min/max. The clamp here covers the RENDERED width; programmatic writes go\r\n * through {@link clampColumnWidth}, which adds the 32px floor on top of `minWidth`.\r\n */\r\nexport function resolveColumnWidth(column: AnyColumnDef, override: number | undefined): number {\r\n  const base = override ?? column.width ?? 150;\r\n  const min = column.minWidth ?? 0;\r\n  const max = column.maxWidth ?? Number.POSITIVE_INFINITY;\r\n  return Math.min(Math.max(base, min), max);\r\n}\r\n\r\n/** Clamps `width` into the column's legal range `[max(MIN_COLUMN_WIDTH, minWidth), maxWidth]` — the one clamp the resize gesture and the store actions share. */\r\nexport function clampColumnWidth(column: AnyColumnDef, width: number): number {\r\n  return resolveColumnWidth(column, Math.max(width, MIN_COLUMN_WIDTH));\r\n}\r\n\r\n/**\r\n * Grows `baseWidths` to fill positive leftover viewport space, proportional to `flexes`\r\n * (standard flexbox grow resolution: distribute leftover proportionally, clamp at maxWidth,\r\n * drop clamped columns from the active set and redistribute their unused share among the rest,\r\n * iterate until stable). Columns with no/zero flex, or every column when leftover <= 0, keep\r\n * their base width — a flex column never shrinks below it. Returns `baseWidths` unchanged (same\r\n * reference) when there's nothing to distribute, a cheap bail for the common non-flex grid.\r\n */\r\nexport function distributeFlexWidths(\r\n  baseWidths: number[],\r\n  flexes: (number | undefined)[],\r\n  maxWidths: number[],\r\n  available: number,\r\n): number[] {\r\n  const totalBase = baseWidths.reduce((a, b) => a + b, 0);\r\n  const totalLeftover = available - totalBase;\r\n  const flexIndices = flexes.reduce<number[]>((acc, f, i) => {\r\n    if (f !== undefined && f > 0) acc.push(i);\r\n    return acc;\r\n  }, []);\r\n  if (totalLeftover <= 0 || flexIndices.length === 0) return baseWidths;\r\n\r\n  // exact/widths/floors/result/order below all index by i, which ranges over flexIndices/active —\r\n  // positions derived from flexes/baseWidths, so every indexed access here is in-bounds by construction.\r\n  const exact = baseWidths.slice(); // fractional target width per column, refined each pass\r\n  let active = flexIndices.slice();\r\n  let remainingLeftover = totalLeftover;\r\n  while (active.length > 0 && remainingLeftover > 1e-9) {\r\n    const totalFlex = active.reduce((sum, i) => sum + (flexes[i] as number), 0);\r\n    const stillActive: number[] = [];\r\n    let clampedAway = 0;\r\n    for (const i of active) {\r\n      const share = (remainingLeftover * (flexes[i] as number)) / totalFlex;\r\n      const uncapped = exact[i]! + share;\r\n      const capped = Math.min(uncapped, maxWidths[i]!);\r\n      clampedAway += uncapped - capped;\r\n      exact[i] = capped;\r\n      if (capped < uncapped) continue; // hit its maxWidth — leaves the active set\r\n      stillActive.push(i);\r\n    }\r\n    if (clampedAway <= 1e-9) break; // nothing clamped this pass — fully distributed, stop\r\n    remainingLeftover = clampedAway;\r\n    active = stillActive;\r\n  }\r\n\r\n  // Largest-remainder rounding so the sum matches `available` exactly (templates are whole px).\r\n  const widths = baseWidths.slice();\r\n  for (const i of flexIndices) widths[i] = exact[i]!;\r\n  const floors = widths.map(Math.floor);\r\n  let remainder = available - floors.reduce((a, b) => a + b, 0);\r\n  const order = flexIndices.map((i) => ({ i, frac: widths[i]! - floors[i]! })).sort((a, b) => b.frac - a.frac);\r\n  const result = floors.slice();\r\n  for (const { i } of order) {\r\n    if (remainder <= 0) break;\r\n    result[i]! += 1;\r\n    remainder -= 1;\r\n  }\r\n  return result;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/columns/resolve-column-width.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/columns/sort-indicator.tsx",
      "content": "\"use client\";\r\n\r\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\r\nimport type { ReactNode } from \"react\";\r\nimport type { SortSpec } from \"../types\";\r\n\r\n/** Props for {@link DataGridSortIndicator}. */\r\nexport type DataGridSortIndicatorProps = {\r\n  columnId: string;\r\n  sortState: readonly SortSpec[];\r\n};\r\n\r\n/**\r\n * Sort direction arrow (muted, size-3.5) for a header, plus its multi-sort priority number when\r\n * more than one sort is active. Renders nothing when the column isn't sorted.\r\n */\r\nexport function DataGridSortIndicator(props: DataGridSortIndicatorProps): ReactNode {\r\n  const { columnId, sortState } = props;\r\n  const index = sortState.findIndex((s) => s.columnId === columnId);\r\n  if (index === -1) return null;\r\n  const { direction } = sortState[index]!; // index !== -1 checked above, so it's a valid position\r\n  const Icon = direction === \"asc\" ? ChevronUp : ChevronDown;\r\n  return (\r\n    <span className=\"ms-1 inline-flex items-center gap-0.5 text-muted-foreground\" data-grid-sort-indicator={direction}>\r\n      <Icon className=\"size-3.5\" />\r\n      {sortState.length > 1 && <span className=\"text-[10px] tabular-nums\">{index + 1}</span>}\r\n    </span>\r\n  );\r\n}\r\n\r\n/** Resolves a header's `aria-sort` attribute from the active multi-sort spec. */\r\nexport function ariaSortFor(columnId: string, sortState: readonly SortSpec[]): \"ascending\" | \"descending\" | \"none\" {\r\n  const entry = sortState.find((s) => s.columnId === columnId);\r\n  if (!entry) return \"none\";\r\n  return entry.direction === \"asc\" ? \"ascending\" : \"descending\";\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/columns/sort-indicator.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/columns/use-column-reorder.ts",
      "content": "\"use client\";\r\n\r\nimport { useCallback, useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from \"react\";\r\n\r\n/** px of pointer movement (in the drag-start direction, off the origin header) before a header press commits to reorder-drag. */\r\nconst REORDER_THRESHOLD = 5;\r\n\r\n/** Live reorder-drag UI state: the dragged column and where the drop indicator should render. */\r\nexport type ColumnReorderState = {\r\n  draggingId: string;\r\n  /** Column id the drop indicator renders next to, or null before the pointer has crossed into another header. */\r\n  overId: string | null;\r\n  /** Drop indicator renders before/after `overId`. */\r\n  position: \"before\" | \"after\";\r\n};\r\n\r\n/** Per-header pointerdown handler + the shared drag state, one instance per `<DataGridHeader>`. */\r\nexport type ColumnReorderHandlers = {\r\n  /** Attach to each header's label-zone pointerdown (never the resize-handle zone). */\r\n  onHeaderDragPointerDown: (columnId: string, event: ReactPointerEvent<HTMLElement>) => void;\r\n  dragState: ColumnReorderState | null;\r\n};\r\n\r\n/**\r\n * Drag-to-reorder columns. MUST coexist with the header multi-column select-drag\r\n * from the interaction layer: both start from a press on the header. The disambiguation rule\r\n * (documented here as the single source of truth, tested by both the reorder and the select-drag\r\n * suites): **a horizontal drag that leaves the origin header becomes a reorder drag** whenever\r\n * `enableColumnReorder` is on and the pressed column is `reorderable`; **Shift+drag is always a\r\n * select-drag**, regardless of movement, so shift-extending a multi-column selection never\r\n * accidentally reorders. A plain press+drag that never leaves the origin header (or moves before\r\n * `REORDER_THRESHOLD`) resolves as a click (selection/sort), matching the existing 5px-jitter rule\r\n * used elsewhere in the interaction layer.\r\n */\r\nexport function useColumnReorder(args: {\r\n  enabled: boolean;\r\n  isColumnReorderable: (columnId: string) => boolean;\r\n  /** Resolves the header element at a client point to a column id + before/after half, or null off any header. */\r\n  hitTestHeader: (clientX: number, clientY: number) => { columnId: string; position: \"before\" | \"after\" } | null;\r\n  onReorder: (id: string, targetId: string, position: \"before\" | \"after\") => void;\r\n  /** Called the instant a press arms into a reorder drag — the caller cancels the sibling header column-select drag that started from the same press (see the JSDoc rule above). */\r\n  onArm?: () => void;\r\n}): ColumnReorderHandlers {\r\n  const { enabled, isColumnReorderable } = args;\r\n  const argsRef = useRef(args);\r\n  argsRef.current = args;\r\n\r\n  const [dragState, setDragState] = useState<ColumnReorderState | null>(null);\r\n  // mirrors dragState synchronously — the document pointermove listener closes over stale React\r\n  // state otherwise (it's attached once per gesture, not re-subscribed on every setDragState).\r\n  const dragStateRef = useRef<ColumnReorderState | null>(null);\r\n  const pendingRef = useRef<{ columnId: string; startX: number; startY: number; pointerId: number } | null>(null);\r\n  const cleanupRef = useRef<(() => void) | null>(null);\r\n\r\n  const setDrag = useCallback((next: ColumnReorderState | null) => {\r\n    dragStateRef.current = next;\r\n    setDragState(next);\r\n  }, []);\r\n\r\n  const teardown = useCallback(() => {\r\n    cleanupRef.current?.();\r\n    cleanupRef.current = null;\r\n    pendingRef.current = null;\r\n    setDrag(null);\r\n  }, [setDrag]);\r\n\r\n  const onHeaderDragPointerDown = useCallback(\r\n    (columnId: string, event: ReactPointerEvent<HTMLElement>) => {\r\n      if (event.button !== 0) return;\r\n      // Shift+drag is always select-drag (disambiguation rule above) — never arm the reorder path.\r\n      if (event.shiftKey) return;\r\n      if (!enabled || !isColumnReorderable(columnId)) return;\r\n\r\n      pendingRef.current = { columnId, startX: event.clientX, startY: event.clientY, pointerId: event.pointerId };\r\n\r\n      const handleMove = (e: PointerEvent) => {\r\n        const pending = pendingRef.current;\r\n        if (!pending || e.pointerId !== pending.pointerId) return;\r\n\r\n        if (!dragStateRef.current) {\r\n          // not yet armed: only commit to reorder once the horizontal move exceeds the threshold\r\n          // AND has left the origin header (hitTest resolves to a different column).\r\n          const dx = Math.abs(e.clientX - pending.startX);\r\n          if (dx < REORDER_THRESHOLD) return;\r\n          const hit = argsRef.current.hitTestHeader(e.clientX, e.clientY);\r\n          if (!hit || hit.columnId === pending.columnId) return;\r\n          argsRef.current.onArm?.();\r\n          setDrag({ draggingId: pending.columnId, overId: hit.columnId, position: hit.position });\r\n          return;\r\n        }\r\n\r\n        const hit = argsRef.current.hitTestHeader(e.clientX, e.clientY);\r\n        const prev = dragStateRef.current;\r\n        setDrag({ ...prev, overId: hit?.columnId ?? null, position: hit?.position ?? prev.position });\r\n      };\r\n\r\n      const handleUp = (e: PointerEvent) => {\r\n        const pending = pendingRef.current;\r\n        if (!pending || e.pointerId !== pending.pointerId) return;\r\n        const finalState = dragStateRef.current;\r\n        if (finalState && finalState.overId) {\r\n          argsRef.current.onReorder(finalState.draggingId, finalState.overId, finalState.position);\r\n        }\r\n        teardown();\r\n      };\r\n\r\n      document.addEventListener(\"pointermove\", handleMove);\r\n      document.addEventListener(\"pointerup\", handleUp);\r\n      document.addEventListener(\"pointercancel\", handleUp);\r\n      cleanupRef.current = () => {\r\n        document.removeEventListener(\"pointermove\", handleMove);\r\n        document.removeEventListener(\"pointerup\", handleUp);\r\n        document.removeEventListener(\"pointercancel\", handleUp);\r\n      };\r\n    },\r\n    [enabled, isColumnReorderable, setDrag, teardown],\r\n  );\r\n\r\n  useEffect(() => teardown, [teardown]);\r\n\r\n  return { onHeaderDragPointerDown, dragState };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/columns/use-column-reorder.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/columns/use-column-resize.ts",
      "content": "\"use client\";\r\n\r\nimport { useCallback, useRef, useState, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type RefObject } from \"react\";\r\nimport type { AnyColumnDef } from \"../store\";\r\nimport { inlineDelta, type GridDirection } from \"../windowing/direction\";\r\nimport { measureColumnAutosizeWidth } from \"./measure-column-text\";\r\nimport { MIN_COLUMN_WIDTH, clampColumnWidth } from \"./resolve-column-width\";\r\n\r\n/** Handlers returned by {@link useColumnResize}, wired onto each header cell's resize handle. */\r\nexport type ColumnResizeHandlers = {\r\n  onPointerDown: (event: ReactPointerEvent<HTMLElement>) => void;\r\n  onDoubleClick: (event: ReactMouseEvent<HTMLElement>) => void;\r\n  /** True while a resize drag is active — drives the handle's `data-resizing` hover-affordance state. */\r\n  isResizing: boolean;\r\n};\r\n\r\n/**\r\n * Pointer-capture column resize: drag the handle to live-update the column's\r\n * width via `setColumnWidth`, clamped to `[minWidth ?? 32, maxWidth ?? Infinity]`; ends on\r\n * `lostpointercapture` (not `pointerup` — pointerup can be missed on alt-tab, per the adazzle\r\n * study). Double-click autosizes: measures the header text + every rendered cell's text in that\r\n * column via `measure-column-text.ts`'s canvas helper, using `fontSourceRef`'s computed font\r\n * (an element inside the grid root, since the grid sets its own `text-sm` and may differ from\r\n * `document.body`).\r\n */\r\nexport function useColumnResize(args: {\r\n  column: AnyColumnDef;\r\n  currentWidth: number;\r\n  setColumnWidth: (id: string, width: number) => void;\r\n  /** Fires once at drag-release/autosize — the `onColumnLayoutChange` commit point (not per drag frame). */\r\n  commitColumnWidth: (id: string, width: number) => void;\r\n  /** Reads the currently-rendered cell text for this column from the DOM (windowed rows only). */\r\n  getRenderedCellTexts: (columnId: string) => string[];\r\n  /** Element whose computed font is used for autosize measurement — must be inside the grid root, not document.body. */\r\n  fontSourceRef: RefObject<Element | null>;\r\n  /**\r\n   * Layout direction. The handle sits at the column's inline-END edge in both directions (the\r\n   * logical `end-0` class places it), so dragging toward the inline end must always widen the\r\n   * column — but `clientX` is physical, so under RTL that is a drag toward the physical LEFT and\r\n   * the raw delta has to be inverted. Defaults to `\"ltr\"`.\r\n   */\r\n  direction?: GridDirection;\r\n}): ColumnResizeHandlers {\r\n  const { column, getRenderedCellTexts } = args;\r\n  const argsRef = useRef(args);\r\n  argsRef.current = args;\r\n  const [isResizing, setIsResizing] = useState(false);\r\n\r\n  const clamp = useCallback((width: number) => clampColumnWidth(column, width), [column]);\r\n\r\n  const dragRef = useRef<{ startX: number; startWidth: number } | null>(null);\r\n\r\n  const onPointerDown = useCallback(\r\n    (event: ReactPointerEvent<HTMLElement>) => {\r\n      if (event.button !== 0) return;\r\n      event.stopPropagation();\r\n      event.preventDefault();\r\n      // React pools synthetic events — event.currentTarget is nulled once this handler returns, so\r\n      // the native listeners below (which outlive it) must close over the real element directly.\r\n      const target = event.currentTarget;\r\n      const pointerId = event.pointerId;\r\n      dragRef.current = { startX: event.clientX, startWidth: argsRef.current.currentWidth };\r\n      target.setPointerCapture?.(pointerId);\r\n      setIsResizing(true);\r\n\r\n      // Listeners on `document`, not the handle element (checklist-consistent with use-grid-interaction.ts's\r\n      // drag machinery): pointer capture retargets real hardware pointer events to the captured element, but\r\n      // that retargeting doesn't apply to events dispatched directly on `document` (incl. in tests), so a\r\n      // handle-scoped listener alone would miss every move once the pointer leaves the handle's own bounds.\r\n      let lastWidth = argsRef.current.currentWidth;\r\n      const handleMove = (e: PointerEvent) => {\r\n        if (e.pointerId !== pointerId) return;\r\n        const drag = dragRef.current;\r\n        if (!drag) return;\r\n        lastWidth = clamp(drag.startWidth + inlineDelta(e.clientX - drag.startX, argsRef.current.direction ?? \"ltr\"));\r\n        argsRef.current.setColumnWidth(column.id, lastWidth);\r\n      };\r\n      const handleEnd = (e: PointerEvent) => {\r\n        if (e.pointerId !== pointerId) return;\r\n        dragRef.current = null;\r\n        setIsResizing(false);\r\n        document.removeEventListener(\"pointermove\", handleMove);\r\n        document.removeEventListener(\"pointerup\", handleEnd);\r\n        document.removeEventListener(\"pointercancel\", handleEnd);\r\n        target.removeEventListener(\"lostpointercapture\", handleEnd);\r\n        // drag-release commit point for onColumnLayoutChange — fires once here, not per handleMove frame.\r\n        argsRef.current.commitColumnWidth(column.id, lastWidth);\r\n      };\r\n      // lostpointercapture (not pointerup) fires reliably even when the pointerup is missed (alt-tab, etc);\r\n      // pointerup/pointercancel on document are the fallback for environments without real capture semantics.\r\n      document.addEventListener(\"pointermove\", handleMove);\r\n      document.addEventListener(\"pointerup\", handleEnd);\r\n      document.addEventListener(\"pointercancel\", handleEnd);\r\n      target.addEventListener(\"lostpointercapture\", handleEnd);\r\n    },\r\n    [clamp, column.id],\r\n  );\r\n\r\n  const onDoubleClick = useCallback(() => {\r\n    const fontSource = argsRef.current.fontSourceRef.current;\r\n    const font = fontSource ? getComputedStyle(fontSource).font : \"\";\r\n    const headerText = column.headerText ?? (typeof column.header === \"string\" ? column.header : \"\");\r\n    const cellTexts = getRenderedCellTexts(column.id);\r\n    const width = measureColumnAutosizeWidth({\r\n      headerText,\r\n      cellTexts,\r\n      font,\r\n      minWidth: Math.max(MIN_COLUMN_WIDTH, column.minWidth ?? 0),\r\n      maxWidth: column.maxWidth,\r\n    });\r\n    argsRef.current.commitColumnWidth(column.id, width);\r\n  }, [column, getRenderedCellTexts]);\r\n\r\n  return { onPointerDown, onDoubleClick, isResizing };\r\n}\r\n\r\n/** Reads the rendered text of every currently-mounted gridcell for `columnId` from the DOM, scoped to `root`. */\r\nexport function readRenderedCellTexts(root: Element | null, columnId: string): string[] {\r\n  if (!root) return [];\r\n  const cells = root.querySelectorAll(`[role=\"gridcell\"][data-column-id=\"${CSS.escape(columnId)}\"]`);\r\n  return Array.from(cells, (cell) => cell.textContent ?? \"\");\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/columns/use-column-resize.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/data-attributes.ts",
      "content": "/**\r\n * Every grid-owned `data-*` DOM attribute, in one place. Each is stamped by exactly one component\r\n * (noted per entry) and read back by selectors/tests elsewhere — collecting them here means a typo\r\n * in either place is a compile error instead of a silently-broken selector. Does NOT cover\r\n * shadcn/Base-UI-owned attributes (`data-slot`, `data-state`, etc.) or plain per-row/column\r\n * identifiers (`data-column-id`) that carry a consumer value rather than a fixed grid enum.\r\n */\r\nexport const GRID_ATTR = {\r\n  /** cell.tsx: `\"left\" | \"right\"` when the column is pinned, absent otherwise. Also stamped boolean-style (`\"\"`/absent) by the selection/fill/presence overlays when their rect falls in a pinned zone. */\r\n  pinned: \"data-pinned\",\r\n  /** cell.tsx: present (`\"\"`) on a `data-grid-pinned-rows` band cell (rendered via `DataGridCell`'s `pinned` mode). */\r\n  pinnedRow: \"data-grid-pinned-row\",\r\n  /** cell.tsx: present (`\"\"`) on the focused cell. */\r\n  active: \"data-active\",\r\n  /** cell.tsx: present (`\"\"`) while the cell is in edit mode. */\r\n  editing: \"data-editing\",\r\n  /** cell.tsx: present (`\"\"`) when the cell matches the current quick-search text. */\r\n  searchMatch: \"data-search-match\",\r\n  /** row.tsx: the row's view-space index (number), immune to pinned-top band shifting `aria-rowindex`; read back by `resolve-context-menu-target.ts`. */\r\n  rowIndex: \"data-grid-row-index\",\r\n  /** header-cell.tsx: present (`\"\"`) while that column's resize handle is being dragged. */\r\n  resizing: \"data-resizing\",\r\n  /** cell-types/checkbox.tsx: present (`\"\"`) on a checkbox cell's box; a press inside it toggles on the first click. */\r\n  checkboxBox: \"data-grid-checkbox\",\r\n  /** rows/marker-cell.tsx: present (`\"\"`) on a rows-channel selected row's marker cell. */\r\n  selected: \"data-row-selected\",\r\n  /** windowing/use-scrolled-edges.ts: present (`\"\"`) on the viewport while scrolled away from its inline-start edge. */\r\n  scrolledLeft: \"data-scrolled-left\",\r\n  /** windowing/use-scrolled-edges.ts: present (`\"\"`) on the viewport while there's more to scroll toward its inline-end edge. */\r\n  scrolledRight: \"data-scrolled-right\",\r\n  /** windowing/use-scrolled-edges.ts: present (`\"\"`) on the viewport while scrolled away from its top edge. */\r\n  scrolledTop: \"data-scrolled-top\",\r\n  /** windowing/use-scrolled-edges.ts: present (`\"\"`) on the viewport while there's more to scroll toward its bottom edge. */\r\n  scrolledBottom: \"data-scrolled-bottom\",\r\n\r\n  /** body.tsx: the scrolling row-canvas container. */\r\n  rowsCanvas: \"data-grid-rows-canvas\",\r\n  /** header.tsx: the sticky header row layer. */\r\n  headerLayer: \"data-grid-header-layer\",\r\n  /** header.tsx (columns) / body.tsx (rows): the reorder drop-position indicator. */\r\n  dropIndicator: \"data-grid-drop-indicator\",\r\n  /** rows/marker-cell.tsx: the checkbox glyph inside a marker cell (`'checkbox'`/`'both'` modes) — its press keeps the row-select gesture and never arms a reorder. */\r\n  markerCheckbox: \"data-grid-marker-checkbox\",\r\n  /** rows/marker-cell.tsx: the grip handle inside a marker cell (`'reorder'` mode). */\r\n  reorderHandle: \"data-grid-reorder-handle\",\r\n  /** header-cell.tsx: the column-menu trigger button. */\r\n  headerMenuTrigger: \"data-grid-header-menu-trigger\",\r\n  /** header-cell.tsx: the column resize handle. */\r\n  resizeHandle: \"data-grid-resize-handle\",\r\n  /** columns/sort-indicator.tsx: `\"asc\" | \"desc\"`, the header's own sort-direction arrow. */\r\n  sortIndicator: \"data-grid-sort-indicator\",\r\n  /** rows/marker-cell.tsx: the row-marker column's per-row cell. */\r\n  markerCell: \"data-grid-marker-cell\",\r\n  /** rows/marker-cell.tsx: the row-number span inside a marker cell (`'number'`/`'both'` modes). */\r\n  markerNumber: \"data-grid-marker-number\",\r\n  /** rows/marker-header.tsx: the row-marker column's header cell (select-all checkbox). */\r\n  markerHeader: \"data-grid-marker-header\",\r\n  /** rows/loading-skeleton.tsx: the empty-data loading skeleton (`loading && rowCount === 0`). */\r\n  loadingSkeleton: \"data-grid-loading-skeleton\",\r\n  /** rows/loading-skeleton.tsx: the data-present loading indicator bar (`loading && rowCount > 0`). */\r\n  loadingBar: \"data-grid-loading-bar\",\r\n  /** root.tsx: the empty-state container (rendered when there are zero rows in view and not loading). */\r\n  emptyState: \"data-grid-empty-state\",\r\n  /** root.tsx: `\"left\" | \"right\" | \"top\" | \"bottom\"`, a frozen-edge shadow shown while scrolled past that edge. */\r\n  pinShadow: \"data-grid-pin-shadow\",\r\n  /** cell-types/date.tsx, cell-types/select.tsx: a cell-type's own editor popup/portal content — `use-grid-interaction.ts` ignores pointer/click events that land inside one. */\r\n  cellEditor: \"data-grid-cell-editor\",\r\n  /** overlays.tsx: the selection-range overlay(s). */\r\n  selectionOverlay: \"data-grid-selection-overlay\",\r\n  /** overlays.tsx: the active-cell focus-ring overlay. */\r\n  activeCellOverlay: \"data-grid-active-cell-overlay\",\r\n\r\n  /** data-grid-fill/fill-overlay.tsx: the dashed fill-drag preview rect. */\r\n  fillPreview: \"data-grid-fill-preview\",\r\n  /** data-grid-fill/fill-overlay.tsx: the draggable fill-handle square at a range's corner. */\r\n  fillHandle: \"data-grid-fill-handle\",\r\n  /** data-grid-presence/presence-overlay.tsx: one remote user's highlight-range overlay. */\r\n  presenceOverlay: \"data-grid-presence-overlay\",\r\n  /** data-grid-presence/presence-overlay.tsx: a remote user's name-chip label. */\r\n  presenceLabel: \"data-grid-presence-label\",\r\n  /** data-grid-pinned-rows/pinned-row-band.tsx: `\"top\" | \"bottom\"`, a pinned-row sticky band. */\r\n  pinnedRowBand: \"data-grid-pinned-row-band\",\r\n  /** data-grid-pinned-rows/pinned-row.tsx: a pinned row's position within its own band (number, 0-based). */\r\n  pinnedRowIndex: \"data-grid-pinned-row-index\",\r\n  /** data-grid-context-menu/context-menu.tsx: the cell/header context-menu popup content. */\r\n  contextMenu: \"data-grid-context-menu\",\r\n  /** data-grid-context-menu/header-dropdown.tsx: the column header's dropdown menu content. */\r\n  headerDropdown: \"data-grid-header-dropdown\",\r\n  /** data-grid-context-menu/header-dropdown.tsx: marks a portal as the header menu popup, so header-cell.tsx's own pointer handlers can ignore events that originate inside it. */\r\n  headerMenuPopup: \"data-grid-header-menu-popup\",\r\n  /** data-grid-sort-list/sort-list.tsx: the active-sort-count badge on the sort trigger button. */\r\n  sortCount: \"data-grid-sort-count\",\r\n  /** data-grid-sort-list/sort-list.tsx: the sort-list popup container. */\r\n  sortList: \"data-grid-sort-list\",\r\n  /** data-grid-sort-list/sort-list.tsx: one reorderable row within the sort list. */\r\n  sortRow: \"data-grid-sort-row\",\r\n  /** data-grid-toolbar/toolbar.tsx: the toolbar container; also used by `data-grid-toolbar/search.tsx` to test whether an event target sits inside the grid's own scope. */\r\n  toolbar: \"data-grid-toolbar\",\r\n  /** data-grid-toolbar/columns-menu.tsx: the show/hide-columns dropdown menu content. */\r\n  columnsMenu: \"data-grid-columns-menu\",\r\n  /** data-grid-toolbar/filter-menu.tsx: the active-filter-count badge on the filter trigger button. */\r\n  filterCount: \"data-grid-filter-count\",\r\n  /** data-grid-toolbar/filter-menu.tsx: the filter-list popup container. */\r\n  filterMenu: \"data-grid-filter-menu\",\r\n  /** data-grid-toolbar/filter-menu.tsx: one reorderable row within the filter list. */\r\n  filterRow: \"data-grid-filter-row\",\r\n  /** data-grid-toolbar/search.tsx: the quick-search input's container. */\r\n  search: \"data-grid-search\",\r\n  /** data-grid-toolbar/search.tsx: the search match-count badge (\"3/17\"). */\r\n  searchCount: \"data-grid-search-count\",\r\n  /** data-grid-pagination/pagination-footer.tsx: the pagination footer bar container. */\r\n  pagination: \"data-grid-pagination\",\r\n} as const;\r\n\r\n/** A key of {@link GRID_ATTR}. */\r\nexport type GridAttrName = keyof typeof GRID_ATTR;\r\n\r\n/** The attribute string for a given {@link GRID_ATTR} key. */\r\nexport type GridAttr = (typeof GRID_ATTR)[GridAttrName];\r\n\r\n/**\r\n * Per-attribute value unions, for the attributes that carry one. Includes the boolean-flag\r\n * attributes (`isX || undefined` at the SET site): React stringifies a `true` JSX value on a\r\n * custom `data-*` attribute to the literal DOM string `\"true\"` (never a bare boolean attribute),\r\n * which is what `toHaveAttribute(attr, \"true\")` in tests is actually asserting against.\r\n */\r\nexport type GridAttrValue = {\r\n  [GRID_ATTR.pinned]: \"left\" | \"right\" | \"\";\r\n  [GRID_ATTR.pinShadow]: \"left\" | \"right\" | \"top\" | \"bottom\";\r\n  [GRID_ATTR.pinnedRowBand]: \"top\" | \"bottom\";\r\n  [GRID_ATTR.sortIndicator]: \"asc\" | \"desc\";\r\n  [GRID_ATTR.active]: \"true\";\r\n  [GRID_ATTR.editing]: \"true\";\r\n  [GRID_ATTR.selected]: \"true\";\r\n  [GRID_ATTR.resizing]: \"true\";\r\n  [GRID_ATTR.searchMatch]: \"true\";\r\n  [GRID_ATTR.pinnedRow]: \"true\";\r\n};\r\n\r\n/** Builds a `[data-grid-marker-cell]` / `[data-grid-pin-shadow=\"left\"]` CSS-attribute-selector string for `querySelector`/`closest`. */\r\nexport function gridAttrSelector<TName extends GridAttrName>(\r\n  name: TName,\r\n  value?: (typeof GRID_ATTR)[TName] extends keyof GridAttrValue ? GridAttrValue[(typeof GRID_ATTR)[TName]] : never,\r\n): string {\r\n  const attr = GRID_ATTR[name];\r\n  return value === undefined ? `[${attr}]` : `[${attr}=\"${value}\"]`;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/data-attributes.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/data-grid.tsx",
      "content": "\"use client\";\r\n\r\nimport type { ReactNode } from \"react\";\r\nimport type { StoreApi } from \"zustand/vanilla\";\r\nimport type {\r\n  CellCoord,\r\n  ColumnLayout,\r\n  DataChange,\r\n  DensityMode,\r\n  FilterJoinOperator,\r\n  FilterSpec,\r\n  GetCellClassName,\r\n  GetRowClassName,\r\n  GridSelection,\r\n  HeaderClickBehavior,\r\n  Keymap,\r\n  OnCellClick,\r\n  OnRowClick,\r\n  RowMarkersMode,\r\n  SortSpec,\r\n} from \"./types\";\r\nimport { DataGridProvider, type DataGridStoreState, type SelectionChangeDetails } from \"./store\";\r\nimport type { GridDirection } from \"./windowing/direction\";\r\nimport { DataGridRoot } from \"./root\";\r\nimport { DataGridHeader } from \"./header\";\r\nimport { DataGridBody } from \"./body\";\r\nimport type { OverlayPlugin } from \"./overlays\";\r\nimport type { MarkerCellRenderer, MarkerHeaderRenderer, RowBandsSpec } from \"./layout-context\";\r\nimport type { DeepPartialLabels } from \"./labels\";\r\n\r\n/** Public entry point — the only module consumers/docs should import from. */\r\nexport { DataGridRoot, type DataGridRootProps } from \"./root\";\r\nexport { DataGridHeader } from \"./header\";\r\nexport { DataGridBody } from \"./body\";\r\n/** The per-cell renderer — `data-grid-pinned-rows`'s `DataGridPinnedRow` reuses this in `pinned` mode for identical cell-type rendering/alignment as data rows. */\r\nexport { DataGridCell, type DataGridCellProps } from \"./cell\";\r\nexport {\r\n  DataGridOverlays,\r\n  type DataGridOverlaysProps,\r\n  type OverlayPlugin,\r\n  type OverlayPluginCtx,\r\n  type PinTrackData,\r\n  type RectSegment,\r\n} from \"./overlays\";\r\n/** Pointer px -> view-coord translation + its layout input — `data-grid-fill`'s drag-tracking loop reuses this rather than reimplementing pointer hit-testing. */\r\nexport { GRID_LAYER, cellLayer, type GridLayer } from \"./layers\";\r\nexport { pointerToCoord, type InteractionLayout } from \"./interaction/use-grid-interaction\";\r\n/** Custom cell-type editors: the same focus/commit primitives the built-in editors use — seed the input's focus with the caret at the end, and guard a one-shot commit against Enter-then-blur double-commits. */\r\nexport { useSeedFocus } from \"./interaction/use-seed-focus\";\r\nexport { useCommitGuard } from \"./interaction/use-commit-guard\";\r\n/**\r\n * Direction (LTR/RTL) math, all of it. Physical pointer/scroll coordinates are converted to the\r\n * grid's inline axis exclusively through these, so add-ons doing their own pointer work stay\r\n * RTL-correct without repeating the sign logic — `data-grid-fill`'s drag auto-scroll is the\r\n * in-tree consumer. Add a helper here rather than writing new physical coordinate math elsewhere.\r\n */\r\nexport {\r\n  applyInlineScrollDelta,\r\n  directionSign,\r\n  inlineAutoScrollStep,\r\n  inlineDelta,\r\n  inlineStartX,\r\n  isInlineStartHalf,\r\n  normalizeScrollLeft,\r\n  visualArrowKey,\r\n  type GridDirection,\r\n} from \"./windowing/direction\";\r\n/** The shared root layout context (scrollRef, column layout, row/header heights) — `data-grid-fill`'s tracker component reads this to reconstruct an `InteractionLayout` from inside `DataGridRoot`'s own subtree, the only place these values exist. */\r\nexport { useDataGridRootContext, type DataGridRootContextValue } from \"./layout-context\";\r\n/** The provider-level row-bands seam — `data-grid-pinned-rows`'s `useDataGridPinnedRows()` returns a `RowBandsSpec` to pass into `rowBands`. */\r\nexport { type RowBandsSpec, type RowBandRenderCtx, type WindowedColumn } from \"./layout-context\";\r\n/** The custom row-marker render slots (`DataGridRootProps.renderMarker`/`renderMarkerHeader`) and their ctx types. */\r\nexport { type MarkerCellRenderCtx, type MarkerHeaderRenderCtx, type MarkerCellRenderer, type MarkerHeaderRenderer } from \"./layout-context\";\r\n/** `layout-context.ts`'s per-column layout (widths/template/track offsets), aliased on export to avoid colliding with `types.ts`'s same-named `ColumnLayout` (the `defaultColumnLayout`/`onColumnLayoutChange` persisted-snapshot shape) below — `data-grid-pinned-rows` needs this one, for the live render-time layout `RowBandRenderCtx.layout` carries. */\r\nexport { type ColumnLayout as GridColumnLayout } from \"./layout-context\";\r\n\r\nexport {\r\n  cellErrorKey,\r\n  flashCellKey,\r\n  DataGridProvider,\r\n  useDataGridStoreApi,\r\n  useDataGridStoreProps,\r\n  useDataGridActions,\r\n  useDataGridScrollToCell,\r\n  useDataGridReadOnly,\r\n  useDataGridKeymap,\r\n  useDataGridLabels,\r\n  useDataGridActiveCell,\r\n  useDataGridActiveColumn,\r\n  useDataGridCellTypes,\r\n  useDataGridHasActiveCell,\r\n  useDataGridSelection,\r\n  useDataGridGetSelectionValues,\r\n  useDataGridEditing,\r\n  useDataGridEditingError,\r\n  useDataGridCellErrors,\r\n  useDataGridRowHasError,\r\n  useDataGridViewIndex,\r\n  useDataGridViewStale,\r\n  useDataGridVisibleColumns,\r\n  useDataGridAllColumns,\r\n  useDataGridIsColumnHidden,\r\n  useDataGridColumnWidth,\r\n  useDataGridColumnWidths,\r\n  useDataGridSortState,\r\n  useDataGridFilterState,\r\n  useDataGridJoinOperator,\r\n  useDataGridSearchText,\r\n  useDataGridSearchMatches,\r\n  useDataGridSearchCapped,\r\n  useDataGridIsSearchMatch,\r\n  useDataGridRow,\r\n  useDataGridRowId,\r\n  useDataGridRowIds,\r\n  useDataGridRowIdToViewRow,\r\n  useDataGridRowCount,\r\n  useDataGridIsRowSelected,\r\n  useDataGridIsRowChannelSelected,\r\n  useDataGridIsRowCellSelected,\r\n  useDataGridIsCellActive,\r\n  useDataGridIsCellEditing,\r\n  useDataGridCellInitialText,\r\n  useDataGridCellEditingError,\r\n  useDataGridCellState,\r\n  type DataGridCellState,\r\n  useDataGridRowCellState,\r\n  type DataGridRowCellState,\r\n  useDataGridRowMarkers,\r\n  useDataGridSelectionConfig,\r\n  useDataGridAllRowsSelected,\r\n  useDataGridHeaderClickBehavior,\r\n  useDataGridColumnFeatureFlags,\r\n  type DataGridSyncProps,\r\n  type SelectionChangeDetails,\r\n  type SelectLineActionOptions,\r\n  type CellPatch,\r\n  type RowPatch,\r\n  type UpdateCellsOptions,\r\n  type UpdateCellsReorder,\r\n  type UpdateCellsSkip,\r\n  type UpdateCellsVerdict,\r\n  type CellErrorTarget,\r\n  type CellErrorEntry,\r\n  type DataGridStoreState,\r\n  type DataGridActions,\r\n  type DataGridStore,\r\n  type DataGridProviderProps,\r\n  type AnyColumnDef,\r\n} from \"./store\";\r\nexport type { AnyCellType, ColumnDefOf, ClipboardProcessCtx } from \"./store\";\r\nimport type { AnyCellType, ColumnDefOf, ClipboardProcessCtx } from \"./store\";\r\n\r\nexport { useDataGridClipboard, type UseDataGridClipboardResult, type PasteFromClipboardResult } from \"./clipboard/use-data-grid-clipboard\";\r\nexport { useDataGridContainer } from \"./interaction/use-data-grid-container\";\r\nexport { DataGridGlobalShortcuts, type DataGridGlobalShortcutsProps } from \"./keyboard/use-data-grid-global-shortcuts\";\r\nexport {\r\n  type GlobalShortcutAction,\r\n  type GlobalShortcutsConfig,\r\n  type DataGridGlobalShortcutActions,\r\n} from \"./keyboard/global-shortcuts\";\r\n\r\nexport {\r\n  cellTypes,\r\n  textCellType,\r\n  numberCellType,\r\n  checkboxCellType,\r\n  selectCellType,\r\n  dateCellType,\r\n} from \"./cell-types/cell-types\";\r\nexport { displayText } from \"./cell-types/display-text\";\r\nexport { CellSpan } from \"./cell-types/cell-span\";\r\n\r\nexport { CompactSelection } from \"./selection/compact-selection\";\r\n/** Bounding-box union of two rects — `data-grid-fill`'s drag pipeline uses this to expand a selection over `combineRects(source, strip)`. */\r\nexport { combineRects } from \"./selection\";\r\n/** View rows covered by a selection (primary range + rangeStack + rows channel), deduped ascending — `data-grid-context-menu`'s row ops and `data-grid-io`'s selection-scope export derive their row set from this. */\r\nexport { getSelectedViewRows } from \"./selection\";\r\nexport { defineColumns, getCellValue, setCellValue } from \"./columns/column-helpers\";\r\nexport type { KeysMatching, TypedColumnDef, TypedTextColumnDef, AnyTypedColumn, AccessorLike, InferredValue } from \"./columns/column-helpers\";\r\n/** A column's sort-direction arrow + multi-sort priority number — string headers get it automatically in `sort` mode; embed it inside a custom (ReactNode) header when you want it there. */\r\nexport { DataGridSortIndicator, type DataGridSortIndicatorProps } from \"./columns/sort-indicator\";\r\n\r\n/** Runs a column's `validate` (function form or Standard Schema) for one cell; an async schema passes through unchanged here — `runValidateBatch`/`resolveBulkWrites` are the awaiting bulk path. */\r\nexport { runValidateSync, runValidatePending, isStandardSchema, formatIssues, type CellValidate, type ValidateResult } from \"./validation/validate-cell\";\r\n/** The bulk validation engine: chunked-concurrency batches for paste/fill/import, and the per-surface race guard that drops a superseded one. See editing-cell-types.mdx. */\r\nexport {\r\n  runValidateBatch,\r\n  resolveBulkWrites,\r\n  VALIDATE_CONCURRENCY,\r\n  type ValidateBatchItem,\r\n  type BulkCandidate,\r\n  type BulkWrite,\r\n} from \"./validation/validate-batch\";\r\nexport {\r\n  useBulkGeneration,\r\n  snapshotBulkBatch,\r\n  candidateRowIds,\r\n  isBulkBatchCurrent,\r\n  type BulkGeneration,\r\n  type BulkBatchSnapshot,\r\n} from \"./validation/bulk-generation\";\r\n\r\nexport { DEFAULT_KEYMAP } from \"./keyboard\";\r\n\r\nexport { createHistory, applyChange, invertChange, type History, type HistoryOptions } from \"./interaction/history\";\r\n\r\nexport {\r\n  DEFAULT_LABELS,\r\n  deepMergeLabels,\r\n  type DataGridLabels,\r\n  type DeepPartialLabels,\r\n  type DataGridToolbarLabels,\r\n  type DataGridFilterOperatorLabels,\r\n  type DataGridContextMenuLabels,\r\n  type DataGridKeybindingsLabels,\r\n  type DataGridMarkerLabels,\r\n  type DataGridGridLabels,\r\n  type DataGridIOLabels,\r\n  type DataGridPaginationLabels,\r\n} from \"./labels\";\r\n\r\nexport { isDev } from \"./is-dev\";\r\n\r\nexport { GRID_ATTR, gridAttrSelector, type GridAttrName, type GridAttr, type GridAttrValue } from \"./data-attributes\";\r\n\r\nexport type {\r\n  CellCoord,\r\n  GridRect,\r\n  GridSelection,\r\n  CompactSelectionLike,\r\n  DataOp,\r\n  DataChange,\r\n  CellRenderProps,\r\n  CellEditorProps,\r\n  CellType,\r\n  ColumnDef,\r\n  SortSpec,\r\n  FilterOperator,\r\n  FilterSpec,\r\n  FilterJoinOperator,\r\n  GridAction,\r\n  KeyBinding,\r\n  Keymap,\r\n  GridCellTypes,\r\n  CellTypeKey,\r\n  CellValueOf,\r\n  CellOptionsOf,\r\n  RowMarkersContent,\r\n  RowMarkersMode,\r\n  SelectionConfig,\r\n  DensityMode,\r\n  HeaderClickBehavior,\r\n  ColumnLayout,\r\n  GetRowClassName,\r\n  GetCellClassName,\r\n  CellClassNameCtx,\r\n  OnCellClick,\r\n  OnRowClick,\r\n  CellClickCtx,\r\n  RowClickCtx,\r\n} from \"./types\";\r\nexport { measureColumnAutosizeWidth, measureTextWidths } from \"./columns/measure-column-text\";\r\nexport { createFilterMatcher, type SearchMatch } from \"./sort-filter\";\r\n\r\n/** Props for {@link DataGrid}. */\r\nexport type DataGridProps<TData> = {\r\n  /**\r\n   * Controlled row array (React `value` semantics) — the app owns it; every mutation only reaches\r\n   * it through `onDataChange`. Mutually exclusive with `defaultData` (which wins is `data`'s doc\r\n   * below); omit `data` and pass `defaultData` for the uncontrolled quick-start instead.\r\n   */\r\n  data?: readonly TData[];\r\n  /**\r\n   * Uncontrolled row array (React `defaultValue` semantics) — seeds the grid once and it then owns\r\n   * the array internally (edit/paste/fill/delete/row-ops all apply in place, no app state needed);\r\n   * `onDataChange` still fires as an optional notification. Ignored on later renders. If both `data`\r\n   * and `defaultData` are given, `data` wins (controlled) and dev mode warns once.\r\n   */\r\n  defaultData?: readonly TData[];\r\n  columns: readonly ColumnDefOf<TData>[];\r\n  getRowId: (row: TData, index: number) => string;\r\n  /**\r\n   * A consumer-created store (from `useDataGridStoreProps`) to serve the grid instead of\r\n   * self-creating one — see `DataGridProviderProps.store` for the shell semantics.\r\n   */\r\n  store?: StoreApi<DataGridStoreState>;\r\n  className?: string;\r\n  /** Row height in px; also exposed as the `--grid-row-height` CSS var. */\r\n  rowHeight?: number;\r\n  /** Fired once per user gesture (edit, delete, paste, fill) with the next data array and an id-keyed delta batch. */\r\n  onDataChange?: (next: readonly TData[], change: DataChange<TData>) => void;\r\n  /** Row-level cross-field validation, once per touched row after a write gesture commits; `columnId -> message` into `cellErrors`, values still commit. See {@link DataGridSyncProps.validateRow}. */\r\n  validateRow?: (row: TData, rowId: string) => Record<string, string> | null;\r\n  /** Undo keymap action (mod+Z); the `data-grid-history` add-on plugs its `undo()` in here. */\r\n  onUndo?: () => void;\r\n  /** Redo keymap action (mod+Y / mod+shift+Z); the `data-grid-history` add-on plugs its `redo()` in here. */\r\n  onRedo?: () => void;\r\n  /**\r\n   * Cell-type registry keyed by `ColumnDef.type`; defaults to the built-in registry. Providing a\r\n   * registry REPLACES the built-ins, it is not merged over them: to extend the built-ins, spread\r\n   * the exported `cellTypes` in (`cellTypes={{ ...cellTypes, myType }}`).\r\n   */\r\n  cellTypes?: Record<string, AnyCellType>;\r\n  /** Overrides a cell's clipboard-copy text; falls back to the cell type's `toText`. */\r\n  processCellForClipboard?: (value: unknown, ctx: ClipboardProcessCtx<TData>) => string;\r\n  /** Overrides parsing pasted text into a cell value; falls back to the cell type's `fromText`. */\r\n  processCellFromClipboard?: (text: string, ctx: ClipboardProcessCtx<TData>) => unknown;\r\n  /** Runs on the parsed paste grid before it's applied; returning `false` vetoes the paste entirely. */\r\n  processPaste?: (cells: string[][], target: CellCoord) => string[][] | false;\r\n  /** Merged over `DEFAULT_KEYMAP`; per-action bindings here take precedence. */\r\n  keymap?: Keymap;\r\n  /** Layout direction; omitted, it follows the page's own direction. See {@link DataGridRootProps.direction}. */\r\n  direction?: GridDirection;\r\n  /** Disables editing and delete grid-wide, independent of any per-column `readOnly`. */\r\n  readOnly?: boolean;\r\n  /** Marker column mode: a pinned-left column rendered before all data columns; default 'none'. */\r\n  rowMarkers?: RowMarkersMode;\r\n  /**\r\n   * Custom row-marker cell renderer: replaces the built-in row number/checkbox content inside the\r\n   * marker column. Requires a non-`'none'` `rowMarkers` mode (the mode still drives the track width,\r\n   * and the cell's own press/drag row-selection gesture stays on the wrapper); an interactive element\r\n   * inside the rendered node owns its own events. See {@link MarkerCellRenderer}. Pass a stable identity.\r\n   */\r\n  renderMarker?: MarkerCellRenderer;\r\n  /** Custom marker-header renderer: replaces the built-in select-all checkbox; same track and width. See {@link MarkerHeaderRenderer}. Pass a stable identity. */\r\n  renderMarkerHeader?: MarkerHeaderRenderer;\r\n  /** Whole-row selection channel gestures (marker click, Shift+Space); default true. */\r\n  enableRowSelection?: boolean;\r\n  /** Whole-column selection channel gestures (header click); default true. */\r\n  enableColumnSelection?: boolean;\r\n  /** Multi-cell rectangular range gestures (shift-click/drag, shift+arrow); false collapses to single-cell active only. Default true. */\r\n  enableRangeSelection?: boolean;\r\n  /** Ctrl/Cmd-click multi-range (rangeStack); false makes it behave as a plain click. Default true. */\r\n  enableMultiRange?: boolean;\r\n  /** Enables the header resize handle grid-wide; default true. Per-column `resizable: false` still wins. */\r\n  enableColumnResize?: boolean;\r\n  /** Enables drag-to-reorder columns grid-wide; default true. Per-column `reorderable: false` still wins. */\r\n  enableColumnReorder?: boolean;\r\n  /** Enables drag-to-reorder rows grid-wide (marker gesture); default true. See the `enableRowReorder` doc in the store's sync props. */\r\n  enableRowReorder?: boolean;\r\n  /** Enables pin/unpin actions grid-wide; default true. Per-column `pinnable: false` still wins. */\r\n  enableColumnPinning?: boolean;\r\n  /** How a plain header click behaves; default 'select'. See {@link HeaderClickBehavior}. */\r\n  headerClickBehavior?: HeaderClickBehavior;\r\n  /** Partial i18n override, deep-merged over `DEFAULT_LABELS`; see {@link useDataGridLabels}. */\r\n  labels?: DeepPartialLabels;\r\n  /** Builds a new row for `insertRow`; absent makes `insertRow` a dev-warning no-op. */\r\n  createRow?: (index: number) => TData;\r\n  /**\r\n   * Builds a duplicated row's identity for `duplicateRows`: given the source row and its new data\r\n   * index, returns the row to insert (with a distinct id). Absent makes `duplicateRows` a\r\n   * dev-warning no-op — mirrors `createRow`/`insertRow`.\r\n   */\r\n  duplicateRow?: (row: TData, index: number) => TData;\r\n  /** Controlled multi-sort spec (server escape hatch); omit for uncontrolled (store-owned) sort state. */\r\n  sortState?: SortSpec[];\r\n  /** Fires whenever the sort would change, controlled or not. */\r\n  onSortChange?: (next: SortSpec[]) => void;\r\n  /** Controlled filter spec; same controlled/uncontrolled semantics as `sortState`. */\r\n  filterState?: FilterSpec[];\r\n  /** Fires whenever the filters would change, controlled or not. */\r\n  onFilterChange?: (next: FilterSpec[]) => void;\r\n  /** Controlled join operator combining `filterState`'s rows; same controlled/uncontrolled semantics as `sortState`. Default `\"and\"`. */\r\n  joinOperator?: FilterJoinOperator;\r\n  /** Fires whenever the join operator would change, controlled or not. */\r\n  onJoinOperatorChange?: (next: FilterJoinOperator) => void;\r\n  /** Controlled quick-search text; same controlled/uncontrolled semantics as `sortState`. */\r\n  searchText?: string;\r\n  /** Fires whenever the search text would change, controlled or not. */\r\n  onSearchTextChange?: (next: string) => void;\r\n  /** Overlay-plugin registration — see `DataGridSyncProps.overlayPlugins` for the full contract; `data-grid-presence` is the motivating consumer. Pass a stable array reference. */\r\n  overlayPlugins?: readonly OverlayPlugin[];\r\n  /** Row-bands registration — see `DataGridSyncProps.rowBands` for the full contract; `data-grid-pinned-rows`'s `useDataGridPinnedRows()` is the motivating (and so far only) producer. Pass a stable reference. */\r\n  rowBands?: RowBandsSpec;\r\n  /** Seeds column widths/order/pins/hidden once at mount, over the `columns` prop's own defaults; NOT controlled — later changes are ignored. Pair with `onColumnLayoutChange` to persist a user's layout. */\r\n  defaultColumnLayout?: ColumnLayout;\r\n  /** Fires once per committed column-layout change (resize commit, reorder drop, pin/hide/autosize) with the full current snapshot. */\r\n  onColumnLayoutChange?: (next: ColumnLayout) => void;\r\n  /** Fires on every live width write during a resize drag (per drag frame) — the in-progress counterpart to `onColumnLayoutChange`. */\r\n  onColumnResizing?: (columnId: string, width: number) => void;\r\n  /** Fires on every committed selection change (click, extend step, row/column/all selection, clear) with a lazy `details.getValues()` accessor; drag-extend fires once per step. */\r\n  onSelectionChange?: (next: GridSelection, details: SelectionChangeDetails) => void;\r\n  /** Fires exactly once when the selection transitions from non-empty to empty, from any clear path. */\r\n  onSelectionCleared?: () => void;\r\n  /** Row-height preset: compact 28 / default 36 / comfortable 44. Ignored when `rowHeight` is set. */\r\n  density?: DensityMode;\r\n  /** Sticky header track height (px); `density` and `rowHeight` affect data rows only. Default 36. */\r\n  headerHeight?: number;\r\n  /** Extra unpinned columns rendered beyond the visible viewport on each side. Default 1. */\r\n  columnOverscan?: number;\r\n  /** Row class hook, merged via `cn()` after the built-in row classes. Pass a stable identity. */\r\n  getRowClassName?: GetRowClassName<TData>;\r\n  /** Cell class hook, merged via `cn()` after the built-in cell classes. Pass a stable identity. */\r\n  getCellClassName?: GetCellClassName<TData>;\r\n  /** Fired on a plain click on any cell — see {@link OnCellClick}. Attached to the cell's own existing click handler, no new subscription. Pass a stable identity. */\r\n  onCellClick?: OnCellClick<TData>;\r\n  /** Fired alongside `onCellClick` once per click regardless of column — see {@link OnRowClick}. */\r\n  onRowClick?: OnRowClick<TData>;\r\n  /** Rendered centered in place of the body when there are zero rows in view. Never shown while `loading` is true. */\r\n  emptyState?: ReactNode;\r\n  /** Presentational loading flag: zero rows renders a viewport-filling skeleton instead of the empty state; rows present adds a slim indeterminate bar under the header. Default `false`. See {@link DataGridRootProps.loading}. */\r\n  loading?: boolean;\r\n  /** Fired after a row-window commit with the rendered data-row range; see {@link DataGridRootProps.onRowWindowChange}. */\r\n  onRowWindowChange?: (range: { start: number; end: number }) => void;\r\n  /** Composition escape hatch; defaults to header + body when omitted. */\r\n  children?: ReactNode;\r\n};\r\n\r\n/**\r\n * Convenience wrapper: provider + scroll root + header + body. Compose the\r\n * parts directly (`DataGridProvider`/`DataGridRoot`/`DataGridHeader`/`DataGridBody`)\r\n * for custom layouts (toolbar, overlays, context menu).\r\n */\r\nexport function DataGrid<TData>(props: DataGridProps<TData>): ReactNode {\r\n  const {\r\n    data,\r\n    defaultData,\r\n    columns,\r\n    getRowId,\r\n    store,\r\n    className,\r\n    rowHeight,\r\n    onDataChange,\r\n    validateRow,\r\n    onUndo,\r\n    onRedo,\r\n    cellTypes,\r\n    processCellForClipboard,\r\n    processCellFromClipboard,\r\n    processPaste,\r\n    keymap,\r\n    direction,\r\n    readOnly,\r\n    rowMarkers,\r\n    renderMarker,\r\n    renderMarkerHeader,\r\n    enableRowSelection,\r\n    enableColumnSelection,\r\n    enableRangeSelection,\r\n    enableMultiRange,\r\n    enableColumnResize,\r\n    enableColumnReorder,\r\n    enableRowReorder,\r\n    enableColumnPinning,\r\n    headerClickBehavior,\r\n    labels,\r\n    createRow,\r\n    duplicateRow,\r\n    sortState,\r\n    onSortChange,\r\n    filterState,\r\n    onFilterChange,\r\n    joinOperator,\r\n    onJoinOperatorChange,\r\n    searchText,\r\n    onSearchTextChange,\r\n    overlayPlugins,\r\n    rowBands,\r\n    defaultColumnLayout,\r\n    onColumnLayoutChange,\r\n    onColumnResizing,\r\n    onSelectionChange,\r\n    onSelectionCleared,\r\n    density,\r\n    headerHeight,\r\n    columnOverscan,\r\n    getRowClassName,\r\n    getCellClassName,\r\n    onCellClick,\r\n    onRowClick,\r\n    emptyState,\r\n    loading,\r\n    onRowWindowChange,\r\n    children,\r\n  } = props;\r\n\r\n  // erasure boundary for the root-level callbacks: TData narrows to the root's unknown — safe for\r\n  // the same reason as toInternalSyncProps (store.tsx): every row these callbacks ever receive\r\n  // originated from this DataGrid's own TData-typed `data`.\r\n  const erasedRowClassName = getRowClassName as GetRowClassName<unknown> | undefined;\r\n  const erasedCellClassName = getCellClassName as GetCellClassName<unknown> | undefined;\r\n  const erasedOnCellClick = onCellClick as OnCellClick<unknown> | undefined;\r\n  const erasedOnRowClick = onRowClick as OnRowClick<unknown> | undefined;\r\n\r\n  return (\r\n    <DataGridProvider\r\n      data={data}\r\n      defaultData={defaultData}\r\n      columns={columns}\r\n      getRowId={getRowId}\r\n      store={store}\r\n      onDataChange={onDataChange}\r\n      validateRow={validateRow}\r\n      onUndo={onUndo}\r\n      onRedo={onRedo}\r\n      cellTypes={cellTypes}\r\n      processCellForClipboard={processCellForClipboard}\r\n      processCellFromClipboard={processCellFromClipboard}\r\n      processPaste={processPaste}\r\n      rowMarkers={rowMarkers}\r\n      enableRowSelection={enableRowSelection}\r\n      enableColumnSelection={enableColumnSelection}\r\n      enableRangeSelection={enableRangeSelection}\r\n      enableMultiRange={enableMultiRange}\r\n      enableColumnResize={enableColumnResize}\r\n      enableColumnReorder={enableColumnReorder}\r\n      enableRowReorder={enableRowReorder}\r\n      enableColumnPinning={enableColumnPinning}\r\n      headerClickBehavior={headerClickBehavior}\r\n      labels={labels}\r\n      createRow={createRow}\r\n      duplicateRow={duplicateRow}\r\n      sortState={sortState}\r\n      onSortChange={onSortChange}\r\n      filterState={filterState}\r\n      onFilterChange={onFilterChange}\r\n      joinOperator={joinOperator}\r\n      onJoinOperatorChange={onJoinOperatorChange}\r\n      searchText={searchText}\r\n      onSearchTextChange={onSearchTextChange}\r\n      overlayPlugins={overlayPlugins}\r\n      rowBands={rowBands}\r\n      defaultColumnLayout={defaultColumnLayout}\r\n      onColumnLayoutChange={onColumnLayoutChange}\r\n      onColumnResizing={onColumnResizing}\r\n      onSelectionChange={onSelectionChange}\r\n      onSelectionCleared={onSelectionCleared}\r\n    >\r\n      <DataGridRoot\r\n        className={className}\r\n        rowHeight={rowHeight}\r\n        density={density}\r\n        headerHeight={headerHeight}\r\n        columnOverscan={columnOverscan}\r\n        keymap={keymap}\r\n        direction={direction}\r\n        readOnly={readOnly}\r\n        renderMarker={renderMarker}\r\n        renderMarkerHeader={renderMarkerHeader}\r\n        emptyState={emptyState}\r\n        loading={loading}\r\n        getRowClassName={erasedRowClassName}\r\n        getCellClassName={erasedCellClassName}\r\n        onCellClick={erasedOnCellClick}\r\n        onRowClick={erasedOnRowClick}\r\n        onRowWindowChange={onRowWindowChange}\r\n      >\r\n        {children ?? (\r\n          <>\r\n            <DataGridHeader />\r\n            <DataGridBody />\r\n          </>\r\n        )}\r\n      </DataGridRoot>\r\n    </DataGridProvider>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/data-grid.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/header-cell.tsx",
      "content": "\"use client\";\r\n\r\nimport { useCallback, useRef, type CSSProperties, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from \"react\";\r\nimport { COLUMN_BORDER } from \"./columns/column-border\";\r\nimport { ChevronDown } from \"lucide-react\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport type { HeaderClickBehavior, SortSpec } from \"./types\";\r\nimport { pinnedInsetStyle } from \"./columns/pinned-inset-style\";\r\nimport { useColumnResize, readRenderedCellTexts } from \"./columns/use-column-resize\";\r\nimport { DataGridSortIndicator, ariaSortFor } from \"./columns/sort-indicator\";\r\nimport type { ColumnReorderState } from \"./columns/use-column-reorder\";\r\nimport type { HeaderMenuRenderer } from \"./layout-context\";\r\nimport type { GridDirection } from \"./windowing/direction\";\r\nimport { useDataGridLabels, type AnyColumnDef } from \"./store\";\r\nimport { columnLabelText as headerLabelText } from \"./columns/column-format-helpers\";\r\nimport { gridAttrSelector } from \"./data-attributes\";\r\n\r\n/** Props for {@link DataGridHeaderCell}. */\r\nexport type DataGridHeaderCellProps = {\r\n  column: AnyColumnDef;\r\n  index: number;\r\n  width: number;\r\n  gridColOffset: number;\r\n  headerClickBehavior: HeaderClickBehavior;\r\n  sortState: readonly SortSpec[];\r\n  resize: {\r\n    enabled: boolean;\r\n    scrollRootRef: React.RefObject<HTMLElement | null>;\r\n    setColumnWidth: (id: string, width: number) => void;\r\n    commitColumnWidth: (id: string, width: number) => void;\r\n    /** Signs the drag delta so the handle always widens toward the inline end — see {@link useColumnResize}. */\r\n    direction: GridDirection;\r\n  };\r\n  reorder: {\r\n    enabled: boolean;\r\n    state: ColumnReorderState | null;\r\n    onPointerDown: (columnId: string, event: ReactPointerEvent<HTMLElement>) => void;\r\n  };\r\n  onSelect: (index: number, event: ReactPointerEvent<HTMLElement>) => void;\r\n  onSort: (columnId: string, additive: boolean) => void;\r\n  /** Optional per-column header menu slot (root `renderHeaderMenu` prop) — renders a ghost chevron trigger when provided. */\r\n  renderHeaderMenu?: HeaderMenuRenderer;\r\n};\r\n\r\n/**\r\n * One header cell: label + sort indicator, a resize handle at the inline-end edge, and the\r\n * drag-to-reorder press zone over the label. See {@link import(\"./use-column-reorder\").useColumnReorder}\r\n * for the full select-vs-reorder disambiguation rule this cell's two pointerdown handlers resolve.\r\n */\r\nexport function DataGridHeaderCell(props: DataGridHeaderCellProps): ReactNode {\r\n  const {\r\n    column,\r\n    index,\r\n    width,\r\n    gridColOffset,\r\n    headerClickBehavior,\r\n    sortState,\r\n    resize: resizeProps,\r\n    reorder,\r\n    onSelect,\r\n    onSort,\r\n    renderHeaderMenu,\r\n  } = props;\r\n\r\n  const labels = useDataGridLabels();\r\n  const pinned = column.pin;\r\n  const cellRef = useRef<HTMLDivElement | null>(null);\r\n  const columnResizable = resizeProps.enabled && (column.resizable ?? true);\r\n\r\n  const getRenderedCellTexts = useCallback(\r\n    (columnId: string) => readRenderedCellTexts(resizeProps.scrollRootRef.current, columnId),\r\n    [resizeProps.scrollRootRef],\r\n  );\r\n  const resize = useColumnResize({\r\n    column,\r\n    currentWidth: width,\r\n    setColumnWidth: resizeProps.setColumnWidth,\r\n    commitColumnWidth: resizeProps.commitColumnWidth,\r\n    getRenderedCellTexts,\r\n    fontSourceRef: cellRef,\r\n    direction: resizeProps.direction,\r\n  });\r\n\r\n  const style: CSSProperties = {\r\n    gridRowStart: 1,\r\n    gridColumnStart: index + gridColOffset,\r\n    zIndex: pinned ? 3 : 2,\r\n    ...pinnedInsetStyle(pinned, index),\r\n  };\r\n\r\n  const isDragging = reorder.state?.draggingId === column.id;\r\n\r\n  const handleLabelPointerDown = useCallback(\r\n    (event: ReactPointerEvent<HTMLElement>) => {\r\n      // the header menu's popup is a React portal: its events still bubble through the REACT tree\r\n      // (not the DOM tree) up to this handler, so a click on e.g. \"Hide column\" would otherwise also\r\n      // fire the column-select gesture here — bail out for any event actually targeting the popup.\r\n      if ((event.target as HTMLElement).closest(gridAttrSelector(\"headerMenuPopup\"))) return;\r\n      // 'sort' mode reassigns the plain click to sorting (handleClick below); the column-select\r\n      // gesture (and its press+drag range-select) only applies in the default 'select' mode.\r\n      if (headerClickBehavior === \"select\") onSelect(index, event);\r\n      if (reorder.enabled) reorder.onPointerDown(column.id, event);\r\n    },\r\n    [headerClickBehavior, onSelect, index, reorder, column.id],\r\n  );\r\n\r\n  const handleClick = useCallback(\r\n    (event: ReactMouseEvent<HTMLElement>) => {\r\n      if ((event.target as HTMLElement).closest(gridAttrSelector(\"headerMenuPopup\"))) return;\r\n      if (headerClickBehavior !== \"sort\" || column.sortable === false) return;\r\n      onSort(column.id, event.shiftKey);\r\n    },\r\n    [headerClickBehavior, column.sortable, onSort, column.id],\r\n  );\r\n\r\n  return (\r\n    <div\r\n      ref={cellRef}\r\n      role=\"columnheader\"\r\n      aria-colindex={index + 1}\r\n      // sort state is announced to AT in every click behavior — only the visual arrow below is click-gated\r\n      aria-sort={column.sortable !== false ? ariaSortFor(column.id, sortState) : undefined}\r\n      data-column-id={column.id}\r\n      data-pinned={pinned || undefined}\r\n      data-dragging={isDragging || undefined}\r\n      className={cn(\r\n        \"group relative flex items-center border-b border-border bg-muted px-2 font-medium select-none\",\r\n        COLUMN_BORDER,\r\n        isDragging && \"opacity-50\",\r\n        column.headerClassName,\r\n      )}\r\n      style={style}\r\n      onPointerDown={handleLabelPointerDown}\r\n      onClick={handleClick}\r\n      onDoubleClick={(event) => event.stopPropagation()}\r\n    >\r\n      {typeof column.header === \"string\" ? <span className=\"truncate\">{column.header}</span> : column.header}\r\n      {/* A custom (ReactNode) header owns its display, so the built-in arrow is only appended for string headers;\r\n          embed DataGridSortIndicator inside a custom header when you still want it. */}\r\n      {headerClickBehavior === \"sort\" && column.sortable !== false && typeof column.header === \"string\" && (\r\n        <DataGridSortIndicator columnId={column.id} sortState={sortState} />\r\n      )}\r\n      {renderHeaderMenu &&\r\n        renderHeaderMenu({\r\n          column,\r\n          index,\r\n          trigger: (\r\n            <button\r\n              type=\"button\"\r\n              data-grid-header-menu-trigger=\"\"\r\n              aria-label={labels.contextMenu.columnMenuAriaLabel(headerLabelText(column))}\r\n              className=\"ms-auto shrink-0 rounded p-0.5 text-muted-foreground opacity-0 outline-none transition-opacity hover:bg-accent hover:text-accent-foreground focus:opacity-100 group-hover:opacity-100\"\r\n              onPointerDown={(event) => event.stopPropagation()}\r\n              onClick={(event) => event.stopPropagation()}\r\n            >\r\n              <ChevronDown className=\"size-3.5\" />\r\n            </button>\r\n          ),\r\n        })}\r\n      {columnResizable && (\r\n        <div\r\n          data-grid-resize-handle=\"\"\r\n          data-resizing={resize.isResizing || undefined}\r\n          role=\"presentation\"\r\n          className=\"absolute end-0 top-0 z-10 h-full w-2 cursor-col-resize touch-none after:absolute after:inset-y-1.5 after:end-[2px] after:w-[3px] after:rounded-full after:bg-primary after:opacity-0 after:transition-opacity hover:after:opacity-60 data-[resizing]:after:opacity-100\"\r\n          style={{ touchAction: \"none\" }}\r\n          onPointerDown={resize.onPointerDown}\r\n          onDoubleClick={resize.onDoubleClick}\r\n          onClick={(event) => event.stopPropagation()}\r\n        />\r\n      )}\r\n    </div>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/header-cell.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/header.tsx",
      "content": "\"use client\";\r\n\r\nimport { useCallback, useMemo, type CSSProperties, type ReactNode } from \"react\";\r\nimport { useDataGridRootContext } from \"./layout-context\";\r\nimport {\r\n  useDataGridActions,\r\n  useDataGridColumnFeatureFlags,\r\n  useDataGridHeaderClickBehavior,\r\n  useDataGridRowMarkers,\r\n  useDataGridSortState,\r\n} from \"./store\";\r\nimport { DataGridMarkerHeader } from \"./rows/marker-header\";\r\nimport { DataGridHeaderCell } from \"./header-cell\";\r\nimport { useColumnReorder } from \"./columns/use-column-reorder\";\r\nimport { pinnedInsetStyle } from \"./columns/pinned-inset-style\";\r\nimport { GRID_LAYER } from \"./layers\";\r\nimport { isInlineStartHalf } from \"./windowing/direction\";\r\n\r\n/**\r\n * Header layer: absolutely positioned inside the sticky Viewport, counter-translated\r\n * horizontally by the live scroll var so it tracks the canvas 1:1 without ever moving via native\r\n * scroll. Renders only the windowed column set (same window the body consumes).\r\n *\r\n * Data-column grid placement is `index + markerColOffset` (1-based `gridColumnStart`): the marker\r\n * column (when present) occupies track 1, so every data column shifts one track right. `index`\r\n * itself, `aria-colindex`, and `aria-colcount` (set on the grid root) are all untouched by this —\r\n * the marker carries no `aria-colindex` at all and sits outside the data header's a11y column\r\n * count (chosen over shifting every data cell's aria-colindex by one).\r\n */\r\nexport function DataGridHeader(): ReactNode {\r\n  const { scrollRef, windowedColumns, template, layout, headerHeight, interaction, direction, renderHeaderMenu } = useDataGridRootContext();\r\n  const rowMarkers = useDataGridRowMarkers();\r\n  const actions = useDataGridActions();\r\n  const sortState = useDataGridSortState();\r\n  const headerClickBehavior = useDataGridHeaderClickBehavior();\r\n  const { enableColumnResize, enableColumnReorder } = useDataGridColumnFeatureFlags();\r\n  const markerColOffset = layout.markerWidth > 0 ? 2 : 1;\r\n\r\n  /** Resolves the header cell (and before/after half) under a client point, for the reorder drag's live drop target. */\r\n  const hitTestHeader = useCallback(\r\n    (clientX: number, clientY: number) => {\r\n      const el = document.elementFromPoint(clientX, clientY)?.closest<HTMLElement>(\"[role='columnheader'][data-column-id]\");\r\n      if (!el) return null;\r\n      const rect = el.getBoundingClientRect();\r\n      // guaranteed present: the closest() selector above only matches elements with [data-column-id]\r\n      const columnId = el.dataset[\"columnId\"]!;\r\n      // \"before\"/\"after\" stay logical (reading order): the inline-START half is \"before\" in both\r\n      // directions, which under RTL is the header's physical RIGHT half.\r\n      const position: \"before\" | \"after\" = isInlineStartHalf(clientX, rect, direction) ? \"before\" : \"after\";\r\n      return { columnId, position };\r\n    },\r\n    [direction],\r\n  );\r\n\r\n  const isColumnReorderable = useCallback(\r\n    (columnId: string) => windowedColumns.some(({ column }) => column.id === columnId && column.reorderable !== false),\r\n    [windowedColumns],\r\n  );\r\n\r\n  const { onHeaderDragPointerDown, dragState } = useColumnReorder({\r\n    enabled: enableColumnReorder,\r\n    isColumnReorderable,\r\n    hitTestHeader,\r\n    onReorder: actions.setColumnOrder,\r\n    onArm: interaction.cancelColumnSelectDrag,\r\n  });\r\n\r\n  /**\r\n   * Single drop-indicator line at the reorder drag's current boundary. Grid-placed on the boundary\r\n   * column's own track: an unpinned column gets raw track coords (the header layer already\r\n   * translates by -scrollLeft), a pinned boundary column reuses the exact pinned-cell offset,\r\n   * so the line lands at the cell's rendered edge either way.\r\n   */\r\n  const dropIndicator = useMemo<{ index: number; style: CSSProperties } | null>(() => {\r\n    if (!dragState?.overId) return null;\r\n    const over = windowedColumns.find(({ column }) => column.id === dragState.overId);\r\n    if (!over) return null;\r\n    const { column, index } = over;\r\n    const width = layout.widths[index];\r\n    const pinStyle = pinnedInsetStyle(column.pin, index);\r\n    // unpinned: the div is grid-placed on the boundary column's own track (raw track coords, like\r\n    // any other header cell), so its local left edge is 0 — \"after\" just adds the column's width.\r\n    // pinned: pinnedInsetStyle's insetInlineStart already lands this div at the pinned cell's own\r\n    // rendered left edge (same track, same formula) — \"after\" shifts that by the column's width too.\r\n    const insetInlineStart =\r\n      dragState.position === \"before\"\r\n        ? (pinStyle.insetInlineStart ?? 0)\r\n        : pinStyle.insetInlineStart\r\n          ? `calc(${pinStyle.insetInlineStart} + ${width}px)`\r\n          : width;\r\n    const style: CSSProperties = {\r\n      gridRowStart: 1,\r\n      gridColumnStart: index + markerColOffset,\r\n      position: \"relative\",\r\n      insetBlockStart: 0,\r\n      insetInlineStart,\r\n      height: \"100%\",\r\n      width: 2,\r\n      zIndex: GRID_LAYER.header,\r\n      pointerEvents: \"none\",\r\n      backgroundColor: \"var(--color-primary)\",\r\n    };\r\n    return { index, style };\r\n  }, [dragState, windowedColumns, layout.widths, markerColOffset]);\r\n\r\n  const layerStyle: CSSProperties = {\r\n    position: \"absolute\",\r\n    insetBlockStart: 0,\r\n    insetInlineStart: 0,\r\n    display: \"grid\",\r\n    gridTemplateColumns: template,\r\n    gridAutoRows: headerHeight,\r\n    // stacking context (transform) painted after the rows canvas in DOM order; without an explicit\r\n    // z-index the canvas's own stacking context wins paint order and buries the header on scroll.\r\n    zIndex: GRID_LAYER.pinnedHeader,\r\n    // header never moves vertically — only the canvas does — so only the horizontal term applies here.\r\n    // --grid-dir signs it for RTL (see body.tsx's canvas transform for the full reasoning).\r\n    transform: \"translate3d(calc(var(--grid-dir, -1) * var(--grid-scroll-left, 0px)), 0, 0)\",\r\n  };\r\n  return (\r\n    <div role=\"row\" style={layerStyle} data-grid-header-layer=\"\">\r\n      {layout.markerWidth > 0 && <DataGridMarkerHeader mode={rowMarkers} headerHeight={headerHeight} />}\r\n      {windowedColumns.map(({ column, index }) => (\r\n        <DataGridHeaderCell\r\n          key={column.id}\r\n          column={column}\r\n          index={index}\r\n          width={layout.widths[index]!} // index is a real index into columns; layout.widths is columns.map(...), same length\r\n          gridColOffset={markerColOffset}\r\n          headerClickBehavior={headerClickBehavior}\r\n          sortState={sortState}\r\n          resize={{\r\n            enabled: enableColumnResize,\r\n            scrollRootRef: scrollRef,\r\n            setColumnWidth: actions.setColumnWidth,\r\n            commitColumnWidth: actions.commitColumnWidth,\r\n            direction,\r\n          }}\r\n          reorder={{\r\n            enabled: enableColumnReorder,\r\n            state: dragState,\r\n            onPointerDown: onHeaderDragPointerDown,\r\n          }}\r\n          onSelect={interaction.onHeaderPointerDown}\r\n          onSort={actions.toggleSort}\r\n          renderHeaderMenu={renderHeaderMenu}\r\n        />\r\n      ))}\r\n      {dragState && dropIndicator && <div data-grid-drop-indicator=\"\" aria-hidden=\"true\" style={dropIndicator.style} />}\r\n    </div>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/header.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/interaction/history.ts",
      "content": "import type { DataChange, DataOp } from \"../types\";\r\n\r\n/** Inverts a single op: update swaps row/prev, insert becomes delete, delete becomes insert. */\r\nfunction invertOp<TData>(op: DataOp<TData>): DataOp<TData> {\r\n  switch (op.type) {\r\n    case \"update\":\r\n      return {\r\n        type: \"update\",\r\n        rowId: op.rowId,\r\n        row: op.prev,\r\n        prev: op.row,\r\n        cells: op.cells?.map((cell) => ({\r\n          columnId: cell.columnId,\r\n          value: cell.prev,\r\n          prev: cell.value,\r\n        })),\r\n      };\r\n    case \"insert\":\r\n      return { type: \"delete\", rowId: op.rowId, row: op.row, index: op.index };\r\n    case \"delete\":\r\n      return { type: \"insert\", rowId: op.rowId, row: op.row, index: op.index };\r\n    // the row sits at `op.to` after the move, so undoing swaps the two snapshot positions\r\n    case \"move\":\r\n      return { type: \"move\", rowId: op.rowId, row: op.row, from: op.to, to: op.from };\r\n  }\r\n}\r\n\r\n/**\r\n * Produces the change that undoes `change`: each op is inverted and the op\r\n * order is reversed (so a batch unwinds in the correct dependency order).\r\n */\r\nexport function invertChange<TData>(change: DataChange<TData>): DataChange<TData> {\r\n  return {\r\n    ops: [...change.ops].reverse().map(invertOp),\r\n    source: \"history\",\r\n    label: change.label,\r\n  };\r\n}\r\n\r\n/**\r\n * Applies a change to `data` immutably in O(n + k log(n + k)), returning a new array.\r\n * Rows are located by id (never by the op's stored index, which is only a\r\n * hint for where an insert should land). If an id referenced by an\r\n * update/delete op is no longer present (row was deleted by a later change),\r\n * that op is skipped silently rather than throwing.\r\n *\r\n * `index` convention: op indices are snapshot positions — each op's index\r\n * is that row's position in the array as it stood before the whole batch\r\n * was applied (the natural output of a delete-range/insert-range emitter),\r\n * not a running \"log-time\" position updated op-by-op. update/delete ops\r\n * are id-keyed so their index is informational only; insert ops (including\r\n * ones produced by inverting a delete batch) are therefore applied in\r\n * ascending-index order regardless of the order they appear in `ops`, so a\r\n * multi-row batch round-trips through invertChange correctly.\r\n */\r\nexport function applyChange<TData>(\r\n  data: readonly TData[],\r\n  change: DataChange<TData>,\r\n  getRowId: (row: TData, index: number) => string,\r\n): TData[] {\r\n  const indexById = new Map<string, number>();\r\n  const idByIndex = new Array<string>(data.length);\r\n  let hasDuplicateId = false;\r\n  for (let i = 0; i < data.length; i++) {\r\n    const id = getRowId(data[i]!, i);\r\n    idByIndex[i] = id;\r\n    if (indexById.has(id)) hasDuplicateId = true;\r\n    else indexById.set(id, i);\r\n  }\r\n\r\n  const deleteIndices = new Set<number>();\r\n  const inserts: Extract<DataOp<TData>, { type: \"insert\" }>[] = [];\r\n  let postDelete: TData[];\r\n\r\n  if (!hasDuplicateId) {\r\n    const updateById = new Map<string, TData>();\r\n    for (const op of change.ops) {\r\n      if (op.type === \"insert\") {\r\n        inserts.push(op);\r\n        continue;\r\n      }\r\n      const idx = indexById.get(op.rowId);\r\n      if (idx === undefined) continue;\r\n      if (op.type === \"update\") updateById.set(op.rowId, op.row);\r\n      else if (op.type === \"delete\") deleteIndices.add(idx);\r\n      // a move is a delete + insert of the same row: the insert lands at the row's post-move slot\r\n      else {\r\n        deleteIndices.add(idx);\r\n        inserts.push({ type: \"insert\", rowId: op.rowId, row: op.row, index: op.to });\r\n      }\r\n    }\r\n\r\n    postDelete = [];\r\n    for (let i = 0; i < data.length; i++) {\r\n      if (deleteIndices.has(i)) continue;\r\n      const id = idByIndex[i]!;\r\n      const updated = updateById.get(id);\r\n      postDelete.push(updated !== undefined ? updated : data[i]!);\r\n    }\r\n  } else {\r\n    // Duplicate row ids are invalid, but this path preserves the legacy first-occurrence scan.\r\n    const indicesById = new Map<string, number[]>();\r\n    for (let i = 0; i < data.length; i++) {\r\n      const id = idByIndex[i]!;\r\n      const indices = indicesById.get(id);\r\n      if (indices) indices.push(i);\r\n      else indicesById.set(id, [i]);\r\n    }\r\n\r\n    const opsById = new Map<string, DataOp<TData>[]>();\r\n    for (const op of change.ops) {\r\n      if (op.type === \"insert\") {\r\n        inserts.push(op);\r\n        continue;\r\n      }\r\n      const ops = opsById.get(op.rowId);\r\n      if (ops) ops.push(op);\r\n      else opsById.set(op.rowId, [op]);\r\n    }\r\n\r\n    const updateByIndex = new Map<number, TData>();\r\n    for (const idOps of opsById.values()) {\r\n      const indices = indicesById.get(idOps[0]!.rowId);\r\n      if (!indices) continue;\r\n      let next = 0;\r\n      for (const op of idOps) {\r\n        if (next >= indices.length) break;\r\n        const target = indices[next]!;\r\n        if (op.type === \"update\") {\r\n          updateByIndex.set(target, op.row);\r\n        } else {\r\n          deleteIndices.add(target);\r\n          if (op.type === \"move\") inserts.push({ type: \"insert\", rowId: op.rowId, row: op.row, index: op.to });\r\n          next++;\r\n        }\r\n      }\r\n    }\r\n\r\n    postDelete = [];\r\n    for (let i = 0; i < data.length; i++) {\r\n      if (deleteIndices.has(i)) continue;\r\n      postDelete.push(updateByIndex.get(i) ?? data[i]!);\r\n    }\r\n  }\r\n\r\n  const sorted = [...inserts].sort((a, b) => a.index - b.index);\r\n  const total = postDelete.length + sorted.length;\r\n  if (total === 0) return [];\r\n\r\n  // Fenwick slot placement reproduces the legacy clamped insert order.\r\n  const bit = new Int32Array(total + 1);\r\n  for (let i = 1; i <= total; i++) bit[i] = i & -i;\r\n  const insertAt = new Int32Array(total).fill(-1);\r\n  const addFree = (pos: number) => {\r\n    for (let i = pos + 1; i <= total; i += i & -i) bit[i]!--;\r\n  };\r\n  const findFreeSlot = (rank: number): number => {\r\n    let idx = 0;\r\n    let target = rank;\r\n    let mask = 1 << Math.floor(Math.log2(total));\r\n    while (mask !== 0) {\r\n      const next = idx + mask;\r\n      if (next <= total && bit[next]! < target) {\r\n        idx = next;\r\n        target -= bit[next]!;\r\n      }\r\n      mask >>= 1;\r\n    }\r\n    return idx;\r\n  };\r\n\r\n  for (let q = sorted.length - 1; q >= 0; q--) {\r\n    const at = Math.min(sorted[q]!.index, postDelete.length + q);\r\n    const pos = findFreeSlot(at + 1);\r\n    insertAt[pos] = q;\r\n    addFree(pos);\r\n  }\r\n\r\n  const out = new Array<TData>(total);\r\n  let original = 0;\r\n  for (let pos = 0; pos < total; pos++) {\r\n    const q = insertAt[pos]!;\r\n    out[pos] = q === -1 ? postDelete[original++]! : sorted[q]!.row;\r\n  }\r\n  return out;\r\n}\r\n\r\n/** Options for {@link createHistory}. */\r\nexport type HistoryOptions = {\r\n  /** Maximum number of entries kept on the undo stack; oldest entries are dropped past this. */\r\n  capacity?: number;\r\n};\r\n\r\n/**\r\n * Op-based undo/redo stack, UI-free. Consumers call `push` after each\r\n * applied gesture, and `undo`/`redo` to get the change to apply back to data.\r\n */\r\nexport type History<TData> = {\r\n  /** Records a change; discards any redo branch. Changes with source \"history\" are ignored. */\r\n  push(change: DataChange<TData>): void;\r\n  /** Returns the inverted top-of-undo-stack change to apply, or null if nothing to undo. */\r\n  undo(): DataChange<TData> | null;\r\n  /** Returns the original change, re-tagged source \"history\", to re-apply; or null if nothing to redo. */\r\n  redo(): DataChange<TData> | null;\r\n  /** Whether `undo()` would return a change. */\r\n  readonly canUndo: boolean;\r\n  /** Whether `redo()` would return a change. */\r\n  readonly canRedo: boolean;\r\n  /** Clears both stacks. */\r\n  clear(): void;\r\n  /** Number of entries on the undo stack. */\r\n  readonly size: number;\r\n};\r\n\r\n/** Creates an id-keyed undo/redo history. */\r\nexport function createHistory<TData>(opts: HistoryOptions = {}): History<TData> {\r\n  const capacity = opts.capacity ?? 100;\r\n  let undoStack: DataChange<TData>[] = [];\r\n  let redoStack: DataChange<TData>[] = [];\r\n\r\n  return {\r\n    push(change) {\r\n      if (change.source === \"history\") return;\r\n      undoStack.push(change);\r\n      if (undoStack.length > capacity) undoStack.shift();\r\n      redoStack = [];\r\n    },\r\n    undo() {\r\n      const change = undoStack.pop();\r\n      if (!change) return null;\r\n      redoStack.push(change);\r\n      return invertChange(change);\r\n    },\r\n    redo() {\r\n      const change = redoStack.pop();\r\n      if (!change) return null;\r\n      undoStack.push(change);\r\n      return { ...change, source: \"history\" };\r\n    },\r\n    get canUndo() {\r\n      return undoStack.length > 0;\r\n    },\r\n    get canRedo() {\r\n      return redoStack.length > 0;\r\n    },\r\n    clear() {\r\n      undoStack = [];\r\n      redoStack = [];\r\n    },\r\n    get size() {\r\n      return undoStack.length;\r\n    },\r\n  };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/interaction/history.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/interaction/index.ts",
      "content": "/** Domain barrel — interaction hooks (grid pointer/keyboard, commit guard, seed focus, container) + history. */\r\nexport { createHistory, applyChange, invertChange, type History, type HistoryOptions } from \"./history\";\r\nexport { useCommitGuard } from \"./use-commit-guard\";\r\nexport { useSeedFocus } from \"./use-seed-focus\";\r\nexport { useDataGridContainer } from \"./use-data-grid-container\";\r\nexport {\r\n  useGridInteraction,\r\n  pointerToCoord,\r\n  jumpToDataBoundary,\r\n  type InteractionLayout,\r\n  type UseGridInteractionOptions,\r\n  type JumpDirection,\r\n  type GridInteractionHandlers,\r\n} from \"./use-grid-interaction\";\r\n",
      "type": "registry:component",
      "target": "components/data-grid/interaction/index.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/interaction/use-async-validate.ts",
      "content": "import { useEffect, useRef, useState } from \"react\";\r\nimport type { AnyColumnDef, DataGridActions } from \"../store\";\r\nimport { formatIssues, isStandardSchema } from \"../validation/validate-cell\";\r\n\r\n/**\r\n * Intercepts a cell-type editor's `commit(movement)` to support an async Standard Schema\r\n * `validate` — the ONLY place this repo awaits validation (spec: \"async branch lives at the\r\n * editor-commit layer, NOT inside a store action\"). `commitCellEdit`/`computeCommit` stay fully\r\n * synchronous; a schema whose `~standard.validate` resolves synchronously (the function form, or a\r\n * sync schema) is forwarded to `commitCellEdit` immediately with ZERO behavior change.\r\n *\r\n * When the schema returns a Promise, editing STAYS OPEN: `pending` flips true (the editor sets its\r\n * input `readOnly` — minimal, reduced-motion-safe treatment that still allows Escape to cancel),\r\n * and only on resolution does this either call\r\n * `commitCellEdit(result.value, movement)` (success — the schema's own transformed value, per spec)\r\n * or, on issues, `actions.setEditingError(message)` (blocking default — editing continues, same as\r\n * the sync rejection UX, and the store bumps `editingRejectionCount` — the editors' commit-guard\r\n * re-arm nonce, single source of truth for sync AND async rejections; the editors re-arm off it,\r\n * never off \"pending cleared\", which also happens on Escape/cancel right before unmount) or\r\n * `commitCellEdit(value, movement, message)` (`onInvalid: \"warn\"` — the raw value commits and the\r\n * cell is flagged in `cellErrors`).\r\n *\r\n * Race guard: a generation counter bumped on every `commit()` call AND on unmount/cancel. A\r\n * resolution whose captured generation no longer matches current is dropped silently — covers\r\n * Escape/cancel, a newer commit superseding an in-flight one, and unmount.\r\n */\r\nexport function useAsyncValidate(actions: DataGridActions, column: AnyColumnDef) {\r\n  const [pending, setPending] = useState(false);\r\n  const generationRef = useRef(0);\r\n\r\n  useEffect(() => {\r\n    return () => {\r\n      generationRef.current += 1;\r\n    };\r\n  }, []);\r\n\r\n  function commit(value: unknown, movement?: { dx: number; dy: number }): void {\r\n    const validate = column.validate;\r\n    if (!isStandardSchema(validate)) {\r\n      actions.commitCellEdit(value, movement);\r\n      return;\r\n    }\r\n\r\n    // Peek: a sync-resolving schema (including one that never returns a Promise at all) takes the\r\n    // exact same path as today — computeCommit re-runs it, cheap and correct, see validate-cell.ts.\r\n    const probe = validate[\"~standard\"].validate(value);\r\n    if (!(probe instanceof Promise)) {\r\n      actions.commitCellEdit(value, movement);\r\n      return;\r\n    }\r\n\r\n    const generation = ++generationRef.current;\r\n    setPending(true);\r\n    probe.then((result) => {\r\n      if (generationRef.current !== generation) return; // stale: cancelled/superseded/unmounted\r\n      setPending(false);\r\n      if (result.issues) {\r\n        const message = formatIssues(result.issues);\r\n        // `onInvalid: \"warn\"`: commit the raw value and flag the cell — the awaited rejection is\r\n        // forwarded because computeCommit's sync re-run cannot see a schema Promise's issues\r\n        if (column.onInvalid === \"warn\") {\r\n          actions.commitCellEdit(value, movement, message);\r\n          return;\r\n        }\r\n        actions.setEditingError(message);\r\n        return;\r\n      }\r\n      actions.commitCellEdit(result.value, movement);\r\n    });\r\n  }\r\n\r\n  /** Escape/cancel while a validation is pending: invalidates it (its resolution is dropped) and clears `pending`. */\r\n  function cancelPending(): void {\r\n    generationRef.current += 1;\r\n    setPending(false);\r\n  }\r\n\r\n  return { commit, cancelPending, pending };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/interaction/use-async-validate.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/interaction/use-commit-guard.ts",
      "content": "import { useRef } from \"react\";\r\n\r\n/**\r\n * Guards a one-shot commit against StrictMode double-invoke and Enter-then-blur double-commit;\r\n * `tryCommit` returns false after the first call. `reset` re-arms it — used after an ASYNC\r\n * Standard Schema commit resolves with issues (editing stays open, see use-async-validate.ts):\r\n * without a reset, the guard would silently swallow every retry for the rest of that edit session.\r\n */\r\nexport function useCommitGuard() {\r\n  const firedRef = useRef(false);\r\n  return {\r\n    tryCommit: () => {\r\n      if (firedRef.current) return false;\r\n      firedRef.current = true;\r\n      return true;\r\n    },\r\n    reset: () => {\r\n      firedRef.current = false;\r\n    },\r\n  };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/interaction/use-commit-guard.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/interaction/use-data-grid-container.ts",
      "content": "\"use client\";\r\n\r\nimport type { RefObject } from \"react\";\r\nimport { useDataGridRootContext } from \"../layout-context\";\r\n\r\n/**\r\n * The grid's scroll container element, for add-ons that need to attach their own listeners (e.g.\r\n * the context-menu add-on's `contextmenu` handler) without reaching into `DataGridRoot`-internal\r\n * refs. Same element `useGridClipboard`/`useGridInteraction` attach to.\r\n */\r\nexport function useDataGridContainer(): RefObject<HTMLDivElement | null> {\r\n  return useDataGridRootContext().scrollRef;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/interaction/use-data-grid-container.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/interaction/use-grid-interaction.ts",
      "content": "\"use client\";\r\n\r\nimport {\r\n  useCallback,\r\n  useEffect,\r\n  useMemo,\r\n  useRef,\r\n  type KeyboardEvent as ReactKeyboardEvent,\r\n  type MouseEvent as ReactMouseEvent,\r\n  type PointerEvent as ReactPointerEvent,\r\n  type RefObject,\r\n} from \"react\";\r\nimport type { StoreApi } from \"zustand/vanilla\";\r\nimport type { CellCoord, GridAction } from \"../types\";\r\nimport { isMacPlatform, isPrintableKey, matchKeymap, type KeymapEvent } from \"../keyboard\";\r\nimport type { Keymap } from \"../types\";\r\nimport { getCellValue } from \"../columns/column-helpers\";\r\nimport {\r\n  getFocusCell,\r\n  useDataGridActions,\r\n  useDataGridStoreApi,\r\n  type DataGridActions,\r\n  type DataGridStoreState,\r\n} from \"../store\";\r\nimport {\r\n  AUTO_SCROLL_STEP,\r\n  AUTO_SCROLL_ZONE,\r\n  applyInlineScrollDelta,\r\n  inlineAutoScrollStep,\r\n  inlineStartX,\r\n  normalizeScrollLeft,\r\n  visualArrowKey,\r\n  type GridDirection,\r\n} from \"../windowing/direction\";\r\nimport { gridAttrSelector } from \"../data-attributes\";\r\nimport { getSelectedViewRows, isSelectionEmpty } from \"../selection\";\r\n\r\n/** Column/row layout the interaction hook needs to translate pointer px <-> view coords and to scroll a cell into view. */\r\nexport type InteractionLayout = {\r\n  /** Cumulative left edge (px) of each visible column's track, data-space index. */\r\n  trackLefts: readonly number[];\r\n  /** Cumulative right edge (px) of each visible column's track, data-space index. */\r\n  trackRights: readonly number[];\r\n  rowHeight: number;\r\n  /** Offset (px) from the scroll element's top to where data row 0 starts: the sticky header track PLUS any pinned-top row band. NOT the header track alone (that's the root context's `headerHeight`). */\r\n  dataRowTop: number;\r\n  /** Pinned-bottom row band height (px) — the effective viewport bottom is `clientHeight - pinnedBottomHeight`. 0 when there's no pinned-bottom band. */\r\n  pinnedBottomHeight: number;\r\n  /** Total pinned-left band width in px (cells must stay clear of this on the left). */\r\n  pinnedLeftWidth: number;\r\n  /** Total pinned-right band width in px (cells must stay clear of this on the right). */\r\n  pinnedRightWidth: number;\r\n  /** Per-column pin state, same order/index as trackLefts/trackRights — lets pointer math hit-test the static pinned bands instead of the scrolled content space. */\r\n  pins: readonly ((\"left\" | \"right\") | undefined)[];\r\n  /**\r\n   * Layout direction of the grid. Every physical pointer coordinate is converted to the\r\n   * inline-start-relative axis this layout's offsets are expressed in before any hit-test runs, so\r\n   * the math below is direction-agnostic. Defaults to `\"ltr\"` when a caller omits it.\r\n   */\r\n  direction?: GridDirection;\r\n};\r\n\r\n/** Options for {@link useGridInteraction}. */\r\nexport type UseGridInteractionOptions = {\r\n  scrollRef: RefObject<HTMLElement | null>;\r\n  layout: InteractionLayout;\r\n  keymap: Keymap;\r\n  readOnly?: boolean;\r\n  /** mod+D — wired to the `data-grid-fill` add-on's fillDown; absent (add-on not installed) it's a no-op. */\r\n  fillDown?: () => void;\r\n  /** mod+R — wired to the `data-grid-fill` add-on's fillRight; absent it's a no-op. */\r\n  fillRight?: () => void;\r\n  /** Escape — wired to the `data-grid-fill` add-on's cancelDrag, so it aborts an in-progress fill drag too; absent it's a no-op. */\r\n  cancelFillDrag?: () => void;\r\n};\r\n\r\n/**\r\n * Resolves the view-space column index under `inlineX` — the pointer's distance from the viewport's\r\n * INLINE-START edge (the left edge in LTR, the right edge in RTL), not a physical screen x. Pinned\r\n * columns render at a fixed inline offset independent of scrollLeft (see pinnedInsetStyle), so\r\n * they're hit-tested directly in that space; only the unpinned middle band needs the content-space\r\n * (scrollLeft-adjusted) `contentX`.\r\n *\r\n * Because both inputs are already inline-start-relative and every offset this compares against\r\n * (trackLefts/trackRights/pin widths) is too, this function has no direction awareness of its own —\r\n * pointerToCoord normalizes at the seam and everything here is shared by both directions.\r\n *\r\n * All interval tests are half-open (`>= start && < end`) so a pointer exactly on the boundary two\r\n * cells share always resolves to the later one, in both directions. RTL's fractional layout can\r\n * land a point precisely on such an edge where LTR would not, so this is what keeps the resolution\r\n * deterministic rather than a coin flip on sub-pixel rounding.\r\n */\r\nfunction columnAtX(\r\n  inlineX: number,\r\n  contentX: number,\r\n  trackLefts: readonly number[],\r\n  trackRights: readonly number[],\r\n  pins: readonly ((\"left\" | \"right\") | undefined)[],\r\n  pinnedLeftWidth: number,\r\n  viewportWidth: number,\r\n  pinnedRightWidth: number,\r\n): number {\r\n  if (inlineX < pinnedLeftWidth) {\r\n    // pinned-left band: inline-start offset equals the cumulative pinned-left width prefix,\r\n    // which is numerically the same as trackLefts/trackRights restricted to that prefix.\r\n    // trackLefts/trackRights/pins are same-length parallel arrays (built together by the caller)\r\n    for (let i = 0; i < trackLefts.length; i++) {\r\n      if (pins[i] === \"left\" && inlineX >= trackLefts[i]! && inlineX < trackRights[i]!) return i;\r\n    }\r\n  } else if (inlineX >= viewportWidth - pinnedRightWidth) {\r\n    // pinned-right band: mirror the same static-position reasoning from the inline-END edge.\r\n    const fromRight = viewportWidth - inlineX;\r\n    let acc = 0;\r\n    for (let i = trackLefts.length - 1; i >= 0; i--) {\r\n      if (pins[i] !== \"right\") continue;\r\n      const width = trackRights[i]! - trackLefts[i]!;\r\n      if (fromRight >= acc && fromRight < acc + width) return i;\r\n      acc += width;\r\n    }\r\n  }\r\n  for (let i = 0; i < trackLefts.length; i++) {\r\n    if (pins[i]) continue;\r\n    if (contentX >= trackLefts[i]! && contentX < trackRights[i]!) return i;\r\n  }\r\n  if (trackLefts.length === 0) return 0;\r\n  return contentX < trackLefts[0]! ? 0 : trackLefts.length - 1;\r\n}\r\n\r\n/** Resolves the view-space row index at content-space `y` (already offset by scrollTop, header-relative). */\r\nfunction rowAtY(y: number, rowHeight: number, rowCount: number): number {\r\n  const row = Math.floor(y / rowHeight);\r\n  return Math.max(0, Math.min(row, Math.max(0, rowCount - 1)));\r\n}\r\n\r\n/**\r\n * Maps a pointer event's viewport-relative position to a view-space cell coord using the current\r\n * scroll offsets. Exported for direct unit testing of the pinned-column hit-testing math; not part\r\n * of the public hook surface.\r\n */\r\nexport function pointerToCoord(\r\n  clientX: number,\r\n  clientY: number,\r\n  scrollElement: HTMLElement,\r\n  layout: InteractionLayout,\r\n  rowCount: number,\r\n): CellCoord {\r\n  const rect = scrollElement.getBoundingClientRect();\r\n  // The direction seam for all pointer input: past these two lines every x is inline-start-relative\r\n  // and matches the space trackLefts/trackRights/pin widths are already expressed in, so no\r\n  // hit-testing below this point knows or asks about direction.\r\n  const inlineX = inlineStartX(clientX, rect, layout.direction ?? \"ltr\");\r\n  const contentX = inlineX + normalizeScrollLeft(scrollElement.scrollLeft);\r\n  const y = clientY - rect.top - layout.dataRowTop + scrollElement.scrollTop;\r\n  const col = columnAtX(\r\n    inlineX,\r\n    contentX,\r\n    layout.trackLefts,\r\n    layout.trackRights,\r\n    layout.pins,\r\n    layout.pinnedLeftWidth,\r\n    scrollElement.clientWidth,\r\n    layout.pinnedRightWidth,\r\n  );\r\n  const row = rowAtY(y, layout.rowHeight, rowCount);\r\n  return { col, row };\r\n}\r\n\r\n/**\r\n * Imperatively scrolls the container so `coord` is visible (nearest behavior), honoring the\r\n * pinned-left/right bands. Never uses `scrollIntoView` — cells live in the transformed\r\n * sticky-viewport layer, so only direct `scrollTop`/`scrollLeft` writes are architecturally safe.\r\n */\r\nfunction scrollCellIntoView(scrollElement: HTMLElement, coord: CellCoord, layout: InteractionLayout): void {\r\n  const { trackLefts, trackRights, rowHeight, dataRowTop, pinnedBottomHeight, pinnedLeftWidth, pinnedRightWidth } = layout;\r\n  const cellTop = coord.row * rowHeight;\r\n  const cellBottom = cellTop + rowHeight;\r\n  const viewTop = scrollElement.scrollTop;\r\n  const viewBottom = viewTop + scrollElement.clientHeight - dataRowTop - pinnedBottomHeight;\r\n\r\n  let nextScrollTop: number | null = null;\r\n  if (cellTop < viewTop) nextScrollTop = cellTop;\r\n  else if (cellBottom > viewBottom) nextScrollTop = cellBottom - (scrollElement.clientHeight - dataRowTop - pinnedBottomHeight);\r\n\r\n  // Inline axis in normalized (positive, inline-start-relative) space, the same space trackLefts\r\n  // and the pin widths live in — so the visibility test itself is shared by both directions.\r\n  const scrollLeft = normalizeScrollLeft(scrollElement.scrollLeft);\r\n  const cellLeft = trackLefts[coord.col] ?? 0;\r\n  const cellRight = trackRights[coord.col] ?? cellLeft;\r\n  const viewLeft = scrollLeft + pinnedLeftWidth;\r\n  const viewRight = scrollLeft + scrollElement.clientWidth - pinnedRightWidth;\r\n\r\n  let nextScrollLeft: number | null = null;\r\n  if (cellLeft < viewLeft) nextScrollLeft = cellLeft - pinnedLeftWidth;\r\n  else if (cellRight > viewRight) nextScrollLeft = cellRight - (scrollElement.clientWidth - pinnedRightWidth);\r\n\r\n  if (nextScrollTop !== null) scrollElement.scrollTop = Math.max(0, nextScrollTop);\r\n  if (nextScrollLeft !== null) {\r\n    // Applied as a RELATIVE inline delta rather than an absolute write: `scrollLeft` means\r\n    // different things in each direction (RTL runs negative), and a relative move is correct in\r\n    // both without this function ever reconstructing the container's own convention. The\r\n    // Math.max(0, …) floor the absolute write needed would have pinned RTL scrolling to zero.\r\n    applyInlineScrollDelta(scrollElement, Math.max(0, nextScrollLeft) - scrollLeft, layout.direction ?? \"ltr\");\r\n  }\r\n}\r\n\r\n/**\r\n * Executes one already-resolved keymap action against the store — the dispatch shared by the\r\n * in-grid keydown handler and the global-shortcut window layer, so a globally registered action\r\n * behaves EXACTLY like its in-grid binding (same scrolling, same store-side guards: `insertRowBelow`\r\n * without `createRow` no-ops, `fillDown` without the fill add-on no-ops, ...). `preventDefault` is\r\n * the caller's event handler, invoked only for the actions that would preventDefault in-grid.\r\n * Not part of the public hook surface.\r\n */\r\nexport function dispatchGridAction(args: {\r\n  action: GridAction;\r\n  actions: DataGridActions;\r\n  storeApi: StoreApi<DataGridStoreState>;\r\n  scrollRef: RefObject<HTMLElement | null>;\r\n  scrollCellIntoView: (coord: CellCoord) => void;\r\n  /** The grid's resolved data-row height (px) — `pageUp`/`pageDown`'s visible-row math. */\r\n  rowHeight: number;\r\n  readOnly?: boolean;\r\n  /** The printable seed char for `editReplace`, when the trigger was a printable key; undefined otherwise. */\r\n  triggerKey?: string;\r\n  preventDefault: () => void;\r\n  fillDown?: () => void;\r\n  fillRight?: () => void;\r\n  cancelFillDrag?: () => void;\r\n}): void {\r\n  const { action, actions, storeApi, scrollRef, scrollCellIntoView, rowHeight, readOnly, triggerKey, preventDefault, fillDown, fillRight, cancelFillDrag } = args;\r\n  const state = storeApi.getState();\r\n  const rowCount = state.viewIndex.length;\r\n  const colCount = state.visibleColumns.length;\r\n\r\n  const moveAndScroll = (d: { dx: number; dy: number }, opts?: { extend?: boolean; retain?: boolean }) => {\r\n    actions._moveActiveCell(d, opts);\r\n    // extend keeps activeCell pinned at the anchor — scroll the growing selection's edge instead.\r\n    const next = getFocusCell(storeApi.getState());\r\n    if (next) scrollCellIntoView(next);\r\n  };\r\n\r\n  const jumpAndScroll = (direction: JumpDirection, opts?: { extend?: boolean }) => {\r\n    const active = state.activeCell ?? { col: 0, row: 0 };\r\n    const target = jumpToDataBoundary(state, active, direction);\r\n    if (opts?.extend) actions.extendTo(target);\r\n    else actions.selectCell(target);\r\n    scrollCellIntoView(target);\r\n  };\r\n\r\n  switch (action) {\r\n    case \"moveUp\":\r\n    case \"moveDown\":\r\n    case \"moveLeft\":\r\n    case \"moveRight\": {\r\n      preventDefault();\r\n      moveAndScroll(MOVE_DELTA[action]);\r\n      break;\r\n    }\r\n    case \"retainMoveUp\":\r\n    case \"retainMoveDown\":\r\n    case \"retainMoveLeft\":\r\n    case \"retainMoveRight\": {\r\n      preventDefault();\r\n      moveAndScroll(MOVE_DELTA[action], { retain: true });\r\n      break;\r\n    }\r\n    case \"scrollActiveIntoView\": {\r\n      preventDefault();\r\n      const active = storeApi.getState().activeCell;\r\n      if (active) scrollCellIntoView(active);\r\n      break;\r\n    }\r\n    // Outside an active edit (the caller guards that), Tab/Shift+Tab behave as plain\r\n    // right/left navigation (glide-behavior-spec.md §2); only the editor's own\r\n    // onKeyDown gives them their commit-and-move contract while editing.\r\n    case \"commitRight\":\r\n    case \"commitLeft\": {\r\n      preventDefault();\r\n      moveAndScroll({ dx: action === \"commitRight\" ? 1 : -1, dy: 0 });\r\n      break;\r\n    }\r\n    case \"extendUp\":\r\n    case \"extendDown\":\r\n    case \"extendLeft\":\r\n    case \"extendRight\": {\r\n      preventDefault();\r\n      moveAndScroll(MOVE_DELTA[action], { extend: true });\r\n      break;\r\n    }\r\n    case \"jumpUp\":\r\n    case \"jumpDown\":\r\n    case \"jumpLeft\":\r\n    case \"jumpRight\": {\r\n      preventDefault();\r\n      jumpAndScroll(JUMP_DIRECTION[action]);\r\n      break;\r\n    }\r\n    case \"extendJumpUp\":\r\n    case \"extendJumpDown\":\r\n    case \"extendJumpLeft\":\r\n    case \"extendJumpRight\": {\r\n      preventDefault();\r\n      jumpAndScroll(JUMP_DIRECTION[action], { extend: true });\r\n      break;\r\n    }\r\n    case \"moveRowStart\": {\r\n      preventDefault();\r\n      const active = state.activeCell ?? { col: 0, row: 0 };\r\n      const target = { col: 0, row: active.row };\r\n      actions.selectCell(target);\r\n      scrollCellIntoView(target);\r\n      break;\r\n    }\r\n    case \"moveRowEnd\": {\r\n      preventDefault();\r\n      const active = state.activeCell ?? { col: 0, row: 0 };\r\n      const target = { col: Math.max(0, colCount - 1), row: active.row };\r\n      actions.selectCell(target);\r\n      scrollCellIntoView(target);\r\n      break;\r\n    }\r\n    case \"moveFirstCell\": {\r\n      preventDefault();\r\n      const target = { col: 0, row: 0 };\r\n      actions.selectCell(target);\r\n      scrollCellIntoView(target);\r\n      break;\r\n    }\r\n    case \"moveLastCell\": {\r\n      preventDefault();\r\n      const target = { col: Math.max(0, colCount - 1), row: Math.max(0, rowCount - 1) };\r\n      actions.selectCell(target);\r\n      scrollCellIntoView(target);\r\n      break;\r\n    }\r\n    case \"extendFirstCell\": {\r\n      preventDefault();\r\n      actions.extendTo({ col: 0, row: 0 });\r\n      const focus = getFocusCell(storeApi.getState());\r\n      if (focus) scrollCellIntoView(focus);\r\n      break;\r\n    }\r\n    case \"extendLastCell\": {\r\n      preventDefault();\r\n      actions.extendTo({ col: Math.max(0, colCount - 1), row: Math.max(0, rowCount - 1) });\r\n      const focus = getFocusCell(storeApi.getState());\r\n      if (focus) scrollCellIntoView(focus);\r\n      break;\r\n    }\r\n    case \"pageUp\":\r\n    case \"pageDown\": {\r\n      preventDefault();\r\n      const scrollElement = scrollRef.current;\r\n      const visibleRows = scrollElement ? Math.max(1, Math.floor(scrollElement.clientHeight / rowHeight) - 4) : 10;\r\n      moveAndScroll({ dx: 0, dy: action === \"pageUp\" ? -visibleRows : visibleRows });\r\n      break;\r\n    }\r\n    case \"selectRow\": {\r\n      preventDefault();\r\n      const active = state.activeCell;\r\n      if (active) actions.selectRow(active.row, { additive: false });\r\n      break;\r\n    }\r\n    case \"selectColumn\": {\r\n      preventDefault();\r\n      const active = state.activeCell;\r\n      if (active) actions.selectColumn(active.col, { additive: false });\r\n      break;\r\n    }\r\n    case \"selectAll\": {\r\n      preventDefault();\r\n      actions.selectAll();\r\n      break;\r\n    }\r\n    case \"edit\": {\r\n      preventDefault();\r\n      if (readOnly || !state.activeCell) break;\r\n      if (isCheckboxCell(state, state.activeCell)) toggleCheckboxCell(state, actions, state.activeCell);\r\n      else actions.startEditing(state.activeCell);\r\n      break;\r\n    }\r\n    case \"editReplace\": {\r\n      if (readOnly || !state.activeCell) break;\r\n      preventDefault();\r\n      if (isCheckboxCell(state, state.activeCell)) break; // checkbox cells have no edit mode; the edit action toggles them, type-to-replace ignores them\r\n      // printable trigger seeds the typed char (Excel replace mode); a non-printable binding (e.g. F3) starts a plain edit\r\n      actions.startEditing(state.activeCell, triggerKey);\r\n      break;\r\n    }\r\n    case \"cancel\": {\r\n      preventDefault();\r\n      cancelFillDrag?.();\r\n      actions.clearSelection();\r\n      break;\r\n    }\r\n    case \"deleteContents\": {\r\n      preventDefault();\r\n      if (!readOnly) actions.deleteSelection();\r\n      break;\r\n    }\r\n    case \"fillDown\": {\r\n      preventDefault();\r\n      if (!readOnly) fillDown?.();\r\n      break;\r\n    }\r\n    case \"fillRight\": {\r\n      preventDefault();\r\n      if (!readOnly) fillRight?.();\r\n      break;\r\n    }\r\n    case \"undo\": {\r\n      preventDefault();\r\n      state.onUndo?.();\r\n      break;\r\n    }\r\n    case \"redo\": {\r\n      preventDefault();\r\n      state.onRedo?.();\r\n      break;\r\n    }\r\n    case \"insertRowBelow\": {\r\n      preventDefault();\r\n      // mirrors cell-menu-content.tsx's canInsertRow guard: without createRow, insertRow is a\r\n      // dev-warning no-op — skip the call so the shortcut doesn't spam that warning on every press.\r\n      if (!readOnly && state.createRow && state.activeCell) actions.insertRow(state.activeCell.row, \"below\");\r\n      break;\r\n    }\r\n    case \"duplicateRow\": {\r\n      preventDefault();\r\n      if (!readOnly && state.duplicateRow) {\r\n        // the context-menu item this shortcut labels duplicates the WHOLE selection; no\r\n        // selection, just the active row\r\n        const selected = getSelectedViewRows(state.selection);\r\n        if (selected.length > 0) actions.duplicateRows(selected);\r\n        else if (state.activeCell) actions.duplicateRows([state.activeCell.row]);\r\n      }\r\n      break;\r\n    }\r\n    case \"insertRowAbove\": {\r\n      preventDefault();\r\n      if (!readOnly && state.createRow && state.activeCell) actions.insertRow(state.activeCell.row, \"above\");\r\n      break;\r\n    }\r\n    case \"deleteRows\": {\r\n      preventDefault();\r\n      if (!readOnly) {\r\n        const selected = getSelectedViewRows(state.selection);\r\n        if (selected.length > 0) actions.deleteRows(selected);\r\n        else if (state.activeCell) actions.deleteRows([state.activeCell.row]);\r\n      }\r\n      break;\r\n    }\r\n  }\r\n}\r\nfunction pointerHitsCheckboxBox(event: ReactPointerEvent<HTMLElement>): boolean {\r\n  const box = event.currentTarget.querySelector(gridAttrSelector(\"checkboxBox\"))?.getBoundingClientRect();\r\n  return box !== undefined && event.clientX >= box.left && event.clientX <= box.right && event.clientY >= box.top && event.clientY <= box.bottom;\r\n}\r\n\r\nfunction isCheckboxCell(state: DataGridStoreState, coord: CellCoord): boolean {\r\n  return state.visibleColumns[coord.col]?.type === \"checkbox\";\r\n}\r\n\r\n/** Toggles a checkbox cell's boolean value in place via the direct-write commit path (no edit session, no move). No-op for a readOnly column, an unresolvable row, or a non-checkbox column. */\r\nfunction toggleCheckboxCell(state: DataGridStoreState, actions: ReturnType<typeof useDataGridActions>, coord: CellCoord): void {\r\n  const column = state.visibleColumns[coord.col];\r\n  if (!column || column.type !== \"checkbox\") return;\r\n  const dataRowIndex = state.viewIndex[coord.row];\r\n  const row = dataRowIndex === undefined ? undefined : state.data[dataRowIndex];\r\n  if (row === undefined) return;\r\n  const readOnly = typeof column.readOnly === \"function\" ? column.readOnly(row) : Boolean(column.readOnly);\r\n  if (readOnly) return;\r\n  // explicit TData=unknown: row's `undefined`-narrowed type ({} | null) would otherwise drive inference instead of column's own already-unknown TData.\r\n  const value = getCellValue<unknown, typeof column>(row, column);\r\n  actions.commitCellValue(coord, !value);\r\n}\r\n\r\n/** Direction for a data-boundary (Ctrl/Cmd+Arrow) jump. */\r\nexport type JumpDirection = \"up\" | \"down\" | \"left\" | \"right\";\r\n\r\n/**\r\n * Excel-style Ctrl/Cmd+Arrow \"jump to data boundary\": from `active`, scans view rows/columns in\r\n * `direction` using the cell type's `isEmpty`. If the next cell is non-empty, jumps to the last\r\n * non-empty cell of the contiguous run; if the next cell is empty, jumps to the first non-empty\r\n * cell found (or the grid edge if none). Clamped at the grid edges either way.\r\n * Exported for direct unit testing of the boundary-scan math; not part of the public hook surface.\r\n */\r\nexport function jumpToDataBoundary(state: DataGridStoreState, active: CellCoord, direction: JumpDirection): CellCoord {\r\n  const rowCount = state.viewIndex.length;\r\n  const colCount = state.visibleColumns.length;\r\n  const dx = direction === \"left\" ? -1 : direction === \"right\" ? 1 : 0;\r\n  const dy = direction === \"up\" ? -1 : direction === \"down\" ? 1 : 0;\r\n\r\n  const isEmptyAt = (coord: CellCoord): boolean => {\r\n    const column = state.visibleColumns[coord.col];\r\n    if (!column) return true;\r\n    const dataRowIndex = state.viewIndex[coord.row];\r\n    const row = dataRowIndex === undefined ? undefined : state.data[dataRowIndex];\r\n    if (row === undefined) return true;\r\n    const cellType = state.cellTypes[column.type ?? \"text\"];\r\n    if (!cellType) return true;\r\n    // explicit TData=unknown: row's `undefined`-narrowed type ({} | null) would otherwise drive inference instead of column's own already-unknown TData.\r\n    const value = getCellValue<unknown, typeof column>(row, column);\r\n    return cellType.isEmpty(value);\r\n  };\r\n\r\n  const inBounds = (coord: CellCoord): boolean =>\r\n    coord.col >= 0 && coord.col < colCount && coord.row >= 0 && coord.row < rowCount;\r\n\r\n  const clampToEdge = (): CellCoord => {\r\n    // no data at all beyond `active` in this direction: land on the grid edge\r\n    let coord = active;\r\n    while (inBounds({ col: coord.col + dx, row: coord.row + dy })) {\r\n      coord = { col: coord.col + dx, row: coord.row + dy };\r\n    }\r\n    return coord;\r\n  };\r\n\r\n  const next = { col: active.col + dx, row: active.row + dy };\r\n  if (!inBounds(next)) return active;\r\n\r\n  if (!isEmptyAt(next)) {\r\n    // scan the contiguous non-empty run, land on the last non-empty cell before an empty one or the edge\r\n    let coord = next;\r\n    while (true) {\r\n      const after = { col: coord.col + dx, row: coord.row + dy };\r\n      if (!inBounds(after) || isEmptyAt(after)) return coord;\r\n      coord = after;\r\n    }\r\n  }\r\n\r\n  // next is empty: scan forward for the first non-empty cell; else land on the grid edge\r\n  let coord = next;\r\n  while (inBounds(coord)) {\r\n    if (!isEmptyAt(coord)) return coord;\r\n    const after = { col: coord.col + dx, row: coord.row + dy };\r\n    if (!inBounds(after)) return clampToEdge();\r\n    coord = after;\r\n  }\r\n  return clampToEdge();\r\n}\r\n\r\n/** Move/extend actions that resolve through {@link MOVE_DELTA} — kept as its own union so the map is total (no `!` needed at the call site). */\r\ntype MoveAction = \"moveUp\" | \"moveDown\" | \"moveLeft\" | \"moveRight\" | \"retainMoveUp\" | \"retainMoveDown\" | \"retainMoveLeft\" | \"retainMoveRight\" | \"extendUp\" | \"extendDown\" | \"extendLeft\" | \"extendRight\";\r\n\r\n/** Jump/extend-jump actions that resolve through {@link JUMP_DIRECTION} — kept as its own union so the map is total (no `!` needed at the call site). */\r\ntype JumpAction = \"jumpUp\" | \"jumpDown\" | \"jumpLeft\" | \"jumpRight\" | \"extendJumpUp\" | \"extendJumpDown\" | \"extendJumpLeft\" | \"extendJumpRight\";\r\n\r\n/** Movement deltas for the plain move* / extend* actions. */\r\nconst MOVE_DELTA: Record<MoveAction, { dx: number; dy: number }> = {\r\n  moveUp: { dx: 0, dy: -1 },\r\n  moveDown: { dx: 0, dy: 1 },\r\n  moveLeft: { dx: -1, dy: 0 },\r\n  moveRight: { dx: 1, dy: 0 },\r\n  retainMoveUp: { dx: 0, dy: -1 },\r\n  retainMoveDown: { dx: 0, dy: 1 },\r\n  retainMoveLeft: { dx: -1, dy: 0 },\r\n  retainMoveRight: { dx: 1, dy: 0 },\r\n  extendUp: { dx: 0, dy: -1 },\r\n  extendDown: { dx: 0, dy: 1 },\r\n  extendLeft: { dx: -1, dy: 0 },\r\n  extendRight: { dx: 1, dy: 0 },\r\n};\r\n\r\nconst JUMP_DIRECTION: Record<JumpAction, JumpDirection> = {\r\n  jumpUp: \"up\",\r\n  jumpDown: \"down\",\r\n  jumpLeft: \"left\",\r\n  jumpRight: \"right\",\r\n  extendJumpUp: \"up\",\r\n  extendJumpDown: \"down\",\r\n  extendJumpLeft: \"left\",\r\n  extendJumpRight: \"right\",\r\n};\r\n\r\n/** Handlers + drag state wiring for keyboard nav, mouse selection, and editing lifecycle (see research/glide-behavior-spec.md §2-3). */\r\nexport type GridInteractionHandlers = {\r\n  onKeyDown: (event: ReactKeyboardEvent<HTMLElement>) => void;\r\n  /** Attach to each rendered cell's pointerdown. */\r\n  onCellPointerDown: (coord: CellCoord, event: ReactPointerEvent<HTMLElement>) => void;\r\n  /** Attach to each rendered cell's click — resolves the deferred active-cell click action. */\r\n  onCellClick: (coord: CellCoord, event: ReactMouseEvent<HTMLElement>) => void;\r\n  /** Attach to each rendered cell's dblclick. */\r\n  onCellDoubleClick: (coord: CellCoord, event: ReactMouseEvent<HTMLElement>) => void;\r\n  /**\r\n   * Attach to a header cell's pointerdown: resolves the plain/shift/ctrl click-select gesture\r\n   * immediately (glide-behavior-spec.md §3 \"Header clicks\") and starts a column-range drag so a\r\n   * press+drag across headers selects the contiguous range (§3 \"Drag\"). Entirely a no-op —\r\n   * including no drag/capture — when `enableColumnSelection` is false.\r\n   */\r\n  onHeaderPointerDown: (columnIndex: number, event: ReactPointerEvent<HTMLElement>) => void;\r\n  /**\r\n   * Attach to a marker cell's pointerdown: same plain/shift/ctrl resolution + drag-start as\r\n   * {@link onHeaderPointerDown}, on the rows channel (glide-behavior-spec.md §3 \"Row-marker clicks\").\r\n   * No-op when `enableRowSelection` is false.\r\n   */\r\n   onMarkerPointerDown: (viewRowIndex: number, event: ReactPointerEvent<HTMLElement>) => void;\r\n  /**\r\n   * Attach to the marker's grip pointerdown (the reorder family's reorder zone). Resolves the\r\n   * plain/ctrl click-select immediately (a stationary press selects the row) but NEVER starts\r\n   * the row-range drag — the row-reorder hook owns the pointer movement, so a press that moves\r\n   * reorders and a stationary release leaves exactly this press's selection. A <kbd>Shift</kbd>\r\n   * press takes the full {@link onMarkerPointerDown} gesture instead (shift+drag is always the\r\n   * row-select range). No-op selection-wise when `enableRowSelection` is false.\r\n   */\r\n  onMarkerGripPointerDown: (viewRowIndex: number, event: ReactPointerEvent<HTMLElement>) => void;\r\n  /**\r\n   * Attach to the marker's checkbox pointerdown ('checkbox'/'both' modes). Arms the same row-range\r\n   * drag as {@link onMarkerPointerDown} (anchor + auto-scroll), but never touches the rows channel\r\n   * itself on pointerdown — a stationary press still resolves as the checkbox's own native `click`\r\n   * (Base UI's `onCheckedChange`, an additive membership toggle), while a press that moves extends\r\n   * the row range exactly like a drag started elsewhere on the marker. This is what lets checkbox\r\n   * markers drag-select at all: the checkbox no longer stops the pointerdown from propagating.\r\n   */\r\n  onMarkerCheckboxPointerDown: (viewRowIndex: number, event: ReactPointerEvent<HTMLElement>) => void;\r\n  /** Attach to the root's pointerdown to clear selection on click-outside-cells. */\r\n  onRootPointerDown: (event: ReactPointerEvent<HTMLElement>) => void;\r\n  /**\r\n   * Ends an in-progress header column-select drag (mode \"column\") without affecting a cell/row\r\n   * drag. Column reorder (use-column-reorder.ts) calls this the moment it arms, so the two\r\n   * gestures started from the same header press never both apply once a reorder commits — see\r\n   * that hook's JSDoc for the full disambiguation rule.\r\n   */\r\n   cancelColumnSelectDrag: () => void;\r\n  /**\r\n   * Imperatively scrolls the container so `coord` (view-space) is visible, honoring the\r\n   * pinned-left/right bands. Public extension point for add-ons that move the active cell\r\n   * programmatically, e.g. `data-grid-toolbar`'s search next/prev — never `scrollIntoView`\r\n   * (see {@link scrollCellIntoView}'s own doc for why).\r\n   */\r\n  scrollCellIntoView: (coord: CellCoord) => void;\r\n};\r\n\r\n/**\r\n * Builds every pointer/keyboard handler the grid root and cells wire up, plus the imperative\r\n * scroll-into-view/drag-autoscroll machinery. One instance per `<DataGridRoot>` mount.\r\n */\r\nexport function useGridInteraction(options: UseGridInteractionOptions): GridInteractionHandlers {\r\n  const { scrollRef, layout, keymap, readOnly, fillDown, fillRight, cancelFillDrag } = options;\r\n  const actions = useDataGridActions();\r\n  const storeApi = useDataGridStoreApi();\r\n  const isMacRef = useRef(false);\r\n  isMacRef.current = isMacPlatform();\r\n\r\n  const layoutRef = useRef(layout);\r\n  layoutRef.current = layout;\r\n\r\n  // drag state lives in a ref, never React state — a drag never re-renders anything but the\r\n  // overlay/active-cell subscribers that selection changes already touch. `anchor` is the\r\n  // press-row for the row-marker drag: the pointer is the moving edge, so the range is always\r\n  // exactly anchor..current (it can grow AND shrink as the pointer moves).\r\n  const dragRef = useRef<{ pointerId: number; mode: \"range\" | \"row\" | \"column\"; anchor?: number } | null>(null);\r\n  const rafRef = useRef<number | null>(null);\r\n  const lastPointerRef = useRef<{ clientX: number; clientY: number } | null>(null);\r\n\r\n  // Active-cell click deferred to native `click` — pointerdown alone can't distinguish it from a drag.\r\n  const pendingActiveClickRef = useRef<CellCoord | null>(null);\r\n  // Coord a single click just resolved, so the following dblclick (same gesture) doesn't re-resolve it.\r\n  const resolvedByClickRef = useRef<CellCoord | null>(null);\r\n\r\n  const stopAutoScrollLoop = useCallback(() => {\r\n    if (rafRef.current !== null) {\r\n      cancelAnimationFrame(rafRef.current);\r\n      rafRef.current = null;\r\n    }\r\n  }, []);\r\n\r\n  const runDragFrame = useCallback(() => {\r\n    const scrollElement = scrollRef.current;\r\n    const pointer = lastPointerRef.current;\r\n    const drag = dragRef.current;\r\n    if (!scrollElement || !pointer || !drag) {\r\n      rafRef.current = null;\r\n      return;\r\n    }\r\n    const rect = scrollElement.getBoundingClientRect();\r\n\r\n    // auto-scroll the container by a fixed step per frame while the pointer sits beyond an edge\r\n    if (pointer.clientY < rect.top + AUTO_SCROLL_ZONE) scrollElement.scrollTop -= AUTO_SCROLL_STEP;\r\n    else if (pointer.clientY > rect.bottom - AUTO_SCROLL_ZONE) scrollElement.scrollTop += AUTO_SCROLL_STEP;\r\n    // Which physical edge means \"scroll toward the inline start\" depends on direction; the step is\r\n    // then applied in the container's own scrollLeft convention.\r\n    const direction = layoutRef.current.direction ?? \"ltr\";\r\n    const inlineStep = inlineAutoScrollStep(pointer.clientX, rect, AUTO_SCROLL_ZONE, direction);\r\n    if (inlineStep !== 0) applyInlineScrollDelta(scrollElement, inlineStep * AUTO_SCROLL_STEP, direction);\r\n\r\n    const state = storeApi.getState();\r\n    const rowCount = state.viewIndex.length;\r\n    const coord = pointerToCoord(pointer.clientX, pointer.clientY, scrollElement, layoutRef.current, rowCount);\r\n    const clamped = {\r\n      col: Math.max(0, Math.min(coord.col, Math.max(0, state.visibleColumns.length - 1))),\r\n      row: Math.max(0, Math.min(coord.row, Math.max(0, rowCount - 1))),\r\n    };\r\n\r\n    if (drag.mode === \"range\") actions.extendTo(clamped);\r\n    else if (drag.mode === \"row\") {\r\n      // A plain marker drag (anchor set) replaces the row channel with exactly anchor..current, so\r\n      // dragging back over selected rows shrinks the range; a shift/ctrl press (anchor undefined)\r\n      // keeps the old union-extend, so an additive multi-selection is never clobbered.\r\n      actions.selectRow(clamped.row, drag.anchor !== undefined ? { replaceFromLast: true, from: drag.anchor } : { extendFromLast: true });\r\n    } else actions.selectColumn(clamped.col, { extendFromLast: true });\r\n\r\n    rafRef.current = requestAnimationFrame(runDragFrame);\r\n  }, [actions, scrollRef, storeApi]);\r\n\r\n  // pointermove/pointerup are attached to `document` only for the lifetime of an active drag —\r\n  // beginDrag attaches them, endDrag tears them down immediately, instead of a permanent\r\n  // always-on subscription.\r\n  const documentListenersRef = useRef<(() => void) | null>(null);\r\n\r\n  // The element that took setPointerCapture for the active drag; released explicitly on end\r\n  // rather than relying on the browser's implicit release, which some environments (real\r\n  // Chromium via Playwright) don't reliably fire before a later, unrelated click gesture —\r\n  // a stale capture otherwise swallows that next gesture's pointerup/click on its real target\r\n  // (e.g. an option in a portaled Select popup opened right after this cell's click).\r\n  const captureElementRef = useRef<Element | null>(null);\r\n\r\n  const endDrag = useCallback(() => {\r\n    const pointerId = dragRef.current?.pointerId;\r\n    const captureEl = captureElementRef.current as (Element & { hasPointerCapture?: (id: number) => boolean; releasePointerCapture?: (id: number) => void }) | null;\r\n    if (captureEl && pointerId !== undefined && captureEl.hasPointerCapture?.(pointerId)) {\r\n      captureEl.releasePointerCapture?.(pointerId);\r\n    }\r\n    captureElementRef.current = null;\r\n    dragRef.current = null;\r\n    lastPointerRef.current = null;\r\n    stopAutoScrollLoop();\r\n    documentListenersRef.current?.();\r\n    documentListenersRef.current = null;\r\n  }, [stopAutoScrollLoop]);\r\n\r\n  const beginDrag = useCallback(\r\n    (mode: \"range\" | \"row\" | \"column\", pointerId: number, captureElement?: Element, anchor?: number) => {\r\n      dragRef.current = { pointerId, mode, anchor };\r\n      captureElementRef.current = captureElement ?? null;\r\n      if (rafRef.current === null) rafRef.current = requestAnimationFrame(runDragFrame);\r\n\r\n      if (!documentListenersRef.current) {\r\n        const onMove = (event: PointerEvent) => {\r\n          if (!dragRef.current || dragRef.current.pointerId !== event.pointerId) return;\r\n          lastPointerRef.current = { clientX: event.clientX, clientY: event.clientY };\r\n          // Pointer capture still fires `click` on the pressed cell after a drag, even one that ends back there.\r\n          const pending = pendingActiveClickRef.current;\r\n          const scrollElement = scrollRef.current;\r\n          if (pending && scrollElement) {\r\n            const coord = pointerToCoord(event.clientX, event.clientY, scrollElement, layoutRef.current, storeApi.getState().viewIndex.length);\r\n            if (coord.col !== pending.col || coord.row !== pending.row) pendingActiveClickRef.current = null;\r\n          }\r\n          // the frame loop kills itself when no move has arrived yet (slow press-then-drag) —\r\n          // restart it here or a drag whose first move lands after frame 1 never paints (user QA).\r\n          if (rafRef.current === null) rafRef.current = requestAnimationFrame(runDragFrame);\r\n        };\r\n        const onUp = (event: PointerEvent) => {\r\n          if (!dragRef.current || dragRef.current.pointerId !== event.pointerId) return;\r\n          endDrag();\r\n        };\r\n        document.addEventListener(\"pointermove\", onMove);\r\n        document.addEventListener(\"pointerup\", onUp);\r\n        document.addEventListener(\"pointercancel\", onUp);\r\n        documentListenersRef.current = () => {\r\n          document.removeEventListener(\"pointermove\", onMove);\r\n          document.removeEventListener(\"pointerup\", onUp);\r\n          document.removeEventListener(\"pointercancel\", onUp);\r\n        };\r\n      }\r\n    },\r\n    [runDragFrame, endDrag, scrollRef, storeApi],\r\n  );\r\n\r\n  // safety net: tear down a still-active drag's listeners/rAF if the component unmounts mid-drag\r\n  useEffect(() => () => documentListenersRef.current?.(), []);\r\n\r\n  const scrollActiveCellIntoView = useCallback(\r\n    (coord: CellCoord) => {\r\n      const scrollElement = scrollRef.current;\r\n      if (!scrollElement) return;\r\n      scrollCellIntoView(scrollElement, coord, layoutRef.current);\r\n    },\r\n    [scrollRef],\r\n  );\r\n\r\n  const onKeyDown = useCallback(\r\n    (event: ReactKeyboardEvent<HTMLElement>) => {\r\n      if (event.nativeEvent.isComposing) return;\r\n\r\n      // A commit/cancel keystroke (e.g. Enter) handled by the editor's own onKeyDown already\r\n      // updated the store (ending edit mode) before this bubbled handler runs on the same\r\n      // native event; re-checking store.editing here would then be stale and re-dispatch the\r\n      // same key against the *new* active cell (e.g. re-opening the editor it just moved to).\r\n      // The editor is the only thing that renders a focusable form control inside a cell, so\r\n      // this key having originated from one means it belongs to that (possibly just-finished) edit.\r\n      const target = event.target as HTMLElement | null;\r\n      if (target && target !== event.currentTarget && /^(INPUT|SELECT|TEXTAREA)$/.test(target.tagName)) return;\r\n\r\n      const state = storeApi.getState();\r\n      const editing = state.editing;\r\n      // while editing, the editor owns every key (including its own Tab/Enter commit contract)\r\n      if (editing) return;\r\n\r\n      const keymapEvent: KeymapEvent = {\r\n        // Arrow keys move VISUALLY: under RTL, ArrowRight goes to the next column on the screen,\r\n        // which is the PREVIOUS column index. Swapping the KEY here — the one place a key becomes\r\n        // an action — is what lets every navigation helper, and any consumer's custom keymap, stay\r\n        // written in logical terms (moveLeft/moveRight/extendLeft/jumpLeft). Tab/Shift+Tab are not\r\n        // swapped: they are already reading-order logical. Home/End need no swap either, since\r\n        // moveRowStart/moveRowEnd are logically named and \"row start\" is column 0 in both directions.\r\n        key: visualArrowKey(event.key, layoutRef.current.direction ?? \"ltr\"),\r\n        ctrlKey: event.ctrlKey,\r\n        metaKey: event.metaKey,\r\n        shiftKey: event.shiftKey,\r\n        altKey: event.altKey,\r\n      };\r\n\r\n      // unbound printable key = implicit editReplace trigger (Excel behavior); a consumer keymap that DEFINES editReplace (even []) takes it over and disables the fallback\r\n      const action =\r\n        matchKeymap(keymapEvent, keymap, isMacRef.current) ??\r\n        (isPrintableKey(keymapEvent) && keymap.editReplace === undefined ? \"editReplace\" : null);\r\n\r\n      if (!action) return;\r\n\r\n      dispatchGridAction({\r\n        action,\r\n        actions,\r\n        storeApi,\r\n        scrollRef,\r\n        scrollCellIntoView: scrollActiveCellIntoView,\r\n        rowHeight: layoutRef.current.rowHeight,\r\n        readOnly,\r\n        triggerKey: isPrintableKey(keymapEvent) ? keymapEvent.key : undefined,\r\n        preventDefault: () => event.preventDefault(),\r\n        fillDown,\r\n        fillRight,\r\n        cancelFillDrag,\r\n      });\r\n    },\r\n    [actions, cancelFillDrag, fillDown, fillRight, keymap, readOnly, scrollActiveCellIntoView, scrollRef, storeApi],\r\n  );\r\n\r\n  const onCellPointerDown = useCallback(\r\n    (coord: CellCoord, event: ReactPointerEvent<HTMLElement>) => {\r\n      if (event.button !== 0) return;\r\n      // React re-dispatches portaled content's events through the React tree of its logical\r\n      // parent, not the DOM tree — so a press inside a cell's own popup editor (Select/Date,\r\n      // rendered via a portal to document.body) still reaches this handler even though the\r\n      // DOM target is nowhere under this cell. Ignore it; the editor's own handlers own it.\r\n      if ((event.target as HTMLElement | null)?.closest(gridAttrSelector(\"cellEditor\"))) return;\r\n      const state = storeApi.getState();\r\n      const isMultiKey = isMacRef.current ? event.metaKey : event.ctrlKey;\r\n\r\n      // Excel activation model: a click NEVER starts editing — it only\r\n      // selects; dblclick/Enter/F2/typing edit. Only checkbox cells resolve a stationary\r\n      // click into a direct toggle (a control, not an editor).\r\n      const wasActive =\r\n        !event.shiftKey &&\r\n        !isMultiKey &&\r\n        state.activeCell !== null &&\r\n        state.activeCell.col === coord.col &&\r\n        state.activeCell.row === coord.row &&\r\n        !state.editing;\r\n\r\n      if (event.shiftKey) {\r\n        actions.extendTo(coord);\r\n      } else if (isMultiKey) {\r\n        actions.pushRange(coord);\r\n      } else {\r\n        actions.selectCell(coord);\r\n      }\r\n\r\n      const plainPress = !event.shiftKey && !isMultiKey && !state.editing;\r\n      if ((wasActive || (plainPress && pointerHitsCheckboxBox(event))) && !readOnly && isCheckboxCell(state, coord)) {\r\n        // Resolved on the cell's native `click` — pointerdown can't yet tell a click from a drag.\r\n        pendingActiveClickRef.current = coord;\r\n      }\r\n\r\n      // jsdom (unit tests) doesn't implement the Pointer Events capture methods; guard for it.\r\n      event.currentTarget.setPointerCapture?.(event.pointerId);\r\n      beginDrag(\"range\", event.pointerId, event.currentTarget);\r\n    },\r\n    [actions, beginDrag, readOnly, storeApi],\r\n  );\r\n\r\n  const onCellClick = useCallback(\r\n    (coord: CellCoord, event: ReactMouseEvent<HTMLElement>) => {\r\n      const pending = pendingActiveClickRef.current;\r\n      pendingActiveClickRef.current = null;\r\n      if (!pending || pending.col !== coord.col || pending.row !== coord.row) return;\r\n      // detail >= 2 is a dblclick's second click — onCellDoubleClick resolves that case instead.\r\n      if (event.detail >= 2) return;\r\n      const state = storeApi.getState();\r\n      // Excel model: only checkbox cells act on a stationary click; text/number/etc. never edit here.\r\n      if (isCheckboxCell(state, coord)) {\r\n        toggleCheckboxCell(state, actions, coord);\r\n        resolvedByClickRef.current = coord;\r\n      }\r\n    },\r\n    [actions, storeApi],\r\n  );\r\n\r\n  const onCellDoubleClick = useCallback(\r\n    (coord: CellCoord, event: ReactMouseEvent<HTMLElement>) => {\r\n      if (readOnly) return;\r\n      // same portal-bubbling concern as onCellPointerDown above.\r\n      if ((event.target as HTMLElement | null)?.closest(gridAttrSelector(\"cellEditor\"))) return;\r\n      pendingActiveClickRef.current = null;\r\n      const state = storeApi.getState();\r\n      actions.selectCell(coord);\r\n      // this gesture's first click may have already resolved the action via onCellClick.\r\n      const alreadyResolved = resolvedByClickRef.current?.col === coord.col && resolvedByClickRef.current?.row === coord.row;\r\n      resolvedByClickRef.current = null;\r\n      if (alreadyResolved) return;\r\n      // checkbox has no edit mode; direct-toggle instead of entering edit mode.\r\n      if (isCheckboxCell(state, coord)) toggleCheckboxCell(state, actions, coord);\r\n      else actions.startEditing(coord);\r\n    },\r\n    [actions, readOnly, storeApi],\r\n  );\r\n\r\n  const onHeaderPointerDown = useCallback(\r\n    (columnIndex: number, event: ReactPointerEvent<HTMLElement>) => {\r\n      if (event.button !== 0) return;\r\n      if (!storeApi.getState().enableColumnSelection) return;\r\n      const isMultiKey = isMacRef.current ? event.metaKey : event.ctrlKey;\r\n      if (event.shiftKey) {\r\n        actions.selectColumn(columnIndex, { extendFromLast: true });\r\n      } else if (isMultiKey) {\r\n        actions.selectColumn(columnIndex, { additive: true });\r\n      } else {\r\n        actions.selectColumn(columnIndex);\r\n      }\r\n      event.currentTarget.setPointerCapture?.(event.pointerId);\r\n      beginDrag(\"column\", event.pointerId, event.currentTarget);\r\n    },\r\n    [actions, beginDrag, storeApi],\r\n  );\r\n\r\n  const onMarkerPointerDown = useCallback(\r\n    (viewRowIndex: number, event: ReactPointerEvent<HTMLElement>) => {\r\n      if (event.button !== 0) return;\r\n      if (!storeApi.getState().enableRowSelection) return;\r\n      const isMultiKey = isMacRef.current ? event.metaKey : event.ctrlKey;\r\n      // Moving-edge drag (replaceFromLast): a plain press anchors at the press row; a shift press\r\n      // anchors at the PREVIOUS last-highlighted row (read before selectRow below overwrites it)\r\n      // so its extension keeps tracking the pointer. A ctrl press holds an additive multi-selection\r\n      // the replace would clobber, so it keeps the old union-extend drag.\r\n      const anchor = isMultiKey ? undefined : event.shiftKey ? (storeApi.getState().lastHighlightedRow ?? viewRowIndex) : viewRowIndex;\r\n      if (event.shiftKey) {\r\n        actions.selectRow(viewRowIndex, { extendFromLast: true });\r\n      } else if (isMultiKey) {\r\n        actions.selectRow(viewRowIndex, { additive: true });\r\n      } else {\r\n        actions.selectRow(viewRowIndex);\r\n      }\r\n      event.currentTarget.setPointerCapture?.(event.pointerId);\r\n      beginDrag(\"row\", event.pointerId, event.currentTarget, anchor);\r\n    },\r\n    [actions, beginDrag, storeApi],\r\n  );\r\n\r\n  const onMarkerGripPointerDown = useCallback(\r\n    (viewRowIndex: number, event: ReactPointerEvent<HTMLElement>) => {\r\n      if (event.button !== 0) return;\r\n      if (event.shiftKey) {\r\n        // shift+drag from the grip is the row-select range gesture, like any other marker press\r\n        onMarkerPointerDown(viewRowIndex, event);\r\n        return;\r\n      }\r\n      if (!storeApi.getState().enableRowSelection) return;\r\n      const isMultiKey = isMacRef.current ? event.metaKey : event.ctrlKey;\r\n      if (isMultiKey) {\r\n        actions.selectRow(viewRowIndex, { additive: true });\r\n      } else {\r\n        actions.selectRow(viewRowIndex);\r\n      }\r\n      // deliberately NO beginDrag: the row-reorder hook owns the pointer from this press (see the\r\n      // grip zone's JSDoc), so the two gestures can never both apply to one press.\r\n    },\r\n    [actions, isMacRef, onMarkerPointerDown, storeApi],\r\n  );\r\n\r\n  const onMarkerCheckboxPointerDown = useCallback(\r\n    (viewRowIndex: number, event: ReactPointerEvent<HTMLElement>) => {\r\n      if (event.button !== 0) return;\r\n      if (!storeApi.getState().enableRowSelection) return;\r\n      // arms the anchor only (lastHighlightedRow), never `selection.rows` itself — a stationary\r\n      // press+release still resolves purely via the checkbox's own click->toggle. lastPointerRef\r\n      // stays unseeded, so runDragFrame's first tick is a no-op (pointer is null) exactly like the\r\n      // other drag starts; only a real pointermove commits to extending from this anchor.\r\n      actions.armRowDragAnchor(viewRowIndex);\r\n      event.currentTarget.setPointerCapture?.(event.pointerId);\r\n      // the checkbox press is the range gesture's anchor: dragging makes the row channel exactly\r\n      // pressRow..current (grow AND shrink), matching the plain marker drag.\r\n      beginDrag(\"row\", event.pointerId, event.currentTarget, viewRowIndex);\r\n    },\r\n    [actions, beginDrag, storeApi],\r\n  );\r\n\r\n  const onRootPointerDown = useCallback(\r\n    (event: ReactPointerEvent<HTMLElement>) => {\r\n      // only fires when the pointerdown wasn't already handled (and stopped) by a cell/header\r\n      if (event.target !== event.currentTarget) return;\r\n      // a press inside a portaled popover editor (select/date) is not a click-away, even if it\r\n      // somehow bubbles here — data-grid-cell-editor is the marker both editors' content carries.\r\n      const target = event.target as HTMLElement;\r\n      if (target.closest(gridAttrSelector(\"cellEditor\"))) return;\r\n      actions.clearSelection();\r\n    },\r\n    [actions],\r\n  );\r\n\r\n  const cancelColumnSelectDrag = useCallback(() => {\r\n    if (dragRef.current?.mode === \"column\") endDrag();\r\n  }, [endDrag]);\r\n\r\n  useEffect(() => stopAutoScrollLoop, [stopAutoScrollLoop]);\r\n\r\n  // Page-area click-outside clear (2026-09-03 audit N4): the listener is attached only while a\r\n  // non-empty selection exists, so no document-level listener is present at rest.\r\n  useEffect(() => {\r\n    let attached = false;\r\n    const onPointerDown = (event: PointerEvent) => {\r\n      if (event.button !== 0) return;\r\n      const target = event.target as Element | null;\r\n      if (!target || !(target instanceof Element)) return;\r\n      if (scrollRef.current?.contains(target)) return;\r\n      if (target.closest(gridAttrSelector(\"cellEditor\"))) return;\r\n      actions.clearSelection();\r\n    };\r\n    const setAttached = (attach: boolean) => {\r\n      if (attach === attached) return;\r\n      if (attach) document.addEventListener(\"pointerdown\", onPointerDown, true);\r\n      else document.removeEventListener(\"pointerdown\", onPointerDown, true);\r\n      attached = attach;\r\n    };\r\n    setAttached(!isSelectionEmpty(storeApi.getState().selection));\r\n    const unsubscribe = storeApi.subscribe((state, prevState) => {\r\n      if (state.selection === prevState.selection) return;\r\n      setAttached(!isSelectionEmpty(state.selection));\r\n    });\r\n    return () => {\r\n      unsubscribe();\r\n      setAttached(false);\r\n    };\r\n  }, [actions, scrollRef, storeApi]);\r\n\r\n  return useMemo(\r\n    () => ({\r\n      onKeyDown,\r\n      onCellPointerDown,\r\n      onCellClick,\r\n      onCellDoubleClick,\r\n      onHeaderPointerDown,\r\n      onMarkerPointerDown,\r\n      onMarkerGripPointerDown,\r\n      onMarkerCheckboxPointerDown,\r\n      onRootPointerDown,\r\n      cancelColumnSelectDrag,\r\n      scrollCellIntoView: scrollActiveCellIntoView,\r\n    }),\r\n    [\r\n      onKeyDown,\r\n      onCellPointerDown,\r\n      onCellClick,\r\n      onCellDoubleClick,\r\n      onHeaderPointerDown,\r\n      onMarkerPointerDown,\r\n      onMarkerGripPointerDown,\r\n      onMarkerCheckboxPointerDown,\r\n      onRootPointerDown,\r\n      cancelColumnSelectDrag,\r\n      scrollActiveCellIntoView,\r\n    ],\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/interaction/use-grid-interaction.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/interaction/use-seed-focus.ts",
      "content": "import { useEffect, type RefObject } from \"react\";\r\n\r\n/** Focuses an input on mount with the caret at the end of its content — never select-all (diceui caret policy). */\r\nexport function useSeedFocus(ref: RefObject<HTMLInputElement | null>, _initialText: string | undefined) {\r\n  useEffect(() => {\r\n    const el = ref.current;\r\n    if (!el) return;\r\n    el.focus();\r\n    const end = el.value.length;\r\n    el.setSelectionRange(end, end);\r\n    // seed-once on mount; re-running on every value change would fight the user's cursor\r\n    // oxlint-disable-next-line react-hooks/exhaustive-deps\r\n  }, []);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/interaction/use-seed-focus.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/is-dev.ts",
      "content": "/** True outside production; reads `process.env.NODE_ENV` via `globalThis` so it typechecks without `@types/node` (Vite consumers don't have it by default). */\r\nexport function isDev(): boolean {\r\n  const env = (globalThis as { process?: { env?: { NODE_ENV?: string } } }).process?.env?.NODE_ENV;\r\n  return env !== \"production\";\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/is-dev.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/keyboard/default-keymap.ts",
      "content": "import type { Keymap } from \"../types\";\r\n\r\n/**\r\n * Default keyboard bindings (see research/glide-behavior-spec.md §2). `mod` resolves to Cmd on\r\n * macOS, Ctrl elsewhere; `ctrl` is the literal physical Ctrl key on every platform (used where\r\n * Cmd is reserved by the OS, e.g. Ctrl+Space vs. macOS Spotlight). Consumers may\r\n * override/extend via the `keymap` prop.\r\n */\r\nexport const DEFAULT_KEYMAP: Keymap = {\r\n  moveUp: [\"ArrowUp\"],\r\n  moveDown: [\"ArrowDown\"],\r\n  moveLeft: [\"ArrowLeft\"],\r\n  moveRight: [\"ArrowRight\"],\r\n  // glide spec :41 — Alt+Arrow moves the active cell but retains the selection.\r\n  retainMoveUp: [\"alt+ArrowUp\"],\r\n  retainMoveDown: [\"alt+ArrowDown\"],\r\n  retainMoveLeft: [\"alt+ArrowLeft\"],\r\n  retainMoveRight: [\"alt+ArrowRight\"],\r\n  // glide spec :46 — primary+Enter scrolls the active cell into view without moving it.\r\n  scrollActiveIntoView: [\"mod+Enter\"],\r\n  moveRowStart: [\"Home\"],\r\n  moveRowEnd: [\"End\"],\r\n  jumpUp: [\"mod+ArrowUp\"],\r\n  jumpDown: [\"mod+ArrowDown\"],\r\n  jumpLeft: [\"mod+ArrowLeft\"],\r\n  jumpRight: [\"mod+ArrowRight\"],\r\n  moveFirstCell: [\"mod+Home\"],\r\n  moveLastCell: [\"mod+End\"],\r\n  pageUp: [\"PageUp\"],\r\n  pageDown: [\"PageDown\"],\r\n  extendUp: [\"shift+ArrowUp\"],\r\n  extendDown: [\"shift+ArrowDown\"],\r\n  extendLeft: [\"shift+ArrowLeft\"],\r\n  extendRight: [\"shift+ArrowRight\"],\r\n  extendJumpUp: [\"mod+shift+ArrowUp\"],\r\n  extendJumpDown: [\"mod+shift+ArrowDown\"],\r\n  extendJumpLeft: [\"mod+shift+ArrowLeft\"],\r\n  extendJumpRight: [\"mod+shift+ArrowRight\"],\r\n  extendFirstCell: [\"mod+shift+Home\"],\r\n  extendLastCell: [\"mod+shift+End\"],\r\n  selectRow: [\"shift+ \"],\r\n  // literal ctrl (not mod): Cmd+Space is Spotlight on macOS, so glide/Excel bind the physical Ctrl key here\r\n  selectColumn: [\"ctrl+ \"],\r\n  selectAll: [\"mod+a\"],\r\n  edit: [\"Enter\", \"F2\", \" \"],\r\n  commitDown: [\"Enter\"],\r\n  commitUp: [\"shift+Enter\"],\r\n  commitRight: [\"Tab\"],\r\n  commitLeft: [\"shift+Tab\"],\r\n  cancel: [\"Escape\"],\r\n  deleteContents: [\"Delete\", \"Backspace\"],\r\n  undo: [\"mod+z\"],\r\n  redo: [\"mod+y\", \"mod+shift+z\"],\r\n  fillDown: [\"mod+d\"],\r\n  fillRight: [\"mod+r\"],\r\n  // mod+shift+i/d/c/j/k/m are real devtools/bookmark-manager browser-chrome shortcuts a page\r\n  // can't reliably preventDefault against; f/x are unreserved in Chrome/Edge/Firefox/Safari.\r\n  insertRowBelow: [\"mod+shift+f\"],\r\n  duplicateRow: [\"mod+shift+x\"],\r\n  // Sheets/Excel chords (Ctrl+Alt+=/-, Ctrl+Shift+=, Ctrl+-) hit AltGr or non-preventable browser zoom; u/Backspace don't.\r\n  insertRowAbove: [\"mod+shift+u\"],\r\n  deleteRows: [\"mod+shift+Backspace\"],\r\n};\r\n",
      "type": "registry:component",
      "target": "components/data-grid/keyboard/default-keymap.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/keyboard/index.ts",
      "content": "/** Domain barrel — keymap matching, defaults, printable-key detection, binding-string validation. */\r\nexport { DEFAULT_KEYMAP } from \"./default-keymap\";\r\nexport { matchKeymap, parseBinding, type KeymapEvent, type ParsedBinding } from \"./match-keymap\";\r\nexport { isPrintableKey } from \"./is-printable-key\";\r\nexport { isMacPlatform } from \"./platform\";\r\nexport { validateKeyBinding, validateKeymap, type KeyBinding } from \"./key-syntax\";\r\n",
      "type": "registry:component",
      "target": "components/data-grid/keyboard/index.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/keyboard/is-printable-key.ts",
      "content": "import type { KeymapEvent } from \"./match-keymap\";\r\n\r\n/**\r\n * True when the event represents a single printable Unicode character with no\r\n * modifier held, per glide-behavior-spec.md §2. Gates the implicit type-to-replace\r\n * fallback (`editReplace`'s default trigger, see the GridAction doc) and seeds the\r\n * typed char when a printable binding dispatches `editReplace`.\r\n */\r\nexport function isPrintableKey(event: KeymapEvent): boolean {\r\n  if (event.ctrlKey || event.metaKey) return false;\r\n  if (event.key.length !== 1) return false;\r\n  return /[\\p{L}\\p{N}\\p{S}\\p{P}]/u.test(event.key);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/keyboard/is-printable-key.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/keyboard/key-syntax.ts",
      "content": "import type { Keymap } from \"../types\";\r\nimport { isDev } from \"../is-dev\";\r\nimport { warnDev } from \"../store/commit\";\r\n\r\n/**\r\n * Key tokens for keymap binding strings (the last `+`-separated part). Letters are lowercase\r\n * in bindings; matching is case-insensitive. The space bar is the literal `\" \"` (`KeyboardEvent.key`\r\n * is `\" \"`; `\"Space\"` never matches it). The literal plus key is absent: the parser reads the\r\n * last `+` as the separator (see `parseBinding`).\r\n */\r\nexport type LetterKey =\r\n  | \"a\" | \"b\" | \"c\" | \"d\" | \"e\" | \"f\" | \"g\" | \"h\" | \"i\" | \"j\"\r\n  | \"k\" | \"l\" | \"m\" | \"n\" | \"o\" | \"p\" | \"q\" | \"r\" | \"s\" | \"t\"\r\n  | \"u\" | \"v\" | \"w\" | \"x\" | \"y\" | \"z\";\r\n\r\nexport type NumberKey = \"0\" | \"1\" | \"2\" | \"3\" | \"4\" | \"5\" | \"6\" | \"7\" | \"8\" | \"9\";\r\n\r\nexport type FunctionKey =\r\n  | \"F1\" | \"F2\" | \"F3\" | \"F4\" | \"F5\" | \"F6\" | \"F7\" | \"F8\" | \"F9\"\r\n  | \"F10\" | \"F11\" | \"F12\" | \"F13\" | \"F14\" | \"F15\" | \"F16\" | \"F17\" | \"F18\" | \"F19\"\r\n  | \"F20\" | \"F21\" | \"F22\" | \"F23\" | \"F24\";\r\n\r\nexport type NavigationKey = \"ArrowUp\" | \"ArrowDown\" | \"ArrowLeft\" | \"ArrowRight\" | \"Home\" | \"End\" | \"PageUp\" | \"PageDown\";\r\n\r\nexport type EditingKey = \"Enter\" | \"Escape\" | \"Tab\" | \"Backspace\" | \"Delete\";\r\n\r\nexport type PunctuationKey =\r\n  | \"/\" | \"[\" | \"]\" | \"\\\\\" | \"=\" | \"-\" | \",\" | \".\" | \";\" | \"`\" | \"'\"\r\n  | \"?\" | \"!\" | \"@\" | \"#\" | \"$\" | \"%\" | \"^\" | \"&\" | \"*\" | \"(\" | \")\"\r\n  | \"_\" | \"{\" | \"}\" | \"|\" | \":\" | '\"' | \"<\" | \">\" | \"~\";\r\n\r\nexport type KeyPart = LetterKey | NumberKey | FunctionKey | NavigationKey | EditingKey | PunctuationKey | \" \";\r\n\r\n/** Modifier tokens of a binding string. `mod` is the platform primary (Cmd on macOS, Ctrl elsewhere); `ctrl` is the literal physical Ctrl key. */\r\nexport type ModifierToken = \"mod\" | \"ctrl\" | \"shift\" | \"alt\";\r\n\r\n/** Canonical modifier prefixes (see the union's order); `mod` + `ctrl` is excluded because the matcher's `ctrl` path ignores `mod`. */\r\nexport type ModifierPrefix =\r\n  | \"mod\" | \"ctrl\" | \"shift\" | \"alt\"\r\n  | \"mod+shift\" | \"mod+alt\" | \"ctrl+shift\" | \"ctrl+alt\" | \"shift+alt\"\r\n  | \"mod+shift+alt\" | \"ctrl+shift+alt\";\r\n\r\n/**\r\n * A keymap binding string: a canonical-order modifier prefix plus a key. `(string & {})` keeps\r\n * arbitrary strings assignable (exotic keys still compile) while IntelliSense lists the valid\r\n * combinations; invalid bindings warn in development (see {@link validateKeymap}).\r\n */\r\nexport type KeyBinding = KeyPart | `${ModifierPrefix}+${KeyPart}` | (string & {});\r\n\r\nconst MODIFIER_TOKENS: readonly string[] = [\"mod\", \"ctrl\", \"shift\", \"alt\"];\r\n\r\n// Stable W3C key values, exact case; IME states (Dead/Process/Unidentified) and modifier values stay excluded.\r\nconst STABLE_NAMED_KEYS: readonly string[] = [\r\n  \"ArrowUp\", \"ArrowDown\", \"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\", \"PageUp\", \"PageDown\",\r\n  \"Enter\", \"Escape\", \"Tab\", \"Backspace\", \"Delete\", \"Insert\", \"Clear\", \"Help\",\r\n  \"CapsLock\", \"NumLock\", \"ScrollLock\", \"Pause\", \"PrintScreen\", \"ContextMenu\",\r\n  \"Execute\", \"Menu\", \"Suspend\", \"EraseEOL\", \"Again\", \"Accept\", \"Redo\", \"Undo\", \"Props\", \"Select\",\r\n  \"BrowserBack\", \"BrowserForward\", \"BrowserHome\", \"BrowserFavorites\", \"BrowserSearch\",\r\n  \"BrowserRefresh\", \"BrowserStop\",\r\n  \"MediaPlayPause\", \"MediaStop\", \"MediaTrackNext\", \"MediaTrackPrevious\",\r\n  \"AudioVolumeUp\", \"AudioVolumeDown\", \"AudioVolumeMute\",\r\n  \"LaunchMail\", \"LaunchApplication1\", \"LaunchApplication2\", \"LaunchApplication3\",\r\n  \"LaunchCalendar\", \"LaunchCommunications\", \"LaunchControlPanel\", \"LaunchFileManager\",\r\n  \"LaunchMediaPlayer\", \"LaunchMusicPlayer\", \"LaunchPhone\", \"LaunchScreenSaver\",\r\n  \"LaunchWebBrowser\", \"LaunchWebCam\", \"LaunchTerminal\",\r\n  \"Power\", \"Sleep\", \"Eject\", \"WakeUp\",\r\n  \"Convert\", \"NonConvert\", \"Hiragana\", \"Katakana\", \"HangulMode\", \"Hangul\", \"HanjaMode\", \"Hanja\",\r\n  \"KanaMode\", \"Zenkaku\", \"Hankaku\", \"ZenkakuHankaku\",\r\n];\r\n\r\nconst NAMED_KEYS: ReadonlySet<string> = new Set([\r\n  ...STABLE_NAMED_KEYS,\r\n  ...Array.from({ length: 24 }, (_, i) => `F${i + 1}`),\r\n]);\r\n\r\n// Same class as is-printable-key.ts: single code points the matcher can hit via `KeyboardEvent.key`.\r\nconst SINGLE_CODE_POINT = /[\\p{L}\\p{N}\\p{S}\\p{P}]/u;\r\n\r\n/**\r\n * Issues for one binding string; empty means valid. Accepts single code points (printable or\r\n * space, any case) and exact-case stable named keys — exactly what the matcher can hit; the\r\n * named-key set is a superset of `KeyPart`'s autocomplete vocabulary.\r\n */\r\nexport function validateKeyBinding(binding: string): string[] {\r\n  const issues: string[] = [];\r\n  const parts = binding.split(\"+\");\r\n  const key = parts.at(-1) ?? \"\";\r\n  const modifiers = parts.slice(0, -1);\r\n\r\n  for (const token of modifiers) {\r\n    if (!token) issues.push(\"has an empty modifier part\");\r\n    else if (!MODIFIER_TOKENS.includes(token)) issues.push(`has unknown modifier \"${token}\"`);\r\n  }\r\n  const seen = new Set<string>();\r\n  for (const token of modifiers) {\r\n    if (!MODIFIER_TOKENS.includes(token)) continue;\r\n    if (seen.has(token)) issues.push(`repeats modifier \"${token}\"`);\r\n    seen.add(token);\r\n  }\r\n\r\n  if (!key) {\r\n    issues.push(\"has no key after the last \\\"+\\\"\");\r\n  } else if (Array.from(key).length === 1) {\r\n    if (key !== \" \" && !SINGLE_CODE_POINT.test(key)) issues.push(`key \"${key}\" is not a printable character`);\r\n  } else if (!NAMED_KEYS.has(key)) {\r\n    issues.push(`key \"${key}\" is not a known key name (named keys match exact case, e.g. \"ArrowUp\")`);\r\n  }\r\n  return issues;\r\n}\r\n\r\nconst warnedBindings = new Set<string>();\r\n\r\n/**\r\n * Warns in development for every invalid binding, once per unique `action:binding` — a typo\r\n * never matches at runtime, so it must not pass silently (the type's `(string & {})` escape\r\n * hatch cannot reject it).\r\n */\r\nexport function validateKeymap(keymap: Keymap): void {\r\n  if (!isDev()) return;\r\n  for (const [action, bindings] of Object.entries(keymap)) {\r\n    for (const binding of bindings ?? []) {\r\n      const issues = validateKeyBinding(binding);\r\n      if (!issues.length) continue;\r\n      const dedupe = `${action}:${binding}`;\r\n      if (warnedBindings.has(dedupe)) continue;\r\n      warnedBindings.add(dedupe);\r\n      warnDev(`keymap binding \"${binding}\" for action \"${action}\": ${issues.join(\"; \")}`);\r\n    }\r\n  }\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/keyboard/key-syntax.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/keyboard/match-keymap.ts",
      "content": "import type { GridAction, Keymap } from \"../types\";\r\n\r\n/** Parsed form of a binding string (see `Keymap` in ../types for the string format). */\r\nexport type ParsedBinding = {\r\n  mod: boolean;\r\n  /** Literal physical Ctrl key, distinct from `mod` (which is Cmd on mac); keymap-local extension for bindings like Ctrl+Space that must not resolve to Cmd on macOS. */\r\n  ctrl: boolean;\r\n  shift: boolean;\r\n  alt: boolean;\r\n  key: string;\r\n};\r\n\r\n/** Splits a binding string like \"mod+shift+ArrowUp\" into its modifier flags and key. */\r\nexport function parseBinding(binding: string): ParsedBinding {\r\n  const parts = binding.split(\"+\");\r\n  // last part is always the key; a literal \"+\" key never occurs in this keymap\r\n  const key = parts[parts.length - 1] ?? \"\";\r\n  const modifiers = parts.slice(0, -1);\r\n  return {\r\n    mod: modifiers.includes(\"mod\"),\r\n    ctrl: modifiers.includes(\"ctrl\"),\r\n    shift: modifiers.includes(\"shift\"),\r\n    alt: modifiers.includes(\"alt\"),\r\n    key,\r\n  };\r\n}\r\n\r\n/** Minimal event shape the matcher needs; matches both DOM and synthetic KeyboardEvents. */\r\nexport type KeymapEvent = {\r\n  key: string;\r\n  ctrlKey: boolean;\r\n  metaKey: boolean;\r\n  shiftKey: boolean;\r\n  altKey: boolean;\r\n};\r\n\r\nfunction keysEqual(eventKey: string, bindingKey: string): boolean {\r\n  if (eventKey.length === 1 && bindingKey.length === 1) {\r\n    return eventKey.toLowerCase() === bindingKey.toLowerCase();\r\n  }\r\n  return eventKey === bindingKey;\r\n}\r\n\r\nfunction matchesBinding(event: KeymapEvent, binding: ParsedBinding, isMac: boolean): boolean {\r\n  if (!keysEqual(event.key, binding.key)) return false;\r\n  if (event.shiftKey !== binding.shift) return false;\r\n  if (event.altKey !== binding.alt) return false;\r\n\r\n  // literal ctrl: bypasses mod/Cmd resolution entirely, e.g. Ctrl+Space must stay Ctrl on mac\r\n  if (binding.ctrl) {\r\n    return event.ctrlKey && !event.metaKey;\r\n  }\r\n\r\n  const modPressed = isMac ? event.metaKey : event.ctrlKey;\r\n  const otherPressed = isMac ? event.ctrlKey : event.metaKey;\r\n  if (otherPressed) return false;\r\n  if (modPressed !== binding.mod) return false;\r\n  return true;\r\n}\r\n\r\n/**\r\n * Resolves a keyboard event to a `GridAction` using `keymap`, or null if unbound.\r\n * When several actions share a binding, the most modifier-specific one wins.\r\n */\r\nexport function matchKeymap(\r\n  event: KeymapEvent,\r\n  keymap: Keymap,\r\n  isMac: boolean,\r\n): GridAction | null {\r\n  let best: { action: GridAction; specificity: number } | null = null;\r\n\r\n  for (const [action, bindings] of Object.entries(keymap) as [GridAction, string[] | undefined][]) {\r\n    if (!bindings) continue;\r\n    for (const raw of bindings) {\r\n      const parsed = parseBinding(raw);\r\n      if (!matchesBinding(event, parsed, isMac)) continue;\r\n      const specificity =\r\n        (parsed.mod ? 1 : 0) + (parsed.ctrl ? 1 : 0) + (parsed.shift ? 1 : 0) + (parsed.alt ? 1 : 0);\r\n      if (!best || specificity > best.specificity) {\r\n        best = { action, specificity };\r\n      }\r\n    }\r\n  }\r\n\r\n  return best?.action ?? null;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/keyboard/match-keymap.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/keyboard/global-shortcuts.ts",
      "content": "import type { GridAction, Keymap } from \"../types\";\r\nimport { matchKeymap } from \"./match-keymap\";\r\nimport { isMacPlatform } from \"./platform\";\r\n\r\n/**\r\n * An action the global-shortcut layer may resolve and dispatch while DOM focus is OUTSIDE the\r\n * grid. The layer is action-generic (the gate + `matchKeymap` + the grid's own dispatch path);\r\n * the SAFE DEFAULT is `undo`/`redo` only. Extension rule for anything added to\r\n * {@link DataGridGlobalShortcutActions}: every binding of the added action must be mod-prefixed\r\n * (no ctrl-only/alt-only combos — Ctrl+Space is the Windows IME toggle, Ctrl+Q the browser quit,\r\n * etc.), and the action must make sense without grid focus (a navigation move scrolls the grid's\r\n * own container, never the page).\r\n */\r\nexport type GlobalShortcutAction = GridAction;\r\n\r\n/**\r\n * Actions the global-shortcut window layer may enable, each keyed `true`. Core lists the\r\n * actions it ships; an add-on that owns a `GridAction` (undo/redo, fill) augments this\r\n * interface via `declare module` to offer its own flag — see `data-grid-history`'s and\r\n * `data-grid-fill`'s barrels for the augmentation.\r\n */\r\nexport interface DataGridGlobalShortcutActions {\r\n  selectAll: true;\r\n  insertRowAbove: true;\r\n  insertRowBelow: true;\r\n  duplicateRow: true;\r\n  deleteRows: true;\r\n}\r\n\r\n/**\r\n * Opt-in global-shortcut config: one `true` flag per enabled action. Keys are constrained to\r\n * `GridAction` so an add-on's augmentation of {@link DataGridGlobalShortcutActions} can never\r\n * compile in a key `dispatchGridAction` cannot resolve. No flag set (or the config omitted)\r\n * enables the safe default — `undo` and `redo` only; any flag set enables exactly the flagged\r\n * actions.\r\n */\r\nexport type GlobalShortcutsConfig = {\r\n  [K in keyof DataGridGlobalShortcutActions & GridAction]?: true;\r\n};\r\n\r\n/** The actions enabled by default: undo/redo only (the safe set — see {@link GlobalShortcutAction}). */\r\nconst DEFAULT_GLOBAL_ACTIONS: readonly GridAction[] = [\"undo\", \"redo\"];\r\n\r\n/** The enabled actions of a config: any `true` flag selects exactly the flagged actions. No flag (or no config) enables the default set. */\r\nexport function enabledGlobalActions(config?: GlobalShortcutsConfig): readonly GlobalShortcutAction[] {\r\n  const flagEnabled = Object.keys(config ?? {}).filter(\r\n    (key) => config?.[key as keyof GlobalShortcutsConfig] === true,\r\n  ) as GlobalShortcutAction[];\r\n  return flagEnabled.length > 0 ? flagEnabled : [...DEFAULT_GLOBAL_ACTIONS];\r\n}\r\n\r\n/** Minimal event shape for {@link resolveGlobalShortcut} — the matcher's `KeymapEvent` plus the two DOM flags the gate needs. */\r\nexport type GlobalShortcutEvent = {\r\n  key: string;\r\n  ctrlKey: boolean;\r\n  metaKey: boolean;\r\n  shiftKey: boolean;\r\n  altKey: boolean;\r\n  isComposing: boolean;\r\n  defaultPrevented: boolean;\r\n};\r\n\r\n/** DOM-derived facts the hook computes per event; keeps the gate pure. */\r\nexport type GlobalShortcutContext = {\r\n  keymap: Keymap;\r\n  isMac: boolean;\r\n  actions: readonly GridAction[];\r\n  /** The event target is `INPUT`/`TEXTAREA`/`SELECT` or `[contenteditable=\"true\"]`. */\r\n  targetIsEditable: boolean;\r\n  /** The event target is inside ANY `role=\"grid\"` element (including another grid). */\r\n  targetInAnyGrid: boolean;\r\n  /** This grid is the last focused opted-in grid (the multi-grid tie-break). */\r\n  ownsFocus: boolean;\r\n};\r\n\r\n/**\r\n * The gate for global shortcuts, pure and unit-tested (same posture as {@link matchKeymap}).\r\n * Rules run in order, any hit returns `null`:\r\n * 1. `isComposing` — IME composition owns the keys.\r\n * 2. `defaultPrevented` — the in-grid handler (earlier in the bubble) or the page already handled it.\r\n * 3. `targetIsEditable` — the field's native undo/redo wins; never hijack it.\r\n * 4. `targetInAnyGrid` — a grid's in-grid handler owns the key (this also excludes other grids).\r\n * 5. `ownsFocus` — multi-grid: only the last focused opted-in grid proceeds.\r\n * 6. The keymap resolves to one of the enabled actions.\r\n */\r\nexport function resolveGlobalShortcut(\r\n  event: GlobalShortcutEvent,\r\n  ctx: GlobalShortcutContext,\r\n): GlobalShortcutAction | null {\r\n  if (event.isComposing) return null;\r\n  if (event.defaultPrevented) return null;\r\n  if (ctx.targetIsEditable) return null;\r\n  if (ctx.targetInAnyGrid) return null;\r\n  if (!ctx.ownsFocus) return null;\r\n  const action = matchKeymap(event, ctx.keymap, ctx.isMac);\r\n  if (action === null) return null;\r\n  return ctx.actions.includes(action) ? action : null;\r\n}\r\n\r\n/** The current platform check, re-exported so consumers/tests never drift from the hook's view. */\r\nexport { isMacPlatform };\r\n",
      "type": "registry:component",
      "target": "components/data-grid/keyboard/global-shortcuts.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/keyboard/platform.ts",
      "content": "/** The platform check the keymap matcher needs: `mod` resolves to Cmd only on macOS. */\r\nexport function isMacPlatform(): boolean {\r\n  if (typeof navigator === \"undefined\") return false;\r\n  // userAgentData is the modern replacement for the deprecated `platform`; both are checked for jsdom/older browsers.\r\n  const uaData = (navigator as Navigator & { userAgentData?: { platform?: string } }).userAgentData;\r\n  const platform = uaData?.platform ?? navigator.platform ?? \"\";\r\n  return /mac/i.test(platform);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/keyboard/platform.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/keyboard/use-data-grid-global-shortcuts.ts",
      "content": "\"use client\";\r\n\r\nimport { useEffect, useMemo, useRef, type ReactNode } from \"react\";\r\nimport type { GlobalShortcutsConfig } from \"./global-shortcuts\";\r\nimport { enabledGlobalActions, isMacPlatform, resolveGlobalShortcut } from \"./global-shortcuts\";\r\nimport { isPrintableKey } from \"./is-printable-key\";\r\nimport { useDataGridContainer } from \"../interaction/use-data-grid-container\";\r\nimport { dispatchGridAction } from \"../interaction/use-grid-interaction\";\r\nimport { useDataGridRootContext } from \"../layout-context\";\r\nimport {\r\n  useDataGridActions,\r\n  useDataGridFillHandlers,\r\n  useDataGridKeymap,\r\n  useDataGridReadOnly,\r\n  useDataGridStoreApi,\r\n} from \"../store\";\r\n\r\n/** Multi-grid focus owner: the id of the last opted-in grid whose container received `focusin`. */\r\nlet lastFocusedGrid: number | null = null;\r\nlet nextGridId = 0;\r\n\r\n/**\r\n * While DOM focus is outside the grid, dispatches the enabled global actions (default: undo/redo)\r\n * on window keydown. Each action runs through the grid's own keymap-dispatch path\r\n * ({@link dispatchGridAction}), so it behaves exactly like its in-grid binding — same scrolling,\r\n * same store-side guards. Mount inside `DataGridRoot`; prefer {@link DataGridGlobalShortcuts}.\r\n */\r\nexport function useDataGridGlobalShortcuts(config?: GlobalShortcutsConfig): void {\r\n  const containerRef = useDataGridContainer();\r\n  const storeApi = useDataGridStoreApi();\r\n  const keymap = useDataGridKeymap();\r\n  const actions = useDataGridActions();\r\n  const readOnly = useDataGridReadOnly();\r\n  const fillHandlers = useDataGridFillHandlers();\r\n  const { rowHeight, interaction } = useDataGridRootContext();\r\n\r\n  // content-based dep (not the `config` object) so an inline config doesn't recompute per render;\r\n  // sorted keys since the flag set is open (add-ons augment it) so no fixed list of deps works\r\n  const configKey = config ? Object.keys(config).filter((k) => config[k as keyof GlobalShortcutsConfig]).sort().join(\"|\") : \"\";\r\n  const enabled = useMemo(\r\n    () => enabledGlobalActions(config),\r\n    // oxlint-disable-next-line react-hooks/exhaustive-deps -- deliberate content-based dep (configKey)\r\n    [configKey],\r\n  );\r\n\r\n  // the window handler reads fresh values per event without re-attaching on every config change\r\n  const latestRef = useRef({ keymap, enabled, actions, readOnly, fillHandlers, scrollCellIntoView: interaction.scrollCellIntoView, rowHeight });\r\n  latestRef.current = { keymap, enabled, actions, readOnly, fillHandlers, scrollCellIntoView: interaction.scrollCellIntoView, rowHeight };\r\n\r\n  // one id per mount: a fresh id on every effect run would drop the grid's focus ownership\r\n  // (lastFocusedGrid would no longer match) whenever the effect re-runs\r\n  const gridIdRef = useRef(0);\r\n  if (gridIdRef.current === 0) gridIdRef.current = ++nextGridId;\r\n\r\n  useEffect(() => {\r\n    const container = containerRef.current;\r\n    if (!container) return;\r\n    const gridId = gridIdRef.current;\r\n\r\n    function onFocusIn() {\r\n      lastFocusedGrid = gridId;\r\n    }\r\n\r\n    function onKeyDown(event: KeyboardEvent) {\r\n      const latest = latestRef.current;\r\n      const target = event.target;\r\n      const targetIsEditable =\r\n        target instanceof HTMLElement &&\r\n        (target.tagName === \"INPUT\" ||\r\n          target.tagName === \"TEXTAREA\" ||\r\n          target.tagName === \"SELECT\" ||\r\n          target.isContentEditable);\r\n      const targetInAnyGrid = target instanceof Element ? target.closest('[role=\"grid\"]') !== null : false;\r\n      const action = resolveGlobalShortcut(event, {\r\n        keymap: latest.keymap,\r\n        isMac: isMacPlatform(),\r\n        actions: latest.enabled,\r\n        targetIsEditable,\r\n        targetInAnyGrid,\r\n        ownsFocus: lastFocusedGrid === gridId,\r\n      });\r\n      if (action === null) return;\r\n      const fill = latest.fillHandlers;\r\n      dispatchGridAction({\r\n        action,\r\n        actions: latest.actions,\r\n        storeApi,\r\n        scrollRef: containerRef,\r\n        scrollCellIntoView: latest.scrollCellIntoView,\r\n        rowHeight: latest.rowHeight,\r\n        readOnly: latest.readOnly,\r\n        triggerKey: isPrintableKey(event) ? event.key : undefined,\r\n        preventDefault: () => event.preventDefault(),\r\n        fillDown: fill?.fillDown,\r\n        fillRight: fill?.fillRight,\r\n        cancelFillDrag: fill?.cancelFillDrag,\r\n      });\r\n    }\r\n\r\n    container.addEventListener(\"focusin\", onFocusIn);\r\n    window.addEventListener(\"keydown\", onKeyDown);\r\n    return () => {\r\n      container.removeEventListener(\"focusin\", onFocusIn);\r\n      window.removeEventListener(\"keydown\", onKeyDown);\r\n      if (lastFocusedGrid === gridId) lastFocusedGrid = null;\r\n    };\r\n  }, [containerRef, storeApi]);\r\n}\r\n\r\n/** Props for {@link DataGridGlobalShortcuts}: the {@link GlobalShortcutsConfig} flags. Omit them all to keep the safe default (undo/redo only). */\r\nexport type DataGridGlobalShortcutsProps = GlobalShortcutsConfig;\r\n\r\n/** Mounts the global-shortcut layer; renders nothing. Must sit inside `DataGridRoot`. */\r\nexport function DataGridGlobalShortcuts(props: DataGridGlobalShortcutsProps): ReactNode {\r\n  useDataGridGlobalShortcuts(props);\r\n  return null;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/keyboard/use-data-grid-global-shortcuts.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/labels.ts",
      "content": "/**\r\n * i18n via a typed labels object: every user-facing string across core + add-ons lives here with\r\n * English defaults. Consumers pass a `labels` prop (deep-merged over {@link DEFAULT_LABELS}) to\r\n * `DataGridProvider`; no i18n library dependency — wiring translated strings into\r\n * `DEFAULT_LABELS`'s shape is the consumer's job.\r\n */\r\n\r\nimport type { GridAction } from \"./types\";\r\n\r\n/** Toolbar add-on strings: quick-search, filter menu, columns menu. */\r\nexport type DataGridToolbarLabels = {\r\n  searchPlaceholder: string;\r\n  searchAriaLabel: string;\r\n  searchPreviousMatch: string;\r\n  searchNextMatch: string;\r\n  /** e.g. \"3/17\". */\r\n  searchMatches: (current: number, total: number) => string;\r\n  /** e.g. \"3/1000+\" when the match count hit the cap. */\r\n  searchMatchesCapped: (current: number) => string;\r\n  /** No search performed yet / zero matches (\"0/0\"). */\r\n  searchNoMatches: string;\r\n  filter: string;\r\n  filterAriaLabel: string;\r\n  filterColumnAriaLabel: string;\r\n  filterOperatorAriaLabel: string;\r\n  filterValuePlaceholder: string;\r\n  filterValueAriaLabel: string;\r\n  /** Trigger text for the `isAnyOf` multi-choice input; `count` is how many choices are selected. */\r\n  filterValueAnyOfSummary: (count: number) => string;\r\n  /** Option text for a `checkbox`-column filter's value select. */\r\n  filterValueTrue: string;\r\n  filterValueFalse: string;\r\n  /** aria-label for the `isBetween` range's minimum-value input. */\r\n  filterValueFromAriaLabel: string;\r\n  /** aria-label for the `isBetween` range's maximum-value input. */\r\n  filterValueToAriaLabel: string;\r\n  removeFilterAriaLabel: string;\r\n  /** aria-label for a filter row's drag-handle grip (also moved via ArrowUp/ArrowDown while focused). */\r\n  reorderFilterAriaLabel: string;\r\n  /** Polite live-region announcement after a filter row moves; `column` is the filter's column label, `position`/`total` are 1-based. */\r\n  filterReorderAnnouncement: (column: string, position: number, total: number) => string;\r\n  addFilter: string;\r\n  clearFilters: string;\r\n  noFiltersApplied: string;\r\n  /** Static \"Where\" label shown in the first filter row's join cell (tablecn parity). */\r\n  filterWhere: string;\r\n  /** aria-label for the AND/OR join-operator select, shown once 2+ filters are active. */\r\n  joinOperatorAriaLabel: string;\r\n  joinOperatorAnd: string;\r\n  joinOperatorOr: string;\r\n  columns: string;\r\n  columnsAriaLabel: string;\r\n};\r\n\r\n/** Sort-list add-on strings (`data-grid-sort-list`): toolbar sort button + popover of applied sorts. */\r\nexport type DataGridSortLabels = {\r\n  sort: string;\r\n  sortAriaLabel: string;\r\n  columnAriaLabel: string;\r\n  directionAriaLabel: string;\r\n  ascending: string;\r\n  descending: string;\r\n  removeSortAriaLabel: string;\r\n  /** aria-label for a sort row's drag-handle grip (also moved via ArrowUp/ArrowDown while focused). */\r\n  reorderSortAriaLabel: string;\r\n  /** Polite live-region announcement after a sort row moves; `column` is the sort's column label, `position`/`total` are 1-based. */\r\n  sortReorderAnnouncement: (column: string, position: number, total: number) => string;\r\n  addSort: string;\r\n  clearSorts: string;\r\n  noSortsApplied: string;\r\n};\r\n\r\n/** Per-column filter operator display names (toolbar filter menu's operator `<Select>`). */\r\nexport type DataGridFilterOperatorLabels = {\r\n  contains: string;\r\n  notContains: string;\r\n  equals: string;\r\n  notEquals: string;\r\n  startsWith: string;\r\n  endsWith: string;\r\n  empty: string;\r\n  notEmpty: string;\r\n  gt: string;\r\n  gte: string;\r\n  lt: string;\r\n  lte: string;\r\n  isBetween: string;\r\n  isAnyOf: string;\r\n};\r\n\r\n/** Cell/header right-click context menu + header dropdown menu strings (shared verbatim between the two surfaces). */\r\nexport type DataGridContextMenuLabels = {\r\n  cut: string;\r\n  copy: string;\r\n  paste: string;\r\n  /** Tooltip shown on a blocked paste item (no clipboard-read permission granted). */\r\n  pasteBlocked: string;\r\n  clearContents: string;\r\n  insertRowAbove: string;\r\n  insertRowBelow: string;\r\n  duplicateRow: string;\r\n  duplicateRows: (count: number) => string;\r\n  deleteRow: string;\r\n  deleteRows: (count: number) => string;\r\n  sortAsc: string;\r\n  sortDesc: string;\r\n  clearSort: string;\r\n  pinLeft: string;\r\n  pinRight: string;\r\n  unpin: string;\r\n  hideColumn: string;\r\n  autosize: string;\r\n  /** aria-label for the per-column header dropdown's ghost chevron trigger; `column` is the header text. */\r\n  columnMenuAriaLabel: (column: string) => string;\r\n};\r\n\r\n/** Keybindings-dialog add-on strings: title/description + the section headings and native-clipboard rows. */\r\nexport type DataGridKeybindingsLabels = {\r\n  title: string;\r\n  description: string;\r\n  categories: {\r\n    navigation: string;\r\n    selection: string;\r\n    editing: string;\r\n    clipboardFill: string;\r\n    history: string;\r\n    other: string;\r\n  };\r\n  /** Native browser clipboard shortcuts listed alongside the keymap (not GridActions). */\r\n  nativeCopy: string;\r\n  nativeCut: string;\r\n  nativePaste: string;\r\n  /** Per-`GridAction` row label shown in the dialog; consumer-added actions absent here fall back to a humanized action name (see `labelForAction`). */\r\n  actions: Partial<Record<GridAction, string>>;\r\n};\r\n\r\n/** Row-marker column strings (select-all header checkbox, per-row checkbox, reorder handle). */\r\nexport type DataGridMarkerLabels = {\r\n  selectAll: string;\r\n  /** `rowNumber` is 1-based (matches the visible row-number marker). */\r\n  selectRow: (rowNumber: number) => string;\r\n  /** aria-label for the reorder-mode marker handle; `rowNumber` is 1-based. */\r\n  reorderRow: (rowNumber: number) => string;\r\n  /** Polite live-region announcement after a row move; `rowNumber` is the dragged row's 1-based number BEFORE the move, `position`/`total` are 1-based. */\r\n  reorderAnnouncement: (rowNumber: number, position: number, total: number) => string;\r\n};\r\n\r\n/** Core grid strings not owned by a specific add-on. */\r\nexport type DataGridGridLabels = {\r\n  /** Shown when the view has zero rows (post filter/search); the `emptyState` prop on `DataGridRoot`, when provided, wins over this default. */\r\n  emptyState: string;\r\n  /** aria-label for the loading skeleton region (`loading && rowCount === 0`); also used as the indeterminate progress bar's aria-label when rows are present. */\r\n  loading: string;\r\n  /** Placeholder for the date editor's typed input (default hints the ISO `yyyy-mm-dd` format). */\r\n  datePlaceholder: string;\r\n};\r\n\r\n/** Import/export add-on strings (`data-grid-io`): export dropdown + import dialog. */\r\nexport type DataGridIOLabels = {\r\n  exportButtonAriaLabel: string;\r\n  exportXlsx: string;\r\n  exportCsv: string;\r\n  importButton: string;\r\n  importDialogTitle: string;\r\n  importDialogDescription: string;\r\n  chooseFile: string;\r\n  noFileChosen: string;\r\n  /** Caption on the compact drop zone shown once a file is already chosen, in the column-mapping step. */\r\n  replaceFile: string;\r\n  /** Fallback column header in the mapping table when \"First row is a header\" is unchecked; `index` is 1-based. */\r\n  columnFallback: (index: number) => string;\r\n  delimiter: string;\r\n  delimiterComma: string;\r\n  delimiterSemicolon: string;\r\n  delimiterTab: string;\r\n  hasHeaderRow: string;\r\n  mapColumns: string;\r\n  /** One-line hint under the mapping grid explaining why mapped columns disappear from other selects. */\r\n  mapColumnsHint: string;\r\n  mapColumnAriaLabel: (importColumn: string) => string;\r\n  skipColumn: string;\r\n  /** aria-label for the per-row quick-skip (X) button next to the mapping Select. */\r\n  skipColumnQuick: string;\r\n  preview: string;\r\n  previewTruncated: (shown: number, total: number) => string;\r\n  /** Label of the sheet picker shown when an imported workbook has more than one sheet. */\r\n  sheet: string;\r\n  import: string;\r\n  cancel: string;\r\n  errorParseFailed: string;\r\n  errorNoRows: string;\r\n  errorUnsupportedFile: string;\r\n  /** Shown under the mapping grid when the import build rejected cells; `count` is the rejected-cell count. */\r\n  importRejectedCells: (count: number) => string;\r\n  /** Shown when the `onImport` callback rejected; the dialog stays open and the import can be retried. */\r\n  importMergeFailed: string;\r\n};\r\n\r\n/** Pagination add-on strings (`data-grid-pagination`): footer prev/next, page numbers, page-size select, range label. */\r\nexport type DataGridPaginationLabels = {\r\n  firstPage: string;\r\n  previousPage: string;\r\n  nextPage: string;\r\n  lastPage: string;\r\n  /** aria-label for a numbered page button; `page` is 1-based. */\r\n  pageAriaLabel: (page: number) => string;\r\n  pageSizeAriaLabel: string;\r\n  /** Option text in the page-size select, e.g. \"25 / page\". */\r\n  pageSizeOption: (size: number) => string;\r\n  /** Footer range label, e.g. \"1–25 of 240\". `total === 0` renders {@link DataGridPaginationLabels.rangeEmpty} instead. */\r\n  range: (from: number, to: number, total: number) => string;\r\n  rangeEmpty: string;\r\n};\r\n\r\n/** Every user-facing default string across core + every add-on, grouped by owning surface. */\r\nexport interface DataGridLabels {\r\n  toolbar: DataGridToolbarLabels;\r\n  sort: DataGridSortLabels;\r\n  filterOperators: DataGridFilterOperatorLabels;\r\n  contextMenu: DataGridContextMenuLabels;\r\n  keybindings: DataGridKeybindingsLabels;\r\n  markers: DataGridMarkerLabels;\r\n  grid: DataGridGridLabels;\r\n  io: DataGridIOLabels;\r\n  pagination: DataGridPaginationLabels;\r\n}\r\n\r\n/** English defaults — every string in the shipped UI today, verbatim (so adopting `labels` never changes visible copy without an explicit override). */\r\nexport const DEFAULT_LABELS: DataGridLabels = {\r\n  toolbar: {\r\n    searchPlaceholder: \"Search…\",\r\n    searchAriaLabel: \"Search grid\",\r\n    searchPreviousMatch: \"Previous match\",\r\n    searchNextMatch: \"Next match\",\r\n    searchMatches: (current, total) => `${current}/${total}`,\r\n    searchMatchesCapped: (current) => `${current}/1000+`,\r\n    searchNoMatches: \"0/0\",\r\n    filter: \"Filter\",\r\n    filterAriaLabel: \"Filters\",\r\n    filterColumnAriaLabel: \"Filter column\",\r\n    filterOperatorAriaLabel: \"Filter operator\",\r\n    filterValuePlaceholder: \"Value\",\r\n    filterValueAriaLabel: \"Filter value\",\r\n    filterValueAnyOfSummary: (count) => (count === 0 ? \"Any value\" : `${count} selected`),\r\n    filterValueTrue: \"true\",\r\n    filterValueFalse: \"false\",\r\n    filterValueFromAriaLabel: \"Filter value from\",\r\n    filterValueToAriaLabel: \"Filter value to\",\r\n    removeFilterAriaLabel: \"Remove filter\",\r\n    reorderFilterAriaLabel: \"Reorder filter\",\r\n    filterReorderAnnouncement: (column, position, total) => `${column} filter moved to position ${position} of ${total}`,\r\n    addFilter: \"Add filter\",\r\n    clearFilters: \"Clear all\",\r\n    noFiltersApplied: \"No filters applied.\",\r\n    filterWhere: \"Where\",\r\n    joinOperatorAriaLabel: \"Match\",\r\n    joinOperatorAnd: \"And\",\r\n    joinOperatorOr: \"Or\",\r\n    columns: \"Columns\",\r\n    columnsAriaLabel: \"Columns\",\r\n  },\r\n  sort: {\r\n    sort: \"Sort\",\r\n    sortAriaLabel: \"Sorts\",\r\n    columnAriaLabel: \"Sort column\",\r\n    directionAriaLabel: \"Sort direction\",\r\n    ascending: \"Ascending\",\r\n    descending: \"Descending\",\r\n    removeSortAriaLabel: \"Remove sort\",\r\n    reorderSortAriaLabel: \"Reorder sort\",\r\n    sortReorderAnnouncement: (column, position, total) => `${column} sort moved to position ${position} of ${total}`,\r\n    addSort: \"Add sort\",\r\n    clearSorts: \"Clear all\",\r\n    noSortsApplied: \"No sorts applied.\",\r\n  },\r\n  filterOperators: {\r\n    contains: \"contains\",\r\n    notContains: \"does not contain\",\r\n    equals: \"equals\",\r\n    notEquals: \"does not equal\",\r\n    startsWith: \"starts with\",\r\n    endsWith: \"ends with\",\r\n    empty: \"is empty\",\r\n    notEmpty: \"is not empty\",\r\n    gt: \"greater than\",\r\n    gte: \"greater than or equal\",\r\n    lt: \"less than\",\r\n    lte: \"less than or equal\",\r\n    isBetween: \"is between\",\r\n    isAnyOf: \"is any of\",\r\n  },\r\n  contextMenu: {\r\n    cut: \"Cut\",\r\n    copy: \"Copy\",\r\n    paste: \"Paste\",\r\n    pasteBlocked: \"requires clipboard permission — use Ctrl+V\",\r\n    clearContents: \"Clear contents\",\r\n    insertRowAbove: \"Insert row above\",\r\n    insertRowBelow: \"Insert row below\",\r\n    duplicateRow: \"Duplicate row\",\r\n    duplicateRows: (count) => (count > 1 ? \"Duplicate rows\" : \"Duplicate row\"),\r\n    deleteRow: \"Delete row\",\r\n    deleteRows: (count) => (count > 1 ? \"Delete rows\" : \"Delete row\"),\r\n    sortAsc: \"Sort ascending\",\r\n    sortDesc: \"Sort descending\",\r\n    clearSort: \"Clear sort\",\r\n    pinLeft: \"Pin left\",\r\n    pinRight: \"Pin right\",\r\n    unpin: \"Unpin\",\r\n    hideColumn: \"Hide column\",\r\n    autosize: \"Autosize column\",\r\n    columnMenuAriaLabel: (column) => `${column} column menu`,\r\n  },\r\n  keybindings: {\r\n    title: \"Keyboard shortcuts\",\r\n    description: \"Every shortcut currently bound in this grid.\",\r\n    categories: {\r\n      navigation: \"Navigation\",\r\n      selection: \"Selection\",\r\n      editing: \"Editing\",\r\n      clipboardFill: \"Clipboard & Fill\",\r\n      history: \"History\",\r\n      other: \"Other\",\r\n    },\r\n    nativeCopy: \"Copy\",\r\n    nativeCut: \"Cut\",\r\n    nativePaste: \"Paste\",\r\n    actions: {\r\n      moveUp: \"Move up\",\r\n      moveDown: \"Move down\",\r\n      moveLeft: \"Move left\",\r\n      moveRight: \"Move right\",\r\n      retainMoveUp: \"Move up (keep selection)\",\r\n      retainMoveDown: \"Move down (keep selection)\",\r\n      retainMoveLeft: \"Move left (keep selection)\",\r\n      retainMoveRight: \"Move right (keep selection)\",\r\n      scrollActiveIntoView: \"Scroll active cell into view\",\r\n      moveRowStart: \"Move to row start\",\r\n      moveRowEnd: \"Move to row end\",\r\n      jumpUp: \"Jump to edge (up)\",\r\n      jumpDown: \"Jump to edge (down)\",\r\n      jumpLeft: \"Jump to edge (left)\",\r\n      jumpRight: \"Jump to edge (right)\",\r\n      moveFirstCell: \"Move to first cell\",\r\n      moveLastCell: \"Move to last cell\",\r\n      pageUp: \"Page up\",\r\n      pageDown: \"Page down\",\r\n\r\n      extendUp: \"Extend selection up\",\r\n      extendDown: \"Extend selection down\",\r\n      extendLeft: \"Extend selection left\",\r\n      extendRight: \"Extend selection right\",\r\n      extendJumpUp: \"Extend selection to edge (up)\",\r\n      extendJumpDown: \"Extend selection to edge (down)\",\r\n      extendJumpLeft: \"Extend selection to edge (left)\",\r\n      extendJumpRight: \"Extend selection to edge (right)\",\r\n      extendFirstCell: \"Extend selection to first cell\",\r\n      extendLastCell: \"Extend selection to last cell\",\r\n      selectRow: \"Select row\",\r\n      selectColumn: \"Select column\",\r\n      selectAll: \"Select all\",\r\n\r\n      edit: \"Edit cell\",\r\n      editReplace: \"Type to replace\",\r\n      // \"(editor)\": these fire only while a cell editor is open (the keymap is ignored during an edit)\r\n      commitDown: \"Commit and move down (editor)\",\r\n      commitUp: \"Commit and move up (editor)\",\r\n      commitRight: \"Commit and move right (editor)\",\r\n      commitLeft: \"Commit and move left (editor)\",\r\n      cancel: \"Cancel edit\",\r\n      deleteContents: \"Delete contents\",\r\n      insertRowBelow: \"Insert row below\",\r\n      insertRowAbove: \"Insert row above\",\r\n      duplicateRow: \"Duplicate row\",\r\n      deleteRows: \"Delete row(s)\",\r\n\r\n      fillDown: \"Fill down\",\r\n      fillRight: \"Fill right\",\r\n\r\n      undo: \"Undo\",\r\n      redo: \"Redo\",\r\n    },\r\n  },\r\n  markers: {\r\n    selectAll: \"Select all rows\",\r\n    selectRow: (rowNumber) => `Select row ${rowNumber}`,\r\n    reorderRow: (rowNumber) => `Reorder row ${rowNumber}`,\r\n    reorderAnnouncement: (rowNumber, position, total) => `Row ${rowNumber} moved to position ${position} of ${total}`,\r\n  },\r\n  grid: {\r\n    emptyState: \"No rows\",\r\n    loading: \"Loading…\",\r\n    datePlaceholder: \"yyyy-mm-dd\",\r\n  },\r\n  io: {\r\n    exportButtonAriaLabel: \"Export\",\r\n    exportXlsx: \"Export as Excel (.xlsx)\",\r\n    exportCsv: \"Export as CSV\",\r\n    importButton: \"Import\",\r\n    importDialogTitle: \"Import file\",\r\n    importDialogDescription: \"Choose a CSV or Excel file, then map its columns to the grid.\",\r\n    chooseFile: \"Choose file\",\r\n    noFileChosen: \"No file chosen\",\r\n    replaceFile: \"Drop or click to replace\",\r\n    columnFallback: (index) => `Column ${index}`,\r\n    delimiter: \"Delimiter\",\r\n    delimiterComma: \"Comma (,)\",\r\n    delimiterSemicolon: \"Semicolon (;)\",\r\n    delimiterTab: \"Tab\",\r\n    hasHeaderRow: \"First row is a header\",\r\n    mapColumns: \"Map columns\",\r\n    mapColumnsHint: \"Each grid column can only be mapped from one source column.\",\r\n    mapColumnAriaLabel: (importColumn) => `Map \"${importColumn}\" to grid column`,\r\n    skipColumn: \"— Skip —\",\r\n    skipColumnQuick: \"Skip column\",\r\n    preview: \"Preview\",\r\n    previewTruncated: (shown, total) => `Showing ${shown} of ${total} rows`,\r\n    sheet: \"Sheet\",\r\n    import: \"Import\",\r\n    cancel: \"Cancel\",\r\n    errorParseFailed: \"Could not read this file.\",\r\n    errorNoRows: \"No rows found in this file.\",\r\n    errorUnsupportedFile: \"Unsupported file type — choose a .csv, .xlsx, or .xls file.\",\r\n    importRejectedCells: (count) => `${count} cell${count === 1 ? \"\" : \"s\"} failed validation and ${count === 1 ? \"was\" : \"were\"} left empty.`,\r\n    importMergeFailed: \"The import could not be completed. Try again.\",\r\n  },\r\n  pagination: {\r\n    firstPage: \"First page\",\r\n    previousPage: \"Previous page\",\r\n    nextPage: \"Next page\",\r\n    lastPage: \"Last page\",\r\n    pageAriaLabel: (page) => `Go to page ${page}`,\r\n    pageSizeAriaLabel: \"Rows per page\",\r\n    pageSizeOption: (size) => `${size} / page`,\r\n    range: (from, to, total) => `${from}–${to} of ${total}`,\r\n    rangeEmpty: \"0 of 0\",\r\n  },\r\n};\r\n\r\n/** Recursive partial override of {@link DataGridLabels}: arrays and functions are replaced wholesale, plain objects merge key-by-key. */\r\nexport type DeepPartialLabels<T = DataGridLabels> = {\r\n  [K in keyof T]?: T[K] extends (...args: never[]) => unknown\r\n    ? T[K]\r\n    : T[K] extends readonly unknown[]\r\n      ? T[K]\r\n      : T[K] extends object\r\n        ? DeepPartialLabels<T[K]>\r\n        : T[K];\r\n};\r\n\r\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\r\n  return typeof value === \"object\" && value !== null && !Array.isArray(value);\r\n}\r\n\r\n/** Pure deep merge for label overrides: plain objects merge recursively, everything else (functions, arrays, primitives) replaces the base value wholesale. */\r\nexport function deepMergeLabels<T>(base: T, override: DeepPartialLabels<T> | undefined): T {\r\n  if (!override) return base;\r\n  if (!isPlainObject(base) || !isPlainObject(override)) return (override as T) ?? base;\r\n\r\n  const overrideRecord = override as Record<string, unknown>;\r\n  const result: Record<string, unknown> = { ...base };\r\n  for (const key of Object.keys(overrideRecord)) {\r\n    const overrideValue = overrideRecord[key];\r\n    const baseValue = (base as Record<string, unknown>)[key];\r\n    result[key] = isPlainObject(baseValue) && isPlainObject(overrideValue) ? deepMergeLabels(baseValue, overrideValue) : overrideValue;\r\n  }\r\n  return result as T;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/labels.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/layout-context.ts",
      "content": "import { createContext, use, useMemo, type ReactElement, type ReactNode, type RefObject } from \"react\";\r\nimport type { GetCellClassName, GetRowClassName, OnCellClick, OnRowClick } from \"./types\";\r\nimport { useDataGridColumnWidths, useDataGridRowMarkers, type AnyColumnDef } from \"./store\";\r\nimport { resolveColumnWidth, distributeFlexWidths } from \"./columns/resolve-column-width\";\r\nimport { encodeTemplate } from \"./columns/column-format-helpers\";\r\nimport { pinLeftOffsets, pinRightOffsets } from \"./columns/pin-offsets\";\r\nimport { markerWidth } from \"./rows/marker-width\";\r\nimport type { GridInteractionHandlers } from \"./interaction/use-grid-interaction\";\r\nimport type { GridDirection } from \"./windowing/direction\";\r\n\r\n/** Cumulative left edge (px) of each column's track within the full grid template. */\r\nfunction trackLefts(widths: number[]): number[] {\r\n  const lefts: number[] = [];\r\n  let acc = 0;\r\n  for (let i = 0; i < widths.length; i++) {\r\n    lefts.push(acc);\r\n    acc += widths[i]!; // i < widths.length by loop condition\r\n  }\r\n  return lefts;\r\n}\r\n\r\n/** Cumulative right edge (px) of each column's track within the full grid template. */\r\nfunction trackRights(widths: number[], lefts: number[]): number[] {\r\n  // lefts is trackLefts(widths)'s output: same length as widths by construction\r\n  return widths.map((w, i) => lefts[i]! + w);\r\n}\r\n\r\n/**\r\n * Shared per-column layout used by header cells, data cells, and the grid template.\r\n * The marker column (when `markerWidth > 0`) is grid-template-column 1, BEFORE every data\r\n * column and before any pinned-left data column — data columns render at\r\n * `gridColumnStart: index + (markerWidth > 0 ? 2 : 1)`. `trackLefts`/`trackRights`/`leftOffsets`\r\n * are already shifted by `markerWidth` so callers never add it themselves; `CellCoord.col` and\r\n * this array's own `index` stay pure data-column indices throughout (the marker lives\r\n * outside the data index space).\r\n */\r\nexport type ColumnLayout = {\r\n  template: string;\r\n  leftOffsets: number[];\r\n  rightOffsets: number[];\r\n  trackLefts: number[];\r\n  trackRights: number[];\r\n  widths: number[];\r\n  pins: (AnyColumnDef[\"pin\"] | undefined)[];\r\n  totalWidth: number;\r\n  /** 0 when `rowMarkers` is 'none' — no marker track is added. */\r\n  markerWidth: number;\r\n};\r\n\r\n/**\r\n * Computes shared column layout (widths, pin offsets, grid-template string) from live width\r\n * overrides. `availableWidth` is the viewport's clientWidth (excludes the scrollbar) used to\r\n * distribute `flex` leftover space; 0 on first render/SSR before measurement, which correctly\r\n * bails flex distribution to base widths until the real measurement lands.\r\n */\r\nexport function useColumnLayout(columns: readonly AnyColumnDef[], availableWidth = 0): ColumnLayout {\r\n  const ids = useMemo(() => columns.map((c) => c.id), [columns]);\r\n  const overrides = useDataGridColumnWidths(ids);\r\n  const rowMarkers = useDataGridRowMarkers();\r\n  return useMemo(() => {\r\n    const mWidth = markerWidth(rowMarkers);\r\n    const baseWidths = columns.map((column, i) => resolveColumnWidth(column, overrides[i]));\r\n    // Manual resize (a live override) fixes the column and removes it from flex distribution.\r\n    const flexes = columns.map((column, i) => (overrides[i] === undefined ? column.flex : undefined));\r\n    const maxWidths = columns.map((column) => column.maxWidth ?? Number.POSITIVE_INFINITY);\r\n    const widths = distributeFlexWidths(baseWidths, flexes, maxWidths, Math.max(0, availableWidth - mWidth));\r\n    const pins = columns.map((c) => c.pin);\r\n    const lefts = trackLefts(widths).map((l) => l + mWidth);\r\n    const dataTemplate = encodeTemplate(widths);\r\n    return {\r\n      template: mWidth > 0 ? `${mWidth}px ${dataTemplate}` : dataTemplate,\r\n      // the marker column is always pinned-left before any pinned data column, so every\r\n      // pin-left offset shifts by its width; pin-right is unaffected (marker never pins right).\r\n      leftOffsets: pinLeftOffsets(widths, pins).map((o) => o + mWidth),\r\n      rightOffsets: pinRightOffsets(widths, pins),\r\n      trackLefts: lefts,\r\n      trackRights: trackRights(widths, lefts),\r\n      widths,\r\n      pins,\r\n      totalWidth: widths.reduce((a, b) => a + b, 0) + mWidth,\r\n      markerWidth: mWidth,\r\n    };\r\n  }, [columns, overrides, rowMarkers, availableWidth]);\r\n}\r\n\r\n/** A visible column paired with its real (unwindowed) index, used for grid placement + pin var lookup. */\r\nexport type WindowedColumn = { column: AnyColumnDef; index: number };\r\n\r\n/**\r\n * Context passed to {@link RowBandsSpec.renderBand} for one band — exactly what root.tsx computes\r\n * for the band's render. `ariaRowIndexBase` is root's own aria index-layout math (header=1, top\r\n * band next, then data rows, bottom band last), so the add-on never has to know rowCount or the\r\n * other band's length.\r\n */\r\nexport type RowBandRenderCtx = {\r\n  position: \"top\" | \"bottom\";\r\n  rows: readonly unknown[];\r\n  windowedColumns: readonly WindowedColumn[];\r\n  layout: ColumnLayout;\r\n  rowHeight: number;\r\n  template: string;\r\n  /** The sticky header track's own height (not including either band). */\r\n  headerHeight: number;\r\n  ariaRowIndexBase: number;\r\n};\r\n\r\n/**\r\n * Provider-level row-bands seam: unlike `overlayPlugins` (pure paint, no layout impact), bands\r\n * affect band heights and `aria-rowcount`, which root.tsx must know SYNCHRONOUSLY at first render\r\n * (SSR + no one-frame layout shift) — so this is a spec object, not a mount-effect registration\r\n * like `fillHandlers`. Core only reads `topRows.length`/`bottomRows.length` for its own arithmetic\r\n * and calls `renderBand`; it never inspects the row contents. `data-grid-pinned-rows`'s\r\n * `useDataGridPinnedRows` is the motivating (and so far only) producer.\r\n */\r\nexport type RowBandsSpec = {\r\n  topRows: readonly unknown[];\r\n  bottomRows: readonly unknown[];\r\n  renderBand: (ctx: RowBandRenderCtx) => ReactNode;\r\n};\r\n\r\n/**\r\n * Render-prop for the per-column header menu slot: the primary pin/sort surface. `trigger` is the\r\n * core's own ghost-chevron button element — the renderer wraps it as its dropdown's trigger\r\n * (e.g. `<DropdownMenuTrigger render={trigger} />`) so core keeps sole ownership of the trigger's\r\n * visuals/aria-label while the add-on owns the popover + menu items.\r\n * A concrete `ReactElement` (not the broader `ReactNode`): Base UI's `render` prop requires one.\r\n */\r\nexport type HeaderMenuRenderer = (ctx: { column: AnyColumnDef; index: number; trigger: ReactElement }) => ReactNode;\r\n\r\n/** Context passed to {@link MarkerCellRenderer} for one row's marker cell. */\r\nexport type MarkerCellRenderCtx = {\r\n  /** 0-based view row index (the marker column's own numbering space, minus one). */\r\n  viewRowIndex: number;\r\n  /** The ROWS channel only: this row is selected as a whole (marker press/drag, Shift+Space). Cell or column selection never flips it — mirrors `useDataGridIsRowChannelSelected`. */\r\n  isRowChannelSelected: boolean;\r\n  /** The CELL or COLUMN channel covers at least one cell of this row. Independent of `isRowChannelSelected`, so a renderer can tell \"this row is selected\" apart from \"cells in this row are selected\" — mirrors `useDataGridIsRowCellSelected`. */\r\n  isCellSelected: boolean;\r\n};\r\n\r\n/** Context passed to {@link MarkerHeaderRenderer} for the marker column's header cell. */\r\nexport type MarkerHeaderRenderCtx = {\r\n  /** The select-all state the built-in header checkbox shows. */\r\n  allSelected: \"checked\" | \"indeterminate\" | \"unchecked\";\r\n};\r\n\r\n/**\r\n * Custom row-marker cell renderer: replaces the built-in row number/checkbox CONTENT inside the\r\n * marker column (requires a non-`'none'` `rowMarkers` mode; the mode still drives the track width,\r\n * and the cell's own press/drag row-selection gesture stays on the wrapper). Pass a stable identity.\r\n */\r\nexport type MarkerCellRenderer = (ctx: MarkerCellRenderCtx) => ReactNode;\r\n\r\n/** Custom marker-header renderer: replaces the built-in select-all checkbox; same track and width. Pass a stable identity. */\r\nexport type MarkerHeaderRenderer = (ctx: MarkerHeaderRenderCtx) => ReactNode;\r\n\r\n/** Layout context shared by header/body so they don't recompute widths independently and never desync on row height, and so both consume the SAME column window (no drift between two independent subscriptions). */\r\nexport type DataGridRootContextValue = {\r\n  scrollRef: RefObject<HTMLDivElement | null>;\r\n  viewportRef: RefObject<HTMLDivElement | null>;\r\n  columns: readonly AnyColumnDef[];\r\n  /** Columns in the current column window, in display order, each tagged with its real index. */\r\n  windowedColumns: readonly WindowedColumn[];\r\n  layout: ColumnLayout;\r\n  rowHeight: number;\r\n  /** The sticky header track's own height — header.tsx's literal row height, NOT including any pinned band. Consumers computing \"where data row 0 starts\" need `headerHeight + pinnedTopHeight`; body.tsx does this itself for `useRowWindow`/the canvas transform. */\r\n  headerHeight: number;\r\n  /** Pinned-top row band height (px); 0 when `rowBands` is unset or its `topRows` is empty. */\r\n  pinnedTopHeight: number;\r\n  /** Pinned-bottom row band height (px); 0 when `rowBands` is unset or its `bottomRows` is empty. */\r\n  pinnedBottomHeight: number;\r\n  /** `rowBands.topRows.length` — body.tsx's `ariaRowIndexOffset` needs only the count, never the row contents (core doesn't own pinned-row semantics). */\r\n  pinnedTopCount: number;\r\n  /** Full RLE grid-template-columns string — the alignment source of truth for both layers. */\r\n  template: string;\r\n  interaction: GridInteractionHandlers;\r\n  /** Resolved layout direction for this grid — the single value every direction-aware seam inside the root reads (header reorder half-test, resize delta, pin-shadow measurement). */\r\n  direction: GridDirection;\r\n  readOnly?: boolean;\r\n  /** Optional per-column header menu slot (ghost chevron trigger); undefined renders nothing. */\r\n  renderHeaderMenu?: HeaderMenuRenderer;\r\n  /** Optional custom row-marker cell renderer; undefined renders the built-in number/checkbox. */\r\n  renderMarker?: MarkerCellRenderer;\r\n  /** Optional custom marker-header renderer; undefined renders the built-in select-all checkbox. */\r\n  renderMarkerHeader?: MarkerHeaderRenderer;\r\n  /**\r\n   * Programmatic row/cell class hooks, routed through this context rather than a\r\n   * per-row/per-cell prop. `DataGridRoot` DOES re-render on active-cell moves (the activeColumn\r\n   * subscription, root.tsx) as well as scroll/window-shift ticks, but its context value is\r\n   * `useMemo`'d on real deps (root.tsx) — a render that doesn't change any field here (e.g. an\r\n   * active-cell move within the same column window) hands out the identical value reference, so\r\n   * `cell.tsx`'s direct `useDataGridRootContext()` read doesn't re-render either, keeping this off\r\n   * the memoized `DataGridRow`/`DataGridCell` hot path the wasted-render regression test guards.\r\n   */\r\n  getRowClassName?: GetRowClassName<unknown>;\r\n  getCellClassName?: GetCellClassName<unknown>;\r\n  /** Forwarded to each cell's click handler; see {@link DataGridRootProps.onCellClick} in root.tsx. */\r\n  onCellClick?: OnCellClick<unknown>;\r\n  /** Forwarded alongside `onCellClick`; see {@link DataGridRootProps.onRowClick} in root.tsx. */\r\n  onRowClick?: OnRowClick<unknown>;\r\n  /** Fired by `DataGridBody` after a row-window commit; see {@link DataGridRootProps.onRowWindowChange} in root.tsx. */\r\n  onRowWindowChange?: (range: { start: number; end: number }) => void;\r\n};\r\n\r\nexport const DataGridRootContext = createContext<DataGridRootContextValue | null>(null);\r\n\r\n/** Resolves the shared root layout context; throws outside a `<DataGridRoot>`. */\r\nexport function useDataGridRootContext(): DataGridRootContextValue {\r\n  const ctx = use(DataGridRootContext);\r\n  if (!ctx) throw new Error(\"gridcn: DataGridHeader/DataGridBody must be used inside a <DataGridRoot>.\");\r\n  return ctx;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/layout-context.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/overlays.tsx",
      "content": "\"use client\";\r\n\r\nimport { Fragment, type CSSProperties, type ReactNode } from \"react\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { GRID_LAYER } from \"./layers\";\r\nimport type { CellCoord, CompactSelectionLike, GridRect } from \"./types\";\r\nimport { intersectRect } from \"./selection\";\r\nimport { pinnedInsetStyle } from \"./columns/pinned-inset-style\";\r\nimport {\r\n  useDataGridActiveCell,\r\n  useDataGridOverlayPlugins,\r\n  useDataGridSelection,\r\n  type AnyColumnDef,\r\n} from \"./store\";\r\n\r\n/** Per-real-column-index pin/track data an overlay segment needs to reproduce a pinned cell's own offset. */\r\nexport type PinTrackData = {\r\n  pins: readonly (AnyColumnDef[\"pin\"] | undefined)[];\r\n  trackLefts: readonly number[];\r\n  trackRights: readonly number[];\r\n  /** Real-index bounds `[start, end)` of the currently rendered UNPINNED column window — pinned zones always render in full (pinned columns are never virtualized out), so only the unpinned middle segment needs this clamp. */\r\n  renderedUnpinnedRange: { start: number; end: number };\r\n};\r\n\r\n/**\r\n * Clamps a data-space rect to the given `[rowStart, rowEnd)` row window and `[0, colCount)`, or\r\n * null if fully outside it — column clamping to the actually-RENDERED window happens per pin-zone\r\n * in {@link splitRectByPinZones} when `pinTrack` is supplied. Exported for {@link OverlayPluginCtx}:\r\n * an overlay plugin (e.g. `data-grid-presence`) needs the same window-clamp math core's own\r\n * built-in layers use, rather than reimplementing it.\r\n */\r\nexport function clampRectToWindow(\r\n  rect: GridRect,\r\n  rowStart: number,\r\n  rowEnd: number,\r\n  colCount: number,\r\n): GridRect | null {\r\n  return intersectRect(rect, { x: 0, y: rowStart, width: colCount, height: rowEnd - rowStart });\r\n}\r\n\r\n/** Contiguous membership runs of `set` within `[start, end)` — O(end - start), window-bounded. */\r\nfunction membershipRuns(set: CompactSelectionLike, start: number, end: number): Array<{ start: number; length: number }> {\r\n  const runs: Array<{ start: number; length: number }> = [];\r\n  let runStart = -1;\r\n  for (let i = start; i < end; i++) {\r\n    if (set.hasIndex(i)) {\r\n      if (runStart === -1) runStart = i;\r\n    } else if (runStart !== -1) {\r\n      runs.push({ start: runStart, length: i - runStart });\r\n      runStart = -1;\r\n    }\r\n  }\r\n  if (runStart !== -1) runs.push({ start: runStart, length: end - runStart });\r\n  return runs;\r\n}\r\n\r\n/** One pin zone's contiguous index range plus (for pinned zones) the offset shared by the whole segment. */\r\nexport type RectSegment = {\r\n  rect: GridRect;\r\n  /** Undefined for the unpinned middle segment — no counter-offset, no elevated z-index. */\r\n  pin?: AnyColumnDef[\"pin\"];\r\n  pinStyle?: CSSProperties;\r\n};\r\n\r\n/**\r\n  * Splits a data-space rect into up to 3 contiguous sub-rects — pinned-left / unpinned / pinned-right —\r\n  * so each zone can be rendered at its own screen position: overlays live in the rows canvas, which\r\n  * translates by -scrollLeft, so a segment spanning a pinned column would otherwise\r\n * paint at that column's TRACK position instead of its counter-offset pinned position. Pinned\r\n * columns are contiguous within their zone (store guarantee, `computeVisibleColumns`), so the\r\n * whole segment reuses the single offset of its first column.\r\n */\r\nexport function splitRectByPinZones(rect: GridRect, track: PinTrackData): RectSegment[] {\r\n  const { pins, renderedUnpinnedRange } = track;\r\n  let leftEnd = 0;\r\n  while (leftEnd < pins.length && pins[leftEnd] === \"left\") leftEnd++;\r\n  let rightStart = pins.length;\r\n  while (rightStart > leftEnd && pins[rightStart - 1] === \"right\") rightStart--;\r\n\r\n  const zones: Array<{ start: number; end: number; pin?: AnyColumnDef[\"pin\"] }> = [\r\n    { start: 0, end: leftEnd, pin: \"left\" },\r\n    // clamped to the rendered unpinned column window: unlike pinned zones (always fully rendered),\r\n    // the unpinned middle is virtualized, so an unclamped segment would place its far edge at an\r\n    // off-screen track position, breaking the \"no gaps\" visual tiling with the pinned segments.\r\n    { start: Math.max(leftEnd, renderedUnpinnedRange.start), end: Math.min(rightStart, renderedUnpinnedRange.end), pin: undefined },\r\n    { start: rightStart, end: pins.length, pin: \"right\" },\r\n  ];\r\n\r\n  const segments: RectSegment[] = [];\r\n  for (const zone of zones) {\r\n    if (zone.end <= zone.start) continue;\r\n    const clamped = intersectRect(rect, { x: zone.start, y: rect.y, width: zone.end - zone.start, height: rect.height });\r\n    if (!clamped) continue;\r\n    if (!zone.pin) {\r\n      segments.push({ rect: clamped });\r\n      continue;\r\n    }\r\n    const firstCol = clamped.x;\r\n    segments.push({\r\n      rect: clamped,\r\n      pin: zone.pin,\r\n      pinStyle: pinnedInsetStyle(zone.pin, firstCol),\r\n    });\r\n  }\r\n  return segments;\r\n}\r\n\r\n/** One selection-range fill overlay, grid-line placed, window-relative; pinned segments add their zone's counter-offset + elevated z-index (above pinned cells). */\r\nfunction RangeOverlay({ rect, windowStart, colOffset, pinStyle }: { rect: GridRect; windowStart: number; colOffset: number; pinStyle?: CSSProperties }) {\r\n  return (\r\n    <div\r\n      data-grid-selection-overlay=\"\"\r\n      data-pinned={pinStyle ? \"\" : undefined}\r\n      aria-hidden=\"true\"\r\n      className=\"pointer-events-none border border-primary/40 bg-primary/10\"\r\n      style={{\r\n        // positioned: cells are position:relative, so a static overlay would paint below them\r\n        position: \"relative\",\r\n        gridColumnStart: rect.x + colOffset,\r\n        gridColumnEnd: rect.x + rect.width + colOffset,\r\n        gridRowStart: rect.y - windowStart + 1,\r\n        gridRowEnd: rect.y + rect.height - windowStart + 1,\r\n        ...pinStyle,\r\n        ...(pinStyle ? { zIndex: GRID_LAYER.pinnedOverlaySegment } : {}),\r\n      }}\r\n    />\r\n  );\r\n}\r\n\r\n/** Props for {@link DataGridOverlays}. */\r\nexport type DataGridOverlaysProps = {\r\n  /** First rendered (data-space) row index — overlays place window-relative like the rows canvas. May sit below `clampRowStart` when an off-window active row pulls it down (see body.tsx effectiveStart). */\r\n  windowStart: number;\r\n  /** Number of rows currently in view (post sort/filter). May include a disjoint off-window active row appended past the contiguous span — NOT a safe range-overlay clamp bound on its own, see `clampRowStart`/`clampRowEnd`. */\r\n  rowCount: number;\r\n  /**\r\n   * True contiguous `[start, end)` rendered row span for clamping range/band overlays — distinct\r\n   * from `windowStart`/`rowCount` because body.tsx appends a single off-window active row outside\r\n   * the contiguous window, which would otherwise make `[windowStart, windowStart+rowCount)` either\r\n   * overshoot or undershoot the real rendered bottom. Defaults to `[windowStart, windowStart+rowCount)`\r\n   * when omitted (no disjoint window in play).\r\n   */\r\n  clampRowStart?: number;\r\n  clampRowEnd?: number;\r\n  /** Total number of visible columns (real index space) — NOT the rendered column-window size, so pin-zone segmentation can reach a pinned-right zone far outside the current window. */\r\n  colCount: number;\r\n  /** 1-based grid-column offset added to every data-space rect.x; 2 when a marker column occupies track 1, else 1 (default). */\r\n  colOffset?: number;\r\n  /** Per-column pin + track data (real column-index space) for pin-aware overlay segmentation; omitted (e.g. no pinned columns) skips segmentation entirely. */\r\n  pinTrack?: PinTrackData;\r\n};\r\n\r\n/**\r\n * Context passed to every registered {@link OverlayPlugin}: the window-clamp + pin-zone\r\n * segmentation helpers and layout vars a plugin needs to reproduce the built-in layers'\r\n * \"clamp to rendered window, split by pin zone, place by grid line\" pipeline.\r\n */\r\nexport type OverlayPluginCtx = {\r\n  windowStart: number;\r\n  clampRowStart: number;\r\n  clampRowEnd: number;\r\n  colCount: number;\r\n  colOffset: number;\r\n  pinTrack?: PinTrackData;\r\n  splitRectByPinZones: typeof splitRectByPinZones;\r\n  clampRectToWindow: typeof clampRectToWindow;\r\n};\r\n\r\n/**\r\n * One overlay-plugin slot: registered via `overlayPlugins` on `DataGridProvider`, rendered by\r\n * {@link DataGridOverlays} after the range/band layers but BEFORE the local active-cell ring —\r\n * plugin content (e.g. a fill drag preview, multiplayer presence) loses visually to local focus.\r\n * The only new core surface for add-ons that paint into the overlay layer: core supplies\r\n * geometry, the plugin supplies content.\r\n */\r\nexport type OverlayPlugin = (ctx: OverlayPluginCtx) => ReactNode;\r\n\r\n/** True when any column is pinned — segmentation is a no-op (and skippable) otherwise. */\r\nfunction hasAnyPin(pinTrack: PinTrackData | undefined): pinTrack is PinTrackData {\r\n  return Boolean(pinTrack && pinTrack.pins.some((p) => p === \"left\" || p === \"right\"));\r\n}\r\n\r\n/**\r\n * Selection-range fill + active-cell ring, rendered inside the rows canvas so they translate with\r\n * scroll content. Placed purely by grid lines (no pixel math), `pointer-events: none` throughout.\r\n * Subscribes to selection/active-cell via atomic selectors: a change re-renders only this\r\n * component (never rows). When any column is pinned, every rect splits into pinned-left/unpinned/\r\n * pinned-right segments so pinned segments paint at their cell's actual screen position, above\r\n * pinned cells' own z-index. Registered `overlayPlugins` render after the built-in layers but\r\n * BEFORE the active-cell ring, so local focus always wins visually over plugin content.\r\n */\r\nexport function DataGridOverlays(props: DataGridOverlaysProps): ReactNode {\r\n  const { windowStart, rowCount, clampRowStart = windowStart, clampRowEnd = windowStart + rowCount, colCount, colOffset = 1, pinTrack } = props;\r\n  const selection = useDataGridSelection();\r\n  const activeCell = useDataGridActiveCell();\r\n  const overlayPlugins = useDataGridOverlayPlugins();\r\n  const pinned = hasAnyPin(pinTrack) ? pinTrack : undefined;\r\n\r\n  /** Splits into pin-zone segments when the grid has pinned columns, else the rect as a single \"segment\". */\r\n  const segment = (rect: GridRect): RectSegment[] => (pinned ? splitRectByPinZones(rect, pinned) : [{ rect }]);\r\n\r\n  const rangeRects: GridRect[] = [];\r\n  if (selection.current) {\r\n    rangeRects.push(selection.current.range);\r\n    for (const rect of selection.current.rangeStack) rangeRects.push(rect);\r\n  }\r\n\r\n  // rows/columns channels (header/marker selection) render as full-band highlights — Excel-style\r\n  // visible feedback (user QA 2026-07-03); runs computed only inside the contiguous rendered span\r\n  // (clampRowStart/End, not windowStart/rowCount) so a disjoint off-window active row can't stretch\r\n  // or shift these bands past the real rendered bottom.\r\n  const columnBands: GridRect[] = membershipRuns(selection.columns, 0, colCount).map((run) => ({\r\n    x: run.start,\r\n    y: clampRowStart,\r\n    width: run.length,\r\n    height: clampRowEnd - clampRowStart,\r\n  }));\r\n  const rowBands: GridRect[] = membershipRuns(selection.rows, clampRowStart, clampRowEnd).map((run) => ({\r\n    x: 0,\r\n    y: run.start,\r\n    width: colCount,\r\n    height: run.length,\r\n  }));\r\n\r\n  const activeInWindow: CellCoord | null =\r\n    activeCell && activeCell.row >= windowStart && activeCell.row < windowStart + rowCount ? activeCell : null;\r\n  const activePinStyle = activeInWindow && pinned ? pinned.pins[activeInWindow.col] && pinnedInsetStyle(\r\n    pinned.pins[activeInWindow.col],\r\n    activeInWindow.col,\r\n  ) : undefined;\r\n\r\n  return (\r\n    <>\r\n      {rangeRects.map((rect, i) => {\r\n        const clamped = clampRectToWindow(rect, clampRowStart, clampRowEnd, colCount);\r\n        if (!clamped) return null;\r\n        return segment(clamped).map((seg, j) => (\r\n          <RangeOverlay key={`${i}-${j}`} rect={seg.rect} windowStart={windowStart} colOffset={colOffset} pinStyle={seg.pinStyle} />\r\n        ));\r\n      })}\r\n      {columnBands.map((rect, i) =>\r\n        segment(rect).map((seg, j) => (\r\n          <RangeOverlay key={`col-band-${i}-${j}`} rect={seg.rect} windowStart={windowStart} colOffset={colOffset} pinStyle={seg.pinStyle} />\r\n        )),\r\n      )}\r\n      {rowBands.map((rect, i) =>\r\n        segment(rect).map((seg, j) => (\r\n          <RangeOverlay key={`row-band-${i}-${j}`} rect={seg.rect} windowStart={windowStart} colOffset={colOffset} pinStyle={seg.pinStyle} />\r\n        )),\r\n      )}\r\n      {overlayPlugins.length > 0 &&\r\n        overlayPlugins.map((plugin, i) => (\r\n          // rendered here (after range/band layers, BEFORE the active-cell ring) so a plugin like\r\n          // data-grid-fill's fill-preview/handle or presence keeps painting below local focus —\r\n          // the ring must always win visually.\r\n          <Fragment key={i}>\r\n            {plugin({ windowStart, clampRowStart, clampRowEnd, colCount, colOffset, pinTrack, splitRectByPinZones, clampRectToWindow })}\r\n          </Fragment>\r\n        ))}\r\n      {activeInWindow && (\r\n        <div\r\n          data-grid-active-cell-overlay=\"\"\r\n          data-pinned={activePinStyle ? \"\" : undefined}\r\n          aria-hidden=\"true\"\r\n          className={cn(\"pointer-events-none ring-2 ring-inset ring-primary\")}\r\n          style={{\r\n            gridColumnStart: activeInWindow.col + colOffset,\r\n            gridColumnEnd: activeInWindow.col + colOffset + 1,\r\n            gridRowStart: activeInWindow.row - windowStart + 1,\r\n            gridRowEnd: activeInWindow.row - windowStart + 2,\r\n            // The ring ranks with the cell it decorates (same DOM-later sibling wins ties): an\r\n            // unpinned ring must lose to any pinned cell that scrolls over it, a pinned ring must\r\n            // beat its own cell's opaque background. A fixed rank floats the former over the pin\r\n            // band and buries the latter under its own cell.\r\n            zIndex: activePinStyle ? GRID_LAYER.activePinnedCell : GRID_LAYER.activeCell,\r\n            ...activePinStyle,\r\n          }}\r\n        />\r\n      )}\r\n    </>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/overlays.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/root.tsx",
      "content": "\"use client\";\r\n\r\nimport {\r\n  useEffect,\r\n  useMemo,\r\n  useRef,\r\n  useState,\r\n  type CSSProperties,\r\n  type ReactNode,\r\n} from \"react\";\r\nimport { DirectionProvider } from \"@base-ui/react/direction-provider\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport type { DensityMode, GetCellClassName, GetRowClassName, Keymap, OnCellClick, OnRowClick } from \"./types\";\r\nimport { DEFAULT_KEYMAP, validateKeymap } from \"./keyboard\";\r\nimport {\r\n  useDataGridActions,\r\n  useDataGridActiveColumn,\r\n  useDataGridFillHandlers,\r\n  useDataGridHasActiveCell,\r\n  useDataGridLabels,\r\n  useDataGridRowBands,\r\n  useDataGridRowCount,\r\n  useDataGridSelectionConfig,\r\n  useDataGridVisibleColumns,\r\n} from \"./store\";\r\nimport { useElementDimensions, useViewportElement } from \"./windowing/use-scroll-snapshot\";\r\nimport { directionSign, readResolvedDirection, type GridDirection } from \"./windowing/direction\";\r\nimport { useColumnWindow } from \"./windowing/use-column-window\";\r\nimport { useGridInteraction, type InteractionLayout } from \"./interaction/use-grid-interaction\";\r\nimport { useGridClipboard } from \"./clipboard/use-grid-clipboard\";\r\nimport {\r\n  useColumnLayout,\r\n  DataGridRootContext,\r\n  type HeaderMenuRenderer,\r\n  type MarkerCellRenderer,\r\n  type MarkerHeaderRenderer,\r\n} from \"./layout-context\";\r\nimport { resolveRowHeight } from \"./rows/density\";\r\nimport { useScrolledEdges } from \"./windowing/use-scrolled-edges\";\r\nimport { usePinShadowEdges } from \"./windowing/use-pin-shadow-edges\";\r\nimport { GRID_LAYER } from \"./layers\";\r\nimport { DataGridLoadingSkeleton, DataGridLoadingBar } from \"./rows/loading-skeleton\";\r\nimport { isDev } from \"./is-dev\";\r\n\r\n/** Default sticky header track height (px); density/rowHeight only affect data rows, never the header. */\r\nconst HEADER_HEIGHT = 36;\r\n// The item's cssVars theme this token on CLI install; the fallback covers a manual copy without them.\r\nconst PIN_SHADOW = \"var(--grid-pin-shadow, oklch(0 0 0 / 10%))\";\r\n// Edge shadows default to the pin color; `--grid-scroll-shadow: transparent` hides them and keeps pinned-band shadows.\r\nconst SCROLL_SHADOW = `var(--grid-scroll-shadow, ${PIN_SHADOW})`;\r\nconst SHADOW_SIZE = \"var(--grid-shadow-size, 8px)\";\r\nconst shadowColor = (pinned: boolean) => (pinned ? PIN_SHADOW : SCROLL_SHADOW);\r\n\r\n/** Props for {@link DataGridRoot}. `TData` (default `unknown`) types the callback props below — annotate explicitly (e.g. `DataGridRoot<Person>`), there's no `data` prop here to infer it from. */\r\nexport type DataGridRootProps<TData = unknown> = {\r\n  className?: string;\r\n  /** Explicit row height (px); overrides `density` when set. */\r\n  rowHeight?: number;\r\n  /** Row-height preset: compact 28 / default 36 / comfortable 44. Ignored when `rowHeight` is set. */\r\n  density?: DensityMode;\r\n  /** Sticky header track height (px); `density` and `rowHeight` affect data rows only. Default 36. */\r\n  headerHeight?: number;\r\n  /** Extra unpinned columns rendered beyond the visible viewport on each side. Default 1. */\r\n  columnOverscan?: number;\r\n  /** Merged over `DEFAULT_KEYMAP`; per-action bindings here take precedence. */\r\n  keymap?: Keymap;\r\n  /**\r\n   * Layout direction. Omitted, the grid reads the direction it inherits from the page (the `dir`\r\n   * attribute on an ancestor, or the document default) once at mount — so a grid inside\r\n   * `<html dir=\"rtl\">` is right-to-left with no prop at all. Pass this to force one direction\r\n   * independent of the page.\r\n   *\r\n   * `\"rtl\"` mirrors column order, pinned bands, pointer hit-testing, drag gestures, and the\r\n   * chrome's popup placement. Arrow keys move VISUALLY: ArrowRight goes to the next column on the\r\n   * screen, which is the previous column index. `Tab` keeps its reading order.\r\n   */\r\n  direction?: GridDirection;\r\n  /** Disables editing and delete grid-wide, independent of any per-column `readOnly`. */\r\n  readOnly?: boolean;\r\n  /** Rendered centered in place of the body when there are zero rows in view. Never shown while `loading` is true. */\r\n  emptyState?: ReactNode;\r\n  /**\r\n   * Presentational loading flag: zero rows renders viewport-filling\r\n   * skeleton rows instead of the empty state; rows present keeps them visible and adds a slim\r\n   * indeterminate bar under the header. Purely presentational — no data-fetch orchestration of its\r\n   * own (that's `data-grid-lazy`'s job for per-window skeletons). Default `false`.\r\n   */\r\n  loading?: boolean;\r\n  /**\r\n   * Per-column header menu slot: rendered inline-end in each header cell as a ghost chevron\r\n   * trigger, only when provided. The context-menu add-on's `DataGridHeaderDropdown` is the\r\n   * intended renderer, reusing the same items as the header right-click menu.\r\n   */\r\n  renderHeaderMenu?: HeaderMenuRenderer;\r\n  /**\r\n   * Custom row-marker cell renderer: replaces the built-in row number/checkbox content inside the\r\n   * marker column. Requires a non-`'none'` `rowMarkers` mode (the mode still drives the track width,\r\n   * and the cell's own press/drag row-selection gesture stays on the wrapper); an interactive element\r\n   * inside the rendered node owns its own events. See {@link MarkerCellRenderer}. Pass a stable identity.\r\n   */\r\n  renderMarker?: MarkerCellRenderer;\r\n  /** Custom marker-header renderer: replaces the built-in select-all checkbox; same track and width. See {@link MarkerHeaderRenderer}. Pass a stable identity. */\r\n  renderMarkerHeader?: MarkerHeaderRenderer;\r\n  /** Row class hook, merged via `cn()` after the built-in row classes. Pass a stable identity — see {@link GetRowClassName}. */\r\n  getRowClassName?: GetRowClassName<TData>;\r\n  /** Cell class hook, merged via `cn()` after the built-in cell classes. Pass a stable identity — see {@link GetCellClassName}. */\r\n  getCellClassName?: GetCellClassName<TData>;\r\n  /** Fired on a plain click on any cell — see {@link OnCellClick}. Attached to the cell's own existing click handler, no new subscription. Pass a stable identity, same guidance as `getCellClassName`. */\r\n  onCellClick?: OnCellClick<TData>;\r\n  /** Fired alongside `onCellClick` once per click regardless of column — see {@link OnRowClick}. */\r\n  onRowClick?: OnRowClick<TData>;\r\n  /**\r\n   * Fired after a row-window commit with the rendered data-row range `[start, end)` (inclusive\r\n   * start, exclusive end — same convention as `useRowWindow`'s `RowWindow`). Only fires when the\r\n   * range actually changed since the last call, including once for the initial mount commit; a\r\n   * scroll tick that stays within the current window's overscan buffer does not re-fire it. Useful\r\n   * for lazy-loading/prefetch/analytics add-ons; core has no fetching logic of its own.\r\n   */\r\n  onRowWindowChange?: (range: { start: number; end: number }) => void;\r\n  children?: ReactNode;\r\n};\r\n\r\n/**\r\n * The scroll container: a plain `overflow:auto` div (real scroll element, native scrollbar) with\r\n * an in-flow Content div that provides the full virtual scroll extent, and inside it a\r\n * `position:sticky` Viewport that is pinned to the scrollport (MUI \"controlled virtualizer\"\r\n * pattern, research/mui-controlled-virtualizer.md). Native scroll only moves the Content div under\r\n * the sticky Viewport; 100% of visible row/header motion is the JS-written transform on the two\r\n * layers inside — the compositor can never reveal unrendered rows because rows never move via\r\n * native scroll (see research/scroll-blanking.md).\r\n */\r\nexport function DataGridRoot<TData = unknown>(props: DataGridRootProps<TData>): ReactNode {\r\n  const {\r\n    className,\r\n    rowHeight: rowHeightProp,\r\n    density,\r\n    headerHeight: headerHeightProp,\r\n    columnOverscan,\r\n    keymap,\r\n    direction: directionProp,\r\n    readOnly,\r\n    emptyState,\r\n    loading = false,\r\n    renderHeaderMenu,\r\n    renderMarker,\r\n    renderMarkerHeader,\r\n    getRowClassName,\r\n    getCellClassName,\r\n    onCellClick,\r\n    onRowClick,\r\n    onRowWindowChange,\r\n    children,\r\n  } = props;\r\n  const rowHeight = resolveRowHeight(density, rowHeightProp);\r\n  const headerHeight = headerHeightProp ?? HEADER_HEIGHT;\r\n  const scrollRef = useRef<HTMLDivElement | null>(null);\r\n  const viewportRef = useRef<HTMLDivElement | null>(null);\r\n  // Roving-tabindex bootstrap (see the root's onFocus below) must only fire for a genuine\r\n  // keyboard Tab into the grid — a mouse gesture on a header/marker cell (row/column channel\r\n  // selection, no `activeCell`) also focuses the root as a side effect and must NOT clobber that\r\n  // selection back to (0,0). Any pointerdown anywhere in the grid (captured phase, fires before\r\n  // the resulting focus event in the same tick) marks the next focus as pointer-originated.\r\n  const pointerInteractionRef = useRef(false);\r\n  // DOM-inherited default: `ltr` until the scroll element exists, then the real computed direction.\r\n  // Only an explicit prop skips the measurement entirely, so an `ltr` page (the overwhelmingly\r\n  // common case) settles on the same value it started with and never re-renders for this.\r\n  const [domDirection, setDomDirection] = useState<GridDirection>(\"ltr\");\r\n  useEffect(() => {\r\n    if (directionProp !== undefined) return;\r\n    setDomDirection(readResolvedDirection(scrollRef.current));\r\n  }, [directionProp]);\r\n  const direction = directionProp ?? domDirection;\r\n  const isRtl = direction === \"rtl\";\r\n  const visibleColumns = useDataGridVisibleColumns();\r\n  const rowCount = useDataGridRowCount();\r\n  // labels.grid.emptyState is the translatable default; the emptyState prop (ReactNode) still wins when provided.\r\n  const gridLabels = useDataGridLabels().grid;\r\n  const emptyStateLabel = gridLabels.emptyState;\r\n  // Provider-level seam: defaults to EMPTY_ROW_BANDS (store.tsx) when no add-on registered one —\r\n  // same identity-guardrailed sync-prop pattern as overlayPlugins, read here (not via a\r\n  // DataGridRootProps prop) so it's available before DataGridRoot even mounts.\r\n  const rowBands = useDataGridRowBands();\r\n  const pinnedTopRows = rowBands.topRows;\r\n  const pinnedBottomRows = rowBands.bottomRows;\r\n  const pinnedTopHeight = pinnedTopRows.length * rowHeight;\r\n  const pinnedBottomHeight = pinnedBottomRows.length * rowHeight;\r\n  // Effective header height: the sticky header track PLUS any pinned-top band — every \"data row 0\r\n  // starts here\" computation (row window, scroll-into-view, canvas transform) uses this, not\r\n  // HEADER_HEIGHT alone, so the pinned-top band is treated as part of the fixed chrome above the\r\n   // scrollable data rows.\r\n  const effectiveHeaderHeight = headerHeight + pinnedTopHeight;\r\n  // column-only primitive subscription: the root re-renders when the active COLUMN changes\r\n  // (force-render-active-column below), never on row-only moves — vertical arrows stay cheap.\r\n  const activeColumn = useDataGridActiveColumn();\r\n  // primitive boolean, not the full activeCell — see useDataGridHasActiveCell's doc for why this\r\n  // matters (a full CellCoord subscription here would re-render the root, and thus the whole tree\r\n  // via its context value, on every single row/column move).\r\n  const hasActiveCell = useDataGridHasActiveCell();\r\n  // WAI-ARIA grid pattern: aria-multiselectable reflects whether more than one cell can be\r\n  // selected at once — true whenever any multi-cell channel (range/row/column/multi-range) is on.\r\n  const { enableRangeSelection, enableRowSelection, enableColumnSelection, enableMultiRange } = useDataGridSelectionConfig();\r\n  const ariaMultiselectable = enableRangeSelection || enableRowSelection || enableColumnSelection || enableMultiRange;\r\n  // Dimensions-only (resize-rate), not the full scroll snapshot — a plain useScrollSnapshot here\r\n  // would re-render this whole tree on every scroll pixel, negating row memoization (mustFix).\r\n  // Read before useColumnLayout: clientWidth drives flex-column distribution below.\r\n  const dimensions = useElementDimensions(scrollRef);\r\n  const layout = useColumnLayout(visibleColumns, dimensions.clientWidth);\r\n  const { template, leftOffsets, rightOffsets, trackLefts: lefts, trackRights: rights, widths, pins, totalWidth } =\r\n    layout;\r\n\r\n  const effectiveKeymap = useMemo(() => ({ ...DEFAULT_KEYMAP, ...keymap }), [keymap]);\r\n  const interactionLayout: InteractionLayout = useMemo(() => {\r\n    // the marker column (when present) is always pinned-left before every data column, so it's\r\n    // part of the static-pinned-left band for pointer hit-testing (pointerToCoord's columnAtX).\r\n    let pinnedLeftWidth = layout.markerWidth;\r\n    let pinnedRightWidth = 0;\r\n    for (let i = 0; i < widths.length; i++) {\r\n      // i < widths.length by loop condition, pins is same-length parallel array\r\n      if (pins[i] === \"left\") pinnedLeftWidth += widths[i]!;\r\n      if (pins[i] === \"right\") pinnedRightWidth += widths[i]!;\r\n    }\r\n    return {\r\n      trackLefts: lefts,\r\n      trackRights: rights,\r\n      rowHeight,\r\n      dataRowTop: effectiveHeaderHeight,\r\n      pinnedBottomHeight,\r\n      pinnedLeftWidth,\r\n      pinnedRightWidth,\r\n      pins,\r\n      direction,\r\n    };\r\n  }, [lefts, rights, rowHeight, pins, widths, layout.markerWidth, effectiveHeaderHeight, pinnedBottomHeight, direction]);\r\n  const hasPinnedLeft = interactionLayout.pinnedLeftWidth > 0;\r\n  const hasPinnedRight = interactionLayout.pinnedRightWidth > 0;\r\n  // Registered by the `data-grid-fill` add-on's tracker component — it must render\r\n  // somewhere inside THIS subtree to reach scrollRef/layout via useDataGridRootContext, a level\r\n  // below this very hook call, so it reaches back up through the store rather than a prop (see\r\n  // `fillHandlers`'s doc comment in store.tsx). `null` (add-on absent, or not yet mounted) makes\r\n  // mod+D/mod+R/Escape-mid-drag no-ops — useGridInteraction already treats each as optional.\r\n  const fillHandlers = useDataGridFillHandlers();\r\n  const interaction = useGridInteraction({\r\n    scrollRef,\r\n    layout: interactionLayout,\r\n    keymap: effectiveKeymap,\r\n    readOnly,\r\n    fillDown: fillHandlers?.fillDown,\r\n    fillRight: fillHandlers?.fillRight,\r\n    cancelFillDrag: fillHandlers?.cancelFillDrag,\r\n  });\r\n  useGridClipboard({ rootRef: scrollRef, readOnly });\r\n\r\n  // Registers this mount's scroll-into-view on the store so add-ons outside this subtree (e.g.\r\n  // data-grid-toolbar's search, a DataGridRoot sibling) can reach it; cleared on unmount.\r\n  const actions = useDataGridActions();\r\n  useEffect(() => {\r\n    actions._registerScrollToCell(interaction.scrollCellIntoView);\r\n    return () => actions._registerScrollToCell(null);\r\n  }, [actions, interaction.scrollCellIntoView]);\r\n\r\n  // Mirrors `readOnly` into the store so add-on mutation surfaces outside this subtree (context\r\n  // menu, useDataGridClipboard) honor it too — false on unmount, matching the prop's own default.\r\n  useEffect(() => {\r\n    actions._registerReadOnly(Boolean(readOnly));\r\n    return () => actions._registerReadOnly(false);\r\n  }, [actions, readOnly]);\r\n\r\n  // Mirrors the effective (merged) keymap into the store so surfaces outside this subtree (the\r\n  // data-grid-keybindings add-on's dialog) read the same single source of truth as interaction handling.\r\n  useEffect(() => {\r\n    actions._registerKeymap(effectiveKeymap);\r\n    validateKeymap(effectiveKeymap);\r\n    return () => actions._registerKeymap(DEFAULT_KEYMAP);\r\n  }, [actions, effectiveKeymap]);\r\n\r\n  useViewportElement(scrollRef, viewportRef);\r\n  // Imperative data-scrolled-left/right writer (for the pinned-edge shadows) — no React re-render per tick.\r\n  useScrolledEdges(scrollRef, viewportRef);\r\n  // Measures the pin-shadow's real anchor from the boundary header cell's own rendered edge,\r\n  // instead of a JS-summed width total that can drift a subpixel from CSS Grid's own track\r\n  // rounding (see the hook's doc comment) — no React re-render, imperative CSS var write.\r\n  // layout signature, not just pins: adding a marker column MOVES the pinned header sideways\r\n  // without resizing it or the viewport, so the ResizeObserver never fires for it.\r\n  usePinShadowEdges(viewportRef, hasPinnedLeft, hasPinnedRight, direction, `${pins.join(\",\")}|${layout.markerWidth}|${widths.join(\",\")}`);\r\n\r\n  // Dev-only guardrail: getRowClassName/getCellClassName reach\r\n  // row.tsx/cell.tsx through this root's context value rather than a per-row prop specifically so a\r\n  // DataGridRoot re-render (rare — structural only, see the useScrollSnapshot comment above) doesn't\r\n  // reach the memoized row/cell hot path; an unstable identity here would still bust the CONTEXT\r\n  // consumers on every DataGridRoot render, same anti-pattern as unstable `columns`/`data`.\r\n  const prevGetRowClassName = useRef(getRowClassName);\r\n  const prevGetCellClassName = useRef(getCellClassName);\r\n  const prevOnCellClick = useRef(onCellClick);\r\n  const prevOnRowClick = useRef(onRowClick);\r\n  useEffect(() => {\r\n    if (isDev()) {\r\n      if (prevGetRowClassName.current !== undefined && prevGetRowClassName.current !== getRowClassName) {\r\n        console.warn(\"[data-grid] getRowClassName identity changed since the last render; pass a stable reference (e.g. useCallback) or every row re-renders\");\r\n      }\r\n      if (prevGetCellClassName.current !== undefined && prevGetCellClassName.current !== getCellClassName) {\r\n        console.warn(\"[data-grid] getCellClassName identity changed since the last render; pass a stable reference (e.g. useCallback) or every cell re-renders\");\r\n      }\r\n      if (prevOnCellClick.current !== undefined && prevOnCellClick.current !== onCellClick) {\r\n        console.warn(\"[data-grid] onCellClick identity changed since the last render; pass a stable reference (e.g. useCallback) or every cell re-renders\");\r\n      }\r\n      if (prevOnRowClick.current !== undefined && prevOnRowClick.current !== onRowClick) {\r\n        console.warn(\"[data-grid] onRowClick identity changed since the last render; pass a stable reference (e.g. useCallback) or every row re-renders\");\r\n      }\r\n    }\r\n    prevGetRowClassName.current = getRowClassName;\r\n    prevGetCellClassName.current = getCellClassName;\r\n    prevOnCellClick.current = onCellClick;\r\n    prevOnRowClick.current = onRowClick;\r\n  }, [getRowClassName, getCellClassName, onCellClick, onRowClick]);\r\n\r\n  const { indices: columnWindowIndices } = useColumnWindow(scrollRef, {\r\n    widths,\r\n    pins,\r\n    markerWidth: layout.markerWidth,\r\n    contentWidth: totalWidth,\r\n    overscan: columnOverscan,\r\n  });\r\n  const columnIndices = useMemo(() => {\r\n    const indices = columnWindowIndices;\r\n    // the active cell's column always renders, even off-window, so focus survives scroll\r\n    if (activeColumn !== null && !indices.includes(activeColumn) && activeColumn < visibleColumns.length) {\r\n      return [...indices, activeColumn].sort((a, b) => a - b);\r\n    }\r\n    return indices;\r\n  }, [columnWindowIndices, activeColumn, visibleColumns.length]);\r\n\r\n  // Full scroll extent includes both pinned bands: the top band sits above row 0 (part of\r\n  // effectiveHeaderHeight already), the bottom band needs its own extra room reserved past the\r\n  // last data row so the native scrollbar's range still covers exactly the data rows plus both bands.\r\n  const contentHeight = effectiveHeaderHeight + rowCount * rowHeight + pinnedBottomHeight;\r\n\r\n  // loading+empty: enough skeleton rows to fill the viewport below the header, plus\r\n  // one so a partial row is visible at the bottom edge like the real windowed body would show.\r\n  const showLoadingSkeleton = loading && rowCount === 0;\r\n  const skeletonRowCount = showLoadingSkeleton\r\n    ? Math.max(1, Math.ceil((dimensions.clientHeight - effectiveHeaderHeight) / rowHeight) + 1)\r\n    : 0;\r\n  const showLoadingBar = loading && rowCount > 0;\r\n\r\n  const rootStyle: CSSProperties = {\r\n    contain: \"content\",\r\n    // Explicit, not incidental: contain's paint containment happens to create a stacking\r\n    // context, but the guarantee the grid RELIES on is isolation — no internal GRID_LAYER\r\n    // value may ever compete with the page's own stacking order (shadcn dialogs sit at z-50).\r\n    isolation: \"isolate\",\r\n  };\r\n\r\n  const contentStyle: CSSProperties = {\r\n    position: \"relative\",\r\n    width: totalWidth,\r\n    height: contentHeight,\r\n  };\r\n\r\n  const viewportStyle: CSSProperties & Record<string, string | number> = {\r\n    position: \"sticky\",\r\n    insetBlockStart: 0,\r\n    insetInlineStart: 0,\r\n    display: \"block\",\r\n    width: dimensions.clientWidth || \"100%\",\r\n    height: dimensions.clientHeight || \"100%\",\r\n    overflow: \"hidden\",\r\n    \"--grid-row-height\": `${rowHeight}px`,\r\n    \"--grid-viewport-width\": `${dimensions.clientWidth}px`,\r\n    \"--grid-content-width\": `${totalWidth}px`,\r\n    // Sign of the horizontal scroll transform. CSS Grid and every inset-inline-* offset mirror\r\n    // themselves under dir=rtl, but `transform` is always physical — so the canvas/header/band\r\n    // transforms multiply the (always positive) scroll offset by this instead of a literal -1. The\r\n    // browser resolves it at compositing time, which is what keeps the per-tick scroll write, and\r\n    // the whole hot path, byte-for-byte identical in both directions.\r\n    \"--grid-dir\": directionSign(direction),\r\n  };\r\n  visibleColumns.forEach((_, i) => {\r\n    // Recomputed meaning: static parts only — the live scroll term is added in\r\n    // the cell's own inset calc(), not baked in here, so one scroll-var write moves every pinned cell.\r\n    viewportStyle[`--grid-pin-left-${i}`] = `${leftOffsets[i]}px`;\r\n    viewportStyle[`--grid-pin-right-${i}`] = `${rightOffsets[i]}px`;\r\n    viewportStyle[`--grid-track-left-${i}`] = `${lefts[i]}px`;\r\n    viewportStyle[`--grid-track-right-${i}`] = `${rights[i]}px`;\r\n  });\r\n\r\n  // Windowed columns, in display order, with their real index preserved for grid placement.\r\n  // columnIndices are indices into visibleColumns by useColumnWindow's contract.\r\n  const windowedColumns = useMemo(\r\n    () => columnIndices.map((index) => ({ column: visibleColumns[index]!, index })),\r\n    [columnIndices, visibleColumns],\r\n  );\r\n\r\n  // Stable context value on real deps (mustFix): DataGridRoot re-renders on every activeCell move\r\n  // (the activeColumn subscription above), and cell.tsx/header.tsx read this context directly —\r\n  // a fresh object literal here would bust every context consumer (all windowed cells, header\r\n  // cells) on every click/arrow-key move even though most of these fields didn't actually change.\r\n  const contextValue = useMemo(\r\n    () => ({\r\n      scrollRef,\r\n      viewportRef,\r\n      columns: visibleColumns,\r\n      windowedColumns,\r\n      layout,\r\n      rowHeight,\r\n      headerHeight,\r\n      pinnedTopHeight,\r\n      pinnedBottomHeight,\r\n      pinnedTopCount: pinnedTopRows.length,\r\n      template,\r\n      interaction,\r\n      direction,\r\n      readOnly,\r\n      renderHeaderMenu,\r\n      renderMarker,\r\n      renderMarkerHeader,\r\n      // Erasure boundary: DataGridRootContextValue is one shared non-generic context for every\r\n      // DataGridRoot<TData>, so a typed caller's callbacks widen to <unknown> here — safe because\r\n      // every row they ever receive at runtime came from this same subtree's DataGridProvider.\r\n      getRowClassName: getRowClassName as GetRowClassName<unknown> | undefined,\r\n      getCellClassName: getCellClassName as GetCellClassName<unknown> | undefined,\r\n      onCellClick: onCellClick as OnCellClick<unknown> | undefined,\r\n      onRowClick: onRowClick as OnRowClick<unknown> | undefined,\r\n      onRowWindowChange,\r\n    }),\r\n    [\r\n      scrollRef,\r\n      viewportRef,\r\n      visibleColumns,\r\n      windowedColumns,\r\n      layout,\r\n      rowHeight,\r\n      headerHeight,\r\n      pinnedTopHeight,\r\n      pinnedBottomHeight,\r\n      pinnedTopRows.length,\r\n      template,\r\n      interaction,\r\n      direction,\r\n      readOnly,\r\n      renderHeaderMenu,\r\n      renderMarker,\r\n      renderMarkerHeader,\r\n      getRowClassName,\r\n      getCellClassName,\r\n      onCellClick,\r\n      onRowClick,\r\n      onRowWindowChange,\r\n    ],\r\n  );\r\n\r\n  return (\r\n    // Two switches, and neither substitutes for the other: `dir` on the scroll root drives every\r\n    // CSS `rtl:` variant and all logical properties (React context is invisible to CSS), while\r\n    // DirectionProvider drives Base UI's own JS positioning and roving focus for all the chrome —\r\n    // menus, popovers, dialogs (the DOM attribute is invisible to React context).\r\n    <DirectionProvider direction={direction}>\r\n    <DataGridRootContext value={contextValue}>\r\n      <div\r\n        ref={scrollRef}\r\n        role=\"grid\"\r\n        dir={direction}\r\n        // Roving-tabindex bootstrap (WAI-ARIA APG grid pattern): before any cell has been made\r\n        // active, every cell renders tabIndex=-1, so the root itself must be the one tabbable\r\n        // entry point or a keyboard user can never reach the grid at all. Once a cell becomes\r\n        // active it takes over as the sole tab stop (see cell.tsx), and the root reverts to -1.\r\n        tabIndex={!hasActiveCell && rowCount > 0 ? 0 : -1}\r\n        aria-rowcount={rowCount + 1 + pinnedTopRows.length + pinnedBottomRows.length}\r\n        aria-colcount={visibleColumns.length}\r\n        aria-multiselectable={ariaMultiselectable || undefined}\r\n        aria-readonly={readOnly || undefined}\r\n        aria-busy={loading || undefined}\r\n        className={cn(\"relative select-none overflow-auto rounded-md border border-border bg-background text-sm\", className)}\r\n        style={rootStyle}\r\n        onKeyDown={interaction.onKeyDown}\r\n        onPointerDown={interaction.onRootPointerDown}\r\n        onPointerDownCapture={() => {\r\n          pointerInteractionRef.current = true;\r\n        }}\r\n        onFocus={(event) => {\r\n          // only a focus landing directly on the root, NOT following a pointer gesture (Tab from\r\n          // outside, the only case tabIndex=0 is reachable in) seeds the first cell — a header/\r\n          // marker click's own row/column-channel selection must not be clobbered back to (0,0),\r\n          // and focus bubbling up from a cell that already made itself active must not re-seed either.\r\n          if (event.target === event.currentTarget && !pointerInteractionRef.current && !hasActiveCell && rowCount > 0) {\r\n            actions.selectCell({ col: 0, row: 0 });\r\n          }\r\n          pointerInteractionRef.current = false;\r\n        }}\r\n      >\r\n        {/* in-flow, sized to the full virtual extent — provides native scroll range; zero visible children besides the viewport */}\r\n        <div style={contentStyle}>\r\n          <div ref={viewportRef} style={viewportStyle}>\r\n            {children}\r\n            {pinnedTopRows.length > 0 &&\r\n              rowBands.renderBand({\r\n                position: \"top\",\r\n                rows: pinnedTopRows,\r\n                windowedColumns,\r\n                layout,\r\n                rowHeight,\r\n                template,\r\n                headerHeight,\r\n                ariaRowIndexBase: 2,\r\n              })}\r\n            {pinnedBottomRows.length > 0 &&\r\n              rowBands.renderBand({\r\n                position: \"bottom\",\r\n                rows: pinnedBottomRows,\r\n                windowedColumns,\r\n                layout,\r\n                rowHeight,\r\n                template,\r\n                headerHeight,\r\n                ariaRowIndexBase: 2 + pinnedTopRows.length + rowCount,\r\n              })}\r\n            {/* the empty state never shows while loading — a zero-row loading grid renders the skeleton below instead */}\r\n            {rowCount === 0 && !loading && (\r\n              <div\r\n                data-grid-empty-state=\"\"\r\n                className=\"pointer-events-none absolute inset-0 flex items-center justify-center text-sm text-muted-foreground\"\r\n                style={{ insetBlockStart: effectiveHeaderHeight }}\r\n              >\r\n                {emptyState ?? emptyStateLabel}\r\n              </div>\r\n            )}\r\n            {showLoadingSkeleton && (\r\n              <DataGridLoadingSkeleton\r\n                rowCount={skeletonRowCount}\r\n                columnCount={visibleColumns.length}\r\n                rowHeight={rowHeight}\r\n                headerHeight={effectiveHeaderHeight}\r\n                ariaLabel={gridLabels.loading}\r\n              />\r\n            )}\r\n            {showLoadingBar && <DataGridLoadingBar headerHeight={effectiveHeaderHeight} ariaLabel={gridLabels.loading} />}\r\n            <div\r\n              data-grid-pin-shadow=\"left\"\r\n              data-pinned={hasPinnedLeft ? \"\" : undefined}\r\n              aria-hidden=\"true\"\r\n              className=\"pointer-events-none absolute opacity-0 transition-opacity in-data-scrolled-left:opacity-100\"\r\n              style={{\r\n                insetInlineStart: `var(--grid-pin-shadow-left-x, ${interactionLayout.pinnedLeftWidth}px)`,\r\n                insetBlockStart: effectiveHeaderHeight,\r\n                insetBlockEnd: pinnedBottomHeight,\r\n                width: SHADOW_SIZE,\r\n                // background gradient, not box-shadow: a box-shadow's offset+blur paints its darkest\r\n                // pixels well outside this element's own box, floating the visible shadow off the\r\n                // pinned cell's edge — a gradient anchored at inset-inline-start:0 (the edge itself)\r\n                // guarantees the darkest pixel sits exactly on the boundary this element is measured to.\r\n                // Gradient direction keywords are physical, so the fade is mirrored explicitly here.\r\n                backgroundImage: `linear-gradient(to ${isRtl ? \"left\" : \"right\"}, ${shadowColor(hasPinnedLeft)}, transparent)`,\r\n                zIndex: GRID_LAYER.pinShadow,\r\n              }}\r\n            />\r\n            <div\r\n              data-grid-pin-shadow=\"right\"\r\n              data-pinned={hasPinnedRight ? \"\" : undefined}\r\n              aria-hidden=\"true\"\r\n              className=\"pointer-events-none absolute opacity-0 transition-opacity in-data-scrolled-right:opacity-100\"\r\n              style={{\r\n                insetInlineEnd: `var(--grid-pin-shadow-right-x, ${interactionLayout.pinnedRightWidth}px)`,\r\n                insetBlockStart: effectiveHeaderHeight,\r\n                insetBlockEnd: pinnedBottomHeight,\r\n                width: SHADOW_SIZE,\r\n                backgroundImage: `linear-gradient(to ${isRtl ? \"right\" : \"left\"}, ${shadowColor(hasPinnedRight)}, transparent)`,\r\n                zIndex: GRID_LAYER.pinShadow,\r\n              }}\r\n            />\r\n            <div\r\n              data-grid-pin-shadow=\"top\"\r\n              data-pinned={pinnedTopRows.length > 0 ? \"\" : undefined}\r\n              aria-hidden=\"true\"\r\n              className=\"pointer-events-none absolute opacity-0 transition-opacity in-data-scrolled-top:opacity-100\"\r\n              style={{\r\n                insetBlockStart: effectiveHeaderHeight,\r\n                insetInlineStart: `var(--grid-pin-shadow-left-x, ${interactionLayout.pinnedLeftWidth}px)`,\r\n                insetInlineEnd: `var(--grid-pin-shadow-right-x, ${interactionLayout.pinnedRightWidth}px)`,\r\n                height: SHADOW_SIZE,\r\n                backgroundImage: `linear-gradient(to bottom, ${shadowColor(pinnedTopRows.length > 0)}, transparent)`,\r\n                zIndex: GRID_LAYER.pinShadow,\r\n              }}\r\n            />\r\n            <div\r\n              data-grid-pin-shadow=\"bottom\"\r\n              data-pinned={pinnedBottomRows.length > 0 ? \"\" : undefined}\r\n              aria-hidden=\"true\"\r\n              className=\"pointer-events-none absolute opacity-0 transition-opacity in-data-scrolled-bottom:opacity-100\"\r\n              style={{\r\n                insetBlockEnd: pinnedBottomHeight,\r\n                insetInlineStart: `var(--grid-pin-shadow-left-x, ${interactionLayout.pinnedLeftWidth}px)`,\r\n                insetInlineEnd: `var(--grid-pin-shadow-right-x, ${interactionLayout.pinnedRightWidth}px)`,\r\n                height: SHADOW_SIZE,\r\n                backgroundImage: `linear-gradient(to top, ${shadowColor(pinnedBottomRows.length > 0)}, transparent)`,\r\n                zIndex: GRID_LAYER.pinShadow,\r\n              }}\r\n            />\r\n          </div>\r\n        </div>\r\n      </div>\r\n    </DataGridRootContext>\r\n    </DirectionProvider>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/root.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/row.tsx",
      "content": "\"use client\";\r\n\r\nimport { memo, type PointerEvent as ReactPointerEvent, type Ref } from \"react\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { useDataGridRow, useDataGridRowCellState } from \"./store\";\r\nimport { colRangesContain } from \"./selection/selected-col-ranges-for-row\";\r\nimport { DataGridCell } from \"./cell\";\r\nimport { DataGridMarkerCell } from \"./rows/marker-cell\";\r\nimport type { ColumnLayout, WindowedColumn } from \"./layout-context\";\r\nimport type { GetCellClassName, GetRowClassName, OnCellClick, OnRowClick, RowMarkersMode } from \"./types\";\r\n\r\nexport type DataGridRowProps = {\r\n  viewRowIndex: number;\r\n  windowedColumns: readonly WindowedColumn[];\r\n  layout: ColumnLayout;\r\n  readOnly?: boolean;\r\n  /** 'none' renders no marker cell for this row. */\r\n  rowMarkers: RowMarkersMode;\r\n  onMarkerPointerDown: (viewRowIndex: number, event: ReactPointerEvent<HTMLElement>) => void;\r\n  /** Attach to the marker's grip pointerdown ('reorder' family only) — see use-grid-interaction.ts' onMarkerGripPointerDown. */\r\n  onMarkerGripPointerDown: (viewRowIndex: number, event: ReactPointerEvent<HTMLElement>) => void;\r\n  /** Attach to the marker's checkbox pointerdown ('checkbox'/'both' modes) — see use-grid-interaction.ts' onMarkerCheckboxPointerDown. */\r\n  onMarkerCheckboxPointerDown: (viewRowIndex: number, event: ReactPointerEvent<HTMLElement>) => void;\r\n  /** Attach to the marker cell's reorder-drag pointerdown — see rows/use-row-reorder.ts. Stable identity (body's hook instance). */\r\n  onMarkerReorderPointerDown: (viewRowIndex: number, event: ReactPointerEvent<HTMLElement>) => void;\r\n  /** True while this row is the reorder-drag source (dims the marker cell). Flips at most twice per gesture. */\r\n  isRowReorderDragging: boolean;\r\n  /** Forwarded to the row's own root div — body.tsx uses it to write `gridRowStart` imperatively (see below). */\r\n  rowRef: Ref<HTMLDivElement>;\r\n  /** Pinned-top row count (a11y index layout: header=1, pinned-top next, then data, pinned-bottom last) — shifts `aria-rowindex` past the pinned-top band; 0 when there is none. */\r\n  ariaRowIndexOffset: number;\r\n  /** Row class hook; stable identity (root's dev guardrail) so this memoized row's props stay shallow-equal across ticks. */\r\n  getRowClassName?: GetRowClassName<unknown>;\r\n  /** Forwarded to each cell; see {@link DataGridRowProps.getRowClassName}. */\r\n  getCellClassName?: GetCellClassName<unknown>;\r\n  /** Forwarded to each cell's click handler; stable identity, same guidance as `getCellClassName`. */\r\n  onCellClick?: OnCellClick<unknown>;\r\n  /** Forwarded to each cell; fired alongside `onCellClick` once per click. */\r\n  onRowClick?: OnRowClick<unknown>;\r\n};\r\n\r\n/**\r\n * A window shift moves EVERY surviving row's canvas slot by the shift amount — gridRowStart\r\n * (viewRowIndex - windowStart + 1) changes for all mounted rows, not just the ones that\r\n * entered/left — so `windowStart` was never a prop this component could shallow-compare away no\r\n * matter what body.tsx precomputed; it's a genuine per-tick input to every row's position.\r\n * Subgrid (`gridTemplateColumns: \"subgrid\"`, rows as direct grid children) also rules out\r\n * absolute-positioning — a positioning wrapper around the row breaks the subgrid column tracks.\r\n *\r\n * Fix: `windowStart` doesn't reach this component at all anymore. It never renders `gridRowStart`\r\n * itself — body.tsx (which already re-renders every window tick regardless) collects each row's DOM\r\n * node via `rowRef` and writes `gridRowStart` straight into `style.gridRowStart` in a `useLayoutEffect`\r\n * that runs after every body render, entirely outside React's props/state for this component. That\r\n * keeps `DataGridRow`'s memo props limited to things that only change when the row's actual content\r\n * should re-render (viewRowIndex swap on identity reuse, column window, readOnly, etc).\r\n */\r\nexport const DataGridRow = memo(function DataGridRow({\r\n  viewRowIndex,\r\n  windowedColumns,\r\n  layout,\r\n  readOnly,\r\n  rowMarkers,\r\n  onMarkerPointerDown,\r\n  onMarkerGripPointerDown,\r\n  onMarkerCheckboxPointerDown,\r\n  onMarkerReorderPointerDown,\r\n  isRowReorderDragging,\r\n  rowRef,\r\n  ariaRowIndexOffset,\r\n  getRowClassName,\r\n  getCellClassName,\r\n  onCellClick,\r\n  onRowClick,\r\n}: DataGridRowProps) {\r\n  const row = useDataGridRow(viewRowIndex);\r\n  // ONE subscription for the whole row's interactive cell state (not one per cell). Referentially\r\n  // stable across ticks that don't touch this row (see useDataGridRowCellState's comparator), so\r\n  // this memoized row only re-renders when its own active/editing/search/selection state changed.\r\n  const cellState = useDataGridRowCellState(viewRowIndex);\r\n  // marker column occupies grid track 1 when present, so every data column shifts one track right\r\n  const markerColOffset = layout.markerWidth > 0 ? 2 : 1;\r\n  // A hole in `data` (lazy loading): row.tsx renders skeleton cells instead of empty ones.\r\n  // `row` flipping undefined->defined is an ordinary content change under the row's\r\n  // existing memo/subscription contract (useDataGridRow re-renders on that identity change like\r\n  // any other row edit) — no extra state or subscription needed for the transition itself.\r\n  const isSkeleton = row === undefined;\r\n  return (\r\n    <div\r\n      ref={rowRef}\r\n      role=\"row\"\r\n      aria-rowindex={viewRowIndex + 2 + ariaRowIndexOffset}\r\n      data-grid-row-index={viewRowIndex}\r\n      aria-busy={isSkeleton || undefined}\r\n      // named group so row hover reveals a bg tint on its cells via CSS only — never React state.\r\n      className={cn(\"group/row\", row === undefined ? undefined : getRowClassName?.(row, viewRowIndex))}\r\n      style={{\r\n        display: \"grid\",\r\n        gridTemplateColumns: \"subgrid\",\r\n        gridColumn: \"1 / -1\",\r\n        // gridRowStart is intentionally absent here — body.tsx writes it imperatively (see doc\r\n        // comment above) so a window shift never touches this component's render inputs.\r\n      }}\r\n    >\r\n      {rowMarkers !== \"none\" && (\r\n        // marker column still shows the row number for a skeleton row — it's index-derived, not read from `row`.\r\n        <DataGridMarkerCell\r\n          mode={rowMarkers}\r\n          viewRowIndex={viewRowIndex}\r\n          onPointerDown={onMarkerPointerDown}\r\n          onGripPointerDown={onMarkerGripPointerDown}\r\n          onCheckboxPointerDown={onMarkerCheckboxPointerDown}\r\n          onReorderPointerDown={onMarkerReorderPointerDown}\r\n          isReorderDragging={isRowReorderDragging}\r\n        />\r\n      )}\r\n      {windowedColumns.map(({ column, index }) => {\r\n        const isActive = cellState.activeCol === index;\r\n        const isEditing = cellState.editingCol === index;\r\n        return (\r\n          <DataGridCell\r\n            key={column.id}\r\n            row={row}\r\n            rowIndex={viewRowIndex}\r\n            column={column}\r\n            columnIndex={index}\r\n            gridColOffset={markerColOffset}\r\n            gridReadOnly={readOnly}\r\n            getCellClassName={getCellClassName}\r\n            onCellClick={onCellClick}\r\n            onRowClick={onRowClick}\r\n            isActive={isActive}\r\n            isEditing={isEditing}\r\n            initialText={isEditing ? cellState.editingInitialText : undefined}\r\n            isSearchMatch={cellState.searchMatchCols?.has(index) ?? false}\r\n            isSelected={isActive || colRangesContain(cellState.selectedColRanges, index)}\r\n            isSkeleton={isSkeleton}\r\n            isFlashing={cellState.flashingCols?.has(index) ?? false}\r\n            cellError={cellState.errorCols?.get(index) ?? null}\r\n          />\r\n        );\r\n      })}\r\n    </div>\r\n  );\r\n});\r\n",
      "type": "registry:component",
      "target": "components/data-grid/row.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/rows/density.ts",
      "content": "import type { DensityMode } from \"../types\";\r\n\r\n/** Row height (px) per density preset; the header track height never changes with density. */\r\nconst DENSITY_ROW_HEIGHT: Record<DensityMode, number> = {\r\n  compact: 28,\r\n  default: 36,\r\n  comfortable: 44,\r\n};\r\n\r\n/** Resolves the effective row height: an explicit `rowHeight` prop always wins over `density`. */\r\nexport function resolveRowHeight(density: DensityMode | undefined, rowHeight: number | undefined): number {\r\n  if (rowHeight !== undefined) return rowHeight;\r\n  return DENSITY_ROW_HEIGHT[density ?? \"default\"];\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/rows/density.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/rows/index.ts",
      "content": "/** Domain barrel — row density, marker column, and loading-state components. */\r\nexport { resolveRowHeight } from \"./density\";\r\nexport { markerWidth } from \"./marker-width\";\r\nexport { DataGridMarkerCell, type DataGridMarkerCellProps } from \"./marker-cell\";\r\nexport { DataGridMarkerHeader, type DataGridMarkerHeaderProps } from \"./marker-header\";\r\nexport { DataGridLoadingSkeleton, type DataGridLoadingSkeletonProps, DataGridLoadingBar, type DataGridLoadingBarProps } from \"./loading-skeleton\";\r\n",
      "type": "registry:component",
      "target": "components/data-grid/rows/index.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/rows/loading-skeleton.tsx",
      "content": "\"use client\";\r\n\r\nimport type { CSSProperties, ReactNode } from \"react\";\r\nimport { GRID_LAYER } from \"../layers\";\r\n\r\n/** Props for {@link DataGridLoadingSkeleton}. */\r\nexport type DataGridLoadingSkeletonProps = {\r\n  /** Enough skeleton rows to fill the viewport at the current row height. */\r\n  rowCount: number;\r\n  /** How many skeleton bars per row — one per visible column (marker column excluded, it has no value to shape). */\r\n  columnCount: number;\r\n  rowHeight: number;\r\n  headerHeight: number;\r\n  ariaLabel: string;\r\n};\r\n\r\n/**\r\n * Grid-wide loading state, empty-data case (`loading && rowCount === 0`). Reuses the\r\n * `data-grid-lazy` add-on's skeleton-cell visual language (muted `animate-pulse` bar per cell) but\r\n * lives in core as one small component — core cannot import from an add-on, and this needs no real\r\n * column layout (there's no data yet), just enough rows/bars to fill the viewport.\r\n */\r\nexport function DataGridLoadingSkeleton(props: DataGridLoadingSkeletonProps): ReactNode {\r\n  const { rowCount, columnCount, rowHeight, headerHeight, ariaLabel } = props;\r\n  const rows = Array.from({ length: rowCount }, (_, i) => i);\r\n  const cols = Array.from({ length: columnCount }, (_, i) => i);\r\n  const style: CSSProperties = {\r\n    position: \"absolute\",\r\n    insetInlineStart: 0,\r\n    insetBlockStart: headerHeight,\r\n    width: \"100%\",\r\n  };\r\n  return (\r\n    <div\r\n      role=\"presentation\"\r\n      aria-label={ariaLabel}\r\n      data-grid-loading-skeleton=\"\"\r\n      className=\"pointer-events-none absolute\"\r\n      style={style}\r\n    >\r\n      {rows.map((r) => (\r\n        <div key={r} className=\"flex items-center gap-2 border-b border-border px-2\" style={{ height: rowHeight }}>\r\n          {cols.map((c) => (\r\n            <div key={c} className=\"h-4 flex-1 motion-safe:animate-pulse rounded bg-muted\" />\r\n          ))}\r\n        </div>\r\n      ))}\r\n    </div>\r\n  );\r\n}\r\n\r\n/** Props for {@link DataGridLoadingBar}. */\r\nexport type DataGridLoadingBarProps = {\r\n  headerHeight: number;\r\n  ariaLabel: string;\r\n};\r\n\r\n/** Scoped keyframes (not a `@theme` global.css addition) so this file stays self-contained as a registry item — no consumer build-config step beyond installing the component. */\r\nconst SWEEP_KEYFRAMES = \"@keyframes grid-loading-sweep{0%{transform:translateX(-100%)}100%{transform:translateX(300%)}}\";\r\n\r\n/**\r\n * Grid-wide loading state, data-present case (`loading && rowCount > 0` — a background\r\n * refresh keeps rows visible). A slim indeterminate bar pinned directly under the header; the\r\n * indeterminate sweep is a `prefers-reduced-motion`-gated CSS animation (`motion-safe:` variant) —\r\n * `motion-reduce` falls back to a static full-width bar, no JS/rAF involved either way.\r\n */\r\nexport function DataGridLoadingBar(props: DataGridLoadingBarProps): ReactNode {\r\n  const { headerHeight, ariaLabel } = props;\r\n  const style: CSSProperties = {\r\n    position: \"absolute\",\r\n    insetInlineStart: 0,\r\n    insetBlockStart: headerHeight,\r\n    zIndex: GRID_LAYER.skeleton,\r\n  };\r\n  return (\r\n    <div\r\n      role=\"progressbar\"\r\n      aria-label={ariaLabel}\r\n      aria-valuetext={ariaLabel}\r\n      data-grid-loading-bar=\"\"\r\n      className=\"pointer-events-none absolute h-0.5 w-full overflow-hidden bg-primary/20\"\r\n      style={style}\r\n    >\r\n      {/* plain <style> tag (not styled-jsx) scopes the sweep keyframes here without a global.css edit */}\r\n      <style>{SWEEP_KEYFRAMES}</style>\r\n      <div className=\"h-full w-1/3 bg-primary motion-safe:animate-[grid-loading-sweep_var(--grid-loading-sweep-duration,1.2s)_ease-in-out_infinite] motion-reduce:w-full\" />\r\n    </div>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/rows/loading-skeleton.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/rows/marker-cell.tsx",
      "content": "\"use client\";\r\n\r\nimport type { CSSProperties, PointerEvent as ReactPointerEvent, ReactNode } from \"react\";\r\nimport { GripVertical } from \"lucide-react\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { COLUMN_BORDER } from \"../columns/column-border\";\r\nimport { Checkbox } from \"@/components/ui/checkbox\";\r\nimport type { RowMarkersMode } from \"../types\";\r\nimport { GRID_LAYER } from \"../layers\";\r\nimport { gridAttrSelector } from \"../data-attributes\";\r\nimport { useDataGridRootContext } from \"../layout-context\";\r\nimport { isReorderMarkerMode, markerContent } from \"./marker-width\";\r\nimport {\r\n  useDataGridActions,\r\n  useDataGridIsRowCellSelected,\r\n  useDataGridIsRowChannelSelected,\r\n  useDataGridIsRowSelected,\r\n  useDataGridLabels,\r\n  useDataGridRowReorderEnabled,\r\n} from \"../store\";\r\n\r\n/** Props for {@link DataGridMarkerCell}. */\r\nexport type DataGridMarkerCellProps = {\r\n  mode: RowMarkersMode;\r\n  viewRowIndex: number;\r\n  /** Number/rest zone of the cell: resolves the plain/shift/ctrl click-select gesture and starts a row-range drag (see use-grid-interaction.ts' onMarkerPointerDown). */\r\n  onPointerDown: (viewRowIndex: number, event: ReactPointerEvent<HTMLDivElement>) => void;\r\n  /** Grip zone of the cell (reorder family): click-select without a row-range drag (see use-grid-interaction.ts' onMarkerGripPointerDown). */\r\n  onGripPointerDown: (viewRowIndex: number, event: ReactPointerEvent<HTMLDivElement>) => void;\r\n  /** Arms the same row-range drag from a press on the checkbox glyph itself, without pre-empting its own click->toggle (see use-grid-interaction.ts' onMarkerCheckboxPointerDown). */\r\n  onCheckboxPointerDown: (viewRowIndex: number, event: ReactPointerEvent<HTMLElement>) => void;\r\n  /** Arms the row-reorder drag from a press on the grip zone; a stable callback from the body's useRowReorder instance. */\r\n  onReorderPointerDown: (viewRowIndex: number, event: ReactPointerEvent<HTMLElement>) => void;\r\n  /** True while this row is the reorder-drag source (dims the marker, like the column reorder does to its header cell). */\r\n  isReorderDragging: boolean;\r\n};\r\n\r\n/**\r\n * The marker column's per-row cell: row number (modes whose content includes number), a row-select\r\n * checkbox (content includes checkbox), both with the number hidden on hover/selected via\r\n * group-hover (content 'both'), and — for the `reorder` family — a grip handle that drags the row\r\n * to a new position on top of the suffix's content. Always pinned-left\r\n * at grid track 1 with a fixed zero offset (nothing pins before it) — outside the data column's\r\n * aria-colindex space (see {@link DataGridMarkerHeader} for why it carries no `aria-colindex`).\r\n *\r\n * Hit registration is zone-based so the press location — not later pointer movement — decides the\r\n * gesture: the checkbox glyph is a pure row-select toggle/range, the grip (reorder family) is the\r\n * only zone that arms the row-reorder drag (and click-selects on a stationary press), and the\r\n * number/rest of the cell is the plain row-select surface.\r\n */\r\nexport function DataGridMarkerCell(props: DataGridMarkerCellProps): ReactNode {\r\n  const { mode, viewRowIndex, onPointerDown, onGripPointerDown, onCheckboxPointerDown, onReorderPointerDown, isReorderDragging } = props;\r\n  const actions = useDataGridActions();\r\n  // chrome (wrapper tint/data-row-selected) and the built-in checkbox keep the any-channel union\r\n  const isSelected = useDataGridIsRowSelected(viewRowIndex);\r\n  // the renderer gets the channels apart: row selection vs cell selection (see MarkerCellRenderCtx)\r\n  const isRowChannelSelected = useDataGridIsRowChannelSelected(viewRowIndex);\r\n  const isCellSelected = useDataGridIsRowCellSelected(viewRowIndex);\r\n  const labels = useDataGridLabels();\r\n  const reorderEnabled = useDataGridRowReorderEnabled();\r\n  const { renderMarker } = useDataGridRootContext();\r\n\r\n  const style: CSSProperties = {\r\n    gridColumnStart: 1,\r\n    position: \"relative\",\r\n    insetInlineStart: \"var(--grid-scroll-left, 0px)\",\r\n    // every cell is position:relative, so DOM order decides paint order among them — the\r\n    // marker is FIRST in the row, so without this it paints under every scrolled data cell.\r\n    zIndex: GRID_LAYER.markerCell,\r\n  };\r\n\r\n  const content = markerContent(mode);\r\n  const showNumber = content === \"number\" || content === \"both\";\r\n  const showCheckbox = content === \"checkbox\" || content === \"both\";\r\n  // the grip renders (and the grab cursor + reorder aria-label apply) only in the reorder family\r\n  // — plain modes keep the marker a pure row-select surface.\r\n  const showGrip = isReorderMarkerMode(mode);\r\n  const swapOnHover = content === \"both\";\r\n\r\n  return (\r\n    <div\r\n      role=\"gridcell\"\r\n      data-grid-marker-cell=\"\"\r\n      data-row-selected={isSelected || undefined}\r\n      data-dragging={isReorderDragging || undefined}\r\n      aria-label={showGrip ? labels.markers.reorderRow(viewRowIndex + 1) : undefined}\r\n      className={cn(\r\n        \"group flex items-center justify-center border-b border-border bg-background transition-colors group-hover/row:transition-none\",\r\n        COLUMN_BORDER,\r\n        // Same rule as a pinned-column cell (see cell.tsx): this sits opaque above the scrolled\r\n        // canvas, so a translucent tint would let the scrolled content show through it.\r\n        // color-mix() pre-composites the identical tint as an OPAQUE color, in both themes.\r\n        \"group-hover/row:bg-[color-mix(in_oklch,var(--color-muted)_50%,var(--color-background))]\",\r\n        isSelected && \"bg-[color-mix(in_oklch,var(--color-muted)_50%,var(--color-background))]\",\r\n        // the grip zone (everything but the number/checkbox) is the reorder handle\r\n        showGrip && reorderEnabled && \"cursor-grab\",\r\n        showGrip && (showNumber || showCheckbox) && \"gap-1\",\r\n        isReorderDragging && \"opacity-50\",\r\n      )}\r\n      style={style}\r\n      onPointerDown={(event) => {\r\n        // Zone routing: the press location decides the gesture, not later pointer movement.\r\n        const target = event.target instanceof Element ? event.target : null;\r\n        if (target?.closest(gridAttrSelector(\"markerCheckbox\"))) {\r\n          // the checkbox glyph's own onPointerDown (below) already ran for this press: additive\r\n          // toggle on a stationary click, row-range drag on move. Running the wrapper's exclusive\r\n          // selectRow here too would clobber an existing multi-row selection.\r\n          return;\r\n        }\r\n        if (showGrip && (content === \"none\" || !target?.closest(gridAttrSelector(\"markerNumber\")))) {\r\n          // grip zone — a pure 'reorder' cell is grip across the whole cell; in the content modes\r\n          // it is everything except the number/checkbox glyphs. A stationary press selects the\r\n          // row; a vertical drag that leaves the origin row reorders\r\n          onGripPointerDown(viewRowIndex, event);\r\n          onReorderPointerDown(viewRowIndex, event);\r\n          return;\r\n        }\r\n        // the number (reorder family) / rest of the cell (plain modes): the plain row-select\r\n        // surface — range drag, never reorder\r\n        onPointerDown(viewRowIndex, event);\r\n      }}\r\n    >\r\n      {renderMarker ? (\r\n        renderMarker({ viewRowIndex, isRowChannelSelected, isCellSelected })\r\n      ) : (\r\n        <>\r\n          {showGrip && (\r\n            <span data-grid-reorder-handle=\"\" aria-hidden=\"true\" className=\"touch-none text-muted-foreground select-none\">\r\n              <GripVertical className=\"size-3.5\" />\r\n            </span>\r\n          )}\r\n          {showCheckbox && (\r\n            <Checkbox\r\n              data-grid-marker-checkbox=\"\"\r\n              className={cn(\r\n                // row-select zone — override the wrapper's grab cursor in the reorder family\r\n                showGrip && \"cursor-pointer\",\r\n                swapOnHover && \"hidden group-hover:flex data-checked:flex\",\r\n                isSelected && \"flex\",\r\n              )}\r\n              checked={isSelected}\r\n              onCheckedChange={(checked) => actions.setRowSelected(viewRowIndex, checked === true)}\r\n              // a stationary press+release still resolves via onCheckedChange's additive toggle above\r\n              // (native click, unaffected by this); a press that moves arms a row-range drag instead —\r\n              // see DataGridMarkerCellProps.onCheckboxPointerDown for why this can't just stopPropagation.\r\n              onPointerDown={(event) => onCheckboxPointerDown(viewRowIndex, event)}\r\n              aria-label={labels.markers.selectRow(viewRowIndex + 1)}\r\n            />\r\n          )}\r\n          {showNumber && (\r\n            <span\r\n              data-grid-marker-number=\"\"\r\n              className={cn(\r\n                \"select-none text-xs tabular-nums text-muted-foreground\",\r\n                // the number is a row-select zone — override the wrapper's grab cursor\r\n                showGrip && \"cursor-default\",\r\n                swapOnHover && \"group-hover:hidden\",\r\n                swapOnHover && isSelected && \"hidden\",\r\n              )}\r\n            >\r\n              {viewRowIndex + 1}\r\n            </span>\r\n          )}\r\n        </>\r\n      )}\r\n    </div>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/rows/marker-cell.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/rows/marker-header.tsx",
      "content": "\"use client\";\r\n\r\nimport type { CSSProperties, ReactNode } from \"react\";\r\nimport { Checkbox } from \"@/components/ui/checkbox\";\r\nimport type { RowMarkersMode } from \"../types\";\r\nimport { markerContent } from \"./marker-width\";\r\nimport { GRID_LAYER } from \"../layers\";\r\nimport { COLUMN_BORDER } from \"../columns/column-border\";\r\nimport { useDataGridRootContext } from \"../layout-context\";\r\nimport { useDataGridActions, useDataGridAllRowsSelected, useDataGridLabels } from \"../store\";\r\n\r\n/** Props for {@link DataGridMarkerHeader}. */\r\nexport type DataGridMarkerHeaderProps = {\r\n  mode: RowMarkersMode;\r\n  headerHeight: number;\r\n};\r\n\r\n/**\r\n * The marker column's header cell: a select-all checkbox for the modes whose content includes a\r\n * checkbox ('checkbox'/'both' and their `reorder-` variants; checked/indeterminate/unchecked against the rows channel vs. view row count — glide-behavior-spec.md\r\n * §3 \"corner marker\"), otherwise blank chrome. `role=\"columnheader\"` with no `aria-colindex` (its\r\n * position is then inferred from DOM order, per the WAI-ARIA grid pattern) — it's a real structural\r\n * child of the header `row`, just outside the DATA header's aria-colindex/aria-colcount space (see\r\n * marker-cell.tsx's doc comment). `role=\"presentation\"` was tried first but axe's\r\n * aria-required-children rule flags it: a `row`'s required-owned-element children have their\r\n * presentational role force-reverted by the accessibility tree builder regardless (Core-AAM\r\n * \"presentational role conflict resolution\"), so it never actually suppressed anything at the AT\r\n * level — it just left the cell unlabeled.\r\n */\r\nexport function DataGridMarkerHeader(props: DataGridMarkerHeaderProps): ReactNode {\r\n  const { mode, headerHeight } = props;\r\n  const actions = useDataGridActions();\r\n  const state = useDataGridAllRowsSelected();\r\n  const labels = useDataGridLabels();\r\n  const { renderMarkerHeader } = useDataGridRootContext();\r\n\r\n  const style: CSSProperties = {\r\n    gridRowStart: 1,\r\n    gridColumnStart: 1,\r\n    position: \"relative\",\r\n    insetInlineStart: \"var(--grid-scroll-left, 0px)\",\r\n    zIndex: GRID_LAYER.pinnedHeader,\r\n    height: headerHeight,\r\n  };\r\n\r\n  return (\r\n    <div\r\n      role=\"columnheader\"\r\n      data-grid-marker-header=\"\"\r\n      className={`flex items-center justify-center border-b border-border bg-muted ${COLUMN_BORDER}`}\r\n      style={style}\r\n    >\r\n      {renderMarkerHeader ? (\r\n        renderMarkerHeader({ allSelected: state })\r\n      ) : (\r\n        (markerContent(mode) === \"checkbox\" || markerContent(mode) === \"both\") && (\r\n          <Checkbox\r\n            checked={state === \"checked\"}\r\n            indeterminate={state === \"indeterminate\"}\r\n            onCheckedChange={(checked) => actions.setAllRowsSelected(checked === true)}\r\n            aria-label={labels.markers.selectAll}\r\n          />\r\n        )\r\n      )}\r\n    </div>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/rows/marker-header.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/rows/marker-width.ts",
      "content": "import type { RowMarkersContent, RowMarkersMode } from \"../types\";\r\n\r\n/** True for the `reorder` family — the only modes that render the grip and arm the drag-to-reorder gesture. */\r\nexport function isReorderMarkerMode(mode: RowMarkersMode): boolean {\r\n  return mode === \"reorder\" || mode.startsWith(\"reorder-\");\r\n}\r\n\r\n/** The marker content a mode renders — the reorder family renders its suffix's content under the grip. */\r\nexport function markerContent(mode: RowMarkersMode): RowMarkersContent | \"none\" {\r\n  switch (mode) {\r\n    case \"none\":\r\n    case \"reorder\":\r\n      return \"none\";\r\n    case \"reorder-number\":\r\n      return \"number\";\r\n    case \"reorder-checkbox\":\r\n      return \"checkbox\";\r\n    case \"reorder-both\":\r\n      return \"both\";\r\n    case \"number\":\r\n    case \"checkbox\":\r\n    case \"both\":\r\n      return mode;\r\n  }\r\n}\r\n\r\n/** Marker column px width per mode (number 44 / checkbox 36 / both 56 / reorder 32; the reorder family widens for the grip); 0 for 'none' (no track at all). */\r\nexport function markerWidth(mode: RowMarkersMode): number {\r\n  switch (mode) {\r\n    case \"number\":\r\n      return 44;\r\n    case \"checkbox\":\r\n      return 36;\r\n    case \"both\":\r\n      return 56;\r\n    case \"reorder\":\r\n      return 32;\r\n    case \"reorder-number\":\r\n      return 56;\r\n    case \"reorder-checkbox\":\r\n      return 48;\r\n    case \"reorder-both\":\r\n      return 64;\r\n    default:\r\n      return 0;\r\n  }\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/rows/marker-width.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/rows/use-row-reorder.ts",
      "content": "\"use client\";\r\n\r\nimport { useCallback, useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from \"react\";\r\nimport { AUTO_SCROLL_STEP, AUTO_SCROLL_ZONE } from \"../windowing/direction\";\r\n\r\n/** px of vertical pointer movement off the origin row before a marker press commits to a reorder drag. */\r\nconst REORDER_THRESHOLD = 5;\r\n\r\n/**\r\n * Final landing view index (arrayMove semantics) for moving the row at view index `from` to the\r\n * boundary half of view index `over`: dragging down, \"before over\" lands on `over - 1`; dragging\r\n * up, \"after over\" lands on `over + 1`. A boundary on the origin row itself lands on `from` (no-op).\r\n */\r\nexport function predictReorderTarget(from: number, over: number, position: \"before\" | \"after\"): number {\r\n  if (from === over) return from;\r\n  return position === \"before\" ? (from < over ? over - 1 : over) : from < over ? over : over + 1;\r\n}\r\n\r\n/** Live reorder-drag UI state: the dragged row and where the drop indicator should render. */\r\nexport type RowReorderState = {\r\n  draggingRow: number;\r\n  /** View row index the drop indicator renders next to, or null before the pointer has crossed into another row. */\r\n  overRow: number | null;\r\n  /** Drop indicator renders above/below `overRow`. */\r\n  position: \"before\" | \"after\";\r\n};\r\n\r\n/** Per-marker pointerdown handler + the shared drag state, one instance per `<DataGridBody>`. */\r\nexport type RowReorderHandlers = {\r\n  /** Attach to every marker cell's pointerdown (never the checkbox glyph's own press). */\r\n  onMarkerDragPointerDown: (viewRowIndex: number, event: ReactPointerEvent<HTMLElement>) => void;\r\n  dragState: RowReorderState | null;\r\n};\r\n\r\n/**\r\n * Drag-to-reorder rows. The marker cell routes presses to this hook from the grip zone only\r\n * (see DataGridMarkerCell's zone routing), so the gesture never shares a press with the\r\n * row-select range-drag — no coexistence disambiguation is needed. The caller passes `enabled`\r\n * only for the marker's `reorder` family (see RowMarkersMode). Within an armed grip press:\r\n * **a vertical drag that leaves the origin row becomes a reorder drag** (mirrors\r\n * {@link import(\"../columns/use-column-reorder\").useColumnReorder}); a press+drag that never leaves\r\n * the origin row (or moves before `REORDER_THRESHOLD`) resolves as a click — the grip's own\r\n * pointerdown handler already resolved it as a row selection. The grip zone is never armed on\r\n * <kbd>Shift</kbd> (the caller routes shift presses to the row-select gesture instead).\r\n *\r\n * While armed, the hook auto-scrolls the grid's scroll element (same zone/step as the interaction\r\n * layer's select-drag) when the pointer sits beyond a viewport edge, and re-resolves the drop\r\n * boundary after each scrolled frame so a stationary pointer keeps tracking the scrolling content\r\n * — rows outside the rendered window stay reachable through that auto-scroll. <kbd>Escape</kbd>\r\n * cancels the gesture in flight.\r\n */\r\nexport function useRowReorder(args: {\r\n  enabled: boolean;\r\n  /** Resolves the row element at a client point to a view row index + above/below half, or null off any row. */\r\n  hitTestRow: (clientX: number, clientY: number) => { row: number; position: \"before\" | \"after\" } | null;\r\n  onReorder: (from: number, over: number, position: \"before\" | \"after\") => void;\r\n  /** The grid's scroll element, for edge auto-scroll; null before the root mounts. */\r\n  getScrollElement: () => HTMLElement | null;\r\n}): RowReorderHandlers {\r\n  const { enabled } = args;\r\n  const argsRef = useRef(args);\r\n  argsRef.current = args;\r\n\r\n  const [dragState, setDragState] = useState<RowReorderState | null>(null);\r\n  // mirrors dragState synchronously — the document pointermove listener closes over stale React\r\n  // state otherwise (it's attached once per gesture, not re-subscribed on every setDragState).\r\n  const dragStateRef = useRef<RowReorderState | null>(null);\r\n  const pendingRef = useRef<{ row: number; startX: number; startY: number; pointerId: number } | null>(null);\r\n  const pointerPointRef = useRef<{ x: number; y: number } | null>(null);\r\n  const cleanupRef = useRef<(() => void) | null>(null);\r\n  const rafRef = useRef<number | null>(null);\r\n\r\n  const setDrag = useCallback((next: RowReorderState | null) => {\r\n    dragStateRef.current = next;\r\n    setDragState(next);\r\n  }, []);\r\n\r\n  // Edge auto-scroll while armed: one rAF per frame scrolls a fixed step while the pointer sits in\r\n  // an edge zone, then re-runs the hit test at the (stationary) pointer so the indicator tracks the\r\n  // content scrolling underneath it. Mirrors the interaction layer's select-drag auto-scroll.\r\n  const runFrame = useCallback(() => {\r\n    const drag = dragStateRef.current;\r\n    const point = pointerPointRef.current;\r\n    if (!drag || !point) {\r\n      rafRef.current = null;\r\n      return;\r\n    }\r\n    const el = argsRef.current.getScrollElement();\r\n    if (el) {\r\n      const rect = el.getBoundingClientRect();\r\n      const before = el.scrollTop;\r\n      if (point.y < rect.top + AUTO_SCROLL_ZONE) el.scrollTop -= AUTO_SCROLL_STEP;\r\n      else if (point.y > rect.bottom - AUTO_SCROLL_ZONE) el.scrollTop += AUTO_SCROLL_STEP;\r\n      if (el.scrollTop !== before) {\r\n        const hit = argsRef.current.hitTestRow(point.x, point.y);\r\n        const overRow = hit?.row ?? null;\r\n        const position = hit?.position ?? drag.position;\r\n        if (overRow !== drag.overRow || position !== drag.position) {\r\n          setDrag({ draggingRow: drag.draggingRow, overRow, position });\r\n        }\r\n      }\r\n    }\r\n    rafRef.current = requestAnimationFrame(runFrame);\r\n  }, [setDrag]);\r\n\r\n  const stopLoop = useCallback(() => {\r\n    if (rafRef.current !== null) {\r\n      cancelAnimationFrame(rafRef.current);\r\n      rafRef.current = null;\r\n    }\r\n  }, []);\r\n\r\n  const teardown = useCallback(() => {\r\n    cleanupRef.current?.();\r\n    cleanupRef.current = null;\r\n    pendingRef.current = null;\r\n    pointerPointRef.current = null;\r\n    stopLoop();\r\n    setDrag(null);\r\n  }, [setDrag, stopLoop]);\r\n\r\n  const onMarkerDragPointerDown = useCallback(\r\n    (viewRowIndex: number, event: ReactPointerEvent<HTMLElement>) => {\r\n      if (event.button !== 0) return;\r\n      // Shift+drag is always row-select (disambiguation rule above) — never arm the reorder path.\r\n      if (event.shiftKey) return;\r\n      if (!enabled) return;\r\n\r\n      pendingRef.current = { row: viewRowIndex, startX: event.clientX, startY: event.clientY, pointerId: event.pointerId };\r\n      pointerPointRef.current = { x: event.clientX, y: event.clientY };\r\n\r\n      const handleMove = (e: PointerEvent) => {\r\n        const pending = pendingRef.current;\r\n        if (!pending || e.pointerId !== pending.pointerId) return;\r\n        pointerPointRef.current = { x: e.clientX, y: e.clientY };\r\n\r\n        if (!dragStateRef.current) {\r\n          // not yet armed: only commit to reorder once the vertical move exceeds the threshold\r\n          // AND has left the origin row (hitTest resolves to a different row).\r\n          const dy = Math.abs(e.clientY - pending.startY);\r\n          if (dy < REORDER_THRESHOLD) return;\r\n          const hit = argsRef.current.hitTestRow(e.clientX, e.clientY);\r\n          if (!hit || hit.row === pending.row) return;\r\n          setDrag({ draggingRow: pending.row, overRow: hit.row, position: hit.position });\r\n          if (rafRef.current === null) rafRef.current = requestAnimationFrame(runFrame);\r\n          return;\r\n        }\r\n\r\n        const hit = argsRef.current.hitTestRow(e.clientX, e.clientY);\r\n        const prev = dragStateRef.current;\r\n        setDrag({ ...prev, overRow: hit?.row ?? null, position: hit?.position ?? prev.position });\r\n      };\r\n\r\n      const handleUp = (e: PointerEvent) => {\r\n        const pending = pendingRef.current;\r\n        if (!pending || e.pointerId !== pending.pointerId) return;\r\n        const finalState = dragStateRef.current;\r\n        if (finalState && finalState.overRow !== null) {\r\n          argsRef.current.onReorder(finalState.draggingRow, finalState.overRow, finalState.position);\r\n        }\r\n        teardown();\r\n      };\r\n\r\n      // Escape cancels the gesture in flight (no reorder, no selection side effects).\r\n      const handleKey = (e: KeyboardEvent) => {\r\n        if (e.key === \"Escape\") teardown();\r\n      };\r\n\r\n      document.addEventListener(\"pointermove\", handleMove);\r\n      document.addEventListener(\"pointerup\", handleUp);\r\n      document.addEventListener(\"pointercancel\", handleUp);\r\n      document.addEventListener(\"keydown\", handleKey);\r\n      cleanupRef.current = () => {\r\n        document.removeEventListener(\"pointermove\", handleMove);\r\n        document.removeEventListener(\"pointerup\", handleUp);\r\n        document.removeEventListener(\"pointercancel\", handleUp);\r\n        document.removeEventListener(\"keydown\", handleKey);\r\n      };\r\n    },\r\n    [enabled, runFrame, setDrag, teardown],\r\n  );\r\n\r\n  useEffect(() => teardown, [teardown]);\r\n\r\n  return { onMarkerDragPointerDown, dragState };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/rows/use-row-reorder.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/selection/compact-selection.ts",
      "content": "/** Half-open integer slice [start, end). */\r\ntype Slice = readonly [start: number, end: number];\r\n\r\nlet sharedEmpty: CompactSelection | undefined;\r\n\r\n/**\r\n * Immutable run-length-encoded integer set: sorted, merged, half-open `[start, end)` slices.\r\n * Used for whole-row/whole-column selection channels; every mutation returns a new instance.\r\n */\r\nexport class CompactSelection {\r\n  private readonly slices: readonly Slice[];\r\n\r\n  private constructor(slices: readonly Slice[]) {\r\n    this.slices = slices;\r\n  }\r\n\r\n  /** Shared empty singleton. */\r\n  static empty(): CompactSelection {\r\n    if (!sharedEmpty) sharedEmpty = new CompactSelection([]);\r\n    return sharedEmpty;\r\n  }\r\n\r\n  /** A selection containing a single index or a single [start, end) range. */\r\n  static fromSingleSelection(input: number | [number, number]): CompactSelection {\r\n    return CompactSelection.empty().add(input);\r\n  }\r\n\r\n  /** A selection built from an arbitrary (unsorted, duplicate-tolerant) list of indices. */\r\n  static fromArray(indices: readonly number[]): CompactSelection {\r\n    if (indices.length === 0) return CompactSelection.empty();\r\n    const sorted = [...indices].sort((a, b) => a - b);\r\n    const slices: Slice[] = [];\r\n    // sorted.length > 0 per the guard above, so index 0 is always present\r\n    let start = sorted[0]!;\r\n    let end = start + 1;\r\n    for (let i = 1; i < sorted.length; i++) {\r\n      const v = sorted[i]!; // i < sorted.length, in-bounds by loop condition\r\n      if (v <= end) {\r\n        if (v + 1 > end) end = v + 1;\r\n      } else {\r\n        slices.push([start, end]);\r\n        start = v;\r\n        end = v + 1;\r\n      }\r\n    }\r\n    slices.push([start, end]);\r\n    return new CompactSelection(slices);\r\n  }\r\n\r\n  /** Merges a normalized (sorted, non-overlapping) slice into the sorted slice list. */\r\n  private static mergeSlice(slices: readonly Slice[], toAdd: Slice): Slice[] {\r\n    const [addStart, addEnd] = toAdd;\r\n    if (addStart >= addEnd) return [...slices];\r\n    const result: Slice[] = [];\r\n    let start = addStart;\r\n    let end = addEnd;\r\n    let inserted = false;\r\n    for (const [s, e] of slices) {\r\n      if (e < start) {\r\n        result.push([s, e]);\r\n      } else if (s > end) {\r\n        if (!inserted) {\r\n          result.push([start, end]);\r\n          inserted = true;\r\n        }\r\n        result.push([s, e]);\r\n      } else {\r\n        start = Math.min(start, s);\r\n        end = Math.max(end, e);\r\n      }\r\n    }\r\n    if (!inserted) result.push([start, end]);\r\n    return result;\r\n  }\r\n\r\n  /** Returns a new selection with the given index or [start, end) range added. */\r\n  add(input: number | [number, number]): CompactSelection {\r\n    const toAdd: Slice = typeof input === \"number\" ? [input, input + 1] : [input[0], input[1]];\r\n    if (toAdd[0] >= toAdd[1]) return this;\r\n    const merged = CompactSelection.mergeSlice(this.slices, toAdd);\r\n    return new CompactSelection(merged);\r\n  }\r\n\r\n  /** Returns a new selection with the given index or [start, end) range removed, splitting slices as needed. */\r\n  remove(input: number | [number, number]): CompactSelection {\r\n    const [remStart, remEnd] = typeof input === \"number\" ? [input, input + 1] : input;\r\n    if (remStart >= remEnd) return this;\r\n    const result: Slice[] = [];\r\n    for (const [s, e] of this.slices) {\r\n      if (e <= remStart || s >= remEnd) {\r\n        result.push([s, e]);\r\n        continue;\r\n      }\r\n      if (s < remStart) result.push([s, remStart]);\r\n      if (e > remEnd) result.push([remEnd, e]);\r\n    }\r\n    return new CompactSelection(result);\r\n  }\r\n\r\n  /** Whether a single index is a member. */\r\n  hasIndex(index: number): boolean {\r\n    for (const [s, e] of this.slices) {\r\n      if (index >= s && index < e) return true;\r\n      if (index < s) break;\r\n    }\r\n    return false;\r\n  }\r\n\r\n  /** Whether every index in [start, end) is a member. */\r\n  hasAll(range: [number, number]): boolean {\r\n    const [start, end] = range;\r\n    if (start >= end) return true;\r\n    for (const [s, e] of this.slices) {\r\n      if (start >= s && end <= e) return true;\r\n    }\r\n    return false;\r\n  }\r\n\r\n  /** Lowest member index, or undefined when empty. */\r\n  first(): number | undefined {\r\n    return this.slices[0]?.[0];\r\n  }\r\n\r\n  /** Highest member index, or undefined when empty. */\r\n  last(): number | undefined {\r\n    const lastSlice = this.slices[this.slices.length - 1];\r\n    return lastSlice ? lastSlice[1] - 1 : undefined;\r\n  }\r\n\r\n  /** Total member count (not slice count). */\r\n  get length(): number {\r\n    let total = 0;\r\n    for (const [s, e] of this.slices) total += e - s;\r\n    return total;\r\n  }\r\n\r\n  /** Shifts every member by `delta` (used to keep row/col selection in sync with insert/delete). Negative results are dropped. */\r\n  offset(delta: number): CompactSelection {\r\n    if (delta === 0) return this;\r\n    const shifted: Slice[] = [];\r\n    for (const [s, e] of this.slices) {\r\n      const ns = s + delta;\r\n      const ne = e + delta;\r\n      if (ne <= 0) continue;\r\n      shifted.push([Math.max(0, ns), ne]);\r\n    }\r\n    return new CompactSelection(shifted);\r\n  }\r\n\r\n  /** All member indices in ascending order. */\r\n  toArray(): number[] {\r\n    const out: number[] = [];\r\n    for (const [s, e] of this.slices) {\r\n      for (let i = s; i < e; i++) out.push(i);\r\n    }\r\n    return out;\r\n  }\r\n\r\n  /** Iterates all member indices in ascending order. */\r\n  [Symbol.iterator](): Iterator<number> {\r\n    const slices = this.slices;\r\n    let sliceIndex = 0;\r\n    let current = slices[0]?.[0] ?? 0;\r\n    return {\r\n      next(): IteratorResult<number> {\r\n        let slice = slices[sliceIndex];\r\n        while (slice !== undefined && current >= slice[1]) {\r\n          sliceIndex++;\r\n          slice = slices[sliceIndex];\r\n          current = slice ? slice[0] : current;\r\n        }\r\n        if (slice === undefined) return { done: true, value: undefined };\r\n        return { done: false, value: current++ };\r\n      },\r\n    };\r\n  }\r\n\r\n  /** Structural equality (same merged slices). */\r\n  equals(other: CompactSelection): boolean {\r\n    if (this === other) return true;\r\n    if (this.slices.length !== other.slices.length) return false;\r\n    for (let i = 0; i < this.slices.length; i++) {\r\n      const a = this.slices[i]!; // i < length by loop condition, both arrays same length (checked above)\r\n      const b = other.slices[i]!;\r\n      if (a[0] !== b[0] || a[1] !== b[1]) {\r\n        return false;\r\n      }\r\n    }\r\n    return true;\r\n  }\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/selection/compact-selection.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/selection/extend-selection.ts",
      "content": "import type { GridRect, GridSelection } from \"../types\";\r\n\r\n/** Direction for shift+arrow selection growth. */\r\nexport type ExtendDirection = \"up\" | \"down\" | \"left\" | \"right\";\r\n\r\n/** Options for extendSelection. */\r\nexport type ExtendSelectionOptions = {\r\n  /** Grow all the way to the grid edge instead of by one cell. */\r\n  toEdge?: boolean;\r\n  rowCount: number;\r\n  colCount: number;\r\n};\r\n\r\n/**\r\n * Shift+arrow growth: extends the edge of the range away from the anchor, or\r\n * shrinks the edge nearest the anchor when that edge has moved past it\r\n * (Excel-correct contraction back toward a 1x1 selection at the anchor).\r\n */\r\nexport function extendSelection(\r\n  selection: GridSelection,\r\n  direction: ExtendDirection,\r\n  opts: ExtendSelectionOptions,\r\n): GridSelection {\r\n  if (!selection.current) return selection;\r\n  const { cell, range, rangeStack } = selection.current;\r\n  const maxCol = opts.colCount - 1;\r\n  const maxRow = opts.rowCount - 1;\r\n\r\n  // The range corner opposite the anchor is the one that moves.\r\n  let left = range.x;\r\n  let right = range.x + range.width - 1;\r\n  let top = range.y;\r\n  let bottom = range.y + range.height - 1;\r\n\r\n  // Each direction either grows the far edge (anchor sits on the opposite edge)\r\n  // or shrinks the near edge back toward the anchor (anchor sits on this edge already).\r\n  // toEdge is unconditional on both edges (glide/Excel: the moving end jumps to the\r\n  // boundary, crossing the anchor, rather than only collapsing the near edge).\r\n  switch (direction) {\r\n    case \"up\": {\r\n      if (opts.toEdge) {\r\n        top = 0;\r\n        bottom = cell.row;\r\n      } else if (cell.row >= bottom) {\r\n        top = Math.max(0, top - 1);\r\n      } else {\r\n        bottom = Math.max(cell.row, bottom - 1);\r\n      }\r\n      break;\r\n    }\r\n    case \"down\": {\r\n      if (opts.toEdge) {\r\n        bottom = maxRow;\r\n        top = cell.row;\r\n      } else if (cell.row <= top) {\r\n        bottom = Math.min(maxRow, bottom + 1);\r\n      } else {\r\n        top = Math.min(cell.row, top + 1);\r\n      }\r\n      break;\r\n    }\r\n    case \"left\": {\r\n      if (opts.toEdge) {\r\n        left = 0;\r\n        right = cell.col;\r\n      } else if (cell.col >= right) {\r\n        left = Math.max(0, left - 1);\r\n      } else {\r\n        right = Math.max(cell.col, right - 1);\r\n      }\r\n      break;\r\n    }\r\n    case \"right\": {\r\n      if (opts.toEdge) {\r\n        right = maxCol;\r\n        left = cell.col;\r\n      } else if (cell.col <= left) {\r\n        right = Math.min(maxCol, right + 1);\r\n      } else {\r\n        left = Math.min(cell.col, left + 1);\r\n      }\r\n      break;\r\n    }\r\n  }\r\n\r\n  const newRange: GridRect = { x: left, y: top, width: right - left + 1, height: bottom - top + 1 };\r\n  return {\r\n    current: { cell, range: newRange, rangeStack },\r\n    rows: selection.rows,\r\n    columns: selection.columns,\r\n  };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/selection/extend-selection.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/selection/index.ts",
      "content": "/** Domain barrel — selection math (rects, compact-selection, range ops). */\r\nexport { CompactSelection } from \"./compact-selection\";\r\nexport {\r\n  rectFromCorners,\r\n  combineRects,\r\n  rectContains,\r\n  pointInRect,\r\n  intersectRect,\r\n} from \"./rects\";\r\nexport { emptySelection, selectCell, extendTo, pushRange, isSelectionEmpty } from \"./selection-ops\";\r\nexport { extendSelection, type ExtendDirection, type ExtendSelectionOptions } from \"./extend-selection\";\r\nexport { selectionContainsCell, selectionRects, selectRow, selectColumn } from \"./line-ops\";\r\nexport { selectLine, type SelectLineOptions } from \"./select-line-options\";\r\nexport { offsetSelectionForRows } from \"./offset-selection-for-rows\";\r\nexport { rowReorderMap, reorderSelectionForRow } from \"./reorder-selection-for-row\";\r\nexport { getSelectedViewRows } from \"./selected-view-rows\";\r\nexport { selectAllProgression, computeDataRegion, type SelectAllStage, type IsEmptyAt } from \"./select-all-progression\";\r\n",
      "type": "registry:component",
      "target": "components/data-grid/selection/index.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/selection/line-ops.ts",
      "content": "import type { CellCoord, GridRect, GridSelection } from \"../types\";\r\nimport { CompactSelection } from \"./compact-selection\";\r\nimport { rectContains } from \"./rects\";\r\nimport { selectLine, type SelectLineOptions } from \"./select-line-options\";\r\n\r\n/**\r\n * Whole-row selection channel op (row-marker click). Always clears the primary range/rangeStack;\r\n * clears the column channel too unless `additive`, `extendFromLast`, or `replaceFromLast` (which preserve it).\r\n */\r\nexport function selectRow(selection: GridSelection, index: number, opts: SelectLineOptions = {}): GridSelection {\r\n  return {\r\n    current: null,\r\n    rows: selectLine(selection.rows, index, opts),\r\n    columns: opts.additive || opts.extendFromLast || opts.replaceFromLast ? selection.columns : CompactSelection.empty(),\r\n  };\r\n}\r\n\r\n/**\r\n * Whole-column selection channel op (header click). Always clears the primary range/rangeStack;\r\n * clears the row channel too unless `additive`, `extendFromLast`, or `replaceFromLast` (which preserve it).\r\n */\r\nexport function selectColumn(selection: GridSelection, index: number, opts: SelectLineOptions = {}): GridSelection {\r\n  return {\r\n    current: null,\r\n    columns: selectLine(selection.columns, index, opts),\r\n    rows: opts.additive || opts.extendFromLast || opts.replaceFromLast ? selection.rows : CompactSelection.empty(),\r\n  };\r\n}\r\n\r\n/**\r\n * Resolves every selected region to a flat list of rects, in the order operations\r\n * like delete/copy should apply: primary range, range stack, full-height column\r\n * selections, then full-width row selections.\r\n */\r\nexport function selectionRects(selection: GridSelection, rowCount: number, colCount: number): GridRect[] {\r\n  const rects: GridRect[] = [];\r\n  if (selection.current) {\r\n    rects.push(selection.current.range);\r\n    for (const rect of selection.current.rangeStack) rects.push(rect);\r\n  }\r\n  for (const col of selection.columns.toArray()) {\r\n    rects.push({ x: col, y: 0, width: 1, height: rowCount });\r\n  }\r\n  for (const row of selection.rows.toArray()) {\r\n    rects.push({ x: 0, y: row, width: colCount, height: 1 });\r\n  }\r\n  return rects;\r\n}\r\n\r\n/**\r\n * Whether `coord` is selected via any channel: primary range, range stack, whole-row, or whole-column.\r\n * rowCount/colCount are accepted for a stable signature across callers but unused: row/column\r\n * channels already store data-space indices, and range containment needs no grid bounds.\r\n */\r\nexport function selectionContainsCell(\r\n  selection: GridSelection,\r\n  coord: CellCoord,\r\n  _rowCount: number,\r\n  _colCount: number,\r\n): boolean {\r\n  if (selection.rows.hasIndex(coord.row)) return true;\r\n  if (selection.columns.hasIndex(coord.col)) return true;\r\n  if (selection.current) {\r\n    if (rectContains(selection.current.range, coord)) return true;\r\n    for (const rect of selection.current.rangeStack) {\r\n      if (rectContains(rect, coord)) return true;\r\n    }\r\n  }\r\n  return false;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/selection/line-ops.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/selection/offset-selection-for-rows.ts",
      "content": "import { CompactSelection } from \"./compact-selection\";\r\nimport type { CellCoord, GridRect, GridSelection } from \"../types\";\r\n\r\n/** Shifts the row selection channel and any range rects to follow a row insert/delete at `atIndex`. */\r\nexport function offsetSelectionForRows(selection: GridSelection, atIndex: number, delta: number): GridSelection {\r\n  const shiftRect = (rect: GridRect): GridRect => {\r\n    if (rect.y >= atIndex) {\r\n      // A delete span starting at/before the rect's first row removes rows from inside it too.\r\n      const deletedSpanEnd = atIndex - delta;\r\n      const overlap = delta < 0 ? Math.max(0, Math.min(rect.y + rect.height, deletedSpanEnd) - rect.y) : 0;\r\n      return { ...rect, y: Math.max(atIndex, rect.y + delta), height: Math.max(0, rect.height - overlap) };\r\n    }\r\n    if (rect.y + rect.height > atIndex) return { ...rect, height: Math.max(0, rect.height + delta) };\r\n    return rect;\r\n  };\r\n  const shiftCell = (cell: CellCoord): CellCoord =>\r\n    cell.row >= atIndex ? { ...cell, row: Math.max(atIndex, cell.row + delta) } : cell;\r\n\r\n  // Only members at/after atIndex move; earlier members are untouched.\r\n  const before = selection.rows.toArray().filter((r) => r < atIndex);\r\n  const afterShifted = selection.rows\r\n    .toArray()\r\n    .filter((r) => r >= atIndex)\r\n    .map((r) => Math.max(atIndex, r + delta))\r\n    .filter((r) => r >= 0);\r\n  const rows = CompactSelection.fromArray([...before, ...afterShifted]);\r\n\r\n  return {\r\n    current: selection.current\r\n      ? {\r\n          cell: shiftCell(selection.current.cell),\r\n          range: shiftRect(selection.current.range),\r\n          rangeStack: selection.current.rangeStack.map(shiftRect),\r\n        }\r\n      : null,\r\n    rows,\r\n    columns: selection.columns,\r\n  };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/selection/offset-selection-for-rows.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/selection/rects.ts",
      "content": "import type { CellCoord, GridRect } from \"../types\";\r\n\r\n/** Intersection of two rects, or null when disjoint. */\r\nexport function intersectRect(a: GridRect, b: GridRect): GridRect | null {\r\n  const x = Math.max(a.x, b.x);\r\n  const y = Math.max(a.y, b.y);\r\n  const right = Math.min(a.x + a.width, b.x + b.width);\r\n  const bottom = Math.min(a.y + a.height, b.y + b.height);\r\n  if (right <= x || bottom <= y) return null;\r\n  return { x, y, width: right - x, height: bottom - y };\r\n}\r\n\r\n/** Whether `coord` lies inside `rect`. */\r\nexport function rectContains(rect: GridRect, coord: CellCoord): boolean {\r\n  return (\r\n    coord.col >= rect.x &&\r\n    coord.col < rect.x + rect.width &&\r\n    coord.row >= rect.y &&\r\n    coord.row < rect.y + rect.height\r\n  );\r\n}\r\n\r\n/** Alias of rectContains with (rect, coord) argument order reversed for call-site readability. */\r\nexport function pointInRect(coord: CellCoord, rect: GridRect): boolean {\r\n  return rectContains(rect, coord);\r\n}\r\n\r\n/** Normalizes two corner cells into a half-open GridRect. */\r\nexport function rectFromCorners(a: CellCoord, b: CellCoord): GridRect {\r\n  const x = Math.min(a.col, b.col);\r\n  const y = Math.min(a.row, b.row);\r\n  const width = Math.abs(a.col - b.col) + 1;\r\n  const height = Math.abs(a.row - b.row) + 1;\r\n  return { x, y, width, height };\r\n}\r\n\r\n/** Bounding-box union of two rects. */\r\nexport function combineRects(a: GridRect, b: GridRect): GridRect {\r\n  const x = Math.min(a.x, b.x);\r\n  const y = Math.min(a.y, b.y);\r\n  const right = Math.max(a.x + a.width, b.x + b.width);\r\n  const bottom = Math.max(a.y + a.height, b.y + b.height);\r\n  return { x, y, width: right - x, height: bottom - y };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/selection/rects.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/selection/reorder-selection-for-row.ts",
      "content": "import { CompactSelection } from \"./compact-selection\";\r\nimport type { CellCoord, GridRect, GridSelection } from \"../types\";\r\n\r\n/** View-space index remap for a single-row reorder: row `from` lands at `to`, every row between the two shifts one slot toward `from`. */\r\nexport function rowReorderMap(from: number, to: number): (index: number) => number {\r\n  if (to > from) return (i) => (i === from ? to : i > from && i <= to ? i - 1 : i);\r\n  if (to < from) return (i) => (i === from ? to : i >= to && i < from ? i + 1 : i);\r\n  return (i) => i;\r\n}\r\n\r\n/**\r\n * Re-anchors a selection after the row at view index `from` moves to `to` (view indices; the data\r\n * move is already applied by the caller). The row channel and the active cell follow the rows they\r\n * belong to; each rect follows its own top/bottom rows, so a rect containing the moved row spans\r\n * the moved row's new slot to the remapped edge (approximation, only reachable by dragging a row\r\n * out of the interior of its own range).\r\n */\r\nexport function reorderSelectionForRow(selection: GridSelection, from: number, to: number): GridSelection {\r\n  if (from === to) return selection;\r\n  const f = rowReorderMap(from, to);\r\n  const rows = CompactSelection.fromArray(selection.rows.toArray().map(f).sort((a, b) => a - b));\r\n  const mapRect = (rect: GridRect): GridRect => {\r\n    const top = f(rect.y);\r\n    const bottom = f(rect.y + rect.height - 1);\r\n    return { ...rect, y: Math.min(top, bottom), height: Math.abs(bottom - top) + 1 };\r\n  };\r\n  const shiftCell = (cell: CellCoord): CellCoord => ({ ...cell, row: f(cell.row) });\r\n  return {\r\n    current: selection.current\r\n      ? {\r\n          cell: shiftCell(selection.current.cell),\r\n          range: mapRect(selection.current.range),\r\n          rangeStack: selection.current.rangeStack.map(mapRect),\r\n        }\r\n      : null,\r\n    rows,\r\n    columns: selection.columns,\r\n  };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/selection/reorder-selection-for-row.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/selection/select-all-progression.ts",
      "content": "import { CompactSelection } from \"./compact-selection\";\r\nimport type { CellCoord, GridRect, GridSelection } from \"../types\";\r\n\r\n/** Two-stage progression tracked by the caller (interaction layer): which stage the last `selectAllProgression` call produced. */\r\nexport type SelectAllStage = \"region\" | \"all\" | null;\r\n\r\n/** Probes whether the cell at `coord` is empty, via the cell type's `isEmpty` — same contract as `jumpToDataBoundary`'s local helper. */\r\nexport type IsEmptyAt = (coord: CellCoord) => boolean;\r\n\r\nfunction wholeGridRect(rowCount: number, colCount: number): GridRect {\r\n  return { x: 0, y: 0, width: colCount, height: rowCount };\r\n}\r\n\r\n/**\r\n * Excel's \"current region\": the maximal rectangle of contiguous non-empty rows/columns containing\r\n * `active`, grown one edge at a time (flood-fill by row/column, not per-cell) until every row/column\r\n * just outside the rect is entirely empty. An empty `active` cell's region is itself (Excel parity).\r\n * O(perimeter × rows/cols touched) — only called on an explicit Ctrl+A, never a hot path.\r\n */\r\nexport function computeDataRegion(active: CellCoord, rowCount: number, colCount: number, isEmptyAt: IsEmptyAt): GridRect {\r\n  if (isEmptyAt(active)) return { x: active.col, y: active.row, width: 1, height: 1 };\r\n\r\n  let left = active.col;\r\n  let right = active.col;\r\n  let top = active.row;\r\n  let bottom = active.row;\r\n\r\n  const rowHasData = (row: number, fromCol: number, toCol: number): boolean => {\r\n    for (let col = fromCol; col <= toCol; col++) if (!isEmptyAt({ col, row })) return true;\r\n    return false;\r\n  };\r\n  const colHasData = (col: number, fromRow: number, toRow: number): boolean => {\r\n    for (let row = fromRow; row <= toRow; row++) if (!isEmptyAt({ col, row })) return true;\r\n    return false;\r\n  };\r\n\r\n  // grow each edge outward while the next row/column just past it still has data; a pass with no\r\n  // growth on any edge means the rect is stable (Excel's flood-fill-by-line, not per-cell).\r\n  let grew = true;\r\n  while (grew) {\r\n    grew = false;\r\n    if (top > 0 && rowHasData(top - 1, left, right)) {\r\n      top--;\r\n      grew = true;\r\n    }\r\n    if (bottom < rowCount - 1 && rowHasData(bottom + 1, left, right)) {\r\n      bottom++;\r\n      grew = true;\r\n    }\r\n    if (left > 0 && colHasData(left - 1, top, bottom)) {\r\n      left--;\r\n      grew = true;\r\n    }\r\n    if (right < colCount - 1 && colHasData(right + 1, top, bottom)) {\r\n      right++;\r\n      grew = true;\r\n    }\r\n  }\r\n\r\n  return { x: left, y: top, width: right - left + 1, height: bottom - top + 1 };\r\n}\r\n\r\nfunction isRect(rect: GridRect, other: GridRect): boolean {\r\n  return rect.x === other.x && rect.y === other.y && rect.width === other.width && rect.height === other.height;\r\n}\r\n\r\nfunction selectionForRect(rect: GridRect, anchor: CellCoord): GridSelection {\r\n  return {\r\n    current: { cell: anchor, range: rect, rangeStack: [] },\r\n    rows: CompactSelection.empty(),\r\n    columns: CompactSelection.empty(),\r\n  };\r\n}\r\n\r\n/**\r\n * Two-stage Ctrl+A (Excel/Sheets parity, docs/agent-work/specs/2026-07-18-two-stage-select-all-design.md):\r\n * stage 1 selects the active cell's data region, stage 2 (an immediate repeat, tracked by the\r\n * caller via `stage`) selects the whole grid. When the region already equals the whole grid\r\n * (dense data), stage 1 already lands on `\"all\"` so a repeated press stays a no-op rather than\r\n * toggling anything — the caller's `stage` never needs a second real transition in that case.\r\n * Pure: takes the progression stage in, returns the next selection + stage out.\r\n */\r\nexport function selectAllProgression(\r\n  selection: GridSelection,\r\n  rowCount: number,\r\n  colCount: number,\r\n  active: CellCoord,\r\n  isEmptyAt: IsEmptyAt,\r\n  stage: SelectAllStage,\r\n): { selection: GridSelection; stage: \"region\" | \"all\" } {\r\n  const wholeGrid = wholeGridRect(rowCount, colCount);\r\n\r\n  if (stage === \"region\") {\r\n    return { selection: selectionForRect(wholeGrid, active), stage: \"all\" };\r\n  }\r\n\r\n  const region = computeDataRegion(active, rowCount, colCount, isEmptyAt);\r\n  if (isRect(region, wholeGrid)) {\r\n    // dense data: region already covers the grid, so this stays a no-op on repeat presses too —\r\n    // only re-derive the selection if it isn't already exactly the whole grid.\r\n    const alreadyWholeGrid =\r\n      selection.current !== null && selection.current.rangeStack.length === 0 && isRect(selection.current.range, wholeGrid);\r\n    return { selection: alreadyWholeGrid ? selection : selectionForRect(wholeGrid, active), stage: \"all\" };\r\n  }\r\n  return { selection: selectionForRect(region, active), stage: \"region\" };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/selection/select-all-progression.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/selection/select-line-options.ts",
      "content": "import { CompactSelection } from \"./compact-selection\";\r\nimport type { CompactSelectionLike } from \"../types\";\r\n\r\n/**\r\n * Options for selectRow/selectColumn. Plain click (no options set) toggles membership of\r\n * just this index, clearing the rest of the channel.\r\n */\r\nexport type SelectLineOptions = {\r\n  /** Ctrl-click: additive toggle of this index, keeping the rest of the channel. */\r\n  additive?: boolean;\r\n  /** Shift-click: range from the last-selected index (falls back to `index` when there is none). */\r\n  extendFromLast?: boolean;\r\n  /**\r\n   * The row-marker drag variant of `extendFromLast`: instead of ADDING the anchor..index span to\r\n   * the channel (a union that can only grow), the channel becomes EXACTLY that span — the pointer\r\n   * is the moving edge, so dragging back over already-selected rows shrinks the selection again.\r\n   */\r\n  replaceFromLast?: boolean;\r\n  /**\r\n   * Explicit last-highlighted index for `extendFromLast`/`replaceFromLast` (insertion order, not\r\n   * the channel's max member — an RLE set can't represent insertion order, so the caller/reducer\r\n   * must track it). Falls back to the channel's max index, then `index`, when absent.\r\n   */\r\n  from?: number;\r\n};\r\n\r\n/** Shared by selectRow/selectColumn: applies a plain/additive/extendFromLast toggle to one selection channel. */\r\nexport function selectLine(\r\n  channelLike: CompactSelectionLike,\r\n  index: number,\r\n  opts: SelectLineOptions,\r\n): CompactSelection {\r\n  const channel = CompactSelection.fromArray(channelLike.toArray());\r\n  if (opts.replaceFromLast) {\r\n    const indices = channel.toArray();\r\n    const last = opts.from ?? indices[indices.length - 1] ?? index;\r\n    const start = Math.min(last, index);\r\n    const end = Math.max(last, index) + 1;\r\n    return CompactSelection.fromSingleSelection([start, end]);\r\n  }\r\n  if (opts.extendFromLast) {\r\n    const indices = channel.toArray();\r\n    const last = opts.from ?? indices[indices.length - 1] ?? index;\r\n    const start = Math.min(last, index);\r\n    const end = Math.max(last, index) + 1;\r\n    return channel.add([start, end]);\r\n  }\r\n  if (opts.additive) {\r\n    return channel.hasIndex(index) ? channel.remove(index) : channel.add(index);\r\n  }\r\n  // plain click: toggle-select just this index\r\n  if (channel.length === 1 && channel.hasIndex(index)) {\r\n    return CompactSelection.empty();\r\n  }\r\n  return CompactSelection.fromSingleSelection(index);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/selection/select-line-options.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/selection/selected-col-ranges-for-row.ts",
      "content": "import type { GridRect, GridSelection } from \"../types\";\r\n\r\n/** Half-open column range `[start, end)`, merged/sorted, describing which columns of one row are selected. */\r\nexport type ColRange = readonly [start: number, end: number];\r\n\r\n/** True when `viewRow` falls inside `rect`'s row span. */\r\nfunction rectCoversRow(rect: GridRect, viewRow: number): boolean {\r\n  return viewRow >= rect.y && viewRow < rect.y + rect.height;\r\n}\r\n\r\n/** Inserts `[start, end)` into `ranges` (sorted, non-overlapping), merging any overlap/adjacency. */\r\nfunction mergeRange(ranges: ColRange[], start: number, end: number): void {\r\n  if (start >= end) return;\r\n  let i = 0;\r\n  // hot path (per-row selector): ranges[i]! guarded by i < ranges.length in the loop condition\r\n  while (i < ranges.length && ranges[i]![1] < start) i++;\r\n  let mergedStart = start;\r\n  let mergedEnd = end;\r\n  while (i < ranges.length && ranges[i]![0] <= mergedEnd) {\r\n    mergedStart = Math.min(mergedStart, ranges[i]![0]);\r\n    mergedEnd = Math.max(mergedEnd, ranges[i]![1]);\r\n    ranges.splice(i, 1);\r\n  }\r\n  ranges.splice(i, 0, [mergedStart, mergedEnd]);\r\n}\r\n\r\n/**\r\n * The selected column ranges for one view row — a row is a rectangle's-worth of columns, not\r\n * individual cells, so this collapses to a handful of `[start, end)` tuples (usually 0 or 1) instead\r\n * of a per-column boolean/Set. Used by {@link useDataGridRowCellState} so a row's selector output\r\n * stays cheap to compute and cheap to compare (small array, content-equal across unrelated updates).\r\n */\r\nexport function selectedColRangesForRow(selection: GridSelection, viewRow: number, colCount: number): ColRange[] {\r\n  const ranges: ColRange[] = [];\r\n  if (selection.rows.hasIndex(viewRow)) mergeRange(ranges, 0, colCount);\r\n  if (selection.columns.length > 0) {\r\n    for (const col of selection.columns.toArray()) mergeRange(ranges, col, col + 1);\r\n  }\r\n  if (selection.current) {\r\n    if (rectCoversRow(selection.current.range, viewRow)) {\r\n      mergeRange(ranges, selection.current.range.x, selection.current.range.x + selection.current.range.width);\r\n    }\r\n    for (const rect of selection.current.rangeStack) {\r\n      if (rectCoversRow(rect, viewRow)) mergeRange(ranges, rect.x, rect.x + rect.width);\r\n    }\r\n  }\r\n  return ranges;\r\n}\r\n\r\n/** Whether `col` falls inside any of `ranges` — the per-cell membership check row.tsx/cell.tsx derive `isSelected` from. */\r\nexport function colRangesContain(ranges: readonly ColRange[], col: number): boolean {\r\n  for (const [start, end] of ranges) {\r\n    if (col >= start && col < end) return true;\r\n  }\r\n  return false;\r\n}\r\n\r\n/** Content equality for two `ColRange[]` — same length, same tuples in the same order (both are built sorted/merged, so order is deterministic). */\r\nexport function colRangesEqual(a: readonly ColRange[], b: readonly ColRange[]): boolean {\r\n  if (a === b) return true;\r\n  if (a.length !== b.length) return false;\r\n  for (let i = 0; i < a.length; i++) {\r\n    const ra = a[i]!; // i < a.length by loop condition, same length as b (checked above)\r\n    const rb = b[i]!;\r\n    if (ra[0] !== rb[0] || ra[1] !== rb[1]) return false;\r\n  }\r\n  return true;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/selection/selected-col-ranges-for-row.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/selection/selected-view-rows.ts",
      "content": "import type { GridSelection } from \"../types\";\r\n\r\n/**\r\n * Every view row index covered by `selection`: the union of the primary range, the ctrl-click\r\n * `rangeStack`, and the whole-row channel (`rows`), deduped and sorted ascending. The core's\r\n * single derivation of the selected row set — add-ons acting on selected rows (cell-menu row\r\n * ops, selection-scope export) consume this instead of re-reading `GridSelection`'s channels.\r\n * The result is view indices, not row ids.\r\n */\r\nexport function getSelectedViewRows(selection: GridSelection): number[] {\r\n  const rows = new Set<number>();\r\n  const current = selection.current;\r\n  if (current) {\r\n    const rects = [current.range, ...current.rangeStack];\r\n    for (const rect of rects) {\r\n      for (let row = rect.y; row < rect.y + rect.height; row++) rows.add(row);\r\n    }\r\n  }\r\n  for (const row of selection.rows.toArray()) rows.add(row);\r\n  return Array.from(rows).sort((a, b) => a - b);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/selection/selected-view-rows.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/selection/selection-ops.ts",
      "content": "import type { CellCoord, GridSelection } from \"../types\";\r\nimport { CompactSelection } from \"./compact-selection\";\r\nimport { rectFromCorners } from \"./rects\";\r\n\r\n/** The selection with nothing selected. */\r\nexport function emptySelection(): GridSelection {\r\n  return { current: null, rows: CompactSelection.empty(), columns: CompactSelection.empty() };\r\n}\r\n\r\n/** True when all three selection channels (current range, rows, columns) are empty. */\r\nexport function isSelectionEmpty(selection: GridSelection): boolean {\r\n  return selection.rows.length === 0 && selection.columns.length === 0 && !selection.current;\r\n}\r\n\r\n/** A fresh 1x1 selection anchored at `coord`, clearing row/column channels. */\r\nexport function selectCell(coord: CellCoord): GridSelection {\r\n  return {\r\n    current: { cell: coord, range: { x: coord.col, y: coord.row, width: 1, height: 1 }, rangeStack: [] },\r\n    rows: CompactSelection.empty(),\r\n    columns: CompactSelection.empty(),\r\n  };\r\n}\r\n\r\n/** Shift-click/shift-arrow: extends the primary range from the existing anchor to `coord`; anchor is unchanged. */\r\nexport function extendTo(selection: GridSelection, coord: CellCoord): GridSelection {\r\n  if (!selection.current) return selectCell(coord);\r\n  const { cell, rangeStack } = selection.current;\r\n  return {\r\n    current: { cell, range: rectFromCorners(cell, coord), rangeStack },\r\n    rows: selection.rows,\r\n    columns: selection.columns,\r\n  };\r\n}\r\n\r\n/** Ctrl-click: pushes the current primary range onto the stack and starts a new 1x1 range at `coord`. */\r\nexport function pushRange(selection: GridSelection, coord: CellCoord): GridSelection {\r\n  if (!selection.current) return selectCell(coord);\r\n  const { range, rangeStack } = selection.current;\r\n  return {\r\n    current: {\r\n      cell: coord,\r\n      range: { x: coord.col, y: coord.row, width: 1, height: 1 },\r\n      rangeStack: [...rangeStack, range],\r\n    },\r\n    rows: selection.rows,\r\n    columns: selection.columns,\r\n  };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/selection/selection-ops.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/sort-filter/build-view-index.ts",
      "content": "import type { FilterJoinOperator, FilterSpec, SortSpec } from \"../types\";\r\nimport { defaultCompareText } from \"./default-compare-text\";\r\nimport { createFilterMatcher } from \"./matches-filter\";\r\n\r\n/**\r\n * Read-only accessor the view-index builder uses to reach cell data without\r\n * knowing the row type. `compare` supplies a custom row-index comparator for a\r\n * column (a column `sortCompare` or its cell type's `compare`; absent comparators\r\n * fall back to `defaultCompareText` on `getText`).\r\n *\r\n * A `compare` a caller returns MUST be deterministic and depend only on the two\r\n * rows' current values — {@link updateViewIndex} reuses the untouched rows'\r\n * order under it instead of re-deriving it.\r\n */\r\nexport type CellAccessor = {\r\n  getText(rowIndex: number, columnId: string): string;\r\n  compare?(columnId: string): ((a: number, b: number) => number) | undefined;\r\n  /** Whether a row's cell in `columnId` is empty for sort placement; defaults to `getText() === \"\"`. */\r\n  isEmpty?(rowIndex: number, columnId: string): boolean;\r\n  /** The column's raw (un-stringified) cell value; only needed by a column's `filterMatch`. */\r\n  getValue?(rowIndex: number, columnId: string): unknown;\r\n  /** The column's `filterMatch` predicate, if the column declares one. */\r\n  filterMatch?(columnId: string): ((value: unknown, filter: FilterSpec) => boolean) | undefined;\r\n};\r\n\r\n/**\r\n * One filter's per-row test: the column's `filterMatch` (raw value) when the accessor provides\r\n * both the predicate and raw values, else the built-in text matcher. Both the rebuild and the\r\n * incremental path build through this so a custom predicate can never desync between them.\r\n */\r\nexport function makeFilterTest(accessor: CellAccessor, filter: FilterSpec): (row: number) => boolean {\r\n  const custom = accessor.filterMatch?.(filter.columnId);\r\n  const getValue = accessor.getValue;\r\n  if (custom && getValue) {\r\n    // a thrown predicate fails the row rather than the view build\r\n    return (row: number) => {\r\n      try {\r\n        return custom(getValue(row, filter.columnId), filter);\r\n      } catch {\r\n        return false;\r\n      }\r\n    };\r\n  }\r\n  const test = createFilterMatcher(filter);\r\n  return (row: number) => test(accessor.getText(row, filter.columnId));\r\n}\r\n\r\n/** Whether `row`'s cell in `columnId` counts as empty for sort placement — empty sorts last in BOTH directions. */\r\nexport function isEmptyCell(accessor: CellAccessor, rowIndex: number, columnId: string): boolean {\r\n  return accessor.isEmpty ? accessor.isEmpty(rowIndex, columnId) : accessor.getText(rowIndex, columnId) === \"\";\r\n}\r\n\r\n/**\r\n * Options for {@link buildViewIndex}. `searchColumnIds` is only consulted\r\n * when `search` is set; if omitted in that case, the search filter is\r\n * skipped (no rows are excluded) rather than matching against zero columns.\r\n */\r\nexport type ViewIndexOptions = {\r\n  sorts: SortSpec[];\r\n  filters: FilterSpec[];\r\n  /** How `filters` combine; default `\"and\"` (every filter must match). */\r\n  joinOperator?: FilterJoinOperator;\r\n  search?: string;\r\n  searchColumnIds?: string[];\r\n};\r\n\r\n/**\r\n * Computes the row-index view: filter, then search, then a stable\r\n * multi-column sort. `sorts[0]` is primary; each subsequent sort only\r\n * breaks ties left by the ones before it. No sorts leaves filtered/searched\r\n * rows in their original order.\r\n */\r\nexport function buildViewIndex(\r\n  rowCount: number,\r\n  accessor: CellAccessor,\r\n  opts: ViewIndexOptions,\r\n): number[] {\r\n  let indices: number[] = [];\r\n  for (let i = 0; i < rowCount; i++) indices.push(i);\r\n\r\n  if (opts.filters.length > 0) {\r\n    // One matcher per filter, built once outside the per-row loop: any numeric bound in\r\n    // gt/gte/lt/lte/isBetween is the same string for every row, so parsing it here instead of on\r\n    // every `matchesFilter` call turns an O(n) redundant re-parse into O(1) setup per filter.\r\n    const matchers = opts.filters.map((filter) => makeFilterTest(accessor, filter));\r\n\r\n    if (opts.joinOperator === \"or\") {\r\n      // OR: a row survives if ANY filter matches — one pass, no per-filter re-scan of `indices`.\r\n      indices = indices.filter((row) => matchers.some((test) => test(row)));\r\n    } else {\r\n      // AND (default): short-circuits per row via the existing filter-per-filter narrowing, same\r\n      // hot path as before this feature — untouched perf for the common (and only, pre-OR) case.\r\n      for (const test of matchers) {\r\n        indices = indices.filter((row) => test(row));\r\n      }\r\n    }\r\n  }\r\n\r\n  const search = opts.search?.trim();\r\n  if (search && opts.searchColumnIds) {\r\n    const needle = search.toLowerCase();\r\n    const columnIds = opts.searchColumnIds;\r\n    indices = indices.filter((row) =>\r\n      columnIds.some((columnId) => accessor.getText(row, columnId).toLowerCase().includes(needle)),\r\n    );\r\n  }\r\n\r\n  if (opts.sorts.length === 0) return indices;\r\n\r\n  // One collator amortized across every default-compare column and every pairwise\r\n  // comparison in this call, instead of a fresh Intl.Collator per `localeCompare`.\r\n  const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: \"base\" });\r\n\r\n  const comparators = opts.sorts.map((sort) => {\r\n    const custom = accessor.compare?.(sort.columnId);\r\n    const dir = sort.direction === \"asc\" ? 1 : -1;\r\n\r\n    if (custom) {\r\n      // Decorate emptiness once per row, same O(n) discipline as the text keys below.\r\n      const empties = new Map<number, boolean>();\r\n      for (const row of indices) empties.set(row, isEmptyCell(accessor, row, sort.columnId));\r\n      // Same empty-last-in-both-directions rule as the default path, so a typed column's blanks\r\n      // don't flip sides on a direction change while a text column's stay put.\r\n      return (a: number, b: number) => {\r\n        const emptyA = empties.get(a) ?? false;\r\n        const emptyB = empties.get(b) ?? false;\r\n        if (emptyA && emptyB) return 0;\r\n        if (emptyA) return 1;\r\n        if (emptyB) return -1;\r\n        return dir * custom(a, b);\r\n      };\r\n    }\r\n\r\n    // Decorate-sort-once: fetch each row's text key for this column exactly once (O(n))\r\n    // instead of on every pairwise comparison (O(n log n) getText calls).\r\n    const keys = new Map<number, string>();\r\n    for (const row of indices) keys.set(row, accessor.getText(row, sort.columnId));\r\n\r\n    // Empty must sort last for both directions, so only the non-empty ordering flips.\r\n    return (a: number, b: number) => {\r\n      const textA = keys.get(a) ?? \"\";\r\n      const textB = keys.get(b) ?? \"\";\r\n      if (textA === \"\" || textB === \"\") return defaultCompareText(textA, textB, collator);\r\n      return dir * defaultCompareText(textA, textB, collator);\r\n    };\r\n  });\r\n\r\n  // Decorate-sort-undecorate keeps the sort stable across the composed comparators.\r\n  const decorated = indices.map((row, position) => ({ row, position }));\r\n  decorated.sort((a, b) => {\r\n    for (const cmp of comparators) {\r\n      const result = cmp(a.row, b.row);\r\n      if (result !== 0) return result;\r\n    }\r\n    return a.position - b.position;\r\n  });\r\n\r\n  return decorated.map((d) => d.row);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/sort-filter/build-view-index.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/sort-filter/incremental-view-index.ts",
      "content": "import type { FilterJoinOperator, FilterSpec, SortSpec } from \"../types\";\r\nimport { isEmptyCell, makeFilterTest, type CellAccessor, type ViewIndexOptions } from \"./build-view-index\";\r\nimport { defaultCompareText } from \"./default-compare-text\";\r\n\r\n/**\r\n * Above this many touched rows, {@link updateViewIndex} refuses the incremental path and the caller\r\n * rebuilds. Each touched row costs one splice-out plus one splice-in against the index array, so the\r\n * cost is linear in k while the rebuild is a flat O(n log n) — the two cross where k × per-row cost\r\n * meets the rebuild.\r\n *\r\n * Measured (prod build, 100k rows, one sort column, paired in one launch): the rebuild is ~415 ms\r\n * flat; the incremental path is 0.3 / 0.9 / 7.6 / 28.5 / 137 ms at k = 1 / 20 / 256 / 1000 / 5000,\r\n * a slope near 0.027 ms per touched row. The curves therefore meet around k ≈ 15,000. 256 sits ~58x\r\n * inside that, so no machine, dataset, or collator can land a batch on the wrong side of it — and a\r\n * batch that big is a bulk import, which the rebuild serves better anyway.\r\n */\r\nexport const INCREMENTAL_PATCH_LIMIT = 256;\r\n\r\n/**\r\n * Why {@link updateViewIndex} declined the incremental path — `null` means it ran. See each valve's\r\n * own guard. `\"custom-comparator\"` is retained for callers that switch on it, but no longer fires:\r\n * a resolved `accessor.compare` rides the incremental path (see {@link makeViewComparator}).\r\n */\r\nexport type IncrementalBailReason = \"too-many-rows\" | \"custom-comparator\" | \"search-active\";\r\n\r\nexport type IncrementalViewIndexResult =\r\n  | { viewIndex: number[]; bail: null }\r\n  | { viewIndex: null; bail: IncrementalBailReason };\r\n\r\n/**\r\n * A row's total-order position under one sort column, resolved once per row per call.\r\n * Empty sorts last in BOTH directions, so the direction multiplier applies only when neither\r\n * side is empty — {@link buildViewIndex}'s own rule, replicated here rather than shared, because the\r\n * rebuild stays the untouched reference implementation. `custom` is the column's resolved\r\n * `accessor.compare` when it has one; otherwise the text keys drive `defaultCompareText`.\r\n */\r\nfunction compareOnColumn(\r\n  accessor: CellAccessor,\r\n  columnId: string,\r\n  dir: 1 | -1,\r\n  custom: ((a: number, b: number) => number) | undefined,\r\n  a: number,\r\n  b: number,\r\n): number {\r\n  if (custom) {\r\n    const emptyA = isEmptyCell(accessor, a, columnId);\r\n    const emptyB = isEmptyCell(accessor, b, columnId);\r\n    if (emptyA && emptyB) return 0;\r\n    if (emptyA) return 1;\r\n    if (emptyB) return -1;\r\n    return dir * custom(a, b);\r\n  }\r\n  const textA = accessor.getText(a, columnId);\r\n  const textB = accessor.getText(b, columnId);\r\n  if (textA === \"\" || textB === \"\") return defaultCompareText(textA, textB);\r\n  return dir * defaultCompareText(textA, textB);\r\n}\r\n\r\n/**\r\n * The comparator chain {@link buildViewIndex} sorts by, expressed over data indices.\r\n *\r\n * Tiebreak equivalence: the rebuild decorates each row with its POSITION in the filtered index array\r\n * and breaks ties on that position. `indices` starts as `[0..n)` and every filter pass preserves\r\n * relative order, so position is strictly increasing in data index — `positionA - positionB` and\r\n * `a - b` always agree in sign. Comparing data indices directly is therefore the identical tiebreak,\r\n * and it makes the order TOTAL (no two rows ever compare equal), which is what lets a binary search\r\n * find one uniquely-correct insertion point.\r\n */\r\nexport function makeViewComparator(accessor: CellAccessor, sorts: readonly SortSpec[]): (a: number, b: number) => number {\r\n  // Resolved ONCE per column here, not per comparison — mirrors the rebuild's per-sort resolution.\r\n  const columns = sorts.map((sort) => ({\r\n    columnId: sort.columnId,\r\n    dir: (sort.direction === \"asc\" ? 1 : -1) as 1 | -1,\r\n    custom: accessor.compare?.(sort.columnId),\r\n  }));\r\n  return (a, b) => {\r\n    for (const { columnId, dir, custom } of columns) {\r\n      const result = compareOnColumn(accessor, columnId, dir, custom, a, b);\r\n      if (result !== 0) return result;\r\n    }\r\n    return a - b;\r\n  };\r\n}\r\n\r\n/** Whether `row` survives the active filters — the same matchers, join semantics, and text source the rebuild uses. */\r\nexport function makeFilterPredicate(\r\n  accessor: CellAccessor,\r\n  filters: readonly FilterSpec[],\r\n  joinOperator: FilterJoinOperator | undefined,\r\n): (row: number) => boolean {\r\n  if (filters.length === 0) return () => true;\r\n  const matchers = filters.map((filter) => makeFilterTest(accessor, filter));\r\n  if (joinOperator === \"or\") return (row) => matchers.some((test) => test(row));\r\n  return (row) => matchers.every((test) => test(row));\r\n}\r\n\r\n/** Index in `view` where `row` belongs under `compare`; `view` must already be sorted by it. */\r\nexport function lowerBound(view: readonly number[], row: number, compare: (a: number, b: number) => number): number {\r\n  let lo = 0;\r\n  let hi = view.length;\r\n  while (lo < hi) {\r\n    const mid = (lo + hi) >>> 1;\r\n    if (compare(view[mid]!, row) < 0) lo = mid + 1;\r\n    else hi = mid;\r\n  }\r\n  return lo;\r\n}\r\n\r\n/**\r\n * Rebuilds `prevViewIndex` for a value patch that touched `touchedRows` (data indices), instead of\r\n * re-sorting all `n` rows. Per touched row: remove it from the order, re-test filter membership\r\n * against the NEW values, and binary-search it back in under the identical comparator chain — so a\r\n * value change may also add a row to, or drop it from, the filtered set.\r\n *\r\n * `touchedRows` must name EVERY row whose values changed since `prevViewIndex` was built (a caller\r\n * holding deferred batches has to pass their rows too) — every other row's key is assumed unchanged,\r\n * which is what makes the remaining order a valid binary-search invariant.\r\n *\r\n * Returns `{viewIndex: null, bail}` whenever the incremental path cannot prove it reproduces\r\n * {@link buildViewIndex} exactly; the caller must then run the full rebuild. Correctness beats\r\n * cleverness: every valve below is a case where equivalence is not provable, not a case where it is\r\n * merely slower.\r\n *\r\n * A column with an `accessor.compare` needs NO valve: both paths now resolve the same comparator\r\n * under the same empty-last rule, and the accessor contract makes it a deterministic function of the\r\n * two rows' values — so an untouched row's key is unchanged and the remaining order stays a valid\r\n * binary-search invariant, exactly as for the default text comparator.\r\n */\r\nexport function updateViewIndex(\r\n  prevViewIndex: readonly number[],\r\n  touchedRows: readonly number[],\r\n  accessor: CellAccessor,\r\n  opts: ViewIndexOptions,\r\n): IncrementalViewIndexResult {\r\n  if (touchedRows.length > INCREMENTAL_PATCH_LIMIT) return { viewIndex: null, bail: \"too-many-rows\" };\r\n  // buildViewIndex narrows by `search` before sorting; this path only knows sort + filter membership.\r\n  if (opts.search?.trim() && opts.searchColumnIds) return { viewIndex: null, bail: \"search-active\" };\r\n  const touched = new Set(touchedRows);\r\n  const survives = makeFilterPredicate(accessor, opts.filters, opts.joinOperator);\r\n  // With no sort columns the comparator degenerates to the data-index tiebreak alone, which is\r\n  // exactly the order the rebuild leaves an unsorted filtered view in.\r\n  const compare = makeViewComparator(accessor, opts.sorts);\r\n\r\n  // `indexOf` and `splice` are native memmoves over the index array, so 2k of them stay far cheaper\r\n  // than any JS-level pass over all n. Measured at 100k rows / one sort column: this path costs\r\n  // 0.47 / 0.91 / 1.57 ms at k = 5 / 20 / 50, against a uniform 3.7-4.1 ms for a single\r\n  // filter-and-merge pass — the JS-loop alternative loses at every k, so there is only one path.\r\n  const next = prevViewIndex.slice();\r\n  for (const row of touched) {\r\n    const position = next.indexOf(row);\r\n    if (position !== -1) next.splice(position, 1);\r\n  }\r\n  for (const row of touched) {\r\n    if (!survives(row)) continue;\r\n    next.splice(lowerBound(next, row, compare), 0, row);\r\n  }\r\n  return { viewIndex: next, bail: null };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/sort-filter/incremental-view-index.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/sort-filter/default-compare-text.ts",
      "content": "/** Module-default collator, used when a call site doesn't supply its own (e.g. to amortize one instance across a whole sort). */\r\nconst defaultCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: \"base\" });\r\n\r\n/**\r\n * Default text comparator: numeric-aware localeCompare, case-insensitive.\r\n * Empty strings always sort last, regardless of direction (callers must not\r\n * negate this result wholesale — see the empty-aware wrapper in buildViewIndex).\r\n * `collator` lets a hot loop (buildViewIndex) reuse one Intl.Collator instead of\r\n * this function constructing a fresh one per call.\r\n */\r\nexport function defaultCompareText(a: string, b: string, collator: Intl.Collator = defaultCollator): number {\r\n  const aEmpty = a === \"\";\r\n  const bEmpty = b === \"\";\r\n  if (aEmpty && bEmpty) return 0;\r\n  if (aEmpty) return 1;\r\n  if (bEmpty) return -1;\r\n  return collator.compare(a, b);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/sort-filter/default-compare-text.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/sort-filter/find-search-matches.ts",
      "content": "import type { CellAccessor } from \"./build-view-index\";\r\n\r\n/** One search hit, in row-major order, for highlight and next/prev navigation. */\r\nexport type SearchMatch = { row: number; columnId: string };\r\n\r\n/**\r\n * Finds every cell matching `search` (case-insensitive substring) across\r\n * `columnIds`, in row-major order — the order next/prev navigation and\r\n * highlight rendering expect. `maxMatches`, when given, early-exits the scan\r\n * the moment that many hits are collected (bounds worst-case work\r\n * at 100k+ rows for a common search term; omitted keeps the full scan).\r\n */\r\nexport function findSearchMatches(\r\n  rowCount: number,\r\n  accessor: CellAccessor,\r\n  search: string,\r\n  columnIds: string[],\r\n  maxMatches?: number,\r\n): SearchMatch[] {\r\n  const needle = search.trim().toLowerCase();\r\n  if (needle === \"\") return [];\r\n\r\n  const matches: SearchMatch[] = [];\r\n  for (let row = 0; row < rowCount; row++) {\r\n    for (const columnId of columnIds) {\r\n      if (accessor.getText(row, columnId).toLowerCase().includes(needle)) {\r\n        matches.push({ row, columnId });\r\n        if (maxMatches !== undefined && matches.length >= maxMatches) return matches;\r\n      }\r\n    }\r\n  }\r\n  return matches;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/sort-filter/find-search-matches.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/sort-filter/index.ts",
      "content": "/** Domain barrel — sort/filter/search: compare fns, filter matching, view-index, search matches. */\r\nexport { defaultCompareText } from \"./default-compare-text\";\r\nexport { matchesFilter, createFilterMatcher } from \"./matches-filter\";\r\nexport { buildViewIndex, isEmptyCell, makeFilterTest, type CellAccessor, type ViewIndexOptions } from \"./build-view-index\";\r\nexport { findSearchMatches, type SearchMatch } from \"./find-search-matches\";\r\nexport {\r\n  updateViewIndex,\r\n  makeViewComparator,\r\n  makeFilterPredicate,\r\n  lowerBound,\r\n  INCREMENTAL_PATCH_LIMIT,\r\n  type IncrementalBailReason,\r\n  type IncrementalViewIndexResult,\r\n} from \"./incremental-view-index\";\r\n",
      "type": "registry:component",
      "target": "components/data-grid/sort-filter/index.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/sort-filter/matches-filter.ts",
      "content": "import type { FilterOperator, FilterSpec } from \"../types\";\r\nimport { defaultCompareText } from \"./default-compare-text\";\r\n\r\nfunction parseNumber(text: string): number | null {\r\n  if (text.trim() === \"\") return null;\r\n  const n = Number(text);\r\n  return Number.isNaN(n) ? null : n;\r\n}\r\n\r\n/** Compares `text` against `bound` numerically when both parse as numbers, else via `defaultCompareText`. */\r\nfunction compareTextTo(text: string, bound: string): number {\r\n  const a = parseNumber(text);\r\n  const b = parseNumber(bound);\r\n  if (a !== null && b !== null) return a - b;\r\n  return defaultCompareText(text, bound);\r\n}\r\n\r\n/**\r\n * Evaluates a single {@link FilterSpec} against a cell's text value.\r\n * `gt`/`gte`/`lt`/`lte` compare numerically when both sides parse as\r\n * numbers, else fall back to `defaultCompareText`; blank cell text never\r\n * satisfies these comparisons. `empty`/`notEmpty` ignore `filter.value`.\r\n * `isBetween` is inclusive on both ends; an empty bound (`\"\"`) leaves that\r\n * side open (e.g. `[\"\", \"10\"]` means \"<= 10\"). `isAnyOf` matches when the cell\r\n * text equals any entry of its `string[]`, compared case-insensitively like `equals`.\r\n *\r\n * This is the correctness reference (re-parses `filter.value` on every call) — {@link createFilterMatcher}\r\n * is the same semantics with the bound(s) parsed once, for a hot per-row loop over many rows.\r\n */\r\nexport function matchesFilter(text: string, filter: FilterSpec): boolean {\r\n  const op: FilterOperator = filter.operator;\r\n  if (op === \"empty\") return text === \"\";\r\n  if (op === \"notEmpty\") return text !== \"\";\r\n\r\n  if (op === \"isAnyOf\") {\r\n    // an empty choice list matches nothing, so a half-built filter row hides every row rather than being a no-op\r\n    const choices = Array.isArray(filter.value) ? filter.value : [];\r\n    const textLower = text.toLowerCase();\r\n    return choices.some((choice) => choice.toLowerCase() === textLower);\r\n  }\r\n\r\n  if (op === \"isBetween\") {\r\n    // Excel/AG Grid semantics: blank cells never satisfy a comparison filter.\r\n    if (text === \"\") return false;\r\n    const [min = \"\", max = \"\"] = Array.isArray(filter.value) ? filter.value : [\"\", \"\"];\r\n    if (min !== \"\" && compareTextTo(text, min) < 0) return false;\r\n    if (max !== \"\" && compareTextTo(text, max) > 0) return false;\r\n    return true;\r\n  }\r\n\r\n  const value = typeof filter.value === \"string\" ? filter.value : \"\";\r\n  const textLower = text.toLowerCase();\r\n  const valueLower = value.toLowerCase();\r\n\r\n  switch (op) {\r\n    case \"contains\":\r\n      return textLower.includes(valueLower);\r\n    case \"notContains\":\r\n      return !textLower.includes(valueLower);\r\n    case \"equals\":\r\n      return textLower === valueLower;\r\n    case \"notEquals\":\r\n      return textLower !== valueLower;\r\n    case \"startsWith\":\r\n      return textLower.startsWith(valueLower);\r\n    case \"endsWith\":\r\n      return textLower.endsWith(valueLower);\r\n    case \"gt\":\r\n    case \"gte\":\r\n    case \"lt\":\r\n    case \"lte\": {\r\n      // Excel/AG Grid semantics: blank cells never satisfy a comparison filter.\r\n      if (text === \"\") return false;\r\n      const cmp = compareTextTo(text, value);\r\n      if (op === \"gt\") return cmp > 0;\r\n      if (op === \"gte\") return cmp >= 0;\r\n      if (op === \"lt\") return cmp < 0;\r\n      return cmp <= 0;\r\n    }\r\n    default: {\r\n      const exhaustive: never = op;\r\n      return exhaustive;\r\n    }\r\n  }\r\n}\r\n\r\n/** A bound pre-parsed once (outside the per-row loop) plus its original text for the string fallback path. */\r\ntype ParsedBound = { raw: string; num: number | null };\r\n\r\nfunction parseBound(raw: string): ParsedBound {\r\n  return { raw, num: parseNumber(raw) };\r\n}\r\n\r\n/** Compares `text` against a {@link ParsedBound}: numeric if `text` also parses, else falls back to `defaultCompareText`. */\r\nfunction compareTextToParsedBound(text: string, bound: ParsedBound): number {\r\n  if (bound.num !== null) {\r\n    const a = parseNumber(text);\r\n    if (a !== null) return a - bound.num;\r\n  }\r\n  return defaultCompareText(text, bound.raw);\r\n}\r\n\r\n/**\r\n * Builds a per-row matcher for `filter` with any numeric bound(s) in `filter.value` parsed exactly\r\n * once, instead of on every {@link matchesFilter} call — `gt`/`gte`/`lt`/`lte`/`isBetween` bounds are\r\n * the same string across every row of a `buildViewIndex` pass, so re-parsing them per row (as a\r\n * direct `matchesFilter` call in a `.filter()` predicate does) is O(n) redundant work. Same\r\n * semantics as `matchesFilter`, verified by build-view-index's own tests; kept in sync manually\r\n * since duplicating a switch this small behind a shared abstraction would obscure both.\r\n */\r\nexport function createFilterMatcher(filter: FilterSpec): (text: string) => boolean {\r\n  const op: FilterOperator = filter.operator;\r\n  if (op === \"empty\") return (text) => text === \"\";\r\n  if (op === \"notEmpty\") return (text) => text !== \"\";\r\n\r\n  if (op === \"isAnyOf\") {\r\n    const choices = (Array.isArray(filter.value) ? filter.value : []).map((choice) => choice.toLowerCase());\r\n    const set = new Set(choices);\r\n    return (text) => set.has(text.toLowerCase());\r\n  }\r\n\r\n  if (op === \"isBetween\") {\r\n    const [minRaw = \"\", maxRaw = \"\"] = Array.isArray(filter.value) ? filter.value : [\"\", \"\"];\r\n    const min = minRaw !== \"\" ? parseBound(minRaw) : null;\r\n    const max = maxRaw !== \"\" ? parseBound(maxRaw) : null;\r\n    return (text) => {\r\n      if (text === \"\") return false;\r\n      if (min && compareTextToParsedBound(text, min) < 0) return false;\r\n      if (max && compareTextToParsedBound(text, max) > 0) return false;\r\n      return true;\r\n    };\r\n  }\r\n\r\n  const value = typeof filter.value === \"string\" ? filter.value : \"\";\r\n\r\n  switch (op) {\r\n    case \"contains\": {\r\n      const needle = value.toLowerCase();\r\n      return (text) => text.toLowerCase().includes(needle);\r\n    }\r\n    case \"notContains\": {\r\n      const needle = value.toLowerCase();\r\n      return (text) => !text.toLowerCase().includes(needle);\r\n    }\r\n    case \"equals\": {\r\n      const needle = value.toLowerCase();\r\n      return (text) => text.toLowerCase() === needle;\r\n    }\r\n    case \"notEquals\": {\r\n      const needle = value.toLowerCase();\r\n      return (text) => text.toLowerCase() !== needle;\r\n    }\r\n    case \"startsWith\": {\r\n      const needle = value.toLowerCase();\r\n      return (text) => text.toLowerCase().startsWith(needle);\r\n    }\r\n    case \"endsWith\": {\r\n      const needle = value.toLowerCase();\r\n      return (text) => text.toLowerCase().endsWith(needle);\r\n    }\r\n    case \"gt\":\r\n    case \"gte\":\r\n    case \"lt\":\r\n    case \"lte\": {\r\n      const bound = parseBound(value);\r\n      return (text) => {\r\n        if (text === \"\") return false;\r\n        const cmp = compareTextToParsedBound(text, bound);\r\n        if (op === \"gt\") return cmp > 0;\r\n        if (op === \"gte\") return cmp >= 0;\r\n        if (op === \"lt\") return cmp < 0;\r\n        return cmp <= 0;\r\n      };\r\n    }\r\n    default: {\r\n      const exhaustive: never = op;\r\n      return exhaustive;\r\n    }\r\n  }\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/sort-filter/matches-filter.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/store/commit.ts",
      "content": "import type { CellCoord, CellType, DataChange, DataOp } from \"../types\";\r\nimport type { OverlayPlugin } from \"../overlays\";\r\nimport type { RowBandsSpec } from \"../layout-context\";\r\nimport { getCellValue, setCellValue } from \"../columns/column-helpers\";\r\nimport { isDev } from \"../is-dev\";\r\nimport { isStandardSchema, runValidateSync, type ValidateResult } from \"../validation/validate-cell\";\r\nimport { runValidateBatch, type ValidateBatchItem } from \"../validation/validate-batch\";\r\nimport { isColumnReadOnly } from \"./compute\";\r\nimport { cellTypes as defaultCellTypes } from \"../cell-types/cell-types\";\r\nimport type { AnyColumnDef, CellPatch, CommitResult, DataGridStoreState, InternalSyncProps, RowEdit, UpdateCellsSkip } from \"./types\";\r\n\r\nexport function warnDev(message: string): void {\r\n  if (isDev()) console.warn(`[data-grid] ${message}`);\r\n}\r\n\r\n/**\r\n * Warns once per instance (per passed Set) for every column whose `type` resolves to nothing in\r\n * the effective cellTypes registry (the passed registry, or the built-ins when none was passed) —\r\n * a typo silently degrades the column to text rendering, and its edit/paste/delete paths go dead\r\n * (see `resolveEditTarget`). The single warn site for the cellTypes replace-semantics footgun\r\n * (a custom registry omitting a used type); do not add a second one.\r\n */\r\nexport function checkUnresolvableColumnTypes(\r\n  columns: readonly AnyColumnDef[],\r\n  cellTypes: Readonly<Record<string, unknown>> | undefined,\r\n  warned: Set<string>,\r\n): void {\r\n  if (!isDev()) return;\r\n  const registry: Record<string, unknown> = cellTypes ?? defaultCellTypes;\r\n  for (const column of columns) {\r\n    if (column.type === undefined || registry[column.type] !== undefined) continue;\r\n    const key = `${column.id}:${column.type}`;\r\n    if (warned.has(key)) continue;\r\n    warned.add(key);\r\n    warnDev(`column \"${column.id}\" has type \"${column.type}\" which resolves to nothing in the cellTypes registry (typo or a custom registry without it); it renders as text and its edits, pastes, and deletes are dropped`);\r\n  }\r\n}\r\n\r\n/** Fires `warnDev` at most once per session — for guardrails whose cause (a mistaken prop combo) doesn't change across re-renders, so repeating it every render would just be noise. */\r\nlet warnedBothDataProps = false;\r\n\r\n/** `data` + `defaultData` are mutually exclusive (React value/defaultValue semantics); `data` wins. */\r\nexport function warnBothDataPropsOnce(): void {\r\n  if (warnedBothDataProps || !isDev()) return;\r\n  warnedBothDataProps = true;\r\n  warnDev(\"both `data` and `defaultData` were provided; `data` wins (controlled) and `defaultData` is ignored\");\r\n}\r\n\r\n/** Dev-only guardrails: duplicate column ids, missing getRowId, unstable columns/data/overlayPlugins/rowBands identity across renders. */\r\nexport function checkDevGuardrails(\r\n  props: InternalSyncProps,\r\n  prevColumns: readonly AnyColumnDef[] | undefined,\r\n  prevData: readonly unknown[] | undefined,\r\n  prevOverlayPlugins: readonly OverlayPlugin[] | undefined,\r\n  prevRowBands: RowBandsSpec | undefined,\r\n): void {\r\n  if (!isDev()) return;\r\n\r\n  const seen = new Set<string>();\r\n  for (const column of props.columns) {\r\n    if (seen.has(column.id)) warnDev(`duplicate column id \"${column.id}\"`);\r\n    seen.add(column.id);\r\n  }\r\n\r\n  if (typeof props.getRowId !== \"function\") {\r\n    warnDev(\"getRowId is required and must be a function\");\r\n  }\r\n\r\n  if (props.data !== undefined && props.defaultData !== undefined) {\r\n    warnBothDataPropsOnce();\r\n  }\r\n\r\n  if (prevColumns !== undefined && prevColumns !== props.columns) {\r\n    warnDev(\r\n      \"columns array identity changed since the last render; pass a stable reference (e.g. useMemo) or every row re-renders\",\r\n    );\r\n  }\r\n  if (prevOverlayPlugins !== undefined && prevOverlayPlugins !== props.overlayPlugins) {\r\n    warnDev(\r\n      \"overlayPlugins array identity changed since the last render; pass a stable reference (module scope or useMemo) or every render re-syncs the provider props into the store\",\r\n    );\r\n  }\r\n  if (prevRowBands !== undefined && prevRowBands !== props.rowBands) {\r\n    warnDev(\r\n      \"rowBands identity changed since the last render; pass a stable reference (the add-on's hook already returns one) or every render recomputes band heights/aria-rowcount\",\r\n    );\r\n  }\r\n  // Controlled mode only: in uncontrolled mode props.data is undefined (data lives in the store),\r\n  // and the store's own array legitimately gets a new reference on every mutation — not a bug.\r\n  if (props.data !== undefined && prevData !== undefined && prevData !== props.data && prevData.length === props.data.length) {\r\n    // Only identity churn with zero row changes is the anti-pattern (array rebuilt per render);\r\n    // a legitimate immutable edit swaps ≥1 row reference — the early exit keeps this cheap for it.\r\n    const data = props.data;\r\n    let anyRowChanged = false;\r\n    for (let i = 0; i < prevData.length; i++) {\r\n      if (prevData[i] !== data[i]) {\r\n        anyRowChanged = true;\r\n        break;\r\n      }\r\n    }\r\n    if (!anyRowChanged) {\r\n      warnDev(\"data array identity changed with no row changes; pass a stable reference (rebuilding it every render defeats row memoization)\");\r\n    }\r\n  }\r\n}\r\n\r\n/**\r\n * Shared value-write path for both `commitCellEdit` (editing session) and `commitCellValue` (direct\r\n * write, e.g. checkbox toggle). `value` may already be schema-validated+transformed (the async\r\n * editor-commit layer passes `result.value`, having awaited the schema itself) — this still\r\n * re-runs `validate` (cheap, idempotent for a well-behaved schema) so the sync/function-form path\r\n * is unchanged and a direct `commitCellValue` call always gets checked.\r\n *\r\n * `onInvalid: \"warn\"` columns commit a rejected value (raw — a rejection carries no transformed\r\n * value) and report it in `warnings` so the store can flag the cell in `cellErrors`; `rejection`\r\n * is the async layer's awaited schema rejection, which the sync re-run here cannot see (a schema\r\n * Promise passes `runValidateSync` untouched).\r\n */\r\nexport function computeCommit(s: DataGridStoreState, coord: CellCoord, value: unknown, rejection?: string): CommitResult {\r\n  const target = resolveEditTarget(s, coord);\r\n  if (!target) return { noop: true };\r\n  const { column, dataRowIndex, row } = target;\r\n  const validated = runValidateSync(column.validate, value, row);\r\n  let nextValue: unknown;\r\n  let warning: string | undefined;\r\n  if (\"error\" in validated) {\r\n    if (column.onInvalid !== \"warn\") return { error: validated.error };\r\n    nextValue = value;\r\n    warning = validated.error;\r\n  } else {\r\n    nextValue = validated.value;\r\n    if (rejection !== undefined && column.onInvalid === \"warn\") warning = rejection;\r\n  }\r\n  const prevValue = getCellValue(row, column);\r\n  if (Object.is(prevValue, nextValue)) {\r\n    // re-committing the SAME invalid value is a deliberate keep: the flag (re)lands\r\n    return warning === undefined\r\n      ? { noop: true }\r\n      : { noop: true, warnings: [{ rowId: s.getRowId(row, dataRowIndex), columnId: column.id, message: warning }] };\r\n  }\r\n\r\n  const nextRow = setCellValue(row, column, nextValue);\r\n  const nextData = s.data.slice();\r\n  nextData[dataRowIndex] = nextRow;\r\n  const rowId = s.getRowId(nextRow, dataRowIndex);\r\n  const change: DataChange<unknown> = {\r\n    source: \"edit\",\r\n    ops: [\r\n      {\r\n        type: \"update\",\r\n        rowId,\r\n        row: nextRow,\r\n        prev: row,\r\n        cells: [{ columnId: column.id, value: nextValue, prev: prevValue }],\r\n      },\r\n    ],\r\n  };\r\n  return warning === undefined\r\n    ? { data: nextData, change }\r\n    : { data: nextData, change, warnings: [{ rowId, columnId: column.id, message: warning }] };\r\n}\r\n\r\n/** Looks up the column, data row, and cell type at a view coord; null when any part is unresolvable. */\r\nexport function resolveEditTarget(\r\n  s: DataGridStoreState,\r\n  coord: CellCoord,\r\n): { column: AnyColumnDef; dataRowIndex: number; row: unknown; cellType: CellType } | null {\r\n  const column = s.visibleColumns[coord.col];\r\n  if (!column) return null;\r\n  const dataRowIndex = s.viewIndex[coord.row];\r\n  if (dataRowIndex === undefined) return null;\r\n  const row = s.data[dataRowIndex];\r\n  if (row === undefined) return null;\r\n  const cellType = s.cellTypes[column.type ?? \"text\"];\r\n  if (!cellType) return null;\r\n  return { column, dataRowIndex, row, cellType };\r\n}\r\n\r\n/**\r\n * Applies `writes` (view-row + column-id + already-resolved value) against `s`, deduping multiple\r\n * writes to the same row/column (last one wins), skipping readOnly cells, unresolvable rows/columns,\r\n * and no-op values (`Object.is` against the current value). Shared by `deleteSelection` and\r\n * `applyCellUpdates` so both funnel through one row-edit accumulation + op-building path.\r\n */\r\nexport function computeRowEditsBatch(\r\n  s: DataGridStoreState,\r\n  writes: { viewRow: number; columnId: string; value: unknown }[],\r\n): { nextData: readonly unknown[]; ops: DataOp<unknown>[] } | null {\r\n  const rowEdits = new Map<number, RowEdit>();\r\n  const columnById = new Map(s.visibleColumns.map((column) => [column.id, column] as const));\r\n\r\n  for (const write of writes) {\r\n    const dataRowIndex = s.viewIndex[write.viewRow];\r\n    if (dataRowIndex === undefined) continue;\r\n    const column = columnById.get(write.columnId);\r\n    if (!column) continue;\r\n\r\n    const entry = rowEdits.get(dataRowIndex);\r\n    const baseRow = entry?.row ?? s.data[dataRowIndex];\r\n    if (baseRow === undefined) continue;\r\n    if (isColumnReadOnly(column, baseRow)) continue;\r\n\r\n    // explicit TData=unknown: baseRow's `undefined`-narrowed type ({} | null) would otherwise drive\r\n    // inference instead of column's own already-unknown TData, tripping ColumnDef<unknown>'s setValue.\r\n    const prevValue = getCellValue<unknown, typeof column>(baseRow, column);\r\n    if (Object.is(prevValue, write.value)) continue;\r\n\r\n    const row = setCellValue<unknown, typeof column>(baseRow, column, write.value);\r\n    const cells = entry?.cells ?? new Map<string, { columnId: string; value: unknown; prev: unknown }>();\r\n    // last write to this row/column wins; `prev` stays the value before ANY write in this batch.\r\n    const existingPrev = cells.get(column.id)?.prev ?? prevValue;\r\n    cells.set(column.id, { columnId: column.id, value: write.value, prev: existingPrev });\r\n    rowEdits.set(dataRowIndex, { row, cells });\r\n  }\r\n\r\n  if (rowEdits.size === 0) return null;\r\n\r\n  const nextData = s.data.slice();\r\n  const ops: DataOp<unknown>[] = [];\r\n  for (const [dataRowIndex, { row, cells }] of rowEdits) {\r\n    const prevRow = s.data[dataRowIndex];\r\n    nextData[dataRowIndex] = row;\r\n    ops.push({\r\n      type: \"update\",\r\n      rowId: s.getRowId(row, dataRowIndex),\r\n      row,\r\n      prev: prevRow,\r\n      cells: Array.from(cells.values()),\r\n    });\r\n  }\r\n  return { nextData, ops };\r\n}\r\n\r\n/**\r\n * Applies id-keyed {@link CellPatch}es against `s` — the direct-update path's counterpart to\r\n * {@link computeRowEditsBatch}. Differences, all deliberate: rows are addressed by STABLE ROW ID\r\n * through the caller-supplied `rowIndex` map (never a view index, so a patch survives an active\r\n * sort), and every column is resolvable, not only visible ones (a streaming producer must be able\r\n * to write a column the user has hidden). Same skip rules otherwise: unknown row, unknown column,\r\n * readOnly cell, and `Object.is`-equal no-op writes are dropped; duplicate patches to one cell\r\n * dedupe last-write-wins with `prev` held at the pre-batch value.\r\n *\r\n * `skipValidation` bypasses the per-column `validate` a 200-cell tick would otherwise re-run for a\r\n * producer that already validated; otherwise a cell whose value fails validation is skipped,\r\n * matching {@link computeRowEditsBatch}'s bulk contract (a stream has no UI surface to reject a\r\n * batch on and must never drop a whole batch for one bad value). Every skipped patch is named in\r\n * the returned `skipped` list for the action's verdict.\r\n */\r\nexport function computeCellPatchBatch(\r\n  s: DataGridStoreState,\r\n  patches: readonly CellPatch[],\r\n  rowIndex: ReadonlyMap<string, number>,\r\n  skipValidation: boolean,\r\n): { nextData: readonly unknown[] | null; ops: DataOp<unknown>[]; touchedRows: number[]; skipped: UpdateCellsSkip[] } {\r\n  const columnsById = new Map(s.columns.map((c) => [c.id, c] as const));\r\n  const rowEdits = new Map<number, RowEdit>();\r\n  const skipped: UpdateCellsSkip[] = [];\r\n\r\n  for (let patchIndex = 0; patchIndex < patches.length; patchIndex++) {\r\n    const patch = patches[patchIndex]!;\r\n    const dataRowIndex = rowIndex.get(patch.rowId);\r\n    if (dataRowIndex === undefined) {\r\n      skipped.push({ patchIndex, reason: \"unknown-row\" });\r\n      continue;\r\n    }\r\n    const column = columnsById.get(patch.columnId);\r\n    if (!column) {\r\n      skipped.push({ patchIndex, reason: \"unknown-column\" });\r\n      continue;\r\n    }\r\n\r\n    const entry = rowEdits.get(dataRowIndex);\r\n    const baseRow = entry?.row ?? s.data[dataRowIndex];\r\n    if (baseRow === undefined) {\r\n      skipped.push({ patchIndex, reason: \"hole\" });\r\n      continue;\r\n    }\r\n    if (isColumnReadOnly(column, baseRow)) {\r\n      skipped.push({ patchIndex, reason: \"readonly\" });\r\n      continue;\r\n    }\r\n\r\n    let value = patch.value;\r\n    if (!skipValidation) {\r\n      const validated = runValidateSync(column.validate, value, baseRow);\r\n      if (\"error\" in validated) {\r\n        skipped.push({ patchIndex, reason: \"invalid\" });\r\n        continue;\r\n      }\r\n      value = validated.value;\r\n    }\r\n\r\n    const prevValue = getCellValue<unknown, typeof column>(baseRow, column);\r\n    if (Object.is(prevValue, value)) {\r\n      skipped.push({ patchIndex, reason: \"no-op\" });\r\n      continue;\r\n    }\r\n\r\n    const row = setCellValue<unknown, typeof column>(baseRow, column, value);\r\n    const cells = entry?.cells ?? new Map<string, { columnId: string; value: unknown; prev: unknown }>();\r\n    const existingPrev = cells.get(column.id)?.prev ?? prevValue;\r\n    cells.set(column.id, { columnId: column.id, value, prev: existingPrev });\r\n    rowEdits.set(dataRowIndex, { row, cells });\r\n  }\r\n\r\n  if (rowEdits.size === 0) return { nextData: null, ops: [], touchedRows: [], skipped };\r\n\r\n  const nextData = s.data.slice();\r\n  const ops: DataOp<unknown>[] = [];\r\n  // The data indices whose values actually changed — the incremental view-index path's input.\r\n  const touchedRows: number[] = [];\r\n  for (const [dataRowIndex, { row, cells }] of rowEdits) {\r\n    const prevRow = s.data[dataRowIndex];\r\n    nextData[dataRowIndex] = row;\r\n    touchedRows.push(dataRowIndex);\r\n    ops.push({\r\n      type: \"update\",\r\n      rowId: s.getRowId(row, dataRowIndex),\r\n      row,\r\n      prev: prevRow,\r\n      cells: Array.from(cells.values()),\r\n    });\r\n  }\r\n  return { nextData, ops, touchedRows, skipped };\r\n}\r\n\r\n/**\r\n * Whether any column `patches` writes carries a Standard Schema, the only `validate` form that can\r\n * be async. A purely structural test — it never CALLS a validator, so the ordinary streaming tick\r\n * (no validators, or the function form) pays one map lookup per patch and nothing else, and\r\n * `computeCellPatchBatch` still runs every validator exactly once, inline, as it always has.\r\n */\r\nexport function patchesNeedAsyncCheck(s: DataGridStoreState, patches: readonly CellPatch[]): boolean {\r\n  const columnsById = new Map(s.columns.map((c) => [c.id, c] as const));\r\n  for (const patch of patches) {\r\n    if (isStandardSchema(columnsById.get(patch.columnId)?.validate)) return true;\r\n  }\r\n  return false;\r\n}\r\n\r\n/**\r\n * Validates `patches` for {@link DataGridActions.updateCells} ahead of the apply, so what reaches\r\n * {@link computeCellPatchBatch} carries only accepted, already-transformed values and runs with\r\n * validation off. There is still exactly ONE apply path — this only decides what enters it, and it\r\n * runs at all only when {@link patchesNeedAsyncCheck} says a schema is in play.\r\n *\r\n * Failing cells are dropped from the apply and reported by index, so one bad value never loses the\r\n * whole batch and the caller still learns which of its patches were rejected.\r\n */\r\nexport function prevalidatePatches(\r\n  s: DataGridStoreState,\r\n  patches: readonly CellPatch[],\r\n  rowIndex: ReadonlyMap<string, number>,\r\n): PrevalidatedPatches | Promise<PrevalidatedPatches> {\r\n  const columnsById = new Map(s.columns.map((c) => [c.id, c] as const));\r\n  const items: ValidateBatchItem[] = patches.map((patch) => {\r\n    const dataRowIndex = rowIndex.get(patch.rowId);\r\n    return {\r\n      validate: columnsById.get(patch.columnId)?.validate,\r\n      value: patch.value,\r\n      row: dataRowIndex === undefined ? undefined : s.data[dataRowIndex],\r\n    };\r\n  });\r\n\r\n  const results = runValidateBatch(items);\r\n  if (results instanceof Promise) return results.then((resolved) => keepAccepted(patches, resolved));\r\n  return keepAccepted(patches, results);\r\n}\r\n\r\n/** Accepted patches with their index in the caller's array, and the caller's indexes of rejected ones. */\r\nexport type PrevalidatedPatches = { accepted: CellPatch[]; acceptedIndexes: number[]; rejectedIndexes: number[] };\r\n\r\nfunction keepAccepted(patches: readonly CellPatch[], results: readonly ValidateResult[]): PrevalidatedPatches {\r\n  const out: PrevalidatedPatches = { accepted: [], acceptedIndexes: [], rejectedIndexes: [] };\r\n  for (let i = 0; i < patches.length; i++) {\r\n    const result = results[i];\r\n    if (!result || \"error\" in result) {\r\n      out.rejectedIndexes.push(i);\r\n      continue;\r\n    }\r\n    out.accepted.push({ ...patches[i]!, value: result.value }); // i < patches.length by loop condition\r\n    out.acceptedIndexes.push(i);\r\n  }\r\n  return out;\r\n}\r\n\r\n/** Builds a multi-row insert batch: `rows` spliced into `s.data` at `dataRowIndex`, plus one id-keyed insert op per row (snapshot indices `dataRowIndex + i`, ascending). */\r\nexport function computeInsertRowsBatch(\r\n  s: DataGridStoreState,\r\n  dataRowIndex: number,\r\n  rows: readonly unknown[],\r\n): { nextData: readonly unknown[]; ops: DataOp<unknown>[] } {\r\n  const nextData = s.data.slice();\r\n  nextData.splice(dataRowIndex, 0, ...rows);\r\n  const ops: DataOp<unknown>[] = [];\r\n  for (let i = 0; i < rows.length; i++) {\r\n    const row = rows[i]!;\r\n    ops.push({ type: \"insert\", rowId: s.getRowId(row, dataRowIndex + i), row, index: dataRowIndex + i });\r\n  }\r\n  return { nextData, ops };\r\n}\r\n\r\n/**\r\n * Builds a multi-row delete batch: `dataRowIndexes` (deduped, descending so earlier splices don't\r\n * shift later indexes) removed from `s.data`, plus their id-keyed delete ops in the original\r\n * (ascending) row order.\r\n */\r\nexport function computeDeleteBatch(\r\n  s: DataGridStoreState,\r\n  dataRowIndexes: readonly number[],\r\n): { nextData: readonly unknown[]; ops: DataOp<unknown>[] } | null {\r\n  const uniqueAscending = Array.from(new Set(dataRowIndexes)).sort((a, b) => a - b);\r\n  if (uniqueAscending.length === 0) return null;\r\n\r\n  const ops: DataOp<unknown>[] = [];\r\n  for (const index of uniqueAscending) {\r\n    const row = s.data[index];\r\n    if (row === undefined) continue;\r\n    ops.push({ type: \"delete\", rowId: s.getRowId(row, index), row, index });\r\n  }\r\n\r\n  const nextData = s.data.slice();\r\n  for (let i = uniqueAscending.length - 1; i >= 0; i--) nextData.splice(uniqueAscending[i]!, 1); // i in-bounds by loop condition\r\n  return { nextData, ops };\r\n}\r\n\r\n/**\r\n * Builds a multi-row duplicate batch: each of `dataRowIndexes` (deduped, ascending) is copied via\r\n * the required `s.duplicateRow` and spliced immediately after its source row. Processed descending\r\n * so earlier splices don't shift the still-pending source indexes; ops are returned in ascending\r\n * source order for a stable, predictable id-keyed batch. Caller (`duplicateRows`) guarantees\r\n  * `duplicateRow` is present — a shallow-spread fallback would keep the source's `getRowId()`,\r\n  * colliding two sibling rows' React key.\r\n */\r\nexport function computeDuplicateBatch(\r\n  s: DataGridStoreState,\r\n  dataRowIndexes: readonly number[],\r\n): { nextData: readonly unknown[]; ops: DataOp<unknown>[] } | null {\r\n  const duplicateRow = s.duplicateRow;\r\n  if (!duplicateRow) return null;\r\n  const uniqueAscending = Array.from(new Set(dataRowIndexes)).sort((a, b) => a - b);\r\n  if (uniqueAscending.length === 0) return null;\r\n\r\n  const nextData = s.data.slice();\r\n  const opsByIndex = new Map<number, DataOp<unknown>>();\r\n  for (let i = uniqueAscending.length - 1; i >= 0; i--) {\r\n    const sourceIndex = uniqueAscending[i]!; // i in-bounds by loop condition\r\n    const sourceRow = s.data[sourceIndex];\r\n    if (sourceRow === undefined) continue;\r\n    const insertAt = sourceIndex + 1;\r\n    const copy = duplicateRow(sourceRow, insertAt);\r\n    nextData.splice(insertAt, 0, copy);\r\n    opsByIndex.set(sourceIndex, { type: \"insert\", rowId: s.getRowId(copy, insertAt), row: copy, index: insertAt });\r\n  }\r\n  const ops = uniqueAscending.map((index) => opsByIndex.get(index)).filter((op): op is DataOp<unknown> => op != null);\r\n  return { nextData, ops };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/store/commit.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/store/compute.ts",
      "content": "import type {\r\n  CellType,\r\n  ColumnLayout,\r\n  DataOp,\r\n  FilterJoinOperator,\r\n  FilterSpec,\r\n  GridRect,\r\n  SortSpec,\r\n} from \"../types\";\r\nimport { DEFAULT_LABELS, deepMergeLabels, type DataGridLabels, type DeepPartialLabels } from \"../labels\";\r\nimport {\r\n  buildViewIndex,\r\n  defaultCompareText,\r\n  findSearchMatches,\r\n  updateViewIndex,\r\n  type CellAccessor,\r\n  type SearchMatch,\r\n} from \"../sort-filter\";\r\nimport { getCellValue } from \"../columns/column-helpers\";\r\nimport { isDev } from \"../is-dev\";\r\nimport type { OverlayPlugin } from \"../overlays\";\r\nimport type { RowBandsSpec } from \"../layout-context\";\r\nimport type {\r\n  AnyColumnDef,\r\n  CellErrorEntry,\r\n  DataGridStoreState,\r\n  InternalSyncProps,\r\n  SelectionChangeDetails,\r\n} from \"./types\";\r\n\r\n/** Stable key for a search-match Set membership check: `\"${viewRow}:${columnId}\"`. */\r\nexport function searchMatchKey(viewRow: number, columnId: string): string {\r\n  return `${viewRow}:${columnId}`;\r\n}\r\n\r\n/** Shared empty-Set identity for the no-search-active state, so `useDataGridIsSearchMatch` never allocates on the hot path. */\r\nexport const EMPTY_SEARCH_MATCH_SET: ReadonlySet<string> = new Set();\r\n\r\n/** Stable key for {@link DataGridStoreState.flashingCells}: `\"${viewRow}:${columnId}\"` — view-space like `searchMatchKey` (a flash is a transient pulse on the cell currently on screen, never row identity). */\r\nexport function flashCellKey(viewRow: number, columnId: string): string {\r\n  return `${viewRow}:${columnId}`;\r\n}\r\n\r\n/** Shared empty-Set identity for the no-flash-active state, so per-row flash derivation never allocates on the common path. */\r\nexport const EMPTY_FLASHING_CELLS: ReadonlySet<string> = new Set();\r\n\r\n/** Shared empty-Map identity for the no-search-active state; mirrors {@link EMPTY_SEARCH_MATCH_SET}. */\r\nexport const EMPTY_SEARCH_MATCH_ROWS: ReadonlyMap<number, ReadonlySet<number>> = new Map();\r\n\r\n/** Shared empty-array identity for the no-overlay-plugins state, so a grid with none never allocates a fresh `[]` per render. */\r\nexport const EMPTY_OVERLAY_PLUGINS: readonly OverlayPlugin[] = [];\r\n\r\n/** Shared empty-bands identity for the no-`rowBands` state — zero-length arrays make root.tsx's height/aria-rowcount math a no-op; `renderBand` is never called since root only calls it when a band's own length is > 0. */\r\nexport const EMPTY_ROW_BANDS: RowBandsSpec = { topRows: [], bottomRows: [], renderBand: () => null };\r\n\r\n/** Stable key for {@link DataGridStoreState.cellErrors}: `\"${rowId}:${columnId}\"` — row-id-space, unlike {@link searchMatchKey}'s view-space, so an error survives a sort/filter that moves its row. */\r\nexport function cellErrorKey(rowId: string, columnId: string): string {\r\n  return `${rowId}:${columnId}`;\r\n}\r\n\r\n/**\r\n * Whether `key` belongs to a row in `liveRowIds`. The key is `\"${rowId}:${columnId}\"` and BOTH ids\r\n * may contain colons, so the split point is ambiguous — every colon is a candidate and the key is\r\n * live when any prefix before one is a live rowId. Splitting on the last colon alone silently\r\n * mis-parses `(\"t1:o9\", \"meta:sku\")` as rowId `\"t1:o9:meta\"` and prunes an error whose row is alive.\r\n */\r\nfunction cellErrorKeyIsLive(key: string, liveRowIds: ReadonlySet<string>): boolean {\r\n  for (let colon = key.indexOf(\":\"); colon !== -1; colon = key.indexOf(\":\", colon + 1)) {\r\n    if (liveRowIds.has(key.slice(0, colon))) return true;\r\n  }\r\n  return false;\r\n}\r\n\r\n/** Shared empty-Map identity for the no-server-errors state, so `setCellErrors`/`clearCellErrors` never allocate on the common empty path. */\r\nexport const EMPTY_CELL_ERRORS: ReadonlyMap<string, string> = new Map();\r\n\r\n/**\r\n * Auto-clear (a stale error must not linger after the user fixed the cell): every `update` op's\r\n * `cells` names the exact rowId+columnId pairs a write path just committed a new value to, so this\r\n * drops exactly those keys from `cellErrors` and returns the SAME map identity when none of them\r\n * were present — the common case (no server errors active) never allocates.\r\n */\r\nexport function clearErrorsForOps(cellErrors: ReadonlyMap<string, string>, ops: readonly DataOp<unknown>[]): ReadonlyMap<string, string> {\r\n  if (cellErrors.size === 0) return cellErrors;\r\n  let next: Map<string, string> | null = null;\r\n  for (const op of ops) {\r\n    if (op.type !== \"update\" || !op.cells) continue;\r\n    for (const cell of op.cells) {\r\n      const key = cellErrorKey(op.rowId, cell.columnId);\r\n      if (!cellErrors.has(key)) continue;\r\n      if (!next) next = new Map(cellErrors);\r\n      next.delete(key);\r\n    }\r\n  }\r\n  return next ?? cellErrors;\r\n}\r\n\r\n/** Merges `entries` into `cellErrors` per-key (a later entry for the same cell wins); returns the SAME map identity when there is nothing to merge. */\r\nexport function mergeCellErrors(cellErrors: ReadonlyMap<string, string>, entries: readonly CellErrorEntry[]): ReadonlyMap<string, string> {\r\n  if (entries.length === 0) return cellErrors;\r\n  const next = new Map(cellErrors);\r\n  for (const entry of entries) next.set(cellErrorKey(entry.rowId, entry.columnId), entry.message);\r\n  return next;\r\n}\r\n\r\n/**\r\n * Prunes any `cellErrors` entry whose rowId no longer exists in `data` (spec: \"pruned on row\r\n * deletion, audited like the rowId-index cache\") — called from every row-shape-changing path\r\n * (`deleteRows`, `duplicateRows`, `insertRow`) and from `_syncProps` on a genuine consumer `data`\r\n * replacement. Returns the SAME map identity when nothing needed pruning.\r\n */\r\nexport function pruneCellErrors(\r\n  cellErrors: ReadonlyMap<string, string>,\r\n  data: readonly unknown[],\r\n  getRowId: (row: unknown, index: number) => string,\r\n): ReadonlyMap<string, string> {\r\n  if (cellErrors.size === 0) return cellErrors;\r\n  const liveRowIds = new Set<string>();\r\n  for (let i = 0; i < data.length; i++) liveRowIds.add(getRowId(data[i], i));\r\n  let next: Map<string, string> | null = null;\r\n  for (const key of cellErrors.keys()) {\r\n    if (cellErrorKeyIsLive(key, liveRowIds)) continue;\r\n    if (!next) next = new Map(cellErrors);\r\n    next.delete(key);\r\n  }\r\n  return next ?? cellErrors;\r\n}\r\n\r\n/**\r\n * One-slot memo for {@link deepMergeLabels}: `_syncProps` runs on every consumer render, but the\r\n * `labels` override prop is normally a stable (or absent) reference — re-merging then would hand\r\n * out a fresh `DataGridLabels` object every render, defeating any subscriber's `useMemo`/`useCallback`\r\n * keyed on label identity. Returns the previous merge result when `override` is reference-equal to\r\n * the last call's.\r\n */\r\nexport function memoizedMergeLabels(): (override: DeepPartialLabels | undefined) => DataGridLabels {\r\n  let lastOverride: DeepPartialLabels | undefined;\r\n  let lastResult: DataGridLabels = DEFAULT_LABELS;\r\n  return (override) => {\r\n    if (override === lastOverride) return lastResult;\r\n    lastOverride = override;\r\n    lastResult = deepMergeLabels(DEFAULT_LABELS, override);\r\n    return lastResult;\r\n  };\r\n}\r\n\r\n/** Module-wide monotonic counter backing {@link genFilterId} — no nanoid/uuid dep for a purely-local, never-persisted id. */\r\nlet filterIdCounter = 0;\r\n\r\n/** Generates a stable-for-this-session filter row id; counter-based (not crypto.randomUUID) so it works identically in SSR and tests without a Web Crypto polyfill. */\r\nexport function genFilterId(): string {\r\n  filterIdCounter += 1;\r\n  return `f${filterIdCounter}`;\r\n}\r\n\r\n/**\r\n * Backfills a missing `filterId` on any {@link FilterSpec} that lacks one (backward-compat: consumers\r\n * may still pass plain `{columnId, operator, value}` via the controlled `filterState` prop) while\r\n * preserving the id of every filter that already has one — so identity stays stable across recomputes\r\n * for filters the consumer/store already assigned one to, and list rendering keyed on `filterId` never\r\n * jitters just because `_syncProps` ran again with the same logical filters.\r\n */\r\nexport function withFilterIds(filters: readonly FilterSpec[]): FilterSpec[] {\r\n  return filters.map((f) => (f.filterId ? f : { ...f, filterId: genFilterId() }));\r\n}\r\n\r\n/**\r\n * Raw cell value for the sort/search accessor. Mirrors `getCellValue`'s precedence but returns\r\n * `undefined` (instead of throwing) for a column with neither accessor, since the view pipeline\r\n * runs over every column including display-only ones.\r\n */\r\nfunction rawCellValue(column: AnyColumnDef, row: unknown): unknown {\r\n  if (column.accessorFn) return column.accessorFn(row);\r\n  if (column.accessorKey) return (row as Record<string, unknown>)[column.accessorKey];\r\n  return undefined;\r\n}\r\n\r\n/**\r\n * The `CellAccessor` the view pipeline reads through. A column's `sortCompare` and (when\r\n * `cellTypes` is wired) its cell type's `compare` supply the sort comparators, so a\r\n * `number`/`date`/`select` column sorts by its own semantics instead of collating `String(value)` —\r\n * the collator segments digit runs, so it orders 1.5 before 1.25 and all negatives backwards.\r\n * Omitting `cellTypes` keeps the pure-text behaviour (search has no comparator).\r\n */\r\nexport function textAccessorFor(\r\n  columns: readonly AnyColumnDef[],\r\n  data: readonly unknown[],\r\n  cellTypes?: Record<string, CellType>,\r\n): CellAccessor {\r\n  const byId = new Map(columns.map((c) => [c.id, c] as const));\r\n  const accessor: CellAccessor = {\r\n    getText(rowIndex, columnId) {\r\n      const column = byId.get(columnId);\r\n      const row = data[rowIndex];\r\n      if (!column || row === undefined) return \"\";\r\n      const value = rawCellValue(column, row);\r\n      return value == null ? \"\" : String(value);\r\n    },\r\n    getValue(rowIndex, columnId) {\r\n      const column = byId.get(columnId);\r\n      const row = data[rowIndex];\r\n      if (!column || row === undefined) return undefined;\r\n      return rawCellValue(column, row);\r\n    },\r\n    filterMatch(columnId) {\r\n      return byId.get(columnId)?.filterMatch;\r\n    },\r\n  };\r\n\r\n  // A column opts into a custom row comparator through its own `sortCompare`, or through its\r\n  // declared cell type's `compare` — only a DECLARED type opts in, because an undeclared column\r\n  // falls back to \"text\", whose `localeCompare` is strictly worse than `defaultCompareText`\r\n  // (numeric-aware and case-insensitive) and throws outright on a non-string value. Without either\r\n  // comparator the whole accessor stays on the default text path (no allocation of `compare`).\r\n  if (!cellTypes && !columns.some((c) => c.sortCompare)) return accessor;\r\n\r\n  const declaredType = (column: AnyColumnDef) => (column.type ? cellTypes?.[column.type] : undefined);\r\n\r\n  accessor.compare = (columnId) => {\r\n    const column = byId.get(columnId);\r\n    if (!column) return undefined;\r\n    // Resolution chain: column-level sortCompare > cell-type compare > default text.\r\n    const columnCompare = column.sortCompare;\r\n    if (columnCompare) {\r\n      return (a, b) => {\r\n        const rowA = data[a];\r\n        const rowB = data[b];\r\n        if (rowA === undefined || rowB === undefined) return 0;\r\n        // A zero (or NaN/throwing) result defers the pair to the default text compare — `getText`\r\n        // is the same raw-value read the comparator itself runs through, so the tie-break is\r\n        // consistent with the pipeline's default ordering.\r\n        try {\r\n          const result = columnCompare(rowA, rowB);\r\n          if (Number.isFinite(result) && result !== 0) return result;\r\n        } catch {\r\n          // fall through to the default text compare below\r\n        }\r\n        return defaultCompareText(accessor.getText(a, columnId), accessor.getText(b, columnId));\r\n      };\r\n    }\r\n    const compare = declaredType(column)?.compare;\r\n    if (!compare) return undefined;\r\n    return (a, b) => {\r\n      const rowA = data[a];\r\n      const rowB = data[b];\r\n      if (rowA === undefined || rowB === undefined) return 0;\r\n      const valueA = rawCellValue(column, rowA);\r\n      const valueB = rawCellValue(column, rowB);\r\n      // A value that does not match the column's declared type (a number in a `text` column, a\r\n      // string in a `number` one) makes a comparator throw or return NaN. Neither may take the grid\r\n      // down or make the order non-transitive, so fall back to the text ordering for that pair.\r\n      try {\r\n        const result = compare(valueA, valueB);\r\n        if (Number.isFinite(result)) return result;\r\n      } catch {\r\n        // fall through to the text comparator below\r\n      }\r\n      return defaultCompareText(valueA == null ? \"\" : String(valueA), valueB == null ? \"\" : String(valueB));\r\n    };\r\n  };\r\n\r\n  accessor.isEmpty = (rowIndex, columnId) => {\r\n    const column = byId.get(columnId);\r\n    const row = data[rowIndex];\r\n    if (!column || row === undefined) return true;\r\n    const value = rawCellValue(column, row);\r\n    // `isEmpty` is per-type semantics (number treats 0 as non-empty); fall back to the text rule.\r\n    const cellType = declaredType(column);\r\n    return cellType ? cellType.isEmpty(value) : value == null || String(value) === \"\";\r\n  };\r\n\r\n  return accessor;\r\n}\r\n\r\n/** Column ids with `hidden: true` in their def; seeds `hiddenColumns` at creation and re-seeds it when a new `columns` array is passed. */\r\nexport function defHiddenColumnIds(columns: readonly AnyColumnDef[]): string[] {\r\n  return columns.filter((c) => c.hidden).map((c) => c.id);\r\n}\r\n\r\n/**\r\n * Visible columns in display order (pinned-left, unpinned, pinned-right). Visibility reads\r\n * `hiddenColumns` alone — a def-level `hidden: true` is honored only through that set's seeding —\r\n * so `setColumnHidden(id, false)` re-shows a def-hidden column, and the def's flag re-applies only\r\n * when a new `columns` array re-seeds it.\r\n */\r\nexport function computeVisibleColumns(\r\n  columns: readonly AnyColumnDef[],\r\n  columnOrder: string[] | null,\r\n  hiddenColumns: readonly string[],\r\n): readonly AnyColumnDef[] {\r\n  const byId = new Map(columns.map((c) => [c.id, c] as const));\r\n  const orderedIds = columnOrder ?? columns.map((c) => c.id);\r\n  const hidden = new Set(hiddenColumns);\r\n  const ordered = orderedIds\r\n    .map((id) => byId.get(id))\r\n    .filter((c): c is AnyColumnDef => c != null && !hidden.has(c.id));\r\n  const left = ordered.filter((c) => c.pin === \"left\");\r\n  const right = ordered.filter((c) => c.pin === \"right\");\r\n  const middle = ordered.filter((c) => c.pin !== \"left\" && c.pin !== \"right\");\r\n  return [...left, ...middle, ...right];\r\n}\r\n\r\n/** Builds the `ColumnLayout` snapshot passed to `onColumnLayoutChange` — see that prop's doc comment. */\r\nexport function computeColumnLayout(\r\n  columns: readonly AnyColumnDef[],\r\n  columnOrder: string[] | null,\r\n  columnWidths: Record<string, number>,\r\n  hiddenColumns: readonly string[],\r\n): ColumnLayout {\r\n  const pins: Record<string, \"left\" | \"right\"> = {};\r\n  for (const c of columns) {\r\n    if (c.pin === \"left\" || c.pin === \"right\") pins[c.id] = c.pin;\r\n  }\r\n  return {\r\n    widths: { ...columnWidths },\r\n    order: columnOrder ?? columns.map((c) => c.id),\r\n    pins,\r\n    hidden: [...hiddenColumns],\r\n  };\r\n}\r\n\r\n/**\r\n * Seeds `columnWidths`/`columnOrder`/`hiddenColumns`/per-column `pin` from `defaultColumnLayout` at\r\n * store-creation time only (never re-applied — see the sync prop's doc comment). Partial layouts\r\n * are fine: any field omitted keeps today's def-derived default.\r\n */\r\nexport function applyDefaultColumnLayout(\r\n  columns: readonly AnyColumnDef[],\r\n  hiddenColumns: string[],\r\n  layout: ColumnLayout | undefined,\r\n): { columns: readonly AnyColumnDef[]; columnWidths: Record<string, number>; columnOrder: string[] | null; hiddenColumns: string[] } {\r\n  if (!layout) return { columns, columnWidths: {}, columnOrder: null, hiddenColumns };\r\n  const nextColumns = layout.pins\r\n    ? columns.map((c) => (layout.pins[c.id] ? { ...c, pin: layout.pins[c.id] } : c))\r\n    : columns;\r\n  return {\r\n    columns: nextColumns,\r\n    columnWidths: layout.widths ? { ...layout.widths } : {},\r\n    columnOrder: layout.order ?? null,\r\n    hiddenColumns: layout.hidden ?? hiddenColumns,\r\n  };\r\n}\r\n\r\n/** True when `a`/`b` have the same length and every element is `===`-equal at each position. */\r\nexport function sameElements(a: readonly number[], b: readonly number[]): boolean {\r\n  if (a.length !== b.length) return false;\r\n  for (let i = 0; i < a.length; i++) {\r\n    if (a[i] !== b[i]) return false;\r\n  }\r\n  return true;\r\n}\r\n\r\n/**\r\n * Computes the row-index view (filter + sort). `prevViewIndex`, when given, is returned instead of\r\n * the freshly built array when the two are element-wise identical (e.g. re-applying an already-\r\n * applied sort) — Zustand subscribers keyed on `viewIndex` identity then skip re-rendering on a\r\n * no-op update. Every call site funnels through this one wrapper so the reuse applies everywhere.\r\n */\r\nexport function computeViewIndex(\r\n  data: readonly unknown[],\r\n  allColumns: readonly AnyColumnDef[],\r\n  sortState: SortSpec[],\r\n  filterState: FilterSpec[],\r\n  joinOperator: FilterJoinOperator,\r\n  prevViewIndex?: readonly number[],\r\n  cellTypes?: Record<string, CellType>,\r\n): number[] {\r\n  const accessor = textAccessorFor(allColumns, data, cellTypes);\r\n  // Quick-search never narrows viewIndex — it only highlights/navigates.\r\n  const next = buildViewIndex(data.length, accessor, { sorts: sortState, filters: filterState, joinOperator });\r\n  if (prevViewIndex && sameElements(next, prevViewIndex)) return prevViewIndex as number[];\r\n  return next;\r\n}\r\n\r\n/**\r\n * Fraction of incremental view-index updates that re-derive the full {@link buildViewIndex} and\r\n * compare, in dev only. Same guard style as the rowId map's `diffRowIndex`: sampled, so a streaming\r\n * feed still runs at streaming speed while a systematic divergence surfaces within a few ticks.\r\n */\r\nexport const INCREMENTAL_VIEW_ASSERT_RATE = 0.05;\r\n\r\nlet incrementalAssertRate = INCREMENTAL_VIEW_ASSERT_RATE;\r\n\r\n/** @internal test seam: forces the sampled dev assertion always on (1) or off (0). */\r\nexport function setIncrementalAssertRate(rate: number): void {\r\n  incrementalAssertRate = rate;\r\n}\r\n\r\n/**\r\n * Maintains `prevViewIndex` across a value patch that touched `touchedRows` (data indices) without\r\n * re-sorting every row: each touched row is pulled out, re-tested for filter membership, and\r\n * binary-searched back in under the comparator chain {@link buildViewIndex} uses. Returns `null`\r\n * whenever the incremental path cannot prove it matches the rebuild — the caller then falls back to\r\n * {@link computeViewIndex}, which stays the untouched reference implementation.\r\n */\r\nexport function incrementalViewIndex(\r\n  data: readonly unknown[],\r\n  allColumns: readonly AnyColumnDef[],\r\n  sortState: SortSpec[],\r\n  filterState: FilterSpec[],\r\n  joinOperator: FilterJoinOperator,\r\n  prevViewIndex: readonly number[],\r\n  touchedRows: readonly number[],\r\n  cellTypes?: Record<string, CellType>,\r\n): number[] | null {\r\n  // A view longer than the data can only come from a row removal the caller failed to declare.\r\n  if (prevViewIndex.length > data.length) return null;\r\n  const accessor = textAccessorFor(allColumns, data, cellTypes);\r\n  const opts = { sorts: sortState, filters: filterState, joinOperator };\r\n  const result = updateViewIndex(prevViewIndex, touchedRows, accessor, opts);\r\n  if (result.viewIndex === null) return null;\r\n  if (isDev() && Math.random() < incrementalAssertRate) {\r\n    const reference = buildViewIndex(data.length, accessor, opts);\r\n    if (!sameElements(result.viewIndex, reference)) {\r\n      console.error(\r\n        \"[data-grid] incremental view index diverged from the full rebuild; using the rebuild. Please report this with the sort/filter state.\",\r\n      );\r\n      return reference;\r\n    }\r\n  }\r\n  return result.viewIndex;\r\n}\r\n\r\n/** Max search hits collected before {@link findSearchMatches} early-exits; bounds worst-case work at 100k+ rows. */\r\nexport const MAX_SEARCH_MATCHES = 1000;\r\n\r\n/** Groups `matches` by view row into column-index Sets — see {@link DataGridStoreState.searchMatchRows}. */\r\nexport function buildSearchMatchRows(matches: readonly SearchMatch[], visibleColumns: readonly AnyColumnDef[]): ReadonlyMap<number, ReadonlySet<number>> {\r\n  if (matches.length === 0) return EMPTY_SEARCH_MATCH_ROWS;\r\n  const colIndexById = new Map(visibleColumns.map((c, i) => [c.id, i] as const));\r\n  const rows = new Map<number, Set<number>>();\r\n  for (const match of matches) {\r\n    const col = colIndexById.get(match.columnId);\r\n    if (col === undefined) continue;\r\n    let cols = rows.get(match.row);\r\n    if (!cols) rows.set(match.row, (cols = new Set()));\r\n    cols.add(col);\r\n  }\r\n  return rows;\r\n}\r\n\r\n/** Recomputes `searchMatches`/`searchMatchSet`/`searchMatchRows`/`searchMatchesCapped` for the given `searchText` against the current view; empty state when `searchText` is blank. */\r\nexport function computeSearchMatches(\r\n  data: readonly unknown[],\r\n  columns: readonly AnyColumnDef[],\r\n  viewIndex: number[],\r\n  visibleColumns: readonly AnyColumnDef[],\r\n  searchText: string,\r\n): Pick<DataGridStoreState, \"searchMatches\" | \"searchMatchSet\" | \"searchMatchRows\" | \"searchMatchesCapped\"> {\r\n  if (searchText.trim() === \"\") {\r\n    return { searchMatches: [], searchMatchSet: EMPTY_SEARCH_MATCH_SET, searchMatchRows: EMPTY_SEARCH_MATCH_ROWS, searchMatchesCapped: false };\r\n  }\r\n  const dataAccessor = textAccessorFor(columns, data);\r\n  // viewRow is always < viewIndex.length (findSearchMatches iterates 0..viewIndex.length)\r\n  const viewAccessor: CellAccessor = {\r\n    getText: (viewRow, columnId) => dataAccessor.getText(viewIndex[viewRow]!, columnId),\r\n    getValue: (viewRow, columnId) => dataAccessor.getValue?.(viewIndex[viewRow]!, columnId),\r\n    filterMatch: (columnId) => dataAccessor.filterMatch?.(columnId),\r\n  };\r\n  const searchMatches = findSearchMatches(\r\n    viewIndex.length,\r\n    viewAccessor,\r\n    searchText,\r\n    visibleColumns.map((c) => c.id),\r\n    MAX_SEARCH_MATCHES,\r\n  );\r\n  const searchMatchSet = new Set(searchMatches.map((m) => searchMatchKey(m.row, m.columnId)));\r\n  const searchMatchRows = buildSearchMatchRows(searchMatches, visibleColumns);\r\n  return { searchMatches, searchMatchSet, searchMatchRows, searchMatchesCapped: searchMatches.length >= MAX_SEARCH_MATCHES };\r\n}\r\n\r\n/** Cycles a column's sort direction: asc -> desc -> none (removed). */\r\nexport function nextDirection(current: SortSpec[\"direction\"] | undefined): SortSpec[\"direction\"] | null {\r\n  if (current === undefined) return \"asc\";\r\n  if (current === \"asc\") return \"desc\";\r\n  return null;\r\n}\r\n\r\n/**\r\n * Additive (shift-click) toggle: cycles the column's direction in place when it's\r\n * already part of the multi-sort, preserving its priority; only newly-sorted\r\n * columns are appended. Matches Excel/AG Grid multi-sort semantics.\r\n */\r\nexport function toggleSortAdditive(sortState: SortSpec[], columnId: string, direction: SortSpec[\"direction\"] | null): SortSpec[] {\r\n  const index = sortState.findIndex((sort) => sort.columnId === columnId);\r\n  if (index === -1) {\r\n    return direction === null ? sortState : [...sortState, { columnId, direction }];\r\n  }\r\n  if (direction === null) return sortState.filter((sort) => sort.columnId !== columnId);\r\n  return sortState.map((sort, i) => (i === index ? { columnId, direction } : sort));\r\n}\r\n\r\n/** Resolves a column's readOnly flag/predicate against its data row. A column with neither `setValue` nor `accessorKey` (accessorFn-only, display computed) has no write path, so it's readOnly by construction — fill/paste/delete skip it instead of crashing in setCellValue. */\r\nexport function isColumnReadOnly(column: AnyColumnDef, row: unknown): boolean {\r\n  if (!column.setValue && !column.accessorKey) return true;\r\n  return typeof column.readOnly === \"function\" ? column.readOnly(row) : Boolean(column.readOnly);\r\n}\r\n\r\n/** Clamps `n` into `[0, max]`; `max < 0` (empty view) clamps to 0. */\r\nexport function clampIndex(n: number, max: number): number {\r\n  return Math.max(0, Math.min(n, Math.max(0, max)));\r\n}\r\n\r\n/** Resolves the optional selection-config sync props to their defaulted (all-true) form. */\r\nexport function resolveSelectionConfig(props: InternalSyncProps): Pick<\r\n  DataGridStoreState,\r\n  | \"rowMarkers\"\r\n  | \"enableRowSelection\"\r\n  | \"enableColumnSelection\"\r\n  | \"enableRangeSelection\"\r\n  | \"enableMultiRange\"\r\n  | \"enableColumnResize\"\r\n  | \"enableColumnReorder\"\r\n  | \"enableRowReorder\"\r\n  | \"enableColumnPinning\"\r\n  | \"headerClickBehavior\"\r\n> {\r\n  return {\r\n    rowMarkers: props.rowMarkers ?? \"none\",\r\n    enableRowSelection: props.enableRowSelection ?? true,\r\n    enableColumnSelection: props.enableColumnSelection ?? true,\r\n    enableRangeSelection: props.enableRangeSelection ?? true,\r\n    enableMultiRange: props.enableMultiRange ?? true,\r\n    enableColumnResize: props.enableColumnResize ?? true,\r\n    enableColumnReorder: props.enableColumnReorder ?? true,\r\n    enableRowReorder: props.enableRowReorder ?? true,\r\n    enableColumnPinning: props.enableColumnPinning ?? true,\r\n    headerClickBehavior: props.headerClickBehavior ?? \"select\",\r\n  };\r\n}\r\n\r\n/** A column's own `resizable`/`reorderable`/`pinnable` flag, defaulted true when absent. */\r\nexport function columnFlag(column: AnyColumnDef | undefined, key: \"resizable\" | \"reorderable\" | \"pinnable\"): boolean {\r\n  return column?.[key] ?? true;\r\n}\r\n\r\n/** Which pin zone a column belongs to, for reorder zone-containment (pinned columns reorder only within their pin zone). */\r\nexport function pinZone(column: AnyColumnDef | undefined): \"left\" | \"right\" | \"middle\" {\r\n  return column?.pin === \"left\" ? \"left\" : column?.pin === \"right\" ? \"right\" : \"middle\";\r\n}\r\n\r\n/**\r\n * Computes the next `columnOrder` id array moving `id` to sit immediately before/after `targetId`.\r\n * Returns the input `orderedIds` unchanged (by reference) when the move is a no-op or crosses pin\r\n * zones (pinned columns only reorder within their own zone — a cross-zone drop target is ignored).\r\n */\r\nexport function reorderColumnIds(\r\n  orderedIds: readonly string[],\r\n  columns: readonly AnyColumnDef[],\r\n  id: string,\r\n  targetId: string,\r\n  position: \"before\" | \"after\",\r\n): string[] {\r\n  if (id === targetId) return orderedIds as string[];\r\n  const byId = new Map(columns.map((c) => [c.id, c] as const));\r\n  if (pinZone(byId.get(id)) !== pinZone(byId.get(targetId))) return orderedIds as string[];\r\n\r\n  const withoutId = orderedIds.filter((c) => c !== id);\r\n  const targetIndex = withoutId.indexOf(targetId);\r\n  if (targetIndex === -1) return orderedIds as string[];\r\n  const insertAt = position === \"before\" ? targetIndex : targetIndex + 1;\r\n  return [...withoutId.slice(0, insertAt), id, ...withoutId.slice(insertAt)];\r\n}\r\n\r\n/**\r\n * Reads raw cell VALUES (not clipboard text — no `toText`/`processCellForClipboard`) over `rect`\r\n * from `s`, view-row-major. Shared by `onSelectionChange`'s `details.getValues()` and\r\n * {@link readSelectionValues}; mirrors clipboard's `serializeRect`/`serializeRowSlice` read path\r\n * (same `getCellValue` per cell) but stops short of text serialization. A hole (unresolvable\r\n * column or data row — e.g. a lazy-loading skeleton row) reads as `undefined`, matching how\r\n * `getCellValue` itself treats a missing row.\r\n */\r\nexport function getRangeValues(s: DataGridStoreState, rect: GridRect): unknown[][] {\r\n  const out: unknown[][] = [];\r\n  for (let viewRow = rect.y; viewRow < rect.y + rect.height; viewRow++) {\r\n    const dataRowIndex = s.viewIndex[viewRow];\r\n    const row = dataRowIndex === undefined ? undefined : s.data[dataRowIndex];\r\n    const cells: unknown[] = [];\r\n    for (let col = rect.x; col < rect.x + rect.width; col++) {\r\n      const column = s.visibleColumns[col];\r\n      cells.push(!column || row === undefined ? undefined : getCellValue<unknown, typeof column>(row, column));\r\n    }\r\n    out.push(cells);\r\n  }\r\n  return out;\r\n}\r\n\r\n/**\r\n * Row ids for every selected row, in view order, across BOTH selection channels: the row channel\r\n * (marker checkboxes) and any cell range, including the ctrl-click `rangeStack` — the same union\r\n * `useDataGridIsRowSelected` reports per row, so a bulk action agrees with the checkmarks on screen.\r\n */\r\nexport function getSelectedRowIds(s: DataGridStoreState): string[] {\r\n  const out: string[] = [];\r\n  const current = s.selection.current;\r\n  const covers = (viewRow: number) => {\r\n    if (s.selection.rows.hasIndex(viewRow)) return true;\r\n    if (!current) return false;\r\n    const inRect = (r: GridRect) => viewRow >= r.y && viewRow < r.y + r.height;\r\n    return inRect(current.range) || current.rangeStack.some(inRect);\r\n  };\r\n  for (let viewRow = 0; viewRow < s.viewIndex.length; viewRow++) {\r\n    if (!covers(viewRow)) continue;\r\n    const dataRowIndex = s.viewIndex[viewRow];\r\n    const row = dataRowIndex === undefined ? undefined : s.data[dataRowIndex];\r\n    if (row !== undefined) out.push(s.getRowId(row, dataRowIndex!));\r\n  }\r\n  return out;\r\n}\r\n\r\n/** Builds the `details` argument passed to `onSelectionChange`; see {@link SelectionChangeDetails}. */\r\nexport function makeSelectionChangeDetails(getSnapshot: () => DataGridStoreState): SelectionChangeDetails {\r\n  return {\r\n    getValues() {\r\n      const s = getSnapshot();\r\n      const range = s.selection.current?.range;\r\n      return range ? getRangeValues(s, range) : [];\r\n    },\r\n    getRowIds() {\r\n      return getSelectedRowIds(getSnapshot());\r\n    },\r\n  };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/store/compute.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/store/create-store.ts",
      "content": "import { createStore, type StoreApi } from \"zustand/vanilla\";\r\nimport type { CellCoord, CellType, DataOp } from \"../types\";\r\nimport { DEFAULT_KEYMAP } from \"../keyboard\";\r\nimport { CompactSelection } from \"../selection/compact-selection\";\r\nimport { getCellValue } from \"../columns/column-helpers\";\r\nimport { clampColumnWidth } from \"../columns/resolve-column-width\";\r\nimport { cellTypes as defaultCellTypes } from \"../cell-types/cell-types\";\r\nimport {\r\n  emptySelection,\r\n  extendTo,\r\n  extendSelection,\r\n  offsetSelectionForRows,\r\n  pushRange,\r\n  rectFromCorners,\r\n  reorderSelectionForRow,\r\n  rowReorderMap,\r\n  selectAllProgression,\r\n  selectCell as selectCellPure,\r\n  selectColumn as selectColumnPure,\r\n  selectionRects,\r\n  selectRow as selectRowPure,\r\n  type ExtendDirection,\r\n} from \"../selection\";\r\nimport {\r\n  applyDefaultColumnLayout,\r\n  cellErrorKey,\r\n  clampIndex,\r\n  clearErrorsForOps,\r\n  columnFlag,\r\n  computeColumnLayout,\r\n  computeSearchMatches,\r\n  computeVisibleColumns,\r\n  computeViewIndex,\r\n  defHiddenColumnIds,\r\n  incrementalViewIndex,\r\n  isColumnReadOnly,\r\n  memoizedMergeLabels,\r\n  mergeCellErrors,\r\n  nextDirection,\r\n  pruneCellErrors,\r\n  reorderColumnIds,\r\n  resolveSelectionConfig,\r\n  sameElements,\r\n  toggleSortAdditive,\r\n  withFilterIds,\r\n  EMPTY_CELL_ERRORS,\r\n  EMPTY_FLASHING_CELLS,\r\n  EMPTY_OVERLAY_PLUGINS,\r\n  EMPTY_ROW_BANDS,\r\n} from \"./compute\";\r\nimport {\r\n  computeCellPatchBatch,\r\n  computeCommit,\r\n  computeDeleteBatch,\r\n  computeDuplicateBatch,\r\n  computeInsertRowsBatch,\r\n  computeRowEditsBatch,\r\n  patchesNeedAsyncCheck,\r\n  prevalidatePatches,\r\n  type PrevalidatedPatches,\r\n  resolveEditTarget,\r\n  warnDev,\r\n} from \"./commit\";\r\nimport { INCREMENTAL_PATCH_LIMIT } from \"../sort-filter\";\r\nimport { createRowIndexCache, resolveReorder, touchesViewInputs } from \"./row-index\";\r\nimport type { AnyColumnDef, CellPatch, DataGridStoreState, InternalSyncProps, SyncInputs, UpdateCellsVerdict } from \"./types\";\r\nimport { syncInputsEqual } from \"./types\";\r\n\r\n/**\r\n * Creates one grid's vanilla store instance. Module-private: never export this\r\n * factory's result or a bound hook from this module — only the hooks below.\r\n */\r\nexport function createDataGridStore(init: InternalSyncProps): StoreApi<DataGridStoreState> {\r\n  const defHidden = defHiddenColumnIds(init.columns);\r\n  const seeded = applyDefaultColumnLayout(init.columns, defHidden, init.defaultColumnLayout);\r\n  const visibleColumns = computeVisibleColumns(seeded.columns, seeded.columnOrder, seeded.hiddenColumns);\r\n  const mergeLabels = memoizedMergeLabels();\r\n  // `data` wins when both are given (React value/defaultValue precedence; checkDevGuardrails warns).\r\n  // `dataControlled` latches this decision for the store's lifetime — later renders can't flip\r\n  // uncontrolled-to-controlled or back, matching `<input>`'s own value/defaultValue semantics.\r\n  const dataControlled = init.data !== undefined;\r\n  const initData: readonly unknown[] = init.data ?? init.defaultData ?? [];\r\n  // Set on every _syncProps call (including the implicit init-time seed below) so the very first\r\n  // real sync has something to compare against instead of special-casing \"no previous sync yet\".\r\n  const initFilterState = withFilterIds(init.filterState ?? []);\r\n  const initJoinOperator = init.joinOperator ?? \"and\";\r\n  // storage widens each per-column CellType<TData, TValue, TOptions> to the untyped registry shape; narrowed back per column at lookup (cell.tsx/store's column-op helpers).\r\n  const initCellTypes =\r\n    (init.cellTypes as unknown as Record<string, CellType> | undefined) ?? (defaultCellTypes as unknown as Record<string, CellType>);\r\n  let lastSyncInputs: SyncInputs = {\r\n    data: initData,\r\n    cellTypes: initCellTypes,\r\n    columns: seeded.columns,\r\n    sortState: init.sortState ?? [],\r\n    filterState: initFilterState,\r\n    joinOperator: initJoinOperator,\r\n    searchText: init.searchText ?? \"\",\r\n    columnOrder: seeded.columnOrder,\r\n    hiddenColumns: seeded.hiddenColumns,\r\n  };\r\n  // The raw `columns` prop reference last seen by _syncProps — separate from `s.columns`, which may\r\n  // carry runtime column-pin mutations (setColumnPin / the initial-layout seed). Only a genuine\r\n  // change of the consumer's own prop should reset `s.columns`; re-syncing with the SAME prop\r\n  // reference (e.g. an unrelated parent re-render) must not silently discard those pin mutations.\r\n  let lastPropsColumns: readonly AnyColumnDef[] = init.columns;\r\n  // `rowId -> dataIndex` for updateCells. Every path that inserts, deletes, duplicates, or replaces\r\n  // rows invalidates it; a pure value patch never moves a row, so a streaming tick keeps it warm.\r\n  const rowIndexCache = createRowIndexCache();\r\n  /**\r\n   * The last `data` array this store handed to `onDataChange`. A controlled consumer that stores it\r\n   * and re-renders feeds that same array back through `_syncProps`; recognizing it as our own echo\r\n   * is what keeps `computeViewIndex` off the controlled streaming path (design spec §3.3 — without\r\n   * this the 56 ms cliff returns for every controlled consumer).\r\n   */\r\n   let lastEmittedData: readonly unknown[] | null = null;\r\n   /** Per-key auto-clear timers for `flashCells`; a re-flash of a live key clears its old timer so the pulse restarts. */\r\n   const flashTimers = new Map<string, ReturnType<typeof setTimeout>>();\r\n  /**\r\n   * Data indices whose values changed since `viewIndex` was last rebuilt, so `reconcileView` and the\r\n   * next `\"immediate\"` batch can take the same incremental path instead of a full re-sort. `null`\r\n   * means \"no longer trackable\" — the limit was passed, or a path that moves rows (insert/delete/\r\n   * duplicate/a consumer `data` replacement) or writes values outside `updateCells` ran — and the\r\n   * caller falls back to the full rebuild.\r\n   */\r\n  /**\r\n   * Generation counter for a HELD async `updateCells` batch — the streaming surface's counterpart to\r\n   * the per-hook `useBulkGeneration` the paste/fill/import surfaces use. Bumped by every path that\r\n   * replaces rows or writes values outside the held batch, so a verdict that resolves against a grid\r\n   * that has moved on is dropped instead of landing late on top of newer data.\r\n   */\r\n  let streamGeneration = 0;\r\n  let deferredRows: Set<number> | null = new Set();\r\n  const forgetDeferredRows = () => {\r\n    streamGeneration += 1;\r\n    deferredRows = null;\r\n  };\r\n  const resetDeferredRows = () => {\r\n    streamGeneration += 1;\r\n    deferredRows = new Set();\r\n  };\r\n  /** Records a non-reordering batch's rows, giving up once the set is bigger than the incremental path serves. */\r\n  const rememberDeferredRows = (touchedRows: readonly number[]) => {\r\n    if (!deferredRows) return;\r\n    for (const row of touchedRows) deferredRows.add(row);\r\n    if (deferredRows.size > INCREMENTAL_PATCH_LIMIT) forgetDeferredRows();\r\n  };\r\n  /** This batch's touched rows plus anything an earlier batch left unreconciled; `null` when untrackable. */\r\n  const pendingRows = (touchedRows: readonly number[]): readonly number[] | null => {\r\n    if (!deferredRows) return null;\r\n    if (deferredRows.size === 0) return touchedRows;\r\n    const union = new Set(deferredRows);\r\n    for (const row of touchedRows) union.add(row);\r\n    return Array.from(union);\r\n  };\r\n  /**\r\n   * The next `viewIndex` for a reorder: incremental when `pending` is known and provably equivalent\r\n   * to the rebuild, the full rebuild otherwise. Returns the PREVIOUS array identity when no row\r\n   * moved, so a tick that changes no position re-renders nothing downstream of `viewIndex`.\r\n   */\r\n  const resolveViewIndex = (\r\n    s: DataGridStoreState,\r\n    data: readonly unknown[],\r\n    pending: readonly number[] | null,\r\n  ): number[] => {\r\n    const incremental = pending\r\n      ? incrementalViewIndex(data, s.columns, s.sortState, s.filterState, s.joinOperator, s.viewIndex, pending, s.cellTypes)\r\n      : null;\r\n    if (incremental) return sameElements(incremental, s.viewIndex) ? s.viewIndex : incremental;\r\n    return computeViewIndex(data, s.columns, s.sortState, s.filterState, s.joinOperator, s.viewIndex, s.cellTypes);\r\n  };\r\n  /**\r\n   * The view bookkeeping every direct write path (edit/paste/fill/delete) owes after committing\r\n   * `nextData`, mirroring what `updateCells` does for a `\"defer\"` batch: `viewIndex` deliberately\r\n   * stays put (no auto-resort), but `viewStale` has to flip so the re-sort affordance and\r\n   * `reconcileView()` can recover, and `searchMatches` has no deferred contract at all so it is\r\n   * recomputed in place. Both halves no-op unless a search or a view-feeding column is involved.\r\n   */\r\n  const reconcileAfterWrite = (\r\n    s: DataGridStoreState,\r\n    nextData: readonly unknown[],\r\n    ops: readonly DataOp<unknown>[],\r\n  ): Partial<DataGridStoreState> => {\r\n    const written: CellPatch[] = [];\r\n    for (const op of ops) {\r\n      if (op.type !== \"update\" || !op.cells) continue;\r\n      for (const cell of op.cells) written.push({ rowId: op.rowId, columnId: cell.columnId, value: cell.value });\r\n    }\r\n    const viewStale = s.viewStale || touchesViewInputs(written, s.sortState, s.filterState);\r\n    if (s.searchText.trim() === \"\") return viewStale === s.viewStale ? {} : { viewStale };\r\n    return {\r\n      ...(viewStale === s.viewStale ? {} : { viewStale }),\r\n      ...computeSearchMatches(nextData, s.columns, s.viewIndex, s.visibleColumns, s.searchText),\r\n    };\r\n  };\r\n  /**\r\n   * The view bookkeeping a row-shifting op (`insertRows`/`deleteRows`/`duplicateRows`) owes: the\r\n   * row count changed, so `computeViewIndex` always returns a fresh array (the content-equality\r\n   * reuse cannot apply), `searchMatches` is recomputed against it, and `viewStale` is cleared —\r\n   * the full rebuild reconciles whatever a deferred `updateCells` batch left stale, mirroring\r\n   * `setColumnHidden`.\r\n   */\r\n  /** With a sort or filter active, inserted rows land anywhere: follow the active row by identity and collapse the selection onto it. */\r\n  const followActiveRow = (s: DataGridStoreState, nextData: readonly unknown[], viewIndex: readonly number[]): Pick<DataGridStoreState, \"selection\" | \"activeCell\"> => {\r\n    const dataRow = s.activeCell ? s.viewIndex[s.activeCell.row] : undefined;\r\n    const row = dataRow === undefined || s.data[dataRow] === undefined ? -1 : viewIndex.indexOf(nextData.indexOf(s.data[dataRow]));\r\n    if (!s.activeCell || row === -1) return { selection: emptySelection(), activeCell: null };\r\n    const cell = { col: s.activeCell.col, row };\r\n    return { selection: { ...emptySelection(), current: { cell, range: rectFromCorners(cell, cell), rangeStack: [] } }, activeCell: cell };\r\n  };\r\n  const rebuildRowOpView = (s: DataGridStoreState, nextData: readonly unknown[]): Partial<DataGridStoreState> => {\r\n    const viewIndex = computeViewIndex(nextData, s.columns, s.sortState, s.filterState, s.joinOperator, s.viewIndex, s.cellTypes);\r\n    return {\r\n      viewIndex,\r\n      viewStale: false,\r\n      ...computeSearchMatches(nextData, s.columns, viewIndex, s.visibleColumns, s.searchText),\r\n    };\r\n  };\r\n  // cellErrors keys owned by each row's last validateRow verdict, so a re-run clears exactly its\r\n  // own stale messages and never a consumer's setCellErrors entries on other cells of that row.\r\n  // Stale entries for deleted rows are harmless: their keys are already pruned, deleting is a no-op.\r\n  const rowValidationKeys = new Map<string, string[]>();\r\n  /**\r\n   * Runs `validateRow` once per touched row of a committed write gesture, AFTER `clearErrorsForOps`\r\n   * — the row in `nextData` already has every cell of the gesture applied, which is the whole\r\n   * point: the verdict cannot depend on column order inside a paste. Returns the same map identity\r\n   * when the prop is absent or nothing changed.\r\n   */\r\n  const applyRowValidation = (\r\n    s: DataGridStoreState,\r\n    cellErrors: ReadonlyMap<string, string>,\r\n    nextData: readonly unknown[],\r\n    ops: readonly DataOp<unknown>[],\r\n  ): ReadonlyMap<string, string> => {\r\n    const validateRow = s.validateRow;\r\n    if (!validateRow) return cellErrors;\r\n    const touched = new Set<string>();\r\n    for (const op of ops) if (op.type === \"update\") touched.add(op.rowId);\r\n    if (touched.size === 0) return cellErrors;\r\n    const rowIndex = rowIndexCache.resolve(nextData, s.getRowId);\r\n    let next: Map<string, string> | null = null;\r\n    const ensure = () => next ?? (next = new Map(cellErrors));\r\n    for (const rowId of touched) {\r\n      const dataRowIndex = rowIndex.get(rowId);\r\n      if (dataRowIndex === undefined) continue;\r\n      const verdict = validateRow(nextData[dataRowIndex], rowId);\r\n      const prevKeys = rowValidationKeys.get(rowId);\r\n      if (prevKeys) for (const key of prevKeys) if ((next ?? cellErrors).has(key)) ensure().delete(key);\r\n      const entries = verdict ? Object.entries(verdict) : [];\r\n      if (entries.length === 0) {\r\n        rowValidationKeys.delete(rowId);\r\n        continue;\r\n      }\r\n      const keys: string[] = [];\r\n      for (const [columnId, message] of entries) {\r\n        const key = cellErrorKey(rowId, columnId);\r\n        if ((next ?? cellErrors).get(key) !== message) ensure().set(key, message);\r\n        keys.push(key);\r\n      }\r\n      rowValidationKeys.set(rowId, keys);\r\n    }\r\n    return next ?? cellErrors;\r\n  };\r\n  // View-space presence entries pin to a display position a row move invalidates (rowId-native\r\n  // entries track their rows and never trip this): warn once when a row-moving op runs while one\r\n  // is active. The predicate is registered by the `data-grid-presence` add-on; null when absent.\r\n  let warnedPresenceReorder = false;\r\n  const warnViewSpacePresenceStale = (s: DataGridStoreState) => {\r\n    if (warnedPresenceReorder || !s.presenceViewSpaceActive?.()) return;\r\n    warnedPresenceReorder = true;\r\n    warnDev(\"rows moved while a view-space presence entry is active — its highlight position is now stale; use rowId-native presence entries for a live feed\");\r\n  };\r\n  const initViewIndex = computeViewIndex(initData, seeded.columns, init.sortState ?? [], initFilterState, initJoinOperator, undefined, initCellTypes);\r\n  return createStore<DataGridStoreState>((set, get) => ({\r\n    ...init,\r\n    ...resolveSelectionConfig(init),\r\n    // storage widens each per-column CellType<TData, TValue, TOptions> to the untyped registry shape; narrowed back per column at lookup (cell.tsx/store's column-op helpers).\r\n    cellTypes: initCellTypes,\r\n    columns: seeded.columns,\r\n    data: initData,\r\n    dataControlled,\r\n    activeCell: null,\r\n    selection: emptySelection(),\r\n    selectAllStage: null,\r\n    editing: null,\r\n    editingError: null,\r\n    editingRejectionCount: 0,\r\n    cellErrors: EMPTY_CELL_ERRORS,\r\n    flashingCells: EMPTY_FLASHING_CELLS,\r\n    overlayPlugins: init.overlayPlugins ?? EMPTY_OVERLAY_PLUGINS,\r\n    rowBands: init.rowBands ?? EMPTY_ROW_BANDS,\r\n    lastHighlightedRow: null,\r\n    lastHighlightedCol: null,\r\n    columnWidths: seeded.columnWidths,\r\n    columnOrder: seeded.columnOrder,\r\n    hiddenColumns: seeded.hiddenColumns,\r\n    sortState: init.sortState ?? [],\r\n    filterState: initFilterState,\r\n    joinOperator: initJoinOperator,\r\n    searchText: init.searchText ?? \"\",\r\n    sortControlled: init.sortState !== undefined,\r\n    filterControlled: init.filterState !== undefined,\r\n    joinOperatorControlled: init.joinOperator !== undefined,\r\n    searchControlled: init.searchText !== undefined,\r\n    ...computeSearchMatches(initData, seeded.columns, initViewIndex, visibleColumns, init.searchText ?? \"\"),\r\n    viewIndex: initViewIndex,\r\n    viewStale: false,\r\n    visibleColumns,\r\n    scrollToCellImpl: null,\r\n    fillHandlers: null,\r\n    presenceViewSpaceActive: null,\r\n    readOnly: false,\r\n    keymap: DEFAULT_KEYMAP,\r\n    labels: mergeLabels(init.labels),\r\n    actions: {\r\n      setActiveCell(coord) {\r\n        set({ activeCell: coord });\r\n      },\r\n      selectCell(coord) {\r\n        set({ activeCell: coord, selection: selectCellPure(coord) });\r\n      },\r\n      extendTo(coord) {\r\n        set((s) => {\r\n          // enableRangeSelection: false collapses any drag/shift-extend gesture to single-cell active only.\r\n          if (!s.enableRangeSelection) return { selection: selectCellPure(coord), activeCell: coord };\r\n          const selection = extendTo(s.selection, coord);\r\n          // extendTo falls back to a fresh anchor at `coord` when there was no prior selection.\r\n          const activeCell = s.selection.current ? s.activeCell : coord;\r\n          return { selection, activeCell };\r\n        });\r\n      },\r\n      extendSelection(direction, opts) {\r\n        // rowCount is the VIEW row count (viewIndex.length), not data.length, so growth stays\r\n        // within the visible display order under an active filter.\r\n        const { viewIndex, visibleColumns, selection, enableRangeSelection } = get();\r\n        if (!enableRangeSelection) return;\r\n        set({\r\n          selection: extendSelection(selection, direction, {\r\n            toEdge: opts?.toEdge,\r\n            rowCount: viewIndex.length,\r\n            colCount: visibleColumns.length,\r\n          }),\r\n        });\r\n      },\r\n      pushRange(coord) {\r\n        set((s) => {\r\n          // enableMultiRange: false makes ctrl-click behave as a plain click (no stack push).\r\n          if (!s.enableMultiRange) return { selection: selectCellPure(coord), activeCell: coord };\r\n          // pushRange moves the anchor to `coord`; keep activeCell in sync with it.\r\n          return { selection: pushRange(s.selection, coord), activeCell: coord };\r\n        });\r\n      },\r\n      selectRow(index, opts = {}) {\r\n        const { lastHighlightedRow, selection, enableRowSelection, enableMultiRange, enableRangeSelection } = get();\r\n        if (!enableRowSelection) return;\r\n        // enableMultiRange: false demotes ctrl-click to a plain toggle-select of just this row.\r\n        // enableRangeSelection: false demotes shift-click to a plain toggle-select too (no row range).\r\n        const additive = opts.additive && enableMultiRange;\r\n        const extendFromLast = opts.extendFromLast && enableRangeSelection;\r\n        const replaceFromLast = opts.replaceFromLast && enableRangeSelection;\r\n        set({\r\n          selection: selectRowPure(selection, index, {\r\n            additive,\r\n            extendFromLast,\r\n            replaceFromLast,\r\n            from: opts.from ?? lastHighlightedRow ?? undefined,\r\n          }),\r\n          lastHighlightedRow: index,\r\n        });\r\n      },\r\n      selectColumn(index, opts = {}) {\r\n        const { lastHighlightedCol, selection, enableColumnSelection, enableMultiRange, enableRangeSelection } = get();\r\n        if (!enableColumnSelection) return;\r\n        const additive = opts.additive && enableMultiRange;\r\n        const extendFromLast = opts.extendFromLast && enableRangeSelection;\r\n        set({\r\n          selection: selectColumnPure(selection, index, { additive, extendFromLast, from: lastHighlightedCol ?? undefined }),\r\n          lastHighlightedCol: index,\r\n        });\r\n      },\r\n      selectAll() {\r\n        const s = get();\r\n        if (!s.enableRangeSelection) return;\r\n        const active = s.activeCell ?? { col: 0, row: 0 };\r\n        // stage carries over only if the last selectAll's own output (selection/activeCell identity)\r\n        // is still exactly what's in the store now — any intervening action replaced one of them.\r\n        const priorStage =\r\n          s.selectAllStage &&\r\n          s.selectAllStage.selection === s.selection &&\r\n          s.selectAllStage.activeCell === s.activeCell\r\n            ? s.selectAllStage.stage\r\n            : null;\r\n        const isEmptyAt = (coord: CellCoord): boolean => {\r\n          const column = s.visibleColumns[coord.col];\r\n          if (!column) return true;\r\n          const dataRowIndex = s.viewIndex[coord.row];\r\n          const row = dataRowIndex === undefined ? undefined : s.data[dataRowIndex];\r\n          if (row === undefined) return true;\r\n          const cellType = s.cellTypes[column.type ?? \"text\"];\r\n          if (!cellType) return true;\r\n          const value = getCellValue<unknown, typeof column>(row, column);\r\n          return cellType.isEmpty(value);\r\n        };\r\n        const result = selectAllProgression(s.selection, s.viewIndex.length, s.visibleColumns.length, active, isEmptyAt, priorStage);\r\n        set({ selection: result.selection, selectAllStage: { stage: result.stage, selection: result.selection, activeCell: active } });\r\n      },\r\n      setRowSelected(index, checked) {\r\n        const { selection, enableRowSelection } = get();\r\n        if (!enableRowSelection) return;\r\n        const current = CompactSelection.fromArray(selection.rows.toArray());\r\n        const rows = checked ? current.add(index) : current.remove(index);\r\n        // additive membership change only — never touches the primary range/rangeStack/column channel.\r\n        set({ selection: { ...selection, rows } });\r\n      },\r\n      armRowDragAnchor(index) {\r\n        if (!get().enableRowSelection) return;\r\n        set({ lastHighlightedRow: index });\r\n      },\r\n      setAllRowsSelected(checked) {\r\n        const { selection, viewIndex, enableRowSelection } = get();\r\n        if (!enableRowSelection) return;\r\n        const rows = checked ? CompactSelection.fromSingleSelection([0, viewIndex.length]) : CompactSelection.empty();\r\n        set({ selection: { ...selection, rows } });\r\n      },\r\n      clearSelection() {\r\n        set({ selection: emptySelection() });\r\n      },\r\n      setColumnWidth(id, width) {\r\n        const column = get().columns.find((c) => c.id === id);\r\n        const next = column ? clampColumnWidth(column, width) : width;\r\n        set((s) => ({ columnWidths: { ...s.columnWidths, [id]: next } }));\r\n        get().onColumnResizing?.(id, next);\r\n      },\r\n      commitColumnWidth(id, width) {\r\n        const s = get();\r\n        const column = s.columns.find((c) => c.id === id);\r\n        const next = column ? clampColumnWidth(column, width) : width;\r\n        const columnWidths = { ...s.columnWidths, [id]: next };\r\n        set({ columnWidths });\r\n        s.onColumnLayoutChange?.(computeColumnLayout(s.columns, s.columnOrder, columnWidths, s.hiddenColumns));\r\n      },\r\n      resetColumnWidth(id) {\r\n        const s = get();\r\n        if (s.columnWidths[id] === undefined) return;\r\n        const { [id]: _dropped, ...columnWidths } = s.columnWidths;\r\n        set({ columnWidths });\r\n        s.onColumnLayoutChange?.(computeColumnLayout(s.columns, s.columnOrder, columnWidths, s.hiddenColumns));\r\n      },\r\n      setColumnOrder(id, targetId, position) {\r\n        const s = get();\r\n        if (!s.enableColumnReorder) return;\r\n        const byId = new Map(s.columns.map((c) => [c.id, c] as const));\r\n        if (!columnFlag(byId.get(id), \"reorderable\") || !columnFlag(byId.get(targetId), \"reorderable\")) return;\r\n        const currentIds = s.columnOrder ?? s.columns.map((c) => c.id);\r\n        const nextIds = reorderColumnIds(currentIds, s.columns, id, targetId, position);\r\n        if (nextIds === currentIds) return;\r\n        set({ columnOrder: nextIds, visibleColumns: computeVisibleColumns(s.columns, nextIds, s.hiddenColumns) });\r\n        s.onColumnLayoutChange?.(computeColumnLayout(s.columns, nextIds, s.columnWidths, s.hiddenColumns));\r\n      },\r\n      setColumnPin(id, pin) {\r\n        const s = get();\r\n        if (!s.enableColumnPinning) return;\r\n        const column = s.columns.find((c) => c.id === id);\r\n        if (!column || !columnFlag(column, \"pinnable\")) return;\r\n        const nextColumns = s.columns.map((c) => (c.id === id ? { ...c, pin: pin ?? undefined } : c));\r\n        set({\r\n          columns: nextColumns,\r\n          visibleColumns: computeVisibleColumns(nextColumns, s.columnOrder, s.hiddenColumns),\r\n        });\r\n        s.onColumnLayoutChange?.(computeColumnLayout(nextColumns, s.columnOrder, s.columnWidths, s.hiddenColumns));\r\n      },\r\n      setColumnHidden(id, hidden) {\r\n        const s = get();\r\n        const current = new Set(s.hiddenColumns);\r\n        if (hidden) current.add(id);\r\n        else current.delete(id);\r\n        const nextHidden = Array.from(current);\r\n        const nextVisibleColumns = computeVisibleColumns(s.columns, s.columnOrder, nextHidden);\r\n        const viewIndex = computeViewIndex(s.data, s.columns, s.sortState, s.filterState, s.joinOperator, s.viewIndex, s.cellTypes);\r\n        resetDeferredRows();\r\n        set({\r\n          hiddenColumns: nextHidden,\r\n          visibleColumns: nextVisibleColumns,\r\n          viewIndex,\r\n          viewStale: false,\r\n          ...computeSearchMatches(s.data, s.columns, viewIndex, nextVisibleColumns, s.searchText),\r\n        });\r\n        s.onColumnLayoutChange?.(computeColumnLayout(s.columns, s.columnOrder, s.columnWidths, nextHidden));\r\n      },\r\n      toggleSort(columnId, additive) {\r\n        const s = get();\r\n        const existing = s.sortState.find((sort) => sort.columnId === columnId);\r\n        const direction = nextDirection(existing?.direction);\r\n        const sorts = additive\r\n          ? toggleSortAdditive(s.sortState, columnId, direction)\r\n          : direction === null\r\n            ? []\r\n            : [{ columnId, direction }];\r\n        s.onSortChange?.(sorts);\r\n        // controlled: `sortState` prop is the source of truth — _syncProps writes it once the\r\n        // consumer's prop actually changes; a callback-ignoring consumer's grid stays fixed.\r\n        if (s.sortControlled) return;\r\n        set((s2) => {\r\n          const viewIndex = computeViewIndex(s2.data, s2.columns, sorts, s2.filterState, s2.joinOperator, s2.viewIndex, s2.cellTypes);\r\n          resetDeferredRows();\r\n          return {\r\n            sortState: sorts,\r\n            viewIndex,\r\n            viewStale: false,\r\n            ...computeSearchMatches(s2.data, s2.columns, viewIndex, s2.visibleColumns, s2.searchText),\r\n          };\r\n        });\r\n      },\r\n      setSorts(sorts) {\r\n        const s = get();\r\n        s.onSortChange?.(sorts);\r\n        if (s.sortControlled) return;\r\n        set((s2) => {\r\n          const viewIndex = computeViewIndex(s2.data, s2.columns, sorts, s2.filterState, s2.joinOperator, s2.viewIndex, s2.cellTypes);\r\n          resetDeferredRows();\r\n          return {\r\n            sortState: sorts,\r\n            viewIndex,\r\n            viewStale: false,\r\n            ...computeSearchMatches(s2.data, s2.columns, viewIndex, s2.visibleColumns, s2.searchText),\r\n          };\r\n        });\r\n      },\r\n      setFilters(filters) {\r\n        const s = get();\r\n        // backfills a filterId for any row the caller added without one (e.g. hand-built controlled updates).\r\n        const withIds = withFilterIds(filters);\r\n        s.onFilterChange?.(withIds);\r\n        if (s.filterControlled) return;\r\n        set((s2) => {\r\n          const viewIndex = computeViewIndex(s2.data, s2.columns, s2.sortState, withIds, s2.joinOperator, s2.viewIndex, s2.cellTypes);\r\n          resetDeferredRows();\r\n          return {\r\n            filterState: withIds,\r\n            viewIndex,\r\n            viewStale: false,\r\n            ...computeSearchMatches(s2.data, s2.columns, viewIndex, s2.visibleColumns, s2.searchText),\r\n          };\r\n        });\r\n      },\r\n      setJoinOperator(joinOperator) {\r\n        const s = get();\r\n        s.onJoinOperatorChange?.(joinOperator);\r\n        if (s.joinOperatorControlled) return;\r\n        set((s2) => {\r\n          const viewIndex = computeViewIndex(s2.data, s2.columns, s2.sortState, s2.filterState, joinOperator, s2.viewIndex, s2.cellTypes);\r\n          resetDeferredRows();\r\n          return {\r\n            joinOperator,\r\n            viewIndex,\r\n            viewStale: false,\r\n            ...computeSearchMatches(s2.data, s2.columns, viewIndex, s2.visibleColumns, s2.searchText),\r\n          };\r\n        });\r\n      },\r\n      setSearch(text) {\r\n        const s = get();\r\n        s.onSearchTextChange?.(text);\r\n        if (s.searchControlled) return;\r\n        // viewIndex is untouched: quick-search highlights + navigates, it never filters.\r\n        set((s2) => ({ searchText: text, ...computeSearchMatches(s2.data, s2.columns, s2.viewIndex, s2.visibleColumns, text) }));\r\n      },\r\n      startEditing(coord, initialText) {\r\n        const s = get();\r\n        const target = resolveEditTarget(s, coord);\r\n        if (!target || isColumnReadOnly(target.column, target.row)) return;\r\n        set({ editing: { coord, initialText }, editingError: null, editingRejectionCount: 0, activeCell: coord });\r\n      },\r\n      cancelEditing() {\r\n        set({ editing: null, editingError: null });\r\n      },\r\n      commitCellEdit(value, movement, rejection) {\r\n        const s = get();\r\n        const editing = s.editing;\r\n        if (!editing) return;\r\n        const maxRow = Math.max(0, s.viewIndex.length - 1);\r\n        const maxCol = Math.max(0, s.visibleColumns.length - 1);\r\n        const nextActiveCell = movement\r\n          ? {\r\n              col: clampIndex(editing.coord.col + movement.dx, maxCol),\r\n              row: clampIndex(editing.coord.row + movement.dy, maxRow),\r\n            }\r\n          : editing.coord;\r\n\r\n        const result = computeCommit(s, editing.coord, value, rejection);\r\n        // Stale session: while a (possibly async) validation was pending the user moved\r\n        // activeCell off the editing cell (a click-away). The data commit is still the user's own\r\n        // value — but restoring `activeCell`/`selection` from the stale editing coord would drag\r\n        // their cursor back, so it is skipped.\r\n        const stale =\r\n          s.activeCell !== null && (s.activeCell.row !== editing.coord.row || s.activeCell.col !== editing.coord.col);\r\n        const restoreSelection = stale ? {} : { activeCell: nextActiveCell, selection: selectCellPure(nextActiveCell) };\r\n        if (\"error\" in result) {\r\n          set({ editingError: result.error, editingRejectionCount: s.editingRejectionCount + 1 });\r\n          return;\r\n        }\r\n        if (\"noop\" in result) {\r\n          set({\r\n            editing: null,\r\n            editingError: null,\r\n            ...restoreSelection,\r\n            // an `onInvalid: \"warn\"` re-commit of the same value still lands its flag\r\n            ...(result.warnings ? { cellErrors: mergeCellErrors(s.cellErrors, result.warnings) } : {}),\r\n          });\r\n          return;\r\n        }\r\n        rowIndexCache.rebase(result.data);\r\n        forgetDeferredRows();\r\n        lastEmittedData = result.data;\r\n        s.onDataChange?.(result.data, result.change);\r\n        const cellErrors = applyRowValidation(s, clearErrorsForOps(s.cellErrors, result.change.ops), result.data, result.change.ops);\r\n        set({\r\n          data: result.data,\r\n          editing: null,\r\n          editingError: null,\r\n          // warn rejections commit AND flag — merged after the auto-clear + validateRow verdict so the freshest signal wins\r\n          cellErrors: result.warnings ? mergeCellErrors(cellErrors, result.warnings) : cellErrors,\r\n          ...restoreSelection,\r\n          ...reconcileAfterWrite(s, result.data, result.change.ops),\r\n        });\r\n      },\r\n      commitCellValue(coord, value) {\r\n        const s = get();\r\n        const result = computeCommit(s, coord, value);\r\n        if (\"error\" in result) return;\r\n        if (\"noop\" in result) {\r\n          if (result.warnings) set({ cellErrors: mergeCellErrors(s.cellErrors, result.warnings) });\r\n          return;\r\n        }\r\n        rowIndexCache.rebase(result.data);\r\n        forgetDeferredRows();\r\n        lastEmittedData = result.data;\r\n        s.onDataChange?.(result.data, result.change);\r\n        const cellErrors = applyRowValidation(s, clearErrorsForOps(s.cellErrors, result.change.ops), result.data, result.change.ops);\r\n        set({\r\n          data: result.data,\r\n          cellErrors: result.warnings ? mergeCellErrors(cellErrors, result.warnings) : cellErrors,\r\n          ...reconcileAfterWrite(s, result.data, result.change.ops),\r\n        });\r\n      },\r\n      setEditingError(message) {\r\n        const s = get();\r\n        if (!s.editing) return;\r\n        set({ editingError: message, editingRejectionCount: s.editingRejectionCount + 1 });\r\n      },\r\n      setCellErrors(errors) {\r\n        if (errors.length === 0) return;\r\n        set({ cellErrors: mergeCellErrors(get().cellErrors, errors) });\r\n      },\r\n      clearCellErrors(targets) {\r\n        const s = get();\r\n        if (s.cellErrors.size === 0) return;\r\n        if (!targets) {\r\n          set({ cellErrors: EMPTY_CELL_ERRORS });\r\n          return;\r\n        }\r\n        if (targets.length === 0) return;\r\n        const next = new Map(s.cellErrors);\r\n        let changed = false;\r\n        for (const target of targets) {\r\n          if (next.delete(cellErrorKey(target.rowId, target.columnId))) changed = true;\r\n        }\r\n        if (changed) set({ cellErrors: next });\r\n      },\r\n      flashCells(keys, durationMs = 1400) {\r\n        if (keys.length === 0) return;\r\n        const next = new Set(get().flashingCells);\r\n        for (const key of keys) {\r\n          next.add(key);\r\n          const existing = flashTimers.get(key);\r\n          if (existing !== undefined) clearTimeout(existing);\r\n          flashTimers.set(\r\n            key,\r\n            setTimeout(() => {\r\n              flashTimers.delete(key);\r\n              const current = get();\r\n              if (!current.flashingCells.has(key)) return;\r\n              const lifted = new Set(current.flashingCells);\r\n              lifted.delete(key);\r\n              set({ flashingCells: lifted });\r\n            }, durationMs),\r\n          );\r\n        }\r\n        set({ flashingCells: next });\r\n      },\r\n      _pruneFlashingCells(keptViewRows) {\r\n        const s = get();\r\n        if (s.flashingCells.size === 0) return;\r\n        const kept = new Set(keptViewRows);\r\n        const next = new Set(s.flashingCells);\r\n        for (const key of next) {\r\n          // flashCellKey is `${viewRow}:${columnId}` — the view row is always a plain integer up to the first colon.\r\n          const viewRow = Number(key.slice(0, key.indexOf(\":\")));\r\n          if (kept.has(viewRow)) continue;\r\n          next.delete(key);\r\n        }\r\n        if (next.size === s.flashingCells.size) return;\r\n        set({ flashingCells: next });\r\n      },\r\n      deleteSelection() {\r\n        const s = get();\r\n        if (s.readOnly) return;\r\n        const rects = selectionRects(s.selection, s.viewIndex.length, s.visibleColumns.length);\r\n        if (rects.length === 0) return;\r\n\r\n        const resolvedCols = s.visibleColumns.map((column) => {\r\n          const cellType = s.cellTypes[column.type ?? \"text\"];\r\n          return cellType ? { columnId: column.id, value: cellType.clearValue(column.options) } : null;\r\n        });\r\n        const writes: { viewRow: number; columnId: string; value: unknown }[] = [];\r\n        for (const rect of rects) {\r\n          for (let viewRow = rect.y; viewRow < rect.y + rect.height; viewRow++) {\r\n            for (let col = rect.x; col < rect.x + rect.width; col++) {\r\n              const resolved = resolvedCols[col];\r\n              if (!resolved) continue;\r\n              writes.push({ viewRow, columnId: resolved.columnId, value: resolved.value });\r\n            }\r\n          }\r\n        }\r\n\r\n        const batch = computeRowEditsBatch(s, writes);\r\n        if (!batch) return;\r\n        rowIndexCache.rebase(batch.nextData);\r\n        forgetDeferredRows();\r\n        lastEmittedData = batch.nextData;\r\n        s.onDataChange?.(batch.nextData, { source: \"delete\", ops: batch.ops });\r\n        set({\r\n          data: batch.nextData,\r\n          cellErrors: applyRowValidation(s, clearErrorsForOps(s.cellErrors, batch.ops), batch.nextData, batch.ops),\r\n          ...reconcileAfterWrite(s, batch.nextData, batch.ops),\r\n        });\r\n      },\r\n      applyCellUpdates(updates, source) {\r\n        const s = get();\r\n        if (s.readOnly || updates.length === 0) return;\r\n        const batch = computeRowEditsBatch(s, updates);\r\n        if (!batch) return;\r\n        rowIndexCache.rebase(batch.nextData);\r\n        forgetDeferredRows();\r\n        lastEmittedData = batch.nextData;\r\n        s.onDataChange?.(batch.nextData, { source, ops: batch.ops });\r\n        const cellErrors = applyRowValidation(s, clearErrorsForOps(s.cellErrors, batch.ops), batch.nextData, batch.ops);\r\n\r\n        // move selection to cover the touched view rect (paste's anchored-expand result).\r\n        let minRow = Infinity, maxRow = -Infinity, minCol = Infinity, maxCol = -Infinity;\r\n        const idByCol = new Map(s.visibleColumns.map((c, i) => [c.id, i] as const));\r\n        for (const u of updates) {\r\n          const col = idByCol.get(u.columnId);\r\n          if (col === undefined) continue;\r\n          minRow = Math.min(minRow, u.viewRow);\r\n          maxRow = Math.max(maxRow, u.viewRow);\r\n          minCol = Math.min(minCol, col);\r\n          maxCol = Math.max(maxCol, col);\r\n        }\r\n        if (minRow === Infinity) {\r\n          set({ data: batch.nextData, cellErrors, ...reconcileAfterWrite(s, batch.nextData, batch.ops) });\r\n          return;\r\n        }\r\n        const range = rectFromCorners({ col: minCol, row: minRow }, { col: maxCol, row: maxRow });\r\n        set({\r\n          data: batch.nextData,\r\n          cellErrors,\r\n          selection: { ...emptySelection(), current: { cell: { col: minCol, row: minRow }, range, rangeStack: [] } },\r\n          activeCell: { col: minCol, row: minRow },\r\n          ...reconcileAfterWrite(s, batch.nextData, batch.ops),\r\n        });\r\n      },\r\n      updateCells(patches, options) {\r\n        const s = get();\r\n        if (s.readOnly || patches.length === 0) return { applied: 0, skipped: [], pending: false };\r\n        const skipValidation = options?.skipValidation ?? false;\r\n\r\n        // Re-enters with skipValidation once accepted, so the incremental view path only ever sees validated values.\r\n        if (!skipValidation && patchesNeedAsyncCheck(s, patches)) {\r\n          const rowIndex = rowIndexCache.resolve(s.data, s.getRowId);\r\n          const validated = prevalidatePatches(s, patches, rowIndex);\r\n          // Skips report indexes into the caller's `patches`, not into the accepted subset.\r\n          const apply = ({ accepted, acceptedIndexes, rejectedIndexes }: PrevalidatedPatches): UpdateCellsVerdict => {\r\n            const invalid = rejectedIndexes.map((patchIndex) => ({ patchIndex, reason: \"invalid\" as const }));\r\n            if (accepted.length === 0) return { applied: 0, skipped: invalid, pending: false };\r\n            const verdict = get().actions.updateCells(accepted, { ...options, skipValidation: true });\r\n            const remapped = verdict.skipped.map((skip) => ({ ...skip, patchIndex: acceptedIndexes[skip.patchIndex]! }));\r\n            return { ...verdict, skipped: [...invalid, ...remapped].sort((a, b) => a.patchIndex - b.patchIndex) };\r\n          };\r\n          if (validated instanceof Promise) {\r\n            const token = ++streamGeneration;\r\n            void validated.then((accepted) => {\r\n              // A newer updateCells, or any path that moved rows, supersedes this batch silently.\r\n              if (streamGeneration !== token) return;\r\n              void apply(accepted);\r\n            });\r\n            // the held batch's outcome (accepted, dropped, or superseded) is not reportable here\r\n            return { applied: 0, skipped: [], pending: true };\r\n          }\r\n          return apply(validated);\r\n        }\r\n\r\n        const rowIndex = rowIndexCache.resolve(s.data, s.getRowId);\r\n        const batch = computeCellPatchBatch(s, patches, rowIndex, skipValidation);\r\n        if (batch.nextData === null) return { applied: 0, skipped: batch.skipped, pending: false };\r\n        const applied = batch.ops.reduce((n, op) => (op.type === \"update\" ? n + (op.cells?.length ?? 0) : n), 0);\r\n\r\n        let reorder = resolveReorder(options?.reorder, patches, s.sortState, s.filterState);\r\n        // an open editor pins a view coordinate an immediate re-sort would silently remount (the\r\n        // commit path re-reads the shifted viewIndex for the same coord), so the batch defers\r\n        if (reorder === \"immediate\" && s.editing) reorder = \"defer\";\r\n        rowIndexCache.rebase(batch.nextData);\r\n        lastEmittedData = batch.nextData;\r\n        s.onDataChange?.(batch.nextData, { source: options?.source ?? \"stream\", ops: batch.ops });\r\n        // clearErrorsForOps returns the SAME map identity when `s.cellErrors` is empty (the common\r\n        // streaming case), so this costs nothing extra on the hot path this action exists for.\r\n        const cellErrors = applyRowValidation(s, clearErrorsForOps(s.cellErrors, batch.ops), batch.nextData, batch.ops);\r\n\r\n        // The fast path, and the whole point of the action: one set() writing `data` alone. Row\r\n        // identities changed only for touched rows, so their per-row subscriptions re-render and\r\n        // every other row's memo holds. viewIndex/searchMatches/visibleColumns keep their identity,\r\n        // so no comparator downstream of them re-runs.\r\n        if (reorder !== \"immediate\") {\r\n          // Recorded for `\"never\"` too: that mode is a caller ASSERTION that no touched column feeds\r\n          // the view, and a later reconcile has to stay correct even when the assertion was wrong.\r\n          rememberDeferredRows(batch.touchedRows);\r\n          set(\r\n            reorder === \"defer\" && !s.viewStale\r\n              ? { data: batch.nextData, viewStale: true, cellErrors }\r\n              : { data: batch.nextData, cellErrors },\r\n          );\r\n          return { applied, skipped: batch.skipped, pending: false };\r\n        }\r\n        warnViewSpacePresenceStale(s);\r\n        // Incremental maintenance: a full rebuild re-sorts all n rows every tick, and a patch of a\r\n        // few rows can only move those rows. Rows an earlier non-reordering batch left unreconciled\r\n        // move in this pass too, since the rest of the order is what the binary search trusts.\r\n        const viewIndex = resolveViewIndex(s, batch.nextData, pendingRows(batch.touchedRows));\r\n        resetDeferredRows();\r\n        set({\r\n          data: batch.nextData,\r\n          cellErrors,\r\n          viewIndex,\r\n          viewStale: false,\r\n          ...computeSearchMatches(batch.nextData, s.columns, viewIndex, s.visibleColumns, s.searchText),\r\n        });\r\n        return { applied, skipped: batch.skipped, pending: false };\r\n      },\r\n      updateRows(updates, options) {\r\n        if (updates.length === 0) return { applied: 0, skipped: [], pending: false };\r\n        const patches: CellPatch[] = [];\r\n        for (const update of updates) {\r\n          for (const columnId of Object.keys(update.changes)) {\r\n            patches.push({ rowId: update.rowId, columnId, value: update.changes[columnId] });\r\n          }\r\n        }\r\n        return get().actions.updateCells(patches, options);\r\n      },\r\n      reconcileView() {\r\n        const s = get();\r\n        if (!s.viewStale) return;\r\n        // Same incremental path an `\"immediate\"` batch takes, over whatever the deferred batches touched.\r\n        const viewIndex = resolveViewIndex(s, s.data, pendingRows([]));\r\n        resetDeferredRows();\r\n        set({\r\n          viewIndex,\r\n          viewStale: false,\r\n          ...computeSearchMatches(s.data, s.columns, viewIndex, s.visibleColumns, s.searchText),\r\n        });\r\n      },\r\n      insertRow(viewRowIndex, position) {\r\n        // single implementation path: the 1-row case of the batch action (same one undo step).\r\n        get().actions.insertRows(viewRowIndex, 1, position);\r\n      },\r\n      insertRows(viewRowIndex, count, position = \"below\") {\r\n        const s = get();\r\n        if (s.readOnly) return;\r\n        // an open editor pins a view coordinate the shift would silently invalidate (the commit\r\n        // path re-reads the shifted viewIndex for the same coord)\r\n        if (s.editing) {\r\n          warnDev(\"insertRows is a no-op while an edit session is open\");\r\n          return;\r\n        }\r\n        if (!s.createRow) {\r\n          warnDev(\"row insertion is a no-op because no `createRow` prop was provided\");\r\n          return;\r\n        }\r\n        if (count <= 0 || !Number.isInteger(count)) {\r\n          if (count > 0) warnDev(`insertRows: count must be an integer (got ${count})`);\r\n          return;\r\n        }\r\n        const dataRowIndex = clampIndex(\r\n          position === \"above\" ? (s.viewIndex[viewRowIndex] ?? s.data.length) : (s.viewIndex[viewRowIndex] ?? s.data.length - 1) + 1,\r\n          s.data.length,\r\n        );\r\n        const rows: unknown[] = [];\r\n        for (let i = 0; i < count; i++) rows.push(s.createRow(dataRowIndex + i));\r\n        const batch = computeInsertRowsBatch(s, dataRowIndex, rows);\r\n        warnViewSpacePresenceStale(s);\r\n        rowIndexCache.invalidate();\r\n        forgetDeferredRows();\r\n        lastEmittedData = batch.nextData;\r\n        s.onDataChange?.(batch.nextData, { source: \"row-op\", ops: batch.ops });\r\n        const view = rebuildRowOpView(s, batch.nextData);\r\n        if (s.sortState.length > 0 || s.filterState.length > 0) {\r\n          set({ data: batch.nextData, cellErrors: pruneCellErrors(s.cellErrors, batch.nextData, s.getRowId), ...followActiveRow(s, batch.nextData, view.viewIndex!), ...view });\r\n          return;\r\n        }\r\n        // unsorted and unfiltered, the new rows land at this view index; shift selection/activeCell to follow them.\r\n        const viewInsertAt = position === \"above\" ? viewRowIndex : viewRowIndex + 1;\r\n        set({\r\n          data: batch.nextData,\r\n          cellErrors: pruneCellErrors(s.cellErrors, batch.nextData, s.getRowId),\r\n          selection: offsetSelectionForRows(s.selection, viewInsertAt, count),\r\n          activeCell: s.activeCell && s.activeCell.row >= viewInsertAt\r\n            ? { ...s.activeCell, row: s.activeCell.row + count }\r\n            : s.activeCell,\r\n          ...view,\r\n        });\r\n      },\r\n      deleteRows(viewRowIndexes) {\r\n        const s = get();\r\n        if (s.readOnly) return;\r\n        // an open editor pins a view coordinate the shift would silently invalidate (the commit\r\n        // path re-reads the shifted viewIndex for the same coord)\r\n        if (s.editing) {\r\n          warnDev(\"deleteRows is a no-op while an edit session is open\");\r\n          return;\r\n        }\r\n        const dataRowIndexes = viewRowIndexes.map((viewRow) => s.viewIndex[viewRow]).filter((i): i is number => i !== undefined);\r\n        const batch = computeDeleteBatch(s, dataRowIndexes);\r\n        if (!batch) return;\r\n        warnViewSpacePresenceStale(s);\r\n        rowIndexCache.invalidate();\r\n        forgetDeferredRows();\r\n        lastEmittedData = batch.nextData;\r\n        s.onDataChange?.(batch.nextData, { source: \"row-op\", ops: batch.ops });\r\n        set({\r\n          data: batch.nextData,\r\n          cellErrors: pruneCellErrors(s.cellErrors, batch.nextData, s.getRowId),\r\n          selection: emptySelection(),\r\n          activeCell: null,\r\n          ...rebuildRowOpView(s, batch.nextData),\r\n        });\r\n      },\r\n      duplicateRows(viewRowIndexes) {\r\n        const s = get();\r\n        if (s.readOnly) return;\r\n        // an open editor pins a view coordinate the shift would silently invalidate (the commit\r\n        // path re-reads the shifted viewIndex for the same coord)\r\n        if (s.editing) {\r\n          warnDev(\"duplicateRows is a no-op while an edit session is open\");\r\n          return;\r\n        }\r\n        if (!s.duplicateRow) {\r\n          warnDev(\"duplicateRows is a no-op because no `duplicateRow` prop was provided\");\r\n          return;\r\n        }\r\n        const dataRowIndexes = viewRowIndexes.map((viewRow) => s.viewIndex[viewRow]).filter((i): i is number => i !== undefined);\r\n        const batch = computeDuplicateBatch(s, dataRowIndexes);\r\n        if (!batch) return;\r\n        warnViewSpacePresenceStale(s);\r\n        rowIndexCache.invalidate();\r\n        forgetDeferredRows();\r\n        lastEmittedData = batch.nextData;\r\n        s.onDataChange?.(batch.nextData, { source: \"row-op\", ops: batch.ops });\r\n        const view = rebuildRowOpView(s, batch.nextData);\r\n        if (s.sortState.length > 0 || s.filterState.length > 0) {\r\n          set({ data: batch.nextData, cellErrors: pruneCellErrors(s.cellErrors, batch.nextData, s.getRowId), ...followActiveRow(s, batch.nextData, view.viewIndex!), ...view });\r\n          return;\r\n        }\r\n        // unsorted and unfiltered, each duplicate lands directly after its source view row; fold one\r\n        // offset per source, ascending, so earlier insertions shift later sources' view indices too.\r\n        const uniqueAscendingViewRows = Array.from(new Set(viewRowIndexes)).sort((a, b) => a - b);\r\n        let selection = s.selection;\r\n        let activeCell = s.activeCell;\r\n        uniqueAscendingViewRows.forEach((viewRow, i) => {\r\n          const insertAt = viewRow + 1 + i;\r\n          selection = offsetSelectionForRows(selection, insertAt, 1);\r\n          if (activeCell && activeCell.row >= insertAt) activeCell = { ...activeCell, row: activeCell.row + 1 };\r\n        });\r\n        set({\r\n          data: batch.nextData,\r\n          cellErrors: pruneCellErrors(s.cellErrors, batch.nextData, s.getRowId),\r\n          selection,\r\n          activeCell,\r\n          ...view,\r\n        });\r\n      },\r\n      reorderRows(from, to) {\r\n        const s = get();\r\n        if (!s.enableRowReorder) return false;\r\n        if (s.readOnly) return false;\r\n        // an open editor pins a view coordinate the move would silently invalidate (the commit\r\n        // path re-reads the shifted viewIndex for the same coord)\r\n        if (s.editing) return false;\r\n        if (s.sortState.length > 0 || s.filterState.length > 0) {\r\n          warnDev(\"reorderRows is a no-op while a sort or filter is active (the view order is owned by the sort/filter)\");\r\n          return false;\r\n        }\r\n        const n = s.data.length;\r\n        if (!Number.isInteger(from) || !Number.isInteger(to) || from < 0 || from >= n || to < 0 || to >= n || from === to) return false;\r\n        // viewIndex is the identity permutation here (sort/filter gated above), so view == data indices\r\n        const fromData = s.viewIndex[from]!;\r\n        const toData = s.viewIndex[to]!;\r\n        if (toData === fromData) return false;\r\n        for (let i = 0; i < n; i++) {\r\n          // indexed access (not .some/forEach, which skip sparse holes): a reorder would shift\r\n          // the lazy add-on's index-keyed loaded-range bookkeeping out from under in-flight fetches\r\n          if (s.data[i] === undefined) {\r\n            warnDev(\"reorderRows is a no-op while rows are still unloaded (useDataGridLazyRows): load the range first\");\r\n            return false;\r\n          }\r\n        }\r\n        const moved = s.data[fromData]!;\r\n        const nextData = s.data.slice();\r\n        nextData.splice(fromData, 1);\r\n        nextData.splice(toData, 0, moved);\r\n        const ops: DataOp<unknown>[] = [{ type: \"move\", rowId: s.getRowId(moved, fromData), row: moved, from: fromData, to: toData }];\r\n        warnViewSpacePresenceStale(s);\r\n        rowIndexCache.invalidate();\r\n        forgetDeferredRows();\r\n        lastEmittedData = nextData;\r\n        s.onDataChange?.(nextData, { source: \"row-op\", ops });\r\n        const f = rowReorderMap(from, to);\r\n        set({\r\n          data: nextData,\r\n          selection: reorderSelectionForRow(s.selection, from, to),\r\n          activeCell: s.activeCell ? { ...s.activeCell, row: f(s.activeCell.row) } : null,\r\n          lastHighlightedRow: s.lastHighlightedRow === null ? null : f(s.lastHighlightedRow),\r\n        });\r\n        return true;\r\n      },\r\n      _moveActiveCell(d, opts) {\r\n        const s = get();\r\n        const active = s.activeCell ?? { col: 0, row: 0 };\r\n        const maxRow = Math.max(0, s.viewIndex.length - 1);\r\n        const maxCol = Math.max(0, s.visibleColumns.length - 1);\r\n        if (opts?.extend && s.enableRangeSelection) {\r\n          // Grow from the anchor's far edge (Excel-correct contraction), never re-deriving\r\n          // `next` from activeCell — the anchor stays put for the whole extend gesture.\r\n          const direction: ExtendDirection = d.dy < 0 ? \"up\" : d.dy > 0 ? \"down\" : d.dx < 0 ? \"left\" : \"right\";\r\n          const selection = extendSelection(s.selection.current ? s.selection : selectCellPure(active), direction, {\r\n            rowCount: s.viewIndex.length,\r\n            colCount: s.visibleColumns.length,\r\n          });\r\n          const edge = selection.current?.range;\r\n          set({\r\n            selection,\r\n            lastHighlightedRow: edge ? (d.dy < 0 ? edge.y : edge.y + edge.height - 1) : active.row,\r\n            lastHighlightedCol: edge ? (d.dx < 0 ? edge.x : edge.x + edge.width - 1) : active.col,\r\n          });\r\n        } else {\r\n          const next = { col: clampIndex(active.col + d.dx, maxCol), row: clampIndex(active.row + d.dy, maxRow) };\r\n          if (opts?.retain) {\r\n            set({ activeCell: next, lastHighlightedRow: next.row, lastHighlightedCol: next.col });\r\n          } else {\r\n            set({ activeCell: next, selection: selectCellPure(next), lastHighlightedRow: next.row, lastHighlightedCol: next.col });\r\n          }\r\n        }\r\n      },\r\n      _syncProps(props) {\r\n        set((s) => {\r\n          // controlled sort/filter/join/search: the prop (when defined) is the source of truth this\r\n          // sync writes into the store; uncontrolled: keep whatever the store's own actions last set.\r\n          const nextSortState = props.sortState ?? s.sortState;\r\n          // backfill: a controlled filterState prop may omit filterId (backward-compat input boundary).\r\n          const nextFilterState = props.filterState ? withFilterIds(props.filterState) : s.filterState;\r\n          const nextJoinOperator = props.joinOperator ?? s.joinOperator;\r\n          const nextSearchText = props.searchText ?? s.searchText;\r\n          // data: controlled (props.data defined) mirrors the prop every sync, byte-identical to\r\n          // pre-defaultData behavior; uncontrolled keeps the store's own array — every commit action\r\n          // (edit/paste/fill/delete/row-ops) already wrote it there directly, and `defaultData` itself\r\n          // is init-only (never re-read past store creation, matching `<input defaultValue>`).\r\n          const nextData = s.dataControlled && props.data !== undefined ? props.data : s.data;\r\n          // Echo detection: a controlled consumer that stores what onDataChange handed it and\r\n          // re-renders feeds that exact array back here. `data` then differs from the last sync by\r\n          // identity, so syncInputsEqual fails and computeViewIndex runs — the ~56 ms cliff at 100k\r\n          // rows with a sort active. Recognizing our own array skips it, which is what makes\r\n          // updateCells fast for controlled consumers too (design spec §3.3). A consumer who maps,\r\n          // filters, or otherwise rebuilds the array before storing it falls back to the slow path.\r\n          const isEcho = nextData === lastEmittedData;\r\n          // A `data` array we did not produce reordered or replaced rows as far as this store knows,\r\n          // so the id map must be rebuilt. Our own echo never moved a row (updateCells patches\r\n          // values in place), so it keeps the map — rebuilding it per tick is the O(n) cost the API\r\n          // exists to remove.\r\n          // A genuine consumer-driven replacement (not our own echo) may have dropped rows (e.g. an\r\n          // import replacing the whole array) — prune any cellErrors entry whose rowId no longer\r\n          // exists, same discipline as the rowId-index cache invalidation right below.\r\n          const nextCellErrors =\r\n            nextData !== s.data && !isEcho ? pruneCellErrors(s.cellErrors, nextData, s.getRowId) : s.cellErrors;\r\n          if (nextData !== s.data && !isEcho) {\r\n            rowIndexCache.invalidate();\r\n            forgetDeferredRows();\r\n          }\r\n          // A genuine consumer-driven `columns` prop change replaces `s.columns` outright; syncing\r\n          // again with the SAME prop reference (any unrelated re-render) keeps `s.columns` as-is so\r\n          // runtime column-pin mutations (setColumnPin / the initial-layout seed) survive it.\r\n          const propsColumnsChanged = props.columns !== lastPropsColumns;\r\n          lastPropsColumns = props.columns;\r\n          const nextColumns = propsColumnsChanged ? props.columns : s.columns;\r\n          // A replaced `columns` array re-asserts its def-level hidden flags into `hiddenColumns`;\r\n          // a same-reference re-render must not re-hide a column the user re-showed via\r\n          // `setColumnHidden` (user intent wins over the def until the def itself changes).\r\n          const reseededHidden = propsColumnsChanged\r\n            ? defHiddenColumnIds(props.columns).filter((id) => !s.hiddenColumns.includes(id))\r\n            : [];\r\n          const nextHiddenColumns = reseededHidden.length > 0 ? [...s.hiddenColumns, ...reseededHidden] : s.hiddenColumns;\r\n          // same registry-widening cast as createDataGridStore's init — see that comment.\r\n          const nextCellTypes =\r\n            (props.cellTypes as unknown as Record<string, CellType> | undefined) ?? (defaultCellTypes as unknown as Record<string, CellType>);\r\n          const nextSyncInputs: SyncInputs = {\r\n            data: nextData,\r\n            cellTypes: nextCellTypes,\r\n            columns: nextColumns,\r\n            sortState: nextSortState,\r\n            filterState: nextFilterState,\r\n            joinOperator: nextJoinOperator,\r\n            searchText: nextSearchText,\r\n            columnOrder: s.columnOrder,\r\n            hiddenColumns: nextHiddenColumns,\r\n          };\r\n          // An echo only licenses skipping the recompute when `data` is the ONLY input that moved —\r\n          // a sync that also changed sort/filter/search/columns still has to rebuild for those.\r\n          const echoOnly =\r\n            isEcho && syncInputsEqual({ ...nextSyncInputs, data: lastSyncInputs.data }, lastSyncInputs);\r\n          const unchanged = syncInputsEqual(nextSyncInputs, lastSyncInputs) || echoOnly;\r\n          lastSyncInputs = nextSyncInputs;\r\n\r\n          const nextVisibleColumns = unchanged ? s.visibleColumns : computeVisibleColumns(nextColumns, s.columnOrder, nextHiddenColumns);\r\n          const nextViewIndex = unchanged\r\n            ? s.viewIndex\r\n            : computeViewIndex(nextData, nextColumns, nextSortState, nextFilterState, nextJoinOperator, s.viewIndex, nextCellTypes);\r\n          // A full rebuild leaves nothing pending for the incremental path to replay.\r\n          if (!unchanged) resetDeferredRows();\r\n          // If viewIndex is unchanged (same reference either from the guard above or from\r\n          // computeViewIndex's own content-equality reuse), no row fell out of view, so this\r\n          // clamp is a no-op — filtering against nextViewIndex.length still runs but keeps every row.\r\n          const rows = s.selection.rows.length === 0\r\n            ? s.selection.rows\r\n            : CompactSelection.fromArray(s.selection.rows.toArray().filter((row) => row < nextViewIndex.length));\r\n          return {\r\n            ...props,\r\n            ...resolveSelectionConfig(props),\r\n            data: nextData,\r\n            cellErrors: nextCellErrors,\r\n            columns: nextColumns,\r\n            hiddenColumns: nextHiddenColumns,\r\n            cellTypes: nextCellTypes,\r\n            labels: mergeLabels(props.labels),\r\n            overlayPlugins: props.overlayPlugins ?? EMPTY_OVERLAY_PLUGINS,\r\n            rowBands: props.rowBands ?? EMPTY_ROW_BANDS,\r\n            visibleColumns: nextVisibleColumns,\r\n            sortState: nextSortState,\r\n            filterState: nextFilterState,\r\n            joinOperator: nextJoinOperator,\r\n            searchText: nextSearchText,\r\n            sortControlled: props.sortState !== undefined,\r\n            filterControlled: props.filterState !== undefined,\r\n            joinOperatorControlled: props.joinOperator !== undefined,\r\n            searchControlled: props.searchText !== undefined,\r\n            viewIndex: nextViewIndex,\r\n            // A real recompute reconciles whatever a deferred updateCells left stale; an echo or an\r\n            // unchanged sync recomputed nothing, so the flag has to survive it.\r\n            viewStale: unchanged ? s.viewStale : false,\r\n            ...(unchanged\r\n              ? { searchMatches: s.searchMatches, searchMatchSet: s.searchMatchSet, searchMatchRows: s.searchMatchRows, searchMatchesCapped: s.searchMatchesCapped }\r\n              : computeSearchMatches(nextData, nextColumns, nextViewIndex, nextVisibleColumns, nextSearchText)),\r\n            selection: rows === s.selection.rows ? s.selection : { ...s.selection, rows },\r\n          };\r\n        });\r\n      },\r\n      _registerScrollToCell(impl) {\r\n        set({ scrollToCellImpl: impl });\r\n      },\r\n      _registerReadOnly(readOnly) {\r\n        set({ readOnly });\r\n      },\r\n      _registerKeymap(keymap) {\r\n        set({ keymap });\r\n      },\r\n      _registerFillHandlers(handlers) {\r\n        set({ fillHandlers: handlers });\r\n      },\r\n      _registerPresenceViewSpaceActive(impl) {\r\n        set({ presenceViewSpaceActive: impl });\r\n      },\r\n    },\r\n  }));\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/store/create-store.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/store/hooks.ts",
      "content": "import { useCallback, useMemo, useRef } from \"react\";\r\nimport { useShallow } from \"zustand/react/shallow\";\r\nimport type {\r\n  CellCoord,\r\n  CellType,\r\n  FilterJoinOperator,\r\n  FilterSpec,\r\n  GridSelection,\r\n  HeaderClickBehavior,\r\n  Keymap,\r\n  RowMarkersMode,\r\n  SortSpec,\r\n} from \"../types\";\r\nimport type { OverlayPlugin } from \"../overlays\";\r\nimport type { RowBandsSpec } from \"../layout-context\";\r\nimport type { DataGridLabels } from \"../labels\";\r\nimport type { SearchMatch } from \"../sort-filter\";\r\nimport { isSelectionEmpty, selectionContainsCell } from \"../selection\";\r\nimport { colRangesEqual, selectedColRangesForRow, type ColRange } from \"../selection/selected-col-ranges-for-row\";\r\nimport { cellErrorKey, flashCellKey, getRangeValues, searchMatchKey } from \"./compute\";\r\nimport { useDataGridStore, useDataGridStoreApi } from \"./provider\";\r\nimport type { AnyColumnDef, ColumnDefOf, DataGridActions, DataGridCellState, DataGridRowCellState, DataGridStoreState } from \"./types\";\r\n\r\n/** The stable actions object for the current grid; never changes identity, so this hook never re-renders. */\r\nexport function useDataGridActions(): DataGridActions {\r\n  return useDataGridStore((s) => s.actions);\r\n}\r\n\r\n/**\r\n * The resolved cell-type registry (the consumer's `cellTypes` prop, or the built-ins) — the same\r\n * registry `cell.tsx` and the store's own edit/clipboard/fill resolution read, so tooling that\r\n * needs to resolve a column's `Cell`/`Editor` never has to guess whether a custom registry is in\r\n * play. Reference is stable after init, so subscribing costs no re-renders.\r\n *\r\n * The registry key is NOT checked against `ColumnDef.type` at the registry's type level\r\n * (`Record<string, AnyCellType>` is erased) — per-key type safety comes from `defineColumns` plus\r\n * a `GridCellTypes` interface extension. A `column.type` string that resolves to nothing renders\r\n * with the built-in `text` cell type and editing it no-ops.\r\n */\r\nexport function useDataGridCellTypes(): Record<string, CellType> {\r\n  return useDataGridStore((s) => s.cellTypes);\r\n}\r\n\r\n/**\r\n * Imperative scroll-into-view for a view-space cell coord, honoring pinned bands — the same\r\n * mechanism keyboard nav uses internally, exposed as a public extension point for add-ons that\r\n * move the active cell from outside `DataGridRoot`'s subtree (e.g. `data-grid-toolbar`'s\r\n * search next/prev). No-op before `DataGridRoot` mounts or after it unmounts.\r\n */\r\nexport function useDataGridScrollToCell(): (coord: CellCoord) => void {\r\n  const storeApi = useDataGridStoreApi();\r\n  return useCallback((coord: CellCoord) => storeApi.getState().scrollToCellImpl?.(coord), [storeApi]);\r\n}\r\n\r\n/**\r\n * Fill-handle keymap handlers registered by the `data-grid-fill` add-on's tracker component (see\r\n * `fillHandlers`'s doc comment) — read by `DataGridRoot` itself on every render and forwarded to\r\n * `useGridInteraction`'s optional `fillDown`/`fillRight`/`cancelFillDrag` params. `null` before the\r\n * add-on's tracker mounts (or when it's absent entirely), matching every other registration slot's\r\n  * no-op-by-default contract. Not part of the public API; consumers should not import it.\r\n  * @internal\r\n  */\r\nexport function useDataGridFillHandlers(): DataGridStoreState[\"fillHandlers\"] {\r\n  return useDataGridStore((s) => s.fillHandlers);\r\n}\r\n\r\n/**\r\n * Mirrors the mounted `DataGridRoot`'s `readOnly` prop — for add-on\r\n * mutation surfaces outside the root's subtree (e.g. the context-menu add-on) that need to hide or\r\n * disable their own actions on a read-only grid. `false` before mount/after unmount.\r\n */\r\nexport function useDataGridReadOnly(): boolean {\r\n  return useDataGridStore((s) => s.readOnly);\r\n}\r\n\r\n/**\r\n * The mounted `DataGridRoot`'s effective keymap — `DEFAULT_KEYMAP` merged over with its `keymap`\r\n * prop — the single source of truth for every bound action (backs the `data-grid-keybindings`\r\n * add-on's dialog so it never hardcodes a shortcut list). `DEFAULT_KEYMAP`\r\n * before mount/after unmount.\r\n */\r\nexport function useDataGridKeymap(): Keymap {\r\n  return useDataGridStore((s) => s.keymap);\r\n}\r\n\r\n/**\r\n * The current grid's effective i18n labels — {@link DEFAULT_LABELS} deep-merged with the\r\n * `DataGridProvider`'s `labels` prop — the single source of truth\r\n * every core + add-on component reads its user-facing strings from. Stable identity across renders\r\n * that don't change the `labels` prop's reference (see `memoizedMergeLabels`).\r\n */\r\nexport function useDataGridLabels(): DataGridLabels {\r\n  return useDataGridStore((s) => s.labels);\r\n}\r\n\r\n/** The currently active (focused) cell, or null when nothing is active. */\r\nexport function useDataGridActiveCell(): CellCoord | null {\r\n  return useDataGridStore((s) => s.activeCell);\r\n}\r\n\r\n/**\r\n * Whether ANY cell is currently active — a primitive boolean, unlike {@link useDataGridActiveCell}'s\r\n * `CellCoord | null`, so it only re-renders its subscriber on the null <-> non-null transition (once,\r\n * on first activation), never on every subsequent row/column move. `DataGridRoot` uses this for its\r\n * roving-tabindex bootstrap (tabIndex 0 before any cell is active, -1 after): subscribing to the full\r\n * `activeCell` there instead would re-render the root (and thus every context consumer, since its\r\n * context value is a fresh object literal every render) on every single cell move, defeating the\r\n * \"only windowedColumns/activeColumn changes re-render the tree\" invariant the perf suite checks.\r\n */\r\nexport function useDataGridHasActiveCell(): boolean {\r\n  return useDataGridStore((s) => s.activeCell !== null);\r\n}\r\n\r\n/**\r\n * The cell that should be scrolled into view for the current interaction: the far corner of the\r\n * selection range opposite the anchor while a range-extend is in progress, else `activeCell`.\r\n * Derived fresh from `selection.current` every call (never cached) so it can't go stale the way a\r\n * separately-written field could — `extendTo` (shift-click/drag) never touched `lastHighlightedRow/Col`,\r\n * only `_moveActiveCell`'s keyboard extend branch did, which silently broke every non-keyboard\r\n * extend gesture's scroll-follow. Anchor-inside-range is a store invariant (types.ts), so the far\r\n * corner is always well-defined as \"whichever edge the anchor isn't on\". Internal focus query —\r\n * not part of the public API; consumers should not import it.\r\n * @internal\r\n */\r\nexport function getFocusCell(state: DataGridStoreState): CellCoord | null {\r\n  const current = state.selection.current;\r\n  if (!current) return state.activeCell;\r\n  const { cell, range } = current;\r\n  const isSingleCell = range.width === 1 && range.height === 1;\r\n  if (isSingleCell) return state.activeCell;\r\n  const row = cell.row === range.y ? range.y + range.height - 1 : range.y;\r\n  const col = cell.col === range.x ? range.x + range.width - 1 : range.x;\r\n  return { col, row };\r\n}\r\n\r\n/**\r\n * The active cell's COLUMN only (primitive selector) — for tooling and subscribers that don't care\r\n * about row moves; vertical arrow navigation never re-renders them.\r\n */\r\nexport function useDataGridActiveColumn(): number | null {\r\n  return useDataGridStore((s) => s.activeCell?.col ?? null);\r\n}\r\n\r\n/** The current selection model. */\r\nexport function useDataGridSelection(): GridSelection {\r\n  return useDataGridStore((s) => s.selection);\r\n}\r\n\r\n/**\r\n * Standalone escape hatch for reading a grid's current selection values OUTSIDE an\r\n * `onSelectionChange` fire — e.g. a toolbar \"copy as JSON\" button that needs the live values on\r\n * click, not from the last change notification. Returns a stable callback (identical across\r\n * renders); calling it reads the store fresh each time, same lazy-per-call, primary-range-only\r\n * semantics as `onSelectionChange`'s `details.getValues()` (see {@link SelectionChangeDetails}) —\r\n * this hook exists so a consumer who only needs values imperatively never has to subscribe to\r\n * `useDataGridSelection()` (and re-render on every selection change) just to read them on click.\r\n */\r\nexport function useDataGridGetSelectionValues(): () => unknown[][] {\r\n  const storeApi = useDataGridStoreApi();\r\n  return useCallback(() => {\r\n    const s = storeApi.getState();\r\n    const range = s.selection.current?.range;\r\n    return range ? getRangeValues(s, range) : [];\r\n  }, [storeApi]);\r\n}\r\n\r\n/**\r\n * Registered overlay plugins; consumed ONLY by {@link DataGridOverlays} — never by row/cell\r\n * subscriptions. Not part of the public API; consumers should not import it.\r\n * @internal\r\n */\r\nexport function useDataGridOverlayPlugins(): readonly OverlayPlugin[] {\r\n  return useDataGridStore(useShallow((s) => s.overlayPlugins));\r\n}\r\n\r\n/**\r\n * Registered row-bands spec; consumed ONLY by {@link DataGridRoot} — reads `topRows`/`bottomRows.length`\r\n * for its own height/aria-rowcount math and calls `renderBand`. Not part of the public API;\r\n * consumers should not import it.\r\n * @internal\r\n */\r\nexport function useDataGridRowBands(): RowBandsSpec {\r\n  return useDataGridStore((s) => s.rowBands);\r\n}\r\n\r\n/** The in-progress cell edit, or null when no cell is being edited. */\r\nexport function useDataGridEditing(): { coord: CellCoord; initialText?: string } | null {\r\n  return useDataGridStore((s) => s.editing);\r\n}\r\n\r\n/** The transient rejection message from the last failed commitCellEdit, or null. */\r\nexport function useDataGridEditingError(): string | null {\r\n  return useDataGridStore((s) => s.editingError);\r\n}\r\n\r\n/**\r\n * Post-commit server-error map, keyed `\"rowId:columnId\"` — see `cellErrors`' doc\r\n * comment for the full auto-clear/history/zero-render contract. Reach for this to build a\r\n * consumer-side summary (an error count badge, a \"N cells need attention\" banner); a rendering\r\n * cell itself should use {@link useDataGridCellState}/{@link useDataGridRowCellState} instead,\r\n * which read the SAME map but scoped to one cell/row so an unrelated error never re-renders them.\r\n */\r\nexport function useDataGridCellErrors(): ReadonlyMap<string, string> {\r\n  return useDataGridStore((s) => s.cellErrors);\r\n}\r\n\r\n/**\r\n * Whether `rowId` has ANY cell error, for a row-level indicator (e.g. a row-marker badge) that\r\n * doesn't need atomic per-cell scoping. Probes one exact key per column rather than prefix-matching\r\n * the map: both ids may contain colons, so `\"a\"` prefix-matches row `\"a:b\"`'s keys (see #85).\r\n */\r\nexport function useDataGridRowHasError(rowId: string | undefined): boolean {\r\n  return useDataGridStore((s) => {\r\n    if (rowId === undefined || s.cellErrors.size === 0) return false;\r\n    for (const column of s.columns) {\r\n      if (s.cellErrors.has(cellErrorKey(rowId, column.id))) return true;\r\n    }\r\n    return false;\r\n  });\r\n}\r\n\r\n/** The sorted/filtered/searched row-index view, in display order. */\r\nexport function useDataGridViewIndex(): number[] {\r\n  return useDataGridStore(useShallow((s) => s.viewIndex));\r\n}\r\n\r\n/**\r\n * True when a deferred `updateCells` batch changed a value the active sort or filter reads, so the\r\n * displayed order no longer matches the data. Show a \"re-sort\" control on this and call\r\n * `actions.reconcileView()` from it. Stays `false` for a stream that touches no sort or filter\r\n * column — that case downgrades to `\"never\"` automatically. See {@link UpdateCellsOptions}.\r\n */\r\nexport function useDataGridViewStale(): boolean {\r\n  return useDataGridStore((s) => s.viewStale);\r\n}\r\n\r\n/**\r\n * Visible columns in display order: pinned-left, then unpinned, then pinned-right. Callable directly\r\n * (`accessorFn`/`setValue`/function-form `readOnly` accept an `unknown` row with no cast) since the\r\n * default `TData = unknown` matches the store's own internal row type; pass your own row type\r\n * (`useDataGridVisibleColumns<Employee>()`) to get back `ColumnDef<Employee, unknown>[]` instead —\r\n * sound because these columns always originated from the same consumer-typed `columns` prop.\r\n */\r\nexport function useDataGridVisibleColumns<TData = unknown>(): readonly ColumnDefOf<TData>[] {\r\n  return useDataGridStore(useShallow((s) => s.visibleColumns)) as readonly ColumnDefOf<TData>[];\r\n}\r\n\r\n/**\r\n * Every column (including currently-hidden ones), in `columnOrder` order (definition order when\r\n * unset) — the columns-visibility/pin menu's full inventory, unlike {@link useDataGridVisibleColumns}\r\n * which excludes hidden columns. Same optional `TData` typed-narrowing as `useDataGridVisibleColumns`.\r\n */\r\nexport function useDataGridAllColumns<TData = unknown>(): readonly ColumnDefOf<TData>[] {\r\n  return useDataGridStore(\r\n    useShallow((s) => {\r\n      const orderedIds = s.columnOrder ?? s.columns.map((c) => c.id);\r\n      const byId = new Map(s.columns.map((c) => [c.id, c] as const));\r\n      return orderedIds.map((id) => byId.get(id)).filter((c): c is AnyColumnDef => c != null);\r\n    }),\r\n  ) as readonly ColumnDefOf<TData>[];\r\n}\r\n\r\n/** Whether a column id is currently hidden (def-level `hidden: true` seeds this set; `setColumnHidden` wins until the `columns` prop identity changes). */\r\nexport function useDataGridIsColumnHidden(columnId: string): boolean {\r\n  return useDataGridStore((s) => s.hiddenColumns.includes(columnId));\r\n}\r\n\r\n/** A single column's current width in px, or undefined when unset (falls back to the column def). */\r\nexport function useDataGridColumnWidth(columnId: string): number | undefined {\r\n  return useDataGridStore((s) => s.columnWidths[columnId]);\r\n}\r\n\r\n/** Live width overrides for a set of column ids, in the same order — one subscription, safe under a changing column count. */\r\nexport function useDataGridColumnWidths(columnIds: readonly string[]): (number | undefined)[] {\r\n  return useDataGridStore(useShallow((s) => columnIds.map((id) => s.columnWidths[id])));\r\n}\r\n\r\n/** The active multi-column sort spec. */\r\nexport function useDataGridSortState(): SortSpec[] {\r\n  return useDataGridStore(useShallow((s) => s.sortState));\r\n}\r\n\r\n/** How a plain header click behaves; see {@link HeaderClickBehavior}. */\r\nexport function useDataGridHeaderClickBehavior(): HeaderClickBehavior {\r\n  return useDataGridStore((s) => s.headerClickBehavior);\r\n}\r\n\r\n/** The resolved (defaulted) column-feature flags: resize/reorder/pin enablement grid-wide. */\r\nexport function useDataGridColumnFeatureFlags(): {\r\n  enableColumnResize: boolean;\r\n  enableColumnReorder: boolean;\r\n  enableColumnPinning: boolean;\r\n} {\r\n  return useDataGridStore(\r\n    useShallow((s) => ({\r\n      enableColumnResize: s.enableColumnResize,\r\n      enableColumnReorder: s.enableColumnReorder,\r\n      enableColumnPinning: s.enableColumnPinning,\r\n    })),\r\n  );\r\n}\r\n\r\n/** The active per-column filter specs. */\r\nexport function useDataGridFilterState(): FilterSpec[] {\r\n  return useDataGridStore(useShallow((s) => s.filterState));\r\n}\r\n\r\n/** How `filterState`'s rows combine; default `\"and\"`. See {@link FilterJoinOperator}. */\r\nexport function useDataGridJoinOperator(): FilterJoinOperator {\r\n  return useDataGridStore((s) => s.joinOperator);\r\n}\r\n\r\n/** The current quick-search text. */\r\nexport function useDataGridSearchText(): string {\r\n  return useDataGridStore((s) => s.searchText);\r\n}\r\n\r\n/**\r\n * Every cell matching the current `searchText`, in view-space row-major order, capped at 1000 hits.\r\n * Reads the store's precomputed slice — `setSearch`/`setSorts`/`setFilters`/`_syncProps` are the\r\n * only places that (re)run `findSearchMatches`, exactly once each, never per-subscriber.\r\n */\r\nexport function useDataGridSearchMatches(): SearchMatch[] {\r\n  return useDataGridStore(useShallow((s) => s.searchMatches));\r\n}\r\n\r\n/** True when `searchMatches` hit the 1000-match cap — drives the toolbar's \"1000+\" badge. */\r\nexport function useDataGridSearchCapped(): boolean {\r\n  return useDataGridStore((s) => s.searchMatchesCapped);\r\n}\r\n\r\n/** Whether `coord` (view-space) is a search-match highlight target — an O(1) Set lookup, never a per-cell scan ({@link DataGridCell}'s `data-search-match`). */\r\nexport function useDataGridIsSearchMatch(coord: CellCoord): boolean {\r\n  const column = useDataGridStore((s) => s.visibleColumns[coord.col]);\r\n  return useDataGridStore((s) => (column ? s.searchMatchSet.has(searchMatchKey(coord.row, column.id)) : false));\r\n}\r\n\r\n/** The data row at a given view (display) row index; re-renders only when that row's identity changes. */\r\nexport function useDataGridRow(viewRowIndex: number): unknown {\r\n  return useDataGridStore((s) => {\r\n    const dataRowIndex = s.viewIndex[viewRowIndex];\r\n    return dataRowIndex === undefined ? undefined : s.data[dataRowIndex];\r\n  });\r\n}\r\n\r\n/** Stable `getRowId` result for the row at a given view row index; used as the React key so rows survive sort/filter/insert/delete. */\r\nexport function useDataGridRowId(viewRowIndex: number): string | undefined {\r\n  return useDataGridStore((s) => {\r\n    const dataRowIndex = s.viewIndex[viewRowIndex];\r\n    if (dataRowIndex === undefined) return undefined;\r\n    const row = s.data[dataRowIndex];\r\n    return row === undefined ? undefined : s.getRowId(row, dataRowIndex);\r\n  });\r\n}\r\n\r\n/** `getRowId` results for a set of view row indices, in the same order — one subscription, safe under a changing window size. */\r\nexport function useDataGridRowIds(viewRowIndices: readonly number[]): (string | undefined)[] {\r\n  return useDataGridStore(\r\n    useShallow((s) =>\r\n      viewRowIndices.map((viewRowIndex) => {\r\n        const dataRowIndex = s.viewIndex[viewRowIndex];\r\n        if (dataRowIndex === undefined) return undefined;\r\n        const row = s.data[dataRowIndex];\r\n        return row === undefined ? undefined : s.getRowId(row, dataRowIndex);\r\n      }),\r\n    ),\r\n  );\r\n}\r\n\r\n/**\r\n * Full `rowId -> view row index` map for the current view (2026-08-02 optimization audit,\r\n * \"rowId-native presence adapter\" — confirmed medium: the docs' DIY mapping built this with\r\n * `useDataGridRowIds` under `useShallow`, which re-runs its full-array `.map()` PLUS an n-element\r\n * `shallow()` compare on every store write, not just view changes). Subscribes to `viewIndex`,\r\n * `data`, and `getRowId` identity ONLY — three atomic primitive-reference reads, cheap on every\r\n * store change — and builds the Map lazily in `useMemo` keyed on those identities, so the O(n)\r\n * build cost is paid once per actual view change (sort/filter/insert/delete) or `getRowId` swap,\r\n * never per keystroke, selection step, or streaming tick. `data` is in the memo key (not just\r\n * `viewIndex`) because `getRowId` reads row objects out of it; a `data` replacement with the same\r\n * `viewIndex` (e.g. a value-only edit) must still rebuild since row identities may have changed.\r\n * Consumers needing view -> rowId (the other direction) already have\r\n * {@link useDataGridRowId}/{@link useDataGridRowIds}.\r\n */\r\nexport function useDataGridRowIdToViewRow(): ReadonlyMap<string, number> {\r\n  const viewIndex = useDataGridStore((s) => s.viewIndex);\r\n  const data = useDataGridStore((s) => s.data);\r\n  const getRowId = useDataGridStore((s) => s.getRowId);\r\n  return useMemo(() => {\r\n    const map = new Map<string, number>();\r\n    for (let viewRow = 0; viewRow < viewIndex.length; viewRow++) {\r\n      const dataRowIndex = viewIndex[viewRow];\r\n      if (dataRowIndex === undefined) continue;\r\n      const row = data[dataRowIndex];\r\n      if (row === undefined) continue;\r\n      map.set(getRowId(row, dataRowIndex), viewRow);\r\n    }\r\n    return map;\r\n  }, [viewIndex, data, getRowId]);\r\n}\r\n\r\n/** Total number of rows currently in view (after filter/search). */\r\nexport function useDataGridRowCount(): number {\r\n  return useDataGridStore((s) => s.viewIndex.length);\r\n}\r\n\r\n/** Shared range-membership predicate: true when `viewRowIndex` falls inside the active range or any stacked range. */\r\nfunction isRowInRangeChannel(selection: GridSelection, viewRowIndex: number): boolean {\r\n  if (!selection.current) return false;\r\n  const { range, rangeStack } = selection.current;\r\n  const inRect = (r: { x: number; y: number; width: number; height: number }) => viewRowIndex >= r.y && viewRowIndex < r.y + r.height;\r\n  return inRect(range) || rangeStack.some(inRect);\r\n}\r\n\r\n/** Whether the row at a given view row index is selected via any selection channel (selection state is view-space, so no viewIndex mapping here). */\r\nexport function useDataGridIsRowSelected(viewRowIndex: number): boolean {\r\n  return useDataGridStore((s) => s.selection.rows.hasIndex(viewRowIndex) || isRowInRangeChannel(s.selection, viewRowIndex));\r\n}\r\n\r\n/** The ROWS channel only: this row is selected as a whole (marker press/drag, Shift+Space). Deliberately excludes the cell/column channels — the marker renderer's `isSelected` must flip on row selection alone, never on a cell click (see {@link MarkerCellRenderCtx}). */\r\nexport function useDataGridIsRowChannelSelected(viewRowIndex: number): boolean {\r\n  return useDataGridStore((s) => s.selection.rows.hasIndex(viewRowIndex));\r\n}\r\n\r\n/** Whether the cell channel (active range/range stack) or the column channel covers at least one cell of this view row. The rows-channel-free half of the marker renderer's `isRowChannelSelected`/`isCellSelected` pair. */\r\nexport function useDataGridIsRowCellSelected(viewRowIndex: number): boolean {\r\n  return useDataGridStore((s) => s.selection.columns.length > 0 || isRowInRangeChannel(s.selection, viewRowIndex));\r\n}\r\n\r\n/** Whether `coord` is the active (focused) cell — an atomic per-cell subscription so an active-cell move re-renders only the two affected cells, never every cell or any row. */\r\nexport function useDataGridIsCellActive(coord: CellCoord): boolean {\r\n  return useDataGridStore((s) => s.activeCell !== null && s.activeCell.col === coord.col && s.activeCell.row === coord.row);\r\n}\r\n\r\n/** Whether `coord` is the cell currently being edited — same atomic-subscription rationale as {@link useDataGridIsCellActive}. */\r\nexport function useDataGridIsCellEditing(coord: CellCoord): boolean {\r\n  return useDataGridStore((s) => s.editing !== null && s.editing.coord.col === coord.col && s.editing.coord.row === coord.row);\r\n}\r\n\r\n/** The initial-text seed for `coord`'s edit session (type-to-replace), or undefined; null when `coord` isn't the editing cell. Atomic per-cell, like {@link useDataGridIsCellEditing}. */\r\nexport function useDataGridCellInitialText(coord: CellCoord): string | undefined {\r\n  return useDataGridStore((s) =>\r\n    s.editing !== null && s.editing.coord.col === coord.col && s.editing.coord.row === coord.row\r\n      ? s.editing.initialText\r\n      : undefined,\r\n  );\r\n}\r\n\r\n/**\r\n * `editingError` scoped to whether `coord` is the currently-editing cell — null for every other\r\n * cell. Deliberately NOT a plain `useDataGridEditingError()` read inside `DataGridCell`: that would\r\n * select the same raw string for every mounted cell, so a rejection would re-render the whole\r\n * visible window instead of only the editing cell — same atomic-subscription rationale as\r\n * {@link useDataGridIsCellEditing}/{@link useDataGridCellInitialText}.\r\n */\r\nexport function useDataGridCellEditingError(coord: CellCoord): string | null {\r\n  return useDataGridStore((s) =>\r\n    s.editing !== null && s.editing.coord.col === coord.col && s.editing.coord.row === coord.row ? s.editingError : null,\r\n  );\r\n}\r\n\r\n/**\r\n * `editingRejectionCount` scoped to whether `coord` is the currently-editing cell (0 for every\r\n * other cell) — the commit-guard re-arm nonce {@link DataGridCell} forwards to its `Editor`. Same\r\n * atomic-subscription rationale as {@link useDataGridCellEditingError}: a rejection re-renders the\r\n * editing cell only, never the whole visible window.\r\n */\r\nexport function useDataGridCellRejectionCount(coord: CellCoord): number {\r\n  return useDataGridStore((s) =>\r\n    s.editing !== null && s.editing.coord.col === coord.col && s.editing.coord.row === coord.row\r\n      ? s.editingRejectionCount\r\n      : 0,\r\n  );\r\n}\r\n\r\n/**\r\n * Consolidated per-cell state: one `useShallow` subscription instead of the ~6 separate store reads\r\n * the individual cell hooks cost together — 7.4→2.7 store.subscribe registrations per mounted cell.\r\n * The 5-tuple object is genuinely fresh per relevant state change, so `useShallow` is the correct\r\n * tool here: it still bails when none of the 5 values changed.\r\n */\r\nexport function useDataGridCellState(coord: CellCoord): DataGridCellState {\r\n  return useDataGridStore(\r\n    useShallow((s) => {\r\n      const isActive = s.activeCell !== null && s.activeCell.col === coord.col && s.activeCell.row === coord.row;\r\n      const isSelected = isActive || selectionContainsCell(s.selection, coord, s.viewIndex.length, s.visibleColumns.length);\r\n      const editing = s.editing;\r\n      const editingHere = editing !== null && editing.coord.col === coord.col && editing.coord.row === coord.row;\r\n      const column = s.visibleColumns[coord.col];\r\n      const isSearchMatch = column ? s.searchMatchSet.has(searchMatchKey(coord.row, column.id)) : false;\r\n      const cellError = column ? (s.cellErrors.size === 0 ? null : (cellErrorAt(s, coord.row, column.id) ?? null)) : null;\r\n      return {\r\n        isActive,\r\n        isSelected,\r\n        isEditing: editingHere,\r\n        initialText: editingHere ? editing.initialText : undefined,\r\n        isSearchMatch,\r\n        cellError,\r\n      };\r\n    }),\r\n  );\r\n}\r\n\r\n/** Resolves `cellErrors`' message for a view coord's row + a known column id, or undefined when none. */\r\nfunction cellErrorAt(s: DataGridStoreState, viewRow: number, columnId: string): string | undefined {\r\n  const dataRowIndex = s.viewIndex[viewRow];\r\n  if (dataRowIndex === undefined) return undefined;\r\n  const row = s.data[dataRowIndex];\r\n  if (row === undefined) return undefined;\r\n  const rowId = s.getRowId(row, dataRowIndex);\r\n  return s.cellErrors.get(cellErrorKey(rowId, columnId));\r\n}\r\n\r\nconst EMPTY_COL_RANGES: readonly ColRange[] = [];\r\n\r\n/**\r\n * This row's errored columns (view-col index -> message), or null when none — mirrors\r\n * `searchMatchRows`' null-not-empty-Set convention. Unlike search matches (precomputed once per\r\n * search-text change, since a hit set can be huge over 100k rows), `cellErrors` is expected to stay\r\n * small — a handful of post-commit server rejections, not a per-row scan target — so this resolves\r\n * the row's id once and does a plain `${rowId}:${columnId}` Map.get per visible column, gated\r\n * entirely behind the `cellErrors.size === 0` fast path that costs nothing when no error is active.\r\n */\r\nfunction rowErrorCols(s: DataGridStoreState, viewRow: number): ReadonlyMap<number, string> | null {\r\n  if (s.cellErrors.size === 0) return null;\r\n  const dataRowIndex = s.viewIndex[viewRow];\r\n  if (dataRowIndex === undefined) return null;\r\n  const row = s.data[dataRowIndex];\r\n  if (row === undefined) return null;\r\n  const rowId = s.getRowId(row, dataRowIndex);\r\n  let errorCols: Map<number, string> | null = null;\r\n  for (let col = 0; col < s.visibleColumns.length; col++) {\r\n    const column = s.visibleColumns[col];\r\n    if (!column) continue;\r\n    const message = s.cellErrors.get(cellErrorKey(rowId, column.id));\r\n    if (message === undefined) continue;\r\n    if (!errorCols) errorCols = new Map();\r\n    errorCols.set(col, message);\r\n  }\r\n  return errorCols;\r\n}\r\n\r\n/** This row's currently-flashing columns (view-col index set), or null when no flash key covers the row — same null-not-empty-Set convention as `searchMatchRows`. */\r\nfunction flashingColsForRow(s: DataGridStoreState, viewRow: number): ReadonlySet<number> | null {\r\n  if (s.flashingCells.size === 0) return null;\r\n  let cols: Set<number> | null = null;\r\n  for (let col = 0; col < s.visibleColumns.length; col++) {\r\n    const column = s.visibleColumns[col];\r\n    if (!column) continue;\r\n    if (!s.flashingCells.has(flashCellKey(viewRow, column.id))) continue;\r\n    if (!cols) cols = new Set();\r\n    cols.add(col);\r\n  }\r\n  return cols;\r\n}\r\n\r\nfunction computeRowCellState(s: DataGridStoreState, viewRow: number): DataGridRowCellState {\r\n  const activeCol = s.activeCell !== null && s.activeCell.row === viewRow ? s.activeCell.col : null;\r\n  const editing = s.editing;\r\n  const editingHere = editing !== null && editing.coord.row === viewRow;\r\n  const editingCol = editingHere ? editing.coord.col : null;\r\n  const editingInitialText = editingHere ? editing.initialText : undefined;\r\n\r\n  // O(1) map lookup instead of an O(visibleColumns) scan + string-key Set.has per column (perf\r\n  // audit 2026-07-17: measured 29x faster at 100k rows/8 cols — see searchMatchRows' doc comment).\r\n  const searchMatchCols: ReadonlySet<number> | null = s.searchMatchRows.get(viewRow) ?? null;\r\n\r\n  const errorCols = rowErrorCols(s, viewRow);\r\n\r\n  const flashingCols = flashingColsForRow(s, viewRow);\r\n\r\n  const selectionEmpty = isSelectionEmpty(s.selection);\r\n  const selectedColRanges = selectionEmpty\r\n    ? EMPTY_COL_RANGES\r\n    : selectedColRangesForRow(s.selection, viewRow, s.visibleColumns.length);\r\n\r\n  return { activeCol, editingCol, editingInitialText, searchMatchCols, errorCols, flashingCols, selectedColRanges };\r\n}\r\n\r\n/** Content equality for two nullable `Set<number>`s — {@link computeRowCellState} allocates a fresh Set per call, so `searchMatchCols` needs content (not reference) comparison to stay referentially stable across unrelated store updates. */\r\nfunction searchMatchColsEqual(a: ReadonlySet<number> | null, b: ReadonlySet<number> | null): boolean {\r\n  if (a === b) return true;\r\n  if (a === null || b === null || a.size !== b.size) return false;\r\n  for (const col of a) if (!b.has(col)) return false;\r\n  return true;\r\n}\r\n\r\n/** Content equality for two nullable `Map<number, string>`s — same rationale as {@link searchMatchColsEqual}, for `errorCols` (a fresh Map per `computeRowCellState` call). */\r\nfunction errorColsEqual(a: ReadonlyMap<number, string> | null, b: ReadonlyMap<number, string> | null): boolean {\r\n  if (a === b) return true;\r\n  if (a === null || b === null || a.size !== b.size) return false;\r\n  for (const [col, message] of a) if (b.get(col) !== message) return false;\r\n  return true;\r\n}\r\n\r\n/** Content equality for {@link DataGridRowCellState} — the comparator `useDataGridRowCellState` uses to keep its selector output referentially stable when nothing in the row actually changed. */\r\nfunction rowCellStateEqual(a: DataGridRowCellState, b: DataGridRowCellState): boolean {\r\n  if (a === b) return true;\r\n  return (\r\n    a.activeCol === b.activeCol &&\r\n    a.editingCol === b.editingCol &&\r\n    a.editingInitialText === b.editingInitialText &&\r\n    searchMatchColsEqual(a.searchMatchCols, b.searchMatchCols) &&\r\n    errorColsEqual(a.errorCols, b.errorCols) &&\r\n    searchMatchColsEqual(a.flashingCols, b.flashingCols) &&\r\n    colRangesEqual(a.selectedColRanges, b.selectedColRanges)\r\n  );\r\n}\r\n\r\n/**\r\n * One subscription per ROW instead of one per CELL: a max-velocity full-window swap mounts ~544\r\n * cells, and per-cell subscriptions are 544 subscribe/unsubscribe cycles per tick; this is one\r\n * per row. Mirrors `useShallow`'s ref-memo pattern with {@link rowCellStateEqual} — the shape\r\n * needs custom comparison (a `Set` by reference, a tuple array by content) that `shallow` cannot\r\n * express. A selection change recomputes every row's selector, but only the affected row's\r\n * comparator sees new content — only that row re-renders. Selection membership flips are the one\r\n * sanctioned cell re-render (aria-selected).\r\n */\r\nexport function useDataGridRowCellState(viewRow: number): DataGridRowCellState {\r\n  const prev = useRef<DataGridRowCellState | undefined>(undefined);\r\n  return useDataGridStore((s) => {\r\n    const next = computeRowCellState(s, viewRow);\r\n    if (prev.current !== undefined && rowCellStateEqual(prev.current, next)) return prev.current;\r\n    return (prev.current = next);\r\n  });\r\n}\r\n\r\n/** The resolved (defaulted) marker-column mode; see {@link RowMarkersMode}. */\r\nexport function useDataGridRowMarkers(): RowMarkersMode {\r\n  return useDataGridStore((s) => s.rowMarkers);\r\n}\r\n\r\n/** Whether drag-to-reorder rows is enabled grid-wide (see `enableRowReorder`). */\r\nexport function useDataGridRowReorderEnabled(): boolean {\r\n  return useDataGridStore((s) => s.enableRowReorder);\r\n}\r\n\r\n/** The resolved (defaulted) selection-gesture config; see {@link SelectionConfig}. */\r\nexport function useDataGridSelectionConfig(): {\r\n  enableRowSelection: boolean;\r\n  enableColumnSelection: boolean;\r\n  enableRangeSelection: boolean;\r\n  enableMultiRange: boolean;\r\n} {\r\n  return useDataGridStore(\r\n    useShallow((s) => ({\r\n      enableRowSelection: s.enableRowSelection,\r\n      enableColumnSelection: s.enableColumnSelection,\r\n      enableRangeSelection: s.enableRangeSelection,\r\n      enableMultiRange: s.enableMultiRange,\r\n    })),\r\n  );\r\n}\r\n\r\n/** Half-open row range `[start, end)`. */\r\ntype RowRange = readonly [start: number, end: number];\r\n\r\n/** Inserts `[start, end)` into `ranges` (sorted, non-overlapping), merging any overlap/adjacency — same algorithm as `mergeRange` in selected-col-ranges-for-row.ts, on the row axis instead of columns. */\r\nfunction mergeRowRange(ranges: RowRange[], start: number, end: number): void {\r\n  if (start >= end) return;\r\n  let i = 0;\r\n  while (i < ranges.length && ranges[i]![1] < start) i++;\r\n  let mergedStart = start;\r\n  let mergedEnd = end;\r\n  while (i < ranges.length && ranges[i]![0] <= mergedEnd) {\r\n    mergedStart = Math.min(mergedStart, ranges[i]![0]);\r\n    mergedEnd = Math.max(mergedEnd, ranges[i]![1]);\r\n    ranges.splice(i, 1);\r\n  }\r\n  ranges.splice(i, 0, [mergedStart, mergedEnd]);\r\n}\r\n\r\n/**\r\n * Select-all header-marker checkbox tri-state: 'checked' (every view row selected), 'indeterminate'\r\n * (some), 'unchecked' (none). Empty grid reads 'unchecked'.\r\n *\r\n * O(rangeStack) instead of O(rowCount): the old implementation looped every view row calling\r\n * `hasIndex`/range-membership per row — a six-figure loop at the 100k-row floor, on every store\r\n * write, in any grid with marker columns (2026-08-02 optimization audit, confirmed high). Coverage\r\n * is derived from the range channel's rectangles (merged into row intervals, same technique\r\n * `selectedColRangesForRow` uses on the column axis) unioned against the rows channel, which answers\r\n * \"is this whole span selected\" via `CompactSelection.hasAll` in O(its own run count) rather than by\r\n * scanning every row.\r\n */\r\nexport function useDataGridAllRowsSelected(): \"checked\" | \"indeterminate\" | \"unchecked\" {\r\n  return useDataGridStore((s) => {\r\n    const rowCount = s.viewIndex.length;\r\n    if (rowCount === 0) return \"unchecked\";\r\n    const { selection } = s;\r\n    const rowsChannelEmpty = selection.rows.length === 0;\r\n    if (rowsChannelEmpty && !selection.current) return \"unchecked\";\r\n    if (selection.rows.hasAll([0, rowCount])) return \"checked\";\r\n\r\n    const rangeCoverage: RowRange[] = [];\r\n    if (selection.current) {\r\n      const { range, rangeStack } = selection.current;\r\n      mergeRowRange(rangeCoverage, Math.max(0, range.y), Math.min(rowCount, range.y + range.height));\r\n      for (const rect of rangeStack) {\r\n        mergeRowRange(rangeCoverage, Math.max(0, rect.y), Math.min(rowCount, rect.y + rect.height));\r\n      }\r\n    }\r\n\r\n    // Gaps left by the range channel must each be fully covered by the rows channel for the union\r\n    // to reach every row — anything less is indeterminate (some selected, not all).\r\n    let cursor = 0;\r\n    let fullyCovered = true;\r\n    for (const [start, end] of rangeCoverage) {\r\n      if (start > cursor && !selection.rows.hasAll([cursor, start])) {\r\n        fullyCovered = false;\r\n        break;\r\n      }\r\n      cursor = Math.max(cursor, end);\r\n    }\r\n    if (fullyCovered && cursor < rowCount && !selection.rows.hasAll([cursor, rowCount])) fullyCovered = false;\r\n    if (fullyCovered) return \"checked\";\r\n\r\n    if (!rowsChannelEmpty || rangeCoverage.length > 0) return \"indeterminate\";\r\n    return \"unchecked\";\r\n  });\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/store/hooks.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/store/index.ts",
      "content": "/** Domain barrel — every symbol previously exported from the monolithic store.tsx, re-exported at the same paths so `from \"./store\"` / `from \"../store\"` call sites are unaffected by the split. */\r\nexport type {\r\n  DataGridSyncProps,\r\n  SelectionChangeDetails,\r\n  AnyColumnDef,\r\n  AnyCellType,\r\n  ColumnDefOf,\r\n  ClipboardProcessCtx,\r\n  SelectLineActionOptions,\r\n  CellPatch,\r\n  RowPatch,\r\n  UpdateCellsOptions,\r\n  UpdateCellsReorder,\r\n  UpdateCellsSkip,\r\n  UpdateCellsVerdict,\r\n  CellErrorTarget,\r\n  CellErrorEntry,\r\n  DataGridStoreState,\r\n  DataGridActions,\r\n  DataGridStore,\r\n  DataGridProviderProps,\r\n  DataGridCellState,\r\n  DataGridRowCellState,\r\n} from \"./types\";\r\nexport { DataGridProvider, useDataGridStoreApi, useDataGridStoreProps } from \"./provider\";\r\nexport { cellErrorKey, flashCellKey } from \"./compute\";\r\nexport { getFocusCell } from \"./hooks\";\r\nexport {\r\n  useDataGridActions,\r\n  useDataGridCellTypes,\r\n  useDataGridScrollToCell,\r\n  useDataGridFillHandlers,\r\n  useDataGridReadOnly,\r\n  useDataGridKeymap,\r\n  useDataGridLabels,\r\n  useDataGridActiveCell,\r\n  useDataGridHasActiveCell,\r\n  useDataGridActiveColumn,\r\n  useDataGridSelection,\r\n  useDataGridGetSelectionValues,\r\n  useDataGridOverlayPlugins,\r\n  useDataGridRowBands,\r\n  useDataGridEditing,\r\n  useDataGridEditingError,\r\n  useDataGridCellErrors,\r\n  useDataGridRowHasError,\r\n  useDataGridViewIndex,\r\n  useDataGridViewStale,\r\n  useDataGridVisibleColumns,\r\n  useDataGridAllColumns,\r\n  useDataGridIsColumnHidden,\r\n  useDataGridColumnWidth,\r\n  useDataGridColumnWidths,\r\n  useDataGridSortState,\r\n  useDataGridHeaderClickBehavior,\r\n  useDataGridColumnFeatureFlags,\r\n  useDataGridFilterState,\r\n  useDataGridJoinOperator,\r\n  useDataGridSearchText,\r\n  useDataGridSearchMatches,\r\n  useDataGridSearchCapped,\r\n  useDataGridIsSearchMatch,\r\n  useDataGridRow,\r\n  useDataGridRowId,\r\n  useDataGridRowIds,\r\n  useDataGridRowIdToViewRow,\r\n  useDataGridRowCount,\r\n  useDataGridIsRowSelected,\r\n  useDataGridIsRowChannelSelected,\r\n  useDataGridIsRowCellSelected,\r\n  useDataGridIsCellActive,\r\n  useDataGridIsCellEditing,\r\n  useDataGridCellInitialText,\r\n  useDataGridCellEditingError,\r\n  useDataGridCellRejectionCount,\r\n  useDataGridCellState,\r\n  useDataGridRowCellState,\r\n  useDataGridRowMarkers,\r\n  useDataGridRowReorderEnabled,\r\n  useDataGridSelectionConfig,\r\n  useDataGridAllRowsSelected,\r\n} from \"./hooks\";\r\n",
      "type": "registry:component",
      "target": "components/data-grid/store/index.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/store/row-index.ts",
      "content": "import type { CellPatch, UpdateCellsOptions } from \"./types\";\r\nimport type { FilterSpec, SortSpec } from \"../types\";\r\n\r\n/**\r\n * Lazily-built, incrementally-maintained `rowId -> dataIndex` map for {@link DataGridActions.updateCells}.\r\n *\r\n * Building it costs O(n) (measured 10.1 ms at 100k rows), so a per-call rebuild would reintroduce\r\n * exactly the per-tick O(n) cost the direct-update API exists to remove. Instead every store path\r\n * that changes which row sits at which data index calls {@link RowIndexCache.invalidate}, and the\r\n * next `updateCells` rebuilds once. A pure value patch never moves a row, so the common streaming\r\n * tick keeps the cache warm forever.\r\n *\r\n * The cache is deliberately keyed on the `data` array identity it was built from: any path that\r\n * replaces `data` without invalidating (a bug, or a future action) is caught on the next lookup\r\n * instead of silently writing to the wrong row.\r\n */\r\nexport type RowIndexCache = {\r\n  /** `rowId -> dataIndex` for `data`, rebuilding when stale. */\r\n  resolve(data: readonly unknown[], getRowId: (row: unknown, index: number) => string): ReadonlyMap<string, number>;\r\n  /** Drops the cached map. Call from every path that inserts, deletes, reorders, or replaces rows. */\r\n  invalidate(): void;\r\n  /**\r\n   * Carries the cached map across a value-only edit that rebuilt the `data` array without moving any\r\n   * row (every cell-write path: `commitCellEdit`, `commitCellValue`, `deleteSelection`,\r\n   * `applyCellUpdates`, `updateCells`). Without this a single user edit between two streaming ticks\r\n   * would force a full O(n) rebuild on the next patch. No-op when the cache is already cold.\r\n   */\r\n  rebase(nextData: readonly unknown[]): void;\r\n};\r\n\r\nexport function createRowIndexCache(): RowIndexCache {\r\n  let cachedData: readonly unknown[] | null = null;\r\n  let cachedMap: ReadonlyMap<string, number> | null = null;\r\n  return {\r\n    resolve(data, getRowId) {\r\n      if (cachedMap && cachedData === data) return cachedMap;\r\n      const map = new Map<string, number>();\r\n      for (let i = 0; i < data.length; i++) map.set(getRowId(data[i], i), i);\r\n      cachedData = data;\r\n      cachedMap = map;\r\n      return map;\r\n    },\r\n    invalidate() {\r\n      cachedData = null;\r\n      cachedMap = null;\r\n    },\r\n    rebase(nextData) {\r\n      if (cachedMap) cachedData = nextData;\r\n    },\r\n  };\r\n}\r\n\r\n/**\r\n * Rebuilds the map from scratch and compares it against `cached`, returning the mismatching row ids.\r\n * Dev-only assertion surface for the invalidation invariant: a missed invalidation lands patches\r\n * on the wrong rows, a silent data-corruption class of bug.\r\n */\r\nexport function diffRowIndex(\r\n  cached: ReadonlyMap<string, number>,\r\n  data: readonly unknown[],\r\n  getRowId: (row: unknown, index: number) => string,\r\n): string[] {\r\n  const mismatches: string[] = [];\r\n  const seen = new Set<string>();\r\n  for (let i = 0; i < data.length; i++) {\r\n    const rowId = getRowId(data[i], i);\r\n    seen.add(rowId);\r\n    if (cached.get(rowId) !== i) mismatches.push(rowId);\r\n  }\r\n  for (const rowId of cached.keys()) {\r\n    if (!seen.has(rowId)) mismatches.push(rowId);\r\n  }\r\n  return mismatches;\r\n}\r\n\r\n/**\r\n * Resolves `reorder` to what the store must actually do this tick. `\"defer\"` downgrades to `\"never\"`\r\n * when no touched column feeds the active sort or filter, so a ticker streaming into a\r\n * non-sort column never flips `viewStale` and never needs a reconcile (design spec §3.4).\r\n */\r\nexport function resolveReorder(\r\n  requested: UpdateCellsOptions[\"reorder\"],\r\n  patches: readonly CellPatch[],\r\n  sortState: readonly SortSpec[],\r\n  filterState: readonly FilterSpec[],\r\n): \"defer\" | \"immediate\" | \"never\" {\r\n  const mode = requested ?? \"defer\";\r\n  if (mode !== \"defer\") return mode;\r\n  return touchesViewInputs(patches, sortState, filterState) ? \"defer\" : \"never\";\r\n}\r\n\r\n/** True when any patch writes a column that the active sort or filter reads. */\r\nexport function touchesViewInputs(\r\n  patches: readonly CellPatch[],\r\n  sortState: readonly SortSpec[],\r\n  filterState: readonly FilterSpec[],\r\n): boolean {\r\n  if (sortState.length === 0 && filterState.length === 0) return false;\r\n  const viewColumns = new Set<string>();\r\n  for (const sort of sortState) viewColumns.add(sort.columnId);\r\n  for (const filter of filterState) viewColumns.add(filter.columnId);\r\n  for (const patch of patches) {\r\n    if (viewColumns.has(patch.columnId)) return true;\r\n  }\r\n  return false;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/store/row-index.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/store/provider.tsx",
      "content": "\"use client\";\r\n\r\nimport { createContext, use, useEffect, useRef, useState, type ReactNode } from \"react\";\r\nimport type { StoreApi } from \"zustand/vanilla\";\r\nimport { useStore } from \"zustand\";\r\nimport type { OverlayPlugin } from \"../overlays\";\r\nimport type { RowBandsSpec } from \"../layout-context\";\r\nimport { checkDevGuardrails, checkUnresolvableColumnTypes } from \"./commit\";\r\nimport { makeSelectionChangeDetails } from \"./compute\";\r\nimport { createDataGridStore } from \"./create-store\";\r\nimport { toInternalSyncProps } from \"./types\";\r\nimport type { AnyColumnDef, DataGridActions, DataGridProviderProps, DataGridStoreState, DataGridSyncProps } from \"./types\";\r\nimport { isSelectionEmpty } from \"../selection\";\r\n\r\n/**\r\n * Wires `onSelectionChange`: one `storeApi.subscribe` on `state.selection`'s identity, not a\r\n * per-action callback insertion — `selection` is replaced by ~15 different call sites (click,\r\n * extend, row/col select, edits shifting rows, `_syncProps` clamping out-of-view rows, etc.), and a\r\n * single subscription covers all of them without threading the callback through every one, and\r\n * without adding a React subscription in row/cell components (this runs entirely outside React).\r\n * Reads `onSelectionChange` fresh off the store on each fire so `_syncProps` updates to it apply\r\n * immediately, same as every other sync-prop callback. Returns the unsubscribe.\r\n */\r\nfunction subscribeSelectionChange(store: StoreApi<DataGridStoreState>): () => void {\r\n  return store.subscribe((state, prevState) => {\r\n    if (state.selection === prevState.selection) return;\r\n    // getValues() closes over store.getState (not this fire's `state`) so a call from inside the\r\n    // handler always sees the current selection even if something re-entrantly changed it first.\r\n    state.onSelectionChange?.(state.selection, makeSelectionChangeDetails(store.getState));\r\n  });\r\n}\r\n\r\nfunction subscribeSelectionCleared(store: StoreApi<DataGridStoreState>): () => void {\r\n  let wasEmpty = isSelectionEmpty(store.getState().selection);\r\n  return store.subscribe((state, prevState) => {\r\n    if (state.selection === prevState.selection) return;\r\n    const empty = isSelectionEmpty(state.selection);\r\n    if (empty && !wasEmpty) state.onSelectionCleared?.();\r\n    wasEmpty = empty;\r\n  });\r\n}\r\n\r\nconst DataGridStoreContext = createContext<StoreApi<DataGridStoreState> | null>(null);\r\n\r\n/**\r\n * Mounts one store instance per grid (multiple grids per page each get their\r\n * own instance) and keeps it in sync with the consumer's live props.\r\n *\r\n * The public escape hatch for code OUTSIDE the provider's subtree: an `onDataChange` closure,\r\n * a WebSocket handler, or a sibling control can hold the returned `actions` (stable for the\r\n * store's lifetime) and the full zustand `StoreApi` without rendering a capture component inside\r\n * the provider. Pass the returned `store` to `DataGridProvider`'s `store` prop to feed the grid;\r\n * the sync loop stays here, so pass the same live props to this hook that the grid renders from.\r\n *\r\n * Generic over the consumer's row type `TData` — pass real `ColumnDef<TData, TValue>[]`\r\n * and a real `getRowId` without an unsafe cast; the internal store stays row-agnostic.\r\n */\r\nexport function useDataGridStoreProps<TData>(props: DataGridSyncProps<TData>): {\r\n  store: StoreApi<DataGridStoreState>;\r\n  actions: DataGridActions;\r\n} {\r\n  const { store, actions } = useGridStoreCore(props, undefined);\r\n  return { store, actions };\r\n}\r\n\r\n/**\r\n * The store-ownership core shared by {@link useDataGridStoreProps} and `DataGridProvider`'s\r\n * default (self-creating) path — one sync loop, guardrail checks, and subscription wiring for\r\n * both. When `externalStore` is set the hook owns nothing: no creation, no sync, no\r\n * subscriptions; the consumer's own `useDataGridStoreProps` call drives the store, and this\r\n * instance only supplies the context value.\r\n */\r\nfunction useGridStoreCore<TData>(\r\n  props: DataGridSyncProps<TData>,\r\n  externalStore: StoreApi<DataGridStoreState> | undefined,\r\n): { store: StoreApi<DataGridStoreState>; actions: DataGridActions } {\r\n  const {\r\n    data,\r\n    defaultData,\r\n    columns,\r\n    getRowId,\r\n    onDataChange,\r\n    validateRow,\r\n    onUndo,\r\n    onRedo,\r\n    cellTypes,\r\n    processCellForClipboard,\r\n    processCellFromClipboard,\r\n    processPaste,\r\n    rowMarkers,\r\n    enableRowSelection,\r\n    enableColumnSelection,\r\n    enableRangeSelection,\r\n    enableMultiRange,\r\n    enableColumnResize,\r\n    enableColumnReorder,\r\n    enableRowReorder,\r\n    enableColumnPinning,\r\n    headerClickBehavior,\r\n    labels,\r\n    createRow,\r\n    duplicateRow,\r\n    sortState,\r\n    onSortChange,\r\n    filterState,\r\n    onFilterChange,\r\n    joinOperator,\r\n    onJoinOperatorChange,\r\n    searchText,\r\n    onSearchTextChange,\r\n    overlayPlugins,\r\n    rowBands,\r\n    defaultColumnLayout,\r\n    onColumnLayoutChange,\r\n    onColumnResizing,\r\n    onSelectionChange,\r\n    onSelectionCleared,\r\n  } = props;\r\n  const internalInit = toInternalSyncProps({\r\n    data,\r\n    defaultData,\r\n    columns,\r\n    getRowId,\r\n    onDataChange,\r\n    validateRow,\r\n    onUndo,\r\n    onRedo,\r\n    cellTypes,\r\n    processCellForClipboard,\r\n    processCellFromClipboard,\r\n    processPaste,\r\n    rowMarkers,\r\n    enableRowSelection,\r\n    enableColumnSelection,\r\n    enableRangeSelection,\r\n    enableMultiRange,\r\n    enableColumnResize,\r\n    enableColumnReorder,\r\n    enableRowReorder,\r\n    enableColumnPinning,\r\n    headerClickBehavior,\r\n    labels,\r\n    createRow,\r\n    duplicateRow,\r\n    sortState,\r\n    onSortChange,\r\n    filterState,\r\n    onFilterChange,\r\n    joinOperator,\r\n    onJoinOperatorChange,\r\n    searchText,\r\n    onSearchTextChange,\r\n    overlayPlugins,\r\n    rowBands,\r\n    defaultColumnLayout,\r\n    onColumnLayoutChange,\r\n    onColumnResizing,\r\n    onSelectionChange,\r\n    onSelectionCleared,\r\n  });\r\n  // Lazy: an external store means this instance owns nothing, and store creation computes the\r\n  // initial view over the full dataset — it must not run for a shell.\r\n  const [ownedStore] = useState<StoreApi<DataGridStoreState> | null>(() => (externalStore ? null : createDataGridStore(internalInit)));\r\n  const store = externalStore ?? (ownedStore as StoreApi<DataGridStoreState>);\r\n  const prevColumns = useRef<readonly AnyColumnDef[] | undefined>(undefined);\r\n  const prevData = useRef<readonly unknown[] | undefined>(undefined);\r\n  const prevOverlayPlugins = useRef<readonly OverlayPlugin[] | undefined>(undefined);\r\n  const prevRowBands = useRef<RowBandsSpec | undefined>(undefined);\r\n  const warnedColumnTypes = useRef<Set<string>>(new Set());\r\n\r\n  // One subscription per store instance, outside React's render path (see subscribeSelectionChange).\r\n  // External stores are subscribed by their owner's useDataGridStoreProps call — subscribing here\r\n  // too would double-fire onSelectionChange.\r\n  useEffect(() => {\r\n    if (externalStore) return;\r\n    return subscribeSelectionChange(store);\r\n  }, [store, externalStore]);\r\n  useEffect(() => {\r\n    if (externalStore) return;\r\n    return subscribeSelectionCleared(store);\r\n  }, [store, externalStore]);\r\n\r\n  useEffect(() => {\r\n    if (externalStore) return;\r\n    checkDevGuardrails(internalInit, prevColumns.current, prevData.current, prevOverlayPlugins.current, prevRowBands.current);\r\n    checkUnresolvableColumnTypes(internalInit.columns, internalInit.cellTypes, warnedColumnTypes.current);\r\n    prevColumns.current = internalInit.columns;\r\n    prevData.current = internalInit.data;\r\n    prevOverlayPlugins.current = internalInit.overlayPlugins;\r\n    prevRowBands.current = internalInit.rowBands;\r\n    store.getState().actions._syncProps(internalInit);\r\n    // internalInit is a fresh cast object each render; depend on the raw props instead.\r\n    // oxlint-disable-next-line react-hooks/exhaustive-deps\r\n  }, [\r\n    store,\r\n    externalStore,\r\n    data,\r\n    columns,\r\n    getRowId,\r\n    onDataChange,\r\n    validateRow,\r\n    onUndo,\r\n    onRedo,\r\n    cellTypes,\r\n    processCellForClipboard,\r\n    processCellFromClipboard,\r\n    processPaste,\r\n    rowMarkers,\r\n    enableRowSelection,\r\n    enableColumnSelection,\r\n    enableRangeSelection,\r\n    enableMultiRange,\r\n    enableColumnResize,\r\n    enableColumnReorder,\r\n    enableRowReorder,\r\n    enableColumnPinning,\r\n    headerClickBehavior,\r\n    labels,\r\n    createRow,\r\n    duplicateRow,\r\n    sortState,\r\n    onSortChange,\r\n    filterState,\r\n    onFilterChange,\r\n    joinOperator,\r\n    onJoinOperatorChange,\r\n    searchText,\r\n    onSearchTextChange,\r\n    overlayPlugins,\r\n    rowBands,\r\n    onColumnLayoutChange,\r\n    onColumnResizing,\r\n    onSelectionChange,\r\n    onSelectionCleared,\r\n  ]);\r\n\r\n  return { store, actions: store.getState().actions };\r\n}\r\n\r\n/**\r\n * Provides the grid's store to the component subtree. By default it self-creates the store via\r\n * the same ownership core as {@link useDataGridStoreProps}; pass `store` to serve a\r\n * consumer-created store instead — then this component is a pure context shell and the sync\r\n * loop lives on the consumer's `useDataGridStoreProps` call.\r\n */\r\nexport function DataGridProvider<TData>(props: DataGridProviderProps<TData>): ReactNode {\r\n  const { store: externalStore, children, ...syncProps } = props;\r\n  const { store } = useGridStoreCore<TData>(syncProps, externalStore);\r\n  return <DataGridStoreContext.Provider value={store}>{children}</DataGridStoreContext.Provider>;\r\n}\r\n\r\n/** Resolves the current grid's store, throwing with a clear message outside the provider. */\r\nfunction useDataGridStoreApiInternal(): StoreApi<DataGridStoreState> {\r\n  const store = use(DataGridStoreContext);\r\n  if (!store) {\r\n    throw new Error(\"gridcn: this hook must be used inside a <DataGridProvider>.\");\r\n  }\r\n  return store;\r\n}\r\n\r\nexport function useDataGridStore<T>(selector: (state: DataGridStoreState) => T): T {\r\n  return useStore(useDataGridStoreApiInternal(), selector);\r\n}\r\n\r\n/**\r\n * Escape hatch for internal engine components that need imperative `getState()`\r\n * access in event handlers. Not for consumer use.\r\n * @internal\r\n */\r\nexport function useDataGridStoreApi(): StoreApi<DataGridStoreState> {\r\n  return useDataGridStoreApiInternal();\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/store/provider.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid/store/types.ts",
      "content": "import type { ReactNode } from \"react\";\r\nimport type { StoreApi } from \"zustand/vanilla\";\r\nimport type {\r\n  CellCoord,\r\n  CellType,\r\n  ColumnDef,\r\n  ColumnLayout,\r\n  DataChange,\r\n  FilterJoinOperator,\r\n  FilterSpec,\r\n  GridSelection,\r\n  HeaderClickBehavior,\r\n  Keymap,\r\n  RowMarkersMode,\r\n  SortSpec,\r\n} from \"../types\";\r\nimport type { OverlayPlugin } from \"../overlays\";\r\nimport type { RowBandsSpec } from \"../layout-context\";\r\nimport type { DataGridLabels, DeepPartialLabels } from \"../labels\";\r\nimport type { SearchMatch } from \"../sort-filter\";\r\nimport type { ExtendDirection, SelectLineOptions } from \"../selection\";\r\nimport type { ColRange } from \"../selection/selected-col-ranges-for-row\";\r\n\r\n/** Live props synced from the consumer's render into the store on every change, typed over the consumer's row shape. */\r\nexport type DataGridSyncProps<TData = unknown> = {\r\n  /**\r\n   * Controlled row array (React `value` semantics): the app owns it, and every mutation\r\n   * (edit/paste/fill/delete/row-ops) only ever reaches it through `onDataChange` — the grid's own\r\n   * display doesn't change until the next render supplies a new `data`. Mutually exclusive with\r\n   * `defaultData`; when both are given, `data` wins (dev-mode warns once). Omit `data` and pass\r\n   * `defaultData` instead for the uncontrolled mode.\r\n   */\r\n  data?: readonly TData[];\r\n  /**\r\n   * Uncontrolled row array (React `defaultValue` semantics): seeds the store ONCE at creation and\r\n   * the store then OWNS it — every mutation applies internally with no app-side state required;\r\n   * `onDataChange` still fires as an optional notification. Changing this prop on a later render is\r\n   * ignored (uncontrolled, matching `<input defaultValue>`). Mutually exclusive with `data` — see\r\n   * that prop's doc for precedence.\r\n   */\r\n  defaultData?: readonly TData[];\r\n  // ColumnDef's 3rd param (TValidate) erases to `any`, not `unknown` — see AnyColumnDef's doc in\r\n  // this file: `validate`'s function-or-schema union can't stay bivariant as a plain property, so\r\n  // a defineColumns literal's narrow-TValue `validate` needs this documented widening to fit here.\r\n  columns: readonly ColumnDefOf<TData>[];\r\n  getRowId: (row: TData, index: number) => string;\r\n  /** Fired once per user gesture with the next data array and an id-keyed delta batch. */\r\n  onDataChange?: (next: readonly TData[], change: DataChange<TData>) => void;\r\n  /**\r\n   * Row-level cross-field validation, run once per touched row after a write gesture commits\r\n   * (edit, paste, fill, delete-contents, updateCells/updateRows) — the row already has ALL of the\r\n   * gesture's cells applied, so the verdict is independent of column order inside a paste. Return\r\n   * `columnId -> message` to surface errors (they land in `cellErrors`, same display as server\r\n   * errors), `null` when the row is fine. Values still commit either way. Not run on row\r\n   * insert/duplicate/delete or on consumer `data` replacement. Sync only.\r\n   */\r\n  validateRow?: (row: TData, rowId: string) => Record<string, string> | null;\r\n  /** Undo keymap action (mod+Z); the `data-grid-history` add-on plugs its `undo()` in here. */\r\n  onUndo?: () => void;\r\n  /** Redo keymap action (mod+Y / mod+shift+Z); the `data-grid-history` add-on plugs its `redo()` in here. */\r\n  onRedo?: () => void;\r\n  /**\r\n   * Cell-type registry keyed by `ColumnDef.type`; defaults to the built-in registry. Providing a\r\n   * registry REPLACES the built-ins, it is not merged over them: to extend the built-ins, spread\r\n   * the exported `cellTypes` in (`cellTypes={{ ...cellTypes, myType }}`).\r\n   */\r\n  cellTypes?: Record<string, AnyCellType>;\r\n  /** Overrides a cell's clipboard-copy text; falls back to the cell type's `toText`. */\r\n  processCellForClipboard?: (value: unknown, ctx: ClipboardProcessCtx<TData>) => string;\r\n  /** Overrides parsing pasted text into a cell value; falls back to the cell type's `fromText`. */\r\n  processCellFromClipboard?: (text: string, ctx: ClipboardProcessCtx<TData>) => unknown;\r\n  /** Runs on the parsed paste grid before it's applied; returning `false` vetoes the paste entirely. */\r\n  processPaste?: (cells: string[][], target: CellCoord) => string[][] | false;\r\n  /** Marker column mode; default 'none'. See {@link RowMarkersMode}. */\r\n  rowMarkers?: RowMarkersMode;\r\n  /** Whole-row selection channel gestures (marker click, Shift+Space); default true. */\r\n  enableRowSelection?: boolean;\r\n  /** Whole-column selection channel gestures (header click); default true. */\r\n  enableColumnSelection?: boolean;\r\n  /** Multi-cell rectangular range gestures (shift-click/drag, shift+arrow); false collapses to single-cell active only. Default true. */\r\n  enableRangeSelection?: boolean;\r\n  /** Ctrl/Cmd-click multi-range (rangeStack); false makes it behave as a plain click. Default true. */\r\n  enableMultiRange?: boolean;\r\n  /** Enables the header resize handle grid-wide; default true. Per-column `resizable: false` still wins. */\r\n  enableColumnResize?: boolean;\r\n  /** Enables drag-to-reorder columns grid-wide; default true. Per-column `reorderable: false` still wins. */\r\n  enableColumnReorder?: boolean;\r\n  /**\r\n   * Enables drag-to-reorder rows grid-wide; default true. The gesture is zone-based: it lives on\r\n   * the marker's `reorder`-family modes only (`reorder`, `reorder-number`, `reorder-checkbox`,\r\n   * `reorder-both`) and only from the grip zone (the grip handle + cell background, or the whole\r\n   * cell in `reorder` mode) — plain `number`/`checkbox`/`both` markers stay pure row-select\r\n   * surfaces, and within the reorder family the number/checkbox glyphs and Shift+drag always\r\n   * stay the row-select gesture, so a selection drag never reorders.\r\n   */\r\n  enableRowReorder?: boolean;\r\n  /** Enables pin/unpin actions grid-wide; default true. Per-column `pinnable: false` still wins. */\r\n  enableColumnPinning?: boolean;\r\n  /** How a plain header click behaves; default 'select'. See {@link HeaderClickBehavior}. */\r\n  headerClickBehavior?: HeaderClickBehavior;\r\n  /** Partial i18n override, deep-merged over {@link DEFAULT_LABELS}; see {@link useDataGridLabels}. */\r\n  labels?: DeepPartialLabels;\r\n  /** Builds a new row for `insertRow`; absent makes `insertRow` a dev-warning no-op (duplicateRows/deleteRows don't need it). */\r\n  createRow?: (index: number) => TData;\r\n  /**\r\n   * Builds a duplicated row's identity for `duplicateRows`: given the\r\n   * source row and its new data index, returns the row to insert (with a distinct id, e.g. a fresh\r\n   * UUID, so the copy's `getRowId()` never collides with its source's React key). Absent makes\r\n   * `duplicateRows` a dev-warning no-op — mirrors `createRow`/`insertRow`.\r\n   */\r\n  duplicateRow?: (row: TData, index: number) => TData;\r\n  /**\r\n   * Controlled multi-sort spec. Standard controlled-input\r\n   * semantics: omitted (undefined) keeps today's uncontrolled behavior — the store owns\r\n   * `sortState`. Provided (even `[]`) makes this prop the source of truth every `_syncProps` sync\r\n   * writes into the store; `onSortChange` still fires on every user-driven change (header click,\r\n   * `setSorts`) either way, but in controlled mode the displayed order only moves once the prop\r\n   * itself changes — a controlled grid whose consumer ignores the callback stays visually fixed.\r\n   */\r\n  sortState?: SortSpec[];\r\n  /** Fires whenever the user (or `actions.toggleSort`/`setSorts`) would change the sort, controlled or not; see `sortState`. */\r\n  onSortChange?: (next: SortSpec[]) => void;\r\n  /** Controlled filter spec; same controlled/uncontrolled semantics as `sortState`. */\r\n  filterState?: FilterSpec[];\r\n  /** Fires whenever the user (or `actions.setFilters`) would change the filters, controlled or not; see `filterState`. */\r\n  onFilterChange?: (next: FilterSpec[]) => void;\r\n  /** Controlled join operator combining `filterState`'s rows; same controlled/uncontrolled semantics as `sortState`. Default `\"and\"`. */\r\n  joinOperator?: FilterJoinOperator;\r\n  /** Fires whenever the user (or `actions.setJoinOperator`) would change the join operator, controlled or not; see `joinOperator`. */\r\n  onJoinOperatorChange?: (next: FilterJoinOperator) => void;\r\n  /** Controlled quick-search text; same controlled/uncontrolled semantics as `sortState`. */\r\n  searchText?: string;\r\n  /** Fires whenever the user (or `actions.setSearch`) would change the search text, controlled or not; see `searchText`. */\r\n  onSearchTextChange?: (next: string) => void;\r\n  /**\r\n   * Overlay-plugin registration: each plugin renders into the overlay layer\r\n   * after the fill preview but before the local active-cell ring, receiving `OverlayPluginCtx`\r\n   * (window-clamp + pin-zone helpers, layout vars). The `data-grid-presence` add-on is the\r\n   * motivating consumer. Pass a stable array reference (e.g. module scope or `useMemo`) — same\r\n   * dev-mode guardrail as `columns`/`getRowClassName`: an unstable identity here re-renders\r\n   * `DataGridOverlays` every tick for nothing.\r\n   */\r\n  overlayPlugins?: readonly OverlayPlugin[];\r\n  /**\r\n   * Row-bands registration: a spec object so root.tsx can compute band heights/`aria-rowcount`\r\n   * SYNCHRONOUSLY at first render (no SSR/first-paint layout shift) while the actual pinned-row\r\n   * semantics live in the `data-grid-pinned-rows` add-on's `useDataGridPinnedRows()`. Same\r\n   * dev-mode identity guardrail as `overlayPlugins` — pass a stable reference (the hook already\r\n   * returns one).\r\n   */\r\n  rowBands?: RowBandsSpec;\r\n  /**\r\n   * Seeds `columnWidths`/`columnOrder`/`hiddenColumns`/per-column `pin` ONCE at store creation,\r\n   * over the `columns` prop's own def defaults (`resolveColumnWidth` precedence: layout beats def).\r\n   * Deliberately NOT a controlled prop — later changes to this value are ignored (uncontrolled-\r\n   * with-events, PLAN events-state gap fix 1); pair it with `onColumnLayoutChange` to save/restore\r\n   * a user's layout (e.g. to localStorage) without a fully-controlled overlay fighting the defs.\r\n   */\r\n  defaultColumnLayout?: ColumnLayout;\r\n  /**\r\n   * Fires once per committed column-layout change (resize drag release/autosize, reorder drop,\r\n   * pin/unpin, show/hide) with the full current `ColumnLayout` snapshot — never per drag frame.\r\n   * Fired from the store actions layer, no React-render dependency. See `defaultColumnLayout` for\r\n   * the matching restore path.\r\n   */\r\n  onColumnLayoutChange?: (next: ColumnLayout) => void;\r\n  /**\r\n   * Fires on every committed selection change (click, extend step, range push, row/column/all\r\n   * selection, clear) with the current `GridSelection` plus a lazy `details.getValues()` accessor.\r\n   * A drag-extend gesture fires once per step (intended — the presence/broadcast use case wants\r\n   * live updates, not just the final drop) — `getValues()` is a function, not a materialized array,\r\n   * specifically so that hot path never pays for building a value matrix on steps nobody reads.\r\n   */\r\n  onSelectionChange?: (next: GridSelection, details: SelectionChangeDetails) => void;\r\n  /**\r\n   * Fired exactly once when the selection transitions from non-empty to empty, from ANY path\r\n   * (outside-click, clearSelection, a cleared range, etc.). `onSelectionChange` also fires on that\r\n   * transition (with an empty selection) — this is the dedicated signal for UI resets. Does not\r\n   * fire when the selection is already empty.\r\n   */\r\n  onSelectionCleared?: () => void;\r\n  /**\r\n   * Fires on every live width write during a resize drag (`actions.setColumnWidth`, per drag\r\n   * frame) — the in-progress counterpart to `onColumnLayoutChange`'s once-per-commit snapshot.\r\n   * Fired from the store actions layer, no React-render dependency. Not called for the commit\r\n   * itself (drag release/autosize) — pair with `onColumnLayoutChange` if you need that too.\r\n   */\r\n  onColumnResizing?: (columnId: string, width: number) => void;\r\n};\r\n\r\n/**\r\n * Second argument to `onSelectionChange` (PLAN events-state gap fix 2). `getValues()` reads the\r\n * CURRENT `selection.current.range` only — a multi-range selection (ctrl-click stack) is not\r\n * flattened or concatenated, matching clipboard copy's own \"primary range only\" scope\r\n * (`resolveCopyScope`) — call it once per range in `selection.current.rangeStack` yourself if you\r\n * need the others. Returns `[]` when there's no active range (row/column-channel-only selection,\r\n * or a clear). Deliberately lazy: built fresh from the store snapshot only when called, so a\r\n * consumer that ignores it (the common case) never pays for reading/allocating a value matrix on\r\n * every drag step.\r\n */\r\nexport type SelectionChangeDetails = {\r\n  /** Raw cell VALUES (not clipboard text) over the primary range, view-row-major: `values[0][0]` is the range's top-left cell. */\r\n  getValues: () => unknown[][];\r\n  /** Row ids of every selected row, view order, across both the row channel (checkboxes) and any cell range. */\r\n  getRowIds: () => string[];\r\n};\r\n\r\n// The store is generic-erased to TData = unknown past this boundary (InternalSyncProps below): one\r\n// runtime engine serves every consumer's row type, and unknown (not never) keeps every ColumnDef\r\n// function field (accessorFn/setValue/readOnly/validate) callable with the store's own `row: unknown`\r\n// values with no cast needed — safe because a column and the row it's called with always came from\r\n// the same consumer-typed `data` array passed in through DataGridSyncProps<TData>. This is also what\r\n// useDataGridVisibleColumns/useDataGridAllColumns return, so a consumer building custom column UI can\r\n// call those same fields without a cast either.\r\n/** `DataGridSyncProps` cast to the store's internal row-agnostic shape; the one unsafe cast, applied once at the boundary. */\r\nexport type InternalSyncProps = DataGridSyncProps<unknown>;\r\n\r\n/**\r\n * `ColumnDef` widened to the store's internal row-agnostic shape (see the erasure comment above).\r\n * `validate`'s own value type erases to `any` (the third param, not `unknown`) — a union of a\r\n * function and a non-callable Standard Schema can't stay bivariant as a plain property the way a\r\n * pure method-shorthand function could, so narrow-`TValue` columns need this one documented,\r\n * single-field cast to widen into this shared type (see types.ts's `ColumnDef.validate` doc).\r\n */\r\nexport type AnyColumnDef = ColumnDef<unknown, unknown, any>; // eslint-disable-line @typescript-eslint/no-explicit-any\r\n\r\n/**\r\n * `ColumnDef<TData, unknown, any>` — the same TValidate-erased-to-`any` widening as `AnyColumnDef`\r\n * (see its doc comment), but keeping `TData` open for call sites that are still generic over the\r\n * consumer's row type (sync-prop declarations, the public `useDataGridVisibleColumns`/`useDataGridAllColumns`\r\n * hooks) rather than already erased to `unknown`. Named once here (was 10 verbatim repeats across\r\n * store.tsx/data-grid.tsx, each with its own eslint-disable) so the widening reason lives in one place.\r\n */\r\nexport type ColumnDefOf<TData> = ColumnDef<TData, unknown, any>; // eslint-disable-line @typescript-eslint/no-explicit-any\r\n\r\n/** `processCellForClipboard`/`processCellFromClipboard`'s callback context — named since it repeats identically across both callbacks in both `DataGridSyncProps` and `DataGridProps`. */\r\nexport type ClipboardProcessCtx<TData> = { row: TData; column: ColumnDefOf<TData> };\r\n\r\n/**\r\n * `CellType<any, any, any>` — the `cellTypes` registry-prop widening (same single-point `any`\r\n * erasure as {@link AnyColumnDef}): `Cell`/`Editor` are `FC<...>` (strictly contravariant in their\r\n * props), so without erasing TData/TValue/TOptions neither the built-ins (concrete TValue) nor a\r\n * consumer's per-column `CellType<TData, TValue, TOptions>` is cast-free assignable to an\r\n * erased-to-`unknown` entry type — the registry prop is the one place a spread of the exported\r\n * `cellTypes` plus a custom type must compile with no cast.\r\n */\r\nexport type AnyCellType = CellType<any, any, any>; // eslint-disable-line @typescript-eslint/no-explicit-any\r\n\r\nexport function toInternalSyncProps<TData>(props: DataGridSyncProps<TData>): InternalSyncProps {\r\n  return props as unknown as InternalSyncProps;\r\n}\r\n\r\n/**\r\n * Options passed to selectRow/selectColumn actions; mirrors lib/selection's SelectLineOptions\r\n * (including `from` — the row-marker drag passes its press-row anchor so `replaceFromLast` can\r\n * replace the channel with exactly the anchor..current span).\r\n */\r\nexport type SelectLineActionOptions = SelectLineOptions;\r\n\r\n/**\r\n * One targeted cell write for {@link DataGridActions.updateCells}, addressed by STABLE ROW ID —\r\n * never a view or data index. A streaming producer cannot know view coordinates under an active\r\n * sort, and row ids are already what {@link DataOp} uses, so history survives sort/filter.\r\n *\r\n * `TColumnId` defaults to `string` — the store's own untyped shape, which the actions keep (one\r\n * runtime engine serves every row type). Instantiate it with the union of your columns' `id`s\r\n * (e.g. `(typeof columns)[number][\"id\"]` from `defineColumns`'s const output) when building a\r\n * typed patch list: a typo'd column id then fails to compile instead of skipping at runtime.\r\n */\r\nexport type CellPatch<TColumnId extends string = string> = {\r\n  /** The row's `getRowId()` value. An id not present in `data` is skipped. */\r\n  rowId: string;\r\n  /** Any column id, including a hidden one. An unknown id is skipped. */\r\n  columnId: TColumnId;\r\n  value: unknown;\r\n};\r\n\r\n/** One whole-row update for {@link DataGridActions.updateRows}: a partial row shallow-merged per column id. `TColumnId` follows {@link CellPatch}. */\r\nexport type RowPatch<TColumnId extends string = string> = {\r\n  rowId: string;\r\n  /** Column id -> new value. Each entry is applied exactly as the matching {@link CellPatch} would be. */\r\n  changes: Readonly<Partial<Record<TColumnId, unknown>>>;\r\n};\r\n\r\n/** How a `updateCells`/`updateRows` batch reconciles with an active sort or filter. */\r\nexport type UpdateCellsReorder = \"defer\" | \"immediate\" | \"never\";\r\n\r\n/** One rowId+columnId target, addressed the same STABLE-ROW-ID way as {@link CellPatch} (no `value` — used only to name a cell for {@link DataGridActions.setCellErrors}/{@link DataGridActions.clearCellErrors}). */\r\nexport type CellErrorTarget = {\r\n  /** The row's `getRowId()` value. An id not present in `data` is stored/cleared anyway (see `cellErrors`' doc) and simply never renders. */\r\n  rowId: string;\r\n  columnId: string;\r\n};\r\n\r\n/** One entry for {@link DataGridActions.setCellErrors}: the target cell plus its rejection message. */\r\nexport type CellErrorEntry = CellErrorTarget & { message: string };\r\n\r\n/** Options for {@link DataGridActions.updateCells} and {@link DataGridActions.updateRows}. */\r\nexport type UpdateCellsOptions = {\r\n  /**\r\n   * Default `\"defer\"`.\r\n   * - `\"defer\"` — values update in place and rows keep their view position; `viewStale` flips.\r\n   *   The view reconciles on the next sort/filter/search change, or on `reconcileView()`. A\r\n   *   `\"defer\"` batch that touches no active sort or filter column downgrades to `\"never\"`\r\n   *   automatically, so `viewStale` never flips for a ticker on a non-sort column.\r\n   * - `\"immediate\"` — moves the patched rows to their new positions now, by removing each one,\r\n   *   re-testing filter membership, and binary-searching it back in under the same comparator chain\r\n   *   a full sort uses. Cost scales with the batch size, not the row count: measured 0.3 / 0.9 /\r\n   *   7.6 ms for 1 / 20 / 256 patched rows at 100k. Batches over\r\n   *   {@link INCREMENTAL_PATCH_LIMIT} rows, a same-tick sort/filter change, or a column with its own\r\n   *   comparator fall back to the full O(n log n) rebuild.\r\n   * - `\"never\"` — updates in place and never marks the view stale. The caller asserts that no\r\n   *   touched column feeds the sort or filter.\r\n   */\r\n  reorder?: UpdateCellsReorder;\r\n  /** `DataChange` source tag; default `\"stream\"`. Pass `\"edit\"` to make the batch undoable. */\r\n  source?: DataChange<unknown>[\"source\"];\r\n  /** Skips each column's `validate`. Default `false`. */\r\n  skipValidation?: boolean;\r\n};\r\n\r\n/** One entry of {@link UpdateCellsVerdict.skipped}: a patch that was not applied, with the reason. */\r\nexport type UpdateCellsSkip = {\r\n  /** Index of the patch in the `patches` array the action received. */\r\n  patchIndex: number;\r\n  /**\r\n   * `\"unknown-row\"` — the row id is not in `data`; `\"unknown-column\"` — the column id is not a\r\n   * known column; `\"hole\"` — the row is an unloaded lazy hole; `\"readonly\"` — the column (or the\r\n   * row, via `readOnly(row)`) is read-only; `\"invalid\"` — the column's `validate` rejected the\r\n   * value; `\"no-op\"` — the value is `Object.is`-equal to the current one.\r\n   */\r\n  reason: \"unknown-row\" | \"unknown-column\" | \"hole\" | \"readonly\" | \"invalid\" | \"no-op\";\r\n};\r\n\r\n/**\r\n * Verdict of {@link DataGridActions.updateCells} / {@link DataGridActions.updateRows}: what the\r\n * batch actually did. `applied` counts the cell writes that landed; `skipped` names every patch\r\n * that did not land and why, so a streaming producer can observe its feed being silently dropped.\r\n *\r\n * `pending` is `true` when the batch is HELD for async validation — the verdict then reports\r\n * nothing about its outcome, and a held batch that is later superseded (a newer `updateCells`, or\r\n * a row-moving op) is dropped silently by design; track supersession on your own feed.\r\n */\r\nexport type UpdateCellsVerdict = {\r\n  /** Cell writes applied (0 when nothing applied or the batch is `pending`). */\r\n  applied: number;\r\n  /** Patches that did not apply, with the reason. Empty when `pending`. */\r\n  skipped: UpdateCellsSkip[];\r\n  /** True when the batch is held for async validation. */\r\n  pending: boolean;\r\n};\r\n\r\n/** Full per-grid interaction + derived state; internally typed over `unknown` rows. */\r\nexport type DataGridStoreState = Omit<\r\n  InternalSyncProps,\r\n  | \"data\"\r\n  | \"defaultData\"\r\n  | \"cellTypes\"\r\n  | \"rowMarkers\"\r\n  | \"enableRowSelection\"\r\n  | \"enableColumnSelection\"\r\n  | \"enableRangeSelection\"\r\n  | \"enableMultiRange\"\r\n  | \"enableColumnResize\"\r\n  | \"enableColumnReorder\"\r\n  | \"enableRowReorder\"\r\n  | \"enableColumnPinning\"\r\n  | \"headerClickBehavior\"\r\n  | \"labels\"\r\n  | \"overlayPlugins\"\r\n  | \"rowBands\"\r\n  | \"defaultColumnLayout\"\r\n> & {\r\n  /** Always defined: controlled mode mirrors the `data` prop every sync; uncontrolled mode seeds from `defaultData` once and the store owns it thereafter (mutated in place by the commit actions). */\r\n  data: readonly unknown[];\r\n  /** Whether `data` is controlled (the `data` prop was defined on the last `_syncProps`/init) — see `data`'s doc comment and `defaultData`. Mirrors the `sortControlled`-style pattern below. */\r\n  dataControlled: boolean;\r\n  /** Row-agnostic view of the cell-type registry; the `never`-typed sync prop is widened once at the store boundary. */\r\n  cellTypes: Record<string, CellType>;\r\n  /** Defaulted (never undefined) — see `resolveSelectionConfig`. */\r\n  rowMarkers: RowMarkersMode;\r\n  enableRowSelection: boolean;\r\n  enableColumnSelection: boolean;\r\n  enableRangeSelection: boolean;\r\n  enableMultiRange: boolean;\r\n  enableColumnResize: boolean;\r\n  enableColumnReorder: boolean;\r\n  enableRowReorder: boolean;\r\n  enableColumnPinning: boolean;\r\n  headerClickBehavior: HeaderClickBehavior;\r\n  activeCell: CellCoord | null;\r\n  selection: GridSelection;\r\n  /**\r\n   * Two-stage Ctrl+A progression (select-all-progression.ts): which stage the last `selectAll`\r\n   * produced, plus the exact `selection`/`activeCell` object identities it produced them against.\r\n   * Any other action that replaces `selection` or `activeCell` (selectCell/extendTo/pushRange/\r\n   * selectRow/selectColumn/_moveActiveCell/edits/row ops) changes that identity, so `selectAll`\r\n   * detects the mismatch and restarts from stage 1 — no need to instrument every reset site.\r\n   */\r\n  selectAllStage: { stage: \"region\" | \"all\"; selection: GridSelection; activeCell: CellCoord | null } | null;\r\n  editing: { coord: CellCoord; initialText?: string } | null;\r\n  /** Transient rejection message from the last failed commitCellEdit; cleared on the next startEditing/commit/cancel. */\r\n  editingError: string | null;\r\n  /**\r\n   * Rejection nonce for the current edit session: increments on every rejected commit attempt\r\n   * (the sync `validate` rejection in `commitCellEdit`, or an async schema resolving with issues\r\n   * via `setEditingError`) and resets to 0 on `startEditing`. Editors re-arm their one-shot commit\r\n   * guard when this changes: a REJECTION is the only signal that the edit is still open and a\r\n   * fresh commit attempt is expected, never \"pending cleared\", which also happens on\r\n   * Escape/cancel right before the editor unmounts. The store is the single source of truth for\r\n   * both rejection paths.\r\n   */\r\n  editingRejectionCount: number;\r\n  /**\r\n   * Post-commit server-rejection messages, keyed `\"rowId:columnId\"` — set via\r\n   * `actions.setCellErrors` after an async `onDataChange` round-trip rejects (e.g. a 422), painted\r\n   * with the SAME visual language as a sync `validate` rejection (ring/tint + message,\r\n   * `aria-invalid`). Deliberately NOT part of the edit lifecycle: `setCellErrors`/`clearCellErrors`\r\n   * never touch `data`, never emit a `DataChange`/`onDataChange`, and are excluded from history —\r\n   * this is metadata ABOUT a cell, not a data change. Auto-cleared for a cell the instant ANY write\r\n   * path (`commitCellEdit`, `commitCellValue`, `applyCellUpdates`, `updateCells`, `deleteSelection`)\r\n   * successfully commits a new value there — the user fixed it, so the stale error must not linger;\r\n   * the server can always re-set it if the fix is still wrong. Pruned of any rowId no longer present\r\n   * in `data` on every row-shape change (delete/duplicate/insert/a consumer `data` replacement) —\r\n   * same maintenance discipline as `RowIndexCache`. Always {@link EMPTY_CELL_ERRORS} when empty, so\r\n   * `useDataGridCellErrors`-style reads never allocate on the common no-error path.\r\n   */\r\n   cellErrors: ReadonlyMap<string, string>;\r\n   /**\r\n    * Transient \"highlight-what-changed\" keys ({@link flashCellKey}, view-space) currently flashing,\r\n    * auto-cleared per key after `flashCells`'s duration (default 1400 ms — the 1.2 s fade pulse\r\n    * finishes before the key lifts, so the cell never visibly snaps back). NOT a data change: no\r\n    * `DataChange`, no history entry. Always {@link EMPTY_FLASHING_CELLS} identity when empty.\r\n    */\r\n   flashingCells: ReadonlySet<string>;\r\n   /** Always defined: defaults to {@link EMPTY_OVERLAY_PLUGINS} so `useDataGridOverlayPlugins` never returns undefined. See `overlayPlugins`'s doc comment (DataGridSyncProps). */\r\n  overlayPlugins: readonly OverlayPlugin[];\r\n  /** Always defined: defaults to {@link EMPTY_ROW_BANDS} (zero-length top/bottom, `render` never called) so `useDataGridRowBands` never returns undefined. See `rowBands`'s doc comment (DataGridSyncProps). */\r\n  rowBands: RowBandsSpec;\r\n  lastHighlightedRow: number | null;\r\n  lastHighlightedCol: number | null;\r\n  columnWidths: Record<string, number>;\r\n  /** null = follow the `columns` prop order. */\r\n  columnOrder: string[] | null;\r\n  /** Single source of truth for visibility; seeded with the def-level `hidden` ids at creation and re-seeded when a new `columns` array is passed. */\r\n  hiddenColumns: readonly string[];\r\n  sortState: SortSpec[];\r\n  filterState: FilterSpec[];\r\n  /** How `filterState`'s rows combine; default `\"and\"`. See `FilterJoinOperator`. */\r\n  joinOperator: FilterJoinOperator;\r\n  /** Quick-search text; never affects `viewIndex` (search highlights + navigates, it doesn't filter). */\r\n  searchText: string;\r\n  /**\r\n   * Whether `sortState`/`filterState`/`joinOperator`/`searchText` are controlled (their sync prop\r\n   * was defined on the last `_syncProps`), set once per sync and read by `toggleSort`/`setSorts`/\r\n   * `setFilters`/`setJoinOperator`/`setSearch` (server escape hatch). Controlled mode:\r\n   * the action still fires its `onXChange` callback but does NOT write the corresponding state\r\n   * field itself — `_syncProps` is the only writer once the consumer's prop (and thus the store)\r\n   * actually changes, standard controlled-input semantics (a callback-ignoring consumer's grid\r\n   * stays visually fixed).\r\n   */\r\n  sortControlled: boolean;\r\n  filterControlled: boolean;\r\n  joinOperatorControlled: boolean;\r\n  searchControlled: boolean;\r\n  /** Every cell matching `searchText`, view-space row-major order; computed once per `setSearch`/`_syncProps`/`setSorts`/`setFilters`, capped at 1000 hits with early exit. */\r\n  searchMatches: SearchMatch[];\r\n  /** `searchMatches` as a `\"${viewRow}:${columnId}\"` Set for O(1) per-cell highlight lookup ({@link useDataGridIsSearchMatch}). */\r\n  searchMatchSet: ReadonlySet<string>;\r\n  /**\r\n   * `searchMatches` grouped by view row -> matched column indices, for {@link computeRowCellState}'s\r\n   * per-row derivation (perf audit 2026-07-17: the previous per-row `for` loop over every visible\r\n   * column, each doing a `searchMatchSet.has(`${row}:${columnId}`)` string-key lookup, cost ~44ms/2000\r\n   * ticks at 100k rows/8 cols with an active search — measured 29x faster building this map once per\r\n   * search-text change and doing an O(1) `Map.get` per row per tick instead).\r\n   */\r\n  searchMatchRows: ReadonlyMap<number, ReadonlySet<number>>;\r\n  /** True when `searchMatches` hit the 1000-match cap (more matches exist but weren't collected). */\r\n  searchMatchesCapped: boolean;\r\n  /** Recomputed by _syncProps/toggleSort/setSorts/setFilters only — search no longer affects it. */\r\n  viewIndex: number[];\r\n  /**\r\n   * True when at least one deferred `updateCells`/`updateRows` batch changed a value that the active\r\n   * sort or filter reads, so the displayed order no longer matches the data. Drives a \"re-sort\"\r\n   * affordance; `reconcileView()` (or any sort/filter/search change) clears it. See\r\n   * {@link useDataGridViewStale}.\r\n   */\r\n  viewStale: boolean;\r\n  /** Recomputed by _syncProps/setColumnOrder-ish actions that touch order/visibility. */\r\n  visibleColumns: readonly AnyColumnDef[];\r\n  /**\r\n   * Imperative scroll-into-view, registered by the mounted `DataGridRoot` (null before mount/after\r\n   * unmount). Lets add-ons outside the root's subtree (e.g. `data-grid-toolbar`'s search, which sits\r\n   * as a `DataGridRoot` sibling) move the viewport without reaching into\r\n   * root-internal refs. See {@link useDataGridScrollToCell}.\r\n   */\r\n  scrollToCellImpl: ((coord: CellCoord) => void) | null;\r\n  /**\r\n   * Fill-handle keymap handlers, registered by the `data-grid-fill` add-on's tracker\r\n   * component — which must render somewhere inside `DataGridRoot`'s subtree to reach `scrollRef`/\r\n   * layout via `useDataGridRootContext()`, a level `DataGridRoot`'s OWN `useGridInteraction` call\r\n   * (which needs these three callbacks) sits above. Registering through the store the same way\r\n   * `scrollToCellImpl` does resolves that ordering: `DataGridRoot` reads this slot and forwards it\r\n   * to `useGridInteraction` on every render; `null` (pre-mount, post-unmount, or add-on absent)\r\n   * makes mod+D/mod+R/Escape-mid-drag the documented no-ops.\r\n   */\r\n  fillHandlers: { fillDown: () => void; fillRight: () => void; cancelFillDrag: () => void } | null;\r\n  /**\r\n   * The `data-grid-presence` add-on's view-space activeness predicate, registered on mount and\r\n   * cleared on unmount (same registration pattern as `fillHandlers`): answers \"is any VIEW-space\r\n   * (range-form) presence entry active right now\". View-space entries pin to a display position,\r\n   * so a row-moving op (`reorderRows`, `insertRows`, `deleteRows`, `duplicateRows`, `updateCells`\r\n   * with `reorder: \"immediate\"`) dev-warns once per grid when it runs while the predicate reports true; rowId-native entries track their rows through\r\n   * reorders and never trip it. `null` before mount/after unmount, or when the add-on is absent.\r\n   */\r\n  presenceViewSpaceActive: (() => boolean) | null;\r\n  /**\r\n   * Mirrors `DataGridRoot`'s `readOnly` prop, registered on mount, so\r\n   * mutation surfaces outside the root's subtree (context menu, `useDataGridClipboard`) can see it\r\n   * too — the root prop alone only reached its own local `useGridInteraction`/`useGridClipboard`.\r\n   * `false` before mount/after unmount.\r\n   */\r\n  readOnly: boolean;\r\n  /**\r\n   * Mirrors `DataGridRoot`'s effective keymap — `DEFAULT_KEYMAP` merged with its `keymap` prop —\r\n   * registered on mount (backing the `data-grid-keybindings` add-on) so surfaces outside\r\n   * the root's subtree (e.g. the keybindings dialog) can read the single source of truth for\r\n   * bindings instead of re-deriving it. `DEFAULT_KEYMAP` before mount/after unmount.\r\n   */\r\n  keymap: Keymap;\r\n  /** {@link DEFAULT_LABELS} deep-merged with the `labels` sync prop; see {@link useDataGridLabels}. */\r\n  labels: DataGridLabels;\r\n  actions: DataGridActions;\r\n};\r\n\r\n/** Mutations for a single grid instance, nested under the stable `actions` key. */\r\nexport type DataGridActions = {\r\n  /**\r\n   * Moves ONLY the active cell, leaving the selection (range, row and column channels) untouched,\r\n   * and fires NO `onSelectionChange` (contrast `selectCell`, which sets a single-cell selection\r\n   * and fires it). After Escape/`clearSelection`, `selection.current` is `null` while `activeCell`\r\n   * still exists — read the active cell from `activeCell`, not from `selection.current.cell`.\r\n   */\r\n  setActiveCell(coord: CellCoord | null): void;\r\n  selectCell(coord: CellCoord): void;\r\n  extendTo(coord: CellCoord): void;\r\n  extendSelection(direction: ExtendDirection, opts?: { toEdge?: boolean }): void;\r\n  pushRange(coord: CellCoord): void;\r\n  selectRow(index: number, opts?: SelectLineActionOptions): void;\r\n  selectColumn(index: number, opts?: SelectLineActionOptions): void;\r\n  selectAll(): void;\r\n  /** Checkbox marker: sets one row's membership in the rows channel absolutely (add if `checked`, remove otherwise) — never a toggle — independent of the last-highlighted/range mechanics `selectRow` uses for click gestures. No-op when `enableRowSelection` is false. */\r\n  setRowSelected(index: number, checked: boolean): void;\r\n  /** Sets the row-drag anchor without touching the rows/columns selection channel — lets a checkbox-marker press arm a potential row-range drag while leaving a stationary click's own toggle untouched. No-op when `enableRowSelection` is false. */\r\n  armRowDragAnchor(index: number): void;\r\n  /** Header marker select-all checkbox: sets every view row's membership in the rows channel at once. No-op when `enableRowSelection` is false. */\r\n  setAllRowsSelected(checked: boolean): void;\r\n  clearSelection(): void;\r\n  /**\r\n   * Live per-frame width write during a resize drag; fires `onColumnResizing` (not\r\n   * `onColumnLayoutChange` — see `commitColumnWidth` for the commit point). The width is clamped\r\n   * to the column's legal range (`[max(32, minWidth), maxWidth]`, the same bounds the resize\r\n   * gesture enforces); a callback-observed width is the clamped one.\r\n   */\r\n  setColumnWidth(id: string, width: number): void;\r\n  /**\r\n   * Sets the column's width AND fires `onColumnLayoutChange` once — the resize-drag-release/autosize\r\n   * commit point. The width is clamped exactly as {@link setColumnWidth} clamps, and the fired\r\n   * snapshot carries the clamped value.\r\n   */\r\n  commitColumnWidth(id: string, width: number): void;\r\n  /**\r\n   * Drops a column's width override, restoring its def `width` (and re-joining `flex` distribution\r\n   * — a manually resized flex column leaves it while an override is set). No-op without an\r\n   * override. Fires `onColumnLayoutChange` once, with the override gone from `widths`.\r\n   */\r\n  resetColumnWidth(id: string): void;\r\n  /**\r\n   * Reorders visible columns. `id` moves to sit immediately before/after `targetId` (per `position`).\r\n   * Pinned columns only reorder within their own pin zone (left/right/unpinned) — a cross-zone\r\n   * request is a no-op recompute (dropping a left-pinned column onto the unpinned band does nothing).\r\n   * No-op when the column's own `reorderable: false` or the grid-wide `enableColumnReorder` is false.\r\n   * Fires `onColumnLayoutChange` once on an actual move.\r\n   */\r\n  setColumnOrder(id: string, targetId: string, position: \"before\" | \"after\"): void;\r\n  /** Pins/unpins a column (`null` = unpinned). No-op when the column's `pinnable: false` or grid-wide `enableColumnPinning` is false. Fires `onColumnLayoutChange` once. */\r\n  setColumnPin(id: string, pin: \"left\" | \"right\" | null): void;\r\n  /**\r\n   * Shows/hides a column via the `hiddenColumns` set; works on a def-level `hidden: true` column\r\n   * — `setColumnHidden(id, false)` re-shows it, and the def's flag re-applies only when a NEW\r\n   * `columns` array is passed (a same-reference re-render keeps the user's choice).\r\n   * Fires `onColumnLayoutChange` once.\r\n   */\r\n  setColumnHidden(id: string, hidden: boolean): void;\r\n  toggleSort(columnId: string, additive: boolean): void;\r\n  setSorts(sorts: SortSpec[]): void;\r\n  setFilters(filters: FilterSpec[]): void;\r\n  /** Sets how `filterState`'s rows combine (\"and\" every filter must match, \"or\" any one does). */\r\n  setJoinOperator(joinOperator: FilterJoinOperator): void;\r\n  /** Sets quick-search text and recomputes `searchMatches`/`searchMatchSet`/`searchMatchesCapped` once; never touches `viewIndex`. */\r\n  setSearch(text: string): void;\r\n  /** Enters edit mode at `coord` (view-space); no-op when the column is readOnly or its type is unregistered. */\r\n  startEditing(coord: CellCoord, initialText?: string): void;\r\n  /** Discards the in-progress edit without emitting a change. */\r\n  cancelEditing(): void;\r\n  /**\r\n   * Validates and commits the in-progress edit; rejects (keeps editing, sets editingError) or emits\r\n   * one DataChange and moves activeCell by `movement`. `rejection` is the async validation layer's\r\n   * awaited schema rejection for an `onInvalid: \"warn\"` column: the commit's sync re-run cannot see\r\n   * a schema Promise's issues, so the layer forwards the message to flag the cell.\r\n   */\r\n  commitCellEdit(value: unknown, movement?: { dx: number; dy: number }, rejection?: string): void;\r\n  /** Direct-write path for types with no edit mode (checkbox): validates and commits `value` at `coord` without requiring `startEditing` first, and never moves the active cell. */\r\n  commitCellValue(coord: CellCoord, value: unknown): void;\r\n  /**\r\n   * Sets `editingError` and bumps `editingRejectionCount` (the editors' commit-guard re-arm\r\n   * nonce) without touching `editing`/`data` — used by the async Standard Schema commit path (see\r\n   * cell.tsx/use-async-validate.ts): an async `validate` resolves AFTER `commitCellEdit` would\r\n   * have run, so the editor-commit layer calls this directly instead of going through the\r\n   * (synchronous) commit action. No-op if editing has since ended (race guard is the caller's\r\n   * job — see the generation counter in use-async-validate.ts).\r\n   */\r\n  setEditingError(message: string): void;\r\n  /**\r\n   * Merges `errors` into `cellErrors` per-key (an existing entry for a cell not named here\r\n   * survives untouched). NOT a data change: no `DataChange`, no history entry, no `onDataChange`\r\n   * echo — see `cellErrors`' doc comment. The typical caller is an `onDataChange` handler's\r\n   * `.catch()` after a server 422, mapping its field errors to `{ rowId, columnId, message }[]`.\r\n   */\r\n  setCellErrors(errors: readonly CellErrorEntry[]): void;\r\n  /** Clears `cellErrors` for `targets`, or every entry when `targets` is omitted. Same non-data-change contract as `setCellErrors`. */\r\n  clearCellErrors(targets?: readonly CellErrorTarget[]): void;\r\n  /**\r\n   * Puts the given {@link flashCellKey} keys into a transient ~1.2 s fade pulse (the\r\n   * \"highlight-what-changed\" effect a fill/paste/move gesture plays on the cells it just wrote).\r\n   * A key already flashing gets its timer RESET, so overlapping gestures each get the full pulse.\r\n   * NOT a data change — no `DataChange`, no history, no selection movement.\r\n   */\r\n  flashCells(keys: readonly string[], durationMs?: number): void;\r\n  /** Clears every non-readOnly cell in the current selection to its type's clearValue(), as one batch DataChange. */\r\n  deleteSelection(): void;\r\n  /**\r\n   * The one generic multi-cell write path: applies `updates` (view-space coords via `viewRow`) as\r\n   * a single `source`-tagged DataChange batch. Dedupes multiple updates to the same row/column\r\n   * (last write wins), skips readOnly cells and no-op writes, preserves identity of untouched rows,\r\n   * suppresses an empty batch entirely, and moves the selection to cover the touched view rect.\r\n   */\r\n  applyCellUpdates(updates: { viewRow: number; columnId: string; value: unknown }[], source: DataChange<unknown>[\"source\"]): void;\r\n  /**\r\n   * The streaming write path: applies id-keyed {@link CellPatch}es in ONE `set()` and skips the\r\n   * `computeViewIndex`/`computeSearchMatches` rebuild that a `data` prop replacement forces. This is\r\n   * what makes streaming under an active sort viable — a controlled `data` replacement costs ~56 ms\r\n   * per tick at 100k rows with one sort column, against ~0.15 ms here.\r\n   *\r\n   * Unlike `applyCellUpdates` it never moves the selection, the active cell, or the editing session,\r\n   * so a live feed cannot pull the grid out from under a user mid-edit. Emits one batched\r\n   * `{source: \"stream\"}` `DataChange` through `onDataChange`, in both controlled and uncontrolled\r\n   * mode. See {@link UpdateCellsOptions} for the sort/filter interaction.\r\n   */\r\n  updateCells(patches: readonly CellPatch[], options?: UpdateCellsOptions): UpdateCellsVerdict;\r\n  /** {@link updateCells} keyed by whole row: each {@link RowPatch}'s `changes` expands to one patch per column id. */\r\n  updateRows(updates: readonly RowPatch[], options?: UpdateCellsOptions): UpdateCellsVerdict;\r\n  /** Rebuilds the view index that a deferred `updateCells` postponed, and clears `viewStale`. No-op when the view is not stale. */\r\n  reconcileView(): void;\r\n  /**\r\n   * Inserts one row built by the `createRow` sync prop above/below `viewRowIndex` (view-space).\r\n   * External name that maps to the single internal batch path - it runs\r\n   * {@link insertRows} with count 1, so both share one implementation, one `onDataChange`,\r\n   * and one undo step.\r\n   */\r\n  insertRow(viewRowIndex: number, position: \"above\" | \"below\"): void;\r\n  /**\r\n   * Inserts `count` rows built by the `createRow` sync prop (called with `dataRowIndex + i`)\r\n   * above/below `viewRowIndex` (view-space) as ONE `{source: 'row-op'}` DataChange with one\r\n   * id-keyed `insert` op per row - a single undo entry. Dev-warning no-op when `createRow` is\r\n   * absent; silent no-op for `count <= 0`.\r\n   */\r\n  insertRows(viewRowIndex: number, count: number, position?: \"above\" | \"below\"): void;\r\n  /** Deletes the rows at `viewRowIndexes` (view-space) as one `{source: 'row-op'}` DataChange with id-keyed `delete` ops. */\r\n  deleteRows(viewRowIndexes: number[]): void;\r\n  /**\r\n   * Duplicates the rows at `viewRowIndexes` (view-space), inserting each copy directly after its\r\n   * source row, as one `{source: 'row-op'}` DataChange with id-keyed `insert` ops. Dev-warning\r\n   * no-op when the `duplicateRow` sync prop is absent — required so\r\n   * every duplicated row gets a distinct id, never colliding with its source's React key.\r\n   */\r\n  duplicateRows(viewRowIndexes: number[]): void;\r\n  /**\r\n   * Moves the row at view index `from` so it lands at view index `to` (final position — arrayMove\r\n   * semantics: `from < to` shifts rows `from+1..to` up one, `from > to` shifts rows `to..from-1`\r\n   * down one). One `{source: 'row-op'}` DataChange with a single id-keyed `move` op (one undo\r\n   * entry). No-ops (silent, dev-warned where the cause is a misconfiguration): `enableRowReorder`\r\n   * off, readOnly, an open edit session (the editor pins a view coordinate the move would\r\n   * invalidate), an active sort/filter (the view order is owned by the sort/filter, not the data),\r\n   * unloaded (lazy) rows (a reorder would shift the lazy add-on's index bookkeeping), and a drop\r\n   * that leaves the row where it already is. Returns `true` when the row moved, `false` for any\r\n   * no-op — the drag gesture uses the verdict for its a11y announcement.\r\n   */\r\n  reorderRows(from: number, to: number): boolean;\r\n  /** @internal keyboard nav helper: moves/extends the active cell by a view-space delta, clamped to view bounds. */\r\n  _moveActiveCell(d: { dx: number; dy: number }, opts?: { extend?: boolean; retain?: boolean }): void;\r\n  /** @internal syncs live consumer props and recomputes derived state; not part of the public hook surface. */\r\n  _syncProps(props: InternalSyncProps): void;\r\n  /** @internal `DataGridRoot` registers/clears its scroll-into-view on mount/unmount; not part of the public hook surface. */\r\n  _registerScrollToCell(impl: ((coord: CellCoord) => void) | null): void;\r\n  /** @internal `DataGridRoot` syncs its `readOnly` prop into the store on every render/unmount; not part of the public hook surface. */\r\n  _registerReadOnly(readOnly: boolean): void;\r\n  /** @internal `DataGridRoot` syncs its effective (merged) keymap into the store on every render/unmount; not part of the public hook surface. */\r\n  _registerKeymap(keymap: Keymap): void;\r\n  /** @internal the `data-grid-fill` add-on's tracker component registers/clears its keymap handlers on mount/unmount; not part of the public hook surface. */\r\n  _registerFillHandlers(handlers: { fillDown: () => void; fillRight: () => void; cancelFillDrag: () => void } | null): void;\r\n  /**\r\n   * @internal the `data-grid-presence` add-on registers/clears its view-space-activeness predicate\r\n   * (see `presenceViewSpaceActive`) on mount/unmount; row-moving ops dev-warn once when they run\r\n   * while it reports true. Not part of the public hook surface.\r\n   */\r\n  _registerPresenceViewSpaceActive(impl: (() => boolean) | null): void;\r\n  /**\r\n   * @internal Drops the flash keys whose view row left `keptViewRows` (the body's rendered window),\r\n   * so a cell that scrolls out and back in never replays its one-shot pulse. Not part of the\r\n   * public hook surface.\r\n   */\r\n  _pruneFlashingCells(keptViewRows: readonly number[]): void;\r\n};\r\n\r\n/** Full store shape: interaction state + the stable actions object. */\r\nexport type DataGridStore = DataGridStoreState;\r\n\r\n/**\r\n * Everything `computeViewIndex`/`computeSearchMatches`/`computeVisibleColumns` read on a\r\n * `_syncProps` sync, captured by reference. All fields `===`-equal to the previous sync's means\r\n * their outputs (`viewIndex`/`searchMatches`/`visibleColumns`) are also unchanged, so `_syncProps`\r\n * can skip recomputing them and reuse the previous references (a controlled parent re-rendering\r\n * for unrelated reasons — e.g. its own local state — shouldn't pay full buildViewIndex cost).\r\n */\r\nexport type SyncInputs = {\r\n  data: readonly unknown[];\r\n  columns: readonly AnyColumnDef[];\r\n  /** Sort reads each column's cell-type `compare`, so a swapped registry changes `viewIndex`. */\r\n  cellTypes: Record<string, CellType>;\r\n  sortState: SortSpec[];\r\n  filterState: FilterSpec[];\r\n  joinOperator: FilterJoinOperator;\r\n  searchText: string;\r\n  columnOrder: string[] | null;\r\n  hiddenColumns: readonly string[];\r\n};\r\n\r\nexport function syncInputsEqual(a: SyncInputs, b: SyncInputs): boolean {\r\n  return (\r\n    a.data === b.data &&\r\n    a.columns === b.columns &&\r\n    a.cellTypes === b.cellTypes &&\r\n    a.sortState === b.sortState &&\r\n    a.filterState === b.filterState &&\r\n    a.joinOperator === b.joinOperator &&\r\n    a.searchText === b.searchText &&\r\n    a.columnOrder === b.columnOrder &&\r\n    a.hiddenColumns === b.hiddenColumns\r\n  );\r\n}\r\n\r\n/** Result of a successful direct-write commit; `null` state fields mean \"leave editing/editingError untouched\". */\r\n/**\r\n * `warnings` (only on the success/noop arms) carries `onInvalid: \"warn\"` rejections that committed\r\n * anyway — the store merges them into `cellErrors` AFTER the gesture's auto-clear and `validateRow`\r\n * verdict, so the freshest per-cell signal wins.\r\n */\r\nexport type CommitResult =\r\n  | { data: readonly unknown[]; change: DataChange<unknown>; warnings?: readonly CellErrorEntry[] }\r\n  | { error: string }\r\n  | { noop: true; warnings?: readonly CellErrorEntry[] };\r\n\r\n/** Per-row accumulator: the row as edited so far, plus its per-column cell deltas (columnId -> entry). */\r\nexport type RowEdit = { row: unknown; cells: Map<string, { columnId: string; value: unknown; prev: unknown }> };\r\n\r\n/** Props for {@link DataGridProvider}. */\r\nexport type DataGridProviderProps<TData = unknown> = DataGridSyncProps<TData> & {\r\n  children: ReactNode;\r\n  /**\r\n   * A consumer-created store (from `useDataGridStoreProps`) to serve to the subtree instead\r\n   * of self-creating one. When set, the provider becomes a pure context shell: no store\r\n   * creation, no prop sync, no subscription wiring — the owner's `useDataGridStoreProps` call\r\n   * drives the store and must pass the same live props.\r\n   */\r\n  store?: StoreApi<DataGridStoreState>;\r\n};\r\n\r\n/** {@link useDataGridCellState}'s return shape — the 5 primitives `DataGridCell` reads every render. */\r\nexport type DataGridCellState = {\r\n  isActive: boolean;\r\n  /** True for any selected cell (active cell, inside the primary range/range stack, or its row/column channel) — drives `aria-selected` (WAI-ARIA grid pattern: every selected cell reports it, not just the focused one). */\r\n  isSelected: boolean;\r\n  isEditing: boolean;\r\n  initialText: string | undefined;\r\n  isSearchMatch: boolean;\r\n  /** This cell's `cellErrors` message, or null when it has none — same visual language as a sync `validate` rejection. */\r\n  cellError: string | null;\r\n};\r\n\r\n/**\r\n * {@link useDataGridRowCellState}'s return shape — one row's worth of interactive cell state,\r\n * derived ONCE per row instead of once per cell. `selectedColRanges` is a rectangle-based summary\r\n * (a row's selected columns are a small union of `[start,end)` runs), so it stays cheap to compute\r\n * and to compare by content.\r\n */\r\nexport type DataGridRowCellState = {\r\n  /** The active cell's column in this row, or null if the active cell isn't in this row. */\r\n  activeCol: number | null;\r\n  /** The editing cell's column in this row, or null if editing isn't happening in this row. */\r\n  editingCol: number | null;\r\n  /** Only set when `editingCol` is non-null (mirrors {@link useDataGridCellInitialText}'s per-cell scoping). */\r\n  editingInitialText: string | undefined;\r\n  /** Every column index in this row matching the current search, or null when search is inactive/no row match — null (not an empty Set) lets a row with no matches skip allocating one. */\r\n  searchMatchCols: ReadonlySet<number> | null;\r\n  /** This row's errored columns (column index -> message), or null when this row has none — same zero-render contract as `searchMatchCols`. */\r\n  errorCols: ReadonlyMap<number, string> | null;\r\n  /** This row's currently-flashing columns (transient write-pulse keys), or null when none — same zero-render contract as `searchMatchCols`. */\r\n  flashingCols: ReadonlySet<number> | null;\r\n  selectedColRanges: readonly ColRange[];\r\n};\r\n",
      "type": "registry:component",
      "target": "components/data-grid/store/types.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/types.ts",
      "content": "import type { FC, ReactNode } from \"react\";\r\nimport type { KeyBinding } from \"./keyboard/key-syntax\";\r\n\r\nexport type { KeyBinding };\r\n\r\n/**\r\n * Vendored from `@standard-schema/spec` v1 (https://github.com/standard-schema/standard-schema) so\r\n * shipped registry code has zero runtime/type dependency on the package — the same choice TanStack\r\n * Form makes for the identical reason (references/form's standardSchemaValidator.ts). The spec is\r\n * frozen at version 1, so drift risk is ~nil; a type-level conformance test (data-grid.type-test.ts)\r\n * asserts this vendored copy stays assignable to/from the real package's type in both directions,\r\n * so any future drift fails `tsc` instead of silently diverging. `@standard-schema/spec` is still a\r\n * repo devDependency (types-only, for that conformance test) — never imported by shipped code.\r\n */\r\nexport type StandardSchemaV1<Input = unknown, Output = Input> = {\r\n  /** The Standard Schema properties. */\r\n  readonly \"~standard\": StandardSchemaV1Props<Input, Output>;\r\n};\r\n\r\ninterface StandardSchemaV1Props<Input = unknown, Output = Input> {\r\n  /** The version number of the standard. */\r\n  readonly version: 1;\r\n  /** The vendor name of the schema library. */\r\n  readonly vendor: string;\r\n  /** Validates unknown input values. */\r\n  readonly validate: (value: unknown) => StandardSchemaV1Result<Output> | Promise<StandardSchemaV1Result<Output>>;\r\n  /** Inferred types associated with the schema. */\r\n  readonly types?: StandardSchemaV1Types<Input, Output> | undefined;\r\n}\r\n\r\n/** Result of one `\"~standard\".validate()` call: `issues` set means failure, never thrown. */\r\nexport type StandardSchemaV1Result<Output> = StandardSchemaV1SuccessResult<Output> | StandardSchemaV1FailureResult;\r\n\r\ninterface StandardSchemaV1SuccessResult<Output> {\r\n  /** The typed output value. */\r\n  readonly value: Output;\r\n  /** A falsy value for `issues` indicates success. */\r\n  readonly issues?: undefined;\r\n}\r\n\r\ninterface StandardSchemaV1FailureResult {\r\n  /** The issues of failed validation. */\r\n  readonly issues: ReadonlyArray<StandardSchemaV1Issue>;\r\n}\r\n\r\n/** One validation failure; cell values are scalars so `path` (when present) is a flat dot-joined prefix. */\r\nexport interface StandardSchemaV1Issue {\r\n  /** The error message of the issue. */\r\n  readonly message: string;\r\n  /** The path of the issue, if any. */\r\n  readonly path?: ReadonlyArray<PropertyKey | StandardSchemaV1PathSegment> | undefined;\r\n}\r\n\r\ninterface StandardSchemaV1PathSegment {\r\n  /** The key representing a path segment. */\r\n  readonly key: PropertyKey;\r\n}\r\n\r\ninterface StandardSchemaV1Types<Input = unknown, Output = Input> {\r\n  /** The input type of the schema. */\r\n  readonly input: Input;\r\n  /** The output type of the schema. */\r\n  readonly output: Output;\r\n}\r\n\r\n// Namespace-merged onto the `StandardSchemaV1` type (matching the official package's own\r\n// declaration, and how consumers write `StandardSchemaV1.InferInput<S>`) — not a general-purpose\r\n// namespace, so the lint rule's usual \"prefer ES modules\" concern doesn't apply here.\r\n// eslint-disable-next-line @typescript-eslint/no-namespace\r\nexport namespace StandardSchemaV1 {\r\n  /** Infers the input type of a Standard Schema. */\r\n  export type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema[\"~standard\"][\"types\"]>[\"input\"];\r\n  /** Infers the output type of a Standard Schema. */\r\n  export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema[\"~standard\"][\"types\"]>[\"output\"];\r\n}\r\n\r\n// `keyof TData & string` alone resolves to `never` (not `string`) when TData=unknown — unlike\r\n// `never`, where `keyof never` is the universal key type `string | number | symbol` — so the\r\n// erasure boundary's `ColumnDef<unknown, ...>` (see store.tsx's AnyColumnDef) needs this special\r\n// case, else `accessorKey` collapses to an uninhabited `undefined` field.\r\nexport type AccessorKeyOf<TData> = unknown extends TData ? string : keyof TData & string;\r\n\r\n/** Cell coordinate in data space: col/row are 0-based data indices (marker columns excluded). */\r\nexport type CellCoord = { col: number; row: number };\r\n\r\n/** What a marker mode renders: the 1-based view row index, the row-select checkbox, or both (number swapped for the checkbox on hover/selected). */\r\nexport type RowMarkersContent = \"number\" | \"checkbox\" | \"both\";\r\n\r\n/**\r\n * Marker column mode: a pinned-left column rendered BEFORE all data columns, outside the data\r\n * column index space (CellCoord.col / aria-colindex / aria-colcount are all untouched by it).\r\n * The plain modes (`number`/`checkbox`/`both`) render the selection chrome only and keep the\r\n * marker a pure row-select surface — a drag there is always the row-range select. The\r\n * `reorder`-prefixed family renders the grip handle on top of the suffix's content and is the\r\n * ONLY family that arms the drag-to-reorder gesture (still gated grid-wide by `enableRowReorder`),\r\n * and the press ZONE decides the gesture: the grip reorders on drag (selects on a stationary\r\n * press), while the checkbox glyph and the number/rest of the cell stay pure row-select.\r\n */\r\nexport type RowMarkersMode = \"none\" | RowMarkersContent | \"reorder\" | `reorder-${RowMarkersContent}`;\r\n\r\n/** Rectangular cell region, half-open on the far edge (a cell c is inside if x <= c < x + width). */\r\nexport type GridRect = { x: number; y: number; width: number; height: number };\r\n\r\n/**\r\n * Selection model (see research/glide-behavior-spec.md §1).\r\n * `cell` is the anchor (Excel's white cell) and is always inside `range`.\r\n * `rangeStack` holds additional rectangles from ctrl-click multi-range; never contains `range`.\r\n * `rows`/`columns` are whole-row/column selection channels, distinct from `current`.\r\n */\r\nexport type GridSelection = {\r\n  current: {\r\n    cell: CellCoord;\r\n    range: GridRect;\r\n    rangeStack: GridRect[];\r\n  } | null;\r\n  rows: CompactSelectionLike;\r\n  columns: CompactSelectionLike;\r\n};\r\n\r\n/** Structural interface of CompactSelection so types.ts stays dependency-free. */\r\nexport type CompactSelectionLike = {\r\n  readonly length: number;\r\n  hasIndex(index: number): boolean;\r\n  hasAll(range: [number, number]): boolean;\r\n  toArray(): number[];\r\n};\r\n\r\n/**\r\n * Selection-gesture configuration (PLAN §3 \"Selection configurability\"). All default `true`.\r\n * Disabling a channel removes both its gestures (header/marker click, drag) AND its selection\r\n * channel: e.g. `enableColumnSelection: false` makes header clicks a no-op, not just un-rendered.\r\n */\r\nexport type SelectionConfig = {\r\n  enableRowSelection: boolean;\r\n  enableColumnSelection: boolean;\r\n  enableRangeSelection: boolean;\r\n  enableMultiRange: boolean;\r\n};\r\n\r\n/** One data mutation. Ops are id-keyed so history survives sort/filter (never index-keyed). */\r\nexport type DataOp<TData> =\r\n  | {\r\n      type: \"update\";\r\n      rowId: string;\r\n      row: TData;\r\n      prev: TData;\r\n      /** Per-cell detail when known (cell edits, paste, fill); absent for whole-row updates. */\r\n      cells?: { columnId: string; value: unknown; prev: unknown }[];\r\n    }\r\n  | { type: \"insert\"; rowId: string; row: TData; index: number }\r\n  | { type: \"delete\"; rowId: string; row: TData; index: number }\r\n  | {\r\n      type: \"move\";\r\n      rowId: string;\r\n      row: TData;\r\n      /** The row's position before the move. */\r\n      from: number;\r\n      /** The row's position after the move (final index; `applyChange` deletes by id and places the row at output slot `to`, `invertChange` swaps `from`/`to`). */\r\n      to: number;\r\n    };\r\n\r\n/** One user gesture = one batch (a paste, a fill, a delete-range is a single entry). */\r\nexport type DataChange<TData> = {\r\n  ops: DataOp<TData>[];\r\n  /**\r\n   * Origin of the change, for history labeling and consumer filtering. `\"stream\"` tags a\r\n   * `updateCells`/`updateRows` batch; `useDataGridHistory` drops it by default, because a\r\n   * 100-updates/s feed would evict the user's whole undo stack in seconds. `\"app\"` tags an\r\n   * imperative `record()` entry (a programmatic change the consumer applied itself).\r\n   */\r\n  source: \"edit\" | \"paste\" | \"fill\" | \"delete\" | \"row-op\" | \"import\" | \"history\" | \"stream\" | \"app\";\r\n  /** Optional human-readable label for this entry (e.g. \"Batch save rollback\"); carried through the undo/redo stack as-is. */\r\n  label?: string;\r\n};\r\n\r\nexport type CellRenderProps<TData, TValue> = {\r\n  /** The resolved cell value (through the column's `accessorKey`/`accessorFn`). */\r\n  value: TValue;\r\n  /** The full data row this cell belongs to. */\r\n  row: TData;\r\n  /** Display (view-space) row index: the sorted/filtered position, matching what the user sees. NOT a stable index into `data` — it reshuffles under sort and filter. Use `getRowId` for stable row identity. */\r\n  rowIndex: number;\r\n  // `column`'s own validate erases independently (3rd param `any`) — same reasoning as\r\n  // CellClassNameCtx.column below; without it, renderCell's own bivariance gets re-poisoned by\r\n  // validate's union whenever TValue is erased to `unknown` at the array-common-type boundary.\r\n  column: ColumnDef<TData, TValue, any>; // eslint-disable-line @typescript-eslint/no-explicit-any\r\n  /** True while this cell is the active cell. */\r\n  isActive: boolean;\r\n};\r\n\r\n/** Context passed to the grid-level {@link ColumnDef}-agnostic `getCellClassName` prop. */\r\nexport type CellClassNameCtx<TData, TValue> = {\r\n  value: TValue;\r\n  row: TData;\r\n  // `column`'s own validate erases independently (3rd param `any`) so THIS type's bivariance\r\n  // (recovered via CellClassNameFnHolder below) doesn't get re-poisoned by validate's union when\r\n  // TValue itself is erased to `unknown` at the array-common-type boundary — see types.ts's\r\n  // ColumnDef.validate doc and store.tsx's AnyColumnDef doc for the full reasoning.\r\n  column: ColumnDef<TData, TValue, any>; // eslint-disable-line @typescript-eslint/no-explicit-any\r\n  /** Display (view-space) row index — the sorted/filtered position, matching what the user sees. */\r\n  viewRowIndex: number;\r\n};\r\n\r\n/**\r\n * Grid-level row class callback (PLAN §6 \"Programmatic style API\"); merged via `cn()` after the\r\n * built-in row classes. `viewRowIndex` is the display (sorted/filtered) position. Pass a stable\r\n * function identity (module scope, or memoized) — like `columns`/`data`, its identity is a dev-mode\r\n * guardrail (unstable identity defeats row memoization) rather than a fresh per-render prop.\r\n */\r\nexport type GetRowClassName<TData> = (row: TData, viewRowIndex: number) => string | undefined;\r\n\r\n/** Grid-level cell class callback; same stable-identity guidance as {@link GetRowClassName}. */\r\nexport type GetCellClassName<TData> = (ctx: CellClassNameCtx<TData, unknown>) => string | undefined;\r\n\r\n/** Context passed to `onCellClick`. Same shape as {@link CellClassNameCtx} plus the column's real index (not just its view row). */\r\nexport type CellClickCtx<TData, TValue> = {\r\n  value: TValue;\r\n  row: TData;\r\n  column: ColumnDef<TData, TValue>;\r\n  /** Display (view-space) row index — matches {@link CellClassNameCtx.viewRowIndex}. */\r\n  rowIndex: number;\r\n  /** Data-column index (not the marker column) — matches `CellCoord.col`. */\r\n  columnIndex: number;\r\n};\r\n\r\n/**\r\n * Fired on a plain click on any non-skeleton, non-pinned-row cell — attached to the cell's own\r\n * existing DOM click handler (no new subscription; see events-state.mdx). Fires alongside\r\n * whatever the click already does (select the cell, toggle a checkbox) — it's a pure\r\n * notification, never a veto point (contrast the `data-grid-fill` add-on's `onFill` veto).\r\n */\r\nexport type OnCellClick<TData> = (ctx: CellClickCtx<TData, unknown>, event: MouseEvent) => void;\r\n\r\n/** Context passed to `onRowClick` — the row is display-index only; map to a stable id via `getRowId` yourself if you need one. */\r\nexport type RowClickCtx<TData> = {\r\n  row: TData;\r\n  /** Display (view-space) row index — matches {@link CellClassNameCtx.viewRowIndex}. */\r\n  rowIndex: number;\r\n};\r\n\r\n/** Same firing site as {@link OnCellClick}, once per click regardless of which column was clicked. */\r\nexport type OnRowClick<TData> = (ctx: RowClickCtx<TData>, event: MouseEvent) => void;\r\n\r\n/**\r\n * Method-shorthand holder purely so {@link ColumnDef.cellClassName}'s function branch can pull its\r\n * signature via indexed access (`CellClassNameFnHolder<...>[\"fn\"]`) instead of writing the function\r\n * type directly in the union — a function type embedded straight in a union property is checked\r\n * contravariantly, but one recovered from a method-shorthand declaration via indexed access keeps\r\n * that declaration's bivariant check, so a `defineColumns` literal's narrow `TValue`-specific\r\n * callback stays assignable to `ColumnDef<TData, unknown>` (see column-helpers.type-test.ts).\r\n */\r\ntype CellClassNameFnHolder<TData, TValue> = { fn(ctx: CellClassNameCtx<TData, TValue>): string | undefined };\r\n\r\n/**\r\n * Editor contract, in order: the editor holds its own draft text; `onChange(value)` stashes the\r\n * CURRENT draft into the grid (this is the only way a value reaches the store); `commit()`\r\n * validates and applies whatever `onChange` last stashed, then closes the editor and moves the\r\n * active cell. Calling `commit()` without an `onChange` since the editor opened commits the OLD\r\n * value (the stash is seeded from it at editor-open, never from your local state); calling\r\n * `onChange` and never `commit` leaves the edit stranded. `cancel()` discards everything. The\r\n * built-in editors' `commitText(movement)` helper shows the canonical order: guard, `onChange`,\r\n * `commit`.\r\n */\r\nexport type CellEditorProps<TData, TValue> = {\r\n  /** The last-COMMITTED value, as it was when the editor opened (seed for your initial draft). NOT your in-progress draft — that lives in your editor's local state. */\r\n  value: TValue;\r\n  /** Initial text seed when editing started by typing (type-to-replace); undefined for Enter/F2. */\r\n  initialText?: string;\r\n  /** The full data row this cell belongs to. */\r\n  row: TData;\r\n  // `column`'s own validate erases independently (3rd param `any`) — same reasoning as\r\n  // CellRenderProps.column above.\r\n  column: ColumnDef<TData, TValue, any>; // eslint-disable-line @typescript-eslint/no-explicit-any\r\n  /** Stash the current draft. Takes effect on the next `commit` — it never writes the store by itself. */\r\n  onChange: (value: TValue) => void;\r\n  /** Validate and apply the last stashed value, close the editor, and move the active cell by the given delta (Enter = {0,1}, blur/click-away = {0,0}). */\r\n  commit: (movement?: { dx: number; dy: number }) => void;\r\n  /** Discard the edit without emitting a change. */\r\n  cancel: () => void;\r\n  /**\r\n   * True while an async Standard Schema `validate` is awaiting its result for THIS commit attempt\r\n   * (see interaction/use-async-validate.ts) — editing is still open. Built-in editors set their\r\n   * input `readOnly` (not `disabled` — a disabled input can't receive Escape/blur) while pending;\r\n   * a custom editor may ignore this or style it differently. Always `false`/`undefined` for the\r\n   * function form and sync schemas.\r\n   */\r\n  pending?: boolean;\r\n  /**\r\n   * Increments on every rejected commit attempt (sync or async) for this cell, and resets to 0\r\n   * when a new edit session starts. A built-in editor's own one-shot commit guard (see\r\n   * interaction/use-commit-guard.ts) must re-arm on a REJECTION specifically — not on every\r\n   * `pending` transition to false, which also happens on Escape/cancel right before the editor\r\n   * unmounts; re-arming there would let the unmount's own blur event fire a second, stale commit\r\n   * through the now-unguarded path.\r\n   */\r\n  rejectionCount?: number;\r\n};\r\n\r\n/**\r\n * A cell type implements rendering plus the value pipeline that powers copy, paste, fill,\r\n * delete, and import/export (see research/react-datasheet-grid-study.md §8). The pipelines read\r\n * the value through DIFFERENT surfaces, so one override does not reach all of them:\r\n *\r\n * - display (the cell's rendered text) = `toDisplayText` when present, else `toText`.\r\n * - clipboard copy and export = `toText`, always.\r\n * - sort = `compare` when the column declares this type, else a numeric-aware locale collation of\r\n *   the RAW `String(value)` — not of `toText` output.\r\n * - search and filter = `String(rawValue)`: the view pipeline is built WITHOUT cellTypes, so a\r\n *   custom `toText` never changes what search or filter see (a `select` column searches the\r\n *   stored VALUE, not the displayed label; a `number` with `decimals` searches the unrounded\r\n *   value; a `date` with a `displayFormat` matches the ISO string).\r\n * - fill (series inference) works on the raw value, not on any text form.\r\n */\r\nexport type CellType<TData = unknown, TValue = unknown, TOptions = unknown> = {\r\n  Cell: FC<CellRenderProps<TData, TValue>>;\r\n  Editor: FC<CellEditorProps<TData, TValue>>;\r\n  /** Serialize for clipboard/export. Search and filter do NOT use this — they match on the raw `String(value)`. */\r\n  toText(value: TValue, options?: TOptions): string;\r\n  /** Optional display-only rendering (e.g. locale-formatted dates); the cell renders `toDisplayText ?? toText`. Clipboard/export always use `toText`. */\r\n  toDisplayText?(value: TValue, options?: TOptions): string;\r\n  /** Parse from paste/import/typing. Must never throw; return clearValue()'s result for garbage. */\r\n  fromText(text: string, options?: TOptions): TValue;\r\n  clearValue(options?: TOptions): TValue;\r\n  isEmpty(value: TValue): boolean;\r\n  /** Sort comparator for this type; when absent, sorting falls back to a numeric-aware locale collation of the raw `String(value)` — not of `toText` output. */\r\n  compare?(a: TValue, b: TValue): number;\r\n  align?: \"left\" | \"right\" | \"center\";\r\n};\r\n\r\n/**\r\n * `TValidate` defaults to `TValue` for normal authoring (nothing changes for consumers) but is a\r\n * SEPARATE type parameter so the store's erasure boundary (`AnyColumnDef`, `DataGridSyncProps.columns`)\r\n * can instantiate it independently as `any` (not `unknown` — proven necessary, see `validate`'s doc\r\n * below) while `TValue` (accessorFn/setValue/renderCell/etc.) keeps its real `unknown` erasure and\r\n * full type safety.\r\n */\r\nexport type ColumnDef<TData, TValue = unknown, TValidate = TValue> = {\r\n  /** Stable identity — never derived from the header label. */\r\n  id: string;\r\n  /**\r\n   * String headers render truncated and get the built-in sort-direction arrow in `sort`\r\n   * header-click mode; a ReactNode header renders as-is and owns its own display (the built-in\r\n   * arrow is NOT appended — embed DataGridSortIndicator inside it when you still want it).\r\n   */\r\n  header: string | ReactNode;\r\n  /** Plain-text header for clipboard/export when `header` is a ReactNode. */\r\n  headerText?: string;\r\n  accessorKey?: AccessorKeyOf<TData>;\r\n  /**\r\n   * Read accessor for computed or derived values. A column with ONLY `accessorFn` (no\r\n   * `accessorKey`/`setValue`) is read-only by construction: edits, pastes, and fills are dropped\r\n   * for it. Pair it with `setValue` to make the computed value writable.\r\n   */\r\n  accessorFn?: (row: TData) => TValue;\r\n  /** Immutable write-back; defaults to spreading `accessorKey`. Required for writability when `accessorKey` is absent. */\r\n  setValue?(row: TData, value: TValue): TData;\r\n  /** Cell type registry key; default \"text\". */\r\n  type?: string;\r\n  options?: unknown;\r\n  readOnly?: boolean | ((row: TData) => boolean);\r\n  /**\r\n   * Rejects a value: the function form returns an error message (`null` accepts), or any\r\n   * `StandardSchemaV1` for `TValue` (Zod/Valibot/ArkType/...) — detected at the call site via\r\n   * `\"~standard\" in validate`. A schema's `validate` may resolve async; only the single-cell\r\n   * editor commit path awaits it (see cell.tsx/use-commit-guard.ts), bulk paths (paste/fill/\r\n   * import) treat a Promise result as unsupported and skip validation for that cell (dev warn).\r\n   * On success the COMMITTED value is `result.value` (schemas may transform), documented in\r\n   * editing-cell-types.mdx.\r\n   * `TValidate` (not `TValue` directly): a union of a function type and a non-callable schema\r\n   * object, as a plain property, is checked contravariantly as a WHOLE — unlike a pure\r\n   * method-shorthand function, TS can't special-case just the function branch bivariant anymore.\r\n   * That would break `defineColumns`' narrow-`TValue` columns widening into the shared\r\n   * `ColumnDef<TData, unknown>` array every consumer prop uses (verified: it also cascades to\r\n   * OTHER unrelated method-shorthand fields like `renderCell` in the same array literal — TS's\r\n   * structural check on the whole object type gets stricter once ANY sibling property stops being\r\n   * bivariant). Giving `validate` its own type parameter keeps that widening exact (`TValue` never\r\n   * becomes incompatible) while only `TValidate` erases at the store boundary — a documented,\r\n   * narrow, single-field cast (see `AnyColumnDef`), not a project-wide `any`.\r\n   */\r\n  validate?: ((value: TValidate, row: TData) => string | null) | StandardSchemaV1<TValidate>;\r\n  /**\r\n   * What a `validate` rejection does in a single-cell commit. `\"block\"` (default): the commit is\r\n   * refused — the editor stays open with the message and the value never enters `data`. `\"warn\"`:\r\n   * the value commits and the cell is flagged in `cellErrors` (same ring/tint/tooltip as server\r\n   * errors, auto-cleared by the next valid commit of that cell). A rejection carries no\r\n   * transformed value, so `\"warn\"` commits the RAW value; per-row softness is expressed inside the\r\n   * function form via its `row` argument (return `null` to skip the rule for that row). Bulk paths\r\n   * (paste/fill/updateCells) still drop rejected cells even for `\"warn\"` columns.\r\n   * ponytail: soft bulk commits would need `onInvalid` threaded through resolveBulkWrites/\r\n   * computeCellPatchBatch/prevalidatePatches — add when a soft paste/fill is actually wanted.\r\n   */\r\n  onInvalid?: \"block\" | \"warn\";\r\n  width?: number;\r\n  minWidth?: number;\r\n  maxWidth?: number;\r\n  /**\r\n   * Proportional share of leftover viewport width. Columns with flex grow from their base\r\n   * `width` to fill positive leftover space; clamped by `maxWidth`. A manual resize fixes the\r\n   * column and removes it from flex distribution.\r\n   */\r\n  flex?: number;\r\n  /** Initial pin direction; change it at runtime through `setColumnPin` or the context menu. Pinned columns only reorder within their own pin zone. */\r\n  pin?: \"left\" | \"right\";\r\n  /** Removes the column from click-to-sort (header clicks no longer sort it); programmatic `setSorts`/`toggleSort` still apply. Default true. */\r\n  sortable?: boolean;\r\n  /** Excludes the column from the filter menu's column picker. Existing filter specs on it keep applying. Default true. */\r\n  filterable?: boolean;\r\n  /**\r\n   * Per-column filter predicate that REPLACES the built-in text matcher (`matchesFilter`/\r\n   * `createFilterMatcher`) for this column: it receives the cell's RAW value — not\r\n   * `String(value)` — and the spec, and returns whether the row passes. The built-in matcher runs\r\n   * when this is absent. A thrown result fails the row for that filter.\r\n   */\r\n  filterMatch?: (value: unknown, filter: FilterSpec) => boolean;\r\n  /**\r\n   * The operator list the filter menu offers for this column, replacing the\r\n   * `operatorsForColumnType` default derived from its `type` — e.g. numeric comparison operators\r\n   * for a consumer-registered numeric cell type (`type: \"currency\"`).\r\n   */\r\n  filterOperators?: readonly FilterOperator[];\r\n  /**\r\n   * Sort comparator over the whole data row (not the cell value), consulted before the column's\r\n   * cell-type `compare` and the default text compare. A non-zero result orders the pair; a zero,\r\n   * NaN, or thrown result defers that pair to the default text compare. `SortSpec` direction and\r\n   * the empty-cell-last rule still apply.\r\n   */\r\n  sortCompare?: (a: TData, b: TData) => number;\r\n  /** Initial visibility; toggle at runtime through `setColumnHidden` or the columns menu. */\r\n  hidden?: boolean;\r\n  /** Disables the resize handle for this column; default true. */\r\n  resizable?: boolean;\r\n  /** Disables drag-to-reorder for this column; default true. */\r\n  reorderable?: boolean;\r\n  /** Disables pin/unpin for this column; default true. */\r\n  pinnable?: boolean;\r\n  /** Display-only override that keeps the column's type pipeline; method-shorthand for the same bivariance reason as `validate`. */\r\n  renderCell?(props: CellRenderProps<TData, TValue>): ReactNode;\r\n  /**\r\n   * Per-column cell class, merged via `cn()` after the built-in cell classes (PLAN §6 \"Programmatic\r\n   * style API\") so it wins on conflicting utilities. A plain string applies to every cell in the\r\n   * column; the function form gets the same per-cell context as the grid-level `getCellClassName`\r\n   * prop (see `CellClassNameFnHolder` for why it's spelled via indexed access, not inline).\r\n   */\r\n  cellClassName?: string | CellClassNameFnHolder<TData, TValue>[\"fn\"];\r\n  /** Per-column header cell class, merged via `cn()` after the built-in header classes. */\r\n  headerClassName?: string;\r\n};\r\n\r\n/** Row height presets (px): 'compact' 28, 'default' 36, 'comfortable' 44; the `rowHeight` prop overrides any of these. */\r\nexport type DensityMode = \"compact\" | \"default\" | \"comfortable\";\r\n\r\n/**\r\n * How a plain header click behaves (PLAN §3 item 4). `'select'` (default) selects the whole\r\n * column, matching the marker/row-selection model. `'sort'` cycles asc->desc->none on the\r\n * clicked column instead (shift+click adds it to a multi-sort); `'none'` disables both.\r\n */\r\nexport type HeaderClickBehavior = \"select\" | \"sort\" | \"none\";\r\n\r\nexport type SortSpec = { columnId: string; direction: \"asc\" | \"desc\" };\r\n\r\nexport type FilterOperator =\r\n  | \"contains\"\r\n  | \"notContains\"\r\n  | \"equals\"\r\n  | \"notEquals\"\r\n  | \"startsWith\"\r\n  | \"endsWith\"\r\n  | \"empty\"\r\n  | \"notEmpty\"\r\n  | \"gt\"\r\n  | \"gte\"\r\n  | \"lt\"\r\n  | \"lte\"\r\n  | \"isBetween\"\r\n  | \"isAnyOf\";\r\n\r\n/** Combines multiple `FilterSpec`s: `\"and\"` (default) requires every filter to match, `\"or\"` requires any one. */\r\nexport type FilterJoinOperator = \"and\" | \"or\";\r\n\r\n/**\r\n * `value` is a single string for most operators. `isBetween` carries an inclusive `[min, max]`\r\n * tuple (either bound may be `\"\"` for an open-ended range); `isAnyOf` carries a `string[]` of\r\n * accepted values and matches when the cell text equals any of them. `filterId` is a stable\r\n * identity for list rendering/updates independent of array position (multiple filter rows on the\r\n * same column are allowed and meaningful under `\"or\"`/mixed joins); optional at the input boundary\r\n * for backward compatibility — the store auto-assigns one when absent (see `genFilterId`).\r\n */\r\nexport type FilterSpec = { filterId?: string; columnId: string; operator: FilterOperator; value?: string | [string, string] | string[] };\r\n\r\n/**\r\n * Named keyboard actions; bindings map keys to these (see `DEFAULT_KEYMAP` in\r\n * `keyboard/default-keymap.ts`). `editReplace` (type-to-replace) has no `DEFAULT_KEYMAP` binding:\r\n * by default any unbound printable key on an active cell dispatches it (Excel behavior, starting\r\n * an edit seeded with the typed char). Defining `keymap.editReplace` takes the action over\r\n * exclusively — `[\"F3\"]` remaps the trigger (plain edit start, no char seed); `[]` disables it.\r\n */\r\nexport type GridAction =\r\n  | \"moveUp\" | \"moveDown\" | \"moveLeft\" | \"moveRight\"\r\n  | \"retainMoveUp\" | \"retainMoveDown\" | \"retainMoveLeft\" | \"retainMoveRight\"\r\n  | \"scrollActiveIntoView\"\r\n  | \"moveRowStart\" | \"moveRowEnd\"\r\n  | \"jumpUp\" | \"jumpDown\" | \"jumpLeft\" | \"jumpRight\"\r\n  | \"moveFirstCell\" | \"moveLastCell\"\r\n  | \"pageUp\" | \"pageDown\"\r\n  | \"extendUp\" | \"extendDown\" | \"extendLeft\" | \"extendRight\"\r\n  | \"extendJumpUp\" | \"extendJumpDown\" | \"extendJumpLeft\" | \"extendJumpRight\"\r\n  | \"extendFirstCell\" | \"extendLastCell\"\r\n  | \"selectRow\" | \"selectColumn\" | \"selectAll\"\r\n  | \"edit\" | \"editReplace\" | \"commitDown\" | \"commitUp\" | \"commitRight\" | \"commitLeft\"\r\n  | \"cancel\" | \"deleteContents\"\r\n  | \"undo\" | \"redo\"\r\n  | \"fillDown\" | \"fillRight\"\r\n  | \"insertRowBelow\" | \"insertRowAbove\" | \"duplicateRow\" | \"deleteRows\";\r\n\r\n/**\r\n * Key binding string: `mod` = Ctrl (win/linux) / Cmd (mac), `ctrl` = the LITERAL physical Ctrl key\r\n * on every platform (it bypasses `mod` resolution), plus `shift`, `alt`.\r\n * Format: \"mod+shift+ArrowUp\", key is `KeyboardEvent.key`. Multiple bindings per action allowed.\r\n * `KeyBinding` (keyboard/key-syntax.ts) types the syntax for autocomplete; arbitrary strings stay\r\n * assignable (exotic keys), and invalid bindings warn in development.\r\n * Keep `ctrl` where `DEFAULT_KEYMAP` uses it (`ctrl+ ` for selectColumn): `mod+ ` on macOS is\r\n * Cmd+Space, which the OS intercepts (Spotlight), so the binding would be dead there.\r\n */\r\nexport type Keymap = Partial<Record<GridAction, KeyBinding[]>>;\r\n\r\n/**\r\n * Map of built-in cell-type keys to their value/options shapes. Consumers\r\n * register custom cell types by augmenting this interface via declaration\r\n * merging in their own module (the path is `@/components/data-grid/types`\r\n * for the default install layout — adjust if your components alias or\r\n * install target differs):\r\n * ```ts\r\n * declare module \"@/components/data-grid/types\" {\r\n *   interface GridCellTypes {\r\n *     currency: { value: number | null; options: { currency: string } };\r\n *   }\r\n * }\r\n * ```\r\n * This gives custom types the same `options`/value inference as built-ins\r\n * through `defineColumns` and `TypedColumnDef`. The installed `cell-types.ts`'s\r\n * `cellTypes` object checks its five built-ins against a literal\r\n * `BuiltinCellTypeKey` union, decoupled from this interface, so augmenting it\r\n * with a new key never breaks that file's compile.\r\n */\r\nexport interface GridCellTypes {\r\n  text: { value: string; options: { placeholder?: string } };\r\n  number: {\r\n    value: number | null;\r\n    options: {\r\n      min?: number;\r\n      max?: number;\r\n      /** @reserved — accepted but a no-op for now: stepping is editor-keyboard behavior and gets decided after P4 lands; do not rely on it. */\r\n      step?: number;\r\n      decimals?: number;\r\n    };\r\n  };\r\n  checkbox: { value: boolean; options: object };\r\n  select: {\r\n    value: string | null;\r\n    options: { choices: readonly { value: string; label: string }[] };\r\n  };\r\n  date: {\r\n    value: string | null;\r\n    /** `locale` defaults to `\"en-US\"` — an explicit default keeps SSR and client HTML identical. */\r\n    options: { min?: string; max?: string; displayFormat?: Intl.DateTimeFormatOptions; locale?: string };\r\n  };\r\n}\r\n\r\n/**\r\n * Plain JSON-serializable snapshot of a grid's column layout (widths/order/pins/hidden) — the save\r\n * payload for `onColumnLayoutChange`/`defaultColumnLayout` (PLAN events-state gap fix 1). `widths`\r\n * holds only columns the user has actually resized (a def's own `width` isn't duplicated in here);\r\n * `order` is always the full current column-id order, not just the moved one.\r\n */\r\nexport type ColumnLayout = {\r\n  widths: Record<string, number>;\r\n  order: string[];\r\n  pins: Record<string, \"left\" | \"right\">;\r\n  hidden: string[];\r\n};\r\n\r\n/** Registered cell-type keys (built-in + consumer-augmented). */\r\nexport type CellTypeKey = keyof GridCellTypes;\r\n\r\n/** Value type for a given cell-type key. */\r\nexport type CellValueOf<K extends CellTypeKey> = GridCellTypes[K][\"value\"];\r\n\r\n/** Options type for a given cell-type key. */\r\nexport type CellOptionsOf<K extends CellTypeKey> = GridCellTypes[K][\"options\"];\r\n",
      "type": "registry:component",
      "target": "components/data-grid/types.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/layers.ts",
      "content": "/**\r\n * The grid's internal stacking order, in one place.\r\n *\r\n * HARD CEILING: every value stays BELOW 50. shadcn puts each portalled overlay (dialog,\r\n * dropdown-menu, popover, select, tooltip, context-menu) at `z-50`, and a dialog must always\r\n * paint over the grid — including for consumers who install this through the registry, whose\r\n * pages we cannot patch. The grid root also sets `isolation: isolate` (root.tsx), so these\r\n * values are contained and can never leak into the page's stacking context; the ceiling is the\r\n * second line of defense for consumers who restyle the root.\r\n *\r\n * Steps of 4 so a new layer can slot between two existing ones without renumbering.\r\n *\r\n * Two rules the numbers encode, both learned from real bugs:\r\n *\r\n * 1. Cells are `position: relative`, so among equally-ranked siblings DOM order decides paint\r\n *    order. The marker is FIRST in each row, so it must outrank the data cells that follow it.\r\n * 2. Pinning is a spatial guarantee; active is a focus state. A pinned cell therefore outranks an\r\n *    active UNPINNED cell — otherwise clicking a cell and scrolling floats it over the pinned\r\n *    column. An active PINNED cell ranks above both.\r\n */\r\nexport const GRID_LAYER = {\r\n  /** Unpinned body cell — in flow, no stacking context of its own. */\r\n  cell: 0,\r\n  /** Active (focused) unpinned cell: above its neighbours, below anything pinned. */\r\n  activeCell: 4,\r\n  /** Pinned-left/right body cell — must cover any unpinned cell scrolled under it. */\r\n  pinnedCell: 8,\r\n  /** Active pinned cell: the focus ring must not be clipped by its own band. */\r\n  activePinnedCell: 12,\r\n  /** Selection/fill/presence segment covering the pinned band — above the pinned cells it decorates, below the marker. */\r\n  pinnedOverlaySegment: 14,\r\n  /** Row-marker body cell — first in DOM order, so it needs to outrank every data cell. */\r\n  markerCell: 16,\r\n  /** Pinned top/bottom row band — spans the full width above the scrolling body. */\r\n  pinnedRowBand: 20,\r\n  /** Header row — above every body cell, including pinned ones. */\r\n  header: 24,\r\n  /** Pinned header cell, and the marker header, within the header row. */\r\n  pinnedHeader: 28,\r\n  /** Pin-edge shadows — above the cells whose boundary they mark. */\r\n  pinShadow: 32,\r\n  /** Selection / fill / presence overlays drawn over the whole grid. */\r\n  overlay: 36,\r\n  /** Loading skeleton — covers everything while the grid has no data to show. */\r\n  skeleton: 40,\r\n} as const;\r\n\r\n/** A value from {@link GRID_LAYER}. */\r\nexport type GridLayer = (typeof GRID_LAYER)[keyof typeof GRID_LAYER];\r\n\r\n/** The stacking rank a body cell should carry, given its pinned and active state. */\r\nexport function cellLayer(pinned: boolean, active: boolean): number | undefined {\r\n  if (pinned) return active ? GRID_LAYER.activePinnedCell : GRID_LAYER.pinnedCell;\r\n  if (active) return GRID_LAYER.activeCell;\r\n  return undefined;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/layers.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/validation/validate-cell.ts",
      "content": "import type { StandardSchemaV1, StandardSchemaV1Issue, StandardSchemaV1Result } from \"../types\";\r\n\r\n/** A column's resolved `validate` field: the sync function form or any Standard Schema for the value. */\r\nexport type CellValidate = ((value: unknown, row: unknown) => string | null) | StandardSchemaV1 | undefined;\r\n\r\n/** `error`: rejected, message to show. `value`: accepted — the value to COMMIT (a schema may transform it, so this may differ from the input). */\r\nexport type ValidateResult = { error: string } | { value: unknown };\r\n\r\n/** Structural detection per the spec — never `instanceof`, so any library's schema object qualifies. */\r\nexport function isStandardSchema(validate: unknown): validate is StandardSchemaV1 {\r\n  return typeof validate === \"object\" && validate !== null && \"~standard\" in validate;\r\n}\r\n\r\n/** First issue's message; multiple issues join with \"; \". Path (when present) prefixes as `a.b: message`. */\r\nexport function formatIssues(issues: readonly StandardSchemaV1Issue[]): string {\r\n  return issues.map(formatIssue).join(\"; \");\r\n}\r\n\r\nfunction formatIssue(issue: StandardSchemaV1Issue): string {\r\n  const path = issue.path\r\n    ?.map((segment) => (typeof segment === \"object\" && segment !== null ? segment.key : segment))\r\n    .join(\".\");\r\n  return path ? `${path}: ${issue.message}` : issue.message;\r\n}\r\n\r\n/** Turns one resolved (never a Promise) Standard Schema result into a {@link ValidateResult}. */\r\nexport function resolveSchemaResult(result: StandardSchemaV1Result<unknown>): ValidateResult {\r\n  return result.issues ? { error: formatIssues(result.issues) } : { value: result.value };\r\n}\r\n\r\n/**\r\n * Runs `validate` (function form or Standard Schema) SYNCHRONOUSLY, for the store's `computeCommit`.\r\n * A schema whose `~standard.validate` returns a Promise cannot be awaited here, so the value passes\r\n * UNCHANGED — the awaiting happens one layer up: single-cell edits in\r\n * `interaction/use-async-validate.ts`, bulk batches in `validation/validate-batch.ts`, both of which\r\n * hand `computeCommit`/`computeCellPatchBatch` an already-resolved `result.value`.\r\n */\r\nexport function runValidateSync(validate: CellValidate, value: unknown, row: unknown): ValidateResult {\r\n  const pending = runValidatePending(validate, value, row);\r\n  return pending instanceof Promise ? { value } : pending;\r\n}\r\n\r\n/**\r\n * The one-cell primitive both the sync and the async bulk paths share: a {@link ValidateResult} when\r\n * `validate` resolves synchronously (function form, no validator, or a sync schema), or the schema's\r\n * own Promise when it does not. Returning the raw Promise — rather than always wrapping in one — is\r\n * what keeps a sync-only batch allocation-free: {@link runValidateBatch} only enters its async branch\r\n * when at least one cell actually hands back a Promise.\r\n */\r\nexport function runValidatePending(\r\n  validate: CellValidate,\r\n  value: unknown,\r\n  row: unknown,\r\n): ValidateResult | Promise<StandardSchemaV1Result<unknown>> {\r\n  if (!validate) return { value };\r\n  if (!isStandardSchema(validate)) {\r\n    const error = validate(value, row);\r\n    return error ? { error } : { value };\r\n  }\r\n\r\n  const result = validate[\"~standard\"].validate(value) as StandardSchemaV1Result<unknown> | Promise<StandardSchemaV1Result<unknown>>;\r\n  if (result instanceof Promise) return result;\r\n  return resolveSchemaResult(result);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/validation/validate-cell.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/validation/validate-batch.ts",
      "content": "import type { StandardSchemaV1Result } from \"../types\";\r\nimport { resolveSchemaResult, runValidatePending, type CellValidate, type ValidateResult } from \"./validate-cell\";\r\n\r\n/** How many async cell validations may be in flight at once. A 10k-cell paste against a network-backed schema must not open 10k requests. */\r\nexport const VALIDATE_CONCURRENCY = 32;\r\n\r\n/** One cell queued for validation: the validator to run plus the `(value, row)` it runs against. */\r\nexport type ValidateBatchItem = {\r\n  validate: CellValidate;\r\n  value: unknown;\r\n  row: unknown;\r\n};\r\n\r\n/**\r\n * Runs `items`' validators and returns one {@link ValidateResult} per item, positionally.\r\n *\r\n * SYNC CONTRACT: when every validator resolves synchronously — no validator at all, the function\r\n * form, or a sync Standard Schema — this returns the result ARRAY directly, never a Promise. No\r\n * promise, no chunk bookkeeping, and no microtask is allocated on that path, so a sync-only bulk\r\n * batch keeps today's straight-line cost. Callers branch on `result instanceof Promise`.\r\n *\r\n * ASYNC CONTRACT: the first validator that hands back a Promise switches this to the async branch,\r\n * which finishes the remaining items with at most `concurrency` validations in flight. Validators\r\n * are INVOKED lazily, one per slot — probing every item up front would start every promise at once\r\n * and make the cap meaningless. Order of invocation stays the item order.\r\n *\r\n * A validator that throws (or a schema whose promise rejects) counts as a rejection for that cell,\r\n * so one bad cell never fails the batch.\r\n */\r\nexport function runValidateBatch(\r\n  items: readonly ValidateBatchItem[],\r\n  concurrency: number = VALIDATE_CONCURRENCY,\r\n): ValidateResult[] | Promise<ValidateResult[]> {\r\n  const results = new Array<ValidateResult>(items.length);\r\n\r\n  for (let i = 0; i < items.length; i++) {\r\n    const item = items[i]!; // i < items.length by loop condition\r\n    const pending = runValidateOne(item);\r\n    if (pending instanceof Promise) {\r\n      return finishAsync(items, results, i, pending, Math.max(1, concurrency));\r\n    }\r\n    results[i] = pending;\r\n  }\r\n  return results;\r\n}\r\n\r\n/** {@link runValidatePending} with a throwing validator downgraded to a plain rejection for that cell. */\r\nfunction runValidateOne(item: ValidateBatchItem): ValidateResult | Promise<StandardSchemaV1Result<unknown>> {\r\n  try {\r\n    return runValidatePending(item.validate, item.value, item.row);\r\n  } catch (error) {\r\n    return { error: errorMessage(error) };\r\n  }\r\n}\r\n\r\nfunction errorMessage(error: unknown): string {\r\n  return error instanceof Error ? error.message : String(error);\r\n}\r\n\r\n/**\r\n * The async tail: `firstPending` (item `firstIndex`, already in flight) plus every item after it,\r\n * kept to `concurrency` simultaneous validations. Each slot pulls the next unstarted item, so a\r\n * long batch never has more than `concurrency` validators running even though invocation order and\r\n * the positional result mapping both stay exactly the sync path's.\r\n */\r\nasync function finishAsync(\r\n  items: readonly ValidateBatchItem[],\r\n  results: ValidateResult[],\r\n  firstIndex: number,\r\n  firstPending: Promise<StandardSchemaV1Result<unknown>>,\r\n  concurrency: number,\r\n): Promise<ValidateResult[]> {\r\n  results[firstIndex] = await settle(firstPending);\r\n\r\n  let next = firstIndex + 1;\r\n  const slot = async (): Promise<void> => {\r\n    while (next < items.length) {\r\n      const index = next++;\r\n      const item = items[index]!; // index < items.length, guarded by the while condition\r\n      const pending = runValidateOne(item);\r\n      results[index] = pending instanceof Promise ? await settle(pending) : pending;\r\n    }\r\n  };\r\n\r\n  const slots: Promise<void>[] = [];\r\n  for (let i = 0; i < concurrency; i++) slots.push(slot());\r\n  await Promise.all(slots);\r\n  return results;\r\n}\r\n\r\n/** Awaits one schema promise into a {@link ValidateResult}; a rejected promise is that cell's rejection. */\r\nasync function settle(pending: Promise<StandardSchemaV1Result<unknown>>): Promise<ValidateResult> {\r\n  try {\r\n    return resolveSchemaResult(await pending);\r\n  } catch (error) {\r\n    return { error: errorMessage(error) };\r\n  }\r\n}\r\n\r\n/** One view-space cell a bulk path wants to write, before validation: the parsed value plus what validates it. */\r\nexport type BulkCandidate = {\r\n  viewRow: number;\r\n  columnId: string;\r\n  value: unknown;\r\n  validate: CellValidate;\r\n  /** The row the value is validated against — the function form's second argument. */\r\n  row: unknown;\r\n  /** The target row's stable id, so a held batch can re-resolve its position after a reorder. */\r\n  rowId?: string;\r\n};\r\n\r\n/** One accepted cell, in `applyCellUpdates`' write shape. `value` is the validator's OUTPUT, so a transforming schema's result is what commits. */\r\nexport type BulkWrite = { viewRow: number; columnId: string; value: unknown; rowId?: string };\r\n\r\n/**\r\n * Validates `candidates` and keeps only the ones that pass — the shared tail of every view-space bulk\r\n * path (paste, fill). Rejected cells drop SILENTLY, one bad cell never failing the batch, matching\r\n * what those paths have always done with a sync rejection.\r\n *\r\n * Returns the write array directly when nothing was async, so a sync-only paste keeps its current\r\n * straight-line behavior; returns a Promise for it when at least one column's schema was async, and\r\n * the caller holds the batch until it resolves.\r\n */\r\nexport function resolveBulkWrites(candidates: readonly BulkCandidate[]): BulkWrite[] | Promise<BulkWrite[]> {\r\n  const results = runValidateBatch(candidates);\r\n  if (results instanceof Promise) return results.then((resolved) => keepPassing(candidates, resolved, true));\r\n  return keepPassing(candidates, results, false);\r\n}\r\n\r\n/**\r\n * `carryRowId` only on the async branch: a sync batch applies in the caller's own tick, where no\r\n * reorder can intervene, so its writes stay the exact `{ viewRow, columnId, value }` shape they\r\n * have always had — the apply-time re-resolution is a held batch's concern alone.\r\n */\r\nfunction keepPassing(\r\n  candidates: readonly BulkCandidate[],\r\n  results: readonly ValidateResult[],\r\n  carryRowId: boolean,\r\n): BulkWrite[] {\r\n  const writes: BulkWrite[] = [];\r\n  for (let i = 0; i < candidates.length; i++) {\r\n    const result = results[i];\r\n    if (!result || \"error\" in result) continue;\r\n    const candidate = candidates[i]!; // i < candidates.length by loop condition\r\n    const write: BulkWrite = { viewRow: candidate.viewRow, columnId: candidate.columnId, value: result.value };\r\n    if (carryRowId && candidate.rowId !== undefined) write.rowId = candidate.rowId;\r\n    writes.push(write);\r\n  }\r\n  return writes;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/validation/validate-batch.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/validation/bulk-generation.ts",
      "content": "\"use client\";\r\n\r\nimport { useEffect, useRef } from \"react\";\r\nimport type { DataGridStoreState } from \"../store\";\r\n\r\n/**\r\n * The store facts an in-flight bulk batch was built against, plus the row ids it targets. A batch\r\n * resolves against a grid that may have moved on, so the apply step re-checks these before writing.\r\n *\r\n * Targets are ROW IDS, never view rows or data indexes. That is the whole point: a pure reorder (the\r\n * consumer hands back the same rows in a new order, a row above the target is deleted, a streaming\r\n * tick moves rows under an active sort) leaves every target id present, and the batch still applies\r\n * to the cells the user aimed at. Only a change that removes a target row, or one that redefines the\r\n * view the targets were picked in, drops the batch.\r\n */\r\nexport type BulkBatchSnapshot = {\r\n  rowIds: readonly string[];\r\n  sortState: DataGridStoreState[\"sortState\"];\r\n  filterState: DataGridStoreState[\"filterState\"];\r\n};\r\n\r\n/** Reads the snapshot an async bulk batch is held against: its target row ids plus the view specs they were chosen under. */\r\nexport function snapshotBulkBatch(s: DataGridStoreState, rowIds: readonly string[]): BulkBatchSnapshot {\r\n  return { rowIds, sortState: s.sortState, filterState: s.filterState };\r\n}\r\n\r\n/** The distinct row ids a view-space bulk batch targets — resolved NOW, so a later reorder can still find them. */\r\nexport function candidateRowIds(s: DataGridStoreState, candidates: readonly { viewRow: number }[]): string[] {\r\n  const rowIds = new Set<string>();\r\n  for (const candidate of candidates) {\r\n    const dataRowIndex = s.viewIndex[candidate.viewRow];\r\n    if (dataRowIndex === undefined) continue;\r\n    const row = s.data[dataRowIndex];\r\n    if (row === undefined) continue;\r\n    rowIds.add(s.getRowId(row, dataRowIndex));\r\n  }\r\n  return Array.from(rowIds);\r\n}\r\n\r\n/**\r\n * Whether a batch held against `snapshot` may still apply to `s`.\r\n *\r\n * Drops when any target row id is gone from `data` (the rows it validated no longer exist — a data\r\n * replacement, a delete) or when the sort or filter spec changed (the batch chose its targets inside\r\n * a view that no longer exists, so applying it would write cells the user never selected). Survives a\r\n * re-render, a scroll, a selection change, an unrelated edit, and any reordering of `data` that keeps\r\n * the target rows present.\r\n */\r\nexport function isBulkBatchCurrent(s: DataGridStoreState, snapshot: BulkBatchSnapshot): boolean {\r\n  if (s.sortState !== snapshot.sortState || s.filterState !== snapshot.filterState) return false;\r\n  if (snapshot.rowIds.length === 0) return true;\r\n  const present = new Set<string>();\r\n  for (let i = 0; i < s.data.length; i++) present.add(s.getRowId(s.data[i], i));\r\n  return snapshot.rowIds.every((rowId) => present.has(rowId));\r\n}\r\n\r\n/**\r\n * Re-points a held batch's writes at the view rows their target ids occupy NOW.\r\n *\r\n * {@link isBulkBatchCurrent} only decides whether the batch as a whole may still land; a pure\r\n * reorder passes it while every write's captured `viewRow` has gone stale, which without this would\r\n * write the user's values onto whichever rows happen to sit at those positions. A write whose\r\n * `rowId` is no longer in the view is dropped, never remapped — the guard's drop decisions must not\r\n * be undone here. Writes with no `rowId` (a caller that never resolved one) pass through untouched.\r\n */\r\nexport function reresolveBulkWrites<T extends { viewRow: number; rowId?: string }>(\r\n  s: DataGridStoreState,\r\n  writes: readonly T[],\r\n): T[] {\r\n  const viewRowById = new Map<string, number>();\r\n  for (let viewRow = 0; viewRow < s.viewIndex.length; viewRow++) {\r\n    const dataRowIndex = s.viewIndex[viewRow]!; // viewRow < length by loop condition\r\n    const row = s.data[dataRowIndex];\r\n    if (row !== undefined) viewRowById.set(s.getRowId(row, dataRowIndex), viewRow);\r\n  }\r\n\r\n  const resolved: T[] = [];\r\n  for (const write of writes) {\r\n    if (write.rowId === undefined) {\r\n      resolved.push(write);\r\n      continue;\r\n    }\r\n    const viewRow = viewRowById.get(write.rowId);\r\n    if (viewRow === undefined) continue;\r\n    resolved.push(viewRow === write.viewRow ? write : { ...write, viewRow });\r\n  }\r\n  return resolved;\r\n}\r\n\r\n/** What {@link useBulkGeneration} hands a caller that is about to hold a batch. */\r\nexport type BulkGeneration = {\r\n  /** Bumps the counter and returns this batch's token. Any batch begun earlier is now stale. */\r\n  begin(): number;\r\n  /** Whether `token` is still the newest batch AND the store still matches `snapshot`. */\r\n  isCurrent(token: number, s: DataGridStoreState, snapshot: BulkBatchSnapshot): boolean;\r\n  /** {@link reresolveBulkWrites} — the apply-time half of the guard, reached without a second import. */\r\n  reresolve<T extends { viewRow: number; rowId?: string }>(s: DataGridStoreState, writes: readonly T[]): T[];\r\n};\r\n\r\n/**\r\n * Per-surface generation counter for held async bulk batches — the same discipline\r\n * `interaction/use-async-validate.ts` uses for a single cell, at batch scope.\r\n *\r\n * One counter per hook instance means one per SURFACE: the paste pipeline, the fill handle, and the\r\n * import dialog each supersede only their own in-flight batch. A second paste fired while the first\r\n * is still validating wins outright; the first resolves into a dropped token and writes nothing, so\r\n * an older paste can never land on top of a newer one. Unmount bumps the counter too, so a batch\r\n * that resolves after the grid is gone is discarded instead of calling into a dead store.\r\n */\r\nexport function useBulkGeneration(): BulkGeneration {\r\n  const generationRef = useRef(0);\r\n  const apiRef = useRef<BulkGeneration | null>(null);\r\n\r\n  useEffect(() => {\r\n    return () => {\r\n      generationRef.current += 1;\r\n    };\r\n  }, []);\r\n\r\n  if (!apiRef.current) {\r\n    apiRef.current = {\r\n      begin: () => ++generationRef.current,\r\n      isCurrent: (token, s, snapshot) => generationRef.current === token && isBulkBatchCurrent(s, snapshot),\r\n      reresolve: reresolveBulkWrites,\r\n    };\r\n  }\r\n  return apiRef.current;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/validation/bulk-generation.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/validation/index.ts",
      "content": "/** Domain barrel — the cell-validation primitives plus the async bulk-batch engine and its race guard. */\r\nexport {\r\n  runValidateSync,\r\n  runValidatePending,\r\n  resolveSchemaResult,\r\n  isStandardSchema,\r\n  formatIssues,\r\n  type CellValidate,\r\n  type ValidateResult,\r\n} from \"./validate-cell\";\r\nexport {\r\n  runValidateBatch,\r\n  resolveBulkWrites,\r\n  VALIDATE_CONCURRENCY,\r\n  type ValidateBatchItem,\r\n  type BulkCandidate,\r\n  type BulkWrite,\r\n} from \"./validate-batch\";\r\nexport {\r\n  useBulkGeneration,\r\n  snapshotBulkBatch,\r\n  candidateRowIds,\r\n  isBulkBatchCurrent,\r\n  reresolveBulkWrites,\r\n  type BulkGeneration,\r\n  type BulkBatchSnapshot,\r\n} from \"./bulk-generation\";\r\n",
      "type": "registry:component",
      "target": "components/data-grid/validation/index.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/windowing/direction.ts",
      "content": "/**\r\n * Every piece of direction math in the grid lives here. Physical pixels (`clientX`, `rect.left`,\r\n * `scrollLeft`) meet logical layout (`insetInlineStart`, CSS Grid tracks) at a small number of\r\n * seams; each seam calls one helper below, and everything downstream stays direction-agnostic.\r\n *\r\n * Keep new physical-coordinate math out of the rest of the block — add a helper here instead. Grid\r\n * implementations that spread direction awareness across their coordinate code accumulate a\r\n * permanent RTL bug class; the whole point of this module is that there is one obvious place to\r\n * look when a coordinate is wrong under `dir=\"rtl\"`.\r\n */\r\n\r\n/** Layout direction of a grid. `\"ltr\"` is the default and its code path is unchanged from pre-RTL. */\r\nexport type GridDirection = \"ltr\" | \"rtl\";\r\n\r\n/**\r\n * Sign for the horizontal scroll transform, emitted as the `--grid-dir` CSS variable on the\r\n * viewport. CSS `transform` is always physical (a negative X moves content toward the physical\r\n * left in both directions) while CSS Grid and `inset-inline-*` mirror automatically — so the\r\n * canvas/header/band transforms multiply the (always positive) scroll offset by this instead of a\r\n * hard-coded `-1`. The browser resolves it during compositing, so the per-frame JS path is\r\n * unchanged and no scroll-tick branch exists anywhere.\r\n */\r\nexport function directionSign(direction: GridDirection): 1 | -1 {\r\n  return direction === \"rtl\" ? 1 : -1;\r\n}\r\n\r\n/**\r\n * Normalizes a container's `scrollLeft` onto one positive inline-start-relative axis that every\r\n * downstream width/offset computation already assumes.\r\n *\r\n * `dir=\"rtl\"` containers report `0` at the inline-start edge and grow NEGATIVE toward the\r\n * inline-end (the spec model every evergreen browser now implements). `Math.abs` is used rather\r\n * than a direction-signed multiply because it needs no direction argument and stays correct under\r\n * the legacy positive-WebKit convention too. Normalize once, here, and never re-branch on\r\n * direction downstream.\r\n */\r\nexport function normalizeScrollLeft(scrollLeft: number): number {\r\n  return Math.abs(scrollLeft);\r\n}\r\n\r\n/**\r\n * Converts a physical client x into an inline-start-relative x within `rect` — the seam that lets\r\n * pointer hit-testing keep its LTR formulation under both directions. In LTR the inline start is\r\n * the rect's left edge; in RTL it is the right edge, and x grows leftward across the screen.\r\n */\r\nexport function inlineStartX(clientX: number, rect: { left: number; right: number }, direction: GridDirection): number {\r\n  return direction === \"rtl\" ? rect.right - clientX : clientX - rect.left;\r\n}\r\n\r\n/**\r\n * Converts a physical pointer delta (e.g. a resize drag's `clientX - startX`) into an inline-axis\r\n * delta. Dragging toward the inline-end must grow a column in both directions; under RTL the\r\n * inline-end is the physical left, so the physical delta inverts.\r\n */\r\nexport function inlineDelta(physicalDelta: number, direction: GridDirection): number {\r\n  return direction === \"rtl\" ? -physicalDelta : physicalDelta;\r\n}\r\n\r\n/**\r\n * Half-test for a drop/split gesture inside an element: is the pointer in the element's\r\n * inline-START half? Under RTL the inline-start half is the right half of the box, so the physical\r\n * comparison flips. Callers map `true` to \"before\" and `false` to \"after\", which stay logical\r\n * (reading-order) terms in both directions.\r\n */\r\nexport function isInlineStartHalf(clientX: number, rect: { left: number; right: number; width: number }, direction: GridDirection): boolean {\r\n  return inlineStartX(clientX, rect, direction) < rect.width / 2;\r\n}\r\n\r\n/** rAF-throttled auto-scroll step (px) applied per frame while a drag pointer sits beyond a viewport edge. */\r\nexport const AUTO_SCROLL_STEP = 16;\r\n/** Distance (px) from a viewport edge at which drag auto-scroll kicks in. */\r\nexport const AUTO_SCROLL_ZONE = 24;\r\n\r\n/**\r\n * Drag auto-scroll intent at the viewport's inline edges, in inline-axis terms: `-1` scrolls toward\r\n * the inline start, `1` toward the inline end, `0` when the pointer is not in either edge zone.\r\n * The edge zones themselves are physical screen bands, so which physical edge means \"inline start\"\r\n * depends on direction — that is the entire content of this helper.\r\n */\r\nexport function inlineAutoScrollStep(\r\n  clientX: number,\r\n  rect: { left: number; right: number },\r\n  zone: number,\r\n  direction: GridDirection,\r\n): -1 | 0 | 1 {\r\n  const atPhysicalLeft = clientX < rect.left + zone;\r\n  const atPhysicalRight = clientX > rect.right - zone;\r\n  if (!atPhysicalLeft && !atPhysicalRight) return 0;\r\n  const towardInlineStart = direction === \"rtl\" ? atPhysicalRight : atPhysicalLeft;\r\n  return towardInlineStart ? -1 : 1;\r\n}\r\n\r\n/**\r\n * Applies an inline-axis scroll delta to a container, in whichever `scrollLeft` convention the\r\n * container itself uses. A relative `+=` is convention-agnostic in LTR; under RTL the axis runs\r\n * negative, so the delta's sign inverts. Positive `delta` always scrolls toward the inline end.\r\n */\r\nexport function applyInlineScrollDelta(element: HTMLElement, delta: number, direction: GridDirection): void {\r\n  element.scrollLeft += direction === \"rtl\" ? -delta : delta;\r\n}\r\n\r\n/**\r\n * Which physical rect edge is the inline start. Used where an element's own rendered edge has to be\r\n * measured (pin shadows), since `getBoundingClientRect()` only reports physical edges.\r\n */\r\nexport function inlineStartEdge(rect: { left: number; right: number }, direction: GridDirection): number {\r\n  return direction === \"rtl\" ? rect.right : rect.left;\r\n}\r\n\r\n/** Which physical rect edge is the inline end — the mirror of {@link inlineStartEdge}. */\r\nexport function inlineEndEdge(rect: { left: number; right: number }, direction: GridDirection): number {\r\n  return direction === \"rtl\" ? rect.left : rect.right;\r\n}\r\n\r\n/**\r\n * Distance from `rect`'s inline-start edge to `edge`, always positive when `edge` sits inside the\r\n * rect. Lets pin-shadow offsets stay in the same inline-start-relative space as `insetInlineStart`.\r\n */\r\nexport function inlineDistanceFromStart(edge: number, rect: { left: number; right: number }, direction: GridDirection): number {\r\n  return direction === \"rtl\" ? rect.right - edge : edge - rect.left;\r\n}\r\n\r\n/** Distance from `rect`'s inline-END edge to `edge` — the mirror of {@link inlineDistanceFromStart}. */\r\nexport function inlineDistanceFromEnd(edge: number, rect: { left: number; right: number }, direction: GridDirection): number {\r\n  return direction === \"rtl\" ? edge - rect.left : rect.right - edge;\r\n}\r\n\r\n/**\r\n * Reads the resolved layout direction of an element from the DOM, following inherited `dir` and\r\n * the document default. Used once at mount to default the `direction` prop, so a grid inside an\r\n * `<html dir=\"rtl\">` page is correct without the consumer passing anything.\r\n */\r\nexport function readResolvedDirection(element: Element | null): GridDirection {\r\n  if (!element || typeof window === \"undefined\") return \"ltr\";\r\n  return window.getComputedStyle(element).direction === \"rtl\" ? \"rtl\" : \"ltr\";\r\n}\r\n\r\n/**\r\n * Arrow-key remapping for visual movement. Under RTL, pressing ArrowRight must move the active cell\r\n * toward the next VISUAL column, which is the PREVIOUS logical index — matching native spreadsheets\r\n * and every RTL-supporting grid. Doing it as a key swap at the single keymap-resolution seam keeps\r\n * every navigation helper, and any consumer's custom keymap, written in logical terms.\r\n *\r\n * `Tab`/`Shift+Tab` are deliberately absent: they are already reading-order logical and must not\r\n * flip. Home/End need no entry either, since those actions are named logically (`moveRowStart`\r\n * /`moveRowEnd`) and \"row start\" is the first column in both directions.\r\n */\r\nconst VISUAL_KEY_SWAP: Readonly<Record<string, string>> = {\r\n  ArrowLeft: \"ArrowRight\",\r\n  ArrowRight: \"ArrowLeft\",\r\n};\r\n\r\n/**\r\n * Maps a physical arrow key to the key whose logically-named action produces VISUAL movement in\r\n * `direction`. Identity in LTR (so the default path is untouched); swaps left/right in RTL.\r\n */\r\nexport function visualArrowKey(key: string, direction: GridDirection): string {\r\n  if (direction !== \"rtl\") return key;\r\n  return VISUAL_KEY_SWAP[key] ?? key;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/windowing/direction.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/windowing/index.ts",
      "content": "/** Domain barrel — row/column virtualization windows and scrolled-edge detection. */\r\nexport { useRowWindow, type RowWindow, type UseRowWindowOptions } from \"./use-row-window\";\r\nexport { getElementStore, useScrollSnapshot, useElementDimensions, useViewportElement, type ScrollSnapshot } from \"./use-scroll-snapshot\";\r\nexport { createVelocityEstimator, updateVelocityEstimate, VELOCITY_OVERSCAN_CAP_PX, type VelocityEstimator } from \"./velocity-estimator\";\r\nexport { useColumnWindow, type UseColumnWindowOptions } from \"./use-column-window\";\r\nexport { useScrolledEdges } from \"./use-scrolled-edges\";\r\nexport { usePinShadowEdges } from \"./use-pin-shadow-edges\";\r\nexport {\r\n  applyInlineScrollDelta,\r\n  directionSign,\r\n  inlineAutoScrollStep,\r\n  inlineDelta,\r\n  inlineDistanceFromEnd,\r\n  inlineDistanceFromStart,\r\n  inlineEndEdge,\r\n  inlineStartEdge,\r\n  inlineStartX,\r\n  isInlineStartHalf,\r\n  normalizeScrollLeft,\r\n  readResolvedDirection,\r\n  visualArrowKey,\r\n  type GridDirection,\r\n} from \"./direction\";\r\n",
      "type": "registry:component",
      "target": "components/data-grid/windowing/index.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/windowing/use-column-window.ts",
      "content": "\"use client\";\r\n\r\nimport { useEffect, useMemo, useReducer, useRef, type RefObject } from \"react\";\r\nimport { getElementStore, type ScrollSnapshot } from \"./use-scroll-snapshot\";\r\nimport { createVelocityEstimator, updateVelocityEstimate, VELOCITY_OVERSCAN_CAP_PX, type VelocityEstimator } from \"./velocity-estimator\";\r\n\r\n/** {@link useColumnWindow} options. */\r\nexport type UseColumnWindowOptions = {\r\n  /** Resolved px width per visible column, in display order. */\r\n  widths: number[];\r\n  /** Pin state per visible column, in the same display order as `widths`. */\r\n  pins: ((\"left\" | \"right\") | undefined)[];\r\n  /** Extra unpinned columns rendered beyond the visible viewport on each side. */\r\n  overscan?: number;\r\n  /** Marker column width (px), 0 when `rowMarkers` is 'none'; shifts the viewport band into the real DOM coordinate space, which includes the marker track (`layout.totalWidth`). */\r\n  markerWidth?: number;\r\n  /** Full content width (px, includes the marker track) — clamps the trailing viewport edge to match pin-right's `min(viewport, content)` anchor (spec 6c-1) so the window never renders past the pinned cell's visual position. */\r\n  contentWidth?: number;\r\n};\r\n\r\n/** SSR-safe / no-element preview: first N unpinned columns plus all pinned ones. */\r\nconst INITIAL_PREVIEW_COUNT = 20;\r\n\r\nfunction cumulativeRights(widths: number[]): number[] {\r\n  const rights = new Array<number>(widths.length);\r\n  let acc = 0;\r\n  for (let i = 0; i < widths.length; i++) {\r\n    acc += widths[i]!; // hot loop (per-scroll-tick): i < widths.length by loop condition\r\n    rights[i] = acc;\r\n  }\r\n  return rights;\r\n}\r\n\r\n/** First index whose cumulative right edge exceeds `x` (binary search over sorted `rights`). */\r\nfunction findFirstIndexPast(rights: number[], x: number): number {\r\n  let lo = 0;\r\n  let hi = rights.length;\r\n  while (lo < hi) {\r\n    const mid = (lo + hi) >>> 1;\r\n    if (rights[mid]! <= x) lo = mid + 1; // hot loop: mid in [lo, hi) which is within [0, rights.length)\r\n    else hi = mid;\r\n  }\r\n  return lo;\r\n}\r\n\r\n/** First index whose cumulative right edge is >= `x` (lower bound) — the last column still intersecting `x`. */\r\nfunction findLastIndexAt(rights: number[], x: number): number {\r\n  let lo = 0;\r\n  let hi = rights.length;\r\n  while (lo < hi) {\r\n    const mid = (lo + hi) >>> 1;\r\n    if (rights[mid]! < x) lo = mid + 1; // hot loop: mid in [lo, hi) which is within [0, rights.length)\r\n    else hi = mid;\r\n  }\r\n  return lo;\r\n}\r\n\r\nfunction buildIndices(\r\n  widths: number[],\r\n  pins: ((\"left\" | \"right\") | undefined)[],\r\n  unpinnedStart: number,\r\n  unpinnedEnd: number,\r\n): number[] {\r\n  const indices: number[] = [];\r\n  for (let i = 0; i < widths.length; i++) {\r\n    if (pins[i] === \"left\") indices.push(i);\r\n  }\r\n  for (let i = Math.max(0, unpinnedStart); i < Math.min(widths.length, unpinnedEnd); i++) {\r\n    if (pins[i] === undefined) indices.push(i);\r\n  }\r\n  for (let i = 0; i < widths.length; i++) {\r\n    if (pins[i] === \"right\") indices.push(i);\r\n  }\r\n  return indices;\r\n}\r\n\r\nfunction initialIndices(widths: number[], pins: ((\"left\" | \"right\") | undefined)[]): number[] {\r\n  return buildIndices(widths, pins, 0, Math.min(widths.length, INITIAL_PREVIEW_COUNT));\r\n}\r\n\r\nfunction computeIndices(snapshot: ScrollSnapshot, opts: Required<UseColumnWindowOptions>, velocityPx: number): number[] {\r\n  const { widths, pins, overscan, markerWidth, contentWidth } = opts;\r\n  if (widths.length === 0) return [];\r\n\r\n  let pinnedLeftWidth = 0;\r\n  let pinnedRightWidth = 0;\r\n  for (let i = 0; i < widths.length; i++) {\r\n    // hot loop (per-scroll-tick): i < widths.length by loop condition, pins is same-length parallel array\r\n    if (pins[i] === \"left\") pinnedLeftWidth += widths[i]!;\r\n    if (pins[i] === \"right\") pinnedRightWidth += widths[i]!;\r\n  }\r\n\r\n  // dir=\"rtl\" scroll containers report scrollLeft <= 0 growing negative as the user scrolls;\r\n  // normalize to the same rightward-positive axis the cumulative widths are built on (adazzle rdg).\r\n  const scrollLeft = Math.abs(snapshot.scrollLeft);\r\n\r\n  // Mirrors pinnedInsetStyle's min(viewportWidth, contentWidth) anchor (spec 6c-1): when content is\r\n  // narrower than the viewport, the trailing edge is the content's own edge, not the viewport's.\r\n  const effectiveClientWidth = contentWidth > 0 ? Math.min(snapshot.clientWidth, contentWidth) : snapshot.clientWidth;\r\n\r\n  // `rights` excludes the marker track; that nets out on the start side but not on viewEnd, which needs an explicit -markerWidth.\r\n  const viewStart = scrollLeft + pinnedLeftWidth;\r\n  const viewEnd = scrollLeft + effectiveClientWidth - pinnedRightWidth - markerWidth;\r\n\r\n  const rights = cumulativeRights(widths);\r\n  const startIndex = findFirstIndexPast(rights, viewStart);\r\n  // Lower-bound search + 1: the column at findLastIndexAt is still partially visible at viewEnd.\r\n  const endIndex = findLastIndexAt(rights, viewEnd) + 1;\r\n\r\n  // Velocity overscan biases the LEADING edge only, same asymmetric-buffer design as useRowWindow.\r\n  // Px-to-column count uses the average width across the currently visible band (columns aren't\r\n  // uniform width, so there's no single \"rowHeight\" to divide by) — a coarse but cheap estimate that\r\n  // only needs to be in the right ballpark, since the true guarantee is the transform never scrolling\r\n  // past rendered content, not pixel-exact column counts.\r\n  const visibleCount = Math.max(1, endIndex - startIndex);\r\n  const avgColWidth = Math.max(1, (rights[endIndex - 1] ?? effectiveClientWidth) - (rights[startIndex - 1] ?? 0)) / visibleCount;\r\n  // Same px cap as useRowWindow so a narrow viewport doesn't undershoot the worst-case single-tick\r\n  // delta — see VELOCITY_OVERSCAN_CAP_PX's doc comment for why \"N viewports\" alone isn't enough.\r\n  const maxVelocityCols = Math.max(Math.ceil((effectiveClientWidth / avgColWidth) * 2), Math.ceil(VELOCITY_OVERSCAN_CAP_PX / avgColWidth));\r\n  const velocityCols = Math.min(maxVelocityCols, Math.max(0, Math.ceil(velocityPx / avgColWidth) - 1));\r\n  const scrollingLeft = snapshot.deltaLeft < 0;\r\n  const startOverscan = scrollingLeft ? overscan + velocityCols : overscan;\r\n  const endOverscan = scrollingLeft ? overscan : overscan + velocityCols;\r\n\r\n  return buildIndices(widths, pins, startIndex - startOverscan, endIndex + endOverscan);\r\n}\r\n\r\nfunction indicesEqual(a: number[], b: number[]): boolean {\r\n  if (a.length !== b.length) return false;\r\n  for (let i = 0; i < a.length; i++) {\r\n    if (a[i] !== b[i]) return false;\r\n  }\r\n  return true;\r\n}\r\n\r\n/**\r\n * Computes the windowed column-index set (display order) to render, driven by the scroll\r\n * element's live horizontal scroll position + width. Mirrors {@link useRowWindow}'s\r\n * SSR-safe-initial / binary-search / requestFlush-while-scrolling machinery, on the column axis:\r\n * all pinned-left indices, then the contiguous unpinned window intersecting the visible\r\n * (non-pinned) viewport band `[scrollLeft + pinnedLeftWidth, scrollLeft + min(clientWidth, contentWidth) - pinnedRightWidth]`\r\n * with `+-overscan`, then all pinned-right indices. Pinned columns always render (they're in\r\n * the set by construction, independent of scrollLeft), so they never blank or lose focus.\r\n * `scrollLeft` is normalized with `Math.abs` so `dir=\"rtl\"` containers (which report it <= 0)\r\n * compute the same band as LTR, and the band's trailing edge includes any column it only\r\n * partially intersects, so the viewport's last pixel column is never dropped. The `min(clientWidth,\r\n * contentWidth)` clamp mirrors pin-right's visual anchor (spec 6c-1) so the window never renders a\r\n * column past where the pinned-right cell actually sits when content is narrower than the viewport.\r\n */\r\nexport function useColumnWindow(\r\n  scrollRef: RefObject<HTMLElement | null>,\r\n  opts: UseColumnWindowOptions,\r\n): { indices: number[] } {\r\n  // The horizontal scroll var tracks every pixel like the row canvas (checklist step 4), so the\r\n  // window only needs to cover the gap to the next recompute — 1 column of overscan is enough.\r\n  const { widths, pins, overscan = 1, markerWidth = 0, contentWidth = 0 } = opts;\r\n  const resolvedOpts = { widths, pins, overscan, markerWidth, contentWidth };\r\n\r\n  const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\r\n\r\n  // Mutable so the store listener (subscribed once per element) always compares against the\r\n  // latest computed set without re-subscribing on every option change.\r\n  const indicesRef = useRef<number[] | null>(null);\r\n  const optsRef = useRef(resolvedOpts);\r\n  optsRef.current = resolvedOpts;\r\n  // Own estimator instance (task 1: state lives per-hook, not in the shared element store).\r\n  // Same feed discipline as useRowWindow: only the effect's listener/healing paths update it; the\r\n  // render-time useMemo only reads the latest estimate.\r\n  const velocityRef = useRef<VelocityEstimator>(createVelocityEstimator());\r\n  const velocityEstimateRef = useRef(0);\r\n\r\n  useEffect(() => {\r\n    const element = scrollRef.current;\r\n    if (!element) return;\r\n    const store = getElementStore(element);\r\n\r\n    // Fires from the retry timer when a settle's idle-reset lands with no further scroll tick to\r\n    // ride in on (see use-row-window.ts's updateVelocityEstimate onIdleReset doc) — same healing\r\n    // shape as the no-element fallback below: a plain forceUpdate, not inside a store notify() cycle.\r\n    // Reads velocityRef directly (not velocityEstimateRef, which only the recompute/heal paths below\r\n    // write) since the retry timer mutates the estimator's `estimate` field out-of-band from those.\r\n    const onIdleReset = () => {\r\n      velocityEstimateRef.current = velocityRef.current.estimate;\r\n      const snap = store.getSnapshot();\r\n      const next = computeIndices(snap, optsRef.current, velocityEstimateRef.current);\r\n      const prev = indicesRef.current;\r\n      if (prev && indicesEqual(prev, next)) return;\r\n      indicesRef.current = next;\r\n      forceUpdate();\r\n    };\r\n\r\n    const recompute = () => {\r\n      const snap = store.getSnapshot();\r\n      velocityEstimateRef.current = updateVelocityEstimate(velocityRef.current, snap.deltaLeft, snap.isScrolling, onIdleReset);\r\n      const next = computeIndices(snap, optsRef.current, velocityEstimateRef.current);\r\n      const prev = indicesRef.current;\r\n      if (prev && indicesEqual(prev, next)) return;\r\n      indicesRef.current = next;\r\n      // Enqueue instead of flushing directly — the store drains every listener's queued update\r\n      // (row window + column window) into one flushSync after this tick's notify() finishes.\r\n      store.requestFlush(forceUpdate);\r\n    };\r\n\r\n    // The first render ran before this ref existed, so indicesRef holds the no-element fallback;\r\n    // heal it now (plain forceUpdate, never flushSync/requestFlush — this runs post-commit, not\r\n    // mid-scroll, and there's no notify() cycle here to drain into).\r\n    const prev = indicesRef.current;\r\n    const snap = store.getSnapshot();\r\n    velocityEstimateRef.current = updateVelocityEstimate(velocityRef.current, snap.deltaLeft, snap.isScrolling, onIdleReset);\r\n    const real = computeIndices(snap, optsRef.current, velocityEstimateRef.current);\r\n    indicesRef.current = real;\r\n    if (!prev || !indicesEqual(prev, real)) forceUpdate();\r\n\r\n    const unsubscribe = store.subscribe(recompute);\r\n    return () => {\r\n      unsubscribe();\r\n      // Unmounting mid-wait must not fire forceUpdate on a gone component after teardown.\r\n      const pendingRetry = velocityRef.current.retryTimer;\r\n      if (pendingRetry !== null) {\r\n        clearTimeout(pendingRetry);\r\n        velocityRef.current.retryTimer = null;\r\n      }\r\n    };\r\n    // scrollRef is a ref object; re-subscribing is driven by its .current changing between calls, not by identity.\r\n    // oxlint-disable-next-line react-hooks/exhaustive-deps\r\n  }, [scrollRef.current]);\r\n\r\n  const element = scrollRef.current;\r\n  const snapshot = element ? getElementStore(element).getSnapshot() : null;\r\n\r\n  const indices = useMemo(() => {\r\n    // Reads (never updates) the estimate — the effect above is the sole feed point per tick.\r\n    const computed = snapshot ? computeIndices(snapshot, resolvedOpts, velocityEstimateRef.current) : initialIndices(widths, pins);\r\n    indicesRef.current = computed;\r\n    return computed;\r\n    // oxlint-disable-next-line react-hooks/exhaustive-deps\r\n  }, [element, snapshot, widths, pins, overscan, markerWidth, contentWidth]);\r\n\r\n  return { indices };\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/windowing/use-column-window.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/windowing/use-pin-shadow-edges.ts",
      "content": "\"use client\";\r\n\r\nimport { useLayoutEffect, type RefObject } from \"react\";\r\nimport { inlineDistanceFromEnd, inlineDistanceFromStart, inlineEndEdge, inlineStartEdge, type GridDirection } from \"./direction\";\r\nimport { gridAttrSelector } from \"../data-attributes\";\r\n\r\n/**\r\n * Measures the ACTUAL rendered edge of the last pinned-left / first pinned-right HEADER cell and\r\n * writes it as `--grid-pin-shadow-left-x`/`-right-x` on `viewportRef`, instead of trusting a\r\n * JS-summed column-width total. CSS Grid rounds each track's flow position independently\r\n * (subpixel snapping), so a pinned cell's real rendered edge can drift a fraction of a px from\r\n * the sum of its own + preceding columns' declared widths — invisible with whole-px widths, but\r\n * visible (shadow \"a hair off\" the cell border) once resizing has produced fractional widths\r\n * across several pinned columns. Reading `getBoundingClientRect()` sidesteps the divergence\r\n * entirely: the shadow inherits the SAME rounded geometry the browser already gave the cell.\r\n * Header cells (not body cells) are the measurement source: they always render regardless of\r\n * `rowCount`/row virtualization, and are pixel-identical to body cells at the same column (same\r\n * grid template, same track).\r\n *\r\n * Re-measures on mount and on any resize of the boundary header cells (`ResizeObserver`) — a\r\n * pinned cell's on-screen position is scroll-independent (see pinned-inset-style.ts), so unlike\r\n * use-scrolled-edges.ts this never needs to run per scroll tick.\r\n */\r\nexport function usePinShadowEdges(\r\n  viewportRef: RefObject<HTMLElement | null>,\r\n  hasPinnedLeft: boolean,\r\n  hasPinnedRight: boolean,\r\n  direction: GridDirection = \"ltr\",\r\n  /**\r\n   * Serialized layout state: pins, marker width, column widths. `hasPinnedLeft` alone cannot drive\r\n   * the re-measure - with a marker column present the band survives unpinning the last pinned\r\n   * COLUMN, so the flag stays true across the change. The ResizeObserver below cannot cover it\r\n   * either: adding a marker column MOVES the boundary header without resizing it or the viewport,\r\n   * so no entry ever fires. Anything that can shift the boundary edge belongs in this string.\r\n   */\r\n  pinSignature: string = \"\",\r\n): void {\r\n  useLayoutEffect(() => {\r\n    const viewport = viewportRef.current;\r\n    if (!viewport || (!hasPinnedLeft && !hasPinnedRight)) return;\r\n\r\n    const write = () => {\r\n      const viewportRect = viewport.getBoundingClientRect();\r\n      // Both vars are consumed as inset-inline-start/-end offsets, so both are measured as inline\r\n      // distances. getBoundingClientRect only reports physical edges, so which edge bounds the\r\n      // pinned band inverts with direction: the last pinned-INLINE-START cell is the one furthest\r\n      // from the inline start, i.e. largest `right` in LTR but smallest `left` in RTL.\r\n      if (hasPinnedLeft) {\r\n        const last = furthestFromInlineStart(viewport.querySelectorAll<HTMLElement>(`[role=\"columnheader\"]${gridAttrSelector(\"pinned\", \"left\")}`), direction);\r\n        if (last) {\r\n          const edge = inlineEndEdge(last.getBoundingClientRect(), direction);\r\n          viewport.style.setProperty(\"--grid-pin-shadow-left-x\", `${inlineDistanceFromStart(edge, viewportRect, direction)}px`);\r\n        } else {\r\n          // No pinned-left COLUMN, but the band still exists when a marker column is on. Leaving\r\n          // a previously-measured value here strands the shadow at the unpinned column's old\r\n          // edge; removing it hands the style back to its marker-width fallback.\r\n          viewport.style.removeProperty(\"--grid-pin-shadow-left-x\");\r\n        }\r\n      }\r\n      if (hasPinnedRight) {\r\n        const first = furthestFromInlineEnd(viewport.querySelectorAll<HTMLElement>(`[role=\"columnheader\"]${gridAttrSelector(\"pinned\", \"right\")}`), direction);\r\n        if (first) {\r\n          const edge = inlineStartEdge(first.getBoundingClientRect(), direction);\r\n          viewport.style.setProperty(\"--grid-pin-shadow-right-x\", `${inlineDistanceFromEnd(edge, viewportRect, direction)}px`);\r\n        } else {\r\n          viewport.style.removeProperty(\"--grid-pin-shadow-right-x\");\r\n        }\r\n      }\r\n    };\r\n\r\n    write();\r\n    // ResizeObserver is absent in jsdom/SSR (matches the same guard in use-row-window.ts) — the\r\n    // mount-time write() above still runs, just without live re-measurement on resize there.\r\n    if (typeof ResizeObserver === \"undefined\") return;\r\n    const observer = new ResizeObserver(write);\r\n    observer.observe(viewport);\r\n    // every header cell, not just the boundary ones: a PRECEDING column's resize shifts the\r\n    // boundary cell's position without changing the boundary cell's own size, so its own\r\n    // ResizeObserver entry alone would miss that — the header row as a whole is small (one row).\r\n    for (const el of viewport.querySelectorAll<HTMLElement>('[role=\"columnheader\"]')) observer.observe(el);\r\n    return () => observer.disconnect();\r\n    // viewportRef is a ref object; re-running is driven by .current/hasPinnedLeft/hasPinnedRight changing.\r\n    // oxlint-disable-next-line react-hooks/exhaustive-deps\r\n  }, [viewportRef.current, hasPinnedLeft, hasPinnedRight, direction, pinSignature]);\r\n}\r\n\r\n/** The pinned-inline-start band's boundary cell: the one whose inline-end edge is furthest along the inline axis. */\r\nfunction furthestFromInlineStart(cells: NodeListOf<HTMLElement>, direction: GridDirection): HTMLElement | undefined {\r\n  let best: HTMLElement | undefined;\r\n  let bestDistance = -Infinity;\r\n  for (const cell of cells) {\r\n    const rect = cell.getBoundingClientRect();\r\n    // Compared against the cell's own rect, so this is a signed position on the inline axis; only\r\n    // the ordering matters here, not the origin.\r\n    const distance = direction === \"rtl\" ? -rect.left : rect.right;\r\n    if (distance > bestDistance) {\r\n      bestDistance = distance;\r\n      best = cell;\r\n    }\r\n  }\r\n  return best;\r\n}\r\n\r\n/** The pinned-inline-end band's boundary cell: the one whose inline-start edge comes earliest on the inline axis. */\r\nfunction furthestFromInlineEnd(cells: NodeListOf<HTMLElement>, direction: GridDirection): HTMLElement | undefined {\r\n  let best: HTMLElement | undefined;\r\n  let bestDistance = Infinity;\r\n  for (const cell of cells) {\r\n    const rect = cell.getBoundingClientRect();\r\n    const distance = direction === \"rtl\" ? -rect.right : rect.left;\r\n    if (distance < bestDistance) {\r\n      bestDistance = distance;\r\n      best = cell;\r\n    }\r\n  }\r\n  return best;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/windowing/use-pin-shadow-edges.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/windowing/use-row-window.ts",
      "content": "\"use client\";\r\n\r\nimport { useEffect, useMemo, useReducer, useRef, type RefObject } from \"react\";\r\nimport { isDev } from \"../is-dev\";\r\nimport { getElementStore, INITIAL_SNAPSHOT, type ScrollSnapshot } from \"./use-scroll-snapshot\";\r\nimport { createVelocityEstimator, updateVelocityEstimate, VELOCITY_OVERSCAN_CAP_PX, type VelocityEstimator } from \"./velocity-estimator\";\r\n\r\n/** Inclusive-exclusive data-row window: rows `[start, end)` should render, plus its px top. */\r\nexport type RowWindow = {\r\n  start: number;\r\n  end: number;\r\n  windowTop: number;\r\n  /**\r\n   * False only for the one render before the scroll element is attached and its mount effect has\r\n   * measured it (`computeWindow`'s `!element` fallback) — that render's `{start,end}` is a fixed\r\n   * guess (`[0, min(rowCount,30))`), not a real layout observation, and is always immediately\r\n   * superseded by a real one in the same effect flush. Consumers gating `onRowWindowChange` on this\r\n   * (see body.tsx) skip notifying for it, so a fresh mount fires the callback once (the real window),\r\n   * not twice (guess, then correction) — same data either way, just the transient guess suppressed.\r\n   */\r\n  measured: boolean;\r\n};\r\n\r\n/** {@link useRowWindow} options. */\r\nexport type UseRowWindowOptions = {\r\n  rowCount: number;\r\n  rowHeight: number;\r\n  /** Height of the sticky header track PLUS any pinned-top band (PLAN §3); data row `i` starts at `dataRowTop + i * rowHeight`. */\r\n  dataRowTop: number;\r\n  /** Pinned-bottom band height (px) — shrinks the effective viewport bottom so the row window never renders rows the band would cover. 0 when there's no pinned-bottom band. */\r\n  bottomInset?: number;\r\n  /** Extra rows rendered beyond the visible viewport on each side. */\r\n  overscan?: number;\r\n};\r\n\r\n/** Hard cap on rendered rows — an unbounded-height grid degrades to this instead of freezing the tab. */\r\nconst MAX_RENDERED_ROWS = 200;\r\n\r\nlet warnedUnboundedHeight = false;\r\n\r\nfunction computeWindow(\r\n  element: HTMLElement | null,\r\n  snapshot: ScrollSnapshot,\r\n  opts: Required<UseRowWindowOptions>,\r\n  velocityPx: number,\r\n): RowWindow {\r\n  const { rowCount, rowHeight, dataRowTop, bottomInset, overscan } = opts;\r\n  if (!element) {\r\n    return { start: 0, end: Math.min(rowCount, 30), windowTop: 0, measured: false };\r\n  }\r\n  const { scrollTop, clientHeight: rawClientHeight, deltaTop } = snapshot;\r\n  const clientHeight = Math.max(0, rawClientHeight - bottomInset);\r\n  // Velocity overscan biases the LEADING edge only (the direction of travel) — the trailing edge\r\n  // a fling is scrolling away from keeps the floor, matching the spec's asymmetric-buffer design.\r\n  // -1 row: the base `overscan` already covers one row of gap, so sub-row jitter (e.g. a 1px\r\n  // scroll-position correction) contributes zero extra — only velocity beyond that baseline counts.\r\n  // Cap: sized in px, not \"viewport count\" — a small viewport (e.g. the blank-detector's 360px/10-row\r\n  // fixture) would otherwise cap below the documented worst-case thumb-drag delta (~1500px). See\r\n  // VELOCITY_OVERSCAN_CAP_PX's doc comment for why 1600 is the measured minimum, not a round number\r\n  // with slack — every px above it is full-swap FPS cost with no blank-coverage benefit.\r\n  const maxVelocityRows = Math.max(\r\n    Math.ceil((clientHeight / rowHeight) * 2), // >= 2 viewports of buffer\r\n    Math.ceil(VELOCITY_OVERSCAN_CAP_PX / rowHeight),\r\n  );\r\n  const velocityRows = Math.min(maxVelocityRows, Math.max(0, Math.ceil(velocityPx / rowHeight) - 1));\r\n  const leadingOverscan = overscan + velocityRows;\r\n  const scrollingUp = deltaTop < 0;\r\n  const startOverscan = scrollingUp ? leadingOverscan : overscan;\r\n  const endOverscan = scrollingUp ? overscan : leadingOverscan;\r\n  const start = Math.max(0, Math.floor((scrollTop - dataRowTop) / rowHeight) - startOverscan);\r\n  let end = Math.min(rowCount, Math.ceil((scrollTop + clientHeight - dataRowTop) / rowHeight) + endOverscan);\r\n  if (end - start > MAX_RENDERED_ROWS) {\r\n    end = start + MAX_RENDERED_ROWS;\r\n    if (isDev() && !warnedUnboundedHeight) {\r\n      warnedUnboundedHeight = true;\r\n      console.warn(\r\n        `gridcn: the grid viewport shows more than ${MAX_RENDERED_ROWS} rows at once — its height is probably unbounded, which disables virtualization. Give the grid a bounded height (e.g. className=\"h-150\" or h-full inside a sized parent). Rendering is capped at ${MAX_RENDERED_ROWS} rows.`,\r\n      );\r\n    }\r\n  }\r\n  return { start, end: Math.max(start, end), windowTop: start * rowHeight, measured: true };\r\n}\r\n\r\n/**\r\n * Computes the windowed data-row range `[start, end)` to render, driven by an element's live\r\n * scroll position + size. The header is in-flow inside the scroll container, so data row `i`\r\n * sits at `dataRowTop + i * rowHeight`.\r\n *\r\n * Commit timing replicates TanStack Virtual's anti-blank mechanism (research/tanstack-virtual-study.md):\r\n * the store listener recomputes the row window and only triggers a re-render when `{start,end}`\r\n * actually changed (scrolling within the overscan buffer is a no-op). A changed window is queued\r\n * via {@link ElementStore.requestFlush} rather than flushed here directly — the store drains every\r\n * listener's queued update (this hook's + {@link useColumnWindow}'s) into a single `flushSync` once\r\n * per tick, so a diagonal scroll that moves both windows in the same event still commits once. The\r\n * new rows land synchronously inside the scroll event — before the compositor's next paint — instead\r\n * of risking a deferred React commit that lets the compositor reveal unrendered rows.\r\n */\r\nexport function useRowWindow(scrollRef: RefObject<HTMLElement | null>, opts: UseRowWindowOptions): RowWindow {\r\n  // The canvas transform tracks every scroll pixel (checklist step 4), so overscan only needs to\r\n  // cover the gap between a scroll tick and the next window recompute — 1 row is enough.\r\n  const { rowCount, rowHeight, dataRowTop, bottomInset = 0, overscan = 1 } = opts;\r\n  const resolvedOpts = { rowCount, rowHeight, dataRowTop, bottomInset, overscan };\r\n\r\n  const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\r\n\r\n  // Mutable so the store listener (a stable callback, subscribed once per element) always\r\n  // compares against the latest computed window without re-subscribing on every option change.\r\n  const windowRef = useRef<RowWindow | null>(null);\r\n  const optsRef = useRef(resolvedOpts);\r\n  optsRef.current = resolvedOpts;\r\n  // One estimator per hook instance (task 1: state lives in the hook, not the shared element\r\n  // store, since the store is per-element and multiple row-window consumers could theoretically\r\n  // want independent overscan tuning). Fed exclusively from the subscribed-listener/healing paths\r\n  // below (the actual per-tick observation points) — the render-time useMemo only *reads* the\r\n  // latest estimate, so a snapshot's delta is never counted twice.\r\n  const velocityRef = useRef<VelocityEstimator>(createVelocityEstimator());\r\n  const velocityEstimateRef = useRef(0);\r\n\r\n  useEffect(() => {\r\n    const element = scrollRef.current;\r\n    if (!element) return;\r\n    const store = getElementStore(element);\r\n\r\n    // Fires from the retry timer when a settle's idle-reset lands with no further scroll tick to\r\n    // ride in on (see updateVelocityEstimate's onIdleReset doc) — same healing shape as the no-element\r\n    // fallback below: a plain forceUpdate, never flushSync, since it's not inside a store notify() cycle.\r\n    // Reads velocityRef directly (not velocityEstimateRef, which only the recompute/heal paths below\r\n    // write) since the retry timer mutates the estimator's `estimate` field out-of-band from those.\r\n    const onIdleReset = () => {\r\n      velocityEstimateRef.current = velocityRef.current.estimate;\r\n      const snap = store.getSnapshot();\r\n      const next = computeWindow(element, snap, optsRef.current, velocityEstimateRef.current);\r\n      const prev = windowRef.current;\r\n      if (prev && prev.start === next.start && prev.end === next.end) return;\r\n      windowRef.current = next;\r\n      forceUpdate();\r\n    };\r\n\r\n    const recompute = () => {\r\n      const snap = store.getSnapshot();\r\n      velocityEstimateRef.current = updateVelocityEstimate(velocityRef.current, snap.deltaTop, snap.isScrolling, onIdleReset);\r\n      const next = computeWindow(element, snap, optsRef.current, velocityEstimateRef.current);\r\n      const prev = windowRef.current;\r\n      if (prev && prev.start === next.start && prev.end === next.end) return;\r\n      windowRef.current = next;\r\n      // Enqueue instead of flushing directly — the store drains every listener's queued update\r\n      // (row window + column window) into one flushSync after this tick's notify() finishes.\r\n      store.requestFlush(forceUpdate);\r\n    };\r\n\r\n    // The first render ran before this ref existed, so windowRef holds the no-element fallback;\r\n    // heal it now (plain forceUpdate, never flushSync/requestFlush — this runs post-commit, not\r\n    // mid-scroll, and there's no notify() cycle here to drain into).\r\n    const prev = windowRef.current;\r\n    const snap = store.getSnapshot();\r\n    velocityEstimateRef.current = updateVelocityEstimate(velocityRef.current, snap.deltaTop, snap.isScrolling, onIdleReset);\r\n    const real = computeWindow(element, snap, optsRef.current, velocityEstimateRef.current);\r\n    windowRef.current = real;\r\n    if (!prev || prev.start !== real.start || prev.end !== real.end) forceUpdate();\r\n\r\n    const unsubscribe = store.subscribe(recompute);\r\n    return () => {\r\n      unsubscribe();\r\n      // Unmounting mid-wait must not fire forceUpdate on a gone component after teardown.\r\n      const pendingRetry = velocityRef.current.retryTimer;\r\n      if (pendingRetry !== null) {\r\n        clearTimeout(pendingRetry);\r\n        velocityRef.current.retryTimer = null;\r\n      }\r\n    };\r\n    // scrollRef is a ref object; re-subscribing is driven by its .current changing between calls, not by identity.\r\n    // oxlint-disable-next-line react-hooks/exhaustive-deps\r\n  }, [scrollRef.current]);\r\n\r\n  const element = scrollRef.current;\r\n  const snapshot = element ? getElementStore(element).getSnapshot() : INITIAL_SNAPSHOT;\r\n\r\n  return useMemo(() => {\r\n    // Reads (never updates) the estimate — the effect above is the sole feed point per tick.\r\n    const computed = computeWindow(element, snapshot, resolvedOpts, velocityEstimateRef.current);\r\n    windowRef.current = computed;\r\n    return computed;\r\n    // oxlint-disable-next-line react-hooks/exhaustive-deps\r\n  }, [element, snapshot, rowCount, rowHeight, dataRowTop, bottomInset, overscan]);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/windowing/use-row-window.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/windowing/use-scroll-snapshot.ts",
      "content": "\"use client\";\r\n\r\nimport { useEffect, useReducer, useRef, type RefObject } from \"react\";\r\nimport { flushSync } from \"react-dom\";\r\nimport { normalizeScrollLeft } from \"./direction\";\r\n\r\n/** Live scroll geometry of a scroll element, shared by row- and (future) column-windowing hooks. */\r\nexport type ScrollSnapshot = {\r\n  scrollTop: number;\r\n  scrollLeft: number;\r\n  clientWidth: number;\r\n  clientHeight: number;\r\n  /** Carried on the snapshot (not re-read from the element) so consumers like useScrolledEdges never touch the DOM after a same-tick style write invalidates layout — see {@link readGeometry}. */\r\n  scrollWidth: number;\r\n  scrollHeight: number;\r\n  /** True from the first scroll event until 150ms of inactivity (or native `scrollend`) — gates the flushSync anti-blank commit. */\r\n  isScrolling: boolean;\r\n  /** Signed px moved since the previous commit — raw per-tick velocity input for overscan sizing (computeWindow/computeIndices stay pure functions of this, no hidden state). */\r\n  deltaTop: number;\r\n  deltaLeft: number;\r\n};\r\n\r\nexport const INITIAL_SNAPSHOT: ScrollSnapshot = {\r\n  scrollTop: 0,\r\n  scrollLeft: 0,\r\n  clientWidth: 0,\r\n  clientHeight: 0,\r\n  scrollWidth: 0,\r\n  scrollHeight: 0,\r\n  isScrolling: false,\r\n  deltaTop: 0,\r\n  deltaLeft: 0,\r\n};\r\n\r\n/** Debounced isScrolling reset, re-armed every scroll tick — the safety net when `scrollend` doesn't fire (Safari has no support; jsdom declares the IDL attribute but never dispatches it). */\r\nconst ISSCROLLING_DEBOUNCE_MS = 150;\r\n\r\n/** Raw scroll-element geometry, all six properties read once. */\r\ntype ScrollGeometry = {\r\n  scrollTop: number;\r\n  scrollLeft: number;\r\n  clientWidth: number;\r\n  clientHeight: number;\r\n  scrollWidth: number;\r\n  scrollHeight: number;\r\n};\r\n\r\n/** One read pass over every geometry property `commit()` needs — called once per tick, before any style write, so nothing downstream forces a reflow by reading after `writeScrollVars` has already dirtied layout. */\r\nfunction readGeometry(element: HTMLElement): ScrollGeometry {\r\n  return {\r\n    scrollTop: element.scrollTop,\r\n    scrollLeft: element.scrollLeft,\r\n    clientWidth: element.clientWidth,\r\n    clientHeight: element.clientHeight,\r\n    scrollWidth: element.scrollWidth,\r\n    scrollHeight: element.scrollHeight,\r\n  };\r\n}\r\n\r\nfunction toSnapshot(geometry: ScrollGeometry, isScrolling: boolean, previous: ScrollSnapshot): ScrollSnapshot {\r\n  const { scrollTop, scrollLeft } = geometry;\r\n  return {\r\n    ...geometry,\r\n    isScrolling,\r\n    deltaTop: scrollTop - previous.scrollTop,\r\n    deltaLeft: scrollLeft - previous.scrollLeft,\r\n  };\r\n}\r\n\r\n/** Deltas excluded on purpose: they're pure functions of scrollTop/scrollLeft, so they never differ when those are equal. */\r\nfunction snapshotsEqual(a: ScrollSnapshot, b: ScrollSnapshot): boolean {\r\n  return (\r\n    a.scrollTop === b.scrollTop &&\r\n    a.scrollLeft === b.scrollLeft &&\r\n    a.clientWidth === b.clientWidth &&\r\n    a.clientHeight === b.clientHeight &&\r\n    a.scrollWidth === b.scrollWidth &&\r\n    a.scrollHeight === b.scrollHeight &&\r\n    a.isScrolling === b.isScrolling\r\n  );\r\n}\r\n\r\ntype ElementStore = {\r\n  subscribe: (onChange: () => void) => () => void;\r\n  getSnapshot: () => ScrollSnapshot;\r\n  /** Registers the sticky Viewport element that receives the imperative `--grid-scroll-*` var writes on every tick. */\r\n  setViewportElement: (el: HTMLElement | null) => void;\r\n  /**\r\n   * Queues a window-change `forceUpdate` to commit once, after every listener in this tick's\r\n   * `notify()` has run — the mechanism that collapses row- and column-window flushSync into a\r\n   * single React commit per scroll tick. Only for listeners whose update is itself gated on a\r\n   * computed value changing (row/column window); {@link useScrollSnapshot} and friends keep\r\n   * calling their own `forceUpdate` directly and must never route through this.\r\n   */\r\n  requestFlush: (fn: () => void) => void;\r\n};\r\n\r\n/** Writes the two live-scroll CSS vars (clamped for rubber-band) onto the Viewport element — one write moves canvas + pinned cells atomically. Takes pre-read geometry so the hot per-tick call site never re-reads the (by-then-dirtied) element after this write. */\r\nfunction writeScrollVars(viewport: HTMLElement, geometry: ScrollGeometry): void {\r\n  const maxTop = Math.max(0, geometry.scrollHeight - geometry.clientHeight);\r\n  const maxLeft = Math.max(0, geometry.scrollWidth - geometry.clientWidth);\r\n  const top = Math.min(Math.max(geometry.scrollTop, 0), maxTop);\r\n  // One positive inline-start-relative axis for both directions: `dir=\"rtl\"` containers report\r\n  // scrollLeft as 0 at the inline start growing negative toward the inline end, so the magnitude is\r\n  // the shared axis every offset here and in use-column-window.ts is built on. abs() also still\r\n  // pins rubber-band overscroll to 0 via the clamp below, which is what the old signed clamp did.\r\n  // The transform's SIGN is not applied here at all — it lives in the --grid-dir var the viewport\r\n  // carries, so this hot per-tick write is identical in both directions (no branch, no extra work).\r\n  const left = Math.min(normalizeScrollLeft(geometry.scrollLeft), maxLeft);\r\n  viewport.style.setProperty(\"--grid-scroll-top\", `${top}px`);\r\n  viewport.style.setProperty(\"--grid-scroll-left\", `${left}px`);\r\n}\r\n\r\n/** Per-element cached subscribe/getSnapshot so repeated hook calls (row window, column window, …) share one store. */\r\nconst elementStores = new WeakMap<HTMLElement, ElementStore>();\r\n\r\n/**\r\n * One shared snapshot + listener Set per element (adazzle useScrollState pattern) so every\r\n * subscriber is notified, not just the first, and isScrolling state isn't duplicated per hook.\r\n * Exported so other windowing hooks (e.g. {@link useColumnWindow}) share the same element store\r\n * instead of double-subscribing to scroll/resize.\r\n */\r\nexport function getElementStore(element: HTMLElement): ElementStore {\r\n  let store = elementStores.get(element);\r\n  if (store) return store;\r\n\r\n  // Seed \"previous\" from the element's own current position (not INITIAL_SNAPSHOT's 0,0): a\r\n  // restored/SSR scroll position observed on first read is not scroll velocity, so delta must be 0.\r\n  const seedGeometry = readGeometry(element);\r\n  let snapshot = toSnapshot(seedGeometry, false, { ...INITIAL_SNAPSHOT, scrollTop: seedGeometry.scrollTop, scrollLeft: seedGeometry.scrollLeft });\r\n  const listeners = new Set<() => void>();\r\n  let debounceTimer: ReturnType<typeof setTimeout> | null = null;\r\n  let viewportElement: HTMLElement | null = null;\r\n\r\n  // Row/column window listeners enqueue here instead of flushing individually (task: one commit\r\n  // per tick) — drained by a single flushSync/batch right after notify() finishes this tick.\r\n  let pendingFlushes: Set<() => void> | null = null;\r\n\r\n  const requestFlush = (fn: () => void) => {\r\n    pendingFlushes ??= new Set();\r\n    pendingFlushes.add(fn);\r\n  };\r\n\r\n  const notify = () => listeners.forEach((listener) => listener());\r\n\r\n  // Standalone call site (setViewportElement, outside commit()'s hoisted-read tick) — not on the\r\n  // hot per-tick path, so a fresh direct read here is fine (checklist step 3 caveat b).\r\n  const syncViewportVars = () => {\r\n    if (viewportElement) writeScrollVars(viewportElement, readGeometry(element));\r\n  };\r\n\r\n  /** Drains this tick's queued window-change updates into exactly one React commit. */\r\n  const drainPendingFlushes = (sync: boolean) => {\r\n    const pending = pendingFlushes;\r\n    pendingFlushes = null;\r\n    if (!pending || pending.size === 0) return;\r\n    // React batches every setState called inside one flushSync callback into a single commit —\r\n    // this is what collapses row- and column-window updates from two commits into one.\r\n    if (sync) flushSync(() => pending.forEach((fn) => fn()));\r\n    else pending.forEach((fn) => fn());\r\n  };\r\n\r\n  // `forceSync`: the scrollend/debounce settle commit is not itself `isScrolling`, but it still\r\n  // fires from a real event/timer callback (never render) — safe, and required, to flush sync\r\n  // (task 2: gates the async-commit blanking window on the final settle).\r\n  //\r\n  // Read-once, write-once per tick (2026-08-02 optimization audit, confirmed medium): the old\r\n  // version read geometry for the viewport-var write, then read it AGAIN for the snapshot — the\r\n  // first write (a style mutation on an ancestor of `viewportElement`'s layout-affecting insets)\r\n  // had already dirtied layout, forcing a reflow on the second read. Hoisting one `readGeometry`\r\n  // call to the top removes that forced reflow; the write still runs before `notify()` (the\r\n  // transform/pinned insets must reflect the new position before any listener re-renders windowed\r\n  // content — moving it past `notify()` would reintroduce tearing).\r\n  const commit = (isScrolling: boolean, forceSync = false) => {\r\n    const geometry = readGeometry(element);\r\n    if (viewportElement) writeScrollVars(viewportElement, geometry);\r\n    const next = toSnapshot(geometry, isScrolling, snapshot);\r\n    if (!snapshotsEqual(next, snapshot)) {\r\n      snapshot = next;\r\n      notify();\r\n      drainPendingFlushes(isScrolling || forceSync);\r\n    }\r\n  };\r\n\r\n  const onScroll = () => {\r\n    // Always re-arm the debounce as a safety net (not gated on scrollend \"support\" — real\r\n    // scrollend cancels it first, so this only fires when scrollend never arrives).\r\n    if (debounceTimer !== null) clearTimeout(debounceTimer);\r\n    debounceTimer = setTimeout(() => {\r\n      debounceTimer = null;\r\n      // The scrollend-fallback commit fires from a real timer callback (never render), so a\r\n      // synchronous flush here is safe and closes the async-commit blanking window (task 2).\r\n      commit(false, true);\r\n    }, ISSCROLLING_DEBOUNCE_MS);\r\n    commit(true);\r\n  };\r\n\r\n  const onScrollEnd = () => {\r\n    if (debounceTimer !== null) {\r\n      clearTimeout(debounceTimer);\r\n      debounceTimer = null;\r\n    }\r\n    // Fires from the native `scrollend` event, never from render — safe to flush synchronously.\r\n    commit(false, true);\r\n  };\r\n\r\n  const onResize = () => commit(snapshot.isScrolling);\r\n\r\n  let cleanup: (() => void) | null = null;\r\n\r\n  store = {\r\n    subscribe(onChange) {\r\n      listeners.add(onChange);\r\n      if (!cleanup) {\r\n        element.addEventListener(\"scroll\", onScroll, { passive: true });\r\n        element.addEventListener(\"scrollend\", onScrollEnd, { passive: true });\r\n        // ResizeObserver is absent in jsdom/SSR; scroll events still drive the window there.\r\n        const resizeObserver = typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(onResize);\r\n        resizeObserver?.observe(element);\r\n        commit(snapshot.isScrolling);\r\n        cleanup = () => {\r\n          element.removeEventListener(\"scroll\", onScroll);\r\n          element.removeEventListener(\"scrollend\", onScrollEnd);\r\n          resizeObserver?.disconnect();\r\n          if (debounceTimer !== null) clearTimeout(debounceTimer);\r\n          // No listeners remain to notify; reset so a later re-subscribe doesn't inherit a stuck isScrolling=true.\r\n          snapshot = { ...snapshot, isScrolling: false };\r\n        };\r\n      }\r\n      return () => {\r\n        listeners.delete(onChange);\r\n        if (listeners.size === 0 && cleanup) {\r\n          cleanup();\r\n          cleanup = null;\r\n        }\r\n      };\r\n    },\r\n    getSnapshot() {\r\n      return snapshot;\r\n    },\r\n    setViewportElement(el) {\r\n      viewportElement = el;\r\n      syncViewportVars();\r\n    },\r\n    requestFlush,\r\n  };\r\n  elementStores.set(element, store);\r\n  return store;\r\n}\r\n\r\n/**\r\n * Subscribes to an element's live scroll geometry ({@link ScrollSnapshot}) without the\r\n * `useSyncExternalStore` re-render-on-every-tick behavior — callers that need a plain\r\n * read-and-rerender subscription (not the flushSync window-change gating in {@link useRowWindow})\r\n * can build on this directly (e.g. a future column-windowing hook).\r\n */\r\nexport function useScrollSnapshot(scrollRef: RefObject<HTMLElement | null>): ScrollSnapshot {\r\n  const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\r\n\r\n  useEffect(() => {\r\n    const element = scrollRef.current;\r\n    if (!element) return;\r\n    const store = getElementStore(element);\r\n    forceUpdate();\r\n    return store.subscribe(forceUpdate);\r\n    // scrollRef is a ref object; re-subscribing is driven by its .current changing between calls, not by identity.\r\n    // oxlint-disable-next-line react-hooks/exhaustive-deps\r\n  }, [scrollRef.current]);\r\n\r\n  const element = scrollRef.current;\r\n  if (element) return getElementStore(element).getSnapshot();\r\n  return INITIAL_SNAPSHOT;\r\n}\r\n\r\n/**\r\n * Subscribes to only an element's `clientWidth`/`clientHeight` (resize-rate, not scroll-rate) —\r\n * for consumers that only need viewport sizing and must NOT re-render on every scroll tick (the\r\n * element store notifies on every scroll pixel and `isScrolling` flip; forwarding all of that\r\n * through React would re-render the whole grid tree every tick, defeating virtualization).\r\n */\r\nexport function useElementDimensions(scrollRef: RefObject<HTMLElement | null>): {\r\n  clientWidth: number;\r\n  clientHeight: number;\r\n} {\r\n  const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\r\n  const lastRef = useRef({ clientWidth: 0, clientHeight: 0 });\r\n\r\n  useEffect(() => {\r\n    const element = scrollRef.current;\r\n    if (!element) return;\r\n    const store = getElementStore(element);\r\n\r\n    const checkForResize = () => {\r\n      const snap = store.getSnapshot();\r\n      const last = lastRef.current;\r\n      if (snap.clientWidth !== last.clientWidth || snap.clientHeight !== last.clientHeight) {\r\n        lastRef.current = { clientWidth: snap.clientWidth, clientHeight: snap.clientHeight };\r\n        forceUpdate();\r\n      }\r\n    };\r\n\r\n    checkForResize();\r\n    return store.subscribe(checkForResize);\r\n    // scrollRef is a ref object; re-subscribing is driven by its .current changing between calls, not by identity.\r\n    // oxlint-disable-next-line react-hooks/exhaustive-deps\r\n  }, [scrollRef.current]);\r\n\r\n  const element = scrollRef.current;\r\n  if (element) {\r\n    const snap = getElementStore(element).getSnapshot();\r\n    lastRef.current = { clientWidth: snap.clientWidth, clientHeight: snap.clientHeight };\r\n    return lastRef.current;\r\n  }\r\n  return lastRef.current;\r\n}\r\n\r\n/**\r\n * Registers the sticky Viewport element as the target of the scroll element's imperative\r\n * `--grid-scroll-top`/`--grid-scroll-left` var writes (checklist step 3). One write per scroll\r\n * tick moves the rows canvas, header layer, and pinned-cell insets atomically, with zero React\r\n * involvement between row/column window changes.\r\n */\r\nexport function useViewportElement(\r\n  scrollRef: RefObject<HTMLElement | null>,\r\n  viewportRef: RefObject<HTMLElement | null>,\r\n): void {\r\n  useEffect(() => {\r\n    const element = scrollRef.current;\r\n    const viewport = viewportRef.current;\r\n    if (!element || !viewport) return;\r\n    const store = getElementStore(element);\r\n    store.setViewportElement(viewport);\r\n    return () => store.setViewportElement(null);\r\n    // oxlint-disable-next-line react-hooks/exhaustive-deps\r\n  }, [scrollRef.current, viewportRef.current]);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/windowing/use-scroll-snapshot.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/windowing/use-scrolled-edges.ts",
      "content": "\"use client\";\r\n\r\nimport { useEffect, type RefObject } from \"react\";\r\nimport { getElementStore } from \"./use-scroll-snapshot\";\r\nimport { normalizeScrollLeft } from \"./direction\";\r\nimport { GRID_ATTR } from \"../data-attributes\";\r\n\r\n/** 1px slack so float rounding at exactly 0 / max doesn't flicker the shadow on/off. */\r\nconst EDGE_EPSILON = 1;\r\n\r\n/**\r\n * Writes `data-scrolled-left`/`data-scrolled-right`/`data-scrolled-top`/`data-scrolled-bottom`\r\n * directly onto `viewportRef`'s element on every scroll tick — imperative DOM writes, no React\r\n * state, no per-tick re-render (PLAN §6 pinned-edge shadow spec: \"no React per tick\", matching the\r\n * existing `--grid-scroll-*` var writer in use-row-window.ts). CSS reads these attributes to\r\n * show/hide a pinned boundary's shadow only when there's actually content scrolled beneath it on\r\n * that side — `data-scrolled-top`/`-bottom` back the pinned ROW band shadow (PLAN §3), the \"content\r\n * actually scrolled beneath\" gate this hook already established for pinned columns, reused verbatim\r\n * on the vertical axis.\r\n */\r\nexport function useScrolledEdges(\r\n  scrollRef: RefObject<HTMLElement | null>,\r\n  viewportRef: RefObject<HTMLElement | null>,\r\n): void {\r\n  useEffect(() => {\r\n    const scrollElement = scrollRef.current;\r\n    const viewport = viewportRef.current;\r\n    if (!scrollElement || !viewport) return;\r\n    const store = getElementStore(scrollElement);\r\n\r\n    const write = () => {\r\n      const snapshot = store.getSnapshot();\r\n      const { scrollTop, clientWidth, clientHeight } = snapshot;\r\n      // Normalized once, here, onto the positive inline-start axis — after which \"> 0\" means\r\n      // \"scrolled away from the inline start\" in BOTH directions and nothing re-branches on\r\n      // direction. The two attributes stay named left/right because the CSS selectors that read\r\n      // them sit on elements already positioned with inset-inline-*, which mirror themselves.\r\n      const scrollLeft = normalizeScrollLeft(snapshot.scrollLeft);\r\n      // Read off the snapshot, not the element — the element's geometry was already read once (and\r\n      // possibly dirtied by a same-tick style write) upstream in commit(); a second direct read here\r\n      // would force a redundant reflow (2026-08-02 optimization audit, confirmed medium).\r\n      const maxLeft = snapshot.scrollWidth - clientWidth;\r\n      const maxTop = snapshot.scrollHeight - clientHeight;\r\n      if (scrollLeft > EDGE_EPSILON) viewport.setAttribute(GRID_ATTR.scrolledLeft, \"\");\r\n      else viewport.removeAttribute(GRID_ATTR.scrolledLeft);\r\n      if (scrollLeft < maxLeft - EDGE_EPSILON) viewport.setAttribute(GRID_ATTR.scrolledRight, \"\");\r\n      else viewport.removeAttribute(GRID_ATTR.scrolledRight);\r\n      if (scrollTop > EDGE_EPSILON) viewport.setAttribute(GRID_ATTR.scrolledTop, \"\");\r\n      else viewport.removeAttribute(GRID_ATTR.scrolledTop);\r\n      if (scrollTop < maxTop - EDGE_EPSILON) viewport.setAttribute(GRID_ATTR.scrolledBottom, \"\");\r\n      else viewport.removeAttribute(GRID_ATTR.scrolledBottom);\r\n    };\r\n\r\n    write();\r\n    return store.subscribe(write);\r\n    // scrollRef/viewportRef are ref objects; re-subscribing is driven by their .current changing, not identity.\r\n    // oxlint-disable-next-line react-hooks/exhaustive-deps\r\n  }, [scrollRef.current, viewportRef.current]);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/windowing/use-scrolled-edges.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/windowing/velocity-estimator.ts",
      "content": "/**\r\n * Decaying-max velocity estimator (Phase 4): the window rendered after tick N must already cover\r\n * tick N+1's jump, but tick N+1's magnitude is unknown when tick N commits, and scrollbar-drag deltas\r\n * don't trend smoothly tick-to-tick (a slow tick can be immediately followed by a hard jump) — so\r\n * predicting \"the next delta\" from recent magnitudes alone can't be made reliable; a run of merely\r\n * average ticks gives no warning before an unprecedented one. Instead: any tick whose delta exceeds\r\n * {@link VELOCITY_TRIGGER_PX} (a real drag, not sub-row jitter) ramps the estimate toward the cap over\r\n * ~{@link VELOCITY_RAMP_TICKS} ticks, on the theory that once a drag is confirmed fast, ANY subsequent\r\n * tick could be the hard one — holding near the cap for the gesture's duration is what \"must hold near\r\n * the MAX of recent deltas, not the mean\" means in practice once deltas are this bursty. A small delta\r\n * WHILE STILL SCROLLING decays the estimate by {@link VELOCITY_DECAY_FACTOR} (gradual, not an instant\r\n * snap) — a mid-fling pause must not erode the buffer before the next hard jump. The settle commit\r\n * (`isScrolling` flips to false: native `scrollend`, or the 150ms debounce fallback) is different in\r\n * kind from a mid-scroll pause: the user has stopped, not paused, so it hard-resets the estimate to 0\r\n * (see the `!isScrolling` branch below) instead of relying on decay-while-scrolling, which never runs\r\n * again once no further scroll tick arrives — the original design left the window stuck at its\r\n * fling-time size indefinitely at rest (tablecn comparison report, 2026-07-17: measured 59 rows\r\n * mounted and unchanged after scroll stop). A new scroll right after settle starts this estimator\r\n * fresh via {@link createVelocityEstimator}'s same `{estimate: 0}` shape, so it ramps normally again.\r\n */\r\nconst VELOCITY_RAMP_TICKS = 3;\r\n\r\n/** Multiplicative decay applied per still-scrolling tick whose delta doesn't trigger the ramp — ~2-3 such ticks erode a maxed-out estimate back near the floor. */\r\nconst VELOCITY_DECAY_FACTOR = 0.5;\r\n\r\n/** Px cap on the leading-edge velocity buffer — the minimum that keeps the blank detector's random\r\n * 200-1500px thumb-drag deltas reliably covered (measured: 200-trial simulation fails below ~1500px,\r\n * clean at 1600px+); every px above that is pure full-swap FPS cost with no blank-coverage benefit\r\n * (spec 6c-8's sustained 800px/tick case never needs more than its own delta). Exported so\r\n * {@link useColumnWindow} shares the same cap. */\r\nexport const VELOCITY_OVERSCAN_CAP_PX = 1600;\r\n\r\n/** Below this, a delta is sub-row jitter (e.g. a 1px scroll-position correction), not drag signal — must not itself trigger the ramp. */\r\nconst VELOCITY_TRIGGER_PX = 72;\r\n\r\nexport type VelocityEstimator = { estimate: number; lastTickAt: number; retryTimer: ReturnType<typeof setTimeout> | null };\r\n\r\nexport function createVelocityEstimator(): VelocityEstimator {\r\n  return { estimate: 0, lastTickAt: 0, retryTimer: null };\r\n}\r\n\r\n/**\r\n * A settle commit (`isScrolling` false) only resets the estimate once this much real wall-clock\r\n * time has actually passed since the last scrolling tick — must be >= {@link ISSCROLLING_DEBOUNCE_MS}\r\n * so a genuine idle settle (native `scrollend` firing right after a real gesture ends, or our own\r\n * debounce fallback) reliably clears it, but a synthetic/discrete `scrollTop` write settles almost\r\n * instantly in real Chromium's own `scrollend` heuristic (confirmed: browser-mode blank-detector\r\n * test fires native `scrollend` after nearly every individual write, ~1 per tick, even at\r\n * requestAnimationFrame cadence) — without this gate, that per-tick \"fake settle\" would zero the\r\n * estimate between every real jump, undersizing the window for the next one and blanking (exactly\r\n * the failure mode the ramp-not-decay design was built to avoid, just moved to a new trigger).\r\n */\r\nconst SETTLE_RESET_MIN_IDLE_MS = 150;\r\n\r\n/**\r\n * Feeds one tick's signed delta + scrolling state in, returns the current estimate (px). A settle\r\n * observation that arrives before the idle gate has elapsed (native `scrollend` can fire faster\r\n * than {@link SETTLE_RESET_MIN_IDLE_MS} after a genuinely-final real tick, not just after a\r\n * synthetic one) schedules exactly one retry for the remaining wait, via `setTimeout`, so a true\r\n * idle settle is never silently dropped just because it was observed slightly too early — this is\r\n * what keeps the DOM from getting stuck at fling size indefinitely (the original bug) without\r\n * reintroducing the \"settle looks identical to a 1-tick pause\" false positive that caused blanking.\r\n * `onIdleReset` fires when the retry actually zeroes the estimate with no caller re-observing it —\r\n * the hook uses it to trigger one more window recompute, since otherwise a reset with no further\r\n * scroll tick would update the estimate but never repaint the (now too-large) DOM window to match.\r\n */\r\nexport function updateVelocityEstimate(\r\n  estimator: VelocityEstimator,\r\n  delta: number,\r\n  isScrolling: boolean,\r\n  onIdleReset?: () => void,\r\n): number {\r\n  const now = typeof performance !== \"undefined\" ? performance.now() : Date.now();\r\n  if (!isScrolling) {\r\n    const idleMs = now - estimator.lastTickAt;\r\n    if (idleMs >= SETTLE_RESET_MIN_IDLE_MS) {\r\n      estimator.estimate = 0;\r\n      if (estimator.retryTimer !== null) {\r\n        clearTimeout(estimator.retryTimer);\r\n        estimator.retryTimer = null;\r\n      }\r\n    } else if (estimator.retryTimer === null) {\r\n      estimator.retryTimer = setTimeout(() => {\r\n        estimator.retryTimer = null;\r\n        // Re-check, don't blindly zero: a real tick may have arrived during the wait and moved\r\n        // lastTickAt forward, in which case this is no longer a settle at all.\r\n        if (performance.now() - estimator.lastTickAt >= SETTLE_RESET_MIN_IDLE_MS && estimator.estimate !== 0) {\r\n          estimator.estimate = 0;\r\n          onIdleReset?.();\r\n        }\r\n      }, SETTLE_RESET_MIN_IDLE_MS - idleMs);\r\n    }\r\n    return estimator.estimate;\r\n  }\r\n  estimator.lastTickAt = now;\r\n  // Re-arm the settle retry from the tick itself, not only from a settle observation: the native\r\n  // scrollend (which is what schedules the retry via the !isScrolling branch) fires right after\r\n  // each tick and its commit can land before/without a retry still pending, and on a contended\r\n  // runner the last scrollend's retry can be dropped so the window parks at fling size. Arming\r\n  // from the tick guarantees a retry is always pending SETTLE_RESET_MIN_IDLE_MS past the LAST tick\r\n  // — the re-check inside still gates on a real 150ms idle, so a continued scroll (next tick < 150ms)\r\n  // clears it and no premature reset/blanking happens.\r\n  if (estimator.retryTimer !== null) {\r\n    clearTimeout(estimator.retryTimer);\r\n    estimator.retryTimer = null;\r\n  }\r\n  estimator.retryTimer = setTimeout(() => {\r\n    estimator.retryTimer = null;\r\n    if (performance.now() - estimator.lastTickAt >= SETTLE_RESET_MIN_IDLE_MS && estimator.estimate !== 0) {\r\n      estimator.estimate = 0;\r\n      onIdleReset?.();\r\n    }\r\n  }, SETTLE_RESET_MIN_IDLE_MS);\r\n  const magnitude = Math.abs(delta);\r\n  if (magnitude >= VELOCITY_TRIGGER_PX) {\r\n    const rampStep = VELOCITY_OVERSCAN_CAP_PX / VELOCITY_RAMP_TICKS;\r\n    // Cap applies to the OBSERVED magnitude too — an outsized single jump (e.g. a scrollbar\r\n    // teleport far bigger than any real drag tick) must still clamp, not blow straight past it.\r\n    estimator.estimate = Math.min(VELOCITY_OVERSCAN_CAP_PX, Math.max(magnitude, estimator.estimate + rampStep));\r\n  } else {\r\n    estimator.estimate *= VELOCITY_DECAY_FACTOR;\r\n  }\r\n  return estimator.estimate;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid/windowing/velocity-estimator.ts"
    },
    {
      "path": "registry/default/blocks/data-grid/LICENSE.md",
      "content": "\r\n                                 Apache License\r\n                           Version 2.0, January 2004\r\n                        http://www.apache.org/licenses/\r\n\r\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\r\n\r\n   1. Definitions.\r\n\r\n      \"License\" shall mean the terms and conditions for use, reproduction,\r\n      and distribution as defined by Sections 1 through 9 of this document.\r\n\r\n      \"Licensor\" shall mean the copyright owner or entity authorized by\r\n      the copyright owner that is granting the License.\r\n\r\n      \"Legal Entity\" shall mean the union of the acting entity and all\r\n      other entities that control, are controlled by, or are under common\r\n      control with that entity. For the purposes of this definition,\r\n      \"control\" means (i) the power, direct or indirect, to cause the\r\n      direction or management of such entity, whether by contract or\r\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\r\n      outstanding shares, or (iii) beneficial ownership of such entity.\r\n\r\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\r\n      exercising permissions granted by this License.\r\n\r\n      \"Source\" form shall mean the preferred form for making modifications,\r\n      including but not limited to software source code, documentation\r\n      source, and configuration files.\r\n\r\n      \"Object\" form shall mean any form resulting from mechanical\r\n      transformation or translation of a Source form, including but\r\n      not limited to compiled object code, generated documentation,\r\n      and conversions to other media types.\r\n\r\n      \"Work\" shall mean the work of authorship, whether in Source or\r\n      Object form, made available under the License, as indicated by a\r\n      copyright notice that is included in or attached to the work\r\n      (an example is provided in the Appendix below).\r\n\r\n      \"Derivative Works\" shall mean any work, whether in Source or Object\r\n      form, that is based on (or derived from) the Work and for which the\r\n      editorial revisions, annotations, elaborations, or other modifications\r\n      represent, as a whole, an original work of authorship. For the purposes\r\n      of this License, Derivative Works shall not include works that remain\r\n      separable from, or merely link (or bind by name) to the interfaces of,\r\n      the Work and Derivative Works thereof.\r\n\r\n      \"Contribution\" shall mean any work of authorship, including\r\n      the original version of the Work and any modifications or additions\r\n      to that Work or Derivative Works thereof, that is intentionally\r\n      submitted to Licensor for inclusion in the Work by the copyright owner\r\n      or by an individual or Legal Entity authorized to submit on behalf of\r\n      the copyright owner. For the purposes of this definition, \"submitted\"\r\n      means any form of electronic, verbal, or written communication sent\r\n      to the Licensor or its representatives, including but not limited to\r\n      communication on electronic mailing lists, source code control systems,\r\n      and issue tracking systems that are managed by, or on behalf of, the\r\n      Licensor for the purpose of discussing and improving the Work, but\r\n      excluding communication that is conspicuously marked or otherwise\r\n      designated in writing by the copyright owner as \"Not a Contribution.\"\r\n\r\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\r\n      on behalf of whom a Contribution has been received by Licensor and\r\n      subsequently incorporated within the Work.\r\n\r\n   2. Grant of Copyright License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      copyright license to reproduce, prepare Derivative Works of,\r\n      publicly display, publicly perform, sublicense, and distribute the\r\n      Work and such Derivative Works in Source or Object form.\r\n\r\n   3. Grant of Patent License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      (except as stated in this section) patent license to make, have made,\r\n      use, offer to sell, sell, import, and otherwise transfer the Work,\r\n      where such license applies only to those patent claims licensable\r\n      by such Contributor that are necessarily infringed by their\r\n      Contribution(s) alone or by combination of their Contribution(s)\r\n      with the Work to which such Contribution(s) was submitted. If You\r\n      institute patent litigation against any entity (including a\r\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\r\n      or a Contribution incorporated within the Work constitutes direct\r\n      or contributory patent infringement, then any patent licenses\r\n      granted to You under this License for that Work shall terminate\r\n      as of the date such litigation is filed.\r\n\r\n   4. Redistribution. You may reproduce and distribute copies of the\r\n      Work or Derivative Works thereof in any medium, with or without\r\n      modifications, and in Source or Object form, provided that You\r\n      meet the following conditions:\r\n\r\n      (a) You must give any other recipients of the Work or\r\n          Derivative Works a copy of this License; and\r\n\r\n      (b) You must cause any modified files to carry prominent notices\r\n          stating that You changed the files; and\r\n\r\n      (c) You must retain, in the Source form of any Derivative Works\r\n          that You distribute, all copyright, patent, trademark, and\r\n          attribution notices from the Source form of the Work,\r\n          excluding those notices that do not pertain to any part of\r\n          the Derivative Works; and\r\n\r\n      (d) If the Work includes a \"NOTICE\" text file as part of its\r\n          distribution, then any Derivative Works that You distribute must\r\n          include a readable copy of the attribution notices contained\r\n          within such NOTICE file, excluding those notices that do not\r\n          pertain to any part of the Derivative Works, in at least one\r\n          of the following places: within a NOTICE text file distributed\r\n          as part of the Derivative Works; within the Source form or\r\n          documentation, if provided along with the Derivative Works; or,\r\n          within a display generated by the Derivative Works, if and\r\n          wherever such third-party notices normally appear. The contents\r\n          of the NOTICE file are for informational purposes only and\r\n          do not modify the License. You may add Your own attribution\r\n          notices within Derivative Works that You distribute, alongside\r\n          or as an addendum to the NOTICE text from the Work, provided\r\n          that such additional attribution notices cannot be construed\r\n          as modifying the License.\r\n\r\n      You may add Your own copyright statement to Your modifications and\r\n      may provide additional or different license terms and conditions\r\n      for use, reproduction, or distribution of Your modifications, or\r\n      for any such Derivative Works as a whole, provided Your use,\r\n      reproduction, and distribution of the Work otherwise complies with\r\n      the conditions stated in this License.\r\n\r\n   5. Submission of Contributions. Unless You explicitly state otherwise,\r\n      any Contribution intentionally submitted for inclusion in the Work\r\n      by You to the Licensor shall be under the terms and conditions of\r\n      this License, without any additional terms or conditions.\r\n      Notwithstanding the above, nothing herein shall supersede or modify\r\n      the terms of any separate license agreement you may have executed\r\n      with Licensor regarding such Contributions.\r\n\r\n   6. Trademarks. This License does not grant permission to use the trade\r\n      names, trademarks, service marks, or product names of the Licensor,\r\n      except as required for reasonable and customary use in describing the\r\n      origin of the Work and reproducing the content of the NOTICE file.\r\n\r\n   7. Disclaimer of Warranty. Unless required by applicable law or\r\n      agreed to in writing, Licensor provides the Work (and each\r\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\r\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\r\n      implied, including, without limitation, any warranties or conditions\r\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\r\n      PARTICULAR PURPOSE. You are solely responsible for determining the\r\n      appropriateness of using or redistributing the Work and assume any\r\n      risks associated with Your exercise of permissions under this License.\r\n\r\n   8. Limitation of Liability. In no event and under no legal theory,\r\n      whether in tort (including negligence), contract, or otherwise,\r\n      unless required by applicable law (such as deliberate and grossly\r\n      negligent acts) or agreed to in writing, shall any Contributor be\r\n      liable to You for damages, including any direct, indirect, special,\r\n      incidental, or consequential damages of any character arising as a\r\n      result of this License or out of the use or inability to use the\r\n      Work (including but not limited to damages for loss of goodwill,\r\n      work stoppage, computer failure or malfunction, or any and all\r\n      other commercial damages or losses), even if such Contributor\r\n      has been advised of the possibility of such damages.\r\n\r\n   9. Accepting Warranty or Additional Liability. While redistributing\r\n      the Work or Derivative Works thereof, You may choose to offer,\r\n      and charge a fee for, acceptance of support, warranty, indemnity,\r\n      or other liability obligations and/or rights consistent with this\r\n      License. However, in accepting such obligations, You may act only\r\n      on Your own behalf and on Your sole responsibility, not on behalf\r\n      of any other Contributor, and only if You agree to indemnify,\r\n      defend, and hold each Contributor harmless for any liability\r\n      incurred by, or claims asserted against, such Contributor by reason\r\n      of your accepting any such warranty or additional liability.\r\n\r\n   END OF TERMS AND CONDITIONS\r\n\r\n   APPENDIX: How to apply the Apache License to your work.\r\n\r\n      To apply the Apache License to your work, attach the following\r\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\r\n      replaced with your own identifying information. (Don't include\r\n      the brackets!)  The text should be enclosed in the appropriate\r\n      comment syntax for the file format. We also recommend that a\r\n      file or class name and description of purpose be included on the\r\n      same \"printed page\" as the copyright notice for easier\r\n      identification within third-party archives.\r\n\r\n   Copyright 2026 DammersCode\r\n\r\n   Licensed under the Apache License, Version 2.0 (the \"License\");\r\n   you may not use this file except in compliance with the License.\r\n   You may obtain a copy of the License at\r\n\r\n       http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n   Unless required by applicable law or agreed to in writing, software\r\n   distributed under the License is distributed on an \"AS IS\" BASIS,\r\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n   See the License for the specific language governing permissions and\r\n   limitations under the License.\r\n",
      "type": "registry:file",
      "target": "components/data-grid/LICENSE.md"
    }
  ],
  "cssVars": {
    "light": {
      "grid-pin-shadow": "oklch(0 0 0 / 10%)"
    },
    "dark": {
      "grid-pin-shadow": "oklch(0 0 0 / 40%)"
    }
  },
  "docs": "Docs & quick start: https://gridcn.vercel.app/docs/quick-start",
  "categories": [
    "data"
  ],
  "type": "registry:block"
}