"use client"; import { useEffect, useRef, useState, type ElementType, type ReactNode, } from "react"; import { cn } from "@/lib/utils"; export type ScrollRevealVariant = "hero" | "card" | "fade-up" | "section"; type Props = { children: ReactNode; className?: string; variant?: ScrollRevealVariant; /** Stagger delay after element enters view (ms) */ delay?: number; /** Animate on mount (for above-the-fold hero content) */ immediate?: boolean; as?: ElementType; }; const hidden: Record = { hero: "opacity-0 translate-y-10 scale-[0.98]", card: "opacity-0 translate-y-7", "fade-up": "opacity-0 translate-y-8", section: "opacity-0 translate-y-14", }; const shown: Record = { hero: "opacity-100 translate-y-0 scale-100", card: "opacity-100 translate-y-0", "fade-up": "opacity-100 translate-y-0", section: "opacity-100 translate-y-0", }; const duration: Record = { hero: "duration-[900ms]", card: "duration-[650ms]", "fade-up": "duration-700", section: "duration-[800ms]", }; export function ScrollReveal({ children, className, variant = "fade-up", delay = 0, immediate = false, as: Tag = "div", }: Props) { const ref = useRef(null); const [visible, setVisible] = useState(false); useEffect(() => { if (immediate) { const id = requestAnimationFrame(() => setVisible(true)); return () => cancelAnimationFrame(id); } const el = ref.current; if (!el) return; const mq = window.matchMedia("(prefers-reduced-motion: reduce)"); if (mq.matches) { setVisible(true); return; } const obs = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setVisible(true); obs.disconnect(); } }, { threshold: 0.1, rootMargin: "0px 0px -6% 0px" } ); obs.observe(el); return () => obs.disconnect(); }, [immediate]); return ( {children} ); }