dioxus-dnd 3.0.0

Modular, accessible drag-and-drop for Dioxus: sortable lists, kanban boards, trees, grids, file drops, multi-select, touch support and more
Documentation
# Architecture

`DndProvider` looks like one component, but it creates three cooperating
layers: a reactive state store, a zone registry, and a pure gesture state
machine. `Draggable` and `DropZone` are ordinary consumers of those layers,
and every layer is public, so anything they do, your own components can do.

API reference: [api/core.md](../api/core.md).
Live demo: every gallery page runs on these layers; the
[Archive](https://kindintelligence.github.io/dioxus-dnd/archive) page renders
the registry's live census over ten thousand virtualized rows.

## The mental model

One provider, three layers:

- A `Store<DragState<T>>` in Dioxus context holds the drag in flight: the
  payload, the source and hovered zones, the pointer, the grab offset, the
  effect and the input mode. `DndContext<T>` is the cheap `Copy` handle over
  it.
- A `ZoneRegistry<T>` alongside it records every mounted drop zone: id,
  label, drop callback, acceptance filter, DOM handle, cached client rect.
  Pointer hit-testing and keyboard navigation are queries against this
  registry, not DOM walks.
- A gesture state machine (`core::machine`) decides what a pointer press
  means: tap, scroll, or drag. It is a pure transition function, so every
  edge case is an exhaustive match arm with a test.

Native drag events appear nowhere in this picture. They are reserved for
the true app boundary - OS file drops, external content in, dragging out -
where the `DataTransfer` protocol is the only option. In-app drags use
pointer events plus keyboard, which keeps the payload a Rust value and the
visuals under your control.

## The state store

The payload travels through the store as a plain `Clone` value: no JSON, no
string ids, no serialization. Stores are Dioxus 0.7's fine-grained
reactivity primitive - each field of `DragState` gets its own lazy
subscription, and every `DndContext` accessor reads through a per-field
lens. A component that calls `dnd.over()` in its render reruns only when the
hovered zone changes, not on every pointer move:

```rust,ignore
let dnd = use_dnd::<Card>();
let lit = dnd.over() == Some(MY_ZONE);   // reruns on hover change only
let ghost_at = dnd.pointer();            // reruns per move - overlays only
```

Writes are granular too: `update_pointer` notifies only `pointer`
subscribers, `enter` and `leave` only `over` subscribers. This is why a
board with hundreds of zones stays smooth mid-drag - per-move work is
confined to the components that actually track the pointer.

## The zone registry

Every mounted `DropZone` registers itself and unregisters on unmount, so
the registry always mirrors what is on screen - a virtualized list with ten
thousand rows keeps only the mounted few dozen registered. Two query
families power the built-in interactions:

- **Pointer hit-testing.** `hit_test_closest` finds the topmost zone that
  contains the point and accepts the payload, using cached rects; when the
  release lands in a gutter, it falls back to the acceptable zone whose rect
  edge is nearest (the built-in drop passes 48px).
- **Keyboard navigation.** `step_zone` and its sibling/child variants walk
  acceptable zones in spatial order (top-to-bottom, then reading order,
  mirrored under `Direction::Rtl`), which is what the arrow keys traverse.
  Zone labels feed the screen-reader announcements.

Rects are cached, measured fresh at pickup. When layout moves under a live
drag - scrolling, a collapsing panel - the `RectRefresh` channel
(`use_rect_refresh`) pings every provider in the tree to re-measure. Idle
providers ignore the ping, so wiring it to raw scroll events costs nothing;
`AutoScroll` pings it for you.

## The gesture machine

A press is not yet a drag. The lifecycle - press, promotion, tracking,
release or abort - is a formal state machine over three phases (`Idle`,
`Pressed`, `Dragging`) with two promotion policies:

- `Promotion::Distance`: travel past the threshold in any direction begins
  the drag. Right for mouse, pen, and touch surfaces that own every gesture.
- `Promotion::HoldOrSideways`: a 250ms hold or a sideways-dominant pull
  begins the drag, while a vertical-dominant pull yields the gesture to
  native scrolling. This is `TouchSense::Auto`, the reason draggables in a
  scrollable list do not fight the finger.

`transition` and `transition_with` are pure functions: same inputs, same
outputs, no side effects. Stray inputs - foreign pointer ids, a second
finger pressing, a hold timer firing after the gesture resolved - are
deliberately inert, each one a tested match arm rather than an ad-hoc `if`.
`Draggable` drives this machine; you can drive it yourself.

## Build your own

Because the layers are public, a custom drop target is just a component
that registers itself and reads the context. This is a working bare zone:

```rust,ignore
#[component]
fn TrashZone(on_trash: EventHandler<DropOutcome<Card>>) -> Element {
    let dnd = use_dnd::<Card>();
    let mut registry = use_zone_registry::<Card>();
    let id = use_zone_id();

    let registration = use_hook(|| registry.register(ZoneRecord {
        id,
        parent: None,
        label: Some("Trash".into()),
        on_drop: Callback::new(move |o| on_trash.call(o)),
        accepts: None,
        mounted: None,
        rect: None,
    }));
    use_drop(move || registry.unregister(id));

    let armed = dnd.dragging();
    let over = dnd.over() == Some(id);

    rsx! {
        div {
            onmounted: move |evt| {
                let m = evt.data();
                registry.set_mounted(registration, m.clone());
                spawn(async move {
                    if let Ok(r) = m.get_client_rect().await {
                        registry.set_rect_if_present(registration,
                            Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height));
                    }
                });
            },
            class: if over { "trash hot" } else if armed { "trash armed" } else { "trash" },
            "Trash"
        }
    }
}
```

Registration is what buys the behavior: pointer drops land here, keyboard
navigation reaches it and announces "Trash", near-miss releases snap to it.
The [Standup](https://kindintelligence.github.io/dioxus-dnd/standup) gallery
page pushes this recipe further, registering one element in two payload
worlds at once.

## Gotchas

- **`records()` subscribes, the id lookups peek.** Rendering from
  `registry.records()` reruns on every mount and unmount - right for
  devtools, wrong inside a hot zone. `get`, `cached_rect` and
  `mounted_handle` never subscribe.
- **`dragging()` is false while a drop settles.** During the overlay's
  settle glide, `payload()` stays readable so the ghost keeps its content,
  but zones have already unlit. Check `settling()` when the distinction
  matters.
- **Do not match the growing enums exhaustively.** `DragMode`,
  `PointerKind` and `GestureEvent` are `non_exhaustive`; new input paths
  arrive as new variants. Match what you handle and let the rest fall
  through.
- **Explicit `ZoneId`s belong below 2^32.** The registry replaces records
  by id, so a collision silently knocks a zone out. Auto ids start at 2^32
  precisely so hand-written `u32`-range ids can never collide.
- **An exact (0,0) pointer is dropped.** `update_pointer` treats it as a
  bogus platform report, so a custom source feeding synthetic moves never
  sees the overlay jump to the corner.

## Related

- [Drag and drop]drag-and-drop.md: the components built on these layers.
- [Virtualized lists]virtualized-lists.md: the registry's
  mount-and-measure model doing its best work.
- [Multi-window desktop drags]multi-window.md: several windows joining
  one shared world built from the same context.
- [Testing]testing.md: `DragSim` drives the production delivery path
  through these same layers, no browser.
- [Debugging]debugging.md: the overlay renders the registry live.