@oalacea/chaosui 0.4.0 → 0.5.1

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 (178) hide show
  1. package/buttons/cta-brutal/css/cta-brutal.module.css +175 -0
  2. package/buttons/cta-brutal/css/index.tsx +78 -0
  3. package/buttons/cta-brutal/tailwind/index.tsx +85 -0
  4. package/buttons/dead-button/css/dead-button.module.css +216 -0
  5. package/buttons/dead-button/css/index.tsx +47 -0
  6. package/buttons/dead-button/tailwind/index.tsx +90 -0
  7. package/buttons/deeper-button/css/deeper-button.module.css +141 -0
  8. package/buttons/deeper-button/css/index.tsx +62 -0
  9. package/buttons/deeper-button/tailwind/index.tsx +78 -0
  10. package/buttons/dual-choice/css/dual-choice.module.css +176 -0
  11. package/buttons/dual-choice/css/index.tsx +56 -0
  12. package/buttons/dual-choice/tailwind/index.tsx +89 -0
  13. package/buttons/tension-bar/css/index.tsx +96 -0
  14. package/buttons/tension-bar/css/tension-bar.module.css +204 -0
  15. package/buttons/tension-bar/tailwind/index.tsx +122 -0
  16. package/decorative/inscription/css/index.tsx +66 -0
  17. package/decorative/inscription/css/inscription.module.css +160 -0
  18. package/decorative/inscription/tailwind/index.tsx +91 -0
  19. package/decorative/ornaments/css/index.tsx +136 -0
  20. package/decorative/ornaments/css/ornaments.module.css +144 -0
  21. package/decorative/ornaments/tailwind/index.tsx +122 -0
  22. package/decorative/rune-symbols/css/index.tsx +116 -0
  23. package/decorative/rune-symbols/css/rune-symbols.module.css +116 -0
  24. package/decorative/rune-symbols/tailwind/index.tsx +108 -0
  25. package/decorative/sheet-music/css/index.tsx +144 -0
  26. package/decorative/sheet-music/css/sheet-music.module.css +152 -0
  27. package/decorative/sheet-music/tailwind/index.tsx +111 -0
  28. package/layout/horizontal-scroll/css/horizontal-scroll.module.css +93 -0
  29. package/layout/horizontal-scroll/css/index.tsx +114 -0
  30. package/layout/horizontal-scroll/tailwind/index.tsx +108 -0
  31. package/layout/spec-grid/css/index.tsx +103 -0
  32. package/layout/spec-grid/css/spec-grid.module.css +125 -0
  33. package/layout/spec-grid/tailwind/index.tsx +91 -0
  34. package/layout/tower-pricing/css/index.tsx +91 -0
  35. package/layout/tower-pricing/css/tower-pricing.module.css +177 -0
  36. package/layout/tower-pricing/tailwind/index.tsx +106 -0
  37. package/layout/tracklist/css/index.tsx +96 -0
  38. package/layout/tracklist/css/tracklist.module.css +141 -0
  39. package/layout/tracklist/tailwind/index.tsx +92 -0
  40. package/layout/void-frame/css/index.tsx +60 -0
  41. package/layout/void-frame/css/void-frame.module.css +141 -0
  42. package/layout/void-frame/tailwind/index.tsx +64 -0
  43. package/navigation/brutal-nav/css/brutal-nav.module.css +178 -0
  44. package/navigation/brutal-nav/css/index.tsx +80 -0
  45. package/navigation/brutal-nav/tailwind/index.tsx +95 -0
  46. package/navigation/progress-dots/css/index.tsx +60 -0
  47. package/navigation/progress-dots/css/progress-dots.module.css +152 -0
  48. package/navigation/progress-dots/tailwind/index.tsx +105 -0
  49. package/navigation/scattered-nav/css/index.tsx +59 -0
  50. package/navigation/scattered-nav/css/scattered-nav.module.css +121 -0
  51. package/navigation/scattered-nav/tailwind/index.tsx +77 -0
  52. package/navigation/scroll-indicator/css/index.tsx +72 -0
  53. package/navigation/scroll-indicator/css/scroll-indicator.module.css +129 -0
  54. package/navigation/scroll-indicator/tailwind/index.tsx +107 -0
  55. package/navigation/vertical-nav/css/index.tsx +69 -0
  56. package/navigation/vertical-nav/css/vertical-nav.module.css +117 -0
  57. package/navigation/vertical-nav/tailwind/index.tsx +91 -0
  58. package/package.json +8 -33
  59. package/text/ascii-art/css/ascii-art.module.css +173 -0
  60. package/text/ascii-art/css/index.tsx +116 -0
  61. package/text/ascii-art/tailwind/index.tsx +124 -0
  62. package/text/blood-drip/css/blood-drip.module.css +142 -0
  63. package/text/blood-drip/css/index.tsx +113 -0
  64. package/text/blood-drip/tailwind/index.tsx +133 -0
  65. package/text/char-glitch/css/char-glitch.module.css +124 -0
  66. package/text/char-glitch/css/index.tsx +153 -0
  67. package/text/char-glitch/tailwind/index.tsx +126 -0
  68. package/text/countdown-display/css/countdown-display.module.css +179 -0
  69. package/text/countdown-display/css/index.tsx +190 -0
  70. package/text/countdown-display/tailwind/index.tsx +155 -0
  71. package/text/giant-layers/css/giant-layers.module.css +156 -0
  72. package/text/giant-layers/css/index.tsx +97 -0
  73. package/text/giant-layers/tailwind/index.tsx +111 -0
  74. package/text/reveal-text/css/index.tsx +180 -0
  75. package/text/reveal-text/css/reveal-text.module.css +129 -0
  76. package/text/reveal-text/tailwind/index.tsx +135 -0
  77. package/text/rotate-text/css/index.tsx +139 -0
  78. package/text/rotate-text/css/rotate-text.module.css +162 -0
  79. package/text/rotate-text/tailwind/index.tsx +127 -0
  80. package/text/strike-reveal/css/index.tsx +124 -0
  81. package/text/strike-reveal/css/strike-reveal.module.css +139 -0
  82. package/text/strike-reveal/tailwind/index.tsx +138 -0
  83. package/text/terminal-output/css/index.tsx +179 -0
  84. package/text/terminal-output/css/terminal-output.module.css +203 -0
  85. package/text/terminal-output/tailwind/index.tsx +174 -0
  86. package/text/typing-text/css/index.tsx +115 -0
  87. package/text/typing-text/css/typing-text.module.css +84 -0
  88. package/text/typing-text/tailwind/index.tsx +126 -0
  89. package/bin/cli.js +0 -278
  90. package/components/backgrounds/light-beams/index.tsx +0 -80
  91. package/components/backgrounds/light-beams/light-beams.module.css +0 -27
  92. package/components/navigation/hexagon-menu/css/hexagon-menu.module.css +0 -55
  93. package/components/navigation/hexagon-menu/css/index.tsx +0 -35
  94. package/components/navigation/hexagon-menu/tailwind/index.tsx +0 -53
  95. /package/{components/backgrounds → backgrounds}/glow-orbs/glow-orbs.module.css +0 -0
  96. /package/{components/backgrounds → backgrounds}/glow-orbs/index.tsx +0 -0
  97. /package/{components/backgrounds → backgrounds}/noise-canvas/index.tsx +0 -0
  98. /package/{components/backgrounds → backgrounds}/noise-canvas/noise-canvas.module.css +0 -0
  99. /package/{components/backgrounds → backgrounds}/particle-field/index.tsx +0 -0
  100. /package/{components/backgrounds → backgrounds}/particle-field/particle-field.module.css +0 -0
  101. /package/{components/buttons → buttons}/chaos-button/chaos-button.module.css +0 -0
  102. /package/{components/buttons → buttons}/chaos-button/index.tsx +0 -0
  103. /package/{components/buttons → buttons}/glitch-button/glitch-button.module.css +0 -0
  104. /package/{components/buttons → buttons}/glitch-button/index.tsx +0 -0
  105. /package/{components/chaos-vars.css → chaos-vars.css} +0 -0
  106. /package/{components/cyber → cyber}/cyber-avatar/css/cyber-avatar.module.css +0 -0
  107. /package/{components/cyber → cyber}/cyber-avatar/css/index.tsx +0 -0
  108. /package/{components/cyber → cyber}/cyber-avatar/tailwind/index.tsx +0 -0
  109. /package/{components/cyber → cyber}/cyber-input/css/cyber-input.module.css +0 -0
  110. /package/{components/cyber → cyber}/cyber-input/css/index.tsx +0 -0
  111. /package/{components/cyber → cyber}/cyber-input/tailwind/index.tsx +0 -0
  112. /package/{components/cyber → cyber}/cyber-loader/css/cyber-loader.module.css +0 -0
  113. /package/{components/cyber → cyber}/cyber-loader/css/index.tsx +0 -0
  114. /package/{components/cyber → cyber}/cyber-loader/tailwind/index.tsx +0 -0
  115. /package/{components/cyber → cyber}/cyber-modal/css/cyber-modal.module.css +0 -0
  116. /package/{components/cyber → cyber}/cyber-modal/css/index.tsx +0 -0
  117. /package/{components/cyber → cyber}/cyber-modal/tailwind/index.tsx +0 -0
  118. /package/{components/cyber → cyber}/cyber-slider/css/cyber-slider.module.css +0 -0
  119. /package/{components/cyber → cyber}/cyber-slider/css/index.tsx +0 -0
  120. /package/{components/cyber → cyber}/cyber-slider/tailwind/index.tsx +0 -0
  121. /package/{components/cyber → cyber}/cyber-tooltip/css/cyber-tooltip.module.css +0 -0
  122. /package/{components/cyber → cyber}/cyber-tooltip/css/index.tsx +0 -0
  123. /package/{components/cyber → cyber}/cyber-tooltip/tailwind/index.tsx +0 -0
  124. /package/{components/effects → effects}/cursor-follower/cursor-follower.module.css +0 -0
  125. /package/{components/effects → effects}/cursor-follower/index.tsx +0 -0
  126. /package/{components/effects → effects}/glitch-image/css/glitch-image.module.css +0 -0
  127. /package/{components/effects → effects}/glitch-image/css/index.tsx +0 -0
  128. /package/{components/effects → effects}/glitch-image/tailwind/index.tsx +0 -0
  129. /package/{components/effects → effects}/glowing-border/css/glowing-border.module.css +0 -0
  130. /package/{components/effects → effects}/glowing-border/css/index.tsx +0 -0
  131. /package/{components/effects → effects}/glowing-border/tailwind/index.tsx +0 -0
  132. /package/{components/effects → effects}/screen-distortion/index.tsx +0 -0
  133. /package/{components/effects → effects}/screen-distortion/screen-distortion.module.css +0 -0
  134. /package/{components/effects → effects}/warning-tape/index.tsx +0 -0
  135. /package/{components/effects → effects}/warning-tape/warning-tape.module.css +0 -0
  136. /package/{components/layout → layout}/data-grid/css/data-grid.module.css +0 -0
  137. /package/{components/layout → layout}/data-grid/css/index.tsx +0 -0
  138. /package/{components/layout → layout}/data-grid/tailwind/index.tsx +0 -0
  139. /package/{components/layout → layout}/hologram-card/css/hologram-card.module.css +0 -0
  140. /package/{components/layout → layout}/hologram-card/css/index.tsx +0 -0
  141. /package/{components/layout → layout}/hologram-card/tailwind/index.tsx +0 -0
  142. /package/{components/neon → neon}/neon-alert/css/index.tsx +0 -0
  143. /package/{components/neon → neon}/neon-alert/css/neon-alert.module.css +0 -0
  144. /package/{components/neon → neon}/neon-alert/tailwind/index.tsx +0 -0
  145. /package/{components/neon → neon}/neon-badge/css/index.tsx +0 -0
  146. /package/{components/neon → neon}/neon-badge/css/neon-badge.module.css +0 -0
  147. /package/{components/neon → neon}/neon-badge/tailwind/index.tsx +0 -0
  148. /package/{components/neon → neon}/neon-button/css/index.tsx +0 -0
  149. /package/{components/neon → neon}/neon-button/css/neon-button.module.css +0 -0
  150. /package/{components/neon → neon}/neon-button/tailwind/index.tsx +0 -0
  151. /package/{components/neon → neon}/neon-divider/css/index.tsx +0 -0
  152. /package/{components/neon → neon}/neon-divider/css/neon-divider.module.css +0 -0
  153. /package/{components/neon → neon}/neon-divider/tailwind/index.tsx +0 -0
  154. /package/{components/neon → neon}/neon-progress/css/index.tsx +0 -0
  155. /package/{components/neon → neon}/neon-progress/css/neon-progress.module.css +0 -0
  156. /package/{components/neon → neon}/neon-progress/tailwind/index.tsx +0 -0
  157. /package/{components/neon → neon}/neon-tabs/css/index.tsx +0 -0
  158. /package/{components/neon → neon}/neon-tabs/css/neon-tabs.module.css +0 -0
  159. /package/{components/neon → neon}/neon-tabs/tailwind/index.tsx +0 -0
  160. /package/{components/neon → neon}/neon-toggle/css/index.tsx +0 -0
  161. /package/{components/neon → neon}/neon-toggle/css/neon-toggle.module.css +0 -0
  162. /package/{components/neon → neon}/neon-toggle/tailwind/index.tsx +0 -0
  163. /package/{components/overlays → overlays}/noise-overlay/index.tsx +0 -0
  164. /package/{components/overlays → overlays}/noise-overlay/noise-overlay.module.css +0 -0
  165. /package/{components/overlays → overlays}/scanlines/index.tsx +0 -0
  166. /package/{components/overlays → overlays}/scanlines/scanlines.module.css +0 -0
  167. /package/{components/overlays → overlays}/static-flicker/index.tsx +0 -0
  168. /package/{components/overlays → overlays}/static-flicker/static-flicker.module.css +0 -0
  169. /package/{components/overlays → overlays}/vignette/index.tsx +0 -0
  170. /package/{components/overlays → overlays}/vignette/vignette.module.css +0 -0
  171. /package/{components/text → text}/distortion-text/distortion-text.module.css +0 -0
  172. /package/{components/text → text}/distortion-text/index.tsx +0 -0
  173. /package/{components/text → text}/falling-text/falling-text.module.css +0 -0
  174. /package/{components/text → text}/falling-text/index.tsx +0 -0
  175. /package/{components/text → text}/flicker-text/flicker-text.module.css +0 -0
  176. /package/{components/text → text}/flicker-text/index.tsx +0 -0
  177. /package/{components/text → text}/glitch-text/glitch-text.module.css +0 -0
  178. /package/{components/text → text}/glitch-text/index.tsx +0 -0
@@ -0,0 +1,180 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, useEffect, useState, useRef } from 'react';
4
+ import styles from './reveal-text.module.css';
5
+
6
+ export interface RevealTextProps extends HTMLAttributes<HTMLDivElement> {
7
+ /** Text to reveal */
8
+ children: string;
9
+ /** Split mode */
10
+ splitBy?: 'word' | 'char' | 'line';
11
+ /** Reveal direction */
12
+ direction?: 'fromBottom' | 'fromTop' | 'fromLeft' | 'fromRight';
13
+ /** Additional effect */
14
+ effect?: 'none' | 'blur' | 'scale' | 'rotate';
15
+ /** Stagger delay between elements in ms */
16
+ stagger?: number;
17
+ /** Animation speed */
18
+ speed?: 'fast' | 'normal' | 'slow';
19
+ /** Trigger threshold (0-1) */
20
+ threshold?: number;
21
+ /** Only animate once */
22
+ once?: boolean;
23
+ /** Show highlight underline */
24
+ highlight?: boolean;
25
+ /** Highlight color */
26
+ highlightColor?: string;
27
+ /** Trigger immediately without scroll */
28
+ immediate?: boolean;
29
+ }
30
+
31
+ export const RevealText = forwardRef<HTMLDivElement, RevealTextProps>(
32
+ (
33
+ {
34
+ children,
35
+ splitBy = 'word',
36
+ direction = 'fromBottom',
37
+ effect = 'none',
38
+ stagger = 50,
39
+ speed = 'normal',
40
+ threshold = 0.2,
41
+ once = true,
42
+ highlight = false,
43
+ highlightColor = '#ff0040',
44
+ immediate = false,
45
+ className,
46
+ style,
47
+ ...props
48
+ },
49
+ ref
50
+ ) => {
51
+ const [visibleIndices, setVisibleIndices] = useState<Set<number>>(new Set());
52
+ const containerRef = useRef<HTMLDivElement>(null);
53
+ const hasAnimated = useRef(false);
54
+
55
+ const elements = splitBy === 'line'
56
+ ? children.split('\n')
57
+ : splitBy === 'char'
58
+ ? children.split('')
59
+ : children.split(' ');
60
+
61
+ useEffect(() => {
62
+ if (immediate) {
63
+ elements.forEach((_, i) => {
64
+ setTimeout(() => {
65
+ setVisibleIndices(prev => new Set(prev).add(i));
66
+ }, i * stagger);
67
+ });
68
+ return;
69
+ }
70
+
71
+ const observer = new IntersectionObserver(
72
+ ([entry]) => {
73
+ if (entry.isIntersecting && (!once || !hasAnimated.current)) {
74
+ hasAnimated.current = true;
75
+ elements.forEach((_, i) => {
76
+ setTimeout(() => {
77
+ setVisibleIndices(prev => new Set(prev).add(i));
78
+ }, i * stagger);
79
+ });
80
+ } else if (!entry.isIntersecting && !once) {
81
+ setVisibleIndices(new Set());
82
+ }
83
+ },
84
+ { threshold }
85
+ );
86
+
87
+ if (containerRef.current) {
88
+ observer.observe(containerRef.current);
89
+ }
90
+
91
+ return () => observer.disconnect();
92
+ }, [elements, stagger, threshold, once, immediate]);
93
+
94
+ const containerClasses = [
95
+ styles.container,
96
+ styles[speed],
97
+ className
98
+ ].filter(Boolean).join(' ');
99
+
100
+ const renderWord = (word: string, index: number) => {
101
+ const isVisible = visibleIndices.has(index);
102
+ const wordClasses = [
103
+ styles.word,
104
+ styles[direction],
105
+ effect !== 'none' && styles[effect],
106
+ isVisible && styles.visible,
107
+ highlight && styles.highlight
108
+ ].filter(Boolean).join(' ');
109
+
110
+ return (
111
+ <span
112
+ key={index}
113
+ className={wordClasses}
114
+ style={{
115
+ transitionDelay: `${index * stagger}ms`,
116
+ '--highlight-color': highlightColor
117
+ } as React.CSSProperties}
118
+ >
119
+ <span className={styles.wordInner}>{word}</span>
120
+ </span>
121
+ );
122
+ };
123
+
124
+ const renderChar = (char: string, index: number) => {
125
+ const isVisible = visibleIndices.has(index);
126
+ return (
127
+ <span
128
+ key={index}
129
+ className={`${styles.char} ${isVisible ? styles.visible : ''}`}
130
+ style={{ transitionDelay: `${index * stagger}ms` }}
131
+ >
132
+ {char === ' ' ? '\u00A0' : char}
133
+ </span>
134
+ );
135
+ };
136
+
137
+ const renderLine = (line: string, index: number) => {
138
+ const isVisible = visibleIndices.has(index);
139
+ const lineClasses = [
140
+ styles.line,
141
+ styles[direction],
142
+ isVisible && styles.visible
143
+ ].filter(Boolean).join(' ');
144
+
145
+ return (
146
+ <span
147
+ key={index}
148
+ className={lineClasses}
149
+ style={{ transitionDelay: `${index * stagger}ms` }}
150
+ >
151
+ <span className={styles.lineInner}>{line}</span>
152
+ </span>
153
+ );
154
+ };
155
+
156
+ return (
157
+ <div
158
+ ref={(node) => {
159
+ (containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
160
+ if (typeof ref === 'function') ref(node);
161
+ else if (ref) ref.current = node;
162
+ }}
163
+ className={containerClasses}
164
+ style={style}
165
+ {...props}
166
+ >
167
+ {elements.map((el, i) =>
168
+ splitBy === 'line'
169
+ ? renderLine(el, i)
170
+ : splitBy === 'char'
171
+ ? renderChar(el, i)
172
+ : renderWord(el, i)
173
+ )}
174
+ </div>
175
+ );
176
+ }
177
+ );
178
+
179
+ RevealText.displayName = 'RevealText';
180
+ export default RevealText;
@@ -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;