/** * `drag-redock` — PURE geometry + descriptor layer for tab drag-to-redock. * * This module is intentionally REACT-FREE and ELECTRON-FREE so it can run under * the node `vitest.config.ts` suite (its spec is `drag-redock.test.ts`). It owns * two responsibilities, both pure functions of their inputs: * * 1. GEOMETRY — `computeDropZone(rect, point)` maps a pointer position over a * group's rectangle to one of five drop zones (the four edge bands plus the * interior `center`). This is the only place edge-band math lives; the React * layer feeds it real `getBoundingClientRect` numbers during dragover. * * 2. DESCRIPTOR — `dropZoneToMutation(zone, dragged, target)` translates a zone * into an engine-NEUTRAL `RedockMutation` intent (`move` | `split`). The * descriptor only NAMES the intent; the React layer realizes it against the * real tree (a split of an EXISTING panel needs extract-then-split, because * `splitPanel` throws if the new panel already exists — see the caller). * * Keeping this split (geometry/descriptor here, engine application in React) * means the geometry is exhaustively unit-testable without a DOM, and the React * layer stays a thin gesture binding. */ /** The five drop zones: four edge bands + the tab-joining interior. */ export type DropZone = 'left' | 'right' | 'top' | 'bottom' | 'center'; /** * Engine-neutral re-dock INTENT. `move` joins the dragged panel into a tab * group; `split` puts it adjacent to a target panel in a new split. This is a * pure descriptor — it does NOT itself touch a tree (the caller applies it, * extract-then-split'ing for an existing panel). */ export type RedockMutation = { kind: 'move'; panelId: string; destGroupId: string; } | { kind: 'split'; atPanelId: string; dir: 'row' | 'column'; side: 'before' | 'after'; newPanelId: string; }; /** * Classify a pointer position (RELATIVE to the rect's top-left; `(0,0)` is the * corner) into a drop zone. * * Band thickness = `edgeFraction * min(width, height)` — a single symmetric band * width derived from the SHORTER side so a wide/short panel doesn't get an * absurdly fat horizontal band. A point in NO band is `center` (the tab-join * interior). A point in TWO bands (a corner) tie-breaks by NORMALIZED distance * to each active edge; on an exact tie HORIZONTAL wins. A point OUTSIDE the rect * clamps to the nearest edge zone (per-axis; diagonal corners pick the larger * overshoot, horizontal breaking ties). */ export declare function computeDropZone(rect: { width: number; height: number; }, point: { x: number; y: number; }, edgeFraction?: number): DropZone; /** * Translate a drop zone into an engine-neutral re-dock descriptor. * * center => move the dragged panel into the target's tab group. * left/right => split the target panel horizontally (row), dragged before/after. * top/bottom => split the target panel vertically (column), dragged before/after. */ export declare function dropZoneToMutation(zone: DropZone, dragged: string, target: { groupId: string; panelId: string; }): RedockMutation; /** * A re-dock is a NO-OP when it would not change the tree: * * 1. The drop targets the dragged panel ITSELF (`dragged === target.panelId`), * in ANY zone — you can neither tab a panel onto its own tab nor split a * panel around itself. Crucially this also guards the SPLIT case: without * it, `extractPanel(dragged)` then `splitPanel(atPanelId = dragged)` removes * the very anchor it then splits at and THROWS (the self-collapse / single- * panel-self-split bug). * 2. A `center` drop into a group the dragged panel ALREADY lives in * (`draggedGroupId === target.groupId`) — `movePanel` would re-append it and * bump the revision for no visible change (churn). * * A SPLIT onto a DIFFERENT panel of the dragged panel's own group is still a real * re-dock (it splits the group around that sibling), so it is NOT a no-op. * * `draggedGroupId` is the id of the tab group currently holding `dragged` * (`undefined` if it is not in the tree). */ export declare function isNoopRedock(dragged: string, draggedGroupId: string | undefined, target: { groupId: string; panelId: string; }, zone: DropZone): boolean; /** * Map a pointer x-position over a horizontal tab strip to an insertion index for * a within-strip REORDER (the `dropPolicy:'reorder-only'` gesture). The strip is * the dragged tab's own group; `tabRects` are the tab buttons' rects in visual * order (each `left` is the rect's left edge, `width` its width). The index is * the count of tabs whose MIDPOINT the pointer has passed: the LEFT half of a tab * inserts BEFORE it, the RIGHT half (and the exact midpoint) inserts AFTER it. A * pointer left of the first tab → 0; past the last tab → `tabRects.length`. Pure * (no DOM): the caller measures the rects and passes the pointer x. An empty * strip or a non-finite pointer → 0. */ export declare function computeReorderIndex(tabRects: readonly { left: number; width: number; }[], pointerX: number): number; /** * Translate a tab strip's VISIBLE-tab drop index into the insertion index * `movePanel`'s same-group reorder expects. * * `computeReorderIndex` reports how many VISIBLE-tab midpoints the pointer has * passed (0..`visibleTabIds.length`), COUNTING the dragged tab's own midpoint and * measured over rects that OMIT `hideTab` panels. `movePanel` inserts into * `panels.filter(p => p !== dragged)` — a different coordinate space. Two shifts * reconcile them: * * 1. Once the pointer passes the dragged tab's OWN midpoint the strip index has * counted the dragged slot, so drop it back out (−1) to get the insertion * slot among the OTHER visible tabs. * 2. Map that visible slot onto the full `panels` order (which may carry hidden * tabs the strip never measured): insert before whichever visible tab now * occupies the slot, or append when the slot is past the last visible tab. */ export declare function resolveReorderInsertIndex(panels: readonly string[], visibleTabIds: readonly string[], draggedPanelId: string, stripInsertIndex: number): number; //# sourceMappingURL=drag-redock.d.ts.map