;
clearDomSelectionRef: React.MutableRefObject<() => void>;
@@ -87,6 +88,7 @@ interface UseAppHotkeysParams {
export function useAppHotkeys({
toggleTimelineVisibility,
handleTimelineElementDelete,
+ handleTimelineElementSplit,
handleDomEditElementDelete,
domEditSelectionRef,
editHistory,
@@ -195,6 +197,8 @@ export function useAppHotkeys({
handleToggleRef.current = handleTimelineToggleHotkey;
const handleDeleteRef = useRef(handleTimelineElementDelete);
handleDeleteRef.current = handleTimelineElementDelete;
+ const handleSplitRef = useRef(handleTimelineElementSplit);
+ handleSplitRef.current = handleTimelineElementSplit;
const handleDomEditDeleteRef = useRef(handleDomEditElementDelete);
handleDomEditDeleteRef.current = handleDomEditElementDelete;
const handleUndoRef = useRef(handleUndo);
@@ -306,6 +310,29 @@ export function useAppHotkeys({
return;
}
+ // S — split selected clip at playhead
+ if (
+ event.key === "s" &&
+ !event.metaKey &&
+ !event.ctrlKey &&
+ !event.altKey &&
+ !isEditableTarget(event.target)
+ ) {
+ const { selectedElementId, elements, currentTime } = usePlayerStore.getState();
+ if (selectedElementId) {
+ const element = elements.find((el) => (el.key ?? el.id) === selectedElementId);
+ if (
+ element &&
+ currentTime > element.start &&
+ currentTime < element.start + element.duration
+ ) {
+ event.preventDefault();
+ void handleSplitRef.current(element, currentTime);
+ return;
+ }
+ }
+ }
+
// Delete / Backspace — remove selected keyframes > reset keyframes > remove element
if (
(event.key === "Delete" || event.key === "Backspace") &&
diff --git a/packages/studio/src/hooks/useTimelineEditing.ts b/packages/studio/src/hooks/useTimelineEditing.ts
index e9045b960..024777dc2 100644
--- a/packages/studio/src/hooks/useTimelineEditing.ts
+++ b/packages/studio/src/hooks/useTimelineEditing.ts
@@ -474,10 +474,10 @@ export function useTimelineEditing({
if (
element.timelineLocked ||
element.timingSource === "implicit" ||
+ element.compositionSrc ||
!element.duration ||
!Number.isFinite(element.duration)
) {
- showToast("This clip cannot be split.", "error");
return;
}
diff --git a/packages/studio/src/player/components/ClipContextMenu.tsx b/packages/studio/src/player/components/ClipContextMenu.tsx
index 4caf1a065..4199d1a27 100644
--- a/packages/studio/src/player/components/ClipContextMenu.tsx
+++ b/packages/studio/src/player/components/ClipContextMenu.tsx
@@ -43,11 +43,15 @@ export const ClipContextMenu = memo(function ClipContextMenu({
const adjustedX = Math.min(x, window.innerWidth - 200);
const adjustedY = Math.min(y, window.innerHeight - 200);
- const canSplit = currentTime > element.start && currentTime < element.start + element.duration;
+ const isComposition = !!element.compositionSrc;
+ const canSplit =
+ !isComposition && currentTime > element.start && currentTime < element.start + element.duration;
- const splitLabel = canSplit
- ? `Split at ${currentTime.toFixed(2)}s`
- : "Split (move playhead inside clip)";
+ const splitLabel = isComposition
+ ? "Split (not available for compositions)"
+ : canSplit
+ ? `Split at ${currentTime.toFixed(2)}s`
+ : "Split (move playhead inside clip)";
return (
,
) => Promise | void;
onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
+ onSplitElement?: (element: TimelineElement, splitTime: number) => Promise | void;
onSelectElement?: (element: TimelineElement | null) => void;
onDeleteKeyframe?: (elementId: string, percentage: number) => void;
onDeleteAllKeyframes?: (elementId: string) => void;
@@ -91,6 +93,7 @@ export const Timeline = memo(function Timeline({
onMoveElement,
onResizeElement,
onBlockedEditAttempt,
+ onSplitElement,
onSelectElement,
onDeleteKeyframe,
onDeleteAllKeyframes,
@@ -135,6 +138,11 @@ export const Timeline = memo(function Timeline({
const [showPopover, setShowPopover] = useState(false);
const [showShortcutHint, setShowShortcutHint] = useState(true);
const [kfContextMenu, setKfContextMenu] = useState(null);
+ const [clipContextMenu, setClipContextMenu] = useState<{
+ x: number;
+ y: number;
+ element: TimelineElement;
+ } | null>(null);
const [viewportWidth, setViewportWidth] = useState(0);
const roRef = useRef(null);
const shortcutHintRafRef = useRef(0);
@@ -532,6 +540,12 @@ export const Timeline = memo(function Timeline({
currentEase: kf?.ease ?? kfData?.ease,
});
}}
+ onContextMenuClip={(e, el) => {
+ e.preventDefault();
+ setSelectedElementId(el.key ?? el.id);
+ onSelectElement?.(el);
+ setClipContextMenu({ x: e.clientX, y: e.clientY, element: el });
+ }}
/>
@@ -583,6 +597,18 @@ export const Timeline = memo(function Timeline({
}}
/>
)}
+
+ {clipContextMenu && (
+ setClipContextMenu(null)}
+ onSplit={(el, time) => onSplitElement?.(el, time)}
+ onDelete={(el) => _onDeleteElement?.(el)}
+ />
+ )}