"use client"; import { useState } from "react"; import { Button } from "../ui/button"; import { MoreVertical, Scissors, Trash2 } from "lucide-react"; import { useMediaStore } from "@/stores/media-store"; import { useTimelineStore } from "@/stores/timeline-store"; import { usePlaybackStore } from "@/stores/playback-store"; import { useDragClip } from "@/hooks/use-drag-clip"; import AudioWaveform from "./audio-waveform"; import { toast } from "sonner"; import { TimelineClipProps, ResizeState } from "@/types/timeline"; export function TimelineClip({ clip, track, zoomLevel, isSelected, onContextMenu, onClipMouseDown, onClipClick, }: TimelineClipProps) { const { mediaItems } = useMediaStore(); const { updateClipTrim, addClipToTrack, removeClipFromTrack } = useTimelineStore(); const { currentTime } = usePlaybackStore(); const { draggedClipId, getDraggedClipPosition } = useDragClip(zoomLevel); const [resizing, setResizing] = useState(null); const [clipMenuOpen, setClipMenuOpen] = useState(false); const effectiveDuration = clip.duration - clip.trimStart - clip.trimEnd; const clipWidth = Math.max(80, effectiveDuration * 50 * zoomLevel); // Use real-time position during drag, otherwise use stored position const dragPosition = getDraggedClipPosition(clip.id); const clipStartTime = dragPosition !== null ? dragPosition : clip.startTime; const clipLeft = clipStartTime * 50 * zoomLevel; const isBeingDragged = draggedClipId === clip.id; const getTrackColor = (type: string) => { switch (type) { case "video": return "bg-blue-500/20 border-blue-500/30"; case "audio": return "bg-green-500/20 border-green-500/30"; case "effects": return "bg-purple-500/20 border-purple-500/30"; default: return "bg-gray-500/20 border-gray-500/30"; } }; const handleResizeStart = ( e: React.MouseEvent, clipId: string, side: "left" | "right" ) => { e.stopPropagation(); e.preventDefault(); setResizing({ clipId, side, startX: e.clientX, initialTrimStart: clip.trimStart, initialTrimEnd: clip.trimEnd, }); }; const updateTrimFromMouseMove = (e: { clientX: number }) => { if (!resizing) return; const deltaX = e.clientX - resizing.startX; const deltaTime = deltaX / (50 * zoomLevel); if (resizing.side === "left") { const newTrimStart = Math.max( 0, Math.min( clip.duration - clip.trimEnd - 0.1, resizing.initialTrimStart + deltaTime ) ); updateClipTrim(track.id, clip.id, newTrimStart, clip.trimEnd); } else { const newTrimEnd = Math.max( 0, Math.min( clip.duration - clip.trimStart - 0.1, resizing.initialTrimEnd - deltaTime ) ); updateClipTrim(track.id, clip.id, clip.trimStart, newTrimEnd); } }; const handleResizeMove = (e: React.MouseEvent) => { updateTrimFromMouseMove(e); }; const handleResizeEnd = () => { setResizing(null); }; const handleDeleteClip = () => { removeClipFromTrack(track.id, clip.id); setClipMenuOpen(false); }; const handleSplitClip = () => { // Use current playback time as split point const splitTime = currentTime; // Only split if splitTime is within the clip's effective range const effectiveStart = clip.startTime; const effectiveEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); if (splitTime <= effectiveStart || splitTime >= effectiveEnd) { toast.error("Playhead must be within clip to split"); return; } const firstDuration = splitTime - effectiveStart; const secondDuration = effectiveEnd - splitTime; // First part: adjust original clip updateClipTrim( track.id, clip.id, clip.trimStart, clip.trimEnd + secondDuration ); // Second part: add new clip after split addClipToTrack(track.id, { mediaId: clip.mediaId, name: clip.name + " (cut)", duration: clip.duration, startTime: splitTime, trimStart: clip.trimStart + firstDuration, trimEnd: clip.trimEnd, }); setClipMenuOpen(false); toast.success("Clip split successfully"); }; const renderClipContent = () => { const mediaItem = mediaItems.find((item) => item.id === clip.mediaId); if (!mediaItem) { return ( {clip.name} ); } if (mediaItem.type === "image") { return (
{mediaItem.name}
); } if (mediaItem.type === "video" && mediaItem.thumbnailUrl) { return (
{mediaItem.name}
{clip.name}
); } if (mediaItem.type === "audio") { return (
); } // Fallback for videos without thumbnails return ( {clip.name} ); }; return (
onClipMouseDown(e, clip)} onClick={(e) => onClipClick(e, clip)} onMouseMove={handleResizeMove} onMouseUp={handleResizeEnd} onMouseLeave={handleResizeEnd} tabIndex={0} onContextMenu={(e) => onContextMenu(e, clip.id)} > {/* Left trim handle */}
handleResizeStart(e, clip.id, "left")} /> {/* Clip content */}
{renderClipContent()} {/* Clip options menu */}
{clipMenuOpen && (
e.stopPropagation()} >
)}
{/* Right trim handle */}
handleResizeStart(e, clip.id, "right")} />
); }