summaryrefslogtreecommitdiff
path: root/apps/web/src/components
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-31 04:03:52 +0200
committerMaze Winther <mazewinther@gmail.com>2026-03-31 04:03:52 +0200
commitdcf54c1250431f5a4a2848f7c07781ef5eebacaf (patch)
tree64270f82d879ce0b0a028384e513ea87940da630 /apps/web/src/components
parent5cc6abc3a44c9d046a1cb71a8c916f1d35827e8f (diff)
downloadopencut-dcf54c1250431f5a4a2848f7c07781ef5eebacaf.tar.gz
feat: migrate time utilities from typescript to rust WASM
Diffstat (limited to 'apps/web/src/components')
-rw-r--r--apps/web/src/components/editable-timecode.tsx299
-rw-r--r--apps/web/src/components/editor/dialogs/project-info-dialog.tsx163
-rw-r--r--apps/web/src/components/editor/panels/preview/toolbar.tsx298
-rw-r--r--apps/web/src/components/editor/panels/timeline/bookmarks.tsx2
4 files changed, 377 insertions, 385 deletions
diff --git a/apps/web/src/components/editable-timecode.tsx b/apps/web/src/components/editable-timecode.tsx
index c346ad53..6a323a1c 100644
--- a/apps/web/src/components/editable-timecode.tsx
+++ b/apps/web/src/components/editable-timecode.tsx
@@ -1,150 +1,149 @@
-"use client";
-
-import { useEffect, useRef, useState } from "react";
-import { formatTimeCode, parseTimeCode } from "@/lib/time";
-import type { TTimeCode } from "@/lib/time";
-import { cn } from "@/utils/ui";
-
-interface EditableTimecodeProps {
- time: number;
- duration: number;
- format?: TTimeCode;
- fps: number;
- onTimeChange?: ({ time }: { time: number }) => void;
- className?: string;
- disabled?: boolean;
-}
-
-export function EditableTimecode({
- time,
- duration,
- format = "HH:MM:SS:FF",
- fps,
- onTimeChange,
- className,
- disabled = false,
-}: EditableTimecodeProps) {
- const [isEditing, setIsEditing] = useState(false);
- const [inputValue, setInputValue] = useState("");
- const [hasError, setHasError] = useState(false);
- const inputRef = useRef<HTMLInputElement>(null);
- const enterPressedRef = useRef(false);
- const formattedTime = formatTimeCode({ timeInSeconds: time, format, fps });
-
- const startEditing = () => {
- if (disabled) return;
- setIsEditing(true);
- setInputValue(formattedTime);
- setHasError(false);
- enterPressedRef.current = false;
- };
-
- const cancelEditing = () => {
- setIsEditing(false);
- setInputValue("");
- setHasError(false);
- enterPressedRef.current = false;
- };
-
- const applyEdit = () => {
- const parsedTime = parseTimeCode({ timeCode: inputValue, format, fps });
-
- if (parsedTime === null) {
- setHasError(true);
- return;
- }
-
- const clampedTime = Math.max(
- 0,
- duration ? Math.min(duration, parsedTime) : parsedTime,
- );
-
- onTimeChange?.({ time: clampedTime });
- setIsEditing(false);
- setInputValue("");
- setHasError(false);
- enterPressedRef.current = false;
- };
-
- const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
- if (event.key === "Enter") {
- event.preventDefault();
- enterPressedRef.current = true;
- applyEdit();
- } else if (event.key === "Escape") {
- event.preventDefault();
- cancelEditing();
- }
- };
-
- const handleInputChange = ({
- target,
- }: React.ChangeEvent<HTMLInputElement>) => {
- setInputValue(target.value);
- setHasError(false);
- };
-
- const handleBlur = () => {
- if (!enterPressedRef.current && isEditing) {
- applyEdit();
- }
- };
-
- const handleDisplayKeyDown = (
- event: React.KeyboardEvent<HTMLButtonElement>,
- ) => {
- if (disabled) return;
-
- if (event.key === "Enter" || event.key === " ") {
- event.preventDefault();
- startEditing();
- }
- };
-
- useEffect(() => {
- if (isEditing && inputRef.current) {
- inputRef.current.focus();
- inputRef.current.select();
- }
- }, [isEditing]);
-
- if (isEditing) {
- return (
- <input
- ref={inputRef}
- type="text"
- value={inputValue}
- onChange={handleInputChange}
- onKeyDown={handleKeyDown}
- onBlur={handleBlur}
- className={cn(
- "-mx-1 border border-transparent bg-transparent px-1 font-mono text-xs outline-none",
- "focus:bg-background focus:border-primary focus:rounded",
- "text-primary tabular-nums",
- hasError && "text-destructive focus:border-destructive",
- className,
- )}
- style={{ width: `${formattedTime.length + 1}ch` }}
- placeholder={formattedTime}
- />
- );
- }
-
- return (
- <button
- type="button"
- onClick={startEditing}
- onKeyDown={handleDisplayKeyDown}
- disabled={disabled}
- className={cn(
- "text-primary cursor-pointer font-mono text-xs tabular-nums",
- "hover:bg-muted/50 -mx-1 px-1 hover:rounded",
- disabled && "cursor-default hover:bg-transparent",
- className,
- )}
- title={disabled ? undefined : "Click to edit time"}
- >
- {formattedTime}
- </button>
- );
-}
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { formatTimeCode, parseTimeCode, type TimeCodeFormat } from "opencut-wasm";
+import { cn } from "@/utils/ui";
+
+interface EditableTimecodeProps {
+ time: number;
+ duration: number;
+ format?: TimeCodeFormat;
+ fps: number;
+ onTimeChange?: ({ time }: { time: number }) => void;
+ className?: string;
+ disabled?: boolean;
+}
+
+export function EditableTimecode({
+ time,
+ duration,
+ format = "HH:MM:SS:FF",
+ fps,
+ onTimeChange,
+ className,
+ disabled = false,
+}: EditableTimecodeProps) {
+ const [isEditing, setIsEditing] = useState(false);
+ const [inputValue, setInputValue] = useState("");
+ const [hasError, setHasError] = useState(false);
+ const inputRef = useRef<HTMLInputElement>(null);
+ const enterPressedRef = useRef(false);
+ const formattedTime = formatTimeCode({ timeInSeconds: time, format, fps }) ?? "";
+
+ const startEditing = () => {
+ if (disabled) return;
+ setIsEditing(true);
+ setInputValue(formattedTime);
+ setHasError(false);
+ enterPressedRef.current = false;
+ };
+
+ const cancelEditing = () => {
+ setIsEditing(false);
+ setInputValue("");
+ setHasError(false);
+ enterPressedRef.current = false;
+ };
+
+ const applyEdit = () => {
+ const parsedTime = parseTimeCode({ timeCode: inputValue, format, fps });
+
+ if (parsedTime == null) {
+ setHasError(true);
+ return;
+ }
+
+ const clampedTime = Math.max(
+ 0,
+ duration ? Math.min(duration, parsedTime) : parsedTime,
+ );
+
+ onTimeChange?.({ time: clampedTime });
+ setIsEditing(false);
+ setInputValue("");
+ setHasError(false);
+ enterPressedRef.current = false;
+ };
+
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ enterPressedRef.current = true;
+ applyEdit();
+ } else if (event.key === "Escape") {
+ event.preventDefault();
+ cancelEditing();
+ }
+ };
+
+ const handleInputChange = ({
+ target,
+ }: React.ChangeEvent<HTMLInputElement>) => {
+ setInputValue(target.value);
+ setHasError(false);
+ };
+
+ const handleBlur = () => {
+ if (!enterPressedRef.current && isEditing) {
+ applyEdit();
+ }
+ };
+
+ const handleDisplayKeyDown = (
+ event: React.KeyboardEvent<HTMLButtonElement>,
+ ) => {
+ if (disabled) return;
+
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ startEditing();
+ }
+ };
+
+ useEffect(() => {
+ if (isEditing && inputRef.current) {
+ inputRef.current.focus();
+ inputRef.current.select();
+ }
+ }, [isEditing]);
+
+ if (isEditing) {
+ return (
+ <input
+ ref={inputRef}
+ type="text"
+ value={inputValue}
+ onChange={handleInputChange}
+ onKeyDown={handleKeyDown}
+ onBlur={handleBlur}
+ className={cn(
+ "-mx-1 border border-transparent bg-transparent px-1 font-mono text-xs outline-none",
+ "focus:bg-background focus:border-primary focus:rounded",
+ "text-primary tabular-nums",
+ hasError && "text-destructive focus:border-destructive",
+ className,
+ )}
+ style={{ width: `${formattedTime.length + 1}ch` }}
+ placeholder={formattedTime}
+ />
+ );
+ }
+
+ return (
+ <button
+ type="button"
+ onClick={startEditing}
+ onKeyDown={handleDisplayKeyDown}
+ disabled={disabled}
+ className={cn(
+ "text-primary cursor-pointer font-mono text-xs tabular-nums",
+ "hover:bg-muted/50 -mx-1 px-1 hover:rounded",
+ disabled && "cursor-default hover:bg-transparent",
+ className,
+ )}
+ title={disabled ? undefined : "Click to edit time"}
+ >
+ {formattedTime}
+ </button>
+ );
+}
diff --git a/apps/web/src/components/editor/dialogs/project-info-dialog.tsx b/apps/web/src/components/editor/dialogs/project-info-dialog.tsx
index fad711e3..c31c8e96 100644
--- a/apps/web/src/components/editor/dialogs/project-info-dialog.tsx
+++ b/apps/web/src/components/editor/dialogs/project-info-dialog.tsx
@@ -1,83 +1,80 @@
-import {
- Dialog,
- DialogBody,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import type { TProjectMetadata } from "@/lib/project/types";
-import { formatDate } from "@/utils/date";
-import { formatTimeCode } from "@/lib/time";
-import { Button } from "@/components/ui/button";
-
-function InfoRow({
- label,
- value,
-}: {
- label: string;
- value: string | React.ReactNode;
-}) {
- return (
- <div className="flex justify-between items-center py-0 last:pb-0">
- <span className="text-muted-foreground text-sm">{label}</span>
- <span className="text-sm font-medium">{value}</span>
- </div>
- );
-}
-
-export function ProjectInfoDialog({
- isOpen,
- onOpenChange,
- project,
-}: {
- isOpen: boolean;
- onOpenChange: (open: boolean) => void;
- project: TProjectMetadata;
-}) {
- const durationFormatted =
- project.duration > 0
- ? formatTimeCode({
- timeInSeconds: project.duration,
- format: project.duration >= 3600 ? "HH:MM:SS" : "MM:SS",
- })
- : "0:00";
-
- return (
- <Dialog open={isOpen} onOpenChange={onOpenChange}>
- <DialogContent onOpenAutoFocus={(event) => event.preventDefault()}>
- <DialogHeader>
- <DialogTitle className="truncate max-w-[350px]">
- {project.name}
- </DialogTitle>
- </DialogHeader>
-
- <DialogBody className="flex flex-col">
- <InfoRow label="Duration" value={durationFormatted} />
- <InfoRow
- label="Created"
- value={formatDate({ date: project.createdAt })}
- />
- <InfoRow
- label="Modified"
- value={formatDate({ date: project.updatedAt })}
- />
- <InfoRow
- label="Project ID"
- value={
- <code className="text-xs bg-muted px-1.5 py-0.5 rounded">
- {project.id.slice(0, 8)}
- </code>
- }
- />
- </DialogBody>
- <DialogFooter>
- <Button variant="outline" onClick={() => onOpenChange(false)}>
- Close
- </Button>
- <Button onClick={() => onOpenChange(false)}>Done</Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-}
+import {
+ Dialog,
+ DialogBody,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import type { TProjectMetadata } from "@/lib/project/types";
+import { formatDate } from "@/utils/date";
+import { formatTimeCode } from "opencut-wasm";
+import { Button } from "@/components/ui/button";
+
+function InfoRow({
+ label,
+ value,
+}: {
+ label: string;
+ value: string | React.ReactNode;
+}) {
+ return (
+ <div className="flex justify-between items-center py-0 last:pb-0">
+ <span className="text-muted-foreground text-sm">{label}</span>
+ <span className="text-sm font-medium">{value}</span>
+ </div>
+ );
+}
+
+export function ProjectInfoDialog({
+ isOpen,
+ onOpenChange,
+ project,
+}: {
+ isOpen: boolean;
+ onOpenChange: (open: boolean) => void;
+ project: TProjectMetadata;
+}) {
+ const durationFormatted =
+ project.duration > 0
+ ? (formatTimeCode({ timeInSeconds: project.duration, format: project.duration >= 3600 ? "HH:MM:SS" : "MM:SS" }) ?? "")
+ : "0:00";
+
+ return (
+ <Dialog open={isOpen} onOpenChange={onOpenChange}>
+ <DialogContent onOpenAutoFocus={(event) => event.preventDefault()}>
+ <DialogHeader>
+ <DialogTitle className="truncate max-w-[350px]">
+ {project.name}
+ </DialogTitle>
+ </DialogHeader>
+
+ <DialogBody className="flex flex-col">
+ <InfoRow label="Duration" value={durationFormatted} />
+ <InfoRow
+ label="Created"
+ value={formatDate({ date: project.createdAt })}
+ />
+ <InfoRow
+ label="Modified"
+ value={formatDate({ date: project.updatedAt })}
+ />
+ <InfoRow
+ label="Project ID"
+ value={
+ <code className="text-xs bg-muted px-1.5 py-0.5 rounded">
+ {project.id.slice(0, 8)}
+ </code>
+ }
+ />
+ </DialogBody>
+ <DialogFooter>
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
+ Close
+ </Button>
+ <Button onClick={() => onOpenChange(false)}>Done</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/preview/toolbar.tsx b/apps/web/src/components/editor/panels/preview/toolbar.tsx
index 5c183bbb..340058d0 100644
--- a/apps/web/src/components/editor/panels/preview/toolbar.tsx
+++ b/apps/web/src/components/editor/panels/preview/toolbar.tsx
@@ -1,151 +1,147 @@
-"use client";
-
-import { useState, useEffect } from "react";
-import { useEditor } from "@/hooks/use-editor";
-import { formatTimeCode } from "@/lib/time";
-import { invokeAction } from "@/lib/actions";
-import { EditableTimecode } from "@/components/editable-timecode";
-import { Button } from "@/components/ui/button";
-import {
- FullScreenIcon,
- GridTableIcon,
- PauseIcon,
- PlayIcon,
-} from "@hugeicons/core-free-icons";
-import { HugeiconsIcon } from "@hugeicons/react";
-import { getGuideById } from "@/lib/guides";
-import { Separator } from "@/components/ui/separator";
-import {
- Select,
- SelectTrigger,
- SelectContent,
- SelectItem,
- SelectSeparator,
-} from "@/components/ui/select";
-import { PREVIEW_ZOOM_PRESETS } from "@/constants/editor-constants";
-import { usePreviewViewport } from "./preview-viewport";
-import { GridPopover } from "./guide-popover";
-import { usePreviewStore } from "@/stores/preview-store";
-
-export function PreviewToolbar({
- onToggleFullscreen,
-}: {
- onToggleFullscreen: () => void;
-}) {
- const activeGuide = usePreviewStore((state) => state.activeGuide);
- const activeGuideDefinition = getGuideById(activeGuide);
-
- return (
- <div className="grid grid-cols-[1fr_auto_1fr] items-center pb-3 pt-5 px-5">
- <TimecodeDisplay />
- <PlayPauseButton />
- <div className="justify-self-end flex items-center gap-2.5">
- <ZoomSelect />
- <Separator orientation="vertical" className="h-4" />
- <GridPopover>
- <Button
- variant={activeGuideDefinition ? "secondary" : "text"}
- size="icon"
- >
- {activeGuideDefinition ? (
- activeGuideDefinition.renderTriggerIcon()
- ) : (
- <HugeiconsIcon icon={GridTableIcon} />
- )}
- </Button>
- </GridPopover>
- <Button variant="text" onClick={onToggleFullscreen}>
- <HugeiconsIcon icon={FullScreenIcon} />
- </Button>
- </div>
- </div>
- );
-}
-
-function TimecodeDisplay() {
- const editor = useEditor();
- const totalDuration = useEditor((e) => e.timeline.getTotalDuration());
- const fps = useEditor((e) => e.project.getActive().settings.fps);
- const [currentTime, setCurrentTime] = useState(() =>
- editor.playback.getCurrentTime(),
- );
-
- useEffect(() => {
- const handler = (e: Event) =>
- setCurrentTime((e as CustomEvent<{ time: number }>).detail.time);
- window.addEventListener("playback-update", handler);
- window.addEventListener("playback-seek", handler);
- return () => {
- window.removeEventListener("playback-update", handler);
- window.removeEventListener("playback-seek", handler);
- };
- }, []);
-
- return (
- <div className="flex items-center">
- <EditableTimecode
- time={currentTime}
- duration={totalDuration}
- format="HH:MM:SS:FF"
- fps={fps}
- onTimeChange={({ time }) => editor.playback.seek({ time })}
- className="text-center"
- />
- <span className="text-muted-foreground px-2 font-mono text-xs">/</span>
- <span className="text-muted-foreground font-mono text-xs">
- {formatTimeCode({
- timeInSeconds: totalDuration,
- format: "HH:MM:SS:FF",
- fps,
- })}
- </span>
- </div>
- );
-}
-
-function ZoomSelect() {
- const { isAtFit, zoomPercent, fitToScreen, setViewportPercent } =
- usePreviewViewport();
-
- const displayLabel = isAtFit ? "Fit" : `${zoomPercent}%`;
-
- const onValueChange = (value: string) => {
- if (value === "fit") {
- fitToScreen();
- } else {
- setViewportPercent({ percent: Number(value) });
- }
- };
-
- return (
- <Select
- value={isAtFit ? "fit" : String(zoomPercent)}
- onValueChange={onValueChange}
- >
- <SelectTrigger className="tabular-nums">{displayLabel}</SelectTrigger>
- <SelectContent>
- <SelectItem value="fit">Fit</SelectItem>
- <SelectSeparator />
- {PREVIEW_ZOOM_PRESETS.map((preset) => (
- <SelectItem key={preset} value={String(preset)}>
- {preset}%
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- );
-}
-
-function PlayPauseButton() {
- const isPlaying = useEditor((e) => e.playback.getIsPlaying());
-
- return (
- <Button
- variant="text"
- size="icon"
- onClick={() => invokeAction("toggle-play")}
- >
- <HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
- </Button>
- );
-}
+"use client";
+
+import { useState, useEffect } from "react";
+import { useEditor } from "@/hooks/use-editor";
+import { formatTimeCode } from "opencut-wasm";
+import { invokeAction } from "@/lib/actions";
+import { EditableTimecode } from "@/components/editable-timecode";
+import { Button } from "@/components/ui/button";
+import {
+ FullScreenIcon,
+ GridTableIcon,
+ PauseIcon,
+ PlayIcon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { getGuideById } from "@/lib/guides";
+import { Separator } from "@/components/ui/separator";
+import {
+ Select,
+ SelectTrigger,
+ SelectContent,
+ SelectItem,
+ SelectSeparator,
+} from "@/components/ui/select";
+import { PREVIEW_ZOOM_PRESETS } from "@/constants/editor-constants";
+import { usePreviewViewport } from "./preview-viewport";
+import { GridPopover } from "./guide-popover";
+import { usePreviewStore } from "@/stores/preview-store";
+
+export function PreviewToolbar({
+ onToggleFullscreen,
+}: {
+ onToggleFullscreen: () => void;
+}) {
+ const activeGuide = usePreviewStore((state) => state.activeGuide);
+ const activeGuideDefinition = getGuideById(activeGuide);
+
+ return (
+ <div className="grid grid-cols-[1fr_auto_1fr] items-center pb-3 pt-5 px-5">
+ <TimecodeDisplay />
+ <PlayPauseButton />
+ <div className="justify-self-end flex items-center gap-2.5">
+ <ZoomSelect />
+ <Separator orientation="vertical" className="h-4" />
+ <GridPopover>
+ <Button
+ variant={activeGuideDefinition ? "secondary" : "text"}
+ size="icon"
+ >
+ {activeGuideDefinition ? (
+ activeGuideDefinition.renderTriggerIcon()
+ ) : (
+ <HugeiconsIcon icon={GridTableIcon} />
+ )}
+ </Button>
+ </GridPopover>
+ <Button variant="text" onClick={onToggleFullscreen}>
+ <HugeiconsIcon icon={FullScreenIcon} />
+ </Button>
+ </div>
+ </div>
+ );
+}
+
+function TimecodeDisplay() {
+ const editor = useEditor();
+ const totalDuration = useEditor((e) => e.timeline.getTotalDuration());
+ const fps = useEditor((e) => e.project.getActive().settings.fps);
+ const [currentTime, setCurrentTime] = useState(() =>
+ editor.playback.getCurrentTime(),
+ );
+
+ useEffect(() => {
+ const handler = (e: Event) =>
+ setCurrentTime((e as CustomEvent<{ time: number }>).detail.time);
+ window.addEventListener("playback-update", handler);
+ window.addEventListener("playback-seek", handler);
+ return () => {
+ window.removeEventListener("playback-update", handler);
+ window.removeEventListener("playback-seek", handler);
+ };
+ }, []);
+
+ return (
+ <div className="flex items-center">
+ <EditableTimecode
+ time={currentTime}
+ duration={totalDuration}
+ format="HH:MM:SS:FF"
+ fps={fps}
+ onTimeChange={({ time }) => editor.playback.seek({ time })}
+ className="text-center"
+ />
+ <span className="text-muted-foreground px-2 font-mono text-xs">/</span>
+ <span className="text-muted-foreground font-mono text-xs">
+ {formatTimeCode({ timeInSeconds: totalDuration, format: "HH:MM:SS:FF", fps })}
+ </span>
+ </div>
+ );
+}
+
+function ZoomSelect() {
+ const { isAtFit, zoomPercent, fitToScreen, setViewportPercent } =
+ usePreviewViewport();
+
+ const displayLabel = isAtFit ? "Fit" : `${zoomPercent}%`;
+
+ const onValueChange = (value: string) => {
+ if (value === "fit") {
+ fitToScreen();
+ } else {
+ setViewportPercent({ percent: Number(value) });
+ }
+ };
+
+ return (
+ <Select
+ value={isAtFit ? "fit" : String(zoomPercent)}
+ onValueChange={onValueChange}
+ >
+ <SelectTrigger className="tabular-nums">{displayLabel}</SelectTrigger>
+ <SelectContent>
+ <SelectItem value="fit">Fit</SelectItem>
+ <SelectSeparator />
+ {PREVIEW_ZOOM_PRESETS.map((preset) => (
+ <SelectItem key={preset} value={String(preset)}>
+ {preset}%
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ );
+}
+
+function PlayPauseButton() {
+ const isPlaying = useEditor((e) => e.playback.getIsPlaying());
+
+ return (
+ <Button
+ variant="text"
+ size="icon"
+ onClick={() => invokeAction("toggle-play")}
+ >
+ <HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
+ </Button>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/timeline/bookmarks.tsx b/apps/web/src/components/editor/panels/timeline/bookmarks.tsx
index b47f9d46..e2dc09f1 100644
--- a/apps/web/src/components/editor/panels/timeline/bookmarks.tsx
+++ b/apps/web/src/components/editor/panels/timeline/bookmarks.tsx
@@ -10,7 +10,7 @@ import {
TIMELINE_BOOKMARK_ROW_HEIGHT,
} from "@/constants/timeline-constants";
import { DEFAULT_FPS } from "@/constants/project-constants";
-import { getSnappedSeekTime } from "@/lib/time";
+import { getSnappedSeekTime } from "opencut-wasm";
import {
ArrowTurnBackwardIcon,
Delete02Icon,