'use client';
import { forwardRef, HTMLAttributes, useEffect, useState, useCallback } from 'react';
import styles from './typing-text.module.css';
export interface TypingTextProps extends Omit, 'children'> {
/** Text to type out */
text: string;
/** Typing speed in ms per character */
speed?: number;
/** Delay before starting in ms */
delay?: number;
/** Show cursor */
showCursor?: boolean;
/** Cursor style */
cursorStyle?: 'block' | 'line' | 'underscore';
/** Visual variant */
variant?: 'default' | 'terminal' | 'hacker' | 'cyber' | 'ghost';
/** Loop the animation */
loop?: boolean;
/** Pause between loops in ms */
loopDelay?: number;
/** Delete speed when looping */
deleteSpeed?: number;
/** Callback when typing completes */
onComplete?: () => void;
}
export const TypingText = forwardRef(
(
{
text,
speed = 50,
delay = 0,
showCursor = true,
cursorStyle = 'block',
variant = 'default',
loop = false,
loopDelay = 2000,
deleteSpeed = 30,
onComplete,
className,
...props
},
ref
) => {
const [displayText, setDisplayText] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const typeText = useCallback(() => {
setIsTyping(true);
let index = 0;
const typeInterval = setInterval(() => {
if (index < text.length) {
setDisplayText(text.slice(0, index + 1));
index++;
} else {
clearInterval(typeInterval);
setIsTyping(false);
if (loop) {
setTimeout(() => {
setIsDeleting(true);
let deleteIndex = text.length;
const deleteInterval = setInterval(() => {
if (deleteIndex > 0) {
setDisplayText(text.slice(0, deleteIndex - 1));
deleteIndex--;
} else {
clearInterval(deleteInterval);
setIsDeleting(false);
setTimeout(typeText, delay);
}
}, deleteSpeed);
}, loopDelay);
} else {
onComplete?.();
}
}
}, speed);
return () => clearInterval(typeInterval);
}, [text, speed, loop, loopDelay, deleteSpeed, delay, onComplete]);
useEffect(() => {
const timeout = setTimeout(typeText, delay);
return () => clearTimeout(timeout);
}, [typeText, delay]);
const containerClasses = [
styles.container,
variant !== 'default' && styles[variant],
className
].filter(Boolean).join(' ');
const cursorClasses = [
styles.cursor,
styles[cursorStyle],
!isTyping && !isDeleting && !loop && styles.cursorHidden
].filter(Boolean).join(' ');
return (
{displayText}
{showCursor && }
);
}
);
TypingText.displayName = 'TypingText';
export default TypingText;