"use client"; import { useState } from "react"; import { Button } from "../ui/button"; import { MoreVertical, Scissors, Trash2, SplitSquareHorizontal, Music, ChevronRight, ChevronLeft, Type, Copy, } from "lucide-react"; import { useMediaStore } from "@/stores/media-store"; import { useTimelineStore } from "@/stores/timeline-store"; import { usePlaybackStore } from "@/stores/playback-store"; import AudioWaveform from "./audio-waveform"; import { toast } from "sonner"; import { TimelineElementProps, TrackType } from "@/types/timeline"; import { useTimelineElementResize } from "@/hooks/use-timeline-element-resize"; import { getTrackElementClasses, TIMELINE_CONSTANTS, } from "@/lib/timeline-constants"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, } from "../ui/dropdown-menu"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, } from "../ui/context-menu"; export function TimelineElement({ element, track, zoomLevel, isSelected, onElementMouseDown, onElementClick, }: TimelineElementProps) { const { mediaItems } = useMediaStore(); const { updateElementTrim, updateElementDuration, removeElementFromTrack, dragState, splitElement, splitAndKeepLeft, splitAndKeepRight, separateAudio, addElementToTrack, } = useTimelineStore(); const { currentTime } = usePlaybackStore(); const [elementMenuOpen, setElementMenuOpen] = useState(false); const { resizing, isResizing, handleResizeStart, handleResizeMove, handleResizeEnd, } = useTimelineElementResize({ element, track, zoomLevel, onUpdateTrim: updateElementTrim, onUpdateDuration: updateElementDuration, }); const effectiveDuration = element.duration - element.trimStart - element.trimEnd; const elementWidth = Math.max( TIMELINE_CONSTANTS.ELEMENT_MIN_WIDTH, effectiveDuration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel ); // Use real-time position during drag, otherwise use stored position const isBeingDragged = dragState.elementId === element.id; const elementStartTime = isBeingDragged && dragState.isDragging ? dragState.currentTime : element.startTime; 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 effectiveStart = element.startTime; const effectiveEnd = element.startTime + (element.duration - element.trimStart - element.trimEnd); if (currentTime > effectiveStart && currentTime < effectiveEnd) { const secondElementId = splitElement(track.id, element.id, currentTime); if (!secondElementId) { toast.error("Failed to split element"); } } else { toast.error("Playhead must be within element to split"); } }; const handleElementDuplicateContext = () => { const { id, ...elementWithoutId } = element; addElementToTrack(track.id, { ...elementWithoutId, name: element.name + " (copy)", startTime: element.startTime + (element.duration - element.trimStart - element.trimEnd) + 0.1, }); }; const handleElementDeleteContext = () => { removeElementFromTrack(track.id, element.id); }; const renderElementContent = () => { if (element.type === "text") { return (
{element.content}
); } // Render media element -> const mediaItem = mediaItems.find((item) => item.id === element.mediaId); if (!mediaItem) { return ( {element.name} ); } if (mediaItem.type === "image") { return (
{mediaItem.name}
); } if (mediaItem.type === "video" && mediaItem.thumbnailUrl) { return (
{mediaItem.name}
{element.name}
); } // Render audio element -> if (mediaItem.type === "audio") { return (
); } return ( {element.name} ); }; const handleElementMouseDown = (e: React.MouseEvent) => { if (onElementMouseDown) { onElementMouseDown(e, element); } }; return (
onElementClick && onElementClick(e, element)} onMouseDown={handleElementMouseDown} onContextMenu={(e) => onElementMouseDown && onElementMouseDown(e, element) } >
{renderElementContent()}
{isSelected && ( <>
handleResizeStart(e, element.id, "left")} />
handleResizeStart(e, element.id, "right")} /> )}
Split at playhead Duplicate {element.type === "text" ? "text" : "clip"} Delete {element.type === "text" ? "text" : "clip"} ); }