description:Use when implementing or modifying OpenChamber sortable or drag-to-reorder behavior, especially `@dnd-kit`, touch/mobile interactions, variable-width items, or wrapping layouts.
OpenChamber uses **@dnd-kit** (`@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`) for drag-to-reorder. Getting it to feel right on both desktop and mobile — especially for chips/tags of **variable width that wrap onto multiple rows** — has several non-obvious gotchas. This skill captures the configuration that works, and the bugs to avoid.
**Reference implementations in this repo:**
-`packages/ui/src/components/chat/DraftPresetChips.tsx` — variable-width chips that wrap (the hardest case; mobile long-press).
- Adding any reorderable list, row of chips/tags, or sortable grid.
- Fixing an existing sortable that stretches the dragged item, jumps/overlaps across rows, throws "Maximum update depth exceeded", or doesn't work on touch.
## The Five Rules (most important first)
### 1. Translate, not Transform — kills the "stretch to slot width" bug
`CSS.Transform.toString(transform)` emits `translate3d(...) scaleX(..) scaleY(..)`. For variable-width items the sorting strategy puts a non-1 scale on the lifted item so it **stretches to the neighbor/slot width**. Use `CSS.Translate.toString(transform)` — translation only, no scale — and the dragged item keeps its own size. Sibling shifting is unaffected (their scale was 1 anyway).
| Single vertical list | `verticalListSortingStrategy` |
`horizontalListSortingStrategy` on a **wrapping** row is the classic mistake: it assumes one row, so dragging to another row makes items overlap instead of reflowing. Use `rectSortingStrategy` for anything that wraps.
### 3. Desktop + mobile = two sensors (MouseSensor + TouchSensor with delay)
Do NOT use a single `PointerSensor` — a distance constraint makes touch fight scrolling, and a delay constraint would force desktop to hold-before-drag. Split them:
This gives: tap = activate the item (e.g. submit), **long-press ≈200ms = drag**, swipe = scroll.
### 4. `touch-action: none` on the draggable
Without it the browser hijacks the touch gesture (scrolls) instead of letting dnd-kit drag. Add Tailwind `touch-none` (and usually `select-none`) to the draggable element.
### 5. Reorder on `onDragEnd`, with stable ids and `arrayMove`
```tsx
import{arrayMove}from'@dnd-kit/sortable';
constonDragEnd=(e: DragEndEvent)=>{
const{active,over}=e;
if(over&&active.id!==over.id){
// find by id, never by array index
constfrom=items.findIndex(i=>i.id===active.id);
constto=items.findIndex(i=>i.id===over.id);
setItems(arrayMove(items,from,to));
}
};
```
IDs must be **stable per item** (derive from the item's identity, e.g. `type:name`), never the array index — index ids break tracking after the first move.
## Minimal working pattern (wrapping, variable width, desktop + touch)
A clickable element can be draggable at the same time: keep `onClick` on the button and the activation constraint (distance/delay) lets a plain click/tap through.
## Pitfalls we already hit (don't repeat)
| Symptom | Cause | Fix |
|---------|-------|-----|
| Dragged item **stretches** to the target slot width | `CSS.Transform.toString` applies scaleX/scaleY | Use `CSS.Translate.toString` (Rule 1) |
| On narrow/multi-row: items **don't reflow to other rows, overlap**, unclear drop target | `horizontalListSortingStrategy` on a wrapping row | Use `rectSortingStrategy` (Rule 2) |
| **"Maximum update depth exceeded"** during drag + dragged element floats **offset from the cursor** | Live-reorder in `onDragOver` (empty strategy + `setState` each over) oscillates A↔B with variable sizes; the empty `DragOverlay` we paired with it was mispositioned | Don't reorder in `onDragOver`. Reorder once in `onDragEnd` (Rule 5). Only reach for live-reorder if you truly need physical row-reflow, and then guard against oscillation. |
| Touch drag scrolls the page instead of dragging | Missing `touch-action: none` | Add `touch-none` (Rule 4) |
| Touch: every finger move drags, or tap doesn't register | Single `PointerSensor` with distance | Split into MouseSensor + TouchSensor(delay) (Rule 3) |
## If `rectSortingStrategy` still isn't crisp enough
Reordering variable-width chips across wrapped rows is a documented rough edge in dnd-kit's box strategies. `rectSortingStrategy` is the best the strategy-based approach offers without instability. If a design needs bulletproof cross-row feedback, switch UX: render a **drop-position indicator** (a line/gap showing where it will land) and keep items static during the drag (no reorder until drop) — this avoids both overlap and the oscillation loop, at the cost of more code. Discuss before building it.