{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"data-grid-table","type":"registry:ui","title":"Data Grid Table","description":"","dependencies":["@tanstack/react-table"],"registryDependencies":["checkbox","@neui/data-grid","spinner"],"files":[{"path":"data-grid-table.tsx","type":"registry:ui","content":"\"use client\"\n\"use no memo\"\n\nimport {\n Fragment,\n memo,\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n} from \"react\"\nimport type {\n CSSProperties,\n MouseEvent as ReactMouseEvent,\n ReactNode,\n TouchEvent as ReactTouchEvent,\n Ref,\n RefObject,\n} from \"react\"\nimport { useDataGrid } from \"@/components/neui/data-grid/data-grid\"\nimport { flexRender } from \"@tanstack/react-table\"\nimport type { Cell, Column, Header, Row, Table } from \"@tanstack/react-table\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport { Spinner } from \"@/components/ui/spinner\"\n\n// Static spacing lookups; called once per cell, so they stay plain string\n// picks instead of runtime variant machinery.\nconst headerCellSpacingVariants = ({ size }: { size?: \"dense\" | \"default\" }) =>\n size === \"dense\" ? \"px-2 h-8\" : \"px-3\"\n\nconst bodyCellSpacingVariants = ({ size }: { size?: \"dense\" | \"default\" }) =>\n size === \"dense\" ? \"px-2 py-1.5\" : \"px-3 py-2\"\n\nconst footerCellSpacingVariants = ({ size }: { size?: \"dense\" | \"default\" }) =>\n size === \"dense\" ? \"px-2 py-1.5\" : \"px-3 py-2\"\n\nfunction getPinningStyles(column: Column): CSSProperties {\n const isPinned = column.getIsPinned()\n\n return {\n // Logical offsets: TanStack's \"left\"/\"right\" buckets are start/end\n // semantics, so pinned columns stick to the correct edge in RTL too\n // (identical to left/right in LTR).\n insetInlineStart:\n isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined,\n insetInlineEnd:\n isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined,\n position: isPinned ? \"sticky\" : undefined,\n transform: isPinned ? \"translateZ(0)\" : undefined,\n contain: isPinned ? \"paint\" : undefined,\n width: column.getSize(),\n zIndex: isPinned ? 30 : undefined,\n backgroundClip: isPinned ? \"padding-box\" : undefined,\n }\n}\n\n// Shared indent contract for tree rows: DataGridTableRowExpand consumes it,\n// and fully custom cells can reuse it for depth alignment without the\n// built-in toggle.\nfunction getDataGridTreeIndentStyle(\n row: Row,\n indent: number = 20\n): CSSProperties {\n return {\n \"--data-grid-tree-padding\": `${row.depth * indent}px`,\n } as CSSProperties\n}\n\nfunction assignRef(ref: Ref | undefined, value: T | null) {\n if (!ref) return\n\n if (typeof ref === \"function\") {\n ref(value)\n return\n }\n\n ;(ref as { current: T | null }).current = value\n}\n\n/**\n * Nearest scroll-area viewport that belongs to THIS grid. A viewport outside\n * the grid's own container (e.g. a page-level ScrollArea) would make the\n * width measurement - and the virtualizer - bind the wrong box.\n */\nfunction getDataGridScrollAreaViewport(node: HTMLElement): HTMLElement | null {\n const scrollViewport = node.closest(\n '[data-slot=\"scroll-area-viewport\"]'\n ) as HTMLElement | null\n\n if (!scrollViewport) return null\n\n const gridContainer = node.closest('[data-slot=\"data-grid\"]')\n if (gridContainer && !gridContainer.contains(scrollViewport)) return null\n\n return scrollViewport\n}\n\ntype DataGridResizeStartEvent =\n | ReactMouseEvent\n | ReactTouchEvent\n\ntype DataGridResizeDocumentEvent = globalThis.MouseEvent | globalThis.TouchEvent\n\nfunction isDataGridTouchEvent(\n event: DataGridResizeStartEvent | DataGridResizeDocumentEvent\n): event is ReactTouchEvent | globalThis.TouchEvent {\n return \"touches\" in event\n}\n\ntype DataGridTouchListLike = {\n length: number\n item: (index: number) => { identifier: number; clientX: number } | null\n}\n\nfunction findTouchClientX(list: DataGridTouchListLike, identifier: number) {\n for (let i = 0; i < list.length; i++) {\n const touch = list.item(i)\n if (touch && touch.identifier === identifier) return touch.clientX\n }\n\n return undefined\n}\n\nfunction getDataGridResizeEventClientX(\n event: DataGridResizeStartEvent | DataGridResizeDocumentEvent,\n touchIdentifier?: number\n) {\n if (isDataGridTouchEvent(event)) {\n if (typeof touchIdentifier === \"number\") {\n return (\n findTouchClientX(event.touches, touchIdentifier) ??\n findTouchClientX(event.changedTouches, touchIdentifier)\n )\n }\n\n return event.touches[0]?.clientX ?? event.changedTouches[0]?.clientX\n }\n\n return event.clientX\n}\n\nfunction startDataGridColumnResizeOnEnd(\n event: DataGridResizeStartEvent,\n header: Header,\n table: Table\n): (() => void) | undefined {\n const column = table.getColumn(header.column.id)\n\n if (!column || !column.getCanResize()) return\n const isTouchSession = isDataGridTouchEvent(event)\n if (isTouchSession && event.touches.length > 1) return\n\n event.persist?.()\n\n const ownerDocument = event.currentTarget.ownerDocument\n const ownerWindow = ownerDocument.defaultView\n const previousBodyCursor = ownerDocument.body.style.cursor\n const previousDocumentCursor = ownerDocument.documentElement.style.cursor\n const startSize = header.getSize()\n // Track the initiating finger so a second touch cannot move or commit the\n // resize with the wrong clientX.\n const touchIdentifier = isTouchSession\n ? event.touches[0]?.identifier\n : undefined\n const dragStartClientX = getDataGridResizeEventClientX(event, touchIdentifier)\n const headerCell = event.currentTarget.closest(\"th\")\n const headerRect = headerCell?.getBoundingClientRect()\n const startOffset =\n headerRect &&\n Number.isFinite(\n table.options.columnResizeDirection === \"rtl\"\n ? headerRect.left\n : headerRect.right\n )\n ? table.options.columnResizeDirection === \"rtl\"\n ? headerRect.left\n : headerRect.right\n : dragStartClientX\n\n if (typeof dragStartClientX !== \"number\" || typeof startOffset !== \"number\") {\n return\n }\n\n ownerDocument.body.style.cursor = \"col-resize\"\n ownerDocument.documentElement.style.cursor = \"col-resize\"\n\n const columnSizingStart = header\n .getLeafHeaders()\n .map(\n (leafHeader) =>\n [leafHeader.column.id, leafHeader.column.getSize()] as [string, number]\n )\n const directionMultiplier =\n table.options.columnResizeDirection === \"rtl\" ? -1 : 1\n\n // Clamp the drag to the leaf columns' min/max sizes so the preview\n // indicator matches what the commit will produce (no overshoot followed by\n // a snap-back on release). columnDef always carries resolved defaults.\n let minDeltaPercentage = -0.999999\n let maxDeltaPercentage = Number.POSITIVE_INFINITY\n columnSizingStart.forEach(([columnId, headerSize]) => {\n if (headerSize <= 0) return\n\n const leafColumn = table.getColumn(columnId)\n const minSize = leafColumn?.columnDef.minSize\n const maxSize = leafColumn?.columnDef.maxSize\n\n if (typeof minSize === \"number\") {\n minDeltaPercentage = Math.max(\n minDeltaPercentage,\n minSize / headerSize - 1\n )\n }\n if (typeof maxSize === \"number\" && Number.isFinite(maxSize)) {\n maxDeltaPercentage = Math.min(\n maxDeltaPercentage,\n maxSize / headerSize - 1\n )\n }\n })\n\n let lastClientX = dragStartClientX\n let ended = false\n const stopListeners: Array<() => void> = []\n\n const updateOffset = (clientXPos?: number, commit = false) => {\n if (typeof clientXPos !== \"number\") return\n\n lastClientX = clientXPos\n\n const nextColumnSizing: Record = {}\n const deltaPercentage = Math.min(\n Math.max(\n ((clientXPos - dragStartClientX) * directionMultiplier) / startSize,\n minDeltaPercentage\n ),\n maxDeltaPercentage\n )\n const deltaOffset = deltaPercentage * startSize\n\n columnSizingStart.forEach(([columnId, headerSize]) => {\n nextColumnSizing[columnId] =\n Math.round(\n Math.max(headerSize + headerSize * deltaPercentage, 0) * 100\n ) / 100\n })\n\n table.setColumnSizingInfo((old) => ({\n ...old,\n startOffset,\n startSize,\n deltaOffset,\n deltaPercentage,\n columnSizingStart,\n isResizingColumn: column.id,\n }))\n\n if (commit) {\n table.setColumnSizing((old) => ({\n ...old,\n ...nextColumnSizing,\n }))\n }\n }\n\n // Single teardown path: commits at the given position, removes every\n // document/window listener, and restores cursors. Safe to call more than\n // once (blur + mouseup + unmount can race).\n const endResize = (clientXPos?: number) => {\n if (ended) return\n ended = true\n\n stopListeners.forEach((stop) => stop())\n updateOffset(clientXPos, true)\n table.setColumnSizingInfo((old) => ({\n ...old,\n isResizingColumn: false,\n startOffset: null,\n startSize: null,\n deltaOffset: null,\n deltaPercentage: null,\n columnSizingStart: [],\n }))\n ownerDocument.body.style.cursor = previousBodyCursor\n ownerDocument.documentElement.style.cursor = previousDocumentCursor\n }\n\n const mouseMoveHandler = (moveEvent: globalThis.MouseEvent) => {\n updateOffset(moveEvent.clientX)\n }\n const mouseUpHandler = (upEvent: globalThis.MouseEvent) => {\n endResize(upEvent.clientX)\n }\n const touchMoveHandler = (moveEvent: globalThis.TouchEvent) => {\n if (moveEvent.cancelable) {\n moveEvent.preventDefault()\n moveEvent.stopPropagation()\n }\n\n updateOffset(getDataGridResizeEventClientX(moveEvent, touchIdentifier))\n }\n const touchEndHandler = (endEvent: globalThis.TouchEvent) => {\n // Ignore other fingers lifting; only the initiating touch ends the drag.\n const clientXPos =\n typeof touchIdentifier === \"number\"\n ? findTouchClientX(endEvent.changedTouches, touchIdentifier)\n : getDataGridResizeEventClientX(endEvent)\n\n if (typeof clientXPos !== \"number\") return\n\n if (endEvent.cancelable) {\n endEvent.preventDefault()\n endEvent.stopPropagation()\n }\n\n endResize(clientXPos)\n }\n // System-interrupted gestures and window focus loss would otherwise leave\n // the session (and its document listeners) live with no pointer held.\n const touchCancelHandler = () => {\n endResize(lastClientX)\n }\n const windowBlurHandler = () => {\n endResize(lastClientX)\n }\n\n const passiveIfSupported = { passive: false } as const\n\n if (isTouchSession) {\n ownerDocument.addEventListener(\n \"touchmove\",\n touchMoveHandler,\n passiveIfSupported\n )\n ownerDocument.addEventListener(\n \"touchend\",\n touchEndHandler,\n passiveIfSupported\n )\n ownerDocument.addEventListener(\"touchcancel\", touchCancelHandler)\n stopListeners.push(() => {\n ownerDocument.removeEventListener(\"touchmove\", touchMoveHandler)\n ownerDocument.removeEventListener(\"touchend\", touchEndHandler)\n ownerDocument.removeEventListener(\"touchcancel\", touchCancelHandler)\n })\n } else {\n ownerDocument.addEventListener(\n \"mousemove\",\n mouseMoveHandler,\n passiveIfSupported\n )\n ownerDocument.addEventListener(\n \"mouseup\",\n mouseUpHandler,\n passiveIfSupported\n )\n stopListeners.push(() => {\n ownerDocument.removeEventListener(\"mousemove\", mouseMoveHandler)\n ownerDocument.removeEventListener(\"mouseup\", mouseUpHandler)\n })\n }\n\n if (ownerWindow) {\n ownerWindow.addEventListener(\"blur\", windowBlurHandler)\n stopListeners.push(() =>\n ownerWindow.removeEventListener(\"blur\", windowBlurHandler)\n )\n }\n\n table.setColumnSizingInfo((old) => ({\n ...old,\n startOffset,\n startSize,\n deltaOffset: 0,\n deltaPercentage: 0,\n columnSizingStart,\n isResizingColumn: column.id,\n }))\n\n return () => endResize(lastClientX)\n}\n\ntype DataGridTablePinnedBoundary = \"top\" | \"bottom\"\n\nfunction getDataGridTableRowSections(\n table: Table,\n rowsPinnable?: boolean\n) {\n if (!rowsPinnable) {\n return {\n topRows: [] as Row[],\n centerRows: table.getRowModel().rows as Row[],\n bottomRows: [] as Row[],\n }\n }\n\n return {\n topRows: table.getTopRows() as Row[],\n centerRows: table.getCenterRows() as Row[],\n bottomRows: table.getBottomRows() as Row[],\n }\n}\n\nfunction getDataGridTableResolvedRows(\n table: Table,\n rowsPinnable?: boolean\n) {\n const { topRows, centerRows, bottomRows } = getDataGridTableRowSections(\n table,\n rowsPinnable\n )\n const resolvedRows: Array<{\n row: Row\n pinnedBoundary?: DataGridTablePinnedBoundary\n }> = []\n\n topRows.forEach((row, index) => {\n resolvedRows.push({\n row,\n pinnedBoundary:\n index === topRows.length - 1 &&\n (centerRows.length > 0 || bottomRows.length > 0)\n ? \"top\"\n : undefined,\n })\n })\n\n centerRows.forEach((row) => {\n resolvedRows.push({ row })\n })\n\n bottomRows.forEach((row, index) => {\n resolvedRows.push({\n row,\n pinnedBoundary:\n index === 0 && (centerRows.length > 0 || topRows.length > 0)\n ? \"bottom\"\n : undefined,\n })\n })\n\n return resolvedRows\n}\n\nfunction getDataGridTableOrderedVisibleColumns(table: Table) {\n return [\n ...table.getLeftVisibleLeafColumns(),\n ...table.getCenterVisibleLeafColumns(),\n ...table.getRightVisibleLeafColumns(),\n ] as Column[]\n}\n\nfunction getDataGridTableOrderedVisibleCells(row: Row) {\n return [\n ...row.getLeftVisibleCells(),\n ...row.getCenterVisibleCells(),\n ...row.getRightVisibleCells(),\n ] as Cell[]\n}\n\nfunction getDataGridTableMergedHeaderGroups(table: Table) {\n const leftHeaderGroups = table.getLeftHeaderGroups()\n const centerHeaderGroups = table.getCenterHeaderGroups()\n const rightHeaderGroups = table.getRightHeaderGroups()\n const headerGroupCount = Math.max(\n leftHeaderGroups.length,\n centerHeaderGroups.length,\n rightHeaderGroups.length\n )\n\n return Array.from({ length: headerGroupCount }, (_, index) => {\n const leftGroup = leftHeaderGroups[index]\n const centerGroup = centerHeaderGroups[index]\n const rightGroup = rightHeaderGroups[index]\n\n return {\n id:\n [leftGroup?.id, centerGroup?.id, rightGroup?.id]\n .filter(Boolean)\n .join(\":\") || `header-group-${index}`,\n headers: [\n ...(leftGroup?.headers ?? []),\n ...(centerGroup?.headers ?? []),\n ...(rightGroup?.headers ?? []),\n ] as Header[],\n }\n })\n}\n\nfunction hasDataGridTableRightPinnedColumns(table: Table) {\n return (table.getState().columnPinning.right?.length ?? 0) > 0\n}\n\nfunction DataGridTableFillCol() {\n const { props } = useDataGrid()\n\n if (!props.tableLayout?.columnsResizable) return null\n\n return (\n \n )\n}\n\nfunction DataGridTableFillHeadCell() {\n const { props } = useDataGrid()\n\n if (!props.tableLayout?.columnsResizable) return null\n\n return (\n \n )\n}\n\nfunction DataGridTableFillBodyCell() {\n const { props } = useDataGrid()\n\n if (!props.tableLayout?.columnsResizable) return null\n\n return (\n \n )\n}\n\nfunction DataGridTableFillFootCell() {\n const { props } = useDataGrid()\n\n if (!props.tableLayout?.columnsResizable) return null\n\n return (\n \n )\n}\n\nfunction DataGridTableBase({ children }: { children: ReactNode }) {\n const { props, table } = useDataGrid()\n const leftVisibleColumns = table.getLeftVisibleLeafColumns()\n const centerVisibleColumns = table.getCenterVisibleLeafColumns()\n const rightVisibleColumns = table.getRightVisibleLeafColumns()\n const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table)\n\n /**\n * Compute column widths as CSS custom properties once upfront (memoized).\n * Cells reference these via calc(var(--col-X-size) * 1px) so the browser\n * handles width propagation without per-cell getSize() calls or React\n * re-renders of the body.\n */\n const columnSizeVars = useMemo(() => {\n if (!props.tableLayout?.columnsResizable) return undefined\n const headers = table.getFlatHeaders()\n const colSizes: Record = {}\n for (let i = 0; i < headers.length; i++) {\n const header = headers[i]!\n colSizes[`--header-${header.id}-size`] = header.getSize()\n colSizes[`--col-${header.column.id}-size`] = header.column.getSize()\n }\n return colSizes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n props.tableLayout?.columnsResizable,\n // Visibility/order/pinning change the flat header set, so a column shown\n // after mount must get its size variable even though sizing is untouched.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n table.getState().columnSizing,\n // eslint-disable-next-line react-hooks/exhaustive-deps\n table.getState().columnVisibility,\n // eslint-disable-next-line react-hooks/exhaustive-deps\n table.getState().columnOrder,\n // eslint-disable-next-line react-hooks/exhaustive-deps\n table.getState().columnPinning,\n ])\n\n return (\n \n \n {[...leftVisibleColumns, ...centerVisibleColumns].map((column) => (\n \n ))}\n {hasRightPinnedColumns ? : null}\n {rightVisibleColumns.map((column) => (\n \n ))}\n {!hasRightPinnedColumns ? : null}\n \n {children}\n \n )\n}\n\nfunction DataGridTableViewport({\n children,\n className,\n viewportRef,\n style,\n}: {\n children: ReactNode\n className?: string\n viewportRef?: Ref\n style?: CSSProperties\n}) {\n const { props, table, autoSize } = useDataGrid()\n const isColumnsResizable = !!props.tableLayout?.columnsResizable\n const viewportNodeRef = useRef(null)\n const fillStateRef = useRef({ containerWidth: 0, appliedFill: -1 })\n const stopContainerObserverRef = useRef<(() => void) | null>(null)\n\n // Free space is written as a CSS variable directly on the viewport node\n // instead of React state, so container resizes and column-size commits\n // reach the fill column without re-rendering the grid.\n const syncFillWidth = useCallback(() => {\n const node = viewportNodeRef.current\n if (!node) return\n\n const fillWidth = Math.max(\n 0,\n fillStateRef.current.containerWidth - table.getTotalSize()\n )\n\n if (fillStateRef.current.appliedFill !== fillWidth) {\n fillStateRef.current.appliedFill = fillWidth\n node.style.setProperty(\"--data-grid-fill-size\", `${fillWidth}px`)\n }\n\n autoSize?.apply(fillWidth)\n }, [autoSize, table])\n\n const handleViewportRef = useCallback(\n (node: HTMLDivElement | null) => {\n stopContainerObserverRef.current?.()\n stopContainerObserverRef.current = null\n viewportNodeRef.current = node\n assignRef(viewportRef, node)\n\n if (!node) return\n\n if (!isColumnsResizable) {\n fillStateRef.current.appliedFill = -1\n node.style.removeProperty(\"--data-grid-fill-size\")\n return\n }\n\n const scrollViewport =\n getDataGridScrollAreaViewport(node) ?? node.parentElement\n const measurementTarget = scrollViewport ?? node\n\n const measure = () => {\n fillStateRef.current.containerWidth = measurementTarget.clientWidth\n syncFillWidth()\n }\n\n // First measure runs inside the mount commit, before paint, so the fill\n // column and any meta.autoSize growth land in the first painted frame.\n measure()\n\n if (typeof ResizeObserver !== \"undefined\") {\n const observer = new ResizeObserver(measure)\n observer.observe(measurementTarget)\n stopContainerObserverRef.current = () => observer.disconnect()\n }\n },\n [isColumnsResizable, syncFillWidth, viewportRef]\n )\n\n // Column sizing commits and visibility changes alter the table's total size\n // without moving the container, so the fill var must re-sync after renders\n // the ResizeObserver never sees. No-ops when the value is unchanged.\n useLayoutEffect(() => {\n if (!isColumnsResizable) return\n syncFillWidth()\n })\n\n return (\n \n {children}\n \n \n )\n}\n\nfunction DataGridTableHead({ children }: { children: ReactNode }) {\n const { props } = useDataGrid()\n\n return (\n \n {children}\n \n )\n}\n\nfunction DataGridTableHeadRow({\n children,\n rowId,\n}: {\n children: ReactNode\n rowId: string\n}) {\n const { props } = useDataGrid()\n\n return (\n th]:border-b\",\n props.tableLayout?.cellBorder && \"*:last:border-e-0\",\n props.tableLayout?.stripped && \"bg-transparent\",\n props.tableLayout?.headerBackground === false && \"bg-transparent\",\n props.tableClassNames?.headerRow\n )}\n >\n {children}\n \n )\n}\n\nfunction DataGridTableHeadRowCell({\n children,\n header,\n dndRef,\n dndStyle,\n}: {\n children: ReactNode\n header: Header\n dndRef?: React.Ref\n dndStyle?: CSSProperties\n}) {\n const { props } = useDataGrid()\n\n const { column } = header\n const isPinned = column.getIsPinned()\n const isFirstLeftPinned =\n isPinned === \"left\" && column.getIsFirstColumn(\"left\")\n const isLastLeftPinned = isPinned === \"left\" && column.getIsLastColumn(\"left\")\n const isFirstRightPinned =\n isPinned === \"right\" && column.getIsFirstColumn(\"right\")\n const isLastRightPinned =\n isPinned === \"right\" && column.getIsLastColumn(\"right\")\n const isLastVisibleColumn =\n column.getIndex() ===\n header.getContext().table.getVisibleLeafColumns().length - 1\n const headerCellSpacing = headerCellSpacingVariants({\n size: props.tableLayout?.dense ? \"dense\" : \"default\",\n })\n\n const sortDirection = column.getIsSorted()\n\n return (\n 1 ? header.colSpan : undefined}\n aria-sort={\n sortDirection === \"asc\"\n ? \"ascending\"\n : sortDirection === \"desc\"\n ? \"descending\"\n : undefined\n }\n style={{\n ...(props.tableLayout?.width === \"fixed\" &&\n !props.tableLayout?.columnsResizable && {\n width: header.getSize(),\n }),\n ...(props.tableLayout?.columnsPinnable &&\n column.getCanPin() &&\n getPinningStyles(column)),\n ...(props.tableLayout?.columnsResizable && {\n width: `calc(var(--header-${header.id}-size) * 1px)`,\n }),\n ...(dndStyle ? dndStyle : null),\n }}\n data-pinned={isPinned || undefined}\n data-outer-pinned-col={\n isFirstLeftPinned ? \"left\" : isLastRightPinned ? \"right\" : undefined\n }\n data-last-col={\n isLastLeftPinned ? \"left\" : isFirstRightPinned ? \"right\" : undefined\n }\n className={cn(\n \"text-foreground relative h-10 text-left align-middle font-medium rtl:text-right [&:has([role=checkbox])]:pe-0\",\n headerCellSpacing,\n props.tableLayout?.headerBackground && \"bg-muted\",\n props.tableLayout?.cellBorder && \"border-e\",\n props.tableLayout?.columnsResizable &&\n column.getCanResize() &&\n (isPinned ? \"overflow-hidden\" : \"overflow-visible\"),\n props.tableLayout?.columnsResizable &&\n column.getCanResize() &&\n isLastVisibleColumn &&\n \"pe-8\",\n props.tableLayout?.columnsPinnable &&\n column.getCanPin() &&\n cn(\n \"data-pinned:bg-muted data-outer-pinned-col:bg-clip-padding data-pinned:isolate\",\n \"[&[data-pinned=left][data-last-col=left]]:shadow-[inset_-1px_0_0_0_var(--border)] [&[data-pinned=right]:last-child_div.cursor-col-resize:last-child]:opacity-0 [&[data-pinned=right][data-last-col=right]]:shadow-[inset_1px_0_0_0_var(--border)]\",\n \"[&:not([data-pinned]):has(+[data-pinned])_div.cursor-col-resize:last-child]:opacity-0 [&[data-last-col=left]_div.cursor-col-resize:last-child]:opacity-0\"\n ),\n header.column.columnDef.meta?.headerClassName,\n // Edge detection spans the full visible leaf order; the header's own\n // group only covers one pinning bucket.\n column.getIndex() === 0 || isLastVisibleColumn\n ? props.tableClassNames?.edgeCell\n : \"\"\n )}\n >\n {children}\n \n )\n}\n\nfunction DataGridTableHeadRowCellResize({\n header,\n}: {\n header: Header\n}) {\n const { props, table } = useDataGrid()\n const { column } = header\n const isPinned = column.getIsPinned()\n const isLastVisibleColumn =\n column.getIndex() ===\n header.getContext().table.getVisibleLeafColumns().length - 1\n const isResizeModeOnEnd =\n (props.tableLayout?.columnsResizeMode ?? table.options.columnResizeMode) ===\n \"onEnd\"\n const stopResizeSessionRef = useRef<(() => void) | undefined>(undefined)\n\n // End a live drag if the handle unmounts mid-resize so document listeners\n // and the app-wide col-resize cursor don't outlive the grid.\n useEffect(() => {\n return () => {\n stopResizeSessionRef.current?.()\n stopResizeSessionRef.current = undefined\n }\n }, [])\n\n const handleMouseDown = (event: ReactMouseEvent) => {\n // Only the primary button starts a resize; guard before preventDefault so\n // right-click still opens the context menu.\n if (event.button !== 0) return\n\n event.preventDefault()\n event.stopPropagation()\n\n if (isResizeModeOnEnd) {\n stopResizeSessionRef.current?.()\n stopResizeSessionRef.current = startDataGridColumnResizeOnEnd(\n event,\n header,\n table\n )\n return\n }\n\n header.getResizeHandler()(event)\n }\n\n const handleTouchStart = (event: ReactTouchEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (isResizeModeOnEnd) {\n stopResizeSessionRef.current?.()\n stopResizeSessionRef.current = startDataGridColumnResizeOnEnd(\n event,\n header,\n table\n )\n return\n }\n\n header.getResizeHandler()(event)\n }\n\n return (\n column.resetSize(),\n onMouseDown: handleMouseDown,\n onTouchStart: handleTouchStart,\n className: cn(\n \"absolute top-0 h-full cursor-col-resize user-select-none touch-none z-10 flex\",\n isLastVisibleColumn\n ? \"end-0 w-5 justify-end before:hidden\"\n : isPinned\n ? cn(\n // A pinned column is sticky, so the handle sits inside the\n // cell instead of straddling the boundary, where the next\n // sticky cell would paint over it.\n \"end-0 w-5 justify-end\",\n // With the pin affordance on, the pinned edge already draws\n // its own separator and a resize line would double it. But\n // pinning is also usable purely as an ordering lock, with no\n // affordance and no separator -- and there this line is the\n // only thing marking the edge, so hiding it left a resizable\n // column showing a resize cursor and no indicator at all.\n props.tableLayout?.columnsPinnable\n ? \"before:hidden\"\n : \"before:absolute before:inset-y-0 before:end-0 before:w-px before:bg-border\"\n )\n : \"-end-2 w-5 justify-center before:absolute before:inset-y-0 before:w-px before:-translate-x-px before:bg-border\",\n column.getIsResizing() &&\n (isResizeModeOnEnd\n ? \"opacity-100\"\n : isLastVisibleColumn\n ? \"before:absolute before:end-0 before:block before:inset-y-0 before:w-0.5 before:bg-primary opacity-100\"\n : \"before:block before:bg-primary before:w-0.5 opacity-100\")\n ),\n }}\n />\n )\n}\n\nfunction DataGridTableResizeIndicator({\n viewportNodeRef,\n}: {\n viewportNodeRef: RefObject\n}) {\n const { props, table } = useDataGrid()\n const indicatorRef = useRef(null)\n const indicatorHeadRef = useRef(null)\n // Header height is stable for the duration of a drag; caching it per\n // session avoids a forced layout (querySelector + getBoundingClientRect)\n // on every mousemove.\n const headerHeightCacheRef = useRef<{\n key: string | false\n value: number\n }>({ key: false, value: 0 })\n const columnSizingInfo = table.getState().columnSizingInfo\n const resizingColumnId = columnSizingInfo.isResizingColumn\n const resizeMode =\n props.tableLayout?.columnsResizeMode ?? table.options.columnResizeMode\n const isActive = !!(\n props.tableLayout?.columnsResizable &&\n resizeMode === \"onEnd\" &&\n resizingColumnId\n )\n\n // Positioning happens imperatively after each drag-frame render: layout\n // reads (viewport rect, thead height) and ref access belong outside render,\n // and writing styles directly avoids holding the viewport node in React\n // state, which would cost every grid a second render pass at mount.\n useLayoutEffect(() => {\n const indicator = indicatorRef.current\n const indicatorHead = indicatorHeadRef.current\n const viewportElement = viewportNodeRef.current\n\n if (!isActive || !indicator || !indicatorHead || !resizingColumnId) return\n\n const resizingHeader = table\n .getFlatHeaders()\n .find(\n (header) =>\n header.column.id === resizingColumnId ||\n header.id === resizingColumnId\n )\n\n if (!resizingHeader) return\n\n // deltaOffset is a logical delta (already direction-adjusted); translate\n // by the physical pointer movement so the indicator follows the cursor\n // in RTL instead of mirroring it.\n const directionMultiplier =\n table.options.columnResizeDirection === \"rtl\" ? -1 : 1\n const deltaOffset =\n (columnSizingInfo.deltaOffset ?? 0) * directionMultiplier\n\n if (headerHeightCacheRef.current.key !== resizingColumnId) {\n headerHeightCacheRef.current = {\n key: resizingColumnId,\n value:\n viewportElement\n ?.querySelector('[data-slot=\"data-grid-table\"] thead')\n ?.getBoundingClientRect().height ?? 0,\n }\n }\n\n const headerHeight = headerHeightCacheRef.current.value\n const indicatorLeft =\n typeof columnSizingInfo.startOffset === \"number\" && viewportElement\n ? columnSizingInfo.startOffset -\n viewportElement.getBoundingClientRect().left\n : resizingHeader.getStart() + resizingHeader.getSize()\n\n indicator.style.left = `${indicatorLeft}px`\n indicator.style.transform = `translateX(${deltaOffset}px)`\n indicatorHead.style.height = `${Math.max(headerHeight, 6)}px`\n })\n\n if (!isActive) return null\n\n return (\n \n
\n \n
\n )\n}\n\nfunction DataGridTableRowSpacer() {\n return (\n \n )\n}\n\nfunction DataGridTableBody({ children }: { children: ReactNode }) {\n const { props } = useDataGrid()\n\n return (\n \n {children}\n \n )\n}\n\nfunction DataGridTableFoot({ children }: { children: ReactNode }) {\n const { props } = useDataGrid()\n return (\n \n {children}\n \n )\n}\n\nfunction DataGridTableFootRow({ children }: { children: ReactNode }) {\n const { props } = useDataGrid()\n const footRowBottomBorderClasses = \"[&:not(:last-child)>td]:border-b\"\n\n return (\n \n {children}\n \n \n )\n}\n\nfunction DataGridTableFootRowCell({\n children,\n colSpan,\n className,\n}: {\n children?: ReactNode\n colSpan?: number\n className?: string\n}) {\n const { props } = useDataGrid()\n const spacing = footerCellSpacingVariants({\n size: props.tableLayout?.dense ? \"dense\" : \"default\",\n })\n return (\n \n {children}\n \n )\n}\n\nfunction DataGridTableBodyRowSkeleton({ children }: { children: ReactNode }) {\n const { table, props } = useDataGrid()\n\n return (\n td]:border-b\",\n props.tableLayout?.cellBorder && \"*:last:border-e-0\",\n props.tableLayout?.stripped &&\n \"odd:bg-muted/90 odd:hover:bg-muted hover:bg-transparent\",\n table.options.enableRowSelection && \"*:first:relative\",\n props.tableClassNames?.bodyRow\n )}\n >\n {children}\n \n )\n}\n\nfunction DataGridTableBodyRowSkeletonCell({\n children,\n column,\n}: {\n children: ReactNode\n column: Column\n}) {\n const { props, table } = useDataGrid()\n const bodyCellSpacing = bodyCellSpacingVariants({\n size: props.tableLayout?.dense ? \"dense\" : \"default\",\n })\n\n return (\n \n {children}\n \n )\n}\n\nfunction DataGridTableBodyRow({\n children,\n row,\n pinnedBoundary,\n rowRef,\n dndRef,\n dndStyle,\n dataIndex,\n}: {\n children: ReactNode\n row: Row\n pinnedBoundary?: DataGridTablePinnedBoundary\n rowRef?: React.Ref\n dndRef?: React.Ref\n dndStyle?: CSSProperties\n dataIndex?: number\n}) {\n const { props, table } = useDataGrid()\n const isRowPinned = row.getIsPinned()\n\n const bodyRowBottomBorderClasses =\n \"[&:not(:last-child)>td]:border-b [tbody:has(+tfoot)_&:last-child>td]:border-b [*:has(>[data-slot=data-grid]+[data-slot=data-grid-pagination])_[data-slot=data-grid]_&:last-child>td]:border-b\"\n\n return (\n {\n assignRef(rowRef, node)\n assignRef(dndRef, node)\n }}\n style={{ ...(dndStyle ? dndStyle : null) }}\n data-state={\n table.options.enableRowSelection && row.getIsSelected()\n ? \"selected\"\n : undefined\n }\n data-index={dataIndex}\n data-row-id={row.id}\n data-depth={row.depth || undefined}\n data-row-pinned={isRowPinned || undefined}\n data-row-pinned-boundary={pinnedBoundary}\n onClick={() => props.onRowClick && props.onRowClick(row.original)}\n className={cn(\n \"hover:bg-muted/40 data-[state=selected]:bg-muted/50\",\n props.onRowClick && \"cursor-pointer\",\n !props.tableLayout?.stripped &&\n props.tableLayout?.rowBorder &&\n bodyRowBottomBorderClasses,\n props.tableLayout?.cellBorder &&\n `*:last:border-e-0 ${bodyRowBottomBorderClasses}`,\n // Virtualized rows stripe by absolute row index (CSS :nth-child\n // parity flips as spacer rows resize while scrolling).\n props.tableLayout?.stripped &&\n (typeof dataIndex === \"number\"\n ? cn(\n \"hover:bg-transparent\",\n dataIndex % 2 === 0 && \"bg-muted/90 hover:bg-muted\"\n )\n : \"odd:bg-muted/90 odd:hover:bg-muted hover:bg-transparent\"),\n table.options.enableRowSelection && \"*:first:relative\",\n props.tableLayout?.rowsPinnable &&\n isRowPinned &&\n \"bg-muted/30 hover:bg-muted/50\",\n pinnedBoundary === \"top\" &&\n \"[&>td]:shadow-[0_2px_0_rgba(0,0,0,0.03)] dark:[&>td]:shadow-[0_2px_0_rgba(255,255,255,0.06)]\",\n pinnedBoundary === \"bottom\" &&\n \"[&>td]:shadow-[0_2px_0_rgba(0,0,0,0.03)] dark:[&>td]:shadow-[0_2px_0_rgba(255,255,255,0.06)]\",\n props.tableClassNames?.bodyRow\n )}\n >\n {children}\n \n )\n}\n\nfunction DataGridTableBodyRowExpandded({ row }: { row: Row }) {\n const { props, table } = useDataGrid()\n const expandedContent = table\n .getAllColumns()\n .find((column) => column.columnDef.meta?.expandedContent)\n ?.columnDef.meta?.expandedContent\n\n // Tree and grouped rows share row.getIsExpanded() with detail expansion.\n // Without a detail column there is nothing to render, and an empty \n // would break striping parity, rowBorder, and virtual row measurement.\n if (!expandedContent) return null\n\n const bodyRowBottomBorderClasses =\n \"[&:not(:last-child)>td]:border-b [tbody:has(+tfoot)_&:last-child>td]:border-b [*:has(>[data-slot=data-grid]+[data-slot=data-grid-pagination])_[data-slot=data-grid]_&:last-child>td]:border-b\"\n\n return (\n \n \n {expandedContent(row.original)}\n \n \n )\n}\n\nfunction DataGridTableBodyRowCell({\n children,\n cell,\n dndRef,\n dndStyle,\n}: {\n children: ReactNode\n cell: Cell\n dndRef?: React.Ref\n dndStyle?: CSSProperties\n}) {\n const { props } = useDataGrid()\n\n const { column, row } = cell\n const isPinned = column.getIsPinned()\n const isLastLeftPinned = isPinned === \"left\" && column.getIsLastColumn(\"left\")\n const isFirstRightPinned =\n isPinned === \"right\" && column.getIsFirstColumn(\"right\")\n const bodyCellSpacing = bodyCellSpacingVariants({\n size: props.tableLayout?.dense ? \"dense\" : \"default\",\n })\n\n return (\n \n {children}\n \n )\n}\n\nfunction DataGridTableRenderedRow({\n row,\n pinnedBoundary,\n rowRef,\n rowIndex,\n}: {\n row: Row\n pinnedBoundary?: DataGridTablePinnedBoundary\n rowRef?: React.Ref\n /** Virtualized list index, rendered as data-index for measureElement. */\n rowIndex?: number\n}) {\n const { props, table } = useDataGrid()\n const leftVisibleCells = row.getLeftVisibleCells()\n const centerVisibleCells = row.getCenterVisibleCells()\n const rightVisibleCells = row.getRightVisibleCells()\n const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table)\n\n return (\n \n \n {[...leftVisibleCells, ...centerVisibleCells].map(\n (cell: Cell) => (\n \n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n \n )\n )}\n {props.tableLayout?.columnsResizable && hasRightPinnedColumns ? (\n \n ) : null}\n {rightVisibleCells.map((cell: Cell) => (\n \n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n \n ))}\n {props.tableLayout?.columnsResizable && !hasRightPinnedColumns ? (\n \n ) : null}\n \n {row.getIsExpanded() && }\n \n )\n}\n\nfunction DataGridTableEmpty() {\n const { table, props } = useDataGrid()\n const visibleColumnCount =\n getDataGridTableOrderedVisibleColumns(table).length +\n (props.tableLayout?.columnsResizable ? 1 : 0)\n\n return (\n \n \n {props.emptyMessage || \"No data available\"}\n \n \n )\n}\n\nfunction DataGridTableLoader() {\n const { props } = useDataGrid()\n\n return (\n
\n
\n \n {props.loadingMessage || \"Loading...\"}\n
\n
\n )\n}\n\nfunction DataGridTableRowPin({ row }: { row: Row }) {\n const isPinned = row.getIsPinned()\n\n return (\n {\n // Pinning must not bubble into the row's onRowClick handler.\n event.stopPropagation()\n\n if (isPinned) {\n row.pin(false)\n } else {\n row.pin(\"top\")\n }\n }}\n className={cn(\n \"text-muted-foreground hover:text-foreground rounded-full inline-flex size-7 items-center justify-center transition-colors\",\n isPinned && \"text-primary hover:text-primary/80\"\n )}\n >\n {isPinned ? (\n \n \n \n ) : (\n \n \n \n \n )}\n \n )\n}\n\nfunction DataGridTableRowSelect({ row }: { row: Row }) {\n return (\n <>\n