"use client"; import { type HTMLMotionProps, motion, type Variants } from "framer-motion"; import type React from "react"; import { useEffect, useState } from "react"; import { fadeInUp, scaleInSpring, stagger } from "@/lib/motion"; import { cn } from "@/lib/utils"; // ─── Viewport-based reveal wrapper ─────────────────────────── type RevealProps = HTMLMotionProps<"div"> & { variants?: Variants; once?: boolean; amount?: number | "some" | "all"; }; /** * Wraps children in a fade-in-up animation triggered when they enter the viewport. * Use `asChild` composition or nest directly — children receive no special bindings. */ export function Reveal({ children, className, variants = fadeInUp, once = true, amount = 0.2, ...props }: RevealProps) { return ( {children} ); } // ─── Stagger container ─────────────────────────────────────── type StaggerContainerProps = HTMLMotionProps<"div"> & { staggerVariants?: Variants; once?: boolean; }; /** * Parent container that staggers child `motion` elements. * Children should have matching `variants` (e.g. `fadeInUp`). */ export function StaggerContainer({ children, className, staggerVariants = stagger, once = true, ...props }: StaggerContainerProps) { return ( {children} ); } // ─── Single-item child with fadeInUp ───────────────────────── type MotionItemProps = HTMLMotionProps<"div"> & { index?: number }; /** * A single motion item meant to be used as a child of StaggerContainer. * Wraps content in a `motion.div` with fadeInUp variants. */ export function MotionItem({ children, className, ...props }: MotionItemProps) { return ( {children} ); } // ─── Scale reveal (for cards, dialogs) ─────────────────────── export function ScaleReveal({ children, className, ...props }: HTMLMotionProps<"div">) { return ( {children} ); } // ─── Animated presence wrapper ─────────────────────────────── /** * Fades + scales content in/out for mount/unmount transitions. */ export function AnimatedPresence({ children, className, }: { children: React.ReactNode; className?: string; }) { return ( {children} ); } // ─── Animated number counter ───────────────────────────────── type AnimatedNumberProps = { value: number; decimals?: number; duration?: number; className?: string; }; /** * Animates from 0 to `value` on mount and when `value` changes. */ export function AnimatedNumber({ value, decimals = 0, duration: dur = 0.6, className, }: AnimatedNumberProps) { return ( {value.toFixed(decimals)} ); } // ─── Animated text reveal (letter by letter) ───────────────── type TypewriterProps = { text: string; className?: string; speed?: number; /** Delay in ms before the first character appears */ startDelay?: number; /** When true, shows a blinking cursor block while typing */ cursor?: boolean; }; /** * Reveals text one character at a time using progressive `setTimeout`. * Characters are appended to a single text node so CSS gradient * (`background-clip: text`) works seamlessly across the full string. */ export function Typewriter({ text, className, speed = 0.03, startDelay = 250, cursor: showCursor = true, }: TypewriterProps) { const [displayed, setDisplayed] = useState(""); const done = displayed.length >= text.length; useEffect(() => { if (done) return; const delay = displayed.length === 0 ? startDelay : Math.round(speed * 1000); const timer = setTimeout( () => setDisplayed(text.slice(0, displayed.length + 1)), delay, ); return () => clearTimeout(timer); }, [displayed, text, speed, startDelay, done]); return ( {displayed} {showCursor && !done && ( )} ); } // ─── Blur-in reveal ────────────────────────────────────────── export function BlurReveal({ children, className, delay = 0, }: { children: React.ReactNode; className?: string; delay?: number; }) { return ( {children} ); } // ─── Interactive card wrapper ──────────────────────────────── type InteractiveCardProps = HTMLMotionProps<"div"> & { hoverScale?: number; }; /** * Card wrapper with subtle hover lift and glow. */ export function InteractiveCard({ children, className, hoverScale = 1.01, ...props }: InteractiveCardProps) { return ( {children} ); }