{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "draggable-dashboard", "type": "registry:component", "title": "DraggableDashboard", "author": "codefellas ", "description": "A draggable dashboard component.", "dependencies": [ "lucide-react", "@dnd-kit/core", "@dnd-kit/sortable", "@dnd-kit/utilities" ], "registryDependencies": [ "@fellasUI/switch", "@fellasUI/label", "@fellasUI/separator", "@fellasUI/utils" ], "files": [ { "path": "registry/@fellas/ui/draggable-dashboard/draggable-dashboard.tsx", "content": "\"use client\"\n\nimport React, { useState, ReactNode, Children, isValidElement, useEffect, useMemo } from 'react'\nimport {\n DndContext,\n closestCenter,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n DragEndEvent,\n DragOverlay,\n DragStartEvent,\n} from '@dnd-kit/core'\nimport {\n arrayMove,\n SortableContext,\n sortableKeyboardCoordinates,\n rectSortingStrategy,\n} from '@dnd-kit/sortable'\nimport {\n useSortable,\n} from '@dnd-kit/sortable'\nimport { CSS } from '@dnd-kit/utilities'\nimport { Switch } from '@/registry/@fellas/ui/switch'\nimport { Label } from '@/registry/@fellas/ui/label'\nimport { Separator } from '@/registry/@fellas/ui/separator'\nimport {\n GripVertical,\n Lock,\n Unlock,\n} from 'lucide-react'\nimport { cn } from '@/lib/utils'\n\ninterface DraggableWrapperProps {\n id: string\n children: ReactNode\n gridSize?: {\n cols: number\n rows: number\n }\n className?: string\n isLocked?: boolean\n showHandle?: boolean\n availableCols?: number\n}\n\ninterface DraggableDashboardProps {\n children: ReactNode\n className?: string\n showLockToggle?: boolean\n showHandles?: boolean\n gridCols?: number\n gap?: number\n defaultLocked?: boolean\n onOrderChange?: (newOrder: string[]) => void\n persistenceKey?: string\n}\n\nexport function DraggableWrapper({\n id,\n children,\n gridSize = { cols: 1, rows: 1 },\n className,\n isLocked = false,\n showHandle = true,\n availableCols = 3,\n}: DraggableWrapperProps) {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({\n id,\n disabled: isLocked\n })\n\n const spanCols = Math.min(gridSize.cols, availableCols)\n const style: React.CSSProperties = {\n transform: CSS.Transform.toString(transform),\n transition,\n gridColumn: `span ${spanCols} / span ${spanCols}`,\n gridRow: `span ${gridSize.rows}`,\n }\n\n return (\n \n {!isLocked && showHandle && (\n \n \n \n )}\n\n {isLocked && showHandle && (\n \n \n \n )}\n\n
\n {children}\n
\n \n )\n}\n\nfunction DragOverlayWrapper({ children }: { children: ReactNode }) {\n return (\n
\n {children}\n
\n )\n}\n\nexport default function DraggableDashboard({\n children,\n className,\n showLockToggle = true,\n showHandles = true,\n gridCols = 3,\n gap = 6,\n defaultLocked = false,\n onOrderChange,\n persistenceKey = 'draggable-dashboard-order'\n}: DraggableDashboardProps) {\n const [isLocked, setIsLocked] = useState(defaultLocked)\n const [activeId, setActiveId] = useState(null)\n const [windowWidth, setWindowWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 0)\n\n useEffect(() => {\n const handler = () => setWindowWidth(window.innerWidth)\n window.addEventListener('resize', handler)\n return () => window.removeEventListener('resize', handler)\n }, [])\n\n const availableCols = windowWidth >= 1024 ? gridCols : windowWidth >= 768 ? 2 : 1\n\n const childrenArray = useMemo(() => Children.toArray(children), [children])\n const initialIds = useMemo(() => childrenArray.map((child, index) => {\n if (isValidElement(child) && child.props && typeof child.props === 'object' && 'id' in child.props) return child.props.id as string\n return `item-${index}`\n }), [childrenArray])\n const [itemOrder, setItemOrder] = useState(initialIds)\n useEffect(() => {\n if (typeof window === 'undefined') return\n try {\n const saved = localStorage.getItem(persistenceKey)\n if (saved) {\n const parsed: string[] = JSON.parse(saved)\n const valid = parsed.length === initialIds.length && parsed.every(id => initialIds.includes(id)) && initialIds.every(id => parsed.includes(id))\n if (valid) {\n const changed = parsed.some((id, i) => id !== initialIds[i])\n if (changed) setItemOrder(parsed)\n } else {\n localStorage.setItem(persistenceKey, JSON.stringify(initialIds))\n }\n } else {\n localStorage.setItem(persistenceKey, JSON.stringify(initialIds))\n }\n } catch { }\n }, [persistenceKey, initialIds])\n\n const sensors = useSensors(\n useSensor(PointerSensor, {\n activationConstraint: {\n distance: 8,\n },\n }),\n useSensor(KeyboardSensor, {\n coordinateGetter: sortableKeyboardCoordinates,\n })\n )\n\n function handleDragStart(event: DragStartEvent) {\n if (isLocked) return\n setActiveId(event.active.id as string)\n }\n\n function handleDragEnd(event: DragEndEvent) {\n const { active, over } = event\n\n if (active.id !== over?.id && over?.id) {\n setItemOrder((items) => {\n const oldIndex = items.findIndex((item) => item === active.id)\n const newIndex = items.findIndex((item) => item === over.id)\n\n const newOrder = arrayMove(items, oldIndex, newIndex)\n\n if (onOrderChange) {\n onOrderChange(newOrder)\n }\n\n return newOrder\n })\n }\n\n setActiveId(null)\n }\n\n const orderedChildren = itemOrder.map(id => {\n return childrenArray.find((child, index) => {\n if (isValidElement(child) && child.props && typeof child.props === 'object' && 'id' in child.props) {\n return child.props.id === id\n }\n return `item-${index}` === id\n })\n }).filter(Boolean)\n\n const activeChild = childrenArray.find((child, index) => {\n if (isValidElement(child) && child.props && typeof child.props === 'object' && 'id' in child.props) {\n return child.props.id === activeId\n }\n return `item-${index}` === activeId\n })\n\n const toggleLock = () => {\n setIsLocked(!isLocked)\n }\n\n const gapClasses = {\n 1: 'gap-1',\n 2: 'gap-2',\n 3: 'gap-3',\n 4: 'gap-4',\n 5: 'gap-5',\n 6: 'gap-6',\n 7: 'gap-7',\n 8: 'gap-8',\n 9: 'gap-9',\n 10: 'gap-10',\n 11: 'gap-11',\n 12: 'gap-12'\n } as Record\n const gapClass = gapClasses[gap] || 'gap-6'\n\n const gridColsClasses = ['lg:grid-cols-1', 'lg:grid-cols-2', 'lg:grid-cols-3', 'lg:grid-cols-4', 'lg:grid-cols-5', 'lg:grid-cols-6', 'lg:grid-cols-7', 'lg:grid-cols-8', 'lg:grid-cols-9', 'lg:grid-cols-10', 'lg:grid-cols-11', 'lg:grid-cols-12']\n const lgColsClass = gridCols >= 1 && gridCols <= 12 ? gridColsClasses[gridCols - 1] : 'lg:grid-cols-3'\n\n return (\n
\n {(showLockToggle) && (\n
\n
\n

Dashboard

\n

\n {isLocked ? \"Dashboard is locked\" : \"Drag items to customize your layout\"}\n

\n
\n\n
\n {showLockToggle && (\n
\n \n \n {isLocked ? (\n \n ) : (\n \n )}\n
\n )}\n
\n
\n )}\n\n \n\n \n \n \n {orderedChildren.map((child, index) => {\n if (!isValidElement(child)) return null\n\n const childProps = child.props && typeof child.props === 'object' ? child.props : {}\n const id = ('id' in childProps ? childProps.id : `item-${index}`) as string\n\n return React.createElement(child.type, {\n ...(child.props || {}),\n key: id,\n isLocked,\n showHandle: showHandles,\n availableCols,\n })\n })}\n
\n \n\n \n {activeChild && isValidElement(activeChild) ? (\n \n {React.createElement(activeChild.type, {\n ...(activeChild.props || {}),\n isLocked: true,\n showHandle: false,\n availableCols,\n key: 'overlay'\n })}\n \n ) : null}\n \n \n \n )\n}", "type": "registry:component", "target": "components/fellasUI/draggable-dashboard/draggable-dashboard.tsx" } ] }