"use client"; import { useState } from "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 { 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, getTrackHeight, } from "@/constants/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, removeElementFromTrackWithRipple, dragState, splitElement, splitAndKeepLeft, splitAndKeepRight, separateAudio, addElementToTrack, replaceElementMedia, rippleEditingEnabled, } = 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; // Element should always be positioned at startTime - trimStart only affects content, not position const elementLeft = elementStartTime * 50 * zoomLevel; 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 = () => { if (rippleEditingEnabled) { removeElementFromTrackWithRipple(track.id, element.id); } else { removeElementFromTrack(track.id, element.id); } }; const handleReplaceClip = () => { if (element.type !== "media") { toast.error("Replace is only available for media clips"); return; } // Create a file input to select replacement media const input = document.createElement("input"); input.type = "file"; input.accept = "video/*,audio/*,image/*"; input.onchange = async (e) => { const file = (e.target as HTMLInputElement).files?.[0]; if (!file) return; try { const success = await replaceElementMedia(track.id, element.id, file); if (success) { toast.success("Clip replaced successfully"); } else { toast.error("Failed to replace clip"); } } catch (error) { toast.error("Failed to replace clip"); console.log( JSON.stringify({ error: "Failed to replace clip", details: error }) ); } }; input.click(); }; const renderElementContent = () => { if (element.type === "text") { return (