mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
refactor all timeline components
This commit is contained in:
@@ -59,7 +59,7 @@ export function useTimelineContentClick({
|
|||||||
|
|
||||||
// Determine if we're clicking in ruler or tracks area
|
// Determine if we're clicking in ruler or tracks area
|
||||||
const isRulerClick = (e.target as HTMLElement).closest(
|
const isRulerClick = (e.target as HTMLElement).closest(
|
||||||
"[data-ruler-area]",
|
"[data-ruler-area]"
|
||||||
);
|
);
|
||||||
|
|
||||||
let mouseX: number;
|
let mouseX: number;
|
||||||
@@ -68,7 +68,7 @@ export function useTimelineContentClick({
|
|||||||
if (isRulerClick) {
|
if (isRulerClick) {
|
||||||
// Calculate based on ruler position
|
// Calculate based on ruler position
|
||||||
const rulerContent = rulerScrollRef.current?.querySelector(
|
const rulerContent = rulerScrollRef.current?.querySelector(
|
||||||
"[data-radix-scroll-area-viewport]",
|
"[data-radix-scroll-area-viewport]"
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
if (!rulerContent) return;
|
if (!rulerContent) return;
|
||||||
const rect = rulerContent.getBoundingClientRect();
|
const rect = rulerContent.getBoundingClientRect();
|
||||||
@@ -77,7 +77,7 @@ export function useTimelineContentClick({
|
|||||||
} else {
|
} else {
|
||||||
// Calculate based on tracks content position
|
// Calculate based on tracks content position
|
||||||
const tracksContent = tracksScrollRef.current?.querySelector(
|
const tracksContent = tracksScrollRef.current?.querySelector(
|
||||||
"[data-radix-scroll-area-viewport]",
|
"[data-radix-scroll-area-viewport]"
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
if (!tracksContent) return;
|
if (!tracksContent) return;
|
||||||
const rect = tracksContent.getBoundingClientRect();
|
const rect = tracksContent.getBoundingClientRect();
|
||||||
@@ -90,8 +90,8 @@ export function useTimelineContentClick({
|
|||||||
Math.min(
|
Math.min(
|
||||||
duration,
|
duration,
|
||||||
(mouseX + scrollLeft) /
|
(mouseX + scrollLeft) /
|
||||||
(TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel),
|
(TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel)
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use frame snapping for timeline clicking
|
// Use frame snapping for timeline clicking
|
||||||
@@ -111,7 +111,7 @@ export function useTimelineContentClick({
|
|||||||
justFinishedSelecting,
|
justFinishedSelecting,
|
||||||
playheadRef,
|
playheadRef,
|
||||||
activeProject?.fps,
|
activeProject?.fps,
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { handleTimelineContentClick };
|
return { handleTimelineContentClick };
|
||||||
|
|||||||
@@ -57,14 +57,13 @@ export function TimelineContent({
|
|||||||
}: TimelineContentProps) {
|
}: TimelineContentProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative min-w-max min-h-max"
|
className="relative min-w-max min-h-max h-full"
|
||||||
style={{ minWidth: `${dynamicTimelineWidth}px` }}
|
style={{ minWidth: `${dynamicTimelineWidth}px` }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`h-full grid grid-cols-[192px_1fr] ${tracks.length > 0 ? `grid-rows-[20px_repeat(${tracks.length},minmax(0,max-content))]` : "grid-rows-[20px_1fr]"}`}
|
className={`h-full grid grid-cols-[192px_1fr] ${tracks.length > 0 ? `grid-rows-[20px_repeat(${tracks.length},minmax(0,max-content))]` : "grid-rows-[20px_1fr]"}`}
|
||||||
>
|
>
|
||||||
{/* Top-Left Corner (Empty space above track labels) */}
|
|
||||||
<div className="sticky top-0 left-0 border-inset bg-card"></div>
|
|
||||||
|
|
||||||
{/* Top Row (Sticky Ruler Header) */}
|
{/* Top Row (Sticky Ruler Header) */}
|
||||||
<TimelineRuler
|
<TimelineRuler
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useTimelineStore } from "@/stores/timeline-store";
|
import { useTimelineStore } from "@/stores/timeline-store";
|
||||||
import { useMediaStore } from "@/stores/media-store";
|
import { useMediaStore } from "@/stores/media-store";
|
||||||
@@ -66,7 +66,7 @@ export function useTimelineDragHandlers({
|
|||||||
|
|
||||||
// Ignore timeline element drags - they're handled by track-specific handlers
|
// Ignore timeline element drags - they're handled by track-specific handlers
|
||||||
const hasTimelineElement = e.dataTransfer.types.includes(
|
const hasTimelineElement = e.dataTransfer.types.includes(
|
||||||
"application/x-timeline-element",
|
"application/x-timeline-element"
|
||||||
);
|
);
|
||||||
if (hasTimelineElement) {
|
if (hasTimelineElement) {
|
||||||
return;
|
return;
|
||||||
@@ -83,7 +83,7 @@ export function useTimelineDragHandlers({
|
|||||||
} else {
|
} else {
|
||||||
// Handle media items
|
// Handle media items
|
||||||
const mediaItem = mediaItems.find(
|
const mediaItem = mediaItems.find(
|
||||||
(item: any) => item.id === dragData.id,
|
(item: any) => item.id === dragData.id
|
||||||
);
|
);
|
||||||
if (!mediaItem) {
|
if (!mediaItem) {
|
||||||
toast.error("Media item not found");
|
toast.error("Media item not found");
|
||||||
@@ -108,15 +108,14 @@ export function useTimelineDragHandlers({
|
|||||||
try {
|
try {
|
||||||
const processedItems = await processMediaFiles(
|
const processedItems = await processMediaFiles(
|
||||||
e.dataTransfer.files,
|
e.dataTransfer.files,
|
||||||
(p) => setProgress(p),
|
(p) => setProgress(p)
|
||||||
);
|
);
|
||||||
for (const processedItem of processedItems) {
|
for (const processedItem of processedItems) {
|
||||||
await addMediaItem(activeProject.id, processedItem);
|
await addMediaItem(activeProject.id, processedItem);
|
||||||
const currentMediaItems = useMediaStore.getState().mediaItems;
|
const currentMediaItems = useMediaStore.getState().mediaItems;
|
||||||
const addedItem = currentMediaItems.find(
|
const addedItem = currentMediaItems.find(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.name === processedItem.name &&
|
item.name === processedItem.name && item.url === processedItem.url
|
||||||
item.url === processedItem.url,
|
|
||||||
);
|
);
|
||||||
if (addedItem) {
|
if (addedItem) {
|
||||||
useTimelineStore.getState().addMediaToNewTrack(addedItem);
|
useTimelineStore.getState().addMediaToNewTrack(addedItem);
|
||||||
|
|||||||
+6
-125
@@ -1,48 +1,25 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { Scissors, Trash2, Copy, RefreshCw } from "lucide-react";
|
||||||
import { Button } from "../ui/button";
|
|
||||||
import {
|
|
||||||
MoreVertical,
|
|
||||||
Scissors,
|
|
||||||
Trash2,
|
|
||||||
SplitSquareHorizontal,
|
|
||||||
Music,
|
|
||||||
ChevronRight,
|
|
||||||
ChevronLeft,
|
|
||||||
Type,
|
|
||||||
Copy,
|
|
||||||
RefreshCw,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useMediaStore } from "@/stores/media-store";
|
import { useMediaStore } from "@/stores/media-store";
|
||||||
import { useTimelineStore } from "@/stores/timeline-store";
|
import { useTimelineStore } from "@/stores/timeline-store";
|
||||||
import { usePlaybackStore } from "@/stores/playback-store";
|
import { usePlaybackStore } from "@/stores/playback-store";
|
||||||
import AudioWaveform from "./audio-waveform";
|
import AudioWaveform from "@/components/editor/audio-waveform";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { TimelineElementProps, TrackType } from "@/types/timeline";
|
import { TimelineElementProps } from "@/types/timeline";
|
||||||
import { useTimelineElementResize } from "@/hooks/use-timeline-element-resize";
|
import { useTimelineElementResize } from "@/hooks/use-timeline-element-resize";
|
||||||
import {
|
import {
|
||||||
getTrackElementClasses,
|
getTrackElementClasses,
|
||||||
TIMELINE_CONSTANTS,
|
TIMELINE_CONSTANTS,
|
||||||
getTrackHeight,
|
getTrackHeight,
|
||||||
} from "@/constants/timeline-constants";
|
} from "@/constants/timeline-constants";
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
} from "../ui/dropdown-menu";
|
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuSeparator,
|
ContextMenuSeparator,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "../ui/context-menu";
|
} from "@/components/ui/context-menu";
|
||||||
|
|
||||||
export function TimelineElement({
|
export function TimelineElement({
|
||||||
element,
|
element,
|
||||||
@@ -59,23 +36,13 @@ export function TimelineElement({
|
|||||||
removeElementFromTrack,
|
removeElementFromTrack,
|
||||||
dragState,
|
dragState,
|
||||||
splitElement,
|
splitElement,
|
||||||
splitAndKeepLeft,
|
|
||||||
splitAndKeepRight,
|
|
||||||
separateAudio,
|
|
||||||
addElementToTrack,
|
addElementToTrack,
|
||||||
replaceElementMedia,
|
replaceElementMedia,
|
||||||
} = useTimelineStore();
|
} = useTimelineStore();
|
||||||
const { currentTime } = usePlaybackStore();
|
const { currentTime } = usePlaybackStore();
|
||||||
|
|
||||||
const [elementMenuOpen, setElementMenuOpen] = useState(false);
|
const { resizing, handleResizeStart, handleResizeMove, handleResizeEnd } =
|
||||||
|
useTimelineElementResize({
|
||||||
const {
|
|
||||||
resizing,
|
|
||||||
isResizing,
|
|
||||||
handleResizeStart,
|
|
||||||
handleResizeMove,
|
|
||||||
handleResizeEnd,
|
|
||||||
} = useTimelineElementResize({
|
|
||||||
element,
|
element,
|
||||||
track,
|
track,
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
@@ -98,92 +65,6 @@ export function TimelineElement({
|
|||||||
: element.startTime;
|
: element.startTime;
|
||||||
const elementLeft = elementStartTime * 50 * zoomLevel;
|
const elementLeft = elementStartTime * 50 * zoomLevel;
|
||||||
|
|
||||||
const handleDeleteElement = () => {
|
|
||||||
removeElementFromTrack(track.id, element.id);
|
|
||||||
setElementMenuOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSplitElement = () => {
|
|
||||||
const effectiveStart = element.startTime;
|
|
||||||
const effectiveEnd =
|
|
||||||
element.startTime +
|
|
||||||
(element.duration - element.trimStart - element.trimEnd);
|
|
||||||
|
|
||||||
if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
|
|
||||||
toast.error("Playhead must be within element to split");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const secondElementId = splitElement(track.id, element.id, currentTime);
|
|
||||||
if (!secondElementId) {
|
|
||||||
toast.error("Failed to split element");
|
|
||||||
}
|
|
||||||
setElementMenuOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSplitAndKeepLeft = () => {
|
|
||||||
const effectiveStart = element.startTime;
|
|
||||||
const effectiveEnd =
|
|
||||||
element.startTime +
|
|
||||||
(element.duration - element.trimStart - element.trimEnd);
|
|
||||||
|
|
||||||
if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
|
|
||||||
toast.error("Playhead must be within element");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
splitAndKeepLeft(track.id, element.id, currentTime);
|
|
||||||
setElementMenuOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSplitAndKeepRight = () => {
|
|
||||||
const effectiveStart = element.startTime;
|
|
||||||
const effectiveEnd =
|
|
||||||
element.startTime +
|
|
||||||
(element.duration - element.trimStart - element.trimEnd);
|
|
||||||
|
|
||||||
if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
|
|
||||||
toast.error("Playhead must be within element");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
splitAndKeepRight(track.id, element.id, currentTime);
|
|
||||||
setElementMenuOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSeparateAudio = () => {
|
|
||||||
if (element.type !== "media") {
|
|
||||||
toast.error("Audio separation only available for media elements");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mediaItem = mediaItems.find((item) => item.id === element.mediaId);
|
|
||||||
if (!mediaItem || mediaItem.type !== "video") {
|
|
||||||
toast.error("Audio separation only available for video elements");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioElementId = separateAudio(track.id, element.id);
|
|
||||||
if (!audioElementId) {
|
|
||||||
toast.error("Failed to separate audio");
|
|
||||||
}
|
|
||||||
setElementMenuOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const canSplitAtPlayhead = () => {
|
|
||||||
const effectiveStart = element.startTime;
|
|
||||||
const effectiveEnd =
|
|
||||||
element.startTime +
|
|
||||||
(element.duration - element.trimStart - element.trimEnd);
|
|
||||||
return currentTime > effectiveStart && currentTime < effectiveEnd;
|
|
||||||
};
|
|
||||||
|
|
||||||
const canSeparateAudio = () => {
|
|
||||||
if (element.type !== "media") return false;
|
|
||||||
const mediaItem = mediaItems.find((item) => item.id === element.mediaId);
|
|
||||||
return mediaItem?.type === "video" && track.type === "media";
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleElementSplitContext = () => {
|
const handleElementSplitContext = () => {
|
||||||
const effectiveStart = element.startTime;
|
const effectiveStart = element.startTime;
|
||||||
const effectiveEnd =
|
const effectiveEnd =
|
||||||
@@ -2,10 +2,7 @@
|
|||||||
|
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { TimelineTrack } from "@/types/timeline";
|
import { TimelineTrack } from "@/types/timeline";
|
||||||
import {
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
TIMELINE_CONSTANTS,
|
|
||||||
getTotalTracksHeight,
|
|
||||||
} from "@/constants/timeline-constants";
|
|
||||||
import { useTimelinePlayhead } from "@/hooks/use-timeline-playhead";
|
import { useTimelinePlayhead } from "@/hooks/use-timeline-playhead";
|
||||||
|
|
||||||
interface TimelinePlayheadProps {
|
interface TimelinePlayheadProps {
|
||||||
@@ -50,10 +47,6 @@ export function TimelinePlayhead({
|
|||||||
playheadRef,
|
playheadRef,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use timeline container height minus a few pixels for breathing room
|
|
||||||
const timelineContainerHeight = timelineRef.current?.offsetHeight || 400;
|
|
||||||
const totalHeight = timelineContainerHeight - 8; // 8px padding from edges
|
|
||||||
|
|
||||||
// Get dynamic track labels width, fallback to 192px (ml-48) if no tracks or no ref
|
// Get dynamic track labels width, fallback to 192px (ml-48) if no tracks or no ref
|
||||||
const trackLabelsWidth = 192; // Fixed width from grid layout
|
const trackLabelsWidth = 192; // Fixed width from grid layout
|
||||||
const leftPosition =
|
const leftPosition =
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export function TimelineRuler({
|
|||||||
>
|
>
|
||||||
{formatTime(time, interval)}
|
{formatTime(time, interval)}
|
||||||
</span>
|
</span>
|
||||||
</div>,
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ export function TimelineRuler({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="sticky top-0 bg-card border-b border-muted/30 z-[99]"
|
className="sticky top-0 border-b border-muted/30 z-[99] w-24"
|
||||||
onMouseDown={handleSelectionMouseDown}
|
onMouseDown={handleSelectionMouseDown}
|
||||||
onClick={handleTimelineContentClick}
|
onClick={handleTimelineContentClick}
|
||||||
data-ruler-area
|
data-ruler-area
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ export function useTimelineScrollSync({
|
|||||||
// --- Horizontal scroll synchronization between ruler and tracks ---
|
// --- Horizontal scroll synchronization between ruler and tracks ---
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const rulerViewport = rulerScrollRef.current?.querySelector(
|
const rulerViewport = rulerScrollRef.current?.querySelector(
|
||||||
"[data-radix-scroll-area-viewport]",
|
"[data-radix-scroll-area-viewport]"
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
const tracksViewport = tracksScrollRef.current?.querySelector(
|
const tracksViewport = tracksScrollRef.current?.querySelector(
|
||||||
"[data-radix-scroll-area-viewport]",
|
"[data-radix-scroll-area-viewport]"
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
|
|
||||||
if (!rulerViewport || !tracksViewport) return;
|
if (!rulerViewport || !tracksViewport) return;
|
||||||
|
|||||||
+1
-3
@@ -7,8 +7,6 @@ import { toast } from "sonner";
|
|||||||
import { TimelineElement } from "./timeline-element";
|
import { TimelineElement } from "./timeline-element";
|
||||||
import {
|
import {
|
||||||
TimelineTrack,
|
TimelineTrack,
|
||||||
sortTracksByOrder,
|
|
||||||
ensureMainTrack,
|
|
||||||
getMainTrack,
|
getMainTrack,
|
||||||
canElementGoOnTrack,
|
canElementGoOnTrack,
|
||||||
} from "@/types/timeline";
|
} from "@/types/timeline";
|
||||||
@@ -54,7 +52,7 @@ export function TimelineTrackContent({
|
|||||||
const { currentTime } = usePlaybackStore();
|
const { currentTime } = usePlaybackStore();
|
||||||
|
|
||||||
// Initialize snapping hook
|
// Initialize snapping hook
|
||||||
const { snapElementPosition, snapElementEdge } = useTimelineSnapping({
|
const { snapElementEdge } = useTimelineSnapping({
|
||||||
snapThreshold: 10,
|
snapThreshold: 10,
|
||||||
enableElementSnapping: snappingEnabled,
|
enableElementSnapping: snappingEnabled,
|
||||||
enablePlayheadSnapping: snappingEnabled,
|
enablePlayheadSnapping: snappingEnabled,
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Fragment } from "react";
|
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
@@ -7,7 +6,7 @@ import {
|
|||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "../../ui/context-menu";
|
} from "../../ui/context-menu";
|
||||||
import { useTimelineStore } from "@/stores/timeline-store";
|
import { useTimelineStore } from "@/stores/timeline-store";
|
||||||
import { TimelineTrackContent } from "../timeline-track";
|
import { TimelineTrackContent } from "./timeline-track";
|
||||||
import { TrackIcon } from "./track-icon";
|
import { TrackIcon } from "./track-icon";
|
||||||
import { getTrackHeight } from "@/constants/timeline-constants";
|
import { getTrackHeight } from "@/constants/timeline-constants";
|
||||||
import type { TimelineTrack } from "@/types/timeline";
|
import type { TimelineTrack } from "@/types/timeline";
|
||||||
@@ -26,16 +25,17 @@ export function TimelineTracksArea({
|
|||||||
handleSnapPointChange,
|
handleSnapPointChange,
|
||||||
clearSelectedElements,
|
clearSelectedElements,
|
||||||
}: TimelineTracksAreaProps) {
|
}: TimelineTracksAreaProps) {
|
||||||
const { addTrack, toggleTrackMute } = useTimelineStore();
|
const { toggleTrackMute } = useTimelineStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Track Rows */}
|
<div className="h-full flex flex-col absolute left-0 top-0 w-24 bg-panel border-r border-black z-[100]">
|
||||||
|
<div className="sticky left-0 h-5 border-inset z-[100]"></div>
|
||||||
|
|
||||||
{tracks.map((track, index) => (
|
{tracks.map((track, index) => (
|
||||||
<Fragment key={track.id}>
|
|
||||||
{/* Left Column (Sticky Track Labels) */}
|
|
||||||
<div
|
<div
|
||||||
className="sticky left-0 flex items-center border-b border-panel border-inset group bg-card z-[100]"
|
key={track.id}
|
||||||
|
className="sticky left-0 flex items-center border-b border-panel border-inset group z-[100]"
|
||||||
style={{ height: `${getTrackHeight(track.type)}px` }}
|
style={{ height: `${getTrackHeight(track.type)}px` }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 px-2">
|
<div className="flex items-center gap-2 px-2">
|
||||||
@@ -45,14 +45,17 @@ export function TimelineTracksArea({
|
|||||||
<span className="text-xs text-red-500 font-semibold">Muted</span>
|
<span className="text-xs text-red-500 font-semibold">Muted</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Scrollable Track Content */}
|
<div className="h-full flex flex-col pt-5 ml-24">
|
||||||
<ContextMenu>
|
{tracks.map((track, index) => (
|
||||||
|
<ContextMenu key={track.id}>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<div
|
<div
|
||||||
className=" h-full"
|
className="h-full"
|
||||||
|
style={{ height: `${getTrackHeight(track.type)}px` }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
// If clicking empty area (not on a element), deselect all elements
|
|
||||||
if (!(e.target as HTMLElement).closest(".timeline-element")) {
|
if (!(e.target as HTMLElement).closest(".timeline-element")) {
|
||||||
clearSelectedElements();
|
clearSelectedElements();
|
||||||
}
|
}
|
||||||
@@ -72,17 +75,9 @@ export function TimelineTracksArea({
|
|||||||
<ContextMenuItem>Track settings (soon)</ContextMenuItem>
|
<ContextMenuItem>Track settings (soon)</ContextMenuItem>
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</Fragment>
|
|
||||||
))}
|
))}
|
||||||
{/* Add Track Button - spans full width */}
|
|
||||||
<div
|
<div style={{ height: `${getTrackHeight("media")}px` }}></div>
|
||||||
onClick={() => addTrack("media")}
|
|
||||||
className="col-span-1 sticky left-0 w-full flex items-center border-b border-muted bg-card hover:bg-card/50 transition-colors cursor-pointer z-[100]"
|
|
||||||
style={{ height: `${getTrackHeight("media")}px` }}
|
|
||||||
>
|
|
||||||
<div className="w-full flex justify-center items-center">
|
|
||||||
<Plus className="w-4 h-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user