import { useEffect, useRef } from "react"; import { useCallbackRef } from "./useCallbackRef"; export interface ScrollRevealOptions { threshold?: number | number[]; rootMargin?: string; } export function useScrollReveal(options: ScrollRevealOptions = {}) { const observerRef = useRef(null); const observedElementsRef = useRef>(new Set()); const mutationObserversRef = useRef>( new Map(), ); const optionsRef = useRef(options); useEffect(() => { optionsRef.current = options; }, [options]); const getObserver = () => { if (!observerRef.current) { observerRef.current = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.setAttribute("data-revealed", "true"); const revealChildren = (el: Element) => { Array.from(el.children).forEach((child) => { if (child.getAttribute("data-revealed") !== "true") { child.setAttribute("data-revealed", "true"); } revealChildren(child); }); }; revealChildren(entry.target); if (!mutationObserversRef.current.has(entry.target)) { const mo = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === "childList") { mutation.addedNodes.forEach((node) => { if (node instanceof Element) { if (node.getAttribute("data-revealed") !== "true") { node.setAttribute("data-revealed", "true"); } revealChildren(node); } }); } }); }); mo.observe(entry.target, { childList: true, subtree: true }); mutationObserversRef.current.set(entry.target, mo); } observerRef.current?.unobserve(entry.target); if (entry.target instanceof HTMLElement) { observedElementsRef.current.delete(entry.target); } } }); }, { threshold: optionsRef.current.threshold ?? 0.15, rootMargin: optionsRef.current.rootMargin ?? "0px 0px -40px 0px", }, ); } return observerRef.current; }; useEffect(() => { return () => { observerRef.current?.disconnect(); observedElementsRef.current.clear(); mutationObserversRef.current.forEach((mo) => mo.disconnect()); mutationObserversRef.current.clear(); }; }, []); const observe = useCallbackRef((element: HTMLElement | null) => { if (!element) return; if (typeof IntersectionObserver === "undefined") { element.setAttribute("data-revealed", "true"); return; } if (typeof window !== "undefined" && window.matchMedia) { const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); if (mediaQuery.matches) { element.setAttribute("data-revealed", "true"); return; } } if ( !observedElementsRef.current.has(element) && element.getAttribute("data-revealed") !== "true" ) { getObserver().observe(element); observedElementsRef.current.add(element); } }); return observe; }