Files
hyperframes/packages/studio/src/hooks/useKeyframeKeyboard.ts
T
Miguel Ángel a468550f82 feat(studio): keyframe system — parser, runtime, timeline UI, design panel, gesture recording (#1311)
* feat(studio): runtime hooks — global time compiler + keyframe runtime

Add the runtime bridge layer: global time compilation (tween % → clip %),
soft reload after mutations, runtime keyframe preview, and keyframe
commit helper.

* feat(studio): runtime hooks — global time compiler + keyframe runtime

Add the runtime bridge layer: global time compilation (tween % → clip %),
soft reload after mutations, runtime keyframe preview, and keyframe
commit helper.

* feat(studio): keyframe cache + commit hooks

Add hooks for keyframe cache population (tween → clip-relative %),
mutation dispatch, keyframe snapping, and audio beat detection.

* feat(studio): timeline UI — dopesheet diamonds + keyboard nav

Add dopesheet strip with diamond keyframe indicators, timeline property
rows, keyboard navigation (J/Shift+J/Delete/K), and feature gate
(STUDIO_KEYFRAMES_ENABLED defaults to false).

* feat(studio): design panel — arc controls + ease curve + stagger

Add arc path controls (curviness slider, auto-rotate), motion path SVG
overlay, ease curve visualization, stagger controls, and expanded
animation card. Includes border-radius editor dependency from #1217.

* feat(studio): gesture recording core

Add gesture recording engine with RAF sampling, modifier key property
mapping (Shift→rotationXY, Alt→rotation, Cmd→opacity),
Ramer-Douglas-Peucker simplification, and ghost trail SVG overlay.

* fix(studio): keyframe drag + recording bug bash

21 fixes: capture GSAP base at drag start, translate:none before
gsap.set, skip reapplyPathOffsets for GSAP elements, clamp recording
seek, _auto flag for 100% keyframes, overlay flash fix, block edits
during recording.

* feat(studio): keyframe integration wiring + docs

Wire App.tsx recording orchestration, TimelineToolbar K/R buttons,
PropertyPanel per-property diamonds, shortcuts panel, toast
notifications, and keyframes guide documentation. All gated on
STUDIO_KEYFRAMES_ENABLED (default false).
2026-06-09 18:30:23 -04:00

104 lines
2.8 KiB
TypeScript

import { useEffect, useCallback } from "react";
import { usePlayerStore } from "../player/store/playerStore";
interface KeyframeKeyboardOptions {
enabled: boolean;
onAddKeyframe?: () => void;
onDeleteKeyframe?: () => void;
onPrevKeyframe?: () => void;
onNextKeyframe?: () => void;
onToggleHold?: () => void;
onToggleExpand?: () => void;
onNudgeKeyframe?: (direction: -1 | 1, large: boolean) => void;
}
function isTextInput(el: Element | null): boolean {
if (!el) return false;
const tag = el.tagName;
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
return (el as HTMLElement).isContentEditable === true;
}
export function useKeyframeKeyboard({
enabled,
onAddKeyframe,
onDeleteKeyframe,
onPrevKeyframe,
onNextKeyframe,
onToggleHold,
onToggleExpand,
onNudgeKeyframe,
}: KeyframeKeyboardOptions): void {
const handler = useCallback(
(e: KeyboardEvent) => {
if (!enabled) return;
if (isTextInput(document.activeElement)) return;
const hasSelectedKeyframes = usePlayerStore.getState().selectedKeyframes.size > 0;
switch (e.key.toLowerCase()) {
case "k":
if (!e.metaKey && !e.ctrlKey) {
e.preventDefault();
onAddKeyframe?.();
}
break;
case "delete":
case "backspace":
if (hasSelectedKeyframes) {
e.preventDefault();
onDeleteKeyframe?.();
}
break;
case "j":
if (!e.metaKey && !e.ctrlKey) {
e.preventDefault();
if (e.shiftKey) onNextKeyframe?.();
else onPrevKeyframe?.();
}
break;
case "h":
if (!e.metaKey && !e.ctrlKey && hasSelectedKeyframes) {
e.preventDefault();
onToggleHold?.();
}
break;
case "u":
if (!e.metaKey && !e.ctrlKey) {
e.preventDefault();
onToggleExpand?.();
}
break;
case "arrowleft":
if (hasSelectedKeyframes && !e.metaKey && !e.ctrlKey && !e.altKey) {
e.preventDefault();
onNudgeKeyframe?.(-1, e.shiftKey);
}
break;
case "arrowright":
if (hasSelectedKeyframes && !e.metaKey && !e.ctrlKey && !e.altKey) {
e.preventDefault();
onNudgeKeyframe?.(1, e.shiftKey);
}
break;
}
},
[
enabled,
onAddKeyframe,
onDeleteKeyframe,
onPrevKeyframe,
onNextKeyframe,
onToggleHold,
onToggleExpand,
onNudgeKeyframe,
],
);
useEffect(() => {
if (!enabled) return;
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [enabled, handler]);
}