mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
feat(studio): razor/blade tool UI for timeline clip splitting (#1331)
Wire the razor tool into Studio's timeline UI: - B enters razor mode (crosshair cursor + red vertical guide line) - Click any clip to split at the click position - Shift+click splits all clips across every track at that time - V or Escape exits razor mode - Toolbar shows selection arrow / scissors toggle Add useRazorSplit hook for split orchestration (HTML + GSAP mutation). Add activeTool state to playerStore. Add preview reload after timeline move/resize operations so the composition re-renders with updated timing.
This commit is contained in:
@@ -6,6 +6,8 @@ import type { LeftSidebarHandle } from "../components/sidebar/LeftSidebar";
|
||||
import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
|
||||
import { shouldHandleTimelineToggleHotkey, isEditableTarget } from "../utils/timelineDiscovery";
|
||||
import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
|
||||
import { canSplitElement } from "../utils/timelineElementSplit";
|
||||
import { STUDIO_RAZOR_TOOL_ENABLED } from "../components/editor/manualEditingAvailability";
|
||||
|
||||
/** Safely resolves contentWindow for a potentially cross-origin iframe. */
|
||||
function iframeContentWindow(iframe: HTMLIFrameElement | null): Window | null {
|
||||
@@ -327,7 +329,7 @@ export function useAppHotkeys({
|
||||
const element = elements.find((el) => (el.key ?? el.id) === selectedElementId);
|
||||
if (
|
||||
element &&
|
||||
["video", "audio", "img"].includes(element.tag) &&
|
||||
canSplitElement(element) &&
|
||||
currentTime > element.start &&
|
||||
currentTime < element.start + element.duration
|
||||
) {
|
||||
@@ -338,6 +340,51 @@ export function useAppHotkeys({
|
||||
}
|
||||
}
|
||||
|
||||
// B — toggle razor tool
|
||||
if (
|
||||
STUDIO_RAZOR_TOOL_ENABLED &&
|
||||
event.key.toLowerCase() === "b" &&
|
||||
!event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.altKey &&
|
||||
!event.shiftKey &&
|
||||
!isEditableTarget(event.target)
|
||||
) {
|
||||
event.preventDefault();
|
||||
const { activeTool, setActiveTool } = usePlayerStore.getState();
|
||||
setActiveTool(activeTool === "razor" ? "select" : "razor");
|
||||
return;
|
||||
}
|
||||
|
||||
// V — return to selection tool
|
||||
if (
|
||||
event.key.toLowerCase() === "v" &&
|
||||
!event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.altKey &&
|
||||
!event.shiftKey &&
|
||||
!isEditableTarget(event.target)
|
||||
) {
|
||||
event.preventDefault();
|
||||
usePlayerStore.getState().setActiveTool("select");
|
||||
return;
|
||||
}
|
||||
|
||||
// Escape — exit razor mode (only when no selection to deselect first)
|
||||
if (event.key === "Escape" && !isEditableTarget(event.target)) {
|
||||
const { activeTool, selectedElementId, setActiveTool, setSelectedElementId } =
|
||||
usePlayerStore.getState();
|
||||
if (activeTool === "razor") {
|
||||
if (selectedElementId) {
|
||||
setSelectedElementId(null);
|
||||
} else {
|
||||
setActiveTool("select");
|
||||
}
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete / Backspace — remove selected keyframes > reset keyframes > remove element
|
||||
if (
|
||||
(event.key === "Delete" || event.key === "Backspace") &&
|
||||
|
||||
Reference in New Issue
Block a user