'use client';
import { forwardRef, HTMLAttributes, useEffect, useState, useRef, useCallback } from 'react';
import styles from './rotate-text.module.css';
export interface RotateTextProps extends HTMLAttributes {
/** Static text before rotating words */
prefix?: string;
/** Static text after rotating words */
suffix?: string;
/** Words to rotate through */
words: string[];
/** Rotation direction/animation */
animation?: 'up' | 'down' | 'left' | 'right' | 'flip' | 'fade' | 'zoom' | 'blur';
/** Duration each word is shown (ms) */
duration?: number;
/** Animation speed */
speed?: 'fast' | 'normal' | 'slow';
/** Highlight active word */
highlight?: boolean;
/** Highlight color */
highlightColor?: string;
/** Show underline on active */
underline?: boolean;
/** Show brackets around rotator */
bracket?: boolean;
/** Bracket color */
bracketColor?: string;
/** Pause on hover */
pauseOnHover?: boolean;
/** Show typing cursor */
cursor?: boolean;
/** Callback when word changes */
onChange?: (word: string, index: number) => void;
}
export const RotateText = forwardRef(
(
{
prefix,
suffix,
words,
animation = 'up',
duration = 2000,
speed = 'normal',
highlight = false,
highlightColor = '#ff0040',
underline = false,
bracket = false,
bracketColor = '#666',
pauseOnHover = false,
cursor = false,
onChange,
className,
style,
...props
},
ref
) => {
const [currentIndex, setCurrentIndex] = useState(0);
const [exitIndex, setExitIndex] = useState(null);
const intervalRef = useRef();
const isPaused = useRef(false);
const maxWidth = Math.max(...words.map(w => w.length));
const rotate = useCallback(() => {
if (isPaused.current) return;
setExitIndex(currentIndex);
const nextIndex = (currentIndex + 1) % words.length;
setCurrentIndex(nextIndex);
onChange?.(words[nextIndex], nextIndex);
setTimeout(() => setExitIndex(null), 500);
}, [currentIndex, words, onChange]);
useEffect(() => {
intervalRef.current = setInterval(rotate, duration);
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [rotate, duration]);
const handleMouseEnter = () => {
if (pauseOnHover) isPaused.current = true;
};
const handleMouseLeave = () => {
if (pauseOnHover) isPaused.current = false;
};
const containerClasses = [
styles.container,
styles[animation],
styles[speed],
highlight && styles.highlight,
underline && styles.underline,
bracket && styles.bracket,
pauseOnHover && styles.pauseOnHover,
cursor && styles.cursor,
className
].filter(Boolean).join(' ');
return (
{prefix && {prefix} }
{words.map((word, i) => (
{word}
))}
{suffix && {suffix}}
);
}
);
RotateText.displayName = 'RotateText';
export default RotateText;