import { useEffect, useRef, useState } from "react"; /** Fires once when the element scrolls into view. */ export function useInView( options: IntersectionObserverInit = { threshold: 0.15 }, ) { const ref = useRef(null); const [inView, setInView] = useState(false); useEffect(() => { const node = ref.current; if (!node || inView) return; const io = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { setInView(true); io.disconnect(); } }, options); io.observe(node); return () => io.disconnect(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [inView]); return [ref, inView] as const; } /** Animates a numeric value once `trigger` flips to true. */ export function useCountUp(target: number, trigger: boolean, durationMs = 900) { const [value, setValue] = useState(0); useEffect(() => { if (!trigger) return; const start = performance.now(); const easeOut = (t: number) => 1 - Math.pow(1 - t, 3); let raf = 0; const tick = (now: number) => { const p = Math.min(1, (now - start) / durationMs); setValue(target * easeOut(p)); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [target, trigger, durationMs]); return value; }