{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drag-select",
  "title": "Drag Select",
  "description": "Rubber-band selection: drag from the empty space around a list, a grid or a board and everything the rectangle touches is selected, with the container scrolling itself when the pointer reaches its edge. Reach for it wherever a screen has many things and an action that takes several of them at once: a file or asset manager, a media and image gallery, a photo-picker grid, an inbox or notification list, a data grid's rows, cards inside a kanban column, nodes and shapes on a canvas, diagram or whiteboard, map markers, calendar slots, clips on a timeline, layers in an editor, tiles in a seat or venue picker, and the icon grid in any admin tool that has a Delete button above it. Common asks it answers: \"drag select react\", \"rubber band selection react\", \"marquee selection react\", \"lasso select react\", \"box selection\", \"rectangle selection\", \"drag to select multiple items\", \"click and drag to select\", \"select multiple items by dragging\", \"multi select drag react\", \"selection rectangle overlay\", \"drag selection box div\", \"shadcn drag select\", \"shadcn multi select rows\", \"react-selectable alternative\", \"react-selectable-fast alternative\", \"dnd-kit does not do rubber band selection\", \"@air/react-drag-to-select alternative\", \"selecto.js react\", \"moveable selecto alternative\", \"drag select without a library\", \"drag select with auto scroll\", \"auto scroll while dragging selection\", \"selection box wrong after scrolling\", \"drag select offset when container is scrolled\", \"marquee selection scroll offset bug\", \"shift click and drag add to selection\", \"cmd drag add to selection\", \"drag select conflicts with drag and drop\", \"drag handle stops working when I add drag select\", \"prevent text selection while dragging\", \"user-select none while dragging react\", \"escape to cancel selection drag\", \"finder style selection react\", \"範囲選択 ドラッグ react\", \"矩形選択 複数選択\", \"ドラッグで複数選択\", \"選択範囲 自動スクロール\". Official shadcn/ui has nothing of the kind, and the measurement is unambiguous rather than a judgement call: across all 63 catalogue entries (62 are fetchable — questionnaire is listed and 404s on both style tracks) and 223,287 bytes of their source, marquee, rubber-band, lasso, setPointerCapture, pointerdown and getBoundingClientRect are every one of them a zero hit. The two words that are not zero were read rather than counted: rectangle's single occurrence is a recharts class name inside the chart component, and all four occurrences of selection are the Tailwind selection: utility that colours highlighted text in an input. So official has text selection and no spatial selection at all. Within this registry it is the missing half of a set that was already here: bulk-action-bar is what appears once rows are selected, nested-checkbox selects down a hierarchy, multi-select and tag-input choose from a flat list inside a field, filter-chips shows which conditions are in force — and sortable-list drags rows to reorder them, which is the component this one has to coexist with rather than replace. Five things separate it from the fifty-line version, and the first is why the fifty-line version is always reported as working before it is reported as broken. First, there are two coordinate systems and the rectangle lives in the inner one. A press gives viewport coordinates; the rows being hit-tested are measured in viewport coordinates too, so intersecting them looks right — until the list is scrolled, at which point a rectangle anchored to the viewport slides off the rows it was drawn around. Here the anchor, the live point and every item box are converted into the container's content coordinates, so scrolling moves the rectangle with the content. The conversion subtracts the container's border as well as its offset, because getBoundingClientRect measures from the border box while an absolutely positioned child — which is what the marquee is — is placed from the padding box: leave it out and the box sits a border-width from the pointer, invisible at 1px and obvious at 4. Second, where a drag may begin. A press that lands on an item belongs to the item, so a sortable-list grip handle, a row's own click and a checkbox all keep working inside a selectable list; only empty space starts a rectangle, data-drag-select-ignore opts out a toolbar or a resizer sitting in the same container, and nothing is drawn until the pointer has travelled past a threshold — which is what leaves a plain click on the background free to mean \"clear the selection\" instead of being a one-pixel drag. Third, the edges scroll. Without that the component is quietly limited to whatever is already on screen: drag to the bottom of a list and the rows below the fold can never be reached, because the list only scrolls for a wheel it is not being given. The speed tapers across the edge band rather than switching on at full tilt, the band is capped at half the container so a short list does not scroll from its own middle, and each frame re-derives the rectangle from the stored pointer position — the finger is still, the content under it is not, and an auto-scroll that forgets this scrolls beautifully while the selection stops growing. Fourth, modifiers and the way out. Shift or Cmd/Ctrl adds to the selection that existed when the press landed, Alt removes from it, and the base is captured once at pointerdown so dragging back and forth does not ratchet; Escape and a cancelled pointer both put the selection back exactly as it was, because neither one released onto anything. Fifth, it is never the only way in. The selection is ordinary React state — controlled with value plus onChange or uncontrolled with defaultValue — so the same set is written by the consumer's own clicks, checkboxes and keyboard shortcuts, which is the only thing that keeps multi-select from becoming a mouse-only feature; rows marked aria-disabled are skipped rather than selected and silently dropped by the action that follows; the rectangle is aria-hidden decoration with pointer-events-none and the container claims no ARIA role of its own, since the list inside it is the consumer's; a finger is left alone by default, because on a touch screen a drag from empty space is how the list is panned; and select-none is applied only while a rectangle is up, so the list's own text stays selectable and copyable the rest of the time. Mark the selectable elements with the spread helper dragSelectItem(id) — an attribute rather than a wrapper, so existing rows do not have to be re-parented. toContentPoint, toContentBox, boxBetween, boxesOverlap, boxHits, dragSelectMode, applyDragSelection, edgeScrollDelta, beginDragSelect and advanceDragSelect are all exported as pure functions, so the geometry and the selection algebra are testable and reusable headless. Styled entirely with shadcn tokens (primary and primary/15 for the rectangle), so it follows light and dark mode. One file, no dependencies at all — no dnd-kit, no selecto, no Radix.",
  "files": [
    {
      "path": "registry/ui/drag-select.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * The attribute that marks a selectable thing, and the id it carries.\n *\n * It is an attribute rather than a wrapper component on purpose: the rows, cards and tiles being\n * selected already exist, written however the app writes them, and a marquee should not require\n * them to be re-parented to work.\n */\nexport const DRAG_SELECT_ITEM_ATTR = \"data-drag-select-item\"\n\n/** Spread onto a selectable element: `<li {...dragSelectItem(file.id)}>`. */\nexport function dragSelectItem(id: string) {\n  return { [DRAG_SELECT_ITEM_ATTR]: id }\n}\n\nexport interface DragSelectPoint {\n  x: number\n  y: number\n}\n\nexport interface DragSelectBox {\n  left: number\n  top: number\n  width: number\n  height: number\n}\n\n/**\n * Everything needed to turn a viewport coordinate into a content coordinate, read off the scroll\n * container in one go.\n *\n * `borderLeft`/`borderTop` are the container's `clientLeft`/`clientTop`. They are here because the\n * two coordinate systems being reconciled do not share an origin: `getBoundingClientRect()` is\n * measured from the border box, while an absolutely positioned child — which is what the marquee\n * is — is placed from the padding box. Leave the border out and the box sits a border-width away\n * from the pointer, which looks like nothing at all on the usual 1px border and like a bug at 4px.\n */\nexport interface DragSelectFrame {\n  left: number\n  top: number\n  borderLeft: number\n  borderTop: number\n  scrollLeft: number\n  scrollTop: number\n}\n\n/** How a press combines with the selection that already exists. */\nexport type DragSelectMode = \"replace\" | \"add\" | \"subtract\"\n\nexport interface DragSelectSession {\n  pointerId: number\n  /** Where the press landed, in content coordinates. Fixed for the life of the session. */\n  anchor: DragSelectPoint\n  /** Where the pointer is now, in content coordinates. */\n  point: DragSelectPoint\n  /**\n   * Where the pointer is now in viewport coordinates, kept because an auto-scroll frame has to\n   * recompute the content point when no pointer event has arrived: the finger is still, the\n   * content underneath it is not.\n   */\n  viewport: DragSelectPoint\n  /** The selection as it stood when the press landed — what `subtract` subtracts from. */\n  base: string[]\n  mode: DragSelectMode\n  /** False until the pointer has travelled past the threshold; no box is drawn before that. */\n  active: boolean\n}\n\n/** Pointer travel, in pixels, before a press becomes a marquee rather than a click. */\nexport const DRAG_SELECT_THRESHOLD = 4\n\n/** How close to the container's edge the pointer has to get before the container scrolls itself. */\nexport const DRAG_SELECT_EDGE = 32\n\n/** Pixels per frame at the very edge, tapering to zero at the start of the edge band. */\nexport const DRAG_SELECT_SPEED = 14\n\n/**\n * A viewport point in the container's content coordinates.\n *\n * This conversion is the whole first half of the component. The marquee is drawn in content\n * coordinates and hit-tested in content coordinates, so that scrolling moves the rectangle with\n * the items instead of out from under them — add the scroll offset in one place and not the other\n * and the selection jumps the moment the list is flicked down.\n */\nexport function toContentPoint(point: DragSelectPoint, frame: DragSelectFrame): DragSelectPoint {\n  return {\n    x: point.x - frame.left - frame.borderLeft + frame.scrollLeft,\n    y: point.y - frame.top - frame.borderTop + frame.scrollTop,\n  }\n}\n\n/**\n * An element's viewport rectangle in the container's content coordinates.\n *\n * Width and height carry over untouched: scrolling moves an element, it does not resize it.\n */\nexport function toContentBox(\n  rect: { left: number; top: number; width: number; height: number },\n  frame: DragSelectFrame\n): DragSelectBox {\n  const origin = toContentPoint({ x: rect.left, y: rect.top }, frame)\n  return { left: origin.x, top: origin.y, width: rect.width, height: rect.height }\n}\n\n/** The rectangle between two corners, in either order — a drag up and left is still a rectangle. */\nexport function boxBetween(a: DragSelectPoint, b: DragSelectPoint): DragSelectBox {\n  return {\n    left: Math.min(a.x, b.x),\n    top: Math.min(a.y, b.y),\n    width: Math.abs(a.x - b.x),\n    height: Math.abs(a.y - b.y),\n  }\n}\n\n/**\n * Whether two boxes overlap, with touching edges not counting as overlap.\n *\n * Exclusive bounds are the difference between a rectangle dragged up to a row and one that has\n * taken it: at the moment the marquee's edge is flush with the row's, inclusive bounds select the\n * row the rectangle is visibly *not* over, which is an off-by-one nobody can reproduce on purpose.\n *\n * The consequence for a degenerate rectangle is the right one rather than a special case. A\n * perfectly horizontal drag has zero height and is a real gesture — dragging along a row of tiles\n * — and it selects what the line passes through; a drag straight down a gutter that coincides with\n * the items' own edge is touching them, not crossing them, and selects nothing. Neither depends on\n * the path the pointer took, because the selection is recomputed from the current rectangle on\n * every move rather than accumulated.\n */\nexport function boxesOverlap(a: DragSelectBox, b: DragSelectBox): boolean {\n  return (\n    a.left < b.left + b.width &&\n    b.left < a.left + a.width &&\n    a.top < b.top + b.height &&\n    b.top < a.top + a.height\n  )\n}\n\n/**\n * Which modifier means what, read off the press that starts the drag.\n *\n * Shift and the platform's own multi-select modifier both add, because a marquee has no \"extend\n * from the last one\" to be different about — in a rectangle, extending and adding are the same\n * gesture. Alt removes, which is how a file manager lets you cut a hole in a selection without\n * starting over.\n */\nexport function dragSelectMode(event: {\n  shiftKey?: boolean\n  metaKey?: boolean\n  ctrlKey?: boolean\n  altKey?: boolean\n}): DragSelectMode {\n  if (event.altKey) return \"subtract\"\n  if (event.shiftKey || event.metaKey || event.ctrlKey) return \"add\"\n  return \"replace\"\n}\n\n/**\n * The selection a box produces, given what was selected before it was drawn.\n *\n * `base` order is preserved and hits are appended in the order they were found, so a selection that\n * is shown back to the user — or sent as a payload — does not reshuffle itself on every move of the\n * pointer.\n */\nexport function applyDragSelection(\n  base: readonly string[],\n  hits: readonly string[],\n  mode: DragSelectMode\n): string[] {\n  if (mode === \"replace\") return [...hits]\n  if (mode === \"subtract\") {\n    const drop = new Set(hits)\n    return base.filter((id) => !drop.has(id))\n  }\n  const seen = new Set(base)\n  return [...base, ...hits.filter((id) => !seen.has(id))]\n}\n\n/** The ids whose boxes the marquee is over, in the order they were given. */\nexport function boxHits(\n  items: readonly { id: string; box: DragSelectBox }[],\n  box: DragSelectBox\n): string[] {\n  return items.filter((item) => boxesOverlap(item.box, box)).map((item) => item.id)\n}\n\n/**\n * How far the container should scroll itself this frame, from where the pointer is inside it.\n *\n * Without this the component is quietly mouse-and-visible-area only: drag to the bottom of a\n * scrolling list and the rows below the fold can never be reached, because the list only scrolls\n * for a wheel it is not being given. The speed tapers across the edge band instead of switching on\n * at full tilt, so arriving near the edge nudges and pinning against it runs.\n *\n * `bounds` is the container's own viewport rectangle and `point` the pointer in viewport\n * coordinates — both outside the content system, because the question \"is the finger near the edge\n * of the window onto the list\" is about the window, not about the list.\n */\nexport function edgeScrollDelta(\n  point: DragSelectPoint,\n  bounds: { left: number; top: number; width: number; height: number },\n  edge = DRAG_SELECT_EDGE,\n  speed = DRAG_SELECT_SPEED\n): { dx: number; dy: number } {\n  const axis = (position: number, start: number, length: number) => {\n    // A band wider than half the container would overlap its own opposite number and scroll both\n    // ways at once in the middle; clamping keeps a short list from scrolling while the pointer sits\n    // still in the centre of it.\n    const band = Math.min(edge, length / 2)\n    if (band <= 0) return 0\n    const fromStart = position - start\n    const fromEnd = start + length - position\n    if (fromStart < band) return -speed * Math.min(1, (band - Math.max(fromStart, 0)) / band)\n    if (fromEnd < band) return speed * Math.min(1, (band - Math.max(fromEnd, 0)) / band)\n    return 0\n  }\n  return {\n    dx: axis(point.x, bounds.left, bounds.width),\n    dy: axis(point.y, bounds.top, bounds.height),\n  }\n}\n\n/** Starts a session, not yet a marquee: `active` stays false until the threshold is crossed. */\nexport function beginDragSelect(init: {\n  pointerId: number\n  viewport: DragSelectPoint\n  content: DragSelectPoint\n  base: readonly string[]\n  mode: DragSelectMode\n}): DragSelectSession {\n  return {\n    pointerId: init.pointerId,\n    anchor: init.content,\n    point: init.content,\n    viewport: init.viewport,\n    base: [...init.base],\n    mode: init.mode,\n    active: false,\n  }\n}\n\n/**\n * Moves a session to a new pointer position, arming it once it has travelled far enough.\n *\n * The threshold is measured from the anchor rather than between consecutive moves, and once armed\n * the session stays armed: a marquee that disarmed itself whenever the pointer came back near where\n * it started would release a selection the user has already seen.\n */\nexport function advanceDragSelect(\n  session: DragSelectSession,\n  next: { viewport: DragSelectPoint; content: DragSelectPoint },\n  threshold = DRAG_SELECT_THRESHOLD\n): DragSelectSession {\n  const travelled =\n    Math.abs(next.content.x - session.anchor.x) >= threshold ||\n    Math.abs(next.content.y - session.anchor.y) >= threshold\n  return {\n    ...session,\n    point: next.content,\n    viewport: next.viewport,\n    active: session.active || travelled,\n  }\n}\n\ninterface DragSelectProps\n  extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\" | \"onChange\"> {\n  /**\n   * The list, grid or canvas being selected over. Mark the selectable elements with\n   * `{...dragSelectItem(id)}`; anything else inside is empty space a drag may start from.\n   */\n  children: React.ReactNode\n  /** Controlled set of selected ids. Pair with `onChange` to own the state. */\n  value?: string[]\n  /** Selected ids on first render when uncontrolled. */\n  defaultValue?: string[]\n  /** Handed the next set of selected ids — during the drag as well as at the end of it. */\n  onChange?: (ids: string[]) => void\n  /** Told when a marquee appears and disappears, for a cursor or a hint in the surrounding UI. */\n  onDraggingChange?: (dragging: boolean) => void\n  /** Turns the marquee off without unmounting it, so the list stays scrollable and clickable. */\n  disabled?: boolean\n  /** Pointer travel before a press becomes a drag rather than a click. */\n  threshold?: number\n  /** Width of the band at each edge inside which the container scrolls itself. 0 turns it off. */\n  edge?: number\n  /** Pixels per frame of auto-scroll at the very edge. */\n  speed?: number\n  /**\n   * Lets a finger draw a marquee too. Off by default: on a touch screen a drag from empty space is\n   * how the list is panned, and taking that over leaves a list that cannot be scrolled.\n   */\n  allowTouch?: boolean\n  /** Styles the marquee rectangle. */\n  marqueeClassName?: string\n}\n\n/**\n * Rubber-band selection: drag from the empty space around a list, a grid or a board, and everything\n * the rectangle touches is selected.\n *\n * It owns the scroll container, because the rectangle only makes sense relative to something that\n * scrolls, and auto-scrolling at the edges is half of what makes it usable. Selection state is\n * ordinary React state — controlled with `value` plus `onChange` or uncontrolled with\n * `defaultValue` — which is the point: the marquee is one way into the same selection the\n * consumer's own checkboxes, clicks and keyboard shortcuts write to, never the only way in.\n *\n * Pair it with `bulk-action-bar` for what happens after, and note that it deliberately does not\n * handle presses that land *on* an item: those stay with the item, which is what keeps a\n * `sortable-list` row draggable inside a selectable list.\n */\nexport function DragSelect({\n  children,\n  value,\n  defaultValue = [],\n  onChange,\n  onDraggingChange,\n  disabled = false,\n  threshold = DRAG_SELECT_THRESHOLD,\n  edge = DRAG_SELECT_EDGE,\n  speed = DRAG_SELECT_SPEED,\n  allowTouch = false,\n  marqueeClassName,\n  className,\n  ...props\n}: DragSelectProps) {\n  const containerRef = React.useRef<HTMLDivElement | null>(null)\n  const [uncontrolled, setUncontrolled] = React.useState<string[]>(defaultValue)\n  const selected = value ?? uncontrolled\n  const [session, setSession] = React.useState<DragSelectSession | null>(null)\n\n  // The live selection is read from a ref inside the pointer handlers rather than from the render\n  // they were created in. A drag emits several changes per second and the handler that computes the\n  // next one must see the last — a stale closure here is the classic \"only the first move sticks\".\n  const selectedRef = React.useRef(selected)\n  selectedRef.current = selected\n\n  const commit = (next: string[]) => {\n    if (value === undefined) setUncontrolled(next)\n    onChange?.(next)\n  }\n  // Reached through a ref by the one listener that outlives the render it was created in, so that\n  // Escape calls this render's `onChange` and not the one that was current when the drag started.\n  const commitRef = React.useRef(commit)\n  commitRef.current = commit\n\n  const frameOf = (node: HTMLElement): DragSelectFrame => {\n    const rect = node.getBoundingClientRect()\n    return {\n      left: rect.left,\n      top: rect.top,\n      borderLeft: node.clientLeft,\n      borderTop: node.clientTop,\n      scrollLeft: node.scrollLeft,\n      scrollTop: node.scrollTop,\n    }\n  }\n\n  const itemsOf = (node: HTMLElement, frame: DragSelectFrame) => {\n    const found = node.querySelectorAll<HTMLElement>(`[${DRAG_SELECT_ITEM_ATTR}]`)\n    const items: { id: string; box: DragSelectBox }[] = []\n    for (const element of Array.from(found)) {\n      const id = element.getAttribute(DRAG_SELECT_ITEM_ATTR)\n      // An item that is marked disabled is skipped rather than selected-and-ignored: a bulk action\n      // that silently drops part of what the rectangle covered is worse than one that never\n      // claimed those rows.\n      if (!id || element.getAttribute(\"aria-disabled\") === \"true\") continue\n      items.push({ id, box: toContentBox(element.getBoundingClientRect(), frame) })\n    }\n    return items\n  }\n\n  const applyFor = (next: DragSelectSession) => {\n    const node = containerRef.current\n    if (!node || !next.active) return\n    const frame = frameOf(node)\n    const hits = boxHits(itemsOf(node, frame), boxBetween(next.anchor, next.point))\n    commit(applyDragSelection(next.base, hits, next.mode))\n  }\n\n  // --- auto-scroll ---------------------------------------------------------\n  // Driven from a ref'd frame loop rather than from pointermove, because the pointer stops sending\n  // moves the instant it is held against the edge — which is exactly when the scrolling has to keep\n  // happening. Each frame scrolls, then re-derives the content point from the stored viewport point\n  // so the rectangle grows into the newly revealed rows.\n  const sessionRef = React.useRef<DragSelectSession | null>(null)\n  sessionRef.current = session\n  const frameIdRef = React.useRef<number | null>(null)\n\n  const stopAutoScroll = () => {\n    if (frameIdRef.current !== null) {\n      cancelAnimationFrame(frameIdRef.current)\n      frameIdRef.current = null\n    }\n  }\n\n  const step = () => {\n    frameIdRef.current = null\n    const node = containerRef.current\n    const current = sessionRef.current\n    if (!node || !current || !current.active || edge <= 0) return\n    const rect = node.getBoundingClientRect()\n    const { dx, dy } = edgeScrollDelta(current.viewport, rect, edge, speed)\n    if (dx !== 0 || dy !== 0) {\n      node.scrollLeft += dx\n      node.scrollTop += dy\n      const next = advanceDragSelect(\n        current,\n        { viewport: current.viewport, content: toContentPoint(current.viewport, frameOf(node)) },\n        threshold\n      )\n      sessionRef.current = next\n      setSession(next)\n      applyFor(next)\n    }\n    frameIdRef.current = requestAnimationFrame(step)\n  }\n\n  const startAutoScroll = () => {\n    if (frameIdRef.current === null && edge > 0) frameIdRef.current = requestAnimationFrame(step)\n  }\n\n  React.useEffect(() => stopAutoScroll, [])\n\n  const endSession = (node: HTMLElement | null, pointerId: number) => {\n    stopAutoScroll()\n    setSession(null)\n    sessionRef.current = null\n    onDraggingChange?.(false)\n    if (node?.hasPointerCapture?.(pointerId)) node.releasePointerCapture(pointerId)\n  }\n\n  // Escape abandons the drag and puts the selection back as it was. It is on the window because the\n  // container is not focused — the press that started the drag was on empty space — and it is\n  // bound on whether a drag exists rather than on the session object, which is replaced on every\n  // move: re-subscribing a listener a hundred times a second is how a drag turns into jank.\n  const pressed = session !== null\n  React.useEffect(() => {\n    if (!pressed) return\n    const onKeyDown = (event: KeyboardEvent) => {\n      const current = sessionRef.current\n      if (event.key !== \"Escape\" || !current) return\n      event.preventDefault()\n      commitRef.current([...current.base])\n      endSession(containerRef.current, current.pointerId)\n    }\n    window.addEventListener(\"keydown\", onKeyDown)\n    return () => window.removeEventListener(\"keydown\", onKeyDown)\n  }, [pressed])\n\n  function handlePointerDown(event: React.PointerEvent<HTMLDivElement>) {\n    props.onPointerDown?.(event)\n    if (disabled || session) return\n    if (event.pointerType === \"mouse\" && event.button !== 0) return\n    if (event.pointerType === \"touch\" && !allowTouch) return\n    const node = containerRef.current\n    if (!node) return\n\n    // Only empty space starts a marquee. A press that lands on an item belongs to the item — its\n    // own click, its checkbox, its reorder handle — and stealing it is how a list ends up with a\n    // rectangle you cannot avoid drawing. `data-drag-select-ignore` is the escape hatch for the\n    // rest: a toolbar, a header, a resizer sitting in the same scroll container.\n    const target = event.target as HTMLElement | null\n    if (target?.closest?.(`[${DRAG_SELECT_ITEM_ATTR}], [data-drag-select-ignore]`)) return\n\n    // Suppresses the text selection the press would otherwise start; a drag across a list that also\n    // highlights every label reads as a broken page rather than as a selection.\n    event.preventDefault()\n    node.setPointerCapture?.(event.pointerId)\n    const frame = frameOf(node)\n    const viewport = { x: event.clientX, y: event.clientY }\n    setSession(\n      beginDragSelect({\n        pointerId: event.pointerId,\n        viewport,\n        content: toContentPoint(viewport, frame),\n        base: selectedRef.current,\n        mode: dragSelectMode(event),\n      })\n    )\n  }\n\n  function handlePointerMove(event: React.PointerEvent<HTMLDivElement>) {\n    props.onPointerMove?.(event)\n    const current = sessionRef.current\n    const node = containerRef.current\n    if (!current || !node || event.pointerId !== current.pointerId) return\n    const viewport = { x: event.clientX, y: event.clientY }\n    const next = advanceDragSelect(\n      current,\n      { viewport, content: toContentPoint(viewport, frameOf(node)) },\n      threshold\n    )\n    const appeared = next.active && !current.active\n    sessionRef.current = next\n    setSession(next)\n    if (appeared) onDraggingChange?.(true)\n    applyFor(next)\n    startAutoScroll()\n  }\n\n  function handlePointerUp(event: React.PointerEvent<HTMLDivElement>) {\n    props.onPointerUp?.(event)\n    const current = sessionRef.current\n    if (!current || event.pointerId !== current.pointerId) return\n    // A press on empty space that never became a drag is a click, and a click on the background\n    // clears — unless it was modified, where clearing would throw away the selection the modifier\n    // says is being built on.\n    if (!current.active && current.mode === \"replace\" && selectedRef.current.length > 0) commit([])\n    endSession(containerRef.current, event.pointerId)\n  }\n\n  function handlePointerCancel(event: React.PointerEvent<HTMLDivElement>) {\n    props.onPointerCancel?.(event)\n    const current = sessionRef.current\n    if (!current || event.pointerId !== current.pointerId) return\n    // A cancelled pointer never released onto anything, so the selection goes back rather than\n    // standing at whatever the rectangle happened to be over when the system took the pointer away.\n    commit([...current.base])\n    endSession(containerRef.current, event.pointerId)\n  }\n\n  const box = session?.active ? boxBetween(session.anchor, session.point) : null\n\n  return (\n    <div\n      {...props}\n      ref={containerRef}\n      data-dragging={session?.active ? \"\" : undefined}\n      onPointerDown={handlePointerDown}\n      onPointerMove={handlePointerMove}\n      onPointerUp={handlePointerUp}\n      onPointerCancel={handlePointerCancel}\n      className={cn(\n        \"relative overflow-auto\",\n        // Only while a marquee is up, so the list's own text stays selectable the rest of the time.\n        session?.active && \"select-none\",\n        className\n      )}\n    >\n      {children}\n      {box ? (\n        <div\n          // Decoration: the selection it describes is announced by whatever the consumer shows for\n          // it (`bulk-action-bar` has the live region), and a rectangle read out as it is dragged\n          // would be noise. `pointer-events-none` keeps it from becoming the target of its own move.\n          aria-hidden=\"true\"\n          className={cn(\n            \"pointer-events-none absolute z-10 rounded-[2px] border border-primary bg-primary/15\",\n            marqueeClassName\n          )}\n          style={{ left: box.left, top: box.top, width: box.width, height: box.height }}\n        />\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui",
  "docs": "Install any pulld component by name: add \"@pulld\": \"https://pulld.pages.dev/r/{name}.json\" to the registries block in components.json, then `npx shadcn add @pulld/<name>`. All 140 components: https://pulld.pages.dev/?utm_source=cli"
}
