"use client"; import { Scissors, Trash2, Copy, RefreshCw, EyeOff, Eye, Volume2, VolumeX, } 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 } from "@/types/timeline"; import { useTimelineElementResize } from "@/hooks/use-timeline-element-resize"; import { getTrackElementClasses, TIMELINE_CONSTANTS, getTrackHeight, } from "@/constants/timeline-constants"; 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, addElementToTrack, replaceElementMedia, rippleEditingEnabled, toggleElementHidden, toggleElementMuted, } = useTimelineStore(); const { currentTime } = usePlaybackStore(); const mediaItem = element.type === "media" ? mediaItems.find((item) => item.id === element.mediaId) : null; const hasAudio = mediaItem?.type === "audio" || mediaItem?.type === "video"; const { resizing, 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 = (e: React.MouseEvent) => { e.stopPropagation(); 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 = (e: React.MouseEvent) => { e.stopPropagation(); 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 = (e: React.MouseEvent) => { e.stopPropagation(); if (rippleEditingEnabled) { removeElementFromTrackWithRipple(track.id, element.id); } else { removeElementFromTrack(track.id, element.id); } }; const handleToggleElementContext = (e: React.MouseEvent) => { e.stopPropagation(); if (hasAudio && element.type === "media") { toggleElementMuted(track.id, element.id); return; } toggleElementHidden(track.id, element.id); }; const handleReplaceClip = (e: React.MouseEvent) => { e.stopPropagation(); 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 result = await replaceElementMedia(track.id, element.id, file); if (result.success) { toast.success("Clip replaced successfully"); } else { toast.error(result.error || "Failed to replace clip"); } } catch (error) { console.error("Unexpected error replacing clip:", error); toast.error( `Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}` ); } }; input.click(); }; const renderElementContent = () => { if (element.type === "text") { return (