@oalacea/chaosui 0.1.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 (45) hide show
  1. package/bin/cli.js +216 -0
  2. package/components/backgrounds/glow-orbs/glow-orbs.module.css +31 -0
  3. package/components/backgrounds/glow-orbs/index.tsx +87 -0
  4. package/components/backgrounds/light-beams/index.tsx +80 -0
  5. package/components/backgrounds/light-beams/light-beams.module.css +27 -0
  6. package/components/backgrounds/noise-canvas/index.tsx +113 -0
  7. package/components/backgrounds/noise-canvas/noise-canvas.module.css +8 -0
  8. package/components/backgrounds/particle-field/index.tsx +81 -0
  9. package/components/backgrounds/particle-field/particle-field.module.css +31 -0
  10. package/components/buttons/chaos-button/chaos-button.module.css +173 -0
  11. package/components/buttons/chaos-button/index.tsx +60 -0
  12. package/components/buttons/glitch-button/glitch-button.module.css +197 -0
  13. package/components/buttons/glitch-button/index.tsx +53 -0
  14. package/components/effects/cursor-follower/cursor-follower.module.css +50 -0
  15. package/components/effects/cursor-follower/index.tsx +83 -0
  16. package/components/effects/screen-distortion/index.tsx +54 -0
  17. package/components/effects/screen-distortion/screen-distortion.module.css +127 -0
  18. package/components/effects/warning-tape/index.tsx +64 -0
  19. package/components/effects/warning-tape/warning-tape.module.css +29 -0
  20. package/components/glow-orbs/glow-orbs.module.css +31 -0
  21. package/components/glow-orbs/index.tsx +87 -0
  22. package/components/light-beams/index.tsx +80 -0
  23. package/components/light-beams/light-beams.module.css +27 -0
  24. package/components/noise-canvas/index.tsx +113 -0
  25. package/components/noise-canvas/noise-canvas.module.css +8 -0
  26. package/components/overlays/noise-overlay/index.tsx +56 -0
  27. package/components/overlays/noise-overlay/noise-overlay.module.css +20 -0
  28. package/components/overlays/scanlines/index.tsx +61 -0
  29. package/components/overlays/scanlines/scanlines.module.css +16 -0
  30. package/components/overlays/static-flicker/index.tsx +51 -0
  31. package/components/overlays/static-flicker/static-flicker.module.css +27 -0
  32. package/components/overlays/vignette/index.tsx +58 -0
  33. package/components/overlays/vignette/vignette.module.css +7 -0
  34. package/components/package.json +13 -0
  35. package/components/particle-field/index.tsx +81 -0
  36. package/components/particle-field/particle-field.module.css +31 -0
  37. package/components/text/distortion-text/distortion-text.module.css +100 -0
  38. package/components/text/distortion-text/index.tsx +53 -0
  39. package/components/text/falling-text/falling-text.module.css +57 -0
  40. package/components/text/falling-text/index.tsx +61 -0
  41. package/components/text/flicker-text/flicker-text.module.css +91 -0
  42. package/components/text/flicker-text/index.tsx +48 -0
  43. package/components/text/glitch-text/glitch-text.module.css +142 -0
  44. package/components/text/glitch-text/index.tsx +53 -0
  45. package/package.json +38 -0
@@ -0,0 +1,173 @@
1
+ .button {
2
+ --accent: #ff0040;
3
+ position: relative;
4
+ padding: 1rem 2.5rem;
5
+ font-family: inherit;
6
+ font-size: 0.85rem;
7
+ font-weight: 600;
8
+ letter-spacing: 0.1em;
9
+ text-transform: uppercase;
10
+ border: none;
11
+ cursor: pointer;
12
+ overflow: visible;
13
+ transition: transform 0.1s;
14
+ }
15
+
16
+ .button:active {
17
+ transform: scale(0.98);
18
+ }
19
+
20
+ /* VARIANTS */
21
+ .solid {
22
+ background: var(--accent);
23
+ color: #000;
24
+ }
25
+
26
+ .outline {
27
+ background: transparent;
28
+ color: inherit;
29
+ border: 1px solid currentColor;
30
+ }
31
+
32
+ .broken {
33
+ background: transparent;
34
+ color: inherit;
35
+ border: 1px solid currentColor;
36
+ clip-path: polygon(0 0, 100% 0, 100% 30%, 95% 30%, 95% 70%, 100% 70%, 100% 100%, 0 100%);
37
+ }
38
+
39
+ /* DEBRIS */
40
+ .debris1, .debris2, .debris3, .debris4 {
41
+ position: absolute;
42
+ background: var(--accent);
43
+ pointer-events: none;
44
+ opacity: 0;
45
+ transition: opacity 0.3s;
46
+ }
47
+
48
+ .button:hover .debris1,
49
+ .button:hover .debris2,
50
+ .button:hover .debris3,
51
+ .button:hover .debris4 {
52
+ opacity: 1;
53
+ }
54
+
55
+ .debris1 {
56
+ width: 20px; height: 2px;
57
+ top: -6px; left: 15%;
58
+ transform: rotate(-5deg);
59
+ }
60
+
61
+ .debris2 {
62
+ width: 2px; height: 15px;
63
+ bottom: -10px; right: 20%;
64
+ }
65
+
66
+ .debris3 {
67
+ width: 15px; height: 1px;
68
+ top: 40%; right: -12px;
69
+ }
70
+
71
+ .debris4 {
72
+ width: 8px; height: 8px;
73
+ bottom: -5px; left: -5px;
74
+ background: transparent;
75
+ border: 1px solid var(--accent);
76
+ transform: rotate(45deg);
77
+ }
78
+
79
+ /* SLICE */
80
+ .slice {
81
+ position: absolute;
82
+ top: 50%;
83
+ left: -5%;
84
+ right: -5%;
85
+ height: 2px;
86
+ background: var(--accent);
87
+ opacity: 0;
88
+ transform: translateY(-50%);
89
+ }
90
+
91
+ .button:hover .slice {
92
+ animation: slice 0.3s steps(4) infinite;
93
+ }
94
+
95
+ @keyframes slice {
96
+ 0% { opacity: 0.8; clip-path: inset(0 80% 0 0); }
97
+ 25% { opacity: 0.6; clip-path: inset(0 30% 0 40%); }
98
+ 50% { opacity: 0.8; clip-path: inset(0 0 0 70%); }
99
+ 75% { opacity: 0.4; clip-path: inset(0 50% 0 20%); }
100
+ 100% { opacity: 0; }
101
+ }
102
+
103
+ /* NOISE */
104
+ .noise {
105
+ position: absolute;
106
+ inset: 0;
107
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
108
+ opacity: 0;
109
+ mix-blend-mode: overlay;
110
+ pointer-events: none;
111
+ }
112
+
113
+ .button:hover .noise {
114
+ opacity: 0.15;
115
+ }
116
+
117
+ /* CONTENT */
118
+ .content {
119
+ position: relative;
120
+ z-index: 2;
121
+ }
122
+
123
+ /* GHOST LAYERS */
124
+ .ghost1, .ghost2 {
125
+ position: absolute;
126
+ inset: 0;
127
+ display: flex;
128
+ align-items: center;
129
+ justify-content: center;
130
+ pointer-events: none;
131
+ opacity: 0;
132
+ }
133
+
134
+ .ghost1 { color: var(--accent); }
135
+ .ghost2 { color: #00ffff; }
136
+
137
+ .button:hover .ghost1 {
138
+ opacity: 0.6;
139
+ animation: ghost1 0.15s infinite;
140
+ }
141
+
142
+ .button:hover .ghost2 {
143
+ opacity: 0.4;
144
+ animation: ghost2 0.15s infinite;
145
+ }
146
+
147
+ @keyframes ghost1 {
148
+ 0%, 100% { transform: translate(0); clip-path: inset(0 0 60% 0); }
149
+ 50% { transform: translate(-3px, 2px); clip-path: inset(30% 0 30% 0); }
150
+ }
151
+
152
+ @keyframes ghost2 {
153
+ 0%, 100% { transform: translate(0); clip-path: inset(60% 0 0 0); }
154
+ 50% { transform: translate(3px, -2px); clip-path: inset(20% 0 50% 0); }
155
+ }
156
+
157
+ /* CHAOS LEVELS */
158
+ .mild .debris1, .mild .debris2, .mild .debris3, .mild .debris4 { opacity: 0.3; }
159
+ .mild:hover .ghost1, .mild:hover .ghost2 { animation-duration: 0.3s; }
160
+
161
+ .extreme .debris1 { width: 30px; height: 3px; }
162
+ .extreme .debris2 { width: 3px; height: 25px; }
163
+ .extreme .debris3 { width: 25px; }
164
+ .extreme .debris4 { width: 12px; height: 12px; }
165
+ .extreme:hover .ghost1, .extreme:hover .ghost2 { animation-duration: 0.08s; }
166
+ .extreme:hover { animation: shake 0.1s infinite; }
167
+
168
+ @keyframes shake {
169
+ 0%, 100% { transform: translate(0); }
170
+ 25% { transform: translate(2px, 1px); }
171
+ 50% { transform: translate(-1px, -2px); }
172
+ 75% { transform: translate(1px, -1px); }
173
+ }
@@ -0,0 +1,60 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, ButtonHTMLAttributes } from 'react';
4
+ import styles from './chaos-button.module.css';
5
+
6
+ export interface ChaosButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
7
+ /** Chaos level */
8
+ chaos?: 'mild' | 'medium' | 'extreme';
9
+ /** Button variant */
10
+ variant?: 'solid' | 'outline' | 'broken';
11
+ /** Accent color */
12
+ accentColor?: string;
13
+ }
14
+
15
+ export const ChaosButton = forwardRef<HTMLButtonElement, ChaosButtonProps>(
16
+ (
17
+ {
18
+ children,
19
+ chaos = 'medium',
20
+ variant = 'solid',
21
+ accentColor = '#ff0040',
22
+ className,
23
+ style,
24
+ ...props
25
+ },
26
+ ref
27
+ ) => {
28
+ return (
29
+ <button
30
+ ref={ref}
31
+ className={`${styles.button} ${styles[chaos]} ${styles[variant]} ${className || ''}`}
32
+ style={{ '--accent': accentColor, ...style } as React.CSSProperties}
33
+ {...props}
34
+ >
35
+ {/* Debris */}
36
+ <span className={styles.debris1} />
37
+ <span className={styles.debris2} />
38
+ <span className={styles.debris3} />
39
+ <span className={styles.debris4} />
40
+
41
+ {/* Glitch slice */}
42
+ <span className={styles.slice} />
43
+
44
+ {/* Noise */}
45
+ <span className={styles.noise} />
46
+
47
+ {/* Content */}
48
+ <span className={styles.content}>{children}</span>
49
+
50
+ {/* Ghost layers */}
51
+ <span className={styles.ghost1}>{children}</span>
52
+ <span className={styles.ghost2}>{children}</span>
53
+ </button>
54
+ );
55
+ }
56
+ );
57
+
58
+ ChaosButton.displayName = 'ChaosButton';
59
+
60
+ export default ChaosButton;
@@ -0,0 +1,197 @@
1
+ .button {
2
+ position: relative;
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ padding: 0.75rem 1.5rem;
7
+ font-family: inherit;
8
+ font-size: 0.875rem;
9
+ font-weight: 600;
10
+ letter-spacing: 0.05em;
11
+ text-transform: uppercase;
12
+ border: none;
13
+ cursor: pointer;
14
+ overflow: hidden;
15
+ transition: transform 0.1s;
16
+ --glitch-color: #ff0040;
17
+ --glitch-color-alt: #00ffff;
18
+ }
19
+
20
+ .button:active {
21
+ transform: scale(0.98);
22
+ }
23
+
24
+ /* Variants */
25
+ .default {
26
+ background: #0a0a0a;
27
+ color: #fafafa;
28
+ border: 1px solid #333;
29
+ }
30
+
31
+ .outline {
32
+ background: transparent;
33
+ color: #fafafa;
34
+ border: 2px solid currentColor;
35
+ }
36
+
37
+ .ghost {
38
+ background: transparent;
39
+ color: #fafafa;
40
+ border: none;
41
+ }
42
+
43
+ /* Content layer */
44
+ .content {
45
+ position: relative;
46
+ z-index: 3;
47
+ }
48
+
49
+ /* Glitch layers */
50
+ .glitchLayer,
51
+ .glitchLayerAlt {
52
+ position: absolute;
53
+ inset: 0;
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ opacity: 0;
58
+ pointer-events: none;
59
+ }
60
+
61
+ .glitchLayer {
62
+ color: var(--glitch-color);
63
+ z-index: 1;
64
+ }
65
+
66
+ .glitchLayerAlt {
67
+ color: var(--glitch-color-alt);
68
+ z-index: 2;
69
+ }
70
+
71
+ /* Noise layer */
72
+ .noiseLayer {
73
+ position: absolute;
74
+ inset: 0;
75
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
76
+ opacity: 0;
77
+ mix-blend-mode: overlay;
78
+ pointer-events: none;
79
+ z-index: 4;
80
+ }
81
+
82
+ /* Hover states */
83
+ .button:hover .noiseLayer {
84
+ opacity: 0.15;
85
+ animation: noise-shift 0.2s steps(5) infinite;
86
+ }
87
+
88
+ /* SUBTLE hover */
89
+ .subtle:hover .glitchLayer {
90
+ opacity: 0.5;
91
+ animation: glitch-subtle 0.5s infinite;
92
+ }
93
+
94
+ .subtle:hover .glitchLayerAlt {
95
+ opacity: 0.3;
96
+ animation: glitch-subtle-alt 0.5s infinite;
97
+ }
98
+
99
+ @keyframes glitch-subtle {
100
+ 0%, 100% { transform: translate(0); clip-path: inset(0 0 70% 0); }
101
+ 25% { transform: translate(-1px, 1px); clip-path: inset(20% 0 50% 0); }
102
+ 50% { transform: translate(1px, -1px); clip-path: inset(40% 0 30% 0); }
103
+ 75% { transform: translate(-1px, -1px); clip-path: inset(60% 0 10% 0); }
104
+ }
105
+
106
+ @keyframes glitch-subtle-alt {
107
+ 0%, 100% { transform: translate(0); clip-path: inset(70% 0 0 0); }
108
+ 25% { transform: translate(1px, -1px); clip-path: inset(50% 0 20% 0); }
109
+ 50% { transform: translate(-1px, 1px); clip-path: inset(30% 0 40% 0); }
110
+ 75% { transform: translate(1px, 1px); clip-path: inset(10% 0 60% 0); }
111
+ }
112
+
113
+ /* MEDIUM hover */
114
+ .medium:hover .glitchLayer {
115
+ opacity: 0.7;
116
+ animation: glitch-medium 0.2s infinite;
117
+ }
118
+
119
+ .medium:hover .glitchLayerAlt {
120
+ opacity: 0.5;
121
+ animation: glitch-medium-alt 0.2s infinite;
122
+ }
123
+
124
+ @keyframes glitch-medium {
125
+ 0%, 100% { transform: translate(0); clip-path: inset(0 0 60% 0); }
126
+ 20% { transform: translate(-2px, 2px); clip-path: inset(10% 0 50% 0); }
127
+ 40% { transform: translate(2px, -2px); clip-path: inset(30% 0 30% 0); }
128
+ 60% { transform: translate(-2px, -2px); clip-path: inset(50% 0 10% 0); }
129
+ 80% { transform: translate(2px, 2px); clip-path: inset(70% 0 0 0); }
130
+ }
131
+
132
+ @keyframes glitch-medium-alt {
133
+ 0%, 100% { transform: translate(0); clip-path: inset(60% 0 0 0); }
134
+ 20% { transform: translate(2px, -2px); clip-path: inset(50% 0 10% 0); }
135
+ 40% { transform: translate(-2px, 2px); clip-path: inset(30% 0 30% 0); }
136
+ 60% { transform: translate(2px, 2px); clip-path: inset(10% 0 50% 0); }
137
+ 80% { transform: translate(-2px, -2px); clip-path: inset(0 0 70% 0); }
138
+ }
139
+
140
+ /* INTENSE hover */
141
+ .intense:hover .glitchLayer {
142
+ opacity: 0.9;
143
+ animation: glitch-intense 0.1s infinite;
144
+ }
145
+
146
+ .intense:hover .glitchLayerAlt {
147
+ opacity: 0.7;
148
+ animation: glitch-intense-alt 0.1s infinite;
149
+ }
150
+
151
+ .intense:hover {
152
+ animation: button-shake 0.1s infinite;
153
+ }
154
+
155
+ @keyframes glitch-intense {
156
+ 0% { transform: translate(0); clip-path: inset(0 0 50% 0); }
157
+ 10% { transform: translate(-3px, 3px); clip-path: inset(5% 0 45% 0); }
158
+ 20% { transform: translate(3px, -3px); clip-path: inset(15% 0 35% 0); }
159
+ 30% { transform: translate(-3px, -3px); clip-path: inset(25% 0 25% 0); }
160
+ 40% { transform: translate(3px, 3px); clip-path: inset(35% 0 15% 0); }
161
+ 50% { transform: translate(-3px, 0); clip-path: inset(45% 0 5% 0); }
162
+ 60% { transform: translate(3px, 0); clip-path: inset(55% 0 0 0); }
163
+ 70% { transform: translate(0, 3px); clip-path: inset(65% 0 0 0); }
164
+ 80% { transform: translate(0, -3px); clip-path: inset(75% 0 0 0); }
165
+ 90% { transform: translate(-3px, 3px); clip-path: inset(85% 0 0 0); }
166
+ 100% { transform: translate(0); clip-path: inset(95% 0 0 0); }
167
+ }
168
+
169
+ @keyframes glitch-intense-alt {
170
+ 0% { transform: translate(0); clip-path: inset(50% 0 0 0); }
171
+ 10% { transform: translate(3px, -3px); clip-path: inset(45% 0 5% 0); }
172
+ 20% { transform: translate(-3px, 3px); clip-path: inset(35% 0 15% 0); }
173
+ 30% { transform: translate(3px, 3px); clip-path: inset(25% 0 25% 0); }
174
+ 40% { transform: translate(-3px, -3px); clip-path: inset(15% 0 35% 0); }
175
+ 50% { transform: translate(3px, 0); clip-path: inset(5% 0 45% 0); }
176
+ 60% { transform: translate(-3px, 0); clip-path: inset(0 0 55% 0); }
177
+ 70% { transform: translate(0, -3px); clip-path: inset(0 0 65% 0); }
178
+ 80% { transform: translate(0, 3px); clip-path: inset(0 0 75% 0); }
179
+ 90% { transform: translate(3px, -3px); clip-path: inset(0 0 85% 0); }
180
+ 100% { transform: translate(0); clip-path: inset(0 0 95% 0); }
181
+ }
182
+
183
+ @keyframes button-shake {
184
+ 0%, 100% { transform: translate(0); }
185
+ 25% { transform: translate(1px, 1px); }
186
+ 50% { transform: translate(-1px, -1px); }
187
+ 75% { transform: translate(1px, -1px); }
188
+ }
189
+
190
+ @keyframes noise-shift {
191
+ 0% { transform: translate(0, 0); }
192
+ 20% { transform: translate(-2px, 1px); }
193
+ 40% { transform: translate(1px, -1px); }
194
+ 60% { transform: translate(2px, 2px); }
195
+ 80% { transform: translate(-1px, -2px); }
196
+ 100% { transform: translate(0, 0); }
197
+ }
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, ButtonHTMLAttributes } from 'react';
4
+ import styles from './glitch-button.module.css';
5
+
6
+ export interface GlitchButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
7
+ /** Button variant */
8
+ variant?: 'default' | 'outline' | 'ghost';
9
+ /** Glitch intensity on hover */
10
+ intensity?: 'subtle' | 'medium' | 'intense';
11
+ /** Primary glitch color */
12
+ glitchColor?: string;
13
+ /** Secondary glitch color */
14
+ glitchColorAlt?: string;
15
+ }
16
+
17
+ export const GlitchButton = forwardRef<HTMLButtonElement, GlitchButtonProps>(
18
+ (
19
+ {
20
+ children,
21
+ variant = 'default',
22
+ intensity = 'medium',
23
+ glitchColor = '#ff0040',
24
+ glitchColorAlt = '#00ffff',
25
+ className,
26
+ style,
27
+ ...props
28
+ },
29
+ ref
30
+ ) => {
31
+ return (
32
+ <button
33
+ ref={ref}
34
+ className={`${styles.button} ${styles[variant]} ${styles[intensity]} ${className || ''}`}
35
+ style={{
36
+ '--glitch-color': glitchColor,
37
+ '--glitch-color-alt': glitchColorAlt,
38
+ ...style,
39
+ } as React.CSSProperties}
40
+ {...props}
41
+ >
42
+ <span className={styles.content}>{children}</span>
43
+ <span className={styles.glitchLayer} aria-hidden="true">{children}</span>
44
+ <span className={styles.glitchLayerAlt} aria-hidden="true">{children}</span>
45
+ <span className={styles.noiseLayer} aria-hidden="true" />
46
+ </button>
47
+ );
48
+ }
49
+ );
50
+
51
+ GlitchButton.displayName = 'GlitchButton';
52
+
53
+ export default GlitchButton;
@@ -0,0 +1,50 @@
1
+ .cursor {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ pointer-events: none;
6
+ z-index: 99999;
7
+ opacity: 0;
8
+ transition: opacity 0.2s;
9
+ }
10
+
11
+ .visible {
12
+ opacity: 1;
13
+ }
14
+
15
+ .ring {
16
+ border-radius: 50%;
17
+ border: 2px solid currentColor;
18
+ }
19
+
20
+ .dot {
21
+ border-radius: 50%;
22
+ }
23
+
24
+ .crosshair {
25
+ background: transparent;
26
+ position: relative;
27
+ }
28
+
29
+ .crosshair::before,
30
+ .crosshair::after {
31
+ content: '';
32
+ position: absolute;
33
+ background: currentColor;
34
+ }
35
+
36
+ .crosshair::before {
37
+ top: 50%;
38
+ left: 0;
39
+ right: 0;
40
+ height: 1px;
41
+ transform: translateY(-50%);
42
+ }
43
+
44
+ .crosshair::after {
45
+ left: 50%;
46
+ top: 0;
47
+ bottom: 0;
48
+ width: 1px;
49
+ transform: translateX(-50%);
50
+ }
@@ -0,0 +1,83 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import styles from './cursor-follower.module.css';
5
+
6
+ export interface CursorFollowerProps {
7
+ /** Cursor size in pixels */
8
+ size?: number;
9
+ /** Cursor color */
10
+ color?: string;
11
+ /** Follow delay (higher = smoother but slower) */
12
+ delay?: number;
13
+ /** Cursor style */
14
+ variant?: 'ring' | 'dot' | 'crosshair';
15
+ /** Mix blend mode */
16
+ blendMode?: 'difference' | 'exclusion' | 'normal';
17
+ }
18
+
19
+ export function CursorFollower({
20
+ size = 20,
21
+ color = '#ff0040',
22
+ delay = 0.1,
23
+ variant = 'ring',
24
+ blendMode = 'difference',
25
+ }: CursorFollowerProps) {
26
+ const cursorRef = useRef<HTMLDivElement>(null);
27
+ const [isVisible, setIsVisible] = useState(false);
28
+ const position = useRef({ x: 0, y: 0 });
29
+ const target = useRef({ x: 0, y: 0 });
30
+
31
+ useEffect(() => {
32
+ const cursor = cursorRef.current;
33
+ if (!cursor) return;
34
+
35
+ const handleMouseMove = (e: MouseEvent) => {
36
+ target.current = { x: e.clientX, y: e.clientY };
37
+ if (!isVisible) setIsVisible(true);
38
+ };
39
+
40
+ const handleMouseLeave = () => setIsVisible(false);
41
+ const handleMouseEnter = () => setIsVisible(true);
42
+
43
+ let animationId: number;
44
+
45
+ const animate = () => {
46
+ position.current.x += (target.current.x - position.current.x) * (1 - delay);
47
+ position.current.y += (target.current.y - position.current.y) * (1 - delay);
48
+
49
+ cursor.style.transform = `translate(${position.current.x - size / 2}px, ${position.current.y - size / 2}px)`;
50
+
51
+ animationId = requestAnimationFrame(animate);
52
+ };
53
+
54
+ document.addEventListener('mousemove', handleMouseMove);
55
+ document.addEventListener('mouseleave', handleMouseLeave);
56
+ document.addEventListener('mouseenter', handleMouseEnter);
57
+ animationId = requestAnimationFrame(animate);
58
+
59
+ return () => {
60
+ document.removeEventListener('mousemove', handleMouseMove);
61
+ document.removeEventListener('mouseleave', handleMouseLeave);
62
+ document.removeEventListener('mouseenter', handleMouseEnter);
63
+ cancelAnimationFrame(animationId);
64
+ };
65
+ }, [delay, size, isVisible]);
66
+
67
+ return (
68
+ <div
69
+ ref={cursorRef}
70
+ className={`${styles.cursor} ${styles[variant]} ${isVisible ? styles.visible : ''}`}
71
+ style={{
72
+ width: size,
73
+ height: size,
74
+ borderColor: color,
75
+ backgroundColor: variant === 'dot' ? color : 'transparent',
76
+ mixBlendMode: blendMode,
77
+ }}
78
+ aria-hidden="true"
79
+ />
80
+ );
81
+ }
82
+
83
+ export default CursorFollower;
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes } from 'react';
4
+ import styles from './screen-distortion.module.css';
5
+
6
+ export interface ScreenDistortionProps extends HTMLAttributes<HTMLDivElement> {
7
+ /** Distortion type */
8
+ type?: 'wave' | 'glitch' | 'chromatic' | 'noise';
9
+ /** Distortion intensity */
10
+ intensity?: 'subtle' | 'medium' | 'intense';
11
+ /** Animation speed */
12
+ speed?: 'slow' | 'normal' | 'fast';
13
+ /** Only show on hover (requires parent with :hover) */
14
+ hoverOnly?: boolean;
15
+ /** Fixed or absolute positioning */
16
+ position?: 'fixed' | 'absolute';
17
+ }
18
+
19
+ export const ScreenDistortion = forwardRef<HTMLDivElement, ScreenDistortionProps>(
20
+ (
21
+ {
22
+ type = 'glitch',
23
+ intensity = 'medium',
24
+ speed = 'normal',
25
+ hoverOnly = false,
26
+ position = 'fixed',
27
+ className,
28
+ style,
29
+ ...props
30
+ },
31
+ ref
32
+ ) => {
33
+ return (
34
+ <div
35
+ ref={ref}
36
+ className={`
37
+ ${styles.distortion}
38
+ ${styles[type]}
39
+ ${styles[intensity]}
40
+ ${styles[speed]}
41
+ ${hoverOnly ? styles.hoverOnly : ''}
42
+ ${className || ''}
43
+ `}
44
+ style={{ position, ...style }}
45
+ aria-hidden="true"
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+ );
51
+
52
+ ScreenDistortion.displayName = 'ScreenDistortion';
53
+
54
+ export default ScreenDistortion;