@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,155 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, useEffect, useState, useRef } from 'react';
4
+
5
+ export interface CountdownDisplayProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
6
+ target: Date | number;
7
+ format?: 'full' | 'hms' | 'ms' | 'dhms';
8
+ size?: 'sm' | 'md' | 'lg';
9
+ variant?: 'default' | 'minimal' | 'neon' | 'brutal' | 'glitch';
10
+ accentColor?: string;
11
+ showLabels?: boolean;
12
+ compact?: boolean;
13
+ flip?: boolean;
14
+ urgentThreshold?: number;
15
+ onComplete?: () => void;
16
+ labels?: { days?: string; hours?: string; minutes?: string; seconds?: string; };
17
+ }
18
+
19
+ interface TimeLeft { days: number; hours: number; minutes: number; seconds: number; }
20
+
21
+ const sizeClasses = {
22
+ sm: { value: 'text-3xl md:text-5xl', separator: 'text-xl md:text-3xl', label: 'text-[0.5rem]' },
23
+ md: { value: 'text-5xl md:text-7xl', separator: 'text-3xl md:text-5xl', label: 'text-[0.6rem]' },
24
+ lg: { value: 'text-7xl md:text-9xl', separator: 'text-5xl md:text-7xl', label: 'text-[0.7rem]' },
25
+ };
26
+
27
+ export const CountdownDisplay = forwardRef<HTMLDivElement, CountdownDisplayProps>(
28
+ (
29
+ {
30
+ target,
31
+ format = 'hms',
32
+ size = 'md',
33
+ variant = 'default',
34
+ accentColor = '#ff0040',
35
+ showLabels = true,
36
+ compact = false,
37
+ flip = false,
38
+ urgentThreshold = 60,
39
+ onComplete,
40
+ labels = {},
41
+ className = '',
42
+ ...props
43
+ },
44
+ ref
45
+ ) => {
46
+ const [timeLeft, setTimeLeft] = useState<TimeLeft>({ days: 0, hours: 0, minutes: 0, seconds: 0 });
47
+ const [isUrgent, setIsUrgent] = useState(false);
48
+ const completedRef = useRef(false);
49
+
50
+ const defaultLabels = {
51
+ days: labels.days || 'DAYS', hours: labels.hours || 'HOURS',
52
+ minutes: labels.minutes || 'MIN', seconds: labels.seconds || 'SEC',
53
+ };
54
+
55
+ useEffect(() => {
56
+ const calculateTimeLeft = (): TimeLeft => {
57
+ let totalSeconds: number;
58
+ if (target instanceof Date) {
59
+ totalSeconds = Math.max(0, Math.floor((target.getTime() - Date.now()) / 1000));
60
+ } else {
61
+ totalSeconds = Math.max(0, target);
62
+ }
63
+ return {
64
+ days: Math.floor(totalSeconds / 86400),
65
+ hours: Math.floor((totalSeconds % 86400) / 3600),
66
+ minutes: Math.floor((totalSeconds % 3600) / 60),
67
+ seconds: totalSeconds % 60,
68
+ };
69
+ };
70
+
71
+ const tick = () => {
72
+ const newTime = calculateTimeLeft();
73
+ const totalSeconds = newTime.days * 86400 + newTime.hours * 3600 + newTime.minutes * 60 + newTime.seconds;
74
+ setTimeLeft(newTime);
75
+ setIsUrgent(totalSeconds <= urgentThreshold && totalSeconds > 0);
76
+ if (totalSeconds === 0 && !completedRef.current) {
77
+ completedRef.current = true;
78
+ onComplete?.();
79
+ }
80
+ };
81
+
82
+ tick();
83
+ const interval = setInterval(tick, 1000);
84
+ return () => clearInterval(interval);
85
+ }, [target, urgentThreshold, onComplete]);
86
+
87
+ const pad = (num: number) => String(num).padStart(2, '0');
88
+ const { value: valueClass, separator: sepClass, label: labelClass } = sizeClasses[size];
89
+
90
+ const getValueStyle = () => {
91
+ switch (variant) {
92
+ case 'neon':
93
+ return { color: accentColor, textShadow: `0 0 10px ${accentColor}, 0 0 20px ${accentColor}, 0 0 40px ${accentColor}` };
94
+ case 'brutal':
95
+ return { background: accentColor, color: '#0a0a0a', padding: '0 0.25em', WebkitTextFillColor: '#0a0a0a' };
96
+ case 'minimal':
97
+ return { color: '#fafafa' };
98
+ default:
99
+ return { background: 'linear-gradient(180deg, #fafafa 0%, #888 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' };
100
+ }
101
+ };
102
+
103
+ const renderBlock = (value: number, label: string) => (
104
+ <div className="flex flex-col items-center relative">
105
+ <span
106
+ className={`${valueClass} font-extrabold leading-none tracking-tight ${isUrgent ? 'animate-pulse' : ''}`}
107
+ style={getValueStyle()}
108
+ >
109
+ {pad(value)}
110
+ </span>
111
+ {showLabels && (
112
+ <span className={`${labelClass} tracking-[0.3em] text-gray-600 mt-2 uppercase font-normal`}>
113
+ {label}
114
+ </span>
115
+ )}
116
+ </div>
117
+ );
118
+
119
+ const renderSeparator = (key: number) => (
120
+ <span
121
+ key={`sep-${key}`}
122
+ className={`${sepClass} font-extrabold text-gray-700 animate-pulse self-start mt-2`}
123
+ style={variant === 'neon' ? { color: accentColor, textShadow: `0 0 10px ${accentColor}` } : undefined}
124
+ >
125
+ :
126
+ </span>
127
+ );
128
+
129
+ const blocks = [];
130
+ if (format === 'dhms' || format === 'full') {
131
+ blocks.push(<div key="days">{renderBlock(timeLeft.days, defaultLabels.days)}</div>);
132
+ blocks.push(renderSeparator(1));
133
+ }
134
+ if (format !== 'ms') {
135
+ blocks.push(<div key="hours">{renderBlock(timeLeft.hours, defaultLabels.hours)}</div>);
136
+ blocks.push(renderSeparator(2));
137
+ }
138
+ blocks.push(<div key="minutes">{renderBlock(timeLeft.minutes, defaultLabels.minutes)}</div>);
139
+ blocks.push(renderSeparator(3));
140
+ blocks.push(<div key="seconds">{renderBlock(timeLeft.seconds, defaultLabels.seconds)}</div>);
141
+
142
+ return (
143
+ <div
144
+ ref={ref}
145
+ className={`flex items-center justify-center ${compact ? 'gap-2' : 'gap-4 md:gap-8'} font-sans ${className}`}
146
+ {...props}
147
+ >
148
+ {blocks}
149
+ </div>
150
+ );
151
+ }
152
+ );
153
+
154
+ CountdownDisplay.displayName = 'CountdownDisplay';
155
+ export default CountdownDisplay;
@@ -0,0 +1,156 @@
1
+ .container {
2
+ position: relative;
3
+ display: inline-block;
4
+ font-weight: 800;
5
+ line-height: 1;
6
+ letter-spacing: -0.02em;
7
+ }
8
+
9
+ .layer {
10
+ display: block;
11
+ position: relative;
12
+ }
13
+
14
+ .layer:not(:first-child) {
15
+ position: absolute;
16
+ top: 0;
17
+ left: 0;
18
+ pointer-events: none;
19
+ }
20
+
21
+ /* Base layer styling */
22
+ .base {
23
+ background: linear-gradient(180deg, #fafafa 0%, #888 100%);
24
+ -webkit-background-clip: text;
25
+ -webkit-text-fill-color: transparent;
26
+ background-clip: text;
27
+ }
28
+
29
+ /* Shadow layers */
30
+ .shadow1 {
31
+ transform: translate(4px, 4px);
32
+ z-index: -1;
33
+ opacity: 0.5;
34
+ background: linear-gradient(180deg, #ff0040 0%, #ff004080 100%);
35
+ -webkit-background-clip: text;
36
+ -webkit-text-fill-color: transparent;
37
+ background-clip: text;
38
+ }
39
+
40
+ .shadow2 {
41
+ transform: translate(8px, 8px);
42
+ z-index: -2;
43
+ opacity: 0.3;
44
+ background: linear-gradient(180deg, #ff0040 0%, #ff004040 100%);
45
+ -webkit-background-clip: text;
46
+ -webkit-text-fill-color: transparent;
47
+ background-clip: text;
48
+ }
49
+
50
+ .shadow3 {
51
+ transform: translate(12px, 12px);
52
+ z-index: -3;
53
+ opacity: 0.15;
54
+ background: linear-gradient(180deg, #ff0040 0%, #ff004020 100%);
55
+ -webkit-background-clip: text;
56
+ -webkit-text-fill-color: transparent;
57
+ background-clip: text;
58
+ }
59
+
60
+ /* Variants */
61
+ .blood .shadow1,
62
+ .blood .shadow2,
63
+ .blood .shadow3 {
64
+ background: linear-gradient(180deg, #ff0040 0%, transparent 100%);
65
+ -webkit-background-clip: text;
66
+ background-clip: text;
67
+ }
68
+
69
+ .cyber .shadow1 { background: linear-gradient(180deg, #00ffff 0%, transparent 100%); -webkit-background-clip: text; background-clip: text; }
70
+ .cyber .shadow2 { background: linear-gradient(180deg, #ff00ff 0%, transparent 100%); -webkit-background-clip: text; background-clip: text; }
71
+ .cyber .shadow3 { background: linear-gradient(180deg, #00ff00 0%, transparent 100%); -webkit-background-clip: text; background-clip: text; }
72
+
73
+ .mono .base,
74
+ .mono .shadow1,
75
+ .mono .shadow2,
76
+ .mono .shadow3 {
77
+ background: none;
78
+ -webkit-text-fill-color: currentColor;
79
+ }
80
+ .mono .shadow1 { color: #666; }
81
+ .mono .shadow2 { color: #444; }
82
+ .mono .shadow3 { color: #222; }
83
+
84
+ .neon .base {
85
+ color: #fff;
86
+ -webkit-text-fill-color: #fff;
87
+ text-shadow: 0 0 10px #ff0040, 0 0 20px #ff0040, 0 0 40px #ff0040;
88
+ }
89
+ .neon .shadow1,
90
+ .neon .shadow2,
91
+ .neon .shadow3 {
92
+ opacity: 0;
93
+ }
94
+
95
+ /* Animated variant */
96
+ .animated .shadow1 {
97
+ animation: layerFloat1 3s ease-in-out infinite;
98
+ }
99
+
100
+ .animated .shadow2 {
101
+ animation: layerFloat2 4s ease-in-out infinite;
102
+ }
103
+
104
+ .animated .shadow3 {
105
+ animation: layerFloat3 5s ease-in-out infinite;
106
+ }
107
+
108
+ @keyframes layerFloat1 {
109
+ 0%, 100% { transform: translate(4px, 4px); }
110
+ 50% { transform: translate(6px, 6px); }
111
+ }
112
+
113
+ @keyframes layerFloat2 {
114
+ 0%, 100% { transform: translate(8px, 8px); }
115
+ 50% { transform: translate(12px, 10px); }
116
+ }
117
+
118
+ @keyframes layerFloat3 {
119
+ 0%, 100% { transform: translate(12px, 12px); }
120
+ 50% { transform: translate(16px, 14px); }
121
+ }
122
+
123
+ /* Hover effect */
124
+ .hover:hover .shadow1 {
125
+ transform: translate(8px, 8px);
126
+ transition: transform 0.3s ease;
127
+ }
128
+
129
+ .hover:hover .shadow2 {
130
+ transform: translate(16px, 16px);
131
+ transition: transform 0.3s ease;
132
+ }
133
+
134
+ .hover:hover .shadow3 {
135
+ transform: translate(24px, 24px);
136
+ transition: transform 0.3s ease;
137
+ }
138
+
139
+ /* Sizes */
140
+ .sm { font-size: clamp(2rem, 8vw, 4rem); }
141
+ .md { font-size: clamp(4rem, 15vw, 8rem); }
142
+ .lg { font-size: clamp(6rem, 20vw, 12rem); }
143
+ .xl { font-size: clamp(8rem, 25vw, 18rem); }
144
+
145
+ /* Offset directions */
146
+ .diagonal .shadow1 { transform: translate(4px, 4px); }
147
+ .diagonal .shadow2 { transform: translate(8px, 8px); }
148
+ .diagonal .shadow3 { transform: translate(12px, 12px); }
149
+
150
+ .horizontal .shadow1 { transform: translateX(4px); }
151
+ .horizontal .shadow2 { transform: translateX(8px); }
152
+ .horizontal .shadow3 { transform: translateX(12px); }
153
+
154
+ .vertical .shadow1 { transform: translateY(4px); }
155
+ .vertical .shadow2 { transform: translateY(8px); }
156
+ .vertical .shadow3 { transform: translateY(12px); }
@@ -0,0 +1,97 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes } from 'react';
4
+ import styles from './giant-layers.module.css';
5
+
6
+ export interface GiantLayersProps extends HTMLAttributes<HTMLSpanElement> {
7
+ /** Text to display */
8
+ children: string;
9
+ /** Number of shadow layers (1-3) */
10
+ layers?: 1 | 2 | 3;
11
+ /** Size preset */
12
+ size?: 'sm' | 'md' | 'lg' | 'xl';
13
+ /** Visual variant */
14
+ variant?: 'blood' | 'cyber' | 'mono' | 'neon';
15
+ /** Shadow offset direction */
16
+ direction?: 'diagonal' | 'horizontal' | 'vertical';
17
+ /** Animate layers */
18
+ animated?: boolean;
19
+ /** Expand on hover */
20
+ hover?: boolean;
21
+ /** Custom layer colors */
22
+ layerColors?: [string, string?, string?];
23
+ }
24
+
25
+ export const GiantLayers = forwardRef<HTMLSpanElement, GiantLayersProps>(
26
+ (
27
+ {
28
+ children,
29
+ layers = 3,
30
+ size = 'lg',
31
+ variant = 'blood',
32
+ direction = 'diagonal',
33
+ animated = false,
34
+ hover = false,
35
+ layerColors,
36
+ className,
37
+ style,
38
+ ...props
39
+ },
40
+ ref
41
+ ) => {
42
+ const containerClasses = [
43
+ styles.container,
44
+ styles[size],
45
+ styles[variant],
46
+ styles[direction],
47
+ animated && styles.animated,
48
+ hover && styles.hover,
49
+ className
50
+ ].filter(Boolean).join(' ');
51
+
52
+ const getLayerStyle = (index: number) => {
53
+ if (layerColors && layerColors[index]) {
54
+ return {
55
+ background: `linear-gradient(180deg, ${layerColors[index]} 0%, transparent 100%)`,
56
+ WebkitBackgroundClip: 'text',
57
+ WebkitTextFillColor: 'transparent',
58
+ backgroundClip: 'text',
59
+ };
60
+ }
61
+ return undefined;
62
+ };
63
+
64
+ return (
65
+ <span
66
+ ref={ref}
67
+ className={containerClasses}
68
+ style={style}
69
+ {...props}
70
+ >
71
+ {/* Shadow layers */}
72
+ {layers >= 3 && (
73
+ <span className={`${styles.layer} ${styles.shadow3}`} style={getLayerStyle(2)} aria-hidden>
74
+ {children}
75
+ </span>
76
+ )}
77
+ {layers >= 2 && (
78
+ <span className={`${styles.layer} ${styles.shadow2}`} style={getLayerStyle(1)} aria-hidden>
79
+ {children}
80
+ </span>
81
+ )}
82
+ {layers >= 1 && (
83
+ <span className={`${styles.layer} ${styles.shadow1}`} style={getLayerStyle(0)} aria-hidden>
84
+ {children}
85
+ </span>
86
+ )}
87
+ {/* Base layer */}
88
+ <span className={`${styles.layer} ${styles.base}`}>
89
+ {children}
90
+ </span>
91
+ </span>
92
+ );
93
+ }
94
+ );
95
+
96
+ GiantLayers.displayName = 'GiantLayers';
97
+ export default GiantLayers;
@@ -0,0 +1,111 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes } from 'react';
4
+
5
+ export interface GiantLayersProps extends HTMLAttributes<HTMLSpanElement> {
6
+ children: string;
7
+ layers?: 1 | 2 | 3;
8
+ size?: 'sm' | 'md' | 'lg' | 'xl';
9
+ variant?: 'blood' | 'cyber' | 'mono' | 'neon';
10
+ direction?: 'diagonal' | 'horizontal' | 'vertical';
11
+ animated?: boolean;
12
+ hover?: boolean;
13
+ layerColors?: [string, string?, string?];
14
+ }
15
+
16
+ const sizeClasses = {
17
+ sm: 'text-4xl md:text-6xl',
18
+ md: 'text-6xl md:text-8xl',
19
+ lg: 'text-7xl md:text-9xl',
20
+ xl: 'text-8xl md:text-[12rem]',
21
+ };
22
+
23
+ const variantStyles = {
24
+ blood: {
25
+ base: 'bg-gradient-to-b from-white to-gray-500 bg-clip-text text-transparent',
26
+ shadow: 'text-rose-500',
27
+ },
28
+ cyber: {
29
+ base: 'bg-gradient-to-b from-white to-gray-500 bg-clip-text text-transparent',
30
+ shadow: 'text-cyan-400',
31
+ },
32
+ mono: {
33
+ base: 'text-white',
34
+ shadow: 'text-gray-600',
35
+ },
36
+ neon: {
37
+ base: 'text-white drop-shadow-[0_0_10px_#ff0040] drop-shadow-[0_0_20px_#ff0040]',
38
+ shadow: 'text-transparent',
39
+ },
40
+ };
41
+
42
+ const getOffset = (direction: string, layer: number) => {
43
+ const px = (layer + 1) * 4;
44
+ switch (direction) {
45
+ case 'horizontal': return { transform: `translateX(${px}px)` };
46
+ case 'vertical': return { transform: `translateY(${px}px)` };
47
+ default: return { transform: `translate(${px}px, ${px}px)` };
48
+ }
49
+ };
50
+
51
+ export const GiantLayers = forwardRef<HTMLSpanElement, GiantLayersProps>(
52
+ (
53
+ {
54
+ children,
55
+ layers = 3,
56
+ size = 'lg',
57
+ variant = 'blood',
58
+ direction = 'diagonal',
59
+ animated = false,
60
+ hover = false,
61
+ layerColors,
62
+ className = '',
63
+ ...props
64
+ },
65
+ ref
66
+ ) => {
67
+ const { base, shadow } = variantStyles[variant];
68
+ const opacities = [0.5, 0.3, 0.15];
69
+
70
+ return (
71
+ <span
72
+ ref={ref}
73
+ className={`relative inline-block font-extrabold leading-none tracking-tight ${sizeClasses[size]} ${
74
+ hover ? 'group' : ''
75
+ } ${className}`}
76
+ {...props}
77
+ >
78
+ {/* Shadow layers */}
79
+ {[...Array(layers)].map((_, i) => {
80
+ const layerIndex = layers - 1 - i;
81
+ const colorStyle = layerColors?.[layerIndex]
82
+ ? { color: layerColors[layerIndex] }
83
+ : undefined;
84
+
85
+ return (
86
+ <span
87
+ key={i}
88
+ className={`absolute top-0 left-0 pointer-events-none ${shadow} ${
89
+ animated ? 'animate-pulse' : ''
90
+ } ${hover ? 'transition-transform duration-300 group-hover:translate-x-6 group-hover:translate-y-6' : ''}`}
91
+ style={{
92
+ ...getOffset(direction, layerIndex),
93
+ opacity: opacities[layerIndex],
94
+ zIndex: -(layerIndex + 1),
95
+ ...colorStyle,
96
+ }}
97
+ aria-hidden
98
+ >
99
+ {children}
100
+ </span>
101
+ );
102
+ })}
103
+ {/* Base layer */}
104
+ <span className={`relative ${base}`}>{children}</span>
105
+ </span>
106
+ );
107
+ }
108
+ );
109
+
110
+ GiantLayers.displayName = 'GiantLayers';
111
+ export default GiantLayers;
@@ -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;