From 783e9ac8462751adc14d78b070095fd63e45bda0 Mon Sep 17 00:00:00 2001 From: cashmere Date: Mon, 27 Jul 2026 01:21:20 +0200 Subject: feat: voice-over recording into the timeline MediaRecorder mic capture (defensive mimeType picking, permission/no-mic toasts) with elapsed display in the timeline toolbar. Stop produces an audio asset and inserts it at the playhead on an audio track as ONE BatchCommand undo step, freeze-frame precedent. voice-over action registered, no default keybinding. 20 tests; smoke-tested in Chromium with a fake mic end to end. --- apps/web/src/voiceover/use-recording-elapsed.ts | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 apps/web/src/voiceover/use-recording-elapsed.ts (limited to 'apps/web/src/voiceover/use-recording-elapsed.ts') diff --git a/apps/web/src/voiceover/use-recording-elapsed.ts b/apps/web/src/voiceover/use-recording-elapsed.ts new file mode 100644 index 00000000..1eeb7f90 --- /dev/null +++ b/apps/web/src/voiceover/use-recording-elapsed.ts @@ -0,0 +1,26 @@ +import { useEffect, useState } from "react"; +import { useVoiceOverStore } from "@/voiceover/voiceover-store"; + +const ELAPSED_TICK_MS = 250; + +/** + * Elapsed seconds of the active voice-over recording, ticking while recording. + * Returns 0 when idle. + */ +export function useRecordingElapsedSeconds(): number { + const status = useVoiceOverStore((state) => state.status); + const startedAtMs = useVoiceOverStore((state) => state.startedAtMs); + const [elapsedSeconds, setElapsedSeconds] = useState(0); + + useEffect(() => { + if (status !== "recording" || startedAtMs === null) { + return; + } + const interval = setInterval(() => { + setElapsedSeconds((Date.now() - startedAtMs) / 1000); + }, ELAPSED_TICK_MS); + return () => clearInterval(interval); + }, [status, startedAtMs]); + + return status === "recording" ? elapsedSeconds : 0; +} -- cgit v1.3.1