/** * Shared drag-and-drop helpers for tab chips. The payload (which tab, from * which tile) travels as a private MIME type so unrelated drags are ignored. */ export const TAB_DND_MIME = 'application/x-textile-tab'; export type TabDragPayload = { tileId: string; tabId: string }; export function setTabDrag(dt: DataTransfer, payload: TabDragPayload): void { dt.setData(TAB_DND_MIME, JSON.stringify(payload)); dt.effectAllowed = 'move'; } export function hasTabDrag(dt: DataTransfer): boolean { return dt.types.includes(TAB_DND_MIME); } export function readTabDrag(dt: DataTransfer): TabDragPayload | null { const raw = dt.getData(TAB_DND_MIME); if (!raw) return null; try { return JSON.parse(raw) as TabDragPayload; } catch { return null; } } /** * Drop zone within a tile body. Edges split the tile (a new tile on that side); * center moves the tab into the tile. */ export type DropZone = 'center' | 'left' | 'right' | 'top' | 'bottom'; /** Pointer must be within this fraction of an edge to pick that edge's zone. */ const EDGE_FRACTION = 0.25; /** * Classify a pointer position (relative to a tile body of `width`×`height`) * into a drop zone: the nearest edge when within `EDGE_FRACTION` of it, else * center. Pure for testing. */ export function computeDropZone( x: number, y: number, width: number, height: number, ): DropZone { if (width <= 0 || height <= 0) return 'center'; const left = x / width; const right = 1 - left; const top = y / height; const bottom = 1 - top; const nearest = Math.min(left, right, top, bottom); if (nearest >= EDGE_FRACTION) return 'center'; if (nearest === left) return 'left'; if (nearest === right) return 'right'; if (nearest === top) return 'top'; return 'bottom'; }