import { useCallback, useLayoutEffect, useRef } from "react"; const FALLBACK_MS = 250; /** * Eases an element's height whenever `watch` changes, so an interaction that * swaps part of its content (a view for an edit form, a reply box opening…) * moves the surrounding layout smoothly instead of jumping. * * Returns a callback ref for the element whose height should transition. * Only the box is animated; give the content that appears the `animate-in` * class so it fades in as the box grows. Duration follows `--dur-reveal`. */ export function useHeightTransition(watch: unknown) { const elRef = useRef(null); // Last rendered height, kept current through a ResizeObserver so that by // the time the swap commits we still know where the box *was* — including // mid-animation, when a second toggle should start from the current frame. const lastHeight = useRef(null); const lastWatch = useRef(watch); const ref = useCallback((el: T | null) => { if (!el) return; elRef.current = el; lastHeight.current = el.getBoundingClientRect().height; const ro = new ResizeObserver(() => { lastHeight.current = el.getBoundingClientRect().height; }); ro.observe(el); return () => { ro.disconnect(); elRef.current = null; lastHeight.current = null; }; }, []); useLayoutEffect(() => { if (Object.is(lastWatch.current, watch)) return; lastWatch.current = watch; const el = elRef.current; const from = lastHeight.current; if (!el || from === null) return; for (const a of el.getAnimations()) { if (a.id === "height-transition") a.cancel(); } const to = el.getBoundingClientRect().height; lastHeight.current = to; if ( Math.abs(from - to) < 1 || matchMedia("(prefers-reduced-motion: reduce)").matches ) return; const style = getComputedStyle(el); const duration = parseFloat(style.getPropertyValue("--dur-reveal")) * 1000 || FALLBACK_MS; const easing = style.getPropertyValue("--ease-out").trim() || "ease-out"; el.animate( [ { height: `${from}px`, boxSizing: "border-box", overflow: "clip" }, { height: `${to}px`, boxSizing: "border-box", overflow: "clip" }, ], { duration, easing, id: "height-transition" }, ); }, [watch]); return ref; }