diff --git a/components/effects/EffectBrowser.tsx b/components/effects/EffectBrowser.tsx new file mode 100644 index 0000000..a4a7fa8 --- /dev/null +++ b/components/effects/EffectBrowser.tsx @@ -0,0 +1,122 @@ +'use client'; + +import * as React from 'react'; +import { X, Search } from 'lucide-react'; +import { Button } from '@/components/ui/Button'; +import { cn } from '@/lib/utils/cn'; +import type { EffectType } from '@/lib/audio/effects/chain'; +import { EFFECT_NAMES } from '@/lib/audio/effects/chain'; + +export interface EffectBrowserProps { + open: boolean; + onClose: () => void; + onSelectEffect: (effectType: EffectType) => void; +} + +const EFFECT_CATEGORIES = { + 'Dynamics': ['compressor', 'limiter', 'gate'] as EffectType[], + 'Filters': ['lowpass', 'highpass', 'bandpass', 'notch', 'lowshelf', 'highshelf', 'peaking'] as EffectType[], + 'Time-Based': ['delay', 'reverb', 'chorus', 'flanger', 'phaser'] as EffectType[], + 'Distortion': ['distortion', 'bitcrusher'] as EffectType[], + 'Pitch & Time': ['pitch', 'timestretch'] as EffectType[], + 'Utility': ['normalize', 'fadeIn', 'fadeOut', 'reverse'] as EffectType[], +}; + +export function EffectBrowser({ open, onClose, onSelectEffect }: EffectBrowserProps) { + const [search, setSearch] = React.useState(''); + const [selectedCategory, setSelectedCategory] = React.useState(null); + + if (!open) return null; + + const handleSelectEffect = (effectType: EffectType) => { + onSelectEffect(effectType); + onClose(); + setSearch(''); + setSelectedCategory(null); + }; + + const filteredCategories = React.useMemo(() => { + if (!search) return EFFECT_CATEGORIES; + + const searchLower = search.toLowerCase(); + const filtered: Record = {}; + + Object.entries(EFFECT_CATEGORIES).forEach(([category, effects]) => { + const matchingEffects = effects.filter((effect) => + EFFECT_NAMES[effect].toLowerCase().includes(searchLower) + ); + if (matchingEffects.length > 0) { + filtered[category] = matchingEffects; + } + }); + + return filtered; + }, [search]); + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+

Add Effect

+ +
+ + {/* Search */} +
+
+ + setSearch(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-background border border-border rounded-md text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary" + autoFocus + /> +
+
+ + {/* Content */} +
+
+ {Object.entries(filteredCategories).map(([category, effects]) => ( +
+

+ {category} +

+
+ {effects.map((effect) => ( + + ))} +
+
+ ))} +
+ + {Object.keys(filteredCategories).length === 0 && ( +
+ No effects found matching "{search}" +
+ )} +
+
+
+ ); +} diff --git a/components/tracks/Track.tsx b/components/tracks/Track.tsx index 3487918..e0af9ee 100644 --- a/components/tracks/Track.tsx +++ b/components/tracks/Track.tsx @@ -6,6 +6,8 @@ import type { Track as TrackType } from '@/types/track'; import { Button } from '@/components/ui/Button'; import { Slider } from '@/components/ui/Slider'; import { cn } from '@/lib/utils/cn'; +import { EffectBrowser } from '@/components/effects/EffectBrowser'; +import { createEffect, type EffectType } from '@/lib/audio/effects/chain'; export interface TrackProps { track: TrackType; @@ -25,6 +27,7 @@ export interface TrackProps { onLoadAudio?: (buffer: AudioBuffer) => void; onToggleEffect?: (effectId: string) => void; onRemoveEffect?: (effectId: string) => void; + onAddEffect?: (effectType: EffectType) => void; } export function Track({ @@ -45,6 +48,7 @@ export function Track({ onLoadAudio, onToggleEffect, onRemoveEffect, + onAddEffect, }: TrackProps) { const canvasRef = React.useRef(null); const containerRef = React.useRef(null); @@ -52,6 +56,7 @@ export function Track({ const [isEditingName, setIsEditingName] = React.useState(false); const [nameInput, setNameInput] = React.useState(String(track.name || 'Untitled Track')); const [showDevices, setShowDevices] = React.useState(true); + const [effectBrowserOpen, setEffectBrowserOpen] = React.useState(false); const inputRef = React.useRef(null); const handleNameClick = () => { @@ -401,10 +406,7 @@ export function Track({