summaryrefslogtreecommitdiff
path: root/apps/web/src/hooks/timeline/element
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-25 16:47:22 +0100
committerMaze Winther <mazewinther@gmail.com>2026-03-29 15:48:22 +0200
commit8db3bead13441a5882d8689dfc6a9f09c82375d9 (patch)
treee9d0cafa93904ffcbec3188c37961bcc16c6422d /apps/web/src/hooks/timeline/element
parent39ea298a9ccba3104c549c0ca079c231981811f5 (diff)
downloadopencut-8db3bead13441a5882d8689dfc6a9f09c82375d9.tar.gz
feat: masks, properties refactor, shaders, storage migrations, and more
Diffstat (limited to 'apps/web/src/hooks/timeline/element')
-rw-r--r--apps/web/src/hooks/timeline/element/use-element-interaction.ts83
-rw-r--r--apps/web/src/hooks/timeline/element/use-element-resize.ts231
-rw-r--r--apps/web/src/hooks/timeline/element/use-element-selection.ts35
-rw-r--r--apps/web/src/hooks/timeline/element/use-keyframe-drag.ts18
-rw-r--r--apps/web/src/hooks/timeline/element/use-keyframe-selection.ts2
5 files changed, 281 insertions, 88 deletions
diff --git a/apps/web/src/hooks/timeline/element/use-element-interaction.ts b/apps/web/src/hooks/timeline/element/use-element-interaction.ts
index ea01c718..fae8c527 100644
--- a/apps/web/src/hooks/timeline/element/use-element-interaction.ts
+++ b/apps/web/src/hooks/timeline/element/use-element-interaction.ts
@@ -18,16 +18,14 @@ import { snapTimeToFrame } from "@/lib/time";
import { computeDropTarget } from "@/lib/timeline/drop-utils";
import { getMouseTimeFromClientX } from "@/lib/timeline/drag-utils";
import { generateUUID } from "@/utils/id";
-import {
- snapElementEdge,
- type SnapPoint,
-} from "@/lib/timeline/snap-utils";
+import { snapElementEdge, type SnapPoint } from "@/lib/timeline/snap-utils";
+import { registerCanceller } from "@/lib/cancel-interaction";
import type {
DropTarget,
ElementDragState,
TimelineElement,
TimelineTrack,
-} from "@/types/timeline";
+} from "@/lib/timeline";
interface UseElementInteractionProps {
zoomLevel: number;
@@ -213,6 +211,20 @@ export function useElementInteraction({
setDragDropTarget(null);
}, []);
+ const cancelCurrentDrag = useCallback(() => {
+ pendingDragRef.current = null;
+ mouseDownLocationRef.current = null;
+ setIsPendingDrag(false);
+ endDrag();
+ onSnapPointChange?.(null);
+ }, [endDrag, onSnapPointChange]);
+
+ useEffect(() => {
+ if (!dragState.isDragging && !isPendingDrag) return;
+
+ return registerCanceller({ fn: cancelCurrentDrag });
+ }, [dragState.isDragging, isPendingDrag, cancelCurrentDrag]);
+
const getDragSnapResult = useCallback(
({
frameSnappedTime,
@@ -445,33 +457,36 @@ export function useElementInteraction({
return;
}
- if (dropTarget.isNewTrack) {
- const newTrackId = generateUUID();
+ if (dropTarget.isNewTrack) {
+ const newTrackId = generateUUID();
- editor.timeline.moveElement({
- sourceTrackId: dragState.trackId,
- targetTrackId: newTrackId,
- elementId: dragState.elementId,
- newStartTime: snappedTime,
- createTrack: { type: sourceTrack.type, index: dropTarget.trackIndex },
- rippleEnabled: rippleEditingEnabled,
- });
- selectElement({ trackId: newTrackId, elementId: dragState.elementId });
- } else {
- const targetTrack = tracks[dropTarget.trackIndex];
- if (targetTrack) {
editor.timeline.moveElement({
sourceTrackId: dragState.trackId,
- targetTrackId: targetTrack.id,
+ targetTrackId: newTrackId,
elementId: dragState.elementId,
newStartTime: snappedTime,
+ createTrack: { type: sourceTrack.type, index: dropTarget.trackIndex },
rippleEnabled: rippleEditingEnabled,
});
- if (targetTrack.id !== dragState.trackId) {
- selectElement({ trackId: targetTrack.id, elementId: dragState.elementId });
+ selectElement({ trackId: newTrackId, elementId: dragState.elementId });
+ } else {
+ const targetTrack = tracks[dropTarget.trackIndex];
+ if (targetTrack) {
+ editor.timeline.moveElement({
+ sourceTrackId: dragState.trackId,
+ targetTrackId: targetTrack.id,
+ elementId: dragState.elementId,
+ newStartTime: snappedTime,
+ rippleEnabled: rippleEditingEnabled,
+ });
+ if (targetTrack.id !== dragState.trackId) {
+ selectElement({
+ trackId: targetTrack.id,
+ elementId: dragState.elementId,
+ });
+ }
}
}
- }
endDrag();
onSnapPointChange?.(null);
@@ -520,10 +535,10 @@ export function useElementInteraction({
element: TimelineElement;
track: TimelineTrack;
}) => {
- const isRightClick = event.button === MOUSE_BUTTON_RIGHT;
+ const isRightClick = event.button === MOUSE_BUTTON_RIGHT;
- // right-click: don't stop propagation so ContextMenu can open
- if (isRightClick) {
+ // right-click: don't stop propagation so ContextMenu can open
+ if (isRightClick) {
const alreadySelected = isElementSelected({
trackId: track.id,
elementId: element.id,
@@ -538,12 +553,12 @@ export function useElementInteraction({
return;
}
- event.stopPropagation();
- mouseDownLocationRef.current = { x: event.clientX, y: event.clientY };
+ event.stopPropagation();
+ mouseDownLocationRef.current = { x: event.clientX, y: event.clientY };
- const isMultiSelect = event.metaKey || event.ctrlKey || event.shiftKey;
+ const isMultiSelect = event.metaKey || event.ctrlKey || event.shiftKey;
- if (isMultiSelect) {
+ if (isMultiSelect) {
handleSelectionClick({
trackId: track.id,
elementId: element.id,
@@ -551,7 +566,7 @@ export function useElementInteraction({
});
}
- const clickOffsetTime = getClickOffsetTime({
+ const clickOffsetTime = getClickOffsetTime({
clientX: event.clientX,
elementRect: event.currentTarget.getBoundingClientRect(),
zoomLevel,
@@ -579,9 +594,9 @@ export function useElementInteraction({
element: TimelineElement;
track: TimelineTrack;
}) => {
- event.stopPropagation();
+ event.stopPropagation();
- if (mouseDownLocationRef.current) {
+ if (mouseDownLocationRef.current) {
const deltaX = Math.abs(event.clientX - mouseDownLocationRef.current.x);
const deltaY = Math.abs(event.clientY - mouseDownLocationRef.current.y);
if (deltaX > DRAG_THRESHOLD_PX || deltaY > DRAG_THRESHOLD_PX) {
@@ -593,7 +608,7 @@ export function useElementInteraction({
// modifier keys already handled in mousedown
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
- const alreadySelected = isElementSelected({
+ const alreadySelected = isElementSelected({
trackId: track.id,
elementId: element.id,
});
diff --git a/apps/web/src/hooks/timeline/element/use-element-resize.ts b/apps/web/src/hooks/timeline/element/use-element-resize.ts
index 3f49e798..b41f82f9 100644
--- a/apps/web/src/hooks/timeline/element/use-element-resize.ts
+++ b/apps/web/src/hooks/timeline/element/use-element-resize.ts
@@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import { snapTimeToFrame } from "@/lib/time";
-import type { TimelineElement, TimelineTrack } from "@/types/timeline";
+import type { TimelineElement, TimelineTrack } from "@/lib/timeline";
import { useEditor } from "@/hooks/use-editor";
import { useShiftKey } from "@/hooks/use-shift-key";
import {
@@ -9,7 +9,13 @@ import {
snapToNearestPoint,
type SnapPoint,
} from "@/lib/timeline/snap-utils";
+import { isRetimableElement } from "@/lib/timeline";
+import {
+ getSourceSpanAtClipTime,
+ getTimelineDurationForSourceSpan,
+} from "@/lib/retime";
import { useTimelineStore } from "@/stores/timeline-store";
+import { registerCanceller } from "@/lib/cancel-interaction";
export interface ResizeState {
elementId: string;
@@ -37,7 +43,6 @@ export function useTimelineElementResize({
onResizeStateChange,
}: UseTimelineElementResizeProps) {
const editor = useEditor();
- const activeProject = editor.project.getActive();
const isShiftHeldRef = useShiftKey();
const snappingEnabled = useTimelineStore((state) => state.snappingEnabled);
const rippleEditingEnabled = useTimelineStore(
@@ -91,6 +96,94 @@ export function useTimelineElementResize({
return element.sourceDuration == null;
}, [element.sourceDuration]);
+ const getSourceDeltaForClipDelta = useCallback(
+ (clipDelta: number) => {
+ if (!isRetimableElement(element)) {
+ return clipDelta;
+ }
+
+ return clipDelta >= 0
+ ? getSourceSpanAtClipTime({
+ clipTime: clipDelta,
+ retime: element.retime,
+ })
+ : -getSourceSpanAtClipTime({
+ clipTime: Math.abs(clipDelta),
+ retime: element.retime,
+ });
+ },
+ [element],
+ );
+
+ const getVisibleSourceSpanForDuration = useCallback(
+ (duration: number) => {
+ if (!isRetimableElement(element)) {
+ return duration;
+ }
+
+ return getSourceSpanAtClipTime({
+ clipTime: duration,
+ retime: element.retime,
+ });
+ },
+ [element],
+ );
+
+ const getDurationForVisibleSourceSpan = useCallback(
+ (sourceSpan: number) => {
+ if (!isRetimableElement(element)) {
+ return sourceSpan;
+ }
+
+ return getTimelineDurationForSourceSpan({
+ sourceSpan,
+ retime: element.retime,
+ });
+ },
+ [element],
+ );
+
+ const getSourceDuration = useCallback(
+ ({
+ trimStart,
+ duration,
+ trimEnd,
+ }: {
+ trimStart: number;
+ duration: number;
+ trimEnd: number;
+ }) => {
+ if (typeof element.sourceDuration === "number") {
+ return element.sourceDuration;
+ }
+
+ return trimStart + getVisibleSourceSpanForDuration(duration) + trimEnd;
+ },
+ [element.sourceDuration, getVisibleSourceSpanForDuration],
+ );
+
+ const cancelResize = useCallback(() => {
+ if (!resizing) return;
+
+ setCurrentTrimStart(resizing.initialTrimStart);
+ setCurrentTrimEnd(resizing.initialTrimEnd);
+ setCurrentStartTime(resizing.initialStartTime);
+ setCurrentDuration(resizing.initialDuration);
+ currentTrimStartRef.current = resizing.initialTrimStart;
+ currentTrimEndRef.current = resizing.initialTrimEnd;
+ currentStartTimeRef.current = resizing.initialStartTime;
+ currentDurationRef.current = resizing.initialDuration;
+ setResizing(null);
+ onResizeStateChange?.({ isResizing: false });
+ onSnapPointChange?.(null);
+ }, [resizing, onResizeStateChange, onSnapPointChange]);
+
+ useEffect(() => {
+ if (!resizing) return;
+
+ return registerCanceller({ fn: cancelResize });
+ }, [resizing, cancelResize]);
+
const updateTrimFromMouseMove = useCallback(
({ clientX }: { clientX: number }) => {
if (!resizing) return;
@@ -100,7 +193,7 @@ export function useTimelineElementResize({
deltaX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel);
let resizeSnapPoint: SnapPoint | null = null;
- const projectFps = activeProject.settings.fps;
+ const projectFps = editor.project.getActive().settings.fps;
const minDurationSeconds = 1 / projectFps;
const shouldSnap = snappingEnabled && !isShiftHeldRef.current;
if (shouldSnap) {
@@ -139,14 +232,20 @@ export function useTimelineElementResize({
}
onSnapPointChange?.(resizeSnapPoint);
- const otherElements = track.elements.filter(({ id }) => id !== element.id);
- const initialEndTime = resizing.initialStartTime + resizing.initialDuration;
+ const otherElements = track.elements.filter(
+ ({ id }) => id !== element.id,
+ );
+ const initialEndTime =
+ resizing.initialStartTime + resizing.initialDuration;
const rightNeighborBound =
resizing.side === "right"
? otherElements
.filter(({ startTime }) => startTime >= initialEndTime)
- .reduce((min, { startTime }) => Math.min(min, startTime), Infinity)
+ .reduce(
+ (min, { startTime }) => Math.min(min, startTime),
+ Infinity,
+ )
: Infinity;
const leftNeighborBound =
@@ -164,20 +263,26 @@ export function useTimelineElementResize({
: -Infinity;
if (resizing.side === "left") {
- const sourceDuration =
- resizing.initialTrimStart +
- resizing.initialDuration +
- resizing.initialTrimEnd;
+ const sourceDuration = getSourceDuration({
+ trimStart: resizing.initialTrimStart,
+ duration: resizing.initialDuration,
+ trimEnd: resizing.initialTrimEnd,
+ });
const minTrimStartForNeighbor = Number.isFinite(leftNeighborBound)
? Math.max(
0,
resizing.initialTrimStart +
- (leftNeighborBound - resizing.initialStartTime),
+ getSourceDeltaForClipDelta(
+ leftNeighborBound - resizing.initialStartTime,
+ ),
)
: 0;
const maxAllowed =
- sourceDuration - resizing.initialTrimEnd - minDurationSeconds;
- const calculated = resizing.initialTrimStart + deltaTime;
+ sourceDuration -
+ resizing.initialTrimEnd -
+ getVisibleSourceSpanForDuration(minDurationSeconds);
+ const calculated =
+ resizing.initialTrimStart + getSourceDeltaForClipDelta(deltaTime);
if (calculated >= 0 && calculated <= maxAllowed) {
const newTrimStart = snapTimeToFrame({
@@ -187,13 +292,17 @@ export function useTimelineElementResize({
),
fps: projectFps,
});
- const trimDelta = newTrimStart - resizing.initialTrimStart;
- const newStartTime = snapTimeToFrame({
- time: resizing.initialStartTime + trimDelta,
+ const visibleSourceSpan = Math.max(
+ 0,
+ sourceDuration - newTrimStart - resizing.initialTrimEnd,
+ );
+ const newDuration = snapTimeToFrame({
+ time: getDurationForVisibleSourceSpan(visibleSourceSpan),
fps: projectFps,
});
- const newDuration = snapTimeToFrame({
- time: resizing.initialDuration - trimDelta,
+ const trimDelta = resizing.initialDuration - newDuration;
+ const newStartTime = snapTimeToFrame({
+ time: resizing.initialStartTime + trimDelta,
fps: projectFps,
});
@@ -233,31 +342,50 @@ export function useTimelineElementResize({
currentStartTimeRef.current = newStartTime;
currentDurationRef.current = newDuration;
} else {
- const trimDelta =
+ const leftBound = Number.isFinite(leftNeighborBound)
+ ? leftNeighborBound
+ : 0;
+ const trimDeltaFromTrimStart =
minTrimStartForNeighbor - resizing.initialTrimStart;
- const newStartTime = snapTimeToFrame({
- time: resizing.initialStartTime + trimDelta,
+ const trimDeltaFromStartTime = getSourceDeltaForClipDelta(
+ leftBound - resizing.initialStartTime,
+ );
+ const trimDelta = Math.max(
+ trimDeltaFromTrimStart,
+ trimDeltaFromStartTime,
+ );
+ const newTrimStart = resizing.initialTrimStart + trimDelta;
+ const visibleSourceSpan = Math.max(
+ 0,
+ sourceDuration - newTrimStart - resizing.initialTrimEnd,
+ );
+ const newDuration = snapTimeToFrame({
+ time: getDurationForVisibleSourceSpan(visibleSourceSpan),
fps: projectFps,
});
- const newDuration = snapTimeToFrame({
- time: resizing.initialDuration - trimDelta,
+ const newStartTime = snapTimeToFrame({
+ time:
+ resizing.initialStartTime +
+ (resizing.initialDuration - newDuration),
fps: projectFps,
});
- setCurrentTrimStart(minTrimStartForNeighbor);
+ setCurrentTrimStart(newTrimStart);
setCurrentStartTime(newStartTime);
setCurrentDuration(newDuration);
- currentTrimStartRef.current = minTrimStartForNeighbor;
+ currentTrimStartRef.current = newTrimStart;
currentStartTimeRef.current = newStartTime;
currentDurationRef.current = newDuration;
}
}
} else {
- const sourceDuration =
- resizing.initialTrimStart +
- resizing.initialDuration +
- resizing.initialTrimEnd;
- const newTrimEnd = resizing.initialTrimEnd - deltaTime;
+ const sourceDuration = getSourceDuration({
+ trimStart: resizing.initialTrimStart,
+ duration: resizing.initialDuration,
+ trimEnd: resizing.initialTrimEnd,
+ });
+ const newTrimEnd =
+ resizing.initialTrimEnd - getSourceDeltaForClipDelta(deltaTime);
const maxAllowedDuration = Number.isFinite(rightNeighborBound)
? rightNeighborBound - resizing.initialStartTime
: Infinity;
@@ -268,7 +396,10 @@ export function useTimelineElementResize({
const baseDuration =
resizing.initialDuration + resizing.initialTrimEnd;
const newDuration = snapTimeToFrame({
- time: Math.min(baseDuration + extensionNeeded, maxAllowedDuration),
+ time: Math.min(
+ baseDuration + extensionNeeded,
+ maxAllowedDuration,
+ ),
fps: projectFps,
});
@@ -277,12 +408,11 @@ export function useTimelineElementResize({
currentDurationRef.current = newDuration;
currentTrimEndRef.current = 0;
} else {
- const extensionToLimit = resizing.initialTrimEnd;
+ const unclampedDuration = getDurationForVisibleSourceSpan(
+ Math.max(0, sourceDuration - resizing.initialTrimStart),
+ );
const newDuration = snapTimeToFrame({
- time: Math.min(
- resizing.initialDuration + extensionToLimit,
- maxAllowedDuration,
- ),
+ time: Math.min(unclampedDuration, maxAllowedDuration),
fps: projectFps,
});
@@ -295,13 +425,15 @@ export function useTimelineElementResize({
const minTrimEndForNeighbor = Number.isFinite(maxAllowedDuration)
? Math.max(
0,
- resizing.initialDuration +
- resizing.initialTrimEnd -
- maxAllowedDuration,
+ sourceDuration -
+ resizing.initialTrimStart -
+ getVisibleSourceSpanForDuration(maxAllowedDuration),
)
: 0;
const maxTrimEnd =
- sourceDuration - resizing.initialTrimStart - minDurationSeconds;
+ sourceDuration -
+ resizing.initialTrimStart -
+ getVisibleSourceSpanForDuration(minDurationSeconds);
const clampedTrimEnd = Math.min(
maxTrimEnd,
Math.max(minTrimEndForNeighbor, newTrimEnd),
@@ -310,9 +442,12 @@ export function useTimelineElementResize({
time: clampedTrimEnd,
fps: projectFps,
});
- const trimDelta = finalTrimEnd - resizing.initialTrimEnd;
+ const visibleSourceSpan = Math.max(
+ 0,
+ sourceDuration - resizing.initialTrimStart - finalTrimEnd,
+ );
const newDuration = snapTimeToFrame({
- time: resizing.initialDuration - trimDelta,
+ time: getDurationForVisibleSourceSpan(visibleSourceSpan),
fps: projectFps,
});
@@ -326,13 +461,16 @@ export function useTimelineElementResize({
[
resizing,
zoomLevel,
- activeProject.settings.fps,
snappingEnabled,
editor,
element.id,
track.elements,
onSnapPointChange,
canExtendElementDuration,
+ getDurationForVisibleSourceSpan,
+ getSourceDeltaForClipDelta,
+ getSourceDuration,
+ getVisibleSourceSpanForDuration,
isShiftHeldRef,
],
);
@@ -349,7 +487,12 @@ export function useTimelineElementResize({
const startTimeChanged = finalStartTime !== resizing.initialStartTime;
const durationChanged = finalDuration !== resizing.initialDuration;
- if (trimStartChanged || trimEndChanged || startTimeChanged || durationChanged) {
+ if (
+ trimStartChanged ||
+ trimEndChanged ||
+ startTimeChanged ||
+ durationChanged
+ ) {
editor.timeline.updateElementTrim({
elementId: element.id,
trimStart: finalTrimStart,
diff --git a/apps/web/src/hooks/timeline/element/use-element-selection.ts b/apps/web/src/hooks/timeline/element/use-element-selection.ts
index c1e5b53f..dcfb5720 100644
--- a/apps/web/src/hooks/timeline/element/use-element-selection.ts
+++ b/apps/web/src/hooks/timeline/element/use-element-selection.ts
@@ -1,14 +1,10 @@
-import { useCallback, useSyncExternalStore } from "react";
+import { useCallback } from "react";
import { useEditor } from "@/hooks/use-editor";
-
-type ElementRef = { trackId: string; elementId: string };
+import type { ElementRef } from "@/lib/timeline/types";
export function useElementSelection() {
const editor = useEditor();
- const selectedElements = useSyncExternalStore(
- (listener) => editor.selection.subscribe(listener),
- () => editor.selection.getSelectedElements(),
- );
+ const selectedElements = useEditor((e) => e.selection.getSelectedElements());
const isElementSelected = useCallback(
({ trackId, elementId }: ElementRef) =>
@@ -82,6 +78,30 @@ export function useElementSelection() {
[editor],
);
+
+ /**
+ * Merges elements into the current selection, deduplicating by identity.
+ * Used for additive box-select where the pre-drag selection is preserved.
+ */
+ const mergeElementsIntoSelection = useCallback(
+ ({ elements }: { elements: ElementRef[] }) => {
+ const merged = [
+ ...selectedElements.filter(
+ (selectedElement) =>
+ !elements.some(
+ (element) =>
+ element.trackId === selectedElement.trackId &&
+ element.elementId === selectedElement.elementId,
+ ),
+ ),
+ ...elements,
+ ];
+ editor.selection.setSelectedElements({ elements: merged });
+ },
+ [selectedElements, editor],
+ );
+
+
/**
* Handles click interaction on an element.
* - Regular click: select only this element
@@ -107,6 +127,7 @@ export function useElementSelection() {
isElementSelected,
selectElement,
setElementSelection,
+ mergeElementsIntoSelection,
addElementToSelection,
removeElementFromSelection,
toggleElementSelection,
diff --git a/apps/web/src/hooks/timeline/element/use-keyframe-drag.ts b/apps/web/src/hooks/timeline/element/use-keyframe-drag.ts
index b18db6eb..4f961a0a 100644
--- a/apps/web/src/hooks/timeline/element/use-keyframe-drag.ts
+++ b/apps/web/src/hooks/timeline/element/use-keyframe-drag.ts
@@ -15,9 +15,10 @@ import {
} from "@/constants/timeline-constants";
import { RetimeKeyframeCommand } from "@/lib/commands/timeline/element/keyframes/retime-keyframe";
import { BatchCommand } from "@/lib/commands";
-import type { SelectedKeyframeRef } from "@/types/animation";
-import type { TimelineElement } from "@/types/timeline";
+import type { SelectedKeyframeRef } from "@/lib/animation/types";
+import type { TimelineElement } from "@/lib/timeline";
import type { Command } from "@/lib/commands/base-command";
+import { registerCanceller } from "@/lib/cancel-interaction";
export interface KeyframeDragState {
isDragging: boolean;
draggingKeyframeIds: Set<string>;
@@ -69,6 +70,13 @@ export function useKeyframeDrag({
setDragState(initialDragState);
}, []);
+ const cancelDrag = useCallback(() => {
+ pendingDragRef.current = null;
+ mouseDownXRef.current = null;
+ setIsPendingDrag(false);
+ endDrag();
+ }, [endDrag]);
+
const commitDrag = useCallback(
({
keyframeRefs,
@@ -110,6 +118,12 @@ export function useKeyframeDrag({
useEffect(() => {
if (!dragState.isDragging && !isPendingDrag) return;
+ return registerCanceller({ fn: cancelDrag });
+ }, [dragState.isDragging, isPendingDrag, cancelDrag]);
+
+ useEffect(() => {
+ if (!dragState.isDragging && !isPendingDrag) return;
+
const handleMouseMove = ({ clientX }: MouseEvent) => {
if (isPendingDrag && pendingDragRef.current) {
const deltaX = Math.abs(clientX - pendingDragRef.current.startMouseX);
diff --git a/apps/web/src/hooks/timeline/element/use-keyframe-selection.ts b/apps/web/src/hooks/timeline/element/use-keyframe-selection.ts
index 0ab05fe3..8634cb48 100644
--- a/apps/web/src/hooks/timeline/element/use-keyframe-selection.ts
+++ b/apps/web/src/hooks/timeline/element/use-keyframe-selection.ts
@@ -1,6 +1,6 @@
import { useCallback, useSyncExternalStore } from "react";
import { useEditor } from "@/hooks/use-editor";
-import type { SelectedKeyframeRef } from "@/types/animation";
+import type { SelectedKeyframeRef } from "@/lib/animation/types";
function getSelectedKeyframeId({
keyframe,