@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,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;