mcpField
D
drag-and-drop

drag-and-drop

github.com/wodsmithDevelopment48 installs
Repository

About drag-and-drop

drag-and-drop is a coding skill for implementing drag-and-drop interfaces with @atlaskit/pragmatic-drag-and-drop. It provides React patterns for draggable items, scoped lists, drop indicators, and persisted reordering.

Key features

  1. 01Set up draggable items and drop targets
  2. 02Track volatile state with refs
  3. 03Scope operations with list instance IDs
  4. 04Show edge-based drop indicators
  5. 05Reorder items with optimistic updates

Use cases

  1. 01Building sortable lists in React
  2. 02Adding drag handles and custom previews
  3. 03Reordering items and saving their positions

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/wodsmith/thewodapp --skill drag-and-drop

After installing, type/drag-and-dropin Claude Code to confirm it loaded.

Details

From the project README

Drag and Drop with Pragmatic DnD

This project uses @atlaskit/pragmatic-drag-and-drop for drag-and-drop functionality.

Required Imports

import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"
import {
  draggable,
  dropTargetForElements,
  type ElementDropTargetEventBasePayload,
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview"
import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview"
import {
  attachClosestEdge,
  type Edge,
  extractClosestEdge,
} from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"
import { DropIndicator } from "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box"

Critical Pattern: Refs for Volatile State

NEVER put volatile drag state in useEffect dependencies. This causes handlers to re-register on every state change.

// BAD - re-registers handlers on every edge change
const [closestEdge, setClosestEdge] = useState<Edge | null>(null)
useEffect(() => {
  // ...handlers using closestEdge
}, [closestEdge]) // Re-runs on every drag movement!

// GOOD - use ref + useCallback for volatile state
const [closestEdge, setClosestEdge] = useState<Edge | null>(null)
const closestEdgeRef = useRef<Edge | null>(null)

// Wrap in useCallback for lint compliance (exhaustive-deps)
const updateClosestEdge = useCallback((edge: Edge | null) => {
  closestEdgeRef.current = edge
  setClosestEdge(edge) // Still update state for rendering
}, [])

useEffect(() => {
  // ...handlers read closestEdgeRef.current instead
}, [/* stable deps */, updateClosestEdge]) // Include updateClosestEdge

Import useCallback:

import { useCallback, useEffect, useRef, useState } from "react"

Basic Draggable Item Pattern

function DraggableItem({ item, index, instanceId, onDrop }) {
  const ref = useRef<HTMLDivElement>(null)
  const dragHandleRef = useRef<HTMLButtonElement>(null)
  const [isDragging, setIsDragging] = useState(false)
  const [closestEdge, setClosestEdge] = useState<Edge | null>(null)
  const closestEdgeRef = useRef<Edge | null>(null)

  const updateClosestEdge = useCallback((edge: Edge | null) => {
    closestEdgeRef.current = edge
    setClosestEdge(edge)
  }, [])

  useEffect(() => {
    const element = ref.current
    const dragHandle = dragHandleRef.current
    if (!element || !dragHandle) return

    const itemData = { id: item.id, index, instanceId }

    return combine(
      draggable({
        element: dragHandle,
        getInitialData: () => itemData,
        onDragStart: () => setIsDragging(true),
        onDrop: () => setIsDragging(false),
        onGenerateDragPreview({ nativeSetDragImage }) {
          setCustomNativeDragPreview({
            nativeSetDragImage,
            getOffset: pointerOutsideOfPreview({ x: "16px", y: "8px" }),
            render({ container }) {
              const preview = document.createElement("div")
              preview.style.cssText = `
                background: hsl(var(--background));
                border: 2px solid hsl(var(--border));
                border-radius: 6px;
                padding: 8px 12px;
                font-size: 14px;
                color: hsl(var(--foreground));
                box-shadow: 0 2px 8px rgba(0,0,0,0.15);
              `
              preview.textContent = item.label
              container.appendChild(preview)
            },
          })
        },
      }),
      dropTargetForElements({
        element,
        canDrop: ({ source }) =>
          source.data.instanceId === instanceId && source.data.index !== index,
        getData({ input }) {
          return attachClosestEdge(itemData, {
            element,
            input,
            allowedEdges: ["top", "bottom"],
          })
        },
        onDrag({ source, self }: ElementDropTargetEventBasePayload) {
          if (source.data.index === index) {
            updateClosestEdge(null)
            return
          }

          const edge = extractClosestEdge(self.data)
          const sourceIndex = source.data.index
          if (typeof sourceIndex !== "number") return

          // Hide indicator when it would be redundant
          const isItemBeforeSource = index === sourceIndex - 1
          const isItemAfterSource = index === sourceIndex + 1
          const isDropIndicatorHidden =
            (isItemBeforeSource && edge === "bottom") ||
            (isItemAfterSource && edge === "top")

          updateClosestEdge(isDropIndicatorHidden ? null : edge)
        },
        onDragLeave: () => updateClosestEdge(null),
        onDrop({ source }) {
          const sourceIndex = source.data.index
          if (typeof sourceIndex === "number" && sourceIndex !== index) {
            const edge = closestEdgeRef.current // Read from ref!
            const targetIndex = edge === "top" ? index : index + 1
            const adjustedTargetIndex =
              sourceIndex < targetIndex ? targetIndex - 1 : targetIndex
            onDrop(sourceIndex, adjustedTargetIndex)
          }
          updateClosestEdge(null)
        },
      }),
    )
  }, [item.id, item.label, index, instanceId, onDrop, updateClosestEdge])

  return (
    <div ref={ref} className="relative">
      {closestEdge && <DropIndicator edge={closestEdge} gap="2px" />}
      <div className={isDragging ? "opacity-50" : ""}>
        <button ref={dragHandleRef} type="button" aria-label="Drag to reorder">
          <GripVertical />
        </button>
        {/* Item content */}
      </div>
    </div>
  )
}

Instance ID for Multiple Lists

Use Symbol to scope drag operations to a single list:

function SortableList({ items }) {
  const [instanceId] = useState(() => Symbol("list"))
  // Pass instanceId to each item
}

Reorder Handler

const handleDrop = async (sourceIndex: number, targetIndex: number) => {
  const newItems = [...items]
  const [movedItem] = newItems.splice(sourceIndex, 1)
  if (movedItem) {
    newItems.splice(targetIndex, 0, movedItem)
    const updated = newItems.map((item, i) => ({ ...item, position: i }))
    setItems(updated) // Optimistic update
    await saveOrder(updated) // Persist
  }
}

Checklist

  • Refs for volatile state (closestEdge, etc.)
  • Wrap updateClosestEdge in useCallback (lint compliance)
  • Include updateClosestEdge in useEffect deps
  • Instance ID for list scoping
  • Drop indicator with edge detection
  • Custom drag preview
  • Optimistic UI updates

Comments

Markdown supported