'use client'; import { useEffect, useRef, useState } from 'react'; /** * Observes the provided element and returns true the first time it enters the viewport. * Uses IntersectionObserver under the hood and disconnects after first intersection. */ export function useInViewOnce(options?: IntersectionObserverInit) { const targetRef = useRef(null); const [hasIntersected, setHasIntersected] = useState(false); const [hasMounted, setHasMounted] = useState(false); useEffect(() => { setHasMounted(true); }, []); useEffect(() => { if (hasIntersected) return; // already observed once const element = targetRef.current; if (!element) return; // Respect reduced motion const prefersReducedMotion = typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (prefersReducedMotion) { setHasIntersected(true); return; } const observer = new IntersectionObserver( (entries, obs) => { for (const entry of entries) { if (entry.isIntersecting) { setHasIntersected(true); obs.disconnect(); break; } } }, { root: options?.root || null, rootMargin: options?.rootMargin ?? '0px 0px -10% 0px', threshold: options?.threshold ?? 0.1, } ); observer.observe(element); return () => { observer.disconnect(); }; }, [options?.root, options?.rootMargin, options?.threshold, hasIntersected]); return { ref: targetRef, isVisible: hasIntersected || !hasMounted ? false : hasIntersected, hasIntersected } as const; }