"use client"; import { useRef, useEffect, useState, type ReactNode } from "react"; interface ScrollFadeInProps { children: ReactNode; className?: string; delay?: number; } /** * Wraps children in a fade-in-up animation triggered by scroll. * Unlike Framer Motion's initial={{ opacity: 0 }}, this does NOT * render opacity:0 in SSR HTML — content is visible by default * and only hidden after JS hydration for the animation effect. */ export default function ScrollFadeIn({ children, className = "", delay = 0 }: ScrollFadeInProps) { const ref = useRef(null); const [isVisible, setIsVisible] = useState(false); const [hasMounted, setHasMounted] = useState(false); const [reduceMotion, setReduceMotion] = useState(false); useEffect(() => { setHasMounted(true); setReduceMotion(window.matchMedia("(prefers-reduced-motion: reduce)").matches); const el = ref.current; if (!el) return; // Fallback for browsers without IntersectionObserver if (typeof IntersectionObserver === "undefined") { setIsVisible(true); return; } const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsVisible(true); observer.unobserve(el); } }, // Start a little before the section is fully in view so it never feels late { threshold: 0.1, rootMargin: "0px 0px -60px 0px" } ); observer.observe(el); return () => observer.disconnect(); }, []); return (
{children}
); }