# Feature request: one pointer-driven drag for sections and floating panels - **Status:** RESOLVED (2026-11-03), on branch `feat/pointer-section-drag` (`src/layout/sectionDrag.ts`, `src/layout/dockHitTest.ts`). Accepted by eye in Chromium, Firefox, and on a real iPhone. - **Differs from the request:** a grip press with no travel does nothing, rather than collapsing. `Section` already treats the chevron as the only collapse control. - **Component:** `layout/DockZone.tsx`, `layout/StageFloatHost.tsx`, `layout/Section.tsx`, `layout/insertLine.ts`, `theme/base.css`; docs in `docs/dnd.md`, `docs/architecture.md`, `docs/feature-ideas.md`, `docs/testing.md` - **Pulled by:** dsts-viewer (first consumer to drag sections over a live animated WebGL canvas) - **Severity:** high for WebGL consumers: every move of a section drag visibly drops frames - **Date:** 2026-11-01 ## What happens today In the viewer, with a model animating: - Dragging a **floating panel by its header** is perfectly smooth. - Dragging a **section off any panel**, side or floating, makes the animation stutter on every move. Holding the section still restores the framerate. ## Cause The two gestures use different drag systems. - **Floating panel header:** pointer events and pointer capture, in `FloatingPanel`. - **Section grip:** a native HTML5 drag, with FormKit for reorder and transfer, and the float host reading the document `dragend` for float-out. **Evidence.** Profiled in Chromium with the native session bypassed (`Input.setInterceptDrags`, so no OS-level drag and no native ghost), section handlers cost about 1ms per move at 60fps, the same range as the smooth header drag (0.6 to 1.3ms). So neither our code, FormKit nor layout is the cost, and nothing re-renders per move. What remains is the native session and its ghost, which matches Nuffle's by-eye result: only the native path lags. Whether the session or the ghost image costs more was not separated, because this request removes both. ## Proposal Replace the native path with **one custom pointer layer** that drives every section and panel drag, built on what `FloatingPanel` already does, and remove `@formkit/drag-and-drop`. `docs/feature-ideas.md` already wants these streams consolidated and records that doing float-out through FormKit hit dead-ends, naming a custom pointer layer as an option. This request picks it. ### Ghost - Build it **once** at drag start and move it only with `transform: translate3d(...)`, on its own layer (`will-change: transform`), written at most once per animation frame. - `pointer-events: none`, so hit-testing sees what's under it. - Keep today's look (a clone dressed as a floating panel, inside `.stage-root` so tokens apply). If a full clone of a tall section proves costly to paint, clamp its height or use a title-only ghost. ### Per-move work Coalesce hit-testing and insert-line updates to one per animation frame. `hitTestDock` reads rects for every card, and pointer events can arrive faster than frames. ## Behavior that must survive The current drag layers carry hard-won behavior. Each of these needs to keep working, and most have comments in `DockZone.tsx` and `StageFloatHost.tsx` explaining why: - **Press versus drag on the grip:** a press with no travel collapses, and a drag past a small threshold starts moving. - **Text selection and interaction in section bodies:** only the grip starts a drag. A press in a body must not be swallowed, which is what once broke document-delegated Kobalte selects. - **Outcomes on release:** over the same dock it reorders and keeps collapse state. Over another dock it moves and lands expanded. Over no dock inside the host it floats. Outside the host it does nothing. - **Accordion docks:** an arriving section becomes the sole open one. - **Insertion line:** same look and clamping, on the existing `createInsertLine`. - **Float placement:** lands where the ghost was, using the grab offset, respects `floatRegion`, and seeds width as it does today (or as `requests/section-preferred-width.md` lands, if that goes first). - **Empty floating panels are pruned**, and a source panel's prune must not end the drag early. - **Movement lock:** no grip, no drag. - **Source dimming:** the dragged section stays in place at `.stage-movable-section-dragging` until drop. New with pointer events, and needed because the browser no longer provides them: - **Cancel:** Escape and `pointercancel` end the drag with no change. - **Touch:** `touch-action: none` on grips only, so a body or panel still scrolls. Suppress native `draggable` entirely, so iOS can't hijack a long-press. - **Text selection during a drag:** suppress it while dragging. ## Acceptance - **The original report:** with a model animating in dsts-viewer, dragging a section off a panel looks as smooth as dragging a floating panel by its header. This needs a person's eyes or a real drag. Synthetic CDP drags never showed the lag, so they can't prove the fix. - **Measured:** frame intervals during a real section drag match the header drag on the same scene. - Every item under "Behavior that must survive" has a test, using real `PointerEvent`s. - Float-out, re-dock, reorder and cross-dock transfer work with a finger on iOS Safari. The drop position is right and the section's background stays intact, fixing both gaps in `docs/feature-ideas.md`. - `@formkit/drag-and-drop` is gone from `package.json`, and the docs no longer describe a native path. - No public API changes: `createSectionLayout`, `DockZone`, `StageFloatHost` and `SectionDef` are the same for consumers. ## Out of scope - Moving floating panels with `transform` instead of `left`/`top`. The header drag already measures smooth, so it isn't needed for this, but it would be an easy follow-up once the pointer layer is shared. - Long-press to start a drag, or other new touch gestures. Scroll-versus-drag only needs the grip rule above.