{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"data-grid-table-dnd-rows","type":"registry:ui","title":"Data Grid Table Dnd Rows","description":"","dependencies":["@dnd-kit/core","@dnd-kit/modifiers","@dnd-kit/sortable","@dnd-kit/utilities","@tanstack/react-table"],"registryDependencies":["button","@neui/data-grid","@neui/data-grid-table"],"files":[{"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"}]}