{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"sortable","type":"registry:ui","title":"Sortable","description":"","dependencies":["@dnd-kit/core","@dnd-kit/sortable","@dnd-kit/utilities","radix-ui"],"registryDependencies":[],"files":[{"path":"sortable.tsx","type":"registry:ui","content":"/* eslint-disable @typescript-eslint/no-explicit-any */\n\"use client\"\n\nimport * as React from \"react\"\nimport type {\n CSSProperties,\n HTMLAttributes,\n ReactElement,\n ReactNode,\n} from \"react\"\nimport {\n Children,\n cloneElement,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useMemo,\n useState,\n useSyncExternalStore,\n} from \"react\"\nimport type {\n DragCancelEvent,\n DragEndEvent,\n DragStartEvent,\n DropAnimation,\n Modifiers,\n UniqueIdentifier,\n} from \"@dnd-kit/core\"\nimport {\n defaultDropAnimationSideEffects,\n DndContext,\n DragOverlay,\n KeyboardSensor,\n MeasuringStrategy,\n MouseSensor,\n TouchSensor,\n useSensor,\n useSensors,\n type DraggableSyntheticListeners,\n} from \"@dnd-kit/core\"\nimport {\n arrayMove,\n defaultAnimateLayoutChanges,\n rectSortingStrategy,\n SortableContext,\n sortableKeyboardCoordinates,\n useSortable,\n verticalListSortingStrategy,\n type AnimateLayoutChanges,\n} from \"@dnd-kit/sortable\"\nimport { CSS } from \"@dnd-kit/utilities\"\nimport { Slot } from \"radix-ui\"\nimport { createPortal } from \"react-dom\"\n\nimport { cn } from \"@/lib/utils\"\n\n// Sortable Item Context\nconst SortableItemContext = createContext<{\n listeners: DraggableSyntheticListeners | undefined\n isDragging?: boolean\n disabled?: boolean\n}>({\n listeners: undefined,\n isDragging: false,\n disabled: false,\n})\n\nconst IsOverlayContext = createContext(false)\n\nconst SortableInternalContext = createContext<{\n activeId: UniqueIdentifier | null\n modifiers?: Modifiers\n}>({\n activeId: null,\n modifiers: undefined,\n})\n\nconst animateLayoutChanges: AnimateLayoutChanges = (args) =>\n defaultAnimateLayoutChanges({ ...args, wasDragging: true })\n\nconst dropAnimationConfig: DropAnimation = {\n sideEffects: defaultDropAnimationSideEffects({\n styles: {\n active: {\n opacity: \"0.4\",\n },\n },\n }),\n}\n\n/**\n * Client-mount gate for the `createPortal` calls below, which need\n * `document.body` and so must not run on the server or during hydration.\n *\n * A never-notifying subscription makes `useSyncExternalStore` return the server\n * snapshot (`false`) while rendering on the server and while hydrating, then the\n * client snapshot (`true`) once mounted - the same gate the previous\n * `useLayoutEffect(() => setMounted(true), [])` provided, minus the extra render\n * pass that `react-hooks/set-state-in-effect` flags. All three functions are\n * module-scoped so their identities stay stable; an inline `getSnapshot` is the\n * classic cause of an infinite re-subscribe loop.\n */\nconst subscribeToNothing = () => () => {}\nconst getIsMounted = () => true\nconst getIsMountedOnServer = () => false\n\nconst MOUSE_SENSOR_OPTIONS = { activationConstraint: { distance: 10 } }\nconst TOUCH_SENSOR_OPTIONS = {\n activationConstraint: { delay: 250, tolerance: 5 },\n}\nconst KEYBOARD_SENSOR_OPTIONS = {\n coordinateGetter: sortableKeyboardCoordinates,\n}\nconst MEASURING_CONFIG = {\n droppable: { strategy: MeasuringStrategy.Always },\n}\nconst STRATEGY_MAP = {\n horizontal: rectSortingStrategy,\n grid: rectSortingStrategy,\n vertical: verticalListSortingStrategy,\n} as const\n\n// Multipurpose Sortable Component\nexport interface SortableCommitMeta {\n event: DragEndEvent\n activeIndex: number\n overIndex: number\n previousValue: T[]\n}\n\nexport interface SortableRootProps extends Omit<\n HTMLAttributes,\n \"onDragStart\" | \"onDragEnd\"\n> {\n value: T[]\n onValueChange: (value: T[]) => void\n getItemValue: (item: T) => string\n children: ReactNode\n onMove?: (event: {\n event: DragEndEvent\n activeIndex: number\n overIndex: number\n }) => void\n onValueCommit?: (value: T[], meta: SortableCommitMeta) => void\n strategy?: \"horizontal\" | \"vertical\" | \"grid\"\n onDragStart?: (event: DragStartEvent) => void\n onDragEnd?: (event: DragEndEvent) => void\n onDragCancel?: (event: DragCancelEvent) => void\n accessibility?: React.ComponentProps[\"accessibility\"]\n modifiers?: Modifiers\n asChild?: boolean\n}\n\nfunction Sortable({\n value,\n onValueChange,\n getItemValue,\n className,\n asChild = false,\n onMove,\n onValueCommit,\n strategy = \"vertical\",\n onDragStart,\n onDragEnd,\n onDragCancel,\n accessibility,\n modifiers,\n children,\n ...props\n}: SortableRootProps) {\n const [activeId, setActiveId] = useState(null)\n const mounted = useSyncExternalStore(\n subscribeToNothing,\n getIsMounted,\n getIsMountedOnServer\n )\n\n const sensors = useSensors(\n useSensor(MouseSensor, MOUSE_SENSOR_OPTIONS),\n useSensor(TouchSensor, TOUCH_SENSOR_OPTIONS),\n useSensor(KeyboardSensor, KEYBOARD_SENSOR_OPTIONS)\n )\n\n const handleDragStart = useCallback(\n (event: DragStartEvent) => {\n setActiveId(event.active.id)\n onDragStart?.(event)\n },\n [onDragStart]\n )\n\n const handleDragEnd = useCallback(\n (event: DragEndEvent) => {\n const { active, over } = event\n setActiveId(null)\n onDragEnd?.(event)\n\n if (!over) return\n\n // Handle item reordering\n const activeIndex = value.findIndex(\n (item: T) => getItemValue(item) === active.id\n )\n const overIndex = value.findIndex(\n (item: T) => getItemValue(item) === over.id\n )\n\n if (activeIndex === -1 || overIndex === -1) return\n\n if (activeIndex !== overIndex) {\n if (onMove) {\n onMove({ event, activeIndex, overIndex })\n } else {\n const newValue = arrayMove(value, activeIndex, overIndex)\n onValueChange(newValue)\n onValueCommit?.(newValue, {\n event,\n activeIndex,\n overIndex,\n previousValue: value,\n })\n }\n }\n },\n [value, getItemValue, onValueChange, onMove, onDragEnd, onValueCommit]\n )\n\n const handleDragCancel = useCallback(\n (event: DragCancelEvent) => {\n setActiveId(null)\n onDragCancel?.(event)\n },\n [onDragCancel]\n )\n\n const itemIds = useMemo(() => {\n const ids = value.map(getItemValue)\n if (process.env.NODE_ENV !== \"production\") {\n const seen = new Set()\n for (const id of ids) {\n if (seen.has(id)) {\n console.warn(\n `[Sortable] Duplicate item id \"${id}\". Item ids must be unique, or drag and drop will misbehave.`\n )\n break\n }\n seen.add(id)\n }\n }\n return ids\n }, [value, getItemValue])\n\n const contextValue = useMemo(\n () => ({ activeId, modifiers }),\n [activeId, modifiers]\n )\n\n // Find the active child for the overlay\n const overlayContent = useMemo(() => {\n if (!activeId) return null\n let result: ReactNode = null\n Children.forEach(children, (child) => {\n if (isValidElement(child) && (child.props as any).value === activeId) {\n result = cloneElement(child as ReactElement, {\n ...(child.props as any),\n className: cn((child.props as any).className, \"z-50\"),\n })\n }\n })\n return result\n }, [activeId, children])\n\n const Comp = asChild ? Slot.Root : \"div\"\n\n return (\n \n \n \n \n {children}\n \n \n {mounted &&\n createPortal(\n \n \n {overlayContent}\n \n ,\n document.body\n )}\n \n \n )\n}\n\nexport interface SortableItemProps extends HTMLAttributes {\n value: string\n disabled?: boolean\n asChild?: boolean\n}\n\nfunction SortableItem({\n value,\n className,\n asChild = false,\n disabled,\n children,\n ...props\n}: SortableItemProps) {\n const isOverlay = useContext(IsOverlayContext)\n\n const {\n setNodeRef,\n transform,\n transition,\n attributes,\n listeners,\n isDragging: isSortableDragging,\n } = useSortable({\n id: value,\n disabled: disabled || isOverlay,\n animateLayoutChanges,\n })\n\n if (isOverlay) {\n const Comp = asChild ? Slot.Root : \"div\"\n\n return (\n \n \n {children}\n \n \n )\n }\n\n const style = {\n transition,\n transform: CSS.Transform.toString(transform),\n } as CSSProperties\n\n const Comp = asChild ? Slot.Root : \"div\"\n\n return (\n \n \n {children}\n \n \n )\n}\n\nexport interface SortableItemHandleProps extends HTMLAttributes {\n cursor?: boolean\n asChild?: boolean\n}\n\nfunction SortableItemHandle({\n className,\n asChild = false,\n cursor = true,\n children,\n ...props\n}: SortableItemHandleProps) {\n const { listeners, isDragging, disabled } = useContext(SortableItemContext)\n\n const Comp = asChild ? Slot.Root : \"div\"\n\n return (\n \n {children}\n \n )\n}\n\nexport interface SortableOverlayProps extends Omit<\n React.ComponentProps,\n \"children\"\n> {\n children?: ReactNode | ((params: { value: UniqueIdentifier }) => ReactNode)\n}\n\nfunction SortableOverlay({\n children,\n className,\n ...props\n}: SortableOverlayProps) {\n const { activeId, modifiers } = useContext(SortableInternalContext)\n const mounted = useSyncExternalStore(\n subscribeToNothing,\n getIsMounted,\n getIsMountedOnServer\n )\n\n const content =\n activeId && children\n ? typeof children === \"function\"\n ? children({ value: activeId })\n : children\n : null\n\n if (!mounted) return null\n\n return createPortal(\n \n \n {content}\n \n ,\n document.body\n )\n}\n\nexport { Sortable, SortableItem, SortableItemHandle, SortableOverlay }","target":"components/neui/sortable.tsx"}]}