# Drag

> The parts every drag in Raft is built from — one sensor set, the lifted copy that follows the hand, the dashed place where an item will land, the reason a place refuses it, and announcements by name.

Source: https://raft-design.botiverse.build/components/drag. Generated from the current release; where it differs from what you remember, this page is right.

The Drag parts are what every drag in Raft is built from, on dnd-kit: `useDragSensors` starts a drag the same way everywhere, `DragLift` is the copy that follows the hand and settles on a drop or a cancel, `DropPlaceholder` is the one look of where an item will land, and `DragRefusalHint` says why a place won't take it. The model they implement, and its reasons, is in [Drag and drop](https://raft-design.botiverse.build/patterns/drag-and-drop.md). Import them from `@botiverse/raft-ui/wip`: the API can still change while Raft Chat moves its drags onto it.

```tsx
import { useCallback, useMemo, useState } from "react";
import type { ComponentProps, ReactNode } from "react";
import { Hash, Lock } from "lucide-react";
import {
  closestCenter,
  DndContext,
  MeasuringStrategy,
  pointerWithin,
  useDroppable,
} from "@dnd-kit/core";
import type { CollisionDetection } from "@dnd-kit/core";
import { SortableContext, useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
  SidebarItem,
  SidebarItemChannelIcon,
  SidebarItemContent,
  SidebarItemIcon,
  SidebarItemTitle,
  SidebarList,
  SidebarRoot,
  SidebarSection,
  SidebarSectionChevron,
  SidebarSectionCount,
  SidebarSectionDisclosure,
  SidebarSectionHeader,
  SidebarSectionTitle,
} from "@botiverse/raft-ui";
import {
  DragLift,
  DragLiveRegion,
  DragRemeasure,
  DraggedSlot,
  dragListStrategy,
  useDragAnnouncer,
  useDragLists,
  useDragSensors,
  useDragSiblingTransition,
} from "@botiverse/raft-ui/wip";
import type { DragListsItems } from "@botiverse/raft-ui/wip";

type Section = "pinned" | "channels";

const SECTIONS = ["pinned", "channels"] as const;
const SECTION_NAME: Record<Section, string> = { pinned: "Pinned", channels: "Channels" };
const PRIVATE = new Set(["launch-week", "hiring"]);
const MEASURING = { droppable: { strategy: MeasuringStrategy.Always } };
const INITIAL: DragListsItems<Section> = {
  pinned: ["launch-week", "design"],
  channels: ["general", "release-notes", "hiring", "research"],
};

/** A row wins over the section it sits in; by keyboard (no pointer), the closest row. */
const collision: CollisionDetection = (args) => {
  const hits = pointerWithin(args);
  if (hits.length > 0) {
    const row = hits.find((hit) => !SECTIONS.includes(hit.id as Section));
    return row ? [row] : [hits[0]!];
  }
  return closestCenter(args);
};

const channelName = (id: string) => `#${id}`;
const sectionName = (section: Section) => SECTION_NAME[section];

export function ListsContent() {
  const [items, setItems] = useState(INITIAL);
  const [selected, setSelected] = useState("launch-week");
  const { message, announce } = useDragAnnouncer();
  const lists = useDragLists({
    lists: SECTIONS,
    items,
    onItemsChange: setItems,
    listName: sectionName,
    itemName: channelName,
    collisionDetection: collision,
    announce,
  });
  const sensors = useDragSensors({ coordinateGetter: lists.coordinateGetter });
  const into = lists.activeId ? lists.listOf(items, lists.activeId) : null;

  return (
    <SidebarRoot className="w-64 py-1">
      <DndContext
        sensors={sensors}
        collisionDetection={lists.collisionDetection}
        measuring={MEASURING}
        autoScroll={false}
        accessibility={lists.accessibility}
        {...lists.handlers}
      >
        <DragRemeasure version={items} />
        {SECTIONS.map((section) => (
          <ChannelSection
            key={section}
            section={section}
            ids={items[section]}
            into={into === section && lists.originList !== section}
          >
            {items[section].map((id) => (
              <ChannelRow
                key={id}
                id={id}
                selected={selected === id}
                isHome={lists.isHome}
                onSelect={setSelected}
              />
            ))}
          </ChannelSection>
        ))}
        <DragLift>
          {lists.activeId ? (
            <Channel id={lists.activeId} selected={selected === lists.activeId} />
          ) : null}
        </DragLift>
      </DndContext>
      <DragLiveRegion message={message} />
    </SidebarRoot>
  );
}

function ChannelSection({
  section,
  ids,
  into,
  children,
}: {
  section: Section;
  ids: readonly string[];
  into: boolean;
  children: ReactNode;
}) {
  const { setNodeRef } = useDroppable({ id: section });
  return (
    <SidebarSection>
      <SidebarSectionHeader>
        <SidebarSectionDisclosure type="button">
          <SidebarSectionChevron />
          <SidebarSectionTitle>{SECTION_NAME[section]}</SidebarSectionTitle>
          <SidebarSectionCount>{ids.length}</SidebarSectionCount>
        </SidebarSectionDisclosure>
      </SidebarSectionHeader>
      <SidebarList>
        <SortableContext id={section} items={ids as string[]} strategy={dragListStrategy}>
          <div
            ref={setNodeRef}
            data-drag-into={into ? "" : undefined}
            className="min-h-8 rounded-control"
          >
            {children}
          </div>
        </SortableContext>
      </SidebarList>
    </SidebarSection>
  );
}

function ChannelRow({
  id,
  selected,
  isHome,
  onSelect,
}: {
  id: string;
  selected: boolean;
  isHome: (id: string, index: number) => boolean;
  onSelect: (id: string) => void;
}) {
  const siblingTransition = useDragSiblingTransition();
  const { attributes, listeners, setNodeRef, transform, transition, isDragging, index, overIndex } =
    useSortable({ id, transition: siblingTransition });
  const handleClick = useCallback(() => onSelect(id), [id, onSelect]);
  const style = useMemo(
    () => ({ transform: CSS.Translate.toString(transform), transition }),
    [transform, transition],
  );
  return (
    <div ref={setNodeRef} style={style}>
      <DraggedSlot dragging={isDragging} home={isHome(id, overIndex >= 0 ? overIndex : index)}>
        <Channel id={id} selected={selected} onClick={handleClick} {...attributes} {...listeners} />
      </DraggedSlot>
    </div>
  );
}

function Channel({
  id,
  selected,
  ...props
}: { id: string; selected: boolean } & Omit<
  ComponentProps<typeof SidebarItem>,
  "children" | "id"
>) {
  return (
    <SidebarItem active={selected} {...props}>
      <SidebarItemIcon aria-hidden>
        <SidebarItemChannelIcon>{PRIVATE.has(id) ? <Lock /> : <Hash />}</SidebarItemChannelIcon>
      </SidebarItemIcon>
      <SidebarItemContent>
        <SidebarItemTitle>{id}</SidebarItemTitle>
      </SidebarItemContent>
    </SidebarItem>
  );
}
```

## When to use

| Situation                                                                       | Use                                                                  |
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Reordering items in a list, or moving them between lists (a sidebar's sections) | `useDragLists` with `DraggedSlot`, `DragLift` and `useDragSensors`   |
| Moving task cards on a board                                                    | [TaskDnd](https://raft-design.botiverse.build/components/task-dnd.md), built on these parts             |
| Reordering tabs                                                                 | [`SortableTabsList`](https://raft-design.botiverse.build/components/tabs.md)                                |
| Reordering composer attachments                                                 | [`SortableComposerAttachments`](https://raft-design.botiverse.build/components/composer.md)             |
| Moving a section of a layout (side sections)                                    | `useDragSensors`, `DragLift` with a compact proxy, `DropPlaceholder` |
| Resizing a column                                                               | [Resizable](https://raft-design.botiverse.build/components/resizable.md)                               |

## Anatomy

- `useDragSensors()`: the one sensor set for a `DndContext`. A mouse or a pen after 6px (`MOUSE_DRAG_DISTANCE_PX`); a finger after a 500ms hold and a 10px move (`useHoldThenMoveTouchSensor`); the keyboard on Space (`DRAG_KEYBOARD_CODES`), never Enter. Pass `coordinateGetter` (or only the getter, `useDragSensors(getter)`) for how the arrows move. Escape during a drag from any of them only puts the item back: it is taken in the capture phase on the window and stopped, so no app Escape handler (a panel, a dialog, a popover) hears it until the drag is over.
- `DragLift`: the lifted copy, inside the `DndContext`. Its children are the copy: the item itself, or a compact proxy. It takes the control radius; pass the item's own in `className`. It also sets the grabbing cursor, cancels on window blur and a hidden tab, scrolls the area the pointer is near the edge of, and sets a keyboard drag's copy 6px off its place. Turn dnd-kit's own auto-scroll off (`autoScroll={false}`). The copy renders in `document.body` by default, in a portal that keeps the theme scope it was rendered in (`light` / `dark`) and the item's writing direction (`dir`), so nothing around the item holds it in: neither a stacking context it couldn't rise out of (a [ScrollArea](https://raft-design.botiverse.build/components/scroll-area.md#rules), an `isolate`) nor a transform (a drawer, a dialog) that would offset it. Among the body's layers it sits on top (`DRAG_LIFT_Z_INDEX`, 999: over popovers, stacked dialogs and toasts). `container` renders it in another element, and `null` in place, where it rises no higher than the stacking context around it. The copy inherits nothing else from where it was picked up (its font, colour, or a custom property a container sets, such as a row height), so give it its own styles.
- `keepGrabInside(proxyWidth)`: a modifier for a compact proxy, so the hand stays on it when the item was grabbed far from its start. `LIFT_CLASS` is the lift look for a copy drawn without `DragLift`.
- `DropPlaceholder`: where the item will land, at the item's size. `opaque` in a layout's gap or new-column strip, so the column behind doesn't show through.
- `DragRefusalHint`: the reason a place refuses the item, in a HUD chip by the pointer, with the not-allowed cursor. Render it only while the place under the hand refuses. The chip sits right of and below the pointer; near the window's right edge it goes left of the pointer and of the copy, near the bottom edge above the pointer. It never sits under the copy and always stays 8px inside the window. Its size is measured when its content changes, not every frame.
- `DraggedSlot`: wraps each item's content in a list; pass `dragging={isDragging}` (it is `false` by default). The dragged item's own slot is at 40% (`ORIGIN_OPACITY`) while home and the place, with the item's `radius`, once it stands elsewhere.
- `useDragLists`: items in one or more lists, reordered and moved between them, with refusals and announcements by name. It returns the `DndContext`'s handlers, collision detection and `accessibility`, and the `coordinateGetter` for `useDragSensors`. Render `DragRemeasure version={items}` inside the context.
- `useDragAcrossLists`, `listKeyboardCoordinates` and `DragRemeasure`: the keyboard fix `useDragLists` is built on, for a surface with lists of its own.
- `createDragAnnouncements`, `dragInstructions`, `DEFAULT_DRAG_MESSAGES`: announcements and instructions for dnd-kit's `accessibility`, by name, in your language. A source whose `name` is empty says nothing. `useDragAnnouncer` and `DragLiveRegion` for a surface that announces on its own.
- `useDragSiblingTransition()` (or `DRAG_SIBLING_TRANSITION`): the 160ms slide for `useSortable({ transition })`, none with reduced motion. `DROP_GLIDE` is the glide `DragLift` uses, and `DRAG_SETTLE` its duration and easing.
- `cancelDrag()`: cancels the drag in progress, whichever sensor holds it. It sends the window a `resize`, which every dnd-kit sensor cancels on, never a synthetic Escape, so no dialog, panel or popover that closes on Escape hears it.
- Data attributes the stylesheet reads: `data-drag-origin` on the item a drag came from (40%), `data-drag-into` on a container the item has moved into (`fill-subtle`).

## States

| State                | Look                                                                                                                                                       |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Lifted               | The copy: `layer-popover`, `shadow-float`, a `line-seam` ring; no scale, no tilt. The cursor is `grabbing` everywhere                                      |
| Home                 | The item stays in its place at 40%                                                                                                                         |
| Over a place         | `DropPlaceholder`: 1.5px dashed `line-strong` on `fill-subtle`, the item's radius and size; opaque on `layer-canvas` in a layout's gap or new-column strip |
| Over a refused place | No placeholder; the `DragRefusalHint` chip by the pointer; the cursor is `not-allowed`                                                                     |
| Dropped or cancelled | The copy glides into the place, or home, in 160ms; then the item shows there                                                                               |

## Behavior

- The copy follows the pointer 1:1 from the grab point; never clamp it or hold it to one axis with a modifier. A compact proxy may take a modifier that keeps it under the hand.
- dnd-kit measures the overlay's first element, so the keyboard offset is drawn inside it and moves no drop target.
- With `prefers-reduced-motion`, neither the copy nor the siblings animate.
- Escape during a pointer or a keyboard drag cancels it: the copy glides home, and the keydown goes no further (`preventDefault`, `stopImmediatePropagation`). After the drag, Escape behaves as everywhere else.
- The refusal chip stays inside the window and off the copy: flipped left of the pointer and the copy at the right edge, above the pointer at the bottom edge, held 8px inside on every side.

## Accessibility

- The parts wrap dnd-kit. A draggable item carries dnd-kit's attributes: it is focusable, with a role description and the instructions.
- Keyboard: Space picks up, the arrow keys move one place, Space drops, Escape cancels (and only cancels: no app Escape handler hears it). Enter never starts a drag. With `useDragLists`, or `useDragAcrossLists` and `DragRemeasure`, each arrow moves exactly one place, also right after a move into another list.
- Pass names, never ids: `createDragAnnouncements({ name, place, refusal }, messages)`. The English defaults are in `DEFAULT_DRAG_MESSAGES`; pass your translations as `messages`, each built from the same details. With no name for the item, say nothing rather than its id: `SortableTabsList` names a tab by its `aria-label` or text, `SortableComposerAttachments` an attachment by its `label`, and `TaskDndProvider` a card by its `describe`, silent without it.
- `DropPlaceholder` and `DragRefusalHint` are hidden from assistive technology: announce the reason as well.

## Rules

- Use `useDragSensors` for every drag, so a surface never picks its own distance, hold or keys.
- Render the copy in `DragLift`, never by moving the item itself.
- Keep the item in its place while it is dragged, and let `data-drag-origin` fade it.

## API

### DragRemeasure

Measures every drop target again once an item's move into another list has rendered. dnd-kit's own observers skip targets whose size didn't change, even when they moved. Render it inside the `DndContext`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `version` (required) | `{}` |  | Anything that changes when an item moves list, such as the lists' items. |

### DragLift

The copy of a dragged item that follows the hand (design/patterns/drag-and-drop.md#what-follows-the-hand): an opaque popover surface with `shadow-float` and a `line-seam` ring, no scale and no tilt, 1:1 with the pointer from the grab point, never clamped to its container and never held to one axis. On a drop it glides into its place, and on a cancel back home (160ms, `motion-drag-settle-*`; none with reduced motion).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `autoScroll` | `boolean` | `true` | Scroll whatever scrollable area the pointer comes within 48px of the edge of. Turn dnd-kit's own off (`<DndContext autoScroll={false}>`) so the two don't scroll together. |
| `children` | `ReactNode` |  | The lifted copy: the dragged item itself, or a compact proxy for a large one. Nothing while nothing is lifted. |
| `container` | `Element \| DocumentFragment` |  | Where the copy renders: `document.body` by default, so no ancestor holds it in, neither a stacking context (a ScrollArea, an `isolate`) it couldn't rise out of nor a transform (a drawer, a dialog) that would offset a `position: fixed` copy. Another element renders it there, and `null` in place. It keeps the theme scope it was rendered in (`light` / `dark`), as raft-ui's portals do, and the item's writing direction (`dir`). Nothing else is inherited from where it was picked up (font, colour, custom properties a container sets): give the copy its own styles. |
| `modifiers` | `Modifier[]` |  | Modifiers for the copy only, such as one that keeps a compact proxy under the hand. |
| `zIndex` | `number` | `DRAG_LIFT_Z_INDEX` |  |

### DragRefusalHint

Shown while the hand is over a place that refuses the item (design/patterns/drag-and-drop.md#refused-places): the reason, in a HUD chip by the pointer (by the lifted copy in a keyboard drag), and the not-allowed cursor. Render it only while the place is refused, inside the `DndContext`, with no placeholder anywhere. Announce the reason as well: the chip is not read out.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` (required) | `string` |  | Why the place under the hand won't take the item, such as "Can't move from Todo to Done". |
| `container` | `Element \| DocumentFragment` |  | Render the hint in another element; by default it renders in `document.body`. |

Also takes the attributes of its element.

### DropPlaceholder

Where a dragged item will land (design/patterns/drag-and-drop.md#where-it-will-land): a 1.5px dashed `line-strong` outline on `fill-subtle`, with the item's radius and size. One look for every place. The control radius is the default; pass the item's own (`rounded-container` for a card). Hidden from assistive technology: the announcements say where the item is.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `opaque` | `boolean` | `false` | In a layout (the gap a column makes, a new column's strip): an opaque `layer-canvas` under the `fill-subtle` wash, so the column behind the gap doesn't show through while its sections reflow. Nothing is drawn over live content (design/patterns/drag-and-drop.md#where-it-will-land). |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### DragLiveRegion

A polite, visually hidden live region for drag announcements.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `message` (required) | `string` |  |  |

### DraggedSlot

Wraps each item's content in a list. The dragged item's own node is at 40% while it is still home, and the dashed place (its size, its radius) once it stands anywhere else. Every other item renders as it is: the wrapper stays the same, so the item keeps focus when its drag starts.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` (required) | `ReactNode` |  |  |
| `dragging` | `boolean` | `false` | This item is the one being dragged. Render the slot around every item and pass `isDragging`, so the dragged one keeps its node (and focus). |
| `home` (required) | `boolean` |  | The dragged item still stands at its own place. |
| `radius` | `string` |  | The item's radius class, for the place: `rounded-control` by default. |

## Changes

- 0.81.0 (Fixed): **`DragLift` renders the copy in `document.body` by default**: the lifted copy goes through a portal to the body unless `container` names another element, or `null` keeps it in place; the portal keeps the theme scope it was rendered in. `TaskDragOverlay` follows, as `SortableTabsList` and `SortableComposerAttachments` already did.
- 0.81.0 (Fixed): **In a right-to-left layout the dragged copy keeps its direction**: `DragLift`'s portal carries the item's computed writing direction (`dir`) to the body, as it carries the theme scope.
