import { useCallback, useRef, useSyncExternalStore, type ReactNode } from "react"; import { PlayerControls } from "../../player"; import type { TimelineElement } from "../../player"; import { NLEPreview } from "./NLEPreview"; import { CompositionBreadcrumb } from "./CompositionBreadcrumb"; import { usePreviewBlockDrop } from "./usePreviewBlockDrop"; import { useNLEContext } from "./NLEContext"; import { AssetPreviewOverlay } from "./AssetPreviewOverlay"; function subscribeFullscreen(cb: () => void) { document.addEventListener("fullscreenchange", cb); return () => document.removeEventListener("fullscreenchange", cb); } function getFullscreenElement() { return document.fullscreenElement; } // Clear the timeline selection when a pointer lands outside the composition // frame (clicks *inside* the frame are handled by the DOM-edit overlay). // fallow-ignore-next-line complexity function deselectIfPointerOutsideFrame( e: React.PointerEvent, iframe: HTMLIFrameElement | null, onDeselect?: (element: null) => void, ): void { const el = iframe?.parentElement ?? iframe; if (!el) return; const rect = el.getBoundingClientRect(); const outside = e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom; if (outside) onDeselect?.(null); } export interface PreviewPaneProps { portrait?: boolean; /** Slot for overlays rendered on top of the preview (cursors, highlights, etc.) */ previewOverlay?: ReactNode; onSelectTimelineElement?: (element: TimelineElement | null) => void; onPreviewBlockDrop?: ( blockName: string, position: { left: number; top: number }, ) => Promise | void; } // fallow-ignore-next-line complexity export function PreviewPane({ portrait, previewOverlay, onSelectTimelineElement, onPreviewBlockDrop, }: PreviewPaneProps) { const { projectId, iframeRef, togglePlay, seek, onIframeLoad, compositionStack, handleNavigateComposition, setCompositionLoading, timelineDisabled, hasLoadedOnceRef, previewCompositionSize, setPreviewCompositionSize, } = useNLEContext(); const stageRefForDrop = useRef(null); const handleStageRef = useCallback((ref: React.RefObject) => { stageRefForDrop.current = ref.current; }, []); const { isDragOver: previewDragOver, handleDragEnter: handlePreviewDragEnter, handleDragOver: handlePreviewDragOver, handleDragLeave: handlePreviewDragLeave, handleDrop: handlePreviewDrop, } = usePreviewBlockDrop({ portrait, compositionSize: previewCompositionSize, stageRef: stageRefForDrop as React.RefObject, onBlockDrop: onPreviewBlockDrop, }); // Preview-only fullscreen: fullscreen targets THIS pane's container, so the // browser shows only the preview (sidebars + timeline are excluded naturally). const containerRef = useRef(null); const fullscreenElement = useSyncExternalStore(subscribeFullscreen, getFullscreenElement); const isFullscreen = fullscreenElement === containerRef.current && fullscreenElement != null; const toggleFullscreen = useCallback(() => { if (!containerRef.current) return; if (document.fullscreenElement) { void document.exitFullscreen(); } else { void containerRef.current.requestFullscreen(); } }, []); const currentLevel = compositionStack[compositionStack.length - 1]; const directUrl = compositionStack.length > 1 ? currentLevel.previewUrl : undefined; return (
deselectIfPointerOutsideFrame(e, iframeRef.current, onSelectTimelineElement) } onDragEnter={handlePreviewDragEnter} onDragOver={handlePreviewDragOver} onDragLeave={handlePreviewDragLeave} onDrop={handlePreviewDrop} >
{previewDragOver && (
)}
{!isFullscreen && previewOverlay}
{/* Transport row: no own background or border — the controls sit flat on the preview panel's surface (CapCut-style). */}
{!isFullscreen && compositionStack.length > 1 && ( )}
); }