{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"data-grid","type":"registry:ui","title":"Data Grid","description":"","dependencies":["@base-ui/react","@dnd-kit/core","@dnd-kit/modifiers","@dnd-kit/sortable","@dnd-kit/utilities","@tanstack/react-table","@tanstack/react-virtual"],"registryDependencies":["@neui/badge","button","checkbox","@neui/data-grid-table","dropdown-menu","input","popover","select","separator","skeleton","spinner"],"files":[{"path":"data-grid-column-filter.tsx","type":"registry:ui","content":"\"use client\"\n\"use no memo\"\n\nimport { useMemo, useState } from \"react\"\nimport { Badge } from \"@/components/neui/badge\"\nimport type { Column } from \"@tanstack/react-table\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\ninterface DataGridColumnFilterProps {\n column?: Column\n title?: string\n options: {\n label: string\n value: string\n icon?: React.ComponentType<{ className?: string }>\n }[]\n}\n\nfunction DataGridColumnFilter({\n column,\n title,\n options,\n}: DataGridColumnFilterProps) {\n const facets = column?.getFacetedUniqueValues()\n const filterValue = column?.getFilterValue()\n const selectedValues = new Set(\n Array.isArray(filterValue) ? (filterValue as string[]) : []\n )\n const [searchQuery, setSearchQuery] = useState(\"\")\n\n const filteredOptions = useMemo(() => {\n if (!searchQuery) return options\n return options.filter((option) =>\n option.label.toLowerCase().includes(searchQuery.toLowerCase())\n )\n }, [options, searchQuery])\n\n return (\n \n \n \n \n \n
\n setSearchQuery(e.target.value)}\n className=\"h-8\"\n />\n
\n
\n {filteredOptions.length === 0 ? (\n
\n No results found.\n
\n ) : (\n
\n {filteredOptions.map((option) => {\n const isSelected = selectedValues.has(option.value)\n const facetCount = facets?.get(option.value)\n const toggleOption = () => {\n if (isSelected) {\n selectedValues.delete(option.value)\n } else {\n selectedValues.add(option.value)\n }\n const filterValues = Array.from(selectedValues)\n column?.setFilterValue(\n filterValues.length ? filterValues : undefined\n )\n }\n return (\n {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault()\n toggleOption()\n }\n }}\n className={cn(\n \"rounded-2xl relative flex cursor-pointer items-center gap-2 px-2 py-1.5 text-sm outline-hidden select-none\",\n \"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground\"\n )}\n >\n \n \n
\n {option.icon && (\n \n )}\n {option.label}\n {facetCount !== undefined && (\n \n {facetCount}\n \n )}\n
\n )\n })}\n \n )}\n {selectedValues.size > 0 && (\n <>\n
\n
\n column?.setFilterValue(undefined)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault()\n column?.setFilterValue(undefined)\n }\n }}\n className=\"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground rounded-2xl relative flex cursor-pointer items-center justify-center px-2 py-1.5 text-sm outline-hidden select-none\"\n >\n Clear filters\n
\n
\n \n )}\n \n
\n
\n )\n}\n\nexport { DataGridColumnFilter, type DataGridColumnFilterProps }","target":"components/neui/data-grid/data-grid-column-filter.tsx"},{"path":"data-grid-column-header.tsx","type":"registry:ui","content":"\"use client\"\n\"use no memo\"\n\nimport { memo, useMemo } from \"react\"\nimport type { HTMLAttributes, ReactNode } from \"react\"\nimport {\n getColumnHeaderLabel,\n useDataGrid,\n} from \"@/components/neui/data-grid/data-grid\"\nimport type { Column } from \"@tanstack/react-table\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\ninterface DataGridColumnHeaderProps<\n TData,\n TValue,\n> extends HTMLAttributes {\n column: Column\n /** When omitted, uses `column.columnDef.meta.headerTitle`, then a string `columnDef.header`, then `column.id`. */\n title?: string\n icon?: ReactNode\n /** Reserved; pin controls are gated by tableLayout.columnsPinnable + column.getCanPin(). */\n pinnable?: boolean\n filter?: ReactNode\n visibility?: boolean\n}\n\nfunction DataGridColumnHeaderInner({\n column,\n title,\n icon,\n className,\n filter,\n visibility = false,\n}: DataGridColumnHeaderProps) {\n const { isLoading, table, props } = useDataGrid()\n const resolvedTitle = title ?? getColumnHeaderLabel(column)\n\n // TanStack's columnOrder defaults to [] until a consumer seeds it; fall\n // back to the definition order so Move Left/Right work out of the box.\n const columnOrderState = table.getState().columnOrder\n const columnOrder =\n columnOrderState.length > 0\n ? columnOrderState\n : table.getAllLeafColumns().map((leafColumn) => leafColumn.id)\n const columnVisibilityKey =\n props.tableLayout?.columnsVisibility && visibility\n ? JSON.stringify(table.getState().columnVisibility)\n : \"\"\n const isSorted = column.getIsSorted()\n const isPinned = column.getIsPinned()\n const canSort = column.getCanSort()\n const canPin = column.getCanPin()\n const canResize = column.getCanResize()\n\n const columnIndex = columnOrder.indexOf(column.id)\n const canMoveLeft = columnIndex > 0\n const canMoveRight = columnIndex < columnOrder.length - 1\n\n const handleSort = () => {\n if (isSorted === \"asc\") {\n column.toggleSorting(true)\n } else if (isSorted === \"desc\") {\n column.clearSorting()\n } else {\n column.toggleSorting(false)\n }\n }\n\n const headerLabelClassName = cn(\n \"text-secondary-foreground/80 inline-flex h-full items-center gap-1.5 font-normal [&_svg]:opacity-60 text-[0.8125rem] leading-[calc(1.125/0.8125)] [&_svg]:size-3.5\",\n className\n )\n\n const headerButtonClassName = cn(\n \"text-secondary-foreground/80 hover:bg-secondary data-[state=open]:bg-secondary hover:text-foreground data-[state=open]:text-foreground px-2 font-normal h-6 rounded-full\",\n className\n )\n\n const sortIcon =\n canSort &&\n (isSorted === \"desc\" ? (\n \n ) : isSorted === \"asc\" ? (\n \n ) : (\n \n ))\n\n const hasControls =\n props.tableLayout?.columnsMovable ||\n (props.tableLayout?.columnsVisibility && visibility) ||\n (props.tableLayout?.columnsPinnable && canPin) ||\n filter\n\n const menuItems = useMemo(() => {\n const items: ReactNode[] = []\n let hasPreviousSection = false\n\n // Filter section\n if (filter) {\n items.push(\n \n {filter}\n \n )\n hasPreviousSection = true\n }\n\n // Sort section\n if (canSort) {\n if (hasPreviousSection) {\n items.push()\n }\n items.push(\n {\n if (isSorted === \"asc\") {\n column.clearSorting()\n } else {\n column.toggleSorting(false)\n }\n }}\n disabled={!canSort}\n >\n \n Asc\n {isSorted === \"asc\" && (\n \n )}\n ,\n {\n if (isSorted === \"desc\") {\n column.clearSorting()\n } else {\n column.toggleSorting(true)\n }\n }}\n disabled={!canSort}\n >\n \n Desc\n {isSorted === \"desc\" && (\n \n )}\n \n )\n hasPreviousSection = true\n }\n\n // Pin section\n if (props.tableLayout?.columnsPinnable && canPin) {\n if (hasPreviousSection) {\n items.push()\n }\n items.push(\n column.pin(isPinned === \"left\" ? false : \"left\")}\n >\n \n Pin to left\n {isPinned === \"left\" && (\n \n )}\n ,\n column.pin(isPinned === \"right\" ? false : \"right\")}\n >\n \n Pin to right\n {isPinned === \"right\" && (\n \n )}\n \n )\n hasPreviousSection = true\n }\n\n // Move section\n if (props.tableLayout?.columnsMovable) {\n if (hasPreviousSection) {\n items.push()\n }\n items.push(\n {\n if (columnIndex > 0) {\n const newOrder = [...columnOrder]\n const [movedColumn] = newOrder.splice(columnIndex, 1)\n newOrder.splice(columnIndex - 1, 0, movedColumn)\n table.setColumnOrder(newOrder)\n }\n }}\n disabled={!canMoveLeft || isPinned !== false}\n >\n \n Move to Left\n ,\n {\n if (columnIndex < columnOrder.length - 1) {\n const newOrder = [...columnOrder]\n const [movedColumn] = newOrder.splice(columnIndex, 1)\n newOrder.splice(columnIndex + 1, 0, movedColumn)\n table.setColumnOrder(newOrder)\n }\n }}\n disabled={!canMoveRight || isPinned !== false}\n >\n \n Move to Right\n \n )\n hasPreviousSection = true\n }\n\n // Visibility section\n if (props.tableLayout?.columnsVisibility && visibility) {\n if (hasPreviousSection) {\n items.push()\n }\n items.push(\n \n \n \n Columns\n \n \n {table\n .getAllColumns()\n .filter((col) => col.getCanHide())\n .map((col) => (\n event.preventDefault()}\n onCheckedChange={(value) => col.toggleVisibility(!!value)}\n className=\"capitalize\"\n >\n {getColumnHeaderLabel(col)}\n \n ))}\n \n \n )\n }\n\n return items\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n filter,\n canSort,\n isSorted,\n column,\n props.tableLayout?.columnsPinnable,\n props.tableLayout?.columnsMovable,\n props.tableLayout?.columnsVisibility,\n canPin,\n isPinned,\n canMoveLeft,\n canMoveRight,\n visibility,\n table,\n columnIndex,\n columnOrder,\n columnVisibilityKey, // Needed to update checkbox states when visibility changes\n ])\n\n if (hasControls) {\n return (\n
\n \n \n \n {icon && icon}\n {resolvedTitle}\n {sortIcon}\n \n \n \n {menuItems}\n \n \n {props.tableLayout?.columnsPinnable && canPin && isPinned && (\n column.pin(false)}\n aria-label={`Unpin ${resolvedTitle} column`}\n title={`Unpin ${resolvedTitle} column`}\n >\n \n \n )}\n
\n )\n }\n\n if (canSort || (props.tableLayout?.columnsResizable && canResize)) {\n return (\n
\n \n {icon && icon}\n {resolvedTitle}\n {sortIcon}\n \n
\n )\n }\n\n return (\n
\n {icon && icon}\n {resolvedTitle}\n
\n )\n}\n\nconst DataGridColumnHeader = memo(\n DataGridColumnHeaderInner\n) as typeof DataGridColumnHeaderInner\n\nexport { DataGridColumnHeader, type DataGridColumnHeaderProps }","target":"components/neui/data-grid/data-grid-column-header.tsx"},{"path":"data-grid-column-visibility.tsx","type":"registry:ui","content":"\"use client\"\n\"use no memo\"\n\nimport type { ReactElement } from \"react\"\nimport { getColumnHeaderLabel } from \"@/components/neui/data-grid/data-grid\"\nimport type { Table } from \"@tanstack/react-table\"\n\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuLabel,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\n\nfunction DataGridColumnVisibility({\n table,\n trigger,\n}: {\n table: Table\n trigger: ReactElement>\n}) {\n return (\n \n {trigger}\n \n \n \n Toggle Columns\n \n {table\n .getAllColumns()\n .filter((column) => column.getCanHide())\n .map((column) => {\n return (\n event.preventDefault()}\n onCheckedChange={(value) => column.toggleVisibility(!!value)}\n >\n {getColumnHeaderLabel(column)}\n \n )\n })}\n \n \n \n )\n}\n\nexport { DataGridColumnVisibility }","target":"components/neui/data-grid/data-grid-column-visibility.tsx"},{"path":"data-grid-pagination.tsx","type":"registry:ui","content":"\"use client\"\n\"use no memo\"\n\nimport type { JSX, ReactNode } from \"react\"\nimport { useDataGrid } from \"@/components/neui/data-grid/data-grid\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\ninterface DataGridPaginationProps {\n sizes?: number[]\n sizesInfo?: string\n sizesLabel?: string\n sizesDescription?: string\n sizesSkeleton?: ReactNode\n more?: boolean\n moreLimit?: number\n info?: string\n infoSkeleton?: ReactNode\n className?: string\n rowsPerPageLabel?: string\n previousPageLabel?: string\n nextPageLabel?: string\n ellipsisText?: string\n}\n\nfunction DataGridPagination(props: DataGridPaginationProps): JSX.Element {\n const { table, recordCount, isLoading } = useDataGrid()\n\n const defaultProps: Partial = {\n sizes: [5, 10, 25, 50, 100],\n sizesSkeleton: ,\n moreLimit: 5,\n info: \"{from} - {to} of {count}\",\n infoSkeleton: ,\n rowsPerPageLabel: \"Rows per page\",\n previousPageLabel: \"Go to previous page\",\n nextPageLabel: \"Go to next page\",\n ellipsisText: \"...\",\n }\n\n const mergedProps: DataGridPaginationProps = { ...defaultProps, ...props }\n\n const btnBaseClasses = \"p-0 text-sm\"\n const btnArrowClasses = btnBaseClasses + \" rtl:transform rtl:rotate-180\"\n const pageIndex = table.getState().pagination.pageIndex\n const pageSize = table.getState().pagination.pageSize\n const from = recordCount === 0 ? 0 : pageIndex * pageSize + 1\n const to = Math.min((pageIndex + 1) * pageSize, recordCount)\n const pageCount = table.getPageCount()\n\n // Replace placeholders in paginationInfo\n const paginationInfo = mergedProps.info\n ? mergedProps.info\n .replaceAll(\"{from}\", from.toString())\n .replaceAll(\"{to}\", to.toString())\n .replaceAll(\"{count}\", recordCount.toString())\n : `${from} - ${to} of ${recordCount}`\n\n // Pagination limit logic\n const paginationMoreLimit = mergedProps.moreLimit || 5\n\n // Determine the start and end of the pagination group\n const currentGroupStart =\n Math.floor(pageIndex / paginationMoreLimit) * paginationMoreLimit\n const currentGroupEnd = Math.min(\n currentGroupStart + paginationMoreLimit,\n pageCount\n )\n\n // Render page buttons based on the current group\n const renderPageButtons = () => {\n const buttons = []\n for (let i = currentGroupStart; i < currentGroupEnd; i++) {\n buttons.push(\n {\n if (pageIndex !== i) {\n table.setPageIndex(i)\n }\n }}\n >\n {i + 1}\n \n )\n }\n return buttons\n }\n\n // Render a \"previous\" ellipsis button if there are previous pages to show\n const renderEllipsisPrevButton = () => {\n if (currentGroupStart > 0) {\n return (\n table.setPageIndex(currentGroupStart - 1)}\n >\n {mergedProps.ellipsisText}\n \n )\n }\n return null\n }\n\n // Render a \"next\" ellipsis button if there are more pages to show after the current group\n const renderEllipsisNextButton = () => {\n if (currentGroupEnd < pageCount) {\n return (\n table.setPageIndex(currentGroupEnd)}\n >\n {mergedProps.ellipsisText}\n \n )\n }\n return null\n }\n\n return (\n \n
\n {isLoading ? (\n mergedProps.sizesSkeleton\n ) : (\n <>\n
\n {mergedProps.rowsPerPageLabel}\n
\n {\n const newPageSize = Number(value)\n table.setPageSize(newPageSize)\n }}\n >\n \n \n \n \n {mergedProps.sizes?.map((size: number) => (\n \n {size}\n \n ))}\n \n \n \n )}\n
\n
\n {isLoading ? (\n mergedProps.infoSkeleton\n ) : (\n <>\n
\n {paginationInfo}\n
\n {pageCount > 1 && (\n
\n table.previousPage()}\n disabled={!table.getCanPreviousPage()}\n >\n \n {mergedProps.previousPageLabel}\n \n \n \n\n {renderEllipsisPrevButton()}\n\n {renderPageButtons()}\n\n {renderEllipsisNextButton()}\n\n table.nextPage()}\n disabled={!table.getCanNextPage()}\n >\n {mergedProps.nextPageLabel}\n \n \n
\n )}\n \n )}\n
\n \n )\n}\n\nexport { DataGridPagination, type DataGridPaginationProps }","target":"components/neui/data-grid/data-grid-pagination.tsx"},{"path":"data-grid-scroll-area.tsx","type":"registry:ui","content":"\"use client\"\n\"use no memo\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\nimport type { PointerEvent, ReactNode } from \"react\"\nimport { useDataGrid } from \"@/components/neui/data-grid/data-grid\"\nimport { ScrollArea as ScrollAreaPrimitive } from \"@base-ui/react/scroll-area\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst MIN_THUMB_SIZE = 24\nconst FALLBACK_SCROLLBAR_SIZE = 12\n\nconst INITIAL_METRICS = {\n hasVerticalOverflow: false,\n headerHeight: 0,\n horizontalScrollbarSize: 0,\n thumbHeight: 0,\n thumbTop: 0,\n trackHeight: 0,\n} as const\n\nconst SCROLLBAR_CLASSNAME =\n \"flex touch-none p-px transition-colors select-none data-[orientation=horizontal]:h-2.5 data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:border-t data-[orientation=horizontal]:border-t-transparent data-[orientation=vertical]:h-full data-[orientation=vertical]:w-2 data-[orientation=vertical]:border-s data-[orientation=vertical]:border-s-transparent\"\n\nconst SCROLLBAR_THUMB_CLASSNAME = \"bg-border rounded-full relative flex-1\"\n\ntype DataGridScrollAreaOrientation = \"horizontal\" | \"vertical\" | \"both\"\n\ntype ScrollbarMetrics = {\n hasVerticalOverflow: boolean\n headerHeight: number\n horizontalScrollbarSize: number\n thumbHeight: number\n thumbTop: number\n trackHeight: number\n}\n\ntype ObservedElements = {\n header: HTMLElement | null\n horizontalScrollbar: HTMLElement | null\n table: HTMLElement | null\n tableViewport: HTMLElement | null\n}\n\ntype DataGridScrollAreaProps = Omit<\n ScrollAreaPrimitive.Root.Props,\n \"children\"\n> & {\n children: ReactNode\n orientation?: DataGridScrollAreaOrientation\n}\n\nfunction clamp(value: number, min: number, max: number) {\n return Math.min(max, Math.max(min, value))\n}\n\nfunction areMetricsEqual(next: ScrollbarMetrics, prev: ScrollbarMetrics) {\n return (\n next.hasVerticalOverflow === prev.hasVerticalOverflow &&\n next.headerHeight === prev.headerHeight &&\n next.horizontalScrollbarSize === prev.horizontalScrollbarSize &&\n next.thumbHeight === prev.thumbHeight &&\n next.thumbTop === prev.thumbTop &&\n next.trackHeight === prev.trackHeight\n )\n}\n\nfunction applyMetrics(element: HTMLElement, metrics: ScrollbarMetrics) {\n element.style.setProperty(\n \"--data-grid-scrollbar-header-height\",\n `${metrics.headerHeight}px`\n )\n element.style.setProperty(\n \"--data-grid-scrollbar-thumb-height\",\n `${metrics.thumbHeight}px`\n )\n element.style.setProperty(\n \"--data-grid-scrollbar-thumb-top\",\n `${metrics.thumbTop}px`\n )\n element.style.setProperty(\n \"--data-grid-scrollbar-track-height\",\n `${metrics.trackHeight}px`\n )\n}\n\nfunction DataGridScrollArea({\n children,\n className,\n orientation = \"both\",\n ...props\n}: DataGridScrollAreaProps) {\n const { props: dataGridProps, table } = useDataGrid()\n const containerRef = useRef(null)\n const overlayRef = useRef(null)\n const viewportRef = useRef(null)\n const dragRef = useRef<{\n pointerId: number\n startScrollTop: number\n startY: number\n } | null>(null)\n const metricsRef = useRef(INITIAL_METRICS)\n const observedElementsRef = useRef({\n header: null,\n horizontalScrollbar: null,\n table: null,\n tableViewport: null,\n })\n\n const showHorizontal = orientation !== \"vertical\"\n const showVertical = orientation !== \"horizontal\"\n const usesCustomVerticalScrollbar =\n showVertical && !!dataGridProps.tableLayout?.headerSticky\n // Pinned columns are sticky and never scroll, so the horizontal scrollbar\n // track is inset to span only the scrollable center region between them.\n const isColumnsPinnable = !!dataGridProps.tableLayout?.columnsPinnable\n const scrollbarInsetStart = isColumnsPinnable ? table.getLeftTotalSize() : 0\n const scrollbarInsetEnd = isColumnsPinnable ? table.getRightTotalSize() : 0\n const [hasCustomVerticalOverflow, setHasCustomVerticalOverflow] =\n useState(false)\n\n const clearDragState = useCallback(() => {\n dragRef.current = null\n document.body.style.userSelect = \"\"\n document.body.style.webkitUserSelect = \"\"\n }, [])\n\n // The overlay is mounted one commit after the sync that detected overflow,\n // so it misses that sync's write. Seeding it from the ref callback lands the\n // geometry during commit, before the browser paints the track.\n const setOverlayRef = useCallback((node: HTMLDivElement | null) => {\n overlayRef.current = node\n\n if (node) applyMetrics(node, metricsRef.current)\n }, [])\n\n const resetMetrics = useCallback(() => {\n if (!areMetricsEqual(INITIAL_METRICS, metricsRef.current)) {\n metricsRef.current = INITIAL_METRICS\n if (overlayRef.current) applyMetrics(overlayRef.current, INITIAL_METRICS)\n }\n\n setHasCustomVerticalOverflow((prev) => (prev ? false : prev))\n }, [])\n\n const syncCustomVerticalScrollbar = useCallback(() => {\n const container = containerRef.current\n const viewport = viewportRef.current\n\n if (!container || !viewport || !usesCustomVerticalScrollbar) {\n resetMetrics()\n return\n }\n\n const { header, horizontalScrollbar } = observedElementsRef.current\n const headerHeight = header?.getBoundingClientRect().height ?? 0\n const viewportHeight = viewport.clientHeight\n const viewportWidth = viewport.clientWidth\n const scrollHeight = viewport.scrollHeight\n const scrollWidth = viewport.scrollWidth\n const hasHorizontalOverflow =\n showHorizontal && scrollWidth > viewportWidth + 0.5\n const horizontalScrollbarSize = hasHorizontalOverflow\n ? horizontalScrollbar?.offsetHeight || FALLBACK_SCROLLBAR_SIZE\n : 0\n const trackHeight = Math.max(\n 0,\n viewportHeight - headerHeight - horizontalScrollbarSize\n )\n const maxScroll = Math.max(0, scrollHeight - viewportHeight)\n\n let nextMetrics: ScrollbarMetrics\n\n if (trackHeight === 0 || maxScroll === 0) {\n nextMetrics = {\n hasVerticalOverflow: false,\n headerHeight,\n horizontalScrollbarSize,\n thumbHeight: trackHeight,\n thumbTop: 0,\n trackHeight,\n }\n } else {\n const bodyContentHeight = Math.max(\n trackHeight,\n scrollHeight - headerHeight\n )\n const thumbHeight = clamp(\n trackHeight * (trackHeight / bodyContentHeight),\n MIN_THUMB_SIZE,\n trackHeight\n )\n const maxThumbTop = Math.max(0, trackHeight - thumbHeight)\n const thumbTop =\n maxThumbTop > 0 ? (viewport.scrollTop / maxScroll) * maxThumbTop : 0\n\n nextMetrics = {\n hasVerticalOverflow: true,\n headerHeight,\n horizontalScrollbarSize,\n thumbHeight,\n thumbTop,\n trackHeight,\n }\n }\n\n if (!areMetricsEqual(nextMetrics, metricsRef.current)) {\n metricsRef.current = nextMetrics\n // Scoped to the overlay, never to the container. These four properties\n // inherit, and thumbTop changes on essentially every scroll frame, so\n // writing them on the element that wraps the whole grid invalidates\n // computed style for every row and cell each frame. The overlay subtree\n // is their only reader.\n if (overlayRef.current) applyMetrics(overlayRef.current, nextMetrics)\n }\n\n setHasCustomVerticalOverflow((prev) =>\n prev === nextMetrics.hasVerticalOverflow\n ? prev\n : nextMetrics.hasVerticalOverflow\n )\n }, [resetMetrics, showHorizontal, usesCustomVerticalScrollbar])\n\n useEffect(() => {\n const container = containerRef.current\n const viewport = viewportRef.current\n\n if (!container || !viewport) return\n\n if (!usesCustomVerticalScrollbar) {\n resetMetrics()\n return\n }\n\n let frame = 0\n\n const scheduleSync = () => {\n cancelAnimationFrame(frame)\n frame = window.requestAnimationFrame(syncCustomVerticalScrollbar)\n }\n\n const observer =\n typeof ResizeObserver === \"undefined\"\n ? null\n : new ResizeObserver(scheduleSync)\n const observed = new Set()\n\n const observeElement = (element: HTMLElement | null) => {\n if (element && observer && !observed.has(element)) {\n observer.observe(element)\n observed.add(element)\n }\n }\n\n const resolveObservedElements = () => {\n observedElementsRef.current = {\n header: container.querySelector(\n '[data-slot=\"data-grid-table\"] thead'\n ) as HTMLElement | null,\n horizontalScrollbar: container.querySelector(\n '[data-slot=\"data-grid-scrollbar\"][data-orientation=\"horizontal\"]'\n ) as HTMLElement | null,\n table: container.querySelector(\n '[data-slot=\"data-grid-table\"]'\n ) as HTMLElement | null,\n tableViewport: container.querySelector(\n '[data-slot=\"data-grid-table-viewport\"]'\n ) as HTMLElement | null,\n }\n\n observeElement(observedElementsRef.current.header)\n observeElement(observedElementsRef.current.table)\n observeElement(observedElementsRef.current.tableViewport)\n\n return !!(\n observedElementsRef.current.header && observedElementsRef.current.table\n )\n }\n\n observeElement(viewport)\n const resolvedOnMount = resolveObservedElements()\n\n scheduleSync()\n viewport.addEventListener(\"scroll\", scheduleSync, { passive: true })\n\n // A table that mounts after this effect (empty state swapped for data)\n // would otherwise never be observed and the custom scrollbar would\n // overlap the sticky header. One-shot: disconnects once resolved.\n let mutationObserver: MutationObserver | null = null\n if (!resolvedOnMount && typeof MutationObserver !== \"undefined\") {\n mutationObserver = new MutationObserver(() => {\n if (resolveObservedElements()) {\n mutationObserver?.disconnect()\n mutationObserver = null\n scheduleSync()\n }\n })\n mutationObserver.observe(container, { childList: true, subtree: true })\n }\n\n return () => {\n cancelAnimationFrame(frame)\n observer?.disconnect()\n mutationObserver?.disconnect()\n viewport.removeEventListener(\"scroll\", scheduleSync)\n clearDragState()\n }\n }, [\n clearDragState,\n resetMetrics,\n syncCustomVerticalScrollbar,\n usesCustomVerticalScrollbar,\n ])\n\n const scrollToThumbOffset = (nextThumbTop: number) => {\n const viewport = viewportRef.current\n const { thumbHeight, trackHeight } = metricsRef.current\n\n if (!viewport) return\n\n const maxScroll = Math.max(0, viewport.scrollHeight - viewport.clientHeight)\n const maxThumbTop = Math.max(0, trackHeight - thumbHeight)\n\n if (maxScroll === 0 || maxThumbTop === 0) {\n viewport.scrollTop = 0\n return\n }\n\n const ratio = clamp(nextThumbTop, 0, maxThumbTop) / maxThumbTop\n viewport.scrollTop = ratio * maxScroll\n }\n\n const handleThumbPointerDown = (event: PointerEvent) => {\n const viewport = viewportRef.current\n\n if (!viewport) return\n\n event.preventDefault()\n event.stopPropagation()\n event.currentTarget.setPointerCapture(event.pointerId)\n\n dragRef.current = {\n pointerId: event.pointerId,\n startScrollTop: viewport.scrollTop,\n startY: event.clientY,\n }\n\n document.body.style.userSelect = \"none\"\n document.body.style.webkitUserSelect = \"none\"\n }\n\n const handleThumbPointerMove = (event: PointerEvent) => {\n const viewport = viewportRef.current\n const dragState = dragRef.current\n const { thumbHeight, trackHeight } = metricsRef.current\n\n if (!viewport || !dragState || dragState.pointerId !== event.pointerId) {\n return\n }\n\n const maxThumbTop = Math.max(0, trackHeight - thumbHeight)\n const maxScroll = Math.max(0, viewport.scrollHeight - viewport.clientHeight)\n\n if (maxThumbTop === 0 || maxScroll === 0) return\n\n const deltaY = event.clientY - dragState.startY\n const nextScrollTop =\n dragState.startScrollTop + (deltaY / maxThumbTop) * maxScroll\n\n viewport.scrollTop = clamp(nextScrollTop, 0, maxScroll)\n }\n\n const handleThumbPointerUp = (event: PointerEvent) => {\n if (dragRef.current?.pointerId !== event.pointerId) return\n clearDragState()\n }\n\n const handleTrackPointerDown = (event: PointerEvent) => {\n const { thumbHeight } = metricsRef.current\n\n if (event.target !== event.currentTarget) return\n\n event.preventDefault()\n event.stopPropagation()\n\n const rect = event.currentTarget.getBoundingClientRect()\n const offsetY = event.clientY - rect.top - thumbHeight / 2\n\n scrollToThumbOffset(offsetY)\n }\n\n return (\n
\n \n \n \n {children}\n \n \n\n {showHorizontal && (\n 0 || scrollbarInsetEnd > 0\n ? {\n marginInlineStart: scrollbarInsetStart || undefined,\n marginInlineEnd: scrollbarInsetEnd || undefined,\n }\n : undefined\n }\n >\n \n \n )}\n\n {showVertical && !usesCustomVerticalScrollbar && (\n \n \n \n )}\n \n\n {usesCustomVerticalScrollbar && hasCustomVerticalOverflow && (\n \n \n \n
\n \n )}\n \n )\n}\n\nexport { DataGridScrollArea }\nexport type { DataGridScrollAreaOrientation, DataGridScrollAreaProps }","target":"components/neui/data-grid/data-grid-scroll-area.tsx"},{"path":"data-grid-table-dnd-rows.tsx","type":"registry:ui","content":"\"use client\"\n\"use no memo\"\n\nimport {\n createContext,\n memo,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from \"react\"\nimport type { CSSProperties, ReactNode } from \"react\"\nimport { useDataGrid } from \"@/components/neui/data-grid/data-grid\"\nimport {\n DataGridTableBase,\n DataGridTableBody,\n DataGridTableBodyRow,\n DataGridTableBodyRowCell,\n DataGridTableBodyRowExpandded,\n DataGridTableBodyRowSkeleton,\n DataGridTableBodyRowSkeletonCell,\n DataGridTableEmpty,\n DataGridTableFillBodyCell,\n DataGridTableFillHeadCell,\n DataGridTableFoot,\n DataGridTableHead,\n DataGridTableHeadRow,\n DataGridTableHeadRowCell,\n DataGridTableHeadRowCellResize,\n DataGridTableRowSpacer,\n DataGridTableViewport,\n} from \"@/components/neui/data-grid/data-grid-table\"\nimport {\n closestCenter,\n DndContext,\n DragOverlay,\n KeyboardSensor,\n MouseSensor,\n TouchSensor,\n useSensor,\n useSensors,\n type CollisionDetection,\n type DragCancelEvent,\n type DragEndEvent,\n type DragMoveEvent,\n type DragOverEvent,\n type DragStartEvent,\n type Modifier,\n type UniqueIdentifier,\n} from \"@dnd-kit/core\"\nimport { restrictToVerticalAxis } from \"@dnd-kit/modifiers\"\nimport {\n SortableContext,\n sortableKeyboardCoordinates,\n useSortable,\n verticalListSortingStrategy,\n type SortingStrategy,\n} from \"@dnd-kit/sortable\"\nimport { CSS } from \"@dnd-kit/utilities\"\nimport { flexRender } from \"@tanstack/react-table\"\nimport type { Cell, HeaderGroup, Row, Table } from \"@tanstack/react-table\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\n// Context to share sortable listeners from row to handle\ntype SortableContextValue = ReturnType\nconst SortableRowContext = createContext | null>(null)\n\n/**\n * Tree metadata attached to every sortable row, readable from\n * `active.data.current` / `over.data.current` in any drag event. Cross-parent\n * drops can be resolved from it without re-deriving the shape of the table.\n */\ntype DataGridTableDndRowData = {\n type: \"data-grid-row\"\n /** Tree depth, 0 for root rows. */\n depth: number\n /** Index within the parent's children, or within the root rows. */\n index: number\n /** Parent row id, or null for root rows. */\n parentId: string | null\n}\n\n/**\n * Per-row render slot for drop indicators and depth guides. The returned node\n * is positioned over the row, so it never adds a column, shifts striping, or\n * gets clipped by a truncating resizable cell.\n */\ntype DataGridTableDndRowDecoration = (context: {\n row: Row\n isDragging: boolean\n isOver: boolean\n}) => ReactNode\n\nfunction DataGridTableDndRowHandle({\n className,\n disabled,\n disabledLabel = \"Reordering unavailable\",\n}: {\n className?: string\n /**\n * Renders the grip inert instead of withdrawing it. A grid that reorders on\n * one truth (manual order) and sorts on another cannot honour both at once,\n * but dropping the handle entirely collapses the gutter and reads as broken\n * rather than as unavailable. Keep the column's shape, mute the control.\n */\n disabled?: boolean\n /** Announced and shown on hover in place of the drag affordance. */\n disabledLabel?: string\n}) {\n const context = useContext(SortableRowContext)\n\n if (!context || disabled) {\n return (\n \n \n \n )\n }\n\n return (\n \n \n \n )\n}\n\nfunction DataGridTableDndRow({\n row,\n renderRowDecoration,\n}: {\n row: Row\n renderRowDecoration?: DataGridTableDndRowDecoration\n}) {\n const rowData: DataGridTableDndRowData = {\n type: \"data-grid-row\",\n depth: row.depth,\n index: row.index,\n parentId: row.getParentRow()?.id ?? null,\n }\n\n const { transform, setNodeRef, isDragging, isOver, attributes, listeners } =\n useSortable({\n id: row.id,\n data: rowData,\n })\n\n const style: CSSProperties = {\n transform: CSS.Transform.toString(transform),\n // dnd-kit's transition is deliberately dropped. A transition on a transform\n // property of a `tr` does not merely fail to animate in Chrome, it stops the\n // transform applying at all: the element sits at the start value forever.\n // The drag source escapes it because dnd-kit disables its own transition\n // while it is being dragged, which is why the carried row used to be the\n // ONLY one that moved and every other row silently refused to open a gap.\n // Displacement therefore lands in one step, which is what a table wants.\n zIndex: isDragging ? 1 : 0,\n position: \"relative\",\n cursor: isDragging ? \"grabbing\" : undefined,\n // The row you are holding is drawn by the DragOverlay below, so the one\n // left behind only has to show that it has been picked up, and it does that\n // by fading. Nothing else: a border or a surface on it would compete with\n // the clone that is actually being carried. It used to paint itself a solid\n // background with inset hairlines, which was for the days when this row WAS\n // the thing following the pointer.\n ...(isDragging && { opacity: 0.5 }),\n }\n\n const decoration = renderRowDecoration?.({ row, isDragging, isOver })\n\n return (\n \n \n {row\n .getVisibleCells()\n .map((cell: Cell, index, cells) => {\n return (\n \n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n {decoration && index === cells.length - 1 ? (\n // Rides inside the last cell rather than in a `td` of its own.\n // An absolutely positioned `td` is still a cell as far as table\n // layout is concerned, so it added a NINTH column with no width\n // of its own, and under `table-layout: fixed` that new column\n // swallowed the whole surplus the real columns had been sharing\n // — every column snapped back to its declared size and the row's\n // content visibly narrowed the moment a drag began. A plain\n // element adds no column. It still anchors to the ROW, because\n // the row is the nearest positioned ancestor, so the decoration\n // spans the full width and is not clipped by the cell.\n \n {decoration}\n \n ) : null}\n \n )\n })}\n \n \n {row.getIsExpanded() && }\n \n )\n}\n\nfunction DataGridTableDndRowsBody({\n table,\n dataIds,\n renderRowDecoration,\n sortingStrategy,\n}: {\n table: Table\n dataIds: UniqueIdentifier[]\n renderRowDecoration?: DataGridTableDndRowDecoration\n sortingStrategy: SortingStrategy\n}) {\n const { isLoading, props } = useDataGrid()\n const pagination = table.getState().pagination\n\n if (props.loadingMode === \"skeleton\" && isLoading && pagination?.pageSize) {\n return (\n <>\n {Array.from({ length: pagination.pageSize }).map((_, rowIndex) => (\n \n {table.getVisibleFlatColumns().map((column, colIndex) => {\n return (\n \n {column.columnDef.meta?.skeleton}\n \n )\n })}\n \n \n ))}\n \n )\n }\n\n if (!table.getRowModel().rows.length) return \n\n return (\n \n {table.getRowModel().rows.map((row: Row) => {\n return (\n \n )\n })}\n \n )\n}\n\n/**\n * Memoized body rows: skip re-renders during active column resize.\n * Column widths update via CSS variables on the element,\n * so the browser handles width changes without React re-renders.\n */\nconst MemoizedDataGridTableDndRowsBody = memo(\n DataGridTableDndRowsBody,\n (_prev, next) => !!next.table.getState().columnSizingInfo.isResizingColumn\n) as typeof DataGridTableDndRowsBody\n\nfunction DataGridTableDndRows({\n handleDragEnd,\n dataIds,\n footerContent,\n collisionDetection = closestCenter,\n modifiers,\n sortingStrategy = verticalListSortingStrategy,\n renderRowDecoration,\n onDragStart,\n onDragMove,\n onDragOver,\n onDragCancel,\n}: {\n handleDragEnd: (event: DragEndEvent) => void\n dataIds: UniqueIdentifier[]\n footerContent?: ReactNode\n /** Overrides the default `closestCenter` strategy. */\n collisionDetection?: CollisionDetection\n /**\n * Replaces the default axis restriction, e.g. drop `restrictToVerticalAxis`\n * to allow the horizontal gesture that tree re-parenting relies on. The\n * table container clamp is always applied after these, so a dragged row\n * cannot leave the grid.\n */\n modifiers?: Modifier[]\n /**\n * Replaces the default `verticalListSortingStrategy`. Return null from a\n * strategy to leave every row exactly where it is. A tree needs that: its drop\n * is either INTO the hovered row or BETWEEN two rows, and which one it is\n * flips as the pointer crosses a single row, so a gap that opens for one and\n * shuts for the other flickers the whole surface. Such a caller draws its own\n * insertion line instead, and pairs this with a modifier that holds the\n * carried row still, since a gap nothing moves into is just a hole.\n */\n sortingStrategy?: SortingStrategy\n /** Per-row slot for drop indicators and depth guides. */\n renderRowDecoration?: DataGridTableDndRowDecoration\n onDragStart?: (event: DragStartEvent) => void\n onDragMove?: (event: DragMoveEvent) => void\n onDragOver?: (event: DragOverEvent) => void\n onDragCancel?: (event: DragCancelEvent) => void\n}) {\n const { table, props } = useDataGrid()\n const tableContainerRef = useRef(null)\n const [isDraggingRow, setIsDraggingRow] = useState(false)\n // The row being carried, plus the column widths measured off the header the\n // moment the drag starts. The clone lives outside the table, so it has no\n // columns of its own and has to be told what they are.\n const [carried, setCarried] = useState<{\n id: UniqueIdentifier\n width: number\n columns: number[]\n } | null>(null)\n\n const pickUpRow = useCallback((id: UniqueIdentifier) => {\n const head = tableContainerRef.current?.querySelector(\"thead tr\")\n if (!head) {\n setCarried(null)\n return\n }\n\n // The fill cell is a header-only spacer that soaks up the surplus a column\n // resize leaves behind, and the clone renders data cells only. Measuring it\n // in would make the clone's table wider than the cells it actually holds,\n // and `table-fixed` hands that orphaned width back out across every column\n // -- the carried row comes out visibly wider than the row it was lifted\n // from. So the width is the sum of what we render, never the header's own.\n const columns = Array.from(head.children)\n .filter(\n (cell) =>\n cell.getAttribute(\"data-slot\") !== \"data-grid-table-fill-head-cell\"\n )\n .map((cell) => cell.getBoundingClientRect().width)\n\n setCarried({\n id,\n width: columns.reduce((total, width) => total + width, 0),\n columns,\n })\n }, [])\n\n const carriedRow = carried\n ? table.getRowModel().rows.find((row: Row) => row.id === carried.id)\n : undefined\n\n const sensors = useSensors(\n useSensor(MouseSensor, {}),\n useSensor(TouchSensor, {}),\n // Keyboard reordering moves one sortable position per keypress instead\n // of the sensor's raw 25px default.\n useSensor(KeyboardSensor, {\n coordinateGetter: sortableKeyboardCoordinates,\n })\n )\n\n useEffect(() => {\n if (!isDraggingRow) return\n\n const { body, documentElement } = document\n const previousBodyCursor = body.style.cursor\n const previousDocumentCursor = documentElement.style.cursor\n\n body.style.cursor = \"grabbing\"\n documentElement.style.cursor = \"grabbing\"\n\n return () => {\n body.style.cursor = previousBodyCursor\n documentElement.style.cursor = previousDocumentCursor\n }\n }, [isDraggingRow])\n\n const resolvedModifiers = useMemo(() => {\n const restrictToTableContainer: Modifier = ({\n transform,\n draggingNodeRect,\n }) => {\n if (!tableContainerRef.current || !draggingNodeRect) {\n return transform\n }\n\n const containerRect = tableContainerRef.current.getBoundingClientRect()\n const { x, y } = transform\n\n const minX = containerRect.left - draggingNodeRect.left\n const maxX = containerRect.right - draggingNodeRect.right\n const minY = containerRect.top - draggingNodeRect.top\n const maxY = containerRect.bottom - draggingNodeRect.bottom\n\n return {\n ...transform,\n // The horizontal rail only engages while the default axis restriction\n // is in force. A row is exactly as wide as the viewport, so minX and\n // maxX both collapse to 0 and clamping x erases it entirely: harmless\n // under restrictToVerticalAxis, which zeroes x anyway, but fatal for a\n // caller that replaced the restriction precisely to READ x, as a tree\n // does to resolve drop depth. Vertical is railed either way, which is\n // what actually keeps a dragged row inside the grid.\n x: modifiers ? x : Math.max(minX, Math.min(maxX, x)),\n y: Math.max(minY, Math.min(maxY, y)),\n }\n }\n\n // The container clamp is a safety rail rather than a policy, so it stays\n // applied even when the caller replaces the axis restriction.\n return [\n ...(modifiers ?? [restrictToVerticalAxis]),\n restrictToTableContainer,\n ]\n }, [modifiers])\n\n return (\n {\n setIsDraggingRow(false)\n setCarried(null)\n onDragCancel?.(event)\n }}\n onDragEnd={(event) => {\n setIsDraggingRow(false)\n setCarried(null)\n handleDragEnd(event)\n }}\n onDragMove={onDragMove}\n onDragOver={onDragOver}\n onDragStart={(event) => {\n setIsDraggingRow(true)\n pickUpRow(event.active.id)\n onDragStart?.(event)\n }}\n sensors={sensors}\n >\n \n \n \n {table\n .getHeaderGroups()\n .map((headerGroup: HeaderGroup, index) => {\n return (\n \n {headerGroup.headers.map((header, index) => {\n const { column } = header\n\n return (\n \n {header.isPlaceholder ? null : props.tableLayout\n ?.columnsResizable && column.getCanResize() ? (\n
\n {flexRender(\n header.column.columnDef.header,\n header.getContext()\n )}\n
\n ) : (\n flexRender(\n header.column.columnDef.header,\n header.getContext()\n )\n )}\n {props.tableLayout?.columnsResizable &&\n column.getCanResize() && (\n \n )}\n
\n )\n })}\n \n
\n )\n })}\n
\n\n {(props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && (\n \n )}\n\n \n \n \n\n {footerContent && (\n {footerContent}\n )}\n
\n \n\n {/* The row you are actually holding. It is a real clone rendered outside\n the table, which is the only way a dragged row can follow the pointer\n without disturbing the grid: it adds no cell, so it cannot alter the\n column widths, and it floats above the rows rather than through them.\n Its presence also tells dnd-kit to stop translating the source row, so\n the row left behind simply dims in place. */}\n \n {carried && carriedRow ? (\n \n
\n {/* Padding rides on the inner element, not the cell. A `td` can\n never render narrower than its own horizontal padding, so a\n column resized below that would silently widen here and the\n clone would stop matching the row it came from. */}\n td]:h-14 [&>td]:p-0 [&>td]:align-middle\">\n {carriedRow\n .getVisibleCells()\n .map((cell: Cell, index: number) => (\n \n
\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext()\n )}\n
\n \n ))}\n
\n \n
\n ) : null}\n \n \n )\n}\n\nexport { DataGridTableDndRowHandle, DataGridTableDndRows }\nexport type { DataGridTableDndRowData, DataGridTableDndRowDecoration }","target":"components/neui/data-grid/data-grid-table-dnd-rows.tsx"},{"path":"data-grid-table-dnd.tsx","type":"registry:ui","content":"\"use client\"\n\"use no memo\"\n\nimport {\n Fragment,\n memo,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from \"react\"\nimport type { CSSProperties, ReactNode } from \"react\"\nimport { useDataGrid } from \"@/components/neui/data-grid/data-grid\"\nimport {\n DataGridTableBase,\n DataGridTableBody,\n DataGridTableBodyRow,\n DataGridTableBodyRowCell,\n DataGridTableBodyRowExpandded,\n DataGridTableBodyRowSkeleton,\n DataGridTableBodyRowSkeletonCell,\n DataGridTableEmpty,\n DataGridTableFillBodyCell,\n DataGridTableFillHeadCell,\n DataGridTableFoot,\n DataGridTableHead,\n DataGridTableHeadRow,\n DataGridTableHeadRowCell,\n DataGridTableHeadRowCellResize,\n DataGridTableRowSpacer,\n DataGridTableViewport,\n} from \"@/components/neui/data-grid/data-grid-table\"\nimport {\n closestCenter,\n DndContext,\n KeyboardSensor,\n MouseSensor,\n TouchSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n type Modifier,\n} from \"@dnd-kit/core\"\nimport {\n horizontalListSortingStrategy,\n SortableContext,\n sortableKeyboardCoordinates,\n useSortable,\n} from \"@dnd-kit/sortable\"\nimport { CSS } from \"@dnd-kit/utilities\"\nimport { flexRender } from \"@tanstack/react-table\"\nimport type {\n Cell,\n Header,\n HeaderGroup,\n Row,\n Table,\n} from \"@tanstack/react-table\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\nfunction DataGridTableDndHeader({\n header,\n}: {\n header: Header\n}) {\n const { props } = useDataGrid()\n const { column } = header\n\n // Check if column ordering is enabled for this column\n const canOrder =\n (column.columnDef as { enableColumnOrdering?: boolean })\n .enableColumnOrdering !== false\n\n const {\n attributes,\n isDragging,\n listeners,\n setNodeRef,\n transform,\n transition,\n } = useSortable({\n id: header.column.id,\n })\n\n const style: CSSProperties = {\n opacity: isDragging ? 0.8 : 1,\n position: \"relative\",\n transform: CSS.Translate.toString(transform),\n transition,\n cursor: isDragging ? \"grabbing\" : undefined,\n whiteSpace: \"nowrap\",\n width: props.tableLayout?.columnsResizable\n ? `calc(var(--header-${header.id}-size) * 1px)`\n : header.column.getSize(),\n zIndex: isDragging ? 1 : 0,\n }\n\n return (\n \n
\n {canOrder && (\n \n \n \n )}\n
\n {header.isPlaceholder\n ? null\n : flexRender(header.column.columnDef.header, header.getContext())}\n
\n {props.tableLayout?.columnsResizable && column.getCanResize() && (\n \n )}\n
\n \n )\n}\n\nfunction DataGridTableDndCell({ cell }: { cell: Cell }) {\n const { props } = useDataGrid()\n const { isDragging, setNodeRef, transform, transition } = useSortable({\n id: cell.column.id,\n })\n\n const style: CSSProperties = {\n opacity: isDragging ? 0.8 : 1,\n position: \"relative\",\n transform: CSS.Translate.toString(transform),\n transition,\n cursor: isDragging ? \"grabbing\" : undefined,\n width: props.tableLayout?.columnsResizable\n ? `calc(var(--col-${cell.column.id}-size) * 1px)`\n : cell.column.getSize(),\n zIndex: isDragging ? 1 : 0,\n }\n\n return (\n \n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n \n )\n}\n\nfunction DataGridTableDndBodyRows({ table }: { table: Table }) {\n const { isLoading, props } = useDataGrid()\n const pagination = table.getState().pagination\n\n if (props.loadingMode === \"skeleton\" && isLoading && pagination?.pageSize) {\n return (\n <>\n {Array.from({ length: pagination.pageSize }).map((_, rowIndex) => (\n \n {table.getVisibleFlatColumns().map((column, colIndex) => {\n return (\n \n {column.columnDef.meta?.skeleton}\n \n )\n })}\n \n \n ))}\n \n )\n }\n\n if (!table.getRowModel().rows.length) return \n\n return (\n <>\n {table.getRowModel().rows.map((row: Row) => {\n return (\n \n \n \n {row.getVisibleCells().map((cell: Cell) => (\n \n ))}\n \n \n \n {row.getIsExpanded() && }\n \n )\n })}\n \n )\n}\n\n/**\n * Memoized body rows: skip re-renders during active column resize.\n * Column widths update via CSS variables on the element,\n * so the browser handles width changes without React re-renders.\n */\nconst MemoizedDataGridTableDndBodyRows = memo(\n DataGridTableDndBodyRows,\n (_prev, next) => !!next.table.getState().columnSizingInfo.isResizingColumn\n) as typeof DataGridTableDndBodyRows\n\nfunction DataGridTableDnd({\n handleDragEnd,\n footerContent,\n}: {\n handleDragEnd: (event: DragEndEvent) => void\n footerContent?: ReactNode\n}) {\n const { table, props } = useDataGrid()\n const containerRef = useRef(null)\n const [isDraggingColumn, setIsDraggingColumn] = useState(false)\n\n const sensors = useSensors(\n useSensor(MouseSensor, {}),\n useSensor(TouchSensor, {}),\n // Keyboard reordering moves one sortable position per keypress instead\n // of the sensor's raw 25px default.\n useSensor(KeyboardSensor, {\n coordinateGetter: sortableKeyboardCoordinates,\n })\n )\n\n useEffect(() => {\n if (!isDraggingColumn) return\n\n const { body, documentElement } = document\n const previousBodyCursor = body.style.cursor\n const previousDocumentCursor = documentElement.style.cursor\n\n body.style.cursor = \"grabbing\"\n documentElement.style.cursor = \"grabbing\"\n\n return () => {\n body.style.cursor = previousBodyCursor\n documentElement.style.cursor = previousDocumentCursor\n }\n }, [isDraggingColumn])\n\n // Custom modifier to restrict dragging within table bounds with edge offset\n const modifiers = useMemo(() => {\n const restrictToTableBounds: Modifier = ({\n draggingNodeRect,\n transform,\n }) => {\n if (!draggingNodeRect || !containerRef.current) {\n return { ...transform, y: 0 }\n }\n\n const containerRect = containerRef.current.getBoundingClientRect()\n const edgeOffset = 0\n\n const minX = containerRect.left - draggingNodeRect.left - edgeOffset\n const maxX =\n containerRect.right -\n draggingNodeRect.left -\n draggingNodeRect.width +\n edgeOffset\n\n return {\n ...transform,\n x: Math.min(Math.max(transform.x, minX), maxX),\n y: 0, // Lock vertical movement\n }\n }\n\n return [restrictToTableBounds]\n }, [])\n\n return (\n setIsDraggingColumn(false)}\n onDragEnd={(event) => {\n setIsDraggingColumn(false)\n handleDragEnd(event)\n }}\n onDragStart={() => setIsDraggingColumn(true)}\n sensors={sensors}\n >\n \n \n \n {table\n .getHeaderGroups()\n .map((headerGroup: HeaderGroup, index) => {\n return (\n \n \n {headerGroup.headers.map((header) => (\n \n ))}\n \n \n \n )\n })}\n \n\n {(props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && (\n \n )}\n\n \n \n \n\n {footerContent && (\n {footerContent}\n )}\n \n \n \n )\n}\n\nexport { DataGridTableDnd }","target":"components/neui/data-grid/data-grid-table-dnd.tsx"},{"path":"data-grid-table-virtual.tsx","type":"registry:ui","content":"\"use client\"\n\"use no memo\"\n\nimport { memo, useCallback, useEffect, useRef, useState } from \"react\"\nimport type { CSSProperties, ReactNode } from \"react\"\nimport { useDataGrid } from \"@/components/neui/data-grid/data-grid\"\nimport {\n DataGridTableBase,\n DataGridTableBody,\n DataGridTableEmpty,\n DataGridTableFillBodyCell,\n DataGridTableFillHeadCell,\n DataGridTableFoot,\n DataGridTableHead,\n DataGridTableHeadRow,\n DataGridTableHeadRowCell,\n DataGridTableHeadRowCellResize,\n DataGridTableRenderedRow,\n DataGridTableRowSpacer,\n DataGridTableViewport,\n getDataGridScrollAreaViewport,\n getDataGridTableMergedHeaderGroups,\n getDataGridTableRowSections,\n getPinningStyles,\n hasDataGridTableRightPinnedColumns,\n} from \"@/components/neui/data-grid/data-grid-table\"\nimport { flexRender } from \"@tanstack/react-table\"\nimport type { Column, Row, Table } from \"@tanstack/react-table\"\nimport { useVirtualizer } from \"@tanstack/react-virtual\"\nimport type {\n VirtualItem,\n Virtualizer,\n VirtualizerOptions,\n} from \"@tanstack/react-virtual\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Spinner } from \"@/components/ui/spinner\"\n\ntype DataGridTableVirtualScrollElements = {\n containerElement: HTMLDivElement | null\n scrollElement: HTMLElement | null\n}\n\ntype DataGridTableVirtualizerInstance = Virtualizer<\n HTMLElement,\n HTMLTableRowElement\n>\n\ntype DataGridTableVirtualScrollAlignment = \"auto\" | \"center\" | \"start\" | \"end\"\n\ntype DataGridTableVirtualScrollRequest = {\n align: DataGridTableVirtualScrollAlignment\n behavior: ScrollBehavior\n containerElement: HTMLDivElement\n headerSticky: boolean\n isVirtualizationEnabled: boolean\n rowId: string | undefined\n rowIndex: number\n scrollElement: HTMLElement\n}\n\nfunction isSameDataGridTableScrollRequest(\n previous: DataGridTableVirtualScrollRequest | null,\n next: DataGridTableVirtualScrollRequest\n) {\n return (\n previous?.align === next.align &&\n previous.behavior === next.behavior &&\n previous.containerElement === next.containerElement &&\n previous.headerSticky === next.headerSticky &&\n previous.isVirtualizationEnabled === next.isVirtualizationEnabled &&\n previous.rowId === next.rowId &&\n previous.rowIndex === next.rowIndex &&\n previous.scrollElement === next.scrollElement\n )\n}\n\nfunction getDataGridTableScrollTarget({\n align,\n clientHeight,\n rowBottom,\n rowHeight,\n rowTop,\n scrollHeight,\n scrollTop,\n viewportTopOffset = 0,\n}: {\n align: DataGridTableVirtualScrollAlignment\n clientHeight: number\n rowBottom: number\n rowHeight: number\n rowTop: number\n scrollHeight: number\n scrollTop: number\n viewportTopOffset?: number\n}) {\n const visibleHeight = Math.max(0, clientHeight - viewportTopOffset)\n const viewportTop = scrollTop + viewportTopOffset\n const viewportBottom = scrollTop + clientHeight\n\n const targetTop =\n align === \"auto\"\n ? rowTop < viewportTop\n ? rowTop - viewportTopOffset\n : rowBottom > viewportBottom\n ? rowBottom - clientHeight\n : null\n : align === \"start\"\n ? rowTop - viewportTopOffset\n : align === \"end\"\n ? rowBottom - clientHeight\n : rowTop -\n viewportTopOffset -\n Math.max(0, (visibleHeight - rowHeight) / 2)\n\n if (targetTop === null) return null\n\n return Math.min(\n Math.max(0, targetTop),\n Math.max(0, scrollHeight - clientHeight)\n )\n}\n\nfunction getDataGridTableHeaderOffset({\n containerElement,\n headerSticky,\n scrollElement,\n}: {\n containerElement: HTMLDivElement\n headerSticky: boolean\n scrollElement: HTMLElement\n}) {\n if (!headerSticky) return 0\n\n const headerElement = containerElement.querySelector(\n ':scope > [data-slot=\"data-grid-table\"] > thead'\n )\n\n if (!headerElement) return 0\n\n const scrollRect = scrollElement.getBoundingClientRect()\n const headerRect = headerElement.getBoundingClientRect()\n const headerBottomOffset = headerRect.bottom - scrollRect.top\n const overlapsViewportTop =\n headerRect.top <= scrollRect.top + 0.5 && headerBottomOffset > 0\n\n if (!overlapsViewportTop) return 0\n\n return Math.min(scrollElement.clientHeight, Math.max(0, headerBottomOffset))\n}\n\nfunction scrollDataGridTableToOffset({\n behavior,\n scrollElement,\n targetTop,\n virtualizer,\n}: {\n behavior: ScrollBehavior\n scrollElement: HTMLElement\n targetTop: number\n virtualizer?: DataGridTableVirtualizerInstance\n}) {\n if (virtualizer) {\n virtualizer.scrollToOffset(targetTop, { align: \"start\", behavior })\n } else if (typeof scrollElement.scrollTo === \"function\") {\n scrollElement.scrollTo({ behavior, top: targetTop })\n } else {\n scrollElement.scrollTop = targetTop\n }\n}\n\nfunction scrollDataGridTableRowIntoView({\n align,\n behavior,\n cancelPendingScroll = false,\n containerElement,\n headerSticky,\n rowIndex,\n scrollElement,\n virtualizer,\n}: {\n align: DataGridTableVirtualScrollAlignment\n behavior: ScrollBehavior\n cancelPendingScroll?: boolean\n containerElement: HTMLDivElement | null\n headerSticky: boolean\n rowIndex: number\n scrollElement: HTMLElement | null\n virtualizer?: DataGridTableVirtualizerInstance\n}) {\n if (!containerElement || !scrollElement) return false\n\n const rowElement = containerElement.querySelector(\n `:scope > [data-slot=\"data-grid-table\"] > tbody > tr[data-index=\"${rowIndex}\"]`\n )\n\n if (!rowElement) return false\n\n const scrollRect = scrollElement.getBoundingClientRect()\n const rowRect = rowElement.getBoundingClientRect()\n const viewportTopOffset = getDataGridTableHeaderOffset({\n containerElement,\n headerSticky,\n scrollElement,\n })\n const rowTop = scrollElement.scrollTop + rowRect.top - scrollRect.top\n const rowBottom = scrollElement.scrollTop + rowRect.bottom - scrollRect.top\n const targetTop = getDataGridTableScrollTarget({\n align,\n clientHeight: scrollElement.clientHeight,\n rowBottom,\n rowHeight: rowRect.height || rowElement.offsetHeight,\n rowTop,\n scrollHeight: scrollElement.scrollHeight,\n scrollTop: scrollElement.scrollTop,\n viewportTopOffset,\n })\n\n if (\n targetTop === null ||\n Math.abs(targetTop - scrollElement.scrollTop) < 0.5\n ) {\n if (cancelPendingScroll) {\n scrollDataGridTableToOffset({\n behavior: \"auto\",\n scrollElement,\n targetTop: scrollElement.scrollTop,\n virtualizer,\n })\n }\n\n return true\n }\n\n scrollDataGridTableToOffset({\n behavior,\n scrollElement,\n targetTop,\n virtualizer,\n })\n\n return true\n}\n\ntype DataGridTableVirtualizerOptions = Omit<\n VirtualizerOptions,\n \"count\" | \"estimateSize\" | \"getItemKey\" | \"getScrollElement\"\n> & {\n estimateSize?: (index: number, row: Row) => number\n getItemKey?: (index: number, row: Row) => string | number\n getScrollElement?: (\n elements: DataGridTableVirtualScrollElements\n ) => HTMLElement | null\n}\n\ninterface DataGridTableVirtualProps {\n height?: number | string\n estimateSize?: number\n overscan?: number\n /** Scroll animation used when revealing a controlled target row. */\n scrollBehavior?: ScrollBehavior\n /** Alignment used when revealing a controlled target row. Defaults to auto. */\n scrollToRowAlign?: DataGridTableVirtualScrollAlignment\n /** Index within the center (non-pinned) row section to reveal. */\n scrollToRowIndex?: number\n footerContent?: ReactNode\n renderHeader?: boolean\n onFetchMore?: () => void\n isFetchingMore?: boolean\n hasMore?: boolean\n fetchMoreOffset?: number\n virtualizerOptions?: DataGridTableVirtualizerOptions\n}\n\ninterface VirtualBodyProps {\n table: Table\n topRows: Row[]\n centerRows: Row[]\n bottomRows: Row[]\n virtualItems: VirtualItem[]\n totalSize: number\n isVirtualizationEnabled: boolean\n isInfiniteMode: boolean\n isFetchingMore: boolean\n hasMore?: boolean\n loadingMoreMessage: ReactNode\n allRowsLoadedMessage: ReactNode\n measureRowRef?: (element: HTMLTableRowElement | null) => void\n}\n\nfunction DataGridTableVirtualPinnedPlaceholderCell({\n column,\n}: {\n column: Column\n}) {\n const { props } = useDataGrid()\n const isPinned = column.getIsPinned()\n const isLastLeftPinned = isPinned === \"left\" && column.getIsLastColumn(\"left\")\n const isFirstRightPinned =\n isPinned === \"right\" && column.getIsFirstColumn(\"right\")\n\n return (\n \n )\n}\n\nfunction DataGridTableVirtualUtilityRow({\n table,\n children,\n centerCellClassName,\n centerCellStyle,\n rowClassName,\n ariaHidden,\n}: {\n table: Table\n children: ReactNode\n centerCellClassName?: string\n centerCellStyle?: CSSProperties\n rowClassName?: string\n ariaHidden?: boolean\n}) {\n const { props } = useDataGrid()\n const leftVisibleColumns = table.getLeftVisibleLeafColumns()\n const centerVisibleColumns = table.getCenterVisibleLeafColumns()\n const rightVisibleColumns = table.getRightVisibleLeafColumns()\n const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table)\n\n return (\n \n {leftVisibleColumns.map((column) => (\n \n ))}\n \n {children}\n \n {props.tableLayout?.columnsResizable && hasRightPinnedColumns ? (\n \n ) : null}\n {rightVisibleColumns.map((column) => (\n \n ))}\n {props.tableLayout?.columnsResizable && !hasRightPinnedColumns ? (\n \n ) : null}\n \n )\n}\n\nfunction DataGridTableVirtualSpacer({\n table,\n height,\n}: {\n table: Table\n height: number\n}) {\n if (height <= 0) return null\n\n return (\n \n {null}\n \n )\n}\n\nfunction DataGridTableVirtualStatusRow({\n table,\n children,\n className,\n}: {\n table: Table\n children: ReactNode\n className?: string\n}) {\n return (\n \n {children}\n \n )\n}\n\nfunction DataGridTableVirtualBody({\n table,\n topRows,\n centerRows,\n bottomRows,\n virtualItems,\n totalSize,\n isVirtualizationEnabled,\n isInfiniteMode,\n isFetchingMore,\n hasMore,\n loadingMoreMessage,\n allRowsLoadedMessage,\n measureRowRef,\n}: VirtualBodyProps) {\n const { isLoading } = useDataGrid()\n const totalRows = topRows.length + centerRows.length + bottomRows.length\n\n if (!totalRows) {\n // Initial load must not flash the empty state as if the query returned\n // nothing.\n if (isLoading) {\n return (\n \n
\n \n {loadingMoreMessage}\n
\n
\n )\n }\n\n return \n }\n\n const hasCenterRows = centerRows.length > 0\n const showFetchingRow = isInfiniteMode && isFetchingMore\n const showCompleteRow = isInfiniteMode && hasMore === false && totalRows > 0\n const hasMiddleSection = hasCenterRows || showFetchingRow || showCompleteRow\n const leadingSpacerHeight =\n isVirtualizationEnabled && hasCenterRows && virtualItems.length > 0\n ? (virtualItems[0]?.start ?? 0)\n : 0\n const trailingSpacerHeight =\n isVirtualizationEnabled && hasCenterRows && virtualItems.length > 0\n ? Math.max(\n 0,\n totalSize - (virtualItems[virtualItems.length - 1]?.end ?? 0)\n )\n : 0\n\n const renderedRows: ReactNode[] = []\n\n topRows.forEach((row, index) => {\n renderedRows.push(\n \n )\n })\n\n if (isVirtualizationEnabled) {\n if (leadingSpacerHeight > 0) {\n renderedRows.push(\n \n )\n }\n\n virtualItems.forEach((virtualRow) => {\n const row = centerRows[virtualRow.index]\n\n if (!row) return\n\n renderedRows.push(\n \n )\n })\n\n if (trailingSpacerHeight > 0) {\n renderedRows.push(\n \n )\n }\n } else {\n centerRows.forEach((row, rowIndex) => {\n renderedRows.push(\n \n )\n })\n }\n\n if (showFetchingRow) {\n renderedRows.push(\n \n
\n \n {loadingMoreMessage}\n
\n
\n )\n }\n\n if (showCompleteRow) {\n renderedRows.push(\n \n {allRowsLoadedMessage}\n \n )\n }\n\n bottomRows.forEach((row, index) => {\n renderedRows.push(\n 0 || hasMiddleSection)\n ? \"bottom\"\n : undefined\n }\n />\n )\n })\n\n return <>{renderedRows}\n}\n\n/**\n * Memoized virtual body: skip re-renders during active column resize.\n * Column widths update via CSS variables on the
element,\n * so the browser handles width changes without React re-renders.\n */\nconst MemoizedVirtualBody = memo(\n DataGridTableVirtualBody,\n (_prev, next) => !!next.table.getState().columnSizingInfo.isResizingColumn\n) as typeof DataGridTableVirtualBody\n\nfunction DataGridTableVirtual({\n height,\n estimateSize = 48,\n overscan = 10,\n scrollBehavior = \"auto\",\n scrollToRowAlign = \"auto\",\n scrollToRowIndex,\n footerContent,\n renderHeader = true,\n onFetchMore,\n isFetchingMore = false,\n hasMore,\n fetchMoreOffset = 0,\n virtualizerOptions,\n}: DataGridTableVirtualProps) {\n const { table, props } = useDataGrid()\n const mergedHeaderGroups = getDataGridTableMergedHeaderGroups(table)\n const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table)\n const { topRows, centerRows, bottomRows } = getDataGridTableRowSections(\n table,\n props.tableLayout?.rowsPinnable\n )\n const isInfiniteMode = typeof onFetchMore === \"function\"\n const [viewportElements, setViewportElements] =\n useState({\n containerElement: null,\n scrollElement: null,\n })\n\n const {\n estimateSize: customEstimateSize,\n getItemKey: customGetItemKey,\n getScrollElement: customGetScrollElement,\n measureElement: customMeasureElement,\n overscan: customOverscan,\n ...virtualizerOptionsRest\n } = virtualizerOptions ?? {}\n\n const isVirtualizationEnabled = virtualizerOptions?.enabled !== false\n const loadingMoreMessage =\n props.fetchingMoreMessage || props.loadingMessage || \"Loading...\"\n const allRowsLoadedMessage =\n props.allRowsLoadedMessage || \"All records loaded\"\n\n const handleViewportRef = useCallback((node: HTMLDivElement | null) => {\n setViewportElements({\n containerElement: node,\n scrollElement: node\n ? (getDataGridScrollAreaViewport(node) ?? node)\n : null,\n })\n }, [])\n\n const usesExternalScrollArea =\n viewportElements.scrollElement !== null &&\n viewportElements.scrollElement !== viewportElements.containerElement\n\n const resolveScrollElement = useCallback(() => {\n if (customGetScrollElement) {\n return customGetScrollElement(viewportElements)\n }\n\n return viewportElements.scrollElement\n }, [customGetScrollElement, viewportElements])\n\n const resolveItemKey = useCallback(\n (index: number) => {\n const row = centerRows[index]\n\n if (!row) return index\n\n return customGetItemKey?.(index, row) ?? row.id ?? index\n },\n [centerRows, customGetItemKey]\n )\n\n const resolveEstimateSize = useCallback(\n (index: number) => {\n const row = centerRows[index]\n\n return row\n ? (customEstimateSize?.(index, row) ?? estimateSize)\n : estimateSize\n },\n [centerRows, customEstimateSize, estimateSize]\n )\n\n const virtualizer = useVirtualizer({\n count: centerRows.length,\n getScrollElement: resolveScrollElement,\n getItemKey: resolveItemKey,\n estimateSize: resolveEstimateSize,\n overscan: customOverscan ?? overscan,\n measureElement: customMeasureElement,\n ...virtualizerOptionsRest,\n }) as DataGridTableVirtualizerInstance\n\n const virtualItems = isVirtualizationEnabled\n ? virtualizer.getVirtualItems()\n : []\n const totalSize = isVirtualizationEnabled ? virtualizer.getTotalSize() : 0\n const measureRowRef =\n isVirtualizationEnabled && customMeasureElement\n ? virtualizer.measureElement\n : undefined\n const resolvedFetchMoreOffset = Math.max(0, fetchMoreOffset)\n const scrollToRowId =\n scrollToRowIndex !== undefined\n ? centerRows[scrollToRowIndex]?.id\n : undefined\n const scrollToRowVirtualItem =\n isVirtualizationEnabled && scrollToRowIndex !== undefined\n ? virtualItems.find((item) => item.index === scrollToRowIndex)\n : undefined\n const pendingScrollToRowIndexRef = useRef(null)\n const lastScrollRequestRef = useRef(\n null\n )\n // Latch onFetchMore per row count: virtualItems gets a new identity every\n // scroll frame, so without it the effect fires duplicate page requests\n // before the consumer flips isFetchingMore, and loops at end-of-data when\n // hasMore is never set.\n const fetchMoreFiredAtCountRef = useRef(null)\n\n // Resolve after every commit so a stable getter can expose a replaced ref;\n // the request signature prevents duplicate scrolling on ordinary renders.\n useEffect(() => {\n const previousRequest = lastScrollRequestRef.current\n\n if (\n scrollToRowIndex === undefined ||\n scrollToRowIndex < 0 ||\n scrollToRowIndex >= centerRows.length\n ) {\n pendingScrollToRowIndexRef.current = null\n lastScrollRequestRef.current = null\n\n if (previousRequest) {\n const scrollElement = resolveScrollElement()\n\n if (scrollElement) {\n scrollDataGridTableToOffset({\n behavior: \"auto\",\n scrollElement,\n targetTop: scrollElement.scrollTop,\n virtualizer: isVirtualizationEnabled ? virtualizer : undefined,\n })\n }\n }\n\n return\n }\n\n const scrollElement = resolveScrollElement()\n const containerElement = viewportElements.containerElement\n if (!containerElement || !scrollElement) return\n\n const headerSticky = renderHeader && !!props.tableLayout?.headerSticky\n const nextRequest: DataGridTableVirtualScrollRequest = {\n align: scrollToRowAlign,\n behavior: scrollBehavior,\n containerElement,\n headerSticky,\n isVirtualizationEnabled,\n rowId: scrollToRowId,\n rowIndex: scrollToRowIndex,\n scrollElement,\n }\n\n if (isSameDataGridTableScrollRequest(previousRequest, nextRequest)) return\n\n pendingScrollToRowIndexRef.current = null\n\n const rowWasHandled = scrollDataGridTableRowIntoView({\n align: scrollToRowAlign,\n behavior: scrollBehavior,\n cancelPendingScroll: previousRequest !== null,\n containerElement,\n headerSticky,\n rowIndex: scrollToRowIndex,\n scrollElement,\n virtualizer: isVirtualizationEnabled ? virtualizer : undefined,\n })\n\n if (rowWasHandled) {\n lastScrollRequestRef.current = nextRequest\n return\n }\n\n if (!isVirtualizationEnabled) return\n\n pendingScrollToRowIndexRef.current = scrollToRowIndex\n lastScrollRequestRef.current = nextRequest\n virtualizer.scrollToIndex(scrollToRowIndex, {\n align: scrollToRowAlign,\n behavior: scrollBehavior,\n })\n })\n\n useEffect(() => {\n if (\n !isVirtualizationEnabled ||\n scrollToRowIndex === undefined ||\n pendingScrollToRowIndexRef.current !== scrollToRowIndex ||\n !scrollToRowVirtualItem\n ) {\n return\n }\n\n const rowWasHandled = scrollDataGridTableRowIntoView({\n align: scrollToRowAlign,\n behavior: \"auto\",\n cancelPendingScroll: true,\n containerElement: viewportElements.containerElement,\n headerSticky: renderHeader && !!props.tableLayout?.headerSticky,\n rowIndex: scrollToRowIndex,\n scrollElement: resolveScrollElement(),\n virtualizer,\n })\n\n if (rowWasHandled) {\n pendingScrollToRowIndexRef.current = null\n }\n }, [\n isVirtualizationEnabled,\n props.tableLayout?.headerSticky,\n renderHeader,\n resolveScrollElement,\n scrollToRowAlign,\n scrollToRowIndex,\n scrollToRowVirtualItem,\n virtualizer,\n viewportElements.containerElement,\n ])\n\n useEffect(() => {\n if (\n !isVirtualizationEnabled ||\n !isInfiniteMode ||\n hasMore === false ||\n isFetchingMore\n ) {\n return\n }\n\n const lastItem = virtualItems[virtualItems.length - 1]\n if (!lastItem) return\n\n if (fetchMoreFiredAtCountRef.current === centerRows.length) return\n\n if (lastItem.index >= centerRows.length - 1 - resolvedFetchMoreOffset) {\n fetchMoreFiredAtCountRef.current = centerRows.length\n onFetchMore?.()\n }\n }, [\n centerRows.length,\n hasMore,\n isFetchingMore,\n isInfiniteMode,\n isVirtualizationEnabled,\n onFetchMore,\n resolvedFetchMoreOffset,\n virtualItems,\n ])\n\n return (\n \n \n {renderHeader && (\n \n {mergedHeaderGroups.map((headerGroup) => (\n \n {headerGroup.headers\n .filter((header) => header.column.getIsPinned() !== \"right\")\n .map((header) => {\n const { column } = header\n\n return (\n \n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext()\n )}\n {props.tableLayout?.columnsResizable &&\n column.getCanResize() && (\n \n )}\n \n )\n })}\n {props.tableLayout?.columnsResizable &&\n hasRightPinnedColumns ? (\n \n ) : null}\n {headerGroup.headers\n .filter((header) => header.column.getIsPinned() === \"right\")\n .map((header) => {\n const { column } = header\n\n return (\n \n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext()\n )}\n {props.tableLayout?.columnsResizable &&\n column.getCanResize() && (\n \n )}\n \n )\n })}\n {props.tableLayout?.columnsResizable &&\n !hasRightPinnedColumns ? (\n \n ) : null}\n \n ))}\n \n )}\n\n {renderHeader &&\n (props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && (\n \n )}\n\n \n \n \n\n {footerContent && (\n {footerContent}\n )}\n \n \n )\n}\n\nexport { DataGridTableVirtual }\nexport type {\n DataGridTableVirtualScrollAlignment,\n DataGridTableVirtualProps,\n DataGridTableVirtualScrollElements,\n DataGridTableVirtualizerOptions,\n}","target":"components/neui/data-grid/data-grid-table-virtual.tsx"},{"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