diff --git a/apps/web/src/components/editor/media-panel/views/media.tsx b/apps/web/src/components/editor/media-panel/views/media.tsx
index e2c7210..50fb6fa 100644
--- a/apps/web/src/components/editor/media-panel/views/media.tsx
+++ b/apps/web/src/components/editor/media-panel/views/media.tsx
@@ -7,7 +7,6 @@ import { useTimelineStore } from "@/stores/timeline-store";
import { Image, Music, Plus, Upload, Video } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
-import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Button } from "@/components/ui/button";
import { DragOverlay } from "@/components/ui/drag-overlay";
import {
@@ -24,6 +23,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
+import { DraggableMediaItem } from "@/components/ui/draggable-item";
export function MediaView() {
const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore();
@@ -98,19 +98,6 @@ export function MediaView() {
return `${min}:${sec.toString().padStart(2, "0")}`;
};
- const startDrag = (e: React.DragEvent, item: MediaItem) => {
- // When dragging a media item, set drag data for timeline to read
- e.dataTransfer.setData(
- "application/x-media-item",
- JSON.stringify({
- id: item.id,
- type: item.type,
- name: item.name,
- })
- );
- e.dataTransfer.effectAllowed = "copy";
- };
-
const [filteredMediaItems, setFilteredMediaItems] = useState(mediaItems);
useEffect(() => {
@@ -140,7 +127,7 @@ export function MediaView() {
@@ -293,45 +280,30 @@ export function MediaView() {
>
{/* Render each media item as a draggable button */}
{filteredMediaItems.map((item) => (
-
-
-
-
-
-
- Export clips
- handleRemove(e, item.id)}
- >
- Delete
-
-
-
-
+
+
+
+
+
+ Export clips
+ handleRemove(e, item.id)}
+ >
+ Delete
+
+
+
))}
)}
diff --git a/apps/web/src/components/editor/media-panel/views/text.tsx b/apps/web/src/components/editor/media-panel/views/text.tsx
index 9e4f619..ab69852 100644
--- a/apps/web/src/components/editor/media-panel/views/text.tsx
+++ b/apps/web/src/components/editor/media-panel/views/text.tsx
@@ -1,11 +1,25 @@
-import { Card } from "@/components/ui/card";
+import { DraggableMediaItem } from "@/components/ui/draggable-item";
export function TextView() {
return (
-
- Default text
-
+
+ Default text
+
+ }
+ dragData={{
+ id: "default-text",
+ type: "text",
+ name: "Default text",
+ content: "Default text",
+ }}
+ aspectRatio={1}
+ className="w-24"
+ showLabel={false}
+ />
);
}
diff --git a/apps/web/src/components/ui/draggable-item.tsx b/apps/web/src/components/ui/draggable-item.tsx
new file mode 100644
index 0000000..c24e31d
--- /dev/null
+++ b/apps/web/src/components/ui/draggable-item.tsx
@@ -0,0 +1,150 @@
+"use client";
+
+import { AspectRatio } from "@/components/ui/aspect-ratio";
+import { Button } from "@/components/ui/button";
+import { ReactNode, useState, useRef, useEffect } from "react";
+import { createPortal } from "react-dom";
+import { Plus } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+export interface DraggableMediaItemProps {
+ name: string;
+ preview: ReactNode;
+ dragData: Record;
+ onDragStart?: (e: React.DragEvent) => void;
+ aspectRatio?: number;
+ className?: string;
+ showPlusOnDrag?: boolean;
+ showLabel?: boolean;
+ rounded?: boolean;
+}
+
+export function DraggableMediaItem({
+ name,
+ preview,
+ dragData,
+ onDragStart,
+ aspectRatio = 16 / 9,
+ className = "",
+ showPlusOnDrag = true,
+ showLabel = true,
+ rounded = true,
+}: DraggableMediaItemProps) {
+ const [isDragging, setIsDragging] = useState(false);
+ const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 });
+ const dragRef = useRef(null);
+
+ useEffect(() => {
+ if (!isDragging) return;
+
+ const handleDragOver = (e: DragEvent) => {
+ setDragPosition({ x: e.clientX, y: e.clientY });
+ };
+
+ document.addEventListener("dragover", handleDragOver);
+
+ return () => {
+ document.removeEventListener("dragover", handleDragOver);
+ };
+ }, [isDragging]);
+
+ const handleDragStart = (e: React.DragEvent) => {
+ // Hide the default ghost image
+ const emptyImg = new Image();
+ emptyImg.src =
+ "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=";
+ e.dataTransfer.setDragImage(emptyImg, 0, 0);
+
+ // Set drag data
+ e.dataTransfer.setData(
+ "application/x-media-item",
+ JSON.stringify(dragData)
+ );
+ e.dataTransfer.effectAllowed = "copy";
+
+ // Set initial position and show custom drag preview
+ setDragPosition({ x: e.clientX, y: e.clientY });
+ setIsDragging(true);
+
+ onDragStart?.(e);
+ };
+
+ const handleDragEnd = () => {
+ setIsDragging(false);
+ };
+
+ return (
+ <>
+
+
+
+
+ {/* Custom drag preview */}
+ {isDragging &&
+ typeof document !== "undefined" &&
+ createPortal(
+
+
+
+
+ {preview}
+
+ {showPlusOnDrag && }
+
+
+
,
+ document.body
+ )}
+ >
+ );
+}
+
+function PlusButton({ className }: { className?: string }) {
+ return (
+
+ );
+}