Files
audio-ui/components/ui/Slider.tsx
Sebastian Krüger c54d5089c5 feat: complete Phase 9.3 - automation recording with write/touch/latch modes
Implemented comprehensive automation recording system for volume, pan, and effect parameters:

- Added automation recording modes:
  - Write: Records continuously during playback when values change
  - Touch: Records only while control is being touched/moved
  - Latch: Records from first touch until playback stops

- Implemented value change detection (0.001 threshold) to prevent infinite loops
- Fixed React setState-in-render errors by:
  - Using queueMicrotask() to defer state updates
  - Moving lane creation logic to useEffect
  - Properly memoizing touch handlers with useMemo

- Added proper value ranges for effect parameters:
  - Frequency: 20-20000 Hz
  - Q: 0.1-20
  - Gain: -40-40 dB

- Enhanced automation lane auto-creation with parameter-specific ranges
- Added touch callbacks to all parameter controls (volume, pan, effects)
- Implemented throttling (100ms) to avoid excessive automation points

Technical improvements:
- Used tracksRef and onRecordAutomationRef to ensure latest values in animation loops
- Added proper cleanup on playback stop
- Optimized recording to only trigger when values actually change

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 23:29:18 +01:00

109 lines
3.2 KiB
TypeScript

'use client';
import * as React from 'react';
import { cn } from '@/lib/utils/cn';
export interface SliderProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'onValueChange'> {
value?: number | number[];
onChange?: (value: number) => void;
onValueChange?: (value: number[]) => void;
min?: number;
max?: number;
step?: number;
label?: string;
showValue?: boolean;
onTouchStart?: () => void;
onTouchEnd?: () => void;
}
const Slider = React.forwardRef<HTMLInputElement, SliderProps>(
(
{
className,
value = 0,
onChange,
onValueChange,
min = 0,
max = 100,
step = 1,
label,
showValue = false,
disabled,
onTouchStart,
onTouchEnd,
...props
},
ref
) => {
// Support both value formats (number or number[])
const currentValue = Array.isArray(value) ? value[0] : value;
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const numValue = parseFloat(e.target.value);
onChange?.(numValue);
onValueChange?.([numValue]);
};
const handleMouseDown = () => {
onTouchStart?.();
};
const handleMouseUp = () => {
onTouchEnd?.();
};
React.useEffect(() => {
if (onTouchEnd) {
window.addEventListener('mouseup', handleMouseUp);
return () => window.removeEventListener('mouseup', handleMouseUp);
}
}, [onTouchEnd]);
return (
<div className={cn('w-full', className)}>
{(label || showValue) && (
<div className="flex items-center justify-between mb-2">
{label && (
<label className="text-sm font-medium text-foreground">
{label}
</label>
)}
{showValue && (
<span className="text-sm text-muted-foreground">{currentValue}</span>
)}
</div>
)}
<input
ref={ref}
type="range"
min={min}
max={max}
step={step}
value={currentValue}
onChange={handleChange}
onMouseDown={handleMouseDown}
disabled={disabled}
className={cn(
'w-full h-2 bg-secondary rounded-lg appearance-none cursor-pointer',
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'disabled:opacity-50 disabled:cursor-not-allowed',
'[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4',
'[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary',
'[&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:transition-colors',
'[&::-webkit-slider-thumb]:hover:bg-primary/90',
'[&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:rounded-full',
'[&::-moz-range-thumb]:bg-primary [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:cursor-pointer',
'[&::-moz-range-thumb]:transition-colors [&::-moz-range-thumb]:hover:bg-primary/90'
)}
{...props}
/>
</div>
);
}
);
Slider.displayName = 'Slider';
export { Slider };