"use client" import { Children, createContext, useContext, useEffect, useMemo, useRef, useState, type ComponentType, type RefAttributes, } from "react" import { motion, useInView, type DOMMotionComponents, type HTMLMotionProps, type MotionProps, } from "motion/react" import { cn } from "@/lib/utils" interface SequenceContextValue { completeItem: (index: number) => void activeIndex: number sequenceStarted: boolean } const SequenceContext = createContext(null) const useSequence = () => useContext(SequenceContext) const ItemIndexContext = createContext(null) const useItemIndex = () => useContext(ItemIndexContext) const motionElements = { article: motion.article, div: motion.div, h1: motion.h1, h2: motion.h2, h3: motion.h3, h4: motion.h4, h5: motion.h5, h6: motion.h6, li: motion.li, p: motion.p, section: motion.section, span: motion.span, } as const type MotionElementType = Extract< keyof DOMMotionComponents, keyof typeof motionElements > type TerminalTypingMotionComponent = ComponentType< Omit, "ref"> & RefAttributes > interface AnimatedSpanProps extends MotionProps { children: React.ReactNode delay?: number className?: string startOnView?: boolean } export const AnimatedSpan = ({ children, delay = 0, className, startOnView = false, ...props }: AnimatedSpanProps) => { const elementRef = useRef(null) const isInView = useInView(elementRef as React.RefObject, { amount: 0.3, once: true, }) const sequence = useSequence() const itemIndex = useItemIndex() const [hasStarted, setHasStarted] = useState(false) useEffect(() => { let startTimeout: ReturnType | null = null if (!sequence || itemIndex === null) return if (!sequence.sequenceStarted) return if (hasStarted) return if (sequence.activeIndex === itemIndex) { startTimeout = setTimeout(() => setHasStarted(true), 0) } return () => { if (startTimeout !== null) clearTimeout(startTimeout) } }, [sequence, hasStarted, itemIndex]) const shouldAnimate = sequence ? hasStarted : startOnView ? isInView : true return ( { if (!sequence) return if (itemIndex === null) return sequence.completeItem(itemIndex) }} {...props} > {children} ) } interface TypingAnimationProps extends Omit { children: string className?: string duration?: number delay?: number as?: MotionElementType startOnView?: boolean } export const TypingAnimation = ({ children, className, duration = 60, delay = 0, as: Component = "span", startOnView = true, ...props }: TypingAnimationProps) => { if (typeof children !== "string") { throw new Error("TypingAnimation: children must be a string. Received:") } const MotionComponent = motionElements[ Component ] as TerminalTypingMotionComponent const [displayedText, setDisplayedText] = useState("") const [started, setStarted] = useState(false) const elementRef = useRef(null) const isInView = useInView(elementRef as React.RefObject, { amount: 0.3, once: true, }) const sequence = useSequence() const itemIndex = useItemIndex() const hasSequence = sequence !== null const sequenceStarted = sequence?.sequenceStarted ?? false const sequenceActiveIndex = sequence?.activeIndex ?? null const sequenceCompleteItemRef = useRef< SequenceContextValue["completeItem"] | null >(null) const sequenceItemIndexRef = useRef(null) useEffect(() => { sequenceCompleteItemRef.current = sequence?.completeItem ?? null sequenceItemIndexRef.current = itemIndex }, [sequence?.completeItem, itemIndex]) useEffect(() => { let startTimeout: ReturnType | null = null if (hasSequence && itemIndex !== null) { if (sequenceStarted && !started && sequenceActiveIndex === itemIndex) { startTimeout = setTimeout(() => setStarted(true), 0) } } else if (!startOnView || isInView) { startTimeout = setTimeout(() => setStarted(true), delay) } return () => { if (startTimeout !== null) { clearTimeout(startTimeout) } } }, [ delay, startOnView, isInView, started, hasSequence, sequenceActiveIndex, sequenceStarted, itemIndex, ]) useEffect(() => { let typingEffect: ReturnType | null = null if (started) { let i = 0 typingEffect = setInterval(() => { if (i < children.length) { setDisplayedText(children.substring(0, i + 1)) i++ } else { if (typingEffect !== null) { clearInterval(typingEffect) } const completeItem = sequenceCompleteItemRef.current const currentItemIndex = sequenceItemIndexRef.current if (completeItem && currentItemIndex !== null) { completeItem(currentItemIndex) } } }, duration) } return () => { if (typingEffect !== null) { clearInterval(typingEffect) } } }, [children, duration, started]) return ( {displayedText} ) } interface TerminalProps { children: React.ReactNode className?: string sequence?: boolean startOnView?: boolean } export const Terminal = ({ children, className, sequence = true, startOnView = true, }: TerminalProps) => { const containerRef = useRef(null) const isInView = useInView(containerRef as React.RefObject, { amount: 0.3, once: true, }) const [activeIndex, setActiveIndex] = useState(0) const sequenceHasStarted = sequence ? !startOnView || isInView : false const contextValue = useMemo(() => { if (!sequence) return null return { completeItem: (index: number) => { setActiveIndex((current) => (index === current ? current + 1 : current)) }, activeIndex, sequenceStarted: sequenceHasStarted, } }, [sequence, activeIndex, sequenceHasStarted]) const wrappedChildren = useMemo(() => { if (!sequence) return children const array = Children.toArray(children) return array.map((child, index) => ( {child as React.ReactNode} )) }, [children, sequence]) const content = (
        {wrappedChildren}
      
) if (!sequence) return content return ( {content} ) }