"use client"; import * as React from "react"; import type { DndContextProps, DraggableSyntheticListeners, DropAnimation, UniqueIdentifier, } from "@dnd-kit/core"; import { closestCenter, defaultDropAnimationSideEffects, DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { restrictToParentElement, restrictToVerticalAxis, } from "@dnd-kit/modifiers"; import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import type { SortableContextProps } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { Slot } from "@radix-ui/react-slot"; import type { SlotProps } from "@radix-ui/react-slot"; import { composeRefs } from "../lib/compose-refs"; import { cn } from "../lib/utils"; import { Button } from "./button"; import type { ButtonProps } from "./button"; interface SortableProps extends DndContextProps { /** * An array of data items that the sortable component will render. * @example * value={[ * { id: 1, name: 'Item 1' }, * { id: 2, name: 'Item 2' }, * ]} */ value: TData[]; /** * An optional callback function that is called when the order of the data items changes. * It receives the new array of items as its argument. * @example * onValueChange={(items) => console.log(items)} */ onValueChange?: (items: TData[]) => void; /** * An optional callback function that is called when an item is moved. * It receives an event object with `activeIndex` and `overIndex` properties, representing the original and new positions of the moved item. * This will override the default behavior of updating the order of the data items. * @type (event: { activeIndex: number; overIndex: number }) => void * @example * onMove={(event) => console.log(`Item moved from index ${event.activeIndex} to index ${event.overIndex}`)} */ onMove?: (event: { activeIndex: number; overIndex: number }) => void; /** * A collision detection strategy that will be used to determine the closest sortable item. * @default closestCenter * @type DndContextProps["collisionDetection"] */ collisionDetection?: DndContextProps["collisionDetection"]; /** * An array of modifiers that will be used to modify the behavior of the sortable component. * @default * [restrictToVerticalAxis, restrictToParentElement] * @type Modifier[] */ modifiers?: DndContextProps["modifiers"]; /** * A sorting strategy that will be used to determine the new order of the data items. * @default verticalListSortingStrategy * @type SortableContextProps["strategy"] */ strategy?: SortableContextProps["strategy"]; /** * An optional React node that is rendered on top of the sortable component. * It can be used to display additional information or controls. * @default null * @type React.ReactNode | null * @example * overlay={} */ overlay?: React.ReactNode | null; } function Sortable({ value, onValueChange, collisionDetection = closestCenter, modifiers = [restrictToVerticalAxis, restrictToParentElement], strategy = verticalListSortingStrategy, onMove, children, overlay, ...props }: SortableProps) { const [activeId, setActiveId] = React.useState(null); const sensors = useSensors( useSensor(MouseSensor), useSensor(TouchSensor), useSensor(KeyboardSensor), ); return ( setActiveId(active.id)} onDragEnd={({ active, over }) => { if (over && active.id !== over?.id) { const activeIndex = value.findIndex((item) => item.id === active.id); const overIndex = value.findIndex((item) => item.id === over.id); if (onMove) { onMove({ activeIndex, overIndex }); } else { onValueChange?.(arrayMove(value, activeIndex, overIndex)); } } setActiveId(null); }} onDragCancel={() => setActiveId(null)} collisionDetection={collisionDetection} {...props} > {children} {overlay ? ( {overlay} ) : null} ); } const dropAnimationOpts: DropAnimation = { sideEffects: defaultDropAnimationSideEffects({ styles: { active: { opacity: "0.4", }, }, }), }; interface SortableOverlayProps extends React.ComponentPropsWithRef { activeId?: UniqueIdentifier | null; } function SortableOverlay({ activeId, dropAnimation = dropAnimationOpts, children, ...props }: SortableOverlayProps) { return ( {activeId ? ( {children} ) : null} ); } interface SortableItemContextProps { attributes: React.HTMLAttributes; listeners: DraggableSyntheticListeners | undefined; } const SortableItemContext = React.createContext({ attributes: {}, listeners: undefined, }); function useSortableItem() { const context = React.useContext(SortableItemContext); if (!context) { throw new Error("useSortableItem must be used within a SortableItem"); } return context; } interface SortableItemProps extends SlotProps { value: UniqueIdentifier; asChild?: boolean; } const SortableItem = React.forwardRef( ({ asChild, className, value, ...props }, ref) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: value }); const context = React.useMemo( () => ({ attributes, listeners, }), [attributes, listeners], ); const style: React.CSSProperties = { opacity: isDragging ? 0.4 : undefined, transform: CSS.Translate.toString(transform), transition, }; const Comp = asChild ? Slot : "div"; return ( )} style={style} {...props} /> ); }, ); SortableItem.displayName = "SortableItem"; interface SortableDragHandleProps extends ButtonProps { withHandle?: boolean; } const SortableDragHandle = React.forwardRef< HTMLButtonElement, SortableDragHandleProps >(({ className, ...props }, ref) => { const { attributes, listeners } = useSortableItem(); return (