"use client"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react"; import { motion } from "framer-motion"; import { cn } from "@/lib/utils"; 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); const splitIntoCharacters = (text) => { if (typeof Intl !== "undefined" && "Segmenter" in Intl) { const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" }); return Array.from(segmenter.segment(text), ({ segment }) => segment); } return Array.from(text); }; 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]); const getStaggerDelay = useCallback( (index) => { 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]); useImperativeHandle(ref, () => ({ startAnimation, reset: () => setIsAnimating(false), })); useEffect(() => { if (autoStart) { startAnimation(); } }, [autoStart]); const variants = { hidden: { y: reverse ? "-100%" : "100%" }, visible: (i) => ({ y: 0, transition: { ...transition, delay: (transition?.delay || 0) + getStaggerDelay(i), }, }), }; return ( {text} {(splitBy === "characters" ? elements : elements.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 { VerticalCutReveal };