'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(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 handleTouchStart = (e: React.TouchEvent) => { e.preventDefault(); const touch = e.touches[0]; setIsDragging(true); onTouchStart?.(); updateValue(touch.clientY); }; const handleTouchMove = React.useCallback( (e: TouchEvent) => { if (!isDragging || e.touches.length === 0) return; const touch = e.touches[0]; updateValue(touch.clientY); }, [isDragging] ); const handleTouchEnd = React.useCallback(() => { setIsDragging(false); onTouchEnd?.(); }, [onTouchEnd]); const updateValue = (clientY: number) => { if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const y = clientY - rect.top; // Track has 32px (2rem) padding on top and bottom (top-8 bottom-8) const trackPadding = 32; const trackHeight = rect.height - (trackPadding * 2); // Clamp y to track bounds const clampedY = Math.max(trackPadding, Math.min(rect.height - trackPadding, y)); // Inverted: top = max (1), bottom = min (0) // Map clampedY from [trackPadding, height-trackPadding] to [1, 0] const percentage = 1 - ((clampedY - trackPadding) / trackHeight); onChange(Math.max(0, Math.min(1, percentage))); }; React.useEffect(() => { if (isDragging) { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); window.addEventListener('touchmove', handleTouchMove); window.addEventListener('touchend', handleTouchEnd); return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); window.removeEventListener('touchmove', handleTouchMove); window.removeEventListener('touchend', handleTouchEnd); }; } }, [isDragging, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]); return (
{/* dB Labels (Left) */}
0 -12 -24 -60
{/* Fader Container */}
{/* Peak Meter (Horizontal Bar - Top) */}
-3 ? 'bg-red-500' : peakDb > -6 ? 'bg-yellow-500' : 'bg-green-500' )} />
{/* RMS Meter (Horizontal Bar - Bottom) */}
-3 ? 'bg-red-500' : rmsDb > -6 ? 'bg-yellow-500' : 'bg-green-500' )} />
{/* Fader Track */}
{/* Fader Handle */}
{/* Handle grip lines */}
{/* dB Scale Markers */}
{/* -12 dB */}
{/* -6 dB */}
{/* -3 dB */}
{/* Value and Level Display (Right) */}
{/* Current dB Value */}
-3 ? 'text-red-500' : valueDb > -6 ? 'text-yellow-500' : 'text-green-500' )}> {valueDb > -60 ? `${valueDb.toFixed(1)}` : '-∞'}
{/* Spacer */}
{/* Peak Level */}
PK -3 ? 'text-red-500' : peakDb > -6 ? 'text-yellow-500' : 'text-green-500' )}> {peakDb > -60 ? `${peakDb.toFixed(1)}` : '-∞'}
{/* RMS Level */}
RM -3 ? 'text-red-500' : rmsDb > -6 ? 'text-yellow-500' : 'text-green-500' )}> {rmsDb > -60 ? `${rmsDb.toFixed(1)}` : '-∞'}
{/* dB Label */} dB
); }