'use client'; import * as React from 'react'; import { motion, type Variants, type TargetAndTransition, type HTMLMotionProps, useInView, type UseInViewOptions, } from 'motion/react'; type DefaultSplittingTextProps = { motionVariants?: { initial?: Record; animate?: Record; transition?: Record; stagger?: number; }; inView?: boolean; inViewMargin?: UseInViewOptions['margin']; inViewOnce?: boolean; delay?: number; } & HTMLMotionProps<'div'>; type CharsOrWordsSplittingTextProps = DefaultSplittingTextProps & { type?: 'chars' | 'words'; text: string; }; type LinesSplittingTextProps = DefaultSplittingTextProps & { type: 'lines'; text: string[]; }; type SplittingTextProps = | CharsOrWordsSplittingTextProps | LinesSplittingTextProps; const defaultItemVariant: Variants = { hidden: { x: 150, opacity: 0 }, visible: { x: 0, opacity: 1, transition: { duration: 0.7, ease: 'easeOut' }, }, }; export const SplittingText: React.FC = ({ ref, text, type = 'chars', motionVariants = {}, inView = false, inViewMargin = '0px', inViewOnce = true, delay = 0, ...props }) => { const items = React.useMemo(() => { if (Array.isArray(text)) { return text.flatMap((line, i) => [ {line}, i < text.length - 1 ?
: null, ]); } if (type === 'words') { const tokens = text.match(/\S+\s*/g) || []; return tokens.map((token, i) => ( {token} )); } return text .split('') .map((char, i) => {char}); }, [text, type]); const containerVariants: Variants = { hidden: {}, visible: { transition: { delayChildren: delay / 1000, staggerChildren: motionVariants.stagger ?? (type === 'chars' ? 0.05 : type === 'words' ? 0.2 : 0.3), }, }, }; const itemVariants: Variants = { hidden: { ...defaultItemVariant.hidden, ...(motionVariants.initial || {}), }, visible: { ...defaultItemVariant.visible, ...(motionVariants.animate || {}), transition: { ...((defaultItemVariant.visible as TargetAndTransition).transition || {}), ...(motionVariants.transition || {}), }, }, }; const localRef = React.useRef(null); React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement); const inViewResult = useInView(localRef, { once: inViewOnce, margin: inViewMargin, }); const isInView = !inView || inViewResult; return ( {items.map( (item, index) => item && ( {item} {type === 'words' && ' '} ), )} ); };