/* Copyright 2026 Marimo. All rights reserved. */ import type { DraggableAttributes } from "@dnd-kit/core"; import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"; import { useSortable } from "@dnd-kit/sortable"; import { CSS, type Transform } from "@dnd-kit/utilities"; import { GripVerticalIcon } from "lucide-react"; import React, { memo, use } from "react"; import type { CellId } from "@/core/cells/ids"; import { cn } from "@/utils/cn"; import { Events } from "@/utils/events"; import { mergeRefs } from "@/utils/mergeRefs"; interface Props extends React.HTMLAttributes { children: React.ReactNode; cellId: CellId; canMoveX?: boolean; } /** * Context for drag handle so it can be rendered in a Slot in the cell */ const DragHandleSlot = React.createContext(null); export const CellDragHandle: React.FC = memo(() => { // Slot for drag handle return use(DragHandleSlot); }); CellDragHandle.displayName = "DragHandle"; function isTransformNoop(transform: Transform | null) { if (!transform) { return true; } return ( transform.x === 0 && transform.y === 0 && transform.scaleX === 1 && transform.scaleY === 1 ); } export const SortableCell = React.forwardRef( ( { cellId, canMoveX, ...props }: Props, ref: React.ForwardedRef, ) => { // This hook re-renders every time _any_ cell is dragged, // so we should avoid any expensive operations in this component const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: cellId.toString() }); // Perf: // If the transform is a noop, keep it as null const transformOrNull = isTransformNoop(transform) ? null : transform; // If there is no transform, we don't need a transition const transitionOrUndefined = transformOrNull == null ? undefined : transition; // Use a new component to avoid re-rendering when the cell is dragged return ( ); }, ); SortableCell.displayName = "SortableCell"; interface SortableCellInternalProps extends Props { children: React.ReactNode; attributes: DraggableAttributes; listeners: SyntheticListenerMap | undefined; setNodeRef: (node: HTMLElement | null) => void; transform: Transform | null; transition: string | undefined; isDragging: boolean; } const SortableCellInternal = React.forwardRef( ( { cellId, canMoveX, children, attributes, listeners, setNodeRef, transform, transition, isDragging, ...props }: SortableCellInternalProps, ref: React.ForwardedRef, ) => { const style: React.CSSProperties = { transform: transform ? CSS.Transform.toString({ x: canMoveX ? transform.x : 0, y: transform.y, scaleX: 1, scaleY: 1, }) : undefined, transition, zIndex: isDragging ? 2 : undefined, position: "relative", }; const dragHandle = (
); const isMoving = Boolean(transform); return (
{ mergeRefs(ref, setNodeRef)(r); }} {...props} data-is-dragging={isDragging} className={cn( props.className, isMoving && "is-moving", "outline-hidden rounded-lg", )} style={style} > {children}
); }, ); SortableCellInternal.displayName = "SortableCellInternal";