+
{shortcuts
.filter((shortcut) => shortcut.category === category)
.map((shortcut) => (
@@ -139,7 +135,7 @@ export function KeyboardShortcutsHelp() {
isRecording={
shortcut.action === recordingShortcut?.action
}
- onStartRecording={handleStartRecording}
+ onStartRecording={() => handleStartRecording(shortcut)}
/>
))}
@@ -164,9 +160,8 @@ function ShortcutItem({
}: {
shortcut: KeyboardShortcut;
isRecording: boolean;
- onStartRecording: (shortcut: KeyboardShortcut) => void;
+ onStartRecording: (params: { shortcut: KeyboardShortcut }) => void;
}) {
- // Filter out lowercase duplicates for display - if both "j" and "J" exist, only show "J"
const displayKeys = shortcut.keys.filter((key: string) => {
if (
key.includes("Cmd") &&
@@ -195,7 +190,7 @@ function ShortcutItem({
onStartRecording(shortcut)}
+ onStartRecording={() => onStartRecording({ shortcut })}
>
{keyPart}
diff --git a/apps/web/src/components/editor/timeline/bookmarks.tsx b/apps/web/src/components/editor/timeline/bookmarks.tsx
index a21a3db0..55e2de9c 100644
--- a/apps/web/src/components/editor/timeline/bookmarks.tsx
+++ b/apps/web/src/components/editor/timeline/bookmarks.tsx
@@ -1,7 +1,8 @@
import { ScrollArea } from "@/components/ui/scroll-area";
import { useEditor } from "@/hooks/use-editor";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
-import { Bookmark } from "lucide-react";
+import { Bookmark02Icon } from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
interface TimelineBookmarksRowProps {
zoomLevel: number;
@@ -84,10 +85,10 @@ export function TimelineBookmark({
onClick={(event) => handleBookmarkClick({ event })}
>
-
diff --git a/apps/web/src/components/editor/timeline/index.tsx b/apps/web/src/components/editor/timeline/index.tsx
index fa2c891e..58dc7f5e 100644
--- a/apps/web/src/components/editor/timeline/index.tsx
+++ b/apps/web/src/components/editor/timeline/index.tsx
@@ -2,14 +2,13 @@
import { ScrollArea } from "@/components/ui/scroll-area";
import {
- Eye,
- EyeOff,
- VolumeOff,
- Volume2,
- type LucideIcon,
- EyeIcon,
- Trash2,
-} from "lucide-react";
+ Delete02Icon,
+ ViewIcon,
+ ViewOffSlashIcon,
+ VolumeHighIcon,
+ VolumeOffIcon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react";
import {
ContextMenu,
ContextMenuContent,
@@ -133,6 +132,7 @@ export function Timeline() {
selectionBox,
handleMouseDown: handleSelectionMouseDown,
isSelecting,
+ shouldIgnoreClick,
} = useSelectionBox({
containerRef: tracksContainerRef,
onSelectionComplete: (elements) => {
@@ -267,17 +267,12 @@ export function Timeline() {
}}
>
- {/* Debug main track */}
- {/* {isMainTrack(track) && (
-
- )} */}
-
{canTracktHaveAudio(track) && (
editor.timeline.toggleTrackMute({
@@ -291,8 +286,8 @@ export function Timeline() {
editor.timeline.toggleTrackVisibility({
@@ -397,6 +392,7 @@ export function Timeline() {
onElementMouseDown={handleElementMouseDown}
onElementClick={handleElementClick}
onTrackMouseDown={handleSelectionMouseDown}
+ shouldIgnoreClick={shouldIgnoreClick}
/>
@@ -409,7 +405,7 @@ export function Timeline() {
});
}}
>
-
+
{canTracktHaveAudio(track) && track.muted
? "Unmute track"
@@ -424,7 +420,7 @@ export function Timeline() {
});
}}
>
-
+
{canTrackBeHidden(track) && track.hidden
? "Show track"
@@ -440,7 +436,7 @@ export function Timeline() {
}}
variant="destructive"
>
-
+
Delete track
@@ -467,20 +463,22 @@ function TrackToggleIcon({
}: {
isOff: boolean;
icons: {
- on: LucideIcon;
- off: LucideIcon;
+ on: IconSvgElement;
+ off: IconSvgElement;
};
onClick: () => void;
}) {
return (
<>
{isOff ? (
-
) : (
-
diff --git a/apps/web/src/components/editor/timeline/timeline-element.tsx b/apps/web/src/components/editor/timeline/timeline-element.tsx
index 535dde4a..e0e5b374 100644
--- a/apps/web/src/components/editor/timeline/timeline-element.tsx
+++ b/apps/web/src/components/editor/timeline/timeline-element.tsx
@@ -213,10 +213,6 @@ export function TimelineElement({
Duplicate {element.type === "text" ? "text" : "clip"}
)}
-
-
- Move to track (Coming soon)
-
{selectedElements.length === 1 && hasMediaId(element) && (
<>
onElementClick(e, element)}
onMouseDown={(e) => onElementMouseDown(e, element)}
- onContextMenu={(e) => onElementMouseDown(e, element)}
>
- {(hasAudio ? isMuted : canElementBeHidden(element) && element.hidden) && (
+ {(hasAudio
+ ? isMuted
+ : canElementBeHidden(element) && element.hidden) && (
{hasAudio ? (
@@ -381,8 +378,8 @@ function ElementContent({
src={`https://api.iconify.design/${element.iconName}.svg?width=20&height=20`}
alt={element.name}
className="size-5 shrink-0"
- width={20}
- height={20}
+ width={20}
+ height={20}
unoptimized
/>
{element.name}
diff --git a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx b/apps/web/src/components/editor/timeline/timeline-toolbar.tsx
index 1803d47e..cb1a4f40 100644
--- a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx
+++ b/apps/web/src/components/editor/timeline/timeline-toolbar.tsx
@@ -7,21 +7,8 @@ import {
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import {
- Pause,
- Play,
SkipBack,
- Bookmark,
- Magnet,
- Link,
- ZoomOut,
- ZoomIn,
- Copy,
- Trash2,
- Snowflake,
- ArrowLeftToLine,
- ArrowRightToLine,
SplitSquareHorizontal,
- Scissors,
LayersIcon,
} from "lucide-react";
import {
@@ -38,6 +25,28 @@ import { ScenesView } from "../scenes-view";
import { type TAction, invokeAction } from "@/lib/actions";
import { cn } from "@/utils/ui";
import { useTimelineStore } from "@/stores/timeline-store";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import {
+ Bookmark02Icon,
+ Delete02Icon,
+ SnowIcon,
+ ScissorIcon,
+ MagnetIcon,
+ Link04Icon,
+ SearchAddIcon,
+ SearchMinusIcon,
+ PauseIcon,
+ PlayIcon,
+ Copy01Icon,
+ ArrowLeft03Icon,
+ ArrowRight03Icon,
+ LayoutAlignLeftIcon,
+ LayoutAlignRightIcon,
+ AlignLeftIcon,
+ AlignRightIcon,
+ Layers01Icon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
export function TimelineToolbar({
zoomLevel,
@@ -60,18 +69,20 @@ export function TimelineToolbar({
};
return (
-
-
+
+
+
-
+
- setZoomLevel({ zoom })}
- onZoom={handleZoom}
- />
-
+ setZoomLevel({ zoom })}
+ onZoom={handleZoom}
+ />
+
+
);
}
@@ -96,7 +107,13 @@ function ToolbarLeftSection() {
: }
+ icon={
+ isPlaying ? (
+
+ ) : (
+
+ )
+ }
tooltip={isPlaying ? "Pause" : "Play"}
onClick={({ event }) =>
handleAction({ action: "toggle-play", event })
@@ -109,14 +126,14 @@ function ToolbarLeftSection() {
onClick={({ event }) => handleAction({ action: "goto-start", event })}
/>
-
+
-
+
}
+ icon={}
tooltip="Split element"
onClick={({ event }) =>
handleAction({ action: "split-selected", event })
@@ -124,18 +141,18 @@ function ToolbarLeftSection() {
/>
}
- tooltip="Split and keep left"
+ icon={}
+ tooltip="Split left"
onClick={({ event }) =>
- handleAction({ action: "split-selected-left", event })
+ handleAction({ action: "split-selected-right", event })
}
/>
}
- tooltip="Split and keep right"
+ icon={}
+ tooltip="Split right"
onClick={({ event }) =>
- handleAction({ action: "split-selected-right", event })
+ handleAction({ action: "split-selected-left", event })
}
/>
@@ -147,7 +164,7 @@ function ToolbarLeftSection() {
/>
}
+ icon={}
tooltip="Duplicate element"
onClick={({ event }) =>
handleAction({ action: "duplicate-selected", event })
@@ -155,14 +172,14 @@ function ToolbarLeftSection() {
/>
}
+ icon={}
tooltip="Coming soon" /* freeze frame */
disabled={true}
onClick={({ event: _event }) => {}}
/>
}
+ icon={}
tooltip="Delete element"
onClick={({ event }) =>
handleAction({ action: "delete-selected", event })
@@ -174,7 +191,8 @@ function ToolbarLeftSection() {
}
@@ -228,7 +246,7 @@ function SceneSelector() {
{}} type="button">
-
+
@@ -259,7 +277,10 @@ function ToolbarRightSection({
+
}
tooltip="Auto snapping"
onClick={() => toggleSnapping()}
@@ -267,7 +288,13 @@ function ToolbarRightSection({
+
}
tooltip="Ripple editing"
onClick={() => toggleRippleEditing()}
@@ -283,7 +310,7 @@ function ToolbarRightSection({
type="button"
onClick={() => onZoom({ direction: "out" })}
>
-
+
onZoom({ direction: "in" })}
>
-
+
diff --git a/apps/web/src/components/editor/timeline/timeline-track.tsx b/apps/web/src/components/editor/timeline/timeline-track.tsx
index 6a8df068..29d58315 100644
--- a/apps/web/src/components/editor/timeline/timeline-track.tsx
+++ b/apps/web/src/components/editor/timeline/timeline-track.tsx
@@ -31,6 +31,7 @@ interface TimelineTrackContentProps {
track: TimelineTrack;
}) => void;
onTrackMouseDown?: (event: React.MouseEvent) => void;
+ shouldIgnoreClick?: () => boolean;
}
export function TimelineTrackContent({
@@ -45,6 +46,7 @@ export function TimelineTrackContent({
onElementMouseDown,
onElementClick,
onTrackMouseDown,
+ shouldIgnoreClick,
}: TimelineTrackContentProps) {
const editor = useEditor();
const { isElementSelected, clearElementSelection } = useElementSelection();
@@ -66,7 +68,10 @@ export function TimelineTrackContent({
return (
-
+
31k+
diff --git a/apps/web/src/components/theme-toggle.tsx b/apps/web/src/components/theme-toggle.tsx
index c727c4db..eded49a4 100644
--- a/apps/web/src/components/theme-toggle.tsx
+++ b/apps/web/src/components/theme-toggle.tsx
@@ -1,9 +1,10 @@
"use client";
import { Button } from "./ui/button";
-import { Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { cn } from "@/utils/ui";
+import { Sun03Icon } from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
interface ThemeToggleProps {
className?: string;
@@ -28,7 +29,7 @@ export function ThemeToggle({
onToggle?.(e);
}}
>
-
+
{theme === "dark" ? "Light" : "Dark"}
);
diff --git a/apps/web/src/components/ui/color-picker.tsx b/apps/web/src/components/ui/color-picker.tsx
index 74341386..ff543d0b 100644
--- a/apps/web/src/components/ui/color-picker.tsx
+++ b/apps/web/src/components/ui/color-picker.tsx
@@ -231,7 +231,7 @@ const ColorPicker = forwardRef
(
};
return (
-
+
) {
+ return (
+
+ )
+}
+
+export { Spinner }
diff --git a/apps/web/src/constants/timeline-constants.tsx b/apps/web/src/constants/timeline-constants.tsx
index 2a153cb2..d7e72c45 100644
--- a/apps/web/src/constants/timeline-constants.tsx
+++ b/apps/web/src/constants/timeline-constants.tsx
@@ -1,5 +1,11 @@
import type { TrackType } from "@/types/timeline";
-import { Video, TypeIcon, Music, Sticker } from "lucide-react";
+import {
+ Happy01Icon,
+ MusicNote03Icon,
+ TextIcon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { OcVideoIcon } from "@opencut/ui/icons";
export const TRACK_COLORS: Record
= {
video: {
@@ -37,8 +43,23 @@ export const TIMELINE_CONSTANTS = {
} as const;
export const TRACK_ICONS: Record = {
- video: ,
- text: ,
- audio: ,
- sticker: ,
+ video: ,
+ text: (
+
+ ),
+ audio: (
+
+ ),
+ sticker: (
+
+ ),
} as const;
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 84b14f43..5a97c533 100644
--- a/apps/web/src/hooks/timeline/element/use-element-interaction.ts
+++ b/apps/web/src/hooks/timeline/element/use-element-interaction.ts
@@ -510,13 +510,9 @@ export function useElementInteraction({
element: TimelineElement;
track: TimelineTrack;
}) => {
- event.stopPropagation();
- mouseDownLocationRef.current = { x: event.clientX, y: event.clientY };
-
const isRightClick = event.button === 2;
- const isMultiSelect = event.metaKey || event.ctrlKey || event.shiftKey;
- // right-click
+ // right-click: don't stop propagation so ContextMenu can open
if (isRightClick) {
const alreadySelected = isElementSelected({
trackId: track.id,
@@ -532,6 +528,12 @@ export function useElementInteraction({
return;
}
+ // left-click: stop propagation for drag operations
+ event.stopPropagation();
+ mouseDownLocationRef.current = { x: event.clientX, y: event.clientY };
+
+ const isMultiSelect = event.metaKey || event.ctrlKey || event.shiftKey;
+
// multi-select: toggle selection
if (isMultiSelect) {
handleSelectionClick({
diff --git a/apps/web/src/hooks/timeline/use-selection-box.ts b/apps/web/src/hooks/timeline/use-selection-box.ts
index 012af263..702abad9 100644
--- a/apps/web/src/hooks/timeline/use-selection-box.ts
+++ b/apps/web/src/hooks/timeline/use-selection-box.ts
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import { getCumulativeHeightBefore, getTrackHeight } from "@/lib/timeline";
import { useEditor } from "../use-editor";
@@ -98,6 +98,7 @@ export function useSelectionBox({
const [selectionBox, setSelectionBox] = useState(
null,
);
+ const justFinishedSelectingRef = useRef(false);
const handleMouseDown = useCallback(
({ clientX, clientY }: React.MouseEvent) => {
@@ -195,6 +196,12 @@ export function useSelectionBox({
};
const handleMouseUp = () => {
+ if (selectionBox?.isActive) {
+ justFinishedSelectingRef.current = true;
+ requestAnimationFrame(() => {
+ justFinishedSelectingRef.current = false;
+ });
+ }
setSelectionBox(null);
};
@@ -223,9 +230,14 @@ export function useSelectionBox({
};
}, [selectionBox, containerRef]);
+ const shouldIgnoreClick = useCallback(() => {
+ return justFinishedSelectingRef.current;
+ }, []);
+
return {
selectionBox,
handleMouseDown,
isSelecting: selectionBox?.isActive || false,
+ shouldIgnoreClick,
};
}
diff --git a/apps/web/src/stores/assets-panel-store.ts b/apps/web/src/stores/assets-panel-store.tsx
similarity index 55%
rename from apps/web/src/stores/assets-panel-store.ts
rename to apps/web/src/stores/assets-panel-store.tsx
index 965d3590..21ceb6a3 100644
--- a/apps/web/src/stores/assets-panel-store.ts
+++ b/apps/web/src/stores/assets-panel-store.tsx
@@ -1,17 +1,18 @@
-import {
- CaptionsIcon,
- ArrowLeftRightIcon,
- SparklesIcon,
- StickerIcon,
- MusicIcon,
- VideoIcon,
- BlendIcon,
- SlidersHorizontalIcon,
- type LucideIcon,
- TypeIcon,
- SettingsIcon,
-} from "lucide-react";
+import type { ElementType } from "react";
import { create } from "zustand";
+import {
+ ArrowRightDoubleIcon,
+ ClosedCaptionIcon,
+ Folder03Icon,
+ Happy01Icon,
+ HeadphonesIcon,
+ MagicWand05Icon,
+ TextIcon,
+ Settings01Icon,
+ SlidersHorizontalIcon,
+ ColorsIcon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react";
export const TAB_KEYS = [
"media",
@@ -28,48 +29,57 @@ export const TAB_KEYS = [
export type Tab = (typeof TAB_KEYS)[number];
+const createHugeiconsIcon =
+ ({ icon }: { icon: IconSvgElement }) =>
+ ({ className }: { className?: string }) => (
+
+ );
+
export const tabs = {
media: {
- icon: VideoIcon,
+ icon: createHugeiconsIcon({ icon: Folder03Icon }),
label: "Media",
},
sounds: {
- icon: MusicIcon,
+ icon: createHugeiconsIcon({ icon: HeadphonesIcon }),
label: "Sounds",
},
text: {
- icon: TypeIcon,
+ icon: createHugeiconsIcon({ icon: TextIcon }),
label: "Text",
},
stickers: {
- icon: StickerIcon,
+ icon: createHugeiconsIcon({ icon: Happy01Icon }),
label: "Stickers",
},
effects: {
- icon: SparklesIcon,
+ icon: createHugeiconsIcon({ icon: MagicWand05Icon }),
label: "Effects",
},
transitions: {
- icon: ArrowLeftRightIcon,
+ icon: createHugeiconsIcon({ icon: ArrowRightDoubleIcon }),
label: "Transitions",
},
captions: {
- icon: CaptionsIcon,
+ icon: createHugeiconsIcon({ icon: ClosedCaptionIcon }),
label: "Captions",
},
filters: {
- icon: BlendIcon,
+ icon: createHugeiconsIcon({ icon: ColorsIcon }),
label: "Filters",
},
adjustment: {
- icon: SlidersHorizontalIcon,
+ icon: createHugeiconsIcon({ icon: SlidersHorizontalIcon }),
label: "Adjustment",
},
settings: {
- icon: SettingsIcon,
+ icon: createHugeiconsIcon({ icon: Settings01Icon }),
label: "Settings",
},
-} satisfies Record;
+} satisfies Record<
+ Tab,
+ { icon: ElementType<{ className?: string }>; label: string }
+>;
type MediaViewMode = "grid" | "list";
diff --git a/bun.lock b/bun.lock
index e7127c2a..b0281e3d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -21,6 +21,8 @@
"@ffmpeg/util": "^0.12.2",
"@hello-pangea/dnd": "^18.0.1",
"@hookform/resolvers": "^3.9.1",
+ "@hugeicons/core-free-icons": "^3.1.1",
+ "@hugeicons/react": "^1.1.4",
"@huggingface/transformers": "^3.8.1",
"@opencut/env": "workspace:*",
"@opencut/ui": "workspace:*",
@@ -218,6 +220,10 @@
"@hookform/resolvers": ["@hookform/resolvers@3.10.0", "", { "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag=="],
+ "@hugeicons/core-free-icons": ["@hugeicons/core-free-icons@3.1.1", "", {}, "sha512-UpS2lUQFi5sKyJSWwM6rO+BnPLvVz1gsyCpPHeZyVuZqi89YH8ksliza4cwaODqKOZyeXmG8juo1ty4QtQofkg=="],
+
+ "@hugeicons/react": ["@hugeicons/react@1.1.4", "", { "peerDependencies": { "react": ">=16.0.0" } }, "sha512-gsc3eZyd2fGqRUThW9+lfjxxsOkz6KNVmRXRgJjP32GL0OnnLJnl3hytKt47CBbiQj2xE2kCw+rnP3UQCThcKw=="],
+
"@huggingface/jinja": ["@huggingface/jinja@0.5.3", "", {}, "sha512-asqfZ4GQS0hD876Uw4qiUb7Tr/V5Q+JZuo2L+BtdrD4U40QU58nIRq3ZSgAzJgT874VLjhGVacaYfrdpXtEvtA=="],
"@huggingface/transformers": ["@huggingface/transformers@3.8.1", "", { "dependencies": { "@huggingface/jinja": "^0.5.3", "onnxruntime-node": "1.21.0", "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", "sharp": "^0.34.1" } }, "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA=="],
diff --git a/packages/ui/src/icons/brand.tsx b/packages/ui/src/icons/brand.tsx
index 279218f4..26aabf86 100644
--- a/packages/ui/src/icons/brand.tsx
+++ b/packages/ui/src/icons/brand.tsx
@@ -1,45 +1,3 @@
-export function OcGoogleIcon({ className }: { className?: string }) {
- return (
-
- );
-}
-
-export function OcGithubIcon({ className }: { className?: string }) {
- return (
-
- );
-}
-
export function OcVercelIcon({ className }: { className?: string }) {
return (
- );
-}
-
-export function OcSocialsIcon({
- className = "",
- size = 32,
-}: {
- className?: string;
- size?: number;
-}) {
- return (
-
- );
-}
-
-export function OcTransitionUpIcon({
- className = "",
- size = 16,
-}: {
- className?: string;
- size?: number;
-}) {
- return (
-
- );
-}
diff --git a/packages/ui/src/icons/index.tsx b/packages/ui/src/icons/index.tsx
index f841cb68..2203b633 100644
--- a/packages/ui/src/icons/index.tsx
+++ b/packages/ui/src/icons/index.tsx
@@ -1,3 +1,2 @@
export * from "./brand";
-export * from "./editor";
export * from "./ui";
diff --git a/packages/ui/src/icons/ui.tsx b/packages/ui/src/icons/ui.tsx
index 151f2c27..3af0eaf6 100644
--- a/packages/ui/src/icons/ui.tsx
+++ b/packages/ui/src/icons/ui.tsx
@@ -1,108 +1,32 @@
-export function OcMenuIcon({
+export function OcVideoIcon({
className = "",
- size = 16,
+ size = 32,
}: {
className?: string;
size?: number;
}) {
return (
- );
-}
-
-export function OcPencilIcon({
- className = "",
- size = 24,
-}: {
- className?: string;
- size?: number;
-}) {
- return (
-
- );
-}
-
-export function OcLeftArrowIcon({
- className = "",
- size = 24,
-}: {
- className?: string;
- size?: number;
-}) {
- return (
-
- );
-}
-
-export function OcTrashIcon({
- className = "",
- size = 24,
-}: {
- className?: string;
- size?: number;
-}) {
- return (
-
);
}