diff --git a/apps/web/src/components/editor/media-panel/store.ts b/apps/web/src/components/editor/media-panel/store.ts index e12120303..eb645ec3a 100644 --- a/apps/web/src/components/editor/media-panel/store.ts +++ b/apps/web/src/components/editor/media-panel/store.ts @@ -71,9 +71,16 @@ export const tabs: { [key in Tab]: { icon: LucideIcon; label: string } } = { interface MediaPanelStore { activeTab: Tab; setActiveTab: (tab: Tab) => void; + highlightMediaId: string | null; + requestRevealMedia: (mediaId: string) => void; + clearHighlight: () => void; } export const useMediaPanelStore = create((set) => ({ activeTab: "media", setActiveTab: (tab) => set({ activeTab: tab }), + highlightMediaId: null, + requestRevealMedia: (mediaId) => + set({ activeTab: "media", highlightMediaId: mediaId }), + clearHighlight: () => set({ highlightMediaId: null }), })); 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 0f0ed92d5..c84dde703 100644 --- a/apps/web/src/components/editor/media-panel/views/media.tsx +++ b/apps/web/src/components/editor/media-panel/views/media.tsx @@ -11,10 +11,10 @@ import { List, Loader2, Music, - Search, Video, } from "lucide-react"; -import { useEffect, useRef, useState, useMemo } from "react"; +import { useRef, useState, useMemo, useEffect } from "react"; +import { useHighlightScroll } from "@/hooks/use-highlight-scroll"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { MediaDragOverlay } from "@/components/editor/media-panel/drag-overlay"; @@ -33,7 +33,6 @@ import { import { DraggableMediaItem } from "@/components/ui/draggable-item"; import { useProjectStore } from "@/stores/project-store"; import { useTimelineStore } from "@/stores/timeline-store"; -import { ScrollArea } from "@/components/ui/scroll-area"; import { Tooltip, TooltipContent, @@ -41,6 +40,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { usePanelStore } from "@/stores/panel-store"; +import { useMediaPanelStore } from "../store"; function MediaItemWithContextMenu({ item, @@ -80,6 +80,11 @@ export function MediaView() { "name" ); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); + const { highlightMediaId, clearHighlight } = useMediaPanelStore(); + const { highlightedId, registerElement } = useHighlightScroll( + highlightMediaId, + clearHighlight + ); const processFiles = async (files: FileList | File[]) => { if (!files || files.length === 0) return; @@ -206,7 +211,7 @@ export function MediaView() { {item.name} @@ -435,7 +440,7 @@ export function MediaView() { -
+
{isDragOver || filteredMediaItems.length === 0 ? ( ) : ( )}
@@ -470,10 +478,14 @@ function GridView({ filteredMediaItems, renderPreview, handleRemove, + highlightedId, + registerElement, }: { filteredMediaItems: MediaItem[]; renderPreview: (item: MediaItem) => React.ReactNode; handleRemove: (e: React.MouseEvent, id: string) => Promise; + highlightedId: string | null; + registerElement: (id: string, element: HTMLElement | null) => void; }) { return (
{filteredMediaItems.map((item) => ( - - - useTimelineStore.getState().addMediaAtTime(item, currentTime) - } - rounded={false} - variant="card" - /> - +
registerElement(item.id, el)}> + + + useTimelineStore.getState().addMediaAtTime(item, currentTime) + } + rounded={false} + variant="card" + isHighlighted={highlightedId === item.id} + /> + +
))}
); @@ -513,36 +524,37 @@ function ListView({ filteredMediaItems, renderPreview, handleRemove, - formatDuration, + highlightedId, + registerElement, }: { filteredMediaItems: MediaItem[]; renderPreview: (item: MediaItem) => React.ReactNode; handleRemove: (e: React.MouseEvent, id: string) => Promise; - formatDuration: (duration: number) => string; + highlightedId: string | null; + registerElement: (id: string, element: HTMLElement | null) => void; }) { return (
{filteredMediaItems.map((item) => ( - - - useTimelineStore.getState().addMediaAtTime(item, currentTime) - } - variant="compact" - /> - +
registerElement(item.id, el)}> + + + useTimelineStore.getState().addMediaAtTime(item, currentTime) + } + variant="compact" + isHighlighted={highlightedId === item.id} + /> + +
))}
); diff --git a/apps/web/src/components/editor/timeline/timeline-element.tsx b/apps/web/src/components/editor/timeline/timeline-element.tsx index ca5d8c0a7..a1a338379 100644 --- a/apps/web/src/components/editor/timeline/timeline-element.tsx +++ b/apps/web/src/components/editor/timeline/timeline-element.tsx @@ -4,6 +4,7 @@ import { Scissors, Trash2, Copy, + Search, RefreshCw, EyeOff, Eye, @@ -29,6 +30,7 @@ import { ContextMenuSeparator, ContextMenuTrigger, } from "../../ui/context-menu"; +import { useMediaPanelStore } from "../media-panel/store"; export function TimelineElement({ element, @@ -69,6 +71,8 @@ export function TimelineElement({ onUpdateDuration: updateElementDuration, }); + const { requestRevealMedia } = useMediaPanelStore.getState(); + const effectiveDuration = element.duration - element.trimStart - element.trimEnd; const elementWidth = Math.max( @@ -166,6 +170,16 @@ export function TimelineElement({ input.click(); }; + const handleRevealInMedia = (e: React.MouseEvent) => { + e.stopPropagation(); + if (element.type !== "media") { + toast.error("Reveal is only available for media clips"); + return; + } + + requestRevealMedia(element.mediaId); + }; + const renderElementContent = () => { if (element.type === "text") { return ( @@ -348,10 +362,16 @@ export function TimelineElement({ Duplicate {element.type === "text" ? "text" : "clip"} {element.type === "media" && ( - - - Replace clip - + <> + + + Reveal in media + + + + Replace clip + + )} state.currentTime) : 0; + const highlightClassName = "ring-2 ring-primary rounded-sm bg-primary/10"; const handleAddToTimeline = () => { onAddToTimeline?.(currentTime); @@ -102,7 +105,11 @@ export function DraggableMediaItem({ className={cn("relative group", containerClassName ?? "w-28 h-28")} >
) : ( -
+
-
+
{preview}
{name} diff --git a/apps/web/src/hooks/use-highlight-scroll.ts b/apps/web/src/hooks/use-highlight-scroll.ts new file mode 100644 index 000000000..241bffb25 --- /dev/null +++ b/apps/web/src/hooks/use-highlight-scroll.ts @@ -0,0 +1,36 @@ +import { useEffect, useState, useRef } from "react"; + +export function useHighlightScroll( + highlightId: string | null, + onClearHighlight: () => void, + highlightDuration = 1000 +) { + const [highlightedId, setHighlightedId] = useState(null); + const elementRefs = useRef>(new Map()); + + const registerElement = (id: string, element: HTMLElement | null) => { + if (element) { + elementRefs.current.set(id, element); + } else { + elementRefs.current.delete(id); + } + }; + + useEffect(() => { + if (!highlightId) return; + + setHighlightedId(highlightId); + + const target = elementRefs.current.get(highlightId); + target?.scrollIntoView({ block: "center" }); + + const timeout = setTimeout(() => { + setHighlightedId(null); + onClearHighlight(); + }, highlightDuration); + + return () => clearTimeout(timeout); + }, [highlightId, onClearHighlight, highlightDuration]); + + return { highlightedId, registerElement }; +}