/** Drag-and-drop MIME + helpers for reordering sidebar labels / folders. */ export const SIDEBAR_NAV_DND_MIME = "application/x-ultimail-sidebar-nav" export type SidebarNavDragPayload = | { kind: "label"; id: string } | { kind: "folder"; id: string } export type SidebarNavDropPlacement = "before" | "after" | "inside" export function setSidebarNavDragData( e: React.DragEvent, payload: SidebarNavDragPayload ) { const encoded = JSON.stringify(payload) e.dataTransfer.setData(SIDEBAR_NAV_DND_MIME, encoded) /** Fallback for browsers that hide custom types until drop. */ e.dataTransfer.setData("text/plain", encoded) e.dataTransfer.effectAllowed = "move" } export function readSidebarNavDragData( e: React.DragEvent, active: SidebarNavDragPayload | null = null ): SidebarNavDragPayload | null { for (const mime of [SIDEBAR_NAV_DND_MIME, "text/plain"]) { const raw = e.dataTransfer.getData(mime) if (!raw) continue try { const parsed = JSON.parse(raw) as SidebarNavDragPayload if (parsed?.kind === "label" || parsed?.kind === "folder") { if (typeof parsed.id === "string" && parsed.id.length > 0) return parsed } } catch { /* ignore */ } } return active } export function hasSidebarNavDrag(e: React.DragEvent): boolean { return e.dataTransfer.types.includes(SIDEBAR_NAV_DND_MIME) } /** Prefer React state — `dataTransfer.types` is unreliable during `dragover`. */ export function isSidebarNavDragEvent( e: React.DragEvent, active: SidebarNavDragPayload | null ): boolean { return active !== null || hasSidebarNavDrag(e) } /** Top/bottom thirds = reorder; middle third = nest (folders only). */ export function resolveNavDropPlacement( e: React.DragEvent, allowNest: boolean ): SidebarNavDropPlacement { const rect = e.currentTarget.getBoundingClientRect() const ratio = (e.clientY - rect.top) / Math.max(rect.height, 1) if (!allowNest) return ratio < 0.5 ? "before" : "after" if (ratio < 0.25) return "before" if (ratio > 0.75) return "after" return "inside" }