import { useState, useCallback, useRef, useEffect, memo } from "react"; import { formatTime, frameToSeconds } from "../lib/time"; import { Tooltip } from "../../components/ui"; const SHORTCUT_SECTIONS = [ { title: "Playback", hints: [ { key: "Space", label: "Play / Pause" }, { key: "J", label: "Play backward" }, { key: "K", label: "Stop" }, { key: "L", label: "Play forward" }, { key: "M", label: "Toggle mute" }, { key: "⇧L", label: "Toggle loop" }, { key: "←/→", label: "Step 1 frame" }, { key: "⇧←/⇧→", label: "Step 10 frames" }, { key: "F", label: "Toggle fullscreen" }, ], }, { title: "Keyframes", hints: [ { key: "K", label: "Add keyframe at playhead" }, { key: "Del", label: "Delete selected keyframe" }, { key: "H", label: "Toggle hold / bezier" }, { key: "U", label: "Expand / collapse properties" }, { key: "R", label: "Record gesture" }, ], }, { title: "Editing", hints: [ { key: "⌘Z", label: "Undo" }, { key: "⌘⇧Z", label: "Redo" }, { key: "⌘C", label: "Copy element" }, { key: "⌘V", label: "Paste element" }, { key: "⌘X", label: "Cut element" }, { key: "S", label: "Split clip at playhead" }, { key: "Del", label: "Delete selected element" }, ], }, { title: "Gesture recording modifiers", hints: [ { key: "Drag", label: "Record x / y position" }, { key: "Scroll", label: "Record z depth" }, { key: "⇧ Drag", label: "Record rotationX / rotationY" }, { key: "⌥ Drag", label: "Record rotation" }, { key: "⌘ Drag↕", label: "Record opacity" }, { key: "⌘ Scroll", label: "Record scale" }, ], }, { title: "Canvas", hints: [ { key: "Drag", label: "Move element / add keyframe" }, { key: "⌥ Drag", label: "Move entire animation path" }, { key: "⇧ Drag", label: "Uniform resize" }, ], }, { title: "Panels", hints: [ { key: "⌘1", label: "Compositions tab" }, { key: "⌘2", label: "Assets tab" }, ], }, { title: "Work area", hints: [ { key: "I", label: "Set in-point" }, { key: "⇧I", label: "Clear in-point" }, { key: "O", label: "Set out-point" }, { key: "⇧O", label: "Clear out-point" }, { key: "A", label: "Jump to in-point" }, { key: "E", label: "Jump to out-point" }, ], }, ] as const; interface ShortcutsPanelProps { disabled: boolean; duration: number; inPoint: number | null; outPoint: number | null; setInPoint: (v: number | null) => void; setOutPoint: (v: number | null) => void; onSeek: (time: number) => void; } export const ShortcutsPanel = memo(function ShortcutsPanel({ disabled, duration, inPoint, outPoint, setInPoint, setOutPoint, onSeek, }: ShortcutsPanelProps) { const [showShortcuts, setShowShortcuts] = useState(false); const [jumpFrame, setJumpFrame] = useState(""); const shortcutsPanelRef = useRef(null); useEffect(() => { if (!showShortcuts) return; const handleMouseDown = (e: MouseEvent) => { if (shortcutsPanelRef.current && !shortcutsPanelRef.current.contains(e.target as Node)) { setShowShortcuts(false); } }; document.addEventListener("mousedown", handleMouseDown); return () => { document.removeEventListener("mousedown", handleMouseDown); }; }, [showShortcuts]); const commitJumpFrame = useCallback(() => { if (disabled) return; const frame = Number.parseInt(jumpFrame, 10); if (!Number.isFinite(frame) || duration <= 0) return; onSeek(Math.min(duration, frameToSeconds(Math.max(0, frame)))); }, [disabled, duration, jumpFrame, onSeek]); const handleJumpSubmit = useCallback( (e: React.FormEvent) => { e.preventDefault(); commitJumpFrame(); }, [commitJumpFrame], ); const handleJumpKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key !== "Enter") return; e.preventDefault(); commitJumpFrame(); }, [commitJumpFrame], ); return (
{showShortcuts && (

Jump to frame

setJumpFrame(e.target.value)} disabled={disabled} inputMode="numeric" pattern="[0-9]*" aria-label="Jump to frame" placeholder="frame number" className="h-6 flex-1 rounded border border-neutral-700 bg-neutral-900 px-2 text-[10px] font-mono tabular-nums text-neutral-200 outline-none transition-colors placeholder:text-neutral-600 focus:border-studio-accent/60" onKeyDown={handleJumpKeyDown} onBlur={commitJumpFrame} />

Work area

I In-point
{inPoint !== null ? ( <> {formatTime(inPoint)} ) : ( )}
O Out-point
{outPoint !== null ? ( <> {formatTime(outPoint)} ) : ( )}
{SHORTCUT_SECTIONS.map((section) => (

{section.title}

{section.hints.map((hint) => (
{hint.key} {hint.label}
))}
))}
)}
); });