/**
 * Panel management system with recursive split layout.
 *
 * Key design: "iframe portal" architecture — iframes live in a fixed
 * portal layer and NEVER move after initial placement. Each panel's
 * `wrapper` stays inside the group content as an empty placeholder;
 * the panel's `element` (containing the iframe) is placed in a portal
 * entry positioned via `position: fixed` over the wrapper using a
 * `requestAnimationFrame` sync loop.
 *
 * This prevents iframe reloads during ALL panel operations (split,
 * float, dock, move, minimize, mode switch) because only empty
 * placeholders are moved by layout operations — the iframes themselves
 * are never reparented after creation.
 *
 * Tab dragging uses pointer events (NOT HTML5 DnD) so there is no
 * browser snap-back animation and we get full control over the ghost
 * element and drop indicators.
 *
 * The SplitLayout uses a recursive binary tree of SplitNode / LeafNode.
 * When splitting a leaf, the new split container is inserted into DOM
 * FIRST (via insertBefore), then the existing leaf is moved INTO it
 * (via appendChild — which moves, never removes). Since panel wrappers
 * are empty placeholders, these moves are always safe.
 */
export interface PanelOptions {
    id: string;
    element: HTMLElement;
    title?: string;
    groupId?: string;
}
export declare class Panel {
    readonly id: string;
    readonly element: HTMLElement;
    title: string;
    group: PanelGroup | null;
    readonly wrapper: HTMLElement;
    constructor(id: string, element: HTMLElement, title?: string);
}
export type DockPosition = "top" | "bottom" | "left" | "right";
export interface GroupOptions {
    id?: string;
    floating?: {
        x: number;
        y: number;
        width: number;
        height: number;
    };
    direction?: DockPosition;
    initialSize?: number | undefined;
}
/**
 * Advance the group ID counter past any restored group IDs so that
 * newly created groups (e.g. via DnD) never collide with restored ones.
 */
export declare function syncGroupIdCounter(existingIds: string[]): void;
export declare class PanelGroup {
    readonly id: string;
    readonly element: HTMLElement;
    readonly headerElement: HTMLElement;
    readonly headerPrefix: HTMLElement;
    readonly tabsElement: HTMLElement;
    readonly headerVoid: HTMLElement;
    readonly headerActions: HTMLElement;
    readonly contentElement: HTMLElement;
    private _panels;
    private _activePanel;
    private _headerHidden;
    private _locked;
    private _onTabClose;
    /** Pointer-based tab drag start (replaces HTML5 dragstart) */
    private _onTabPointerDown;
    private _onTabPopOut;
    private _location;
    private _onLocationChangedCb;
    constructor(id?: string);
    get panels(): readonly Panel[];
    get activePanel(): Panel | null;
    get size(): number;
    get locked(): string | false;
    set locked(value: string | false);
    setHeaderHidden(hidden: boolean): void;
    get headerHidden(): boolean;
    setHeaderActions(element: HTMLElement): void;
    setHeaderPrefix(element: HTMLElement): void;
    onTabClose(cb: (panelId: string) => void): void;
    onTabPointerDown(cb: (panelId: string, event: PointerEvent) => void): void;
    get location(): "grid" | "floating";
    set location(value: "grid" | "floating");
    onLocationChanged(cb: (loc: "grid" | "floating") => void): void;
    /** Force-fire the location changed callback without changing the value. */
    fireLocationChanged(): void;
    private _isInFloatingSplit;
    private _onSplitStateChangedCb;
    get isInFloatingSplit(): boolean;
    set isInFloatingSplit(value: boolean);
    onSplitStateChanged(cb: (isInSplit: boolean) => void): void;
    onTabPopOut(cb: (panelId: string) => void): void;
    /** Drag constraint: "any" allows splits, "merge-only" blocks new splits in the main layout. */
    private _dragConstraint;
    get dragConstraint(): "any" | "merge-only";
    set dragConstraint(v: "any" | "merge-only");
    /** When true, tabs from this group cannot be docked (moved from floating to grid). */
    private _dockingBlocked;
    get dockingBlocked(): boolean;
    set dockingBlocked(v: boolean);
    /** When true, tabs from this group cannot create splits inside floating overlays. */
    private _dialogDockingBlocked;
    get dialogDockingBlocked(): boolean;
    set dialogDockingBlocked(v: boolean);
    private _onPanelCountChangedCb;
    private _onActivePanelChangedCb;
    onPanelCountChanged(cb: (count: number) => void): void;
    onActivePanelChanged(cb: (panelId: string) => void): void;
    addPanel(panel: Panel): void;
    /**
     * Remove a panel and its wrapper from DOM. Used when closing/destroying
     * a panel. For moves between groups, use detachPanel instead.
     */
    removePanel(panelId: string): Panel | null;
    /**
     * Detach a panel from this group WITHOUT removing its wrapper from DOM.
     * The caller is expected to move the wrapper to a new parent via
     * addPanel, whose appendChild call will MOVE (not unmount) the iframe.
     */
    detachPanel(panelId: string): Panel | null;
    activatePanel(panelId: string): void;
    reorderPanel(panelId: string, dropX: number): void;
    private createTab;
    dispose(): void;
}
export interface FloatingOptions {
    x: number;
    y: number;
    width: number;
    height: number;
}
export declare class FloatingOverlay {
    readonly element: HTMLElement;
    /** Current owner group. Updated when ownership transfers after the original owner is removed from an internal split. */
    group: PanelGroup;
    /** Internal split layout for splitting within this floating overlay. */
    internalLayout: SplitLayout | null;
    private toolbarElement;
    private bounds;
    private savedBounds;
    private _isMaximized;
    private _onDragEnd;
    private _onDragMove;
    private _onBoundsChanged;
    constructor(group: PanelGroup, opts: FloatingOptions, parentElement?: HTMLElement, insertBefore?: HTMLElement | null);
    get isMaximized(): boolean;
    set onDragEnd(cb: (centerX: number, centerY: number) => void);
    set onDragMove(cb: (centerX: number, centerY: number) => void);
    set onBoundsChanged(cb: () => void);
    bringToFront(): void;
    maximize(): void;
    restore(): void;
    hide(): void;
    show(): void;
    dispose(): void;
    getBounds(): FloatingOptions;
    /** Update specific bound dimensions without a full resize. Clamps to viewport with padding for drag handles. */
    setBounds(partial: Partial<{
        width: number;
        height: number;
    }>): void;
    /** Replace all bounds (x, y, width, height), clamping to current viewport with padding. */
    setFullBounds(bounds: FloatingOptions): void;
    private applyBounds;
    /**
     * Wire a group's headerVoid as a drag handle for this overlay.
     * Used after ownership transfer when the original group is removed.
     */
    setDragGroup(newGroup: PanelGroup): void;
    /**
     * Allow dragging from the headerActions area (toolbar buttons region)
     * without interfering with actual button clicks.
     */
    private wireDragOnActions;
    /**
     * Place a toolbar actions element into the correct group's header.
     * Appends to headerActions (doesn't replace) so it coexists with
     * ForkGroupActions which occupies the same container.
     */
    setToolbar(el: HTMLElement): void;
    /**
     * Remove the toolbar element from whichever group's header it's in.
     */
    removeToolbar(): void;
    /**
     * Move the toolbar to the correct group header after a split change.
     * Uses appendChild which MOVES the element if already in DOM.
     */
    repositionToolbar(): void;
    /**
     * Find the group whose header should host the toolbar buttons.
     * For a single group: the owner. For splits: the topmost-rightmost
     * group, so the buttons appear at the far right of the top row.
     */
    private getToolbarHost;
    /**
     * Recursively find the topmost-rightmost leaf in a layout tree.
     * Horizontal split → go right (second). Vertical split → go top (first).
     */
    private static topRightGroup;
    /** Whether a toolbar is already attached. */
    get hasToolbar(): boolean;
    /** Return all groups in this overlay (internal split or just the owner). */
    getAllGroups(): PanelGroup[];
    /** Update data-total-tab-count on the overlay element. */
    updateTotalTabCount(): void;
    private onDragStart;
    private onResizeStart;
}
type SplitDirection = "horizontal" | "vertical";
interface SplitNode {
    kind: "split";
    direction: SplitDirection;
    first: LayoutNode;
    second: LayoutNode;
    element: HTMLElement;
    separator: HTMLElement;
}
interface LeafNode {
    kind: "leaf";
    group: PanelGroup;
    element: HTMLElement;
}
type LayoutNode = SplitNode | LeafNode;
export interface SerializedLeafNode {
    kind: "leaf";
    groupId: string;
    flex: string;
}
export interface SerializedSplitNode {
    kind: "split";
    direction: SplitDirection;
    first: SerializedLayoutNode;
    second: SerializedLayoutNode;
    flex: string;
}
export type SerializedLayoutNode = SerializedLeafNode | SerializedSplitNode;
export interface SerializedFloatingGroup {
    groupId: string;
    bounds: FloatingOptions;
    internalLayout: SerializedLayoutNode | null;
    isMaximized?: boolean;
}
export interface SerializedPanelLayout {
    mainLayout: SerializedLayoutNode | null;
    floatingGroups: SerializedFloatingGroup[];
    panelLocations: Record<string, string>;
}
export declare class SplitLayout {
    readonly element: HTMLElement;
    private root;
    onSplitResized: (() => void) | null;
    constructor(container: HTMLElement);
    /** Get the root layout node. */
    getRoot(): LayoutNode | null;
    /**
     * Set the initial main group as the root leaf.
     */
    setMainGroup(group: PanelGroup): void;
    /**
     * Split the leaf containing targetGroup, placing newGroup beside it.
     * Returns true if the split was performed, false if blocked (e.g. max splits).
     *
     * CRITICAL: DOM operations are ordered so iframes are NEVER detached:
     * 1. Create the split container element
     * 2. Insert split container WHERE the leaf is (insertBefore)
     * 3. Move the existing leaf INTO the split container (appendChild moves it)
     * 4. Create new leaf and add it to split container
     */
    splitGroup(targetGroup: PanelGroup, newGroup: PanelGroup, direction: DockPosition, newGroupSize?: number): boolean;
    /**
     * Remove a group's leaf and collapse its parent split.
     *
     * CRITICAL: When collapsing, the sibling is moved BEFORE the parent
     * split container is removed, so iframes are never detached:
     * 1. insertBefore(sibling.element, parent.element) — sibling stays in document
     * 2. parent.element.remove() — only removes the empty parent shell
     */
    removeGroup(group: PanelGroup): void;
    /**
     * Find the leaf node containing a given group.
     */
    findGroupNode(group: PanelGroup): LeafNode | null;
    /**
     * Determine the dock position of a group relative to its sibling.
     * Returns the sibling group and the direction needed to re-create
     * this split with splitGroup(siblingGroup, group, direction).
     */
    getGroupPosition(group: PanelGroup): {
        siblingGroup: PanelGroup;
        direction: DockPosition;
    } | null;
    private firstGroupIn;
    /**
     * Collapse a group's leaf to zero size, hiding it from view while
     * keeping it in the tree so splitGroup() calls still find it.
     */
    collapseLeaf(group: PanelGroup): void;
    /**
     * Expand a previously collapsed leaf back to normal size.
     */
    expandLeaf(group: PanelGroup): void;
    /**
     * Maximize a group's leaf so it fills the entire layout.
     * Walks from the leaf up to the root, hiding the sibling and separator
     * at each split level so the leaf visually occupies the full area.
     */
    maximizeLeaf(group: PanelGroup): void;
    /**
     * Restore a previously maximized leaf back to normal layout.
     */
    restoreMaximizedLeaf(group: PanelGroup): void;
    /**
     * Restore all maximize-related saved state in a subtree.
     * Used before removeGroup to ensure no display:none or flex overrides leak.
     */
    private restoreMaximizedSubtree;
    /**
     * Iterate all leaf nodes (groups) in the layout.
     */
    allGroups(): PanelGroup[];
    /**
     * Serialize the layout tree to JSON for persistence.
     */
    toJSON(): SerializedLayoutNode | null;
    /**
     * Restore flex values from a serialized layout tree.
     *
     * Uses a parallel tree walk when structures match (same kind at each level),
     * which correctly restores flex on split containers as well as leaves.
     * Falls back to a flat groupId→flex map when trees diverge structurally.
     */
    restoreFlexFromSerialized(serialized: SerializedLayoutNode | null): void;
    private restoreFlexParallel;
    /**
     * Temporarily enable flex transition on all layout nodes so that
     * dock/undock/maximize causes a smooth resize animation.
     */
    animateTransition(): void;
    /**
     * Replace the entire layout tree with one built from a serialized tree.
     * Correctly handles ANY tree topology (nested horizontal/vertical splits
     * in any order) which the incremental splitGroup approach cannot.
     *
     * IFRAME SAFETY: The new tree structure is appended to the DOM FIRST,
     * then group.elements are MOVED (via appendChild, which never detaches
     * from the document) from their old positions into the new leaves.
     * Finally the old tree is removed (now empty).
     */
    rebuildFromSerialized(serialized: SerializedLayoutNode, groupLookup: (groupId: string) => PanelGroup | undefined): void;
    private buildNodeFromSerialized;
    /** Move each group's element into its corresponding leaf in the tree. */
    private moveGroupsIntoTree;
    dispose(): void;
    private createLeaf;
    private dockToSplitDirection;
    private findGroupNodeIn;
    private collectGroups;
    private findParentSplit;
    private findParentSplitIn;
    /**
     * Replace oldNode with newNode in the tree structure.
     */
    private replaceNodeInTree;
    private collectFlexValues;
    private applyFlexValues;
    /** Check that a flex string is a valid CSS flex value. */
    private isValidFlex;
    private serializeNode;
    private onSeparatorDrag;
    /**
     * Lock descendant splits in the same direction so their inner
     * separators don't move when the ancestor container resizes.
     * First child gets pixel-locked, second child absorbs changes.
     */
    private lockDescendantSizes;
    /**
     * Restore descendant splits to proportional flex based on their
     * current rendered sizes (no visual jump).
     */
    private unlockDescendantSizes;
}
export interface PanelManagerOptions {
    container: HTMLElement;
    onPanelRemoved?: (panel: Panel) => void;
    /** Called when DnD creates a new floating group. */
    onGroupCreatedByDrag?: (group: PanelGroup) => void;
    /** Called whenever the layout changes (panel moved, floating drag/resize, split resize). */
    onLayoutChanged?: () => void;
    /** Edge size in px for split zone detection (default 100) */
    splitEdgeSize?: number;
}
export declare class PanelManager {
    private container;
    private layout;
    private groups;
    private panels;
    private floatingOverlays;
    private mainGroupId;
    private onPanelRemovedCbs;
    private onGroupCreatedByDragCb;
    private onLayoutChangedCb;
    private disposed;
    private dragState;
    private dropOverlay;
    private splitEdgeSize;
    private portalLayer;
    private portals;
    private portalRaf;
    private mainGroupCollapsed;
    private _beforeTabClose;
    private zBase;
    constructor(opts: PanelManagerOptions);
    private getZBase;
    setBeforeTabClose(cb: ((panelId: string) => boolean | Promise<boolean>) | null): void;
    addPanel(opts: PanelOptions): Panel;
    removePanel(panelId: string): void;
    getPanel(panelId: string): Panel | undefined;
    addGroup(opts?: GroupOptions): PanelGroup;
    removeGroup(groupId: string): void;
    getGroup(groupId: string): PanelGroup | undefined;
    getGroupIds(): string[];
    getMainGroup(): PanelGroup | undefined;
    setMainGroup(group: PanelGroup): void;
    collapseMainGroup(): void;
    expandMainGroup(): void;
    isMainGroupCollapsed(): boolean;
    getDockedGroups(): PanelGroup[];
    collapseDockedGroup(group: PanelGroup): void;
    expandDockedGroup(group: PanelGroup): void;
    maximizeDockedGroup(group: PanelGroup): void;
    restoreMaximizedDockedGroup(group: PanelGroup): void;
    /** Restore split flex values from a serialized layout tree. */
    restoreLayoutFlex(serialized: SerializedLayoutNode | null): void;
    /**
     * Rebuild the entire docked layout from a serialized tree.
     * All groups referenced in the tree must already exist in the groups map.
     * Correctly reproduces ANY tree topology including mixed h/v splits.
     * Flex values from the serialized tree are applied during construction.
     */
    rebuildDockedLayout(serialized: SerializedLayoutNode): void;
    /**
     * Rebuild a floating overlay's internal split layout from serialized data.
     * All groups referenced must already exist in the groups map.
     */
    rebuildFloatingInternalLayout(ownerGroupId: string, serialized: SerializedLayoutNode): void;
    addFloatingGroup(group: PanelGroup, opts: FloatingOptions): FloatingOverlay;
    /**
     * Move a floating group into a split position without unmounting iframes.
     * splitGroup's insertBefore+appendChild MOVES group.element out of the
     * overlay first, then the empty overlay shell is cleaned up.
     *
     * Handles all cases: direct overlay owner, owner with internal splits,
     * and groups inside another overlay's internal split.
     */
    dockGroup(groupId: string, position: DockPosition, initialSize?: number): void;
    getFloatingOverlay(groupId: string): FloatingOverlay | undefined;
    /**
     * Dock an entire floating overlay to the grid, preserving internal splits.
     * All groups in the overlay are moved into the main split layout.
     */
    dockOverlay(groupId: string): void;
    /**
     * Find the floating overlay containing a group — either as the owner
     * or inside the overlay's internal split layout.
     */
    findOverlayContaining(groupId: string): FloatingOverlay | undefined;
    /**
     * Move a panel from one group to another WITHOUT unmounting.
     * Uses detachPanel (keeps wrapper in DOM) + addPanel (appendChild moves it).
     */
    movePanel(panelId: string, targetGroupId: string): void;
    /**
     * Serialize the full layout state for persistence.
     */
    serializeLayout(): SerializedPanelLayout;
    /**
     * Get all floating overlay entries (for external iteration).
     */
    getFloatingOverlays(): ReadonlyMap<string, FloatingOverlay>;
    /**
     * Remove all empty non-main groups from the layout.
     * Safety net that catches edge cases where movePanel/removePanel
     * cleanup didn't fire (e.g. complex split + drag sequences).
     */
    cleanupEmptyGroups(): void;
    onDidRemovePanel(cb: (panel: Panel) => void): {
        dispose(): void;
    };
    dispose(): void;
    private wireGroupCallbacks;
    private onTabPointerDown;
    private cancelTabDrag;
    private createDragGhost;
    private hitTestDropZone;
    /**
     * Determine which content drop zone the cursor is in.
     *
     * Edge detection on ALL groups (not just locked):
     * - Within splitEdgeSize px of edge -> content-split (split this group)
     * - Center -> content-center (merge into this group)
     * - Locked groups -> entire area is content-split (closest edge)
     */
    private classifyContentZone;
    /**
     * Apply drag constraints to a drop zone, converting or nullifying zones
     * that violate the source group's constraints.
     */
    private applyDragConstraints;
    private updateDropOverlay;
    private showSplitIndicator;
    private positionDropOverlay;
    private detachPanelToFloating;
    /**
     * Dock a dragged tab into a split position on the target group.
     * Creates a new group and splits the target group.
     *
     * For floating groups: creates an internal SplitLayout inside the
     * floating overlay so the split happens WITHIN the overlay.
     */
    private dockTabFromDrag;
    /**
     * Find (or create) the SplitLayout that manages a given group.
     *
     * - Groups in the main layout → this.layout
     * - Groups inside a floating overlay → overlay.internalLayout (created on demand)
     */
    private getLayoutForGroup;
    /**
     * Break a group out of a floating overlay's internal split into its own
     * floating overlay. The group.element is MOVED (not unmounted) by creating
     * the new overlay BEFORE removing the empty leaf from the old one.
     */
    escapeGroupFromSplit(groupId: string): void;
    /**
     * Update isInFloatingSplit on all groups inside an overlay's internal layout.
     */
    private updateFloatingSplitState;
    /**
     * Clean up a floating overlay's internal split after a group has been
     * removed from it. Collapses the internal layout to a single group
     * when only one remains, or updates split state if multiple remain.
     *
     * Callers are responsible for ownership transfer in the floatingOverlays
     * map BEFORE calling this method.
     *
     * MUST be called AFTER overlay.internalLayout.removeGroup(group) and
     * AFTER the group's element has already been moved out of the overlay
     * (e.g. via FloatingOverlay constructor or splitGroup's appendChild).
     *
     * @param overlay - The overlay whose internal layout lost a group
     * @returns The remaining groups in the overlay (empty if none left)
     */
    private cleanupOrphanedFloatingLeaf;
    /**
     * Remove a group from the main split layout, collapsing its space.
     * The group stays alive (not disposed) so it can be re-added later.
     * Returns position info so the group can be restored to the same spot.
     */
    minimizeDocked(group: PanelGroup): {
        siblingGroupId: string;
        direction: DockPosition;
    } | null;
    /**
     * Re-add a group to the main split layout at the given dock position,
     * relative to a specific sibling group (or main group as fallback).
     */
    restoreDocked(group: PanelGroup, position: DockPosition, initialSize?: number, siblingGroupId?: string): void;
    /**
     * Find the FloatingOverlay that contains a given group
     * (either as the owner or inside the internal layout).
     */
    private findOverlayForGroup;
    /**
     * Move a panel's element from its wrapper into a portal entry in the
     * portal layer. The wrapper becomes an empty placeholder used for
     * position reference. This is the ONLY time the element moves — after
     * this the iframe stays in the portal layer forever.
     */
    private registerPortal;
    private unregisterPortal;
    /**
     * RAF loop that syncs each portal entry's position to match its
     * anchor's bounding rect. Batches reads then writes to avoid
     * layout thrashing.
     */
    private syncPortals;
}
export {};
