"use client" import type { DndContextProps, UniqueIdentifier } from "@dnd-kit/core" import { closestCenter, DndContext, DragOverlay, KeyboardSensor, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core" import { horizontalListSortingStrategy, type NewIndexGetter, SortableContext, type SortableContextProps, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable" import { CSS, type Transform } from "@dnd-kit/utilities" import { composeEventHandlers } from "@radix-ui/primitive" import { composeRefs } from "@radix-ui/react-compose-refs" import * as PortalPrimitive from "@radix-ui/react-portal" import { Primitive } from "@radix-ui/react-primitive" import clsx from "clsx" import type { ComponentProps, ReactNode } from "react" import { createContext, useContext, useState } from "react" const SortableImplContext = createContext< Pick & Required> & { activeId: UniqueIdentifier | null } >({ activeId: null, getTransformStyle: CSS.Transform.toString, }) function useSortableImplContext() { const context = useContext(SortableImplContext) if (!context) { throw new Error("useSortableImplContext must be used within a ") } return context } export interface SortableProps extends DndContextProps { onReorder?: (oldIndex: number, newIndex: number) => void getNewIndex?: NewIndexGetter getTransformStyle?: (transform: Transform | null) => React.CSSProperties["transform"] } export const Sortable = ({ onDragStart, onDragEnd, onDragCancel, getNewIndex, collisionDetection = closestCenter, getTransformStyle = CSS.Transform.toString, ...props }: SortableProps) => { const [activeId, setActiveId] = useState(null) const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ) return ( setActiveId(active.id))} // @ts-expect-error Temporary fix for dnd-kit types onDragEnd={composeEventHandlers(onDragEnd, () => setActiveId(null))} // @ts-expect-error Temporary fix for dnd-kit types onDragCancel={composeEventHandlers(onDragCancel, () => setActiveId(null))} collisionDetection={collisionDetection} sensors={sensors} {...props} /> ) } export interface SortableListProps extends Omit, ComponentProps { orientation?: "vertical" | "horizontal" } export const SortableList = ({ orientation = "vertical", strategy = orientation === "vertical" ? verticalListSortingStrategy : horizontalListSortingStrategy, items, disabled, id, ...props }: SortableListProps) => ( ) SortableList.displayName = "SortableList" export type SortableGridProps = Omit & ComponentProps export const SortableGrid = ({ strategy, items, disabled, id, ...props }: SortableGridProps) => ( ) SortableGrid.displayName = "SortableGrid" const SortableItemContext = createContext>({ id: "", disabled: false, }) function useSortableItemContext() { const context = useContext(SortableItemContext) if (!context) { throw new Error("useSortableItemContext must be used within a ") } return context } export type SortableItemProps = Omit, "id"> & Pick[0], "id" | "disabled"> export const SortableItem = ({ id, disabled, style: styleProp, ...props }: SortableItemProps) => { const { getTransformStyle, getNewIndex } = useSortableImplContext() const { attributes, setNodeRef, transform, transition, isDragging, isOver, isSorting } = useSortable({ id, disabled, getNewIndex, }) const style = { transform: getTransformStyle(transform), transition, ...styleProp, } return ( )} style={style} data-dragging={isDragging ? true : undefined} data-over={isOver ? true : undefined} data-sorting={isSorting ? true : undefined} {...attributes} {...props} /> ) } SortableItem.displayName = "SortableItem" export const SortableItemTrigger = ({ className, disabled: disabledProp, ...props }: ComponentProps) => { const { getNewIndex } = useSortableImplContext() const { id, disabled } = useSortableItemContext() const { listeners, setActivatorNodeRef, isDragging, isOver, isSorting } = useSortable({ id, disabled: disabledProp ?? disabled, getNewIndex, }) return ( )} data-dragging={isDragging ? true : undefined} data-over={isOver ? true : undefined} data-sorting={isSorting ? true : undefined} disabled={disabledProp} className={clsx("touch-none", className)} {...listeners} {...props} /> ) } SortableItemTrigger.displayName = "SortableItemTrigger" export interface SortableOverlayProps extends Omit, "children"> { children?: ReactNode | ((id: UniqueIdentifier) => ReactNode) } export const SortableOverlay = ({ children, ...props }: SortableOverlayProps) => { const { activeId } = useSortableImplContext() return ( {activeId && (typeof children === "function" ? children(activeId) : children)} ) }