import { useState } from 'react' import { shallow } from 'zustand/vanilla/shallow' import { Trash2, FileVideo, FileImage, FileAudio, Layers, Type, FlipHorizontal2, FlipVertical2, ChevronDown, ChevronRight, Palette, Eye, Sun, Contrast, Droplets, Thermometer, SunDim, Moon, RotateCcw, Film, // EFFECTS HIDDEN: removed EyeOff, Sparkles, Plus, X AlignLeft, AlignCenter, AlignRight, } from 'lucide-react' import type { Asset, TimelineClip, LetterboxSettings, TextOverlayStyle, TransitionType } from '../../types/project-model' // EFFECTS HIDDEN: removed EffectMask import { DEFAULT_COLOR_CORRECTION, DEFAULT_LETTERBOX } from '../../types/project-model' // EFFECTS HIDDEN: removed EFFECT_DEFINITIONS, DEFAULT_EFFECT_MASK import { TEXT_PRESETS } from '../../types/project' import { namedResolutionTier } from '../../lib/video-resolution' import { formatTime } from './video-editor-utils' import { Tooltip } from '../../components/ui/tooltip' import { selectAssets, selectSelectedClipAudioControls, selectSelectedClipForProperties, selectTracks, } from './editor-selectors' import { useEditorActions, useEditorStore } from './editor-store' interface ClipPropertiesPanelProps { onCreateVideoFromImage: (clip: TimelineClip) => void } export function ClipPropertiesPanel(props: ClipPropertiesPanelProps) { const { onCreateVideoFromImage, } = props const { deleteClipDisplayedTake, setClipAudioLevel, setClipAudioMuted, updateClip, } = useEditorActions() const assets = useEditorStore(selectAssets) const tracks = useEditorStore(selectTracks) const selectedClip = useEditorStore(selectSelectedClipForProperties) const clipAudioControls = useEditorStore(selectSelectedClipAudioControls, shallow) if (!selectedClip) return null const effectiveMuted = clipAudioControls?.muted ?? (selectedClip.muted || false) const effectiveVolume = clipAudioControls?.volume ?? (selectedClip.volume ?? 1) const getLiveAsset = (clip: TimelineClip): Asset | null | undefined => { if (!clip.assetId) return clip.asset return assets.find(asset => asset.id === clip.assetId) || clip.asset } const getMaxClipDuration = (clip: TimelineClip): number => { const liveAsset = getLiveAsset(clip) if (clip.type !== 'video' || !liveAsset?.duration) return Infinity const mediaDuration = liveAsset.duration const usableMedia = mediaDuration - clip.trimStart - clip.trimEnd return Math.max(0.5, usableMedia / clip.speed) } const handleDeleteDisplayedTake = () => { deleteClipDisplayedTake(selectedClip.id) } const [propertiesTab, setPropertiesTab] = useState<'properties' | 'metadata'>('properties') const [showFlip, setShowFlip] = useState(false) const [showTransitions, setShowTransitions] = useState(false) const [showColorCorrection, setShowColorCorrection] = useState(false) const getClipDimensions = (clip: TimelineClip): { width: number; height: number } | null => { if (clip.type === 'audio') return null const liveAsset = getLiveAsset(clip) if (!liveAsset) return null const takeIndex = clip.takeIndex ?? liveAsset.activeTakeIndex if (liveAsset.takes && liveAsset.takes.length > 0 && takeIndex !== undefined) { const idx = Math.max(0, Math.min(takeIndex, liveAsset.takes.length - 1)) const take = liveAsset.takes[idx] if (take.width && take.height) { return { width: take.width, height: take.height } } } if (liveAsset.width && liveAsset.height) { return { width: liveAsset.width, height: liveAsset.height } } return null } const isTextClip = selectedClip.type === 'text' const hasPlaybackControls = selectedClip.type === 'video' || selectedClip.type === 'audio' const hasAudioControls = selectedClip.type === 'video' || selectedClip.type === 'audio' const hasVisualTransformControls = selectedClip.type === 'video' || selectedClip.type === 'image' const hasTransitionControls = selectedClip.type === 'video' || selectedClip.type === 'image' const hasColorCorrectionControls = selectedClip.type === 'video' || selectedClip.type === 'image' return (
{/* Tab header */}
{/* Metadata Tab */} {propertiesTab === 'metadata' && (() => { const liveAsset = getLiveAsset(selectedClip) const dims = getClipDimensions(selectedClip) const genParams = liveAsset?.generationParams // Current take info const totalTakes = liveAsset?.takes?.length || 1 const currentTakeIdx = selectedClip.takeIndex ?? (liveAsset?.activeTakeIndex ?? (totalTakes - 1)) const displayTakeNum = Math.min(currentTakeIdx, totalTakes - 1) + 1 // Get the file path for the current take let filePath = liveAsset?.path || '' if (liveAsset?.takes && liveAsset.takes.length > 0 && selectedClip.takeIndex !== undefined) { const idx = Math.max(0, Math.min(selectedClip.takeIndex, liveAsset.takes.length - 1)) filePath = liveAsset.takes[idx].path } // Determine if this is an upscaled take (take index > 0 and resolution is higher than original) const originalRes = liveAsset?.generationParams?.resolution const qualityTier = dims ? namedResolutionTier(Math.min(dims.width, dims.height)) : 0 const isUpscaled = dims && originalRes ? qualityTier > parseInt(originalRes, 10) : false return (
{/* Currently Displayed */}

Currently Displayed

Take
{displayTakeNum} / {totalTakes} {totalTakes > 1 && ( )}
{dims ? ( <>
Quality {qualityTier >= 2160 ? 'Ultra HD' : qualityTier >= 1080 ? 'Full HD' : qualityTier >= 720 ? 'HD' : 'SD'} {isUpscaled && (Upscaled)}
{dims.width > 0 && (
Dimensions {dims.width} × {dims.height}
)} ) : (
Dimension metadata unavailable.
)} {originalRes && originalRes !== 'imported' && (
Original Gen {originalRes}
)}
{/* Clip Info */}

Clip Info

Type
{selectedClip.type === 'video' && } {selectedClip.type === 'image' && } {selectedClip.type === 'audio' && } {selectedClip.type === 'text' && } {selectedClip.type}
Duration {selectedClip.duration.toFixed(2)}s
{liveAsset?.duration && (
Source Duration {liveAsset.duration.toFixed(2)}s
)}
Speed {selectedClip.speed}x
Track {tracks[selectedClip.trackIndex]?.name || `Track ${selectedClip.trackIndex + 1}`}
Start {formatTime(selectedClip.startTime)}
End {formatTime(selectedClip.startTime + selectedClip.duration)}
Trim In {selectedClip.trimStart.toFixed(2)}s
Trim Out {selectedClip.trimEnd.toFixed(2)}s
Opacity {selectedClip.opacity}%
{/* Takes */} {liveAsset?.takes && liveAsset.takes.length > 1 && (

Takes

Total Takes {liveAsset.takes.length}
Active Take #{(selectedClip.takeIndex ?? (liveAsset.activeTakeIndex ?? liveAsset.takes.length - 1)) + 1}
)} {/* Generation Parameters */} {genParams && (

Generation

Mode {genParams.mode.replace(/-/g, ' ')}
Model {genParams.model}
Gen Resolution {genParams.resolution}
FPS {genParams.fps}
Duration {genParams.duration}s
{genParams.cameraMotion && genParams.cameraMotion !== 'none' && (
Camera {genParams.cameraMotion}
)} {genParams.prompt && (
Prompt

{genParams.prompt}

)}
)} {/* File Path */} {filePath && (

File

{filePath}

)} {/* Asset Created At */} {liveAsset?.createdAt && (

Created

{new Date(liveAsset.createdAt).toLocaleString()}
)}
) })()} {/* Properties Tab */} {propertiesTab === 'properties' &&
{/* Adjustment Layer properties */} {selectedClip.type === 'adjustment' && (() => { const lb = { ...DEFAULT_LETTERBOX, ...selectedClip.letterbox } const updateLetterbox = (patch: Partial) => { updateClip(selectedClip.id, { letterbox: { ...lb, ...patch } }) } return (

Adjustment Layer

{/* Letterbox toggle */}
Letterbox
{lb.enabled && ( <> {/* Aspect ratio */}
Aspect Ratio
{/* Custom ratio input */} {lb.aspectRatio === 'custom' && (
Custom Ratio updateLetterbox({ customRatio: parseFloat(e.target.value) || 2.35 })} onKeyDown={e => e.stopPropagation()} className="w-20 bg-zinc-800 border border-zinc-700 rounded px-2 py-0.5 text-[10px] text-white text-center focus:outline-none focus:border-blue-500/50" />
)} {/* Bar color */}
Bar Color updateLetterbox({ color: e.target.value })} className="w-7 h-6 rounded cursor-pointer border border-zinc-700" />
{/* Bar opacity */}
Bar Opacity
updateLetterbox({ opacity: parseInt(e.target.value) })} className="w-20 accent-blue-500" /> {lb.opacity}%
)} {/* Color correction note */}

Color correction on this layer affects all tracks below.

) })()} {/* Text overlay properties */} {selectedClip.type === 'text' && selectedClip.textStyle && (() => { const ts = selectedClip.textStyle const updateText = (patch: Partial) => { updateClip(selectedClip.id, { textStyle: { ...ts, ...patch } }) } return (

Text Overlay

{/* Text content */}
Content