@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,231 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * AudioPlayer - Audio playback controls and waveform display
5
+ *
6
+ * Uses AudioContext for state management
7
+ * Renders waveform (via container ref), controls, timer, and equalizer
8
+ * Supports keyboard shortcuts for playback control
9
+ */
10
+
11
+ import { forwardRef } from 'react';
12
+ import {
13
+ Play,
14
+ Pause,
15
+ RotateCcw,
16
+ SkipBack,
17
+ SkipForward,
18
+ Volume2,
19
+ VolumeX,
20
+ Loader2,
21
+ Repeat,
22
+ } from 'lucide-react';
23
+ import { Button, Slider, cn } from '@djangocfg/ui-nextjs';
24
+ import { useAudio } from '../context';
25
+ import { AudioEqualizer } from './AudioEqualizer';
26
+ import { useAudioHotkeys } from '../hooks';
27
+ import { AudioShortcutsPopover } from './AudioShortcutsPopover';
28
+ import { formatTime } from '../utils';
29
+ import type { AudioPlayerProps } from '../types';
30
+
31
+ // =============================================================================
32
+ // COMPONENT
33
+ // =============================================================================
34
+
35
+ export const AudioPlayer = forwardRef<HTMLDivElement, AudioPlayerProps>(
36
+ function AudioPlayer(
37
+ {
38
+ showControls = true,
39
+ showWaveform = true,
40
+ showEqualizer = false,
41
+ showTimer = true,
42
+ showVolume = true,
43
+ showLoop = true,
44
+ equalizerOptions = {},
45
+ className,
46
+ style,
47
+ },
48
+ ref
49
+ ) {
50
+ // Get all state and controls from context
51
+ const {
52
+ isReady,
53
+ isPlaying,
54
+ currentTime,
55
+ duration,
56
+ volume,
57
+ isMuted,
58
+ isLooping,
59
+ togglePlay,
60
+ restart,
61
+ skip,
62
+ setVolume,
63
+ toggleMute,
64
+ toggleLoop,
65
+ } = useAudio();
66
+
67
+ // Enable keyboard shortcuts
68
+ useAudioHotkeys({ enabled: isReady });
69
+
70
+ const isLoading = !isReady;
71
+
72
+ const handleVolumeChange = (value: number[]) => {
73
+ setVolume(value[0] / 100);
74
+ };
75
+
76
+ return (
77
+ <div
78
+ className={cn(
79
+ 'flex flex-col gap-3 p-4 rounded-lg bg-card border',
80
+ className
81
+ )}
82
+ style={style}
83
+ >
84
+ {/* Waveform container - rendered by WaveSurfer via context */}
85
+ {showWaveform && (
86
+ <div className="relative">
87
+ <div
88
+ ref={ref}
89
+ className={cn(
90
+ 'w-full rounded transition-opacity',
91
+ isLoading && 'opacity-50'
92
+ )}
93
+ />
94
+ {isLoading && (
95
+ <div className="absolute inset-0 flex items-center justify-center">
96
+ <Loader2 className="h-6 w-6 animate-spin text-primary" />
97
+ </div>
98
+ )}
99
+ </div>
100
+ )}
101
+
102
+ {/* Equalizer animation */}
103
+ {showEqualizer && (
104
+ <AudioEqualizer
105
+ barCount={equalizerOptions.barCount}
106
+ height={equalizerOptions.height}
107
+ gap={equalizerOptions.gap}
108
+ showPeaks={equalizerOptions.showPeaks}
109
+ barColor={equalizerOptions.barColor}
110
+ peakColor={equalizerOptions.peakColor}
111
+ className="px-1"
112
+ />
113
+ )}
114
+
115
+ {/* Timer */}
116
+ {showTimer && (
117
+ <div className="flex justify-between text-xs text-muted-foreground tabular-nums px-1">
118
+ <span>{formatTime(currentTime)}</span>
119
+ <span>{formatTime(duration)}</span>
120
+ </div>
121
+ )}
122
+
123
+ {/* Controls */}
124
+ {showControls && (
125
+ <div className="flex items-center justify-center gap-1">
126
+ {/* Restart */}
127
+ <Button
128
+ variant="ghost"
129
+ size="icon"
130
+ className="h-9 w-9"
131
+ onClick={restart}
132
+ disabled={!isReady}
133
+ title="Restart"
134
+ >
135
+ <RotateCcw className="h-4 w-4" />
136
+ </Button>
137
+
138
+ {/* Skip back 5s */}
139
+ <Button
140
+ variant="ghost"
141
+ size="icon"
142
+ className="h-9 w-9"
143
+ onClick={() => skip(-5)}
144
+ disabled={!isReady}
145
+ title="Back 5 seconds"
146
+ >
147
+ <SkipBack className="h-4 w-4" />
148
+ </Button>
149
+
150
+ {/* Play/Pause */}
151
+ <Button
152
+ variant="default"
153
+ size="icon"
154
+ className="h-12 w-12 rounded-full"
155
+ onClick={togglePlay}
156
+ disabled={!isReady && !isLoading}
157
+ title={isPlaying ? 'Pause' : 'Play'}
158
+ >
159
+ {isLoading ? (
160
+ <Loader2 className="h-5 w-5 animate-spin" />
161
+ ) : isPlaying ? (
162
+ <Pause className="h-5 w-5" />
163
+ ) : (
164
+ <Play className="h-5 w-5 ml-0.5" />
165
+ )}
166
+ </Button>
167
+
168
+ {/* Skip forward 5s */}
169
+ <Button
170
+ variant="ghost"
171
+ size="icon"
172
+ className="h-9 w-9"
173
+ onClick={() => skip(5)}
174
+ disabled={!isReady}
175
+ title="Forward 5 seconds"
176
+ >
177
+ <SkipForward className="h-4 w-4" />
178
+ </Button>
179
+
180
+ {/* Volume */}
181
+ {showVolume && (
182
+ <>
183
+ <Button
184
+ variant="ghost"
185
+ size="icon"
186
+ className="h-9 w-9"
187
+ onClick={toggleMute}
188
+ title={isMuted ? 'Unmute' : 'Mute'}
189
+ >
190
+ {isMuted || volume === 0 ? (
191
+ <VolumeX className="h-4 w-4" />
192
+ ) : (
193
+ <Volume2 className="h-4 w-4" />
194
+ )}
195
+ </Button>
196
+
197
+ <Slider
198
+ value={[isMuted ? 0 : volume * 100]}
199
+ max={100}
200
+ step={1}
201
+ onValueChange={handleVolumeChange}
202
+ className="w-20"
203
+ aria-label="Volume"
204
+ />
205
+ </>
206
+ )}
207
+
208
+ {/* Loop/Repeat */}
209
+ {showLoop && (
210
+ <Button
211
+ variant="ghost"
212
+ size="icon"
213
+ className={cn('h-9 w-9', isLooping && 'text-primary')}
214
+ onClick={toggleLoop}
215
+ disabled={!isReady}
216
+ title={isLooping ? 'Disable loop' : 'Enable loop'}
217
+ >
218
+ <Repeat className="h-4 w-4" />
219
+ </Button>
220
+ )}
221
+
222
+ {/* Shortcuts help */}
223
+ <AudioShortcutsPopover compact />
224
+ </div>
225
+ )}
226
+ </div>
227
+ );
228
+ }
229
+ );
230
+
231
+ export default AudioPlayer;
@@ -0,0 +1,99 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * AudioShortcutsPopover
5
+ *
6
+ * Displays keyboard shortcuts for audio player controls.
7
+ */
8
+
9
+ import {
10
+ Popover,
11
+ PopoverContent,
12
+ PopoverTrigger,
13
+ Button,
14
+ Kbd,
15
+ KbdGroup,
16
+ Separator,
17
+ Tooltip,
18
+ TooltipTrigger,
19
+ TooltipContent,
20
+ } from '@djangocfg/ui-nextjs';
21
+ import { Keyboard } from 'lucide-react';
22
+ import { AUDIO_SHORTCUTS } from '../hooks';
23
+
24
+ // =============================================================================
25
+ // TYPES
26
+ // =============================================================================
27
+
28
+ interface AudioShortcutsPopoverProps {
29
+ /** Compact mode for smaller displays */
30
+ compact?: boolean;
31
+ }
32
+
33
+ // =============================================================================
34
+ // COMPONENT
35
+ // =============================================================================
36
+
37
+ export function AudioShortcutsPopover({ compact = false }: AudioShortcutsPopoverProps) {
38
+ const trigger = (
39
+ <PopoverTrigger asChild>
40
+ <Button
41
+ variant="ghost"
42
+ size="icon"
43
+ className={compact ? 'h-6 w-6' : 'size-7 text-muted-foreground hover:text-foreground'}
44
+ title={compact ? undefined : 'Keyboard shortcuts'}
45
+ >
46
+ <Keyboard className={compact ? 'h-3.5 w-3.5' : 'size-4'} />
47
+ </Button>
48
+ </PopoverTrigger>
49
+ );
50
+
51
+ return (
52
+ <Popover>
53
+ {compact ? (
54
+ <Tooltip>
55
+ <TooltipTrigger asChild>{trigger}</TooltipTrigger>
56
+ <TooltipContent side="bottom">Shortcuts</TooltipContent>
57
+ </Tooltip>
58
+ ) : (
59
+ trigger
60
+ )}
61
+ <PopoverContent className="w-56 p-0" align="end">
62
+ <div className="px-3 py-2 border-b">
63
+ <h4 className="font-medium text-sm">Player Shortcuts</h4>
64
+ </div>
65
+ <div className="p-2 space-y-3 max-h-72 overflow-y-auto">
66
+ {AUDIO_SHORTCUTS.map((group, groupIndex) => (
67
+ <div key={group.title}>
68
+ {groupIndex > 0 && <Separator className="my-2" />}
69
+ <div className="text-xs font-medium text-muted-foreground mb-1.5 px-1">
70
+ {group.title}
71
+ </div>
72
+ <div className="space-y-1">
73
+ {group.shortcuts.map((shortcut) => (
74
+ <div
75
+ key={shortcut.label}
76
+ className="flex items-center justify-between px-1 py-0.5"
77
+ >
78
+ <span className="text-sm text-foreground">
79
+ {shortcut.label}
80
+ </span>
81
+ <KbdGroup>
82
+ {shortcut.keys.map((key, i) => (
83
+ <Kbd key={i} size="sm">
84
+ {key}
85
+ </Kbd>
86
+ ))}
87
+ </KbdGroup>
88
+ </div>
89
+ ))}
90
+ </div>
91
+ </div>
92
+ ))}
93
+ </div>
94
+ </PopoverContent>
95
+ </Popover>
96
+ );
97
+ }
98
+
99
+ export default AudioShortcutsPopover;
@@ -0,0 +1,147 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * AudioReactiveCover - Album art with audio-reactive animations
5
+ *
6
+ * Uses effects utilities for clean data preparation before render.
7
+ * Click on cover to switch between effect variants.
8
+ *
9
+ * Audio analysis is performed in the AudioProvider context to persist
10
+ * across variant changes (including switching to 'none' and back).
11
+ */
12
+
13
+ import { type ReactNode } from 'react';
14
+ import { cn } from '@djangocfg/ui-nextjs';
15
+ import { useAudioElement } from '../../context';
16
+ import {
17
+ type EffectVariant,
18
+ type EffectIntensity,
19
+ type EffectColorScheme,
20
+ getEffectConfig,
21
+ prepareEffectColors,
22
+ calculateGlowLayers,
23
+ calculateOrbs,
24
+ calculateMeshGradients,
25
+ calculateSpotlight,
26
+ EFFECT_ANIMATIONS,
27
+ } from '../../effects';
28
+ import { GlowEffect, OrbsEffect, SpotlightEffect, MeshEffect, type GlowEffectData } from './effects';
29
+
30
+ // =============================================================================
31
+ // TYPES
32
+ // =============================================================================
33
+
34
+ export interface AudioReactiveCoverProps {
35
+ children: ReactNode;
36
+ size?: 'sm' | 'md' | 'lg';
37
+ variant?: EffectVariant;
38
+ intensity?: EffectIntensity;
39
+ colorScheme?: EffectColorScheme;
40
+ onClick?: () => void;
41
+ className?: string;
42
+ }
43
+
44
+ // =============================================================================
45
+ // CONSTANTS
46
+ // =============================================================================
47
+
48
+ const SIZES = {
49
+ sm: { container: 'w-32 h-32', orbBase: 40 },
50
+ md: { container: 'w-40 h-40', orbBase: 50 },
51
+ lg: { container: 'w-48 h-48', orbBase: 60 },
52
+ };
53
+
54
+ // =============================================================================
55
+ // COMPONENT
56
+ // =============================================================================
57
+
58
+ export function AudioReactiveCover({
59
+ children,
60
+ size = 'lg',
61
+ variant = 'spotlight',
62
+ intensity = 'medium',
63
+ colorScheme = 'primary',
64
+ onClick,
65
+ className,
66
+ }: AudioReactiveCoverProps) {
67
+ // Get audio levels from context (persists across variant changes)
68
+ const { isPlaying, audioLevels: levels } = useAudioElement();
69
+
70
+ // =========================================================================
71
+ // PREPARE DATA BEFORE RENDER
72
+ // =========================================================================
73
+
74
+ const sizeConfig = SIZES[size];
75
+ const effectConfig = getEffectConfig(intensity);
76
+ const { colors, hueShift } = prepareEffectColors(colorScheme, levels);
77
+
78
+ // Calculate scale based on overall level
79
+ const containerScale = 1 + levels.overall * effectConfig.scale;
80
+
81
+ // Prepare effect-specific data - NO memoization for real-time reactivity
82
+ const glowData: GlowEffectData | null = variant === 'glow' ? {
83
+ layers: calculateGlowLayers(levels, effectConfig, colors),
84
+ hueShift,
85
+ showPulseRings: levels.bass > 0.5,
86
+ showSparkle: levels.high > 0.4,
87
+ } : null;
88
+
89
+ const orbsData = variant === 'orbs'
90
+ ? calculateOrbs(levels, effectConfig, colors, sizeConfig.orbBase)
91
+ : null;
92
+
93
+ const meshData = variant === 'mesh'
94
+ ? calculateMeshGradients(levels, effectConfig, colors)
95
+ : null;
96
+
97
+ const spotlightData = variant === 'spotlight'
98
+ ? calculateSpotlight(levels, effectConfig, colors, levels.mid * 360)
99
+ : null;
100
+
101
+ // =========================================================================
102
+ // RENDER
103
+ // =========================================================================
104
+
105
+ return (
106
+ <div
107
+ className={cn('relative', sizeConfig.container, className)}
108
+ style={{
109
+ transform: `scale(${containerScale})`,
110
+ transition: 'transform 0.1s ease-out',
111
+ }}
112
+ >
113
+ {/* Effect layers */}
114
+ {glowData && (
115
+ <GlowEffect data={glowData} colors={colors} isPlaying={isPlaying} />
116
+ )}
117
+
118
+ {orbsData && (
119
+ <OrbsEffect orbs={orbsData} blur={effectConfig.blur} isPlaying={isPlaying} />
120
+ )}
121
+
122
+ {spotlightData && (
123
+ <SpotlightEffect data={spotlightData} colors={colors} blur={effectConfig.blur} isPlaying={isPlaying} />
124
+ )}
125
+
126
+ {meshData && (
127
+ <MeshEffect gradients={meshData} blur={effectConfig.blur} isPlaying={isPlaying} />
128
+ )}
129
+
130
+ {/* Content (cover art) */}
131
+ <div
132
+ className="relative w-full h-full rounded-lg overflow-hidden shadow-2xl z-10 bg-background cursor-pointer"
133
+ onClick={onClick}
134
+ role={onClick ? 'button' : undefined}
135
+ tabIndex={onClick ? 0 : undefined}
136
+ onKeyDown={onClick ? (e) => e.key === 'Enter' && onClick() : undefined}
137
+ >
138
+ {children}
139
+ </div>
140
+
141
+ {/* Inject animations once */}
142
+ <style dangerouslySetInnerHTML={{ __html: EFFECT_ANIMATIONS }} />
143
+ </div>
144
+ );
145
+ }
146
+
147
+ export default AudioReactiveCover;
@@ -0,0 +1,110 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * GlowEffect - Multi-layer glow effect with pulse rings
5
+ */
6
+
7
+ import { cn } from '@djangocfg/ui-nextjs';
8
+ import type { calculateGlowLayers } from '../../../effects';
9
+
10
+ // =============================================================================
11
+ // TYPES
12
+ // =============================================================================
13
+
14
+ export interface GlowEffectData {
15
+ layers: ReturnType<typeof calculateGlowLayers>;
16
+ hueShift: number;
17
+ showPulseRings: boolean;
18
+ showSparkle: boolean;
19
+ }
20
+
21
+ interface GlowEffectProps {
22
+ data: GlowEffectData;
23
+ colors: string[];
24
+ isPlaying: boolean;
25
+ }
26
+
27
+ // =============================================================================
28
+ // COMPONENT
29
+ // =============================================================================
30
+
31
+ export function GlowEffect({ data, colors, isPlaying }: GlowEffectProps) {
32
+ const { layers, hueShift, showPulseRings, showSparkle } = data;
33
+
34
+ return (
35
+ <>
36
+ {/* Main glow layers */}
37
+ {layers.map((layer, i) => (
38
+ <div
39
+ key={i}
40
+ className={cn('absolute rounded-2xl -z-10', layer.blur)}
41
+ style={{
42
+ inset: `-${layer.inset}px`,
43
+ background: layer.background,
44
+ opacity: isPlaying ? layer.opacity : 0,
45
+ transform: i < 2 ? `scaleY(${layer.scale})` : `scale(${layer.scale})`,
46
+ animation: isPlaying && layer.animation ? layer.animation : 'none',
47
+ transition: 'opacity 0.3s',
48
+ }}
49
+ />
50
+ ))}
51
+
52
+ {/* Rotating color sweep */}
53
+ <div
54
+ className="absolute rounded-2xl blur-xl overflow-hidden -z-10"
55
+ style={{
56
+ inset: '-75px',
57
+ opacity: isPlaying ? 0.6 : 0,
58
+ transition: 'opacity 0.5s',
59
+ }}
60
+ >
61
+ <div
62
+ className="absolute inset-0"
63
+ style={{
64
+ background: `conic-gradient(
65
+ from ${hueShift}deg at 50% 50%,
66
+ hsl(${colors[0]} / 0.4) 0deg,
67
+ hsl(${colors[1] || colors[0]} / 0.3) 90deg,
68
+ hsl(${colors[2] || colors[0]} / 0.3) 180deg,
69
+ hsl(${colors[3] || colors[0]} / 0.3) 270deg,
70
+ hsl(${colors[0]} / 0.4) 360deg
71
+ )`,
72
+ animation: isPlaying ? 'glow-rotate 6s linear infinite' : 'none',
73
+ }}
74
+ />
75
+ </div>
76
+
77
+ {/* Pulse rings on bass hits */}
78
+ {showPulseRings && (
79
+ <>
80
+ <div
81
+ className="absolute -inset-6 rounded-xl border-2 animate-ping -z-10"
82
+ style={{
83
+ borderColor: `hsl(${colors[0]} / 0.4)`,
84
+ animationDuration: '1s',
85
+ }}
86
+ />
87
+ <div
88
+ className="absolute -inset-12 rounded-2xl border animate-ping -z-10"
89
+ style={{
90
+ borderColor: `hsl(${colors[1] || colors[0]} / 0.3)`,
91
+ animationDuration: '1.5s',
92
+ animationDelay: '0.2s',
93
+ }}
94
+ />
95
+ </>
96
+ )}
97
+
98
+ {/* Sparkle on high frequencies */}
99
+ {showSparkle && (
100
+ <div
101
+ className="absolute -inset-18 rounded-3xl -z-10"
102
+ style={{
103
+ background: `radial-gradient(circle at 50% 30%, hsl(${colors[2] || colors[0]} / 0.5) 0%, transparent 30%)`,
104
+ animation: 'sparkle-move 0.5s ease-out',
105
+ }}
106
+ />
107
+ )}
108
+ </>
109
+ );
110
+ }
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * MeshEffect - Mesh gradient blobs that react to audio
5
+ */
6
+
7
+ import { cn } from '@djangocfg/ui-nextjs';
8
+ import type { calculateMeshGradients } from '../../../effects';
9
+
10
+ // =============================================================================
11
+ // TYPES
12
+ // =============================================================================
13
+
14
+ interface MeshEffectProps {
15
+ gradients: ReturnType<typeof calculateMeshGradients>;
16
+ blur: string;
17
+ isPlaying: boolean;
18
+ }
19
+
20
+ // =============================================================================
21
+ // COMPONENT
22
+ // =============================================================================
23
+
24
+ export function MeshEffect({ gradients, isPlaying }: MeshEffectProps) {
25
+ return (
26
+ <>
27
+ {gradients.map((g, i) => {
28
+ const isCenter = 'isCenter' in g && g.isCenter;
29
+ const scale = 'scale' in g ? g.scale : 1;
30
+ const rotation = 'rotation' in g ? g.rotation : 0;
31
+ const itemBlur = 'blur' in g ? g.blur : 'blur-2xl';
32
+
33
+ return (
34
+ <div
35
+ key={i}
36
+ className={cn('absolute rounded-full -z-10', itemBlur)}
37
+ style={{
38
+ width: g.width,
39
+ height: g.height,
40
+ top: 'top' in g ? g.top : undefined,
41
+ bottom: 'bottom' in g ? g.bottom : undefined,
42
+ left: 'left' in g ? g.left : undefined,
43
+ right: 'right' in g ? g.right : undefined,
44
+ background: isCenter
45
+ ? `radial-gradient(circle, hsl(${g.color} / 0.6) 0%, hsl(${g.color} / 0.3) 30%, transparent 60%)`
46
+ : `radial-gradient(circle, hsl(${g.color}) 0%, hsl(${g.color} / 0.5) 30%, transparent 65%)`,
47
+ opacity: isPlaying ? g.opacity : 0,
48
+ transform: isCenter
49
+ ? `translate(-50%, -50%) scale(${scale})`
50
+ : `scale(${scale}) rotate(${rotation}deg)`,
51
+ transition: 'all 0.08s ease-out',
52
+ }}
53
+ />
54
+ );
55
+ })}
56
+ </>
57
+ );
58
+ }
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * OrbsEffect - Floating orb particles that react to audio
5
+ */
6
+
7
+ import { cn } from '@djangocfg/ui-nextjs';
8
+ import type { calculateOrbs } from '../../../effects';
9
+
10
+ // =============================================================================
11
+ // TYPES
12
+ // =============================================================================
13
+
14
+ interface OrbsEffectProps {
15
+ orbs: ReturnType<typeof calculateOrbs>;
16
+ blur: string;
17
+ isPlaying: boolean;
18
+ }
19
+
20
+ // =============================================================================
21
+ // COMPONENT
22
+ // =============================================================================
23
+
24
+ export function OrbsEffect({ orbs, blur, isPlaying }: OrbsEffectProps) {
25
+ return (
26
+ <>
27
+ {orbs.map((orb, i) => (
28
+ <div
29
+ key={i}
30
+ className={cn('absolute rounded-full -z-10', blur)}
31
+ style={{
32
+ width: orb.size,
33
+ height: orb.size,
34
+ left: `${orb.x}%`,
35
+ top: `${orb.y}%`,
36
+ background: `radial-gradient(circle at 30% 30%, hsl(${orb.color}) 0%, hsl(${orb.color} / 0.5) 40%, transparent 70%)`,
37
+ opacity: isPlaying ? orb.opacity : 0,
38
+ transform: `translate(-50%, -50%) scale(${orb.scale})`,
39
+ transition: 'all 0.08s ease-out',
40
+ }}
41
+ />
42
+ ))}
43
+ </>
44
+ );
45
+ }