'use client';
import { forwardRef, HTMLAttributes, useEffect, useState, useRef, useCallback } from 'react';
export interface RotateTextProps extends HTMLAttributes {
prefix?: string;
suffix?: string;
words: string[];
animation?: 'up' | 'down' | 'left' | 'right' | 'flip' | 'fade' | 'zoom' | 'blur';
duration?: number;
speed?: 'fast' | 'normal' | 'slow';
highlight?: boolean;
highlightColor?: string;
underline?: boolean;
bracket?: boolean;
bracketColor?: string;
pauseOnHover?: boolean;
cursor?: boolean;
onChange?: (word: string, index: number) => void;
}
const speedDurations = { fast: 'duration-300', normal: 'duration-500', slow: 'duration-700' };
const getTransform = (animation: string, state: 'hidden' | 'active' | 'exit') => {
const transforms: Record> = {
up: { hidden: 'translate-y-full opacity-0', active: 'translate-y-0 opacity-100', exit: '-translate-y-full opacity-0' },
down: { hidden: '-translate-y-full opacity-0', active: 'translate-y-0 opacity-100', exit: 'translate-y-full opacity-0' },
left: { hidden: 'translate-x-full opacity-0', active: 'translate-x-0 opacity-100', exit: '-translate-x-full opacity-0' },
right: { hidden: '-translate-x-full opacity-0', active: 'translate-x-0 opacity-100', exit: 'translate-x-full opacity-0' },
fade: { hidden: 'opacity-0', active: 'opacity-100', exit: 'opacity-0' },
zoom: { hidden: 'scale-0 opacity-0', active: 'scale-100 opacity-100', exit: 'scale-150 opacity-0' },
blur: { hidden: 'translate-y-1/2 opacity-0 blur-sm', active: 'translate-y-0 opacity-100 blur-0', exit: '-translate-y-1/2 opacity-0 blur-sm' },
flip: { hidden: 'rotateX-90 opacity-0', active: 'rotateX-0 opacity-100', exit: '-rotateX-90 opacity-0' },
};
return transforms[animation]?.[state] || '';
};
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 = '',
...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; };
return (
{prefix && {prefix}}
{bracket && [}
{words.map((word, i) => {
const isActive = i === currentIndex;
const isExit = i === exitIndex;
const state = isActive ? 'active' : isExit ? 'exit' : 'hidden';
return (
{word}
);
})}
{bracket && ]}
{suffix && {suffix}}
{cursor && |}
);
}
);
RotateText.displayName = 'RotateText';
export default RotateText;