"use client"; import { useEffect, useRef, useState } from "react"; type Variant = | "fade-up" | "fade-down" | "fade-in" | "fade-left" | "fade-right" | "scale-up"; interface AnimateInProps { children: React.ReactNode; variant?: Variant; /** Delay (ms) after the element enters the viewport before animating */ delay?: number; /** Animation duration in ms */ duration?: number; /** Extra classes to put on the wrapper div */ className?: string; /** If true (default) the animation only fires once */ once?: boolean; } const transforms: Record = { "fade-up": { hidden: "opacity-0 translate-y-10", visible: "opacity-100 translate-y-0" }, "fade-down": { hidden: "opacity-0 -translate-y-10", visible: "opacity-100 translate-y-0" }, "fade-in": { hidden: "opacity-0", visible: "opacity-100" }, "fade-left": { hidden: "opacity-0 -translate-x-10", visible: "opacity-100 translate-x-0" }, "fade-right": { hidden: "opacity-0 translate-x-10", visible: "opacity-100 translate-x-0" }, "scale-up": { hidden: "opacity-0 scale-95", visible: "opacity-100 scale-100" }, }; export default function AnimateIn({ children, variant = "fade-up", delay = 0, duration = 650, className = "", once = true, }: AnimateInProps) { const ref = useRef(null); const [visible, setVisible] = useState(false); useEffect(() => { // Respect the user's motion preference if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setVisible(true); return; } const el = ref.current; if (!el) return; const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setVisible(true); if (once) observer.disconnect(); } else if (!once) { setVisible(false); } }, { threshold: 0.08, rootMargin: "0px 0px -60px 0px" }, ); observer.observe(el); return () => observer.disconnect(); }, [once]); const { hidden, visible: vis } = transforms[variant]; return (
{children}
); }