Drag & Drop

The 2D drag/drop engine that powers the canvas, sidebar layer reordering, and toolbox-to-canvas drops.

The 2D drag/drop engine that powers the canvas, sidebar layer reordering, and toolbox-to-canvas drops.

Key files

Concepts

  • findPosition2D — given a pointer position over a target container, returns the drop slot (before/after a child, into a child, into the empty container). 2D-aware — uses both axes to disambiguate row vs column targets.
  • Direction-based alignment — for flex containers, drop indicators align with the main axis. The DOM-derived flex direction is the default; per-component overrides via SPATIAL_MAIN_AXIS_BY_NAME.
  • Cross-axis suppression — selected table-internal node names suppress cross-axis alignment because cells flow strictly within rows.
  • Ghost preview — the engine renders a translucent preview at the proposed drop position before commit.
  • Compound intents — drag onto an empty container = drop in. Drag near the edge = insert before/after. Drag onto a child = drop alongside.

Rules (drag listener policy)

From .claude/rules/drag-listeners.md:

  • Drag/resize/pointer gestures attach pointermove/pointerup/pointercancel/blur to window on pointerdown.
  • Never use React onPointer* props for the move/up phase.
  • Never trust setPointerCapture alone — outside the captured target, events still leak.

Drop promote guard

shouldPromoteToParent in the drop logic must inspect the parent flex direction, not just the current container's. Without that check, flex-row sections trap drops — the cursor crosses an inner column boundary and the engine wrongly flags "drop into this column" instead of "drop after the section". See feedback_drop_promote_guard and the project memory for context.

Related