/** * Drag-handle list reordering (direct-manipulation primitive). * * Pointer-based (no DnD library): grab the handle, rows reorder live as you * drag, commit on release. Used for ordering query group-by chips, dashboard * widget stacking, and anywhere an ordered list needs rearranging. */ import { useRef, useState, type ReactNode } from 'react'; import Box from '@mui/material/Box'; import { GripVertical } from 'lucide-react'; import { tokens } from '../../theme'; export interface DragListItem { id: string; content: ReactNode; } interface DragListProps { items: DragListItem[]; onReorder: (orderedIds: string[]) => void; dense?: boolean; } export default function DragList({ items, onReorder, dense = false }: DragListProps) { const [order, setOrder] = useState(null); // non-null while dragging const [dragId, setDragId] = useState(null); const containerRef = useRef(null); const ids = order ?? items.map((i) => i.id); const byId = new Map(items.map((i) => [i.id, i])); const indexFromPointer = (clientY: number): number => { const container = containerRef.current; if (!container) return 0; const rows = Array.from(container.querySelectorAll('[data-dragrow]')); for (let i = 0; i < rows.length; i += 1) { const r = rows[i].getBoundingClientRect(); if (clientY < r.top + r.height / 2) return i; } return rows.length - 1; }; return ( {ids.map((id) => { const item = byId.get(id); if (!item) return null; const dragging = dragId === id; return ( { e.preventDefault(); (e.target as Element).setPointerCapture(e.pointerId); setDragId(id); setOrder(items.map((i) => i.id)); }} onPointerMove={(e) => { if (dragId !== id || order === null) return; const target = indexFromPointer(e.clientY); const current = order.indexOf(id); if (target !== current && target >= 0) { const next = [...order]; next.splice(current, 1); next.splice(target, 0, id); setOrder(next); } }} onPointerUp={(e) => { (e.target as Element).releasePointerCapture(e.pointerId); if (order) onReorder(order); setOrder(null); setDragId(null); }} > {item.content} ); })} ); }