@oalacea/chaosui 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/bin/cli.js +32 -2
  2. package/components/buttons/cta-brutal/cta-brutal.module.css +81 -0
  3. package/components/buttons/cta-brutal/index.tsx +56 -0
  4. package/components/buttons/dead-button/dead-button.module.css +111 -0
  5. package/components/buttons/dead-button/index.tsx +47 -0
  6. package/components/buttons/deeper-button/deeper-button.module.css +76 -0
  7. package/components/buttons/deeper-button/index.tsx +51 -0
  8. package/components/buttons/dual-choice/dual-choice.module.css +90 -0
  9. package/components/buttons/dual-choice/index.tsx +54 -0
  10. package/components/buttons/tension-bar/index.tsx +79 -0
  11. package/components/buttons/tension-bar/tension-bar.module.css +105 -0
  12. package/components/decorative/coffee-stain/coffee-stain.module.css +24 -0
  13. package/components/decorative/coffee-stain/index.tsx +55 -0
  14. package/components/decorative/ornaments/index.tsx +51 -0
  15. package/components/decorative/ornaments/ornaments.module.css +33 -0
  16. package/components/decorative/rune-symbols/index.tsx +55 -0
  17. package/components/decorative/rune-symbols/rune-symbols.module.css +22 -0
  18. package/components/layout/horizontal-scroll/horizontal-scroll.module.css +30 -0
  19. package/components/layout/horizontal-scroll/index.tsx +78 -0
  20. package/components/layout/spec-grid/index.tsx +56 -0
  21. package/components/layout/spec-grid/spec-grid.module.css +21 -0
  22. package/components/layout/tower-pricing/index.tsx +56 -0
  23. package/components/layout/tower-pricing/tower-pricing.module.css +27 -0
  24. package/components/layout/tracklist/index.tsx +45 -0
  25. package/components/layout/tracklist/tracklist.module.css +24 -0
  26. package/components/layout/void-frame/index.tsx +32 -0
  27. package/components/layout/void-frame/void-frame.module.css +38 -0
  28. package/components/navigation/brutal-nav/brutal-nav.module.css +85 -0
  29. package/components/navigation/brutal-nav/index.tsx +71 -0
  30. package/components/navigation/progress-dots/index.tsx +55 -0
  31. package/components/navigation/progress-dots/progress-dots.module.css +91 -0
  32. package/components/navigation/scattered-nav/index.tsx +59 -0
  33. package/components/navigation/scattered-nav/scattered-nav.module.css +113 -0
  34. package/components/navigation/scroll-indicator/index.tsx +58 -0
  35. package/components/navigation/scroll-indicator/scroll-indicator.module.css +82 -0
  36. package/components/navigation/vertical-nav/index.tsx +59 -0
  37. package/components/navigation/vertical-nav/vertical-nav.module.css +98 -0
  38. package/components/text/ascii-art/css/ascii-art.module.css +173 -0
  39. package/components/text/ascii-art/css/index.tsx +116 -0
  40. package/components/text/ascii-art/tailwind/index.tsx +124 -0
  41. package/components/text/blood-drip/css/blood-drip.module.css +142 -0
  42. package/components/text/blood-drip/css/index.tsx +113 -0
  43. package/components/text/blood-drip/tailwind/index.tsx +133 -0
  44. package/components/text/char-glitch/css/char-glitch.module.css +124 -0
  45. package/components/text/char-glitch/css/index.tsx +153 -0
  46. package/components/text/char-glitch/tailwind/index.tsx +126 -0
  47. package/components/text/countdown-display/css/countdown-display.module.css +179 -0
  48. package/components/text/countdown-display/css/index.tsx +190 -0
  49. package/components/text/countdown-display/tailwind/index.tsx +155 -0
  50. package/components/text/giant-layers/css/giant-layers.module.css +156 -0
  51. package/components/text/giant-layers/css/index.tsx +97 -0
  52. package/components/text/giant-layers/tailwind/index.tsx +111 -0
  53. package/components/text/reveal-text/css/index.tsx +180 -0
  54. package/components/text/reveal-text/css/reveal-text.module.css +129 -0
  55. package/components/text/reveal-text/tailwind/index.tsx +135 -0
  56. package/components/text/rotate-text/css/index.tsx +139 -0
  57. package/components/text/rotate-text/css/rotate-text.module.css +162 -0
  58. package/components/text/rotate-text/tailwind/index.tsx +127 -0
  59. package/components/text/strike-reveal/css/index.tsx +124 -0
  60. package/components/text/strike-reveal/css/strike-reveal.module.css +139 -0
  61. package/components/text/strike-reveal/tailwind/index.tsx +138 -0
  62. package/components/text/terminal-output/css/index.tsx +179 -0
  63. package/components/text/terminal-output/css/terminal-output.module.css +203 -0
  64. package/components/text/terminal-output/tailwind/index.tsx +174 -0
  65. package/components/text/typing-text/css/index.tsx +115 -0
  66. package/components/text/typing-text/css/typing-text.module.css +84 -0
  67. package/components/text/typing-text/tailwind/index.tsx +126 -0
  68. package/package.json +1 -1
@@ -0,0 +1,129 @@
1
+ .container {
2
+ position: relative;
3
+ display: block;
4
+ }
5
+
6
+ .word {
7
+ display: inline-block;
8
+ overflow: hidden;
9
+ vertical-align: top;
10
+ margin-right: 0.25em;
11
+ }
12
+
13
+ .wordInner {
14
+ display: inline-block;
15
+ transform: translateY(100%);
16
+ opacity: 0;
17
+ transition: transform 0.6s cubic-bezier(0.19, 1, 0.22, 1), opacity 0.6s ease;
18
+ }
19
+
20
+ .word.visible .wordInner {
21
+ transform: translateY(0);
22
+ opacity: 1;
23
+ }
24
+
25
+ /* Direction variants */
26
+ .fromBottom .wordInner { transform: translateY(100%); }
27
+ .fromTop .wordInner { transform: translateY(-100%); }
28
+ .fromLeft .wordInner { transform: translateX(-100%); }
29
+ .fromRight .wordInner { transform: translateX(100%); }
30
+
31
+ .fromBottom.visible .wordInner,
32
+ .fromTop.visible .wordInner { transform: translateY(0); }
33
+ .fromLeft.visible .wordInner,
34
+ .fromRight.visible .wordInner { transform: translateX(0); }
35
+
36
+ /* Blur variant */
37
+ .blur .wordInner {
38
+ filter: blur(10px);
39
+ }
40
+
41
+ .blur.visible .wordInner {
42
+ filter: blur(0);
43
+ }
44
+
45
+ /* Scale variant */
46
+ .scale .wordInner {
47
+ transform: translateY(100%) scale(0.8);
48
+ }
49
+
50
+ .scale.visible .wordInner {
51
+ transform: translateY(0) scale(1);
52
+ }
53
+
54
+ /* Rotate variant */
55
+ .rotate .wordInner {
56
+ transform: translateY(100%) rotateX(90deg);
57
+ transform-origin: bottom;
58
+ }
59
+
60
+ .rotate.visible .wordInner {
61
+ transform: translateY(0) rotateX(0deg);
62
+ }
63
+
64
+ /* Split char mode */
65
+ .char {
66
+ display: inline-block;
67
+ transform: translateY(100%);
68
+ opacity: 0;
69
+ transition: transform 0.4s cubic-bezier(0.19, 1, 0.22, 1), opacity 0.4s ease;
70
+ }
71
+
72
+ .char.visible {
73
+ transform: translateY(0);
74
+ opacity: 1;
75
+ }
76
+
77
+ /* Line mode */
78
+ .line {
79
+ display: block;
80
+ overflow: hidden;
81
+ margin-bottom: 0.1em;
82
+ }
83
+
84
+ .lineInner {
85
+ display: block;
86
+ transform: translateY(100%);
87
+ opacity: 0;
88
+ transition: transform 0.8s cubic-bezier(0.19, 1, 0.22, 1), opacity 0.8s ease;
89
+ }
90
+
91
+ .line.visible .lineInner {
92
+ transform: translateY(0);
93
+ opacity: 1;
94
+ }
95
+
96
+ /* Highlight effect */
97
+ .highlight {
98
+ position: relative;
99
+ }
100
+
101
+ .highlight::after {
102
+ content: '';
103
+ position: absolute;
104
+ bottom: 0;
105
+ left: 0;
106
+ width: 0;
107
+ height: 30%;
108
+ background: var(--highlight-color, #ff0040);
109
+ opacity: 0.3;
110
+ z-index: -1;
111
+ transition: width 0.6s cubic-bezier(0.19, 1, 0.22, 1) 0.3s;
112
+ }
113
+
114
+ .highlight.visible::after {
115
+ width: 100%;
116
+ }
117
+
118
+ /* Speed variants */
119
+ .fast .wordInner,
120
+ .fast .char,
121
+ .fast .lineInner {
122
+ transition-duration: 0.3s;
123
+ }
124
+
125
+ .slow .wordInner,
126
+ .slow .char,
127
+ .slow .lineInner {
128
+ transition-duration: 1s;
129
+ }
@@ -0,0 +1,135 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, useEffect, useState, useRef } from 'react';
4
+
5
+ export interface RevealTextProps extends HTMLAttributes<HTMLDivElement> {
6
+ children: string;
7
+ splitBy?: 'word' | 'char' | 'line';
8
+ direction?: 'fromBottom' | 'fromTop' | 'fromLeft' | 'fromRight';
9
+ effect?: 'none' | 'blur' | 'scale' | 'rotate';
10
+ stagger?: number;
11
+ speed?: 'fast' | 'normal' | 'slow';
12
+ threshold?: number;
13
+ once?: boolean;
14
+ highlight?: boolean;
15
+ highlightColor?: string;
16
+ immediate?: boolean;
17
+ }
18
+
19
+ const speedDurations = { fast: 'duration-300', normal: 'duration-500', slow: 'duration-1000' };
20
+
21
+ const hiddenTransforms = {
22
+ fromBottom: 'translate-y-full',
23
+ fromTop: '-translate-y-full',
24
+ fromLeft: '-translate-x-full',
25
+ fromRight: 'translate-x-full',
26
+ };
27
+
28
+ export const RevealText = forwardRef<HTMLDivElement, RevealTextProps>(
29
+ (
30
+ {
31
+ children,
32
+ splitBy = 'word',
33
+ direction = 'fromBottom',
34
+ effect = 'none',
35
+ stagger = 50,
36
+ speed = 'normal',
37
+ threshold = 0.2,
38
+ once = true,
39
+ highlight = false,
40
+ highlightColor = '#ff0040',
41
+ immediate = false,
42
+ className = '',
43
+ ...props
44
+ },
45
+ ref
46
+ ) => {
47
+ const [visibleIndices, setVisibleIndices] = useState<Set<number>>(new Set());
48
+ const containerRef = useRef<HTMLDivElement>(null);
49
+ const hasAnimated = useRef(false);
50
+
51
+ const elements = splitBy === 'line'
52
+ ? children.split('\n')
53
+ : splitBy === 'char'
54
+ ? children.split('')
55
+ : children.split(' ');
56
+
57
+ useEffect(() => {
58
+ if (immediate) {
59
+ elements.forEach((_, i) => {
60
+ setTimeout(() => {
61
+ setVisibleIndices(prev => new Set(prev).add(i));
62
+ }, i * stagger);
63
+ });
64
+ return;
65
+ }
66
+
67
+ const observer = new IntersectionObserver(
68
+ ([entry]) => {
69
+ if (entry.isIntersecting && (!once || !hasAnimated.current)) {
70
+ hasAnimated.current = true;
71
+ elements.forEach((_, i) => {
72
+ setTimeout(() => {
73
+ setVisibleIndices(prev => new Set(prev).add(i));
74
+ }, i * stagger);
75
+ });
76
+ } else if (!entry.isIntersecting && !once) {
77
+ setVisibleIndices(new Set());
78
+ }
79
+ },
80
+ { threshold }
81
+ );
82
+
83
+ if (containerRef.current) observer.observe(containerRef.current);
84
+ return () => observer.disconnect();
85
+ }, [elements, stagger, threshold, once, immediate]);
86
+
87
+ const renderElement = (text: string, index: number) => {
88
+ const isVisible = visibleIndices.has(index);
89
+ const duration = speedDurations[speed];
90
+
91
+ const baseClasses = `inline-block overflow-hidden ${splitBy === 'line' ? 'block mb-1' : 'mr-1'}`;
92
+ const innerBase = `inline-block transition-all ease-out ${duration}`;
93
+ const hiddenState = `${hiddenTransforms[direction]} opacity-0 ${effect === 'blur' ? 'blur-sm' : ''} ${effect === 'scale' ? 'scale-75' : ''}`;
94
+ const visibleState = 'translate-y-0 translate-x-0 opacity-100 blur-0 scale-100';
95
+
96
+ return (
97
+ <span key={index} className={`${baseClasses} ${highlight ? 'relative' : ''}`}>
98
+ <span
99
+ className={`${innerBase} ${isVisible ? visibleState : hiddenState}`}
100
+ style={{ transitionDelay: `${index * stagger}ms` }}
101
+ >
102
+ {text === ' ' ? '\u00A0' : text}
103
+ </span>
104
+ {highlight && (
105
+ <span
106
+ className={`absolute bottom-0 left-0 h-[30%] -z-10 transition-all ${duration} opacity-30`}
107
+ style={{
108
+ backgroundColor: highlightColor,
109
+ width: isVisible ? '100%' : '0%',
110
+ transitionDelay: `${index * stagger + 200}ms`,
111
+ }}
112
+ />
113
+ )}
114
+ </span>
115
+ );
116
+ };
117
+
118
+ return (
119
+ <div
120
+ ref={(node) => {
121
+ (containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
122
+ if (typeof ref === 'function') ref(node);
123
+ else if (ref) ref.current = node;
124
+ }}
125
+ className={`relative ${className}`}
126
+ {...props}
127
+ >
128
+ {elements.map(renderElement)}
129
+ </div>
130
+ );
131
+ }
132
+ );
133
+
134
+ RevealText.displayName = 'RevealText';
135
+ export default RevealText;
@@ -0,0 +1,139 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, useEffect, useState, useRef, useCallback } from 'react';
4
+ import styles from './rotate-text.module.css';
5
+
6
+ export interface RotateTextProps extends HTMLAttributes<HTMLSpanElement> {
7
+ /** Static text before rotating words */
8
+ prefix?: string;
9
+ /** Static text after rotating words */
10
+ suffix?: string;
11
+ /** Words to rotate through */
12
+ words: string[];
13
+ /** Rotation direction/animation */
14
+ animation?: 'up' | 'down' | 'left' | 'right' | 'flip' | 'fade' | 'zoom' | 'blur';
15
+ /** Duration each word is shown (ms) */
16
+ duration?: number;
17
+ /** Animation speed */
18
+ speed?: 'fast' | 'normal' | 'slow';
19
+ /** Highlight active word */
20
+ highlight?: boolean;
21
+ /** Highlight color */
22
+ highlightColor?: string;
23
+ /** Show underline on active */
24
+ underline?: boolean;
25
+ /** Show brackets around rotator */
26
+ bracket?: boolean;
27
+ /** Bracket color */
28
+ bracketColor?: string;
29
+ /** Pause on hover */
30
+ pauseOnHover?: boolean;
31
+ /** Show typing cursor */
32
+ cursor?: boolean;
33
+ /** Callback when word changes */
34
+ onChange?: (word: string, index: number) => void;
35
+ }
36
+
37
+ export const RotateText = forwardRef<HTMLSpanElement, RotateTextProps>(
38
+ (
39
+ {
40
+ prefix,
41
+ suffix,
42
+ words,
43
+ animation = 'up',
44
+ duration = 2000,
45
+ speed = 'normal',
46
+ highlight = false,
47
+ highlightColor = '#ff0040',
48
+ underline = false,
49
+ bracket = false,
50
+ bracketColor = '#666',
51
+ pauseOnHover = false,
52
+ cursor = false,
53
+ onChange,
54
+ className,
55
+ style,
56
+ ...props
57
+ },
58
+ ref
59
+ ) => {
60
+ const [currentIndex, setCurrentIndex] = useState(0);
61
+ const [exitIndex, setExitIndex] = useState<number | null>(null);
62
+ const intervalRef = useRef<NodeJS.Timeout>();
63
+ const isPaused = useRef(false);
64
+
65
+ const maxWidth = Math.max(...words.map(w => w.length));
66
+
67
+ const rotate = useCallback(() => {
68
+ if (isPaused.current) return;
69
+
70
+ setExitIndex(currentIndex);
71
+ const nextIndex = (currentIndex + 1) % words.length;
72
+ setCurrentIndex(nextIndex);
73
+ onChange?.(words[nextIndex], nextIndex);
74
+
75
+ setTimeout(() => setExitIndex(null), 500);
76
+ }, [currentIndex, words, onChange]);
77
+
78
+ useEffect(() => {
79
+ intervalRef.current = setInterval(rotate, duration);
80
+ return () => {
81
+ if (intervalRef.current) clearInterval(intervalRef.current);
82
+ };
83
+ }, [rotate, duration]);
84
+
85
+ const handleMouseEnter = () => {
86
+ if (pauseOnHover) isPaused.current = true;
87
+ };
88
+
89
+ const handleMouseLeave = () => {
90
+ if (pauseOnHover) isPaused.current = false;
91
+ };
92
+
93
+ const containerClasses = [
94
+ styles.container,
95
+ styles[animation],
96
+ styles[speed],
97
+ highlight && styles.highlight,
98
+ underline && styles.underline,
99
+ bracket && styles.bracket,
100
+ pauseOnHover && styles.pauseOnHover,
101
+ cursor && styles.cursor,
102
+ className
103
+ ].filter(Boolean).join(' ');
104
+
105
+ return (
106
+ <span
107
+ ref={ref}
108
+ className={containerClasses}
109
+ style={{
110
+ '--highlight-color': highlightColor,
111
+ '--bracket-color': bracketColor,
112
+ ...style
113
+ } as React.CSSProperties}
114
+ onMouseEnter={handleMouseEnter}
115
+ onMouseLeave={handleMouseLeave}
116
+ {...props}
117
+ >
118
+ {prefix && <span className={styles.static}>{prefix} </span>}
119
+ <span
120
+ className={styles.rotator}
121
+ style={{ width: `${maxWidth}ch` }}
122
+ >
123
+ {words.map((word, i) => (
124
+ <span
125
+ key={i}
126
+ className={`${styles.word} ${i === currentIndex ? styles.active : ''} ${i === exitIndex ? styles.exit : ''}`}
127
+ >
128
+ {word}
129
+ </span>
130
+ ))}
131
+ </span>
132
+ {suffix && <span className={styles.static}> {suffix}</span>}
133
+ </span>
134
+ );
135
+ }
136
+ );
137
+
138
+ RotateText.displayName = 'RotateText';
139
+ export default RotateText;
@@ -0,0 +1,162 @@
1
+ .container {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: 0.5em;
5
+ overflow: hidden;
6
+ }
7
+
8
+ .static {
9
+ display: inline;
10
+ }
11
+
12
+ .rotator {
13
+ display: inline-block;
14
+ position: relative;
15
+ height: 1.2em;
16
+ overflow: hidden;
17
+ }
18
+
19
+ .word {
20
+ display: block;
21
+ position: absolute;
22
+ top: 0;
23
+ left: 0;
24
+ width: 100%;
25
+ transform: translateY(100%);
26
+ opacity: 0;
27
+ transition: transform 0.5s cubic-bezier(0.19, 1, 0.22, 1), opacity 0.5s ease;
28
+ }
29
+
30
+ .word.active {
31
+ transform: translateY(0);
32
+ opacity: 1;
33
+ }
34
+
35
+ .word.exit {
36
+ transform: translateY(-100%);
37
+ opacity: 0;
38
+ }
39
+
40
+ /* Direction variants */
41
+ .up .word { transform: translateY(100%); }
42
+ .up .word.active { transform: translateY(0); }
43
+ .up .word.exit { transform: translateY(-100%); }
44
+
45
+ .down .word { transform: translateY(-100%); }
46
+ .down .word.active { transform: translateY(0); }
47
+ .down .word.exit { transform: translateY(100%); }
48
+
49
+ .left .word { transform: translateX(100%); }
50
+ .left .word.active { transform: translateX(0); }
51
+ .left .word.exit { transform: translateX(-100%); }
52
+
53
+ .right .word { transform: translateX(-100%); }
54
+ .right .word.active { transform: translateX(0); }
55
+ .right .word.exit { transform: translateX(100%); }
56
+
57
+ /* Flip variant */
58
+ .flip .word {
59
+ transform: rotateX(90deg);
60
+ transform-origin: bottom;
61
+ }
62
+
63
+ .flip .word.active {
64
+ transform: rotateX(0deg);
65
+ }
66
+
67
+ .flip .word.exit {
68
+ transform: rotateX(-90deg);
69
+ transform-origin: top;
70
+ }
71
+
72
+ /* Fade variant */
73
+ .fade .word {
74
+ transform: none;
75
+ opacity: 0;
76
+ }
77
+
78
+ .fade .word.active {
79
+ opacity: 1;
80
+ }
81
+
82
+ .fade .word.exit {
83
+ opacity: 0;
84
+ }
85
+
86
+ /* Zoom variant */
87
+ .zoom .word {
88
+ transform: scale(0);
89
+ opacity: 0;
90
+ }
91
+
92
+ .zoom .word.active {
93
+ transform: scale(1);
94
+ opacity: 1;
95
+ }
96
+
97
+ .zoom .word.exit {
98
+ transform: scale(2);
99
+ opacity: 0;
100
+ }
101
+
102
+ /* Blur variant */
103
+ .blur .word {
104
+ transform: translateY(50%);
105
+ opacity: 0;
106
+ filter: blur(10px);
107
+ }
108
+
109
+ .blur .word.active {
110
+ transform: translateY(0);
111
+ opacity: 1;
112
+ filter: blur(0);
113
+ }
114
+
115
+ .blur .word.exit {
116
+ transform: translateY(-50%);
117
+ opacity: 0;
118
+ filter: blur(10px);
119
+ }
120
+
121
+ /* Color variants */
122
+ .highlight .word.active {
123
+ color: var(--highlight-color, #ff0040);
124
+ text-shadow: 0 0 10px var(--highlight-color, #ff0040);
125
+ }
126
+
127
+ /* Styling variants */
128
+ .underline .word.active {
129
+ text-decoration: underline;
130
+ text-decoration-color: var(--highlight-color, #ff0040);
131
+ text-underline-offset: 4px;
132
+ }
133
+
134
+ .bracket::before,
135
+ .bracket::after {
136
+ color: var(--bracket-color, #666);
137
+ }
138
+
139
+ .bracket::before { content: '['; margin-right: 0.25em; }
140
+ .bracket::after { content: ']'; margin-left: 0.25em; }
141
+
142
+ /* Speed variants */
143
+ .fast .word { transition-duration: 0.3s; }
144
+ .slow .word { transition-duration: 0.8s; }
145
+
146
+ /* Hover pause */
147
+ .pauseOnHover:hover .word {
148
+ animation-play-state: paused;
149
+ }
150
+
151
+ /* Cursor indicator */
152
+ .cursor::after {
153
+ content: '|';
154
+ display: inline-block;
155
+ margin-left: 2px;
156
+ animation: cursorBlink 1s step-end infinite;
157
+ }
158
+
159
+ @keyframes cursorBlink {
160
+ 0%, 100% { opacity: 1; }
161
+ 50% { opacity: 0; }
162
+ }
@@ -0,0 +1,127 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, useEffect, useState, useRef, useCallback } from 'react';
4
+
5
+ export interface RotateTextProps extends HTMLAttributes<HTMLSpanElement> {
6
+ prefix?: string;
7
+ suffix?: string;
8
+ words: string[];
9
+ animation?: 'up' | 'down' | 'left' | 'right' | 'flip' | 'fade' | 'zoom' | 'blur';
10
+ duration?: number;
11
+ speed?: 'fast' | 'normal' | 'slow';
12
+ highlight?: boolean;
13
+ highlightColor?: string;
14
+ underline?: boolean;
15
+ bracket?: boolean;
16
+ bracketColor?: string;
17
+ pauseOnHover?: boolean;
18
+ cursor?: boolean;
19
+ onChange?: (word: string, index: number) => void;
20
+ }
21
+
22
+ const speedDurations = { fast: 'duration-300', normal: 'duration-500', slow: 'duration-700' };
23
+
24
+ const getTransform = (animation: string, state: 'hidden' | 'active' | 'exit') => {
25
+ const transforms: Record<string, Record<string, string>> = {
26
+ up: { hidden: 'translate-y-full opacity-0', active: 'translate-y-0 opacity-100', exit: '-translate-y-full opacity-0' },
27
+ down: { hidden: '-translate-y-full opacity-0', active: 'translate-y-0 opacity-100', exit: 'translate-y-full opacity-0' },
28
+ left: { hidden: 'translate-x-full opacity-0', active: 'translate-x-0 opacity-100', exit: '-translate-x-full opacity-0' },
29
+ right: { hidden: '-translate-x-full opacity-0', active: 'translate-x-0 opacity-100', exit: 'translate-x-full opacity-0' },
30
+ fade: { hidden: 'opacity-0', active: 'opacity-100', exit: 'opacity-0' },
31
+ zoom: { hidden: 'scale-0 opacity-0', active: 'scale-100 opacity-100', exit: 'scale-150 opacity-0' },
32
+ 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' },
33
+ flip: { hidden: 'rotateX-90 opacity-0', active: 'rotateX-0 opacity-100', exit: '-rotateX-90 opacity-0' },
34
+ };
35
+ return transforms[animation]?.[state] || '';
36
+ };
37
+
38
+ export const RotateText = forwardRef<HTMLSpanElement, RotateTextProps>(
39
+ (
40
+ {
41
+ prefix,
42
+ suffix,
43
+ words,
44
+ animation = 'up',
45
+ duration = 2000,
46
+ speed = 'normal',
47
+ highlight = false,
48
+ highlightColor = '#ff0040',
49
+ underline = false,
50
+ bracket = false,
51
+ bracketColor = '#666',
52
+ pauseOnHover = false,
53
+ cursor = false,
54
+ onChange,
55
+ className = '',
56
+ ...props
57
+ },
58
+ ref
59
+ ) => {
60
+ const [currentIndex, setCurrentIndex] = useState(0);
61
+ const [exitIndex, setExitIndex] = useState<number | null>(null);
62
+ const intervalRef = useRef<NodeJS.Timeout>();
63
+ const isPaused = useRef(false);
64
+
65
+ const maxWidth = Math.max(...words.map(w => w.length));
66
+
67
+ const rotate = useCallback(() => {
68
+ if (isPaused.current) return;
69
+
70
+ setExitIndex(currentIndex);
71
+ const nextIndex = (currentIndex + 1) % words.length;
72
+ setCurrentIndex(nextIndex);
73
+ onChange?.(words[nextIndex], nextIndex);
74
+
75
+ setTimeout(() => setExitIndex(null), 500);
76
+ }, [currentIndex, words, onChange]);
77
+
78
+ useEffect(() => {
79
+ intervalRef.current = setInterval(rotate, duration);
80
+ return () => { if (intervalRef.current) clearInterval(intervalRef.current); };
81
+ }, [rotate, duration]);
82
+
83
+ const handleMouseEnter = () => { if (pauseOnHover) isPaused.current = true; };
84
+ const handleMouseLeave = () => { if (pauseOnHover) isPaused.current = false; };
85
+
86
+ return (
87
+ <span
88
+ ref={ref}
89
+ className={`inline-flex items-center gap-2 ${className}`}
90
+ onMouseEnter={handleMouseEnter}
91
+ onMouseLeave={handleMouseLeave}
92
+ {...props}
93
+ >
94
+ {prefix && <span>{prefix}</span>}
95
+ {bracket && <span style={{ color: bracketColor }}>[</span>}
96
+ <span
97
+ className="relative inline-block overflow-hidden"
98
+ style={{ width: `${maxWidth}ch`, height: '1.2em' }}
99
+ >
100
+ {words.map((word, i) => {
101
+ const isActive = i === currentIndex;
102
+ const isExit = i === exitIndex;
103
+ const state = isActive ? 'active' : isExit ? 'exit' : 'hidden';
104
+
105
+ return (
106
+ <span
107
+ key={i}
108
+ className={`absolute top-0 left-0 w-full transition-all ease-out ${speedDurations[speed]} ${getTransform(animation, state)} ${
109
+ isActive && highlight ? 'drop-shadow-lg' : ''
110
+ } ${isActive && underline ? 'underline underline-offset-4' : ''}`}
111
+ style={isActive && highlight ? { color: highlightColor, textShadow: `0 0 10px ${highlightColor}` } : undefined}
112
+ >
113
+ {word}
114
+ </span>
115
+ );
116
+ })}
117
+ </span>
118
+ {bracket && <span style={{ color: bracketColor }}>]</span>}
119
+ {suffix && <span>{suffix}</span>}
120
+ {cursor && <span className="animate-pulse">|</span>}
121
+ </span>
122
+ );
123
+ }
124
+ );
125
+
126
+ RotateText.displayName = 'RotateText';
127
+ export default RotateText;