Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b1fab6f
fix(i18n): replace remaining placeholder strings in en locale
walterlow May 25, 2026
7e7e8e5
fix(editor): prevent preview shift when entering path/pen edit mode
walterlow May 27, 2026
c82e90c
refactor(timeline): split items-store into normalize + indexes modules
walterlow May 27, 2026
41b9627
refactor(timeline): collapse EQ field normalization into a data table
walterlow May 27, 2026
ac7aa20
perf(timeline): consolidate redundant store subscriptions on timeline…
walterlow May 27, 2026
863b7c4
test(timeline): cover items-store normalization helpers
walterlow May 27, 2026
8f7d29e
refactor(timeline): extract caption dialog state + transcribe control…
walterlow May 27, 2026
adbf6f7
refactor(timeline): extract fade-edit state machines into useFadeEditors
walterlow May 27, 2026
8ae4bc8
refactor(timeline): extract fade-ratio math into useFadeMath hook
walterlow May 27, 2026
32ceff4
refactor(timeline): consolidate edit-preview store subscriptions into…
walterlow May 27, 2026
4f15309
refactor(timeline): extract visual frame geometry into useTimelineIte…
walterlow May 27, 2026
f57253e
refactor(timeline): extract smart-trim hover state into useSmartTrimH…
walterlow May 27, 2026
86e3057
refactor(timeline): extract context-menu state into useContextMenuState
walterlow May 27, 2026
040a4ff
refactor(timeline): split EdgeHalos and TransitionDropGhost into memo…
walterlow May 27, 2026
f2bff6b
docs(timeline): document TimelineItem hook composition in CLAUDE.md
walterlow May 27, 2026
3ee94dc
refactor: code-quality cleanup from audit
walterlow May 27, 2026
b8319a7
fix(timeline): restore behavior regressed by TimelineItem hook extrac…
walterlow May 27, 2026
7679905
refactor: address remaining items from PR review
walterlow May 27, 2026
1ad7a3d
perf: eliminate O(N) scans in hot selectors and store reads
walterlow May 27, 2026
fb9011a
perf(timeline): render filmstrip/waveform at settled zoom to fix zoom…
walterlow May 28, 2026
7e8ddbb
perf(timeline): defer filmstrip/waveform mount during zoom to fix zoo…
walterlow May 28, 2026
bd0c444
perf(audio): speed up WAV encode and Int16/Float32 conversion on deco…
walterlow May 28, 2026
0f66c73
perf(audio): move full audio decode off the main thread (Phase 1)
walterlow May 28, 2026
196b403
perf(audio): move playback-window decode off the main thread (Phase 2)
walterlow May 28, 2026
6266331
perf(audio): persist decoded bins inside the worker (Phase 3)
walterlow May 28, 2026
bc2c2f9
perf(audio): skip redundant conform decode/encode on clip revisit
walterlow May 28, 2026
697a64e
perf(timeline): skip redundant filmstrip disk re-hydration on remount
walterlow May 28, 2026
f63b1af
chore: ignore DevTools performance trace exports
walterlow May 28, 2026
2ec22c3
perf(audio): assemble decoded bins off the main thread
walterlow May 28, 2026
d47a548
perf(timeline): cut forced reflows during timeline scroll/zoom
walterlow May 28, 2026
b6ae349
perf(timeline): use Math.sqrt over Math.hypot in compound waveform mi…
walterlow May 28, 2026
4b9f791
perf(timeline): skip off-screen clip interiors during zoom via conten…
walterlow May 28, 2026
46cabbe
fix(timeline): close perf-mark buffer leak and clip-content zoom defe…
walterlow May 28, 2026
817ee5b
feat(export): add one-click quality presets to export dialog
walterlow May 28, 2026
0dfc327
refactor(i18n): dissolve missing.json fallback bucket into per-featur…
walterlow May 28, 2026
5166a2a
feat(editor): caption auto-styling, per-item task progress, subtitle-…
walterlow May 28, 2026
cee8f9c
fix: address PR review findings across timeline, i18n, and perf marks
walterlow May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ coverage
.temp

*.mp4

# DevTools performance trace exports
Trace-*.json

# TanStack Router auto-generated files
# Note: Some teams commit this, uncomment if you prefer to commit
# src/routeTree.gen.ts
Expand Down
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ src/
- **State**: Zustand stores + Zundo for undo/redo
- **Timeline store split**: `useTimelineStore` (from `timeline-store.ts`) is a **facade** over domain stores (`items-store`, `transitions-store`, `keyframes-store`, `markers-store`, `timeline-settings-store`, `timeline-command-store`). Components use the facade with selectors; action code accesses domain stores via `.getState()` directly
- **Timeline mutations**: Action modules in `features/timeline/stores/actions/*.ts` use `execute()` wrapper from `shared.ts` for undo/redo integration. Never mutate timeline stores directly — use these actions
- **TimelineItem composition**: The per-clip component in `features/timeline/components/timeline-item/index.tsx` delegates to dedicated hooks: `useCaptionDialogState`, `useFadeEditors`, `useFadeMath`, `useEditPreviewShifts`, `useTimelineItemBounds`, `useSmartTrimHover`, `useContextMenuState`, plus the existing `useTimelineItemActions` / `useTimelineItemDropHandlers` / `useDragVisualState`. The host file orchestrates these hooks and renders the JSX; sub-components live alongside (`EdgeHalos`, `TransitionDropGhost`, `TranscribeDialogController`, etc.). When adding new clip state, prefer a new hook over inlining
- **Timeline item types**: `TimelineItem` is a discriminated union on `type`: `video | audio | text | image | shape | adjustment | composition` — GIFs use `image` type, no separate gif type. Types in `src/types/timeline.ts`
- **Item positioning**: Remotion convention — `from` (start frame in project FPS) + `durationInFrames`
- **Compositions**: Pre-compositions (sub-comps) have dedicated stores (`compositions-store.ts`, `composition-navigation-store.ts`). 1-level nesting only. Actions in `composition-actions.ts`
Expand Down Expand Up @@ -136,6 +137,9 @@ src/
- **Progressive downscaling** — when scaling high-res canvases to small sizes (e.g. 1920→320 thumbnails), halve dimensions repeatedly instead of one large jump. Single-step downscaling causes moire/aliasing with high-frequency GPU effects (halftone, pixelate, etc.)
- `StableVideoSequence`'s `areGroupPropsEqual` in `stable-video-sequence.tsx` whitelists item properties for React.memo comparison. When adding new visual properties to `TimelineItem`, add them to this comparison — missing properties cause stale renders during playback
- **GPU pipeline caching** — `EffectsPipeline.requestCachedDevice()` caches the WebGPU adapter + device globally. Subsequent `EffectsPipeline.create()` calls reuse the device (~50-100ms saved). The device-loss handler checks identity before clearing to avoid discarding a freshly acquired device. The preview component eagerly warms the GPU pipeline on mount (parallel with media resolution)
- **`__DEBUG__` API** — `window.__DEBUG__` (DEV-only, tree-shaken in prod) provides console debugging: `stores()`, `getTransitions()`, `getTransitionWindows()`, `getPlaybackState()`, `getTracks()`, `getMediaLibrary()`, `jitter()` (frame timing), `previewPerf()`, `transitionTrace()`, `prewarmCache()`, `filmstripMetrics()`, plus playback control (`seekTo`, `play`, `pause`). All use lazy `await import()` to avoid pulling in stores eagerly
- **`__DEBUG__` API** — `window.__DEBUG__` (DEV-only, tree-shaken in prod) provides console debugging: `stores()`, `getTransitions()`, `getTransitionWindows()`, `getPlaybackState()`, `getTracks()`, `getMediaLibrary()`, `jitter()` (frame timing), `previewPerf()`, `transitionTrace()`, `prewarmCache()`, `filmstripMetrics()`, `perfSummary(prefix?)` / `perfClear()` (User Timing aggregation, default prefix `tl.`), plus playback control (`seekTo`, `play`, `pause`). All use lazy `await import()` to avoid pulling in stores eagerly
- **Timeline perf-marks** — `withPerfMeasure(name, fn)` in `src/shared/logging/perf-marks.ts` wraps hot paths so they appear as named entries on the User Timing track in Chrome DevTools Performance. Currently instruments `tl.action.*` (every timeline mutation, via `actions/shared.ts::execute`), `tl.repairTransitions`, and the RAF loops `tl.raf.{viewportSync,previewHover,zoomApply,scrollThumb,momentum,playheadScrub}`. `withPerfMeasure` is opt-in — gated on `window.__TL_PERF__ = true` (off by default, zero overhead) so the User Timing buffer doesn't grow unbounded in normal use; set the flag before profiling (`npm run perf`), then read marks via the Performance tab or `__DEBUG__.perfSummary()`. `perfMarkRender(name)` adds per-render `tl.render.*` marks to the high-fanout components (ClipContent, TimelineItem, TimelineTrack, TimelineContent, TimelineMarkers, TimelinePlayhead, TransitionItem) — gated on `window.__TL_RENDER_MARKS__ = true` (off by default, zero overhead) for diagnosing which components re-render during a gesture
- **Clip content tracks SETTLED zoom** — `ClipContent` (`timeline-item/clip-content.tsx`) drives filmstrip/waveform width from `contentPixelsPerSecond` (settled, updates ~100ms after a zoom gesture ends), NOT the live per-frame `pixelsPerSecond`. The clip shell resizes smoothly during the gesture via the `--timeline-px-per-frame` CSS variable (no React); the filmstrip tile grid would otherwise rebuild on every wheel/momentum frame (~73% of zoom cost). During the gesture the content is briefly at pre-zoom scale, hidden by the repeating cover-frame background (zoom-in) or `overflow:hidden` clipping (zoom-out), snapping sharp on settle. The `preferImmediateRendering` prop opts back into live pps for active edit previews (trim/slide) where settle lag would distract
- **Clip content defers mount during zoom** — `ClipContent` also reads `isZoomInteracting` **once at mount** via `getState()` (not a reactive subscription) into `deferVisual` state. A clip that first mounts mid-gesture (e.g. entering the viewport while zooming out) renders only its colored shell — no filmstrip/waveform — until the zoom settles, then a one-shot `useZoomStore.subscribe` flips it on. This was ~90% of zoom-OUT cost: zooming out brings many clips into view at once and mounting each one's tile grid + canvas draws stalled the gesture (226ms/frame → ~42ms/frame). Reading at mount (not subscribing) is critical — already-mounted clips must NOT re-render when `isZoomInteracting` flips, or they'd flash empty
- **Transition prearm covers all types** — the `forceFastScrubOverlay` subscription uses `getPlayingAnyTransitionPrewarmStartFrame` (not complex-only) so all transitions get their session pinned and DOM video elements playing before entry. Also checks `getTransitionWindowForFrame` for playback starting inside an active transition
- **Feature boundary rules** — cross-feature imports must go through `deps/` adapter modules. The pre-push hook enforces this via `check:boundaries`. (A `check:legacy-lib-imports` tripwire also catches any reintroduction of `@/lib/*` imports — the `src/lib/` layer was removed and merged into `infrastructure/`.)
65 changes: 65 additions & 0 deletions src/app/debug/project-debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,19 @@ interface ProjectDebugAPI {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
overlaps: () => Promise<any>

// User Timing summary: aggregates `performance.measure` entries (default
// `tl.*` prefix from timeline perf-marks) into a per-name stat row.
perfSummary: (prefix?: string) => Array<{
name: string
count: number
minMs: number
maxMs: number
avgMs: number
p95Ms: number
totalMs: number
}>
perfClear: () => void

// Render pipeline diagnostics — delegates to existing ad-hoc window globals
// eslint-disable-next-line @typescript-eslint/no-explicit-any
previewPerf: () => any
Expand Down Expand Up @@ -481,6 +494,58 @@ function createDebugAPI(): ProjectDebugAPI {
}
},

perfSummary: (prefix = 'tl.') => {
if (
typeof performance === 'undefined' ||
typeof performance.getEntriesByType !== 'function'
) {
return []
}
const entries = performance.getEntriesByType('measure') as PerformanceMeasure[]
const byName = new Map<string, number[]>()
for (const entry of entries) {
if (!entry.name.startsWith(prefix)) continue
let durations = byName.get(entry.name)
if (!durations) {
durations = []
byName.set(entry.name, durations)
}
durations.push(entry.duration)
}
const rows: Array<{
name: string
count: number
minMs: number
maxMs: number
avgMs: number
p95Ms: number
totalMs: number
}> = []
for (const [name, durations] of byName) {
const sorted = [...durations].sort((a, b) => a - b)
const count = sorted.length
const total = durations.reduce((sum, value) => sum + value, 0)
const p95Index = Math.min(count - 1, Math.floor(count * 0.95))
rows.push({
name,
count,
minMs: Number((sorted[0] ?? 0).toFixed(3)),
maxMs: Number((sorted[count - 1] ?? 0).toFixed(3)),
avgMs: Number((total / count).toFixed(3)),
p95Ms: Number((sorted[p95Index] ?? 0).toFixed(3)),
totalMs: Number(total.toFixed(2)),
})
}
rows.sort((a, b) => b.totalMs - a.totalMs)
return rows
},

perfClear: () => {
if (typeof performance !== 'undefined' && typeof performance.clearMeasures === 'function') {
performance.clearMeasures()
}
},

// Render pipeline diagnostics — thin delegates to existing window globals
// so we never need to add/remove ad-hoc globals in components again.
previewPerf: () => {
Expand Down
9 changes: 5 additions & 4 deletions src/features/editor/components/media-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { useTimelineStore } from '@/features/editor/deps/timeline-store'
import { usePlaybackStore } from '@/shared/state/playback'
import { useSelectionStore } from '@/shared/state/selection'
import { useProjectStore } from '@/features/editor/deps/projects'
import { DEFAULT_PROJECT_HEIGHT, DEFAULT_PROJECT_WIDTH } from '@/shared/projects/defaults'
import {
clearMediaDragData,
MediaLibrary,
Expand Down Expand Up @@ -390,8 +391,8 @@ export const MediaSidebar = memo(function MediaSidebar() {
proposedPosition // Fallback to proposed if no space found

// Get canvas dimensions for initial transform
const canvasWidth = currentProject?.metadata.width ?? 1920
const canvasHeight = currentProject?.metadata.height ?? 1080
const canvasWidth = currentProject?.metadata.width ?? DEFAULT_PROJECT_WIDTH
const canvasHeight = currentProject?.metadata.height ?? DEFAULT_PROJECT_HEIGHT

const textStylePreset = presetId
? TEXT_STYLE_PRESETS.find((preset) => preset.id === presetId)
Expand Down Expand Up @@ -444,8 +445,8 @@ export const MediaSidebar = memo(function MediaSidebar() {
findNearestAvailableSpace(proposedPosition, durationInFrames, targetTrack.id, items) ??
proposedPosition

const canvasWidth = currentProject?.metadata.width ?? 1920
const canvasHeight = currentProject?.metadata.height ?? 1080
const canvasWidth = currentProject?.metadata.width ?? DEFAULT_PROJECT_WIDTH
const canvasHeight = currentProject?.metadata.height ?? DEFAULT_PROJECT_HEIGHT

const shapeItem: ShapeItem = createDefaultShapeItem({
trackId: targetTrack.id,
Expand Down
26 changes: 9 additions & 17 deletions src/features/editor/components/preview-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
InlineCompositionPreview,
ColorScopesMonitor,
} from '@/features/editor/deps/preview'
import { useTimelineStore } from '@/features/editor/deps/timeline-store'
import { useProjectStore } from '@/features/editor/deps/projects'
import { useSettingsStore } from '@/features/editor/deps/settings'
import { useMaskEditorStore, useItemsStore } from '@/features/editor/deps/preview'
Expand Down Expand Up @@ -178,20 +177,9 @@ export const PreviewArea = memo(function PreviewArea({ project }: PreviewAreaPro
const fps = projectFps ?? project.fps
const backgroundColor = projectBgColor ?? '#000000'

// Derive timeline end frame directly from store state to avoid recreating selector functions.
const timelineEndFrame = useTimelineStore((s) => {
if (s.items.length === 0) return null
let maxFrame = 0
for (const item of s.items) {
const itemEnd = item.from + item.durationInFrames
if (itemEnd > maxFrame) {
maxFrame = itemEnd
}
}
return maxFrame
})

const totalFrames = timelineEndFrame ?? fps * DEFAULT_EMPTY_TIMELINE_SECONDS
// Use the precomputed index from items-store; returns 0 when there are no items.
const maxItemEndFrame = useItemsStore((s) => s.maxItemEndFrame)
const totalFrames = maxItemEndFrame > 0 ? maxItemEndFrame : fps * DEFAULT_EMPTY_TIMELINE_SECONDS
const isPathEditModeActive = isMaskEditingActive && !isPenModeActive
const canFinishPenPath = isShapePenModeActive && penVertexCount >= 3
const selectedVertexCount = selectedVertexIndices.length
Expand Down Expand Up @@ -534,7 +522,9 @@ export const PreviewArea = memo(function PreviewArea({ project }: PreviewAreaPro
{isPenModeActive ? (
<div
className="border-t border-border panel-header flex items-center px-3 flex-shrink-0 gap-3 overflow-hidden"
style={{ height: EDITOR_LAYOUT_CSS_VALUES.previewControlsHeight }}
style={{
height: `calc(1.75rem + ${EDITOR_LAYOUT_CSS_VALUES.previewControlsHeight})`,
}}
role="toolbar"
aria-label="Path pen controls"
>
Expand Down Expand Up @@ -578,7 +568,9 @@ export const PreviewArea = memo(function PreviewArea({ project }: PreviewAreaPro
) : isPathEditModeActive ? (
<div
className="border-t border-border panel-header flex items-center px-3 flex-shrink-0 gap-3 overflow-hidden"
style={{ height: EDITOR_LAYOUT_CSS_VALUES.previewControlsHeight }}
style={{
height: `calc(1.75rem + ${EDITOR_LAYOUT_CSS_VALUES.previewControlsHeight})`,
}}
role="toolbar"
aria-label="Path edit controls"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { ArrowLeftRight, RotateCcw, LayoutDashboard, Clock } from 'lucide-react'
import { useProjectStore } from '@/features/editor/deps/projects'
import { DEFAULT_PROJECT_HEIGHT, DEFAULT_PROJECT_WIDTH } from '@/shared/projects/defaults'
import { useTimelineStore } from '@/features/editor/deps/timeline-store'
import { useGizmoStore } from '@/features/editor/deps/preview'
import { HexColorPicker } from 'react-colorful'
Expand Down Expand Up @@ -113,8 +114,8 @@ export const CanvasPanel = memo(function CanvasPanel() {
)

// All handlers must be defined before any early returns (Rules of Hooks)
const width = currentProject?.metadata.width ?? 1920
const height = currentProject?.metadata.height ?? 1080
const width = currentProject?.metadata.width ?? DEFAULT_PROJECT_WIDTH
const height = currentProject?.metadata.height ?? DEFAULT_PROJECT_HEIGHT
const storedBackgroundColor = currentProject?.metadata.backgroundColor ?? '#000000'

const applyProjectMetadataChange = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
type CaptionStylePreset,
detectActiveCaptionPreset,
resolveCaptionStylePatch,
} from './caption-style-presets'
} from '@/shared/typography/caption-style-presets'

type CaptionStylableItem = SubtitleSegmentItem | TextItem

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import { useEditorStore } from '@/shared/state/editor'
import { useSelectionStore } from '@/shared/state/selection'
import { useItemsStore, useTimelineStore } from '@/features/editor/deps/timeline-store'
import { useProjectStore } from '@/features/editor/deps/projects'
import {
DEFAULT_PROJECT_FPS,
DEFAULT_PROJECT_HEIGHT,
DEFAULT_PROJECT_WIDTH,
} from '@/shared/projects/defaults'
import type { ClipInspectorTab } from '@/shared/state/editor'
import type { SelectionState, SelectionActions } from '@/shared/state/selection'
import type { TimelineState, TimelineActions } from '@/features/editor/deps/timeline-store'
Expand Down Expand Up @@ -77,9 +82,13 @@ export const ClipPanel = memo(function ClipPanel() {
const updateItemsTransform = useTimelineStore(
(s: TimelineState & TimelineActions) => s.updateItemsTransform,
)
const projectWidth = useProjectStore((s) => s.currentProject?.metadata.width ?? 1920)
const projectHeight = useProjectStore((s) => s.currentProject?.metadata.height ?? 1080)
const projectFps = useProjectStore((s) => s.currentProject?.metadata.fps ?? 30)
const projectWidth = useProjectStore(
(s) => s.currentProject?.metadata.width ?? DEFAULT_PROJECT_WIDTH,
)
const projectHeight = useProjectStore(
(s) => s.currentProject?.metadata.height ?? DEFAULT_PROJECT_HEIGHT,
)
const projectFps = useProjectStore((s) => s.currentProject?.metadata.fps ?? DEFAULT_PROJECT_FPS)
const selectedItems = useItemsStore(
useShallow(
useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ export const LayoutSection = memo(function LayoutSection({
)

// Get media items for fallback source dimensions lookup
const mediaItems = useMediaLibraryStore((s) => s.mediaItems)
const mediaById = useMediaLibraryStore((s) => s.mediaById)

// Reset scale to source dimensions (1:1 scale)
// For shapes: reset to 1:1 aspect ratio (square based on smaller dimension)
Expand Down Expand Up @@ -599,7 +599,7 @@ export const LayoutSection = memo(function LayoutSection({

// Fallback: look up dimensions from media library if item has mediaId
if (!source && item.mediaId) {
const media = mediaItems.find((m) => m.id === item.mediaId)
const media = mediaById[item.mediaId]
if (media && media.width && media.height) {
source = { width: media.width, height: media.height }
}
Expand All @@ -622,7 +622,7 @@ export const LayoutSection = memo(function LayoutSection({
onTransformChange([item.id], updates)
})
queueMicrotask(clearTransformUiState)
}, [items, onTransformChange, mediaItems, canvas, clearTransformUiState])
}, [items, onTransformChange, mediaById, canvas, clearTransformUiState])

// Reset position to center (x=0, y=0)
const handleResetPosition = useCallback(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { i18n } from '@/i18n'

import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { DEFAULT_PROJECT_HEIGHT, DEFAULT_PROJECT_WIDTH } from '@/shared/projects/defaults'
import { useTimelineStore } from '@/features/editor/deps/timeline-store'
import { usePlaybackStore } from '@/shared/state/playback'
import {
Expand Down Expand Up @@ -57,8 +58,8 @@ export const SubtitleSection = memo(function SubtitleSection({

if (segments.length === 0) return null

const canvasWidth = canvas?.width ?? 1920
const canvasHeight = canvas?.height ?? 1080
const canvasWidth = canvas?.width ?? DEFAULT_PROJECT_WIDTH
const canvasHeight = canvas?.height ?? DEFAULT_PROJECT_HEIGHT

if (segments.length > 1) {
const totalCues = segments.reduce((sum, segment) => sum + segment.cues.length, 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
useTimelineCommandStore,
useTimelineStore,
} from '@/features/editor/deps/timeline-store'
import { DEFAULT_PROJECT_HEIGHT, DEFAULT_PROJECT_WIDTH } from '@/shared/projects/defaults'
import { useGizmoStore, useThrottledFrame } from '@/features/editor/deps/preview'
import type { TimelineState, TimelineActions } from '@/features/editor/deps/timeline-store'
import {
Expand Down Expand Up @@ -61,8 +62,8 @@ const CROP_EDGE_PROPERTY: Record<CropEdge, Exclude<CropProperty, 'cropSoftness'>

function getCropDimensions(item: VideoItem): CropDimensions {
return {
width: Math.max(1, item.sourceWidth ?? item.transform?.width ?? 1920),
height: Math.max(1, item.sourceHeight ?? item.transform?.height ?? 1080),
width: Math.max(1, item.sourceWidth ?? item.transform?.width ?? DEFAULT_PROJECT_WIDTH),
height: Math.max(1, item.sourceHeight ?? item.transform?.height ?? DEFAULT_PROJECT_HEIGHT),
}
}

Expand Down
Loading
Loading