@djangocfg/ui-nextjs 2.1.66 → 2.1.68

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 (90) hide show
  1. package/package.json +8 -6
  2. package/src/stores/index.ts +8 -0
  3. package/src/stores/mediaCache.ts +474 -0
  4. package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +148 -0
  5. package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +301 -0
  6. package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +281 -0
  7. package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +328 -0
  8. package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +251 -0
  9. package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +427 -0
  10. package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +193 -0
  11. package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +146 -0
  12. package/src/tools/AudioPlayer/README.md +35 -11
  13. package/src/tools/AudioPlayer/{AudioEqualizer.tsx → components/AudioEqualizer.tsx} +29 -64
  14. package/src/tools/AudioPlayer/{AudioPlayer.tsx → components/AudioPlayer.tsx} +22 -14
  15. package/src/tools/AudioPlayer/{AudioShortcutsPopover.tsx → components/AudioShortcutsPopover.tsx} +6 -2
  16. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +147 -0
  17. package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
  18. package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
  19. package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
  20. package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
  21. package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
  22. package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
  23. package/src/tools/AudioPlayer/{SimpleAudioPlayer.tsx → components/SimpleAudioPlayer.tsx} +12 -7
  24. package/src/tools/AudioPlayer/{VisualizationToggle.tsx → components/VisualizationToggle.tsx} +2 -6
  25. package/src/tools/AudioPlayer/components/index.ts +21 -0
  26. package/src/tools/AudioPlayer/context/AudioProvider.tsx +292 -0
  27. package/src/tools/AudioPlayer/context/index.ts +11 -0
  28. package/src/tools/AudioPlayer/context/selectors.ts +96 -0
  29. package/src/tools/AudioPlayer/hooks/index.ts +29 -0
  30. package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
  31. package/src/tools/AudioPlayer/{useAudioHotkeys.ts → hooks/useAudioHotkeys.ts} +11 -4
  32. package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
  33. package/src/tools/AudioPlayer/{useAudioVisualization.tsx → hooks/useVisualization.tsx} +11 -5
  34. package/src/tools/AudioPlayer/index.ts +104 -49
  35. package/src/tools/AudioPlayer/types/audio.ts +107 -0
  36. package/src/tools/AudioPlayer/{types.ts → types/components.ts} +20 -84
  37. package/src/tools/AudioPlayer/types/effects.ts +73 -0
  38. package/src/tools/AudioPlayer/types/index.ts +35 -0
  39. package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
  40. package/src/tools/AudioPlayer/utils/index.ts +5 -0
  41. package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
  42. package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
  43. package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
  44. package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
  45. package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
  46. package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
  47. package/src/tools/ImageViewer/README.md +16 -3
  48. package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
  49. package/src/tools/ImageViewer/components/ImageToolbar.tsx +150 -0
  50. package/src/tools/ImageViewer/components/ImageViewer.tsx +235 -0
  51. package/src/tools/ImageViewer/components/index.ts +7 -0
  52. package/src/tools/ImageViewer/hooks/index.ts +9 -0
  53. package/src/tools/ImageViewer/hooks/useImageLoading.ts +153 -0
  54. package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
  55. package/src/tools/ImageViewer/index.ts +47 -3
  56. package/src/tools/ImageViewer/types.ts +75 -0
  57. package/src/tools/ImageViewer/utils/constants.ts +59 -0
  58. package/src/tools/ImageViewer/utils/index.ts +16 -0
  59. package/src/tools/ImageViewer/utils/lqip.ts +47 -0
  60. package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
  61. package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
  62. package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
  63. package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
  64. package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
  65. package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
  66. package/src/tools/VideoPlayer/README.md +26 -10
  67. package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
  68. package/src/tools/VideoPlayer/{VideoErrorFallback.tsx → components/VideoErrorFallback.tsx} +2 -2
  69. package/src/tools/VideoPlayer/{VideoPlayer.tsx → components/VideoPlayer.tsx} +4 -5
  70. package/src/tools/VideoPlayer/components/index.ts +14 -0
  71. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
  72. package/src/tools/VideoPlayer/context/index.ts +8 -0
  73. package/src/tools/VideoPlayer/hooks/index.ts +9 -0
  74. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +109 -0
  75. package/src/tools/VideoPlayer/index.ts +29 -20
  76. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +118 -28
  77. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +89 -11
  78. package/src/tools/VideoPlayer/types/index.ts +38 -0
  79. package/src/tools/VideoPlayer/types/player.ts +116 -0
  80. package/src/tools/VideoPlayer/types/provider.ts +93 -0
  81. package/src/tools/VideoPlayer/types/sources.ts +97 -0
  82. package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
  83. package/src/tools/VideoPlayer/utils/index.ts +11 -0
  84. package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
  85. package/src/tools/index.ts +10 -0
  86. package/src/tools/AudioPlayer/AudioReactiveCover.tsx +0 -389
  87. package/src/tools/AudioPlayer/context.tsx +0 -426
  88. package/src/tools/ImageViewer/ImageViewer.tsx +0 -416
  89. package/src/tools/VideoPlayer/VideoPlayerContext.tsx +0 -125
  90. package/src/tools/VideoPlayer/types.ts +0 -367
@@ -0,0 +1,301 @@
1
+ # Phase 2: Types Split
2
+
3
+ ## Source: `types.ts` (185 lines)
4
+
5
+ Split into 3 files by domain.
6
+
7
+ ---
8
+
9
+ ## `types/audio.ts`
10
+
11
+ Core audio-related types.
12
+
13
+ ```typescript
14
+ import type WaveSurfer from 'wavesurfer.js';
15
+ import type { AudioLevels } from '../effects';
16
+
17
+ // =============================================================================
18
+ // AUDIO SOURCE
19
+ // =============================================================================
20
+
21
+ export interface AudioSource {
22
+ uri: string;
23
+ }
24
+
25
+ // =============================================================================
26
+ // PLAYBACK STATE
27
+ // =============================================================================
28
+
29
+ export interface PlaybackStatus {
30
+ isLoaded: boolean;
31
+ isPlaying: boolean;
32
+ duration: number;
33
+ currentTime: number;
34
+ volume: number;
35
+ isMuted: boolean;
36
+ }
37
+
38
+ // =============================================================================
39
+ // WAVEFORM OPTIONS
40
+ // =============================================================================
41
+
42
+ export interface WaveformOptions {
43
+ waveColor?: string;
44
+ progressColor?: string;
45
+ height?: number;
46
+ barWidth?: number;
47
+ barRadius?: number;
48
+ barGap?: number;
49
+ cursorWidth?: number;
50
+ cursorColor?: string;
51
+ }
52
+
53
+ // =============================================================================
54
+ // EQUALIZER OPTIONS
55
+ // =============================================================================
56
+
57
+ export interface EqualizerOptions {
58
+ barCount?: number;
59
+ height?: number;
60
+ gap?: number;
61
+ showPeaks?: boolean;
62
+ barColor?: string;
63
+ peakColor?: string;
64
+ }
65
+
66
+ // =============================================================================
67
+ // SHARED WEB AUDIO CONTEXT
68
+ // =============================================================================
69
+
70
+ export interface SharedWebAudioContext {
71
+ audioContext: AudioContext | null;
72
+ sourceNode: MediaElementAudioSourceNode | null;
73
+ createAnalyser: (options?: { fftSize?: number; smoothing?: number }) => AnalyserNode | null;
74
+ disconnectAnalyser: (analyser: AnalyserNode) => void;
75
+ }
76
+
77
+ // =============================================================================
78
+ // AUDIO CONTEXT STATE
79
+ // =============================================================================
80
+
81
+ export interface AudioContextState {
82
+ // Core instances
83
+ wavesurfer: WaveSurfer | null;
84
+ audioElement: HTMLMediaElement | null;
85
+ sharedAudio: SharedWebAudioContext;
86
+
87
+ // Playback state
88
+ isReady: boolean;
89
+ isPlaying: boolean;
90
+ currentTime: number;
91
+ duration: number;
92
+ volume: number;
93
+ isMuted: boolean;
94
+ isLooping: boolean;
95
+
96
+ // Audio analysis
97
+ audioLevels: AudioLevels;
98
+
99
+ // Actions
100
+ play: () => Promise<void>;
101
+ pause: () => void;
102
+ togglePlay: () => void;
103
+ seek: (time: number) => void;
104
+ seekTo: (progress: number) => void;
105
+ skip: (seconds: number) => void;
106
+ setVolume: (volume: number) => void;
107
+ toggleMute: () => void;
108
+ toggleLoop: () => void;
109
+ setLoop: (enabled: boolean) => void;
110
+ restart: () => void;
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ## `types/components.ts`
117
+
118
+ Component props types.
119
+
120
+ ```typescript
121
+ import type { CSSProperties } from 'react';
122
+ import type { WaveformOptions, EqualizerOptions, AudioSource, PlaybackStatus } from './audio';
123
+
124
+ // =============================================================================
125
+ // AUDIO PLAYER PROPS
126
+ // =============================================================================
127
+
128
+ export interface AudioPlayerProps {
129
+ showControls?: boolean;
130
+ showWaveform?: boolean;
131
+ showEqualizer?: boolean;
132
+ showTimer?: boolean;
133
+ showVolume?: boolean;
134
+ showLoop?: boolean;
135
+ waveformOptions?: WaveformOptions;
136
+ equalizerOptions?: EqualizerOptions;
137
+ className?: string;
138
+ style?: CSSProperties;
139
+ }
140
+
141
+ // =============================================================================
142
+ // AUDIO EQUALIZER PROPS
143
+ // =============================================================================
144
+
145
+ export interface AudioEqualizerProps {
146
+ barCount?: number;
147
+ height?: number;
148
+ gap?: number;
149
+ showPeaks?: boolean;
150
+ barColor?: string;
151
+ peakColor?: string;
152
+ className?: string;
153
+ }
154
+
155
+ // =============================================================================
156
+ // AUDIO REACTIVE COVER PROPS
157
+ // =============================================================================
158
+
159
+ export interface AudioReactiveCoverProps {
160
+ variant?: 'glow' | 'orbs' | 'spotlight' | 'mesh';
161
+ intensity?: 'subtle' | 'medium' | 'strong';
162
+ colorScheme?: 'primary' | 'vibrant' | 'cool' | 'warm';
163
+ }
164
+
165
+ // =============================================================================
166
+ // AUDIO VIEWER PROPS (legacy)
167
+ // =============================================================================
168
+
169
+ export interface AudioViewerProps {
170
+ source: AudioSource;
171
+ autoPlay?: boolean;
172
+ showControls?: boolean;
173
+ showWaveform?: boolean;
174
+ showEqualizer?: boolean;
175
+ showTimer?: boolean;
176
+ showVolume?: boolean;
177
+ waveformOptions?: WaveformOptions;
178
+ equalizerOptions?: EqualizerOptions;
179
+ onPlaybackStatusUpdate?: (status: PlaybackStatus) => void;
180
+ className?: string;
181
+ style?: CSSProperties;
182
+ }
183
+ ```
184
+
185
+ ---
186
+
187
+ ## `types/effects.ts`
188
+
189
+ Effect-related types (moved from effects/index.ts).
190
+
191
+ ```typescript
192
+ // =============================================================================
193
+ // EFFECT TYPES
194
+ // =============================================================================
195
+
196
+ export type EffectVariant = 'glow' | 'orbs' | 'spotlight' | 'mesh';
197
+ export type EffectIntensity = 'subtle' | 'medium' | 'strong';
198
+ export type EffectColorScheme = 'primary' | 'vibrant' | 'cool' | 'warm';
199
+
200
+ export interface AudioLevels {
201
+ bass: number;
202
+ mid: number;
203
+ high: number;
204
+ overall: number;
205
+ }
206
+
207
+ export interface EffectConfig {
208
+ opacity: number;
209
+ scale: number;
210
+ blur: string;
211
+ }
212
+
213
+ export interface EffectColors {
214
+ colors: string[];
215
+ hueShift: number;
216
+ }
217
+
218
+ export interface EffectLayer {
219
+ inset: number;
220
+ opacity: number;
221
+ scale: number;
222
+ background: string;
223
+ blur: string;
224
+ animation?: string;
225
+ }
226
+
227
+ export interface Orb {
228
+ x: number;
229
+ y: number;
230
+ size: number;
231
+ color: string;
232
+ opacity: number;
233
+ scale: number;
234
+ }
235
+
236
+ export interface MeshGradient {
237
+ width: string;
238
+ height: string;
239
+ top?: string;
240
+ bottom?: string;
241
+ left?: string;
242
+ right?: string;
243
+ color: string;
244
+ opacity: number;
245
+ scale: number;
246
+ rotation: number;
247
+ blur: string;
248
+ isCenter?: boolean;
249
+ }
250
+
251
+ export interface SpotlightData {
252
+ rotation: number;
253
+ inset: number;
254
+ colors: Array<{ color: string; opacity: number }>;
255
+ pulseInset: number;
256
+ pulseOpacity: number;
257
+ pulseScale: number;
258
+ ringOpacity: number;
259
+ ringScale: number;
260
+ }
261
+ ```
262
+
263
+ ---
264
+
265
+ ## `types/index.ts`
266
+
267
+ Re-export all types.
268
+
269
+ ```typescript
270
+ // Audio types
271
+ export type {
272
+ AudioSource,
273
+ PlaybackStatus,
274
+ WaveformOptions,
275
+ EqualizerOptions,
276
+ SharedWebAudioContext,
277
+ AudioContextState,
278
+ } from './audio';
279
+
280
+ // Component props
281
+ export type {
282
+ AudioPlayerProps,
283
+ AudioEqualizerProps,
284
+ AudioReactiveCoverProps,
285
+ AudioViewerProps,
286
+ } from './components';
287
+
288
+ // Effect types
289
+ export type {
290
+ EffectVariant,
291
+ EffectIntensity,
292
+ EffectColorScheme,
293
+ AudioLevels,
294
+ EffectConfig,
295
+ EffectColors,
296
+ EffectLayer,
297
+ Orb,
298
+ MeshGradient,
299
+ SpotlightData,
300
+ } from './effects';
301
+ ```
@@ -0,0 +1,281 @@
1
+ # Phase 3: Hooks Extraction
2
+
3
+ ## Source: `context.tsx` (lines 25-229)
4
+
5
+ Extract two internal hooks from context.tsx.
6
+
7
+ ---
8
+
9
+ ## `hooks/useSharedWebAudio.ts`
10
+
11
+ Web Audio API context management. Prevents "InvalidStateError" from multiple MediaElementSourceNodes.
12
+
13
+ ```typescript
14
+ 'use client';
15
+
16
+ import { useRef, useEffect, useCallback } from 'react';
17
+ import type { SharedWebAudioContext } from '../types';
18
+
19
+ /**
20
+ * Manages a shared Web Audio context and source node.
21
+ * All analyzers share the same source to prevent InvalidStateError.
22
+ */
23
+ export function useSharedWebAudio(audioElement: HTMLMediaElement | null): SharedWebAudioContext {
24
+ const audioContextRef = useRef<AudioContext | null>(null);
25
+ const sourceRef = useRef<MediaElementAudioSourceNode | null>(null);
26
+ const connectedElementRef = useRef<HTMLMediaElement | null>(null);
27
+ const analyserNodesRef = useRef<Set<AnalyserNode>>(new Set());
28
+
29
+ // Initialize Web Audio on first play
30
+ useEffect(() => {
31
+ if (!audioElement) return;
32
+
33
+ if (connectedElementRef.current === audioElement && audioContextRef.current) {
34
+ return;
35
+ }
36
+
37
+ const initAudio = () => {
38
+ try {
39
+ if (!audioContextRef.current) {
40
+ const AudioContextClass = window.AudioContext ||
41
+ (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
42
+ audioContextRef.current = new AudioContextClass();
43
+ }
44
+
45
+ const audioContext = audioContextRef.current;
46
+
47
+ if (connectedElementRef.current !== audioElement) {
48
+ if (sourceRef.current) {
49
+ try { sourceRef.current.disconnect(); } catch { /* ignore */ }
50
+ }
51
+
52
+ sourceRef.current = audioContext.createMediaElementSource(audioElement);
53
+ sourceRef.current.connect(audioContext.destination);
54
+ connectedElementRef.current = audioElement;
55
+ }
56
+ } catch (error) {
57
+ console.warn('[SharedWebAudio] Could not initialize:', error);
58
+ }
59
+ };
60
+
61
+ const handlePlay = () => {
62
+ initAudio();
63
+ if (audioContextRef.current?.state === 'suspended') {
64
+ audioContextRef.current.resume();
65
+ }
66
+ };
67
+
68
+ audioElement.addEventListener('play', handlePlay);
69
+ if (!audioElement.paused) {
70
+ handlePlay();
71
+ }
72
+
73
+ return () => {
74
+ audioElement.removeEventListener('play', handlePlay);
75
+ };
76
+ }, [audioElement]);
77
+
78
+ const createAnalyser = useCallback((options?: { fftSize?: number; smoothing?: number }): AnalyserNode | null => {
79
+ if (!audioContextRef.current || !sourceRef.current) return null;
80
+
81
+ try {
82
+ const analyser = audioContextRef.current.createAnalyser();
83
+ analyser.fftSize = options?.fftSize ?? 256;
84
+ analyser.smoothingTimeConstant = options?.smoothing ?? 0.85;
85
+
86
+ sourceRef.current.connect(analyser);
87
+ analyser.connect(audioContextRef.current.destination);
88
+
89
+ analyserNodesRef.current.add(analyser);
90
+ return analyser;
91
+ } catch (error) {
92
+ console.warn('[SharedWebAudio] Could not create analyser:', error);
93
+ return null;
94
+ }
95
+ }, []);
96
+
97
+ const disconnectAnalyser = useCallback((analyser: AnalyserNode) => {
98
+ try {
99
+ analyser.disconnect();
100
+ analyserNodesRef.current.delete(analyser);
101
+ } catch { /* ignore */ }
102
+ }, []);
103
+
104
+ return {
105
+ audioContext: audioContextRef.current,
106
+ sourceNode: sourceRef.current,
107
+ createAnalyser,
108
+ disconnectAnalyser,
109
+ };
110
+ }
111
+ ```
112
+
113
+ ---
114
+
115
+ ## `hooks/useAudioAnalysis.ts`
116
+
117
+ Real-time audio frequency analysis for reactive effects.
118
+
119
+ ```typescript
120
+ 'use client';
121
+
122
+ import { useState, useRef, useEffect, useCallback } from 'react';
123
+ import type { SharedWebAudioContext, AudioLevels } from '../types';
124
+
125
+ /**
126
+ * Analyzes audio frequencies for bass, mid, high, and overall levels.
127
+ * Uses shared Web Audio context to prevent duplicate source nodes.
128
+ */
129
+ export function useAudioAnalysis(
130
+ sharedAudio: SharedWebAudioContext,
131
+ isPlaying: boolean
132
+ ): AudioLevels {
133
+ const [levels, setLevels] = useState<AudioLevels>({ bass: 0, mid: 0, high: 0, overall: 0 });
134
+ const analyserRef = useRef<AnalyserNode | null>(null);
135
+ const animationRef = useRef<number | null>(null);
136
+ const dataArrayRef = useRef<Uint8Array | null>(null);
137
+
138
+ const cleanup = useCallback(() => {
139
+ if (animationRef.current) {
140
+ cancelAnimationFrame(animationRef.current);
141
+ animationRef.current = null;
142
+ }
143
+ }, []);
144
+
145
+ // Create analyser when shared audio is ready
146
+ useEffect(() => {
147
+ if (!sharedAudio.sourceNode || analyserRef.current) return;
148
+
149
+ const analyser = sharedAudio.createAnalyser({ fftSize: 256, smoothing: 0.85 });
150
+ if (analyser) {
151
+ analyserRef.current = analyser;
152
+ dataArrayRef.current = new Uint8Array(analyser.frequencyBinCount);
153
+ }
154
+
155
+ return () => {
156
+ if (analyserRef.current) {
157
+ sharedAudio.disconnectAnalyser(analyserRef.current);
158
+ analyserRef.current = null;
159
+ dataArrayRef.current = null;
160
+ }
161
+ };
162
+ }, [sharedAudio.sourceNode, sharedAudio.createAnalyser, sharedAudio.disconnectAnalyser]);
163
+
164
+ // Animation loop
165
+ useEffect(() => {
166
+ if (!isPlaying || !analyserRef.current || !dataArrayRef.current) {
167
+ cleanup();
168
+ // Smooth fade out
169
+ setLevels(prev => ({
170
+ bass: prev.bass * 0.95 < 0.01 ? 0 : prev.bass * 0.95,
171
+ mid: prev.mid * 0.95,
172
+ high: prev.high * 0.95,
173
+ overall: prev.overall * 0.95,
174
+ }));
175
+ return;
176
+ }
177
+
178
+ const analyser = analyserRef.current;
179
+ const dataArray = dataArrayRef.current;
180
+
181
+ const animate = () => {
182
+ analyser.getByteFrequencyData(dataArray);
183
+ const binCount = dataArray.length;
184
+
185
+ // Bass (0-15%)
186
+ const bassEnd = Math.floor(binCount * 0.15);
187
+ let bassSum = 0;
188
+ for (let i = 0; i < bassEnd; i++) bassSum += dataArray[i];
189
+ const bass = bassSum / bassEnd / 255;
190
+
191
+ // Mids (15-50%)
192
+ const midStart = bassEnd;
193
+ const midEnd = Math.floor(binCount * 0.5);
194
+ let midSum = 0;
195
+ for (let i = midStart; i < midEnd; i++) midSum += dataArray[i];
196
+ const mid = midSum / (midEnd - midStart) / 255;
197
+
198
+ // Highs (50-100%)
199
+ const highStart = midEnd;
200
+ let highSum = 0;
201
+ for (let i = highStart; i < binCount; i++) highSum += dataArray[i];
202
+ const high = highSum / (binCount - highStart) / 255;
203
+
204
+ // Overall
205
+ let totalSum = 0;
206
+ for (let i = 0; i < binCount; i++) totalSum += dataArray[i];
207
+ const overall = totalSum / binCount / 255;
208
+
209
+ // Smooth with lerp
210
+ setLevels(prev => ({
211
+ bass: prev.bass * 0.7 + bass * 0.3,
212
+ mid: prev.mid * 0.7 + mid * 0.3,
213
+ high: prev.high * 0.7 + high * 0.3,
214
+ overall: prev.overall * 0.7 + overall * 0.3,
215
+ }));
216
+
217
+ animationRef.current = requestAnimationFrame(animate);
218
+ };
219
+
220
+ animationRef.current = requestAnimationFrame(animate);
221
+ return cleanup;
222
+ }, [isPlaying, cleanup]);
223
+
224
+ return levels;
225
+ }
226
+ ```
227
+
228
+ ---
229
+
230
+ ## `hooks/useAudioHotkeys.ts`
231
+
232
+ Move existing file, update imports.
233
+
234
+ ```typescript
235
+ // Same content, just update imports:
236
+ // - import type { ... } from '../types';
237
+ // - import { useAudioControls, useAudioState } from '../context';
238
+ ```
239
+
240
+ ---
241
+
242
+ ## `hooks/useVisualization.tsx`
243
+
244
+ Rename from `useAudioVisualization.tsx`, update imports.
245
+
246
+ ```typescript
247
+ // Same content, just update imports
248
+ // Export both old and new names for backward compatibility
249
+ export { useVisualization as useAudioVisualization };
250
+ ```
251
+
252
+ ---
253
+
254
+ ## `hooks/index.ts`
255
+
256
+ ```typescript
257
+ // Internal hooks (used by provider)
258
+ export { useSharedWebAudio } from './useSharedWebAudio';
259
+ export { useAudioAnalysis } from './useAudioAnalysis';
260
+
261
+ // Public hooks
262
+ export { useAudioHotkeys, AUDIO_SHORTCUTS } from './useAudioHotkeys';
263
+ export type { AudioHotkeyOptions, ShortcutItem, ShortcutGroup } from './useAudioHotkeys';
264
+
265
+ export {
266
+ useVisualization,
267
+ useAudioVisualization, // backward compat alias
268
+ VisualizationProvider,
269
+ VARIANT_INFO,
270
+ INTENSITY_INFO,
271
+ COLOR_SCHEME_INFO,
272
+ } from './useVisualization';
273
+ export type {
274
+ VisualizationSettings,
275
+ VisualizationVariant,
276
+ VisualizationIntensity,
277
+ VisualizationColorScheme,
278
+ UseAudioVisualizationReturn,
279
+ VisualizationProviderProps,
280
+ } from './useVisualization';
281
+ ```