@djangocfg/ui-nextjs 2.1.65 → 2.1.67

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 (92) hide show
  1. package/package.json +13 -8
  2. package/src/blocks/SplitHero/SplitHeroMedia.tsx +2 -1
  3. package/src/stores/index.ts +8 -0
  4. package/src/stores/mediaCache.ts +464 -0
  5. package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +148 -0
  6. package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +301 -0
  7. package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +281 -0
  8. package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +328 -0
  9. package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +251 -0
  10. package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +427 -0
  11. package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +193 -0
  12. package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +146 -0
  13. package/src/tools/AudioPlayer/README.md +325 -0
  14. package/src/tools/AudioPlayer/components/AudioEqualizer.tsx +200 -0
  15. package/src/tools/AudioPlayer/components/AudioPlayer.tsx +231 -0
  16. package/src/tools/AudioPlayer/components/AudioShortcutsPopover.tsx +99 -0
  17. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +147 -0
  18. package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
  19. package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
  20. package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
  21. package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
  22. package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
  23. package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
  24. package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +280 -0
  25. package/src/tools/AudioPlayer/components/VisualizationToggle.tsx +64 -0
  26. package/src/tools/AudioPlayer/components/index.ts +21 -0
  27. package/src/tools/AudioPlayer/context/AudioProvider.tsx +292 -0
  28. package/src/tools/AudioPlayer/context/index.ts +11 -0
  29. package/src/tools/AudioPlayer/context/selectors.ts +96 -0
  30. package/src/tools/AudioPlayer/effects/index.ts +412 -0
  31. package/src/tools/AudioPlayer/hooks/index.ts +29 -0
  32. package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
  33. package/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts +149 -0
  34. package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
  35. package/src/tools/AudioPlayer/hooks/useVisualization.tsx +201 -0
  36. package/src/tools/AudioPlayer/index.ts +139 -0
  37. package/src/tools/AudioPlayer/types/audio.ts +107 -0
  38. package/src/tools/AudioPlayer/types/components.ts +98 -0
  39. package/src/tools/AudioPlayer/types/effects.ts +73 -0
  40. package/src/tools/AudioPlayer/types/index.ts +35 -0
  41. package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
  42. package/src/tools/AudioPlayer/utils/index.ts +5 -0
  43. package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
  44. package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
  45. package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
  46. package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
  47. package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
  48. package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
  49. package/src/tools/ImageViewer/README.md +174 -0
  50. package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
  51. package/src/tools/ImageViewer/components/ImageToolbar.tsx +150 -0
  52. package/src/tools/ImageViewer/components/ImageViewer.tsx +235 -0
  53. package/src/tools/ImageViewer/components/index.ts +7 -0
  54. package/src/tools/ImageViewer/hooks/index.ts +9 -0
  55. package/src/tools/ImageViewer/hooks/useImageLoading.ts +153 -0
  56. package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
  57. package/src/tools/ImageViewer/index.ts +60 -0
  58. package/src/tools/ImageViewer/types.ts +75 -0
  59. package/src/tools/ImageViewer/utils/constants.ts +59 -0
  60. package/src/tools/ImageViewer/utils/index.ts +16 -0
  61. package/src/tools/ImageViewer/utils/lqip.ts +47 -0
  62. package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
  63. package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
  64. package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
  65. package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
  66. package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
  67. package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
  68. package/src/tools/VideoPlayer/README.md +212 -187
  69. package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
  70. package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +174 -0
  71. package/src/tools/VideoPlayer/components/VideoPlayer.tsx +201 -0
  72. package/src/tools/VideoPlayer/components/index.ts +14 -0
  73. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
  74. package/src/tools/VideoPlayer/context/index.ts +8 -0
  75. package/src/tools/VideoPlayer/hooks/index.ts +9 -0
  76. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +109 -0
  77. package/src/tools/VideoPlayer/index.ts +70 -9
  78. package/src/tools/VideoPlayer/providers/NativeProvider.tsx +206 -0
  79. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +401 -0
  80. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +332 -0
  81. package/src/tools/VideoPlayer/providers/index.ts +8 -0
  82. package/src/tools/VideoPlayer/types/index.ts +38 -0
  83. package/src/tools/VideoPlayer/types/player.ts +116 -0
  84. package/src/tools/VideoPlayer/types/provider.ts +93 -0
  85. package/src/tools/VideoPlayer/types/sources.ts +97 -0
  86. package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
  87. package/src/tools/VideoPlayer/utils/index.ts +11 -0
  88. package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
  89. package/src/tools/index.ts +92 -4
  90. package/src/tools/VideoPlayer/NativePlayer.tsx +0 -141
  91. package/src/tools/VideoPlayer/VideoPlayer.tsx +0 -231
  92. package/src/tools/VideoPlayer/types.ts +0 -118
@@ -0,0 +1,328 @@
1
+ # Phase 4: Context Refactoring
2
+
3
+ ## Source: `context.tsx` (lines 231-574)
4
+
5
+ Split provider from selector hooks.
6
+
7
+ ---
8
+
9
+ ## `context/AudioProvider.tsx`
10
+
11
+ Provider component only (~150 lines).
12
+
13
+ ```typescript
14
+ 'use client';
15
+
16
+ import {
17
+ createContext,
18
+ useRef,
19
+ useMemo,
20
+ useCallback,
21
+ useState,
22
+ useEffect,
23
+ type ReactNode,
24
+ } from 'react';
25
+ import { useWavesurfer } from '@wavesurfer/react';
26
+ import type { AudioContextState, AudioSource, WaveformOptions } from '../types';
27
+ import { useSharedWebAudio, useAudioAnalysis } from '../hooks';
28
+ import { useAudioCache } from '../../../stores/mediaCache';
29
+
30
+ // =============================================================================
31
+ // CONTEXT
32
+ // =============================================================================
33
+
34
+ export const AudioPlayerContext = createContext<AudioContextState | null>(null);
35
+
36
+ // =============================================================================
37
+ // PROVIDER PROPS
38
+ // =============================================================================
39
+
40
+ interface AudioProviderProps {
41
+ source: AudioSource;
42
+ autoPlay?: boolean;
43
+ waveformOptions?: WaveformOptions;
44
+ containerRef: React.RefObject<HTMLDivElement | null>;
45
+ children: ReactNode;
46
+ }
47
+
48
+ // =============================================================================
49
+ // PROVIDER
50
+ // =============================================================================
51
+
52
+ export function AudioProvider({
53
+ source,
54
+ autoPlay = false,
55
+ waveformOptions = {},
56
+ containerRef,
57
+ children,
58
+ }: AudioProviderProps) {
59
+ // Cache for playback position persistence
60
+ const { saveAudioPosition, getAudioPosition } = useAudioCache();
61
+ const lastSavedTimeRef = useRef<number>(0);
62
+
63
+ // Memoize WaveSurfer options
64
+ const options = useMemo(
65
+ () => ({
66
+ container: containerRef,
67
+ url: source.uri,
68
+ waveColor: waveformOptions.waveColor || 'hsl(217 91% 60% / 0.3)',
69
+ progressColor: waveformOptions.progressColor || 'hsl(217 91% 60%)',
70
+ cursorColor: waveformOptions.cursorColor || 'hsl(217 91% 60%)',
71
+ cursorWidth: waveformOptions.cursorWidth ?? 2,
72
+ height: waveformOptions.height ?? 64,
73
+ barWidth: waveformOptions.barWidth ?? 3,
74
+ barRadius: waveformOptions.barRadius ?? 3,
75
+ barGap: waveformOptions.barGap ?? 2,
76
+ normalize: true,
77
+ interact: true,
78
+ hideScrollbar: true,
79
+ autoplay: autoPlay,
80
+ }),
81
+ [source.uri, autoPlay, waveformOptions, containerRef]
82
+ );
83
+
84
+ const { wavesurfer, isReady, isPlaying, currentTime } = useWavesurfer(options);
85
+
86
+ // Restore cached position
87
+ useEffect(() => {
88
+ if (isReady && wavesurfer && source.uri) {
89
+ const cachedPosition = getAudioPosition(source.uri);
90
+ if (cachedPosition && cachedPosition > 0) {
91
+ const duration = wavesurfer.getDuration();
92
+ if (cachedPosition < duration - 1) {
93
+ wavesurfer.setTime(cachedPosition);
94
+ }
95
+ }
96
+ }
97
+ }, [isReady, wavesurfer, source.uri, getAudioPosition]);
98
+
99
+ // Save position periodically
100
+ useEffect(() => {
101
+ if (!source.uri) return;
102
+
103
+ if (isPlaying && currentTime > 0) {
104
+ const timeSinceLastSave = currentTime - lastSavedTimeRef.current;
105
+ if (timeSinceLastSave >= 5 || timeSinceLastSave < 0) {
106
+ saveAudioPosition(source.uri, currentTime);
107
+ lastSavedTimeRef.current = currentTime;
108
+ }
109
+ }
110
+
111
+ if (!isPlaying && currentTime > 0) {
112
+ saveAudioPosition(source.uri, currentTime);
113
+ lastSavedTimeRef.current = currentTime;
114
+ }
115
+ }, [isPlaying, currentTime, source.uri, saveAudioPosition]);
116
+
117
+ // Derived state
118
+ const duration = wavesurfer?.getDuration() ?? 0;
119
+ const volume = wavesurfer?.getVolume() ?? 1;
120
+ const isMuted = wavesurfer?.getMuted() ?? false;
121
+
122
+ // Loop state
123
+ const [isLooping, setIsLooping] = useState(false);
124
+
125
+ useEffect(() => {
126
+ if (!wavesurfer) return;
127
+
128
+ const handleFinish = () => {
129
+ if (isLooping) {
130
+ wavesurfer.seekTo(0);
131
+ wavesurfer.play();
132
+ }
133
+ };
134
+
135
+ wavesurfer.on('finish', handleFinish);
136
+ return () => {
137
+ wavesurfer.un('finish', handleFinish);
138
+ };
139
+ }, [wavesurfer, isLooping]);
140
+
141
+ // Audio element
142
+ const audioElement = useMemo(() => wavesurfer?.getMediaElement() ?? null, [wavesurfer]);
143
+
144
+ // Shared Web Audio context
145
+ const sharedAudio = useSharedWebAudio(audioElement);
146
+
147
+ // Audio analysis
148
+ const audioLevels = useAudioAnalysis(sharedAudio, isPlaying);
149
+
150
+ // Actions
151
+ const play = useCallback(async () => { await wavesurfer?.play(); }, [wavesurfer]);
152
+ const pause = useCallback(() => { wavesurfer?.pause(); }, [wavesurfer]);
153
+ const togglePlay = useCallback(() => { wavesurfer?.playPause(); }, [wavesurfer]);
154
+
155
+ const seek = useCallback((time: number) => {
156
+ if (wavesurfer) {
157
+ wavesurfer.setTime(Math.max(0, Math.min(time, duration)));
158
+ }
159
+ }, [wavesurfer, duration]);
160
+
161
+ const seekTo = useCallback((progress: number) => {
162
+ if (wavesurfer) {
163
+ wavesurfer.seekTo(Math.max(0, Math.min(progress, 1)));
164
+ }
165
+ }, [wavesurfer]);
166
+
167
+ const skip = useCallback((seconds: number) => { wavesurfer?.skip(seconds); }, [wavesurfer]);
168
+
169
+ const setVolume = useCallback((vol: number) => {
170
+ if (wavesurfer) {
171
+ wavesurfer.setVolume(Math.max(0, Math.min(vol, 1)));
172
+ }
173
+ }, [wavesurfer]);
174
+
175
+ const toggleMute = useCallback(() => {
176
+ if (wavesurfer) {
177
+ wavesurfer.setMuted(!wavesurfer.getMuted());
178
+ }
179
+ }, [wavesurfer]);
180
+
181
+ const restart = useCallback(() => {
182
+ if (wavesurfer) {
183
+ wavesurfer.seekTo(0);
184
+ wavesurfer.play();
185
+ }
186
+ }, [wavesurfer]);
187
+
188
+ const toggleLoop = useCallback(() => { setIsLooping(prev => !prev); }, []);
189
+ const setLoop = useCallback((enabled: boolean) => { setIsLooping(enabled); }, []);
190
+
191
+ // Context value
192
+ const contextValue = useMemo<AudioContextState>(
193
+ () => ({
194
+ wavesurfer,
195
+ audioElement,
196
+ sharedAudio,
197
+ isReady,
198
+ isPlaying,
199
+ currentTime,
200
+ duration,
201
+ volume,
202
+ isMuted,
203
+ isLooping,
204
+ audioLevels,
205
+ play,
206
+ pause,
207
+ togglePlay,
208
+ seek,
209
+ seekTo,
210
+ skip,
211
+ setVolume,
212
+ toggleMute,
213
+ toggleLoop,
214
+ setLoop,
215
+ restart,
216
+ }),
217
+ [
218
+ wavesurfer, audioElement, sharedAudio,
219
+ isReady, isPlaying, currentTime, duration, volume, isMuted, isLooping,
220
+ audioLevels,
221
+ play, pause, togglePlay, seek, seekTo, skip, setVolume, toggleMute, toggleLoop, setLoop, restart,
222
+ ]
223
+ );
224
+
225
+ return (
226
+ <AudioPlayerContext.Provider value={contextValue}>
227
+ {children}
228
+ </AudioPlayerContext.Provider>
229
+ );
230
+ }
231
+ ```
232
+
233
+ ---
234
+
235
+ ## `context/selectors.ts`
236
+
237
+ Selector hooks for performance optimization.
238
+
239
+ ```typescript
240
+ 'use client';
241
+
242
+ import { useContext } from 'react';
243
+ import { AudioPlayerContext } from './AudioProvider';
244
+ import type { AudioContextState } from '../types';
245
+
246
+ // =============================================================================
247
+ // MAIN HOOK
248
+ // =============================================================================
249
+
250
+ export function useAudio(): AudioContextState {
251
+ const context = useContext(AudioPlayerContext);
252
+ if (!context) {
253
+ throw new Error('useAudio must be used within an AudioProvider');
254
+ }
255
+ return context;
256
+ }
257
+
258
+ // =============================================================================
259
+ // SELECTIVE HOOKS - for performance optimization
260
+ // =============================================================================
261
+
262
+ /** Hook for playback controls only (no re-render on time updates) */
263
+ export function useAudioControls() {
264
+ const {
265
+ isReady,
266
+ play,
267
+ pause,
268
+ togglePlay,
269
+ skip,
270
+ restart,
271
+ setVolume,
272
+ toggleMute,
273
+ toggleLoop,
274
+ setLoop,
275
+ } = useAudio();
276
+
277
+ return {
278
+ isReady,
279
+ play,
280
+ pause,
281
+ togglePlay,
282
+ skip,
283
+ restart,
284
+ setVolume,
285
+ toggleMute,
286
+ toggleLoop,
287
+ setLoop,
288
+ };
289
+ }
290
+
291
+ /** Hook for playback state (read-only) */
292
+ export function useAudioState() {
293
+ const {
294
+ isReady,
295
+ isPlaying,
296
+ currentTime,
297
+ duration,
298
+ volume,
299
+ isMuted,
300
+ isLooping,
301
+ } = useAudio();
302
+
303
+ return {
304
+ isReady,
305
+ isPlaying,
306
+ currentTime,
307
+ duration,
308
+ volume,
309
+ isMuted,
310
+ isLooping,
311
+ };
312
+ }
313
+
314
+ /** Hook for audio element access (for equalizer and reactive effects) */
315
+ export function useAudioElement() {
316
+ const { audioElement, sharedAudio, isPlaying, audioLevels } = useAudio();
317
+ return { audioElement, sharedAudio, isPlaying, audioLevels };
318
+ }
319
+ ```
320
+
321
+ ---
322
+
323
+ ## `context/index.ts`
324
+
325
+ ```typescript
326
+ export { AudioPlayerContext, AudioProvider } from './AudioProvider';
327
+ export { useAudio, useAudioControls, useAudioState, useAudioElement } from './selectors';
328
+ ```
@@ -0,0 +1,251 @@
1
+ # Phase 5: Components Reorganization
2
+
3
+ ## Overview
4
+
5
+ Move all components to `components/` folder and split `AudioReactiveCover` into subcomponents.
6
+
7
+ ---
8
+
9
+ ## Files to Move (with import updates)
10
+
11
+ | Current | New Location | Changes |
12
+ |---------|--------------|---------|
13
+ | `AudioPlayer.tsx` | `components/AudioPlayer.tsx` | Update imports |
14
+ | `AudioEqualizer.tsx` | `components/AudioEqualizer.tsx` | Update imports |
15
+ | `SimpleAudioPlayer.tsx` | `components/SimpleAudioPlayer.tsx` | Update imports |
16
+ | `AudioShortcutsPopover.tsx` | `components/ShortcutsPopover.tsx` | Rename + update imports |
17
+ | `VisualizationToggle.tsx` | `components/VisualizationToggle.tsx` | Update imports |
18
+
19
+ ---
20
+
21
+ ## `AudioReactiveCover` Split
22
+
23
+ Current: 390 lines with 4 inline effect components.
24
+
25
+ ### `components/ReactiveCover/index.tsx`
26
+
27
+ Main component (~100 lines).
28
+
29
+ ```typescript
30
+ 'use client';
31
+
32
+ import { type ReactNode } from 'react';
33
+ import { cn } from '@djangocfg/ui-nextjs';
34
+ import { useAudioElement } from '../../context';
35
+ import {
36
+ getEffectConfig,
37
+ prepareEffectColors,
38
+ EFFECT_ANIMATIONS,
39
+ } from '../../effects';
40
+ import type { EffectVariant, EffectIntensity, EffectColorScheme } from '../../types';
41
+
42
+ import { GlowEffect } from './GlowEffect';
43
+ import { OrbsEffect } from './OrbsEffect';
44
+ import { SpotlightEffect } from './SpotlightEffect';
45
+ import { MeshEffect } from './MeshEffect';
46
+
47
+ // ... Props and constants ...
48
+
49
+ export function AudioReactiveCover({
50
+ children,
51
+ size = 'lg',
52
+ variant = 'spotlight',
53
+ intensity = 'medium',
54
+ colorScheme = 'primary',
55
+ onClick,
56
+ className,
57
+ }: AudioReactiveCoverProps) {
58
+ const { isPlaying, audioLevels: levels } = useAudioElement();
59
+
60
+ const sizeConfig = SIZES[size];
61
+ const effectConfig = getEffectConfig(intensity);
62
+ const { colors, hueShift } = prepareEffectColors(colorScheme, levels);
63
+ const containerScale = 1 + levels.overall * effectConfig.scale;
64
+
65
+ return (
66
+ <div
67
+ className={cn('relative', sizeConfig.container, className)}
68
+ style={{
69
+ transform: `scale(${containerScale})`,
70
+ transition: 'transform 0.1s ease-out',
71
+ }}
72
+ >
73
+ {variant === 'glow' && (
74
+ <GlowEffect
75
+ levels={levels}
76
+ config={effectConfig}
77
+ colors={colors}
78
+ hueShift={hueShift}
79
+ isPlaying={isPlaying}
80
+ />
81
+ )}
82
+
83
+ {variant === 'orbs' && (
84
+ <OrbsEffect
85
+ levels={levels}
86
+ config={effectConfig}
87
+ colors={colors}
88
+ baseSize={sizeConfig.orbBase}
89
+ isPlaying={isPlaying}
90
+ />
91
+ )}
92
+
93
+ {variant === 'spotlight' && (
94
+ <SpotlightEffect
95
+ levels={levels}
96
+ config={effectConfig}
97
+ colors={colors}
98
+ isPlaying={isPlaying}
99
+ />
100
+ )}
101
+
102
+ {variant === 'mesh' && (
103
+ <MeshEffect
104
+ levels={levels}
105
+ config={effectConfig}
106
+ colors={colors}
107
+ isPlaying={isPlaying}
108
+ />
109
+ )}
110
+
111
+ {/* Content */}
112
+ <div
113
+ className="relative w-full h-full rounded-lg overflow-hidden shadow-2xl z-10 bg-background cursor-pointer"
114
+ onClick={onClick}
115
+ role={onClick ? 'button' : undefined}
116
+ tabIndex={onClick ? 0 : undefined}
117
+ onKeyDown={onClick ? (e) => e.key === 'Enter' && onClick() : undefined}
118
+ >
119
+ {children}
120
+ </div>
121
+
122
+ <style dangerouslySetInnerHTML={{ __html: EFFECT_ANIMATIONS }} />
123
+ </div>
124
+ );
125
+ }
126
+ ```
127
+
128
+ ### `components/ReactiveCover/GlowEffect.tsx`
129
+
130
+ ```typescript
131
+ 'use client';
132
+
133
+ import { cn } from '@djangocfg/ui-nextjs';
134
+ import { calculateGlowLayers } from '../../effects';
135
+ import type { AudioLevels, EffectConfig } from '../../types';
136
+
137
+ interface GlowEffectProps {
138
+ levels: AudioLevels;
139
+ config: EffectConfig;
140
+ colors: string[];
141
+ hueShift: number;
142
+ isPlaying: boolean;
143
+ }
144
+
145
+ export function GlowEffect({ levels, config, colors, hueShift, isPlaying }: GlowEffectProps) {
146
+ const layers = calculateGlowLayers(levels, config, colors);
147
+ const showPulseRings = levels.bass > 0.5;
148
+ const showSparkle = levels.high > 0.4;
149
+
150
+ return (
151
+ <>
152
+ {/* Main glow layers */}
153
+ {layers.map((layer, i) => (
154
+ <div
155
+ key={i}
156
+ className={cn('absolute rounded-2xl -z-10', layer.blur)}
157
+ style={{
158
+ inset: `-${layer.inset}px`,
159
+ background: layer.background,
160
+ opacity: isPlaying ? layer.opacity : 0,
161
+ transform: i < 2 ? `scaleY(${layer.scale})` : `scale(${layer.scale})`,
162
+ animation: isPlaying && layer.animation ? layer.animation : 'none',
163
+ transition: 'opacity 0.3s',
164
+ }}
165
+ />
166
+ ))}
167
+
168
+ {/* Rotating color sweep */}
169
+ <div
170
+ className="absolute rounded-2xl blur-xl overflow-hidden -z-10"
171
+ style={{
172
+ inset: '-75px',
173
+ opacity: isPlaying ? 0.6 : 0,
174
+ transition: 'opacity 0.5s',
175
+ }}
176
+ >
177
+ <div
178
+ className="absolute inset-0"
179
+ style={{
180
+ background: `conic-gradient(
181
+ from ${hueShift}deg at 50% 50%,
182
+ hsl(${colors[0]} / 0.4) 0deg,
183
+ hsl(${colors[1] || colors[0]} / 0.3) 90deg,
184
+ hsl(${colors[2] || colors[0]} / 0.3) 180deg,
185
+ hsl(${colors[3] || colors[0]} / 0.3) 270deg,
186
+ hsl(${colors[0]} / 0.4) 360deg
187
+ )`,
188
+ animation: isPlaying ? 'glow-rotate 6s linear infinite' : 'none',
189
+ }}
190
+ />
191
+ </div>
192
+
193
+ {/* Pulse rings on bass hits */}
194
+ {showPulseRings && (
195
+ <>
196
+ <div
197
+ className="absolute -inset-6 rounded-xl border-2 animate-ping -z-10"
198
+ style={{
199
+ borderColor: `hsl(${colors[0]} / 0.4)`,
200
+ animationDuration: '1s',
201
+ }}
202
+ />
203
+ <div
204
+ className="absolute -inset-12 rounded-2xl border animate-ping -z-10"
205
+ style={{
206
+ borderColor: `hsl(${colors[1] || colors[0]} / 0.3)`,
207
+ animationDuration: '1.5s',
208
+ animationDelay: '0.2s',
209
+ }}
210
+ />
211
+ </>
212
+ )}
213
+
214
+ {/* Sparkle on high frequencies */}
215
+ {showSparkle && (
216
+ <div
217
+ className="absolute -inset-18 rounded-3xl -z-10"
218
+ style={{
219
+ background: `radial-gradient(circle at 50% 30%, hsl(${colors[2] || colors[0]} / 0.5) 0%, transparent 30%)`,
220
+ animation: 'sparkle-move 0.5s ease-out',
221
+ }}
222
+ />
223
+ )}
224
+ </>
225
+ );
226
+ }
227
+ ```
228
+
229
+ ### Similar split for:
230
+ - `components/ReactiveCover/OrbsEffect.tsx`
231
+ - `components/ReactiveCover/SpotlightEffect.tsx`
232
+ - `components/ReactiveCover/MeshEffect.tsx`
233
+
234
+ ---
235
+
236
+ ## `components/index.ts`
237
+
238
+ ```typescript
239
+ // Main components
240
+ export { AudioPlayer } from './AudioPlayer';
241
+ export { AudioEqualizer } from './AudioEqualizer';
242
+ export { SimpleAudioPlayer } from './SimpleAudioPlayer';
243
+ export type { SimpleAudioPlayerProps } from './SimpleAudioPlayer';
244
+
245
+ // ReactiveCover
246
+ export { AudioReactiveCover } from './ReactiveCover';
247
+
248
+ // UI components
249
+ export { ShortcutsPopover as AudioShortcutsPopover } from './ShortcutsPopover';
250
+ export { VisualizationToggle } from './VisualizationToggle';
251
+ ```