"use client"; import { motion, useInView, useReducedMotion } from "motion/react"; import { useRef, type ElementType, type ReactNode } from "react"; import { cn } from "@/lib/utils"; type SplitMode = "word" | "char"; export interface TextRevealProps { text: string | string[]; as?: ElementType; className?: string; split?: SplitMode; stagger?: number; delay?: number; blur?: number; yOffset?: string | number; spring?: { stiffness?: number; damping?: number; mass?: number }; once?: boolean; whileInView?: boolean; children?: ReactNode; } const DEFAULT_SPRING = { stiffness: 140, damping: 26, mass: 1.2 }; export function TextReveal({ text, as: Comp = "span", className, split = "word", stagger = 0.09, delay = 0, blur = 12, yOffset = "40%", spring, once = true, whileInView = false, children, }: TextRevealProps) { const ref = useRef(null); const inView = useInView(ref, { once, amount: 0.4 }); const reduce = useReducedMotion(); const shouldAnimate = whileInView ? inView : true; const lines = Array.isArray(text) ? text : [text]; const s = { ...DEFAULT_SPRING, ...spring }; let unitIndex = 0; return ( {lines.map((line, li) => { const units = split === "word" ? line.split(" ") : Array.from(line); return ( {units.map((unit, i) => { const d = delay + unitIndex * stagger; unitIndex += 1; const initial = reduce ? { opacity: 0 } : { y: yOffset, opacity: 0, filter: `blur(${blur}px)` }; const animate = shouldAnimate ? reduce ? { opacity: 1 } : { y: 0, opacity: 1, filter: "blur(0px)" } : initial; const transition = reduce ? { opacity: { duration: 0.25, ease: [0.16, 1, 0.3, 1], delay: d * 0.3 } } : { y: { type: "spring" as const, ...s, delay: d }, opacity: { duration: 0.7, ease: [0.16, 1, 0.3, 1], delay: d }, filter: { duration: 0.9, ease: [0.16, 1, 0.3, 1], delay: d }, }; return ( {unit} {split === "word" && i < units.length - 1 ? (   ) : null} ); })} ); })} {children} ); }