From 16d23c6a7725b7aece7a928549d69520f20da04e Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Tue, 18 Aug 2026 22:38:32 -0400 Subject: [PATCH] feat(studio): add ripple, roll, slip and slide trim tools The timeline could only trim one clip's own in/out, so every re-edit of a multi-clip track left a gap or an overlap to close by hand. Four tools now own the standard NLE edit operations, each committing as one atomic, single-undo batch through the existing group-resize persist. - Ripple (T): trim an edge, every later clip on the track follows. - Roll (Shift+T): move the cut between two clips; nothing downstream moves. - Slip (Y): change which part of the source plays; nothing moves. - Slide (Shift+Y): move a clip; its neighbours absorb the move. All four are lane-scoped, clamped by available source media and the minimum clip duration, snap the edge that actually moves, and refuse up front when the tool cannot act on the clip (a roll with no clip across the cut, a slip on generated pixels) with a message that says which. Two adjacent cleanups came with it: the blocked-edit toast now reads the intent it was already being handed instead of ignoring it, and the duplicated remove-element POST behind the two delete paths moved into one helper. --- .../studio/src/components/TimelineToolbar.tsx | 21 ++ .../src/hooks/timelineEditingHelpers.ts | 34 ++ packages/studio/src/hooks/useAppHotkeys.ts | 12 + .../src/hooks/useElementLifecycleOps.ts | 11 +- .../studio/src/hooks/useTimelineEditing.ts | 19 +- packages/studio/src/icons/TrimToolIcons.tsx | 71 ++++ .../src/player/components/ShortcutsPanel.tsx | 9 + .../studio/src/player/components/Timeline.tsx | 11 +- .../player/components/timelineBlockedEdits.ts | 28 ++ .../components/timelineClipDragPreview.ts | 24 +- .../components/timelineClipDragTypes.ts | 7 + .../components/timelineClipGestureHandlers.ts | 126 +++++-- .../src/player/components/timelineEditing.ts | 3 +- .../src/player/components/timelineTheme.ts | 13 + .../player/components/timelineTrimOps.test.ts | 221 +++++++++++ .../src/player/components/timelineTrimOps.ts | 351 ++++++++++++++++++ .../components/timelineTrimSession.test.ts | 111 ++++++ .../player/components/timelineTrimSession.ts | 295 +++++++++++++++ .../player/components/timelineTrimTools.ts | 78 ++++ .../useTimelineClipDrag.resize.test.tsx | 109 +++++- .../player/components/useTimelineClipDrag.ts | 68 +++- .../studio/src/player/store/playerStore.ts | 2 +- 22 files changed, 1553 insertions(+), 71 deletions(-) create mode 100644 packages/studio/src/icons/TrimToolIcons.tsx create mode 100644 packages/studio/src/player/components/timelineBlockedEdits.ts create mode 100644 packages/studio/src/player/components/timelineTrimOps.test.ts create mode 100644 packages/studio/src/player/components/timelineTrimOps.ts create mode 100644 packages/studio/src/player/components/timelineTrimSession.test.ts create mode 100644 packages/studio/src/player/components/timelineTrimSession.ts create mode 100644 packages/studio/src/player/components/timelineTrimTools.ts diff --git a/packages/studio/src/components/TimelineToolbar.tsx b/packages/studio/src/components/TimelineToolbar.tsx index 12cde02a6c..4793cea64c 100644 --- a/packages/studio/src/components/TimelineToolbar.tsx +++ b/packages/studio/src/components/TimelineToolbar.tsx @@ -17,6 +17,8 @@ import { useTimelineZoom } from "../player/components/useTimelineZoom"; import { usePlayerStore, type TimelineElement } from "../player"; import { Tooltip } from "./ui"; import { Scissors } from "../icons/SystemIcons"; +import { TRIM_TOOL_ICONS } from "../icons/TrimToolIcons"; +import { TIMELINE_TRIM_TOOLS } from "../player/components/timelineTrimTools"; import type { GsapAnimation } from "@hyperframes/core/gsap-parser"; import type { DomEditSelection } from "./editor/domEditingTypes"; import { canSplitElement } from "../utils/timelineElementSplit"; @@ -215,6 +217,25 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool + {/* Trim tools: one per NLE edit operation, so grabbing the same pixel + can mean ripple or roll without a hidden modifier. */} + {TIMELINE_TRIM_TOOLS.map((tool) => { + const Icon = TRIM_TOOL_ICONS[tool.mode]; + const active = activeTool === tool.mode; + return ( + + + + ); + })} {/* Divider: tool-mode | editing-actions */}