"use client"; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import type { SpeedPreset, VisualizationData, VisualizationStep, } from "@/types/visualization"; const speedValues: Record = { slow: 2000, normal: 1000, fast: 500, }; interface VisualizationContextValue { // Data data: VisualizationData | null; currentStep: VisualizationStep | null; stepIndex: number; totalSteps: number; // Playback state isPlaying: boolean; speed: SpeedPreset; // Navigation actions goToStep: (index: number) => void; nextStep: () => void; prevStep: () => void; play: () => void; pause: () => void; togglePlayback: () => void; setSpeed: (speed: SpeedPreset) => void; // State queries canGoNext: boolean; canGoPrev: boolean; } const VisualizationContext = createContext( null ); interface VisualizationProviderProps { data: VisualizationData; initialStep?: number; autoPlay?: boolean; children: ReactNode; } export function VisualizationProvider({ data, initialStep = 0, autoPlay = false, children, }: VisualizationProviderProps) { const [stepIndex, setStepIndex] = useState(initialStep); const [isPlaying, setIsPlaying] = useState(autoPlay); const [speed, setSpeed] = useState("normal"); const intervalRef = useRef | null>(null); const totalSteps = data.steps.length; const currentStep = data.steps[stepIndex] ?? null; const canGoNext = stepIndex < totalSteps - 1; const canGoPrev = stepIndex > 0; const goToStep = useCallback( (index: number) => { const clampedIndex = Math.max(0, Math.min(index, totalSteps - 1)); setStepIndex(clampedIndex); }, [totalSteps] ); const nextStep = useCallback(() => { if (canGoNext) { setStepIndex((prev) => prev + 1); } else { setIsPlaying(false); } }, [canGoNext]); const prevStep = useCallback(() => { if (canGoPrev) { setStepIndex((prev) => prev - 1); } }, [canGoPrev]); const play = useCallback(() => { if (stepIndex >= totalSteps - 1) { setStepIndex(0); } setIsPlaying(true); }, [stepIndex, totalSteps]); const pause = useCallback(() => { setIsPlaying(false); }, []); const togglePlayback = useCallback(() => { if (isPlaying) { pause(); } else { play(); } }, [isPlaying, play, pause]); // Auto-advance when playing useEffect(() => { if (isPlaying) { intervalRef.current = setInterval(() => { setStepIndex((prev) => { if (prev >= totalSteps - 1) { setIsPlaying(false); return prev; } return prev + 1; }); }, speedValues[speed]); } return () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; }, [isPlaying, speed, totalSteps]); // Keyboard navigation useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { // Don't capture if user is typing in an input if ( event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement ) { return; } switch (event.key) { case "ArrowLeft": event.preventDefault(); prevStep(); break; case "ArrowRight": event.preventDefault(); nextStep(); break; case " ": event.preventDefault(); togglePlayback(); break; } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [prevStep, nextStep, togglePlayback]); const value = useMemo( () => ({ data, currentStep, stepIndex, totalSteps, isPlaying, speed, goToStep, nextStep, prevStep, play, pause, togglePlayback, setSpeed, canGoNext, canGoPrev, }), [ data, currentStep, stepIndex, totalSteps, isPlaying, speed, goToStep, nextStep, prevStep, play, pause, togglePlayback, canGoNext, canGoPrev, ] ); return ( {children} ); } export function useVisualization(): VisualizationContextValue { const context = useContext(VisualizationContext); if (!context) { throw new Error( "useVisualization must be used within a VisualizationProvider" ); } return context; }