'use client'; import { useLayoutEffect, useRef, useState, type CSSProperties, type RefObject } from 'react'; const VIEWPORT_PADDING = 12; const DEFAULT_GAP = 8; export type AnchoredPanelAlign = 'start' | 'stretch'; type UseAnchoredPanelPositionArgs = { open: boolean; /** Element the panel should attach to (field / trigger shell). */ anchorRef: RefObject; /** Prefer aligning the panel's inline-start with the anchor's inline-start. */ align?: AnchoredPanelAlign; gap?: number; }; /** * Positions a panel with `position: fixed`, clamped to the viewport. * Flips above the anchor when there is not enough room below. * Handles LTR/RTL via `document.documentElement.dir`. */ export function useAnchoredPanelPosition({ open, anchorRef, align = 'start', gap = DEFAULT_GAP, }: UseAnchoredPanelPositionArgs) { const panelRef = useRef(null); const [style, setStyle] = useState(undefined); useLayoutEffect(() => { if (!open) { setStyle(undefined); return; } function update() { const anchor = anchorRef.current; const panel = panelRef.current; if (!anchor || !panel) return; const anchorRect = anchor.getBoundingClientRect(); const panelRect = panel.getBoundingClientRect(); const vw = window.innerWidth; const vh = window.innerHeight; const isRtl = document.documentElement.dir === 'rtl'; let width = align === 'stretch' ? Math.min(anchorRect.width, vw - VIEWPORT_PADDING * 2) : Math.min(panelRect.width || panel.offsetWidth, vw - VIEWPORT_PADDING * 2); if (width < 1) width = Math.min(280, vw - VIEWPORT_PADDING * 2); let left: number; if (align === 'stretch') { left = anchorRect.left; } else if (isRtl) { left = anchorRect.right - width; } else { left = anchorRect.left; } left = Math.max(VIEWPORT_PADDING, Math.min(left, vw - width - VIEWPORT_PADDING)); const height = panelRect.height || panel.offsetHeight; let top = anchorRect.bottom + gap; if (top + height > vh - VIEWPORT_PADDING) { const above = anchorRect.top - height - gap; top = above >= VIEWPORT_PADDING ? above : Math.max(VIEWPORT_PADDING, vh - height - VIEWPORT_PADDING); } setStyle({ position: 'fixed', top, left, width, zIndex: 50, }); } update(); // Second pass after fonts/layout settle panel size. const raf = window.requestAnimationFrame(update); window.addEventListener('resize', update); window.addEventListener('scroll', update, true); return () => { window.cancelAnimationFrame(raf); window.removeEventListener('resize', update); window.removeEventListener('scroll', update, true); }; }, [align, anchorRef, gap, open]); return { panelRef, style }; }