+
diff --git a/apps/web/src/hooks/use-editor-actions.ts b/apps/web/src/hooks/use-editor-actions.ts
index b2fad55f..bfe9a7b9 100644
--- a/apps/web/src/hooks/use-editor-actions.ts
+++ b/apps/web/src/hooks/use-editor-actions.ts
@@ -26,136 +26,204 @@ export function useEditorActions() {
const { activeProject } = useProjectStore();
// Playback actions
- useActionHandler("toggle-play", () => {
- toggle();
- }, undefined);
-
- useActionHandler("stop-playback", () => {
- if (isPlaying) {
+ useActionHandler(
+ "toggle-play",
+ () => {
toggle();
- }
- seek(0);
- }, undefined);
+ },
+ undefined
+ );
- useActionHandler("seek-forward", (args) => {
- const seconds = args?.seconds ?? 1;
- seek(Math.min(duration, currentTime + seconds));
- }, undefined);
+ useActionHandler(
+ "stop-playback",
+ () => {
+ if (isPlaying) {
+ toggle();
+ }
+ seek(0);
+ },
+ undefined
+ );
- useActionHandler("seek-backward", (args) => {
- const seconds = args?.seconds ?? 1;
- seek(Math.max(0, currentTime - seconds));
- }, undefined);
+ useActionHandler(
+ "seek-forward",
+ (args) => {
+ const seconds = args?.seconds ?? 1;
+ seek(Math.min(duration, currentTime + seconds));
+ },
+ undefined
+ );
- useActionHandler("frame-step-forward", () => {
- const projectFps = activeProject?.fps || 30;
- seek(Math.min(duration, currentTime + 1 / projectFps));
- }, undefined);
+ useActionHandler(
+ "seek-backward",
+ (args) => {
+ const seconds = args?.seconds ?? 1;
+ seek(Math.max(0, currentTime - seconds));
+ },
+ undefined
+ );
- useActionHandler("frame-step-backward", () => {
- const projectFps = activeProject?.fps || 30;
- seek(Math.max(0, currentTime - 1 / projectFps));
- }, undefined);
+ useActionHandler(
+ "frame-step-forward",
+ () => {
+ const projectFps = activeProject?.fps || 30;
+ seek(Math.min(duration, currentTime + 1 / projectFps));
+ },
+ undefined
+ );
- useActionHandler("jump-forward", (args) => {
- const seconds = args?.seconds ?? 5;
- seek(Math.min(duration, currentTime + seconds));
- }, undefined);
+ useActionHandler(
+ "frame-step-backward",
+ () => {
+ const projectFps = activeProject?.fps || 30;
+ seek(Math.max(0, currentTime - 1 / projectFps));
+ },
+ undefined
+ );
- useActionHandler("jump-backward", (args) => {
- const seconds = args?.seconds ?? 5;
- seek(Math.max(0, currentTime - seconds));
- }, undefined);
+ useActionHandler(
+ "jump-forward",
+ (args) => {
+ const seconds = args?.seconds ?? 5;
+ seek(Math.min(duration, currentTime + seconds));
+ },
+ undefined
+ );
- useActionHandler("goto-start", () => {
- seek(0);
- }, undefined);
+ useActionHandler(
+ "jump-backward",
+ (args) => {
+ const seconds = args?.seconds ?? 5;
+ seek(Math.max(0, currentTime - seconds));
+ },
+ undefined
+ );
- useActionHandler("goto-end", () => {
- seek(duration);
- }, undefined);
+ useActionHandler(
+ "goto-start",
+ () => {
+ seek(0);
+ },
+ undefined
+ );
+
+ useActionHandler(
+ "goto-end",
+ () => {
+ seek(duration);
+ },
+ undefined
+ );
// Timeline editing actions
- useActionHandler("split-element", () => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one element to split");
- return;
- }
-
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t: any) => t.id === trackId);
- const element = track?.elements.find((el: any) => el.id === elementId);
-
- if (element) {
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
-
- if (currentTime > effectiveStart && currentTime < effectiveEnd) {
- splitElement(trackId, elementId, currentTime);
- } else {
- toast.error("Playhead must be within selected element");
+ useActionHandler(
+ "split-element",
+ () => {
+ if (selectedElements.length !== 1) {
+ toast.error("Select exactly one element to split");
+ return;
}
- }
- }, undefined);
- useActionHandler("delete-selected", () => {
- if (selectedElements.length === 0) {
- return;
- }
- selectedElements.forEach(
- ({ trackId, elementId }: { trackId: string; elementId: string }) => {
- removeElementFromTrack(trackId, elementId);
+ const { trackId, elementId } = selectedElements[0];
+ const track = tracks.find((t: any) => t.id === trackId);
+ const element = track?.elements.find((el: any) => el.id === elementId);
+
+ if (element) {
+ const effectiveStart = element.startTime;
+ const effectiveEnd =
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd);
+
+ if (currentTime > effectiveStart && currentTime < effectiveEnd) {
+ splitElement(trackId, elementId, currentTime);
+ } else {
+ toast.error("Playhead must be within selected element");
+ }
}
- );
- clearSelectedElements();
- }, undefined);
+ },
+ undefined
+ );
- useActionHandler("select-all", () => {
- const allElements = tracks.flatMap((track: any) =>
- track.elements.map((element: any) => ({
- trackId: track.id,
- elementId: element.id,
- }))
- );
- setSelectedElements(allElements);
- }, undefined);
+ useActionHandler(
+ "delete-selected",
+ () => {
+ if (selectedElements.length === 0) {
+ return;
+ }
+ selectedElements.forEach(
+ ({ trackId, elementId }: { trackId: string; elementId: string }) => {
+ removeElementFromTrack(trackId, elementId);
+ }
+ );
+ clearSelectedElements();
+ },
+ undefined
+ );
- useActionHandler("duplicate-selected", () => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one element to duplicate");
- return;
- }
+ useActionHandler(
+ "select-all",
+ () => {
+ const allElements = tracks.flatMap((track: any) =>
+ track.elements.map((element: any) => ({
+ trackId: track.id,
+ elementId: element.id,
+ }))
+ );
+ setSelectedElements(allElements);
+ },
+ undefined
+ );
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t: any) => t.id === trackId);
- const element = track?.elements.find((el: any) => el.id === elementId);
+ useActionHandler(
+ "duplicate-selected",
+ () => {
+ if (selectedElements.length !== 1) {
+ toast.error("Select exactly one element to duplicate");
+ return;
+ }
- if (element) {
- const newStartTime =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd) +
- 0.1;
- const { id, ...elementWithoutId } = element;
+ const { trackId, elementId } = selectedElements[0];
+ const track = tracks.find((t: any) => t.id === trackId);
+ const element = track?.elements.find((el: any) => el.id === elementId);
- addElementToTrack(trackId, {
- ...elementWithoutId,
- startTime: newStartTime,
- });
- }
- }, undefined);
+ if (element) {
+ const newStartTime =
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd) +
+ 0.1;
+ const { id, ...elementWithoutId } = element;
- useActionHandler("toggle-snapping", () => {
- toggleSnapping();
- }, undefined);
+ addElementToTrack(trackId, {
+ ...elementWithoutId,
+ startTime: newStartTime,
+ });
+ }
+ },
+ undefined
+ );
+
+ useActionHandler(
+ "toggle-snapping",
+ () => {
+ toggleSnapping();
+ },
+ undefined
+ );
// History actions
- useActionHandler("undo", () => {
- undo();
- }, undefined);
+ useActionHandler(
+ "undo",
+ () => {
+ undo();
+ },
+ undefined
+ );
- useActionHandler("redo", () => {
- redo();
- }, undefined);
+ useActionHandler(
+ "redo",
+ () => {
+ redo();
+ },
+ undefined
+ );
}
diff --git a/bun.lock b/bun.lock
index 72bd43c3..894837c8 100644
--- a/bun.lock
+++ b/bun.lock
@@ -9,6 +9,7 @@
},
"devDependencies": {
"@biomejs/biome": "2.1.2",
+ "@types/node": "^24.1.0",
"husky": "^9.1.7",
"turbo": "^2.5.4",
"typescript": "5.8.3",
@@ -75,6 +76,7 @@
"devDependencies": {
"@tailwindcss/typography": "^0.5.16",
"@types/bun": "latest",
+ "@types/node": "^24.1.0",
"@types/pg": "^8.15.4",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
@@ -536,7 +538,7 @@
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
- "@types/node": ["@types/node@22.16.5", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ=="],
+ "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
"@types/pg": ["@types/pg@8.15.4", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg=="],
@@ -1236,7 +1238,7 @@
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
- "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+ "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
@@ -1300,10 +1302,16 @@
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
+ "@opencut/auth/@types/node": ["@types/node@22.16.5", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ=="],
+
+ "@types/pg/@types/node": ["@types/node@22.16.5", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ=="],
+
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"better-auth/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+ "bun-types/@types/node": ["@types/node@22.16.5", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ=="],
+
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -1388,6 +1396,12 @@
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
+ "@opencut/auth/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+
+ "@types/pg/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+
+ "bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+
"motion/framer-motion/motion-dom": ["motion-dom@12.23.6", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-G2w6Nw7ZOVSzcQmsdLc0doMe64O/Sbuc2bVAbgMz6oP/6/pRStKRiVRV4bQfHp5AHYAKEGhEdVHTM+R3FDgi5w=="],
"motion/framer-motion/motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
diff --git a/package.json b/package.json
index 6512dd11..9bd12305 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"packageManager": "bun@1.2.18",
"devDependencies": {
"@biomejs/biome": "2.1.2",
+ "@types/node": "^24.1.0",
"husky": "^9.1.7",
"turbo": "^2.5.4",
"typescript": "5.8.3",