"use client" import { AnimationOptions, motion } from "motion/react" import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react" import { cn } from "@/lib/utils" interface TextProps { children: React.ReactNode reverse?: boolean transition?: AnimationOptions splitBy?: "words" | "characters" | "lines" | string staggerDuration?: number staggerFrom?: "first" | "last" | "center" | "random" | number containerClassName?: string wordLevelClassName?: string elementLevelClassName?: string onClick?: () => void onStart?: () => void onComplete?: () => void autoStart?: boolean // Whether to start the animation automatically } // Ref interface to allow external control of the animation export interface VerticalCutRevealRef { startAnimation: () => void reset: () => void } interface WordObject { characters: string[] needsSpace: boolean } const VerticalCutReveal = forwardRef( ( { children, reverse = false, transition = { type: "spring", stiffness: 190, damping: 22, }, splitBy = "words", staggerDuration = 0.2, staggerFrom = "first", containerClassName, wordLevelClassName, elementLevelClassName, onClick, onStart, onComplete, autoStart = true, ...props }, ref ) => { const containerRef = useRef(null) const text = typeof children === "string" ? children : children?.toString() || "" const [isAnimating, setIsAnimating] = useState(false) // handy function to split text into characters with support for unicode and emojis const splitIntoCharacters = (text: string): string[] => { if (typeof Intl !== "undefined" && "Segmenter" in Intl) { const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" }) return Array.from(segmenter.segment(text), ({ segment }) => segment) } // Fallback for browsers that don't support Intl.Segmenter return Array.from(text) } // Split text based on splitBy parameter const elements = useMemo(() => { const words = text.split(" ") if (splitBy === "characters") { return words.map((word, i) => ({ characters: splitIntoCharacters(word), needsSpace: i !== words.length - 1, })) } return splitBy === "words" ? text.split(" ") : splitBy === "lines" ? text.split("\n") : text.split(splitBy) }, [text, splitBy]) // Calculate stagger delays based on staggerFrom const getStaggerDelay = useCallback( (index: number) => { const total = splitBy === "characters" ? elements.reduce( (acc, word) => acc + (typeof word === "string" ? 1 : word.characters.length + (word.needsSpace ? 1 : 0)), 0 ) : elements.length if (staggerFrom === "first") return index * staggerDuration if (staggerFrom === "last") return (total - 1 - index) * staggerDuration if (staggerFrom === "center") { const center = Math.floor(total / 2) return Math.abs(center - index) * staggerDuration } if (staggerFrom === "random") { const randomIndex = Math.floor(Math.random() * total) return Math.abs(randomIndex - index) * staggerDuration } return Math.abs(staggerFrom - index) * staggerDuration }, [elements.length, staggerFrom, staggerDuration] ) const startAnimation = useCallback(() => { setIsAnimating(true) onStart?.() }, [onStart]) // Expose the startAnimation function via ref useImperativeHandle(ref, () => ({ startAnimation, reset: () => setIsAnimating(false), })) // Auto start animation useEffect(() => { if (autoStart) { startAnimation() } }, [autoStart]) const variants = { hidden: { y: reverse ? "-100%" : "100%" }, visible: (i: number) => ({ y: 0, transition: { ...transition, delay: ((transition?.delay as number) || 0) + getStaggerDelay(i), }, }), } return ( {text} {(splitBy === "characters" ? (elements as WordObject[]) : (elements as string[]).map((el, i) => ({ characters: [el], needsSpace: i !== elements.length - 1, })) ).map((wordObj, wordIndex, array) => { const previousCharsCount = array .slice(0, wordIndex) .reduce((sum, word) => sum + word.characters.length, 0) return ( ) })} ) } ) VerticalCutReveal.displayName = "VerticalCutReveal" export default VerticalCutReveal