import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react' const ALL_KEYS_WITH_MODES = [ 'C major', 'C minor', 'C# major', 'C# minor', 'D major', 'D minor', 'D# major', 'D# minor', 'E major', 'E minor', 'F major', 'F minor', 'F# major', 'F# minor', 'G major', 'G minor', 'G# major', 'G# minor', 'A major', 'A minor', 'A# major', 'A# minor', 'B major', 'B minor' ] const KEY_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] const MIN_BPM = 20 const MAX_BPM = 400 function clampBpm(value) { return Math.max(MIN_BPM, Math.min(MAX_BPM, value)) } function formatBpm(value) { if (!Number.isFinite(value)) return '' return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(2))) } function AnalysisDisplay({ detection, loading, onReady, onLiveTransform }) { const [targetKeyWithMode, setTargetKeyWithMode] = useState('C major') const [targetBpm, setTargetBpm] = useState(120) const [targetBpmInput, setTargetBpmInput] = useState('120') const readyDetectionRef = useRef(null) const setBpmValue = useCallback((value) => { const parsed = Number(value) if (!Number.isFinite(parsed)) return const next = clampBpm(parsed) setTargetBpm(next) setTargetBpmInput(formatBpm(next)) }, []) useEffect(() => { if (detection && onReady && readyDetectionRef.current !== detection) { readyDetectionRef.current = detection onReady() } }, [detection, onReady]) useEffect(() => { if (detection) { setTargetKeyWithMode(`${detection.key} ${detection.mode}`) setBpmValue(detection.bpm) } }, [detection, setBpmValue]) const targetKey = targetKeyWithMode.split(' ')[0] const semitones = useMemo(() => { if (!detection || !targetKey) return 0 const fromIdx = KEY_NAMES.indexOf(detection.key) const toIdx = KEY_NAMES.indexOf(targetKey) let diff = toIdx - fromIdx if (diff > 6) diff -= 12 if (diff < -6) diff += 12 return diff }, [detection, targetKey]) const handleKeyShift = useCallback((shift) => { const currentIdx = KEY_NAMES.indexOf(targetKey) const newIdx = (currentIdx + shift + 12) % 12 const mode = targetKeyWithMode.split(' ')[1] setTargetKeyWithMode(`${KEY_NAMES[newIdx]} ${mode}`) }, [targetKey, targetKeyWithMode]) const handleBpmInputChange = useCallback((event) => { const rawValue = event.target.value setTargetBpmInput(rawValue) if (rawValue.trim() === '') return const parsed = Number(rawValue) if (Number.isFinite(parsed) && parsed >= MIN_BPM && parsed <= MAX_BPM) { setTargetBpm(parsed) } }, []) const handleBpmInputBlur = useCallback(() => { const trimmedValue = targetBpmInput.trim() const parsed = trimmedValue === '' ? NaN : Number(trimmedValue) setBpmValue(Number.isFinite(parsed) ? parsed : targetBpm) }, [setBpmValue, targetBpm, targetBpmInput]) const handleBpmStep = useCallback((delta) => { setBpmValue(targetBpm + delta) }, [setBpmValue, targetBpm]) useEffect(() => { if (!detection || !onLiveTransform) return onLiveTransform({ semitones, originalBpm: detection.bpm, targetBpm, highQuality: true }) }, [detection, onLiveTransform, semitones, targetBpm]) const hasChanges = detection && (semitones !== 0 || Math.abs(targetBpm - detection.bpm) > 0.1) if (loading) { return (