import { useRef, useCallback, useEffect, memo } from "react";
import { formatFrameTime, formatTime, stepFrameTime } from "../lib/time";
import { shouldMutePreviewAudio } from "../lib/timelineIframeHelpers";
import { usePlayerStore } from "../store/playerStore";
import { trackStudioEvent } from "../../utils/studioTelemetry";
import { Tooltip } from "../../components/ui";
import { ShortcutsPanel } from "./ShortcutsPanel";
import { SpeedMenu } from "./SpeedMenu";
import { useSeekBarDrag, resolveSeekPercent } from "./useSeekBarDrag";
import { useState } from "react";
export { resolveSeekPercent };
type TimeDisplayMode = "time" | "frame";
/* ── Icon sub-components ─────────────────────────────────────────── */
function PlayIcon() {
return (
);
}
function PauseIcon() {
return (
);
}
/* ── Button sub-components ───────────────────────────────────────── */
const MuteButton = memo(function MuteButton({
audioMuted,
audioAutoMuted,
effectiveAudioMuted,
controlsDisabled,
setAudioMuted,
}: {
audioMuted: boolean;
audioAutoMuted: boolean;
effectiveAudioMuted: boolean;
controlsDisabled: boolean;
setAudioMuted: (v: boolean) => void;
}) {
const label = audioAutoMuted
? "Audio muted above 1x speed"
: audioMuted
? "Unmute audio"
: "Mute audio";
return (
);
});
const LoopButton = memo(function LoopButton({
loopEnabled,
disabled,
setLoopEnabled,
}: {
loopEnabled: boolean;
disabled: boolean;
setLoopEnabled: (v: boolean) => void;
}) {
return (
);
});
const FullscreenButton = memo(function FullscreenButton({
isFullscreen,
onToggleFullscreen,
}: {
isFullscreen: boolean;
onToggleFullscreen: () => void;
}) {
return (
);
});
/* ── Seek bar sub-component ──────────────────────────────────────── */
function SeekBarMarker({ position, duration }: { position: number; duration: number }) {
if (duration <= 0) return null;
return (
);
}
function WorkAreaOverlay({
inPoint,
outPoint,
duration,
}: {
inPoint: number | null;
outPoint: number | null;
duration: number;
}) {
if ((inPoint === null && outPoint === null) || duration <= 0) return null;
return (
<>
{inPoint !== null && }
{outPoint !== null && }
>
);
}
const SeekBar = memo(function SeekBar({
disabled,
duration,
inPoint,
outPoint,
progressFillRef,
progressThumbRef,
seekBarRef,
sliderRef,
onPointerDown,
onKeyDown,
}: {
disabled: boolean;
duration: number;
inPoint: number | null;
outPoint: number | null;
progressFillRef: React.RefObject;
progressThumbRef: React.RefObject;
seekBarRef: React.RefObject;
sliderRef: React.RefObject;
onPointerDown: (e: React.PointerEvent) => void;
onKeyDown: (e: React.KeyboardEvent) => void;
}) {
return (
{
(seekBarRef as React.MutableRefObject
).current = el;
(sliderRef as React.MutableRefObject).current = el;
}}
role="slider"
tabIndex={disabled ? -1 : 0}
aria-label="Seek"
aria-disabled={disabled || undefined}
aria-valuemin={0}
aria-valuemax={Math.round(duration)}
aria-valuenow={0}
className={`min-w-[96px] flex-1 h-6 flex items-center group outline-none focus-visible:ring-1 focus-visible:ring-white/30 focus-visible:rounded ${
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"
}`}
style={{ touchAction: "none" }}
onPointerDown={onPointerDown}
onKeyDown={onKeyDown}
>
);
});
/* ── Main component ──────────────────────────────────────────────── */
interface PlayerControlsProps {
onTogglePlay: () => void;
onSeek: (time: number) => void;
disabled?: boolean;
isFullscreen?: boolean;
onToggleFullscreen?: () => void;
}
export const PlayerControls = memo(function PlayerControls({
onTogglePlay,
onSeek,
disabled = false,
isFullscreen = false,
onToggleFullscreen,
}: PlayerControlsProps) {
const isPlaying = usePlayerStore((s) => s.isPlaying);
const duration = usePlayerStore((s) => s.duration);
const timelineReady = usePlayerStore((s) => s.timelineReady);
const playbackRate = usePlayerStore((s) => s.playbackRate);
const audioMuted = usePlayerStore((s) => s.audioMuted);
const loopEnabled = usePlayerStore((s) => s.loopEnabled);
const setPlaybackRate = usePlayerStore.getState().setPlaybackRate;
const setAudioMuted = usePlayerStore.getState().setAudioMuted;
const setLoopEnabled = usePlayerStore.getState().setLoopEnabled;
const inPoint = usePlayerStore((s) => s.inPoint);
const outPoint = usePlayerStore((s) => s.outPoint);
const setInPoint = usePlayerStore.getState().setInPoint;
const setOutPoint = usePlayerStore.getState().setOutPoint;
const [timeDisplayMode, setTimeDisplayMode] = useState("time");
const progressFillRef = useRef(null);
const progressThumbRef = useRef(null);
const timeDisplayRef = useRef(null);
const seekBarRef = useRef(null);
const sliderRef = useRef(null);
const isDraggingRef = useRef(false);
const currentTimeRef = useRef(0);
const timeDisplayModeRef = useRef(timeDisplayMode);
timeDisplayModeRef.current = timeDisplayMode;
const durationRef = useRef(duration);
durationRef.current = duration;
const controlsDisabled = disabled || !timelineReady;
const audioAutoMuted = playbackRate > 1;
const effectiveAudioMuted = shouldMutePreviewAudio(audioMuted, playbackRate);
useEffect(() => {
if (!timeDisplayRef.current) return;
const t = currentTimeRef.current;
timeDisplayRef.current.textContent =
timeDisplayMode === "frame" ? formatFrameTime(t, duration) : formatTime(t);
}, [duration, timeDisplayMode]);
const { handlePointerDown } = useSeekBarDrag(
{
seekBarRef,
progressFillRef,
progressThumbRef,
sliderRef,
timeDisplayRef,
isDraggingRef,
durationRef,
currentTimeRef,
timeDisplayModeRef,
},
onSeek,
disabled,
duration,
);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (disabled || !timelineReady || duration <= 0) return;
const step = e.shiftKey ? 10 : 1;
if (e.key === "ArrowLeft") {
e.preventDefault();
onSeek(stepFrameTime(currentTimeRef.current, -step));
} else if (e.key === "ArrowRight") {
e.preventDefault();
onSeek(Math.min(duration, stepFrameTime(currentTimeRef.current, step)));
}
},
[disabled, timelineReady, duration, onSeek],
);
return (
{onToggleFullscreen && (
)}
);
});