{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-drag-reorder",
  "type": "registry:hook",
  "title": "useDragReorder",
  "description": "Drag-to-reorder list logic with an accessible keyboard path, behind ReorderList.",
  "categories": [
    "hooks"
  ],
  "files": [
    {
      "path": "hooks/use-drag-reorder.ts",
      "type": "registry:hook",
      "target": "hooks/use-drag-reorder.ts",
      "content": "import * as React from \"react\";\n\nexport interface DragReorderItemProps {\n  draggable: true;\n  onDragStart: (event: React.DragEvent<HTMLElement>) => void;\n  onDragEnter: (event: React.DragEvent<HTMLElement>) => void;\n  onDragOver: (event: React.DragEvent<HTMLElement>) => void;\n  onDragLeave: (event: React.DragEvent<HTMLElement>) => void;\n  onDrop: (event: React.DragEvent<HTMLElement>) => void;\n  onDragEnd: (event: React.DragEvent<HTMLElement>) => void;\n}\n\nexport interface DragReorderHandleProps {\n  onPointerDown: (event: React.PointerEvent<HTMLElement>) => void;\n  /**\n   * `touchAction: 'none'` is the whole trick: a gesture that starts on the\n   * handle is never claimed by the scroller, so the drag runs instead of the\n   * container scrolling out from under it.\n   */\n  style: React.CSSProperties;\n}\n\nexport interface UseDragReorderOptions {\n  /** How many items the list has. Required for the pointer path's hit-testing. */\n  count?: number;\n  /** Resolves the row element at `index`, so a pointer drag can hit-test rows. */\n  getItemElement?: (index: number) => HTMLElement | null | undefined;\n  /**\n   * The scroller to auto-scroll when a pointer drag nears its edge. Usually the\n   * list itself when height-capped, else its nearest scrollable ancestor.\n   */\n  getScrollContainer?: () => HTMLElement | null | undefined;\n}\n\nexport interface UseDragReorderResult {\n  /** Index of the item currently being dragged, or null. */\n  draggingIndex: number | null;\n  /** Index of the item currently being hovered as a drop target, or null. */\n  dragOverIndex: number | null;\n  /** True while a touch/pen drag is in flight (the pointer path, not HTML5). */\n  pointerDragging: boolean;\n  /** Returns drag handlers (and `draggable: true`) for the item at `index`. */\n  getItemProps: (index: number) => DragReorderItemProps;\n  /** Returns pointer handlers for the drag handle of the item at `index`. */\n  getHandleProps: (index: number) => DragReorderHandleProps;\n}\n\n// How close to a scroller edge the pointer has to get before the list starts\n// auto-scrolling, and how fast it then moves (px per frame).\nconst AUTOSCROLL_EDGE_PX = 48;\nconst AUTOSCROLL_SPEED_PX = 12;\n\n/**\n * Drag-to-reorder for both input models.\n *\n * **Mouse** uses HTML5 drag-and-drop via `getItemProps` -- the whole row is\n * `draggable`, and the browser supplies the drag image and ancestor\n * auto-scroll for free.\n *\n * **Touch and pen** use Pointer Events via `getHandleProps`, because HTML5\n * drag-and-drop does not exist on touch: mobile browsers never synthesize\n * `dragstart` from a finger, so before this path the handle did nothing at all\n * and the gesture just scrolled the container. The handle carries\n * `touch-action: none`, so a drag that starts there is the drag rather than a\n * scroll, and the pointer path does its own hit-testing and edge auto-scroll\n * (both of which the HTML5 path gets from the browser).\n *\n * The hook only tracks state -- the caller renders the items and decides what\n * visual feedback to show (e.g. opacity-down on the dragged item, ring on the\n * drop target). Drop semantics: `onReorder(from, to)` is called with indices\n * into the *original* array, and the splice-based caller pattern places the\n * moved item at `to` in the resulting array.\n *\n * The pointer path needs geometry the hook cannot see, so a caller that wants\n * it passes `count`, `getItemElement`, and `getScrollContainer`. Omit them and\n * the hook still works exactly as before, mouse-only -- which is why adding\n * this was additive for callers like FS Generator that only spread\n * `getItemProps`.\n *\n * Typical usage:\n *\n * ```tsx\n * const reorder = useDragReorder((from, to) => setItems(prev => {\n *   const next = [...prev]\n *   const [m] = next.splice(from, 1)\n *   next.splice(to, 0, m)\n *   return next\n * }))\n *\n * items.map((item, idx) => (\n *   <div\n *     key={item.id}\n *     {...reorder.getItemProps(idx)}\n *     className={cn(\n *       reorder.draggingIndex === idx && \"opacity-40\",\n *       reorder.dragOverIndex === idx &&\n *         reorder.draggingIndex !== idx &&\n *         \"ring-2 ring-accent\",\n *     )}\n *   >\n *     <span {...reorder.getHandleProps(idx)}>::</span>\n *     {item.content}\n *   </div>\n * ))\n * ```\n */\nexport function useDragReorder(\n  onReorder: (from: number, to: number) => void,\n  options: UseDragReorderOptions = {},\n): UseDragReorderResult {\n  const [draggingIndex, setDraggingIndex] = React.useState<number | null>(null);\n  const [dragOverIndex, setDragOverIndex] = React.useState<number | null>(null);\n  const [pointerDragging, setPointerDragging] = React.useState(false);\n\n  // Mirror draggingIndex into a ref so event handlers always read the latest\n  // without having to be regenerated on every drag-state change.\n  const draggingRef = React.useRef<number | null>(null);\n  React.useEffect(() => {\n    draggingRef.current = draggingIndex;\n  }, [draggingIndex]);\n\n  // The pointer path reads the live drop target on pointerup, where a state\n  // read would be a render behind.\n  const overRef = React.useRef<number | null>(null);\n\n  const onReorderRef = React.useRef(onReorder);\n  React.useEffect(() => {\n    onReorderRef.current = onReorder;\n  }, [onReorder]);\n\n  const optionsRef = React.useRef(options);\n  React.useEffect(() => {\n    optionsRef.current = options;\n  });\n\n  const getItemProps = React.useCallback((index: number): DragReorderItemProps => ({\n    draggable: true,\n    onDragStart: (event) => {\n      // setData is required for Firefox to actually start the drag.\n      if (event.dataTransfer) {\n        event.dataTransfer.effectAllowed = \"move\";\n        event.dataTransfer.setData(\"text/plain\", String(index));\n      }\n      setDraggingIndex(index);\n    },\n    onDragEnter: (event) => {\n      event.preventDefault();\n      setDragOverIndex(index);\n    },\n    onDragOver: (event) => {\n      // Both dragOver preventDefault and dropEffect are required for\n      // the drop event to fire reliably across browsers.\n      event.preventDefault();\n      if (event.dataTransfer) {\n        event.dataTransfer.dropEffect = \"move\";\n      }\n    },\n    onDragLeave: () => {\n      // Intentionally a no-op. dragenter on a sibling races with dragleave\n      // on the previous; clearing here causes flicker. State is cleared on\n      // drop / dragend instead.\n    },\n    onDrop: (event) => {\n      event.preventDefault();\n      const from = draggingRef.current;\n      const to = index;\n      setDraggingIndex(null);\n      setDragOverIndex(null);\n      if (from === null || from === to) return;\n      onReorderRef.current(from, to);\n    },\n    onDragEnd: () => {\n      setDraggingIndex(null);\n      setDragOverIndex(null);\n    },\n  }), []);\n\n  // ---- Pointer (touch / pen) path ------------------------------------------\n\n  // Latest pointer Y, read by both the move handler and the auto-scroll frame\n  // (which re-hit-tests after moving the scroller, since the rows have shifted\n  // under a stationary finger).\n  const pointerYRef = React.useRef(0);\n  const rafRef = React.useRef<number | null>(null);\n\n  /** Which row `clientY` currently sits over, clamped to the list. */\n  const hitTest = React.useCallback((clientY: number): number | null => {\n    const { count, getItemElement } = optionsRef.current;\n    if (!count || !getItemElement) return null;\n    let candidate: number | null = null;\n    for (let i = 0; i < count; i += 1) {\n      const el = getItemElement(i);\n      if (!el) continue;\n      const rect = el.getBoundingClientRect();\n      if (candidate === null) candidate = i;\n      if (clientY >= rect.top && clientY <= rect.bottom) return i;\n      // Past the row's bottom edge -- keep it as the running best so a pointer\n      // dragged below the last row lands on the last row rather than nowhere.\n      if (clientY > rect.bottom) candidate = i;\n    }\n    return candidate;\n  }, []);\n\n  const syncOver = React.useCallback(() => {\n    const to = hitTest(pointerYRef.current);\n    if (to === null) return;\n    overRef.current = to;\n    setDragOverIndex(to);\n  }, [hitTest]);\n\n  const stopAutoScroll = React.useCallback(() => {\n    if (rafRef.current !== null) {\n      cancelAnimationFrame(rafRef.current);\n      rafRef.current = null;\n    }\n  }, []);\n\n  const autoScrollFrame = React.useCallback(() => {\n    const el = optionsRef.current.getScrollContainer?.();\n    if (el) {\n      const rect = el.getBoundingClientRect();\n      const y = pointerYRef.current;\n      if (y < rect.top + AUTOSCROLL_EDGE_PX) {\n        el.scrollTop -= AUTOSCROLL_SPEED_PX;\n        syncOver();\n      } else if (y > rect.bottom - AUTOSCROLL_EDGE_PX) {\n        el.scrollTop += AUTOSCROLL_SPEED_PX;\n        syncOver();\n      }\n    }\n    rafRef.current = requestAnimationFrame(autoScrollFrame);\n  }, [syncOver]);\n\n  // A drag interrupted by unmount must not leave a frame loop running.\n  React.useEffect(() => stopAutoScroll, [stopAutoScroll]);\n\n  const getHandleProps = React.useCallback((index: number): DragReorderHandleProps => ({\n    style: { touchAction: \"none\" },\n    onPointerDown: (event) => {\n      // Mouse keeps the HTML5 path on the row, which already gives it a drag\n      // image and native ancestor auto-scroll.\n      if (event.pointerType === \"mouse\") return;\n      const target = event.currentTarget;\n      // Capture so the drag survives the finger leaving the handle -- without\n      // it the very first move ends the gesture.\n      target.setPointerCapture?.(event.pointerId);\n      pointerYRef.current = event.clientY;\n      draggingRef.current = index;\n      overRef.current = index;\n      setDraggingIndex(index);\n      setDragOverIndex(index);\n      setPointerDragging(true);\n      stopAutoScroll();\n      rafRef.current = requestAnimationFrame(autoScrollFrame);\n\n      const handleMove = (e: PointerEvent) => {\n        if (e.pointerId !== event.pointerId) return;\n        // The handle's touch-action already stops the scroller claiming this,\n        // but a listener that reaches here should not also fire a click.\n        e.preventDefault();\n        pointerYRef.current = e.clientY;\n        syncOver();\n      };\n\n      const finish = (e: PointerEvent) => {\n        if (e.pointerId !== event.pointerId) return;\n        target.releasePointerCapture?.(event.pointerId);\n        target.removeEventListener(\"pointermove\", handleMove);\n        target.removeEventListener(\"pointerup\", finish);\n        target.removeEventListener(\"pointercancel\", cancel);\n        stopAutoScroll();\n        const from = draggingRef.current;\n        const to = overRef.current;\n        draggingRef.current = null;\n        overRef.current = null;\n        setDraggingIndex(null);\n        setDragOverIndex(null);\n        setPointerDragging(false);\n        if (from === null || to === null || from === to) return;\n        onReorderRef.current(from, to);\n      };\n\n      const cancel = (e: PointerEvent) => {\n        if (e.pointerId !== event.pointerId) return;\n        target.releasePointerCapture?.(event.pointerId);\n        target.removeEventListener(\"pointermove\", handleMove);\n        target.removeEventListener(\"pointerup\", finish);\n        target.removeEventListener(\"pointercancel\", cancel);\n        stopAutoScroll();\n        draggingRef.current = null;\n        overRef.current = null;\n        setDraggingIndex(null);\n        setDragOverIndex(null);\n        setPointerDragging(false);\n      };\n\n      // Bound to the capturing element rather than the document: pointer\n      // capture routes every subsequent event for this pointer here anyway,\n      // and it keeps the listeners scoped to the gesture.\n      target.addEventListener(\"pointermove\", handleMove);\n      target.addEventListener(\"pointerup\", finish);\n      target.addEventListener(\"pointercancel\", cancel);\n    },\n  }), [autoScrollFrame, stopAutoScroll, syncOver]);\n\n  return {\n    draggingIndex,\n    dragOverIndex,\n    pointerDragging,\n    getItemProps,\n    getHandleProps,\n  };\n}\n"
    }
  ],
  "docs": "Most apps want ReorderList, which wraps this hook with rendering and arrow buttons. Reach for the hook directly when you need a different row presentation.",
  "meta": {
    "group": "hooks",
    "related": [
      "reorder-list"
    ],
    "exports": [
      "useDragReorder"
    ],
    "siteSlug": "use-drag-reorder"
  }
}
