📄 GuitaleleViewer.js
/home/palash/git/site/node_modules/new_site/src/components/tab-viewer/GuitaleleViewer.js
Language: js • Lines: 903
import React, { useState, useMemo, useRef, useEffect } from "react";
import {
    DARK_THEME,
    calculateSchedulerBoundaries,
    getDurationLabel,
} from "./guitaleleViewerUtils";

import {
    stopPlaying,
    pausePlaying,
    resumePlaying,
    startPlaying,
    runScheduler
} from "./audio";
import { Table, Row, Col, Button, Form } from "react-bootstrap";

import { buildSvg } from "./svgUtils";
import { useBuildScoreLayout } from "./scoreBuilder";

export default function GuitaleleViewer({ scoreData }) {
    const [clickedNoteIndex, setClickedNoteIndex] = useState(null);
    const [isPlaying, setIsPlaying] = useState(false);
    const [isPaused, setIsPaused] = useState(false);
    const [playbackIndex, setPlaybackIndex] = useState(null);
    const [bpm, setBpm] = useState(100);
    const [playbackProgress, setPlaybackProgress] = useState(null);
    const [slotWidth, setSlotWidth] = useState(6);
    const containerRef = useRef(null);
    const viewerRef = useRef(null);
    const [isFullscreen, setIsFullscreen] = useState(false);
    const [isAudioCompiled, setIsAudioCompiled] = useState(false);

    const [voice1Enabled, setVoice1Enabled] = useState(true);
    const [voice2Enabled, setVoice2Enabled] = useState(true);
    const [metronomeEnabled, setMetronomeEnabled] = useState(false);
    const [viewMode, setViewMode] = useState('tab'); // 'tab', 'both', 'sheet'

    const lookaheadTimerRef = useRef(null);

    const { lookaheadInterval, scheduleAheadTime } = useMemo(() => {
        return calculateSchedulerBoundaries(bpm);
    }, [bpm]);

    const currentTimelineBeatsRef = useRef([]); // Holds unique sorted beat time slices
    const nextBeatIndexRef = useRef(0);

    // --- Responsive Layout State ---
    const [measuresPerRow, setMeasuresPerRow] = useState(4);
    const [isStackedLayout, setIsStackedLayout] = useState(false);

    const audioCtxRef = useRef(null);
    const playbackTimeoutsRef = useRef([]);

    // Track playback state mapping
    const currentPlaybackEventsRef = useRef([]);
    const playbackStartBeatRef = useRef(0);
    const pausedTimeRef = useRef(0);
    const playbackStartTimeRef = useRef(0);

    // Responsive window observer
    useEffect(() => {
        const updateLayoutBoundaries = () => {
            if (window.innerWidth < 480) {
                setMeasuresPerRow(1);
                setSlotWidth(40);
                setIsStackedLayout(true);
            } else if (window.innerWidth < 768) {
                setMeasuresPerRow(2);
                setSlotWidth(50);
                setIsStackedLayout(true);
            } else {
                setMeasuresPerRow(4);
                setSlotWidth(50);
                setIsStackedLayout(false);
            }
        };

        updateLayoutBoundaries();
        window.addEventListener("resize", updateLayoutBoundaries);
        return () =>
            window.removeEventListener("resize", updateLayoutBoundaries);
    }, []);

    useEffect(() => {
        if (!isPlaying) {
            setClickedNoteIndex(null);
        }
    }, [isPlaying]);

    const toggleFullscreen = () => {
        if (!document.fullscreenElement) {
            viewerRef.current?.requestFullscreen?.();
        } else {
            document.exitFullscreen?.();
        }
    };

    useEffect(() => {
        const handler = () => setIsFullscreen(!!document.fullscreenElement);
        document.addEventListener('fullscreenchange', handler);
        return () => document.removeEventListener('fullscreenchange', handler);
    }, []);

    const scoreLayout = useBuildScoreLayout(scoreData, slotWidth, measuresPerRow);

    const preCompiledTimelineRef = useRef([]);

    useEffect(() => {
        if (!scoreLayout) return;

        setIsAudioCompiled(false);

        const timer = setTimeout(() => {
            const allEvents = scoreLayout.computedRows.flatMap(
                r => r.rowEvents
            );
            const compiledAudioTimeline = [];
            const consumedPitches = new Set();



            allEvents.forEach((ev, evIdx) => {
                if (ev.isMetronomeTick) {
                    compiledAudioTimeline.push({
                        ...ev,
                        type: "metronome",
                        isMetronomeTick: true,
                        voice: 0,
                        startBeat: ev.startBeat,
                        globalIndex: ev.globalIndex,
                        preCalculatedJitter: 0,
                        preCalculatedVelocity: 0,
                        segments: [{ type: "rest", duration: ev.beatValue }]
                    });
                    return;
                }

                if (ev.isRest) {
                    // Include rest events in the precompiled timeline so they can be visually highlighted
                    compiledAudioTimeline.push({
                        type: "rest",
                        voice: ev.voice,
                        startBeat: ev.startBeat,
                        globalIndex: ev.globalIndex,
                        preCalculatedJitter: 0,
                        preCalculatedVelocity: 0,
                        segments: [{ type: "rest", duration: ev.beatValue }]
                    });
                    return;
                }

                ev.processedPitches.forEach(pitch => {
                    const pitchKey = `${ev.globalIndex}_${pitch.string}`;
                    if (consumedPitches.has(pitchKey)) return;

                    if (pitch.fret === null) {
                        compiledAudioTimeline.push({
                            type: "mute",
                            voice: ev.voice,
                            startBeat: ev.startBeat,
                            globalIndex: ev.globalIndex,
                            preCalculatedJitter: (Math.random() - 0.5) * 0.003, // Pre-calculate here!
                            preCalculatedVelocity: 0.88 + Math.random() * 0.22,
                            segments: [{ type: "mute", duration: ev.beatValue }]
                        });
                        return;
                    }

                    const segments = [
                        {
                            type: "pluck",
                            midi: pitch.midi,
                            duration: ev.beatValue
                        }
                    ];

                    let currentEvent = ev;
                    let currentEventIdx = evIdx;
                    let currentPitch = pitch;

                    while (currentEvent.isTiedToNext) {
                        const nextEvent = allEvents
                            .slice(currentEventIdx + 1)
                            .find(
                                e => e.voice === currentEvent.voice && !e.isRest
                            );
                        if (!nextEvent) break;

                        const nextPitch = nextEvent.processedPitches.find(
                            np => np.string === currentPitch.string
                        );
                        if (!nextPitch) break;

                        const nextDuration = nextEvent.beatValue;

                        if (!segments[segments.length - 1].tiedEventIndices) {
                            segments[segments.length - 1].tiedEventIndices = [];
                        }
                        segments[segments.length - 1].tiedEventIndices.push({
                            globalIndex: nextEvent.globalIndex,
                            beatOffset: nextEvent.startBeat - ev.startBeat
                        });

                        if (nextPitch.midi === currentPitch.midi) {
                            segments.push({
                                type: "tie",
                                midi: nextPitch.midi,
                                duration: nextDuration
                            });
                        } else {
                            const currentSeg = segments[segments.length - 1];
                            const halfDuration = currentSeg.duration / 2;
                            currentSeg.duration = halfDuration;

                            segments.push({
                                type: "slide",
                                midi: nextPitch.midi,
                                duration: halfDuration
                            });
                            segments.push({
                                type: "tie",
                                midi: nextPitch.midi,
                                duration: nextDuration
                            });
                        }

                        const nextKey = `${nextEvent.globalIndex}_${nextPitch.string}`;
                        consumedPitches.add(nextKey);

                        currentEventIdx = allEvents.indexOf(nextEvent);
                        currentEvent = nextEvent;
                        currentPitch = nextPitch;
                    }

                    compiledAudioTimeline.push({
                        type: "pluck",
                        voice: ev.voice,
                        startBeat: ev.startBeat,
                        globalIndex: ev.globalIndex,
                        midi: pitch.midi,
                        preCalculatedJitter: (Math.random() - 0.5) * 0.003, // Pre-calculate here!
                        preCalculatedVelocity: 0.88 + Math.random() * 0.22, // Pre-calculate here!
                        segments: segments
                    });
                });
            });

            preCompiledTimelineRef.current = compiledAudioTimeline;
            setIsAudioCompiled(true);
        }, 100);

        return () => clearTimeout(timer);
    }, [scoreLayout]);

    // Extract an array of all unique voice IDs present in the score
    const availableVoices = useMemo(() => {
        if (!scoreLayout || !scoreLayout.computedRows) return [];

        const allEvents = scoreLayout.computedRows.flatMap(r => r.rowEvents);
        const voiceIds = allEvents.map(ev => ev.voice);

        // Using a Set eliminates duplicates, then Array.from turns it back into a sorted array
        return Array.from(new Set(voiceIds)).sort((a, b) => a - b);
    }, [scoreLayout]);

    const stopPlayback = stopPlaying(lookaheadTimerRef, playbackTimeoutsRef, setIsPlaying, setIsPaused, setPlaybackIndex, pausedTimeRef, audioCtxRef);

    const pausePlayback = pausePlaying(isPlaying, isPaused, lookaheadTimerRef, playbackTimeoutsRef, audioCtxRef, playbackStartTimeRef, pausedTimeRef, setIsPaused);

    const runSchedulerLoop = runScheduler(playbackStartBeatRef, audioCtxRef, bpm, playbackStartTimeRef, pausedTimeRef, nextBeatIndexRef, currentTimelineBeatsRef, scheduleAheadTime, setPlaybackIndex, playbackTimeoutsRef, stopPlayback, lookaheadTimerRef, lookaheadInterval, metronomeEnabled);

    const resumePlayback = resumePlaying(isPlaying, isPaused, audioCtxRef, playbackStartTimeRef, setIsPaused, runSchedulerLoop);

    const startPlayback = startPlaying(isPlaying, scoreLayout, isAudioCompiled, audioCtxRef, setIsPlaying, setIsPaused, pausedTimeRef, playbackStartTimeRef, currentPlaybackEventsRef, playbackStartBeatRef, preCompiledTimelineRef, currentTimelineBeatsRef, nextBeatIndexRef, runSchedulerLoop, voice1Enabled, voice2Enabled, metronomeEnabled);


    useEffect(() => {
        return () => stopPlayback();
    }, []);

    const countInBeat = isPlaying && playbackIndex !== null && playbackIndex < 0 ? -playbackIndex : 0;
    const lastVoiceEventsRef = useRef({ v1: [], v2: [] });
    const activeTargetIndex = isPlaying
        ? (isPaused && clickedNoteIndex !== null ? clickedNoteIndex : playbackIndex)
        : clickedNoteIndex;

    const handleNoteClick = (globalIndex) => {
        if (isPlaying && !isPaused) return;

        setClickedNoteIndex(prevIndex => {
            return prevIndex === globalIndex ? null : globalIndex;
        });
    };

    // Replace `activeEvent` with `activeEvents` and `activeIndices`
    const activeEvents = useMemo(() => {
        if (activeTargetIndex === null || !scoreLayout) return [];

        const allEvents = scoreLayout.computedRows.flatMap(r => r.rowEvents);
        const targetEvent = allEvents.find(
            ev => ev.globalIndex === activeTargetIndex
        );

        if (!targetEvent) return [];

        // Fetch all playable events at this beat. Metronome-only beats should not drive note highlighting.
        return allEvents.filter(
            ev =>
                ev.measureNumber === targetEvent.measureNumber &&
                ev.startBeat === targetEvent.startBeat &&
                !ev.isMetronomeTick
        ).sort((a, b) => (a.voice || 1) - (b.voice || 1));
    }, [activeTargetIndex, scoreLayout]);

    const activeIndices = useMemo(
        () => activeEvents.map(e => e.globalIndex),
        [activeEvents]
    );

    // Compute global track statistics
    const trackStats = useMemo(() => {
        if (!scoreLayout || !scoreData) return null;

        const totalMeasures = scoreData.measures?.length || 0;
        const timeSig = scoreData.timeSignature || '4/4';
        const keySig = scoreData.key || scoreData.keySignature || 'C';
        const numerator = parseInt(timeSig.split('/')[0], 10) || 4;
        const denominator = parseInt(timeSig.split('/')[1], 10) || 4;
        const beatsPerMeasure = numerator * (4 / denominator);
        const totalBeats = totalMeasures * beatsPerMeasure;
        const totalSeconds = (totalBeats * 60) / bpm;

        const minutes = Math.floor(totalSeconds / 60);
        const seconds = Math.round(totalSeconds % 60);

        // Count distinct notes (non-rest, non-metronome)
        const allEvents = scoreLayout.computedRows?.flatMap(r => r.rowEvents) || [];
        const noteCount = allEvents.filter(ev => !ev.isRest && !ev.isMetronomeTick).length;

        return {
            totalMeasures,
            timeSig,
            keySig,
            bpm,
            totalDuration: `${minutes}:${seconds.toString().padStart(2, '0')}`,
            noteCount,
            beatsPerMeasure
        };
    }, [scoreLayout, scoreData, bpm]);

    useEffect(() => {
        if (!isPlaying) {
            lastVoiceEventsRef.current = { v1: [], v2: [] };
            return;
        }

        const currentV1 = activeEvents.filter(ev => ev.voice === 1);
        const currentV2 = activeEvents.filter(ev => ev.voice === 2);

        if (currentV1.length > 0) lastVoiceEventsRef.current.v1 = currentV1;
        if (currentV2.length > 0) lastVoiceEventsRef.current.v2 = currentV2;
    }, [activeEvents, isPlaying]);

    const displayEvents = useMemo(() => {
        if (!isPlaying) return activeEvents;

        const currentV1 = activeEvents.filter(ev => ev.voice === 1);
        const currentV2 = activeEvents.filter(ev => ev.voice === 2);

        const merged = [...activeEvents];

        if (currentV1.length === 0 && lastVoiceEventsRef.current.v1.length > 0) {
            merged.push(...lastVoiceEventsRef.current.v1);
        }
        if (currentV2.length === 0 && lastVoiceEventsRef.current.v2.length > 0) {
            merged.push(...lastVoiceEventsRef.current.v2);
        }

        return merged;
    }, [activeEvents, isPlaying]);

    const activeDescription = useMemo(() => {
        if (!displayEvents || displayEvents.length === 0) return null;

        const voiceColors = ['#21cea3', '#fb923c'];

        const v1Events = displayEvents.filter(ev => ev.voice === 1);
        const v2Events = displayEvents.filter(ev => ev.voice === 2);

        const renderVoiceColumn = (events, voiceLabel, color) => {
            if (events.length === 0) return null;
            return (
                <div style={{ flex: '1 1 0', minWidth: 0, borderLeft: `2px solid ${color}`, paddingLeft: '8px' }}>
                    {events.map((ev, idx) => {
                        const notes = ev.isRest
                            ? [{ noteName: 'Rest' }]
                            : ev.processedPitches;
                        return (
                            <div key={idx}>
                                <div style={{ fontSize: '10px', color: '#8892b0', marginBottom: '1px' }}>
                                    <span style={{ color }}>{voiceLabel}</span>
                                    {!ev.isRest && <span style={{ marginLeft: '4px' }}>{getDurationLabel(ev.beatValue)}</span>}
                                </div>
                                {notes.map((p, pIdx) => (
                                    <div key={pIdx} style={{ color, fontSize: '13px', fontWeight: '600', lineHeight: 1.3 }}>
                                        {p.fret === null ? (
                                            <span style={{ color: '#aaccff', fontStyle: 'italic', fontWeight: '400', fontSize: '11px' }}>
                                                Muted s{p.string}
                                            </span>
                                        ) : (
                                            <span style={{ color: '#dfe6e9', fontWeight: '400' }}>
                                                {p.noteName}
                                                {p.string !== undefined && <span style={{ color: '#8892b0', fontSize: '11px', marginLeft: '4px' }}>s{p.string} f{p.fret}</span>}
                                            </span>
                                        )}
                                    </div>
                                ))}
                                {ev.description && (
                                    <div style={{ fontSize: '10px', color: '#aaccff', fontStyle: 'italic', lineHeight: 1.2, marginTop: '1px' }}>
                                        {ev.description}
                                    </div>
                                )}
                            </div>
                        );
                    })}
                </div>
            );
        };

        return (
            <div className="d-flex" style={{ gap: '12px' }}>
                {v1Events.length > 0 && renderVoiceColumn(v1Events, 'V1', voiceColors[0])}
                {v2Events.length > 0 && renderVoiceColumn(v2Events, 'V2', voiceColors[1])}
            </div>
        );
    }, [displayEvents]);

    // Compute playback progress from beat position (monotonic, unlike event index)
    const beatRange = useMemo(() => {
        if (!scoreLayout || !scoreLayout.computedRows) return null;
        const allEvents = scoreLayout.computedRows.flatMap(r => r.rowEvents);
        if (allEvents.length === 0) return null;
        const firstBeat = allEvents[0].startBeat;
        const lastEv = allEvents[allEvents.length - 1];
        const lastBeat = lastEv.startBeat + (lastEv.beatValue || 1);
        return { firstBeat, lastBeat, totalBeats: lastBeat - firstBeat };
    }, [scoreLayout]);

    useEffect(() => {
        if (isPlaying && playbackIndex !== null && beatRange && beatRange.totalBeats > 0) {
            if (playbackIndex < 0) {
                // Count-in beats — progress is 0
                setPlaybackProgress(0);
                return;
            }
            const allEvents = scoreLayout.computedRows.flatMap(r => r.rowEvents);
            const currentEvent = allEvents.find(ev => ev.globalIndex === playbackIndex);
            if (currentEvent) {
                const currentBeat = currentEvent.startBeat;
                const rawProgress = ((currentBeat - beatRange.firstBeat) / beatRange.totalBeats) * 100;
                const progress = Math.max(0, Math.min(100, Math.round(rawProgress)));
                setPlaybackProgress(progress);
            } else {
                setPlaybackProgress(0);
            }
        } else if (!isPlaying) {
            setPlaybackProgress(null);
        }
    }, [playbackIndex, isPlaying, beatRange, scoreLayout]);

    // 1. Find which row index contains the currently playing note index
    const activeRowIndex = useMemo(() => {
        if (playbackIndex === null || !scoreLayout) return -1;
        return scoreLayout.computedRows.findIndex(row =>
            row.rowEvents.some(ev => ev.globalIndex === playbackIndex)
        );
    }, [playbackIndex, scoreLayout]);

    // 2. Efficiently snap to the top of the row ONLY when changing lines
    const prevRowIndexRef = useRef(-1);
    useEffect(() => {
        const scrollContainer = containerRef.current;
        if (!scrollContainer) return;

        if (!isPlaying) {
            scrollContainer.scrollTo({
                top: 0,
                behavior: "smooth"
            });
            prevRowIndexRef.current = -1;
            return;
        }

        if (activeRowIndex !== -1 && activeRowIndex !== prevRowIndexRef.current) {
            prevRowIndexRef.current = activeRowIndex;

            const rowElements = scrollContainer.querySelectorAll("tbody tr");
            const targetRow = rowElements[activeRowIndex];

            if (targetRow) {
                const containerRect = scrollContainer.getBoundingClientRect();
                const rowRect = targetRow.getBoundingClientRect();

                // Calculate the position to line up the row's top edge perfectly with the container's top
                const targetScrollTop = rowRect.top - containerRect.top + scrollContainer.scrollTop;

                scrollContainer.scrollTo({
                    top: Math.max(0, targetScrollTop - 20), // 4px padding for a clean aesthetic look
                    behavior: "auto"
                });
            }
        }
    }, [activeRowIndex, isPlaying]);


    if (!scoreLayout) {
        return (
            <div className="text-slate-500 font-mono text-xs p-4">
                No notation data package available.
            </div>
        );
    }

    const {
        computedRows,
        timeSigTop,
        timeSigBottom,
        paddingX,
        trebleTopY,
        bassTopY,
        tabTopY,
        rhythmTopY,
        lineSpacing,
        SLOT_WIDTH,
        measureValidityMap,
        beatsPerMeasure
    } = scoreLayout;

    const rhythm1TopY = rhythmTopY;
    const rhythm2TopY = rhythmTopY + 28;

    return (
        <>
            <style>{`
                @keyframes countin-fade {
                    0% { opacity: 1; transform: scale(1.3); }
                    100% { opacity: 0; transform: scale(1); }
                }
            `}</style>
            <div
                ref={viewerRef}
                className="d-flex flex-column bg-dark"
                style={{
                    height: isFullscreen ? '100vh' : 'calc(100vh - 56px)',
                    overflow: 'hidden'
                }}
            >
            <div className={`bg-dark border-bottom border-secondary text-light shrink-0 ${isStackedLayout ? 'px-0 py-1' : 'p-2'}`} style={{ height: 'auto' }}>
                {isStackedLayout ? (
                    /* ----- Mobile: stacked rows ----- */
                    <div className="d-flex flex-column gap-1">
                        <div className="d-flex gap-1">
                            <div className="d-flex flex-column gap-1" style={{ flex: '2 1 auto' }}>
                                <div className="btn-group bg-black p-1 rounded border border-secondary" style={{ height: '30px', alignItems: 'center' }}>
                                    {!isPlaying ? (
                                        <Button variant="link" onClick={startPlayback} className="text-success p-1" title="Start"
                                            disabled={!((voice1Enabled && availableVoices.includes(1)) || (voice2Enabled && availableVoices.includes(2)) || metronomeEnabled)}>
                                            <svg width="18" height="18" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z" /></svg>
                                        </Button>
                                    ) : isPaused ? (
                                        <Button variant="link" onClick={resumePlayback} className="text-warning p-1" title="Resume">
                                            <svg width="18" height="18" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z" /></svg>
                                        </Button>
                                    ) : (
                                        <Button variant="link" onClick={pausePlayback} className="text-warning p-1" title="Pause">
                                            <svg width="18" height="18" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" /></svg>
                                        </Button>
                                    )}
                                    <Button variant="link" onClick={stopPlayback} disabled={!isPlaying} className={`p-1 ${isPlaying ? 'text-danger' : 'text-muted'}`} title="Stop">
                                        <svg width="18" height="18" fill="currentColor" viewBox="0 0 24 24"><path d="M6 6h12v12H6z" /></svg>
                                    </Button>
                                </div>
                                <div className="bg-black px-2 py-1 rounded border border-secondary d-flex align-items-center" style={{ height: '26px' }}>
                                    <Form.Range min="40" max="240" value={bpm}
                                        onChange={(e) => setBpm(parseInt(e.target.value, 10))}
                                        disabled={isPlaying} className="flex-grow-1" style={{ height: '5px' }} />
                                    <span className="text-warning fw-bold font-monospace" style={{ fontSize: '9px', minWidth: '22px' }}>{bpm}</span>
                                </div>
                                <div className="bg-black px-1 rounded border border-secondary d-flex align-items-center" style={{ height: '12px' }}>
                                    <div style={{ width: `${isPlaying && playbackProgress !== null ? playbackProgress : 0}%`, height: '4px',
                                        background: DARK_THEME.progressFill, borderRadius: '2px', transition: 'width 0.3s ease' }} />
                                </div>
                            </div>
                            <div className="d-flex flex-column gap-1" style={{ flex: '1 1 auto' }}>
                                <div className="bg-black px-2 py-1 rounded border border-secondary d-flex flex-column gap-1">
                                    <div style={{ fontSize: '8px', textTransform: 'uppercase', letterSpacing: '1px', color: '#586278' }}>Audio Tracks</div>
                                    <div className="d-flex align-items-center gap-1">
                                        <div className="d-flex align-items-center justify-content-between" style={{ width: '50px', height: '16px' }}>
                                            <span style={{ fontSize: '10px', color: voice1Enabled ? DARK_THEME.voice1Color : '#8892b0', fontWeight: 'bold' }}>V1</span>
                                            <Form.Check type="switch" id="voice-toggle-0" label=""
                                                className="m-0 d-flex align-items-center"
                                                style={{ transform: 'scale(0.65)', transformOrigin: 'right center' }}
                                                checked={voice1Enabled && availableVoices.includes(1)}
                                                disabled={isPlaying || !availableVoices.includes(1)}
                                                onChange={(e) => setVoice1Enabled(e.target.checked)} />
                                        </div>
                                        <div className="d-flex align-items-center justify-content-between" style={{ width: '50px', height: '16px' }}>
                                            <span style={{ fontSize: '10px', color: voice2Enabled ? DARK_THEME.voice2Color : '#8892b0', fontWeight: 'bold' }}>V2</span>
                                            <Form.Check type="switch" id="voice-toggle-1" label=""
                                                className="m-0 d-flex align-items-center"
                                                style={{ transform: 'scale(0.65)', transformOrigin: 'right center' }}
                                                checked={voice2Enabled && availableVoices.includes(2)}
                                                disabled={isPlaying || !availableVoices.includes(2)}
                                                onChange={(e) => setVoice2Enabled(e.target.checked)} />
                                        </div>
                                        <div className="d-flex align-items-center justify-content-between" style={{ width: '50px', height: '16px' }}>
                                            <span style={{ fontSize: '10px', color: metronomeEnabled ? DARK_THEME.metronomeControlMedium : '#8892b0', fontWeight: 'bold' }}>Met</span>
                                            <Form.Check type="switch" id="metronome-toggle" label=""
                                                className="m-0 d-flex align-items-center"
                                                style={{ transform: 'scale(0.65)', transformOrigin: 'right center' }}
                                                checked={metronomeEnabled} disabled={isPlaying}
                                                onChange={(e) => setMetronomeEnabled(e.target.checked)} />
                                        </div>
                                    </div>
                                </div>
                                <div className="bg-black px-2 py-1 rounded border border-secondary d-flex flex-column gap-1">
                                    <div style={{ fontSize: '8px', textTransform: 'uppercase', letterSpacing: '1px', color: '#586278' }}>Audio Tracks</div>
                                    <div className="d-flex align-items-center gap-1" style={{ height: '18px' }}>
                                        {['tab','both','sheet'].map(m => (
                                            <button key={m} onClick={() => !isPlaying && setViewMode(m)} disabled={isPlaying}
                                                style={{ fontSize: '9px', fontWeight: viewMode === m ? '700' : '400',
                                                    color: viewMode === m ? '#fff' : '#8892b0',
                                                    background: viewMode === m ? (m === 'tab' ? '#1a6b4a' : m === 'sheet' ? '#6b4a8a' : '#2a5a7a') : 'transparent',
                                                    border: `1px solid ${viewMode === m ? 'transparent' : '#3a3a5a'}`,
                                                    borderRadius: m === 'tab' ? '4px 0 0 4px' : m === 'sheet' ? '0 4px 4px 0' : '0',
                                                    padding: '0 7px', lineHeight: '16px', cursor: isPlaying ? 'default' : 'pointer',
                                                    opacity: isPlaying ? 0.5 : 1, transition: 'all 0.15s ease',
                                                    textTransform: 'uppercase', letterSpacing: '0.3px' }}
                                            >
                                                {m === 'tab' ? 'Tab' : m === 'both' ? 'Both' : 'Staff'}
                                            </button>
                                        ))}
                                    </div>
                                </div>
                            </div>
                        </div>
                        <div className="bg-black border border-secondary rounded p-2 font-monospace"
                            style={{ fontSize: '12px', lineHeight: '1.2', overflowY: 'auto', height: '74px' }}>
                            {activeDescription ? (
                                activeDescription
                            ) : trackStats ? (
                                <div className="d-flex flex-column" style={{ gap: '3px', color: '#8892b0' }}>
                                    <div style={{ fontSize: '8px', textTransform: 'uppercase', letterSpacing: '1px', color: '#586278' }}>Track Info</div>
                                    <div className="d-flex" style={{ gap: '8px', flexWrap: 'wrap' }}>
                                        <span style={{ fontSize: '10px' }}><span style={{ color: '#64748b' }}>⌛</span> {trackStats.totalDuration}</span>
                                        <span style={{ fontSize: '10px' }}><span style={{ color: '#64748b' }}>♩</span> {trackStats.timeSig}</span>
                                        <span style={{ fontSize: '10px' }}><span style={{ color: '#64748b' }}>♯</span> {trackStats.keySig}</span>
                                        <span style={{ fontSize: '10px' }}><span style={{ color: '#64748b' }}>𝄆</span> {trackStats.totalMeasures} bars</span>
                                        <span style={{ fontSize: '10px' }}><span style={{ color: '#64748b' }}>♪</span> {trackStats.noteCount} notes</span>
                                        <span style={{ fontSize: '10px' }}><span style={{ color: '#64748b' }}>BPM</span> {trackStats.bpm}</span>
                                    </div>
                                </div>
                            ) : (
                                <span className="text-muted fst-italic" style={{ fontSize: '11px' }}>Select a note to view properties.</span>
                            )}
                        </div>
                    </div>
                ) : (
                    /* ----- Desktop: 3-column layout ----- */
                    <div className="d-flex gap-3" style={{ minHeight: '76px' }}>
                        <div className="d-flex flex-column gap-1" style={{ minWidth: '160px', flexShrink: 0 }}>
                            <div className="btn-group bg-black p-1 rounded border border-secondary" style={{ height: '30px', alignItems: 'center' }}>
                                {!isPlaying ? (
                                    <Button variant="link" onClick={startPlayback} className="text-success p-1" title="Start"
                                        disabled={!((voice1Enabled && availableVoices.includes(1)) || (voice2Enabled && availableVoices.includes(2)) || metronomeEnabled)}>
                                        <svg width="18" height="18" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z" /></svg>
                                    </Button>
                                ) : isPaused ? (
                                    <Button variant="link" onClick={resumePlayback} className="text-warning p-1" title="Resume">
                                        <svg width="18" height="18" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z" /></svg>
                                    </Button>
                                ) : (
                                    <Button variant="link" onClick={pausePlayback} className="text-warning p-1" title="Pause">
                                        <svg width="18" height="18" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" /></svg>
                                    </Button>
                                )}
                                <Button variant="link" onClick={stopPlayback} disabled={!isPlaying} className={`p-1 ${isPlaying ? 'text-danger' : 'text-muted'}`} title="Stop">
                                    <svg width="18" height="18" fill="currentColor" viewBox="0 0 24 24"><path d="M6 6h12v12H6z" /></svg>
                                </Button>
                            </div>
                            <div className="bg-black px-2 py-1 rounded border border-secondary d-flex align-items-center" style={{ height: '26px' }}>
                                <Form.Range min="40" max="240" value={bpm}
                                    onChange={(e) => setBpm(parseInt(e.target.value, 10))}
                                    disabled={isPlaying} className="flex-grow-1" style={{ height: '5px' }} />
                                <span className="text-warning fw-bold font-monospace" style={{ fontSize: '9px', minWidth: '22px' }}>{bpm}</span>
                            </div>
                            <div className="bg-black px-1 rounded border border-secondary d-flex align-items-center" style={{ height: '12px' }}>
                                <div style={{ width: `${isPlaying && playbackProgress !== null ? playbackProgress : 0}%`, height: '4px',
                                    background: DARK_THEME.progressFill, borderRadius: '2px', transition: 'width 0.3s ease' }} />
                            </div>
                        </div>
                        <div className="d-flex flex-column gap-1" style={{ minWidth: '180px', flexShrink: 0 }}>
                                <div className="bg-black px-2 py-1 rounded border border-secondary d-flex flex-column gap-1">
                                    <div style={{ fontSize: '8px', textTransform: 'uppercase', letterSpacing: '1px', color: '#586278' }}>View Options</div>
                                <div className="d-flex align-items-center gap-2">
                                    <div className="d-flex align-items-center justify-content-between" style={{ width: '52px', height: '16px' }}>
                                        <span style={{ fontSize: '10px', color: voice1Enabled ? DARK_THEME.voice1Color : '#8892b0', fontWeight: 'bold' }}>V1</span>
                                        <Form.Check type="switch" id="voice-toggle-0" label=""
                                            className="m-0 d-flex align-items-center"
                                            style={{ transform: 'scale(0.7)', transformOrigin: 'right center' }}
                                            checked={voice1Enabled && availableVoices.includes(1)}
                                            disabled={isPlaying || !availableVoices.includes(1)}
                                            onChange={(e) => setVoice1Enabled(e.target.checked)} />
                                    </div>
                                    <div className="d-flex align-items-center justify-content-between" style={{ width: '52px', height: '16px' }}>
                                        <span style={{ fontSize: '10px', color: voice2Enabled ? DARK_THEME.voice2Color : '#8892b0', fontWeight: 'bold' }}>V2</span>
                                        <Form.Check type="switch" id="voice-toggle-1" label=""
                                            className="m-0 d-flex align-items-center"
                                            style={{ transform: 'scale(0.7)', transformOrigin: 'right center' }}
                                            checked={voice2Enabled && availableVoices.includes(2)}
                                            disabled={isPlaying || !availableVoices.includes(2)}
                                            onChange={(e) => setVoice2Enabled(e.target.checked)} />
                                    </div>
                                    <div className="d-flex align-items-center justify-content-between" style={{ width: '52px', height: '16px' }}>
                                        <span style={{ fontSize: '10px', color: metronomeEnabled ? DARK_THEME.metronomeControlMedium : '#8892b0', fontWeight: 'bold' }}>Met</span>
                                        <Form.Check type="switch" id="metronome-toggle" label=""
                                            className="m-0 d-flex align-items-center"
                                            style={{ transform: 'scale(0.7)', transformOrigin: 'right center' }}
                                            checked={metronomeEnabled} disabled={isPlaying}
                                            onChange={(e) => setMetronomeEnabled(e.target.checked)} />
                                    </div>
                                </div>
                            </div>
                            <div className="bg-black px-2 py-1 rounded border border-secondary d-flex flex-column gap-1">
                                <div style={{ fontSize: '8px', textTransform: 'uppercase', letterSpacing: '1px', color: '#586278' }}>View Options</div>
                                <div className="d-flex align-items-center gap-1" style={{ height: '18px' }}>
                                    {['tab','both','sheet'].map(m => (
                                        <button key={m} onClick={() => !isPlaying && setViewMode(m)} disabled={isPlaying}
                                            style={{ fontSize: '9px', fontWeight: viewMode === m ? '700' : '400',
                                                color: viewMode === m ? '#fff' : '#8892b0',
                                                background: viewMode === m ? (m === 'tab' ? '#1a6b4a' : m === 'sheet' ? '#6b4a8a' : '#2a5a7a') : 'transparent',
                                                border: `1px solid ${viewMode === m ? 'transparent' : '#3a3a5a'}`,
                                                borderRadius: m === 'tab' ? '4px 0 0 4px' : m === 'sheet' ? '0 4px 4px 0' : '0',
                                                padding: '0 8px', lineHeight: '16px', cursor: isPlaying ? 'default' : 'pointer',
                                                opacity: isPlaying ? 0.5 : 1, transition: 'all 0.15s ease',
                                                textTransform: 'uppercase', letterSpacing: '0.3px' }}
                                        >
                                            {m === 'tab' ? 'Tab' : m === 'both' ? 'Both' : 'Staff'}
                                        </button>
                                    ))}
                                </div>
                            </div>
                        </div>
                        <div className="bg-black border border-secondary rounded p-2 font-monospace flex-grow-1 d-flex flex-column justify-content-center"
                            style={{ fontSize: '12px', lineHeight: '1.2', height: '76px', overflowY: 'auto' }}>
                            {activeDescription ? (
                                activeDescription
                            ) : trackStats ? (
                                <div className="d-flex flex-column" style={{ gap: '4px', color: '#8892b0' }}>
                                    <div style={{ fontSize: '9px', textTransform: 'uppercase', letterSpacing: '1px', color: '#586278', marginBottom: '2px' }}>Track Info</div>
                                    <div className="d-flex" style={{ gap: '12px', flexWrap: 'wrap' }}>
                                        <span><span style={{ color: '#64748b' }}>⌛</span> {trackStats.totalDuration}</span>
                                        <span><span style={{ color: '#64748b' }}>♩</span> {trackStats.timeSig}</span>
                                        <span><span style={{ color: '#64748b' }}>♯</span> {trackStats.keySig}</span>
                                        <span><span style={{ color: '#64748b' }}>𝄆</span> {trackStats.totalMeasures} bars</span>
                                        <span><span style={{ color: '#64748b' }}>♪</span> {trackStats.noteCount} notes</span>
                                        <span><span style={{ color: '#64748b' }}>BPM</span> {trackStats.bpm}</span>
                                    </div>
                                    <div style={{ fontSize: '10px', color: '#586278', marginTop: '2px' }}>
                                        Click any note on the score to inspect its properties.
                                    </div>
                                </div>
                            ) : (
                                <span className="text-muted fst-italic">Select a note to view properties.</span>
                            )}
                        </div>
                    </div>
                )}
            </div>

            {/* 3. THE TRUE SCROLL VIEWPORT: Only things inside this box will move or scroll */}
            <div
                ref={containerRef}
                className="flex-grow-1"
                style={{
                    overflowY: 'auto',
                    paddingTop: '12px',
                    paddingBottom: '150px',
                    position: 'relative'
                }}
            >
                <button
                    onClick={toggleFullscreen}
                    title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
                    style={{
                        position: 'sticky',
                        top: isStackedLayout ? '4px' : '8px',
                        float: 'right',
                        zIndex: 10,
                        fontSize: isStackedLayout ? '9px' : '11px',
                        padding: isStackedLayout ? '2px 4px' : '4px 6px',
                        lineHeight: isStackedLayout ? '10px' : '14px',
                        color: isFullscreen ? '#22d3ee' : '#8892b0',
                        background: 'rgba(0,0,0,0.7)',
                        border: '1px solid #3a3a5a',
                        borderRadius: '4px',
                        cursor: 'pointer',
                        marginRight: isStackedLayout ? '4px' : '8px',
                        transition: 'all 0.15s ease'
                    }}
                >
                    <svg width={isStackedLayout ? 10 : 14} height={isStackedLayout ? 10 : 14} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                        <path d={isFullscreen ? "M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3" : "M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"} />
                    </svg>
                </button>
                <Table responsive bordered={false} style={{ margin: 0, width: '100%' }}>
                    <tbody>
                        {computedRows.map((row, index) => {
                            const rowPaddingBottom = measuresPerRow === 1 ? '20px' : measuresPerRow === 2 ? '32px' : '48px';

                            return (
                                <tr key={index}>
                                    <td style={{ border: 0, padding: 0 }}>
                                        <div
                                            style={{
                                                background: 'rgba(255,255,255,0.015)',
                                                borderRadius: '6px',
                                                border: '1px solid rgba(255,255,255,0.04)',
                                                marginTop: index === 0 ? 0 : '24px',
                                                padding: `0 ${isStackedLayout ? '4px' : '12px'} ${rowPaddingBottom} ${isStackedLayout ? '4px' : '12px'}`,
                                            }}
                                        >
                                            {buildSvg(
                                                paddingX,
                                                trebleTopY,
                                                bassTopY,
                                                lineSpacing,
                                                timeSigTop,
                                                timeSigBottom,
                                                tabTopY,
                                                measureValidityMap,
                                                rhythmTopY,
                                                beatsPerMeasure,
                                                activeIndices,
                                                rhythm2TopY,
                                                rhythm1TopY,
                                                SLOT_WIDTH,
                                                isPlaying,
                                                isPaused,
                                                playbackIndex,
                                                () => {},
                                                handleNoteClick,
                                                measuresPerRow,
                                                voice1Enabled && availableVoices.includes(1),
                                                voice2Enabled && availableVoices.includes(2),
                                                metronomeEnabled,
                                                viewMode
                                            )(row, index)}
                                        </div>
                                    </td>
                                </tr>
                            );
                        })}
                    </tbody>
                </Table>
            </div>

                {countInBeat > 0 && (
                    <div
                        key={countInBeat}
                        style={{
                            position: 'fixed',
                            top: 0,
                            left: 0,
                            width: '100vw',
                            height: '100vh',
                            backgroundColor: 'rgba(0, 0, 0, 0.55)',
                            display: 'flex',
                            alignItems: 'center',
                            justifyContent: 'center',
                            zIndex: 9999,
                            pointerEvents: 'none',
                        }}
                    >
                        <div
                            style={{
                                fontSize: '140px',
                                color: 'rgba(167, 139, 250, 0.95)',
                                fontWeight: 'bold',
                                fontFamily: 'monospace',
                                textShadow: '0 0 40px rgba(167, 139, 250, 0.5)',
                                animation: 'countin-fade 0.8s ease-out forwards',
                            }}
                        >
                            {countInBeat}
                        </div>
                    </div>
                )}
            </div>
        </>
    );
}