diff --git a/src/components/MainContent.tsx b/src/components/MainContent.tsx index 67dacd3..dece0b7 100644 --- a/src/components/MainContent.tsx +++ b/src/components/MainContent.tsx @@ -31,6 +31,8 @@ const MainContent: React.FC = ({ updateTrackProperties, timeSignature, setPlayheadPosition, + playheadPosition, + isPlaying, clearAllSelections, setSelectedTrack, showPianoRoll, @@ -80,6 +82,12 @@ const MainContent: React.FC = ({ // Refs to track pending updates for verification const pendingUpdates = useRef>(new Map()); + // Refs for auto-scroll during playback + const mainContentRef = useRef(null); + const userManuallyScrolledRef = useRef(false); + const expectedScrollLeftRef = useRef(-1); + const isPlayingRef = useRef(false); + // Refs for bar numbers and loop range drag functionality const barNumbersRef = useRef(null); const isLoopDraggingRef = useRef(false); @@ -87,6 +95,56 @@ const MainContent: React.FC = ({ const loopDragStartXRef = useRef(null); const loopDragOriginalSettingsRef = useRef<{ isLooping: boolean; loopingRange: [number, number] } | null>(null); + // Sync isPlayingRef and reset manual-scroll flag when playback starts + useEffect(() => { + if (isPlaying && !isPlayingRef.current) { + userManuallyScrolledRef.current = false; + } + isPlayingRef.current = isPlaying; + }, [isPlaying]); + + // Detect manual horizontal scroll during playback + useEffect(() => { + const container = mainContentRef.current; + if (!container) return; + + const handleScroll = () => { + if (!isPlayingRef.current) return; + // If scrollLeft matches what we programmatically set (within 1px), it's our scroll — ignore + if (Math.abs(container.scrollLeft - expectedScrollLeftRef.current) < 1) return; + userManuallyScrolledRef.current = true; + }; + + container.addEventListener('scroll', handleScroll); + return () => container.removeEventListener('scroll', handleScroll); + }, []); + + // Auto-scroll to keep playhead centered during playback + useEffect(() => { + if (!isPlaying || userManuallyScrolledRef.current) return; + + const container = mainContentRef.current; + if (!container) return; + + const beatsPerBar = timeSignature.numerator; + const barPosition = playheadPosition / beatsPerBar; + const barWidth = parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--track-grid-bar-width') + ) || 40; + const playheadPixel = barPosition * barWidth; + + // Center the playhead in the visible grid area (excluding the 200px sticky info panel) + const infoWidth = 200; + const targetScrollLeft = playheadPixel - (container.clientWidth - infoWidth) / 2; + const clampedScrollLeft = Math.max( + 0, + Math.min(targetScrollLeft, container.scrollWidth - container.clientWidth) + ); + + expectedScrollLeftRef.current = clampedScrollLeft; + container.scrollLeft = clampedScrollLeft; + }, [playheadPosition, isPlaying, timeSignature]); + // Effect to verify track updates useEffect(() => { // Check for pending updates @@ -722,7 +780,7 @@ const MainContent: React.FC = ({ }; return ( -
+
{/* Top-left spacer */}