@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,146 @@
1
+ # Execution Checklist
2
+
3
+ ## Pre-flight
4
+
5
+ - [ ] Run `pnpm check` - ensure current code compiles
6
+ - [ ] Commit current state as backup
7
+
8
+ ---
9
+
10
+ ## Phase 1: Create Folder Structure
11
+
12
+ ```bash
13
+ cd src/tools/AudioPlayer
14
+ mkdir -p types hooks context components/ReactiveCover effects utils
15
+ ```
16
+
17
+ - [ ] Create `types/` folder
18
+ - [ ] Create `hooks/` folder
19
+ - [ ] Create `context/` folder
20
+ - [ ] Create `components/` folder
21
+ - [ ] Create `components/ReactiveCover/` folder
22
+ - [ ] Create `effects/` subfolder (already exists, will add files)
23
+ - [ ] Create `utils/` folder
24
+
25
+ ---
26
+
27
+ ## Phase 2: Types Split
28
+
29
+ - [ ] Create `types/audio.ts`
30
+ - [ ] Create `types/components.ts`
31
+ - [ ] Create `types/effects.ts`
32
+ - [ ] Create `types/index.ts`
33
+ - [ ] Run `pnpm check`
34
+
35
+ ---
36
+
37
+ ## Phase 3: Hooks Extraction
38
+
39
+ - [ ] Create `hooks/useSharedWebAudio.ts` (extract from context.tsx)
40
+ - [ ] Create `hooks/useAudioAnalysis.ts` (extract from context.tsx)
41
+ - [ ] Move `useAudioHotkeys.ts` → `hooks/useAudioHotkeys.ts`
42
+ - [ ] Move `useAudioVisualization.tsx` → `hooks/useVisualization.tsx`
43
+ - [ ] Create `hooks/index.ts`
44
+ - [ ] Run `pnpm check`
45
+
46
+ ---
47
+
48
+ ## Phase 4: Context Refactoring
49
+
50
+ - [ ] Create `context/AudioProvider.tsx`
51
+ - [ ] Create `context/selectors.ts`
52
+ - [ ] Create `context/index.ts`
53
+ - [ ] Delete old `context.tsx`
54
+ - [ ] Run `pnpm check`
55
+
56
+ ---
57
+
58
+ ## Phase 5: Components Reorganization
59
+
60
+ - [ ] Move `AudioPlayer.tsx` → `components/AudioPlayer.tsx`
61
+ - [ ] Move `AudioEqualizer.tsx` → `components/AudioEqualizer.tsx`
62
+ - [ ] Move `SimpleAudioPlayer.tsx` → `components/SimpleAudioPlayer.tsx`
63
+ - [ ] Move `AudioShortcutsPopover.tsx` → `components/ShortcutsPopover.tsx`
64
+ - [ ] Move `VisualizationToggle.tsx` → `components/VisualizationToggle.tsx`
65
+ - [ ] Split `AudioReactiveCover.tsx` into `components/ReactiveCover/`
66
+ - [ ] Create `components/ReactiveCover/index.tsx`
67
+ - [ ] Create `components/ReactiveCover/GlowEffect.tsx`
68
+ - [ ] Create `components/ReactiveCover/OrbsEffect.tsx`
69
+ - [ ] Create `components/ReactiveCover/SpotlightEffect.tsx`
70
+ - [ ] Create `components/ReactiveCover/MeshEffect.tsx`
71
+ - [ ] Create `components/index.ts`
72
+ - [ ] Delete old component files from root
73
+ - [ ] Run `pnpm check`
74
+
75
+ ---
76
+
77
+ ## Phase 6: Effects Refactoring
78
+
79
+ - [ ] Create `effects/constants.ts`
80
+ - [ ] Create `effects/calculations.ts`
81
+ - [ ] Create `effects/animations.ts`
82
+ - [ ] Update `effects/index.ts`
83
+ - [ ] Run `pnpm check`
84
+
85
+ ---
86
+
87
+ ## Phase 7: Utils
88
+
89
+ - [ ] Create `utils/formatTime.ts`
90
+ - [ ] Create `utils/index.ts`
91
+ - [ ] Run `pnpm check`
92
+
93
+ ---
94
+
95
+ ## Phase 8: Update Main Index
96
+
97
+ - [ ] Rewrite `index.ts` with new import paths
98
+ - [ ] Run `pnpm check`
99
+
100
+ ---
101
+
102
+ ## Phase 9: Cleanup & Verification
103
+
104
+ - [ ] Delete old files:
105
+ - [ ] `context.tsx`
106
+ - [ ] `types.ts`
107
+ - [ ] `useAudioHotkeys.ts` (moved)
108
+ - [ ] `useAudioVisualization.tsx` (moved)
109
+ - [ ] `AudioPlayer.tsx` (moved)
110
+ - [ ] `AudioEqualizer.tsx` (moved)
111
+ - [ ] `SimpleAudioPlayer.tsx` (moved)
112
+ - [ ] `AudioShortcutsPopover.tsx` (moved)
113
+ - [ ] `VisualizationToggle.tsx` (moved)
114
+ - [ ] `AudioReactiveCover.tsx` (split)
115
+
116
+ - [ ] Run `pnpm check` - final verification
117
+ - [ ] Test in playground
118
+ - [ ] Commit refactoring
119
+
120
+ ---
121
+
122
+ ## Rollback Plan
123
+
124
+ If something goes wrong:
125
+
126
+ ```bash
127
+ git checkout HEAD -- src/tools/AudioPlayer/
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Files to Delete (after successful migration)
133
+
134
+ ```
135
+ src/tools/AudioPlayer/
136
+ ├── context.tsx # → context/AudioProvider.tsx + context/selectors.ts
137
+ ├── types.ts # → types/*.ts
138
+ ├── useAudioHotkeys.ts # → hooks/useAudioHotkeys.ts
139
+ ├── useAudioVisualization.tsx # → hooks/useVisualization.tsx
140
+ ├── AudioPlayer.tsx # → components/AudioPlayer.tsx
141
+ ├── AudioEqualizer.tsx # → components/AudioEqualizer.tsx
142
+ ├── SimpleAudioPlayer.tsx # → components/SimpleAudioPlayer.tsx
143
+ ├── AudioShortcutsPopover.tsx # → components/ShortcutsPopover.tsx
144
+ ├── VisualizationToggle.tsx # → components/VisualizationToggle.tsx
145
+ └── AudioReactiveCover.tsx # → components/ReactiveCover/*.tsx
146
+ ```
@@ -0,0 +1,325 @@
1
+ # AudioPlayer
2
+
3
+ Audio player with waveform visualization, built on WaveSurfer.js.
4
+
5
+ ## Features
6
+
7
+ - Waveform visualization with interactive seeking
8
+ - Audio-reactive cover effects (glow, orbs, spotlight, mesh)
9
+ - Real-time frequency equalizer
10
+ - Keyboard shortcuts
11
+ - Volume control with mute
12
+ - Skip forward/backward
13
+ - Playback speed control
14
+
15
+ ## Quick Start (Recommended)
16
+
17
+ ```tsx
18
+ import { SimpleAudioPlayer } from '@djangocfg/ui-nextjs';
19
+
20
+ // Minimal
21
+ <SimpleAudioPlayer src="https://example.com/audio.mp3" />
22
+
23
+ // With metadata
24
+ <SimpleAudioPlayer
25
+ src={audioUrl}
26
+ title="Track Title"
27
+ artist="Artist Name"
28
+ coverArt="/path/to/cover.jpg"
29
+ />
30
+
31
+ // Full customization
32
+ <SimpleAudioPlayer
33
+ src={audioUrl}
34
+ title="Track Title"
35
+ coverArt={coverUrl}
36
+ showWaveform
37
+ showEqualizer={false}
38
+ reactiveCover
39
+ variant="spotlight"
40
+ layout="horizontal"
41
+ />
42
+ ```
43
+
44
+ ### SimpleAudioPlayer Props
45
+
46
+ | Prop | Type | Default | Description |
47
+ |------|------|---------|-------------|
48
+ | `src` | `string` | required | Audio URL |
49
+ | `title` | `string` | - | Track title |
50
+ | `artist` | `string` | - | Artist name |
51
+ | `coverArt` | `string \| ReactNode` | - | Cover image URL or custom element |
52
+ | `coverSize` | `'sm' \| 'md' \| 'lg'` | `'md'` | Cover art size |
53
+ | `showWaveform` | `boolean` | `true` | Show waveform |
54
+ | `showEqualizer` | `boolean` | `false` | Show equalizer bars |
55
+ | `showTimer` | `boolean` | `true` | Show time display |
56
+ | `showVolume` | `boolean` | `true` | Show volume control |
57
+ | `reactiveCover` | `boolean` | `true` | Enable reactive effects |
58
+ | `variant` | `VisualizationVariant` | - | Effect variant |
59
+ | `intensity` | `EffectIntensity` | - | Effect intensity |
60
+ | `colorScheme` | `EffectColorScheme` | - | Effect colors |
61
+ | `autoPlay` | `boolean` | `false` | Auto-play on load |
62
+ | `layout` | `'vertical' \| 'horizontal'` | `'vertical'` | Layout direction |
63
+
64
+ ---
65
+
66
+ ## Advanced Usage
67
+
68
+ For full control, use `AudioProvider` + `AudioPlayer` directly:
69
+
70
+ ```tsx
71
+ import { useRef } from 'react';
72
+ import { AudioProvider, AudioPlayer } from '@djangocfg/ui-nextjs';
73
+
74
+ function MyAudioPlayer({ audioUrl }: { audioUrl: string }) {
75
+ const containerRef = useRef<HTMLDivElement>(null);
76
+
77
+ return (
78
+ <AudioProvider
79
+ source={{ uri: audioUrl }}
80
+ containerRef={containerRef}
81
+ >
82
+ <AudioPlayer
83
+ ref={containerRef}
84
+ showControls
85
+ showWaveform
86
+ showTimer
87
+ showVolume
88
+ />
89
+ </AudioProvider>
90
+ );
91
+ }
92
+ ```
93
+
94
+ ## Components
95
+
96
+ ### AudioProvider
97
+
98
+ Context provider for audio state. Wraps all audio components.
99
+
100
+ ```tsx
101
+ <AudioProvider
102
+ source={{ uri: 'https://example.com/audio.mp3' }}
103
+ containerRef={containerRef}
104
+ autoPlay={false}
105
+ waveformOptions={{
106
+ waveColor: 'hsl(217 91% 60% / 0.3)',
107
+ progressColor: 'hsl(217 91% 60%)',
108
+ height: 64,
109
+ barWidth: 3,
110
+ barRadius: 3,
111
+ barGap: 2,
112
+ }}
113
+ >
114
+ {children}
115
+ </AudioProvider>
116
+ ```
117
+
118
+ ### AudioPlayer
119
+
120
+ Main player component with waveform and controls.
121
+
122
+ | Prop | Type | Default | Description |
123
+ |------|------|---------|-------------|
124
+ | `showControls` | `boolean` | `true` | Show playback controls |
125
+ | `showWaveform` | `boolean` | `true` | Show waveform visualization |
126
+ | `showEqualizer` | `boolean` | `false` | Show equalizer animation |
127
+ | `showTimer` | `boolean` | `true` | Show time display |
128
+ | `showVolume` | `boolean` | `true` | Show volume slider |
129
+ | `className` | `string` | - | Additional CSS class |
130
+
131
+ ### AudioEqualizer
132
+
133
+ Real-time frequency visualizer with animated bars.
134
+
135
+ ```tsx
136
+ <AudioEqualizer
137
+ barCount={24}
138
+ height={48}
139
+ gap={2}
140
+ showPeaks
141
+ barColor="hsl(217 91% 60%)"
142
+ peakColor="hsl(217 91% 70%)"
143
+ />
144
+ ```
145
+
146
+ ### AudioReactiveCover
147
+
148
+ Album art wrapper with audio-reactive effects.
149
+
150
+ ```tsx
151
+ <AudioReactiveCover
152
+ variant="spotlight" // 'glow' | 'orbs' | 'spotlight' | 'mesh'
153
+ intensity="medium" // 'subtle' | 'medium' | 'strong'
154
+ colorScheme="primary" // 'primary' | 'vibrant' | 'cool' | 'warm'
155
+ onClick={() => nextVariant()}
156
+ >
157
+ <img src={coverArt} alt="Album cover" />
158
+ </AudioReactiveCover>
159
+ ```
160
+
161
+ ### VisualizationToggle
162
+
163
+ Button to cycle through visualization variants.
164
+
165
+ ```tsx
166
+ <VisualizationToggle compact />
167
+ ```
168
+
169
+ ## Hooks
170
+
171
+ ### useAudio
172
+
173
+ Access full audio state and controls.
174
+
175
+ ```tsx
176
+ const {
177
+ isReady,
178
+ isPlaying,
179
+ currentTime,
180
+ duration,
181
+ volume,
182
+ isMuted,
183
+ audioLevels, // { bass, mid, high, overall }
184
+ play,
185
+ pause,
186
+ togglePlay,
187
+ seek,
188
+ seekTo,
189
+ skip,
190
+ setVolume,
191
+ toggleMute,
192
+ restart,
193
+ } = useAudio();
194
+ ```
195
+
196
+ ### useAudioControls
197
+
198
+ Controls only (no re-render on time updates).
199
+
200
+ ```tsx
201
+ const { play, pause, togglePlay, skip, restart } = useAudioControls();
202
+ ```
203
+
204
+ ### useAudioState
205
+
206
+ Read-only playback state.
207
+
208
+ ```tsx
209
+ const { isPlaying, currentTime, duration, volume } = useAudioState();
210
+ ```
211
+
212
+ ### useAudioElement
213
+
214
+ Access audio element for custom visualizations.
215
+
216
+ ```tsx
217
+ const { audioElement, isPlaying, audioLevels } = useAudioElement();
218
+ ```
219
+
220
+ ### useAudioVisualization
221
+
222
+ Manage visualization settings (persisted in localStorage).
223
+
224
+ ```tsx
225
+ const {
226
+ settings, // { enabled, variant, intensity, colorScheme }
227
+ toggle,
228
+ setSetting,
229
+ nextVariant,
230
+ nextIntensity,
231
+ nextColorScheme,
232
+ reset,
233
+ } = useAudioVisualization();
234
+ ```
235
+
236
+ ### useAudioHotkeys
237
+
238
+ Enable keyboard shortcuts.
239
+
240
+ ```tsx
241
+ useAudioHotkeys({
242
+ enabled: true,
243
+ skipDuration: 10,
244
+ volumeStep: 0.1,
245
+ });
246
+ ```
247
+
248
+ ## Keyboard Shortcuts
249
+
250
+ | Key | Action |
251
+ |-----|--------|
252
+ | `Space` | Play/Pause |
253
+ | `←` / `J` | Skip 10s backward |
254
+ | `→` / `L` | Skip 10s forward |
255
+ | `↑` | Volume up |
256
+ | `↓` | Volume down |
257
+ | `M` | Mute/Unmute |
258
+ | `0-9` | Jump to 0-90% |
259
+
260
+ ## Waveform Options
261
+
262
+ ```typescript
263
+ interface WaveformOptions {
264
+ waveColor?: string; // Unplayed wave color
265
+ progressColor?: string; // Played wave color
266
+ cursorColor?: string; // Playhead cursor color
267
+ cursorWidth?: number; // Cursor width in px
268
+ height?: number; // Waveform height in px
269
+ barWidth?: number; // Bar width in px
270
+ barRadius?: number; // Bar corner radius
271
+ barGap?: number; // Gap between bars
272
+ }
273
+ ```
274
+
275
+ ## Effect Variants
276
+
277
+ | Variant | Description |
278
+ |---------|-------------|
279
+ | `spotlight` | Rotating conic gradient with bass pulse |
280
+ | `glow` | Multi-layered radial glows from edges |
281
+ | `orbs` | Floating orbs that react to frequencies |
282
+ | `mesh` | Large gradient blobs with movement |
283
+ | `none` | Effects disabled |
284
+
285
+ ## Architecture
286
+
287
+ ```
288
+ AudioPlayer/
289
+ ├── index.ts # Public API exports
290
+ ├── types/
291
+ │ ├── index.ts # Type re-exports
292
+ │ ├── audio.ts # Audio state & source types
293
+ │ ├── components.ts # Component prop types
294
+ │ └── effects.ts # Visualization effect types
295
+ ├── hooks/
296
+ │ ├── index.ts
297
+ │ ├── useAudioHotkeys.ts # Keyboard shortcuts
298
+ │ ├── useVisualization.tsx # Visualization settings
299
+ │ ├── useAudioAnalysis.ts # Web Audio frequency analysis
300
+ │ └── useSharedWebAudio.ts # Shared AudioContext
301
+ ├── utils/
302
+ │ ├── index.ts
303
+ │ └── formatTime.ts # Time formatting
304
+ ├── context/
305
+ │ ├── index.ts
306
+ │ ├── AudioProvider.tsx # Audio state provider
307
+ │ └── selectors.ts # useAudio, useAudioControls hooks
308
+ ├── components/
309
+ │ ├── index.ts
310
+ │ ├── AudioPlayer.tsx # Main player component
311
+ │ ├── SimpleAudioPlayer.tsx # All-in-one wrapper
312
+ │ ├── AudioEqualizer.tsx # Frequency bars
313
+ │ ├── AudioShortcutsPopover.tsx # Shortcuts help
314
+ │ ├── VisualizationToggle.tsx # Effect toggle button
315
+ │ └── ReactiveCover/
316
+ │ ├── index.ts
317
+ │ ├── AudioReactiveCover.tsx # Reactive album art
318
+ │ └── effects/ # Effect components
319
+ │ ├── GlowEffect.tsx
320
+ │ ├── OrbsEffect.tsx
321
+ │ ├── SpotlightEffect.tsx
322
+ │ └── MeshEffect.tsx
323
+ └── effects/
324
+ └── index.ts # Effect calculations
325
+ ```
@@ -0,0 +1,200 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * AudioEqualizer - Real-time frequency visualizer with animated bars
5
+ *
6
+ * Uses shared Web Audio context from AudioProvider for real-time frequency analysis.
7
+ * This prevents "InvalidStateError" from creating multiple MediaElementSourceNodes.
8
+ *
9
+ * Features:
10
+ * - Real-time frequency visualization
11
+ * - Configurable number of bars
12
+ * - Theme-aware colors (dark/light support)
13
+ * - Smooth animations with CSS transitions
14
+ * - Peak hold indicators
15
+ * - Pre-allocated buffers for performance
16
+ */
17
+
18
+ import { useEffect, useRef, useState, useCallback } from 'react';
19
+ import { cn } from '@djangocfg/ui-nextjs';
20
+ import { useAudioElement } from '../context';
21
+ import type { AudioEqualizerProps } from '../types';
22
+
23
+ // =============================================================================
24
+ // CONSTANTS
25
+ // =============================================================================
26
+
27
+ const DEFAULT_BAR_COUNT = 24;
28
+ const DEFAULT_HEIGHT = 48;
29
+ const DEFAULT_GAP = 2;
30
+ const PEAK_DECAY_RATE = 0.02;
31
+ const PEAK_HOLD_TIME = 500;
32
+
33
+ // =============================================================================
34
+ // COMPONENT
35
+ // =============================================================================
36
+
37
+ export function AudioEqualizer({
38
+ barCount = DEFAULT_BAR_COUNT,
39
+ height = DEFAULT_HEIGHT,
40
+ gap = DEFAULT_GAP,
41
+ showPeaks = true,
42
+ barColor,
43
+ peakColor,
44
+ className,
45
+ }: AudioEqualizerProps) {
46
+ // Get shared audio context from AudioProvider
47
+ const { sharedAudio, isPlaying } = useAudioElement();
48
+
49
+ const [frequencies, setFrequencies] = useState<number[]>(() =>
50
+ new Array(barCount).fill(0)
51
+ );
52
+ const [peaks, setPeaks] = useState<number[]>(() =>
53
+ new Array(barCount).fill(0)
54
+ );
55
+
56
+ // Refs for analyzer and animation
57
+ const analyserRef = useRef<AnalyserNode | null>(null);
58
+ const animationRef = useRef<number | null>(null);
59
+ const peakTimersRef = useRef<number[]>(new Array(barCount).fill(0));
60
+ // Pre-allocated buffer for frequency data (prevents allocation per frame)
61
+ const dataArrayRef = useRef<Uint8Array | null>(null);
62
+
63
+ // Cleanup function
64
+ const cleanup = useCallback(() => {
65
+ if (animationRef.current) {
66
+ cancelAnimationFrame(animationRef.current);
67
+ animationRef.current = null;
68
+ }
69
+ }, []);
70
+
71
+ // Create analyzer using shared context
72
+ useEffect(() => {
73
+ // Wait for shared audio to be ready
74
+ if (!sharedAudio.sourceNode) {
75
+ return;
76
+ }
77
+
78
+ // Only create analyzer once
79
+ if (analyserRef.current) {
80
+ return;
81
+ }
82
+
83
+ const analyser = sharedAudio.createAnalyser({ fftSize: 64, smoothing: 0.8 });
84
+ if (analyser) {
85
+ analyserRef.current = analyser;
86
+ // Pre-allocate data array
87
+ dataArrayRef.current = new Uint8Array(analyser.frequencyBinCount);
88
+ }
89
+
90
+ return () => {
91
+ if (analyserRef.current) {
92
+ sharedAudio.disconnectAnalyser(analyserRef.current);
93
+ analyserRef.current = null;
94
+ dataArrayRef.current = null;
95
+ }
96
+ };
97
+ }, [sharedAudio.sourceNode, sharedAudio.createAnalyser, sharedAudio.disconnectAnalyser]);
98
+
99
+ // Animation loop
100
+ useEffect(() => {
101
+ if (!isPlaying || !analyserRef.current || !dataArrayRef.current) {
102
+ cleanup();
103
+ setFrequencies(new Array(barCount).fill(0));
104
+ return;
105
+ }
106
+
107
+ const analyser = analyserRef.current;
108
+ const dataArray = dataArrayRef.current;
109
+
110
+ const animate = () => {
111
+ analyser.getByteFrequencyData(dataArray as Uint8Array<ArrayBuffer>);
112
+
113
+ const step = Math.floor(dataArray.length / barCount);
114
+ const newFrequencies: number[] = [];
115
+ const newPeaks: number[] = [...peaks];
116
+ const now = Date.now();
117
+
118
+ for (let i = 0; i < barCount; i++) {
119
+ let sum = 0;
120
+ for (let j = 0; j < step; j++) {
121
+ sum += dataArray[i * step + j] || 0;
122
+ }
123
+ const value = sum / step / 255;
124
+ newFrequencies.push(value);
125
+
126
+ if (showPeaks) {
127
+ if (value > newPeaks[i]) {
128
+ newPeaks[i] = value;
129
+ peakTimersRef.current[i] = now;
130
+ } else if (now - peakTimersRef.current[i] > PEAK_HOLD_TIME) {
131
+ newPeaks[i] = Math.max(0, newPeaks[i] - PEAK_DECAY_RATE);
132
+ }
133
+ }
134
+ }
135
+
136
+ setFrequencies(newFrequencies);
137
+ if (showPeaks) {
138
+ setPeaks(newPeaks);
139
+ }
140
+
141
+ animationRef.current = requestAnimationFrame(animate);
142
+ };
143
+
144
+ animationRef.current = requestAnimationFrame(animate);
145
+
146
+ return cleanup;
147
+ }, [isPlaying, barCount, showPeaks, cleanup, peaks]);
148
+
149
+ // Reset peaks array when barCount changes
150
+ useEffect(() => {
151
+ setPeaks(new Array(barCount).fill(0));
152
+ peakTimersRef.current = new Array(barCount).fill(0);
153
+ }, [barCount]);
154
+
155
+ // Calculate bar width
156
+ const totalGaps = (barCount - 1) * gap;
157
+ const barWidth = `calc((100% - ${totalGaps}px) / ${barCount})`;
158
+
159
+ return (
160
+ <div
161
+ className={cn('relative flex items-end justify-between', className)}
162
+ style={{ height }}
163
+ >
164
+ {frequencies.map((freq, index) => (
165
+ <div
166
+ key={index}
167
+ className="relative flex flex-col justify-end"
168
+ style={{
169
+ width: barWidth,
170
+ height: '100%',
171
+ }}
172
+ >
173
+ {/* Peak indicator */}
174
+ {showPeaks && peaks[index] > 0.01 && (
175
+ <div
176
+ className="absolute w-full transition-all duration-75"
177
+ style={{
178
+ height: 2,
179
+ bottom: `${peaks[index] * 100}%`,
180
+ backgroundColor: peakColor || 'hsl(217 91% 70%)',
181
+ }}
182
+ />
183
+ )}
184
+
185
+ {/* Frequency bar */}
186
+ <div
187
+ className="w-full rounded-t-sm transition-all duration-75"
188
+ style={{
189
+ height: `${Math.max(freq * 100, 2)}%`,
190
+ backgroundColor: barColor || 'hsl(217 91% 60%)',
191
+ opacity: 0.3 + freq * 0.7,
192
+ }}
193
+ />
194
+ </div>
195
+ ))}
196
+ </div>
197
+ );
198
+ }
199
+
200
+ export default AudioEqualizer;