"use client"; import { Scissors, Trash2, Copy, Search, 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, MediaElement } 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"; import { useMediaPanelStore } from "../media-panel/store"; export function TimelineElement({ element, track, zoomLevel, isSelected, onElementMouseDown, onElementClick, }: TimelineElementProps) { const { mediaFiles } = useMediaStore(); const { dragState, copySelected, selectedElements, deleteSelected, splitSelected, toggleSelectedHidden, toggleSelectedMuted, duplicateElement, revealElementInMedia, replaceElementWithFile, getContextMenuState, } = useTimelineStore(); const { currentTime } = usePlaybackStore(); const mediaItem = element.type === "media" ? mediaFiles.find((file) => file.id === element.mediaId) : null; const hasAudio = mediaItem?.type === "audio" || mediaItem?.type === "video"; const { resizing, handleResizeStart, handleResizeMove, handleResizeEnd } = useTimelineElementResize({ element, track, zoomLevel, }); const { isMultipleSelected, isCurrentElementSelected, hasAudioElements, canSplitSelected, } = getContextMenuState(track.id, element.id); const effectiveDuration = element.duration - element.trimStart - element.trimEnd; const elementWidth = Math.max( TIMELINE_CONSTANTS.ELEMENT_MIN_WIDTH, effectiveDuration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel ); const isBeingDragged = dragState.elementId === element.id; const elementStartTime = isBeingDragged && dragState.isDragging ? dragState.currentTime : element.startTime; const elementLeft = elementStartTime * 50 * zoomLevel; const handleElementSplitContext = (e: React.MouseEvent) => { e.stopPropagation(); splitSelected( currentTime, isMultipleSelected && isCurrentElementSelected ? undefined : track.id, isMultipleSelected && isCurrentElementSelected ? undefined : element.id ); }; const handleElementDuplicateContext = (e: React.MouseEvent) => { e.stopPropagation(); duplicateElement(track.id, element.id); }; const handleElementCopyContext = (e: React.MouseEvent) => { e.stopPropagation(); copySelected(); }; const handleElementDeleteContext = (e: React.MouseEvent) => { e.stopPropagation(); deleteSelected( isMultipleSelected && isCurrentElementSelected ? undefined : track.id, isMultipleSelected && isCurrentElementSelected ? undefined : element.id ); }; const handleToggleElementContext = (e: React.MouseEvent) => { e.stopPropagation(); if (hasAudio && element.type === "media") { toggleSelectedMuted( isMultipleSelected && isCurrentElementSelected ? undefined : track.id, isMultipleSelected && isCurrentElementSelected ? undefined : element.id ); } else { toggleSelectedHidden( isMultipleSelected && isCurrentElementSelected ? undefined : track.id, isMultipleSelected && isCurrentElementSelected ? undefined : element.id ); } }; const handleReplaceClip = (e: React.MouseEvent) => { e.stopPropagation(); 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) { await replaceElementWithFile(track.id, element.id, file); } }; input.click(); }; const handleRevealInMedia = (e: React.MouseEvent) => { e.stopPropagation(); revealElementInMedia(element.id); }; const renderElementContent = () => { if (element.type === "text") { return (
{element.content}
); } const mediaItem = mediaFiles.find((file) => file.id === element.mediaId); if (!mediaItem) { return ( {element.name} ); } if ( mediaItem.type === "image" || (mediaItem.type === "video" && mediaItem.thumbnailUrl) ) { const trackHeight = getTrackHeight(track.type); const tileWidth = trackHeight * (16 / 9); const imageUrl = mediaItem.type === "image" ? mediaItem.url : mediaItem.thumbnailUrl; return (
); } if (mediaItem.type === "audio") { return (
); } return ( {element.name} ); }; const handleElementMouseDown = (e: React.MouseEvent) => { if (onElementMouseDown) { onElementMouseDown(e, element); } }; const isMuted = element.type === "media" && element.muted; return (
onElementClick && onElementClick(e, element)} onMouseDown={handleElementMouseDown} onContextMenu={(e) => onElementMouseDown && onElementMouseDown(e, element) } >
{renderElementContent()}
{(hasAudio ? isMuted : element.hidden) && (
{hasAudio ? ( ) : ( )}
)} {isSelected && ( <>
handleResizeStart(e, element.id, "left")} >
handleResizeStart(e, element.id, "right")} >
)}
{(!isMultipleSelected || (isMultipleSelected && isCurrentElementSelected && canSplitSelected)) && ( {isMultipleSelected && isCurrentElementSelected ? `Split ${selectedElements.length} elements at playhead` : "Split at playhead"} )} {isMultipleSelected && isCurrentElementSelected ? `Copy ${selectedElements.length} elements` : "Copy element"} {isMultipleSelected && isCurrentElementSelected ? ( hasAudioElements ? ( ) : ( ) ) : hasAudio ? ( isMuted ? ( ) : ( ) ) : element.hidden ? ( ) : ( )} {isMultipleSelected && isCurrentElementSelected ? hasAudioElements ? `Toggle mute ${selectedElements.length} elements` : `Toggle visibility ${selectedElements.length} elements` : hasAudio ? isMuted ? "Unmute" : "Mute" : element.hidden ? "Show" : "Hide"}{" "} {!isMultipleSelected && (element.type === "text" ? "text" : "clip")} {!isMultipleSelected && ( Duplicate {element.type === "text" ? "text" : "clip"} )} {!isMultipleSelected && element.type === "media" && ( <> Reveal in media Replace clip )} {isMultipleSelected && isCurrentElementSelected ? `Delete ${selectedElements.length} elements` : `Delete ${element.type === "text" ? "text" : "clip"}`} ); }