feat: redesign track and master controls with integrated fader+meters+pan

Unified design language across all tracks and master section:
- Created TrackFader component: vertical fader with horizontal meter bars
- Created TrackControls: integrated pan + fader + mute in compact layout
- Created MasterFader: similar design but larger for master output
- Created MasterControls: master version with pan + fader + mute
- Updated Track component to use new TrackControls
- Updated PlaybackControls to use new MasterControls
- Removed old VerticalFader and separate meter components

New features:
- Horizontal peak/RMS meter bars behind fader (top=peak, bottom=RMS)
- Color-coded meters (green/yellow/red based on dB levels)
- dB scale labels and numeric readouts
- Integrated mute button in controls
- Consistent circular pan knobs
- Professional DAW-style channel strip appearance
- Master section includes clip indicator

Visual improvements:
- Unified design across all tracks and master
- Compact vertical layout saves space
- Real-time level monitoring integrated with volume control
- Smooth animations and transitions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-19 00:08:36 +01:00
parent c33a77270b
commit 441920ee70
7 changed files with 619 additions and 103 deletions

View File

@@ -0,0 +1,92 @@
'use client';
import * as React from 'react';
import { Volume2, VolumeX } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { CircularKnob } from '@/components/ui/CircularKnob';
import { MasterFader } from './MasterFader';
import { cn } from '@/lib/utils/cn';
export interface MasterControlsProps {
volume: number;
pan: number;
peakLevel: number;
rmsLevel: number;
isClipping: boolean;
isMuted?: boolean;
onVolumeChange: (volume: number) => void;
onPanChange: (pan: number) => void;
onMuteToggle: () => void;
onResetClip?: () => void;
className?: string;
}
export function MasterControls({
volume,
pan,
peakLevel,
rmsLevel,
isClipping,
isMuted = false,
onVolumeChange,
onPanChange,
onMuteToggle,
onResetClip,
className,
}: MasterControlsProps) {
return (
<div className={cn(
'flex flex-col items-center gap-3 px-4 py-3 bg-muted/10 border-2 border-accent/30 rounded-lg',
className
)}>
{/* Master Label */}
<div className="text-[10px] font-bold text-accent uppercase tracking-wider">
Master
</div>
{/* Pan Control */}
<CircularKnob
value={pan}
onChange={onPanChange}
min={-1}
max={1}
step={0.01}
label="PAN"
size={48}
formatter={(value) => {
if (Math.abs(value) < 0.01) return 'C';
if (value < 0) return `${Math.abs(value * 100).toFixed(0)}L`;
return `${(value * 100).toFixed(0)}R`;
}}
/>
{/* Master Fader with Integrated Meters */}
<MasterFader
value={volume}
peakLevel={peakLevel}
rmsLevel={rmsLevel}
isClipping={isClipping}
onChange={onVolumeChange}
onResetClip={onResetClip}
/>
{/* Mute Button */}
<Button
variant={isMuted ? 'default' : 'outline'}
size="sm"
onClick={onMuteToggle}
title={isMuted ? 'Unmute' : 'Mute'}
className={cn(
'w-full h-8',
isMuted && 'bg-red-500/20 hover:bg-red-500/30 border-red-500/50'
)}
>
{isMuted ? (
<VolumeX className="h-4 w-4" />
) : (
<Volume2 className="h-4 w-4" />
)}
</Button>
</div>
);
}

View File

@@ -0,0 +1,217 @@
'use client';
import * as React from 'react';
import { cn } from '@/lib/utils/cn';
export interface MasterFaderProps {
value: number;
peakLevel: number;
rmsLevel: number;
isClipping: boolean;
onChange: (value: number) => void;
onResetClip?: () => void;
className?: string;
}
export function MasterFader({
value,
peakLevel,
rmsLevel,
isClipping,
onChange,
onResetClip,
className,
}: MasterFaderProps) {
const [isDragging, setIsDragging] = React.useState(false);
const containerRef = React.useRef<HTMLDivElement>(null);
// Convert linear 0-1 to dB scale for display
const linearToDb = (linear: number): number => {
if (linear === 0) return -60;
const db = 20 * Math.log10(linear);
return Math.max(-60, Math.min(0, db));
};
const valueDb = linearToDb(value);
const peakDb = linearToDb(peakLevel);
const rmsDb = linearToDb(rmsLevel);
// Calculate bar widths (0-100%)
const peakWidth = ((peakDb + 60) / 60) * 100;
const rmsWidth = ((rmsDb + 60) / 60) * 100;
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
setIsDragging(true);
updateValue(e.clientY);
};
const handleMouseMove = React.useCallback(
(e: MouseEvent) => {
if (!isDragging) return;
updateValue(e.clientY);
},
[isDragging]
);
const handleMouseUp = React.useCallback(() => {
setIsDragging(false);
}, []);
const updateValue = (clientY: number) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const y = clientY - rect.top;
// Inverted: top = max (1), bottom = min (0)
const percentage = Math.max(0, Math.min(1, 1 - (y / rect.height)));
onChange(percentage);
};
React.useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}
}, [isDragging, handleMouseMove, handleMouseUp]);
return (
<div className={cn('flex gap-3', className)}>
{/* dB Labels (Left) */}
<div className="flex flex-col justify-between text-[10px] font-mono text-muted-foreground py-1">
<span>0</span>
<span>-12</span>
<span>-24</span>
<span>-60</span>
</div>
{/* Fader Container */}
<div
ref={containerRef}
className="relative w-12 h-40 bg-background/50 rounded-md border border-border/50 cursor-pointer"
onMouseDown={handleMouseDown}
>
{/* Peak Meter (Horizontal Bar - Top) */}
<div className="absolute inset-x-2 top-2 h-3 bg-background/80 rounded-sm overflow-hidden border border-border/30">
<div
className="absolute left-0 top-0 bottom-0 transition-all duration-75 ease-out"
style={{ width: `${Math.max(0, Math.min(100, peakWidth))}%` }}
>
<div className={cn(
'w-full h-full',
peakDb > -3 ? 'bg-red-500' :
peakDb > -6 ? 'bg-yellow-500' :
'bg-green-500'
)} />
</div>
</div>
{/* RMS Meter (Horizontal Bar - Bottom) */}
<div className="absolute inset-x-2 bottom-2 h-3 bg-background/80 rounded-sm overflow-hidden border border-border/30">
<div
className="absolute left-0 top-0 bottom-0 transition-all duration-150 ease-out"
style={{ width: `${Math.max(0, Math.min(100, rmsWidth))}%` }}
>
<div className={cn(
'w-full h-full',
rmsDb > -3 ? 'bg-red-400' :
rmsDb > -6 ? 'bg-yellow-400' :
'bg-green-400'
)} />
</div>
</div>
{/* Fader Track */}
<div className="absolute top-8 bottom-8 left-1/2 -translate-x-1/2 w-1.5 bg-muted/50 rounded-full" />
{/* Fader Handle */}
<div
className="absolute left-1/2 -translate-x-1/2 w-10 h-4 bg-primary/80 border-2 border-primary rounded-md shadow-lg cursor-grab active:cursor-grabbing pointer-events-none transition-all"
style={{
// Inverted: value 1 = top, value 0 = bottom
top: `calc(${(1 - value) * 100}% - 0.5rem)`,
}}
>
{/* Handle grip lines */}
<div className="absolute inset-0 flex items-center justify-center gap-0.5">
<div className="h-2 w-px bg-primary-foreground/30" />
<div className="h-2 w-px bg-primary-foreground/30" />
<div className="h-2 w-px bg-primary-foreground/30" />
</div>
</div>
{/* Clip Indicator */}
{isClipping && (
<button
onClick={onResetClip}
className="absolute top-0 left-0 right-0 px-1 py-0.5 text-[9px] font-bold text-white bg-red-500 border-b border-red-600 rounded-t-md shadow-lg shadow-red-500/50 animate-pulse"
title="Click to reset clip indicator"
>
CLIP
</button>
)}
{/* dB Scale Markers */}
<div className="absolute inset-0 px-2 py-8 pointer-events-none">
<div className="relative h-full">
{/* -12 dB */}
<div className="absolute left-0 right-0 h-px bg-border/20" style={{ top: '50%' }} />
{/* -6 dB */}
<div className="absolute left-0 right-0 h-px bg-yellow-500/20" style={{ top: '20%' }} />
{/* -3 dB */}
<div className="absolute left-0 right-0 h-px bg-red-500/30" style={{ top: '10%' }} />
</div>
</div>
</div>
{/* Value and Level Display (Right) */}
<div className="flex flex-col justify-between items-start text-[9px] font-mono py-1">
{/* Current dB Value */}
<div className={cn(
'font-bold text-[11px]',
valueDb > -3 ? 'text-red-500' :
valueDb > -6 ? 'text-yellow-500' :
'text-green-500'
)}>
{valueDb > -60 ? `${valueDb.toFixed(1)}` : '-∞'}
</div>
{/* Spacer */}
<div className="flex-1" />
{/* Peak Level */}
<div className="flex flex-col items-start">
<span className="text-muted-foreground/60">PK</span>
<span className={cn(
'font-mono text-[10px]',
peakDb > -3 ? 'text-red-500' :
peakDb > -6 ? 'text-yellow-500' :
'text-green-500'
)}>
{peakDb > -60 ? `${peakDb.toFixed(1)}` : '-∞'}
</span>
</div>
{/* RMS Level */}
<div className="flex flex-col items-start">
<span className="text-muted-foreground/60">RM</span>
<span className={cn(
'font-mono text-[10px]',
rmsDb > -3 ? 'text-red-400' :
rmsDb > -6 ? 'text-yellow-400' :
'text-green-400'
)}>
{rmsDb > -60 ? `${rmsDb.toFixed(1)}` : '-∞'}
</span>
</div>
{/* dB Label */}
<span className="text-muted-foreground/60 text-[8px]">dB</span>
</div>
</div>
);
}

View File

@@ -35,6 +35,8 @@ export function AudioEditor() {
const [selectedTrackId, setSelectedTrackId] = React.useState<string | null>(null);
const [zoom, setZoom] = React.useState(1);
const [masterVolume, setMasterVolume] = React.useState(0.8);
const [masterPan, setMasterPan] = React.useState(0);
const [isMasterMuted, setIsMasterMuted] = React.useState(false);
const [clipboard, setClipboard] = React.useState<AudioBuffer | null>(null);
const [recordingTrackId, setRecordingTrackId] = React.useState<string | null>(null);
const [punchInEnabled, setPunchInEnabled] = React.useState(false);
@@ -1042,11 +1044,22 @@ export function AudioEditor() {
currentTime={currentTime}
duration={duration}
volume={masterVolume}
pan={masterPan}
onPlay={play}
onPause={pause}
onStop={stop}
onSeek={seek}
onVolumeChange={setMasterVolume}
onPanChange={setMasterPan}
onMuteToggle={() => {
if (isMasterMuted) {
setMasterVolume(0.8);
setIsMasterMuted(false);
} else {
setMasterVolume(0);
setIsMasterMuted(true);
}
}}
currentTimeFormatted={formatDuration(currentTime)}
durationFormatted={formatDuration(duration)}
isRecording={recordingState.isRecording}

View File

@@ -1,10 +1,9 @@
'use client';
import * as React from 'react';
import { Play, Pause, Square, SkipBack, Volume2, VolumeX, Circle, AlignVerticalJustifyStart, AlignVerticalJustifyEnd, Layers } from 'lucide-react';
import { Play, Pause, Square, SkipBack, Circle, AlignVerticalJustifyStart, AlignVerticalJustifyEnd, Layers } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { Slider } from '@/components/ui/Slider';
import { MasterMeter } from '@/components/controls/MasterMeter';
import { MasterControls } from '@/components/controls/MasterControls';
import { cn } from '@/lib/utils/cn';
export interface PlaybackControlsProps {
@@ -13,11 +12,14 @@ export interface PlaybackControlsProps {
currentTime: number;
duration: number;
volume: number;
pan?: number;
onPlay: () => void;
onPause: () => void;
onStop: () => void;
onSeek: (time: number, autoPlay?: boolean) => void;
onVolumeChange: (volume: number) => void;
onPanChange?: (pan: number) => void;
onMuteToggle?: () => void;
disabled?: boolean;
className?: string;
currentTimeFormatted?: string;
@@ -45,11 +47,14 @@ export function PlaybackControls({
currentTime,
duration,
volume,
pan = 0,
onPlay,
onPause,
onStop,
onSeek,
onVolumeChange,
onPanChange,
onMuteToggle,
disabled = false,
className,
currentTimeFormatted,
@@ -70,9 +75,6 @@ export function PlaybackControls({
masterIsClipping = false,
onResetClip,
}: PlaybackControlsProps) {
const [isMuted, setIsMuted] = React.useState(false);
const [previousVolume, setPreviousVolume] = React.useState(volume);
const handlePlayPause = () => {
if (isPlaying) {
onPause();
@@ -81,26 +83,6 @@ export function PlaybackControls({
}
};
const handleMuteToggle = () => {
if (isMuted) {
onVolumeChange(previousVolume);
setIsMuted(false);
} else {
setPreviousVolume(volume);
onVolumeChange(0);
setIsMuted(true);
}
};
const handleVolumeChange = (newVolume: number) => {
onVolumeChange(newVolume);
if (newVolume === 0) {
setIsMuted(true);
} else {
setIsMuted(false);
}
};
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
return (
@@ -284,41 +266,21 @@ export function PlaybackControls({
)}
</div>
{/* Volume Control & Master Meter */}
<div className="flex items-center gap-4">
{/* Master Meter */}
<MasterMeter
{/* Master Controls */}
{onPanChange && onMuteToggle && (
<MasterControls
volume={volume}
pan={pan}
peakLevel={masterPeakLevel}
rmsLevel={masterRmsLevel}
isClipping={masterIsClipping}
isMuted={volume === 0}
onVolumeChange={onVolumeChange}
onPanChange={onPanChange}
onMuteToggle={onMuteToggle}
onResetClip={onResetClip}
/>
{/* Volume Control */}
<div className="flex items-center gap-3 min-w-[200px]">
<Button
variant="ghost"
size="icon"
onClick={handleMuteToggle}
title={isMuted ? 'Unmute' : 'Mute'}
>
{isMuted || volume === 0 ? (
<VolumeX className="h-5 w-5" />
) : (
<Volume2 className="h-5 w-5" />
)}
</Button>
<Slider
value={volume}
onChange={handleVolumeChange}
min={0}
max={1}
step={0.01}
className="flex-1"
/>
</div>
</div>
)}
</div>
</div>
);

View File

@@ -8,8 +8,7 @@ import { Button } from '@/components/ui/Button';
import { Slider } from '@/components/ui/Slider';
import { cn } from '@/lib/utils/cn';
import type { EffectType } from '@/lib/audio/effects/chain';
import { VerticalFader } from '@/components/ui/VerticalFader';
import { CircularKnob } from '@/components/ui/CircularKnob';
import { TrackControls } from './TrackControls';
import { AutomationLane } from '@/components/automation/AutomationLane';
import type { AutomationLane as AutomationLaneType, AutomationPoint as AutomationPointType } from '@/types/automation';
import { createAutomationPoint } from '@/lib/audio/automation/utils';
@@ -647,39 +646,25 @@ export function Track({
{/* Track Controls - Only show when not collapsed */}
{!track.collapsed && (
<div className="flex-1 flex flex-col items-center justify-between min-h-0 overflow-hidden">
{/* Pan Knob */}
<div className="flex-shrink-0">
<CircularKnob
value={track.pan}
onChange={onPanChange}
min={-1}
max={1}
step={0.01}
size={48}
label="PAN"
onTouchStart={handlePanTouchStart}
onTouchEnd={handlePanTouchEnd}
/>
</div>
{/* Integrated Track Controls (Pan + Fader + Mute) */}
<TrackControls
volume={track.volume}
pan={track.pan}
peakLevel={track.recordEnabled || isRecording ? recordingLevel : playbackLevel}
rmsLevel={track.recordEnabled || isRecording ? recordingLevel * 0.7 : playbackLevel * 0.7}
isMuted={track.mute}
onVolumeChange={onVolumeChange}
onPanChange={onPanChange}
onMuteToggle={onToggleMute}
onVolumeTouchStart={handleVolumeTouchStart}
onVolumeTouchEnd={handleVolumeTouchEnd}
onPanTouchStart={handlePanTouchStart}
onPanTouchEnd={handlePanTouchEnd}
/>
{/* Vertical Volume Fader with integrated meter */}
<div className="flex-1 flex items-center justify-center min-h-0">
<VerticalFader
value={track.volume}
level={track.recordEnabled || isRecording ? recordingLevel : playbackLevel}
onChange={onVolumeChange}
min={0}
max={1}
step={0.01}
showDb={true}
onTouchStart={handleVolumeTouchStart}
onTouchEnd={handleVolumeTouchEnd}
/>
</div>
{/* Inline Button Row - Below fader */}
{/* Inline Button Row - Below controls */}
<div className="flex-shrink-0 w-full">
{/* R/S/M inline row with icons */}
{/* R/S/A inline row with icons */}
<div className="flex items-center gap-1 justify-center">
{/* Record Arm */}
{onToggleRecordEnable && (
@@ -712,20 +697,6 @@ export function Track({
<Headphones className="h-3 w-3" />
</button>
{/* Mute Button */}
<button
onClick={onToggleMute}
className={cn(
'h-6 w-6 rounded flex items-center justify-center transition-all',
track.mute
? 'bg-blue-500 text-white shadow-md shadow-blue-500/30'
: 'bg-card hover:bg-accent text-muted-foreground border border-border/50'
)}
title="Mute track"
>
{track.mute ? <VolumeX className="h-3 w-3" /> : <Volume2 className="h-3 w-3" />}
</button>
{/* Automation Toggle */}
<button
onClick={() => {

View File

@@ -0,0 +1,85 @@
'use client';
import * as React from 'react';
import { Volume2, VolumeX } from 'lucide-react';
import { CircularKnob } from '@/components/ui/CircularKnob';
import { TrackFader } from './TrackFader';
import { cn } from '@/lib/utils/cn';
export interface TrackControlsProps {
volume: number;
pan: number;
peakLevel: number;
rmsLevel: number;
isMuted?: boolean;
onVolumeChange: (volume: number) => void;
onPanChange: (pan: number) => void;
onMuteToggle: () => void;
onVolumeTouchStart?: () => void;
onVolumeTouchEnd?: () => void;
onPanTouchStart?: () => void;
onPanTouchEnd?: () => void;
className?: string;
}
export function TrackControls({
volume,
pan,
peakLevel,
rmsLevel,
isMuted = false,
onVolumeChange,
onPanChange,
onMuteToggle,
onVolumeTouchStart,
onVolumeTouchEnd,
onPanTouchStart,
onPanTouchEnd,
className,
}: TrackControlsProps) {
return (
<div className={cn('flex flex-col items-center gap-2 py-2', className)}>
{/* Pan Control */}
<CircularKnob
value={pan}
onChange={onPanChange}
onTouchStart={onPanTouchStart}
onTouchEnd={onPanTouchEnd}
min={-1}
max={1}
step={0.01}
label="PAN"
size={40}
formatter={(value) => {
if (Math.abs(value) < 0.01) return 'C';
if (value < 0) return `${Math.abs(value * 100).toFixed(0)}L`;
return `${(value * 100).toFixed(0)}R`;
}}
/>
{/* Track Fader with Integrated Meters */}
<TrackFader
value={volume}
peakLevel={peakLevel}
rmsLevel={rmsLevel}
onChange={onVolumeChange}
onTouchStart={onVolumeTouchStart}
onTouchEnd={onVolumeTouchEnd}
/>
{/* Mute Button */}
<button
onClick={onMuteToggle}
className={cn(
'w-8 h-6 rounded text-[10px] font-bold transition-colors border',
isMuted
? 'bg-red-500/20 hover:bg-red-500/30 border-red-500/50 text-red-500'
: 'bg-muted/20 hover:bg-muted/30 border-border/50 text-muted-foreground'
)}
title={isMuted ? 'Unmute' : 'Mute'}
>
M
</button>
</div>
);
}

View File

@@ -0,0 +1,176 @@
'use client';
import * as React from 'react';
import { cn } from '@/lib/utils/cn';
export interface TrackFaderProps {
value: number;
peakLevel: number;
rmsLevel: number;
onChange: (value: number) => void;
onTouchStart?: () => void;
onTouchEnd?: () => void;
className?: string;
}
export function TrackFader({
value,
peakLevel,
rmsLevel,
onChange,
onTouchStart,
onTouchEnd,
className,
}: TrackFaderProps) {
const [isDragging, setIsDragging] = React.useState(false);
const containerRef = React.useRef<HTMLDivElement>(null);
// Convert linear 0-1 to dB scale for display
const linearToDb = (linear: number): number => {
if (linear === 0) return -60;
const db = 20 * Math.log10(linear);
return Math.max(-60, Math.min(0, db));
};
const valueDb = linearToDb(value);
const peakDb = linearToDb(peakLevel);
const rmsDb = linearToDb(rmsLevel);
// Calculate bar widths (0-100%)
const peakWidth = ((peakDb + 60) / 60) * 100;
const rmsWidth = ((rmsDb + 60) / 60) * 100;
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
setIsDragging(true);
onTouchStart?.();
updateValue(e.clientY);
};
const handleMouseMove = React.useCallback(
(e: MouseEvent) => {
if (!isDragging) return;
updateValue(e.clientY);
},
[isDragging]
);
const handleMouseUp = React.useCallback(() => {
setIsDragging(false);
onTouchEnd?.();
}, [onTouchEnd]);
const updateValue = (clientY: number) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const y = clientY - rect.top;
// Inverted: top = max (1), bottom = min (0)
const percentage = Math.max(0, Math.min(1, 1 - (y / rect.height)));
onChange(percentage);
};
React.useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}
}, [isDragging, handleMouseMove, handleMouseUp]);
return (
<div className={cn('flex gap-2', className)}>
{/* dB Labels (Left) */}
<div className="flex flex-col justify-between text-[9px] font-mono text-muted-foreground py-1">
<span>0</span>
<span>-12</span>
<span>-24</span>
<span>-60</span>
</div>
{/* Fader Container */}
<div
ref={containerRef}
className="relative w-10 h-32 bg-background/50 rounded-md border border-border/50 cursor-pointer"
onMouseDown={handleMouseDown}
>
{/* Peak Meter (Horizontal Bar - Top) */}
<div className="absolute inset-x-1.5 top-1.5 h-2.5 bg-background/80 rounded-sm overflow-hidden border border-border/30">
<div
className="absolute left-0 top-0 bottom-0 transition-all duration-75 ease-out"
style={{ width: `${Math.max(0, Math.min(100, peakWidth))}%` }}
>
<div className={cn(
'w-full h-full',
peakDb > -3 ? 'bg-red-500' :
peakDb > -6 ? 'bg-yellow-500' :
'bg-green-500'
)} />
</div>
</div>
{/* RMS Meter (Horizontal Bar - Bottom) */}
<div className="absolute inset-x-1.5 bottom-1.5 h-2.5 bg-background/80 rounded-sm overflow-hidden border border-border/30">
<div
className="absolute left-0 top-0 bottom-0 transition-all duration-150 ease-out"
style={{ width: `${Math.max(0, Math.min(100, rmsWidth))}%` }}
>
<div className={cn(
'w-full h-full',
rmsDb > -3 ? 'bg-red-400' :
rmsDb > -6 ? 'bg-yellow-400' :
'bg-green-400'
)} />
</div>
</div>
{/* Fader Track */}
<div className="absolute top-6 bottom-6 left-1/2 -translate-x-1/2 w-1 bg-muted/50 rounded-full" />
{/* Fader Handle */}
<div
className="absolute left-1/2 -translate-x-1/2 w-9 h-3.5 bg-primary/80 border-2 border-primary rounded-md shadow-lg cursor-grab active:cursor-grabbing pointer-events-none transition-all"
style={{
// Inverted: value 1 = top, value 0 = bottom
top: `calc(${(1 - value) * 100}% - 0.4375rem)`,
}}
>
{/* Handle grip lines */}
<div className="absolute inset-0 flex items-center justify-center gap-0.5">
<div className="h-1.5 w-px bg-primary-foreground/30" />
<div className="h-1.5 w-px bg-primary-foreground/30" />
<div className="h-1.5 w-px bg-primary-foreground/30" />
</div>
</div>
{/* dB Scale Markers */}
<div className="absolute inset-0 px-1.5 py-6 pointer-events-none">
<div className="relative h-full">
{/* -12 dB */}
<div className="absolute left-0 right-0 h-px bg-border/20" style={{ top: '50%' }} />
{/* -6 dB */}
<div className="absolute left-0 right-0 h-px bg-yellow-500/20" style={{ top: '20%' }} />
{/* -3 dB */}
<div className="absolute left-0 right-0 h-px bg-red-500/30" style={{ top: '10%' }} />
</div>
</div>
</div>
{/* Value Display (Right) */}
<div className="flex flex-col justify-center items-start text-[9px] font-mono">
{/* Current dB Value */}
<div className={cn(
'font-bold text-[10px]',
valueDb > -3 ? 'text-red-500' :
valueDb > -6 ? 'text-yellow-500' :
'text-green-500'
)}>
{valueDb > -60 ? `${valueDb.toFixed(1)}` : '-∞'}
</div>
</div>
</div>
);
}