Files
cabrits-math/components/explorers/step-controls.tsx
2026-03-01 18:50:29 -04:00

135 lines
5.2 KiB
TypeScript

"use client";
import { useEffect, useCallback, useRef } from "react";
interface StepControlsProps {
currentStep: number;
totalSteps: number;
isPlaying: boolean;
onStepForward: () => void;
onStepBack: () => void;
onTogglePlay: () => void;
onReset: () => void;
canStepForward: boolean;
canStepBack: boolean;
playIntervalMs?: number;
}
export function StepControls({
currentStep,
totalSteps,
isPlaying,
onStepForward,
onStepBack,
onTogglePlay,
onReset,
canStepForward,
canStepBack,
playIntervalMs = 950,
}: StepControlsProps) {
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
// Auto-play timer
useEffect(() => {
if (isPlaying && canStepForward) {
timerRef.current = setInterval(() => {
onStepForward();
}, playIntervalMs);
}
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, [isPlaying, canStepForward, onStepForward, playIntervalMs]);
// Keyboard shortcuts
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
const tag = (e.target as HTMLElement).tagName;
if (["INPUT", "TEXTAREA", "SELECT"].includes(tag)) return;
if (e.key === "ArrowRight" || e.key === " ") {
e.preventDefault();
if (canStepForward) onStepForward();
}
if (e.key === "ArrowLeft") {
e.preventDefault();
if (canStepBack) onStepBack();
}
if (e.key.toLowerCase() === "p") onTogglePlay();
if (e.key.toLowerCase() === "r") onReset();
},
[canStepForward, canStepBack, onStepForward, onStepBack, onTogglePlay, onReset],
);
useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [handleKeyDown]);
const progress = totalSteps > 0 ? (currentStep / totalSteps) * 100 : 0;
return (
<div className="space-y-3">
{/* Progress bar */}
{totalSteps > 0 && (
<div className="mx-auto max-w-md">
<div className="mb-1.5 flex items-center justify-between text-xs text-muted">
<span>Step {currentStep} of {totalSteps}</span>
<span>{Math.round(progress)}%</span>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-border/60">
<div
className="h-full rounded-full bg-gradient-to-r from-unit-1 to-unit-4 transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
</div>
)}
{/* Controls */}
<div className="flex flex-wrap items-center justify-center gap-2">
<button
onClick={onStepBack}
disabled={!canStepBack || isPlaying}
className="rounded-xl border border-border/60 bg-surface px-4 py-2.5 text-sm font-semibold text-foreground shadow-[var(--shadow-sm)] transition-all duration-200 hover:shadow-[var(--shadow-md)] hover:-translate-y-px disabled:cursor-not-allowed disabled:opacity-35 disabled:hover:translate-y-0 disabled:hover:shadow-[var(--shadow-sm)]"
>
&#9664; Back
</button>
<button
onClick={onTogglePlay}
disabled={!canStepForward && !isPlaying}
className="min-w-[110px] rounded-xl bg-correct px-5 py-2.5 text-sm font-semibold text-white shadow-[var(--shadow-sm)] transition-all duration-200 hover:shadow-[var(--shadow-md)] hover:-translate-y-px disabled:cursor-not-allowed disabled:opacity-35 disabled:hover:translate-y-0 disabled:hover:shadow-[var(--shadow-sm)]"
>
{isPlaying ? "⏸ Pause" : "▶ Play"}
</button>
<button
onClick={onStepForward}
disabled={!canStepForward || isPlaying}
className="rounded-xl bg-foreground px-5 py-2.5 text-sm font-semibold text-background shadow-[var(--shadow-sm)] transition-all duration-200 hover:shadow-[var(--shadow-md)] hover:-translate-y-px disabled:cursor-not-allowed disabled:opacity-35 disabled:hover:translate-y-0 disabled:hover:shadow-[var(--shadow-sm)]"
>
Next Step &#9654;
</button>
<button
onClick={onReset}
className="rounded-xl border border-incorrect/30 bg-surface px-4 py-2.5 text-sm font-semibold text-incorrect shadow-[var(--shadow-sm)] transition-all duration-200 hover:bg-incorrect hover:text-white hover:shadow-[var(--shadow-md)] hover:-translate-y-px"
>
&#8634; Reset
</button>
</div>
{/* Keyboard hint */}
<p className="text-center text-[0.7rem] text-muted/50">
<kbd className="rounded border border-border/60 bg-background px-1 py-px font-mono text-[0.6rem]">Space</kbd>{" / "}
<kbd className="rounded border border-border/60 bg-background px-1 py-px font-mono text-[0.6rem]">&#8594;</kbd>{" "}
next{" · "}
<kbd className="rounded border border-border/60 bg-background px-1 py-px font-mono text-[0.6rem]">&#8592;</kbd>{" "}
back{" · "}
<kbd className="rounded border border-border/60 bg-background px-1 py-px font-mono text-[0.6rem]">P</kbd>{" "}
play{" · "}
<kbd className="rounded border border-border/60 bg-background px-1 py-px font-mono text-[0.6rem]">R</kbd>{" "}
reset
</p>
</div>
);
}