Drag and drop

Blocks are dragged with a hand-written pointer events system in features/level/dnd/. There are no HTML5 drag events (dragstart, drop, dataTransfer), so dragging works identically with mouse, touch and pen, and the code never fights the browser's native drag behaviour.

The API components see

DndProvider wraps the level screen and exposes one stable interface (useDnd()):

Components never attach raw pointer or drag listeners. Two reactive hooks drive styling: useActiveZoneId() (which zone is under the pointer, for BlockShell highlights and the root zone border) and useActiveDrag() (what is being dragged, so the source can be hidden).

The DraggableDescriptor carries everything the layer needs: the action, the label, and, for existing blocks, the id, source zone, index and the default flag (which is how the delete guard knows the starter blocks are undeletable).

The drag session (session.ts)

A session follows the pointer through these stages:

  1. Press - begin records the descriptor. A hold/slop filter distinguishes a drag from a tap or a scroll: 50 ms hold and 8 px of movement for touch/pen, 4 px for mouse (DND_CONFIG).
  2. Drag - a floating ghost (a cloned node, fixed-positioned, at z-index 1000) follows the pointer. The body gets data-dragging, and data-deletable-drag when the dragged block is deletable.
  3. Targeting - every move, targets.ts hit-tests with document.elementFromPoint against [data-drop-zone] / [data-drop-delete], then computeHoverIndex (in lib/hover.ts) computes the insertion index from the pointer's Y against the zone's direct items (skipping items that belong to nested zones). The active zone gets data-drag-over='true', which CSS turns into an outline.
  4. Drop - resolveDrop reuses the tree helpers from domain/sequence-tree.ts: palette insert via insertAt, same-zone reorder via moveWithinZone, cross-zone move via removeAt + insertAt, delete via removeAt. Inserts clamp past locked items, so locked starter blocks stay ahead of anything you place.
  5. Cancel - Escape or pointercancel ends the session without a change.

Autoscroll

While dragging near the top or bottom viewport edge (within autoscrollEdgePx = 56 px), the session scrolls the window on an animation frame at up to autoscrollSpeedPx = 14 px per frame. Long programs can therefore be dragged into without manually scrolling first.

Tunables

Every threshold lives in dnd/config.ts (hold, slop, ghost z-index, autoscroll edge and speed). The rule is to tune numbers there only, never to inline them elsewhere, so the feel of dragging is adjustable from one file.

Drop zone ids

Recap from The program tree: root, <itemId>, <itemId>:else, <itemId>:branch-<n>. Container blocks receive their zone ids as props and attach them internally, so the renderer never invents zone ids on the fly.

Next: The playground.