@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,153 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, useEffect, useState, useCallback, useRef } from 'react';
4
+ import styles from './char-glitch.module.css';
5
+
6
+ export interface CharGlitchProps extends HTMLAttributes<HTMLSpanElement> {
7
+ /** Text to display */
8
+ children: string;
9
+ /** Glitch intensity */
10
+ intensity?: 'subtle' | 'medium' | 'intense';
11
+ /** Visual variant */
12
+ variant?: 'blood' | 'cyber' | 'matrix' | 'corrupt';
13
+ /** Trigger mode */
14
+ mode?: 'random' | 'hover' | 'continuous' | 'wave';
15
+ /** Interval between random glitches in ms */
16
+ interval?: number;
17
+ /** Characters to use for scramble effect */
18
+ glitchChars?: string;
19
+ /** Enable scramble reveal effect */
20
+ scramble?: boolean;
21
+ }
22
+
23
+ const GLITCH_CHARS = '!@#$%^&*()_+-=[]{}|;:,.<>?/~`0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
24
+
25
+ export const CharGlitch = forwardRef<HTMLSpanElement, CharGlitchProps>(
26
+ (
27
+ {
28
+ children,
29
+ intensity = 'medium',
30
+ variant = 'blood',
31
+ mode = 'random',
32
+ interval = 100,
33
+ glitchChars = GLITCH_CHARS,
34
+ scramble = false,
35
+ className,
36
+ ...props
37
+ },
38
+ ref
39
+ ) => {
40
+ const [glitchingIndices, setGlitchingIndices] = useState<Set<number>>(new Set());
41
+ const [scrambledChars, setScrambledChars] = useState<string[]>([]);
42
+ const [revealed, setRevealed] = useState<boolean[]>([]);
43
+ const intervalRef = useRef<NodeJS.Timeout>();
44
+
45
+ const triggerGlitch = useCallback((index: number) => {
46
+ setGlitchingIndices(prev => new Set(prev).add(index));
47
+ setTimeout(() => {
48
+ setGlitchingIndices(prev => {
49
+ const next = new Set(prev);
50
+ next.delete(index);
51
+ return next;
52
+ });
53
+ }, intensity === 'subtle' ? 300 : intensity === 'intense' ? 80 : 150);
54
+ }, [intensity]);
55
+
56
+ useEffect(() => {
57
+ if (scramble) {
58
+ setScrambledChars(children.split('').map(() =>
59
+ glitchChars[Math.floor(Math.random() * glitchChars.length)]
60
+ ));
61
+ setRevealed(new Array(children.length).fill(false));
62
+
63
+ let index = 0;
64
+ const revealInterval = setInterval(() => {
65
+ if (index < children.length) {
66
+ setRevealed(prev => {
67
+ const next = [...prev];
68
+ next[index] = true;
69
+ return next;
70
+ });
71
+ index++;
72
+ } else {
73
+ clearInterval(revealInterval);
74
+ }
75
+ }, 50);
76
+
77
+ return () => clearInterval(revealInterval);
78
+ }
79
+ }, [children, scramble, glitchChars]);
80
+
81
+ useEffect(() => {
82
+ if (mode === 'random' && !scramble) {
83
+ intervalRef.current = setInterval(() => {
84
+ const randomIndex = Math.floor(Math.random() * children.length);
85
+ triggerGlitch(randomIndex);
86
+ }, interval);
87
+
88
+ return () => {
89
+ if (intervalRef.current) clearInterval(intervalRef.current);
90
+ };
91
+ }
92
+
93
+ if (mode === 'continuous' && !scramble) {
94
+ children.split('').forEach((_, i) => {
95
+ setTimeout(() => triggerGlitch(i), i * 50);
96
+ });
97
+
98
+ intervalRef.current = setInterval(() => {
99
+ children.split('').forEach((_, i) => {
100
+ setTimeout(() => triggerGlitch(i), i * 50);
101
+ });
102
+ }, interval * children.length);
103
+
104
+ return () => {
105
+ if (intervalRef.current) clearInterval(intervalRef.current);
106
+ };
107
+ }
108
+
109
+ if (mode === 'wave' && !scramble) {
110
+ let waveIndex = 0;
111
+ intervalRef.current = setInterval(() => {
112
+ triggerGlitch(waveIndex % children.length);
113
+ waveIndex++;
114
+ }, interval);
115
+
116
+ return () => {
117
+ if (intervalRef.current) clearInterval(intervalRef.current);
118
+ };
119
+ }
120
+ }, [mode, interval, children, triggerGlitch, scramble]);
121
+
122
+ const containerClasses = [
123
+ styles.container,
124
+ styles[intensity],
125
+ styles[variant],
126
+ mode === 'hover' && styles.hover,
127
+ scramble && styles.scramble,
128
+ className
129
+ ].filter(Boolean).join(' ');
130
+
131
+ return (
132
+ <span ref={ref} className={containerClasses} {...props}>
133
+ {children.split('').map((char, i) => {
134
+ const isGlitching = glitchingIndices.has(i);
135
+ const displayChar = scramble && !revealed[i] ? scrambledChars[i] : char;
136
+
137
+ return (
138
+ <span
139
+ key={i}
140
+ className={`${styles.char} ${isGlitching ? styles.glitching : ''} ${scramble && !revealed[i] ? styles.scrambling : ''}`}
141
+ data-char={displayChar}
142
+ >
143
+ {displayChar}
144
+ </span>
145
+ );
146
+ })}
147
+ </span>
148
+ );
149
+ }
150
+ );
151
+
152
+ CharGlitch.displayName = 'CharGlitch';
153
+ export default CharGlitch;
@@ -0,0 +1,126 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, useEffect, useState, useCallback, useRef } from 'react';
4
+
5
+ export interface CharGlitchProps extends HTMLAttributes<HTMLSpanElement> {
6
+ children: string;
7
+ intensity?: 'subtle' | 'medium' | 'intense';
8
+ variant?: 'blood' | 'cyber' | 'matrix' | 'corrupt';
9
+ mode?: 'random' | 'hover' | 'continuous' | 'wave';
10
+ interval?: number;
11
+ glitchChars?: string;
12
+ scramble?: boolean;
13
+ }
14
+
15
+ const GLITCH_CHARS = '!@#$%^&*()_+-=[]{}|;:,.<>?/~`0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
16
+
17
+ const variantColors = {
18
+ blood: { primary: 'text-rose-500', shadow: 'drop-shadow-[0_0_2px_#ff0040]' },
19
+ cyber: { primary: 'text-cyan-400', shadow: 'drop-shadow-[0_0_2px_#00ffff]' },
20
+ matrix: { primary: 'text-green-400', shadow: 'drop-shadow-[0_0_2px_#00ff00]' },
21
+ corrupt: { primary: 'text-white', shadow: 'drop-shadow-[0_0_2px_#ffffff]' },
22
+ };
23
+
24
+ export const CharGlitch = forwardRef<HTMLSpanElement, CharGlitchProps>(
25
+ (
26
+ {
27
+ children,
28
+ intensity = 'medium',
29
+ variant = 'blood',
30
+ mode = 'random',
31
+ interval = 100,
32
+ glitchChars = GLITCH_CHARS,
33
+ scramble = false,
34
+ className = '',
35
+ ...props
36
+ },
37
+ ref
38
+ ) => {
39
+ const [glitchingIndices, setGlitchingIndices] = useState<Set<number>>(new Set());
40
+ const [scrambledChars, setScrambledChars] = useState<string[]>([]);
41
+ const [revealed, setRevealed] = useState<boolean[]>([]);
42
+ const intervalRef = useRef<NodeJS.Timeout>();
43
+
44
+ const durations = { subtle: 300, medium: 150, intense: 80 };
45
+
46
+ const triggerGlitch = useCallback((index: number) => {
47
+ setGlitchingIndices(prev => new Set(prev).add(index));
48
+ setTimeout(() => {
49
+ setGlitchingIndices(prev => {
50
+ const next = new Set(prev);
51
+ next.delete(index);
52
+ return next;
53
+ });
54
+ }, durations[intensity]);
55
+ }, [intensity]);
56
+
57
+ useEffect(() => {
58
+ if (scramble) {
59
+ setScrambledChars(children.split('').map(() =>
60
+ glitchChars[Math.floor(Math.random() * glitchChars.length)]
61
+ ));
62
+ setRevealed(new Array(children.length).fill(false));
63
+
64
+ let index = 0;
65
+ const revealInterval = setInterval(() => {
66
+ if (index < children.length) {
67
+ setRevealed(prev => {
68
+ const next = [...prev];
69
+ next[index] = true;
70
+ return next;
71
+ });
72
+ index++;
73
+ } else {
74
+ clearInterval(revealInterval);
75
+ }
76
+ }, 50);
77
+
78
+ return () => clearInterval(revealInterval);
79
+ }
80
+ }, [children, scramble, glitchChars]);
81
+
82
+ useEffect(() => {
83
+ if (mode === 'random' && !scramble) {
84
+ intervalRef.current = setInterval(() => {
85
+ const randomIndex = Math.floor(Math.random() * children.length);
86
+ triggerGlitch(randomIndex);
87
+ }, interval);
88
+ return () => { if (intervalRef.current) clearInterval(intervalRef.current); };
89
+ }
90
+
91
+ if (mode === 'wave' && !scramble) {
92
+ let waveIndex = 0;
93
+ intervalRef.current = setInterval(() => {
94
+ triggerGlitch(waveIndex % children.length);
95
+ waveIndex++;
96
+ }, interval);
97
+ return () => { if (intervalRef.current) clearInterval(intervalRef.current); };
98
+ }
99
+ }, [mode, interval, children, triggerGlitch, scramble]);
100
+
101
+ return (
102
+ <span ref={ref} className={`inline-block ${className}`} {...props}>
103
+ {children.split('').map((char, i) => {
104
+ const isGlitching = glitchingIndices.has(i);
105
+ const displayChar = scramble && !revealed[i] ? scrambledChars[i] : char;
106
+ const { primary, shadow } = variantColors[variant];
107
+
108
+ return (
109
+ <span
110
+ key={i}
111
+ className={`inline-block relative transition-transform duration-100 ${
112
+ isGlitching ? `${primary} ${shadow} animate-pulse scale-110` : ''
113
+ } ${mode === 'hover' ? 'hover:animate-pulse hover:scale-110' : ''}`}
114
+ data-char={displayChar}
115
+ >
116
+ {displayChar}
117
+ </span>
118
+ );
119
+ })}
120
+ </span>
121
+ );
122
+ }
123
+ );
124
+
125
+ CharGlitch.displayName = 'CharGlitch';
126
+ export default CharGlitch;
@@ -0,0 +1,179 @@
1
+ .container {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ gap: 1rem;
6
+ font-family: 'Syne', 'Inter', sans-serif;
7
+ font-weight: 800;
8
+ }
9
+
10
+ .block {
11
+ display: flex;
12
+ flex-direction: column;
13
+ align-items: center;
14
+ position: relative;
15
+ }
16
+
17
+ .value {
18
+ font-size: clamp(3rem, 12vw, 8rem);
19
+ line-height: 1;
20
+ letter-spacing: -0.02em;
21
+ position: relative;
22
+ background: linear-gradient(180deg, #fafafa 0%, #888 100%);
23
+ -webkit-background-clip: text;
24
+ -webkit-text-fill-color: transparent;
25
+ background-clip: text;
26
+ }
27
+
28
+ /* 3D shadow effect */
29
+ .value::before {
30
+ content: attr(data-value);
31
+ position: absolute;
32
+ left: 3px;
33
+ top: 3px;
34
+ z-index: -1;
35
+ background: linear-gradient(180deg, var(--accent-color, #ff0040) 0%, transparent 100%);
36
+ -webkit-background-clip: text;
37
+ -webkit-text-fill-color: transparent;
38
+ background-clip: text;
39
+ opacity: 0.5;
40
+ }
41
+
42
+ .label {
43
+ font-size: 0.6rem;
44
+ letter-spacing: 0.3em;
45
+ color: #444;
46
+ margin-top: 0.5rem;
47
+ text-transform: uppercase;
48
+ font-weight: 400;
49
+ }
50
+
51
+ .separator {
52
+ font-size: clamp(2rem, 8vw, 5rem);
53
+ font-weight: 800;
54
+ color: #333;
55
+ animation: pulseSep 1s ease-in-out infinite;
56
+ align-self: flex-start;
57
+ margin-top: 0.5rem;
58
+ }
59
+
60
+ @keyframes pulseSep {
61
+ 0%, 100% { opacity: 1; }
62
+ 50% { opacity: 0.2; }
63
+ }
64
+
65
+ /* Size variants */
66
+ .sm .value { font-size: clamp(1.5rem, 6vw, 3rem); }
67
+ .sm .separator { font-size: clamp(1rem, 4vw, 2rem); }
68
+ .sm .label { font-size: 0.5rem; }
69
+
70
+ .lg .value { font-size: clamp(5rem, 18vw, 12rem); }
71
+ .lg .separator { font-size: clamp(3rem, 10vw, 7rem); }
72
+ .lg .label { font-size: 0.7rem; }
73
+
74
+ /* Variant: minimal */
75
+ .minimal .value {
76
+ background: none;
77
+ -webkit-text-fill-color: currentColor;
78
+ color: #fafafa;
79
+ }
80
+
81
+ .minimal .value::before {
82
+ display: none;
83
+ }
84
+
85
+ /* Variant: neon */
86
+ .neon .value {
87
+ background: none;
88
+ -webkit-text-fill-color: currentColor;
89
+ color: var(--accent-color, #ff0040);
90
+ text-shadow:
91
+ 0 0 10px var(--accent-color, #ff0040),
92
+ 0 0 20px var(--accent-color, #ff0040),
93
+ 0 0 40px var(--accent-color, #ff0040);
94
+ }
95
+
96
+ .neon .value::before {
97
+ display: none;
98
+ }
99
+
100
+ .neon .separator {
101
+ color: var(--accent-color, #ff0040);
102
+ text-shadow: 0 0 10px var(--accent-color, #ff0040);
103
+ }
104
+
105
+ /* Variant: brutal */
106
+ .brutal .value {
107
+ background: var(--accent-color, #ff0040);
108
+ -webkit-text-fill-color: #0a0a0a;
109
+ padding: 0 0.25em;
110
+ }
111
+
112
+ .brutal .value::before {
113
+ display: none;
114
+ }
115
+
116
+ /* Variant: glitch */
117
+ .glitch .value::after {
118
+ content: attr(data-value);
119
+ position: absolute;
120
+ left: 0;
121
+ top: 0;
122
+ z-index: -2;
123
+ background: linear-gradient(180deg, #00ffff 0%, transparent 100%);
124
+ -webkit-background-clip: text;
125
+ -webkit-text-fill-color: transparent;
126
+ background-clip: text;
127
+ opacity: 0;
128
+ animation: glitchOffset 3s infinite;
129
+ }
130
+
131
+ @keyframes glitchOffset {
132
+ 0%, 90%, 100% { opacity: 0; transform: translate(0); }
133
+ 92% { opacity: 0.8; transform: translate(-3px, 0); }
134
+ 94% { opacity: 0.8; transform: translate(3px, 0); }
135
+ }
136
+
137
+ /* Flip animation on change */
138
+ .flip .value {
139
+ perspective: 500px;
140
+ }
141
+
142
+ .flip .value.changing {
143
+ animation: flipValue 0.3s ease-in-out;
144
+ }
145
+
146
+ @keyframes flipValue {
147
+ 0% { transform: rotateX(0deg); }
148
+ 50% { transform: rotateX(-90deg); }
149
+ 100% { transform: rotateX(0deg); }
150
+ }
151
+
152
+ /* Urgency states */
153
+ .urgent .value {
154
+ animation: urgentPulse 0.5s ease-in-out infinite;
155
+ }
156
+
157
+ @keyframes urgentPulse {
158
+ 0%, 100% { transform: scale(1); }
159
+ 50% { transform: scale(1.02); }
160
+ }
161
+
162
+ .urgent .separator {
163
+ color: var(--accent-color, #ff0040);
164
+ animation: pulseSep 0.3s ease-in-out infinite;
165
+ }
166
+
167
+ /* Hide labels option */
168
+ .hideLabels .label {
169
+ display: none;
170
+ }
171
+
172
+ /* Compact mode */
173
+ .compact {
174
+ gap: 0.5rem;
175
+ }
176
+
177
+ .compact .separator {
178
+ margin: 0 0.25rem;
179
+ }
@@ -0,0 +1,190 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, useEffect, useState, useRef } from 'react';
4
+ import styles from './countdown-display.module.css';
5
+
6
+ export interface CountdownDisplayProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
7
+ /** Target date/time or duration in seconds */
8
+ target: Date | number;
9
+ /** Display format */
10
+ format?: 'full' | 'hms' | 'ms' | 'dhms';
11
+ /** Size variant */
12
+ size?: 'sm' | 'md' | 'lg';
13
+ /** Visual variant */
14
+ variant?: 'default' | 'minimal' | 'neon' | 'brutal' | 'glitch';
15
+ /** Accent color */
16
+ accentColor?: string;
17
+ /** Show labels under numbers */
18
+ showLabels?: boolean;
19
+ /** Compact mode (less spacing) */
20
+ compact?: boolean;
21
+ /** Enable flip animation */
22
+ flip?: boolean;
23
+ /** Urgent mode threshold (seconds) - when to show urgency effect */
24
+ urgentThreshold?: number;
25
+ /** Callback when countdown reaches zero */
26
+ onComplete?: () => void;
27
+ /** Labels customization */
28
+ labels?: {
29
+ days?: string;
30
+ hours?: string;
31
+ minutes?: string;
32
+ seconds?: string;
33
+ };
34
+ }
35
+
36
+ interface TimeLeft {
37
+ days: number;
38
+ hours: number;
39
+ minutes: number;
40
+ seconds: number;
41
+ }
42
+
43
+ export const CountdownDisplay = forwardRef<HTMLDivElement, CountdownDisplayProps>(
44
+ (
45
+ {
46
+ target,
47
+ format = 'hms',
48
+ size = 'md',
49
+ variant = 'default',
50
+ accentColor = '#ff0040',
51
+ showLabels = true,
52
+ compact = false,
53
+ flip = false,
54
+ urgentThreshold = 60,
55
+ onComplete,
56
+ labels = {},
57
+ className,
58
+ style,
59
+ ...props
60
+ },
61
+ ref
62
+ ) => {
63
+ const [timeLeft, setTimeLeft] = useState<TimeLeft>({ days: 0, hours: 0, minutes: 0, seconds: 0 });
64
+ const [isUrgent, setIsUrgent] = useState(false);
65
+ const [changing, setChanging] = useState<string[]>([]);
66
+ const prevValues = useRef<TimeLeft>({ days: 0, hours: 0, minutes: 0, seconds: 0 });
67
+ const completedRef = useRef(false);
68
+
69
+ const defaultLabels = {
70
+ days: labels.days || 'DAYS',
71
+ hours: labels.hours || 'HOURS',
72
+ minutes: labels.minutes || 'MIN',
73
+ seconds: labels.seconds || 'SEC',
74
+ };
75
+
76
+ useEffect(() => {
77
+ const calculateTimeLeft = (): TimeLeft => {
78
+ let totalSeconds: number;
79
+
80
+ if (target instanceof Date) {
81
+ totalSeconds = Math.max(0, Math.floor((target.getTime() - Date.now()) / 1000));
82
+ } else {
83
+ totalSeconds = Math.max(0, target);
84
+ }
85
+
86
+ return {
87
+ days: Math.floor(totalSeconds / (24 * 60 * 60)),
88
+ hours: Math.floor((totalSeconds % (24 * 60 * 60)) / (60 * 60)),
89
+ minutes: Math.floor((totalSeconds % (60 * 60)) / 60),
90
+ seconds: totalSeconds % 60,
91
+ };
92
+ };
93
+
94
+ const tick = () => {
95
+ const newTime = calculateTimeLeft();
96
+ const totalSeconds = newTime.days * 86400 + newTime.hours * 3600 + newTime.minutes * 60 + newTime.seconds;
97
+
98
+ // Detect changes for flip animation
99
+ if (flip) {
100
+ const changed: string[] = [];
101
+ if (newTime.days !== prevValues.current.days) changed.push('days');
102
+ if (newTime.hours !== prevValues.current.hours) changed.push('hours');
103
+ if (newTime.minutes !== prevValues.current.minutes) changed.push('minutes');
104
+ if (newTime.seconds !== prevValues.current.seconds) changed.push('seconds');
105
+ setChanging(changed);
106
+ setTimeout(() => setChanging([]), 300);
107
+ }
108
+
109
+ prevValues.current = newTime;
110
+ setTimeLeft(newTime);
111
+ setIsUrgent(totalSeconds <= urgentThreshold && totalSeconds > 0);
112
+
113
+ if (totalSeconds === 0 && !completedRef.current) {
114
+ completedRef.current = true;
115
+ onComplete?.();
116
+ }
117
+ };
118
+
119
+ tick();
120
+ const interval = setInterval(tick, 1000);
121
+ return () => clearInterval(interval);
122
+ }, [target, urgentThreshold, flip, onComplete]);
123
+
124
+ const pad = (num: number) => String(num).padStart(2, '0');
125
+
126
+ const containerClasses = [
127
+ styles.container,
128
+ styles[size],
129
+ styles[variant],
130
+ flip && styles.flip,
131
+ isUrgent && styles.urgent,
132
+ !showLabels && styles.hideLabels,
133
+ compact && styles.compact,
134
+ className
135
+ ].filter(Boolean).join(' ');
136
+
137
+ const renderBlock = (value: number, label: string, key: string) => (
138
+ <div className={styles.block} key={key}>
139
+ <span
140
+ className={`${styles.value} ${changing.includes(key) ? styles.changing : ''}`}
141
+ data-value={pad(value)}
142
+ >
143
+ {pad(value)}
144
+ </span>
145
+ {showLabels && <span className={styles.label}>{label}</span>}
146
+ </div>
147
+ );
148
+
149
+ const renderSeparator = (key: number) => (
150
+ <span className={styles.separator} key={`sep-${key}`}>:</span>
151
+ );
152
+
153
+ const renderBlocks = () => {
154
+ const blocks = [];
155
+
156
+ if (format === 'dhms' || format === 'full') {
157
+ blocks.push(renderBlock(timeLeft.days, defaultLabels.days, 'days'));
158
+ blocks.push(renderSeparator(1));
159
+ }
160
+
161
+ if (format !== 'ms') {
162
+ blocks.push(renderBlock(timeLeft.hours, defaultLabels.hours, 'hours'));
163
+ blocks.push(renderSeparator(2));
164
+ }
165
+
166
+ blocks.push(renderBlock(timeLeft.minutes, defaultLabels.minutes, 'minutes'));
167
+ blocks.push(renderSeparator(3));
168
+ blocks.push(renderBlock(timeLeft.seconds, defaultLabels.seconds, 'seconds'));
169
+
170
+ return blocks;
171
+ };
172
+
173
+ return (
174
+ <div
175
+ ref={ref}
176
+ className={containerClasses}
177
+ style={{
178
+ '--accent-color': accentColor,
179
+ ...style
180
+ } as React.CSSProperties}
181
+ {...props}
182
+ >
183
+ {renderBlocks()}
184
+ </div>
185
+ );
186
+ }
187
+ );
188
+
189
+ CountdownDisplay.displayName = 'CountdownDisplay';
190
+ export default CountdownDisplay;