'use client' import { useScene } from '@pascal-app/core' import { useCallback, useRef, useState } from 'react' import NumberFlow from '@number-flow/react' interface NumberInputProps { label: string value: number onChange: (value: number) => void min?: number max?: number precision?: number step?: number className?: string } export function NumberInput({ label, value, onChange, min, max, precision = 2, step = 0.1, className = '', }: NumberInputProps) { const [isEditing, setIsEditing] = useState(false) const [isDragging, setIsDragging] = useState(false) const [inputValue, setInputValue] = useState(value.toFixed(precision)) const startXRef = useRef(0) const startValueRef = useRef(0) const labelRef = useRef(null) const clamp = useCallback( (val: number) => { if (min !== undefined && val < min) return min if (max !== undefined && val > max) return max return val }, [min, max], ) const handleLabelMouseDown = useCallback( (e: React.MouseEvent) => { if (isEditing) return e.preventDefault() setIsDragging(true) startXRef.current = e.clientX startValueRef.current = value // Pause history tracking during drag useScene.temporal.getState().pause() let finalValue = value const handleMouseMove = (moveEvent: MouseEvent) => { const deltaX = moveEvent.clientX - startXRef.current // Determine step size based on modifier keys let dragStep = step // Default from prop if (moveEvent.shiftKey) { dragStep = step * 10 // Coarse } else if (moveEvent.altKey) { dragStep = step * 0.1 // Fine } const deltaValue = deltaX * dragStep const newValue = clamp(startValueRef.current + deltaValue) const newFinalValue = Number.parseFloat(newValue.toFixed(precision)) // Only call onChange if value actually changed (avoid extra processing on tiny moves) if (newFinalValue !== finalValue) { finalValue = newFinalValue onChange(finalValue) } } const handleMouseUp = () => { setIsDragging(false) document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) // Reset to initial value while still paused (no history entry) // Then resume and apply final value (creates single history entry) if (finalValue !== startValueRef.current) { onChange(startValueRef.current) useScene.temporal.getState().resume() onChange(finalValue) } else { useScene.temporal.getState().resume() } } document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) }, [isEditing, value, onChange, clamp, precision], ) const handleValueClick = useCallback(() => { setIsEditing(true) setInputValue(value.toFixed(precision)) }, [value, precision]) const handleInputChange = useCallback((e: React.ChangeEvent) => { setInputValue(e.target.value) }, []) const handleInputBlur = useCallback(() => { const numValue = Number.parseFloat(inputValue) if (!Number.isNaN(numValue)) { onChange(clamp(Number.parseFloat(numValue.toFixed(precision)))) } setIsEditing(false) }, [inputValue, onChange, clamp, precision]) const handleInputKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter') { const numValue = Number.parseFloat(inputValue) if (!Number.isNaN(numValue)) { onChange(clamp(Number.parseFloat(numValue.toFixed(precision)))) } setIsEditing(false) } else if (e.key === 'Escape') { setInputValue(value.toFixed(precision)) setIsEditing(false) } }, [inputValue, onChange, value, clamp, precision], ) return (
= (max ?? Math.max(10, value)) ? '0.5rem' : '0', borderBottomRightRadius: value >= (max ?? Math.max(10, value)) ? '0.5rem' : '0', borderTopLeftRadius: '0.5rem', borderBottomLeftRadius: '0.5rem', }} />
{label}
{isEditing ? ( ) : (
)}
) }