"use client" import { ElementType, useEffect, useState } from "react" import { motion, Variants } from "motion/react" import { cn } from "@/lib/utils" interface TypewriterProps { /** * Text or array of texts to type out */ text: string | string[] /** * HTML Tag to render the component as * @default div */ as?: ElementType /** * Speed of typing in milliseconds * @default 50 */ speed?: number /** * Initial delay before typing starts * @default 0 */ initialDelay?: number /** * Time to wait between typing and deleting * @default 2000 */ waitTime?: number /** * Speed of deleting characters * @default 30 */ deleteSpeed?: number /** * Whether to loop through texts array * @default true */ loop?: boolean /** * Optional class name for styling */ className?: string /** * Whether to show the cursor * @default true */ showCursor?: boolean /** * Hide cursor while typing * @default false */ hideCursorOnType?: boolean /** * Character or React node to use as cursor * @default "|" */ cursorChar?: string | React.ReactNode /** * Animation variants for cursor */ cursorAnimationVariants?: { initial: Variants["initial"] animate: Variants["animate"] } /** * Optional class name for cursor styling */ cursorClassName?: string } const Typewriter = ({ text, as: Tag = "div", speed = 50, initialDelay = 0, waitTime = 2000, deleteSpeed = 30, loop = true, className, showCursor = true, hideCursorOnType = false, cursorChar = "|", cursorClassName = "ml-1", cursorAnimationVariants = { initial: { opacity: 0 }, animate: { opacity: 1, transition: { duration: 0.01, repeat: Infinity, repeatDelay: 0.4, repeatType: "reverse", }, }, }, ...props }: TypewriterProps & React.HTMLAttributes) => { const [displayText, setDisplayText] = useState("") const [currentIndex, setCurrentIndex] = useState(0) const [isDeleting, setIsDeleting] = useState(false) const [currentTextIndex, setCurrentTextIndex] = useState(0) const texts = Array.isArray(text) ? text : [text] useEffect(() => { let timeout: NodeJS.Timeout const currentText = texts[currentTextIndex] const startTyping = () => { if (isDeleting) { if (displayText === "") { setIsDeleting(false) if (currentTextIndex === texts.length - 1 && !loop) { return } setCurrentTextIndex((prev) => (prev + 1) % texts.length) setCurrentIndex(0) timeout = setTimeout(() => {}, waitTime) } else { timeout = setTimeout(() => { setDisplayText((prev) => prev.slice(0, -1)) }, deleteSpeed) } } else { if (currentIndex < currentText.length) { timeout = setTimeout(() => { setDisplayText((prev) => prev + currentText[currentIndex]) setCurrentIndex((prev) => prev + 1) }, speed) } else if (texts.length > 1) { timeout = setTimeout(() => { setIsDeleting(true) }, waitTime) } } } // Apply initial delay only at the start if (currentIndex === 0 && !isDeleting && displayText === "") { timeout = setTimeout(startTyping, initialDelay) } else { startTyping() } return () => clearTimeout(timeout) }, [ currentIndex, displayText, isDeleting, speed, deleteSpeed, waitTime, texts, currentTextIndex, loop, ]) return ( {displayText} {showCursor && ( {cursorChar} )} ) } export default Typewriter