"use client"; import * as React from "react"; import { motion, type HTMLMotionProps } from "motion/react"; import { useIsInView, type UseIsInViewOptions } from "~/lib/use-is-in-view"; import { getStrictContext } from "~/lib/get-strict-context"; type TypingTextContextType = { isTyping: boolean; setIsTyping: (isTyping: boolean) => void; }; const [TypingTextProvider, useTypingText] = getStrictContext("TypingTextContext"); type TypingTextProps = React.ComponentProps<"span"> & { duration?: number; delay?: number; loop?: boolean; holdDelay?: number; text: string | string[]; } & UseIsInViewOptions; function TypingText({ ref, children, duration = 100, delay = 0, inView = false, inViewMargin = "0px", inViewOnce = true, loop = false, holdDelay = 1000, text, ...props }: TypingTextProps) { const { ref: localRef, isInView } = useIsInView( ref as React.Ref, { inView, inViewOnce, inViewMargin, } ); const [isTyping, setIsTyping] = React.useState(false); const [started, setStarted] = React.useState(false); const [displayedText, setDisplayedText] = React.useState(""); React.useEffect(() => { if (isInView) { const timeoutId = setTimeout(() => { setStarted(true); }, delay); return () => clearTimeout(timeoutId); } }, [isInView, delay]); React.useEffect(() => { if (!started) return; const timeoutIds: Array> = []; const texts: string[] = typeof text === "string" ? [text] : text; const typeText = (str: string, onComplete: () => void) => { setIsTyping(true); let currentIndex = 0; const type = () => { if (currentIndex <= str.length) { setDisplayedText(str.substring(0, currentIndex)); currentIndex++; const id = setTimeout(type, duration); timeoutIds.push(id); } else { setIsTyping(false); onComplete(); } }; type(); }; const eraseText = (str: string, onComplete: () => void) => { setIsTyping(true); let currentIndex = str.length; const erase = () => { if (currentIndex >= 0) { setDisplayedText(str.substring(0, currentIndex)); currentIndex--; const id = setTimeout(erase, duration); timeoutIds.push(id); } else { setIsTyping(false); onComplete(); } }; erase(); }; const animateTexts = (index: number) => { typeText(texts[index] ?? "", () => { const isLast = index === texts.length - 1; if (isLast && !loop) { return; } const id = setTimeout(() => { eraseText(texts[index] ?? "", () => { const nextIndex = isLast ? 0 : index + 1; animateTexts(nextIndex); }); }, holdDelay); timeoutIds.push(id); }); }; animateTexts(0); return () => { timeoutIds.forEach(clearTimeout); }; }, [text, duration, started, loop, holdDelay]); return ( {displayedText} {children} ); } type TypingTextCursorProps = Omit, "children">; function TypingTextCursor({ style, variants, ...props }: TypingTextCursorProps) { const { isTyping } = useTypingText(); return ( ); } export { TypingText, TypingTextCursor, type TypingTextProps, type TypingTextCursorProps, };