2025-06-22 10:02:50 +02:00
|
|
|
"use client";
|
|
|
|
|
|
2025-06-22 13:07:02 +02:00
|
|
|
import { ScrollArea } from "../ui/scroll-area";
|
|
|
|
|
import { Button } from "../ui/button";
|
2025-06-22 10:02:50 +02:00
|
|
|
import {
|
|
|
|
|
Scissors,
|
|
|
|
|
ArrowLeftToLine,
|
|
|
|
|
ArrowRightToLine,
|
|
|
|
|
Trash2,
|
|
|
|
|
Snowflake,
|
|
|
|
|
Copy,
|
|
|
|
|
SplitSquareHorizontal,
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
import {
|
|
|
|
|
Tooltip,
|
|
|
|
|
TooltipContent,
|
|
|
|
|
TooltipTrigger,
|
|
|
|
|
TooltipProvider,
|
2025-06-22 13:07:02 +02:00
|
|
|
} from "../ui/tooltip";
|
2025-06-22 14:50:22 +02:00
|
|
|
import { DragOverlay } from "../ui/drag-overlay";
|
2025-06-22 13:07:02 +02:00
|
|
|
import { useTimelineStore, type TimelineTrack } from "@/stores/timeline-store";
|
2025-06-22 14:50:22 +02:00
|
|
|
import { useDragDrop } from "@/hooks/use-drag-drop";
|
2025-06-22 10:02:50 +02:00
|
|
|
|
|
|
|
|
export function Timeline() {
|
2025-06-22 13:07:02 +02:00
|
|
|
const { tracks, addTrack } = useTimelineStore();
|
|
|
|
|
|
2025-06-22 14:50:22 +02:00
|
|
|
const { isDragOver, dragProps } = useDragDrop({
|
|
|
|
|
onDrop: (files) => {
|
|
|
|
|
// TODO: Handle file drop functionality for timeline
|
|
|
|
|
console.log("Files dropped on timeline:", files);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-22 10:02:50 +02:00
|
|
|
return (
|
2025-06-22 14:50:22 +02:00
|
|
|
<div
|
|
|
|
|
className={`h-full flex flex-col transition-colors duration-200 relative ${
|
|
|
|
|
isDragOver ? "bg-accent/30 border-accent" : ""
|
|
|
|
|
}`}
|
|
|
|
|
{...dragProps}
|
|
|
|
|
>
|
|
|
|
|
<DragOverlay
|
|
|
|
|
isVisible={isDragOver}
|
|
|
|
|
title="Drop files here"
|
|
|
|
|
description="Add media to timeline tracks"
|
|
|
|
|
/>
|
|
|
|
|
|
2025-06-22 10:02:50 +02:00
|
|
|
{/* Toolbar */}
|
|
|
|
|
<div className="border-b flex items-center px-2 py-1 gap-1">
|
2025-06-22 13:07:02 +02:00
|
|
|
<TooltipProvider delayDuration={500}>
|
2025-06-22 10:02:50 +02:00
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button variant="ghost" size="icon">
|
|
|
|
|
<Scissors className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Split clip (S)</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button variant="ghost" size="icon">
|
|
|
|
|
<ArrowLeftToLine className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Split and keep left (A)</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button variant="ghost" size="icon">
|
|
|
|
|
<ArrowRightToLine className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Split and keep right (D)</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button variant="ghost" size="icon">
|
|
|
|
|
<SplitSquareHorizontal className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Separate audio (E)</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button variant="ghost" size="icon">
|
|
|
|
|
<Copy className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Duplicate clip (Ctrl+D)</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button variant="ghost" size="icon">
|
|
|
|
|
<Snowflake className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Freeze frame (F)</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button variant="ghost" size="icon">
|
|
|
|
|
<Trash2 className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Delete clip (Delete)</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</TooltipProvider>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Tracks Area */}
|
|
|
|
|
<ScrollArea className="flex-1">
|
|
|
|
|
<div className="min-w-[800px]">
|
|
|
|
|
{/* Time Markers */}
|
|
|
|
|
<div className="py-2 pt-1 flex items-center">
|
|
|
|
|
{Array.from({ length: 16 }).map((_, i) => (
|
|
|
|
|
<div
|
|
|
|
|
key={i}
|
|
|
|
|
className="w-[50px] flex items-end justify-center text-xs text-muted-foreground"
|
|
|
|
|
>
|
|
|
|
|
{i}s
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-06-22 13:07:02 +02:00
|
|
|
{/* Timeline Tracks */}
|
|
|
|
|
{tracks.length === 0 ? (
|
|
|
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
|
|
|
<div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4">
|
|
|
|
|
<SplitSquareHorizontal className="h-8 w-8 text-muted-foreground" />
|
2025-06-22 10:02:50 +02:00
|
|
|
</div>
|
2025-06-22 13:07:02 +02:00
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
No tracks in timeline
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground/70 mt-1">
|
|
|
|
|
Add a video or audio track to get started
|
|
|
|
|
</p>
|
2025-06-22 10:02:50 +02:00
|
|
|
</div>
|
2025-06-22 13:07:02 +02:00
|
|
|
) : (
|
|
|
|
|
<div className="flex flex-col gap-2.5">
|
|
|
|
|
{tracks.map((track) => (
|
|
|
|
|
<TimelineTrackComponent key={track.id} track={track} />
|
|
|
|
|
))}
|
2025-06-22 10:02:50 +02:00
|
|
|
</div>
|
2025-06-22 13:07:02 +02:00
|
|
|
)}
|
2025-06-22 10:02:50 +02:00
|
|
|
</div>
|
|
|
|
|
</ScrollArea>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-22 13:07:02 +02:00
|
|
|
function TimelineTrackComponent({ track }: { track: TimelineTrack }) {
|
|
|
|
|
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";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-22 10:02:50 +02:00
|
|
|
return (
|
2025-06-22 13:07:02 +02:00
|
|
|
<div className="flex items-center px-2">
|
|
|
|
|
<div className="w-24 text-xs text-muted-foreground flex-shrink-0 mr-2">
|
|
|
|
|
{track.name}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex-1 h-[60px]">
|
|
|
|
|
{track.clips.length === 0 ? (
|
|
|
|
|
<div className="h-full rounded-sm border-2 border-dashed border-muted/30 flex items-center justify-center text-xs text-muted-foreground">
|
|
|
|
|
Drop media here
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div
|
|
|
|
|
className={`h-full rounded-sm border cursor-pointer transition-colors ${getTrackColor(track.type)} flex items-center px-2`}
|
|
|
|
|
>
|
|
|
|
|
<span className="text-xs text-foreground/80">
|
|
|
|
|
{track.clips.length} clip{track.clips.length !== 1 ? "s" : ""}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-06-22 10:02:50 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|