{"version":"v1.0","client_state":{"DocsNavigation.default":{"fields":{"query":""},"persist":"memory","send_to_server":true},"ColorScheme.default":{"fields":{"mode":"system"},"persist":"local","send_to_server":true}},"endpoints":{"action":"/_bretzel/action","sse":"/_bretzel/sse","refetch":"/_bretzel/refetch"},"csrf":"e049dfd0908aa07d6a99070aae26fdde","page_id":"d42264ff044c44dfacff3910bbb7ef1d","address":""}

Drag and drop

Reordering a list, dragging a card from one column to another. The browser makes the gesture, the server receives the result — and it is the server that decides whether to apply it.

The two halves

`ui.dropzone` is the region that accepts; `ui.draggable` wraps an item one can grab. Both are CONTAINERS: their content is written inside a `with`.
with ui.dropzone(name="todo", accepts=["task"],
                 on_move=move_task):
    for t in ui.each(tasks, key="id"):
        with ui.draggable(key=str(t.id), group="task"):
            ui.card(t.title)
Entry is explicit
A zone always lets its own cards be reordered. To receive a card from another zone, list its `group=` in `accepts=`. With no `accepts=`, unrelated zones cannot exchange cards.

Four drop behaviors

ModeDuring the drag
Reorder in one zoneThe card moves between its neighbors; no `accepts=` is needed.
Move between zonesThe destination lists the card's `group=` in `accepts=`.
Single place`holds="one"` previews replacement without inserting a second card.
Action target`terminal=True` reports the drop without inserting the card; use it for archive or delete.

What the handler receives

A single object, `Move`, with five fields. It says everything needed to apply OR refuse the gesture — and refusing is a normal case.
FieldWhat it says
item_keythe moved item's key — the `ui.draggable(key=…)`'s
from_zonethe departure zone's `name=`
to_zonethe arrival zone's `name=`
from_indexits original position in the zone
to_indexthe position aimed at on arrival
from bretzel.components import Move

def on_drop(move: Move) -> None:
    """What a drop applies — or refuses."""
    if not move.to_zone.startswith("column-"):
        return                 # refused, without a word
    task_id = int(move.item_key)
    move_in_database(task_id, move.to_zone,
                     move.to_index)
`item_key` is a STRING — that is what the DOM carries. A numeric key converts back on arrival, and an `int()` that raises on an unexpected value is better than a move made at random.

Controlling entry and exit

`accepts=` controls entry from another zone. `locked=True` prevents cards from leaving a zone; it can still receive cards. For an archive, combine `accepts=`, `locked=True`, and `terminal=True`. The handler applies or refuses the action.
with ui.dropzone(name="archive", accepts=["task"],
                 locked=True, terminal=True,
                 on_move=archive_task):
    ...

The handle

`handle=True` on the `ui.draggable`: the item is no longer grabbed anywhere, but by a dedicated area. To be taken as soon as the card itself contains controls — without it, dragging on a button moves the card instead of clicking.

A real case, in the repository

`examples/kanban` lets you reorder cards, move them between columns, and archive them in a terminal zone. The server enforces each column’s work-in-progress limit.