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()):
draggableProps(descriptor)- attach to any draggable; returns anonPointerDownthat starts a session.dropZoneProps(zoneId)- attach to any drop target; returnsdata-drop-zone="<zoneId>".deleteZoneProps()- returnsdata-drop-delete="true"(the palette tray).
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:
- Press -
beginrecords 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). - Drag - a floating ghost (a cloned node, fixed-positioned, at
z-index 1000) follows the pointer. The body gets
data-dragging, anddata-deletable-dragwhen the dragged block is deletable. - Targeting - every move,
targets.tshit-tests withdocument.elementFromPointagainst[data-drop-zone]/[data-drop-delete], thencomputeHoverIndex(inlib/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 getsdata-drag-over='true', which CSS turns into an outline. - Drop -
resolveDropreuses the tree helpers fromdomain/sequence-tree.ts: palette insert viainsertAt, same-zone reorder viamoveWithinZone, cross-zone move viaremoveAt+insertAt, delete viaremoveAt. Inserts clamp past locked items, so locked starter blocks stay ahead of anything you place. - Cancel -
Escapeorpointercancelends 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.