{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "data-grid", "title": "Data Grid", "description": "Virtualized spreadsheet-style data grid with inline editors, range selection, copy and paste, search, sorting, resizing, and pinned columns.", "dependencies": [ "@base-ui/react", "@tanstack/react-table", "@tanstack/react-virtual", "cmdk", "react-day-picker", "sonner" ], "registryDependencies": [ "https://zeron-ui.vercel.app/r/surfaces.json", "utils", "https://zeron-ui.vercel.app/r/shape-context.json", "https://zeron-ui.vercel.app/r/surface-context.json", "https://zeron-ui.vercel.app/r/surface-classes.json", "https://zeron-ui.vercel.app/r/portal-container-context.json", "https://zeron-ui.vercel.app/r/badge.json", "https://zeron-ui.vercel.app/r/button.json", "https://zeron-ui.vercel.app/r/checkbox.json", "https://zeron-ui.vercel.app/r/dialog.json", "https://zeron-ui.vercel.app/r/dropdown.json", "https://zeron-ui.vercel.app/r/input.json", "https://zeron-ui.vercel.app/r/kbd.json", "https://zeron-ui.vercel.app/r/select.json" ], "files": [ { "path": "src/components/ui/data-grid/index.ts", "content": "export * from \"@/hooks/use-data-grid\";\nexport * from \"@/lib/data-grid\";\nexport * from \"@/lib/data-grid-types\";\nexport * from \"./data-grid\";\nexport * from \"./data-grid-cell\";\nexport * from \"./data-grid-cell-variants\";\nexport * from \"./data-grid-cell-wrapper\";\nexport * from \"./data-grid-column-header\";\nexport * from \"./data-grid-context-menu\";\nexport * from \"./data-grid-keyboard-shortcuts\";\nexport * from \"./data-grid-paste-dialog\";\nexport * from \"./data-grid-row\";\nexport * from \"./data-grid-search\";\n", "type": "registry:ui", "target": "components/ui/data-grid/index.ts" }, { "path": "src/components/ui/data-grid/data-grid.tsx", "content": "/* biome-ignore-all lint/a11y/useSemanticElements: Virtualized ARIA grid uses div roles to support spreadsheet navigation, pinned columns, and measured rows. */\n\n\"use client\";\n\nimport * as React from \"react\";\nimport { useAsRef } from \"@/hooks/use-as-ref\";\nimport type { useDataGrid } from \"@/hooks/use-data-grid\";\nimport {\n flexRender,\n getColumnBorderVisibility,\n getColumnPinningStyle,\n} from \"@/lib/data-grid\";\nimport type { Direction } from \"@/lib/data-grid-types\";\nimport { useShape } from \"@/lib/shape-context\";\nimport { cn } from \"@/lib/utils\";\nimport { createIconSlot } from \"@/lib/icon-context\";\nimport { DataGridColumnHeader } from \"./data-grid-column-header\";\nimport { DataGridContextMenu } from \"./data-grid-context-menu\";\nimport { DataGridPasteDialog } from \"./data-grid-paste-dialog\";\nimport { DataGridRow } from \"./data-grid-row\";\nimport { DataGridSearch } from \"./data-grid-search\";\n\nconst EMPTY_CELL_SELECTION_SET = new Set();\nconst Plus = createIconSlot(\"plus\");\n\ninterface DataGridProps\n extends Omit>, \"dir\">,\n Omit, \"contextMenu\"> {\n dir?: Direction;\n height?: number;\n stretchColumns?: boolean;\n}\n\nexport function DataGrid({\n dataGridRef,\n headerRef,\n rowMapRef,\n footerRef,\n dir = \"ltr\",\n table,\n tableMeta,\n virtualTotalSize,\n virtualItems,\n measureElement,\n columns,\n columnSizeVars,\n searchState,\n searchMatchesByRow,\n activeSearchMatch,\n cellSelectionMap,\n focusedCell,\n editingCell,\n rowHeight,\n contextMenu,\n pasteDialog,\n onRowAdd: onRowAddProp,\n height = 600,\n stretchColumns = false,\n adjustLayout = false,\n className,\n ...props\n}: DataGridProps) {\n const shape = useShape();\n const rows = table.getRowModel().rows;\n const readOnly = tableMeta?.readOnly ?? false;\n const columnVisibility = table.getState().columnVisibility;\n const columnPinning = table.getState().columnPinning;\n\n const onRowAddRef = useAsRef(onRowAddProp);\n\n const onRowAdd = React.useCallback(\n (event: React.MouseEvent) => {\n onRowAddRef.current?.(event);\n },\n [onRowAddRef],\n );\n\n const onDataGridContextMenu = React.useCallback(\n (event: React.MouseEvent) => {\n event.preventDefault();\n },\n [],\n );\n\n const onFooterCellKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (!onRowAddRef.current) return;\n\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onRowAddRef.current();\n }\n },\n [onRowAddRef],\n );\n\n return (\n \n {searchState && }\n \n \n \n \n {table.getHeaderGroups().map((headerGroup, rowIndex) => (\n \n {headerGroup.headers.map((header, colIndex) => {\n const sorting = table.getState().sorting;\n const currentSort = sorting.find(\n (sort) => sort.id === header.column.id,\n );\n const isSortable = header.column.getCanSort();\n\n const nextHeader = headerGroup.headers[colIndex + 1];\n const isLastColumn =\n colIndex === headerGroup.headers.length - 1;\n\n const { showEndBorder, showStartBorder } =\n getColumnBorderVisibility({\n column: header.column,\n nextColumn: nextHeader?.column,\n isLastColumn,\n });\n\n return (\n \n {header.isPlaceholder ? null : typeof header.column\n .columnDef.header === \"function\" ? (\n
\n {flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n
\n ) : (\n \n )}\n \n );\n })}\n \n ))}\n \n \n {virtualItems.map((virtualItem) => {\n const row = rows[virtualItem.index];\n if (!row) return null;\n\n const cellSelectionKeys =\n cellSelectionMap?.get(virtualItem.index) ??\n EMPTY_CELL_SELECTION_SET;\n\n const searchMatchColumns =\n searchMatchesByRow?.get(virtualItem.index) ?? null;\n const isActiveSearchRow =\n activeSearchMatch?.rowIndex === virtualItem.index;\n\n return (\n \n );\n })}\n \n {!readOnly && onRowAdd && (\n \n \n \n
\n \n Add row\n
\n \n \n \n )}\n \n \n );\n}\n", "type": "registry:ui", "target": "components/ui/data-grid/data-grid.tsx" }, { "path": "src/components/ui/data-grid/data-grid-row.tsx", "content": "/* biome-ignore-all lint/a11y/useSemanticElements: Virtualized ARIA grid rows and cells must remain divs for measured layout and pinned column positioning. */\n\n\"use client\";\n\nimport type {\n ColumnPinningState,\n Row,\n TableMeta,\n VisibilityState,\n} from \"@tanstack/react-table\";\nimport type { VirtualItem } from \"@tanstack/react-virtual\";\nimport * as React from \"react\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport {\n flexRender,\n getCellKey,\n getColumnBorderVisibility,\n getColumnPinningStyle,\n getRowHeightValue,\n} from \"@/lib/data-grid\";\nimport type {\n CellPosition,\n Direction,\n RowHeightValue,\n} from \"@/lib/data-grid-types\";\nimport { cn } from \"@/lib/utils\";\nimport { DataGridCell } from \"./data-grid-cell\";\n\ninterface DataGridRowProps extends React.ComponentProps<\"div\"> {\n row: Row;\n tableMeta: TableMeta;\n virtualItem: VirtualItem;\n measureElement: (node: Element | null) => void;\n rowMapRef: React.RefObject>;\n rowHeight: RowHeightValue;\n columnVisibility: VisibilityState;\n columnPinning: ColumnPinningState;\n focusedCell: CellPosition | null;\n editingCell: CellPosition | null;\n cellSelectionKeys: Set;\n searchMatchColumns: Set | null;\n activeSearchMatch: CellPosition | null;\n dir: Direction;\n readOnly: boolean;\n stretchColumns: boolean;\n adjustLayout: boolean;\n}\n\nexport const DataGridRow = React.memo(DataGridRowImpl, (prev, next) => {\n const prevRowIndex = prev.virtualItem.index;\n const nextRowIndex = next.virtualItem.index;\n\n // Re-render if row identity changed\n if (prev.row.id !== next.row.id) {\n return false;\n }\n\n // Re-render if row data (original) reference changed\n if (prev.row.original !== next.row.original) {\n return false;\n }\n\n // Re-render if virtual position changed (handles transform updates)\n if (prev.virtualItem.start !== next.virtualItem.start) {\n return false;\n }\n\n // Re-render if focus state changed for this row\n const prevHasFocus = prev.focusedCell?.rowIndex === prevRowIndex;\n const nextHasFocus = next.focusedCell?.rowIndex === nextRowIndex;\n\n if (prevHasFocus !== nextHasFocus) {\n return false;\n }\n\n // Re-render if focused column changed within this row\n if (nextHasFocus && prevHasFocus) {\n if (prev.focusedCell?.columnId !== next.focusedCell?.columnId) {\n return false;\n }\n }\n\n // Re-render if editing state changed for this row\n const prevHasEditing = prev.editingCell?.rowIndex === prevRowIndex;\n const nextHasEditing = next.editingCell?.rowIndex === nextRowIndex;\n\n if (prevHasEditing !== nextHasEditing) {\n return false;\n }\n\n // Re-render if editing column changed within this row\n if (nextHasEditing && prevHasEditing) {\n if (prev.editingCell?.columnId !== next.editingCell?.columnId) {\n return false;\n }\n }\n\n // Re-render if this row's selected cells changed\n // Using stable Set reference that only includes this row's cells\n if (prev.cellSelectionKeys !== next.cellSelectionKeys) {\n return false;\n }\n\n // Re-render if column visibility changed\n if (prev.columnVisibility !== next.columnVisibility) {\n return false;\n }\n\n // Re-render if row height changed\n if (prev.rowHeight !== next.rowHeight) {\n return false;\n }\n\n // Re-render if column pinning state changed\n if (prev.columnPinning !== next.columnPinning) {\n return false;\n }\n\n // Re-render if readOnly changed\n if (prev.readOnly !== next.readOnly) {\n return false;\n }\n\n // Re-render if search match columns changed for this row\n if (prev.searchMatchColumns !== next.searchMatchColumns) {\n return false;\n }\n\n // Re-render if active search match changed for this row\n if (prev.activeSearchMatch?.columnId !== next.activeSearchMatch?.columnId) {\n return false;\n }\n\n // Re-render if direction changed\n if (prev.dir !== next.dir) {\n return false;\n }\n\n // Re-render if adjustLayout state changed\n if (prev.adjustLayout !== next.adjustLayout) {\n return false;\n }\n\n // Re-render if stretchColumns changed\n if (prev.stretchColumns !== next.stretchColumns) {\n return false;\n }\n\n // Skip re-render - props are equal\n return true;\n}) as typeof DataGridRowImpl;\n\nfunction DataGridRowImpl({\n row,\n tableMeta,\n virtualItem,\n measureElement,\n rowMapRef,\n rowHeight,\n columnVisibility,\n columnPinning,\n focusedCell,\n editingCell,\n cellSelectionKeys,\n searchMatchColumns,\n activeSearchMatch,\n dir,\n readOnly,\n stretchColumns,\n adjustLayout,\n className,\n style,\n ref,\n ...props\n}: DataGridRowProps) {\n const virtualRowIndex = virtualItem.index;\n\n const onRowChange = React.useCallback(\n (node: HTMLDivElement | null) => {\n if (typeof virtualRowIndex === \"undefined\") return;\n\n if (node) {\n measureElement(node);\n rowMapRef.current?.set(virtualRowIndex, node);\n } else {\n rowMapRef.current?.delete(virtualRowIndex);\n }\n },\n [virtualRowIndex, measureElement, rowMapRef],\n );\n\n const rowRef = useComposedRefs(ref, onRowChange);\n\n const isRowSelected = row.getIsSelected();\n\n // Memoize visible cells to avoid recreating cell array on every render\n // Though TanStack returns new Cell wrappers, memoizing the array helps React's reconciliation\n // biome-ignore lint/correctness/useExhaustiveDependencies: columnVisibility and columnPinning are used for calculating the visible cells\n const visibleCells = React.useMemo(\n () => row.getVisibleCells(),\n [row, columnVisibility, columnPinning],\n );\n\n return (\n \n {visibleCells.map((cell, colIndex) => {\n const columnId = cell.column.id;\n\n const isCellFocused =\n focusedCell?.rowIndex === virtualRowIndex &&\n focusedCell?.columnId === columnId;\n const isCellEditing =\n editingCell?.rowIndex === virtualRowIndex &&\n editingCell?.columnId === columnId;\n const isCellSelected =\n cellSelectionKeys?.has(getCellKey(virtualRowIndex, columnId)) ??\n false;\n\n const isSearchMatch = searchMatchColumns?.has(columnId) ?? false;\n const isActiveSearchMatch = activeSearchMatch?.columnId === columnId;\n\n const nextCell = visibleCells[colIndex + 1];\n const isLastColumn = colIndex === visibleCells.length - 1;\n const { showEndBorder, showStartBorder } = getColumnBorderVisibility({\n column: cell.column,\n nextColumn: nextCell?.column,\n isLastColumn,\n });\n\n return (\n \n {typeof cell.column.columnDef.header === \"function\" ? (\n \n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n \n ) : (\n \n )}\n \n );\n })}\n \n );\n}\n", "type": "registry:ui", "target": "components/ui/data-grid/data-grid-row.tsx" }, { "path": "src/components/ui/data-grid/data-grid-cell.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport type { DataGridCellProps } from \"@/lib/data-grid-types\";\nimport {\n CheckboxCell,\n DateCell,\n FileCell,\n LongTextCell,\n MultiSelectCell,\n NumberCell,\n SelectCell,\n ShortTextCell,\n UrlCell,\n} from \"./data-grid-cell-variants\";\n\nexport const DataGridCell = React.memo(DataGridCellImpl, (prev, next) => {\n // Fast path: check stable primitive props first\n if (prev.isFocused !== next.isFocused) return false;\n if (prev.isEditing !== next.isEditing) return false;\n if (prev.isSelected !== next.isSelected) return false;\n if (prev.isSearchMatch !== next.isSearchMatch) return false;\n if (prev.isActiveSearchMatch !== next.isActiveSearchMatch) return false;\n if (prev.readOnly !== next.readOnly) return false;\n if (prev.rowIndex !== next.rowIndex) return false;\n if (prev.columnId !== next.columnId) return false;\n if (prev.rowHeight !== next.rowHeight) return false;\n\n // Check cell value using row.original instead of getValue() for stability\n // getValue() is unstable and recreates on every render, breaking memoization\n const prevValue = (prev.cell.row.original as Record)[\n prev.columnId\n ];\n const nextValue = (next.cell.row.original as Record)[\n next.columnId\n ];\n if (prevValue !== nextValue) {\n return false;\n }\n\n // Check cell/row identity\n if (prev.cell.row.id !== next.cell.row.id) return false;\n\n return true;\n}) as typeof DataGridCellImpl;\n\nfunction DataGridCellImpl({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n rowHeight,\n}: DataGridCellProps) {\n const cellOpts = cell.column.columnDef.meta?.cell;\n const variant = cellOpts?.variant ?? \"text\";\n\n let Comp: React.ComponentType>;\n\n switch (variant) {\n case \"short-text\":\n Comp = ShortTextCell;\n break;\n case \"long-text\":\n Comp = LongTextCell;\n break;\n case \"number\":\n Comp = NumberCell;\n break;\n case \"url\":\n Comp = UrlCell;\n break;\n case \"checkbox\":\n Comp = CheckboxCell;\n break;\n case \"select\":\n Comp = SelectCell;\n break;\n case \"multi-select\":\n Comp = MultiSelectCell;\n break;\n case \"date\":\n Comp = DateCell;\n break;\n case \"file\":\n Comp = FileCell;\n break;\n\n default:\n Comp = ShortTextCell;\n break;\n }\n\n return (\n \n );\n}\n", "type": "registry:ui", "target": "components/ui/data-grid/data-grid-cell.tsx" }, { "path": "src/components/ui/data-grid/data-grid-cell-wrapper.tsx", "content": "/* biome-ignore-all lint/a11y/useSemanticElements: Grid cells use div button semantics so selection, editing, and resize interactions share one focus model. */\n\n\"use client\";\n\nimport * as React from \"react\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { getCellKey } from \"@/lib/data-grid\";\nimport type { DataGridCellProps } from \"@/lib/data-grid-types\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataGridCellWrapperProps\n extends DataGridCellProps,\n React.ComponentProps<\"div\"> {}\n\nexport function DataGridCellWrapper({\n tableMeta,\n rowIndex,\n columnId,\n isEditing,\n isFocused,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n rowHeight,\n className,\n onClick: onClickProp,\n onKeyDown: onKeyDownProp,\n ref,\n ...props\n}: DataGridCellWrapperProps) {\n const cellMapRef = tableMeta?.cellMapRef;\n\n const onCellChange = React.useCallback(\n (node: HTMLDivElement | null) => {\n if (!cellMapRef) return;\n\n const cellKey = getCellKey(rowIndex, columnId);\n\n if (node) {\n cellMapRef.current.set(cellKey, node);\n } else {\n cellMapRef.current.delete(cellKey);\n }\n },\n [rowIndex, columnId, cellMapRef],\n );\n\n const composedRef = useComposedRefs(ref, onCellChange);\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (!isEditing) {\n event.preventDefault();\n onClickProp?.(event);\n if (isFocused && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n tableMeta?.onCellClick?.(rowIndex, columnId, event);\n }\n }\n },\n [\n tableMeta,\n rowIndex,\n columnId,\n isEditing,\n isFocused,\n readOnly,\n onClickProp,\n ],\n );\n\n const onContextMenu = React.useCallback(\n (event: React.MouseEvent) => {\n if (!isEditing) {\n tableMeta?.onCellContextMenu?.(rowIndex, columnId, event);\n }\n },\n [tableMeta, rowIndex, columnId, isEditing],\n );\n\n const onDoubleClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (!isEditing) {\n event.preventDefault();\n tableMeta?.onCellDoubleClick?.(rowIndex, columnId);\n }\n },\n [tableMeta, rowIndex, columnId, isEditing],\n );\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n onKeyDownProp?.(event);\n\n if (event.defaultPrevented) return;\n\n if (\n event.key === \"ArrowUp\" ||\n event.key === \"ArrowDown\" ||\n event.key === \"ArrowLeft\" ||\n event.key === \"ArrowRight\" ||\n event.key === \"Home\" ||\n event.key === \"End\" ||\n event.key === \"PageUp\" ||\n event.key === \"PageDown\" ||\n event.key === \"Tab\"\n ) {\n return;\n }\n\n if (isFocused && !isEditing && !readOnly) {\n if (event.key === \"F2\" || event.key === \"Enter\") {\n event.preventDefault();\n event.stopPropagation();\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n return;\n }\n\n if (event.key === \" \") {\n event.preventDefault();\n event.stopPropagation();\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n return;\n }\n\n if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {\n event.preventDefault();\n event.stopPropagation();\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n }\n }\n },\n [\n onKeyDownProp,\n isFocused,\n isEditing,\n readOnly,\n tableMeta,\n rowIndex,\n columnId,\n ],\n );\n\n const onMouseDown = React.useCallback(\n (event: React.MouseEvent) => {\n if (!isEditing) {\n tableMeta?.onCellMouseDown?.(rowIndex, columnId, event);\n }\n },\n [tableMeta, rowIndex, columnId, isEditing],\n );\n\n const onMouseEnter = React.useCallback(() => {\n if (!isEditing) {\n tableMeta?.onCellMouseEnter?.(rowIndex, columnId);\n }\n }, [tableMeta, rowIndex, columnId, isEditing]);\n\n const onMouseUp = React.useCallback(() => {\n if (!isEditing) {\n tableMeta?.onCellMouseUp?.();\n }\n }, [tableMeta, isEditing]);\n\n return (\n \n );\n}\n", "type": "registry:ui", "target": "components/ui/data-grid/data-grid-cell-wrapper.tsx" }, { "path": "src/components/ui/data-grid/data-grid-cell-variants.tsx", "content": "/* biome-ignore-all lint/a11y/useSemanticElements: Editable grid cells use ARIA roles on divs to preserve spreadsheet-style focus and virtualization behavior. */\n\n\"use client\";\n\nimport * as React from \"react\";\nimport { toast } from \"sonner\";\nimport { useBadgeOverflow } from \"@/hooks/use-badge-overflow\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport {\n formatDateForDisplay,\n formatDateToString,\n formatFileSize,\n getCellKey,\n getFileIcon,\n getLineCount,\n getUrlHref,\n parseLocalDate,\n} from \"@/lib/data-grid\";\nimport type {\n DataGridCellProps,\n FileCellData,\n} from \"@/lib/data-grid-types\";\nimport { cn } from \"@/lib/utils\";\nimport { createIconSlot } from \"@/lib/icon-context\";\nimport { Badge } from \"../badge\";\nimport { Button } from \"../button\";\nimport { Calendar } from \"./data-grid-calendar\";\nimport { Checkbox } from \"../checkbox\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from \"./data-grid-command\";\nimport { Popover, PopoverAnchor, PopoverContent } from \"./data-grid-popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n} from \"../select\";\nimport { Skeleton, Textarea } from \"./data-grid-primitives\";\nimport { DataGridCellWrapper } from \"./data-grid-cell-wrapper\";\n\nconst Check = createIconSlot(\"check\");\nconst Upload = createIconSlot(\"upload\");\nconst X = createIconSlot(\"x\");\n\nexport function ShortTextCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isEditing,\n isFocused,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue);\n const cellRef = React.useRef(null);\n const containerRef = React.useRef(null);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue);\n if (cellRef.current && !isEditing) {\n cellRef.current.textContent = initialValue;\n }\n }\n\n const onBlur = React.useCallback(() => {\n // Read the current value directly from the DOM to avoid stale state\n const currentValue = cellRef.current?.textContent ?? \"\";\n if (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: currentValue });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, rowIndex, columnId, initialValue, readOnly]);\n\n const onInput = React.useCallback(\n (event: React.FormEvent) => {\n const currentValue = event.currentTarget.textContent ?? \"\";\n setValue(currentValue);\n },\n [],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Enter\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent ?? \"\";\n if (currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue,\n });\n }\n tableMeta?.onCellEditingStop?.({ moveToNextRow: true });\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent ?? \"\";\n if (currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue,\n });\n }\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n } else if (event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue);\n cellRef.current?.blur();\n }\n } else if (\n isFocused &&\n event.key.length === 1 &&\n !event.ctrlKey &&\n !event.metaKey\n ) {\n // Handle typing to pre-fill the value when editing starts\n setValue(event.key);\n\n queueMicrotask(() => {\n if (cellRef.current && cellRef.current.contentEditable === \"true\") {\n cellRef.current.textContent = event.key;\n const range = document.createRange();\n const selection = window.getSelection();\n range.selectNodeContents(cellRef.current);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n });\n }\n },\n [isEditing, isFocused, initialValue, tableMeta, rowIndex, columnId],\n );\n\n React.useEffect(() => {\n if (isEditing && cellRef.current) {\n cellRef.current.focus();\n\n if (!cellRef.current.textContent && value) {\n cellRef.current.textContent = value;\n }\n\n if (cellRef.current.textContent) {\n const range = document.createRange();\n const selection = window.getSelection();\n range.selectNodeContents(cellRef.current);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n }\n }, [isEditing, value]);\n\n const displayValue = !isEditing ? (value ?? \"\") : \"\";\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n \n {displayValue}\n \n \n );\n}\n\nexport function LongTextCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue ?? \"\");\n const textareaRef = React.useRef(null);\n const containerRef = React.useRef(null);\n const pendingCharRef = React.useRef(null);\n const sideOffset = -(containerRef.current?.clientHeight ?? 0);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue ?? \"\");\n }\n\n const debouncedSave = useDebouncedCallback((newValue: string) => {\n if (!readOnly) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValue });\n }\n }, 300);\n\n const onSave = React.useCallback(() => {\n // Immediately save any pending changes and close the popover\n if (!readOnly && value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, value, initialValue, rowIndex, columnId, readOnly]);\n\n const onCancel = React.useCallback(() => {\n // Restore the original value\n setValue(initialValue ?? \"\");\n if (!readOnly) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: initialValue });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, initialValue, rowIndex, columnId, readOnly]);\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n // Immediately save any pending changes when closing\n if (!readOnly && value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, value, initialValue, rowIndex, columnId, readOnly],\n );\n\n const onOpenAutoFocus: NonNullable<\n React.ComponentProps[\"onOpenAutoFocus\"]\n > = React.useCallback((event) => {\n event.preventDefault();\n if (textareaRef.current) {\n textareaRef.current.focus();\n const length = textareaRef.current.value.length;\n textareaRef.current.setSelectionRange(length, length);\n\n // Insert pending character using execCommand so it's part of undo history\n // Use requestAnimationFrame to ensure focus has fully settled\n if (pendingCharRef.current) {\n const char = pendingCharRef.current;\n pendingCharRef.current = null;\n requestAnimationFrame(() => {\n if (\n textareaRef.current &&\n document.activeElement === textareaRef.current\n ) {\n document.execCommand(\"insertText\", false, char);\n textareaRef.current.scrollTop = textareaRef.current.scrollHeight;\n }\n });\n } else {\n textareaRef.current.scrollTop = textareaRef.current.scrollHeight;\n }\n }\n }, []);\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n isFocused &&\n !isEditing &&\n !readOnly &&\n event.key.length === 1 &&\n !event.ctrlKey &&\n !event.metaKey\n ) {\n // Store the character to be inserted after textarea focuses\n // This ensures it's part of the textarea's undo history\n pendingCharRef.current = event.key;\n }\n },\n [isFocused, isEditing, readOnly],\n );\n\n const onBlur = React.useCallback(() => {\n // Immediately save any pending changes on blur\n if (!readOnly && value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, value, initialValue, rowIndex, columnId, readOnly]);\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const newValue = event.target.value;\n setValue(newValue);\n debouncedSave(newValue);\n },\n [debouncedSave],\n );\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n onCancel();\n } else if (event.key === \"Enter\" && (event.ctrlKey || event.metaKey)) {\n event.preventDefault();\n onSave();\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n // Save any pending changes\n if (value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n return;\n }\n // Stop propagation to prevent grid navigation\n event.stopPropagation();\n },\n [onSave, onCancel, value, initialValue, tableMeta, rowIndex, columnId],\n );\n\n return (\n \n \n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {value}\n \n \n \n \n \n \n );\n}\n\nexport function NumberCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as number;\n const [value, setValue] = React.useState(String(initialValue ?? \"\"));\n const inputRef = React.useRef(null);\n const containerRef = React.useRef(null);\n\n const cellOpts = cell.column.columnDef.meta?.cell;\n const numberCellOpts = cellOpts?.variant === \"number\" ? cellOpts : null;\n const min = numberCellOpts?.min;\n const max = numberCellOpts?.max;\n const step = numberCellOpts?.step;\n\n const prevIsEditingRef = React.useRef(isEditing);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(String(initialValue ?? \"\"));\n }\n\n const onBlur = React.useCallback(() => {\n const numValue = value === \"\" ? null : Number(value);\n if (!readOnly && numValue !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, rowIndex, columnId, initialValue, value, readOnly]);\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent) => {\n setValue(event.target.value);\n },\n [],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Enter\") {\n event.preventDefault();\n const numValue = value === \"\" ? null : Number(value);\n if (numValue !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\n }\n tableMeta?.onCellEditingStop?.({ moveToNextRow: true });\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n const numValue = value === \"\" ? null : Number(value);\n if (numValue !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\n }\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n } else if (event.key === \"Escape\") {\n event.preventDefault();\n setValue(String(initialValue ?? \"\"));\n inputRef.current?.blur();\n }\n } else if (isFocused) {\n // Handle Backspace to start editing with empty value\n if (event.key === \"Backspace\") {\n setValue(\"\");\n } else if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {\n // Handle typing to pre-fill the value when editing starts\n setValue(event.key);\n }\n }\n },\n [isEditing, isFocused, initialValue, tableMeta, rowIndex, columnId, value],\n );\n\n React.useEffect(() => {\n const wasEditing = prevIsEditingRef.current;\n prevIsEditingRef.current = isEditing;\n\n // Only focus when we start editing (transition from false to true)\n if (isEditing && !wasEditing && inputRef.current) {\n inputRef.current.focus();\n }\n }, [isEditing]);\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n ) : (\n {value}\n )}\n \n );\n}\n\nexport function UrlCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isEditing,\n isFocused,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue ?? \"\");\n const cellRef = React.useRef(null);\n const containerRef = React.useRef(null);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue ?? \"\");\n if (cellRef.current && !isEditing) {\n cellRef.current.textContent = initialValue ?? \"\";\n }\n }\n\n const onBlur = React.useCallback(() => {\n const currentValue = cellRef.current?.textContent?.trim() ?? \"\";\n\n if (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue || null,\n });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, rowIndex, columnId, initialValue, readOnly]);\n\n const onInput = React.useCallback(\n (event: React.FormEvent) => {\n const currentValue = event.currentTarget.textContent ?? \"\";\n setValue(currentValue);\n },\n [],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Enter\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent?.trim() ?? \"\";\n if (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue || null,\n });\n }\n tableMeta?.onCellEditingStop?.({ moveToNextRow: true });\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent?.trim() ?? \"\";\n if (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue || null,\n });\n }\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n } else if (event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue ?? \"\");\n cellRef.current?.blur();\n }\n } else if (\n isFocused &&\n !readOnly &&\n event.key.length === 1 &&\n !event.ctrlKey &&\n !event.metaKey\n ) {\n // Handle typing to pre-fill the value when editing starts\n setValue(event.key);\n\n queueMicrotask(() => {\n if (cellRef.current && cellRef.current.contentEditable === \"true\") {\n cellRef.current.textContent = event.key;\n const range = document.createRange();\n const selection = window.getSelection();\n range.selectNodeContents(cellRef.current);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n });\n }\n },\n [\n isEditing,\n isFocused,\n initialValue,\n tableMeta,\n rowIndex,\n columnId,\n readOnly,\n ],\n );\n\n const onLinkClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (isEditing) {\n event.preventDefault();\n return;\n }\n\n // Check if URL was rejected due to dangerous protocol\n const href = getUrlHref(value);\n if (!href) {\n event.preventDefault();\n toast.error(\"Invalid URL\", {\n description:\n \"URL contains a dangerous protocol (javascript:, data:, vbscript:, or file:)\",\n });\n return;\n }\n\n // Stop propagation to prevent grid from interfering with link navigation\n event.stopPropagation();\n },\n [isEditing, value],\n );\n\n React.useEffect(() => {\n if (isEditing && cellRef.current) {\n cellRef.current.focus();\n\n if (!cellRef.current.textContent && value) {\n cellRef.current.textContent = value;\n }\n\n if (cellRef.current.textContent) {\n const range = document.createRange();\n const selection = window.getSelection();\n range.selectNodeContents(cellRef.current);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n }\n }, [isEditing, value]);\n\n const displayValue = !isEditing ? (value ?? \"\") : \"\";\n const urlHref = displayValue ? getUrlHref(displayValue) : \"\";\n const isDangerousUrl = displayValue && !urlHref;\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {!isEditing && displayValue ? (\n \n \n {displayValue}\n \n \n ) : (\n \n {displayValue}\n \n )}\n \n );\n}\n\nexport function CheckboxCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: Omit, \"isEditing\">) {\n const initialValue = cell.getValue() as boolean;\n const [value, setValue] = React.useState(Boolean(initialValue));\n const containerRef = React.useRef(null);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(Boolean(initialValue));\n }\n\n const onCheckedChange = React.useCallback(\n (checked: boolean) => {\n if (readOnly) return;\n setValue(checked);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: checked });\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n isFocused &&\n !readOnly &&\n (event.key === \" \" || event.key === \"Enter\")\n ) {\n event.preventDefault();\n event.stopPropagation();\n onCheckedChange(!value);\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isFocused, value, onCheckedChange, tableMeta, readOnly],\n );\n\n const onWrapperClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (isFocused && !readOnly) {\n event.preventDefault();\n event.stopPropagation();\n onCheckedChange(!value);\n }\n },\n [isFocused, value, onCheckedChange, readOnly],\n );\n\n const onCheckboxClick = React.useCallback((event: React.MouseEvent) => {\n event.stopPropagation();\n }, []);\n\n const onCheckboxMouseDown = React.useCallback((event: React.MouseEvent) => {\n event.stopPropagation();\n }, []);\n\n const onCheckboxDoubleClick = React.useCallback((event: React.MouseEvent) => {\n event.stopPropagation();\n }, []);\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={false}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n className=\"flex size-full justify-center\"\n onClick={onWrapperClick}\n onKeyDown={onWrapperKeyDown}\n >\n \n \n );\n}\n\nexport function SelectCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue);\n const containerRef = React.useRef(null);\n const cellOpts = cell.column.columnDef.meta?.cell;\n const options = cellOpts?.variant === \"select\" ? cellOpts.options : [];\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue);\n }\n\n const onValueChange = React.useCallback(\n (newValue: string | null) => {\n if (newValue === null) return;\n if (readOnly) return;\n setValue(newValue);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValue });\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing && event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue);\n tableMeta?.onCellEditingStop?.();\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isEditing, isFocused, initialValue, tableMeta],\n );\n\n const selectedOption = options.find((option) => option.value === value);\n const displayLabel = selectedOption?.label ?? value;\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n className={cn(!isEditing && \"flex items-center\")}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n span]:items-start [&>svg]:hidden\"\n />\n \n {options.map((option, index) => (\n \n {option.label}\n \n ))}\n \n \n ) : displayLabel ? (\n \n {displayLabel}\n \n ) : null}\n \n );\n}\n\nexport function MultiSelectCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const cellValue = React.useMemo(() => {\n const value = cell.getValue() as string[];\n return value ?? [];\n }, [cell]);\n\n const cellKey = getCellKey(rowIndex, columnId);\n const prevCellKeyRef = React.useRef(cellKey);\n\n const [selectedValues, setSelectedValues] =\n React.useState(cellValue);\n const [searchValue, setSearchValue] = React.useState(\"\");\n const containerRef = React.useRef(null);\n const inputRef = React.useRef(null);\n const cellOpts = cell.column.columnDef.meta?.cell;\n const options = cellOpts?.variant === \"multi-select\" ? cellOpts.options : [];\n const sideOffset = -(containerRef.current?.clientHeight ?? 0);\n\n const prevCellValueRef = React.useRef(cellValue);\n if (cellValue !== prevCellValueRef.current) {\n prevCellValueRef.current = cellValue;\n setSelectedValues(cellValue);\n }\n\n if (prevCellKeyRef.current !== cellKey) {\n prevCellKeyRef.current = cellKey;\n setSearchValue(\"\");\n }\n\n const onValueChange = React.useCallback(\n (value: string) => {\n if (readOnly) return;\n const newValues = selectedValues.includes(value)\n ? selectedValues.filter((v) => v !== value)\n : [...selectedValues, value];\n\n setSelectedValues(newValues);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValues });\n setSearchValue(\"\");\n queueMicrotask(() => inputRef.current?.focus());\n },\n [selectedValues, tableMeta, rowIndex, columnId, readOnly],\n );\n\n const removeValue = React.useCallback(\n (valueToRemove: string, event?: React.MouseEvent) => {\n if (readOnly) return;\n event?.stopPropagation();\n event?.preventDefault();\n const newValues = selectedValues.filter((v) => v !== valueToRemove);\n setSelectedValues(newValues);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValues });\n // Focus back on input after removing\n setTimeout(() => inputRef.current?.focus(), 0);\n },\n [selectedValues, tableMeta, rowIndex, columnId, readOnly],\n );\n\n const clearAll = React.useCallback(() => {\n if (readOnly) return;\n setSelectedValues([]);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: [] });\n queueMicrotask(() => inputRef.current?.focus());\n }, [tableMeta, rowIndex, columnId, readOnly]);\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n setSearchValue(\"\");\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onOpenAutoFocus: NonNullable<\n React.ComponentProps[\"onOpenAutoFocus\"]\n > = React.useCallback((event) => {\n event.preventDefault();\n inputRef.current?.focus();\n }, []);\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing && event.key === \"Escape\") {\n event.preventDefault();\n setSelectedValues(cellValue);\n setSearchValue(\"\");\n tableMeta?.onCellEditingStop?.();\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n setSearchValue(\"\");\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isEditing, isFocused, cellValue, tableMeta],\n );\n\n const onInputKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n // Handle backspace when input is empty - remove last selected item\n if (\n event.key === \"Backspace\" &&\n searchValue === \"\" &&\n selectedValues.length > 0\n ) {\n event.preventDefault();\n const lastValue = selectedValues[selectedValues.length - 1];\n if (lastValue) {\n removeValue(lastValue);\n }\n }\n // Prevent escape from propagating to close the popover immediately\n // Let the command handle it first\n if (event.key === \"Escape\") {\n event.stopPropagation();\n }\n },\n [searchValue, selectedValues, removeValue],\n );\n\n const displayOptions = selectedValues\n .map((value) => {\n const option = options.find((candidate) => candidate.value === value);\n return {\n color: option?.color,\n label: option?.label ?? value,\n value,\n };\n })\n .filter((option) => option.label);\n\n const lineCount = getLineCount(rowHeight);\n\n const { visibleItems: visibleOptions, hiddenCount: hiddenBadgeCount } =\n useBadgeOverflow({\n items: displayOptions,\n getLabel: (option) => option.label,\n containerRef,\n lineCount,\n });\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n \n
\n \n \n \n
\n {selectedValues.map((value) => {\n const option = options.find((opt) => opt.value === value);\n const label = option?.label ?? value;\n\n return (\n \n {label}\n removeValue(value, event)}\n onPointerDown={(event) => {\n event.preventDefault();\n event.stopPropagation();\n }}\n >\n \n \n \n );\n })}\n \n
\n \n No options found.\n \n {options.map((option) => {\n const isSelected = selectedValues.includes(option.value);\n\n return (\n onValueChange(option.value)}\n >\n \n \n
\n {option.label}\n \n );\n })}\n \n {selectedValues.length > 0 && (\n <>\n \n \n \n Clear all\n \n \n \n )}\n \n \n \n
\n ) : null}\n {displayOptions.length > 0 ? (\n
\n {visibleOptions.map((option) => (\n \n {option.label}\n \n ))}\n {hiddenBadgeCount > 0 && (\n \n +{hiddenBadgeCount}\n \n )}\n
\n ) : null}\n \n );\n}\n\nexport function DateCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue ?? \"\");\n const containerRef = React.useRef(null);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue ?? \"\");\n }\n\n // Parse date as local time to avoid timezone shifts\n const selectedDate = value ? (parseLocalDate(value) ?? undefined) : undefined;\n\n const onDateSelect = React.useCallback(\n (date: Date | undefined) => {\n if (!date || readOnly) return;\n\n // Format using local date components to avoid timezone issues\n const formattedDate = formatDateToString(date);\n setValue(formattedDate);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: formattedDate });\n tableMeta?.onCellEditingStop?.();\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing && event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue);\n tableMeta?.onCellEditingStop?.();\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isEditing, isFocused, initialValue, tableMeta],\n );\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n \n \n \n {formatDateForDisplay(value)}\n \n \n {isEditing && (\n \n \n \n )}\n \n \n );\n}\n\nexport function FileCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const cellValue = React.useMemo(\n () => (cell.getValue() as FileCellData[]) ?? [],\n [cell],\n );\n\n const cellKey = getCellKey(rowIndex, columnId);\n const prevCellKeyRef = React.useRef(cellKey);\n\n const labelId = React.useId();\n const descriptionId = React.useId();\n\n const [files, setFiles] = React.useState(cellValue);\n const [uploadingFiles, setUploadingFiles] = React.useState>(\n new Set(),\n );\n const [deletingFiles, setDeletingFiles] = React.useState>(\n new Set(),\n );\n const [isDraggingOver, setIsDraggingOver] = React.useState(false);\n const [isDragging, setIsDragging] = React.useState(false);\n const [error, setError] = React.useState(null);\n\n const isUploading = uploadingFiles.size > 0;\n const isDeleting = deletingFiles.size > 0;\n const isPending = isUploading || isDeleting;\n const containerRef = React.useRef(null);\n const fileInputRef = React.useRef(null);\n const dropzoneRef = React.useRef(null);\n const cellOpts = cell.column.columnDef.meta?.cell;\n const sideOffset = -(containerRef.current?.clientHeight ?? 0);\n\n const fileCellOpts = cellOpts?.variant === \"file\" ? cellOpts : null;\n const maxFileSize = fileCellOpts?.maxFileSize ?? 10 * 1024 * 1024;\n const maxFiles = fileCellOpts?.maxFiles ?? 10;\n const accept = fileCellOpts?.accept;\n const multiple = fileCellOpts?.multiple ?? false;\n\n const acceptedTypes = React.useMemo(\n () => (accept ? accept.split(\",\").map((t) => t.trim()) : null),\n [accept],\n );\n\n const prevCellValueRef = React.useRef(cellValue);\n if (cellValue !== prevCellValueRef.current) {\n prevCellValueRef.current = cellValue;\n for (const file of files) {\n if (file.url) {\n URL.revokeObjectURL(file.url);\n }\n }\n setFiles(cellValue);\n setError(null);\n }\n\n if (prevCellKeyRef.current !== cellKey) {\n prevCellKeyRef.current = cellKey;\n setError(null);\n }\n\n const validateFile = React.useCallback(\n (file: File): string | null => {\n if (maxFileSize && file.size > maxFileSize) {\n return `File size exceeds ${formatFileSize(maxFileSize)}`;\n }\n if (acceptedTypes) {\n const fileExtension = `.${file.name.split(\".\").pop()}`;\n const isAccepted = acceptedTypes.some((type) => {\n if (type.endsWith(\"/*\")) {\n const baseType = type.slice(0, -2);\n return file.type.startsWith(`${baseType}/`);\n }\n if (type.startsWith(\".\")) {\n return fileExtension.toLowerCase() === type.toLowerCase();\n }\n return file.type === type;\n });\n if (!isAccepted) {\n return \"File type not accepted\";\n }\n }\n return null;\n },\n [maxFileSize, acceptedTypes],\n );\n\n const addFiles = React.useCallback(\n async (newFiles: File[], skipUpload = false) => {\n if (readOnly || isPending) return;\n setError(null);\n\n if (maxFiles && files.length + newFiles.length > maxFiles) {\n const errorMessage = `Maximum ${maxFiles} files allowed`;\n setError(errorMessage);\n toast(errorMessage);\n setTimeout(() => {\n setError(null);\n }, 2000);\n return;\n }\n\n const rejectedFiles: Array<{ name: string; reason: string }> = [];\n const filesToValidate: File[] = [];\n\n for (const file of newFiles) {\n const validationError = validateFile(file);\n if (validationError) {\n rejectedFiles.push({ name: file.name, reason: validationError });\n continue;\n }\n filesToValidate.push(file);\n }\n\n if (rejectedFiles.length > 0) {\n const firstError = rejectedFiles[0];\n if (firstError) {\n setError(firstError.reason);\n\n const truncatedName =\n firstError.name.length > 20\n ? `${firstError.name.slice(0, 20)}...`\n : firstError.name;\n\n if (rejectedFiles.length === 1) {\n toast(firstError.reason, {\n description: `\"${truncatedName}\" has been rejected`,\n });\n } else {\n toast(firstError.reason, {\n description: `\"${truncatedName}\" and ${rejectedFiles.length - 1} more rejected`,\n });\n }\n\n setTimeout(() => {\n setError(null);\n }, 2000);\n }\n }\n\n if (filesToValidate.length > 0) {\n if (!skipUpload) {\n const tempFiles: FileCellData[] = filesToValidate.map((f) => ({\n id: crypto.randomUUID(),\n name: f.name,\n size: f.size,\n type: f.type,\n url: undefined,\n }));\n const filesWithTemp = [...files, ...tempFiles];\n setFiles(filesWithTemp);\n\n const uploadingIds = new Set(tempFiles.map((f) => f.id));\n setUploadingFiles(uploadingIds);\n\n let uploadedFiles: FileCellData[] = [];\n\n if (tableMeta?.onFilesUpload) {\n try {\n uploadedFiles = await tableMeta.onFilesUpload({\n files: filesToValidate,\n rowIndex,\n columnId,\n });\n } catch (error) {\n toast.error(\n error instanceof Error\n ? error.message\n : `Failed to upload ${filesToValidate.length} file${filesToValidate.length !== 1 ? \"s\" : \"\"}`,\n );\n setFiles((prev) => prev.filter((f) => !uploadingIds.has(f.id)));\n setUploadingFiles(new Set());\n return;\n }\n } else {\n uploadedFiles = filesToValidate.map((f, i) => ({\n id: tempFiles[i]?.id ?? crypto.randomUUID(),\n name: f.name,\n size: f.size,\n type: f.type,\n url: URL.createObjectURL(f),\n }));\n }\n\n const finalFiles = filesWithTemp\n .map((f) => {\n if (uploadingIds.has(f.id)) {\n return uploadedFiles.find((uf) => uf.name === f.name) ?? f;\n }\n return f;\n })\n .filter((f) => f.url !== undefined);\n\n setFiles(finalFiles);\n setUploadingFiles(new Set());\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: finalFiles });\n } else {\n const newFilesData: FileCellData[] = filesToValidate.map((f) => ({\n id: crypto.randomUUID(),\n name: f.name,\n size: f.size,\n type: f.type,\n url: URL.createObjectURL(f),\n }));\n const updatedFiles = [...files, ...newFilesData];\n setFiles(updatedFiles);\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: updatedFiles,\n });\n }\n }\n },\n [\n files,\n maxFiles,\n validateFile,\n tableMeta,\n rowIndex,\n columnId,\n readOnly,\n isPending,\n ],\n );\n\n const removeFile = React.useCallback(\n async (fileId: string) => {\n if (readOnly || isPending) return;\n setError(null);\n\n const fileToRemove = files.find((f) => f.id === fileId);\n if (!fileToRemove) return;\n\n setDeletingFiles((prev) => new Set(prev).add(fileId));\n\n if (tableMeta?.onFilesDelete) {\n try {\n await tableMeta.onFilesDelete({\n fileIds: [fileId],\n rowIndex,\n columnId,\n });\n } catch (error) {\n toast.error(\n error instanceof Error\n ? error.message\n : `Failed to delete ${fileToRemove.name}`,\n );\n setDeletingFiles((prev) => {\n const next = new Set(prev);\n next.delete(fileId);\n return next;\n });\n return;\n }\n }\n\n if (fileToRemove.url?.startsWith(\"blob:\")) {\n URL.revokeObjectURL(fileToRemove.url);\n }\n\n const updatedFiles = files.filter((f) => f.id !== fileId);\n setFiles(updatedFiles);\n setDeletingFiles((prev) => {\n const next = new Set(prev);\n next.delete(fileId);\n return next;\n });\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: updatedFiles });\n },\n [files, tableMeta, rowIndex, columnId, readOnly, isPending],\n );\n\n const clearAll = React.useCallback(async () => {\n if (readOnly || isPending) return;\n setError(null);\n\n const fileIds = files.map((f) => f.id);\n setDeletingFiles(new Set(fileIds));\n\n if (tableMeta?.onFilesDelete && files.length > 0) {\n try {\n await tableMeta.onFilesDelete({\n fileIds,\n rowIndex,\n columnId,\n });\n } catch (error) {\n toast.error(\n error instanceof Error ? error.message : \"Failed to delete files\",\n );\n setDeletingFiles(new Set());\n return;\n }\n }\n\n for (const file of files) {\n if (file.url?.startsWith(\"blob:\")) {\n URL.revokeObjectURL(file.url);\n }\n }\n setFiles([]);\n setDeletingFiles(new Set());\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: [] });\n }, [files, tableMeta, rowIndex, columnId, readOnly, isPending]);\n\n const onCellDragEnter = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n if (event.dataTransfer.types.includes(\"Files\")) {\n setIsDraggingOver(true);\n }\n }, []);\n\n const onCellDragLeave = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n const rect = event.currentTarget.getBoundingClientRect();\n const x = event.clientX;\n const y = event.clientY;\n\n if (\n x <= rect.left ||\n x >= rect.right ||\n y <= rect.top ||\n y >= rect.bottom\n ) {\n setIsDraggingOver(false);\n }\n }, []);\n\n const onCellDragOver = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n }, []);\n\n const onCellDrop = React.useCallback(\n (event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n setIsDraggingOver(false);\n\n const droppedFiles = Array.from(event.dataTransfer.files);\n if (droppedFiles.length > 0) {\n addFiles(droppedFiles, false);\n }\n },\n [addFiles],\n );\n\n const onDropzoneDragEnter = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n setIsDragging(true);\n }, []);\n\n const onDropzoneDragLeave = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n const rect = event.currentTarget.getBoundingClientRect();\n const x = event.clientX;\n const y = event.clientY;\n\n if (\n x <= rect.left ||\n x >= rect.right ||\n y <= rect.top ||\n y >= rect.bottom\n ) {\n setIsDragging(false);\n }\n }, []);\n\n const onDropzoneDragOver = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n }, []);\n\n const onDropzoneDrop = React.useCallback(\n (event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n setIsDragging(false);\n\n const droppedFiles = Array.from(event.dataTransfer.files);\n addFiles(droppedFiles, false);\n },\n [addFiles],\n );\n\n const onDropzoneClick = React.useCallback(() => {\n fileInputRef.current?.click();\n }, []);\n\n const onDropzoneKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onDropzoneClick();\n }\n },\n [onDropzoneClick],\n );\n\n const onFileInputChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const selectedFiles = Array.from(event.target.files ?? []);\n addFiles(selectedFiles, false);\n event.target.value = \"\";\n },\n [addFiles],\n );\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n setError(null);\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n setError(null);\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onEscapeKeyDown: NonNullable<\n React.ComponentProps[\"onEscapeKeyDown\"]\n > = React.useCallback((event) => {\n // Prevent the escape key from propagating to the data grid's keyboard handler\n // which would call blurCell() and remove focus from the cell\n event.stopPropagation();\n }, []);\n\n const onOpenAutoFocus: NonNullable<\n React.ComponentProps[\"onOpenAutoFocus\"]\n > = React.useCallback((event) => {\n event.preventDefault();\n queueMicrotask(() => {\n dropzoneRef.current?.focus();\n });\n }, []);\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Escape\") {\n event.preventDefault();\n setFiles(cellValue);\n setError(null);\n tableMeta?.onCellEditingStop?.();\n } else if (event.key === \" \") {\n event.preventDefault();\n onDropzoneClick();\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n } else if (isFocused && event.key === \"Enter\") {\n event.preventDefault();\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [\n isEditing,\n isFocused,\n cellValue,\n tableMeta,\n onDropzoneClick,\n rowIndex,\n columnId,\n ],\n );\n\n React.useEffect(() => {\n return () => {\n for (const file of files) {\n if (file.url) {\n URL.revokeObjectURL(file.url);\n }\n }\n };\n }, [files]);\n\n const lineCount = getLineCount(rowHeight);\n\n const { visibleItems: visibleFiles, hiddenCount: hiddenFileCount } =\n useBadgeOverflow({\n items: files,\n getLabel: (file) => file.name,\n containerRef,\n lineCount,\n cacheKeyPrefix: \"file\",\n iconSize: 12,\n maxWidth: 100,\n });\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n className={cn({\n \"ring-1 ring-focus-ring ring-inset\": isDraggingOver,\n })}\n onDragEnter={onCellDragEnter}\n onDragLeave={onCellDragLeave}\n onDragOver={onCellDragOver}\n onDrop={onCellDrop}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n \n
\n \n \n
\n \n File upload\n \n \n \n
\n

\n {isDragging ? \"Drop files here\" : \"Drag files here\"}\n

\n

\n or click to browse\n

\n
\n

\n {maxFileSize\n ? `Max size: ${formatFileSize(maxFileSize)}${maxFiles ? ` • Max ${maxFiles} files` : \"\"}`\n : maxFiles\n ? `Max ${maxFiles} files`\n : \"Select files to upload\"}\n

\n
\n \n {files.length > 0 && (\n
\n
\n

\n {files.length} {files.length === 1 ? \"file\" : \"files\"}\n

\n \n Clear all\n \n
\n
\n {files.map((file) => {\n const FileIcon = getFileIcon(file.type);\n const isFileUploading = uploadingFiles.has(file.id);\n const isFileDeleting = deletingFiles.has(file.id);\n const isFilePending = isFileUploading || isFileDeleting;\n\n return (\n \n {FileIcon && (\n \n )}\n
\n

{file.name}

\n

\n {isFileUploading\n ? \"Uploading...\"\n : isFileDeleting\n ? \"Deleting...\"\n : formatFileSize(file.size)}\n

\n
\n removeFile(file.id)}\n disabled={isPending}\n >\n \n \n
\n );\n })}\n
\n
\n )}\n \n \n
\n ) : null}\n {isDraggingOver ? (\n
\n \n Drop files here\n
\n ) : files.length > 0 ? (\n
\n {visibleFiles.map((file) => {\n const isUploading = uploadingFiles.has(file.id);\n\n if (isUploading) {\n return (\n \n );\n }\n\n const FileIcon = getFileIcon(file.type);\n\n return (\n \n {FileIcon && }\n {file.name}\n \n );\n })}\n {hiddenFileCount > 0 && (\n \n +{hiddenFileCount}\n \n )}\n
\n ) : null}\n \n );\n}\n", "type": "registry:ui", "target": "components/ui/data-grid/data-grid-cell-variants.tsx" }, { "path": "src/components/ui/data-grid/data-grid-column-header.tsx", "content": "/* biome-ignore-all lint/a11y/useSemanticElements: The resize handle is a focusable ARIA separator, not a document hr. */\n\n\"use client\";\n\nimport type {\n ColumnSort,\n Header,\n SortDirection,\n SortingState,\n Table,\n} from \"@tanstack/react-table\";\nimport * as React from \"react\";\nimport { getColumnVariant } from \"@/lib/data-grid\";\nimport { cn } from \"@/lib/utils\";\nimport { createIconSlot } from \"@/lib/icon-context\";\nimport {\n DropdownContent,\n DropdownMenu,\n DropdownSeparator,\n DropdownTrigger,\n} from \"../dropdown\";\nimport { MenuItem } from \"../menu-item\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"./data-grid-tooltip\";\n\nconst ChevronDownIcon = createIconSlot(\"chevron-down\");\nconst ChevronUpIcon = createIconSlot(\"chevron-up\");\nconst EyeOffIcon = createIconSlot(\"eye-off\");\nconst PinIcon = createIconSlot(\"pin\");\nconst PinOffIcon = createIconSlot(\"pin-off\");\nconst XIcon = createIconSlot(\"x\");\n\ninterface DataGridColumnHeaderProps\n extends React.ComponentProps<\"button\"> {\n header: Header;\n table: Table;\n}\n\nexport function DataGridColumnHeader({\n header,\n table,\n className,\n onPointerDown,\n ...props\n}: DataGridColumnHeaderProps) {\n const column = header.column;\n const label = column.columnDef.meta?.label\n ? column.columnDef.meta.label\n : typeof column.columnDef.header === \"string\"\n ? column.columnDef.header\n : column.id;\n\n const isAnyColumnResizing =\n table.getState().columnSizingInfo.isResizingColumn;\n\n const cellVariant = column.columnDef.meta?.cell;\n const columnVariant = getColumnVariant(cellVariant?.variant);\n\n const pinnedPosition = column.getIsPinned();\n const isPinnedLeft = pinnedPosition === \"left\";\n const isPinnedRight = pinnedPosition === \"right\";\n const sorted = column.getIsSorted();\n const pinStartIndex = column.getCanSort() ? (sorted ? 3 : 2) : 0;\n const hideIndex = pinStartIndex + (column.getCanPin() ? 2 : 0);\n\n const onSortingChange = React.useCallback(\n (direction: SortDirection) => {\n table.setSorting((prev: SortingState) => {\n const existingSortIndex = prev.findIndex(\n (sort) => sort.id === column.id,\n );\n const newSort: ColumnSort = {\n id: column.id,\n desc: direction === \"desc\",\n };\n\n if (existingSortIndex >= 0) {\n const updated = [...prev];\n updated[existingSortIndex] = newSort;\n return updated;\n } else {\n return [...prev, newSort];\n }\n });\n },\n [column.id, table],\n );\n\n const onSortRemove = React.useCallback(() => {\n table.setSorting((prev: SortingState) =>\n prev.filter((sort) => sort.id !== column.id),\n );\n }, [column.id, table]);\n\n const onLeftPin = React.useCallback(() => {\n column.pin(\"left\");\n }, [column]);\n\n const onRightPin = React.useCallback(() => {\n column.pin(\"right\");\n }, [column]);\n\n const onUnpin = React.useCallback(() => {\n column.pin(false);\n }, [column]);\n\n const onTriggerPointerDown = React.useCallback(\n (event: React.PointerEvent) => {\n onPointerDown?.(\n event as Parameters>[0],\n );\n if (event.defaultPrevented) return;\n\n if (event.button !== 0) {\n return;\n }\n table.options.meta?.onColumnClick?.(column.id);\n },\n [table.options.meta, column.id, onPointerDown],\n );\n\n return (\n <>\n \n \n
\n {columnVariant && (\n \n \n \n \n \n

{columnVariant.label}

\n
\n
\n )}\n {label}\n
\n \n \n }\n />\n \n {column.getCanSort() && (\n <>\n onSortingChange(\"asc\")}\n />\n onSortingChange(\"desc\")}\n />\n {sorted && (\n \n )}\n \n )}\n {column.getCanPin() && (\n <>\n {column.getCanSort() && }\n\n {isPinnedLeft ? (\n \n ) : (\n \n )}\n {isPinnedRight ? (\n \n ) : (\n \n )}\n \n )}\n {column.getCanHide() && (\n <>\n \n column.toggleVisibility(false)}\n />\n \n )}\n \n
\n {header.column.getCanResize() && (\n \n )}\n \n );\n}\n\nconst DataGridColumnResizer = React.memo(\n DataGridColumnResizerImpl,\n (prev, next) => {\n const prevColumn = prev.header.column;\n const nextColumn = next.header.column;\n\n if (\n prevColumn.getIsResizing() !== nextColumn.getIsResizing() ||\n prevColumn.getSize() !== nextColumn.getSize()\n ) {\n return false;\n }\n\n if (prev.label !== next.label) return false;\n\n return true;\n },\n) as typeof DataGridColumnResizerImpl;\n\ninterface DataGridColumnResizerProps\n extends DataGridColumnHeaderProps {\n label: string;\n}\n\nfunction DataGridColumnResizerImpl({\n header,\n table,\n label,\n}: DataGridColumnResizerProps) {\n const defaultColumnDef = table._getDefaultColumnDef();\n\n const onDoubleClick = React.useCallback(() => {\n header.column.resetSize();\n }, [header.column]);\n\n return (\n \n );\n}\n", "type": "registry:ui", "target": "components/ui/data-grid/data-grid-column-header.tsx" }, { "path": "src/components/ui/data-grid/data-grid-context-menu.tsx", "content": "\"use client\";\n\nimport type { ColumnDef, TableMeta } from \"@tanstack/react-table\";\nimport * as React from \"react\";\nimport { toast } from \"sonner\";\nimport { useAsRef } from \"@/hooks/use-as-ref\";\nimport { parseCellKey } from \"@/lib/data-grid\";\nimport { createIconSlot } from \"@/lib/icon-context\";\nimport type { CellUpdate, ContextMenuState } from \"@/lib/data-grid-types\";\nimport {\n DropdownContent,\n DropdownMenu,\n DropdownSeparator,\n DropdownTrigger,\n} from \"../dropdown\";\nimport { MenuItem } from \"../menu-item\";\n\nconst CopyIcon = createIconSlot(\"copy\");\nconst EraserIcon = createIconSlot(\"eraser\");\nconst ScissorsIcon = createIconSlot(\"scissors\");\nconst Trash2Icon = createIconSlot(\"trash\");\n\ninterface DataGridContextMenuProps {\n tableMeta: TableMeta;\n columns: Array>;\n contextMenu: ContextMenuState;\n}\n\nexport function DataGridContextMenu({\n tableMeta,\n columns,\n contextMenu,\n}: DataGridContextMenuProps) {\n const onContextMenuOpenChange = tableMeta?.onContextMenuOpenChange;\n const selectionState = tableMeta?.selectionState;\n const dataGridRef = tableMeta?.dataGridRef;\n const onDataUpdate = tableMeta?.onDataUpdate;\n const onRowsDelete = tableMeta?.onRowsDelete;\n const onCellsCopy = tableMeta?.onCellsCopy;\n const onCellsCut = tableMeta?.onCellsCut;\n\n if (!contextMenu.open) return null;\n\n return (\n \n );\n}\n\ninterface ContextMenuProps\n extends Pick<\n TableMeta,\n | \"dataGridRef\"\n | \"onContextMenuOpenChange\"\n | \"selectionState\"\n | \"onDataUpdate\"\n | \"onRowsDelete\"\n | \"onCellsCopy\"\n | \"onCellsCut\"\n | \"readOnly\"\n >,\n Required, \"contextMenu\">> {\n tableMeta: TableMeta;\n columns: Array>;\n}\n\nconst ContextMenu = React.memo(ContextMenuImpl, (prev, next) => {\n if (prev.contextMenu.open !== next.contextMenu.open) return false;\n if (!next.contextMenu.open) return true;\n if (prev.contextMenu.x !== next.contextMenu.x) return false;\n if (prev.contextMenu.y !== next.contextMenu.y) return false;\n\n const prevSize = prev.selectionState?.selectedCells?.size ?? 0;\n const nextSize = next.selectionState?.selectedCells?.size ?? 0;\n if (prevSize !== nextSize) return false;\n\n return true;\n}) as typeof ContextMenuImpl;\n\nfunction ContextMenuImpl({\n tableMeta,\n columns,\n dataGridRef,\n contextMenu,\n onContextMenuOpenChange,\n selectionState,\n onDataUpdate,\n onRowsDelete,\n onCellsCopy,\n onCellsCut,\n}: ContextMenuProps) {\n const propsRef = useAsRef({\n dataGridRef,\n selectionState,\n onDataUpdate,\n onRowsDelete,\n onCellsCopy,\n onCellsCut,\n columns,\n });\n\n const triggerStyle = React.useMemo(\n () => ({\n position: \"fixed\",\n left: `${contextMenu.x}px`,\n top: `${contextMenu.y}px`,\n width: \"1px\",\n height: \"1px\",\n padding: 0,\n margin: 0,\n border: \"none\",\n background: \"transparent\",\n pointerEvents: \"none\",\n opacity: 0,\n }),\n [contextMenu.x, contextMenu.y],\n );\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n onContextMenuOpenChange?.(open);\n if (!open) {\n requestAnimationFrame(() => propsRef.current.dataGridRef?.current?.focus());\n }\n },\n [onContextMenuOpenChange, propsRef],\n );\n\n const onCopy = React.useCallback(() => {\n propsRef.current.onCellsCopy?.();\n }, [propsRef]);\n\n const onCut = React.useCallback(() => {\n propsRef.current.onCellsCut?.();\n }, [propsRef]);\n\n const onClear = React.useCallback(() => {\n const { selectionState, columns, onDataUpdate } = propsRef.current;\n\n if (\n !selectionState?.selectedCells ||\n selectionState.selectedCells.size === 0\n )\n return;\n\n const updates: Array = [];\n\n for (const cellKey of selectionState.selectedCells) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n\n // Get column from columns array\n const column = columns.find((col) => {\n if (col.id) return col.id === columnId;\n if (\"accessorKey\" in col) return col.accessorKey === columnId;\n return false;\n });\n const cellVariant = column?.meta?.cell?.variant;\n\n let emptyValue: unknown = \"\";\n if (cellVariant === \"multi-select\" || cellVariant === \"file\") {\n emptyValue = [];\n } else if (cellVariant === \"number\" || cellVariant === \"date\") {\n emptyValue = null;\n } else if (cellVariant === \"checkbox\") {\n emptyValue = false;\n }\n\n updates.push({ rowIndex, columnId, value: emptyValue });\n }\n\n onDataUpdate?.(updates);\n\n toast.success(\n `${updates.length} cell${updates.length !== 1 ? \"s\" : \"\"} cleared`,\n );\n }, [propsRef]);\n\n const onDelete = React.useCallback(async () => {\n const { selectionState, onRowsDelete } = propsRef.current;\n\n if (\n !selectionState?.selectedCells ||\n selectionState.selectedCells.size === 0\n )\n return;\n\n const rowIndices = new Set();\n for (const cellKey of selectionState.selectedCells) {\n const { rowIndex } = parseCellKey(cellKey);\n rowIndices.add(rowIndex);\n }\n\n const rowIndicesArray = Array.from(rowIndices).sort((a, b) => a - b);\n const rowCount = rowIndicesArray.length;\n\n await onRowsDelete?.(rowIndicesArray);\n\n toast.success(`${rowCount} row${rowCount !== 1 ? \"s\" : \"\"} deleted`);\n }, [propsRef]);\n\n return (\n \n \n }\n />\n \n \n \n \n {onRowsDelete && (\n <>\n \n \n \n )}\n \n \n );\n}\n", "type": "registry:ui", "target": "components/ui/data-grid/data-grid-context-menu.tsx" }, { "path": "src/components/ui/data-grid/data-grid-keyboard-shortcuts.tsx", "content": "/* biome-ignore-all lint/suspicious/noArrayIndexKey: Shortcut groups are static copy and do not reorder at runtime. */\n\n\"use client\";\n\nimport * as React from \"react\";\nimport { createIconSlot } from \"@/lib/icon-context\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from \"../dialog\";\nimport { Input } from \"../input\";\nimport { Kbd, KbdGroup } from \"../kbd\";\nimport { Separator } from \"./data-grid-primitives\";\n\nconst SHORTCUT_KEY = \"/\";\nconst SearchIcon = createIconSlot(\"search\");\n\ninterface ShortcutGroup {\n title: string;\n shortcuts: Array<{\n keys: string[];\n description: string;\n }>;\n}\n\ninterface DataGridKeyboardShortcutsProps {\n enableSearch?: boolean;\n}\n\nexport const DataGridKeyboardShortcuts = React.memo(\n DataGridKeyboardShortcutsImpl,\n (prev, next) => {\n return prev.enableSearch === next.enableSearch;\n },\n);\n\nfunction DataGridKeyboardShortcutsImpl({\n enableSearch = false,\n}: DataGridKeyboardShortcutsProps) {\n const [open, setOpen] = React.useState(false);\n const [input, setInput] = React.useState(\"\");\n const inputRef = React.useRef(null);\n\n const isMac =\n typeof navigator !== \"undefined\"\n ? /Mac|iPhone|iPad|iPod/.test(navigator.userAgent)\n : false;\n\n const modKey = isMac ? \"⌘\" : \"Ctrl\";\n\n const onOpenChange = React.useCallback((isOpen: boolean) => {\n setOpen(isOpen);\n if (!isOpen) {\n setInput(\"\");\n }\n }, []);\n\n React.useEffect(() => {\n if (!open) return;\n const frame = requestAnimationFrame(() => inputRef.current?.focus());\n return () => cancelAnimationFrame(frame);\n }, [open]);\n\n const onInputChange = React.useCallback(\n (event: React.ChangeEvent) => {\n setInput(event.target.value);\n },\n [],\n );\n\n const shortcutGroups: ShortcutGroup[] = React.useMemo(\n () => [\n {\n title: \"Navigation\",\n shortcuts: [\n {\n keys: [\"↑\", \"↓\", \"←\", \"→\"],\n description: \"Navigate between cells\",\n },\n {\n keys: [\"Tab\"],\n description: \"Move to next cell\",\n },\n {\n keys: [\"Shift\", \"Tab\"],\n description: \"Move to previous cell\",\n },\n {\n keys: [\"Home\"],\n description: \"Move to first column\",\n },\n {\n keys: [\"End\"],\n description: \"Move to last column\",\n },\n {\n keys: [modKey, \"Home\"],\n description: \"Move to first cell\",\n },\n {\n keys: [modKey, \"End\"],\n description: \"Move to last cell\",\n },\n {\n keys: [\"PgUp\"],\n description: \"Move up one page\",\n },\n {\n keys: [\"PgDn\"],\n description: \"Move down one page\",\n },\n ],\n },\n {\n title: \"Selection\",\n shortcuts: [\n {\n keys: [\"Shift\", \"↑↓←→\"],\n description: \"Extend selection\",\n },\n {\n keys: [modKey, \"A\"],\n description: \"Select all cells\",\n },\n {\n keys: [modKey, \"Click\"],\n description: \"Toggle cell selection\",\n },\n {\n keys: [\"Shift\", \"Click\"],\n description: \"Select range\",\n },\n {\n keys: [\"Esc\"],\n description: \"Clear selection\",\n },\n ],\n },\n {\n title: \"Editing\",\n shortcuts: [\n {\n keys: [\"Enter\"],\n description: \"Start editing cell\",\n },\n {\n keys: [\"Double Click\"],\n description: \"Start editing cell\",\n },\n {\n keys: [\"Delete\"],\n description: \"Clear selected cells\",\n },\n {\n keys: [\"Backspace\"],\n description: \"Clear selected cells\",\n },\n ],\n },\n ...(enableSearch\n ? [\n {\n title: \"Search\",\n shortcuts: [\n {\n keys: [modKey, \"F\"],\n description: \"Open search\",\n },\n {\n keys: [\"Enter\"],\n description: \"Next match\",\n },\n {\n keys: [\"Shift\", \"Enter\"],\n description: \"Previous match\",\n },\n {\n keys: [\"Esc\"],\n description: \"Close search\",\n },\n ],\n },\n ]\n : []),\n {\n title: \"Sorting\",\n shortcuts: [\n {\n keys: [modKey, \"Shift\", \"S\"],\n description: \"Toggle the sort menu\",\n },\n {\n keys: [\"Backspace\"],\n description: \"Remove sort (when focused)\",\n },\n {\n keys: [\"Delete\"],\n description: \"Remove sort (when focused)\",\n },\n ],\n },\n {\n title: \"General\",\n shortcuts: [\n {\n keys: [modKey, \"/\"],\n description: \"Show keyboard shortcuts\",\n },\n ],\n },\n ],\n [modKey, enableSearch],\n );\n\n const filteredGroups = React.useMemo(() => {\n if (!input.trim()) return shortcutGroups;\n\n const query = input.toLowerCase();\n return shortcutGroups\n .map((group) => ({\n ...group,\n shortcuts: group.shortcuts.filter(\n (shortcut) =>\n shortcut.description.toLowerCase().includes(query) ||\n shortcut.keys.some((key) => key.toLowerCase().includes(query)),\n ),\n }))\n .filter((group) => group.shortcuts.length > 0);\n }, [shortcutGroups, input]);\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if ((event.ctrlKey || event.metaKey) && event.key === SHORTCUT_KEY) {\n event.preventDefault();\n setOpen(true);\n }\n }\n\n window.addEventListener(\"keydown\", onKeyDown);\n return () => {\n window.removeEventListener(\"keydown\", onKeyDown);\n };\n }, []);\n\n return (\n \n \n \n Keyboard shortcuts\n \n Use these keyboard shortcuts to navigate and interact with the data\n grid more efficiently.\n \n \n
\n
\n \n \n
\n
\n \n
\n {filteredGroups.length === 0 ? (\n
\n
\n \n
\n
\n
\n No shortcuts found\n
\n

\n Try searching for a different term.\n

\n
\n
\n ) : (\n
\n {filteredGroups.map((shortcutGroup) => (\n
\n

\n {shortcutGroup.title}\n

\n
\n {shortcutGroup.shortcuts.map((shortcut, index) => (\n \n ))}\n
\n
\n ))}\n
\n )}\n
\n \n
\n );\n}\n\nfunction ShortcutCard({\n keys,\n description,\n}: ShortcutGroup[\"shortcuts\"][number]) {\n return (\n
\n {description}\n \n {keys.map((key, index) => (\n \n {index > 0 && (\n +\n )}\n {key}\n \n ))}\n \n
\n );\n}\n", "type": "registry:ui", "target": "components/ui/data-grid/data-grid-keyboard-shortcuts.tsx" }, { "path": "src/components/ui/data-grid/data-grid-paste-dialog.tsx", "content": "/* biome-ignore-all lint/a11y/noLabelWithoutControl: Radio controls are rendered inside labels but wrapped by a local styled input helper. */\n\n\"use client\";\n\nimport type { TableMeta } from \"@tanstack/react-table\";\nimport * as React from \"react\";\nimport { useAsRef } from \"@/hooks/use-as-ref\";\nimport type { PasteDialogState } from \"@/lib/data-grid-types\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"../button\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"../dialog\";\n\ninterface DataGridPasteDialogProps {\n tableMeta: TableMeta;\n pasteDialog: PasteDialogState;\n}\n\nexport function DataGridPasteDialog({\n tableMeta,\n pasteDialog,\n}: DataGridPasteDialogProps) {\n const onPasteDialogOpenChange = tableMeta?.onPasteDialogOpenChange;\n const onCellsPaste = tableMeta?.onCellsPaste;\n\n if (!pasteDialog.open) return null;\n\n return (\n \n );\n}\n\ninterface PasteDialogProps\n extends Pick, \"onPasteDialogOpenChange\" | \"onCellsPaste\">,\n Required, \"pasteDialog\">> {}\n\nconst PasteDialog = React.memo(PasteDialogImpl, (prev, next) => {\n if (prev.pasteDialog.open !== next.pasteDialog.open) return false;\n if (!next.pasteDialog.open) return true;\n if (prev.pasteDialog.rowsNeeded !== next.pasteDialog.rowsNeeded) return false;\n\n return true;\n});\n\nfunction PasteDialogImpl({\n pasteDialog,\n onPasteDialogOpenChange,\n onCellsPaste,\n}: PasteDialogProps) {\n const propsRef = useAsRef({\n onPasteDialogOpenChange,\n onCellsPaste,\n });\n\n const expandRadioRef = React.useRef(null);\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n propsRef.current.onPasteDialogOpenChange?.(open);\n },\n [propsRef],\n );\n\n const onCancel = React.useCallback(() => {\n propsRef.current.onPasteDialogOpenChange?.(false);\n }, [propsRef]);\n\n const onContinue = React.useCallback(() => {\n propsRef.current.onCellsPaste?.(expandRadioRef.current?.checked ?? false);\n }, [propsRef]);\n\n return (\n \n \n \n Do you want to add more rows?\n \n We need {pasteDialog.rowsNeeded} additional row\n {pasteDialog.rowsNeeded !== 1 ? \"s\" : \"\"} to paste everything from\n your clipboard.\n \n \n
\n \n \n
\n \n \n \n \n
\n
\n );\n}\n\nfunction RadioItem({ className, ...props }: React.ComponentProps<\"input\">) {\n return (\n \n );\n}\n", "type": "registry:ui", "target": "components/ui/data-grid/data-grid-paste-dialog.tsx" }, { "path": "src/components/ui/data-grid/data-grid-search.tsx", "content": "/* biome-ignore-all lint/a11y/useSemanticElements: Search panel is positioned inside the grid overlay and keeps div semantics for layout. */\n\n\"use client\";\n\nimport * as React from \"react\";\nimport { useAsRef } from \"@/hooks/use-as-ref\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport type { SearchState } from \"@/lib/data-grid-types\";\nimport { useShape } from \"@/lib/shape-context\";\nimport { cn } from \"@/lib/utils\";\nimport { createIconSlot } from \"@/lib/icon-context\";\nimport { Button } from \"../button\";\nimport { Input } from \"../input\";\n\nconst ChevronDown = createIconSlot(\"chevron-down\");\nconst ChevronUp = createIconSlot(\"chevron-up\");\nconst X = createIconSlot(\"x\");\n\ntype DataGridSearchProps = SearchState;\n\nexport const DataGridSearch = React.memo(DataGridSearchImpl, (prev, next) => {\n if (prev.searchOpen !== next.searchOpen) return false;\n\n if (!next.searchOpen) return true;\n\n if (\n prev.searchQuery !== next.searchQuery ||\n prev.matchIndex !== next.matchIndex\n ) {\n return false;\n }\n\n if (prev.searchMatches.length !== next.searchMatches.length) return false;\n\n for (let i = 0; i < prev.searchMatches.length; i++) {\n const prevMatch = prev.searchMatches[i];\n const nextMatch = next.searchMatches[i];\n\n if (!prevMatch || !nextMatch) return false;\n\n if (\n prevMatch.rowIndex !== nextMatch.rowIndex ||\n prevMatch.columnId !== nextMatch.columnId\n ) {\n return false;\n }\n }\n\n return true;\n});\n\nfunction DataGridSearchImpl({\n searchMatches,\n matchIndex,\n searchOpen,\n onSearchOpenChange,\n searchQuery,\n onSearchQueryChange,\n onSearch,\n onNavigateToNextMatch,\n onNavigateToPrevMatch,\n}: DataGridSearchProps) {\n const shape = useShape();\n const propsRef = useAsRef({\n onSearchOpenChange,\n onSearchQueryChange,\n onSearch,\n onNavigateToNextMatch,\n onNavigateToPrevMatch,\n });\n\n const inputRef = React.useRef(null);\n\n React.useEffect(() => {\n if (searchOpen) {\n requestAnimationFrame(() => {\n inputRef.current?.focus();\n });\n }\n }, [searchOpen]);\n\n React.useEffect(() => {\n if (!searchOpen) return;\n\n function onEscape(event: KeyboardEvent) {\n if (event.key === \"Escape\") {\n event.preventDefault();\n propsRef.current.onSearchOpenChange(false);\n }\n }\n\n document.addEventListener(\"keydown\", onEscape);\n return () => document.removeEventListener(\"keydown\", onEscape);\n }, [searchOpen, propsRef]);\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n event.stopPropagation();\n\n if (event.key === \"Enter\") {\n event.preventDefault();\n if (event.shiftKey) {\n propsRef.current.onNavigateToPrevMatch();\n } else {\n propsRef.current.onNavigateToNextMatch();\n }\n }\n },\n [propsRef],\n );\n\n const debouncedSearch = useDebouncedCallback((query: string) => {\n propsRef.current.onSearch(query);\n }, 150);\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const value = event.target.value;\n propsRef.current.onSearchQueryChange(value);\n debouncedSearch(value);\n },\n [propsRef, debouncedSearch],\n );\n\n const onTriggerPointerDown = React.useCallback(\n (event: React.PointerEvent) => {\n // prevent implicit pointer capture\n const target = event.target;\n if (!(target instanceof HTMLElement)) return;\n if (target.hasPointerCapture(event.pointerId)) {\n target.releasePointerCapture(event.pointerId);\n }\n\n // Only prevent default if we're not clicking on the input\n // This allows text selection in the input while still preventing focus stealing elsewhere\n if (\n event.button === 0 &&\n event.ctrlKey === false &&\n event.pointerType === \"mouse\" &&\n !(event.target instanceof HTMLInputElement)\n ) {\n event.preventDefault();\n }\n },\n [],\n );\n\n const onPrevMatchPointerDown = React.useCallback(\n (event: React.PointerEvent) =>\n onTriggerPointerDown(event),\n [onTriggerPointerDown],\n );\n\n const onNextMatchPointerDown = React.useCallback(\n (event: React.PointerEvent) =>\n onTriggerPointerDown(event),\n [onTriggerPointerDown],\n );\n\n const onClose = React.useCallback(() => {\n propsRef.current.onSearchOpenChange(false);\n }, [propsRef]);\n\n const onPrevMatch = React.useCallback(() => {\n propsRef.current.onNavigateToPrevMatch();\n }, [propsRef]);\n\n const onNextMatch = React.useCallback(() => {\n propsRef.current.onNavigateToNextMatch();\n }, [propsRef]);\n\n if (!searchOpen) return null;\n\n return (\n \n
\n \n
\n \n \n \n \n \n \n \n \n \n
\n
\n
\n {searchMatches.length > 0 ? (\n \n {matchIndex + 1} of {searchMatches.length}\n \n ) : searchQuery ? (\n No results\n ) : (\n Type to search\n )}\n
\n \n );\n}\n", "type": "registry:ui", "target": "components/ui/data-grid/data-grid-search.tsx" }, { "path": "src/components/ui/data-grid/data-grid-command.tsx", "content": "\"use client\";\n\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport type * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { createIconSlot } from \"@/lib/icon-context\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from \"../dialog\";\n\nconst SearchIcon = createIconSlot(\"search\");\n\nfunction Command({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction CommandDialog({\n title = \"Command Palette\",\n description = \"Search for a command to run...\",\n children,\n ...props\n}: React.ComponentProps & {\n title?: string;\n description?: string;\n}) {\n return (\n \n \n {title}\n {description}\n \n \n \n {children as React.ReactNode}\n \n \n \n );\n}\n\nfunction CommandInput({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n \n \n \n );\n}\n\nfunction CommandList({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction CommandEmpty({\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction CommandGroup({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction CommandSeparator({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction CommandItem({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction CommandShortcut({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n \n );\n}\n\nexport {\n Command,\n CommandDialog,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n CommandShortcut,\n};\n", "type": "registry:ui", "target": "components/ui/data-grid/data-grid-command.tsx" }, { "path": "src/components/ui/data-grid/data-grid-calendar.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n type DayButton,\n DayPicker,\n getDefaultClassNames,\n} from \"react-day-picker\";\nimport { cn } from \"@/lib/utils\";\nimport { createIconSlot } from \"@/lib/icon-context\";\nimport { Button, buttonVariants } from \"../button\";\n\nconst ChevronDownIcon = createIconSlot(\"chevron-down\");\nconst ChevronLeftIcon = createIconSlot(\"chevron-left\");\nconst ChevronRightIcon = createIconSlot(\"chevron-right\");\n\nfunction Calendar({\n className,\n classNames,\n showOutsideDays = true,\n captionLayout = \"label\",\n buttonVariant = \"ghost\",\n formatters,\n components,\n ...props\n}: React.ComponentProps & {\n buttonVariant?: React.ComponentProps[\"variant\"];\n}) {\n const defaultClassNames = getDefaultClassNames();\n\n return (\n svg]:rotate-180`,\n String.raw`rtl:**:[.rdp-button\\_previous>svg]:rotate-180`,\n className,\n )}\n captionLayout={captionLayout}\n formatters={{\n formatMonthDropdown: (date) =>\n date.toLocaleString(\"default\", { month: \"short\" }),\n ...formatters,\n }}\n classNames={{\n root: cn(\"w-fit\", defaultClassNames.root),\n months: cn(\n \"flex gap-4 flex-col md:flex-row relative\",\n defaultClassNames.months,\n ),\n month: cn(\"flex flex-col w-full gap-4\", defaultClassNames.month),\n nav: cn(\n \"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between\",\n defaultClassNames.nav,\n ),\n button_previous: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none\",\n defaultClassNames.button_previous,\n ),\n button_next: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none\",\n defaultClassNames.button_next,\n ),\n month_caption: cn(\n \"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)\",\n defaultClassNames.month_caption,\n ),\n dropdowns: cn(\n \"w-full flex items-center text-body font-medium justify-center h-(--cell-size) gap-1.5\",\n defaultClassNames.dropdowns,\n ),\n dropdown_root: cn(\n \"relative border border-input shadow-control has-[select:focus-visible]:ring-1 has-[select:focus-visible]:ring-focus-ring rounded-control\",\n defaultClassNames.dropdown_root,\n ),\n dropdown: cn(\n \"absolute bg-surface-floating inset-0 opacity-0\",\n defaultClassNames.dropdown,\n ),\n caption_label: cn(\n \"select-none font-medium\",\n captionLayout === \"label\"\n ? \"text-body\"\n : \"rounded-control pl-2 pr-1 flex items-center gap-1 text-body h-8 [&>svg]:text-fg-muted [&>svg]:size-3.5\",\n defaultClassNames.caption_label,\n ),\n table: \"w-full border-collapse\",\n weekdays: cn(\"flex\", defaultClassNames.weekdays),\n weekday: cn(\n \"text-fg-muted rounded-control flex-1 font-normal text-label select-none\",\n defaultClassNames.weekday,\n ),\n week: cn(\"flex w-full mt-2\", defaultClassNames.week),\n week_number_header: cn(\n \"select-none w-(--cell-size)\",\n defaultClassNames.week_number_header,\n ),\n week_number: cn(\n \"text-label select-none text-fg-muted\",\n defaultClassNames.week_number,\n ),\n day: cn(\n \"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-control [&:last-child[data-selected=true]_button]:rounded-r-control group/day aspect-square select-none\",\n defaultClassNames.day,\n ),\n range_start: cn(\n \"rounded-l-control bg-selection\",\n defaultClassNames.range_start,\n ),\n range_middle: cn(\"rounded-none\", defaultClassNames.range_middle),\n range_end: cn(\"rounded-r-control bg-selection\", defaultClassNames.range_end),\n today: cn(\n \"bg-emphasis text-fg-default rounded-control data-[selected=true]:rounded-none\",\n defaultClassNames.today,\n ),\n outside: cn(\n \"text-fg-muted aria-selected:text-fg-muted\",\n defaultClassNames.outside,\n ),\n disabled: cn(\n \"text-fg-muted opacity-50\",\n defaultClassNames.disabled,\n ),\n hidden: cn(\"invisible\", defaultClassNames.hidden),\n ...classNames,\n }}\n components={{\n Root: ({ className, rootRef, ...props }) => {\n return (\n \n );\n },\n Chevron: ({ className, orientation, ...props }) => {\n if (orientation === \"left\") {\n return (\n \n );\n }\n\n if (orientation === \"right\") {\n return (\n \n );\n }\n\n return (\n \n );\n },\n DayButton: CalendarDayButton,\n WeekNumber: ({ children, ...props }) => {\n return (\n \n
\n {children}\n
\n \n );\n },\n ...components,\n }}\n {...props}\n />\n );\n}\n\nfunction CalendarDayButton({\n className,\n day,\n modifiers,\n ...props\n}: React.ComponentProps) {\n const defaultClassNames = getDefaultClassNames();\n\n const ref = React.useRef(null);\n React.useEffect(() => {\n if (modifiers.focused) ref.current?.focus();\n }, [modifiers.focused]);\n\n return (\n span]:text-label [&>span]:opacity-70\",\n defaultClassNames.day,\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { Calendar, CalendarDayButton };\n", "type": "registry:ui", "target": "components/ui/data-grid/data-grid-calendar.tsx" }, { "path": "src/components/ui/data-grid/data-grid-tooltip.tsx", "content": "\"use client\";\n\nimport { Tooltip as TooltipPrimitive } from \"@base-ui/react/tooltip\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { usePortalContainer } from \"@/lib/portal-container-context\";\n\nexport function TooltipProvider({\n delay = 0,\n ...props\n}: TooltipPrimitive.Provider.Props) {\n return (\n \n );\n}\n\nexport function Tooltip(props: TooltipPrimitive.Root.Props) {\n return ;\n}\n\nexport function TooltipTrigger({\n asChild,\n children,\n render,\n ...props\n}: TooltipPrimitive.Trigger.Props & {\n asChild?: boolean;\n}) {\n return (\n \n {asChild ? null : children}\n \n );\n}\n\nexport function TooltipContent({\n align = \"center\",\n alignOffset = 0,\n children,\n className,\n side = \"top\",\n sideOffset = 8,\n ...props\n}: TooltipPrimitive.Popup.Props &\n Pick<\n TooltipPrimitive.Positioner.Props,\n \"align\" | \"alignOffset\" | \"side\" | \"sideOffset\"\n >) {\n const portalContainer = usePortalContainer();\n\n return (\n \n \n \n {children}\n \n \n \n \n );\n}\n", "type": "registry:ui", "target": "components/ui/data-grid/data-grid-tooltip.tsx" }, { "path": "src/components/ui/data-grid/data-grid-popover.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Popover as PopoverPrimitive } from \"@base-ui/react/popover\";\nimport { cn } from \"@/lib/utils\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { useShape } from \"@/lib/shape-context\";\nimport {\n resolveSurface,\n SurfaceProvider,\n useSurface,\n} from \"@/lib/surface-context\";\nimport { surfaceClasses } from \"@/lib/surface-classes\";\nimport { usePortalContainer } from \"@/lib/portal-container-context\";\n\ninterface DataGridPopoverContextValue {\n anchorRef: React.RefObject;\n open: boolean;\n}\n\nconst DataGridPopoverContext = React.createContext(null);\n\nfunction Popover({\n defaultOpen = false,\n onOpenChange,\n open: controlledOpen,\n ...props\n}: PopoverPrimitive.Root.Props) {\n const anchorRef = React.useRef(null);\n const [internalOpen, setInternalOpen] = React.useState(defaultOpen);\n const open = controlledOpen ?? internalOpen;\n\n return (\n \n {\n if (controlledOpen === undefined) setInternalOpen(nextOpen);\n onOpenChange?.(nextOpen, eventDetails);\n }}\n />\n \n );\n}\n\nfunction PopoverAnchor({\n asChild,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & { asChild?: boolean }) {\n const context = React.useContext(DataGridPopoverContext);\n const child = React.isValidElement(children)\n ? (children as React.ReactElement<{ ref?: React.Ref }>)\n : null;\n const childRef = child?.props.ref;\n const ref = useComposedRefs(childRef, (node) => {\n if (context) context.anchorRef.current = node;\n });\n\n if (asChild && child) {\n return React.cloneElement(child, { ref });\n }\n\n return (\n
} data-slot=\"data-grid-popover-anchor\" {...props}>\n {children}\n
\n );\n}\n\ntype PopoverContentProps = PopoverPrimitive.Popup.Props &\n Pick<\n PopoverPrimitive.Positioner.Props,\n \"align\" | \"alignOffset\" | \"side\" | \"sideOffset\"\n > & {\n onEscapeKeyDown?: (event: Event) => void;\n onOpenAutoFocus?: (event: Event) => void;\n };\n\nfunction PopoverContent({\n align = \"center\",\n alignOffset = 0,\n className,\n onEscapeKeyDown,\n onKeyDownCapture,\n onOpenAutoFocus,\n side = \"bottom\",\n sideOffset = 4,\n ...props\n}: PopoverContentProps) {\n const context = React.useContext(DataGridPopoverContext);\n const portalContainer = usePortalContainer();\n const shape = useShape();\n const substrate = useSurface();\n const surface = resolveSurface(substrate, \"floating\");\n\n React.useEffect(() => {\n if (!context?.open || !onOpenAutoFocus) return;\n const frame = requestAnimationFrame(() => {\n onOpenAutoFocus(new Event(\"openAutoFocus\", { cancelable: true }));\n });\n return () => cancelAnimationFrame(frame);\n }, [context?.open, onOpenAutoFocus]);\n\n return (\n \n \n {\n onKeyDownCapture?.(event);\n if (event.defaultPrevented || event.key !== \"Escape\" || !onEscapeKeyDown) return;\n const escapeEvent = new Event(\"escapeKeyDown\", { cancelable: true });\n onEscapeKeyDown(escapeEvent);\n if (escapeEvent.defaultPrevented) {\n event.preventDefault();\n event.stopPropagation();\n }\n }}\n >\n {props.children}\n \n \n \n );\n}\n\nexport { Popover, PopoverAnchor, PopoverContent };\n", "type": "registry:ui", "target": "components/ui/data-grid/data-grid-popover.tsx" }, { "path": "src/components/ui/data-grid/data-grid-primitives.tsx", "content": "\"use client\";\n\nimport type * as React from \"react\";\nimport { Separator as SeparatorPrimitive } from \"@base-ui/react/separator\";\nimport { cn } from \"@/lib/utils\";\nimport { useShape } from \"@/lib/shape-context\";\n\nfunction Separator({\n className,\n orientation = \"horizontal\",\n ...props\n}: SeparatorPrimitive.Props) {\n return (\n \n );\n}\n\nfunction Skeleton({ className, ...props }: React.ComponentProps<\"div\">) {\n const shape = useShape();\n return (\n \n );\n}\n\nfunction Textarea({ className, rows = 1, ...props }: React.ComponentProps<\"textarea\">) {\n const shape = useShape();\n return (\n \n );\n}\n\nexport { Separator, Skeleton, Textarea };\n", "type": "registry:ui", "target": "components/ui/data-grid/data-grid-primitives.tsx" }, { "path": "src/system/hooks/use-data-grid.ts", "content": "import {\n type ColumnDef,\n type ColumnFiltersState,\n getCoreRowModel,\n getFilteredRowModel,\n getSortedRowModel,\n type Row,\n type RowSelectionState,\n type SortingState,\n type TableMeta,\n type TableOptions,\n type TableState,\n type Updater,\n useReactTable,\n} from \"@tanstack/react-table\";\nimport { useVirtualizer, type Virtualizer } from \"@tanstack/react-virtual\";\nimport * as React from \"react\";\nimport { toast } from \"sonner\";\nimport {\n getCellKey,\n getIsFileCellData,\n getIsInPopover,\n getRowHeightValue,\n getScrollDirection,\n matchSelectOption,\n parseCellKey,\n scrollCellIntoView,\n} from \"@/lib/data-grid\";\nimport type {\n CellPosition,\n CellUpdate,\n ContextMenuState,\n Direction,\n FileCellData,\n NavigationDirection,\n PasteDialogState,\n RowHeightValue,\n SearchState,\n SelectionState,\n} from \"@/lib/data-grid-types\";\nimport { useAsRef } from \"./use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"./use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"./use-lazy-ref\";\n\nconst DEFAULT_ROW_HEIGHT = \"short\";\nconst OVERSCAN = 6;\nconst VIEWPORT_OFFSET = 1;\nconst HORIZONTAL_PAGE_SIZE = 5;\nconst SCROLL_SYNC_RETRY_COUNT = 16;\nconst MIN_COLUMN_SIZE = 60;\nconst MAX_COLUMN_SIZE = 800;\nconst SEARCH_SHORTCUT_KEY = \"f\";\nconst NON_NAVIGABLE_COLUMN_IDS = [\"select\", \"actions\"];\n\nconst DOMAIN_REGEX = /^[\\w.-]+\\.[a-z]{2,}(\\/\\S*)?$/i;\nconst ISO_DATE_REGEX = /^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}.*)?$/;\nconst TRUTHY_BOOLEANS = new Set([\"true\", \"1\", \"yes\", \"checked\"]);\nconst VALID_BOOLEANS = new Set([\n \"true\",\n \"false\",\n \"1\",\n \"0\",\n \"yes\",\n \"no\",\n \"checked\",\n \"unchecked\",\n]);\n\ninterface DataGridState {\n sorting: SortingState;\n columnFilters: ColumnFiltersState;\n rowHeight: RowHeightValue;\n rowSelection: RowSelectionState;\n selectionState: SelectionState;\n focusedCell: CellPosition | null;\n editingCell: CellPosition | null;\n cutCells: Set;\n contextMenu: ContextMenuState;\n searchQuery: string;\n searchMatches: CellPosition[];\n matchIndex: number;\n searchOpen: boolean;\n lastClickedRowIndex: number | null;\n pasteDialog: PasteDialogState;\n}\n\ninterface DataGridStore {\n subscribe: (callback: () => void) => () => void;\n getState: () => DataGridState;\n setState: (\n key: K,\n value: DataGridState[K],\n ) => void;\n notify: () => void;\n batch: (fn: () => void) => void;\n}\n\nfunction useStore(\n store: DataGridStore,\n selector: (state: DataGridState) => T,\n): T {\n const getSnapshot = React.useCallback(\n () => selector(store.getState()),\n [store, selector],\n );\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface UseDataGridProps\n extends Omit, \"pageCount\" | \"getCoreRowModel\"> {\n onDataChange?: (data: TData[]) => void;\n onRowAdd?: (\n event?: React.MouseEvent,\n ) => Partial | Promise | null> | null;\n onRowsAdd?: (count: number) => void | Promise;\n onRowsDelete?: (rows: TData[], rowIndices: number[]) => void | Promise;\n onPaste?: (updates: Array) => void | Promise;\n onFilesUpload?: (params: {\n files: File[];\n rowIndex: number;\n columnId: string;\n }) => Promise;\n onFilesDelete?: (params: {\n fileIds: string[];\n rowIndex: number;\n columnId: string;\n }) => void | Promise;\n rowHeight?: RowHeightValue;\n onRowHeightChange?: (rowHeight: RowHeightValue) => void;\n overscan?: number;\n dir?: Direction;\n autoFocus?: boolean | Partial;\n enableSingleCellSelection?: boolean;\n enableColumnSelection?: boolean;\n enableSearch?: boolean;\n enablePaste?: boolean;\n readOnly?: boolean;\n}\n\nfunction useDataGrid({\n data,\n columns,\n rowHeight: rowHeightProp = DEFAULT_ROW_HEIGHT,\n overscan = OVERSCAN,\n dir: dirProp,\n initialState,\n ...props\n}: UseDataGridProps) {\n const dir = dirProp ?? \"ltr\";\n const dataGridRef = React.useRef(null);\n const tableRef = React.useRef>>(null);\n const rowVirtualizerRef =\n React.useRef>(null);\n const headerRef = React.useRef(null);\n const rowMapRef = React.useRef>(new Map());\n const cellMapRef = React.useRef>(new Map());\n const footerRef = React.useRef(null);\n const focusGuardRef = React.useRef(false);\n\n const propsRef = useAsRef({\n ...props,\n data,\n columns,\n initialState,\n });\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n\n const stateRef = useLazyRef(() => {\n return {\n sorting: initialState?.sorting ?? [],\n columnFilters: initialState?.columnFilters ?? [],\n rowHeight: rowHeightProp,\n rowSelection: initialState?.rowSelection ?? {},\n selectionState: {\n selectedCells: new Set(),\n selectionRange: null,\n isSelecting: false,\n },\n focusedCell: null,\n editingCell: null,\n cutCells: new Set(),\n contextMenu: {\n open: false,\n x: 0,\n y: 0,\n },\n searchQuery: \"\",\n searchMatches: [],\n matchIndex: -1,\n searchOpen: false,\n lastClickedRowIndex: null,\n pasteDialog: {\n open: false,\n rowsNeeded: 0,\n clipboardText: \"\",\n },\n };\n });\n\n const store = React.useMemo(() => {\n let isBatching = false;\n let pendingNotification = false;\n\n return {\n subscribe: (callback) => {\n listenersRef.current.add(callback);\n return () => listenersRef.current.delete(callback);\n },\n getState: () => stateRef.current,\n setState: (key, value) => {\n if (Object.is(stateRef.current[key], value)) return;\n stateRef.current[key] = value;\n\n if (isBatching) {\n pendingNotification = true;\n } else {\n if (!pendingNotification) {\n pendingNotification = true;\n queueMicrotask(() => {\n pendingNotification = false;\n store.notify();\n });\n }\n }\n },\n notify: () => {\n for (const listener of listenersRef.current) {\n listener();\n }\n },\n batch: (fn) => {\n if (isBatching) {\n fn();\n return;\n }\n\n isBatching = true;\n const wasPending = pendingNotification;\n pendingNotification = false;\n\n try {\n fn();\n } finally {\n isBatching = false;\n if (pendingNotification || wasPending) {\n pendingNotification = false;\n store.notify();\n }\n }\n },\n };\n }, [listenersRef, stateRef]);\n\n const focusedCell = useStore(store, (state) => state.focusedCell);\n const editingCell = useStore(store, (state) => state.editingCell);\n const selectionState = useStore(store, (state) => state.selectionState);\n const searchQuery = useStore(store, (state) => state.searchQuery);\n const searchMatches = useStore(store, (state) => state.searchMatches);\n const matchIndex = useStore(store, (state) => state.matchIndex);\n const searchOpen = useStore(store, (state) => state.searchOpen);\n const sorting = useStore(store, (state) => state.sorting);\n const columnFilters = useStore(store, (state) => state.columnFilters);\n const rowSelection = useStore(store, (state) => state.rowSelection);\n const rowHeight = useStore(store, (state) => state.rowHeight);\n const contextMenu = useStore(store, (state) => state.contextMenu);\n const pasteDialog = useStore(store, (state) => state.pasteDialog);\n\n const rowHeightValue = getRowHeightValue(rowHeight);\n\n const prevCellSelectionMapRef = useLazyRef(\n () => new Map>(),\n );\n\n // Memoize per-row selection sets to prevent unnecessary row re-renders\n // Each row gets a stable Set reference that only changes when its cells' selection changes\n const cellSelectionMap = React.useMemo(() => {\n const selectedCells = selectionState.selectedCells;\n\n if (selectedCells.size === 0) {\n prevCellSelectionMapRef.current.clear();\n return null;\n }\n\n const newRowCells = new Map>();\n for (const cellKey of selectedCells) {\n const { rowIndex } = parseCellKey(cellKey);\n let rowSet = newRowCells.get(rowIndex);\n if (!rowSet) {\n rowSet = new Set();\n newRowCells.set(rowIndex, rowSet);\n }\n rowSet.add(cellKey);\n }\n\n const stableMap = new Map>();\n for (const [rowIndex, newSet] of newRowCells) {\n const prevSet = prevCellSelectionMapRef.current.get(rowIndex);\n if (\n prevSet &&\n prevSet.size === newSet.size &&\n [...newSet].every((key) => prevSet.has(key))\n ) {\n stableMap.set(rowIndex, prevSet);\n } else {\n stableMap.set(rowIndex, newSet);\n }\n }\n\n prevCellSelectionMapRef.current = stableMap;\n return stableMap;\n }, [selectionState.selectedCells, prevCellSelectionMapRef]);\n\n const visualRowIndexCacheRef = React.useRef<{\n rows: Row[] | null;\n map: Map;\n } | null>(null);\n\n // Pre-compute visual row index map for O(1) lookups (used by select column)\n // Cache is invalidated when row model identity changes (sorting/filtering)\n const getVisualRowIndex = React.useCallback(\n (rowId: string): number | undefined => {\n const rows = tableRef.current?.getRowModel().rows;\n if (!rows) return undefined;\n\n if (visualRowIndexCacheRef.current?.rows !== rows) {\n const map = new Map();\n for (const [i, row] of rows.entries()) {\n map.set(row.id, i + 1);\n }\n visualRowIndexCacheRef.current = { rows, map };\n }\n\n return visualRowIndexCacheRef.current.map.get(rowId);\n },\n [],\n );\n\n const columnIds = React.useMemo(() => {\n return columns\n .map((c) => {\n if (c.id) return c.id;\n if (\"accessorKey\" in c) return c.accessorKey as string;\n return undefined;\n })\n .filter((id): id is string => Boolean(id));\n }, [columns]);\n\n const navigableColumnIds = React.useMemo(() => {\n return columnIds.filter((c) => !NON_NAVIGABLE_COLUMN_IDS.includes(c));\n }, [columnIds]);\n\n const onDataUpdate = React.useCallback(\n (updates: CellUpdate | Array) => {\n if (propsRef.current.readOnly) return;\n\n const updateArray = Array.isArray(updates) ? updates : [updates];\n\n if (updateArray.length === 0) return;\n\n const currentTable = tableRef.current;\n const currentData = propsRef.current.data;\n const rows = currentTable?.getRowModel().rows;\n\n const rowUpdatesMap = new Map<\n number,\n Array>\n >();\n\n for (const update of updateArray) {\n if (!rows || !currentTable) {\n const existingUpdates = rowUpdatesMap.get(update.rowIndex) ?? [];\n existingUpdates.push({\n columnId: update.columnId,\n value: update.value,\n });\n rowUpdatesMap.set(update.rowIndex, existingUpdates);\n } else {\n const row = rows[update.rowIndex];\n if (!row) continue;\n\n const originalData = row.original;\n const originalRowIndex = currentData.indexOf(originalData);\n\n const targetIndex =\n originalRowIndex !== -1 ? originalRowIndex : update.rowIndex;\n\n const existingUpdates = rowUpdatesMap.get(targetIndex) ?? [];\n existingUpdates.push({\n columnId: update.columnId,\n value: update.value,\n });\n rowUpdatesMap.set(targetIndex, existingUpdates);\n }\n }\n\n const tableRowCount = rows?.length ?? currentData.length;\n const newData: TData[] = new Array(tableRowCount);\n\n for (let i = 0; i < tableRowCount; i++) {\n const updates = rowUpdatesMap.get(i);\n const existingRow = currentData[i];\n const tableRow = rows?.[i];\n\n if (updates) {\n const baseRow = existingRow ?? tableRow?.original ?? ({} as TData);\n const updatedRow = { ...baseRow } as Record;\n for (const { columnId, value } of updates) {\n updatedRow[columnId] = value;\n }\n newData[i] = updatedRow as TData;\n } else {\n newData[i] = existingRow ?? tableRow?.original ?? ({} as TData);\n }\n }\n\n propsRef.current.onDataChange?.(newData);\n },\n [propsRef],\n );\n\n const getIsCellSelected = React.useCallback(\n (rowIndex: number, columnId: string) => {\n const currentSelectionState = store.getState().selectionState;\n return currentSelectionState.selectedCells.has(\n getCellKey(rowIndex, columnId),\n );\n },\n [store],\n );\n\n const onSelectionClear = React.useCallback(() => {\n store.batch(() => {\n store.setState(\"selectionState\", {\n selectedCells: new Set(),\n selectionRange: null,\n isSelecting: false,\n });\n store.setState(\"rowSelection\", {});\n });\n }, [store]);\n\n const selectAll = React.useCallback(() => {\n const allCells = new Set();\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows ?? [];\n const rowCount = rows.length ?? propsRef.current.data.length;\n\n for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {\n for (const columnId of columnIds) {\n allCells.add(getCellKey(rowIndex, columnId));\n }\n }\n\n const firstColumnId = columnIds[0];\n const lastColumnId = columnIds[columnIds.length - 1];\n\n store.setState(\"selectionState\", {\n selectedCells: allCells,\n selectionRange:\n columnIds.length > 0 && rowCount > 0 && firstColumnId && lastColumnId\n ? {\n start: { rowIndex: 0, columnId: firstColumnId },\n end: { rowIndex: rowCount - 1, columnId: lastColumnId },\n }\n : null,\n isSelecting: false,\n });\n }, [columnIds, propsRef, store]);\n\n const selectColumn = React.useCallback(\n (columnId: string) => {\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows ?? [];\n const rowCount = rows.length ?? propsRef.current.data.length;\n\n if (rowCount === 0) return;\n\n const selectedCells = new Set();\n\n for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {\n selectedCells.add(getCellKey(rowIndex, columnId));\n }\n\n store.setState(\"selectionState\", {\n selectedCells,\n selectionRange: {\n start: { rowIndex: 0, columnId },\n end: { rowIndex: rowCount - 1, columnId },\n },\n isSelecting: false,\n });\n },\n [propsRef, store],\n );\n\n const selectRange = React.useCallback(\n (start: CellPosition, end: CellPosition, isSelecting = false) => {\n const startColIndex = columnIds.indexOf(start.columnId);\n const endColIndex = columnIds.indexOf(end.columnId);\n\n const minRow = Math.min(start.rowIndex, end.rowIndex);\n const maxRow = Math.max(start.rowIndex, end.rowIndex);\n const minCol = Math.min(startColIndex, endColIndex);\n const maxCol = Math.max(startColIndex, endColIndex);\n\n const selectedCells = new Set();\n\n for (let rowIndex = minRow; rowIndex <= maxRow; rowIndex++) {\n for (let colIndex = minCol; colIndex <= maxCol; colIndex++) {\n const columnId = columnIds[colIndex];\n if (columnId) {\n selectedCells.add(getCellKey(rowIndex, columnId));\n }\n }\n }\n\n store.setState(\"selectionState\", {\n selectedCells,\n selectionRange: { start, end },\n isSelecting,\n });\n },\n [columnIds, store],\n );\n\n const onCellsCopy = React.useCallback(async () => {\n const currentState = store.getState();\n\n let selectedCellsArray: string[];\n if (!currentState.selectionState.selectedCells.size) {\n if (!currentState.focusedCell) return;\n const focusedCellKey = getCellKey(\n currentState.focusedCell.rowIndex,\n currentState.focusedCell.columnId,\n );\n selectedCellsArray = [focusedCellKey];\n } else {\n selectedCellsArray = Array.from(\n currentState.selectionState.selectedCells,\n );\n }\n\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows;\n if (!rows) return;\n\n const selectedColumnIds: string[] = [];\n\n for (const cellKey of selectedCellsArray) {\n const { columnId } = parseCellKey(cellKey);\n if (columnId && !selectedColumnIds.includes(columnId)) {\n selectedColumnIds.push(columnId);\n }\n }\n\n const cellData = new Map();\n for (const cellKey of selectedCellsArray) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n const row = rows[rowIndex];\n if (row) {\n const cell = row\n .getVisibleCells()\n .find((c) => c.column.id === columnId);\n if (cell) {\n const value = cell.getValue();\n const cellVariant = cell.column.columnDef?.meta?.cell?.variant;\n\n let serializedValue = \"\";\n if (cellVariant === \"file\" || cellVariant === \"multi-select\") {\n serializedValue = value ? JSON.stringify(value) : \"\";\n } else if (value instanceof Date) {\n serializedValue = value.toISOString();\n } else {\n serializedValue = String(value ?? \"\");\n }\n\n cellData.set(cellKey, serializedValue);\n }\n }\n }\n\n const rowIndices = new Set();\n const colIndices = new Set();\n\n for (const cellKey of selectedCellsArray) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n rowIndices.add(rowIndex);\n const colIndex = selectedColumnIds.indexOf(columnId);\n if (colIndex >= 0) {\n colIndices.add(colIndex);\n }\n }\n\n const sortedRowIndices = Array.from(rowIndices).sort((a, b) => a - b);\n const sortedColIndices = Array.from(colIndices).sort((a, b) => a - b);\n const sortedColumnIds = sortedColIndices.map((i) => selectedColumnIds[i]);\n\n const tsvData = sortedRowIndices\n .map((rowIndex) =>\n sortedColumnIds\n .map((columnId) => {\n const cellKey = `${rowIndex}:${columnId}`;\n return cellData.get(cellKey) ?? \"\";\n })\n .join(\"\\t\"),\n )\n .join(\"\\n\");\n\n try {\n await navigator.clipboard.writeText(tsvData);\n\n const currentState = store.getState();\n if (currentState.cutCells.size > 0) {\n store.setState(\"cutCells\", new Set());\n }\n\n toast.success(\n `${selectedCellsArray.length} cell${\n selectedCellsArray.length !== 1 ? \"s\" : \"\"\n } copied`,\n );\n } catch (error) {\n toast.error(\n error instanceof Error ? error.message : \"Failed to copy to clipboard\",\n );\n }\n }, [store]);\n\n const onCellsCut = React.useCallback(async () => {\n if (propsRef.current.readOnly) return;\n\n const currentState = store.getState();\n\n let selectedCellsArray: string[];\n if (!currentState.selectionState.selectedCells.size) {\n if (!currentState.focusedCell) return;\n const focusedCellKey = getCellKey(\n currentState.focusedCell.rowIndex,\n currentState.focusedCell.columnId,\n );\n selectedCellsArray = [focusedCellKey];\n } else {\n selectedCellsArray = Array.from(\n currentState.selectionState.selectedCells,\n );\n }\n\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows;\n if (!rows) return;\n\n const selectedColumnIds: string[] = [];\n\n for (const cellKey of selectedCellsArray) {\n const { columnId } = parseCellKey(cellKey);\n if (columnId && !selectedColumnIds.includes(columnId)) {\n selectedColumnIds.push(columnId);\n }\n }\n\n const cellData = new Map();\n for (const cellKey of selectedCellsArray) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n const row = rows[rowIndex];\n if (row) {\n const cell = row\n .getVisibleCells()\n .find((c) => c.column.id === columnId);\n if (cell) {\n const value = cell.getValue();\n const cellVariant = cell.column.columnDef?.meta?.cell?.variant;\n\n let serializedValue = \"\";\n if (cellVariant === \"file\" || cellVariant === \"multi-select\") {\n serializedValue = value ? JSON.stringify(value) : \"\";\n } else if (value instanceof Date) {\n serializedValue = value.toISOString();\n } else {\n serializedValue = String(value ?? \"\");\n }\n\n cellData.set(cellKey, serializedValue);\n }\n }\n }\n\n const rowIndices = new Set();\n const colIndices = new Set();\n\n for (const cellKey of selectedCellsArray) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n rowIndices.add(rowIndex);\n const colIndex = selectedColumnIds.indexOf(columnId);\n if (colIndex >= 0) {\n colIndices.add(colIndex);\n }\n }\n\n const sortedRowIndices = Array.from(rowIndices).sort((a, b) => a - b);\n const sortedColIndices = Array.from(colIndices).sort((a, b) => a - b);\n const sortedColumnIds = sortedColIndices.map((i) => selectedColumnIds[i]);\n\n const tsvData = sortedRowIndices\n .map((rowIndex) =>\n sortedColumnIds\n .map((columnId) => {\n const cellKey = `${rowIndex}:${columnId}`;\n return cellData.get(cellKey) ?? \"\";\n })\n .join(\"\\t\"),\n )\n .join(\"\\n\");\n\n try {\n await navigator.clipboard.writeText(tsvData);\n\n store.setState(\"cutCells\", new Set(selectedCellsArray));\n\n toast.success(\n `${selectedCellsArray.length} cell${\n selectedCellsArray.length !== 1 ? \"s\" : \"\"\n } cut`,\n );\n } catch (error) {\n toast.error(\n error instanceof Error ? error.message : \"Failed to cut to clipboard\",\n );\n }\n }, [store, propsRef]);\n\n const restoreFocus = React.useCallback((element: HTMLDivElement | null) => {\n if (element && document.activeElement !== element) {\n requestAnimationFrame(() => {\n element.focus();\n });\n }\n }, []);\n\n const onCellsPaste = React.useCallback(\n async (expandRows = false) => {\n if (propsRef.current.readOnly) return;\n\n const currentState = store.getState();\n if (!currentState.focusedCell) return;\n\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows;\n if (!rows) return;\n\n try {\n let clipboardText = currentState.pasteDialog.clipboardText;\n\n if (!clipboardText) {\n clipboardText = await navigator.clipboard.readText();\n if (!clipboardText) return;\n }\n\n const pastedRows = clipboardText\n .split(\"\\n\")\n .filter((row) => row.length > 0);\n const pastedData = pastedRows.map((row) => row.split(\"\\t\"));\n\n const startRowIndex = currentState.focusedCell.rowIndex;\n const startColIndex = navigableColumnIds.indexOf(\n currentState.focusedCell.columnId,\n );\n\n if (startColIndex === -1) return;\n\n const rowCount = rows.length ?? propsRef.current.data.length;\n const rowsNeeded = startRowIndex + pastedData.length - rowCount;\n\n if (\n rowsNeeded > 0 &&\n !expandRows &&\n propsRef.current.onRowAdd &&\n !currentState.pasteDialog.clipboardText\n ) {\n store.setState(\"pasteDialog\", {\n open: true,\n rowsNeeded,\n clipboardText,\n });\n return;\n }\n\n if (expandRows && rowsNeeded > 0) {\n const expectedRowCount = rowCount + rowsNeeded;\n\n if (propsRef.current.onRowsAdd) {\n await propsRef.current.onRowsAdd(rowsNeeded);\n } else if (propsRef.current.onRowAdd) {\n for (let i = 0; i < rowsNeeded; i++) {\n await propsRef.current.onRowAdd();\n }\n }\n\n let attempts = 0;\n const maxAttempts = 50;\n let currentTableRowCount =\n tableRef.current?.getRowModel().rows.length ?? 0;\n\n while (\n currentTableRowCount < expectedRowCount &&\n attempts < maxAttempts\n ) {\n await new Promise((resolve) => setTimeout(resolve, 100));\n currentTableRowCount =\n tableRef.current?.getRowModel().rows.length ?? 0;\n attempts++;\n }\n }\n\n const updates: Array = [];\n const tableColumns = currentTable?.getAllColumns() ?? [];\n let cellsUpdated = 0;\n let endRowIndex = startRowIndex;\n let endColIndex = startColIndex;\n\n const updatedTable = tableRef.current;\n const updatedRows = updatedTable?.getRowModel().rows;\n const currentRowCount = updatedRows?.length ?? 0;\n\n let cellsSkipped = 0;\n\n const columnMap = new Map(tableColumns.map((c) => [c.id, c]));\n\n for (\n let pasteRowIdx = 0;\n pasteRowIdx < pastedData.length;\n pasteRowIdx++\n ) {\n const pasteRow = pastedData[pasteRowIdx];\n if (!pasteRow) continue;\n\n const targetRowIndex = startRowIndex + pasteRowIdx;\n if (targetRowIndex >= currentRowCount) break;\n\n for (\n let pasteColIdx = 0;\n pasteColIdx < pasteRow.length;\n pasteColIdx++\n ) {\n const targetColIndex = startColIndex + pasteColIdx;\n if (targetColIndex >= navigableColumnIds.length) break;\n\n const targetColumnId = navigableColumnIds[targetColIndex];\n if (!targetColumnId) continue;\n\n const pastedValue = pasteRow[pasteColIdx] ?? \"\";\n const column = columnMap.get(targetColumnId);\n const cellOpts = column?.columnDef?.meta?.cell;\n const cellVariant = cellOpts?.variant;\n\n let processedValue: unknown = pastedValue;\n let shouldSkip = false;\n\n switch (cellVariant) {\n case \"number\": {\n if (!pastedValue) {\n processedValue = null;\n } else {\n const num = Number.parseFloat(pastedValue);\n if (Number.isNaN(num)) shouldSkip = true;\n else processedValue = num;\n }\n break;\n }\n\n case \"checkbox\": {\n if (!pastedValue) {\n processedValue = false;\n } else {\n const lower = pastedValue.toLowerCase();\n if (VALID_BOOLEANS.has(lower)) {\n processedValue = TRUTHY_BOOLEANS.has(lower);\n } else {\n shouldSkip = true;\n }\n }\n break;\n }\n\n case \"date\": {\n if (!pastedValue) {\n processedValue = null;\n } else {\n const date = new Date(pastedValue);\n if (Number.isNaN(date.getTime())) shouldSkip = true;\n else processedValue = date;\n }\n break;\n }\n\n case \"select\": {\n const options = cellOpts?.options ?? [];\n if (!pastedValue) {\n processedValue = \"\";\n } else {\n const matched = matchSelectOption(pastedValue, options);\n if (matched) processedValue = matched;\n else shouldSkip = true;\n }\n break;\n }\n\n case \"multi-select\": {\n const options = cellOpts?.options ?? [];\n let values: string[] = [];\n try {\n const parsed = JSON.parse(pastedValue);\n if (Array.isArray(parsed)) {\n values = parsed.filter(\n (v): v is string => typeof v === \"string\",\n );\n }\n } catch {\n values = pastedValue\n ? pastedValue.split(\",\").map((v) => v.trim())\n : [];\n }\n\n const validated = values\n .map((v) => matchSelectOption(v, options))\n .filter(Boolean) as string[];\n\n if (values.length > 0 && validated.length === 0) {\n shouldSkip = true;\n } else {\n processedValue = validated;\n }\n break;\n }\n\n case \"file\": {\n if (!pastedValue) {\n processedValue = [];\n } else {\n try {\n const parsed = JSON.parse(pastedValue);\n if (!Array.isArray(parsed)) {\n shouldSkip = true;\n } else {\n const validFiles = parsed.filter(getIsFileCellData);\n if (parsed.length > 0 && validFiles.length === 0) {\n shouldSkip = true;\n } else {\n processedValue = validFiles;\n }\n }\n } catch {\n shouldSkip = true;\n }\n }\n break;\n }\n\n case \"url\": {\n if (!pastedValue) {\n processedValue = \"\";\n } else {\n const firstChar = pastedValue[0];\n if (firstChar === \"[\" || firstChar === \"{\") {\n shouldSkip = true;\n } else {\n try {\n new URL(pastedValue);\n processedValue = pastedValue;\n } catch {\n if (DOMAIN_REGEX.test(pastedValue)) {\n processedValue = pastedValue;\n } else {\n shouldSkip = true;\n }\n }\n }\n }\n break;\n }\n\n default: {\n if (!pastedValue) {\n processedValue = \"\";\n break;\n }\n\n if (ISO_DATE_REGEX.test(pastedValue)) {\n const date = new Date(pastedValue);\n if (!Number.isNaN(date.getTime())) {\n processedValue = date.toLocaleDateString();\n break;\n }\n }\n\n const firstChar = pastedValue[0];\n if (\n firstChar === \"[\" ||\n firstChar === \"{\" ||\n firstChar === \"t\" ||\n firstChar === \"f\"\n ) {\n try {\n const parsed = JSON.parse(pastedValue);\n\n if (Array.isArray(parsed)) {\n if (\n parsed.length > 0 &&\n parsed.every(getIsFileCellData)\n ) {\n processedValue = parsed.map((f) => f.name).join(\", \");\n } else if (parsed.every((v) => typeof v === \"string\")) {\n processedValue = (parsed as string[]).join(\", \");\n }\n } else if (typeof parsed === \"boolean\") {\n processedValue = parsed ? \"Checked\" : \"Unchecked\";\n }\n } catch {\n const lower = pastedValue.toLowerCase();\n if (lower === \"true\" || lower === \"false\") {\n processedValue =\n lower === \"true\" ? \"Checked\" : \"Unchecked\";\n }\n }\n }\n }\n }\n\n if (shouldSkip) {\n cellsSkipped++;\n endRowIndex = Math.max(endRowIndex, targetRowIndex);\n endColIndex = Math.max(endColIndex, targetColIndex);\n continue;\n }\n\n updates.push({\n rowIndex: targetRowIndex,\n columnId: targetColumnId,\n value: processedValue,\n });\n cellsUpdated++;\n\n endRowIndex = Math.max(endRowIndex, targetRowIndex);\n endColIndex = Math.max(endColIndex, targetColIndex);\n }\n }\n\n if (updates.length > 0) {\n if (propsRef.current.onPaste) {\n await propsRef.current.onPaste(updates);\n }\n\n const allUpdates = [...updates];\n\n if (currentState.cutCells.size > 0) {\n for (const cellKey of currentState.cutCells) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n\n const column = tableColumns.find((c) => c.id === columnId);\n const cellVariant = column?.columnDef?.meta?.cell?.variant;\n\n let emptyValue: unknown = \"\";\n if (cellVariant === \"multi-select\" || cellVariant === \"file\") {\n emptyValue = [];\n } else if (cellVariant === \"number\" || cellVariant === \"date\") {\n emptyValue = null;\n } else if (cellVariant === \"checkbox\") {\n emptyValue = false;\n }\n\n allUpdates.push({ rowIndex, columnId, value: emptyValue });\n }\n\n store.setState(\"cutCells\", new Set());\n }\n\n onDataUpdate(allUpdates);\n\n if (cellsSkipped > 0) {\n toast.success(\n `${cellsUpdated} cell${\n cellsUpdated !== 1 ? \"s\" : \"\"\n } pasted, ${cellsSkipped} skipped`,\n );\n } else {\n toast.success(\n `${cellsUpdated} cell${cellsUpdated !== 1 ? \"s\" : \"\"} pasted`,\n );\n }\n\n const endColumnId = navigableColumnIds[endColIndex];\n if (endColumnId) {\n selectRange(\n {\n rowIndex: startRowIndex,\n columnId: currentState.focusedCell.columnId,\n },\n { rowIndex: endRowIndex, columnId: endColumnId },\n );\n }\n\n restoreFocus(dataGridRef.current);\n } else if (cellsSkipped > 0) {\n toast.error(\n `${cellsSkipped} cell${\n cellsSkipped !== 1 ? \"s\" : \"\"\n } skipped pasting for invalid data`,\n );\n }\n\n if (currentState.pasteDialog.open) {\n store.setState(\"pasteDialog\", {\n open: false,\n rowsNeeded: 0,\n clipboardText: \"\",\n });\n }\n } catch (error) {\n toast.error(\n error instanceof Error\n ? error.message\n : \"Failed to paste. Please try again.\",\n );\n }\n },\n [\n store,\n navigableColumnIds,\n propsRef,\n onDataUpdate,\n selectRange,\n restoreFocus,\n ],\n );\n\n // Release focus guard after delay to allow async data re-renders to settle.\n // 300ms accounts for db sync and virtualized cell mounting.\n const releaseFocusGuard = React.useCallback((immediate = false) => {\n if (immediate) {\n focusGuardRef.current = false;\n return;\n }\n\n setTimeout(() => {\n focusGuardRef.current = false;\n }, 300);\n }, []);\n\n const focusCellWrapper = React.useCallback(\n (rowIndex: number, columnId: string) => {\n focusGuardRef.current = true;\n\n requestAnimationFrame(() => {\n const cellKey = getCellKey(rowIndex, columnId);\n const cellWrapperElement = cellMapRef.current.get(cellKey);\n\n if (!cellWrapperElement) {\n const container = dataGridRef.current;\n if (container) {\n container.focus();\n }\n releaseFocusGuard();\n return;\n }\n\n cellWrapperElement.focus();\n releaseFocusGuard();\n });\n },\n [releaseFocusGuard],\n );\n\n const focusCell = React.useCallback(\n (rowIndex: number, columnId: string) => {\n store.batch(() => {\n store.setState(\"focusedCell\", { rowIndex, columnId });\n store.setState(\"editingCell\", null);\n });\n\n const currentState = store.getState();\n\n if (currentState.searchOpen) return;\n\n focusCellWrapper(rowIndex, columnId);\n },\n [store, focusCellWrapper],\n );\n\n const onRowsDelete = React.useCallback(\n async (rowIndices: number[]) => {\n if (\n propsRef.current.readOnly ||\n !propsRef.current.onRowsDelete ||\n rowIndices.length === 0\n )\n return;\n\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows;\n\n if (!rows || rows.length === 0) return;\n\n const currentState = store.getState();\n const currentFocusedColumn =\n currentState.focusedCell?.columnId ?? navigableColumnIds[0];\n\n const minDeletedRowIndex = Math.min(...rowIndices);\n\n const rowsToDelete: TData[] = [];\n for (const rowIndex of rowIndices) {\n const row = rows[rowIndex];\n if (row) {\n rowsToDelete.push(row.original);\n }\n }\n\n await propsRef.current.onRowsDelete(rowsToDelete, rowIndices);\n\n store.batch(() => {\n store.setState(\"selectionState\", {\n selectedCells: new Set(),\n selectionRange: null,\n isSelecting: false,\n });\n store.setState(\"rowSelection\", {});\n store.setState(\"editingCell\", null);\n });\n\n requestAnimationFrame(() => {\n const currentTable = tableRef.current;\n const currentRows = currentTable?.getRowModel().rows ?? [];\n const newRowCount = currentRows.length ?? propsRef.current.data.length;\n\n if (newRowCount > 0 && currentFocusedColumn) {\n const targetRowIndex = Math.min(minDeletedRowIndex, newRowCount - 1);\n focusCell(targetRowIndex, currentFocusedColumn);\n }\n });\n },\n [propsRef, store, navigableColumnIds, focusCell],\n );\n\n const navigateCell = React.useCallback(\n (direction: NavigationDirection) => {\n const currentState = store.getState();\n if (!currentState.focusedCell) return;\n\n const { rowIndex, columnId } = currentState.focusedCell;\n const currentColIndex = navigableColumnIds.indexOf(columnId);\n const rowVirtualizer = rowVirtualizerRef.current;\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows ?? [];\n const rowCount = rows.length ?? propsRef.current.data.length;\n\n let newRowIndex = rowIndex;\n let newColumnId = columnId;\n\n const isRtl = dir === \"rtl\";\n\n switch (direction) {\n case \"up\":\n newRowIndex = Math.max(0, rowIndex - 1);\n break;\n case \"down\":\n newRowIndex = Math.min(rowCount - 1, rowIndex + 1);\n break;\n case \"left\":\n if (isRtl) {\n if (currentColIndex < navigableColumnIds.length - 1) {\n const nextColumnId = navigableColumnIds[currentColIndex + 1];\n if (nextColumnId) newColumnId = nextColumnId;\n }\n } else {\n if (currentColIndex > 0) {\n const prevColumnId = navigableColumnIds[currentColIndex - 1];\n if (prevColumnId) newColumnId = prevColumnId;\n }\n }\n break;\n case \"right\":\n if (isRtl) {\n if (currentColIndex > 0) {\n const prevColumnId = navigableColumnIds[currentColIndex - 1];\n if (prevColumnId) newColumnId = prevColumnId;\n }\n } else {\n if (currentColIndex < navigableColumnIds.length - 1) {\n const nextColumnId = navigableColumnIds[currentColIndex + 1];\n if (nextColumnId) newColumnId = nextColumnId;\n }\n }\n break;\n case \"home\":\n if (navigableColumnIds.length > 0) {\n newColumnId = navigableColumnIds[0] ?? columnId;\n }\n break;\n case \"end\":\n if (navigableColumnIds.length > 0) {\n newColumnId =\n navigableColumnIds[navigableColumnIds.length - 1] ?? columnId;\n }\n break;\n case \"ctrl+home\":\n newRowIndex = 0;\n if (navigableColumnIds.length > 0) {\n newColumnId = navigableColumnIds[0] ?? columnId;\n }\n break;\n case \"ctrl+end\":\n newRowIndex = Math.max(0, rowCount - 1);\n if (navigableColumnIds.length > 0) {\n newColumnId =\n navigableColumnIds[navigableColumnIds.length - 1] ?? columnId;\n }\n break;\n case \"ctrl+up\":\n newRowIndex = 0;\n break;\n case \"ctrl+down\":\n newRowIndex = Math.max(0, rowCount - 1);\n break;\n case \"pageup\":\n if (rowVirtualizer) {\n const visibleRange = rowVirtualizer.getVirtualItems();\n const pageSize = visibleRange.length ?? 10;\n newRowIndex = Math.max(0, rowIndex - pageSize);\n } else {\n newRowIndex = Math.max(0, rowIndex - 10);\n }\n break;\n case \"pagedown\":\n if (rowVirtualizer) {\n const visibleRange = rowVirtualizer.getVirtualItems();\n const pageSize = visibleRange.length ?? 10;\n newRowIndex = Math.min(rowCount - 1, rowIndex + pageSize);\n } else {\n newRowIndex = Math.min(rowCount - 1, rowIndex + 10);\n }\n break;\n case \"pageleft\":\n if (currentColIndex > 0) {\n const targetIndex = Math.max(\n 0,\n currentColIndex - HORIZONTAL_PAGE_SIZE,\n );\n const targetColumnId = navigableColumnIds[targetIndex];\n if (targetColumnId) newColumnId = targetColumnId;\n }\n break;\n case \"pageright\":\n if (currentColIndex < navigableColumnIds.length - 1) {\n const targetIndex = Math.min(\n navigableColumnIds.length - 1,\n currentColIndex + HORIZONTAL_PAGE_SIZE,\n );\n const targetColumnId = navigableColumnIds[targetIndex];\n if (targetColumnId) newColumnId = targetColumnId;\n }\n break;\n }\n\n if (newRowIndex !== rowIndex || newColumnId !== columnId) {\n focusCell(newRowIndex, newColumnId);\n\n // Calculate and apply scrolls synchronously to avoid flashing\n const container = dataGridRef.current;\n if (!container) return;\n\n const targetRow = rowMapRef.current.get(newRowIndex);\n const cellKey = getCellKey(newRowIndex, newColumnId);\n const targetCell = cellMapRef.current.get(cellKey);\n\n // If target row is not rendered, scroll it into view first\n if (!targetRow) {\n if (rowVirtualizer) {\n const align =\n direction === \"up\" ||\n direction === \"pageup\" ||\n direction === \"ctrl+up\" ||\n direction === \"ctrl+home\"\n ? \"start\"\n : direction === \"down\" ||\n direction === \"pagedown\" ||\n direction === \"ctrl+down\" ||\n direction === \"ctrl+end\"\n ? \"end\"\n : \"center\";\n\n rowVirtualizer.scrollToIndex(newRowIndex, { align });\n\n // Wait for row to render before horizontal scroll\n if (newColumnId !== columnId) {\n requestAnimationFrame(() => {\n const cellKeyRetry = getCellKey(newRowIndex, newColumnId);\n const targetCellRetry = cellMapRef.current.get(cellKeyRetry);\n\n if (targetCellRetry) {\n const scrollDirection = getScrollDirection(direction);\n\n scrollCellIntoView({\n container,\n targetCell: targetCellRetry,\n tableRef,\n viewportOffset: VIEWPORT_OFFSET,\n direction: scrollDirection,\n isRtl: dir === \"rtl\",\n });\n }\n });\n }\n } else {\n // Fallback: use direct scroll calculation when virtualizer is not available\n const rowHeightValue = getRowHeightValue(rowHeight);\n const estimatedScrollTop = newRowIndex * rowHeightValue;\n container.scrollTop = estimatedScrollTop;\n }\n\n return;\n }\n\n // Vertical scrolling for rendered rows that changed\n if (newRowIndex !== rowIndex && targetRow) {\n requestAnimationFrame(() => {\n const containerRect = container.getBoundingClientRect();\n const headerHeight =\n headerRef.current?.getBoundingClientRect().height ?? 0;\n const footerHeight =\n footerRef.current?.getBoundingClientRect().height ?? 0;\n const viewportTop =\n containerRect.top + headerHeight + VIEWPORT_OFFSET;\n const viewportBottom =\n containerRect.bottom - footerHeight - VIEWPORT_OFFSET;\n\n const rowRect = targetRow.getBoundingClientRect();\n const isFullyVisible =\n rowRect.top >= viewportTop && rowRect.bottom <= viewportBottom;\n\n if (!isFullyVisible) {\n // Only apply vertical scroll for vertical navigation\n const isVerticalNavigation =\n direction === \"up\" ||\n direction === \"down\" ||\n direction === \"pageup\" ||\n direction === \"pagedown\" ||\n direction === \"ctrl+up\" ||\n direction === \"ctrl+down\" ||\n direction === \"ctrl+home\" ||\n direction === \"ctrl+end\";\n\n if (isVerticalNavigation) {\n if (\n direction === \"down\" ||\n direction === \"pagedown\" ||\n direction === \"ctrl+down\" ||\n direction === \"ctrl+end\"\n ) {\n container.scrollTop += rowRect.bottom - viewportBottom;\n } else {\n container.scrollTop -= viewportTop - rowRect.top;\n }\n }\n }\n });\n }\n\n // Horizontal scrolling for rendered cells\n if (newColumnId !== columnId && targetCell) {\n requestAnimationFrame(() => {\n const scrollDirection = getScrollDirection(direction);\n\n scrollCellIntoView({\n container,\n targetCell,\n tableRef,\n viewportOffset: VIEWPORT_OFFSET,\n direction: scrollDirection,\n isRtl: dir === \"rtl\",\n });\n });\n }\n }\n },\n [dir, store, navigableColumnIds, focusCell, propsRef, rowHeight],\n );\n\n const onCellEditingStart = React.useCallback(\n (rowIndex: number, columnId: string) => {\n if (propsRef.current.readOnly) return;\n\n store.batch(() => {\n store.setState(\"focusedCell\", { rowIndex, columnId });\n store.setState(\"editingCell\", { rowIndex, columnId });\n });\n },\n [store, propsRef],\n );\n\n const onCellEditingStop = React.useCallback(\n (opts?: { moveToNextRow?: boolean; direction?: NavigationDirection }) => {\n const currentState = store.getState();\n const currentEditing = currentState.editingCell;\n\n store.setState(\"editingCell\", null);\n\n if (opts?.moveToNextRow && currentEditing) {\n const { rowIndex, columnId } = currentEditing;\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows ?? [];\n const rowCount = rows.length ?? propsRef.current.data.length;\n\n const nextRowIndex = rowIndex + 1;\n if (nextRowIndex < rowCount) {\n requestAnimationFrame(() => {\n focusCell(nextRowIndex, columnId);\n });\n }\n } else if (opts?.direction && currentEditing) {\n const { rowIndex, columnId } = currentEditing;\n focusCell(rowIndex, columnId);\n requestAnimationFrame(() => {\n navigateCell(opts.direction ?? \"right\");\n });\n } else if (currentEditing) {\n const { rowIndex, columnId } = currentEditing;\n focusCellWrapper(rowIndex, columnId);\n }\n },\n [store, propsRef, focusCell, navigateCell, focusCellWrapper],\n );\n\n const onSearchOpenChange = React.useCallback(\n (open: boolean) => {\n if (open) {\n store.setState(\"searchOpen\", true);\n return;\n }\n\n const currentState = store.getState();\n const currentMatch =\n currentState.matchIndex >= 0 &&\n currentState.searchMatches[currentState.matchIndex];\n\n store.batch(() => {\n store.setState(\"searchOpen\", false);\n store.setState(\"searchQuery\", \"\");\n store.setState(\"searchMatches\", []);\n store.setState(\"matchIndex\", -1);\n\n if (currentMatch) {\n store.setState(\"focusedCell\", {\n rowIndex: currentMatch.rowIndex,\n columnId: currentMatch.columnId,\n });\n }\n });\n\n if (\n dataGridRef.current &&\n document.activeElement !== dataGridRef.current\n ) {\n dataGridRef.current.focus();\n }\n },\n [store],\n );\n\n const onSearch = React.useCallback(\n (query: string) => {\n if (!query.trim()) {\n store.batch(() => {\n store.setState(\"searchMatches\", []);\n store.setState(\"matchIndex\", -1);\n });\n return;\n }\n\n const matches: CellPosition[] = [];\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows ?? [];\n\n const lowerQuery = query.toLowerCase();\n\n for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {\n const row = rows[rowIndex];\n if (!row) continue;\n\n for (const columnId of columnIds) {\n const cell = row\n .getVisibleCells()\n .find((c) => c.column.id === columnId);\n if (!cell) continue;\n\n const value = cell.getValue();\n const stringValue = String(value ?? \"\").toLowerCase();\n\n if (stringValue.includes(lowerQuery)) {\n matches.push({ rowIndex, columnId });\n }\n }\n }\n\n store.batch(() => {\n store.setState(\"searchMatches\", matches);\n store.setState(\"matchIndex\", matches.length > 0 ? 0 : -1);\n });\n\n if (matches.length > 0 && matches[0]) {\n const firstMatch = matches[0];\n rowVirtualizerRef.current?.scrollToIndex(firstMatch.rowIndex, {\n align: \"center\",\n });\n }\n },\n [columnIds, store],\n );\n\n const onSearchQueryChange = React.useCallback(\n (query: string) => store.setState(\"searchQuery\", query),\n [store],\n );\n\n const onNavigateToPrevMatch = React.useCallback(() => {\n const currentState = store.getState();\n if (currentState.searchMatches.length === 0) return;\n\n const prevIndex =\n currentState.matchIndex - 1 < 0\n ? currentState.searchMatches.length - 1\n : currentState.matchIndex - 1;\n const match = currentState.searchMatches[prevIndex];\n\n if (match) {\n rowVirtualizerRef.current?.scrollToIndex(match.rowIndex, {\n align: \"center\",\n });\n\n requestAnimationFrame(() => {\n store.setState(\"matchIndex\", prevIndex);\n requestAnimationFrame(() => {\n focusCell(match.rowIndex, match.columnId);\n });\n });\n }\n }, [store, focusCell]);\n\n const onNavigateToNextMatch = React.useCallback(() => {\n const currentState = store.getState();\n if (currentState.searchMatches.length === 0) return;\n\n const nextIndex =\n (currentState.matchIndex + 1) % currentState.searchMatches.length;\n const match = currentState.searchMatches[nextIndex];\n\n if (match) {\n rowVirtualizerRef.current?.scrollToIndex(match.rowIndex, {\n align: \"center\",\n });\n\n requestAnimationFrame(() => {\n store.setState(\"matchIndex\", nextIndex);\n requestAnimationFrame(() => {\n focusCell(match.rowIndex, match.columnId);\n });\n });\n }\n }, [store, focusCell]);\n\n const getIsSearchMatch = React.useCallback(\n (rowIndex: number, columnId: string) => {\n const currentSearchMatches = store.getState().searchMatches;\n return currentSearchMatches.some(\n (match) => match.rowIndex === rowIndex && match.columnId === columnId,\n );\n },\n [store],\n );\n\n const getIsActiveSearchMatch = React.useCallback(\n (rowIndex: number, columnId: string) => {\n const currentState = store.getState();\n if (currentState.matchIndex < 0) return false;\n const currentMatch = currentState.searchMatches[currentState.matchIndex];\n return (\n currentMatch?.rowIndex === rowIndex &&\n currentMatch?.columnId === columnId\n );\n },\n [store],\n );\n\n // Compute search match data for targeted row re-renders\n // Maps rowIndex -> Set of columnIds that have matches in that row\n const searchMatchesByRow = React.useMemo(() => {\n if (searchMatches.length === 0) return null;\n const rowMap = new Map>();\n for (const match of searchMatches) {\n let columnSet = rowMap.get(match.rowIndex);\n if (!columnSet) {\n columnSet = new Set();\n rowMap.set(match.rowIndex, columnSet);\n }\n columnSet.add(match.columnId);\n }\n return rowMap;\n }, [searchMatches]);\n\n const activeSearchMatch = React.useMemo(() => {\n if (matchIndex < 0 || searchMatches.length === 0) return null;\n return searchMatches[matchIndex] ?? null;\n }, [searchMatches, matchIndex]);\n\n const blurCell = React.useCallback(() => {\n const currentState = store.getState();\n if (\n currentState.editingCell &&\n document.activeElement instanceof HTMLElement\n ) {\n document.activeElement.blur();\n }\n\n store.batch(() => {\n store.setState(\"focusedCell\", null);\n store.setState(\"editingCell\", null);\n });\n }, [store]);\n\n const onCellClick = React.useCallback(\n (rowIndex: number, columnId: string, event?: React.MouseEvent) => {\n if (event?.button === 2) {\n return;\n }\n\n const currentState = store.getState();\n const currentFocused = currentState.focusedCell;\n\n function scrollToCell() {\n requestAnimationFrame(() => {\n const container = dataGridRef.current;\n const cellKey = getCellKey(rowIndex, columnId);\n const targetCell = cellMapRef.current.get(cellKey);\n\n if (container && targetCell) {\n scrollCellIntoView({\n container,\n targetCell,\n tableRef,\n viewportOffset: VIEWPORT_OFFSET,\n isRtl: dir === \"rtl\",\n });\n }\n });\n }\n\n if (event) {\n if (event.ctrlKey || event.metaKey) {\n event.preventDefault();\n const cellKey = getCellKey(rowIndex, columnId);\n const newSelectedCells = new Set(\n currentState.selectionState.selectedCells,\n );\n\n if (newSelectedCells.has(cellKey)) {\n newSelectedCells.delete(cellKey);\n } else {\n newSelectedCells.add(cellKey);\n }\n\n store.setState(\"selectionState\", {\n selectedCells: newSelectedCells,\n selectionRange: null,\n isSelecting: false,\n });\n focusCell(rowIndex, columnId);\n scrollToCell();\n return;\n }\n\n if (event.shiftKey && currentState.focusedCell) {\n event.preventDefault();\n selectRange(currentState.focusedCell, { rowIndex, columnId });\n scrollToCell();\n return;\n }\n }\n\n const hasSelectedCells =\n currentState.selectionState.selectedCells.size > 0;\n const hasSelectedRows = Object.keys(currentState.rowSelection).length > 0;\n\n if (hasSelectedCells && !currentState.selectionState.isSelecting) {\n const cellKey = getCellKey(rowIndex, columnId);\n const isClickingSelectedCell =\n currentState.selectionState.selectedCells.has(cellKey);\n\n if (!isClickingSelectedCell) {\n onSelectionClear();\n } else {\n focusCell(rowIndex, columnId);\n scrollToCell();\n return;\n }\n } else if (hasSelectedRows && columnId !== \"select\") {\n onSelectionClear();\n }\n\n if (\n currentFocused?.rowIndex === rowIndex &&\n currentFocused?.columnId === columnId\n ) {\n onCellEditingStart(rowIndex, columnId);\n } else {\n focusCell(rowIndex, columnId);\n scrollToCell();\n }\n },\n [store, focusCell, onCellEditingStart, selectRange, onSelectionClear, dir],\n );\n\n const onCellDoubleClick = React.useCallback(\n (rowIndex: number, columnId: string, event?: React.MouseEvent) => {\n if (event?.defaultPrevented) return;\n\n onCellEditingStart(rowIndex, columnId);\n },\n [onCellEditingStart],\n );\n\n const onCellMouseDown = React.useCallback(\n (rowIndex: number, columnId: string, event: React.MouseEvent) => {\n if (event.button === 2) {\n return;\n }\n\n event.preventDefault();\n\n if (!event.ctrlKey && !event.metaKey && !event.shiftKey) {\n const cellKey = getCellKey(rowIndex, columnId);\n store.batch(() => {\n store.setState(\"selectionState\", {\n selectedCells: propsRef.current.enableSingleCellSelection\n ? new Set([cellKey])\n : new Set(),\n selectionRange: {\n start: { rowIndex, columnId },\n end: { rowIndex, columnId },\n },\n isSelecting: true,\n });\n store.setState(\"rowSelection\", {});\n });\n }\n },\n [store, propsRef],\n );\n\n const onCellMouseEnter = React.useCallback(\n (rowIndex: number, columnId: string) => {\n const currentState = store.getState();\n if (\n currentState.selectionState.isSelecting &&\n currentState.selectionState.selectionRange\n ) {\n const start = currentState.selectionState.selectionRange.start;\n const end = { rowIndex, columnId };\n\n if (\n currentState.focusedCell?.rowIndex !== start.rowIndex ||\n currentState.focusedCell?.columnId !== start.columnId\n ) {\n focusCell(start.rowIndex, start.columnId);\n }\n\n selectRange(start, end, true);\n }\n },\n [store, selectRange, focusCell],\n );\n\n const onCellMouseUp = React.useCallback(() => {\n const currentState = store.getState();\n store.setState(\"selectionState\", {\n ...currentState.selectionState,\n isSelecting: false,\n });\n }, [store]);\n\n const onCellContextMenu = React.useCallback(\n (rowIndex: number, columnId: string, event: React.MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n\n const currentState = store.getState();\n const cellKey = getCellKey(rowIndex, columnId);\n const isTargetCellSelected =\n currentState.selectionState.selectedCells.has(cellKey);\n\n if (!isTargetCellSelected) {\n store.batch(() => {\n store.setState(\"selectionState\", {\n selectedCells: new Set([cellKey]),\n selectionRange: {\n start: { rowIndex, columnId },\n end: { rowIndex, columnId },\n },\n isSelecting: false,\n });\n store.setState(\"focusedCell\", { rowIndex, columnId });\n });\n }\n\n store.setState(\"contextMenu\", {\n open: true,\n x: event.clientX,\n y: event.clientY,\n });\n },\n [store],\n );\n\n const onContextMenuOpenChange = React.useCallback(\n (open: boolean) => {\n if (!open) {\n const currentMenu = store.getState().contextMenu;\n store.setState(\"contextMenu\", {\n open: false,\n x: currentMenu.x,\n y: currentMenu.y,\n });\n }\n },\n [store],\n );\n\n const onSortingChange = React.useCallback(\n (updater: Updater) => {\n const currentState = store.getState();\n const newSorting =\n typeof updater === \"function\" ? updater(currentState.sorting) : updater;\n store.setState(\"sorting\", newSorting);\n\n propsRef.current.onSortingChange?.(newSorting);\n },\n [store, propsRef],\n );\n\n const onColumnFiltersChange = React.useCallback(\n (updater: Updater) => {\n const currentState = store.getState();\n const newColumnFilters =\n typeof updater === \"function\"\n ? updater(currentState.columnFilters)\n : updater;\n store.setState(\"columnFilters\", newColumnFilters);\n\n propsRef.current.onColumnFiltersChange?.(newColumnFilters);\n },\n [store, propsRef],\n );\n\n const onRowSelectionChange = React.useCallback(\n (updater: Updater) => {\n const currentState = store.getState();\n const newRowSelection =\n typeof updater === \"function\"\n ? updater(currentState.rowSelection)\n : updater;\n\n const selectedRows = Object.keys(newRowSelection).filter(\n (key) => newRowSelection[key],\n );\n\n const selectedCells = new Set();\n const rows = tableRef.current?.getRowModel().rows ?? [];\n\n for (const rowId of selectedRows) {\n const rowIndex = rows.findIndex((r) => r.id === rowId);\n if (rowIndex === -1) continue;\n\n for (const columnId of columnIds) {\n selectedCells.add(getCellKey(rowIndex, columnId));\n }\n }\n\n store.batch(() => {\n store.setState(\"rowSelection\", newRowSelection);\n store.setState(\"selectionState\", {\n selectedCells,\n selectionRange: null,\n isSelecting: false,\n });\n store.setState(\"focusedCell\", null);\n store.setState(\"editingCell\", null);\n });\n },\n [store, columnIds],\n );\n\n const onRowSelect = React.useCallback(\n (rowIndex: number, selected: boolean, shiftKey: boolean) => {\n const currentState = store.getState();\n const rows = tableRef.current?.getRowModel().rows ?? [];\n const currentRow = rows[rowIndex];\n if (!currentRow) return;\n\n if (shiftKey && currentState.lastClickedRowIndex !== null) {\n const startIndex = Math.min(currentState.lastClickedRowIndex, rowIndex);\n const endIndex = Math.max(currentState.lastClickedRowIndex, rowIndex);\n\n const newRowSelection: RowSelectionState = {\n ...currentState.rowSelection,\n };\n\n for (let i = startIndex; i <= endIndex; i++) {\n const row = rows[i];\n if (row) {\n newRowSelection[row.id] = selected;\n }\n }\n\n onRowSelectionChange(newRowSelection);\n } else {\n onRowSelectionChange({\n ...currentState.rowSelection,\n [currentRow.id]: selected,\n });\n }\n\n store.setState(\"lastClickedRowIndex\", rowIndex);\n },\n [store, onRowSelectionChange],\n );\n\n const onRowHeightChange = React.useCallback(\n (updater: Updater) => {\n const currentState = store.getState();\n const newRowHeight =\n typeof updater === \"function\"\n ? updater(currentState.rowHeight)\n : updater;\n store.setState(\"rowHeight\", newRowHeight);\n propsRef.current.onRowHeightChange?.(newRowHeight);\n },\n [store, propsRef],\n );\n\n const onColumnClick = React.useCallback(\n (columnId: string) => {\n if (!propsRef.current.enableColumnSelection) {\n onSelectionClear();\n return;\n }\n\n selectColumn(columnId);\n },\n [propsRef, selectColumn, onSelectionClear],\n );\n\n const onPasteDialogOpenChange = React.useCallback(\n (open: boolean) => {\n if (!open) {\n store.setState(\"pasteDialog\", {\n open: false,\n rowsNeeded: 0,\n clipboardText: \"\",\n });\n }\n },\n [store],\n );\n\n const defaultColumn: Partial> = React.useMemo(\n () => ({\n // Note: cell is rendered directly in DataGridRow to bypass flexRender's\n // unstable cell.getContext() (see TanStack Table issue #4794)\n minSize: MIN_COLUMN_SIZE,\n maxSize: MAX_COLUMN_SIZE,\n }),\n [],\n );\n\n const tableMeta = React.useMemo>(() => {\n return {\n ...propsRef.current.meta,\n dataGridRef,\n cellMapRef,\n // Use getters for frequently changing state values to avoid recreating meta\n get focusedCell() {\n return store.getState().focusedCell;\n },\n get editingCell() {\n return store.getState().editingCell;\n },\n get selectionState() {\n return store.getState().selectionState;\n },\n get searchOpen() {\n return store.getState().searchOpen;\n },\n get contextMenu() {\n return store.getState().contextMenu;\n },\n get pasteDialog() {\n return store.getState().pasteDialog;\n },\n get rowHeight() {\n return store.getState().rowHeight;\n },\n get readOnly() {\n return propsRef.current.readOnly;\n },\n getIsCellSelected,\n getIsSearchMatch,\n getIsActiveSearchMatch,\n getVisualRowIndex,\n onRowHeightChange,\n onRowSelect,\n onDataUpdate,\n onRowsDelete: propsRef.current.onRowsDelete ? onRowsDelete : undefined,\n onColumnClick,\n onCellClick,\n onCellDoubleClick,\n onCellMouseDown,\n onCellMouseEnter,\n onCellMouseUp,\n onCellContextMenu,\n onCellEditingStart,\n onCellEditingStop,\n onCellsCopy,\n onCellsCut,\n onCellsPaste,\n onSelectionClear,\n onFilesUpload: propsRef.current.onFilesUpload\n ? propsRef.current.onFilesUpload\n : undefined,\n onFilesDelete: propsRef.current.onFilesDelete\n ? propsRef.current.onFilesDelete\n : undefined,\n onContextMenuOpenChange,\n onPasteDialogOpenChange,\n };\n }, [\n propsRef,\n store,\n getIsCellSelected,\n getIsSearchMatch,\n getIsActiveSearchMatch,\n getVisualRowIndex,\n onRowHeightChange,\n onRowSelect,\n onDataUpdate,\n onRowsDelete,\n onColumnClick,\n onCellClick,\n onCellDoubleClick,\n onCellMouseDown,\n onCellMouseEnter,\n onCellMouseUp,\n onCellContextMenu,\n onCellEditingStart,\n onCellEditingStop,\n onCellsCopy,\n onCellsCut,\n onCellsPaste,\n onSelectionClear,\n onContextMenuOpenChange,\n onPasteDialogOpenChange,\n ]);\n\n const getMemoizedCoreRowModel = React.useMemo(() => getCoreRowModel(), []);\n const getMemoizedFilteredRowModel = React.useMemo(\n () => getFilteredRowModel(),\n [],\n );\n const getMemoizedSortedRowModel = React.useMemo(\n () => getSortedRowModel(),\n [],\n );\n\n // Memoize state object to reduce shallow equality checks\n const tableState = React.useMemo>(\n () => ({\n ...propsRef.current.state,\n sorting,\n columnFilters,\n rowSelection,\n }),\n [propsRef, sorting, columnFilters, rowSelection],\n );\n\n const tableOptions = React.useMemo>(() => {\n return {\n ...propsRef.current,\n data,\n columns,\n defaultColumn,\n initialState: propsRef.current.initialState,\n state: tableState,\n onRowSelectionChange,\n onSortingChange,\n onColumnFiltersChange,\n columnResizeMode: \"onChange\",\n columnResizeDirection: dir,\n getCoreRowModel: getMemoizedCoreRowModel,\n getFilteredRowModel: getMemoizedFilteredRowModel,\n getSortedRowModel: getMemoizedSortedRowModel,\n meta: tableMeta,\n };\n }, [\n propsRef,\n data,\n columns,\n defaultColumn,\n tableState,\n dir,\n onRowSelectionChange,\n onSortingChange,\n onColumnFiltersChange,\n getMemoizedCoreRowModel,\n getMemoizedFilteredRowModel,\n getMemoizedSortedRowModel,\n tableMeta,\n ]);\n\n const table = useReactTable(tableOptions);\n\n if (!tableRef.current) {\n tableRef.current = table;\n }\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: columnSizingInfo and columnSizing are used for calculating the column size vars\n const columnSizeVars = React.useMemo(() => {\n const headers = table.getFlatHeaders();\n const colSizes: { [key: string]: number } = {};\n for (const header of headers) {\n colSizes[`--header-${header.id}-size`] = header.getSize();\n colSizes[`--col-${header.column.id}-size`] = header.column.getSize();\n }\n return colSizes;\n }, [table.getState().columnSizingInfo, table.getState().columnSizing]);\n\n const isFirefox = React.useSyncExternalStore(\n React.useCallback(() => () => {}, []),\n React.useCallback(() => {\n if (typeof window === \"undefined\" || typeof navigator === \"undefined\") {\n return false;\n }\n return navigator.userAgent.indexOf(\"Firefox\") !== -1;\n }, []),\n React.useCallback(() => false, []),\n );\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: columnPinning is used for calculating the adjustLayout\n const adjustLayout = React.useMemo(() => {\n const columnPinning = table.getState().columnPinning;\n return (\n isFirefox &&\n ((columnPinning.left?.length ?? 0) > 0 ||\n (columnPinning.right?.length ?? 0) > 0)\n );\n }, [isFirefox, table.getState().columnPinning]);\n\n const rowVirtualizer = useVirtualizer({\n count: table.getRowModel().rows.length,\n getScrollElement: () => dataGridRef.current,\n estimateSize: () => rowHeightValue,\n overscan,\n measureElement: !isFirefox\n ? (element) => element?.getBoundingClientRect().height\n : undefined,\n });\n\n if (!rowVirtualizerRef.current) {\n rowVirtualizerRef.current = rowVirtualizer;\n }\n\n const onScrollToRow = React.useCallback(\n async (opts: Partial) => {\n const rowIndex = opts?.rowIndex ?? 0;\n const columnId = opts?.columnId;\n\n focusGuardRef.current = true;\n\n const navigableIds = propsRef.current.columns\n .map((c) => {\n if (c.id) return c.id;\n if (\"accessorKey\" in c) return c.accessorKey as string;\n return undefined;\n })\n .filter((id): id is string => Boolean(id))\n .filter((c) => !NON_NAVIGABLE_COLUMN_IDS.includes(c));\n\n const targetColumnId = columnId ?? navigableIds[0];\n\n if (!targetColumnId) {\n releaseFocusGuard(true);\n return;\n }\n\n async function onScrollAndFocus(retryCount: number) {\n if (!targetColumnId) return;\n const currentRowCount = propsRef.current.data.length;\n\n // If the requested row doesn't exist yet, wait for data to update\n if (rowIndex >= currentRowCount && retryCount > 0) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n await onScrollAndFocus(retryCount - 1);\n return;\n }\n\n const safeRowIndex = Math.min(\n rowIndex,\n Math.max(0, currentRowCount - 1),\n );\n\n const isBottomHalf = safeRowIndex > currentRowCount / 2;\n rowVirtualizer.scrollToIndex(safeRowIndex, {\n align: isBottomHalf ? \"end\" : \"start\",\n });\n\n await new Promise((resolve) => requestAnimationFrame(resolve));\n\n // Adjust scroll position to account for sticky header/footer\n const container = dataGridRef.current;\n const targetRow = rowMapRef.current.get(safeRowIndex);\n\n if (container && targetRow) {\n const containerRect = container.getBoundingClientRect();\n const headerHeight =\n headerRef.current?.getBoundingClientRect().height ?? 0;\n const footerHeight =\n footerRef.current?.getBoundingClientRect().height ?? 0;\n\n const viewportTop =\n containerRect.top + headerHeight + VIEWPORT_OFFSET;\n const viewportBottom =\n containerRect.bottom - footerHeight - VIEWPORT_OFFSET;\n\n const rowRect = targetRow.getBoundingClientRect();\n const isFullyVisible =\n rowRect.top >= viewportTop && rowRect.bottom <= viewportBottom;\n\n if (!isFullyVisible) {\n if (rowRect.top < viewportTop) {\n // Row is partially hidden by header - scroll up\n container.scrollTop -= viewportTop - rowRect.top;\n } else if (rowRect.bottom > viewportBottom) {\n // Row is partially hidden by footer - scroll down\n container.scrollTop += rowRect.bottom - viewportBottom;\n }\n }\n }\n\n store.batch(() => {\n store.setState(\"focusedCell\", {\n rowIndex: safeRowIndex,\n columnId: targetColumnId,\n });\n store.setState(\"editingCell\", null);\n });\n\n const cellKey = getCellKey(safeRowIndex, targetColumnId);\n const cellElement = cellMapRef.current.get(cellKey);\n\n if (cellElement) {\n cellElement.focus();\n releaseFocusGuard();\n } else if (retryCount > 0) {\n await new Promise((resolve) => requestAnimationFrame(resolve));\n await onScrollAndFocus(retryCount - 1);\n } else {\n dataGridRef.current?.focus();\n releaseFocusGuard();\n }\n }\n\n await onScrollAndFocus(SCROLL_SYNC_RETRY_COUNT);\n },\n [rowVirtualizer, propsRef, store, releaseFocusGuard],\n );\n\n const onRowAdd = React.useCallback(\n async (event?: React.MouseEvent) => {\n if (propsRef.current.readOnly || !propsRef.current.onRowAdd) return;\n\n const initialRowCount = propsRef.current.data.length;\n\n let result: Partial | null;\n try {\n result = await propsRef.current.onRowAdd(event);\n } catch {\n // Callback threw an error, don't proceed with scroll/focus\n return;\n }\n\n if (result === null || event?.defaultPrevented) return;\n\n onSelectionClear();\n\n // Trust the returned rowIndex from the callback\n // onScrollToRow will handle retries if the row isn't rendered yet\n const targetRowIndex = result.rowIndex ?? initialRowCount;\n const targetColumnId = result.columnId;\n\n onScrollToRow({\n rowIndex: targetRowIndex,\n columnId: targetColumnId,\n });\n },\n [propsRef, onScrollToRow, onSelectionClear],\n );\n\n const onDataGridKeyDown = React.useCallback(\n (event: KeyboardEvent) => {\n const currentState = store.getState();\n const { key, ctrlKey, metaKey, shiftKey, altKey } = event;\n const isCtrlPressed = ctrlKey || metaKey;\n\n if (\n propsRef.current.enableSearch &&\n isCtrlPressed &&\n !shiftKey &&\n key === SEARCH_SHORTCUT_KEY\n ) {\n event.preventDefault();\n onSearchOpenChange(true);\n return;\n }\n\n if (\n propsRef.current.enableSearch &&\n currentState.searchOpen &&\n !currentState.editingCell\n ) {\n if (key === \"Enter\") {\n event.preventDefault();\n if (shiftKey) {\n onNavigateToPrevMatch();\n } else {\n onNavigateToNextMatch();\n }\n return;\n }\n if (key === \"Escape\") {\n event.preventDefault();\n onSearchOpenChange(false);\n return;\n }\n return;\n }\n\n // Cell editing keyboard events (Enter, Tab, Escape) are handled by the cell variants\n // to ensure proper value commitment before navigation\n if (currentState.editingCell) return;\n\n if (\n isCtrlPressed &&\n (key === \"Backspace\" || key === \"Delete\") &&\n !propsRef.current.readOnly &&\n propsRef.current.onRowsDelete\n ) {\n const rowIndices = new Set();\n\n const selectedRowIds = Object.keys(currentState.rowSelection);\n if (selectedRowIds.length > 0) {\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows ?? [];\n for (const row of rows) {\n if (currentState.rowSelection[row.id]) {\n rowIndices.add(row.index);\n }\n }\n } else if (currentState.selectionState.selectedCells.size > 0) {\n for (const cellKey of currentState.selectionState.selectedCells) {\n const { rowIndex } = parseCellKey(cellKey);\n rowIndices.add(rowIndex);\n }\n } else if (currentState.focusedCell) {\n rowIndices.add(currentState.focusedCell.rowIndex);\n }\n\n if (rowIndices.size > 0) {\n event.preventDefault();\n onRowsDelete(Array.from(rowIndices));\n }\n return;\n }\n\n if (!currentState.focusedCell) return;\n\n let direction: NavigationDirection | null = null;\n\n if (isCtrlPressed && !shiftKey && key === \"a\") {\n event.preventDefault();\n selectAll();\n return;\n }\n\n if (isCtrlPressed && !shiftKey && key === \"c\") {\n event.preventDefault();\n onCellsCopy();\n return;\n }\n\n if (\n isCtrlPressed &&\n !shiftKey &&\n key === \"x\" &&\n !propsRef.current.readOnly\n ) {\n event.preventDefault();\n onCellsCut();\n return;\n }\n\n if (\n propsRef.current.enablePaste &&\n isCtrlPressed &&\n !shiftKey &&\n key === \"v\" &&\n !propsRef.current.readOnly\n ) {\n event.preventDefault();\n onCellsPaste();\n return;\n }\n\n if (\n (key === \"Delete\" || key === \"Backspace\") &&\n !isCtrlPressed &&\n !propsRef.current.readOnly\n ) {\n const cellsToClear =\n currentState.selectionState.selectedCells.size > 0\n ? Array.from(currentState.selectionState.selectedCells)\n : currentState.focusedCell\n ? [\n getCellKey(\n currentState.focusedCell.rowIndex,\n currentState.focusedCell.columnId,\n ),\n ]\n : [];\n\n if (cellsToClear.length > 0) {\n event.preventDefault();\n\n const updates: Array<{\n rowIndex: number;\n columnId: string;\n value: unknown;\n }> = [];\n\n const currentTable = tableRef.current;\n const tableColumns = currentTable?.getAllColumns() ?? [];\n\n for (const cellKey of cellsToClear) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n\n const column = tableColumns.find((c) => c.id === columnId);\n const cellVariant = column?.columnDef?.meta?.cell?.variant;\n\n let emptyValue: unknown = \"\";\n if (cellVariant === \"multi-select\" || cellVariant === \"file\") {\n emptyValue = [];\n } else if (cellVariant === \"number\" || cellVariant === \"date\") {\n emptyValue = null;\n } else if (cellVariant === \"checkbox\") {\n emptyValue = false;\n }\n\n updates.push({ rowIndex, columnId, value: emptyValue });\n }\n\n onDataUpdate(updates);\n\n if (currentState.selectionState.selectedCells.size > 0) {\n onSelectionClear();\n }\n\n if (currentState.cutCells.size > 0) {\n store.setState(\"cutCells\", new Set());\n }\n }\n return;\n }\n\n if (\n key === \"Enter\" &&\n shiftKey &&\n !propsRef.current.readOnly &&\n propsRef.current.onRowAdd\n ) {\n event.preventDefault();\n const initialRowCount = propsRef.current.data.length;\n const currentColumnId = currentState.focusedCell.columnId;\n\n Promise.resolve(propsRef.current.onRowAdd())\n .then(async (result) => {\n if (result === null) return;\n\n onSelectionClear();\n\n const targetRowIndex = result.rowIndex ?? initialRowCount;\n const targetColumnId = result.columnId ?? currentColumnId;\n\n onScrollToRow({\n rowIndex: targetRowIndex,\n columnId: targetColumnId,\n });\n })\n .catch(() => {\n // Callback threw an error, don't proceed with scroll/focus\n });\n return;\n }\n\n switch (key) {\n case \"ArrowUp\":\n if (altKey && !isCtrlPressed && !shiftKey) {\n direction = \"pageup\";\n } else if (isCtrlPressed && shiftKey) {\n const selectionEdge =\n currentState.selectionState.selectionRange?.end ||\n currentState.focusedCell;\n const currentColIndex = navigableColumnIds.indexOf(\n selectionEdge.columnId,\n );\n const selectionStart =\n currentState.selectionState.selectionRange?.start ||\n currentState.focusedCell;\n\n selectRange(selectionStart, {\n rowIndex: 0,\n columnId:\n navigableColumnIds[currentColIndex] ?? selectionEdge.columnId,\n });\n\n const rowVirtualizer = rowVirtualizerRef.current;\n if (rowVirtualizer) {\n rowVirtualizer.scrollToIndex(0, { align: \"start\" });\n }\n\n restoreFocus(dataGridRef.current);\n\n event.preventDefault();\n return;\n } else if (isCtrlPressed && !shiftKey) {\n direction = \"ctrl+up\";\n } else {\n direction = \"up\";\n }\n break;\n case \"ArrowDown\":\n if (altKey && !isCtrlPressed && !shiftKey) {\n direction = \"pagedown\";\n } else if (isCtrlPressed && shiftKey) {\n const rowCount =\n tableRef.current?.getRowModel().rows.length ||\n propsRef.current.data.length;\n const selectionEdge =\n currentState.selectionState.selectionRange?.end ||\n currentState.focusedCell;\n const currentColIndex = navigableColumnIds.indexOf(\n selectionEdge.columnId,\n );\n const selectionStart =\n currentState.selectionState.selectionRange?.start ||\n currentState.focusedCell;\n\n selectRange(selectionStart, {\n rowIndex: Math.max(0, rowCount - 1),\n columnId:\n navigableColumnIds[currentColIndex] ?? selectionEdge.columnId,\n });\n\n const rowVirtualizer = rowVirtualizerRef.current;\n if (rowVirtualizer) {\n rowVirtualizer.scrollToIndex(Math.max(0, rowCount - 1), {\n align: \"end\",\n });\n }\n\n restoreFocus(dataGridRef.current);\n\n event.preventDefault();\n return;\n } else if (isCtrlPressed && !shiftKey) {\n direction = \"ctrl+down\";\n } else {\n direction = \"down\";\n }\n break;\n case \"ArrowLeft\":\n if (isCtrlPressed && shiftKey) {\n const selectionEdge =\n currentState.selectionState.selectionRange?.end ||\n currentState.focusedCell;\n const selectionStart =\n currentState.selectionState.selectionRange?.start ||\n currentState.focusedCell;\n const targetColumnId =\n dir === \"rtl\"\n ? navigableColumnIds[navigableColumnIds.length - 1]\n : navigableColumnIds[0];\n\n if (targetColumnId) {\n selectRange(selectionStart, {\n rowIndex: selectionEdge.rowIndex,\n columnId: targetColumnId,\n });\n\n const container = dataGridRef.current;\n const cellKey = getCellKey(\n selectionEdge.rowIndex,\n targetColumnId,\n );\n const targetCell = cellMapRef.current.get(cellKey);\n if (container && targetCell) {\n scrollCellIntoView({\n container,\n targetCell,\n tableRef,\n viewportOffset: VIEWPORT_OFFSET,\n direction: \"home\",\n isRtl: dir === \"rtl\",\n });\n }\n\n restoreFocus(container);\n }\n event.preventDefault();\n return;\n } else if (isCtrlPressed && !shiftKey) {\n direction = \"home\";\n } else {\n direction = \"left\";\n }\n break;\n case \"ArrowRight\":\n if (isCtrlPressed && shiftKey) {\n const selectionEdge =\n currentState.selectionState.selectionRange?.end ||\n currentState.focusedCell;\n const selectionStart =\n currentState.selectionState.selectionRange?.start ||\n currentState.focusedCell;\n const targetColumnId =\n dir === \"rtl\"\n ? navigableColumnIds[0]\n : navigableColumnIds[navigableColumnIds.length - 1];\n\n if (targetColumnId) {\n selectRange(selectionStart, {\n rowIndex: selectionEdge.rowIndex,\n columnId: targetColumnId,\n });\n\n const container = dataGridRef.current;\n const cellKey = getCellKey(\n selectionEdge.rowIndex,\n targetColumnId,\n );\n const targetCell = cellMapRef.current.get(cellKey);\n if (container && targetCell) {\n scrollCellIntoView({\n container,\n targetCell,\n tableRef,\n viewportOffset: VIEWPORT_OFFSET,\n direction: \"end\",\n isRtl: dir === \"rtl\",\n });\n }\n\n restoreFocus(container);\n }\n event.preventDefault();\n return;\n } else if (isCtrlPressed && !shiftKey) {\n direction = \"end\";\n } else {\n direction = \"right\";\n }\n break;\n case \"Home\":\n direction = isCtrlPressed ? \"ctrl+home\" : \"home\";\n break;\n case \"End\":\n direction = isCtrlPressed ? \"ctrl+end\" : \"end\";\n break;\n case \"PageUp\":\n direction = altKey ? \"pageleft\" : \"pageup\";\n break;\n case \"PageDown\":\n direction = altKey ? \"pageright\" : \"pagedown\";\n break;\n case \"Escape\":\n event.preventDefault();\n if (\n currentState.selectionState.selectedCells.size > 0 ||\n Object.keys(currentState.rowSelection).length > 0\n ) {\n onSelectionClear();\n } else {\n blurCell();\n }\n return;\n case \"Tab\":\n event.preventDefault();\n if (dir === \"rtl\") {\n direction = event.shiftKey ? \"right\" : \"left\";\n } else {\n direction = event.shiftKey ? \"left\" : \"right\";\n }\n break;\n }\n\n if (direction) {\n event.preventDefault();\n\n if (shiftKey && key !== \"Tab\" && currentState.focusedCell) {\n const selectionEdge =\n currentState.selectionState.selectionRange?.end ||\n currentState.focusedCell;\n\n const currentColIndex = navigableColumnIds.indexOf(\n selectionEdge.columnId,\n );\n let newRowIndex = selectionEdge.rowIndex;\n let newColumnId = selectionEdge.columnId;\n\n const isRtl = dir === \"rtl\";\n\n const rowCount =\n tableRef.current?.getRowModel().rows.length ||\n propsRef.current.data.length;\n\n switch (direction) {\n case \"up\":\n newRowIndex = Math.max(0, selectionEdge.rowIndex - 1);\n break;\n case \"down\":\n newRowIndex = Math.min(rowCount - 1, selectionEdge.rowIndex + 1);\n break;\n case \"left\":\n if (isRtl) {\n if (currentColIndex < navigableColumnIds.length - 1) {\n const nextColumnId = navigableColumnIds[currentColIndex + 1];\n if (nextColumnId) newColumnId = nextColumnId;\n }\n } else {\n if (currentColIndex > 0) {\n const prevColumnId = navigableColumnIds[currentColIndex - 1];\n if (prevColumnId) newColumnId = prevColumnId;\n }\n }\n break;\n case \"right\":\n if (isRtl) {\n if (currentColIndex > 0) {\n const prevColumnId = navigableColumnIds[currentColIndex - 1];\n if (prevColumnId) newColumnId = prevColumnId;\n }\n } else {\n if (currentColIndex < navigableColumnIds.length - 1) {\n const nextColumnId = navigableColumnIds[currentColIndex + 1];\n if (nextColumnId) newColumnId = nextColumnId;\n }\n }\n break;\n case \"home\":\n if (navigableColumnIds.length > 0) {\n newColumnId = navigableColumnIds[0] ?? newColumnId;\n }\n break;\n case \"end\":\n if (navigableColumnIds.length > 0) {\n newColumnId =\n navigableColumnIds[navigableColumnIds.length - 1] ??\n newColumnId;\n }\n break;\n }\n\n const selectionStart =\n currentState.selectionState.selectionRange?.start ||\n currentState.focusedCell;\n\n selectRange(selectionStart, {\n rowIndex: newRowIndex,\n columnId: newColumnId,\n });\n\n const container = dataGridRef.current;\n const targetRow = rowMapRef.current.get(newRowIndex);\n const cellKey = getCellKey(newRowIndex, newColumnId);\n const targetCell = cellMapRef.current.get(cellKey);\n\n if (\n newRowIndex !== selectionEdge.rowIndex &&\n (direction === \"up\" || direction === \"down\")\n ) {\n if (container && targetRow) {\n const containerRect = container.getBoundingClientRect();\n const headerHeight =\n headerRef.current?.getBoundingClientRect().height ?? 0;\n const footerHeight =\n footerRef.current?.getBoundingClientRect().height ?? 0;\n\n const viewportTop =\n containerRect.top + headerHeight + VIEWPORT_OFFSET;\n const viewportBottom =\n containerRect.bottom - footerHeight - VIEWPORT_OFFSET;\n\n const rowRect = targetRow.getBoundingClientRect();\n const isFullyVisible =\n rowRect.top >= viewportTop && rowRect.bottom <= viewportBottom;\n\n if (!isFullyVisible) {\n const scrollNeeded =\n direction === \"down\"\n ? rowRect.bottom - viewportBottom\n : viewportTop - rowRect.top;\n\n if (direction === \"down\") {\n container.scrollTop += scrollNeeded;\n } else {\n container.scrollTop -= scrollNeeded;\n }\n\n restoreFocus(container);\n }\n } else {\n const rowVirtualizer = rowVirtualizerRef.current;\n if (rowVirtualizer) {\n const align = direction === \"up\" ? \"start\" : \"end\";\n rowVirtualizer.scrollToIndex(newRowIndex, { align });\n\n restoreFocus(container);\n }\n }\n }\n\n if (\n newColumnId !== selectionEdge.columnId &&\n (direction === \"left\" ||\n direction === \"right\" ||\n direction === \"home\" ||\n direction === \"end\")\n ) {\n if (container && targetCell) {\n scrollCellIntoView({\n container,\n targetCell,\n tableRef,\n viewportOffset: VIEWPORT_OFFSET,\n direction,\n isRtl,\n });\n }\n }\n } else {\n if (currentState.selectionState.selectedCells.size > 0) {\n onSelectionClear();\n }\n navigateCell(direction);\n }\n }\n },\n [\n dir,\n store,\n propsRef,\n blurCell,\n navigateCell,\n selectAll,\n onCellsCopy,\n onCellsCut,\n onCellsPaste,\n onDataUpdate,\n onSelectionClear,\n navigableColumnIds,\n selectRange,\n onSearchOpenChange,\n onNavigateToNextMatch,\n onNavigateToPrevMatch,\n onRowsDelete,\n restoreFocus,\n onScrollToRow,\n ],\n );\n\n const searchState = React.useMemo(() => {\n if (!propsRef.current.enableSearch) return undefined;\n\n return {\n searchMatches,\n matchIndex,\n searchOpen,\n onSearchOpenChange,\n searchQuery,\n onSearchQueryChange,\n onSearch,\n onNavigateToNextMatch,\n onNavigateToPrevMatch,\n };\n }, [\n propsRef,\n searchMatches,\n matchIndex,\n searchOpen,\n onSearchOpenChange,\n searchQuery,\n onSearchQueryChange,\n onSearch,\n onNavigateToNextMatch,\n onNavigateToPrevMatch,\n ]);\n\n React.useEffect(() => {\n const dataGridElement = dataGridRef.current;\n if (!dataGridElement) return;\n\n dataGridElement.addEventListener(\"keydown\", onDataGridKeyDown);\n return () => {\n dataGridElement.removeEventListener(\"keydown\", onDataGridKeyDown);\n };\n }, [onDataGridKeyDown]);\n\n React.useEffect(() => {\n function onGlobalKeyDown(event: KeyboardEvent) {\n const dataGridElement = dataGridRef.current;\n if (!dataGridElement) return;\n\n const target = event.target;\n if (!(target instanceof HTMLElement)) return;\n\n const { key, ctrlKey, metaKey, shiftKey } = event;\n const isCommandPressed = ctrlKey || metaKey;\n\n if (\n propsRef.current.enableSearch &&\n isCommandPressed &&\n !shiftKey &&\n key === SEARCH_SHORTCUT_KEY\n ) {\n const isInInput =\n target.tagName === \"INPUT\" || target.tagName === \"TEXTAREA\";\n const isInDataGrid = dataGridElement.contains(target);\n const isInSearchInput = target.closest('[role=\"search\"]') !== null;\n\n if (isInDataGrid || isInSearchInput || !isInInput) {\n event.preventDefault();\n event.stopPropagation();\n\n const nextSearchOpen = !store.getState().searchOpen;\n onSearchOpenChange(nextSearchOpen);\n\n if (nextSearchOpen && !isInDataGrid && !isInSearchInput) {\n requestAnimationFrame(() => {\n dataGridElement.focus();\n });\n }\n return;\n }\n }\n\n const isInDataGrid = dataGridElement.contains(target);\n if (!isInDataGrid) return;\n\n if (key === \"Escape\") {\n const currentState = store.getState();\n const hasSelections =\n currentState.selectionState.selectedCells.size > 0 ||\n Object.keys(currentState.rowSelection).length > 0;\n\n if (hasSelections) {\n event.preventDefault();\n event.stopPropagation();\n onSelectionClear();\n }\n }\n }\n\n window.addEventListener(\"keydown\", onGlobalKeyDown, true);\n return () => {\n window.removeEventListener(\"keydown\", onGlobalKeyDown, true);\n };\n }, [propsRef, onSearchOpenChange, store, onSelectionClear]);\n\n React.useEffect(() => {\n const currentState = store.getState();\n const autoFocus = propsRef.current.autoFocus;\n\n if (\n autoFocus &&\n data.length > 0 &&\n columns.length > 0 &&\n !currentState.focusedCell\n ) {\n if (navigableColumnIds.length > 0) {\n const rafId = requestAnimationFrame(() => {\n if (typeof autoFocus === \"object\") {\n const { rowIndex, columnId } = autoFocus;\n if (columnId) {\n focusCell(rowIndex ?? 0, columnId);\n }\n return;\n }\n\n const firstColumnId = navigableColumnIds[0];\n if (firstColumnId) {\n focusCell(0, firstColumnId);\n }\n });\n return () => cancelAnimationFrame(rafId);\n }\n }\n }, [store, propsRef, data, columns, navigableColumnIds, focusCell]);\n\n // Restore focus to container when virtualized cells are unmounted\n React.useEffect(() => {\n const container = dataGridRef.current;\n if (!container) return;\n\n function onFocusOut(event: FocusEvent) {\n if (focusGuardRef.current) return;\n\n const currentContainer = dataGridRef.current;\n if (!currentContainer) return;\n\n const currentState = store.getState();\n\n if (!currentState.focusedCell || currentState.editingCell) return;\n\n const relatedTarget = event.relatedTarget;\n\n const isFocusMovingOutsideGrid =\n !relatedTarget || !currentContainer.contains(relatedTarget as Node);\n\n const isFocusMovingToPopover = getIsInPopover(relatedTarget);\n\n if (isFocusMovingOutsideGrid && !isFocusMovingToPopover) {\n const { rowIndex, columnId } = currentState.focusedCell;\n const cellKey = getCellKey(rowIndex, columnId);\n const cellElement = cellMapRef.current.get(cellKey);\n\n requestAnimationFrame(() => {\n if (focusGuardRef.current) return;\n\n if (cellElement && document.body.contains(cellElement)) {\n cellElement.focus();\n } else {\n currentContainer.focus();\n }\n });\n }\n }\n\n container.addEventListener(\"focusout\", onFocusOut);\n\n return () => {\n container.removeEventListener(\"focusout\", onFocusOut);\n };\n }, [store]);\n\n React.useEffect(() => {\n function onOutsideClick(event: MouseEvent) {\n if (event.button === 2) {\n return;\n }\n\n if (\n dataGridRef.current &&\n !dataGridRef.current.contains(event.target as Node)\n ) {\n const elements = document.elementsFromPoint(\n event.clientX,\n event.clientY,\n );\n\n // Compensate for event.target bubbling up\n const isInsidePopover = elements.some((element) =>\n getIsInPopover(element),\n );\n\n if (!isInsidePopover) {\n blurCell();\n const currentState = store.getState();\n if (\n currentState.selectionState.selectedCells.size > 0 ||\n Object.keys(currentState.rowSelection).length > 0\n ) {\n onSelectionClear();\n }\n }\n }\n }\n\n document.addEventListener(\"mousedown\", onOutsideClick);\n return () => {\n document.removeEventListener(\"mousedown\", onOutsideClick);\n };\n }, [store, blurCell, onSelectionClear]);\n\n React.useEffect(() => {\n function onSelectStart(event: Event) {\n event.preventDefault();\n }\n\n function onContextMenu(event: Event) {\n event.preventDefault();\n }\n\n function onCleanup() {\n document.removeEventListener(\"selectstart\", onSelectStart);\n document.removeEventListener(\"contextmenu\", onContextMenu);\n document.body.style.userSelect = \"\";\n }\n\n const onUnsubscribe = store.subscribe(() => {\n const currentState = store.getState();\n if (currentState.selectionState.isSelecting) {\n document.addEventListener(\"selectstart\", onSelectStart);\n document.addEventListener(\"contextmenu\", onContextMenu);\n document.body.style.userSelect = \"none\";\n } else {\n onCleanup();\n }\n });\n\n return () => {\n onCleanup();\n onUnsubscribe();\n };\n }, [store]);\n\n useIsomorphicLayoutEffect(() => {\n const rafId = requestAnimationFrame(() => {\n rowVirtualizer.measure();\n });\n return () => cancelAnimationFrame(rafId);\n }, [\n rowHeight,\n table.getState().columnFilters,\n table.getState().columnOrder,\n table.getState().columnPinning,\n table.getState().columnSizing,\n table.getState().columnVisibility,\n table.getState().expanded,\n table.getState().globalFilter,\n table.getState().grouping,\n table.getState().rowSelection,\n table.getState().sorting,\n ]);\n\n // Calculate virtual values outside of child render to avoid flushSync issues\n const virtualTotalSize = rowVirtualizer.getTotalSize();\n const virtualItems = rowVirtualizer.getVirtualItems();\n const measureElement = rowVirtualizer.measureElement;\n\n return React.useMemo(\n () => ({\n dataGridRef,\n headerRef,\n rowMapRef,\n footerRef,\n dir,\n table,\n tableMeta,\n virtualTotalSize,\n virtualItems,\n measureElement,\n columns,\n columnSizeVars,\n searchState,\n searchMatchesByRow,\n activeSearchMatch,\n cellSelectionMap,\n focusedCell,\n editingCell,\n rowHeight,\n contextMenu,\n pasteDialog,\n onRowAdd: propsRef.current.onRowAdd ? onRowAdd : undefined,\n adjustLayout,\n }),\n [\n propsRef,\n dir,\n table,\n tableMeta,\n virtualTotalSize,\n virtualItems,\n measureElement,\n columns,\n columnSizeVars,\n searchState,\n searchMatchesByRow,\n activeSearchMatch,\n cellSelectionMap,\n focusedCell,\n editingCell,\n rowHeight,\n contextMenu,\n pasteDialog,\n onRowAdd,\n adjustLayout,\n ],\n );\n}\n\nexport {\n //\n type UseDataGridProps,\n useDataGrid,\n};\n", "type": "registry:hook", "target": "hooks/use-data-grid.ts" }, { "path": "src/system/hooks/use-as-ref.ts", "content": "import * as React from \"react\";\n\nimport { useIsomorphicLayoutEffect } from \"./use-isomorphic-layout-effect\";\n\nfunction useAsRef(props: T) {\n const ref = React.useRef(props);\n\n useIsomorphicLayoutEffect(() => {\n ref.current = props;\n });\n\n return ref;\n}\n\nexport { useAsRef };\n\n", "type": "registry:hook", "target": "hooks/use-as-ref.ts" }, { "path": "src/system/hooks/use-badge-overflow.ts", "content": "import * as React from \"react\";\n\nconst badgeWidthCache = new Map();\n\nconst DEFAULT_CONTAINER_PADDING = 16; // px-2 = 8px * 2\nconst DEFAULT_BADGE_GAP = 4; // gap-1 = 4px\nconst DEFAULT_OVERFLOW_BADGE_WIDTH = 40; // Approximate width of \"+N\" badge\n\ninterface MeasureBadgeWidthProps {\n label: string;\n cacheKey: string;\n iconSize?: number;\n maxWidth?: number;\n className?: string;\n}\n\nfunction measureBadgeWidth({\n label,\n cacheKey,\n iconSize,\n maxWidth,\n className,\n}: MeasureBadgeWidthProps): number {\n const cached = badgeWidthCache.get(cacheKey);\n if (cached !== undefined) {\n return cached;\n }\n\n const measureEl = document.createElement(\"div\");\n measureEl.className = `inline-flex items-center rounded-control border px-1.5 text-label font-semibold h-5 gap-1 shrink-0 absolute invisible pointer-events-none ${\n className ?? \"\"\n }`;\n measureEl.style.whiteSpace = \"nowrap\";\n\n if (iconSize) {\n const icon = document.createElement(\"span\");\n icon.className = \"shrink-0\";\n icon.style.width = `${iconSize}px`;\n icon.style.height = `${iconSize}px`;\n measureEl.appendChild(icon);\n }\n\n if (maxWidth) {\n const text = document.createElement(\"span\");\n text.className = \"truncate\";\n text.style.maxWidth = `${maxWidth}px`;\n text.textContent = label;\n measureEl.appendChild(text);\n } else {\n measureEl.textContent = label;\n }\n\n document.body.appendChild(measureEl);\n const width = measureEl.offsetWidth;\n document.body.removeChild(measureEl);\n\n badgeWidthCache.set(cacheKey, width);\n return width;\n}\n\ninterface UseBadgeOverflowProps {\n items: T[];\n getLabel: (item: T) => string;\n containerRef: React.RefObject;\n lineCount: number;\n cacheKeyPrefix?: string;\n iconSize?: number;\n maxWidth?: number;\n className?: string;\n containerPadding?: number;\n badgeGap?: number;\n overflowBadgeWidth?: number;\n}\n\ninterface UseBadgeOverflowReturn {\n visibleItems: T[];\n hiddenCount: number;\n containerWidth: number;\n}\n\nexport function useBadgeOverflow({\n items,\n getLabel,\n containerRef,\n lineCount,\n cacheKeyPrefix = \"\",\n containerPadding = DEFAULT_CONTAINER_PADDING,\n badgeGap = DEFAULT_BADGE_GAP,\n overflowBadgeWidth = DEFAULT_OVERFLOW_BADGE_WIDTH,\n iconSize,\n maxWidth,\n className,\n}: UseBadgeOverflowProps): UseBadgeOverflowReturn {\n const [containerWidth, setContainerWidth] = React.useState(0);\n\n React.useEffect(() => {\n if (!containerRef.current) return;\n\n function measureWidth() {\n if (containerRef.current) {\n const width = containerRef.current.clientWidth - containerPadding;\n setContainerWidth(width);\n }\n }\n\n measureWidth();\n\n const resizeObserver = new ResizeObserver(measureWidth);\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, [containerRef, containerPadding]);\n\n const result = React.useMemo(() => {\n if (!containerWidth || items.length === 0) {\n return { visibleItems: items, hiddenCount: 0, containerWidth };\n }\n\n let currentLineWidth = 0;\n let currentLine = 1;\n const visible: T[] = [];\n\n for (const item of items) {\n const label = getLabel(item);\n const cacheKey = cacheKeyPrefix ? `${cacheKeyPrefix}:${label}` : label;\n const badgeWidth = measureBadgeWidth({\n label,\n cacheKey,\n iconSize,\n maxWidth,\n className,\n });\n const widthWithGap = badgeWidth + badgeGap;\n\n if (currentLineWidth + widthWithGap <= containerWidth) {\n currentLineWidth += widthWithGap;\n visible.push(item);\n } else if (currentLine < lineCount) {\n currentLine++;\n currentLineWidth = widthWithGap;\n visible.push(item);\n } else {\n if (\n currentLineWidth + overflowBadgeWidth > containerWidth &&\n visible.length > 0\n ) {\n visible.pop();\n }\n\n break;\n }\n }\n\n return {\n visibleItems: visible,\n hiddenCount: Math.max(0, items.length - visible.length),\n containerWidth,\n };\n }, [\n items,\n getLabel,\n containerWidth,\n lineCount,\n cacheKeyPrefix,\n iconSize,\n maxWidth,\n className,\n badgeGap,\n overflowBadgeWidth,\n ]);\n\n return result;\n}\n\nexport function clearBadgeWidthCache(): void {\n badgeWidthCache.clear();\n}\n\n", "type": "registry:hook", "target": "hooks/use-badge-overflow.ts" }, { "path": "src/system/hooks/use-debounced-callback.ts", "content": "import * as React from \"react\";\n\nimport { useCallbackRef } from \"./use-callback-ref\";\n\nexport function useDebouncedCallback unknown>(\n callback: T,\n delay: number,\n) {\n const handleCallback = useCallbackRef(callback);\n const debounceTimerRef = React.useRef(0);\n React.useEffect(\n () => () => window.clearTimeout(debounceTimerRef.current),\n [],\n );\n\n const setValue = React.useCallback(\n (...args: Parameters) => {\n window.clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = window.setTimeout(\n () => handleCallback(...args),\n delay,\n );\n },\n [handleCallback, delay],\n );\n\n return setValue;\n}\n\n", "type": "registry:hook", "target": "hooks/use-debounced-callback.ts" }, { "path": "src/system/hooks/use-callback-ref.ts", "content": "import * as React from \"react\";\n\n/** Keeps a callback identity stable while always calling its latest version. */\nfunction useCallbackRef unknown>(\n callback: T | undefined\n): T {\n const callbackRef = React.useRef(callback);\n\n React.useEffect(() => {\n callbackRef.current = callback;\n });\n\n return React.useMemo(\n () => ((...args) => callbackRef.current?.(...args)) as T,\n []\n );\n}\n\nexport { useCallbackRef };\n", "type": "registry:hook", "target": "hooks/use-callback-ref.ts" }, { "path": "src/system/hooks/use-isomorphic-layout-effect.ts", "content": "import * as React from \"react\";\n\nconst useIsomorphicLayoutEffect =\n typeof window !== \"undefined\" ? React.useLayoutEffect : React.useEffect;\n\nexport { useIsomorphicLayoutEffect };\n\n", "type": "registry:hook", "target": "hooks/use-isomorphic-layout-effect.ts" }, { "path": "src/system/hooks/use-lazy-ref.ts", "content": "import * as React from \"react\";\n\nfunction useLazyRef(fn: () => T) {\n const ref = React.useRef(null);\n\n if (ref.current === null) {\n ref.current = fn();\n }\n\n return ref as React.RefObject;\n}\n\nexport { useLazyRef };\n\n", "type": "registry:hook", "target": "hooks/use-lazy-ref.ts" }, { "path": "src/system/data-grid.ts", "content": "import type { Column, Table } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\nimport { createIconSlot, type IconComponent } from \"./icon-context\";\nimport type {\n CellOpts,\n CellPosition,\n Direction,\n FileCellData,\n RowHeightValue,\n} from \"./data-grid-types\";\n\nconst BaselineIcon = createIconSlot(\"baseline\");\nconst CalendarIcon = createIconSlot(\"calendar\");\nconst CheckSquareIcon = createIconSlot(\"check-square\");\nconst File = createIconSlot(\"file\");\nconst FileArchive = createIconSlot(\"file-archive\");\nconst FileAudio = createIconSlot(\"file-audio\");\nconst FileIcon = createIconSlot(\"file\");\nconst FileImage = createIconSlot(\"file-image\");\nconst FileSpreadsheet = createIconSlot(\"file-spreadsheet\");\nconst FileText = createIconSlot(\"file-text\");\nconst FileVideo = createIconSlot(\"file-video\");\nconst HashIcon = createIconSlot(\"hash\");\nconst LinkIcon = createIconSlot(\"link\");\nconst ListChecksIcon = createIconSlot(\"list-checks\");\nconst ListIcon = createIconSlot(\"list\");\nconst Presentation = createIconSlot(\"presentation\");\nconst TypeIcon = createIconSlot(\"type\");\n\nexport function flexRender(\n Comp: ((props: TProps) => React.ReactNode) | string | undefined,\n props: TProps,\n): React.ReactNode {\n if (typeof Comp === \"string\") {\n return Comp;\n }\n return Comp?.(props);\n}\n\nexport function getIsFileCellData(item: unknown): item is FileCellData {\n return (\n !!item &&\n typeof item === \"object\" &&\n \"id\" in item &&\n \"name\" in item &&\n \"size\" in item &&\n \"type\" in item\n );\n}\n\nexport function matchSelectOption(\n value: string,\n options: { value: string; label: string }[],\n): string | undefined {\n return options.find(\n (o) =>\n o.value === value ||\n o.value.toLowerCase() === value.toLowerCase() ||\n o.label.toLowerCase() === value.toLowerCase(),\n )?.value;\n}\n\nexport function getCellKey(rowIndex: number, columnId: string) {\n return `${rowIndex}:${columnId}`;\n}\n\nexport function parseCellKey(cellKey: string): Required {\n const parts = cellKey.split(\":\");\n const rowIndexStr = parts[0];\n const columnId = parts[1];\n if (rowIndexStr && columnId) {\n const rowIndex = parseInt(rowIndexStr, 10);\n if (!Number.isNaN(rowIndex)) {\n return { rowIndex, columnId };\n }\n }\n return { rowIndex: 0, columnId: \"\" };\n}\n\nexport function getRowHeightValue(rowHeight: RowHeightValue): number {\n const rowHeightMap: Record = {\n short: 36,\n medium: 56,\n tall: 76,\n \"extra-tall\": 96,\n };\n\n return rowHeightMap[rowHeight];\n}\n\nexport function getLineCount(rowHeight: RowHeightValue): number {\n const lineCountMap: Record = {\n short: 1,\n medium: 2,\n tall: 3,\n \"extra-tall\": 4,\n };\n\n return lineCountMap[rowHeight];\n}\n\nexport function getColumnBorderVisibility(params: {\n column: Column;\n nextColumn?: Column;\n isLastColumn: boolean;\n}): {\n showEndBorder: boolean;\n showStartBorder: boolean;\n} {\n const { column, nextColumn, isLastColumn } = params;\n\n const isPinned = column.getIsPinned();\n const isFirstRightPinnedColumn =\n isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n const isLastRightPinnedColumn =\n isPinned === \"right\" && column.getIsLastColumn(\"right\");\n\n const nextIsPinned = nextColumn?.getIsPinned();\n const isBeforeRightPinned =\n nextIsPinned === \"right\" && nextColumn?.getIsFirstColumn(\"right\");\n\n const showEndBorder =\n !isBeforeRightPinned && (isLastColumn || !isLastRightPinnedColumn);\n\n const showStartBorder = isFirstRightPinnedColumn;\n\n return {\n showEndBorder,\n showStartBorder,\n };\n}\n\nexport function getColumnPinningStyle(params: {\n column: Column;\n withBorder?: boolean;\n dir?: Direction;\n}): React.CSSProperties {\n const { column, dir = \"ltr\", withBorder = false } = params;\n\n const isPinned = column.getIsPinned();\n const isLastLeftPinnedColumn =\n isPinned === \"left\" && column.getIsLastColumn(\"left\");\n const isFirstRightPinnedColumn =\n isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n\n const isRtl = dir === \"rtl\";\n\n const leftPosition =\n isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined;\n const rightPosition =\n isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined;\n\n return {\n boxShadow: withBorder\n ? isLastLeftPinnedColumn\n ? isRtl\n ? \"4px 0 4px -4px var(--border) inset\"\n : \"-4px 0 4px -4px var(--border) inset\"\n : isFirstRightPinnedColumn\n ? isRtl\n ? \"-4px 0 4px -4px var(--border) inset\"\n : \"4px 0 4px -4px var(--border) inset\"\n : undefined\n : undefined,\n left: isRtl ? rightPosition : leftPosition,\n right: isRtl ? leftPosition : rightPosition,\n opacity: 1,\n position: isPinned ? \"sticky\" : \"relative\",\n // Sticky columns need an opaque surface so scrolling cells cannot show\n // through them. Keep it aligned with the DataGrid's floating panel.\n background: \"var(--surface-floating)\",\n width: column.getSize(),\n zIndex: isPinned ? \"var(--layer-raised)\" : undefined,\n };\n}\n\nexport function getScrollDirection(\n direction: string,\n): \"left\" | \"right\" | \"home\" | \"end\" | undefined {\n if (\n direction === \"left\" ||\n direction === \"right\" ||\n direction === \"home\" ||\n direction === \"end\"\n ) {\n return direction as \"left\" | \"right\" | \"home\" | \"end\";\n }\n if (direction === \"pageleft\") return \"left\";\n if (direction === \"pageright\") return \"right\";\n return undefined;\n}\n\nexport function scrollCellIntoView(params: {\n container: HTMLDivElement;\n targetCell: HTMLDivElement;\n tableRef: React.RefObject | null>;\n viewportOffset: number;\n direction?: \"left\" | \"right\" | \"home\" | \"end\";\n isRtl: boolean;\n}): void {\n const { container, targetCell, tableRef, direction, viewportOffset, isRtl } =\n params;\n\n const containerRect = container.getBoundingClientRect();\n const cellRect = targetCell.getBoundingClientRect();\n\n const hasNegativeScroll = container.scrollLeft < 0;\n const isActuallyRtl = isRtl || hasNegativeScroll;\n\n const currentTable = tableRef.current;\n const leftPinnedColumns = currentTable?.getLeftVisibleLeafColumns() ?? [];\n const rightPinnedColumns = currentTable?.getRightVisibleLeafColumns() ?? [];\n\n const leftPinnedWidth = leftPinnedColumns.reduce(\n (sum, c) => sum + c.getSize(),\n 0,\n );\n const rightPinnedWidth = rightPinnedColumns.reduce(\n (sum, c) => sum + c.getSize(),\n 0,\n );\n\n const viewportLeft = isActuallyRtl\n ? containerRect.left + rightPinnedWidth + viewportOffset\n : containerRect.left + leftPinnedWidth + viewportOffset;\n const viewportRight = isActuallyRtl\n ? containerRect.right - leftPinnedWidth - viewportOffset\n : containerRect.right - rightPinnedWidth - viewportOffset;\n\n const isFullyVisible =\n cellRect.left >= viewportLeft && cellRect.right <= viewportRight;\n\n if (isFullyVisible) return;\n\n const isClippedLeft = cellRect.left < viewportLeft;\n const isClippedRight = cellRect.right > viewportRight;\n\n let scrollDelta = 0;\n\n if (!direction) {\n if (isClippedRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else if (isClippedLeft) {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n } else {\n const shouldScrollRight = isActuallyRtl\n ? direction === \"right\" || direction === \"home\"\n : direction === \"right\" || direction === \"end\";\n\n if (shouldScrollRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n }\n\n container.scrollLeft += scrollDelta;\n}\n\nexport function getIsInPopover(element: unknown): boolean {\n return (\n element instanceof Element &&\n (element.closest(\"[data-grid-cell-editor]\") ||\n element.closest(\"[data-grid-popover]\")) !== null\n );\n}\n\nexport function getColumnVariant(variant?: CellOpts[\"variant\"]): {\n icon: IconComponent;\n label: string;\n} | null {\n switch (variant) {\n case \"short-text\":\n return { label: \"Short text\", icon: BaselineIcon };\n case \"long-text\":\n return { label: \"Long text\", icon: TypeIcon };\n case \"number\":\n return { label: \"Number\", icon: HashIcon };\n case \"url\":\n return { label: \"URL\", icon: LinkIcon };\n case \"checkbox\":\n return { label: \"Checkbox\", icon: CheckSquareIcon };\n case \"select\":\n return { label: \"Select\", icon: ListIcon };\n case \"multi-select\":\n return { label: \"Multi-select\", icon: ListChecksIcon };\n case \"date\":\n return { label: \"Date\", icon: CalendarIcon };\n case \"file\":\n return { label: \"File\", icon: FileIcon };\n default:\n return null;\n }\n}\n\nexport function getUrlHref(urlString: string): string {\n if (!urlString || urlString.trim() === \"\") return \"\";\n\n const trimmed = urlString.trim();\n\n // Reject dangerous protocols (extra safety, though our http:// prefix would neutralize them)\n if (/^(javascript|data|vbscript|file):/i.test(trimmed)) {\n return \"\";\n }\n\n if (trimmed.startsWith(\"http://\") || trimmed.startsWith(\"https://\")) {\n return trimmed;\n }\n\n return `http://${trimmed}`;\n}\n\nexport function parseLocalDate(dateStr: unknown): Date | null {\n if (!dateStr) return null;\n if (dateStr instanceof Date) return dateStr;\n if (typeof dateStr !== \"string\") return null;\n const [year, month, day] = dateStr.split(\"-\").map(Number);\n if (!year || !month || !day) return null;\n const date = new Date(year, month - 1, day);\n // Verify date wasn't auto-corrected (e.g. Feb 30 -> Mar 1)\n if (\n date.getFullYear() !== year ||\n date.getMonth() !== month - 1 ||\n date.getDate() !== day\n ) {\n return null;\n }\n return date;\n}\n\nexport function formatDateToString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\nexport function formatDateForDisplay(dateStr: unknown): string {\n if (!dateStr) return \"\";\n const date = parseLocalDate(dateStr);\n if (!date) return typeof dateStr === \"string\" ? dateStr : \"\";\n return date.toLocaleDateString();\n}\n\nexport function formatFileSize(bytes: number): string {\n if (bytes <= 0 || !Number.isFinite(bytes)) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.min(\n sizes.length - 1,\n Math.floor(Math.log(bytes) / Math.log(k)),\n );\n return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;\n}\n\nexport function getFileIcon(\n type: string,\n): IconComponent {\n if (type.startsWith(\"image/\")) return FileImage;\n if (type.startsWith(\"video/\")) return FileVideo;\n if (type.startsWith(\"audio/\")) return FileAudio;\n if (type.includes(\"pdf\")) return FileText;\n if (type.includes(\"zip\") || type.includes(\"rar\")) return FileArchive;\n if (\n type.includes(\"word\") ||\n type.includes(\"document\") ||\n type.includes(\"doc\")\n )\n return FileText;\n if (type.includes(\"sheet\") || type.includes(\"excel\") || type.includes(\"xls\"))\n return FileSpreadsheet;\n if (\n type.includes(\"presentation\") ||\n type.includes(\"powerpoint\") ||\n type.includes(\"ppt\")\n )\n return Presentation;\n return File;\n}\n", "type": "registry:lib", "target": "lib/data-grid.ts" }, { "path": "src/system/data-grid-types.ts", "content": "import type { Cell, RowData, TableMeta } from \"@tanstack/react-table\";\nimport type { BadgeColor } from \"@/components/ui/badge\";\n\nexport type Direction = \"ltr\" | \"rtl\";\n\nexport type RowHeightValue = \"short\" | \"medium\" | \"tall\" | \"extra-tall\";\n\nexport interface CellSelectOption {\n label: string;\n value: string;\n /** Uses the same named color palette as the project Badge component. */\n color?: BadgeColor;\n icon?: React.FC>;\n count?: number;\n}\n\nexport type CellOpts =\n | {\n variant: \"short-text\";\n }\n | {\n variant: \"long-text\";\n }\n | {\n variant: \"number\";\n min?: number;\n max?: number;\n step?: number;\n }\n | {\n variant: \"select\";\n options: CellSelectOption[];\n }\n | {\n variant: \"multi-select\";\n options: CellSelectOption[];\n }\n | {\n variant: \"checkbox\";\n }\n | {\n variant: \"date\";\n }\n | {\n variant: \"url\";\n }\n | {\n variant: \"file\";\n maxFileSize?: number;\n maxFiles?: number;\n accept?: string;\n multiple?: boolean;\n };\n\nexport interface CellUpdate {\n rowIndex: number;\n columnId: string;\n value: unknown;\n}\n\ndeclare module \"@tanstack/react-table\" {\n // Generic parameter names must match TanStack's declarations for module merging.\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n interface ColumnMeta {\n label?: string;\n cell?: CellOpts;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n interface TableMeta {\n dataGridRef?: React.RefObject;\n cellMapRef?: React.RefObject>;\n focusedCell?: CellPosition | null;\n editingCell?: CellPosition | null;\n selectionState?: SelectionState;\n searchOpen?: boolean;\n getIsCellSelected?: (rowIndex: number, columnId: string) => boolean;\n getIsSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getIsActiveSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getVisualRowIndex?: (rowId: string) => number | undefined;\n rowHeight?: RowHeightValue;\n onRowHeightChange?: (value: RowHeightValue) => void;\n onRowSelect?: (\n rowIndex: number,\n checked: boolean,\n shiftKey: boolean,\n ) => void;\n onDataUpdate?: (params: CellUpdate | Array) => void;\n onRowsDelete?: (rowIndices: number[]) => void | Promise;\n onColumnClick?: (columnId: string) => void;\n onCellClick?: (\n rowIndex: number,\n columnId: string,\n event?: React.MouseEvent,\n ) => void;\n onCellDoubleClick?: (rowIndex: number, columnId: string) => void;\n onCellMouseDown?: (\n rowIndex: number,\n columnId: string,\n event: React.MouseEvent,\n ) => void;\n onCellMouseEnter?: (rowIndex: number, columnId: string) => void;\n onCellMouseUp?: () => void;\n onCellContextMenu?: (\n rowIndex: number,\n columnId: string,\n event: React.MouseEvent,\n ) => void;\n onCellEditingStart?: (rowIndex: number, columnId: string) => void;\n onCellEditingStop?: (opts?: {\n direction?: NavigationDirection;\n moveToNextRow?: boolean;\n }) => void;\n onCellsCopy?: () => void;\n onCellsCut?: () => void;\n onCellsPaste?: (expand?: boolean) => void;\n onSelectionClear?: () => void;\n onFilesUpload?: (params: {\n files: File[];\n rowIndex: number;\n columnId: string;\n }) => Promise;\n onFilesDelete?: (params: {\n fileIds: string[];\n rowIndex: number;\n columnId: string;\n }) => void | Promise;\n contextMenu?: ContextMenuState;\n onContextMenuOpenChange?: (open: boolean) => void;\n pasteDialog?: PasteDialogState;\n onPasteDialogOpenChange?: (open: boolean) => void;\n readOnly?: boolean;\n }\n}\n\nexport interface CellPosition {\n rowIndex: number;\n columnId: string;\n}\n\nexport interface CellRange {\n start: CellPosition;\n end: CellPosition;\n}\n\nexport interface SelectionState {\n selectedCells: Set;\n selectionRange: CellRange | null;\n isSelecting: boolean;\n}\n\nexport interface ContextMenuState {\n open: boolean;\n x: number;\n y: number;\n}\n\nexport interface PasteDialogState {\n open: boolean;\n rowsNeeded: number;\n clipboardText: string;\n}\n\nexport type NavigationDirection =\n | \"up\"\n | \"down\"\n | \"left\"\n | \"right\"\n | \"home\"\n | \"end\"\n | \"ctrl+up\"\n | \"ctrl+down\"\n | \"ctrl+home\"\n | \"ctrl+end\"\n | \"pageup\"\n | \"pagedown\"\n | \"pageleft\"\n | \"pageright\";\n\nexport interface SearchState {\n searchMatches: CellPosition[];\n matchIndex: number;\n searchOpen: boolean;\n onSearchOpenChange: (open: boolean) => void;\n searchQuery: string;\n onSearchQueryChange: (query: string) => void;\n onSearch: (query: string) => void;\n onNavigateToNextMatch: () => void;\n onNavigateToPrevMatch: () => void;\n}\n\nexport interface DataGridCellProps {\n cell: Cell;\n tableMeta: TableMeta;\n rowIndex: number;\n columnId: string;\n rowHeight: RowHeightValue;\n isEditing: boolean;\n isFocused: boolean;\n isSelected: boolean;\n isSearchMatch: boolean;\n isActiveSearchMatch: boolean;\n readOnly: boolean;\n}\n\nexport interface FileCellData {\n id: string;\n name: string;\n size: number;\n type: string;\n url?: string;\n}\n\nexport type TextFilterOperator =\n | \"contains\"\n | \"notContains\"\n | \"equals\"\n | \"notEquals\"\n | \"startsWith\"\n | \"endsWith\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type NumberFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"lessThan\"\n | \"lessThanOrEqual\"\n | \"greaterThan\"\n | \"greaterThanOrEqual\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type DateFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"before\"\n | \"after\"\n | \"onOrBefore\"\n | \"onOrAfter\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type SelectFilterOperator =\n | \"is\"\n | \"isNot\"\n | \"isAnyOf\"\n | \"isNoneOf\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type BooleanFilterOperator = \"isTrue\" | \"isFalse\";\n\nexport type FilterOperator =\n | TextFilterOperator\n | NumberFilterOperator\n | DateFilterOperator\n | SelectFilterOperator\n | BooleanFilterOperator;\n\nexport interface FilterValue {\n operator: FilterOperator;\n value?: string | number | string[];\n endValue?: string | number;\n}\n", "type": "registry:lib", "target": "lib/data-grid-types.ts" }, { "path": "src/system/compose-refs.ts", "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef = React.Ref | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef(ref: PossibleRef, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n }\n\n if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs(...refs: PossibleRef[]): React.RefCallback {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup === \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup === \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs(...refs: PossibleRef[]): React.RefCallback {\n // biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n\n\n", "type": "registry:lib", "target": "lib/compose-refs.ts" } ], "type": "registry:ui" }