"use client";
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 {
restrictToHorizontalAxis,
restrictToParentElement,
restrictToVerticalAxis,
} from "@dnd-kit/modifiers";
import {
arrayMove,
horizontalListSortingStrategy,
SortableContext,
useSortable,
verticalListSortingStrategy,
type SortableContextProps,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Button } from "@openstatus/ui/components/ui/button";
import { composeRefs } from "@openstatus/ui/lib/compose-refs";
import { cn } from "@openstatus/ui/lib/utils";
import { Slot, type SlotProps } from "@radix-ui/react-slot";
import * as React from "react";
import { createPortal } from "react-dom";
const orientationConfig = {
vertical: {
modifiers: [restrictToVerticalAxis, restrictToParentElement],
strategy: verticalListSortingStrategy,
},
horizontal: {
modifiers: [restrictToHorizontalAxis, restrictToParentElement],
strategy: horizontalListSortingStrategy,
},
mixed: {
modifiers: [restrictToParentElement],
strategy: undefined,
},
};
interface SortableProps<
TData extends { id: UniqueIdentifier },
> 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"];
/**
* Specifies the axis for the drag-and-drop operation. It can be "vertical", "horizontal", or "both".
* @default "vertical"
* @type "vertical" | "horizontal" | "mixed"
*/
orientation?: "vertical" | "horizontal" | "mixed";
/**
* 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,
onDragStart,
onDragEnd,
onDragCancel,
collisionDetection = closestCenter,
modifiers,
strategy,
onMove,
orientation = "vertical",
overlay,
children,
...props
}: SortableProps) {
const [activeId, setActiveId] = React.useState(null);
const sensors = useSensors(
useSensor(MouseSensor),
useSensor(TouchSensor),
useSensor(KeyboardSensor),
);
const config = orientationConfig[orientation];
return (
{
setActiveId(event.active.id);
onDragStart?.(event);
}}
onDragEnd={(event) => {
const { active, over } = event;
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);
onDragEnd?.(event);
}}
onDragCancel={(event) => {
setActiveId?.(null);
onDragCancel?.(event);
}}
collisionDetection={collisionDetection}
{...props}
>
{children}
{overlay
? // https://docs.dndkit.com/api-documentation/draggable/drag-overlay#portals
createPortal(
{overlay},
document.body,
)
: null}
);
}
const dropAnimationOpts: DropAnimation = {
sideEffects: defaultDropAnimationSideEffects({
styles: {
active: {
opacity: "0.4",
},
},
}),
};
interface SortableOverlayProps extends React.ComponentPropsWithRef<
typeof DragOverlay
> {
activeId?: UniqueIdentifier | null;
}
function SortableOverlay({
activeId,
dropAnimation = dropAnimationOpts,
children,
...props
}: SortableOverlayProps) {
return (
{activeId ? (
{children}
) : null}
);
}
interface SortableItemContextProps {
attributes: React.HTMLAttributes;
listeners: DraggableSyntheticListeners | undefined;
isDragging?: boolean;
}
const SortableItemContext = React.createContext({
attributes: {},
listeners: undefined,
isDragging: false,
});
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 {
/**
* The unique identifier of the item.
* @example "item-1"
* @type UniqueIdentifier
*/
value: UniqueIdentifier;
/**
* Specifies whether the item should act as a trigger for the drag-and-drop action.
* @default false
* @type boolean | undefined
*/
asTrigger?: boolean;
/**
* Merges the item's props into its immediate child.
* @default false
* @type boolean | undefined
*/
asChild?: boolean;
}
function SortableItem({
value,
asTrigger,
asChild,
className,
ref,
...props
}: SortableItemProps & { ref?: React.Ref }) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: value });
const context = React.useMemo(
() => ({
attributes,
listeners,
isDragging,
}),
[attributes, listeners, isDragging],
);
const style: React.CSSProperties = {
opacity: isDragging ? 0.5 : 1,
transform: CSS.Translate.toString(transform),
transition,
};
const Comp = asChild ? Slot : "div";
return (
)}
style={style}
{...(asTrigger ? attributes : {})}
{...(asTrigger ? listeners : {})}
{...props}
/>
);
}
interface SortableDragHandleProps extends React.ComponentProps {
withHandle?: boolean;
}
function SortableDragHandle({
className,
ref,
...props
}: SortableDragHandleProps) {
const { attributes, listeners, isDragging } = useSortableItem();
return (
);
}
export { Sortable, SortableDragHandle, SortableItem, SortableOverlay };