@oalacea/chaosui 0.1.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 (139) hide show
  1. package/bin/cli.js +105 -13
  2. package/components/backgrounds/glow-orbs/index.tsx +1 -1
  3. package/components/buttons/chaos-button/chaos-button.module.css +3 -2
  4. package/components/buttons/cta-brutal/cta-brutal.module.css +81 -0
  5. package/components/buttons/cta-brutal/index.tsx +56 -0
  6. package/components/buttons/dead-button/dead-button.module.css +111 -0
  7. package/components/buttons/dead-button/index.tsx +47 -0
  8. package/components/buttons/deeper-button/deeper-button.module.css +76 -0
  9. package/components/buttons/deeper-button/index.tsx +51 -0
  10. package/components/buttons/dual-choice/dual-choice.module.css +90 -0
  11. package/components/buttons/dual-choice/index.tsx +54 -0
  12. package/components/buttons/glitch-button/glitch-button.module.css +7 -7
  13. package/components/buttons/tension-bar/index.tsx +79 -0
  14. package/components/buttons/tension-bar/tension-bar.module.css +105 -0
  15. package/components/chaos-vars.css +27 -0
  16. package/components/cyber/cyber-avatar/css/cyber-avatar.module.css +60 -0
  17. package/components/cyber/cyber-avatar/css/index.tsx +28 -0
  18. package/components/cyber/cyber-avatar/tailwind/index.tsx +46 -0
  19. package/components/cyber/cyber-input/css/cyber-input.module.css +87 -0
  20. package/components/cyber/cyber-input/css/index.tsx +49 -0
  21. package/components/cyber/cyber-input/tailwind/index.tsx +55 -0
  22. package/components/cyber/cyber-loader/css/cyber-loader.module.css +102 -0
  23. package/components/cyber/cyber-loader/css/index.tsx +58 -0
  24. package/components/cyber/cyber-loader/tailwind/index.tsx +63 -0
  25. package/components/cyber/cyber-modal/css/cyber-modal.module.css +124 -0
  26. package/components/cyber/cyber-modal/css/index.tsx +75 -0
  27. package/components/cyber/cyber-modal/tailwind/index.tsx +87 -0
  28. package/components/cyber/cyber-slider/css/cyber-slider.module.css +61 -0
  29. package/components/cyber/cyber-slider/css/index.tsx +41 -0
  30. package/components/cyber/cyber-slider/tailwind/index.tsx +51 -0
  31. package/components/cyber/cyber-tooltip/css/cyber-tooltip.module.css +67 -0
  32. package/components/cyber/cyber-tooltip/css/index.tsx +36 -0
  33. package/components/cyber/cyber-tooltip/tailwind/index.tsx +48 -0
  34. package/components/decorative/coffee-stain/coffee-stain.module.css +24 -0
  35. package/components/decorative/coffee-stain/index.tsx +55 -0
  36. package/components/decorative/ornaments/index.tsx +51 -0
  37. package/components/decorative/ornaments/ornaments.module.css +33 -0
  38. package/components/decorative/rune-symbols/index.tsx +55 -0
  39. package/components/decorative/rune-symbols/rune-symbols.module.css +22 -0
  40. package/components/effects/glitch-image/css/glitch-image.module.css +64 -0
  41. package/components/effects/glitch-image/css/index.tsx +25 -0
  42. package/components/effects/glitch-image/tailwind/index.tsx +49 -0
  43. package/components/effects/glowing-border/css/glowing-border.module.css +73 -0
  44. package/components/effects/glowing-border/css/index.tsx +45 -0
  45. package/components/effects/glowing-border/tailwind/index.tsx +40 -0
  46. package/components/effects/screen-distortion/screen-distortion.module.css +2 -2
  47. package/components/effects/warning-tape/index.tsx +4 -4
  48. package/components/effects/warning-tape/warning-tape.module.css +2 -0
  49. package/components/layout/data-grid/css/data-grid.module.css +52 -0
  50. package/components/layout/data-grid/css/index.tsx +76 -0
  51. package/components/layout/data-grid/tailwind/index.tsx +74 -0
  52. package/components/layout/hologram-card/css/hologram-card.module.css +102 -0
  53. package/components/layout/hologram-card/css/index.tsx +46 -0
  54. package/components/layout/hologram-card/tailwind/index.tsx +61 -0
  55. package/components/layout/horizontal-scroll/horizontal-scroll.module.css +30 -0
  56. package/components/layout/horizontal-scroll/index.tsx +78 -0
  57. package/components/layout/spec-grid/index.tsx +56 -0
  58. package/components/layout/spec-grid/spec-grid.module.css +21 -0
  59. package/components/layout/tower-pricing/index.tsx +56 -0
  60. package/components/layout/tower-pricing/tower-pricing.module.css +27 -0
  61. package/components/layout/tracklist/index.tsx +45 -0
  62. package/components/layout/tracklist/tracklist.module.css +24 -0
  63. package/components/layout/void-frame/index.tsx +32 -0
  64. package/components/layout/void-frame/void-frame.module.css +38 -0
  65. package/components/navigation/brutal-nav/brutal-nav.module.css +85 -0
  66. package/components/navigation/brutal-nav/index.tsx +71 -0
  67. package/components/navigation/hexagon-menu/css/hexagon-menu.module.css +55 -0
  68. package/components/navigation/hexagon-menu/css/index.tsx +35 -0
  69. package/components/navigation/hexagon-menu/tailwind/index.tsx +53 -0
  70. package/components/navigation/progress-dots/index.tsx +55 -0
  71. package/components/navigation/progress-dots/progress-dots.module.css +91 -0
  72. package/components/navigation/scattered-nav/index.tsx +59 -0
  73. package/components/navigation/scattered-nav/scattered-nav.module.css +113 -0
  74. package/components/navigation/scroll-indicator/index.tsx +58 -0
  75. package/components/navigation/scroll-indicator/scroll-indicator.module.css +82 -0
  76. package/components/navigation/vertical-nav/index.tsx +59 -0
  77. package/components/navigation/vertical-nav/vertical-nav.module.css +98 -0
  78. package/components/neon/neon-alert/css/index.tsx +53 -0
  79. package/components/neon/neon-alert/css/neon-alert.module.css +60 -0
  80. package/components/neon/neon-alert/tailwind/index.tsx +59 -0
  81. package/components/neon/neon-badge/css/index.tsx +49 -0
  82. package/components/neon/neon-badge/css/neon-badge.module.css +53 -0
  83. package/components/neon/neon-badge/tailwind/index.tsx +50 -0
  84. package/components/neon/neon-button/css/index.tsx +54 -0
  85. package/components/neon/neon-button/css/neon-button.module.css +114 -0
  86. package/components/neon/neon-button/tailwind/index.tsx +51 -0
  87. package/components/neon/neon-divider/css/index.tsx +26 -0
  88. package/components/neon/neon-divider/css/neon-divider.module.css +43 -0
  89. package/components/neon/neon-divider/tailwind/index.tsx +36 -0
  90. package/components/neon/neon-progress/css/index.tsx +65 -0
  91. package/components/neon/neon-progress/css/neon-progress.module.css +88 -0
  92. package/components/neon/neon-progress/tailwind/index.tsx +46 -0
  93. package/components/neon/neon-tabs/css/index.tsx +41 -0
  94. package/components/neon/neon-tabs/css/neon-tabs.module.css +45 -0
  95. package/components/neon/neon-tabs/tailwind/index.tsx +53 -0
  96. package/components/neon/neon-toggle/css/index.tsx +58 -0
  97. package/components/neon/neon-toggle/css/neon-toggle.module.css +79 -0
  98. package/components/neon/neon-toggle/tailwind/index.tsx +57 -0
  99. package/components/text/ascii-art/css/ascii-art.module.css +173 -0
  100. package/components/text/ascii-art/css/index.tsx +116 -0
  101. package/components/text/ascii-art/tailwind/index.tsx +124 -0
  102. package/components/text/blood-drip/css/blood-drip.module.css +142 -0
  103. package/components/text/blood-drip/css/index.tsx +113 -0
  104. package/components/text/blood-drip/tailwind/index.tsx +133 -0
  105. package/components/text/char-glitch/css/char-glitch.module.css +124 -0
  106. package/components/text/char-glitch/css/index.tsx +153 -0
  107. package/components/text/char-glitch/tailwind/index.tsx +126 -0
  108. package/components/text/countdown-display/css/countdown-display.module.css +179 -0
  109. package/components/text/countdown-display/css/index.tsx +190 -0
  110. package/components/text/countdown-display/tailwind/index.tsx +155 -0
  111. package/components/text/giant-layers/css/giant-layers.module.css +156 -0
  112. package/components/text/giant-layers/css/index.tsx +97 -0
  113. package/components/text/giant-layers/tailwind/index.tsx +111 -0
  114. package/components/text/glitch-text/glitch-text.module.css +2 -2
  115. package/components/text/reveal-text/css/index.tsx +180 -0
  116. package/components/text/reveal-text/css/reveal-text.module.css +129 -0
  117. package/components/text/reveal-text/tailwind/index.tsx +135 -0
  118. package/components/text/rotate-text/css/index.tsx +139 -0
  119. package/components/text/rotate-text/css/rotate-text.module.css +162 -0
  120. package/components/text/rotate-text/tailwind/index.tsx +127 -0
  121. package/components/text/strike-reveal/css/index.tsx +124 -0
  122. package/components/text/strike-reveal/css/strike-reveal.module.css +139 -0
  123. package/components/text/strike-reveal/tailwind/index.tsx +138 -0
  124. package/components/text/terminal-output/css/index.tsx +179 -0
  125. package/components/text/terminal-output/css/terminal-output.module.css +203 -0
  126. package/components/text/terminal-output/tailwind/index.tsx +174 -0
  127. package/components/text/typing-text/css/index.tsx +115 -0
  128. package/components/text/typing-text/css/typing-text.module.css +84 -0
  129. package/components/text/typing-text/tailwind/index.tsx +126 -0
  130. package/package.json +1 -1
  131. package/components/glow-orbs/glow-orbs.module.css +0 -31
  132. package/components/glow-orbs/index.tsx +0 -87
  133. package/components/light-beams/index.tsx +0 -80
  134. package/components/light-beams/light-beams.module.css +0 -27
  135. package/components/noise-canvas/index.tsx +0 -113
  136. package/components/noise-canvas/noise-canvas.module.css +0 -8
  137. package/components/package.json +0 -13
  138. package/components/particle-field/index.tsx +0 -81
  139. package/components/particle-field/particle-field.module.css +0 -31
@@ -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;