'use client'; import { useEffect, useRef, useState } from 'react'; interface Corner { top: number; right: number; bottom: number; } /** * Tracks the top-right corner of a referenced element in viewport coordinates. * Returns { top, right } in px — ready for `position: fixed` toolbar placement. * * Uses `visualViewport` for accurate viewport width — correctly handles: * - DevTools panel open (docked left/right/bottom) * - Browser zoom * - Mobile virtual keyboard / pinch-zoom * - Scrollbars * * Falls back to `document.documentElement.clientWidth` (excludes scrollbars, * matches `position: fixed` coordinate space) when visualViewport is unavailable. * * Updates on: * - Any ancestor scroll (capture phase) * - visualViewport resize/scroll * - ResizeObserver on the element itself */ export function useElementCorner(ref: React.RefObject) { const [corner, setCorner] = useState(null); const updateRef = useRef<() => void>(() => {}); updateRef.current = () => { if (!ref.current) return; const rect = ref.current.getBoundingClientRect(); // `position: fixed` is relative to the layout viewport (document.documentElement.clientWidth), // NOT window.innerWidth (which includes scrollbar gutter). // visualViewport.width is the visible area — same as layout viewport when no zoom/keyboard. const viewportWidth = window.visualViewport?.width ?? document.documentElement.clientWidth; setCorner({ top: rect.top, right: viewportWidth - rect.right, bottom: rect.bottom, }); }; useEffect(() => { const handler = () => updateRef.current(); handler(); // Element size changes const ro = new ResizeObserver(handler); if (ref.current) ro.observe(ref.current); // Any ancestor scroll (capture catches all, including overflow:auto containers) window.addEventListener('scroll', handler, { capture: true, passive: true }); // visualViewport handles: DevTools resize, browser zoom, mobile keyboard const vv = window.visualViewport; if (vv) { vv.addEventListener('resize', handler); vv.addEventListener('scroll', handler); } else { window.addEventListener('resize', handler, { passive: true }); } return () => { ro.disconnect(); window.removeEventListener('scroll', handler, { capture: true }); if (vv) { vv.removeEventListener('resize', handler); vv.removeEventListener('scroll', handler); } else { window.removeEventListener('resize', handler); } }; }, [ref]); return corner; }