summaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src')
-rw-r--r--apps/web/src/actions/components/shortcuts-dialog.tsx (renamed from apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx)4
-rw-r--r--apps/web/src/actions/definitions.ts (renamed from apps/web/src/lib/actions/definitions.ts)2
-rw-r--r--apps/web/src/actions/index.ts (renamed from apps/web/src/lib/actions/index.ts)0
-rw-r--r--apps/web/src/actions/keybinding.ts (renamed from apps/web/src/lib/actions/keybinding.ts)0
-rw-r--r--apps/web/src/actions/keybindings-store.ts (renamed from apps/web/src/stores/keybindings-store.ts)6
-rw-r--r--apps/web/src/actions/keybindings/migrations/index.ts (renamed from apps/web/src/stores/keybindings/migrations/index.ts)0
-rw-r--r--apps/web/src/actions/keybindings/migrations/v2-to-v3.ts (renamed from apps/web/src/stores/keybindings/migrations/v2-to-v3.ts)4
-rw-r--r--apps/web/src/actions/keybindings/migrations/v3-to-v4.ts (renamed from apps/web/src/stores/keybindings/migrations/v3-to-v4.ts)6
-rw-r--r--apps/web/src/actions/keybindings/migrations/v4-to-v5.ts (renamed from apps/web/src/stores/keybindings/migrations/v4-to-v5.ts)2
-rw-r--r--apps/web/src/actions/keybindings/migrations/v5-to-v6.ts (renamed from apps/web/src/stores/keybindings/migrations/v5-to-v6.ts)2
-rw-r--r--apps/web/src/actions/keybindings/migrations/v6-to-v7.ts (renamed from apps/web/src/stores/keybindings/migrations/v6-to-v7.ts)2
-rw-r--r--apps/web/src/actions/registry.ts (renamed from apps/web/src/lib/actions/registry.ts)0
-rw-r--r--apps/web/src/actions/types.ts (renamed from apps/web/src/lib/actions/types.ts)0
-rw-r--r--apps/web/src/actions/use-action-handler.ts (renamed from apps/web/src/hooks/actions/use-action-handler.ts)4
-rw-r--r--apps/web/src/actions/use-editor-actions.ts (renamed from apps/web/src/hooks/actions/use-editor-actions.ts)22
-rw-r--r--apps/web/src/actions/use-keybindings.ts (renamed from apps/web/src/hooks/use-keybindings.ts)6
-rw-r--r--apps/web/src/actions/use-keyboard-shortcuts-help.ts (renamed from apps/web/src/hooks/use-keyboard-shortcuts-help.ts)4
-rw-r--r--apps/web/src/animation/__tests__/binding-values.test.ts (renamed from apps/web/src/lib/animation/__tests__/binding-values.test.ts)2
-rw-r--r--apps/web/src/animation/bezier.ts (renamed from apps/web/src/lib/animation/bezier.ts)2
-rw-r--r--apps/web/src/animation/binding-values.ts (renamed from apps/web/src/lib/animation/binding-values.ts)2
-rw-r--r--apps/web/src/animation/curve-bridge.ts (renamed from apps/web/src/lib/animation/curve-bridge.ts)4
-rw-r--r--apps/web/src/animation/effect-param-channel.ts (renamed from apps/web/src/lib/animation/effect-param-channel.ts)6
-rw-r--r--apps/web/src/animation/graph-channels.ts (renamed from apps/web/src/lib/animation/graph-channels.ts)2
-rw-r--r--apps/web/src/animation/graphic-param-channel.ts (renamed from apps/web/src/lib/animation/graphic-param-channel.ts)6
-rw-r--r--apps/web/src/animation/index.ts (renamed from apps/web/src/lib/animation/index.ts)0
-rw-r--r--apps/web/src/animation/interpolation.ts (renamed from apps/web/src/lib/animation/interpolation.ts)2
-rw-r--r--apps/web/src/animation/keyframe-query.ts (renamed from apps/web/src/lib/animation/keyframe-query.ts)2
-rw-r--r--apps/web/src/animation/keyframes.ts (renamed from apps/web/src/lib/animation/keyframes.ts)2
-rw-r--r--apps/web/src/animation/property-groups.ts (renamed from apps/web/src/lib/animation/property-groups.ts)4
-rw-r--r--apps/web/src/animation/property-registry.ts (renamed from apps/web/src/lib/animation/property-registry.ts)14
-rw-r--r--apps/web/src/animation/resolve.ts (renamed from apps/web/src/lib/animation/resolve.ts)4
-rw-r--r--apps/web/src/animation/target-resolver.ts (renamed from apps/web/src/lib/animation/target-resolver.ts)16
-rw-r--r--apps/web/src/animation/timeline-snap-points.ts35
-rw-r--r--apps/web/src/animation/transform.ts (renamed from apps/web/src/lib/animation/transform.ts)0
-rw-r--r--apps/web/src/animation/types.ts (renamed from apps/web/src/lib/animation/types.ts)2
-rw-r--r--apps/web/src/app/api/auth/[...all]/route.ts2
-rw-r--r--apps/web/src/app/api/feedback/route.ts4
-rw-r--r--apps/web/src/app/api/sounds/search/route.ts4
-rw-r--r--apps/web/src/app/blog/[slug]/page.tsx4
-rw-r--r--apps/web/src/app/blog/page.tsx4
-rw-r--r--apps/web/src/app/changelog/[version]/page.tsx6
-rw-r--r--apps/web/src/app/changelog/page.tsx4
-rw-r--r--apps/web/src/app/contributors/page.tsx2
-rw-r--r--apps/web/src/app/editor/[project_id]/page.tsx76
-rw-r--r--apps/web/src/app/layout.tsx4
-rw-r--r--apps/web/src/app/metadata.ts2
-rw-r--r--apps/web/src/app/page.tsx2
-rw-r--r--apps/web/src/app/privacy/page.tsx2
-rw-r--r--apps/web/src/app/projects/page.tsx16
-rw-r--r--apps/web/src/app/projects/store.ts2
-rw-r--r--apps/web/src/app/robots.ts2
-rw-r--r--apps/web/src/app/rss.xml/route.ts4
-rw-r--r--apps/web/src/app/sitemap.ts4
-rw-r--r--apps/web/src/app/sponsors/page.tsx2
-rw-r--r--apps/web/src/app/terms/page.tsx2
-rw-r--r--apps/web/src/auth/client.ts (renamed from apps/web/src/lib/auth/client.ts)2
-rw-r--r--apps/web/src/auth/rate-limit.ts (renamed from apps/web/src/lib/rate-limit.ts)2
-rw-r--r--apps/web/src/auth/server.ts (renamed from apps/web/src/lib/auth/server.ts)4
-rw-r--r--apps/web/src/background/blur.ts (renamed from apps/web/src/lib/background/blur.ts)0
-rw-r--r--apps/web/src/background/color.ts (renamed from apps/web/src/lib/background/color.ts)0
-rw-r--r--apps/web/src/blog/query.ts (renamed from apps/web/src/lib/blog/query.ts)2
-rw-r--r--apps/web/src/blog/types.ts (renamed from apps/web/src/lib/blog/types.ts)0
-rw-r--r--apps/web/src/canvas/sizes.ts (renamed from apps/web/src/lib/canvas/sizes.ts)2
-rw-r--r--apps/web/src/changelog/components/changelog-notification.tsx (renamed from apps/web/src/lib/changelog/components/changelog-notification.tsx)0
-rw-r--r--apps/web/src/changelog/components/copy-markdown-button.tsx (renamed from apps/web/src/lib/changelog/components/copy-markdown-button.tsx)0
-rw-r--r--apps/web/src/changelog/components/release.tsx (renamed from apps/web/src/lib/changelog/components/release.tsx)0
-rw-r--r--apps/web/src/changelog/entries/0.1.0.md (renamed from apps/web/src/lib/changelog/entries/0.1.0.md)0
-rw-r--r--apps/web/src/changelog/entries/0.2.0.md (renamed from apps/web/src/lib/changelog/entries/0.2.0.md)0
-rw-r--r--apps/web/src/changelog/entries/0.3.0.md (renamed from apps/web/src/lib/changelog/entries/0.3.0.md)0
-rw-r--r--apps/web/src/changelog/entries/0.4.0.md (renamed from apps/web/src/lib/changelog/entries/0.4.0.md)0
-rw-r--r--apps/web/src/changelog/utils.ts (renamed from apps/web/src/lib/changelog/utils.ts)0
-rw-r--r--apps/web/src/clipboard/handlers/elements.ts (renamed from apps/web/src/lib/clipboard/handlers/elements.ts)2
-rw-r--r--apps/web/src/clipboard/handlers/index.ts (renamed from apps/web/src/lib/clipboard/handlers/index.ts)0
-rw-r--r--apps/web/src/clipboard/handlers/keyframes.ts (renamed from apps/web/src/lib/clipboard/handlers/keyframes.ts)8
-rw-r--r--apps/web/src/clipboard/index.ts (renamed from apps/web/src/lib/clipboard/index.ts)0
-rw-r--r--apps/web/src/clipboard/types.ts (renamed from apps/web/src/lib/clipboard/types.ts)6
-rw-r--r--apps/web/src/commands/base-command.ts (renamed from apps/web/src/lib/commands/base-command.ts)4
-rw-r--r--apps/web/src/commands/batch-command.ts (renamed from apps/web/src/lib/commands/batch-command.ts)0
-rw-r--r--apps/web/src/commands/index.ts (renamed from apps/web/src/lib/commands/index.ts)0
-rw-r--r--apps/web/src/commands/media/add-media-asset.ts (renamed from apps/web/src/lib/commands/media/add-media-asset.ts)10
-rw-r--r--apps/web/src/commands/media/index.ts (renamed from apps/web/src/lib/commands/media/index.ts)0
-rw-r--r--apps/web/src/commands/media/remove-media-asset.ts (renamed from apps/web/src/lib/commands/media/remove-media-asset.ts)10
-rw-r--r--apps/web/src/commands/preview-tracker.ts (renamed from apps/web/src/lib/commands/preview-tracker.ts)0
-rw-r--r--apps/web/src/commands/project/index.ts (renamed from apps/web/src/lib/commands/project/index.ts)0
-rw-r--r--apps/web/src/commands/project/update-project-settings.ts (renamed from apps/web/src/lib/commands/project/update-project-settings.ts)4
-rw-r--r--apps/web/src/commands/scene/create-scene.ts (renamed from apps/web/src/lib/commands/scene/create-scene.ts)6
-rw-r--r--apps/web/src/commands/scene/delete-scene.ts (renamed from apps/web/src/lib/commands/scene/delete-scene.ts)6
-rw-r--r--apps/web/src/commands/scene/index.ts (renamed from apps/web/src/lib/commands/scene/index.ts)0
-rw-r--r--apps/web/src/commands/scene/move-bookmark.ts (renamed from apps/web/src/lib/commands/scene/move-bookmark.ts)8
-rw-r--r--apps/web/src/commands/scene/remove-bookmark.ts (renamed from apps/web/src/lib/commands/scene/remove-bookmark.ts)8
-rw-r--r--apps/web/src/commands/scene/rename-scene.ts (renamed from apps/web/src/lib/commands/scene/rename-scene.ts)6
-rw-r--r--apps/web/src/commands/scene/toggle-bookmark.ts (renamed from apps/web/src/lib/commands/scene/toggle-bookmark.ts)11
-rw-r--r--apps/web/src/commands/scene/update-bookmark.ts (renamed from apps/web/src/lib/commands/scene/update-bookmark.ts)11
-rw-r--r--apps/web/src/commands/timeline/clipboard/index.ts (renamed from apps/web/src/lib/commands/timeline/clipboard/index.ts)0
-rw-r--r--apps/web/src/commands/timeline/clipboard/paste-keyframes.ts (renamed from apps/web/src/lib/commands/timeline/clipboard/paste-keyframes.ts)10
-rw-r--r--apps/web/src/commands/timeline/clipboard/paste.ts193
-rw-r--r--apps/web/src/commands/timeline/element/delete-elements.ts (renamed from apps/web/src/lib/commands/timeline/element/delete-elements.ts)6
-rw-r--r--apps/web/src/commands/timeline/element/duplicate-elements.ts134
-rw-r--r--apps/web/src/commands/timeline/element/effects/add-effect.ts (renamed from apps/web/src/lib/commands/timeline/element/effects/add-effect.ts)8
-rw-r--r--apps/web/src/commands/timeline/element/effects/index.ts (renamed from apps/web/src/lib/commands/timeline/element/effects/index.ts)0
-rw-r--r--apps/web/src/commands/timeline/element/effects/remove-effect.ts (renamed from apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts)6
-rw-r--r--apps/web/src/commands/timeline/element/effects/reorder-effect.ts (renamed from apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts)6
-rw-r--r--apps/web/src/commands/timeline/element/effects/toggle-effect.ts (renamed from apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts)6
-rw-r--r--apps/web/src/commands/timeline/element/effects/update-effect-params.ts (renamed from apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts)8
-rw-r--r--apps/web/src/commands/timeline/element/index.ts (renamed from apps/web/src/lib/commands/timeline/element/index.ts)0
-rw-r--r--apps/web/src/commands/timeline/element/insert-element.ts (renamed from apps/web/src/lib/commands/timeline/element/insert-element.ts)16
-rw-r--r--apps/web/src/commands/timeline/element/keyframes/index.ts (renamed from apps/web/src/lib/commands/timeline/element/keyframes/index.ts)0
-rw-r--r--apps/web/src/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts (renamed from apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts)10
-rw-r--r--apps/web/src/commands/timeline/element/keyframes/remove-keyframe.ts (renamed from apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts)10
-rw-r--r--apps/web/src/commands/timeline/element/keyframes/retime-keyframe.ts (renamed from apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts)10
-rw-r--r--apps/web/src/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts (renamed from apps/web/src/lib/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts)10
-rw-r--r--apps/web/src/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts (renamed from apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts)12
-rw-r--r--apps/web/src/commands/timeline/element/keyframes/upsert-keyframe.ts (renamed from apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts)10
-rw-r--r--apps/web/src/commands/timeline/element/masks/delete-custom-mask-points.ts (renamed from apps/web/src/lib/commands/timeline/element/masks/delete-custom-mask-points.ts)10
-rw-r--r--apps/web/src/commands/timeline/element/masks/index.ts (renamed from apps/web/src/lib/commands/timeline/element/masks/index.ts)0
-rw-r--r--apps/web/src/commands/timeline/element/masks/insert-custom-mask-point.ts (renamed from apps/web/src/lib/commands/timeline/element/masks/insert-custom-mask-point.ts)12
-rw-r--r--apps/web/src/commands/timeline/element/masks/remove-mask.ts (renamed from apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts)6
-rw-r--r--apps/web/src/commands/timeline/element/masks/toggle-mask-inverted.ts (renamed from apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts)8
-rw-r--r--apps/web/src/commands/timeline/element/move-elements.ts183
-rw-r--r--apps/web/src/commands/timeline/element/split-elements.ts182
-rw-r--r--apps/web/src/commands/timeline/element/toggle-source-audio-separation.ts (renamed from apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts)10
-rw-r--r--apps/web/src/commands/timeline/element/update-elements.ts (renamed from apps/web/src/lib/commands/timeline/element/update-elements.ts)8
-rw-r--r--apps/web/src/commands/timeline/index.ts (renamed from apps/web/src/lib/commands/timeline/index.ts)0
-rw-r--r--apps/web/src/commands/timeline/track/add-track.ts (renamed from apps/web/src/lib/commands/timeline/track/add-track.ts)6
-rw-r--r--apps/web/src/commands/timeline/track/index.ts (renamed from apps/web/src/lib/commands/timeline/track/index.ts)0
-rw-r--r--apps/web/src/commands/timeline/track/remove-track.ts (renamed from apps/web/src/lib/commands/timeline/track/remove-track.ts)4
-rw-r--r--apps/web/src/commands/timeline/track/toggle-track-mute.ts (renamed from apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts)6
-rw-r--r--apps/web/src/commands/timeline/track/toggle-track-visibility.ts (renamed from apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts)6
-rw-r--r--apps/web/src/commands/timeline/tracks-snapshot.ts (renamed from apps/web/src/lib/commands/timeline/tracks-snapshot.ts)4
-rw-r--r--apps/web/src/components/editor/editor-header.tsx14
-rw-r--r--apps/web/src/components/editor/export-button.tsx8
-rw-r--r--apps/web/src/components/editor/onboarding.tsx4
-rw-r--r--apps/web/src/components/editor/panels/assets/assets-panel-store.tsx (renamed from apps/web/src/stores/assets-panel-store.tsx)0
-rw-r--r--apps/web/src/components/editor/panels/assets/draggable-item.tsx6
-rw-r--r--apps/web/src/components/editor/panels/assets/index.tsx12
-rw-r--r--apps/web/src/components/editor/panels/assets/tabbar.tsx2
-rw-r--r--apps/web/src/components/editor/panels/assets/views/assets.tsx24
-rw-r--r--apps/web/src/components/editor/panels/assets/views/settings/background.tsx6
-rw-r--r--apps/web/src/components/editor/panels/assets/views/settings/index.tsx10
-rw-r--r--apps/web/src/components/editor/panels/preview/bookmark-note-overlay.tsx59
-rw-r--r--apps/web/src/components/editor/panels/preview/guide-overlay.tsx32
-rw-r--r--apps/web/src/components/editor/panels/properties/components/property-param-field.tsx2
-rw-r--r--apps/web/src/components/editor/panels/properties/hooks/use-element-playhead.ts4
-rw-r--r--apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts8
-rw-r--r--apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts8
-rw-r--r--apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts10
-rw-r--r--apps/web/src/components/editor/panels/properties/index.tsx4
-rw-r--r--apps/web/src/components/editor/panels/properties/registry.tsx20
-rw-r--r--apps/web/src/components/editor/scenes-view.tsx4
-rw-r--r--apps/web/src/components/footer.tsx4
-rw-r--r--apps/web/src/components/gitHub-contribute-section.tsx2
-rw-r--r--apps/web/src/components/header.tsx4
-rw-r--r--apps/web/src/components/providers/editor-provider.tsx12
-rw-r--r--apps/web/src/components/storage-provider.tsx2
-rw-r--r--apps/web/src/components/ui/font-picker.tsx8
-rw-r--r--apps/web/src/components/ui/use-overlay-open-change.ts2
-rw-r--r--apps/web/src/core/index.ts6
-rw-r--r--apps/web/src/core/managers/audio-manager.ts14
-rw-r--r--apps/web/src/core/managers/clipboard-manager.ts2
-rw-r--r--apps/web/src/core/managers/commands.ts8
-rw-r--r--apps/web/src/core/managers/diagnostics-manager.ts2
-rw-r--r--apps/web/src/core/managers/media-manager.ts4
-rw-r--r--apps/web/src/core/managers/playback-manager.ts2
-rw-r--r--apps/web/src/core/managers/project-manager.ts24
-rw-r--r--apps/web/src/core/managers/renderer-manager.ts6
-rw-r--r--apps/web/src/core/managers/scenes-manager.ts640
-rw-r--r--apps/web/src/core/managers/selection-manager.ts6
-rw-r--r--apps/web/src/core/managers/timeline-manager.ts24
-rw-r--r--apps/web/src/db/index.ts (renamed from apps/web/src/lib/db/index.ts)2
-rw-r--r--apps/web/src/db/schema.ts (renamed from apps/web/src/lib/db/schema.ts)0
-rw-r--r--apps/web/src/diagnostics/types.ts (renamed from apps/web/src/lib/diagnostics/types.ts)0
-rw-r--r--apps/web/src/editor/cancel-interaction.ts (renamed from apps/web/src/lib/cancel-interaction.ts)0
-rw-r--r--apps/web/src/editor/editor-store.ts (renamed from apps/web/src/stores/editor-store.ts)4
-rw-r--r--apps/web/src/editor/panel-store.ts (renamed from apps/web/src/stores/panel-store.ts)2
-rw-r--r--apps/web/src/editor/use-editor.ts (renamed from apps/web/src/hooks/use-editor.ts)0
-rw-r--r--apps/web/src/editor/use-menu-preview.ts (renamed from apps/web/src/hooks/use-menu-preview.ts)2
-rw-r--r--apps/web/src/effects/components/assets-view.tsx (renamed from apps/web/src/components/editor/panels/assets/views/effects.tsx)8
-rw-r--r--apps/web/src/effects/components/effects-tab.tsx (renamed from apps/web/src/components/editor/panels/properties/tabs/effects-tab.tsx)16
-rw-r--r--apps/web/src/effects/definitions/blur.ts (renamed from apps/web/src/lib/effects/definitions/blur.ts)2
-rw-r--r--apps/web/src/effects/definitions/index.ts (renamed from apps/web/src/lib/effects/definitions/index.ts)0
-rw-r--r--apps/web/src/effects/index.ts (renamed from apps/web/src/lib/effects/index.ts)8
-rw-r--r--apps/web/src/effects/registry.ts (renamed from apps/web/src/lib/effects/registry.ts)4
-rw-r--r--apps/web/src/effects/types.ts (renamed from apps/web/src/lib/effects/types.ts)2
-rw-r--r--apps/web/src/env/web.ts (renamed from apps/web/src/lib/env/web.ts)0
-rw-r--r--apps/web/src/export/defaults.ts (renamed from apps/web/src/lib/export/defaults.ts)0
-rw-r--r--apps/web/src/export/index.ts (renamed from apps/web/src/lib/export/index.ts)0
-rw-r--r--apps/web/src/export/mime-types.ts (renamed from apps/web/src/lib/export/mime-types.ts)0
-rw-r--r--apps/web/src/feedback/components/feedback-popover.tsx (renamed from apps/web/src/lib/feedback/components/feedback-popover.tsx)0
-rw-r--r--apps/web/src/feedback/index.ts (renamed from apps/web/src/lib/feedback/index.ts)0
-rw-r--r--apps/web/src/feedback/queries.ts (renamed from apps/web/src/lib/feedback/queries.ts)2
-rw-r--r--apps/web/src/feedback/types.ts (renamed from apps/web/src/lib/feedback/types.ts)0
-rw-r--r--apps/web/src/fonts/google-fonts.ts (renamed from apps/web/src/lib/fonts/google-fonts.ts)4
-rw-r--r--apps/web/src/fonts/system-fonts.ts (renamed from apps/web/src/lib/fonts/system-fonts.ts)0
-rw-r--r--apps/web/src/fonts/types.ts (renamed from apps/web/src/lib/fonts/types.ts)0
-rw-r--r--apps/web/src/fonts/use-font-atlas.ts (renamed from apps/web/src/hooks/use-font-atlas.ts)6
-rw-r--r--apps/web/src/fps/__tests__/fps.test.ts (renamed from apps/web/src/lib/fps/__tests__/fps.test.ts)2
-rw-r--r--apps/web/src/fps/defaults.ts (renamed from apps/web/src/lib/fps/defaults.ts)0
-rw-r--r--apps/web/src/fps/presets.ts (renamed from apps/web/src/lib/fps/presets.ts)0
-rw-r--r--apps/web/src/fps/utils.ts (renamed from apps/web/src/lib/fps/utils.ts)2
-rw-r--r--apps/web/src/gradients/canvas.ts (renamed from apps/web/src/lib/gradients/canvas.ts)0
-rw-r--r--apps/web/src/gradients/index.ts (renamed from apps/web/src/lib/gradients/index.ts)0
-rw-r--r--apps/web/src/gradients/parser.ts (renamed from apps/web/src/lib/gradients/parser.ts)0
-rw-r--r--apps/web/src/graphics/components/graphic-tab.tsx (renamed from apps/web/src/components/editor/panels/properties/tabs/graphic-tab.tsx)54
-rw-r--r--apps/web/src/graphics/definitions/ellipse.ts (renamed from apps/web/src/lib/graphics/definitions/ellipse.ts)2
-rw-r--r--apps/web/src/graphics/definitions/index.ts (renamed from apps/web/src/lib/graphics/definitions/index.ts)0
-rw-r--r--apps/web/src/graphics/definitions/polygon.ts (renamed from apps/web/src/lib/graphics/definitions/polygon.ts)2
-rw-r--r--apps/web/src/graphics/definitions/rectangle.ts (renamed from apps/web/src/lib/graphics/definitions/rectangle.ts)2
-rw-r--r--apps/web/src/graphics/definitions/shared.ts (renamed from apps/web/src/lib/graphics/definitions/shared.ts)2
-rw-r--r--apps/web/src/graphics/definitions/star.ts (renamed from apps/web/src/lib/graphics/definitions/star.ts)2
-rw-r--r--apps/web/src/graphics/index.ts (renamed from apps/web/src/lib/graphics/index.ts)4
-rw-r--r--apps/web/src/graphics/registry.ts (renamed from apps/web/src/lib/graphics/registry.ts)2
-rw-r--r--apps/web/src/graphics/stroke.ts (renamed from apps/web/src/lib/graphics/stroke.ts)0
-rw-r--r--apps/web/src/graphics/types.ts (renamed from apps/web/src/lib/graphics/types.ts)2
-rw-r--r--apps/web/src/guides/definitions/custom.tsx (renamed from apps/web/src/lib/guides/definitions/custom.tsx)2
-rw-r--r--apps/web/src/guides/definitions/grid.tsx (renamed from apps/web/src/lib/guides/definitions/grid.tsx)6
-rw-r--r--apps/web/src/guides/definitions/platforms.tsx (renamed from apps/web/src/lib/guides/definitions/platforms.tsx)2
-rw-r--r--apps/web/src/guides/definitions/tiktok-layout.tsx (renamed from apps/web/src/lib/guides/definitions/tiktok-layout.tsx)0
-rw-r--r--apps/web/src/guides/grid.ts (renamed from apps/web/src/lib/guides/grid.ts)0
-rw-r--r--apps/web/src/guides/index.ts (renamed from apps/web/src/lib/guides/index.ts)27
-rw-r--r--apps/web/src/guides/preview-overlay.tsx33
-rw-r--r--apps/web/src/guides/registry.tsx (renamed from apps/web/src/lib/guides/registry.tsx)4
-rw-r--r--apps/web/src/guides/types.ts (renamed from apps/web/src/lib/guides/types.ts)0
-rw-r--r--apps/web/src/lib/selection/index.ts4
-rw-r--r--apps/web/src/lib/timeline/snap-utils.ts182
-rw-r--r--apps/web/src/masks/__tests__/snap.test.ts (renamed from apps/web/src/lib/masks/__tests__/snap.test.ts)16
-rw-r--r--apps/web/src/masks/components/masks-tab.tsx (renamed from apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx)21
-rw-r--r--apps/web/src/masks/custom-path.ts (renamed from apps/web/src/lib/masks/custom-path.ts)2
-rw-r--r--apps/web/src/masks/definitions/box-like.ts (renamed from apps/web/src/lib/masks/definitions/box-like.ts)10
-rw-r--r--apps/web/src/masks/definitions/cinematic-bars.ts (renamed from apps/web/src/lib/masks/definitions/cinematic-bars.ts)2
-rw-r--r--apps/web/src/masks/definitions/custom.ts (renamed from apps/web/src/lib/masks/definitions/custom.ts)18
-rw-r--r--apps/web/src/masks/definitions/diamond.ts (renamed from apps/web/src/lib/masks/definitions/diamond.ts)2
-rw-r--r--apps/web/src/masks/definitions/ellipse.ts (renamed from apps/web/src/lib/masks/definitions/ellipse.ts)2
-rw-r--r--apps/web/src/masks/definitions/heart.ts (renamed from apps/web/src/lib/masks/definitions/heart.ts)2
-rw-r--r--apps/web/src/masks/definitions/index.ts (renamed from apps/web/src/lib/masks/definitions/index.ts)2
-rw-r--r--apps/web/src/masks/definitions/rectangle.ts (renamed from apps/web/src/lib/masks/definitions/rectangle.ts)2
-rw-r--r--apps/web/src/masks/definitions/split.ts (renamed from apps/web/src/lib/masks/definitions/split.ts)6
-rw-r--r--apps/web/src/masks/definitions/star.ts (renamed from apps/web/src/lib/masks/definitions/star.ts)2
-rw-r--r--apps/web/src/masks/definitions/text.ts (renamed from apps/web/src/lib/masks/definitions/text.ts)22
-rw-r--r--apps/web/src/masks/dimensions.ts (renamed from apps/web/src/lib/masks/dimensions.ts)0
-rw-r--r--apps/web/src/masks/feather.ts (renamed from apps/web/src/lib/masks/feather.ts)0
-rw-r--r--apps/web/src/masks/geometry.ts (renamed from apps/web/src/lib/masks/geometry.ts)8
-rw-r--r--apps/web/src/masks/handle-positions.ts (renamed from apps/web/src/lib/masks/handle-positions.ts)6
-rw-r--r--apps/web/src/masks/index.ts (renamed from apps/web/src/lib/masks/index.ts)2
-rw-r--r--apps/web/src/masks/param-update.ts (renamed from apps/web/src/lib/masks/param-update.ts)2
-rw-r--r--apps/web/src/masks/registry.ts (renamed from apps/web/src/lib/masks/registry.ts)8
-rw-r--r--apps/web/src/masks/snap.ts (renamed from apps/web/src/lib/masks/snap.ts)8
-rw-r--r--apps/web/src/masks/types.ts (renamed from apps/web/src/lib/masks/types.ts)10
-rw-r--r--apps/web/src/masks/use-mask-handles.ts (renamed from apps/web/src/hooks/use-mask-handles.ts)22
-rw-r--r--apps/web/src/masks/utils.ts (renamed from apps/web/src/lib/masks/utils.ts)0
-rw-r--r--apps/web/src/media/audio-mastering.ts (renamed from apps/web/src/lib/media/audio-mastering.ts)0
-rw-r--r--apps/web/src/media/audio.ts (renamed from apps/web/src/lib/media/audio.ts)26
-rw-r--r--apps/web/src/media/media-utils.ts (renamed from apps/web/src/lib/media/media-utils.ts)2
-rw-r--r--apps/web/src/media/mediabunny.ts (renamed from apps/web/src/lib/media/mediabunny.ts)8
-rw-r--r--apps/web/src/media/processing.ts (renamed from apps/web/src/lib/media/processing.ts)4
-rw-r--r--apps/web/src/media/types.ts (renamed from apps/web/src/lib/media/types.ts)0
-rw-r--r--apps/web/src/media/upload-toast.ts (renamed from apps/web/src/lib/media/upload-toast.ts)0
-rw-r--r--apps/web/src/media/use-file-upload.ts (renamed from apps/web/src/hooks/use-file-upload.ts)2
-rw-r--r--apps/web/src/media/use-paste-media.ts (renamed from apps/web/src/hooks/use-paste-media.ts)20
-rw-r--r--apps/web/src/media/waveform-summary.ts (renamed from apps/web/src/lib/media/waveform-summary.ts)4
-rw-r--r--apps/web/src/panels/layout.ts (renamed from apps/web/src/lib/panels/layout.ts)0
-rw-r--r--apps/web/src/params/index.ts (renamed from apps/web/src/lib/params.ts)0
-rw-r--r--apps/web/src/params/registry.ts (renamed from apps/web/src/lib/registry.ts)2
-rw-r--r--apps/web/src/preview/components/context-menu.tsx (renamed from apps/web/src/components/editor/panels/preview/context-menu.tsx)158
-rw-r--r--apps/web/src/preview/components/cursors.ts (renamed from apps/web/src/components/editor/panels/preview/cursors.ts)0
-rw-r--r--apps/web/src/preview/components/guide-popover.tsx (renamed from apps/web/src/components/editor/panels/preview/guide-popover.tsx)4
-rw-r--r--apps/web/src/preview/components/handle-primitives.tsx (renamed from apps/web/src/components/editor/panels/preview/handle-primitives.tsx)0
-rw-r--r--apps/web/src/preview/components/index.tsx (renamed from apps/web/src/components/editor/panels/preview/index.tsx)68
-rw-r--r--apps/web/src/preview/components/mask-handles.tsx (renamed from apps/web/src/components/editor/panels/preview/mask-handles.tsx)8
-rw-r--r--apps/web/src/preview/components/overlay-layer.tsx197
-rw-r--r--apps/web/src/preview/components/preview-interaction-overlay.tsx (renamed from apps/web/src/components/editor/panels/preview/preview-interaction-overlay.tsx)10
-rw-r--r--apps/web/src/preview/components/preview-viewport.tsx (renamed from apps/web/src/components/editor/panels/preview/preview-viewport.tsx)4
-rw-r--r--apps/web/src/preview/components/snap-guides.tsx (renamed from apps/web/src/components/editor/panels/preview/snap-guides.tsx)4
-rw-r--r--apps/web/src/preview/components/text-edit-overlay.tsx (renamed from apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx)12
-rw-r--r--apps/web/src/preview/components/toolbar.tsx (renamed from apps/web/src/components/editor/panels/preview/toolbar.tsx)10
-rw-r--r--apps/web/src/preview/components/transform-handles.tsx (renamed from apps/web/src/components/editor/panels/preview/transform-handles.tsx)10
-rw-r--r--apps/web/src/preview/element-bounds.ts (renamed from apps/web/src/lib/preview/element-bounds.ts)12
-rw-r--r--apps/web/src/preview/hit-test.ts (renamed from apps/web/src/lib/preview/hit-test.ts)2
-rw-r--r--apps/web/src/preview/hooks/use-preview-interaction.ts (renamed from apps/web/src/hooks/use-preview-interaction.ts)18
-rw-r--r--apps/web/src/preview/hooks/use-transform-handles.ts (renamed from apps/web/src/hooks/use-transform-handles.ts)64
-rw-r--r--apps/web/src/preview/overlays.ts108
-rw-r--r--apps/web/src/preview/preview-coords.ts (renamed from apps/web/src/lib/preview/preview-coords.ts)0
-rw-r--r--apps/web/src/preview/preview-snap.ts (renamed from apps/web/src/lib/preview/preview-snap.ts)0
-rw-r--r--apps/web/src/preview/preview-store.ts (renamed from apps/web/src/stores/preview-store.ts)216
-rw-r--r--apps/web/src/preview/zoom.ts (renamed from apps/web/src/lib/preview/zoom.ts)0
-rw-r--r--apps/web/src/project/components/delete-project-dialog.tsx (renamed from apps/web/src/components/editor/dialogs/delete-project-dialog.tsx)0
-rw-r--r--apps/web/src/project/components/migration-dialog.tsx (renamed from apps/web/src/components/editor/dialogs/migration-dialog.tsx)2
-rw-r--r--apps/web/src/project/components/project-info-dialog.tsx (renamed from apps/web/src/components/editor/dialogs/project-info-dialog.tsx)2
-rw-r--r--apps/web/src/project/components/rename-project-dialog.tsx (renamed from apps/web/src/components/editor/dialogs/rename-project-dialog.tsx)0
-rw-r--r--apps/web/src/project/types.ts (renamed from apps/web/src/lib/project/types.ts)2
-rw-r--r--apps/web/src/rendering/components/blending-tab.tsx (renamed from apps/web/src/components/editor/panels/properties/tabs/blending-tab.tsx)20
-rw-r--r--apps/web/src/rendering/components/transform-tab.tsx (renamed from apps/web/src/components/editor/panels/properties/tabs/transform-tab.tsx)16
-rw-r--r--apps/web/src/rendering/index.ts (renamed from apps/web/src/lib/rendering.ts)0
-rw-r--r--apps/web/src/retime/__tests__/resolve.test.ts (renamed from apps/web/src/lib/retime/__tests__/resolve.test.ts)4
-rw-r--r--apps/web/src/retime/__tests__/split.test.ts (renamed from apps/web/src/lib/retime/__tests__/split.test.ts)4
-rw-r--r--apps/web/src/retime/audio-stretch.ts (renamed from apps/web/src/lib/retime/audio-stretch.ts)4
-rw-r--r--apps/web/src/retime/index.ts (renamed from apps/web/src/lib/retime/index.ts)0
-rw-r--r--apps/web/src/retime/presets.ts (renamed from apps/web/src/lib/retime/presets.ts)4
-rw-r--r--apps/web/src/retime/rate.ts (renamed from apps/web/src/lib/retime/rate.ts)0
-rw-r--r--apps/web/src/retime/resolve.ts (renamed from apps/web/src/lib/retime/resolve.ts)4
-rw-r--r--apps/web/src/retime/split.ts (renamed from apps/web/src/lib/retime/split.ts)2
-rw-r--r--apps/web/src/ripple/apply.ts (renamed from apps/web/src/lib/ripple/apply.ts)2
-rw-r--r--apps/web/src/ripple/diff.ts (renamed from apps/web/src/lib/ripple/diff.ts)2
-rw-r--r--apps/web/src/ripple/index.ts (renamed from apps/web/src/lib/ripple/index.ts)0
-rw-r--r--apps/web/src/ripple/shift.ts (renamed from apps/web/src/lib/ripple/shift.ts)2
-rw-r--r--apps/web/src/selection/attributes.ts (renamed from apps/web/src/lib/selection/attributes.ts)0
-rw-r--r--apps/web/src/selection/context.tsx (renamed from apps/web/src/lib/selection/context.tsx)0
-rw-r--r--apps/web/src/selection/editor-selection.ts (renamed from apps/web/src/lib/selection/editor-selection.ts)4
-rw-r--r--apps/web/src/selection/hit-testing.ts (renamed from apps/web/src/lib/selection/hit-testing.ts)0
-rw-r--r--apps/web/src/selection/hooks/use-box-select.ts (renamed from apps/web/src/lib/selection/hooks/use-box-select.ts)340
-rw-r--r--apps/web/src/selection/hooks/use-selection-scope.ts (renamed from apps/web/src/lib/selection/hooks/use-selection-scope.ts)4
-rw-r--r--apps/web/src/selection/index.ts4
-rw-r--r--apps/web/src/selection/scope.ts (renamed from apps/web/src/lib/selection/scope.ts)0
-rw-r--r--apps/web/src/selection/selectable-item.tsx (renamed from apps/web/src/lib/selection/selectable-item.tsx)6
-rw-r--r--apps/web/src/selection/selectable-surface.tsx (renamed from apps/web/src/lib/selection/selectable-surface.tsx)14
-rw-r--r--apps/web/src/selection/selection-box.tsx (renamed from apps/web/src/lib/selection/selection-box.tsx)0
-rw-r--r--apps/web/src/selection/state.ts (renamed from apps/web/src/lib/selection/state.ts)2
-rw-r--r--apps/web/src/selection/types.ts (renamed from apps/web/src/lib/selection/types.ts)0
-rw-r--r--apps/web/src/services/renderer/compositor/frame-descriptor.ts6
-rw-r--r--apps/web/src/services/renderer/compositor/types.ts4
-rw-r--r--apps/web/src/services/renderer/effect-preview.ts6
-rw-r--r--apps/web/src/services/renderer/gpu-renderer.ts2
-rw-r--r--apps/web/src/services/renderer/nodes/blur-background-node.ts4
-rw-r--r--apps/web/src/services/renderer/nodes/effect-layer-node.ts4
-rw-r--r--apps/web/src/services/renderer/nodes/graphic-node.ts4
-rw-r--r--apps/web/src/services/renderer/nodes/sticker-node.ts2
-rw-r--r--apps/web/src/services/renderer/nodes/text-node.ts10
-rw-r--r--apps/web/src/services/renderer/nodes/visual-node.ts8
-rw-r--r--apps/web/src/services/renderer/resolve.ts16
-rw-r--r--apps/web/src/services/renderer/scene-builder.ts8
-rw-r--r--apps/web/src/services/renderer/scene-exporter.ts6
-rw-r--r--apps/web/src/services/storage/components/storage-persistence-dialog.tsx (renamed from apps/web/src/components/editor/dialogs/storage-persistence-dialog.tsx)2
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts6
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts6
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts2
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts4
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts2
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts2
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts2
-rw-r--r--apps/web/src/services/storage/service.ts10
-rw-r--r--apps/web/src/services/storage/types.ts6
-rw-r--r--apps/web/src/services/storage/use-local-storage.ts (renamed from apps/web/src/hooks/storage/use-local-storage.ts)0
-rw-r--r--apps/web/src/services/storage/use-storage-persistence.ts (renamed from apps/web/src/hooks/use-storage-persistence.ts)0
-rw-r--r--apps/web/src/services/transcription/service.ts4
-rw-r--r--apps/web/src/services/transcription/worker.ts4
-rw-r--r--apps/web/src/services/waveform-cache/service.ts4
-rw-r--r--apps/web/src/site/brand.ts (renamed from apps/web/src/lib/site/brand.ts)0
-rw-r--r--apps/web/src/site/external-tools.ts (renamed from apps/web/src/lib/site/external-tools.ts)0
-rw-r--r--apps/web/src/site/social.ts (renamed from apps/web/src/lib/site/social.ts)0
-rw-r--r--apps/web/src/site/sponsors.ts (renamed from apps/web/src/lib/site/sponsors.ts)0
-rw-r--r--apps/web/src/sounds/components/assets-view.tsx (renamed from apps/web/src/components/editor/panels/assets/views/sounds.tsx)6
-rw-r--r--apps/web/src/sounds/sounds-store.ts (renamed from apps/web/src/stores/sounds-store.ts)4
-rw-r--r--apps/web/src/sounds/types.ts (renamed from apps/web/src/lib/sounds/types.ts)0
-rw-r--r--apps/web/src/sounds/use-sound-search.ts (renamed from apps/web/src/hooks/use-sound-search.ts)2
-rw-r--r--apps/web/src/speed/components/speed-tab.tsx (renamed from apps/web/src/components/editor/panels/properties/tabs/speed-tab.tsx)10
-rw-r--r--apps/web/src/stickers/__tests__/sticker-id.test.ts (renamed from apps/web/src/lib/stickers/__tests__/sticker-id.test.ts)0
-rw-r--r--apps/web/src/stickers/categories.ts (renamed from apps/web/src/lib/stickers/categories.ts)0
-rw-r--r--apps/web/src/stickers/components/assets-view.tsx (renamed from apps/web/src/components/editor/panels/assets/views/stickers.tsx)18
-rw-r--r--apps/web/src/stickers/index.ts (renamed from apps/web/src/lib/stickers/index.ts)6
-rw-r--r--apps/web/src/stickers/intrinsic-size.ts (renamed from apps/web/src/lib/stickers/intrinsic-size.ts)0
-rw-r--r--apps/web/src/stickers/providers/countries-data.ts (renamed from apps/web/src/lib/stickers/providers/countries-data.ts)0
-rw-r--r--apps/web/src/stickers/providers/flags.ts (renamed from apps/web/src/lib/stickers/providers/flags.ts)0
-rw-r--r--apps/web/src/stickers/providers/index.ts (renamed from apps/web/src/lib/stickers/providers/index.ts)2
-rw-r--r--apps/web/src/stickers/providers/logos.ts (renamed from apps/web/src/lib/stickers/providers/logos.ts)0
-rw-r--r--apps/web/src/stickers/providers/shapes.ts (renamed from apps/web/src/lib/stickers/providers/shapes.ts)4
-rw-r--r--apps/web/src/stickers/registry.ts (renamed from apps/web/src/lib/stickers/registry.ts)4
-rw-r--r--apps/web/src/stickers/resolver.ts (renamed from apps/web/src/lib/stickers/resolver.ts)2
-rw-r--r--apps/web/src/stickers/sticker-id.ts (renamed from apps/web/src/lib/stickers/sticker-id.ts)0
-rw-r--r--apps/web/src/stickers/stickers-store.ts (renamed from apps/web/src/stores/stickers-store.ts)14
-rw-r--r--apps/web/src/stickers/types.ts (renamed from apps/web/src/lib/stickers/types.ts)2
-rw-r--r--apps/web/src/stores/properties-store.ts19
-rw-r--r--apps/web/src/subtitles/ass.ts (renamed from apps/web/src/lib/subtitles/ass.ts)0
-rw-r--r--apps/web/src/subtitles/build-subtitle-text-element.ts (renamed from apps/web/src/lib/subtitles/build-subtitle-text-element.ts)10
-rw-r--r--apps/web/src/subtitles/components/assets-view.tsx (renamed from apps/web/src/components/editor/panels/assets/views/captions.tsx)22
-rw-r--r--apps/web/src/subtitles/insert.ts (renamed from apps/web/src/lib/subtitles/insert.ts)2
-rw-r--r--apps/web/src/subtitles/parse.ts (renamed from apps/web/src/lib/subtitles/parse.ts)0
-rw-r--r--apps/web/src/subtitles/srt.ts (renamed from apps/web/src/lib/subtitles/srt.ts)0
-rw-r--r--apps/web/src/subtitles/types.ts (renamed from apps/web/src/lib/subtitles/types.ts)4
-rw-r--r--apps/web/src/text/background.ts (renamed from apps/web/src/lib/text/background.ts)0
-rw-r--r--apps/web/src/text/components/assets-view.tsx (renamed from apps/web/src/components/editor/panels/assets/views/text.tsx)6
-rw-r--r--apps/web/src/text/components/text-tab.tsx (renamed from apps/web/src/components/editor/panels/properties/tabs/text-tab.tsx)27
-rw-r--r--apps/web/src/text/layout.ts (renamed from apps/web/src/lib/text/layout.ts)4
-rw-r--r--apps/web/src/text/measure-element.ts (renamed from apps/web/src/lib/text/measure-element.ts)8
-rw-r--r--apps/web/src/text/primitives.ts (renamed from apps/web/src/lib/text/primitives.ts)4
-rw-r--r--apps/web/src/text/typography.ts (renamed from apps/web/src/lib/text/typography.ts)0
-rw-r--r--apps/web/src/timeline/audio-constants.ts (renamed from apps/web/src/lib/timeline/audio-constants.ts)0
-rw-r--r--apps/web/src/timeline/audio-display.ts (renamed from apps/web/src/lib/timeline/audio-display.ts)0
-rw-r--r--apps/web/src/timeline/audio-separation/index.ts (renamed from apps/web/src/lib/timeline/audio-separation/index.ts)8
-rw-r--r--apps/web/src/timeline/audio-state.ts (renamed from apps/web/src/lib/timeline/audio-state.ts)6
-rw-r--r--apps/web/src/timeline/bookmarks/components/bookmarks.tsx (renamed from apps/web/src/components/editor/panels/timeline/bookmarks.tsx)24
-rw-r--r--apps/web/src/timeline/bookmarks/hooks/use-bookmark-drag.ts (renamed from apps/web/src/hooks/timeline/use-bookmark-drag.ts)58
-rw-r--r--apps/web/src/timeline/bookmarks/index.ts19
-rw-r--r--apps/web/src/timeline/bookmarks/preview-overlay-source.tsx85
-rw-r--r--apps/web/src/timeline/bookmarks/snap-source.ts20
-rw-r--r--apps/web/src/timeline/bookmarks/utils.ts (renamed from apps/web/src/lib/timeline/bookmarks.ts)2
-rw-r--r--apps/web/src/timeline/components/audio-tab.tsx (renamed from apps/web/src/components/editor/panels/properties/tabs/audio-tab.tsx)18
-rw-r--r--apps/web/src/timeline/components/audio-volume-line.tsx (renamed from apps/web/src/components/editor/panels/timeline/audio-volume-line.tsx)12
-rw-r--r--apps/web/src/timeline/components/audio-waveform.tsx (renamed from apps/web/src/components/editor/panels/timeline/audio-waveform.tsx)6
-rw-r--r--apps/web/src/timeline/components/drag-line.tsx (renamed from apps/web/src/components/editor/panels/timeline/drag-line.tsx)2
-rw-r--r--apps/web/src/timeline/components/drop-target.ts (renamed from apps/web/src/components/editor/panels/timeline/drop-target.ts)8
-rw-r--r--apps/web/src/timeline/components/expanded-layout.ts (renamed from apps/web/src/components/editor/panels/timeline/expanded-layout.ts)6
-rw-r--r--apps/web/src/timeline/components/graph-editor/bezier-graph.tsx (renamed from apps/web/src/components/editor/panels/timeline/graph-editor/bezier-graph.tsx)4
-rw-r--r--apps/web/src/timeline/components/graph-editor/custom-presets-store.ts (renamed from apps/web/src/components/editor/panels/timeline/graph-editor/custom-presets-store.ts)2
-rw-r--r--apps/web/src/timeline/components/graph-editor/easing-presets.ts (renamed from apps/web/src/components/editor/panels/timeline/graph-editor/easing-presets.ts)2
-rw-r--r--apps/web/src/timeline/components/graph-editor/popover.tsx (renamed from apps/web/src/components/editor/panels/timeline/graph-editor/popover.tsx)4
-rw-r--r--apps/web/src/timeline/components/graph-editor/session.ts (renamed from apps/web/src/components/editor/panels/timeline/graph-editor/session.ts)6
-rw-r--r--apps/web/src/timeline/components/graph-editor/use-controller.ts (renamed from apps/web/src/components/editor/panels/timeline/graph-editor/use-controller.ts)8
-rw-r--r--apps/web/src/timeline/components/index.tsx (renamed from apps/web/src/components/editor/panels/timeline/index.tsx)44
-rw-r--r--apps/web/src/timeline/components/interaction.ts (renamed from apps/web/src/components/editor/panels/timeline/interaction.ts)0
-rw-r--r--apps/web/src/timeline/components/layers.ts (renamed from apps/web/src/components/editor/panels/timeline/layers.ts)0
-rw-r--r--apps/web/src/timeline/components/layout.ts (renamed from apps/web/src/components/editor/panels/timeline/layout.ts)2
-rw-r--r--apps/web/src/timeline/components/selection-hit-testing.ts (renamed from apps/web/src/components/editor/panels/timeline/selection-hit-testing.ts)4
-rw-r--r--apps/web/src/timeline/components/snap-indicator.tsx (renamed from apps/web/src/components/editor/panels/timeline/snap-indicator.tsx)100
-rw-r--r--apps/web/src/timeline/components/theme.ts (renamed from apps/web/src/components/editor/panels/timeline/theme.ts)2
-rw-r--r--apps/web/src/timeline/components/timeline-element.tsx (renamed from apps/web/src/components/editor/panels/timeline/timeline-element.tsx)51
-rw-r--r--apps/web/src/timeline/components/timeline-playhead.tsx (renamed from apps/web/src/components/editor/panels/timeline/timeline-playhead.tsx)21
-rw-r--r--apps/web/src/timeline/components/timeline-ruler.tsx (renamed from apps/web/src/components/editor/panels/timeline/timeline-ruler.tsx)27
-rw-r--r--apps/web/src/timeline/components/timeline-tick.tsx (renamed from apps/web/src/components/editor/panels/timeline/timeline-tick.tsx)4
-rw-r--r--apps/web/src/timeline/components/timeline-toolbar.tsx (renamed from apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx)16
-rw-r--r--apps/web/src/timeline/components/timeline-track.tsx (renamed from apps/web/src/components/editor/panels/timeline/timeline-track.tsx)14
-rw-r--r--apps/web/src/timeline/components/track-layout.ts (renamed from apps/web/src/components/editor/panels/timeline/track-layout.ts)2
-rw-r--r--apps/web/src/timeline/creation.ts (renamed from apps/web/src/lib/timeline/creation.ts)2
-rw-r--r--apps/web/src/timeline/defaults.ts (renamed from apps/web/src/lib/timeline/defaults.ts)6
-rw-r--r--apps/web/src/timeline/drag-data.ts (renamed from apps/web/src/lib/drag-data.ts)2
-rw-r--r--apps/web/src/timeline/drag-utils.ts (renamed from apps/web/src/lib/timeline/drag-utils.ts)4
-rw-r--r--apps/web/src/timeline/drag.ts (renamed from apps/web/src/lib/timeline/drag.ts)2
-rw-r--r--apps/web/src/timeline/element-snap-source.ts38
-rw-r--r--apps/web/src/timeline/element-utils.ts (renamed from apps/web/src/lib/timeline/element-utils.ts)14
-rw-r--r--apps/web/src/timeline/group-move/build-group.ts (renamed from apps/web/src/lib/timeline/group-move/build-group.ts)4
-rw-r--r--apps/web/src/timeline/group-move/index.ts (renamed from apps/web/src/lib/timeline/group-move/index.ts)0
-rw-r--r--apps/web/src/timeline/group-move/resolve-move.ts (renamed from apps/web/src/lib/timeline/group-move/resolve-move.ts)6
-rw-r--r--apps/web/src/timeline/group-move/snap.ts (renamed from apps/web/src/lib/timeline/group-move/snap.ts)38
-rw-r--r--apps/web/src/timeline/group-move/track-placement.ts (renamed from apps/web/src/lib/timeline/group-move/track-placement.ts)2
-rw-r--r--apps/web/src/timeline/group-move/types.ts (renamed from apps/web/src/lib/timeline/group-move/types.ts)2
-rw-r--r--apps/web/src/timeline/group-resize/compute-resize.ts (renamed from apps/web/src/lib/timeline/group-resize/compute-resize.ts)4
-rw-r--r--apps/web/src/timeline/group-resize/index.ts (renamed from apps/web/src/lib/timeline/group-resize/index.ts)0
-rw-r--r--apps/web/src/timeline/group-resize/types.ts (renamed from apps/web/src/lib/timeline/group-resize/types.ts)2
-rw-r--r--apps/web/src/timeline/hooks/element/use-element-interaction.ts (renamed from apps/web/src/hooks/timeline/element/use-element-interaction.ts)22
-rw-r--r--apps/web/src/timeline/hooks/element/use-element-selection.ts (renamed from apps/web/src/hooks/timeline/element/use-element-selection.ts)4
-rw-r--r--apps/web/src/timeline/hooks/element/use-keyframe-box-select.ts (renamed from apps/web/src/hooks/timeline/element/use-keyframe-box-select.ts)21
-rw-r--r--apps/web/src/timeline/hooks/element/use-keyframe-drag.ts (renamed from apps/web/src/hooks/timeline/element/use-keyframe-drag.ts)84
-rw-r--r--apps/web/src/timeline/hooks/element/use-keyframe-selection.ts (renamed from apps/web/src/hooks/timeline/element/use-keyframe-selection.ts)4
-rw-r--r--apps/web/src/timeline/hooks/use-edge-auto-scroll.ts (renamed from apps/web/src/hooks/timeline/use-edge-auto-scroll.ts)0
-rw-r--r--apps/web/src/timeline/hooks/use-element-preview.ts (renamed from apps/web/src/hooks/use-element-preview.ts)4
-rw-r--r--apps/web/src/timeline/hooks/use-initial-scroll-bottom.ts (renamed from apps/web/src/hooks/timeline/use-initial-scroll-bottom.ts)0
-rw-r--r--apps/web/src/timeline/hooks/use-scroll-position.ts (renamed from apps/web/src/hooks/timeline/use-scroll-position.ts)0
-rw-r--r--apps/web/src/timeline/hooks/use-scroll-sync.ts (renamed from apps/web/src/hooks/timeline/use-scroll-sync.ts)0
-rw-r--r--apps/web/src/timeline/hooks/use-snap-indicator-position.ts (renamed from apps/web/src/hooks/timeline/use-snap-indicator-position.ts)4
-rw-r--r--apps/web/src/timeline/hooks/use-timeline-drag-drop.ts (renamed from apps/web/src/hooks/timeline/use-timeline-drag-drop.ts)26
-rw-r--r--apps/web/src/timeline/hooks/use-timeline-playhead.ts (renamed from apps/web/src/hooks/timeline/use-timeline-playhead.ts)77
-rw-r--r--apps/web/src/timeline/hooks/use-timeline-resize.ts (renamed from apps/web/src/hooks/timeline/use-timeline-resize.ts)58
-rw-r--r--apps/web/src/timeline/hooks/use-timeline-seek.ts (renamed from apps/web/src/hooks/timeline/use-timeline-seek.ts)31
-rw-r--r--apps/web/src/timeline/hooks/use-timeline-zoom.ts (renamed from apps/web/src/hooks/timeline/use-timeline-zoom.ts)72
-rw-r--r--apps/web/src/timeline/index.ts (renamed from apps/web/src/lib/timeline/index.ts)0
-rw-r--r--apps/web/src/timeline/pixel-utils.ts (renamed from apps/web/src/lib/timeline/pixel-utils.ts)4
-rw-r--r--apps/web/src/timeline/placement/__tests__/resolve.test.ts (renamed from apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts)6
-rw-r--r--apps/web/src/timeline/placement/apply.ts (renamed from apps/web/src/lib/timeline/placement/apply.ts)4
-rw-r--r--apps/web/src/timeline/placement/compatibility.ts (renamed from apps/web/src/lib/timeline/placement/compatibility.ts)2
-rw-r--r--apps/web/src/timeline/placement/index.ts (renamed from apps/web/src/lib/timeline/placement/index.ts)0
-rw-r--r--apps/web/src/timeline/placement/insert-index.ts (renamed from apps/web/src/lib/timeline/placement/insert-index.ts)2
-rw-r--r--apps/web/src/timeline/placement/main-track.ts (renamed from apps/web/src/lib/timeline/placement/main-track.ts)2
-rw-r--r--apps/web/src/timeline/placement/overlap.ts (renamed from apps/web/src/lib/timeline/placement/overlap.ts)2
-rw-r--r--apps/web/src/timeline/placement/resolve.ts (renamed from apps/web/src/lib/timeline/placement/resolve.ts)2
-rw-r--r--apps/web/src/timeline/placement/track-factory.ts (renamed from apps/web/src/lib/timeline/placement/track-factory.ts)4
-rw-r--r--apps/web/src/timeline/placement/types.ts (renamed from apps/web/src/lib/timeline/placement/types.ts)2
-rw-r--r--apps/web/src/timeline/playhead-snap-source.ts9
-rw-r--r--apps/web/src/timeline/ruler-utils.ts (renamed from apps/web/src/lib/timeline/ruler-utils.ts)4
-rw-r--r--apps/web/src/timeline/scale.ts (renamed from apps/web/src/lib/timeline/scale.ts)0
-rw-r--r--apps/web/src/timeline/scenes.ts (renamed from apps/web/src/lib/scenes.ts)6
-rw-r--r--apps/web/src/timeline/snapping/build.ts17
-rw-r--r--apps/web/src/timeline/snapping/index.ts9
-rw-r--r--apps/web/src/timeline/snapping/resolve.ts28
-rw-r--r--apps/web/src/timeline/snapping/threshold.ts15
-rw-r--r--apps/web/src/timeline/snapping/types.ts21
-rw-r--r--apps/web/src/timeline/timeline-store.ts (renamed from apps/web/src/stores/timeline-store.ts)0
-rw-r--r--apps/web/src/timeline/track-capabilities.ts (renamed from apps/web/src/lib/timeline/track-capabilities.ts)2
-rw-r--r--apps/web/src/timeline/track-element-update.ts (renamed from apps/web/src/lib/timeline/track-element-update.ts)2
-rw-r--r--apps/web/src/timeline/tracks.ts (renamed from apps/web/src/lib/timeline/tracks.ts)2
-rw-r--r--apps/web/src/timeline/types.ts (renamed from apps/web/src/lib/timeline/types.ts)10
-rw-r--r--apps/web/src/timeline/update-pipeline.ts (renamed from apps/web/src/lib/timeline/update-pipeline.ts)8
-rw-r--r--apps/web/src/timeline/zoom-utils.ts (renamed from apps/web/src/lib/timeline/zoom-utils.ts)4
-rw-r--r--apps/web/src/transcription/audio.ts (renamed from apps/web/src/lib/transcription/audio.ts)0
-rw-r--r--apps/web/src/transcription/caption-defaults.ts (renamed from apps/web/src/lib/transcription/caption-defaults.ts)0
-rw-r--r--apps/web/src/transcription/caption.ts (renamed from apps/web/src/lib/transcription/caption.ts)4
-rw-r--r--apps/web/src/transcription/diagnostics.ts (renamed from apps/web/src/lib/transcription/diagnostics.ts)2
-rw-r--r--apps/web/src/transcription/languages.ts (renamed from apps/web/src/lib/transcription/languages.ts)0
-rw-r--r--apps/web/src/transcription/models.ts (renamed from apps/web/src/lib/transcription/models.ts)0
-rw-r--r--apps/web/src/transcription/supported-languages.ts (renamed from apps/web/src/lib/transcription/supported-languages.ts)0
-rw-r--r--apps/web/src/transcription/types.ts (renamed from apps/web/src/lib/transcription/types.ts)0
-rw-r--r--apps/web/src/wasm/index.ts (renamed from apps/web/src/lib/wasm/index.ts)0
-rw-r--r--apps/web/src/wasm/ticks.ts (renamed from apps/web/src/lib/wasm/ticks.ts)0
490 files changed, 3564 insertions, 2371 deletions
diff --git a/apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx b/apps/web/src/actions/components/shortcuts-dialog.tsx
index 70802b5b..cd9c0242 100644
--- a/apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx
+++ b/apps/web/src/actions/components/shortcuts-dialog.tsx
@@ -5,8 +5,8 @@ import { toast } from "sonner";
import {
type KeyboardShortcut,
useKeyboardShortcutsHelp,
-} from "@/hooks/use-keyboard-shortcuts-help";
-import { useKeybindingsStore } from "@/stores/keybindings-store";
+} from "@/actions/use-keyboard-shortcuts-help";
+import { useKeybindingsStore } from "@/actions/keybindings-store";
import { Button } from "@/components/ui/button";
import {
Dialog,
diff --git a/apps/web/src/lib/actions/definitions.ts b/apps/web/src/actions/definitions.ts
index f6846846..6f0ba700 100644
--- a/apps/web/src/lib/actions/definitions.ts
+++ b/apps/web/src/actions/definitions.ts
@@ -1,7 +1,7 @@
import type {
KeybindingConfig,
ShortcutKey,
-} from "@/lib/actions/keybinding";
+} from "@/actions/keybinding";
import type { TActionWithOptionalArgs } from "./types";
export type TActionCategory =
diff --git a/apps/web/src/lib/actions/index.ts b/apps/web/src/actions/index.ts
index 801a846a..801a846a 100644
--- a/apps/web/src/lib/actions/index.ts
+++ b/apps/web/src/actions/index.ts
diff --git a/apps/web/src/lib/actions/keybinding.ts b/apps/web/src/actions/keybinding.ts
index 65ef4538..65ef4538 100644
--- a/apps/web/src/lib/actions/keybinding.ts
+++ b/apps/web/src/actions/keybinding.ts
diff --git a/apps/web/src/stores/keybindings-store.ts b/apps/web/src/actions/keybindings-store.ts
index ff77e0b4..c0a1a5e2 100644
--- a/apps/web/src/stores/keybindings-store.ts
+++ b/apps/web/src/actions/keybindings-store.ts
@@ -2,11 +2,11 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
-import type { TActionWithOptionalArgs } from "@/lib/actions";
-import { getDefaultShortcuts } from "@/lib/actions";
+import type { TActionWithOptionalArgs } from "@/actions";
+import { getDefaultShortcuts } from "@/actions";
import { isTypableDOMElement } from "@/utils/browser";
import { isAppleDevice } from "@/utils/platform";
-import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
+import type { KeybindingConfig, ShortcutKey } from "@/actions/keybinding";
import { runMigrations, CURRENT_VERSION } from "./keybindings/migrations";
const defaultKeybindings: KeybindingConfig = getDefaultShortcuts();
diff --git a/apps/web/src/stores/keybindings/migrations/index.ts b/apps/web/src/actions/keybindings/migrations/index.ts
index 71fd81a6..71fd81a6 100644
--- a/apps/web/src/stores/keybindings/migrations/index.ts
+++ b/apps/web/src/actions/keybindings/migrations/index.ts
diff --git a/apps/web/src/stores/keybindings/migrations/v2-to-v3.ts b/apps/web/src/actions/keybindings/migrations/v2-to-v3.ts
index 6ebff92c..0171c5a5 100644
--- a/apps/web/src/stores/keybindings/migrations/v2-to-v3.ts
+++ b/apps/web/src/actions/keybindings/migrations/v2-to-v3.ts
@@ -1,5 +1,5 @@
-import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
-import type { TActionWithOptionalArgs } from "@/lib/actions";
+import type { KeybindingConfig, ShortcutKey } from "@/actions/keybinding";
+import type { TActionWithOptionalArgs } from "@/actions";
interface V2State {
keybindings: KeybindingConfig;
diff --git a/apps/web/src/stores/keybindings/migrations/v3-to-v4.ts b/apps/web/src/actions/keybindings/migrations/v3-to-v4.ts
index 0f9b35da..8541071b 100644
--- a/apps/web/src/stores/keybindings/migrations/v3-to-v4.ts
+++ b/apps/web/src/actions/keybindings/migrations/v3-to-v4.ts
@@ -1,6 +1,6 @@
-import type { TActionWithOptionalArgs } from "@/lib/actions";
-import type { ShortcutKey } from "@/lib/actions/keybinding";
-import type { KeybindingConfig } from "@/lib/actions/keybinding";
+import type { TActionWithOptionalArgs } from "@/actions";
+import type { ShortcutKey } from "@/actions/keybinding";
+import type { KeybindingConfig } from "@/actions/keybinding";
interface V3State {
keybindings: KeybindingConfig;
diff --git a/apps/web/src/stores/keybindings/migrations/v4-to-v5.ts b/apps/web/src/actions/keybindings/migrations/v4-to-v5.ts
index 880df1dc..f6937c39 100644
--- a/apps/web/src/stores/keybindings/migrations/v4-to-v5.ts
+++ b/apps/web/src/actions/keybindings/migrations/v4-to-v5.ts
@@ -1,4 +1,4 @@
-import type { KeybindingConfig } from "@/lib/actions/keybinding";
+import type { KeybindingConfig } from "@/actions/keybinding";
interface V4State {
keybindings: KeybindingConfig;
diff --git a/apps/web/src/stores/keybindings/migrations/v5-to-v6.ts b/apps/web/src/actions/keybindings/migrations/v5-to-v6.ts
index ebabb7c0..09b44307 100644
--- a/apps/web/src/stores/keybindings/migrations/v5-to-v6.ts
+++ b/apps/web/src/actions/keybindings/migrations/v5-to-v6.ts
@@ -1,4 +1,4 @@
-import type { KeybindingConfig } from "@/lib/actions/keybinding";
+import type { KeybindingConfig } from "@/actions/keybinding";
interface V5State {
keybindings: KeybindingConfig;
diff --git a/apps/web/src/stores/keybindings/migrations/v6-to-v7.ts b/apps/web/src/actions/keybindings/migrations/v6-to-v7.ts
index 8c5fd96c..46a99ac1 100644
--- a/apps/web/src/stores/keybindings/migrations/v6-to-v7.ts
+++ b/apps/web/src/actions/keybindings/migrations/v6-to-v7.ts
@@ -1,4 +1,4 @@
-import type { KeybindingConfig } from "@/lib/actions/keybinding";
+import type { KeybindingConfig } from "@/actions/keybinding";
interface V6State {
keybindings: KeybindingConfig;
diff --git a/apps/web/src/lib/actions/registry.ts b/apps/web/src/actions/registry.ts
index e69b3b9f..e69b3b9f 100644
--- a/apps/web/src/lib/actions/registry.ts
+++ b/apps/web/src/actions/registry.ts
diff --git a/apps/web/src/lib/actions/types.ts b/apps/web/src/actions/types.ts
index 41258c7b..41258c7b 100644
--- a/apps/web/src/lib/actions/types.ts
+++ b/apps/web/src/actions/types.ts
diff --git a/apps/web/src/hooks/actions/use-action-handler.ts b/apps/web/src/actions/use-action-handler.ts
index 3f6dbd21..a8bb0054 100644
--- a/apps/web/src/hooks/actions/use-action-handler.ts
+++ b/apps/web/src/actions/use-action-handler.ts
@@ -5,8 +5,8 @@ import type {
TActionHandlerOptions,
TArgOfAction,
TInvocationTrigger,
-} from "@/lib/actions";
-import { bindAction, unbindAction } from "@/lib/actions";
+} from "@/actions";
+import { bindAction, unbindAction } from "@/actions";
export function useActionHandler<A extends TAction>(
action: A,
diff --git a/apps/web/src/hooks/actions/use-editor-actions.ts b/apps/web/src/actions/use-editor-actions.ts
index 873fd33a..e62a8ce0 100644
--- a/apps/web/src/hooks/actions/use-editor-actions.ts
+++ b/apps/web/src/actions/use-editor-actions.ts
@@ -1,21 +1,21 @@
"use client";
import { useEffect, useRef } from "react";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { useActionHandler } from "@/hooks/actions/use-action-handler";
-import { useEditor } from "../use-editor";
-import { useElementSelection } from "../timeline/element/use-element-selection";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
-import { useKeyframeSelection } from "../timeline/element/use-keyframe-selection";
-import { getElementsAtTime, hasMediaId } from "@/lib/timeline";
-import { cancelInteraction } from "@/lib/cancel-interaction";
-import { invokeAction } from "@/lib/actions";
-import { canToggleSourceAudio } from "@/lib/timeline/audio-separation";
+import { useTimelineStore } from "@/timeline/timeline-store";
+import { useActionHandler } from "@/actions/use-action-handler";
+import { useEditor } from "@/editor/use-editor";
+import { useElementSelection } from "@/timeline/hooks/element/use-element-selection";
+import { TICKS_PER_SECOND } from "@/wasm";
+import { useKeyframeSelection } from "@/timeline/hooks/element/use-keyframe-selection";
+import { getElementsAtTime, hasMediaId } from "@/timeline";
+import { cancelInteraction } from "@/editor/cancel-interaction";
+import { invokeAction } from "@/actions";
+import { canToggleSourceAudio } from "@/timeline/audio-separation";
import {
activateScope,
clearActiveScope,
type ScopeEntry,
-} from "@/lib/selection/scope";
+} from "@/selection/scope";
export function useEditorActions() {
const editor = useEditor();
diff --git a/apps/web/src/hooks/use-keybindings.ts b/apps/web/src/actions/use-keybindings.ts
index 0c524214..c129795c 100644
--- a/apps/web/src/hooks/use-keybindings.ts
+++ b/apps/web/src/actions/use-keybindings.ts
@@ -1,7 +1,7 @@
import { useEffect } from "react";
-import { invokeAction } from "@/lib/actions";
-import { useEditor } from "@/hooks/use-editor";
-import { useKeybindingsStore } from "@/stores/keybindings-store";
+import { invokeAction } from "@/actions";
+import { useEditor } from "@/editor/use-editor";
+import { useKeybindingsStore } from "@/actions/keybindings-store";
import { isTypableDOMElement } from "@/utils/browser";
/**
diff --git a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts b/apps/web/src/actions/use-keyboard-shortcuts-help.ts
index 9a2f6cf5..7681e664 100644
--- a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts
+++ b/apps/web/src/actions/use-keyboard-shortcuts-help.ts
@@ -1,8 +1,8 @@
"use client";
import { useMemo } from "react";
-import { useKeybindingsStore } from "@/stores/keybindings-store";
-import { ACTIONS, type TActionWithOptionalArgs } from "@/lib/actions";
+import { useKeybindingsStore } from "@/actions/keybindings-store";
+import { ACTIONS, type TActionWithOptionalArgs } from "@/actions";
import {
getPlatformAlternateKey,
getPlatformSpecialKey,
diff --git a/apps/web/src/lib/animation/__tests__/binding-values.test.ts b/apps/web/src/animation/__tests__/binding-values.test.ts
index c5a354e4..a1f2010c 100644
--- a/apps/web/src/lib/animation/__tests__/binding-values.test.ts
+++ b/apps/web/src/animation/__tests__/binding-values.test.ts
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
import {
composeAnimationValue,
createAnimationBinding,
-} from "@/lib/animation/binding-values";
+} from "@/animation/binding-values";
describe("binding values", () => {
test("formats composed animated colors as hex", () => {
diff --git a/apps/web/src/lib/animation/bezier.ts b/apps/web/src/animation/bezier.ts
index 5a9758ed..6f9a1d18 100644
--- a/apps/web/src/lib/animation/bezier.ts
+++ b/apps/web/src/animation/bezier.ts
@@ -1,4 +1,4 @@
-import type { ScalarAnimationKey } from "@/lib/animation/types";
+import type { ScalarAnimationKey } from "@/animation/types";
const BEZIER_SOLVE_ITERATIONS = 20;
diff --git a/apps/web/src/lib/animation/binding-values.ts b/apps/web/src/animation/binding-values.ts
index 66fb2b5d..a46aa9a6 100644
--- a/apps/web/src/lib/animation/binding-values.ts
+++ b/apps/web/src/animation/binding-values.ts
@@ -12,7 +12,7 @@ import type {
DiscreteValue,
Vector2AnimationBinding,
VectorValue,
-} from "@/lib/animation/types";
+} from "@/animation/types";
import { clamp } from "@/utils/math";
interface LinearRgba {
diff --git a/apps/web/src/lib/animation/curve-bridge.ts b/apps/web/src/animation/curve-bridge.ts
index 385fb448..af411dd1 100644
--- a/apps/web/src/lib/animation/curve-bridge.ts
+++ b/apps/web/src/animation/curve-bridge.ts
@@ -1,12 +1,12 @@
import {
getDefaultLeftHandle,
getDefaultRightHandle,
-} from "@/lib/animation/bezier";
+} from "@/animation/bezier";
import type {
CurveHandle,
NormalizedCubicBezier,
ScalarAnimationKey,
-} from "@/lib/animation/types";
+} from "@/animation/types";
const VALUE_EPSILON = 1e-6;
diff --git a/apps/web/src/lib/animation/effect-param-channel.ts b/apps/web/src/animation/effect-param-channel.ts
index 3080e6ff..c635bb82 100644
--- a/apps/web/src/lib/animation/effect-param-channel.ts
+++ b/apps/web/src/animation/effect-param-channel.ts
@@ -1,9 +1,9 @@
-import type { ParamValues } from "@/lib/params";
-import type { Effect } from "@/lib/effects/types";
+import type { ParamValues } from "@/params";
+import type { Effect } from "@/effects/types";
import type {
ElementAnimations,
EffectParamPath,
-} from "@/lib/animation/types";
+} from "@/animation/types";
import { removeElementKeyframe } from "./keyframes";
import { resolveAnimationPathValueAtTime } from "./resolve";
diff --git a/apps/web/src/lib/animation/graph-channels.ts b/apps/web/src/animation/graph-channels.ts
index 19d53e27..d6482602 100644
--- a/apps/web/src/lib/animation/graph-channels.ts
+++ b/apps/web/src/animation/graph-channels.ts
@@ -5,7 +5,7 @@ import type {
ScalarAnimationChannel,
ScalarGraphChannel,
ScalarGraphKeyframeContext,
-} from "@/lib/animation/types";
+} from "@/animation/types";
export interface EditableScalarChannels {
binding: AnimationBindingInstance;
diff --git a/apps/web/src/lib/animation/graphic-param-channel.ts b/apps/web/src/animation/graphic-param-channel.ts
index de595060..ebf0f940 100644
--- a/apps/web/src/lib/animation/graphic-param-channel.ts
+++ b/apps/web/src/animation/graphic-param-channel.ts
@@ -1,12 +1,12 @@
import type {
ElementAnimations,
GraphicParamPath,
-} from "@/lib/animation/types";
-import type { ParamValues } from "@/lib/params";
+} from "@/animation/types";
+import type { ParamValues } from "@/params";
import {
getGraphicDefinition,
resolveGraphicParams,
-} from "@/lib/graphics";
+} from "@/graphics";
import { resolveAnimationPathValueAtTime } from "./resolve";
export const GRAPHIC_PARAM_PATH_PREFIX = "params.";
diff --git a/apps/web/src/lib/animation/index.ts b/apps/web/src/animation/index.ts
index 384f3d17..384f3d17 100644
--- a/apps/web/src/lib/animation/index.ts
+++ b/apps/web/src/animation/index.ts
diff --git a/apps/web/src/lib/animation/interpolation.ts b/apps/web/src/animation/interpolation.ts
index 1a3c0fed..277bfbb7 100644
--- a/apps/web/src/lib/animation/interpolation.ts
+++ b/apps/web/src/animation/interpolation.ts
@@ -7,7 +7,7 @@ import type {
ScalarAnimationChannel,
ScalarAnimationKey,
ScalarSegmentType,
-} from "@/lib/animation/types";
+} from "@/animation/types";
import { clamp } from "@/utils/math";
import {
getBezierPoint,
diff --git a/apps/web/src/lib/animation/keyframe-query.ts b/apps/web/src/animation/keyframe-query.ts
index dbcd041c..231aa5f2 100644
--- a/apps/web/src/lib/animation/keyframe-query.ts
+++ b/apps/web/src/animation/keyframe-query.ts
@@ -4,7 +4,7 @@ import type {
AnimationPath,
ElementAnimations,
ElementKeyframe,
-} from "@/lib/animation/types";
+} from "@/animation/types";
import {
type AnimationComponentValue,
composeAnimationValue,
diff --git a/apps/web/src/lib/animation/keyframes.ts b/apps/web/src/animation/keyframes.ts
index a5efb6ee..12dc777d 100644
--- a/apps/web/src/lib/animation/keyframes.ts
+++ b/apps/web/src/animation/keyframes.ts
@@ -13,7 +13,7 @@ import type {
ScalarAnimationKey,
ScalarCurveKeyframePatch,
ScalarSegmentType,
-} from "@/lib/animation/types";
+} from "@/animation/types";
import { generateUUID } from "@/utils/id";
import {
cloneAnimationBinding,
diff --git a/apps/web/src/lib/animation/property-groups.ts b/apps/web/src/animation/property-groups.ts
index 1e475dac..7b71af15 100644
--- a/apps/web/src/lib/animation/property-groups.ts
+++ b/apps/web/src/animation/property-groups.ts
@@ -2,8 +2,8 @@ import type {
AnimationPropertyGroup,
AnimationPropertyPath,
ElementAnimations,
-} from "@/lib/animation/types";
-import { ANIMATION_PROPERTY_GROUPS } from "@/lib/animation/types";
+} from "@/animation/types";
+import { ANIMATION_PROPERTY_GROUPS } from "@/animation/types";
import { getKeyframeAtTime } from "./keyframe-query";
export interface GroupKeyframeRef {
diff --git a/apps/web/src/lib/animation/property-registry.ts b/apps/web/src/animation/property-registry.ts
index 56340fd8..aacb9972 100644
--- a/apps/web/src/lib/animation/property-registry.ts
+++ b/apps/web/src/animation/property-registry.ts
@@ -3,20 +3,20 @@ import type {
AnimationInterpolation,
AnimationPropertyPath,
AnimationValue,
-} from "@/lib/animation/types";
+} from "@/animation/types";
import { parseColorToLinearRgba } from "./binding-values";
-import type { TimelineElement } from "@/lib/timeline";
-import { MIN_TRANSFORM_SCALE } from "@/lib/animation/transform";
+import type { TimelineElement } from "@/timeline";
+import { MIN_TRANSFORM_SCALE } from "@/animation/transform";
import {
CORNER_RADIUS_MAX,
CORNER_RADIUS_MIN,
-} from "@/lib/text/background";
+} from "@/text/background";
import {
canElementHaveAudio,
isVisualElement,
-} from "@/lib/timeline/element-utils";
-import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/lib/timeline/audio-constants";
-import { DEFAULTS } from "@/lib/timeline/defaults";
+} from "@/timeline/element-utils";
+import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/timeline/audio-constants";
+import { DEFAULTS } from "@/timeline/defaults";
import { snapToStep } from "@/utils/math";
export interface NumericSpec {
diff --git a/apps/web/src/lib/animation/resolve.ts b/apps/web/src/animation/resolve.ts
index 42c1c73c..1b51c126 100644
--- a/apps/web/src/lib/animation/resolve.ts
+++ b/apps/web/src/animation/resolve.ts
@@ -5,8 +5,8 @@ import type {
AnimationPropertyPath,
AnimationValueForPath,
ElementAnimations,
-} from "@/lib/animation/types";
-import type { Transform } from "@/lib/rendering";
+} from "@/animation/types";
+import type { Transform } from "@/rendering";
import {
type AnimationComponentValue,
composeAnimationValue,
diff --git a/apps/web/src/lib/animation/target-resolver.ts b/apps/web/src/animation/target-resolver.ts
index 64bb8709..32e7917c 100644
--- a/apps/web/src/lib/animation/target-resolver.ts
+++ b/apps/web/src/animation/target-resolver.ts
@@ -3,20 +3,20 @@ import type {
AnimationInterpolation,
AnimationPath,
AnimationValue,
-} from "@/lib/animation/types";
+} from "@/animation/types";
import {
parseEffectParamPath,
isEffectParamPath,
-} from "@/lib/animation/effect-param-channel";
+} from "@/animation/effect-param-channel";
import {
isGraphicParamPath,
parseGraphicParamPath,
-} from "@/lib/animation/graphic-param-channel";
-import type { ParamDefinition } from "@/lib/params";
-import { effectsRegistry, registerDefaultEffects } from "@/lib/effects";
-import { getGraphicDefinition } from "@/lib/graphics";
-import type { TimelineElement } from "@/lib/timeline";
-import { isVisualElement } from "@/lib/timeline/element-utils";
+} from "@/animation/graphic-param-channel";
+import type { ParamDefinition } from "@/params";
+import { effectsRegistry, registerDefaultEffects } from "@/effects";
+import { getGraphicDefinition } from "@/graphics";
+import type { TimelineElement } from "@/timeline";
+import { isVisualElement } from "@/timeline/element-utils";
import { snapToStep } from "@/utils/math";
import {
coerceAnimationValueForProperty,
diff --git a/apps/web/src/animation/timeline-snap-points.ts b/apps/web/src/animation/timeline-snap-points.ts
new file mode 100644
index 00000000..f7bf549a
--- /dev/null
+++ b/apps/web/src/animation/timeline-snap-points.ts
@@ -0,0 +1,35 @@
+import { getElementKeyframes } from "@/animation";
+import type { SceneTracks } from "@/timeline";
+import type { SnapPoint } from "@/timeline/snapping";
+
+export function getAnimationKeyframeSnapPointsForTimeline({
+ tracks,
+ excludeElementIds,
+}: {
+ tracks: SceneTracks;
+ excludeElementIds?: Set<string>;
+}): SnapPoint[] {
+ const snapPoints: SnapPoint[] = [];
+ const orderedTracks = [...tracks.overlay, tracks.main, ...tracks.audio];
+
+ for (const track of orderedTracks) {
+ for (const element of track.elements) {
+ if (excludeElementIds?.has(element.id)) {
+ continue;
+ }
+
+ for (const keyframe of getElementKeyframes({
+ animations: element.animations,
+ })) {
+ snapPoints.push({
+ time: element.startTime + keyframe.time,
+ type: "keyframe",
+ elementId: element.id,
+ trackId: track.id,
+ });
+ }
+ }
+ }
+
+ return snapPoints;
+}
diff --git a/apps/web/src/lib/animation/transform.ts b/apps/web/src/animation/transform.ts
index ab932ea2..ab932ea2 100644
--- a/apps/web/src/lib/animation/transform.ts
+++ b/apps/web/src/animation/transform.ts
diff --git a/apps/web/src/lib/animation/types.ts b/apps/web/src/animation/types.ts
index 115b4dbf..3afddfa1 100644
--- a/apps/web/src/lib/animation/types.ts
+++ b/apps/web/src/animation/types.ts
@@ -1,4 +1,4 @@
-import type { ParamValues } from "@/lib/params";
+import type { ParamValues } from "@/params";
export const ANIMATION_PROPERTY_PATHS = [
"transform.positionX",
diff --git a/apps/web/src/app/api/auth/[...all]/route.ts b/apps/web/src/app/api/auth/[...all]/route.ts
index 544457e2..edf0d0c2 100644
--- a/apps/web/src/app/api/auth/[...all]/route.ts
+++ b/apps/web/src/app/api/auth/[...all]/route.ts
@@ -1,4 +1,4 @@
-import { auth } from "@/lib/auth/server";
+import { auth } from "@/auth/server";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
diff --git a/apps/web/src/app/api/feedback/route.ts b/apps/web/src/app/api/feedback/route.ts
index b13ec2bf..cd853eea 100644
--- a/apps/web/src/app/api/feedback/route.ts
+++ b/apps/web/src/app/api/feedback/route.ts
@@ -1,7 +1,7 @@
import { type NextRequest, NextResponse } from "next/server";
import { z } from "zod";
-import { checkRateLimit } from "@/lib/rate-limit";
-import { submitFeedback, MAX_MESSAGE_LENGTH } from "@/lib/feedback";
+import { checkRateLimit } from "@/auth/rate-limit";
+import { submitFeedback, MAX_MESSAGE_LENGTH } from "@/feedback";
const submitSchema = z.object({
message: z
diff --git a/apps/web/src/app/api/sounds/search/route.ts b/apps/web/src/app/api/sounds/search/route.ts
index c4d065a9..aa7a1b60 100644
--- a/apps/web/src/app/api/sounds/search/route.ts
+++ b/apps/web/src/app/api/sounds/search/route.ts
@@ -1,7 +1,7 @@
-import { webEnv } from "@/lib/env/web";
+import { webEnv } from "@/env/web";
import { type NextRequest, NextResponse } from "next/server";
import { z } from "zod";
-import { checkRateLimit } from "@/lib/rate-limit";
+import { checkRateLimit } from "@/auth/rate-limit";
const searchParamsSchema = z.object({
q: z.string().max(500, "Query too long").optional(),
diff --git a/apps/web/src/app/blog/[slug]/page.tsx b/apps/web/src/app/blog/[slug]/page.tsx
index b581de95..dba9b504 100644
--- a/apps/web/src/app/blog/[slug]/page.tsx
+++ b/apps/web/src/app/blog/[slug]/page.tsx
@@ -4,8 +4,8 @@ import { notFound } from "next/navigation";
import { BasePage } from "@/app/base-page";
import Prose from "@/components/ui/prose";
import { Separator } from "@/components/ui/separator";
-import { getPosts, getSinglePost, processHtmlContent } from "@/lib/blog/query";
-import type { Author, Post } from "@/lib/blog/types";
+import { getPosts, getSinglePost, processHtmlContent } from "@/blog/query";
+import type { Author, Post } from "@/blog/types";
type PageProps = {
params: Promise<{ slug: string }>;
diff --git a/apps/web/src/app/blog/page.tsx b/apps/web/src/app/blog/page.tsx
index 9f5a8972..b40bc2fc 100644
--- a/apps/web/src/app/blog/page.tsx
+++ b/apps/web/src/app/blog/page.tsx
@@ -2,8 +2,8 @@ import type { Metadata } from "next";
import Link from "next/link";
import { BasePage } from "@/app/base-page";
import { Separator } from "@/components/ui/separator";
-import { getPosts } from "@/lib/blog/query";
-import type { Post } from "@/lib/blog/types";
+import { getPosts } from "@/blog/query";
+import type { Post } from "@/blog/types";
export const metadata: Metadata = {
title: "Blog - OpenCut",
diff --git a/apps/web/src/app/changelog/[version]/page.tsx b/apps/web/src/app/changelog/[version]/page.tsx
index f069832b..6c529ce1 100644
--- a/apps/web/src/app/changelog/[version]/page.tsx
+++ b/apps/web/src/app/changelog/[version]/page.tsx
@@ -3,15 +3,15 @@ import Link from "next/link";
import { notFound } from "next/navigation";
import { BasePage } from "@/app/base-page";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
-import { getReleaseByVersion, getSortedReleases } from "@/lib/changelog/utils";
+import { getReleaseByVersion, getSortedReleases } from "@/changelog/utils";
import {
ReleaseArticle,
ReleaseMeta,
ReleaseTitle,
ReleaseDescription,
ReleaseChanges,
-} from "@/lib/changelog/components/release";
-import { CopyMarkdownButton } from "@/lib/changelog/components/copy-markdown-button";
+} from "@/changelog/components/release";
+import { CopyMarkdownButton } from "@/changelog/components/copy-markdown-button";
type Props = { params: Promise<{ version: string }> };
diff --git a/apps/web/src/app/changelog/page.tsx b/apps/web/src/app/changelog/page.tsx
index 8440eaa2..25ae25db 100644
--- a/apps/web/src/app/changelog/page.tsx
+++ b/apps/web/src/app/changelog/page.tsx
@@ -4,14 +4,14 @@ import { Separator } from "@/components/ui/separator";
import {
type Release as ReleaseType,
getSortedReleases,
-} from "@/lib/changelog/utils";
+} from "@/changelog/utils";
import {
ReleaseArticle,
ReleaseMeta,
ReleaseTitle,
ReleaseDescription,
ReleaseChanges,
-} from "@/lib/changelog/components/release";
+} from "@/changelog/components/release";
export const metadata: Metadata = {
title: "Changelog - OpenCut",
diff --git a/apps/web/src/app/contributors/page.tsx b/apps/web/src/app/contributors/page.tsx
index b15d9f27..f4d13732 100644
--- a/apps/web/src/app/contributors/page.tsx
+++ b/apps/web/src/app/contributors/page.tsx
@@ -3,7 +3,7 @@ import Link from "next/link";
import { GitHubContributeSection } from "@/components/gitHub-contribute-section";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent } from "@/components/ui/card";
-import { EXTERNAL_TOOLS } from "@/lib/site/external-tools";
+import { EXTERNAL_TOOLS } from "@/site/external-tools";
import { BasePage } from "../base-page";
export const metadata: Metadata = {
diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx
index a3a7afa5..b3d40a37 100644
--- a/apps/web/src/app/editor/[project_id]/page.tsx
+++ b/apps/web/src/app/editor/[project_id]/page.tsx
@@ -8,21 +8,32 @@ import {
} from "@/components/ui/resizable";
import { AssetsPanel } from "@/components/editor/panels/assets";
import { PropertiesPanel } from "@/components/editor/panels/properties";
-import { Timeline } from "@/components/editor/panels/timeline";
-import { PreviewPanel } from "@/components/editor/panels/preview";
+import { Timeline } from "@/timeline/components";
+import { PreviewPanel } from "@/preview/components";
import { EditorHeader } from "@/components/editor/editor-header";
import { EditorProvider } from "@/components/providers/editor-provider";
import { Onboarding } from "@/components/editor/onboarding";
-import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
-import { usePanelStore } from "@/stores/panel-store";
-import { usePasteMedia } from "@/hooks/use-paste-media";
+import { MigrationDialog } from "@/project/components/migration-dialog";
+import { usePanelStore } from "@/editor/panel-store";
+import { usePasteMedia } from "@/media/use-paste-media";
import { MobileGate } from "@/components/editor/mobile-gate";
-import { useState } from "react";
-import { useEditor } from "@/hooks/use-editor";
+import { useMemo, useState } from "react";
+import { useEditor } from "@/editor/use-editor";
import { Cancel01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Button } from "@/components/ui/button";
-import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification";
+import { ChangelogNotification } from "@/changelog/components/changelog-notification";
+import {
+ createPreviewOverlayControl,
+ isPreviewOverlayVisible,
+ mergePreviewOverlaySources,
+} from "@/preview/overlays";
+import { usePreviewStore } from "@/preview/preview-store";
+import { getGuidePreviewOverlaySource } from "@/guides";
+import {
+ bookmarkNotesPreviewOverlay,
+ getBookmarkPreviewOverlaySource,
+} from "@/timeline/bookmarks/index";
export default function Editor() {
const params = useParams();
@@ -70,6 +81,49 @@ function DegradedRendererBanner() {
function EditorLayout() {
usePasteMedia();
const { panels, setPanel } = usePanelStore();
+ const activeScene = useEditor((editor) =>
+ editor.scenes.getActiveSceneOrNull(),
+ );
+ const currentTime = useEditor((editor) => editor.playback.getCurrentTime());
+ const activeGuide = usePreviewStore((state) => state.activeGuide);
+ const overlays = usePreviewStore((state) => state.overlays);
+ const setOverlayVisibility = usePreviewStore(
+ (state) => state.setOverlayVisibility,
+ );
+ const showBookmarkNotes = isPreviewOverlayVisible({
+ overlay: bookmarkNotesPreviewOverlay,
+ overlays,
+ });
+
+ const overlaySource = useMemo(
+ () =>
+ mergePreviewOverlaySources({
+ sources: [
+ getGuidePreviewOverlaySource({
+ guideId: activeGuide,
+ }),
+ activeScene
+ ? getBookmarkPreviewOverlaySource({
+ bookmarks: activeScene.bookmarks,
+ time: currentTime,
+ isVisible: showBookmarkNotes,
+ })
+ : {
+ definitions: [bookmarkNotesPreviewOverlay],
+ instances: [],
+ },
+ ],
+ }),
+ [activeGuide, activeScene, currentTime, showBookmarkNotes],
+ );
+
+ const overlayControls = useMemo(
+ () =>
+ overlaySource.definitions.map((overlay) =>
+ createPreviewOverlayControl({ overlay, overlays }),
+ ),
+ [overlaySource.definitions, overlays],
+ );
return (
<ResizablePanelGroup
@@ -111,7 +165,11 @@ function EditorLayout() {
minSize={30}
className="min-h-0 min-w-0 flex-1"
>
- <PreviewPanel />
+ <PreviewPanel
+ overlayControls={overlayControls}
+ overlayInstances={overlaySource.instances}
+ onOverlayVisibilityChange={setOverlayVisibility}
+ />
</ResizablePanel>
<ResizableHandle withHandle />
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 6d4878c5..558ae538 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -2,11 +2,11 @@ import { ThemeProvider } from "next-themes";
import Script from "next/script";
import "./globals.css";
import { Toaster } from "../components/ui/sonner";
-import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification";
+import { ChangelogNotification } from "@/changelog/components/changelog-notification";
import { TooltipProvider } from "../components/ui/tooltip";
import { baseMetaData } from "./metadata";
import { BotIdClient } from "botid/client";
-import { webEnv } from "@/lib/env/web";
+import { webEnv } from "@/env/web";
import { Inter } from "next/font/google";
const siteFont = Inter({ subsets: ["latin"] });
diff --git a/apps/web/src/app/metadata.ts b/apps/web/src/app/metadata.ts
index 06276baa..aa11c43e 100644
--- a/apps/web/src/app/metadata.ts
+++ b/apps/web/src/app/metadata.ts
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
-import { SITE_INFO, SITE_URL } from "@/lib/site/brand";
+import { SITE_INFO, SITE_URL } from "@/site/brand";
export const baseMetaData: Metadata = {
metadataBase: new URL(SITE_URL),
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index 460e8bc3..6084a912 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -2,7 +2,7 @@ import { Hero } from "@/components/landing/hero";
import { Header } from "@/components/header";
import { Footer } from "@/components/footer";
import type { Metadata } from "next";
-import { SITE_URL } from "@/lib/site/brand";
+import { SITE_URL } from "@/site/brand";
export const metadata: Metadata = {
alternates: {
diff --git a/apps/web/src/app/privacy/page.tsx b/apps/web/src/app/privacy/page.tsx
index 4963e2ab..6362146e 100644
--- a/apps/web/src/app/privacy/page.tsx
+++ b/apps/web/src/app/privacy/page.tsx
@@ -7,7 +7,7 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { Separator } from "@/components/ui/separator";
-import { SOCIAL_LINKS } from "@/lib/site/social";
+import { SOCIAL_LINKS } from "@/site/social";
export const metadata: Metadata = {
title: "Privacy Policy - OpenCut",
diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx
index e8b09291..c39c4fd9 100644
--- a/apps/web/src/app/projects/page.tsx
+++ b/apps/web/src/app/projects/page.tsx
@@ -7,20 +7,20 @@ import type { KeyboardEvent, MouseEvent } from "react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import type { EditorCore } from "@/core";
-import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
-import { StoragePersistenceDialog } from "@/components/editor/dialogs/storage-persistence-dialog";
+import { MigrationDialog } from "@/project/components/migration-dialog";
+import { StoragePersistenceDialog } from "@/services/storage/components/storage-persistence-dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import { useProjectsStore } from "./store";
import type {
TProjectMetadata,
TProjectSortKey,
TProjectSortOption,
-} from "@/lib/project/types";
+} from "@/project/types";
import { formatTimecode, mediaTimeToSeconds } from "opencut-wasm";
import { formatDate } from "@/utils/date";
import { HugeiconsIcon } from "@hugeicons/react";
@@ -62,11 +62,11 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
-import { DeleteProjectDialog } from "@/components/editor/dialogs/delete-project-dialog";
-import { ProjectInfoDialog } from "@/components/editor/dialogs/project-info-dialog";
-import { RenameProjectDialog } from "@/components/editor/dialogs/rename-project-dialog";
+import { DeleteProjectDialog } from "@/project/components/delete-project-dialog";
+import { ProjectInfoDialog } from "@/project/components/project-info-dialog";
+import { RenameProjectDialog } from "@/project/components/rename-project-dialog";
import { cn } from "@/utils/ui";
-import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification";
+import { ChangelogNotification } from "@/changelog/components/changelog-notification";
const formatProjectDuration = ({
duration,
}: {
diff --git a/apps/web/src/app/projects/store.ts b/apps/web/src/app/projects/store.ts
index 71f89b4a..82d2744e 100644
--- a/apps/web/src/app/projects/store.ts
+++ b/apps/web/src/app/projects/store.ts
@@ -1,6 +1,6 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
-import type { TProjectSortKey } from "@/lib/project/types";
+import type { TProjectSortKey } from "@/project/types";
export type ProjectsViewMode = "grid" | "list";
diff --git a/apps/web/src/app/robots.ts b/apps/web/src/app/robots.ts
index 38f6f487..1b1ca865 100644
--- a/apps/web/src/app/robots.ts
+++ b/apps/web/src/app/robots.ts
@@ -1,5 +1,5 @@
import type { MetadataRoute } from "next";
-import { SITE_URL } from "@/lib/site/brand";
+import { SITE_URL } from "@/site/brand";
export default function robots(): MetadataRoute.Robots {
return {
diff --git a/apps/web/src/app/rss.xml/route.ts b/apps/web/src/app/rss.xml/route.ts
index 4dfccd59..d4e9570a 100644
--- a/apps/web/src/app/rss.xml/route.ts
+++ b/apps/web/src/app/rss.xml/route.ts
@@ -1,6 +1,6 @@
import { Feed } from "feed";
-import { getPosts } from "@/lib/blog/query";
-import { SITE_INFO, SITE_URL } from "@/lib/site/brand";
+import { getPosts } from "@/blog/query";
+import { SITE_INFO, SITE_URL } from "@/site/brand";
export async function GET() {
try {
diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts
index 7b404642..73b4180f 100644
--- a/apps/web/src/app/sitemap.ts
+++ b/apps/web/src/app/sitemap.ts
@@ -1,5 +1,5 @@
-import { SITE_URL } from "@/lib/site/brand";
-import { getPosts } from "@/lib/blog/query";
+import { SITE_URL } from "@/site/brand";
+import { getPosts } from "@/blog/query";
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
diff --git a/apps/web/src/app/sponsors/page.tsx b/apps/web/src/app/sponsors/page.tsx
index 0de78c85..510419f0 100644
--- a/apps/web/src/app/sponsors/page.tsx
+++ b/apps/web/src/app/sponsors/page.tsx
@@ -3,7 +3,7 @@ import Image from "next/image";
import Link from "next/link";
import { BasePage } from "@/app/base-page";
import { Card, CardContent } from "@/components/ui/card";
-import { SPONSORS, type Sponsor } from "@/lib/site/sponsors";
+import { SPONSORS, type Sponsor } from "@/site/sponsors";
import { HugeiconsIcon } from "@hugeicons/react";
import { LinkSquare02Icon } from "@hugeicons/core-free-icons";
import { cn } from "@/utils/ui";
diff --git a/apps/web/src/app/terms/page.tsx b/apps/web/src/app/terms/page.tsx
index 73a6cce1..0845892c 100644
--- a/apps/web/src/app/terms/page.tsx
+++ b/apps/web/src/app/terms/page.tsx
@@ -7,7 +7,7 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { Separator } from "@/components/ui/separator";
-import { SOCIAL_LINKS } from "@/lib/site/social";
+import { SOCIAL_LINKS } from "@/site/social";
export const metadata: Metadata = {
title: "Terms of Service - OpenCut",
diff --git a/apps/web/src/lib/auth/client.ts b/apps/web/src/auth/client.ts
index 3bec5072..58be51c3 100644
--- a/apps/web/src/lib/auth/client.ts
+++ b/apps/web/src/auth/client.ts
@@ -1,5 +1,5 @@
import { createAuthClient } from "better-auth/react";
-import { webEnv } from "@/lib/env/web";
+import { webEnv } from "@/env/web";
export const { signIn, signUp, useSession } = createAuthClient({
baseURL: webEnv.NEXT_PUBLIC_SITE_URL,
diff --git a/apps/web/src/lib/rate-limit.ts b/apps/web/src/auth/rate-limit.ts
index 234bf48f..c10c3819 100644
--- a/apps/web/src/lib/rate-limit.ts
+++ b/apps/web/src/auth/rate-limit.ts
@@ -1,6 +1,6 @@
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
-import { webEnv } from "@/lib/env/web";
+import { webEnv } from "@/env/web";
const redis = new Redis({
url: webEnv.UPSTASH_REDIS_REST_URL,
diff --git a/apps/web/src/lib/auth/server.ts b/apps/web/src/auth/server.ts
index 39fce38f..5e49387a 100644
--- a/apps/web/src/lib/auth/server.ts
+++ b/apps/web/src/auth/server.ts
@@ -1,8 +1,8 @@
import { betterAuth, type RateLimit } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { Redis } from "@upstash/redis";
-import { db } from "@/lib/db";
-import { webEnv } from "@/lib/env/web";
+import { db } from "@/db";
+import { webEnv } from "@/env/web";
const redis = new Redis({
url: webEnv.UPSTASH_REDIS_REST_URL,
diff --git a/apps/web/src/lib/background/blur.ts b/apps/web/src/background/blur.ts
index 3b111482..3b111482 100644
--- a/apps/web/src/lib/background/blur.ts
+++ b/apps/web/src/background/blur.ts
diff --git a/apps/web/src/lib/background/color.ts b/apps/web/src/background/color.ts
index 7b86faf5..7b86faf5 100644
--- a/apps/web/src/lib/background/color.ts
+++ b/apps/web/src/background/color.ts
diff --git a/apps/web/src/lib/blog/query.ts b/apps/web/src/blog/query.ts
index 91c2a514..a7742db2 100644
--- a/apps/web/src/lib/blog/query.ts
+++ b/apps/web/src/blog/query.ts
@@ -5,7 +5,7 @@ import type {
MarblePost,
MarblePostList,
MarbleTagList,
-} from "@/lib/blog/types";
+} from "@/blog/types";
import { unified } from "unified";
import rehypeParse from "rehype-parse";
import rehypeStringify from "rehype-stringify";
diff --git a/apps/web/src/lib/blog/types.ts b/apps/web/src/blog/types.ts
index 7b2f450a..7b2f450a 100644
--- a/apps/web/src/lib/blog/types.ts
+++ b/apps/web/src/blog/types.ts
diff --git a/apps/web/src/lib/canvas/sizes.ts b/apps/web/src/canvas/sizes.ts
index dce75bcb..76361336 100644
--- a/apps/web/src/lib/canvas/sizes.ts
+++ b/apps/web/src/canvas/sizes.ts
@@ -1,4 +1,4 @@
-import type { TCanvasSize } from "@/lib/project/types";
+import type { TCanvasSize } from "@/project/types";
export const DEFAULT_CANVAS_PRESETS: TCanvasSize[] = [
{ width: 1920, height: 1080 },
diff --git a/apps/web/src/lib/changelog/components/changelog-notification.tsx b/apps/web/src/changelog/components/changelog-notification.tsx
index 4433df30..4433df30 100644
--- a/apps/web/src/lib/changelog/components/changelog-notification.tsx
+++ b/apps/web/src/changelog/components/changelog-notification.tsx
diff --git a/apps/web/src/lib/changelog/components/copy-markdown-button.tsx b/apps/web/src/changelog/components/copy-markdown-button.tsx
index bc7e94bf..bc7e94bf 100644
--- a/apps/web/src/lib/changelog/components/copy-markdown-button.tsx
+++ b/apps/web/src/changelog/components/copy-markdown-button.tsx
diff --git a/apps/web/src/lib/changelog/components/release.tsx b/apps/web/src/changelog/components/release.tsx
index 475b0a6d..475b0a6d 100644
--- a/apps/web/src/lib/changelog/components/release.tsx
+++ b/apps/web/src/changelog/components/release.tsx
diff --git a/apps/web/src/lib/changelog/entries/0.1.0.md b/apps/web/src/changelog/entries/0.1.0.md
index 33c5413c..33c5413c 100644
--- a/apps/web/src/lib/changelog/entries/0.1.0.md
+++ b/apps/web/src/changelog/entries/0.1.0.md
diff --git a/apps/web/src/lib/changelog/entries/0.2.0.md b/apps/web/src/changelog/entries/0.2.0.md
index 261b47d2..261b47d2 100644
--- a/apps/web/src/lib/changelog/entries/0.2.0.md
+++ b/apps/web/src/changelog/entries/0.2.0.md
diff --git a/apps/web/src/lib/changelog/entries/0.3.0.md b/apps/web/src/changelog/entries/0.3.0.md
index 86c62cff..86c62cff 100644
--- a/apps/web/src/lib/changelog/entries/0.3.0.md
+++ b/apps/web/src/changelog/entries/0.3.0.md
diff --git a/apps/web/src/lib/changelog/entries/0.4.0.md b/apps/web/src/changelog/entries/0.4.0.md
index 141df133..141df133 100644
--- a/apps/web/src/lib/changelog/entries/0.4.0.md
+++ b/apps/web/src/changelog/entries/0.4.0.md
diff --git a/apps/web/src/lib/changelog/utils.ts b/apps/web/src/changelog/utils.ts
index 682d376a..682d376a 100644
--- a/apps/web/src/lib/changelog/utils.ts
+++ b/apps/web/src/changelog/utils.ts
diff --git a/apps/web/src/lib/clipboard/handlers/elements.ts b/apps/web/src/clipboard/handlers/elements.ts
index 1b58ac78..b2f5f2c3 100644
--- a/apps/web/src/lib/clipboard/handlers/elements.ts
+++ b/apps/web/src/clipboard/handlers/elements.ts
@@ -1,4 +1,4 @@
-import { PasteCommand } from "@/lib/commands/timeline";
+import { PasteCommand } from "@/commands/timeline";
import type { ClipboardHandler } from "../types";
export const ElementsClipboardHandler = {
diff --git a/apps/web/src/lib/clipboard/handlers/index.ts b/apps/web/src/clipboard/handlers/index.ts
index 29d4d1d0..29d4d1d0 100644
--- a/apps/web/src/lib/clipboard/handlers/index.ts
+++ b/apps/web/src/clipboard/handlers/index.ts
diff --git a/apps/web/src/lib/clipboard/handlers/keyframes.ts b/apps/web/src/clipboard/handlers/keyframes.ts
index b746c179..ad156e12 100644
--- a/apps/web/src/lib/clipboard/handlers/keyframes.ts
+++ b/apps/web/src/clipboard/handlers/keyframes.ts
@@ -1,7 +1,7 @@
-import { getKeyframeById } from "@/lib/animation";
-import type { SelectedKeyframeRef } from "@/lib/animation/types";
-import type { TimelineElement } from "@/lib/timeline";
-import { PasteKeyframesCommand } from "@/lib/commands/timeline";
+import { getKeyframeById } from "@/animation";
+import type { SelectedKeyframeRef } from "@/animation/types";
+import type { TimelineElement } from "@/timeline";
+import { PasteKeyframesCommand } from "@/commands/timeline";
import type {
ClipboardHandler,
KeyframeClipboardCurvePatch,
diff --git a/apps/web/src/lib/clipboard/index.ts b/apps/web/src/clipboard/index.ts
index 76d5bddf..76d5bddf 100644
--- a/apps/web/src/lib/clipboard/index.ts
+++ b/apps/web/src/clipboard/index.ts
diff --git a/apps/web/src/lib/clipboard/types.ts b/apps/web/src/clipboard/types.ts
index 41e107ef..0d249c9d 100644
--- a/apps/web/src/lib/clipboard/types.ts
+++ b/apps/web/src/clipboard/types.ts
@@ -5,13 +5,13 @@ import type {
AnimationValue,
ScalarCurveKeyframePatch,
SelectedKeyframeRef,
-} from "@/lib/animation/types";
-import type { Command } from "@/lib/commands/base-command";
+} from "@/animation/types";
+import type { Command } from "@/commands/base-command";
import type {
CreateTimelineElement,
ElementRef,
TrackType,
-} from "@/lib/timeline";
+} from "@/timeline";
export interface ElementClipboardItem {
trackId: string;
diff --git a/apps/web/src/lib/commands/base-command.ts b/apps/web/src/commands/base-command.ts
index b244f2f3..2ab9af09 100644
--- a/apps/web/src/lib/commands/base-command.ts
+++ b/apps/web/src/commands/base-command.ts
@@ -1,5 +1,5 @@
-import type { EditorSelectionPatch } from "@/lib/selection/editor-selection";
-import type { ElementRef } from "@/lib/timeline/types";
+import type { EditorSelectionPatch } from "@/selection/editor-selection";
+import type { ElementRef } from "@/timeline/types";
export interface CommandResult {
selection?: EditorSelectionPatch;
diff --git a/apps/web/src/lib/commands/batch-command.ts b/apps/web/src/commands/batch-command.ts
index c4ee7e0b..c4ee7e0b 100644
--- a/apps/web/src/lib/commands/batch-command.ts
+++ b/apps/web/src/commands/batch-command.ts
diff --git a/apps/web/src/lib/commands/index.ts b/apps/web/src/commands/index.ts
index 78927ca9..78927ca9 100644
--- a/apps/web/src/lib/commands/index.ts
+++ b/apps/web/src/commands/index.ts
diff --git a/apps/web/src/lib/commands/media/add-media-asset.ts b/apps/web/src/commands/media/add-media-asset.ts
index 12e0906e..d22241a2 100644
--- a/apps/web/src/lib/commands/media/add-media-asset.ts
+++ b/apps/web/src/commands/media/add-media-asset.ts
@@ -1,13 +1,13 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import { toast } from "sonner";
-import type { MediaAsset } from "@/lib/media/types";
+import type { MediaAsset } from "@/media/types";
import { generateUUID } from "@/utils/id";
import { storageService } from "@/services/storage/service";
import type { FrameRate } from "opencut-wasm";
-import { hasMediaId } from "@/lib/timeline/element-utils";
-import { frameRatesEqual, getHighestImportedVideoFps } from "@/lib/fps/utils";
-import { UpdateProjectSettingsCommand } from "@/lib/commands/project";
+import { hasMediaId } from "@/timeline/element-utils";
+import { frameRatesEqual, getHighestImportedVideoFps } from "@/fps/utils";
+import { UpdateProjectSettingsCommand } from "@/commands/project";
export class AddMediaAssetCommand extends Command {
private assetId: string;
diff --git a/apps/web/src/lib/commands/media/index.ts b/apps/web/src/commands/media/index.ts
index 466222f2..466222f2 100644
--- a/apps/web/src/lib/commands/media/index.ts
+++ b/apps/web/src/commands/media/index.ts
diff --git a/apps/web/src/lib/commands/media/remove-media-asset.ts b/apps/web/src/commands/media/remove-media-asset.ts
index 85d187d9..8da62181 100644
--- a/apps/web/src/lib/commands/media/remove-media-asset.ts
+++ b/apps/web/src/commands/media/remove-media-asset.ts
@@ -1,12 +1,12 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
-import type { MediaAsset } from "@/lib/media/types";
-import { buildWaveformSourceKey } from "@/lib/media/waveform-summary";
+import type { MediaAsset } from "@/media/types";
+import { buildWaveformSourceKey } from "@/media/waveform-summary";
import { storageService } from "@/services/storage/service";
import { videoCache } from "@/services/video-cache/service";
import { waveformCache } from "@/services/waveform-cache/service";
-import { hasMediaId } from "@/lib/timeline/element-utils";
-import type { SceneTracks } from "@/lib/timeline";
+import { hasMediaId } from "@/timeline/element-utils";
+import type { SceneTracks } from "@/timeline";
export class RemoveMediaAssetCommand extends Command {
private savedAssets: MediaAsset[] | null = null;
diff --git a/apps/web/src/lib/commands/preview-tracker.ts b/apps/web/src/commands/preview-tracker.ts
index 68c8dee3..68c8dee3 100644
--- a/apps/web/src/lib/commands/preview-tracker.ts
+++ b/apps/web/src/commands/preview-tracker.ts
diff --git a/apps/web/src/lib/commands/project/index.ts b/apps/web/src/commands/project/index.ts
index a9f26edc..a9f26edc 100644
--- a/apps/web/src/lib/commands/project/index.ts
+++ b/apps/web/src/commands/project/index.ts
diff --git a/apps/web/src/lib/commands/project/update-project-settings.ts b/apps/web/src/commands/project/update-project-settings.ts
index 32258e12..8d653db2 100644
--- a/apps/web/src/lib/commands/project/update-project-settings.ts
+++ b/apps/web/src/commands/project/update-project-settings.ts
@@ -1,6 +1,6 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
-import type { TProject, TProjectSettings } from "@/lib/project/types";
+import type { TProject, TProjectSettings } from "@/project/types";
export class UpdateProjectSettingsCommand extends Command {
private savedSettings: TProjectSettings | null = null;
diff --git a/apps/web/src/lib/commands/scene/create-scene.ts b/apps/web/src/commands/scene/create-scene.ts
index 1cc72c0f..b554fe90 100644
--- a/apps/web/src/lib/commands/scene/create-scene.ts
+++ b/apps/web/src/commands/scene/create-scene.ts
@@ -1,7 +1,7 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
-import type { TScene } from "@/lib/timeline";
-import { buildDefaultScene } from "@/lib/scenes";
+import type { TScene } from "@/timeline";
+import { buildDefaultScene } from "@/timeline/scenes";
export class CreateSceneCommand extends Command {
private savedScenes: TScene[] | null = null;
diff --git a/apps/web/src/lib/commands/scene/delete-scene.ts b/apps/web/src/commands/scene/delete-scene.ts
index ab269b12..92de518f 100644
--- a/apps/web/src/lib/commands/scene/delete-scene.ts
+++ b/apps/web/src/commands/scene/delete-scene.ts
@@ -1,7 +1,7 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
-import type { TScene } from "@/lib/timeline";
-import { canDeleteScene, getFallbackSceneAfterDelete } from "@/lib/scenes";
+import type { TScene } from "@/timeline";
+import { canDeleteScene, getFallbackSceneAfterDelete } from "@/timeline/scenes";
export class DeleteSceneCommand extends Command {
private savedScenes: TScene[] | null = null;
diff --git a/apps/web/src/lib/commands/scene/index.ts b/apps/web/src/commands/scene/index.ts
index 2b843c99..2b843c99 100644
--- a/apps/web/src/lib/commands/scene/index.ts
+++ b/apps/web/src/commands/scene/index.ts
diff --git a/apps/web/src/lib/commands/scene/move-bookmark.ts b/apps/web/src/commands/scene/move-bookmark.ts
index 103caa74..a1624eb0 100644
--- a/apps/web/src/lib/commands/scene/move-bookmark.ts
+++ b/apps/web/src/commands/scene/move-bookmark.ts
@@ -1,8 +1,8 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
-import type { TScene } from "@/lib/timeline";
-import { updateSceneInArray } from "@/lib/scenes";
-import { getFrameTime, moveBookmarkInArray } from "@/lib/timeline/bookmarks";
+import type { TScene } from "@/timeline";
+import { updateSceneInArray } from "@/timeline/scenes";
+import { getFrameTime, moveBookmarkInArray } from "@/timeline/bookmarks/index";
export class MoveBookmarkCommand extends Command {
private savedScenes: TScene[] | null = null;
diff --git a/apps/web/src/lib/commands/scene/remove-bookmark.ts b/apps/web/src/commands/scene/remove-bookmark.ts
index 5eac920f..ea592556 100644
--- a/apps/web/src/lib/commands/scene/remove-bookmark.ts
+++ b/apps/web/src/commands/scene/remove-bookmark.ts
@@ -1,11 +1,11 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
-import type { TScene } from "@/lib/timeline";
-import { updateSceneInArray } from "@/lib/scenes";
+import type { TScene } from "@/timeline";
+import { updateSceneInArray } from "@/timeline/scenes";
import {
getFrameTime,
removeBookmarkFromArray,
-} from "@/lib/timeline/bookmarks";
+} from "@/timeline/bookmarks/index";
export class RemoveBookmarkCommand extends Command {
private savedScenes: TScene[] | null = null;
diff --git a/apps/web/src/lib/commands/scene/rename-scene.ts b/apps/web/src/commands/scene/rename-scene.ts
index ccdc85d6..19d6bfd8 100644
--- a/apps/web/src/lib/commands/scene/rename-scene.ts
+++ b/apps/web/src/commands/scene/rename-scene.ts
@@ -1,7 +1,7 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
-import type { TScene } from "@/lib/timeline";
-import { updateSceneInArray } from "@/lib/scenes";
+import type { TScene } from "@/timeline";
+import { updateSceneInArray } from "@/timeline/scenes";
export class RenameSceneCommand extends Command {
private savedScenes: TScene[] | null = null;
diff --git a/apps/web/src/lib/commands/scene/toggle-bookmark.ts b/apps/web/src/commands/scene/toggle-bookmark.ts
index cb4fa655..4193c76e 100644
--- a/apps/web/src/lib/commands/scene/toggle-bookmark.ts
+++ b/apps/web/src/commands/scene/toggle-bookmark.ts
@@ -1,8 +1,11 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
-import type { TScene } from "@/lib/timeline";
-import { updateSceneInArray } from "@/lib/scenes";
-import { getFrameTime, toggleBookmarkInArray } from "@/lib/timeline/bookmarks";
+import type { TScene } from "@/timeline";
+import { updateSceneInArray } from "@/timeline/scenes";
+import {
+ getFrameTime,
+ toggleBookmarkInArray,
+} from "@/timeline/bookmarks/index";
export class ToggleBookmarkCommand extends Command {
private savedScenes: TScene[] | null = null;
diff --git a/apps/web/src/lib/commands/scene/update-bookmark.ts b/apps/web/src/commands/scene/update-bookmark.ts
index 1d1c7283..0d6a0193 100644
--- a/apps/web/src/lib/commands/scene/update-bookmark.ts
+++ b/apps/web/src/commands/scene/update-bookmark.ts
@@ -1,8 +1,11 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
-import type { Bookmark, TScene } from "@/lib/timeline";
-import { updateSceneInArray } from "@/lib/scenes";
-import { getFrameTime, updateBookmarkInArray } from "@/lib/timeline/bookmarks";
+import type { Bookmark, TScene } from "@/timeline";
+import { updateSceneInArray } from "@/timeline/scenes";
+import {
+ getFrameTime,
+ updateBookmarkInArray,
+} from "@/timeline/bookmarks/index";
export class UpdateBookmarkCommand extends Command {
private savedScenes: TScene[] | null = null;
diff --git a/apps/web/src/lib/commands/timeline/clipboard/index.ts b/apps/web/src/commands/timeline/clipboard/index.ts
index e893b62b..e893b62b 100644
--- a/apps/web/src/lib/commands/timeline/clipboard/index.ts
+++ b/apps/web/src/commands/timeline/clipboard/index.ts
diff --git a/apps/web/src/lib/commands/timeline/clipboard/paste-keyframes.ts b/apps/web/src/commands/timeline/clipboard/paste-keyframes.ts
index 210afbd3..e05b49fa 100644
--- a/apps/web/src/lib/commands/timeline/clipboard/paste-keyframes.ts
+++ b/apps/web/src/commands/timeline/clipboard/paste-keyframes.ts
@@ -4,11 +4,11 @@ import {
resolveAnimationTarget,
updateScalarKeyframeCurve,
upsertPathKeyframe,
-} from "@/lib/animation";
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import type { KeyframeClipboardItem } from "@/lib/clipboard";
-import type { SceneTracks, TimelineElement } from "@/lib/timeline";
-import { updateElementInSceneTracks } from "@/lib/timeline";
+} from "@/animation";
+import { Command, type CommandResult } from "@/commands/base-command";
+import type { KeyframeClipboardItem } from "@/clipboard";
+import type { SceneTracks, TimelineElement } from "@/timeline";
+import { updateElementInSceneTracks } from "@/timeline";
import { generateUUID } from "@/utils/id";
function pasteKeyframesIntoElement({
diff --git a/apps/web/src/commands/timeline/clipboard/paste.ts b/apps/web/src/commands/timeline/clipboard/paste.ts
new file mode 100644
index 00000000..4523566e
--- /dev/null
+++ b/apps/web/src/commands/timeline/clipboard/paste.ts
@@ -0,0 +1,193 @@
+import {
+ Command,
+ createElementSelectionResult,
+ type CommandResult,
+} from "@/commands/base-command";
+import { EditorCore } from "@/core";
+import type { SceneTracks, TimelineElement } from "@/timeline";
+import type { ElementClipboardItem } from "@/clipboard";
+import { generateUUID } from "@/utils/id";
+import {
+ applyPlacement,
+ resolveTrackPlacement,
+ enforceMainTrackStart,
+} from "@/timeline/placement";
+import { cloneAnimations } from "@/animation";
+
+export class PasteCommand extends Command {
+ private savedState: SceneTracks | null = null;
+ private pastedElements: { trackId: string; elementId: string }[] = [];
+ private readonly time: number;
+ private readonly clipboardItems: ElementClipboardItem[];
+
+ constructor({
+ time,
+ clipboardItems,
+ }: {
+ time: number;
+ clipboardItems: ElementClipboardItem[];
+ }) {
+ super();
+ this.time = time;
+ this.clipboardItems = clipboardItems;
+ }
+
+ execute(): CommandResult | undefined {
+ if (this.clipboardItems.length === 0) return undefined;
+
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.scenes.getActiveScene().tracks;
+ this.pastedElements = [];
+
+ const minStart = Math.min(
+ ...this.clipboardItems.map((item) => item.element.startTime),
+ );
+
+ let updatedTracks = this.savedState;
+ const itemsByTrackId = groupClipboardItemsByTrackId({
+ clipboardItems: this.clipboardItems,
+ });
+
+ for (const [trackId, items] of itemsByTrackId) {
+ const elementsToAdd = buildPastedElements({
+ items,
+ minStart,
+ time: this.time,
+ });
+
+ if (elementsToAdd.length === 0) {
+ continue;
+ }
+
+ const trackType = items[0].trackType;
+ const sourceTrackIndex = [
+ ...updatedTracks.overlay,
+ updatedTracks.main,
+ ...updatedTracks.audio,
+ ].findIndex((track) => track.id === trackId);
+ const placementResult = resolveTrackPlacement({
+ tracks: updatedTracks,
+ trackType,
+ timeSpans: elementsToAdd.map((element) => ({
+ startTime: element.startTime,
+ duration: element.duration,
+ })),
+ strategy: { type: "aboveSource", sourceTrackIndex },
+ });
+ if (!placementResult) {
+ continue;
+ }
+
+ let elementsForPlacement = elementsToAdd;
+ if (placementResult.kind === "existingTrack") {
+ const targetTrack =
+ placementResult.trackIndex < updatedTracks.overlay.length
+ ? updatedTracks.overlay[placementResult.trackIndex]
+ : placementResult.trackIndex === updatedTracks.overlay.length
+ ? updatedTracks.main
+ : updatedTracks.audio[
+ placementResult.trackIndex - updatedTracks.overlay.length - 1
+ ];
+ if (targetTrack?.id === updatedTracks.main.id) {
+ const earliestElement = elementsToAdd.reduce((earliest, element) =>
+ element.startTime < earliest.startTime ? element : earliest,
+ );
+ const adjustedEarliestStartTime = enforceMainTrackStart({
+ tracks: updatedTracks,
+ targetTrackId: targetTrack.id,
+ requestedStartTime: earliestElement.startTime,
+ });
+ const delta = adjustedEarliestStartTime - earliestElement.startTime;
+
+ if (delta !== 0) {
+ elementsForPlacement = elementsToAdd.map((element) => ({
+ ...element,
+ startTime: Math.max(0, element.startTime + delta),
+ }));
+ }
+ }
+ }
+
+ const applied = applyPlacement({
+ tracks: updatedTracks,
+ placementResult,
+ elements: elementsForPlacement,
+ });
+ if (!applied) {
+ continue;
+ }
+
+ updatedTracks = applied.updatedTracks;
+
+ for (const element of elementsForPlacement) {
+ this.pastedElements.push({
+ trackId: applied.targetTrackId,
+ elementId: element.id,
+ });
+ }
+ }
+
+ editor.timeline.updateTracks(updatedTracks);
+
+ if (this.pastedElements.length > 0) {
+ return createElementSelectionResult(this.pastedElements);
+ }
+ return undefined;
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+
+ getPastedElements(): { trackId: string; elementId: string }[] {
+ return this.pastedElements;
+ }
+}
+
+function groupClipboardItemsByTrackId({
+ clipboardItems,
+}: {
+ clipboardItems: ElementClipboardItem[];
+}): Map<string, ElementClipboardItem[]> {
+ const groupedItems = new Map<string, ElementClipboardItem[]>();
+
+ for (const item of clipboardItems) {
+ const existingItems = groupedItems.get(item.trackId) ?? [];
+ groupedItems.set(item.trackId, [...existingItems, item]);
+ }
+
+ return groupedItems;
+}
+
+function buildPastedElements({
+ items,
+ minStart,
+ time,
+}: {
+ items: ElementClipboardItem[];
+ minStart: number;
+ time: number;
+}): TimelineElement[] {
+ const elementsToAdd: TimelineElement[] = [];
+
+ for (const item of items) {
+ const relativeOffset = item.element.startTime - minStart;
+ const startTime = Math.max(0, time + relativeOffset);
+ const newElementId = generateUUID();
+
+ elementsToAdd.push({
+ ...item.element,
+ id: newElementId,
+ startTime,
+ animations: cloneAnimations({
+ animations: item.element.animations,
+ shouldRegenerateKeyframeIds: true,
+ }),
+ } as TimelineElement);
+ }
+
+ return elementsToAdd;
+}
diff --git a/apps/web/src/lib/commands/timeline/element/delete-elements.ts b/apps/web/src/commands/timeline/element/delete-elements.ts
index 0fc7948f..ad0267b7 100644
--- a/apps/web/src/lib/commands/timeline/element/delete-elements.ts
+++ b/apps/web/src/commands/timeline/element/delete-elements.ts
@@ -1,7 +1,7 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import type { SceneTracks } from "@/lib/timeline";
+import { Command, type CommandResult } from "@/commands/base-command";
+import type { SceneTracks } from "@/timeline";
import { EditorCore } from "@/core";
-import type { TimelineTrack } from "@/lib/timeline";
+import type { TimelineTrack } from "@/timeline";
function removeTrackElements<TTrack extends TimelineTrack>({
track,
diff --git a/apps/web/src/commands/timeline/element/duplicate-elements.ts b/apps/web/src/commands/timeline/element/duplicate-elements.ts
new file mode 100644
index 00000000..8f65f7c7
--- /dev/null
+++ b/apps/web/src/commands/timeline/element/duplicate-elements.ts
@@ -0,0 +1,134 @@
+import {
+ Command,
+ createElementSelectionResult,
+ type CommandResult,
+} from "@/commands/base-command";
+import type { SceneTracks, TimelineElement } from "@/timeline";
+import { generateUUID } from "@/utils/id";
+import { EditorCore } from "@/core";
+import { applyPlacement, resolveTrackPlacement } from "@/timeline/placement";
+import { cloneAnimations } from "@/animation";
+
+interface DuplicateElementsParams {
+ elements: { trackId: string; elementId: string }[];
+}
+
+export class DuplicateElementsCommand extends Command {
+ private duplicatedElements: { trackId: string; elementId: string }[] = [];
+ private savedState: SceneTracks | null = null;
+ private elements: DuplicateElementsParams["elements"];
+
+ constructor({ elements }: DuplicateElementsParams) {
+ super();
+ this.elements = elements;
+ }
+
+ execute(): CommandResult | undefined {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.scenes.getActiveScene().tracks;
+ this.duplicatedElements = [];
+
+ let updatedTracks = this.savedState;
+
+ for (const track of [
+ ...this.savedState.overlay,
+ this.savedState.main,
+ ...this.savedState.audio,
+ ]) {
+ const elementsToDuplicate = this.elements.filter(
+ (elementEntry) => elementEntry.trackId === track.id,
+ );
+
+ if (elementsToDuplicate.length === 0) {
+ continue;
+ }
+
+ const elementIdsToDuplicate = new Set(
+ elementsToDuplicate.map((element) => element.elementId),
+ );
+ const newTrackElements: TimelineElement[] = [];
+
+ for (const element of track.elements) {
+ if (!elementIdsToDuplicate.has(element.id)) {
+ continue;
+ }
+
+ const newId = generateUUID();
+ newTrackElements.push(
+ buildDuplicateElement({
+ element,
+ id: newId,
+ startTime: element.startTime,
+ }),
+ );
+ }
+
+ const placementResult = resolveTrackPlacement({
+ tracks: updatedTracks,
+ trackType: track.type,
+ timeSpans: [],
+ strategy: { type: "alwaysNew", position: "highest" },
+ });
+ if (!placementResult || placementResult.kind !== "newTrack") {
+ continue;
+ }
+
+ const applied = applyPlacement({
+ tracks: updatedTracks,
+ placementResult,
+ elements: newTrackElements,
+ });
+ if (!applied) {
+ continue;
+ }
+
+ updatedTracks = applied.updatedTracks;
+
+ for (const element of newTrackElements) {
+ this.duplicatedElements.push({
+ trackId: applied.targetTrackId,
+ elementId: element.id,
+ });
+ }
+ }
+
+ editor.timeline.updateTracks(updatedTracks);
+
+ if (this.duplicatedElements.length > 0) {
+ return createElementSelectionResult(this.duplicatedElements);
+ }
+ return undefined;
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+
+ getDuplicatedElements(): { trackId: string; elementId: string }[] {
+ return this.duplicatedElements;
+ }
+}
+
+function buildDuplicateElement({
+ element,
+ id,
+ startTime,
+}: {
+ element: TimelineElement;
+ id: string;
+ startTime: number;
+}): TimelineElement {
+ return {
+ ...element,
+ id,
+ name: `${element.name} (copy)`,
+ startTime,
+ animations: cloneAnimations({
+ animations: element.animations,
+ shouldRegenerateKeyframeIds: true,
+ }),
+ };
+}
diff --git a/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts b/apps/web/src/commands/timeline/element/effects/add-effect.ts
index 902ca341..be08b433 100644
--- a/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts
+++ b/apps/web/src/commands/timeline/element/effects/add-effect.ts
@@ -1,8 +1,8 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
-import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline";
-import type { SceneTracks, VisualElement } from "@/lib/timeline";
-import { buildDefaultEffectInstance } from "@/lib/effects";
+import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
+import type { SceneTracks, VisualElement } from "@/timeline";
+import { buildDefaultEffectInstance } from "@/effects";
function addEffectToElement({
element,
diff --git a/apps/web/src/lib/commands/timeline/element/effects/index.ts b/apps/web/src/commands/timeline/element/effects/index.ts
index 133ce661..133ce661 100644
--- a/apps/web/src/lib/commands/timeline/element/effects/index.ts
+++ b/apps/web/src/commands/timeline/element/effects/index.ts
diff --git a/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts b/apps/web/src/commands/timeline/element/effects/remove-effect.ts
index f453f2cc..e6ef9934 100644
--- a/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts
+++ b/apps/web/src/commands/timeline/element/effects/remove-effect.ts
@@ -1,7 +1,7 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
-import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline";
-import type { SceneTracks, VisualElement } from "@/lib/timeline";
+import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
+import type { SceneTracks, VisualElement } from "@/timeline";
function removeEffectFromElement({
element,
diff --git a/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts b/apps/web/src/commands/timeline/element/effects/reorder-effect.ts
index 28b7c4e1..554804e2 100644
--- a/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts
+++ b/apps/web/src/commands/timeline/element/effects/reorder-effect.ts
@@ -1,7 +1,7 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
-import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline";
-import type { SceneTracks, VisualElement } from "@/lib/timeline";
+import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
+import type { SceneTracks, VisualElement } from "@/timeline";
function reorderEffectsOnElement({
element,
diff --git a/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts b/apps/web/src/commands/timeline/element/effects/toggle-effect.ts
index a703cd94..89149a79 100644
--- a/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts
+++ b/apps/web/src/commands/timeline/element/effects/toggle-effect.ts
@@ -1,7 +1,7 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
-import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline";
-import type { SceneTracks, VisualElement } from "@/lib/timeline";
+import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
+import type { SceneTracks, VisualElement } from "@/timeline";
export function toggleEffectOnElement({
element,
diff --git a/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts b/apps/web/src/commands/timeline/element/effects/update-effect-params.ts
index 5e75c1f0..62b3c937 100644
--- a/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts
+++ b/apps/web/src/commands/timeline/element/effects/update-effect-params.ts
@@ -1,8 +1,8 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
-import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline";
-import type { ParamValues } from "@/lib/params";
-import type { SceneTracks, VisualElement } from "@/lib/timeline";
+import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
+import type { ParamValues } from "@/params";
+import type { SceneTracks, VisualElement } from "@/timeline";
function updateEffectParamsOnElement({
element,
diff --git a/apps/web/src/lib/commands/timeline/element/index.ts b/apps/web/src/commands/timeline/element/index.ts
index ff365801..ff365801 100644
--- a/apps/web/src/lib/commands/timeline/element/index.ts
+++ b/apps/web/src/commands/timeline/element/index.ts
diff --git a/apps/web/src/lib/commands/timeline/element/insert-element.ts b/apps/web/src/commands/timeline/element/insert-element.ts
index ed05b8c6..49ac6f2d 100644
--- a/apps/web/src/lib/commands/timeline/element/insert-element.ts
+++ b/apps/web/src/commands/timeline/element/insert-element.ts
@@ -1,23 +1,23 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type {
CreateTimelineElement,
SceneTracks,
TimelineElement,
TrackType,
-} from "@/lib/timeline";
+} from "@/timeline";
import { generateUUID } from "@/utils/id";
-import { requiresMediaId } from "@/lib/timeline/element-utils";
-import type { MediaAsset } from "@/lib/media/types";
-import { DEFAULT_NEW_ELEMENT_DURATION } from "@/lib/timeline/creation";
-import { floatToFrameRate } from "@/lib/fps/utils";
-import { graphicsRegistry, registerDefaultGraphics } from "@/lib/graphics";
+import { requiresMediaId } from "@/timeline/element-utils";
+import type { MediaAsset } from "@/media/types";
+import { DEFAULT_NEW_ELEMENT_DURATION } from "@/timeline/creation";
+import { floatToFrameRate } from "@/fps/utils";
+import { graphicsRegistry, registerDefaultGraphics } from "@/graphics";
import {
applyPlacement,
canElementGoOnTrack,
resolveTrackPlacement,
validateElementTrackCompatibility,
-} from "@/lib/timeline/placement";
+} from "@/timeline/placement";
type InsertElementPlacement =
| { mode: "explicit"; trackId: string }
diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/index.ts b/apps/web/src/commands/timeline/element/keyframes/index.ts
index 319a827b..319a827b 100644
--- a/apps/web/src/lib/commands/timeline/element/keyframes/index.ts
+++ b/apps/web/src/commands/timeline/element/keyframes/index.ts
diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts b/apps/web/src/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts
index b1ef4aef..c049d5bb 100644
--- a/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts
+++ b/apps/web/src/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts
@@ -1,9 +1,9 @@
import { EditorCore } from "@/core";
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import { removeEffectParamKeyframe } from "@/lib/animation/effect-param-channel";
-import { updateElementInSceneTracks } from "@/lib/timeline";
-import { isVisualElement } from "@/lib/timeline/element-utils";
-import type { SceneTracks } from "@/lib/timeline";
+import { Command, type CommandResult } from "@/commands/base-command";
+import { removeEffectParamKeyframe } from "@/animation/effect-param-channel";
+import { updateElementInSceneTracks } from "@/timeline";
+import { isVisualElement } from "@/timeline/element-utils";
+import type { SceneTracks } from "@/timeline";
export class RemoveEffectParamKeyframeCommand extends Command {
private savedState: SceneTracks | null = null;
diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts b/apps/web/src/commands/timeline/element/keyframes/remove-keyframe.ts
index 3c3d1c6b..ed917406 100644
--- a/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts
+++ b/apps/web/src/commands/timeline/element/keyframes/remove-keyframe.ts
@@ -3,11 +3,11 @@ import {
hasKeyframesForPath,
removeElementKeyframe,
resolveAnimationTarget,
-} from "@/lib/animation";
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import { updateElementInSceneTracks } from "@/lib/timeline";
-import type { AnimationPath, AnimationValue } from "@/lib/animation/types";
-import type { SceneTracks, TimelineElement } from "@/lib/timeline";
+} from "@/animation";
+import { Command, type CommandResult } from "@/commands/base-command";
+import { updateElementInSceneTracks } from "@/timeline";
+import type { AnimationPath, AnimationValue } from "@/animation/types";
+import type { SceneTracks, TimelineElement } from "@/timeline";
function removeKeyframeAndPersist({
element,
diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts b/apps/web/src/commands/timeline/element/keyframes/retime-keyframe.ts
index ad7f5651..ccbdbc5d 100644
--- a/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts
+++ b/apps/web/src/commands/timeline/element/keyframes/retime-keyframe.ts
@@ -1,9 +1,9 @@
import { EditorCore } from "@/core";
-import { resolveAnimationTarget, retimeElementKeyframe } from "@/lib/animation";
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import { updateElementInSceneTracks } from "@/lib/timeline";
-import type { AnimationPath } from "@/lib/animation/types";
-import type { SceneTracks } from "@/lib/timeline";
+import { resolveAnimationTarget, retimeElementKeyframe } from "@/animation";
+import { Command, type CommandResult } from "@/commands/base-command";
+import { updateElementInSceneTracks } from "@/timeline";
+import type { AnimationPath } from "@/animation/types";
+import type { SceneTracks } from "@/timeline";
export class RetimeKeyframeCommand extends Command {
private savedState: SceneTracks | null = null;
diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts b/apps/web/src/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts
index 9a4c2304..4a3db39d 100644
--- a/apps/web/src/lib/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts
+++ b/apps/web/src/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts
@@ -2,14 +2,14 @@ import { EditorCore } from "@/core";
import {
resolveAnimationTarget,
updateScalarKeyframeCurve,
-} from "@/lib/animation";
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import { updateElementInSceneTracks } from "@/lib/timeline";
+} from "@/animation";
+import { Command, type CommandResult } from "@/commands/base-command";
+import { updateElementInSceneTracks } from "@/timeline";
import type {
AnimationPath,
ScalarCurveKeyframePatch,
-} from "@/lib/animation/types";
-import type { SceneTracks } from "@/lib/timeline";
+} from "@/animation/types";
+import type { SceneTracks } from "@/timeline";
export class UpdateScalarKeyframeCurveCommand extends Command {
private savedState: SceneTracks | null = null;
diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts b/apps/web/src/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts
index f436721a..eae8f039 100644
--- a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts
+++ b/apps/web/src/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts
@@ -1,14 +1,14 @@
import { EditorCore } from "@/core";
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import {
buildEffectParamPath,
resolveAnimationTarget,
upsertPathKeyframe,
-} from "@/lib/animation";
-import { updateElementInSceneTracks } from "@/lib/timeline";
-import { isVisualElement } from "@/lib/timeline/element-utils";
-import type { AnimationInterpolation } from "@/lib/animation/types";
-import type { SceneTracks } from "@/lib/timeline";
+} from "@/animation";
+import { updateElementInSceneTracks } from "@/timeline";
+import { isVisualElement } from "@/timeline/element-utils";
+import type { AnimationInterpolation } from "@/animation/types";
+import type { SceneTracks } from "@/timeline";
export class UpsertEffectParamKeyframeCommand extends Command {
private savedState: SceneTracks | null = null;
diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts b/apps/web/src/commands/timeline/element/keyframes/upsert-keyframe.ts
index f77e69b8..30514146 100644
--- a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts
+++ b/apps/web/src/commands/timeline/element/keyframes/upsert-keyframe.ts
@@ -1,13 +1,13 @@
import { EditorCore } from "@/core";
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import { resolveAnimationTarget, upsertPathKeyframe } from "@/lib/animation";
-import { updateElementInSceneTracks } from "@/lib/timeline";
-import type { SceneTracks } from "@/lib/timeline";
+import { Command, type CommandResult } from "@/commands/base-command";
+import { resolveAnimationTarget, upsertPathKeyframe } from "@/animation";
+import { updateElementInSceneTracks } from "@/timeline";
+import type { SceneTracks } from "@/timeline";
import type {
AnimationPath,
AnimationInterpolation,
AnimationValue,
-} from "@/lib/animation/types";
+} from "@/animation/types";
export class UpsertKeyframeCommand extends Command {
private savedState: SceneTracks | null = null;
diff --git a/apps/web/src/lib/commands/timeline/element/masks/delete-custom-mask-points.ts b/apps/web/src/commands/timeline/element/masks/delete-custom-mask-points.ts
index 346eb8d9..1445b1f8 100644
--- a/apps/web/src/lib/commands/timeline/element/masks/delete-custom-mask-points.ts
+++ b/apps/web/src/commands/timeline/element/masks/delete-custom-mask-points.ts
@@ -1,12 +1,12 @@
import { EditorCore } from "@/core";
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import {
getCustomMaskClosedStateAfterPointRemoval,
removeCustomMaskPoints,
-} from "@/lib/masks/custom-path";
-import type { CustomMask } from "@/lib/masks/types";
-import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline";
-import type { MaskableElement, SceneTracks } from "@/lib/timeline";
+} from "@/masks/custom-path";
+import type { CustomMask } from "@/masks/types";
+import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
+import type { MaskableElement, SceneTracks } from "@/timeline";
function deletePointsFromCustomMask({
mask,
diff --git a/apps/web/src/lib/commands/timeline/element/masks/index.ts b/apps/web/src/commands/timeline/element/masks/index.ts
index dd6373b1..dd6373b1 100644
--- a/apps/web/src/lib/commands/timeline/element/masks/index.ts
+++ b/apps/web/src/commands/timeline/element/masks/index.ts
diff --git a/apps/web/src/lib/commands/timeline/element/masks/insert-custom-mask-point.ts b/apps/web/src/commands/timeline/element/masks/insert-custom-mask-point.ts
index 9f837836..58559af6 100644
--- a/apps/web/src/lib/commands/timeline/element/masks/insert-custom-mask-point.ts
+++ b/apps/web/src/commands/timeline/element/masks/insert-custom-mask-point.ts
@@ -1,10 +1,10 @@
import { EditorCore } from "@/core";
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import { insertPointOnCustomMaskSegment } from "@/lib/masks/definitions/custom";
-import type { ElementBounds } from "@/lib/preview/element-bounds";
-import type { CustomMask } from "@/lib/masks/types";
-import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline";
-import type { MaskableElement, SceneTracks } from "@/lib/timeline";
+import { Command, type CommandResult } from "@/commands/base-command";
+import { insertPointOnCustomMaskSegment } from "@/masks/definitions/custom";
+import type { ElementBounds } from "@/preview/element-bounds";
+import type { CustomMask } from "@/masks/types";
+import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
+import type { MaskableElement, SceneTracks } from "@/timeline";
function insertPointIntoCustomMask({
mask,
diff --git a/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts b/apps/web/src/commands/timeline/element/masks/remove-mask.ts
index 9f6c5f77..b543bce1 100644
--- a/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts
+++ b/apps/web/src/commands/timeline/element/masks/remove-mask.ts
@@ -1,7 +1,7 @@
import { EditorCore } from "@/core";
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline";
-import type { SceneTracks, MaskableElement } from "@/lib/timeline";
+import { Command, type CommandResult } from "@/commands/base-command";
+import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
+import type { SceneTracks, MaskableElement } from "@/timeline";
function removeMaskFromElement({
element,
diff --git a/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts b/apps/web/src/commands/timeline/element/masks/toggle-mask-inverted.ts
index 2b5c1694..fec9b923 100644
--- a/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts
+++ b/apps/web/src/commands/timeline/element/masks/toggle-mask-inverted.ts
@@ -1,8 +1,8 @@
import { EditorCore } from "@/core";
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline";
-import type { Mask } from "@/lib/masks/types";
-import type { SceneTracks, MaskableElement } from "@/lib/timeline";
+import { Command, type CommandResult } from "@/commands/base-command";
+import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
+import type { Mask } from "@/masks/types";
+import type { SceneTracks, MaskableElement } from "@/timeline";
export function toggleMaskInvertedOnElement({
element,
diff --git a/apps/web/src/commands/timeline/element/move-elements.ts b/apps/web/src/commands/timeline/element/move-elements.ts
new file mode 100644
index 00000000..3ed31d42
--- /dev/null
+++ b/apps/web/src/commands/timeline/element/move-elements.ts
@@ -0,0 +1,183 @@
+import {
+ Command,
+ createElementSelectionResult,
+ type CommandResult,
+} from "@/commands/base-command";
+import { EditorCore } from "@/core";
+import type { SceneTracks, TimelineElement, TimelineTrack } from "@/timeline";
+import {
+ buildEmptyTrack,
+ validateElementTrackCompatibility,
+} from "@/timeline/placement";
+import type {
+ PlannedElementMove,
+ PlannedTrackCreation,
+} from "@/timeline/group-move";
+import { findTrackInSceneTracks } from "@/timeline/track-element-update";
+
+export class MoveElementCommand extends Command {
+ private savedState: SceneTracks | null = null;
+
+ constructor({
+ moves,
+ createTracks = [],
+ }: {
+ moves: PlannedElementMove[];
+ createTracks?: PlannedTrackCreation[];
+ }) {
+ super();
+ this.moves = moves;
+ this.createTracks = createTracks;
+ }
+
+ private readonly moves: PlannedElementMove[];
+ private readonly createTracks: PlannedTrackCreation[];
+
+ execute(): CommandResult | undefined {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.scenes.getActiveScene().tracks;
+
+ let tracksToUpdate = this.savedState;
+ for (const createTrack of [...this.createTracks].sort(
+ (firstTrack, secondTrack) => firstTrack.index - secondTrack.index,
+ )) {
+ tracksToUpdate = insertTrackAtDisplayIndex({
+ tracks: tracksToUpdate,
+ track: buildEmptyTrack({
+ id: createTrack.id,
+ type: createTrack.type,
+ }),
+ insertIndex: createTrack.index,
+ });
+ }
+
+ const movedElementsById = new Map<string, TimelineElement>();
+ for (const move of this.moves) {
+ const sourceTrack = findTrackInSceneTracks({
+ tracks: this.savedState,
+ trackId: move.sourceTrackId,
+ });
+ const sourceElement = sourceTrack?.elements.find(
+ (trackElement) => trackElement.id === move.elementId,
+ );
+ if (!sourceTrack || !sourceElement) {
+ throw new Error("Source track or element not found");
+ }
+
+ const targetTrack = findTrackInSceneTracks({
+ tracks: tracksToUpdate,
+ trackId: move.targetTrackId,
+ });
+ if (!targetTrack) {
+ throw new Error("Target track not found");
+ }
+
+ const validation = validateElementTrackCompatibility({
+ element: sourceElement,
+ track: targetTrack,
+ });
+ if (!validation.isValid) {
+ throw new Error(validation.errorMessage);
+ }
+
+ movedElementsById.set(move.elementId, {
+ ...sourceElement,
+ startTime: move.newStartTime,
+ });
+ }
+
+ const movedElementIds = new Set(this.moves.map((move) => move.elementId));
+ const movedElementsByTargetTrackId = new Map<string, TimelineElement[]>();
+ for (const move of this.moves) {
+ const movedElement = movedElementsById.get(move.elementId);
+ if (!movedElement) {
+ continue;
+ }
+
+ const nextTargetElements =
+ movedElementsByTargetTrackId.get(move.targetTrackId) ?? [];
+ nextTargetElements.push(movedElement);
+ movedElementsByTargetTrackId.set(move.targetTrackId, nextTargetElements);
+ }
+
+ const updatedTracks = mapSceneTracks({
+ tracks: tracksToUpdate,
+ update: (track) => ({
+ ...track,
+ elements: [
+ ...track.elements.filter(
+ (element) => !movedElementIds.has(element.id),
+ ),
+ ...(movedElementsByTargetTrackId.get(track.id) ?? []),
+ ],
+ }),
+ });
+
+ editor.timeline.updateTracks(updatedTracks);
+ return createElementSelectionResult(
+ this.moves.map(({ elementId, targetTrackId }) => ({
+ trackId: targetTrackId,
+ elementId,
+ })),
+ );
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+}
+
+function mapSceneTracks({
+ tracks,
+ update,
+}: {
+ tracks: SceneTracks;
+ update: <TTrack extends TimelineTrack>(track: TTrack) => TTrack;
+}): SceneTracks {
+ return {
+ overlay: tracks.overlay.map((track) => update(track)),
+ main: update(tracks.main),
+ audio: tracks.audio.map((track) => update(track)),
+ };
+}
+
+function insertTrackAtDisplayIndex({
+ tracks,
+ track,
+ insertIndex,
+}: {
+ tracks: SceneTracks;
+ track: TimelineTrack;
+ insertIndex: number;
+}): SceneTracks {
+ if (track.type === "audio") {
+ const audioInsertIndex = Math.max(
+ 0,
+ Math.min(insertIndex - tracks.overlay.length - 1, tracks.audio.length),
+ );
+ return {
+ ...tracks,
+ audio: [
+ ...tracks.audio.slice(0, audioInsertIndex),
+ track,
+ ...tracks.audio.slice(audioInsertIndex),
+ ],
+ };
+ }
+
+ const overlayInsertIndex = Math.max(
+ 0,
+ Math.min(insertIndex, tracks.overlay.length),
+ );
+ return {
+ ...tracks,
+ overlay: [
+ ...tracks.overlay.slice(0, overlayInsertIndex),
+ track,
+ ...tracks.overlay.slice(overlayInsertIndex),
+ ],
+ };
+}
diff --git a/apps/web/src/commands/timeline/element/split-elements.ts b/apps/web/src/commands/timeline/element/split-elements.ts
new file mode 100644
index 00000000..11c317f4
--- /dev/null
+++ b/apps/web/src/commands/timeline/element/split-elements.ts
@@ -0,0 +1,182 @@
+import {
+ Command,
+ createElementSelectionResult,
+ type CommandResult,
+} from "@/commands/base-command";
+import type { SceneTracks, TimelineElement } from "@/timeline";
+import { generateUUID } from "@/utils/id";
+import { EditorCore } from "@/core";
+import { isRetimableElement } from "@/timeline";
+import { splitAnimationsAtTime } from "@/animation";
+import { getSourceSpanAtClipTime } from "@/retime";
+
+export class SplitElementsCommand extends Command {
+ private savedState: SceneTracks | null = null;
+ private rightSideElements: { trackId: string; elementId: string }[] = [];
+ private readonly elements: { trackId: string; elementId: string }[];
+ private readonly splitTime: number;
+ private readonly retainSide: "both" | "left" | "right";
+
+ constructor({
+ elements,
+ splitTime,
+ retainSide = "both",
+ }: {
+ elements: { trackId: string; elementId: string }[];
+ splitTime: number;
+ retainSide?: "both" | "left" | "right";
+ }) {
+ super();
+ this.elements = elements;
+ this.splitTime = splitTime;
+ this.retainSide = retainSide;
+ }
+
+ getRightSideElements(): { trackId: string; elementId: string }[] {
+ return this.rightSideElements;
+ }
+
+ execute(): CommandResult | undefined {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.scenes.getActiveScene().tracks;
+ this.rightSideElements = [];
+
+ const splitTrack = <
+ TTrack extends { id: string; elements: TimelineElement[] },
+ >(
+ track: TTrack,
+ ): TTrack => {
+ const elementsToSplit = this.elements.filter(
+ (target) => target.trackId === track.id,
+ );
+
+ if (elementsToSplit.length === 0) {
+ return track;
+ }
+
+ const elements = track.elements.flatMap((element) => {
+ const shouldSplit = elementsToSplit.some(
+ (target) => target.elementId === element.id,
+ );
+
+ if (!shouldSplit) {
+ return [element];
+ }
+
+ const effectiveStart = element.startTime;
+ const effectiveEnd = element.startTime + element.duration;
+
+ if (
+ this.splitTime <= effectiveStart ||
+ this.splitTime >= effectiveEnd
+ ) {
+ return [element];
+ }
+
+ const relativeTime = this.splitTime - element.startTime;
+ const leftVisibleDuration = relativeTime;
+ const rightVisibleDuration = element.duration - relativeTime;
+ const retimeRef = isRetimableElement(element)
+ ? element.retime
+ : undefined;
+ const leftSourceSpan = getSourceSpanAtClipTime({
+ clipTime: leftVisibleDuration,
+ retime: retimeRef,
+ });
+ const totalSourceSpan = getSourceSpanAtClipTime({
+ clipTime: element.duration,
+ retime: retimeRef,
+ });
+ const rightSourceSpan = totalSourceSpan - leftSourceSpan;
+ const { leftAnimations, rightAnimations } = splitAnimationsAtTime({
+ animations: element.animations,
+ splitTime: relativeTime,
+ shouldIncludeSplitBoundary: true,
+ });
+ let splitResult: TimelineElement[];
+
+ if (this.retainSide === "left") {
+ splitResult = [
+ {
+ ...element,
+ duration: leftVisibleDuration,
+ trimEnd: element.trimEnd + rightSourceSpan,
+ name: `${element.name} (left)`,
+ animations: leftAnimations,
+ ...(retimeRef !== undefined ? { retime: retimeRef } : {}),
+ },
+ ];
+ } else if (this.retainSide === "right") {
+ const newId = generateUUID();
+ this.rightSideElements.push({
+ trackId: track.id,
+ elementId: newId,
+ });
+ splitResult = [
+ {
+ ...element,
+ id: newId,
+ startTime: this.splitTime,
+ duration: rightVisibleDuration,
+ trimStart: element.trimStart + leftSourceSpan,
+ name: `${element.name} (right)`,
+ animations: rightAnimations,
+ ...(retimeRef !== undefined ? { retime: retimeRef } : {}),
+ },
+ ];
+ } else {
+ // "both" - split into two pieces
+ const secondElementId = generateUUID();
+ this.rightSideElements.push({
+ trackId: track.id,
+ elementId: secondElementId,
+ });
+ splitResult = [
+ {
+ ...element,
+ duration: leftVisibleDuration,
+ trimEnd: element.trimEnd + rightSourceSpan,
+ name: `${element.name} (left)`,
+ animations: leftAnimations,
+ ...(retimeRef !== undefined ? { retime: retimeRef } : {}),
+ },
+ {
+ ...element,
+ id: secondElementId,
+ startTime: this.splitTime,
+ duration: rightVisibleDuration,
+ trimStart: element.trimStart + leftSourceSpan,
+ name: `${element.name} (right)`,
+ animations: rightAnimations,
+ ...(retimeRef !== undefined ? { retime: retimeRef } : {}),
+ },
+ ];
+ }
+
+ return splitResult;
+ });
+
+ return { ...track, elements } as TTrack;
+ };
+
+ const updatedTracks: SceneTracks = {
+ overlay: this.savedState.overlay.map((track) => splitTrack(track)),
+ main: splitTrack(this.savedState.main),
+ audio: this.savedState.audio.map((track) => splitTrack(track)),
+ };
+
+ editor.timeline.updateTracks(updatedTracks);
+
+ if (this.rightSideElements.length > 0) {
+ return createElementSelectionResult(this.rightSideElements);
+ }
+ return undefined;
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts b/apps/web/src/commands/timeline/element/toggle-source-audio-separation.ts
index 7300f540..36d4cd87 100644
--- a/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts
+++ b/apps/web/src/commands/timeline/element/toggle-source-audio-separation.ts
@@ -1,20 +1,20 @@
import { EditorCore } from "@/core";
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import {
buildSeparatedAudioElement,
canExtractSourceAudio,
isSourceAudioSeparated,
-} from "@/lib/timeline/audio-separation";
+} from "@/timeline/audio-separation";
import {
applyPlacement,
resolveTrackPlacement,
-} from "@/lib/timeline/placement";
-import { updateElementInSceneTracks } from "@/lib/timeline/track-element-update";
+} from "@/timeline/placement";
+import { updateElementInSceneTracks } from "@/timeline/track-element-update";
import type {
SceneTracks,
TimelineElement,
VideoElement,
-} from "@/lib/timeline/types";
+} from "@/timeline/types";
import { generateUUID } from "@/utils/id";
export class ToggleSourceAudioSeparationCommand extends Command {
diff --git a/apps/web/src/lib/commands/timeline/element/update-elements.ts b/apps/web/src/commands/timeline/element/update-elements.ts
index 54d1ca88..f03f2498 100644
--- a/apps/web/src/lib/commands/timeline/element/update-elements.ts
+++ b/apps/web/src/commands/timeline/element/update-elements.ts
@@ -1,11 +1,11 @@
import { EditorCore } from "@/core";
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import type { SceneTracks, TimelineElement } from "@/lib/timeline";
+import { Command, type CommandResult } from "@/commands/base-command";
+import type { SceneTracks, TimelineElement } from "@/timeline";
import {
findTrackInSceneTracks,
updateElementInSceneTracks,
-} from "@/lib/timeline";
-import { applyElementUpdate } from "@/lib/timeline/update-pipeline";
+} from "@/timeline";
+import { applyElementUpdate } from "@/timeline/update-pipeline";
export class UpdateElementsCommand extends Command {
private savedState: SceneTracks | null = null;
diff --git a/apps/web/src/lib/commands/timeline/index.ts b/apps/web/src/commands/timeline/index.ts
index cb285d94..cb285d94 100644
--- a/apps/web/src/lib/commands/timeline/index.ts
+++ b/apps/web/src/commands/timeline/index.ts
diff --git a/apps/web/src/lib/commands/timeline/track/add-track.ts b/apps/web/src/commands/timeline/track/add-track.ts
index a577c693..6b58b687 100644
--- a/apps/web/src/lib/commands/timeline/track/add-track.ts
+++ b/apps/web/src/commands/timeline/track/add-track.ts
@@ -1,11 +1,11 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import type { SceneTracks, TrackType } from "@/lib/timeline";
+import { Command, type CommandResult } from "@/commands/base-command";
+import type { SceneTracks, TrackType } from "@/timeline";
import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core";
import {
buildEmptyTrack,
getDefaultInsertIndexForTrack,
-} from "@/lib/timeline/placement";
+} from "@/timeline/placement";
export class AddTrackCommand extends Command {
private trackId: string;
diff --git a/apps/web/src/lib/commands/timeline/track/index.ts b/apps/web/src/commands/timeline/track/index.ts
index 9d4bd9c3..9d4bd9c3 100644
--- a/apps/web/src/lib/commands/timeline/track/index.ts
+++ b/apps/web/src/commands/timeline/track/index.ts
diff --git a/apps/web/src/lib/commands/timeline/track/remove-track.ts b/apps/web/src/commands/timeline/track/remove-track.ts
index 5fcaa898..13a9ff4e 100644
--- a/apps/web/src/lib/commands/timeline/track/remove-track.ts
+++ b/apps/web/src/commands/timeline/track/remove-track.ts
@@ -1,6 +1,6 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
-import type { SceneTracks } from "@/lib/timeline";
+import type { SceneTracks } from "@/timeline";
export class RemoveTrackCommand extends Command {
private savedState: SceneTracks | null = null;
diff --git a/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts b/apps/web/src/commands/timeline/track/toggle-track-mute.ts
index 7ffe2fd7..0d1f41cd 100644
--- a/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts
+++ b/apps/web/src/commands/timeline/track/toggle-track-mute.ts
@@ -1,7 +1,7 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import type { SceneTracks } from "@/lib/timeline";
+import { Command, type CommandResult } from "@/commands/base-command";
+import type { SceneTracks } from "@/timeline";
import { EditorCore } from "@/core";
-import { canTrackHaveAudio, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/lib/timeline";
+import { canTrackHaveAudio, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/timeline";
export class ToggleTrackMuteCommand extends Command {
private savedState: SceneTracks | null = null;
diff --git a/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts b/apps/web/src/commands/timeline/track/toggle-track-visibility.ts
index 2292c83a..f4160e3f 100644
--- a/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts
+++ b/apps/web/src/commands/timeline/track/toggle-track-visibility.ts
@@ -1,7 +1,7 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import type { SceneTracks } from "@/lib/timeline";
+import { Command, type CommandResult } from "@/commands/base-command";
+import type { SceneTracks } from "@/timeline";
import { EditorCore } from "@/core";
-import { canTrackBeHidden, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/lib/timeline";
+import { canTrackBeHidden, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/timeline";
export class ToggleTrackVisibilityCommand extends Command {
private savedState: SceneTracks | null = null;
diff --git a/apps/web/src/lib/commands/timeline/tracks-snapshot.ts b/apps/web/src/commands/timeline/tracks-snapshot.ts
index daed6a9e..5b6d871a 100644
--- a/apps/web/src/lib/commands/timeline/tracks-snapshot.ts
+++ b/apps/web/src/commands/timeline/tracks-snapshot.ts
@@ -1,5 +1,5 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import type { SceneTracks } from "@/lib/timeline";
+import { Command, type CommandResult } from "@/commands/base-command";
+import type { SceneTracks } from "@/timeline";
import { EditorCore } from "@/core";
export class TracksSnapshotCommand extends Command {
diff --git a/apps/web/src/components/editor/editor-header.tsx b/apps/web/src/components/editor/editor-header.tsx
index 44e8af68..6404dc01 100644
--- a/apps/web/src/components/editor/editor-header.tsx
+++ b/apps/web/src/components/editor/editor-header.tsx
@@ -10,20 +10,20 @@ import {
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import Link from "next/link";
-import { RenameProjectDialog } from "./dialogs/rename-project-dialog";
-import { DeleteProjectDialog } from "./dialogs/delete-project-dialog";
+import { RenameProjectDialog } from "@/project/components/rename-project-dialog";
+import { DeleteProjectDialog } from "@/project/components/delete-project-dialog";
import { useRouter } from "next/navigation";
import { FaDiscord } from "react-icons/fa6";
import { ExportButton } from "./export-button";
-import { FeedbackPopover } from "@/lib/feedback/components/feedback-popover";
+import { FeedbackPopover } from "@/feedback/components/feedback-popover";
import { ThemeToggle } from "../theme-toggle";
-import { DEFAULT_LOGO_URL } from "@/lib/site/brand";
-import { SOCIAL_LINKS } from "@/lib/site/social";
+import { DEFAULT_LOGO_URL } from "@/site/brand";
+import { SOCIAL_LINKS } from "@/site/social";
import { toast } from "sonner";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import { CommandIcon, Logout05Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
-import { ShortcutsDialog } from "./dialogs/shortcuts-dialog";
+import { ShortcutsDialog } from "@/actions/components/shortcuts-dialog";
import Image from "next/image";
import { cn } from "@/utils/ui";
diff --git a/apps/web/src/components/editor/export-button.tsx b/apps/web/src/components/editor/export-button.tsx
index e66067da..e738fb68 100644
--- a/apps/web/src/components/editor/export-button.tsx
+++ b/apps/web/src/components/editor/export-button.tsx
@@ -18,22 +18,22 @@ import {
getExportMimeType,
getExportFileExtension,
downloadBuffer,
-} from "@/lib/export";
+} from "@/export";
import { Check, Copy, Download, RotateCcw } from "lucide-react";
import {
EXPORT_FORMAT_VALUES,
EXPORT_QUALITY_VALUES,
type ExportFormat,
type ExportQuality,
-} from "@/lib/export";
+} from "@/export";
import {
Section,
SectionContent,
SectionHeader,
SectionTitle,
} from "@/components/section";
-import { useEditor } from "@/hooks/use-editor";
-import { DEFAULT_EXPORT_OPTIONS } from "@/lib/export/defaults";
+import { useEditor } from "@/editor/use-editor";
+import { DEFAULT_EXPORT_OPTIONS } from "@/export/defaults";
function isExportFormat(value: string): value is ExportFormat {
return EXPORT_FORMAT_VALUES.some((formatValue) => formatValue === value);
diff --git a/apps/web/src/components/editor/onboarding.tsx b/apps/web/src/components/editor/onboarding.tsx
index 54196c76..bc5533ce 100644
--- a/apps/web/src/components/editor/onboarding.tsx
+++ b/apps/web/src/components/editor/onboarding.tsx
@@ -3,8 +3,8 @@
import { ArrowRightIcon } from "lucide-react";
import { useState } from "react";
import ReactMarkdown from "react-markdown";
-import { SOCIAL_LINKS } from "@/lib/site/social";
-import { useLocalStorage } from "@/hooks/storage/use-local-storage";
+import { SOCIAL_LINKS } from "@/site/social";
+import { useLocalStorage } from "@/services/storage/use-local-storage";
import { Button } from "../ui/button";
import { Dialog, DialogBody, DialogContent, DialogTitle } from "../ui/dialog";
diff --git a/apps/web/src/stores/assets-panel-store.tsx b/apps/web/src/components/editor/panels/assets/assets-panel-store.tsx
index 1539d305..1539d305 100644
--- a/apps/web/src/stores/assets-panel-store.tsx
+++ b/apps/web/src/components/editor/panels/assets/assets-panel-store.tsx
diff --git a/apps/web/src/components/editor/panels/assets/draggable-item.tsx b/apps/web/src/components/editor/panels/assets/draggable-item.tsx
index 166c81db..2f8bc64a 100644
--- a/apps/web/src/components/editor/panels/assets/draggable-item.tsx
+++ b/apps/web/src/components/editor/panels/assets/draggable-item.tsx
@@ -10,9 +10,9 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
-import { useEditor } from "@/hooks/use-editor";
-import { clearDragData, setDragData } from "@/lib/drag-data";
-import type { TimelineDragData } from "@/lib/timeline/drag";
+import { useEditor } from "@/editor/use-editor";
+import { clearDragData, setDragData } from "@/timeline/drag-data";
+import type { TimelineDragData } from "@/timeline/drag";
import { cn } from "@/utils/ui";
export interface DraggableItemProps {
diff --git a/apps/web/src/components/editor/panels/assets/index.tsx b/apps/web/src/components/editor/panels/assets/index.tsx
index 1cdbf295..bae73299 100644
--- a/apps/web/src/components/editor/panels/assets/index.tsx
+++ b/apps/web/src/components/editor/panels/assets/index.tsx
@@ -1,15 +1,15 @@
"use client";
import { Separator } from "@/components/ui/separator";
-import { type Tab, useAssetsPanelStore } from "@/stores/assets-panel-store";
+import { type Tab, useAssetsPanelStore } from "@/components/editor/panels/assets/assets-panel-store";
import { TabBar } from "./tabbar";
-import { Captions } from "./views/captions";
+import { Captions } from "@/subtitles/components/assets-view";
import { MediaView } from "./views/assets";
import { SettingsView } from "./views/settings";
-import { SoundsView } from "./views/sounds";
-import { StickersView } from "./views/stickers";
-import { TextView } from "./views/text";
-import { EffectsView } from "./views/effects";
+import { SoundsView } from "@/sounds/components/assets-view";
+import { StickersView } from "@/stickers/components/assets-view";
+import { TextView } from "@/text/components/assets-view";
+import { EffectsView } from "@/effects/components/assets-view";
export function AssetsPanel() {
const { activeTab } = useAssetsPanelStore();
diff --git a/apps/web/src/components/editor/panels/assets/tabbar.tsx b/apps/web/src/components/editor/panels/assets/tabbar.tsx
index da53bed0..3b084c0d 100644
--- a/apps/web/src/components/editor/panels/assets/tabbar.tsx
+++ b/apps/web/src/components/editor/panels/assets/tabbar.tsx
@@ -12,7 +12,7 @@ import {
TAB_KEYS,
tabs,
useAssetsPanelStore,
-} from "@/stores/assets-panel-store";
+} from "@/components/editor/panels/assets/assets-panel-store";
export function TabBar() {
const { activeTab, setActiveTab } = useAssetsPanelStore();
diff --git a/apps/web/src/components/editor/panels/assets/views/assets.tsx b/apps/web/src/components/editor/panels/assets/views/assets.tsx
index 33b47c67..4afd22b7 100644
--- a/apps/web/src/components/editor/panels/assets/views/assets.tsx
+++ b/apps/web/src/components/editor/panels/assets/views/assets.tsx
@@ -25,28 +25,28 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
-import { DEFAULT_NEW_ELEMENT_DURATION } from "@/lib/timeline/creation";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
-import { useEditor } from "@/hooks/use-editor";
-import { useFileUpload } from "@/hooks/use-file-upload";
-import { invokeAction } from "@/lib/actions";
-import { processMediaAssets } from "@/lib/media/processing";
-import { showMediaUploadToast } from "@/lib/media/upload-toast";
+import { DEFAULT_NEW_ELEMENT_DURATION } from "@/timeline/creation";
+import { TICKS_PER_SECOND } from "@/wasm";
+import { useEditor } from "@/editor/use-editor";
+import { useFileUpload } from "@/media/use-file-upload";
+import { invokeAction } from "@/actions";
+import { processMediaAssets } from "@/media/processing";
+import { showMediaUploadToast } from "@/media/upload-toast";
import {
SelectableItem,
SelectableSurface,
useSelection,
useSelectionScope,
-} from "@/lib/selection";
-import { buildElementFromMedia } from "@/lib/timeline/element-utils";
+} from "@/selection";
+import { buildElementFromMedia } from "@/timeline/element-utils";
import {
type MediaSortKey,
type MediaSortOrder,
type MediaViewMode,
useAssetsPanelStore,
-} from "@/stores/assets-panel-store";
-import { MASKABLE_ELEMENT_TYPES } from "@/lib/timeline";
-import type { MediaAsset } from "@/lib/media/types";
+} from "@/components/editor/panels/assets/assets-panel-store";
+import { MASKABLE_ELEMENT_TYPES } from "@/timeline";
+import type { MediaAsset } from "@/media/types";
import { cn } from "@/utils/ui";
import {
CloudUploadIcon,
diff --git a/apps/web/src/components/editor/panels/assets/views/settings/background.tsx b/apps/web/src/components/editor/panels/assets/views/settings/background.tsx
index 099e335a..9ce34ebb 100644
--- a/apps/web/src/components/editor/panels/assets/views/settings/background.tsx
+++ b/apps/web/src/components/editor/panels/assets/views/settings/background.tsx
@@ -12,12 +12,12 @@ import { Popover, PopoverTrigger } from "@/components/ui/popover";
import {
BACKGROUND_BLUR_INTENSITY_PRESETS,
DEFAULT_BACKGROUND_BLUR_INTENSITY,
-} from "@/lib/background/blur";
-import { DEFAULT_BACKGROUND_COLOR } from "@/lib/background/color";
+} from "@/background/blur";
+import { DEFAULT_BACKGROUND_COLOR } from "@/background/color";
import { patternCraftGradients } from "@/data/colors/pattern-craft";
import { colors } from "@/data/colors/solid";
import { syntaxUIGradients } from "@/data/colors/syntax-ui";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import { effectPreviewService } from "@/services/renderer/effect-preview";
import { cn } from "@/utils/ui";
diff --git a/apps/web/src/components/editor/panels/assets/views/settings/index.tsx b/apps/web/src/components/editor/panels/assets/views/settings/index.tsx
index 4d87493a..bf3a1c16 100644
--- a/apps/web/src/components/editor/panels/assets/views/settings/index.tsx
+++ b/apps/web/src/components/editor/panels/assets/views/settings/index.tsx
@@ -9,9 +9,9 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
-import { FPS_PRESETS } from "@/lib/fps/presets";
-import { floatToFrameRate, frameRateToFloat } from "@/lib/fps/utils";
-import { useEditor } from "@/hooks/use-editor";
+import { FPS_PRESETS } from "@/fps/presets";
+import { floatToFrameRate, frameRateToFloat } from "@/fps/utils";
+import { useEditor } from "@/editor/use-editor";
import {
Section,
SectionContent,
@@ -22,7 +22,7 @@ import { BackgroundContent } from "./background";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { NumberField } from "@/components/ui/number-field";
-import { useEditorStore } from "@/stores/editor-store";
+import { useEditorStore } from "@/editor/editor-store";
import { usePropertyDraft } from "@/components/editor/panels/properties/hooks/use-property-draft";
import { HugeiconsIcon } from "@hugeicons/react";
import { Tick02Icon } from "@hugeicons/core-free-icons";
@@ -30,7 +30,7 @@ import { cn } from "@/utils/ui";
import { dimensionToAspectRatio } from "@/utils/geometry";
import { formatNumberForDisplay } from "@/utils/math";
import { OcSquarePlusIcon } from "@/components/icons";
-import type { TCanvasSize } from "@/lib/project/types";
+import type { TCanvasSize } from "@/project/types";
type SettingsView = "project-info" | "background";
diff --git a/apps/web/src/components/editor/panels/preview/bookmark-note-overlay.tsx b/apps/web/src/components/editor/panels/preview/bookmark-note-overlay.tsx
deleted file mode 100644
index 3d96f762..00000000
--- a/apps/web/src/components/editor/panels/preview/bookmark-note-overlay.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-"use client";
-
-import { useEditor } from "@/hooks/use-editor";
-import { findCurrentScene } from "@/lib/scenes";
-import { getBookmarksActiveAtTime } from "@/lib/timeline/bookmarks";
-
-export function BookmarkNoteOverlay() {
- const editor = useEditor();
- const currentTime = editor.playback.getCurrentTime();
- const activeProject = editor.project.getActive();
-
- if (!activeProject) {
- return null;
- }
-
- const activeScene = findCurrentScene({
- scenes: activeProject.scenes,
- currentSceneId: activeProject.currentSceneId,
- });
-
- if (!activeScene) {
- return null;
- }
-
- const bookmarks = activeScene.bookmarks;
- const activeBookmarks = getBookmarksActiveAtTime({
- bookmarks,
- time: currentTime,
- });
- const bookmarksWithNotes = activeBookmarks.filter(
- (bookmark) => bookmark.note != null && bookmark.note.trim() !== "",
- );
-
- if (bookmarksWithNotes.length === 0) {
- return null;
- }
-
- return (
- <div
- className="pointer-events-none absolute top-2 left-2 flex flex-col gap-1.5"
- aria-live="polite"
- >
- {bookmarksWithNotes.map((bookmark) => (
- <div
- key={bookmark.time}
- className="flex max-w-[min(200px,50vw)] px-2.5 py-1.5 text-left text-white text-xs shadow-md backdrop-blur-sm"
- style={{
- backgroundColor: "rgb(0 0 0 / 0.5)",
- borderLeft: bookmark.color
- ? `3px solid ${bookmark.color}`
- : "3px solid var(--primary)",
- }}
- >
- {bookmark.note}
- </div>
- ))}
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/panels/preview/guide-overlay.tsx b/apps/web/src/components/editor/panels/preview/guide-overlay.tsx
deleted file mode 100644
index 4270fcb3..00000000
--- a/apps/web/src/components/editor/panels/preview/guide-overlay.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-"use client";
-
-import { getGuideById } from "@/lib/guides";
-import { usePreviewStore } from "@/stores/preview-store";
-import { usePreviewViewport } from "./preview-viewport";
-
-export function GuideOverlay() {
- const activeGuide = usePreviewStore((state) => state.activeGuide);
- const viewport = usePreviewViewport();
- const guide = getGuideById(activeGuide);
-
- if (!guide) {
- return null;
- }
-
- return (
- <div
- className="pointer-events-none absolute"
- style={{
- left: viewport.sceneLeft,
- top: viewport.sceneTop,
- width: viewport.sceneWidth,
- height: viewport.sceneHeight,
- }}
- >
- {guide.renderOverlay({
- width: viewport.sceneWidth,
- height: viewport.sceneHeight,
- })}
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/panels/properties/components/property-param-field.tsx b/apps/web/src/components/editor/panels/properties/components/property-param-field.tsx
index 9cc2b02e..c9a9c22e 100644
--- a/apps/web/src/components/editor/panels/properties/components/property-param-field.tsx
+++ b/apps/web/src/components/editor/panels/properties/components/property-param-field.tsx
@@ -1,6 +1,6 @@
"use client";
-import type { ParamDefinition, NumberParamDefinition } from "@/lib/params";
+import type { ParamDefinition, NumberParamDefinition } from "@/params";
import {
formatNumberForDisplay,
getFractionDigitsForStep,
diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-element-playhead.ts b/apps/web/src/components/editor/panels/properties/hooks/use-element-playhead.ts
index 27300784..967156b3 100644
--- a/apps/web/src/components/editor/panels/properties/hooks/use-element-playhead.ts
+++ b/apps/web/src/components/editor/panels/properties/hooks/use-element-playhead.ts
@@ -1,5 +1,5 @@
-import { useEditor } from "@/hooks/use-editor";
-import { getElementLocalTime } from "@/lib/animation";
+import { useEditor } from "@/editor/use-editor";
+import { getElementLocalTime } from "@/animation";
export function useElementPlayhead({
startTime,
diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts
index a42e422f..734b514f 100644
--- a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts
+++ b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts
@@ -1,11 +1,11 @@
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import {
getKeyframeAtTime,
hasKeyframesForPath,
upsertElementKeyframe,
-} from "@/lib/animation";
-import type { AnimationPropertyPath, ElementAnimations } from "@/lib/animation/types";
-import type { TimelineElement } from "@/lib/timeline";
+} from "@/animation";
+import type { AnimationPropertyPath, ElementAnimations } from "@/animation/types";
+import type { TimelineElement } from "@/timeline";
export function useKeyframedColorProperty({
trackId,
diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts
index 872d55c0..9dd39768 100644
--- a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts
+++ b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts
@@ -1,11 +1,11 @@
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import {
getKeyframeAtTime,
hasKeyframesForPath,
upsertElementKeyframe,
-} from "@/lib/animation";
-import type { AnimationPropertyPath, ElementAnimations } from "@/lib/animation/types";
-import type { TimelineElement } from "@/lib/timeline";
+} from "@/animation";
+import type { AnimationPropertyPath, ElementAnimations } from "@/animation/types";
+import type { TimelineElement } from "@/timeline";
import { snapToStep } from "@/utils/math";
import { usePropertyDraft } from "./use-property-draft";
diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts
index 29b290d9..b0b6f01a 100644
--- a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts
+++ b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts
@@ -1,6 +1,6 @@
"use client";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import {
buildGraphicParamPath,
coerceAnimationValueForParam,
@@ -9,12 +9,12 @@ import {
getParamValueKind,
hasKeyframesForPath,
upsertPathKeyframe,
-} from "@/lib/animation";
+} from "@/animation";
import type {
ElementAnimations,
-} from "@/lib/animation/types";
-import type { ParamDefinition } from "@/lib/params";
-import type { TimelineElement } from "@/lib/timeline";
+} from "@/animation/types";
+import type { ParamDefinition } from "@/params";
+import type { TimelineElement } from "@/timeline";
export interface KeyframedParamPropertyResult {
hasAnimatedKeyframes: boolean;
diff --git a/apps/web/src/components/editor/panels/properties/index.tsx b/apps/web/src/components/editor/panels/properties/index.tsx
index 75319ef3..52fa42b5 100644
--- a/apps/web/src/components/editor/panels/properties/index.tsx
+++ b/apps/web/src/components/editor/panels/properties/index.tsx
@@ -8,8 +8,8 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
-import { useEditor } from "@/hooks/use-editor";
-import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
+import { useEditor } from "@/editor/use-editor";
+import { useElementSelection } from "@/timeline/hooks/element/use-element-selection";
import { usePropertiesStore } from "./stores/properties-store";
import { getPropertiesConfig } from "./registry";
import { cn } from "@/utils/ui";
diff --git a/apps/web/src/components/editor/panels/properties/registry.tsx b/apps/web/src/components/editor/panels/properties/registry.tsx
index df6c0e52..f9e40ab3 100644
--- a/apps/web/src/components/editor/panels/properties/registry.tsx
+++ b/apps/web/src/components/editor/panels/properties/registry.tsx
@@ -11,8 +11,8 @@ import type {
VideoElement,
AudioElement,
TimelineElement,
-} from "@/lib/timeline";
-import type { MediaAsset } from "@/lib/media/types";
+} from "@/timeline";
+import type { MediaAsset } from "@/media/types";
import { HugeiconsIcon } from "@hugeicons/react";
import {
TextFontIcon,
@@ -22,14 +22,14 @@ import {
MagicWand05Icon,
DashboardSpeed02Icon,
} from "@hugeicons/core-free-icons";
-import { TransformTab } from "./tabs/transform-tab";
-import { BlendingTab } from "./tabs/blending-tab";
-import { AudioTab } from "./tabs/audio-tab";
-import { TextTab } from "./tabs/text-tab";
-import { ClipEffectsTab, StandaloneEffectTab } from "./tabs/effects-tab";
-import { MasksTab } from "./tabs/masks-tab";
-import { SpeedTab } from "./tabs/speed-tab";
-import { GraphicTab } from "./tabs/graphic-tab";
+import { TransformTab } from "@/rendering/components/transform-tab";
+import { BlendingTab } from "@/rendering/components/blending-tab";
+import { AudioTab } from "@/timeline/components/audio-tab";
+import { TextTab } from "@/text/components/text-tab";
+import { ClipEffectsTab, StandaloneEffectTab } from "@/effects/components/effects-tab";
+import { MasksTab } from "@/masks/components/masks-tab";
+import { SpeedTab } from "@/speed/components/speed-tab";
+import { GraphicTab } from "@/graphics/components/graphic-tab";
import { OcShapesIcon } from "@/components/icons";
export type TabContentProps = {
diff --git a/apps/web/src/components/editor/scenes-view.tsx b/apps/web/src/components/editor/scenes-view.tsx
index 34efeaff..b0f487a4 100644
--- a/apps/web/src/components/editor/scenes-view.tsx
+++ b/apps/web/src/components/editor/scenes-view.tsx
@@ -21,9 +21,9 @@ import {
DialogFooter,
DialogTrigger,
} from "@/components/ui/dialog";
-import { canDeleteScene, getMainScene } from "@/lib/scenes";
+import { canDeleteScene, getMainScene } from "@/timeline/scenes";
import { toast } from "sonner";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
export function ScenesView({ children }: { children: React.ReactNode }) {
const editor = useEditor();
diff --git a/apps/web/src/components/footer.tsx b/apps/web/src/components/footer.tsx
index 21334267..b56f8c6c 100644
--- a/apps/web/src/components/footer.tsx
+++ b/apps/web/src/components/footer.tsx
@@ -2,8 +2,8 @@ import Link from "next/link";
import { RiDiscordFill, RiTwitterXLine } from "react-icons/ri";
import { FaGithub } from "react-icons/fa6";
import Image from "next/image";
-import { DEFAULT_LOGO_URL } from "@/lib/site/brand";
-import { SOCIAL_LINKS } from "@/lib/site/social";
+import { DEFAULT_LOGO_URL } from "@/site/brand";
+import { SOCIAL_LINKS } from "@/site/social";
import { capitalizeFirstLetter } from "@/utils/string";
type Category = "resources" | "company";
diff --git a/apps/web/src/components/gitHub-contribute-section.tsx b/apps/web/src/components/gitHub-contribute-section.tsx
index 0239e366..6978b81a 100644
--- a/apps/web/src/components/gitHub-contribute-section.tsx
+++ b/apps/web/src/components/gitHub-contribute-section.tsx
@@ -1,6 +1,6 @@
import { Button } from "./ui/button";
import Link from "next/link";
-import { SOCIAL_LINKS } from "@/lib/site/social";
+import { SOCIAL_LINKS } from "@/site/social";
import { GithubIcon, Link04Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx
index 66fec0ea..99ba1730 100644
--- a/apps/web/src/components/header.tsx
+++ b/apps/web/src/components/header.tsx
@@ -16,8 +16,8 @@ import {
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { cn } from "@/utils/ui";
-import { DEFAULT_LOGO_URL, SITE_URL } from "@/lib/site/brand";
-import { SOCIAL_LINKS } from "@/lib/site/social";
+import { DEFAULT_LOGO_URL, SITE_URL } from "@/site/brand";
+import { SOCIAL_LINKS } from "@/site/social";
import {
ContextMenu,
ContextMenuContent,
diff --git a/apps/web/src/components/providers/editor-provider.tsx b/apps/web/src/components/providers/editor-provider.tsx
index 98e6d4c6..fdc84af0 100644
--- a/apps/web/src/components/providers/editor-provider.tsx
+++ b/apps/web/src/components/providers/editor-provider.tsx
@@ -4,12 +4,12 @@ import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Loader2 } from "lucide-react";
import { EditorCore } from "@/core";
-import { useEditor } from "@/hooks/use-editor";
-import { useKeybindingsListener } from "@/hooks/use-keybindings";
-import { useKeybindingsStore } from "@/stores/keybindings-store";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { useEditorActions } from "@/hooks/actions/use-editor-actions";
-import { loadFontAtlas } from "@/lib/fonts/google-fonts";
+import { useEditor } from "@/editor/use-editor";
+import { useKeybindingsListener } from "@/actions/use-keybindings";
+import { useKeybindingsStore } from "@/actions/keybindings-store";
+import { useTimelineStore } from "@/timeline/timeline-store";
+import { useEditorActions } from "@/actions/use-editor-actions";
+import { loadFontAtlas } from "@/fonts/google-fonts";
import {
initializeGpuRenderer,
isGpuAvailable,
diff --git a/apps/web/src/components/storage-provider.tsx b/apps/web/src/components/storage-provider.tsx
index fc7ae121..185a44ef 100644
--- a/apps/web/src/components/storage-provider.tsx
+++ b/apps/web/src/components/storage-provider.tsx
@@ -2,7 +2,7 @@
import { createContext, useContext, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import { storageService } from "@/services/storage/service";
interface StorageContextType {
diff --git a/apps/web/src/components/ui/font-picker.tsx b/apps/web/src/components/ui/font-picker.tsx
index 297d58a7..27ea4bdb 100644
--- a/apps/web/src/components/ui/font-picker.tsx
+++ b/apps/web/src/components/ui/font-picker.tsx
@@ -9,10 +9,10 @@ import {
} from "@/components/ui/popover";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
-import { loadFullFont } from "@/lib/fonts/google-fonts";
-import { SYSTEM_FONTS } from "@/lib/fonts/system-fonts";
-import type { FontAtlas, FontAtlasEntry } from "@/lib/fonts/types";
-import { useFontAtlas } from "@/hooks/use-font-atlas";
+import { loadFullFont } from "@/fonts/google-fonts";
+import { SYSTEM_FONTS } from "@/fonts/system-fonts";
+import type { FontAtlas, FontAtlasEntry } from "@/fonts/types";
+import { useFontAtlas } from "@/fonts/use-font-atlas";
import { cn } from "@/utils/ui";
import { ChevronDown, Search } from "lucide-react";
import { HugeiconsIcon } from "@hugeicons/react";
diff --git a/apps/web/src/components/ui/use-overlay-open-change.ts b/apps/web/src/components/ui/use-overlay-open-change.ts
index dbc46cfc..b9cb87d8 100644
--- a/apps/web/src/components/ui/use-overlay-open-change.ts
+++ b/apps/web/src/components/ui/use-overlay-open-change.ts
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useId, useRef } from "react";
-import { useKeybindingsStore } from "@/stores/keybindings-store";
+import { useKeybindingsStore } from "@/actions/keybindings-store";
export function useOverlayOpenChange({
open,
diff --git a/apps/web/src/core/index.ts b/apps/web/src/core/index.ts
index 483a26c1..3e55e1d6 100644
--- a/apps/web/src/core/index.ts
+++ b/apps/web/src/core/index.ts
@@ -10,9 +10,9 @@ import { AudioManager } from "./managers/audio-manager";
import { SelectionManager } from "./managers/selection-manager";
import { ClipboardManager } from "./managers/clipboard-manager";
import { DiagnosticsManager } from "./managers/diagnostics-manager";
-import { registerDefaultEffects } from "@/lib/effects";
-import { registerDefaultMasks } from "@/lib/masks";
-import { registerTranscriptionDiagnostics } from "@/lib/transcription/diagnostics";
+import { registerDefaultEffects } from "@/effects";
+import { registerDefaultMasks } from "@/masks";
+import { registerTranscriptionDiagnostics } from "@/transcription/diagnostics";
export class EditorCore {
private static instance: EditorCore | null = null;
diff --git a/apps/web/src/core/managers/audio-manager.ts b/apps/web/src/core/managers/audio-manager.ts
index 076b998d..545565be 100644
--- a/apps/web/src/core/managers/audio-manager.ts
+++ b/apps/web/src/core/managers/audio-manager.ts
@@ -1,21 +1,21 @@
import type { EditorCore } from "@/core";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
+import { TICKS_PER_SECOND } from "@/wasm";
import {
clampRetimeRate,
shouldMaintainPitch,
-} from "@/lib/retime/rate";
-import type { AudioClipSource } from "@/lib/media/audio";
-import { createAudioContext, collectAudioClips } from "@/lib/media/audio";
+} from "@/retime/rate";
+import type { AudioClipSource } from "@/media/audio";
+import { createAudioContext, collectAudioClips } from "@/media/audio";
import {
buildAudioGainAutomation,
hasAnimatedVolume,
-} from "@/lib/timeline/audio-state";
-import { createAudioMasteringChain } from "@/lib/media/audio-mastering";
+} from "@/timeline/audio-state";
+import { createAudioMasteringChain } from "@/media/audio-mastering";
import {
getClipTimeAtSourceTime,
getSourceTimeAtClipTime,
renderRetimedBuffer,
-} from "@/lib/retime";
+} from "@/retime";
import {
ALL_FORMATS,
AudioBufferSink,
diff --git a/apps/web/src/core/managers/clipboard-manager.ts b/apps/web/src/core/managers/clipboard-manager.ts
index b19343df..8a2a51c9 100644
--- a/apps/web/src/core/managers/clipboard-manager.ts
+++ b/apps/web/src/core/managers/clipboard-manager.ts
@@ -5,7 +5,7 @@ import {
type ClipboardEntry,
type CopyContext,
type PasteContext,
-} from "@/lib/clipboard";
+} from "@/clipboard";
export class ClipboardManager {
private entry: ClipboardEntry | null = null;
diff --git a/apps/web/src/core/managers/commands.ts b/apps/web/src/core/managers/commands.ts
index 7cdd6373..9082db10 100644
--- a/apps/web/src/core/managers/commands.ts
+++ b/apps/web/src/core/managers/commands.ts
@@ -1,8 +1,8 @@
import type { EditorCore } from "@/core";
-import type { Command, CommandResult } from "@/lib/commands";
-import type { EditorSelectionSnapshot } from "@/lib/selection/editor-selection";
-import { applyRippleAdjustments, computeRippleAdjustments } from "@/lib/ripple";
-import type { SceneTracks } from "@/lib/timeline/types";
+import type { Command, CommandResult } from "@/commands";
+import type { EditorSelectionSnapshot } from "@/selection/editor-selection";
+import { applyRippleAdjustments, computeRippleAdjustments } from "@/ripple";
+import type { SceneTracks } from "@/timeline/types";
interface CommandHistoryEntry {
command: Command;
diff --git a/apps/web/src/core/managers/diagnostics-manager.ts b/apps/web/src/core/managers/diagnostics-manager.ts
index d022a350..0a32fab5 100644
--- a/apps/web/src/core/managers/diagnostics-manager.ts
+++ b/apps/web/src/core/managers/diagnostics-manager.ts
@@ -1,5 +1,5 @@
import type { EditorCore } from "@/core";
-import type { DiagnosticDefinition } from "@/lib/diagnostics/types";
+import type { DiagnosticDefinition } from "@/diagnostics/types";
interface DiagnosticRegistration extends DiagnosticDefinition {
check: (editor: EditorCore) => boolean;
diff --git a/apps/web/src/core/managers/media-manager.ts b/apps/web/src/core/managers/media-manager.ts
index e43340a7..553d1557 100644
--- a/apps/web/src/core/managers/media-manager.ts
+++ b/apps/web/src/core/managers/media-manager.ts
@@ -1,11 +1,11 @@
import type { EditorCore } from "@/core";
import { toast } from "sonner";
-import type { MediaAsset } from "@/lib/media/types";
+import type { MediaAsset } from "@/media/types";
import { storageService } from "@/services/storage/service";
import { generateUUID } from "@/utils/id";
import { videoCache } from "@/services/video-cache/service";
import { waveformCache } from "@/services/waveform-cache/service";
-import { BatchCommand, RemoveMediaAssetCommand } from "@/lib/commands";
+import { BatchCommand, RemoveMediaAssetCommand } from "@/commands";
export class MediaManager {
private assets: MediaAsset[] = [];
diff --git a/apps/web/src/core/managers/playback-manager.ts b/apps/web/src/core/managers/playback-manager.ts
index 341a0eae..1ea09826 100644
--- a/apps/web/src/core/managers/playback-manager.ts
+++ b/apps/web/src/core/managers/playback-manager.ts
@@ -1,5 +1,5 @@
import type { EditorCore } from "@/core";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
+import { TICKS_PER_SECOND } from "@/wasm";
import { roundToFrame } from "opencut-wasm";
export class PlaybackManager {
diff --git a/apps/web/src/core/managers/project-manager.ts b/apps/web/src/core/managers/project-manager.ts
index 8555786d..b38fd50d 100644
--- a/apps/web/src/core/managers/project-manager.ts
+++ b/apps/web/src/core/managers/project-manager.ts
@@ -6,16 +6,16 @@ import type {
TProjectSortOption,
TProjectSettings,
TTimelineViewState,
-} from "@/lib/project/types";
-import type { ExportOptions, ExportResult, ExportState } from "@/lib/export";
+} from "@/project/types";
+import type { ExportOptions, ExportResult, ExportState } from "@/export";
import { storageService } from "@/services/storage/service";
import { toast } from "sonner";
import { generateUUID } from "@/utils/id";
-import { UpdateProjectSettingsCommand } from "@/lib/commands/project";
-import { DEFAULT_BACKGROUND_COLOR } from "@/lib/background/color";
-import { DEFAULT_CANVAS_SIZE } from "@/lib/canvas/sizes";
-import { DEFAULT_FPS } from "@/lib/fps/defaults";
-import { buildDefaultScene, getProjectDurationFromScenes } from "@/lib/scenes";
+import { UpdateProjectSettingsCommand } from "@/commands/project";
+import { DEFAULT_BACKGROUND_COLOR } from "@/background/color";
+import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes";
+import { DEFAULT_FPS } from "@/fps/defaults";
+import { buildDefaultScene, getProjectDurationFromScenes } from "@/timeline/scenes";
import { buildScene } from "@/services/renderer/scene-builder";
import { CanvasRenderer } from "@/services/renderer/canvas-renderer";
import {
@@ -24,11 +24,11 @@ import {
runStorageMigrations,
type MigrationProgress,
} from "@/services/storage/migrations";
-import { loadFonts } from "@/lib/fonts/google-fonts";
-import { DEFAULTS } from "@/lib/timeline/defaults";
-import { getElementFontFamilies } from "@/lib/timeline/element-utils";
-import { getRaisedProjectFpsForImportedMedia } from "@/lib/fps/utils";
-import type { MediaAsset } from "@/lib/media/types";
+import { loadFonts } from "@/fonts/google-fonts";
+import { DEFAULTS } from "@/timeline/defaults";
+import { getElementFontFamilies } from "@/timeline/element-utils";
+import { getRaisedProjectFpsForImportedMedia } from "@/fps/utils";
+import type { MediaAsset } from "@/media/types";
export interface MigrationState {
isMigrating: boolean;
diff --git a/apps/web/src/core/managers/renderer-manager.ts b/apps/web/src/core/managers/renderer-manager.ts
index 495292e6..7984655c 100644
--- a/apps/web/src/core/managers/renderer-manager.ts
+++ b/apps/web/src/core/managers/renderer-manager.ts
@@ -1,12 +1,12 @@
import type { EditorCore } from "@/core";
import type { RootNode } from "@/services/renderer/nodes/root-node";
-import type { ExportOptions, ExportResult } from "@/lib/export";
+import type { ExportOptions, ExportResult } from "@/export";
import { CanvasRenderer } from "@/services/renderer/canvas-renderer";
import { SceneExporter } from "@/services/renderer/scene-exporter";
import { buildScene } from "@/services/renderer/scene-builder";
-import { createTimelineAudioBuffer } from "@/lib/media/audio";
+import { createTimelineAudioBuffer } from "@/media/audio";
import { formatTimecode } from "opencut-wasm";
-import { frameRateToFloat } from "@/lib/fps/utils";
+import { frameRateToFloat } from "@/fps/utils";
import { downloadBlob } from "@/utils/browser";
type SnapshotResult =
diff --git a/apps/web/src/core/managers/scenes-manager.ts b/apps/web/src/core/managers/scenes-manager.ts
index 3508a066..13f251a1 100644
--- a/apps/web/src/core/managers/scenes-manager.ts
+++ b/apps/web/src/core/managers/scenes-manager.ts
@@ -1,320 +1,320 @@
-import type { EditorCore } from "@/core";
-import type { SceneTracks, TScene } from "@/lib/timeline";
-import { storageService } from "@/services/storage/service";
-import {
- getMainScene,
- ensureMainScene,
- canDeleteScene,
- findCurrentScene,
-} from "@/lib/scenes";
-import {
- getBookmarkAtTime,
- getFrameTime,
- isBookmarkAtTime,
-} from "@/lib/timeline/bookmarks";
-import {
- CreateSceneCommand,
- DeleteSceneCommand,
- MoveBookmarkCommand,
- RemoveBookmarkCommand,
- RenameSceneCommand,
- ToggleBookmarkCommand,
- UpdateBookmarkCommand,
-} from "@/lib/commands/scene";
-
-export class ScenesManager {
- private active: TScene | null = null;
- private list: TScene[] = [];
- private listeners = new Set<() => void>();
-
- constructor(private editor: EditorCore) {}
-
- async createScene({
- name,
- isMain = false,
- }: {
- name: string;
- isMain: boolean;
- }): Promise<string> {
- if (!this.editor.project.getActive()) {
- throw new Error("No active project");
- }
-
- const command = new CreateSceneCommand(name, isMain);
- this.editor.command.execute({ command });
- return command.getSceneId();
- }
-
- async deleteScene({ sceneId }: { sceneId: string }): Promise<void> {
- const sceneToDelete = this.list.find((s) => s.id === sceneId);
-
- if (!sceneToDelete) {
- throw new Error("Scene not found");
- }
-
- const { canDelete, reason } = canDeleteScene({ scene: sceneToDelete });
- if (!canDelete) {
- throw new Error(reason);
- }
-
- if (!this.editor.project.getActive()) {
- throw new Error("No active project");
- }
-
- const command = new DeleteSceneCommand(sceneId);
- this.editor.command.execute({ command });
- }
-
- async renameScene({
- sceneId,
- name,
- }: {
- sceneId: string;
- name: string;
- }): Promise<void> {
- if (!this.editor.project.getActive()) {
- throw new Error("No active project");
- }
-
- const command = new RenameSceneCommand(sceneId, name);
- this.editor.command.execute({ command });
- }
-
- async switchToScene({ sceneId }: { sceneId: string }): Promise<void> {
- const targetScene = this.list.find((s) => s.id === sceneId);
-
- if (!targetScene) {
- throw new Error("Scene not found");
- }
-
- const activeProject = this.editor.project.getActive();
-
- if (activeProject) {
- const updatedProject = {
- ...activeProject,
- currentSceneId: sceneId,
- metadata: {
- ...activeProject.metadata,
- updatedAt: new Date(),
- },
- };
-
- this.editor.project.setActiveProject({ project: updatedProject });
- }
-
- this.active = targetScene;
- this.notify();
- }
-
- async toggleBookmark({ time }: { time: number }): Promise<void> {
- const command = new ToggleBookmarkCommand(time);
- this.editor.command.execute({ command });
- }
-
- isBookmarked({ time }: { time: number }): boolean {
- const activeScene = this.getActiveScene();
- const activeProject = this.editor.project.getActive();
-
- if (!activeScene || !this.active || !activeProject) return false;
-
- const frameTime = getFrameTime({
- time,
- fps: activeProject.settings.fps,
- });
-
- return isBookmarkAtTime({ bookmarks: activeScene.bookmarks, frameTime });
- }
-
- async removeBookmark({ time }: { time: number }): Promise<void> {
- const command = new RemoveBookmarkCommand(time);
- this.editor.command.execute({ command });
- }
-
- async updateBookmark({
- time,
- updates,
- }: {
- time: number;
- updates: Partial<{ note: string; color: string; duration: number }>;
- }): Promise<void> {
- const command = new UpdateBookmarkCommand(time, updates);
- this.editor.command.execute({ command });
- }
-
- async moveBookmark({
- fromTime,
- toTime,
- }: {
- fromTime: number;
- toTime: number;
- }): Promise<void> {
- const command = new MoveBookmarkCommand(fromTime, toTime);
- this.editor.command.execute({ command });
- }
-
- getBookmarkAtTime({ time }: { time: number }) {
- const activeScene = this.active;
- const activeProject = this.editor.project.getActive();
-
- if (!activeScene || !activeProject) return null;
-
- const frameTime = getFrameTime({
- time,
- fps: activeProject.settings.fps,
- });
-
- return getBookmarkAtTime({
- bookmarks: activeScene.bookmarks,
- frameTime,
- });
- }
-
- async loadProjectScenes({ projectId }: { projectId: string }): Promise<void> {
- try {
- const result = await storageService.loadProject({ id: projectId });
- if (result?.project.scenes) {
- const ensuredScenes = result.project.scenes ?? [];
- const currentScene = findCurrentScene({
- scenes: ensuredScenes,
- currentSceneId: result.project.currentSceneId,
- });
-
- this.list = ensuredScenes;
- this.active = currentScene;
- this.notify();
- }
- } catch (error) {
- console.error("Failed to load project scenes:", error);
- this.list = [];
- this.active = null;
- this.notify();
- }
- }
-
- initializeScenes({
- scenes,
- currentSceneId,
- }: {
- scenes: TScene[];
- currentSceneId?: string;
- }): void {
- const ensuredScenes = ensureMainScene({ scenes });
- const currentScene = currentSceneId
- ? ensuredScenes.find((s) => s.id === currentSceneId)
- : null;
-
- const fallbackScene = getMainScene({ scenes: ensuredScenes });
-
- this.list = ensuredScenes;
- this.active = currentScene || fallbackScene;
- this.notify();
-
- const hasAddedMainScene = ensuredScenes.length > scenes.length;
- if (hasAddedMainScene) {
- const activeProject = this.editor.project.getActive();
-
- if (activeProject) {
- const updatedProject = {
- ...activeProject,
- scenes: ensuredScenes,
- metadata: {
- ...activeProject.metadata,
- updatedAt: new Date(),
- },
- };
-
- this.editor.project.setActiveProject({ project: updatedProject });
- this.editor.save.markDirty({ force: true });
- }
- }
- }
-
- clearScenes(): void {
- this.list = [];
- this.active = null;
- this.notify();
- }
-
- getActiveScene(): TScene {
- if (!this.active) {
- throw new Error("No active scene.");
- }
- return this.active;
- }
-
- getActiveSceneOrNull(): TScene | null {
- return this.active;
- }
-
- getScenes(): TScene[] {
- return this.list;
- }
-
- setScenes({
- scenes,
- activeSceneId,
- }: {
- scenes: TScene[];
- activeSceneId?: string;
- }): void {
- this.list = scenes;
- const nextActiveSceneId = activeSceneId ?? this.active?.id ?? null;
- this.active = nextActiveSceneId
- ? (scenes.find((scene) => scene.id === nextActiveSceneId) ?? null)
- : null;
- this.notify();
-
- const activeProject = this.editor.project.getActive();
- if (activeProject) {
- const updatedProject = {
- ...activeProject,
- scenes,
- metadata: {
- ...activeProject.metadata,
- updatedAt: new Date(),
- },
- };
- this.editor.project.setActiveProject({ project: updatedProject });
- }
- }
-
- subscribe(listener: () => void): () => void {
- this.listeners.add(listener);
- return () => this.listeners.delete(listener);
- }
-
- private notify(): void {
- this.listeners.forEach((fn) => {
- fn();
- });
- }
-
- updateSceneTracks({ tracks }: { tracks: SceneTracks }): void {
- if (!this.active) return;
-
- const updatedScene: TScene = {
- ...this.active,
- tracks,
- updatedAt: new Date(),
- };
-
- this.list = this.list.map((s) =>
- s.id === this.active?.id ? updatedScene : s,
- );
- this.active = updatedScene;
- this.notify();
-
- const activeProject = this.editor.project.getActive();
- if (activeProject) {
- const updatedProject = {
- ...activeProject,
- scenes: this.list,
- metadata: {
- ...activeProject.metadata,
- updatedAt: new Date(),
- },
- };
- this.editor.project.setActiveProject({ project: updatedProject });
- }
- }
-}
+import type { EditorCore } from "@/core";
+import type { SceneTracks, TScene } from "@/timeline";
+import { storageService } from "@/services/storage/service";
+import {
+ getMainScene,
+ ensureMainScene,
+ canDeleteScene,
+ findCurrentScene,
+} from "@/timeline/scenes";
+import {
+ getBookmarkAtTime,
+ getFrameTime,
+ isBookmarkAtTime,
+} from "@/timeline/bookmarks/index";
+import {
+ CreateSceneCommand,
+ DeleteSceneCommand,
+ MoveBookmarkCommand,
+ RemoveBookmarkCommand,
+ RenameSceneCommand,
+ ToggleBookmarkCommand,
+ UpdateBookmarkCommand,
+} from "@/commands/scene";
+
+export class ScenesManager {
+ private active: TScene | null = null;
+ private list: TScene[] = [];
+ private listeners = new Set<() => void>();
+
+ constructor(private editor: EditorCore) {}
+
+ async createScene({
+ name,
+ isMain = false,
+ }: {
+ name: string;
+ isMain: boolean;
+ }): Promise<string> {
+ if (!this.editor.project.getActive()) {
+ throw new Error("No active project");
+ }
+
+ const command = new CreateSceneCommand(name, isMain);
+ this.editor.command.execute({ command });
+ return command.getSceneId();
+ }
+
+ async deleteScene({ sceneId }: { sceneId: string }): Promise<void> {
+ const sceneToDelete = this.list.find((s) => s.id === sceneId);
+
+ if (!sceneToDelete) {
+ throw new Error("Scene not found");
+ }
+
+ const { canDelete, reason } = canDeleteScene({ scene: sceneToDelete });
+ if (!canDelete) {
+ throw new Error(reason);
+ }
+
+ if (!this.editor.project.getActive()) {
+ throw new Error("No active project");
+ }
+
+ const command = new DeleteSceneCommand(sceneId);
+ this.editor.command.execute({ command });
+ }
+
+ async renameScene({
+ sceneId,
+ name,
+ }: {
+ sceneId: string;
+ name: string;
+ }): Promise<void> {
+ if (!this.editor.project.getActive()) {
+ throw new Error("No active project");
+ }
+
+ const command = new RenameSceneCommand(sceneId, name);
+ this.editor.command.execute({ command });
+ }
+
+ async switchToScene({ sceneId }: { sceneId: string }): Promise<void> {
+ const targetScene = this.list.find((s) => s.id === sceneId);
+
+ if (!targetScene) {
+ throw new Error("Scene not found");
+ }
+
+ const activeProject = this.editor.project.getActive();
+
+ if (activeProject) {
+ const updatedProject = {
+ ...activeProject,
+ currentSceneId: sceneId,
+ metadata: {
+ ...activeProject.metadata,
+ updatedAt: new Date(),
+ },
+ };
+
+ this.editor.project.setActiveProject({ project: updatedProject });
+ }
+
+ this.active = targetScene;
+ this.notify();
+ }
+
+ async toggleBookmark({ time }: { time: number }): Promise<void> {
+ const command = new ToggleBookmarkCommand(time);
+ this.editor.command.execute({ command });
+ }
+
+ isBookmarked({ time }: { time: number }): boolean {
+ const activeScene = this.getActiveScene();
+ const activeProject = this.editor.project.getActive();
+
+ if (!activeScene || !this.active || !activeProject) return false;
+
+ const frameTime = getFrameTime({
+ time,
+ fps: activeProject.settings.fps,
+ });
+
+ return isBookmarkAtTime({ bookmarks: activeScene.bookmarks, frameTime });
+ }
+
+ async removeBookmark({ time }: { time: number }): Promise<void> {
+ const command = new RemoveBookmarkCommand(time);
+ this.editor.command.execute({ command });
+ }
+
+ async updateBookmark({
+ time,
+ updates,
+ }: {
+ time: number;
+ updates: Partial<{ note: string; color: string; duration: number }>;
+ }): Promise<void> {
+ const command = new UpdateBookmarkCommand(time, updates);
+ this.editor.command.execute({ command });
+ }
+
+ async moveBookmark({
+ fromTime,
+ toTime,
+ }: {
+ fromTime: number;
+ toTime: number;
+ }): Promise<void> {
+ const command = new MoveBookmarkCommand(fromTime, toTime);
+ this.editor.command.execute({ command });
+ }
+
+ getBookmarkAtTime({ time }: { time: number }) {
+ const activeScene = this.active;
+ const activeProject = this.editor.project.getActive();
+
+ if (!activeScene || !activeProject) return null;
+
+ const frameTime = getFrameTime({
+ time,
+ fps: activeProject.settings.fps,
+ });
+
+ return getBookmarkAtTime({
+ bookmarks: activeScene.bookmarks,
+ frameTime,
+ });
+ }
+
+ async loadProjectScenes({ projectId }: { projectId: string }): Promise<void> {
+ try {
+ const result = await storageService.loadProject({ id: projectId });
+ if (result?.project.scenes) {
+ const ensuredScenes = result.project.scenes ?? [];
+ const currentScene = findCurrentScene({
+ scenes: ensuredScenes,
+ currentSceneId: result.project.currentSceneId,
+ });
+
+ this.list = ensuredScenes;
+ this.active = currentScene;
+ this.notify();
+ }
+ } catch (error) {
+ console.error("Failed to load project scenes:", error);
+ this.list = [];
+ this.active = null;
+ this.notify();
+ }
+ }
+
+ initializeScenes({
+ scenes,
+ currentSceneId,
+ }: {
+ scenes: TScene[];
+ currentSceneId?: string;
+ }): void {
+ const ensuredScenes = ensureMainScene({ scenes });
+ const currentScene = currentSceneId
+ ? ensuredScenes.find((s) => s.id === currentSceneId)
+ : null;
+
+ const fallbackScene = getMainScene({ scenes: ensuredScenes });
+
+ this.list = ensuredScenes;
+ this.active = currentScene || fallbackScene;
+ this.notify();
+
+ const hasAddedMainScene = ensuredScenes.length > scenes.length;
+ if (hasAddedMainScene) {
+ const activeProject = this.editor.project.getActive();
+
+ if (activeProject) {
+ const updatedProject = {
+ ...activeProject,
+ scenes: ensuredScenes,
+ metadata: {
+ ...activeProject.metadata,
+ updatedAt: new Date(),
+ },
+ };
+
+ this.editor.project.setActiveProject({ project: updatedProject });
+ this.editor.save.markDirty({ force: true });
+ }
+ }
+ }
+
+ clearScenes(): void {
+ this.list = [];
+ this.active = null;
+ this.notify();
+ }
+
+ getActiveScene(): TScene {
+ if (!this.active) {
+ throw new Error("No active scene.");
+ }
+ return this.active;
+ }
+
+ getActiveSceneOrNull(): TScene | null {
+ return this.active;
+ }
+
+ getScenes(): TScene[] {
+ return this.list;
+ }
+
+ setScenes({
+ scenes,
+ activeSceneId,
+ }: {
+ scenes: TScene[];
+ activeSceneId?: string;
+ }): void {
+ this.list = scenes;
+ const nextActiveSceneId = activeSceneId ?? this.active?.id ?? null;
+ this.active = nextActiveSceneId
+ ? (scenes.find((scene) => scene.id === nextActiveSceneId) ?? null)
+ : null;
+ this.notify();
+
+ const activeProject = this.editor.project.getActive();
+ if (activeProject) {
+ const updatedProject = {
+ ...activeProject,
+ scenes,
+ metadata: {
+ ...activeProject.metadata,
+ updatedAt: new Date(),
+ },
+ };
+ this.editor.project.setActiveProject({ project: updatedProject });
+ }
+ }
+
+ subscribe(listener: () => void): () => void {
+ this.listeners.add(listener);
+ return () => this.listeners.delete(listener);
+ }
+
+ private notify(): void {
+ this.listeners.forEach((fn) => {
+ fn();
+ });
+ }
+
+ updateSceneTracks({ tracks }: { tracks: SceneTracks }): void {
+ if (!this.active) return;
+
+ const updatedScene: TScene = {
+ ...this.active,
+ tracks,
+ updatedAt: new Date(),
+ };
+
+ this.list = this.list.map((s) =>
+ s.id === this.active?.id ? updatedScene : s,
+ );
+ this.active = updatedScene;
+ this.notify();
+
+ const activeProject = this.editor.project.getActive();
+ if (activeProject) {
+ const updatedProject = {
+ ...activeProject,
+ scenes: this.list,
+ metadata: {
+ ...activeProject.metadata,
+ updatedAt: new Date(),
+ },
+ };
+ this.editor.project.setActiveProject({ project: updatedProject });
+ }
+ }
+}
diff --git a/apps/web/src/core/managers/selection-manager.ts b/apps/web/src/core/managers/selection-manager.ts
index 5da4fa60..cfc46ccc 100644
--- a/apps/web/src/core/managers/selection-manager.ts
+++ b/apps/web/src/core/managers/selection-manager.ts
@@ -1,12 +1,12 @@
import type { EditorCore } from "@/core";
-import type { SelectedKeyframeRef } from "@/lib/animation/types";
+import type { SelectedKeyframeRef } from "@/animation/types";
import type {
EditorSelectionKind,
EditorSelectionPatch,
EditorSelectionSnapshot,
SelectedMaskPointSelection,
-} from "@/lib/selection/editor-selection";
-import type { ElementRef } from "@/lib/timeline/types";
+} from "@/selection/editor-selection";
+import type { ElementRef } from "@/timeline/types";
export class SelectionManager {
private selectedElements: ElementRef[] = [];
diff --git a/apps/web/src/core/managers/timeline-manager.ts b/apps/web/src/core/managers/timeline-manager.ts
index c58128fb..dbc3332b 100644
--- a/apps/web/src/core/managers/timeline-manager.ts
+++ b/apps/web/src/core/managers/timeline-manager.ts
@@ -1,33 +1,33 @@
import type { EditorCore } from "@/core";
-import type { ElementBounds } from "@/lib/preview/element-bounds";
-import type { ParamValues } from "@/lib/params";
+import type { ElementBounds } from "@/preview/element-bounds";
+import type { ParamValues } from "@/params";
import type {
SceneTracks,
TrackType,
TimelineTrack,
TimelineElement,
RetimeConfig,
-} from "@/lib/timeline";
-import { calculateTotalDuration } from "@/lib/timeline";
-import { findTrackInSceneTracks } from "@/lib/timeline/track-element-update";
+} from "@/timeline";
+import { calculateTotalDuration } from "@/timeline";
+import { findTrackInSceneTracks } from "@/timeline/track-element-update";
import {
canElementBeHidden,
canElementHaveAudio,
-} from "@/lib/timeline/element-utils";
+} from "@/timeline/element-utils";
import type {
AnimationPath,
AnimationInterpolation,
AnimationValue,
AnimationValueForPath,
ScalarCurveKeyframePatch,
-} from "@/lib/animation/types";
+} from "@/animation/types";
import {
getElementLocalTime,
resolveAnimationTarget,
resolveAnimationPathValueAtTime,
-} from "@/lib/animation";
+} from "@/animation";
import { lastFrameTime } from "opencut-wasm";
-import { BatchCommand } from "@/lib/commands";
+import { BatchCommand } from "@/commands";
import {
AddTrackCommand,
RemoveTrackCommand,
@@ -56,12 +56,12 @@ import {
UpsertEffectParamKeyframeCommand,
RemoveEffectParamKeyframeCommand,
ToggleSourceAudioSeparationCommand,
-} from "@/lib/commands/timeline";
-import type { InsertElementParams } from "@/lib/commands/timeline/element/insert-element";
+} from "@/commands/timeline";
+import type { InsertElementParams } from "@/commands/timeline/element/insert-element";
import type {
PlannedElementMove,
PlannedTrackCreation,
-} from "@/lib/timeline/group-move";
+} from "@/timeline/group-move";
export class TimelineManager {
private listeners = new Set<() => void>();
diff --git a/apps/web/src/lib/db/index.ts b/apps/web/src/db/index.ts
index b1ffd119..29e3ffda 100644
--- a/apps/web/src/lib/db/index.ts
+++ b/apps/web/src/db/index.ts
@@ -1,7 +1,7 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
-import { webEnv } from "@/lib/env/web";
+import { webEnv } from "@/env/web";
let _db: ReturnType<typeof drizzle> | null = null;
diff --git a/apps/web/src/lib/db/schema.ts b/apps/web/src/db/schema.ts
index 8f8be248..8f8be248 100644
--- a/apps/web/src/lib/db/schema.ts
+++ b/apps/web/src/db/schema.ts
diff --git a/apps/web/src/lib/diagnostics/types.ts b/apps/web/src/diagnostics/types.ts
index 486f362c..486f362c 100644
--- a/apps/web/src/lib/diagnostics/types.ts
+++ b/apps/web/src/diagnostics/types.ts
diff --git a/apps/web/src/lib/cancel-interaction.ts b/apps/web/src/editor/cancel-interaction.ts
index 297a2062..297a2062 100644
--- a/apps/web/src/lib/cancel-interaction.ts
+++ b/apps/web/src/editor/cancel-interaction.ts
diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/editor/editor-store.ts
index 07cb433e..6b05e8bd 100644
--- a/apps/web/src/stores/editor-store.ts
+++ b/apps/web/src/editor/editor-store.ts
@@ -1,6 +1,6 @@
import { create } from "zustand";
-import { DEFAULT_CANVAS_PRESETS } from "@/lib/canvas/sizes";
-import type { TCanvasSize } from "@/lib/project/types";
+import { DEFAULT_CANVAS_PRESETS } from "@/canvas/sizes";
+import type { TCanvasSize } from "@/project/types";
interface EditorState {
isInitializing: boolean;
diff --git a/apps/web/src/stores/panel-store.ts b/apps/web/src/editor/panel-store.ts
index 757a37a6..2cb768a1 100644
--- a/apps/web/src/stores/panel-store.ts
+++ b/apps/web/src/editor/panel-store.ts
@@ -1,6 +1,6 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
-import { PANEL_CONFIG } from "@/lib/panels/layout";
+import { PANEL_CONFIG } from "@/panels/layout";
export interface PanelSizes {
tools: number;
diff --git a/apps/web/src/hooks/use-editor.ts b/apps/web/src/editor/use-editor.ts
index ce48e036..ce48e036 100644
--- a/apps/web/src/hooks/use-editor.ts
+++ b/apps/web/src/editor/use-editor.ts
diff --git a/apps/web/src/hooks/use-menu-preview.ts b/apps/web/src/editor/use-menu-preview.ts
index a3c14489..ca42124e 100644
--- a/apps/web/src/hooks/use-menu-preview.ts
+++ b/apps/web/src/editor/use-menu-preview.ts
@@ -1,5 +1,5 @@
import { useRef } from "react";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
export function useMenuPreview() {
const editor = useEditor();
diff --git a/apps/web/src/components/editor/panels/assets/views/effects.tsx b/apps/web/src/effects/components/assets-view.tsx
index 74128df6..000af7e6 100644
--- a/apps/web/src/components/editor/panels/assets/views/effects.tsx
+++ b/apps/web/src/effects/components/assets-view.tsx
@@ -3,11 +3,11 @@
import { useEffect, useRef, useCallback } from "react";
import { PanelView } from "@/components/editor/panels/assets/views/base-panel";
import { DraggableItem } from "@/components/editor/panels/assets/draggable-item";
-import { effectsRegistry, EFFECT_TARGET_ELEMENT_TYPES } from "@/lib/effects";
+import { effectsRegistry, EFFECT_TARGET_ELEMENT_TYPES } from "@/effects";
import { effectPreviewService } from "@/services/renderer/effect-preview";
-import { useEditor } from "@/hooks/use-editor";
-import { buildEffectElement } from "@/lib/timeline/element-utils";
-import type { EffectDefinition } from "@/lib/effects/types";
+import { useEditor } from "@/editor/use-editor";
+import { buildEffectElement } from "@/timeline/element-utils";
+import type { EffectDefinition } from "@/effects/types";
export function EffectsView() {
const effects = effectsRegistry.getAll();
diff --git a/apps/web/src/components/editor/panels/properties/tabs/effects-tab.tsx b/apps/web/src/effects/components/effects-tab.tsx
index 1b94ee2d..102d818a 100644
--- a/apps/web/src/components/editor/panels/properties/tabs/effects-tab.tsx
+++ b/apps/web/src/effects/components/effects-tab.tsx
@@ -1,12 +1,12 @@
"use client";
import { useState } from "react";
-import type { ParamValues } from "@/lib/params";
-import type { Effect } from "@/lib/effects/types";
-import type { EffectElement, VisualElement } from "@/lib/timeline";
-import { effectsRegistry } from "@/lib/effects";
-import { useEditor } from "@/hooks/use-editor";
-import { useElementPreview } from "@/hooks/use-element-preview";
+import type { ParamValues } from "@/params";
+import type { Effect } from "@/effects/types";
+import type { EffectElement, VisualElement } from "@/timeline";
+import { effectsRegistry } from "@/effects";
+import { useEditor } from "@/editor/use-editor";
+import { useElementPreview } from "@/timeline/hooks/use-element-preview";
import {
Section,
SectionContent,
@@ -14,7 +14,7 @@ import {
SectionTitle,
SectionFields,
} from "@/components/section";
-import { PropertyParamField } from "../components/property-param-field";
+import { PropertyParamField } from "@/components/editor/panels/properties/components/property-param-field";
import { Button } from "@/components/ui/button";
import { HugeiconsIcon } from "@hugeicons/react";
import {
@@ -25,7 +25,7 @@ import {
} from "@hugeicons/core-free-icons";
import { cn } from "@/utils/ui";
import { Separator } from "@/components/ui/separator";
-import { useAssetsPanelStore } from "@/stores/assets-panel-store";
+import { useAssetsPanelStore } from "@/components/editor/panels/assets/assets-panel-store";
export function StandaloneEffectTab({
element,
diff --git a/apps/web/src/lib/effects/definitions/blur.ts b/apps/web/src/effects/definitions/blur.ts
index b464db3a..194196e9 100644
--- a/apps/web/src/lib/effects/definitions/blur.ts
+++ b/apps/web/src/effects/definitions/blur.ts
@@ -1,4 +1,4 @@
-import type { EffectDefinition, EffectPass } from "@/lib/effects/types";
+import type { EffectDefinition, EffectPass } from "@/effects/types";
export const GAUSSIAN_BLUR_SHADER = "gaussian-blur";
diff --git a/apps/web/src/lib/effects/definitions/index.ts b/apps/web/src/effects/definitions/index.ts
index edc8c5e2..edc8c5e2 100644
--- a/apps/web/src/lib/effects/definitions/index.ts
+++ b/apps/web/src/effects/definitions/index.ts
diff --git a/apps/web/src/lib/effects/index.ts b/apps/web/src/effects/index.ts
index 92484ee2..eebb8cbc 100644
--- a/apps/web/src/lib/effects/index.ts
+++ b/apps/web/src/effects/index.ts
@@ -1,9 +1,9 @@
import { generateUUID } from "@/utils/id";
-import { buildDefaultParamValues } from "@/lib/registry";
+import { buildDefaultParamValues } from "@/params/registry";
import { effectsRegistry } from "./registry";
-import type { ParamValues } from "@/lib/params";
-import type { Effect, EffectDefinition, EffectPass } from "@/lib/effects/types";
-import { VISUAL_ELEMENT_TYPES } from "@/lib/timeline";
+import type { ParamValues } from "@/params";
+import type { Effect, EffectDefinition, EffectPass } from "@/effects/types";
+import { VISUAL_ELEMENT_TYPES } from "@/timeline";
export { effectsRegistry } from "./registry";
export { registerDefaultEffects } from "./definitions";
diff --git a/apps/web/src/lib/effects/registry.ts b/apps/web/src/effects/registry.ts
index 29cc3202..610bca02 100644
--- a/apps/web/src/lib/effects/registry.ts
+++ b/apps/web/src/effects/registry.ts
@@ -1,5 +1,5 @@
-import { DefinitionRegistry } from "@/lib/registry";
-import type { EffectDefinition } from "@/lib/effects/types";
+import { DefinitionRegistry } from "@/params/registry";
+import type { EffectDefinition } from "@/effects/types";
export class EffectsRegistry extends DefinitionRegistry<string, EffectDefinition> {
constructor() {
diff --git a/apps/web/src/lib/effects/types.ts b/apps/web/src/effects/types.ts
index 1862b440..6b85e743 100644
--- a/apps/web/src/lib/effects/types.ts
+++ b/apps/web/src/effects/types.ts
@@ -1,4 +1,4 @@
-import type { ParamDefinition, ParamValues } from "@/lib/params";
+import type { ParamDefinition, ParamValues } from "@/params";
export interface Effect {
id: string;
diff --git a/apps/web/src/lib/env/web.ts b/apps/web/src/env/web.ts
index aa051615..aa051615 100644
--- a/apps/web/src/lib/env/web.ts
+++ b/apps/web/src/env/web.ts
diff --git a/apps/web/src/lib/export/defaults.ts b/apps/web/src/export/defaults.ts
index 0247f8a8..0247f8a8 100644
--- a/apps/web/src/lib/export/defaults.ts
+++ b/apps/web/src/export/defaults.ts
diff --git a/apps/web/src/lib/export/index.ts b/apps/web/src/export/index.ts
index c5c522b3..c5c522b3 100644
--- a/apps/web/src/lib/export/index.ts
+++ b/apps/web/src/export/index.ts
diff --git a/apps/web/src/lib/export/mime-types.ts b/apps/web/src/export/mime-types.ts
index b98900bf..b98900bf 100644
--- a/apps/web/src/lib/export/mime-types.ts
+++ b/apps/web/src/export/mime-types.ts
diff --git a/apps/web/src/lib/feedback/components/feedback-popover.tsx b/apps/web/src/feedback/components/feedback-popover.tsx
index f94b7253..f94b7253 100644
--- a/apps/web/src/lib/feedback/components/feedback-popover.tsx
+++ b/apps/web/src/feedback/components/feedback-popover.tsx
diff --git a/apps/web/src/lib/feedback/index.ts b/apps/web/src/feedback/index.ts
index dfd0743c..dfd0743c 100644
--- a/apps/web/src/lib/feedback/index.ts
+++ b/apps/web/src/feedback/index.ts
diff --git a/apps/web/src/lib/feedback/queries.ts b/apps/web/src/feedback/queries.ts
index 6962e71f..121c6fe9 100644
--- a/apps/web/src/lib/feedback/queries.ts
+++ b/apps/web/src/feedback/queries.ts
@@ -1,4 +1,4 @@
-import { db, feedback } from "@/lib/db";
+import { db, feedback } from "@/db";
import { generateUUID } from "@/utils/id";
import type { FeedbackEntry, SubmitFeedbackInput } from "./types";
diff --git a/apps/web/src/lib/feedback/types.ts b/apps/web/src/feedback/types.ts
index 358b1354..358b1354 100644
--- a/apps/web/src/lib/feedback/types.ts
+++ b/apps/web/src/feedback/types.ts
diff --git a/apps/web/src/lib/fonts/google-fonts.ts b/apps/web/src/fonts/google-fonts.ts
index 8ac55a08..36fca068 100644
--- a/apps/web/src/lib/fonts/google-fonts.ts
+++ b/apps/web/src/fonts/google-fonts.ts
@@ -1,5 +1,5 @@
-import type { FontAtlas } from "@/lib/fonts/types";
-import { SYSTEM_FONTS } from "@/lib/fonts/system-fonts";
+import type { FontAtlas } from "@/fonts/types";
+import { SYSTEM_FONTS } from "@/fonts/system-fonts";
const GOOGLE_FONTS_CSS = "https://fonts.googleapis.com/css2";
const FONT_ATLAS_PATH = "/fonts/font-atlas.json";
diff --git a/apps/web/src/lib/fonts/system-fonts.ts b/apps/web/src/fonts/system-fonts.ts
index 9cc363a3..9cc363a3 100644
--- a/apps/web/src/lib/fonts/system-fonts.ts
+++ b/apps/web/src/fonts/system-fonts.ts
diff --git a/apps/web/src/lib/fonts/types.ts b/apps/web/src/fonts/types.ts
index 89ebce08..89ebce08 100644
--- a/apps/web/src/lib/fonts/types.ts
+++ b/apps/web/src/fonts/types.ts
diff --git a/apps/web/src/hooks/use-font-atlas.ts b/apps/web/src/fonts/use-font-atlas.ts
index aa48f3b4..153eebd4 100644
--- a/apps/web/src/hooks/use-font-atlas.ts
+++ b/apps/web/src/fonts/use-font-atlas.ts
@@ -3,9 +3,9 @@ import {
getCachedFontAtlas,
loadFontAtlas,
clearFontAtlasCache,
-} from "@/lib/fonts/google-fonts";
-import type { FontAtlas } from "@/lib/fonts/types";
-import { SYSTEM_FONTS } from "@/lib/fonts/system-fonts";
+} from "@/fonts/google-fonts";
+import type { FontAtlas } from "@/fonts/types";
+import { SYSTEM_FONTS } from "@/fonts/system-fonts";
type Status = "idle" | "loading" | "error";
diff --git a/apps/web/src/lib/fps/__tests__/fps.test.ts b/apps/web/src/fps/__tests__/fps.test.ts
index 10eed8d9..e025a188 100644
--- a/apps/web/src/lib/fps/__tests__/fps.test.ts
+++ b/apps/web/src/fps/__tests__/fps.test.ts
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
import {
getHighestImportedVideoFps,
getRaisedProjectFpsForImportedMedia,
-} from "@/lib/fps/utils";
+} from "@/fps/utils";
describe("getHighestImportedVideoFps", () => {
test("returns the highest valid video fps", () => {
diff --git a/apps/web/src/lib/fps/defaults.ts b/apps/web/src/fps/defaults.ts
index 66bf0b1c..66bf0b1c 100644
--- a/apps/web/src/lib/fps/defaults.ts
+++ b/apps/web/src/fps/defaults.ts
diff --git a/apps/web/src/lib/fps/presets.ts b/apps/web/src/fps/presets.ts
index b09920d4..b09920d4 100644
--- a/apps/web/src/lib/fps/presets.ts
+++ b/apps/web/src/fps/presets.ts
diff --git a/apps/web/src/lib/fps/utils.ts b/apps/web/src/fps/utils.ts
index 932788c4..e30e263a 100644
--- a/apps/web/src/lib/fps/utils.ts
+++ b/apps/web/src/fps/utils.ts
@@ -1,5 +1,5 @@
import type { FrameRate } from "opencut-wasm";
-import type { MediaAsset } from "@/lib/media/types";
+import type { MediaAsset } from "@/media/types";
type MediaAssetFpsInput = Pick<MediaAsset, "type" | "fps">;
diff --git a/apps/web/src/lib/gradients/canvas.ts b/apps/web/src/gradients/canvas.ts
index e2287333..e2287333 100644
--- a/apps/web/src/lib/gradients/canvas.ts
+++ b/apps/web/src/gradients/canvas.ts
diff --git a/apps/web/src/lib/gradients/index.ts b/apps/web/src/gradients/index.ts
index 490a8fbf..490a8fbf 100644
--- a/apps/web/src/lib/gradients/index.ts
+++ b/apps/web/src/gradients/index.ts
diff --git a/apps/web/src/lib/gradients/parser.ts b/apps/web/src/gradients/parser.ts
index 342780b4..342780b4 100644
--- a/apps/web/src/lib/gradients/parser.ts
+++ b/apps/web/src/gradients/parser.ts
diff --git a/apps/web/src/components/editor/panels/properties/tabs/graphic-tab.tsx b/apps/web/src/graphics/components/graphic-tab.tsx
index c61c9cd7..60599772 100644
--- a/apps/web/src/components/editor/panels/properties/tabs/graphic-tab.tsx
+++ b/apps/web/src/graphics/components/graphic-tab.tsx
@@ -1,17 +1,17 @@
"use client";
import { useRef } from "react";
-import { useElementPlayhead } from "../hooks/use-element-playhead";
+import { useElementPlayhead } from "@/components/editor/panels/properties/hooks/use-element-playhead";
import {
useKeyframedParamProperty,
type KeyframedParamPropertyResult,
-} from "../hooks/use-keyframed-param-property";
-import { resolveGraphicParamsAtTime } from "@/lib/animation";
-import type { ParamDefinition, ParamValues } from "@/lib/params";
-import type { GraphicElement } from "@/lib/timeline";
-import { graphicsRegistry, registerDefaultGraphics } from "@/lib/graphics";
-import { useElementPreview } from "@/hooks/use-element-preview";
-import { useEditor } from "@/hooks/use-editor";
+} from "@/components/editor/panels/properties/hooks/use-keyframed-param-property";
+import { resolveGraphicParamsAtTime } from "@/animation";
+import type { ParamDefinition, ParamValues } from "@/params";
+import type { GraphicElement } from "@/timeline";
+import { graphicsRegistry, registerDefaultGraphics } from "@/graphics";
+import { useElementPreview } from "@/timeline/hooks/use-element-preview";
+import { useEditor } from "@/editor/use-editor";
import {
Section,
SectionContent,
@@ -19,7 +19,7 @@ import {
SectionHeader,
SectionTitle,
} from "@/components/section";
-import { PropertyParamField } from "../components/property-param-field";
+import { PropertyParamField } from "@/components/editor/panels/properties/components/property-param-field";
import { Button } from "@/components/ui/button";
import { HugeiconsIcon } from "@hugeicons/react";
import { MinusSignIcon, PlusSignIcon } from "@hugeicons/core-free-icons";
@@ -78,9 +78,7 @@ export function GraphicTab({
</SectionFields>
</SectionContent>
</Section>
- {hasStrokeParams && (
- <StrokeSection element={element} trackId={trackId} />
- )}
+ {hasStrokeParams && <StrokeSection element={element} trackId={trackId} />}
</div>
);
}
@@ -206,21 +204,23 @@ function AnimatedGraphicParamField({
isPlayheadWithinElementRange: boolean;
resolvedParams: ParamValues;
}) {
- const animatedParam: KeyframedParamPropertyResult = useKeyframedParamProperty({
- param,
- trackId,
- elementId: element.id,
- animations: element.animations,
- localTime,
- isPlayheadWithinElementRange,
- resolvedValue: resolvedParams[param.key] ?? param.default,
- buildBaseUpdates: ({ value }) => ({
- params: {
- ...element.params,
- [param.key]: value,
- },
- }),
- });
+ const animatedParam: KeyframedParamPropertyResult = useKeyframedParamProperty(
+ {
+ param,
+ trackId,
+ elementId: element.id,
+ animations: element.animations,
+ localTime,
+ isPlayheadWithinElementRange,
+ resolvedValue: resolvedParams[param.key] ?? param.default,
+ buildBaseUpdates: ({ value }) => ({
+ params: {
+ ...element.params,
+ [param.key]: value,
+ },
+ }),
+ },
+ );
return (
<PropertyParamField
diff --git a/apps/web/src/lib/graphics/definitions/ellipse.ts b/apps/web/src/graphics/definitions/ellipse.ts
index 04b93c27..7ae9e8e3 100644
--- a/apps/web/src/lib/graphics/definitions/ellipse.ts
+++ b/apps/web/src/graphics/definitions/ellipse.ts
@@ -1,4 +1,4 @@
-import type { ParamDefinition } from "@/lib/params";
+import type { ParamDefinition } from "@/params";
import { applyAlignedStroke } from "../stroke";
import { STROKE_ALIGN_PARAM, type GraphicStrokeAlign } from "./shared";
import type { GraphicDefinition } from "../types";
diff --git a/apps/web/src/lib/graphics/definitions/index.ts b/apps/web/src/graphics/definitions/index.ts
index 6fd9e446..6fd9e446 100644
--- a/apps/web/src/lib/graphics/definitions/index.ts
+++ b/apps/web/src/graphics/definitions/index.ts
diff --git a/apps/web/src/lib/graphics/definitions/polygon.ts b/apps/web/src/graphics/definitions/polygon.ts
index deecb6ff..11273fed 100644
--- a/apps/web/src/lib/graphics/definitions/polygon.ts
+++ b/apps/web/src/graphics/definitions/polygon.ts
@@ -1,4 +1,4 @@
-import type { ParamDefinition } from "@/lib/params";
+import type { ParamDefinition } from "@/params";
import { applyAlignedStroke } from "../stroke";
import { STROKE_ALIGN_PARAM, type GraphicStrokeAlign } from "./shared";
import type { GraphicDefinition } from "../types";
diff --git a/apps/web/src/lib/graphics/definitions/rectangle.ts b/apps/web/src/graphics/definitions/rectangle.ts
index fb52c03a..3cd0a449 100644
--- a/apps/web/src/lib/graphics/definitions/rectangle.ts
+++ b/apps/web/src/graphics/definitions/rectangle.ts
@@ -1,4 +1,4 @@
-import type { ParamDefinition } from "@/lib/params";
+import type { ParamDefinition } from "@/params";
import { applyAlignedStroke } from "../stroke";
import { STROKE_ALIGN_PARAM, type GraphicStrokeAlign } from "./shared";
import type { GraphicDefinition } from "../types";
diff --git a/apps/web/src/lib/graphics/definitions/shared.ts b/apps/web/src/graphics/definitions/shared.ts
index 4f56c77c..2bd92875 100644
--- a/apps/web/src/lib/graphics/definitions/shared.ts
+++ b/apps/web/src/graphics/definitions/shared.ts
@@ -1,4 +1,4 @@
-import type { ParamDefinition } from "@/lib/params";
+import type { ParamDefinition } from "@/params";
export type GraphicStrokeAlign = "inside" | "center" | "outside";
diff --git a/apps/web/src/lib/graphics/definitions/star.ts b/apps/web/src/graphics/definitions/star.ts
index 2160c125..3aa40fee 100644
--- a/apps/web/src/lib/graphics/definitions/star.ts
+++ b/apps/web/src/graphics/definitions/star.ts
@@ -1,4 +1,4 @@
-import type { ParamDefinition } from "@/lib/params";
+import type { ParamDefinition } from "@/params";
import { applyAlignedStroke } from "../stroke";
import { STROKE_ALIGN_PARAM, type GraphicStrokeAlign } from "./shared";
import type { GraphicDefinition } from "../types";
diff --git a/apps/web/src/lib/graphics/index.ts b/apps/web/src/graphics/index.ts
index 9002a151..c69a4ab0 100644
--- a/apps/web/src/lib/graphics/index.ts
+++ b/apps/web/src/graphics/index.ts
@@ -1,5 +1,5 @@
-import { buildDefaultParamValues } from "@/lib/registry";
-import type { ParamValues } from "@/lib/params";
+import { buildDefaultParamValues } from "@/params/registry";
+import type { ParamValues } from "@/params";
import { graphicsRegistry } from "./registry";
import {
registerDefaultGraphics,
diff --git a/apps/web/src/lib/graphics/registry.ts b/apps/web/src/graphics/registry.ts
index 2f6a5850..ea40a43a 100644
--- a/apps/web/src/lib/graphics/registry.ts
+++ b/apps/web/src/graphics/registry.ts
@@ -1,4 +1,4 @@
-import { DefinitionRegistry } from "@/lib/registry";
+import { DefinitionRegistry } from "@/params/registry";
import type { GraphicDefinition } from "./types";
export class GraphicsRegistry extends DefinitionRegistry<string, GraphicDefinition> {
diff --git a/apps/web/src/lib/graphics/stroke.ts b/apps/web/src/graphics/stroke.ts
index 230d9904..230d9904 100644
--- a/apps/web/src/lib/graphics/stroke.ts
+++ b/apps/web/src/graphics/stroke.ts
diff --git a/apps/web/src/lib/graphics/types.ts b/apps/web/src/graphics/types.ts
index 22bfc5ac..1c6cc10e 100644
--- a/apps/web/src/lib/graphics/types.ts
+++ b/apps/web/src/graphics/types.ts
@@ -1,4 +1,4 @@
-import type { ParamDefinition, ParamValues } from "@/lib/params";
+import type { ParamDefinition, ParamValues } from "@/params";
export const DEFAULT_GRAPHIC_SOURCE_SIZE = 512;
diff --git a/apps/web/src/lib/guides/definitions/custom.tsx b/apps/web/src/guides/definitions/custom.tsx
index fe7944cb..a5bede0b 100644
--- a/apps/web/src/lib/guides/definitions/custom.tsx
+++ b/apps/web/src/guides/definitions/custom.tsx
@@ -1,7 +1,7 @@
import { PlusSignIcon, RulerIcon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Button } from "@/components/ui/button";
-import type { GuideDefinition } from "@/lib/guides/types";
+import type { GuideDefinition } from "@/guides/types";
function CustomGuideOptions() {
return (
diff --git a/apps/web/src/lib/guides/definitions/grid.tsx b/apps/web/src/guides/definitions/grid.tsx
index 7677a1d9..7df6425d 100644
--- a/apps/web/src/lib/guides/definitions/grid.tsx
+++ b/apps/web/src/guides/definitions/grid.tsx
@@ -9,11 +9,11 @@ import {
GRID_MIN,
GRID_MAX,
DEFAULT_GRID_CONFIG,
-} from "@/lib/guides/grid";
-import { usePreviewStore } from "@/stores/preview-store";
+} from "@/guides/grid";
+import { usePreviewStore } from "@/preview/preview-store";
import { clampRound } from "@/utils/math";
import { cn } from "@/utils/ui";
-import type { GuideDefinition } from "@/lib/guides/types";
+import type { GuideDefinition } from "@/guides/types";
function GridLines({
rows,
diff --git a/apps/web/src/lib/guides/definitions/platforms.tsx b/apps/web/src/guides/definitions/platforms.tsx
index 8c403eb1..b2471a74 100644
--- a/apps/web/src/lib/guides/definitions/platforms.tsx
+++ b/apps/web/src/guides/definitions/platforms.tsx
@@ -1,5 +1,5 @@
import Image from "next/image";
-import type { GuideDefinition } from "@/lib/guides/types";
+import type { GuideDefinition } from "@/guides/types";
import { TikTokLayout } from "./tiktok-layout";
function PlatformLogo({
diff --git a/apps/web/src/lib/guides/definitions/tiktok-layout.tsx b/apps/web/src/guides/definitions/tiktok-layout.tsx
index b7513bac..b7513bac 100644
--- a/apps/web/src/lib/guides/definitions/tiktok-layout.tsx
+++ b/apps/web/src/guides/definitions/tiktok-layout.tsx
diff --git a/apps/web/src/lib/guides/grid.ts b/apps/web/src/guides/grid.ts
index 0b707de7..0b707de7 100644
--- a/apps/web/src/lib/guides/grid.ts
+++ b/apps/web/src/guides/grid.ts
diff --git a/apps/web/src/lib/guides/index.ts b/apps/web/src/guides/index.ts
index 9ea75631..a01d3d24 100644
--- a/apps/web/src/lib/guides/index.ts
+++ b/apps/web/src/guides/index.ts
@@ -1,13 +1,14 @@
-import { GUIDE_REGISTRY } from "./registry";
-import type { GuideDefinition } from "@/lib/guides/types";
-
-export { GUIDE_REGISTRY, isGuideId } from "./registry";
-export type { GuideDefinition, GuideId, GuideRenderProps } from "./registry";
-
-export function getGuideById(guideId: string | null): GuideDefinition | null {
- if (!guideId) {
- return null;
- }
-
- return GUIDE_REGISTRY.find((guide) => guide.id === guideId) ?? null;
-}
+import { GUIDE_REGISTRY } from "./registry";
+import type { GuideDefinition } from "@/guides/types";
+
+export { GUIDE_REGISTRY, isGuideId } from "./registry";
+export type { GuideDefinition, GuideId, GuideRenderProps } from "./registry";
+export { getGuidePreviewOverlaySource } from "./preview-overlay";
+
+export function getGuideById(guideId: string | null): GuideDefinition | null {
+ if (!guideId) {
+ return null;
+ }
+
+ return GUIDE_REGISTRY.find((guide) => guide.id === guideId) ?? null;
+}
diff --git a/apps/web/src/guides/preview-overlay.tsx b/apps/web/src/guides/preview-overlay.tsx
new file mode 100644
index 00000000..d71adb12
--- /dev/null
+++ b/apps/web/src/guides/preview-overlay.tsx
@@ -0,0 +1,33 @@
+import { getGuideById, type GuideId } from "@/guides";
+import {
+ EMPTY_PREVIEW_OVERLAY_SOURCE_RESULT,
+ type PreviewOverlaySourceResult,
+} from "@/preview/overlays";
+
+export function getGuidePreviewOverlaySource({
+ guideId,
+}: {
+ guideId: GuideId | null;
+}): PreviewOverlaySourceResult {
+ const guide = getGuideById(guideId);
+ if (!guide) {
+ return EMPTY_PREVIEW_OVERLAY_SOURCE_RESULT;
+ }
+
+ return {
+ definitions: [],
+ instances: [
+ {
+ id: `guide-${guide.id}`,
+ mount: { kind: "scene" },
+ plane: "under-interaction",
+ pointerEvents: "none",
+ render: ({ sceneHeight, sceneWidth }) =>
+ guide.renderOverlay({
+ width: sceneWidth,
+ height: sceneHeight,
+ }),
+ },
+ ],
+ };
+}
diff --git a/apps/web/src/lib/guides/registry.tsx b/apps/web/src/guides/registry.tsx
index 8000d989..d1c6d652 100644
--- a/apps/web/src/lib/guides/registry.tsx
+++ b/apps/web/src/guides/registry.tsx
@@ -1,4 +1,4 @@
-import type { GuideDefinition } from "@/lib/guides/types";
+import type { GuideDefinition } from "@/guides/types";
import { gridGuide } from "./definitions/grid";
// import { customGuide } from "./definitions/custom";
import {
@@ -8,7 +8,7 @@ import {
spotlightGuide,
} from "./definitions/platforms";
-export type { GuideDefinition, GuideRenderProps } from "@/lib/guides/types";
+export type { GuideDefinition, GuideRenderProps } from "@/guides/types";
export const GUIDE_REGISTRY = [
gridGuide,
diff --git a/apps/web/src/lib/guides/types.ts b/apps/web/src/guides/types.ts
index 5b776090..5b776090 100644
--- a/apps/web/src/lib/guides/types.ts
+++ b/apps/web/src/guides/types.ts
diff --git a/apps/web/src/lib/selection/index.ts b/apps/web/src/lib/selection/index.ts
deleted file mode 100644
index b1ded820..00000000
--- a/apps/web/src/lib/selection/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export { SelectableItem } from "@/lib/selection/selectable-item";
-export { SelectableSurface } from "@/lib/selection/selectable-surface";
-export { useSelection } from "@/lib/selection/context";
-export { useSelectionScope } from "@/lib/selection/hooks/use-selection-scope";
diff --git a/apps/web/src/lib/timeline/snap-utils.ts b/apps/web/src/lib/timeline/snap-utils.ts
deleted file mode 100644
index b2be95a9..00000000
--- a/apps/web/src/lib/timeline/snap-utils.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-import type { Bookmark, SceneTracks } from "@/lib/timeline";
-import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
-import { getElementKeyframes } from "@/lib/animation";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
-
-export interface SnapPoint {
- time: number;
- type: "element-start" | "element-end" | "playhead" | "bookmark" | "keyframe";
- elementId?: string;
- trackId?: string;
-}
-
-export interface SnapResult {
- snappedTime: number;
- snapPoint: SnapPoint | null;
- snapDistance: number;
-}
-
-const DEFAULT_SNAP_THRESHOLD_PX = 10;
-
-export function findSnapPoints({
- tracks,
- playheadTime,
- excludeElementId,
- excludeElementIds,
- bookmarks = [],
- excludeBookmarkTime,
- enableElementSnapping = true,
- enablePlayheadSnapping = true,
- enableBookmarkSnapping = true,
- enableKeyframeSnapping = true,
-}: {
- tracks: SceneTracks;
- playheadTime: number;
- excludeElementId?: string;
- excludeElementIds?: Set<string>;
- bookmarks?: Array<Bookmark>;
- excludeBookmarkTime?: number;
- enableElementSnapping?: boolean;
- enablePlayheadSnapping?: boolean;
- enableBookmarkSnapping?: boolean;
- enableKeyframeSnapping?: boolean;
-}): SnapPoint[] {
- const snapPoints: SnapPoint[] = [];
- const orderedTracks = [...tracks.overlay, tracks.main, ...tracks.audio];
- const blockedElementIds = new Set(excludeElementIds);
- if (excludeElementId) {
- blockedElementIds.add(excludeElementId);
- }
-
- for (const track of orderedTracks) {
- for (const element of track.elements) {
- if (blockedElementIds.has(element.id)) continue;
-
- if (enableElementSnapping) {
- snapPoints.push(
- {
- time: element.startTime,
- type: "element-start",
- elementId: element.id,
- trackId: track.id,
- },
- {
- time: element.startTime + element.duration,
- type: "element-end",
- elementId: element.id,
- trackId: track.id,
- },
- );
- }
-
- if (enableKeyframeSnapping) {
- for (const keyframe of getElementKeyframes({
- animations: element.animations,
- })) {
- snapPoints.push({
- time: element.startTime + keyframe.time,
- type: "keyframe",
- elementId: element.id,
- trackId: track.id,
- });
- }
- }
- }
- }
-
- if (enablePlayheadSnapping) {
- snapPoints.push({ time: playheadTime, type: "playhead" });
- }
-
- if (enableBookmarkSnapping) {
- for (const bookmark of bookmarks) {
- if (
- excludeBookmarkTime != null &&
- bookmark.time === excludeBookmarkTime
- ) {
- continue;
- }
- snapPoints.push({ time: bookmark.time, type: "bookmark" });
- }
- }
-
- return snapPoints;
-}
-
-export function snapToNearestPoint({
- targetTime,
- snapPoints,
- zoomLevel,
- snapThreshold = DEFAULT_SNAP_THRESHOLD_PX,
-}: {
- targetTime: number;
- snapPoints: Array<SnapPoint>;
- zoomLevel: number;
- snapThreshold?: number;
-}): SnapResult {
- const pixelsPerSecond = BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel;
- const thresholdInTicks = (snapThreshold / pixelsPerSecond) * TICKS_PER_SECOND;
-
- let closestSnapPoint: SnapPoint | null = null;
- let closestDistance = Infinity;
-
- for (const snapPoint of snapPoints) {
- const distance = Math.abs(targetTime - snapPoint.time);
- if (distance < thresholdInTicks && distance < closestDistance) {
- closestDistance = distance;
- closestSnapPoint = snapPoint;
- }
- }
-
- return {
- snappedTime: closestSnapPoint ? closestSnapPoint.time : targetTime,
- snapPoint: closestSnapPoint,
- snapDistance: closestDistance,
- };
-}
-
-export function snapElementEdge({
- targetTime,
- elementDuration,
- tracks,
- playheadTime,
- zoomLevel,
- excludeElementId,
- excludeElementIds,
- snapToStart = true,
- bookmarks = [],
-}: {
- targetTime: number;
- elementDuration: number;
- tracks: SceneTracks;
- playheadTime: number;
- zoomLevel: number;
- excludeElementId?: string;
- excludeElementIds?: Set<string>;
- snapToStart?: boolean;
- bookmarks?: Array<Bookmark>;
-}): SnapResult {
- const snapPoints = findSnapPoints({
- tracks,
- playheadTime,
- excludeElementId,
- excludeElementIds,
- bookmarks,
- });
-
- const effectiveTargetTime = snapToStart
- ? targetTime
- : targetTime + elementDuration;
-
- const snapResult = snapToNearestPoint({
- targetTime: effectiveTargetTime,
- snapPoints,
- zoomLevel,
- });
-
- if (!snapToStart && snapResult.snapPoint) {
- snapResult.snappedTime = snapResult.snappedTime - elementDuration;
- }
-
- return snapResult;
-}
diff --git a/apps/web/src/lib/masks/__tests__/snap.test.ts b/apps/web/src/masks/__tests__/snap.test.ts
index e2f9cafb..2f33615f 100644
--- a/apps/web/src/lib/masks/__tests__/snap.test.ts
+++ b/apps/web/src/masks/__tests__/snap.test.ts
@@ -4,23 +4,23 @@ import {
getCustomMaskClosedStateAfterPointRemoval,
insertPointIntoCustomMaskSegment,
removeCustomMaskPoints,
-} from "@/lib/masks/custom-path";
+} from "@/masks/custom-path";
import {
appendPointToCustomMask,
customMaskDefinition,
insertPointOnCustomMaskSegment,
-} from "@/lib/masks/definitions/custom";
-import { getSplitMaskStrokeSegment } from "@/lib/masks/definitions/split";
-import { textMaskDefinition } from "@/lib/masks/definitions/text";
-import { getMaskSnapGeometry } from "@/lib/masks/geometry";
-import { snapMaskInteraction } from "@/lib/masks/snap";
-import type { ElementBounds } from "@/lib/preview/element-bounds";
+} from "@/masks/definitions/custom";
+import { getSplitMaskStrokeSegment } from "@/masks/definitions/split";
+import { textMaskDefinition } from "@/masks/definitions/text";
+import { getMaskSnapGeometry } from "@/masks/geometry";
+import { snapMaskInteraction } from "@/masks/snap";
+import type { ElementBounds } from "@/preview/element-bounds";
import type {
CustomMaskParams,
RectangleMaskParams,
SplitMaskParams,
TextMaskParams,
-} from "@/lib/masks/types";
+} from "@/masks/types";
const bounds: ElementBounds = {
cx: 200,
diff --git a/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx b/apps/web/src/masks/components/masks-tab.tsx
index 090fce64..97fba3ab 100644
--- a/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx
+++ b/apps/web/src/masks/components/masks-tab.tsx
@@ -1,16 +1,13 @@
"use client";
-import type { MaskableElement } from "@/lib/timeline";
-import type { Mask, MaskType, TextMask } from "@/lib/masks/types";
-import type {
- NumberParamDefinition,
- SelectParamDefinition,
-} from "@/lib/params";
-import { masksRegistry, buildDefaultMaskInstance } from "@/lib/masks";
-import { useEditor } from "@/hooks/use-editor";
-import { useElementPreview } from "@/hooks/use-element-preview";
-import { useMenuPreview } from "@/hooks/use-menu-preview";
-import { getVisibleElementsWithBounds } from "@/lib/preview/element-bounds";
+import type { MaskableElement } from "@/timeline";
+import type { Mask, MaskType, TextMask } from "@/masks/types";
+import type { NumberParamDefinition, SelectParamDefinition } from "@/params";
+import { masksRegistry, buildDefaultMaskInstance } from "@/masks";
+import { useEditor } from "@/editor/use-editor";
+import { useElementPreview } from "@/timeline/hooks/use-element-preview";
+import { useMenuPreview } from "@/editor/use-menu-preview";
+import { getVisibleElementsWithBounds } from "@/preview/element-bounds";
import { HugeiconsIcon } from "@hugeicons/react";
import {
ArrowExpandIcon,
@@ -58,7 +55,7 @@ import {
SectionHeader,
SectionTitle,
} from "@/components/section";
-import { usePropertyDraft } from "../hooks/use-property-draft";
+import { usePropertyDraft } from "@/components/editor/panels/properties/hooks/use-property-draft";
import {
OcMirrorIcon,
OcShapesIcon,
diff --git a/apps/web/src/lib/masks/custom-path.ts b/apps/web/src/masks/custom-path.ts
index 94a9043c..0349df63 100644
--- a/apps/web/src/lib/masks/custom-path.ts
+++ b/apps/web/src/masks/custom-path.ts
@@ -1,4 +1,4 @@
-import type { ElementBounds } from "@/lib/preview/element-bounds";
+import type { ElementBounds } from "@/preview/element-bounds";
export interface CustomMaskPathPoint {
id: string;
diff --git a/apps/web/src/lib/masks/definitions/box-like.ts b/apps/web/src/masks/definitions/box-like.ts
index fe0f1a85..7db51df7 100644
--- a/apps/web/src/lib/masks/definitions/box-like.ts
+++ b/apps/web/src/masks/definitions/box-like.ts
@@ -1,7 +1,7 @@
import {
DEFAULT_SHAPE_MASK_SHORT_SIDE_RATIO,
MIN_MASK_DIMENSION,
-} from "@/lib/masks/dimensions";
+} from "@/masks/dimensions";
import { computeFeatherUpdate } from "../param-update";
import type {
BaseMaskParams,
@@ -10,13 +10,13 @@ import type {
MaskInteractionDefinition,
MaskParamUpdateArgs,
RectangleMaskParams,
-} from "@/lib/masks/types";
-import type { NumberParamDefinition, ParamDefinition } from "@/lib/params";
+} from "@/masks/types";
+import type { NumberParamDefinition, ParamDefinition } from "@/params";
import {
getBoxMaskHandlePositions,
getBoxMaskOverlays,
-} from "@/lib/masks/handle-positions";
-import { snapMaskInteraction } from "@/lib/masks/snap";
+} from "@/masks/handle-positions";
+import { snapMaskInteraction } from "@/masks/snap";
const PERCENTAGE_DISPLAY: Pick<
NumberParamDefinition,
diff --git a/apps/web/src/lib/masks/definitions/cinematic-bars.ts b/apps/web/src/masks/definitions/cinematic-bars.ts
index ef9c6e00..36b31328 100644
--- a/apps/web/src/lib/masks/definitions/cinematic-bars.ts
+++ b/apps/web/src/masks/definitions/cinematic-bars.ts
@@ -2,7 +2,7 @@ import type {
MaskDefaultContext,
MaskDefinition,
RectangleMaskParams,
-} from "@/lib/masks/types";
+} from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
diff --git a/apps/web/src/lib/masks/definitions/custom.ts b/apps/web/src/masks/definitions/custom.ts
index 0fc69929..cd6e2055 100644
--- a/apps/web/src/lib/masks/definitions/custom.ts
+++ b/apps/web/src/masks/definitions/custom.ts
@@ -1,7 +1,7 @@
import { generateUUID } from "@/utils/id";
-import type { ParamDefinition } from "@/lib/params";
-import { PEN_CURSOR } from "@/components/editor/panels/preview/cursors";
-import type { ElementBounds } from "@/lib/preview/element-bounds";
+import type { ParamDefinition } from "@/params";
+import { PEN_CURSOR } from "@/preview/components/cursors";
+import type { ElementBounds } from "@/preview/element-bounds";
import type {
CustomMask,
CustomMaskParams,
@@ -9,7 +9,7 @@ import type {
MaskHandlePosition,
MaskOverlay,
MaskParamUpdateArgs,
-} from "@/lib/masks/types";
+} from "@/masks/types";
import {
buildCustomMaskPath2D,
buildCustomMaskSvgPath,
@@ -23,18 +23,18 @@ import {
parseCustomMaskHandleId,
recenterCustomMaskPath,
type CustomMaskPathPoint,
-} from "@/lib/masks/custom-path";
-import { getBoxMaskHandlePositions } from "@/lib/masks/handle-positions";
-import { computeFeatherUpdate } from "@/lib/masks/param-update";
+} from "@/masks/custom-path";
+import { getBoxMaskHandlePositions } from "@/masks/handle-positions";
+import { computeFeatherUpdate } from "@/masks/param-update";
import {
setMaskLocalCenter,
toGlobalMaskSnapLines,
-} from "@/lib/masks/geometry";
+} from "@/masks/geometry";
import {
snapPosition,
snapRotation,
snapScale,
-} from "@/lib/preview/preview-snap";
+} from "@/preview/preview-snap";
const PERCENTAGE_DISPLAY = {
displayMultiplier: 100,
diff --git a/apps/web/src/lib/masks/definitions/diamond.ts b/apps/web/src/masks/definitions/diamond.ts
index 56930678..037aee70 100644
--- a/apps/web/src/lib/masks/definitions/diamond.ts
+++ b/apps/web/src/masks/definitions/diamond.ts
@@ -1,4 +1,4 @@
-import type { MaskDefinition, RectangleMaskParams } from "@/lib/masks/types";
+import type { MaskDefinition, RectangleMaskParams } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
diff --git a/apps/web/src/lib/masks/definitions/ellipse.ts b/apps/web/src/masks/definitions/ellipse.ts
index 2eb85f0c..d0567817 100644
--- a/apps/web/src/lib/masks/definitions/ellipse.ts
+++ b/apps/web/src/masks/definitions/ellipse.ts
@@ -1,4 +1,4 @@
-import type { MaskDefinition, RectangleMaskParams } from "@/lib/masks/types";
+import type { MaskDefinition, RectangleMaskParams } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
diff --git a/apps/web/src/lib/masks/definitions/heart.ts b/apps/web/src/masks/definitions/heart.ts
index 429c5d98..d15c09e6 100644
--- a/apps/web/src/lib/masks/definitions/heart.ts
+++ b/apps/web/src/masks/definitions/heart.ts
@@ -1,4 +1,4 @@
-import type { MaskDefinition, RectangleMaskParams } from "@/lib/masks/types";
+import type { MaskDefinition, RectangleMaskParams } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
diff --git a/apps/web/src/lib/masks/definitions/index.ts b/apps/web/src/masks/definitions/index.ts
index fd00cab1..9cee1020 100644
--- a/apps/web/src/lib/masks/definitions/index.ts
+++ b/apps/web/src/masks/definitions/index.ts
@@ -1,4 +1,4 @@
-import type { BaseMaskParams, MaskDefinition } from "@/lib/masks/types";
+import type { BaseMaskParams, MaskDefinition } from "@/masks/types";
import { masksRegistry, type MaskIconProps } from "../registry";
import { cinematicBarsMaskDefinition } from "./cinematic-bars";
import { customMaskDefinition } from "./custom";
diff --git a/apps/web/src/lib/masks/definitions/rectangle.ts b/apps/web/src/masks/definitions/rectangle.ts
index 832b8307..7bf1a7fd 100644
--- a/apps/web/src/lib/masks/definitions/rectangle.ts
+++ b/apps/web/src/masks/definitions/rectangle.ts
@@ -1,4 +1,4 @@
-import type { MaskDefinition, RectangleMaskParams } from "@/lib/masks/types";
+import type { MaskDefinition, RectangleMaskParams } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
diff --git a/apps/web/src/lib/masks/definitions/split.ts b/apps/web/src/masks/definitions/split.ts
index c1a14d90..d50a2571 100644
--- a/apps/web/src/lib/masks/definitions/split.ts
+++ b/apps/web/src/masks/definitions/split.ts
@@ -3,13 +3,13 @@ import type {
MaskDefinition,
MaskParamUpdateArgs,
SplitMaskParams,
-} from "@/lib/masks/types";
+} from "@/masks/types";
import { halfPlaneSign, lineEdgeIntersection } from "../utils";
import {
getLineMaskHandlePositions,
getLineMaskOverlay,
-} from "@/lib/masks/handle-positions";
-import { snapMaskInteraction } from "@/lib/masks/snap";
+} from "@/masks/handle-positions";
+import { snapMaskInteraction } from "@/masks/snap";
// cos(Ï€/2) returns ~6e-17 in JS, not 0. Values below this threshold are snapped
// to exactly 0 to prevent opposite-sign float noise on canvas corners that lie
diff --git a/apps/web/src/lib/masks/definitions/star.ts b/apps/web/src/masks/definitions/star.ts
index d311ab80..a7f8819c 100644
--- a/apps/web/src/lib/masks/definitions/star.ts
+++ b/apps/web/src/masks/definitions/star.ts
@@ -1,4 +1,4 @@
-import type { MaskDefinition, RectangleMaskParams } from "@/lib/masks/types";
+import type { MaskDefinition, RectangleMaskParams } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
diff --git a/apps/web/src/lib/masks/definitions/text.ts b/apps/web/src/masks/definitions/text.ts
index b774ed28..c285f1c8 100644
--- a/apps/web/src/lib/masks/definitions/text.ts
+++ b/apps/web/src/masks/definitions/text.ts
@@ -1,34 +1,34 @@
-import type { ParamDefinition } from "@/lib/params";
+import type { ParamDefinition } from "@/params";
import type {
MaskDefinition,
MaskParamUpdateArgs,
TextMask,
TextMaskParams,
-} from "@/lib/masks/types";
-import { DEFAULTS } from "@/lib/timeline/defaults";
-import { MIN_FONT_SIZE, MAX_FONT_SIZE } from "@/lib/text/typography";
+} from "@/masks/types";
+import { DEFAULTS } from "@/timeline/defaults";
+import { MIN_FONT_SIZE, MAX_FONT_SIZE } from "@/text/typography";
import {
drawMeasuredTextLayout,
measureTextLayout,
strokeMeasuredTextLayout,
-} from "@/lib/text/primitives";
-import { getTextMeasurementContext } from "@/lib/text/measure-element";
-import { getTextVisualRect } from "@/lib/text/layout";
+} from "@/text/primitives";
+import { getTextMeasurementContext } from "@/text/measure-element";
+import { getTextVisualRect } from "@/text/layout";
import {
getBoxMaskHandlePositions,
getBoxMaskRectOverlay,
-} from "@/lib/masks/handle-positions";
-import { computeFeatherUpdate } from "@/lib/masks/param-update";
+} from "@/masks/handle-positions";
+import { computeFeatherUpdate } from "@/masks/param-update";
import {
setMaskLocalCenter,
toGlobalMaskSnapLines,
-} from "@/lib/masks/geometry";
+} from "@/masks/geometry";
import {
snapPosition,
snapRotation,
snapScale,
type ScaleEdgePreference,
-} from "@/lib/preview/preview-snap";
+} from "@/preview/preview-snap";
const PERCENTAGE_DISPLAY = {
displayMultiplier: 100,
diff --git a/apps/web/src/lib/masks/dimensions.ts b/apps/web/src/masks/dimensions.ts
index dcfcdc0c..dcfcdc0c 100644
--- a/apps/web/src/lib/masks/dimensions.ts
+++ b/apps/web/src/masks/dimensions.ts
diff --git a/apps/web/src/lib/masks/feather.ts b/apps/web/src/masks/feather.ts
index 83e80a0c..83e80a0c 100644
--- a/apps/web/src/lib/masks/feather.ts
+++ b/apps/web/src/masks/feather.ts
diff --git a/apps/web/src/lib/masks/geometry.ts b/apps/web/src/masks/geometry.ts
index a5b05030..007dfa49 100644
--- a/apps/web/src/lib/masks/geometry.ts
+++ b/apps/web/src/masks/geometry.ts
@@ -1,7 +1,7 @@
-import type { ElementBounds } from "@/lib/preview/element-bounds";
-import type { SnapLine } from "@/lib/preview/preview-snap";
-import { MIN_MASK_DIMENSION } from "@/lib/masks/dimensions";
-import type { RectangleMaskParams } from "@/lib/masks/types";
+import type { ElementBounds } from "@/preview/element-bounds";
+import type { SnapLine } from "@/preview/preview-snap";
+import { MIN_MASK_DIMENSION } from "@/masks/dimensions";
+import type { RectangleMaskParams } from "@/masks/types";
type CenterMaskParams = {
centerX: number;
diff --git a/apps/web/src/lib/masks/handle-positions.ts b/apps/web/src/masks/handle-positions.ts
index d32b8424..75668959 100644
--- a/apps/web/src/lib/masks/handle-positions.ts
+++ b/apps/web/src/masks/handle-positions.ts
@@ -1,5 +1,5 @@
-import { FEATHER_HANDLE_SCALE } from "@/lib/masks/feather";
-import type { ElementBounds } from "@/lib/preview/element-bounds";
+import { FEATHER_HANDLE_SCALE } from "@/masks/feather";
+import type { ElementBounds } from "@/preview/element-bounds";
import type {
MaskFeatures,
MaskHandlePosition,
@@ -8,7 +8,7 @@ import type {
MaskRectOverlay,
RectangleMaskParams,
MaskShapeOverlay,
-} from "@/lib/masks/types";
+} from "@/masks/types";
const LINE_HANDLE_OFFSET_SCREEN_PX = 20;
const BOX_HANDLE_OFFSET_SCREEN_PX = 20;
diff --git a/apps/web/src/lib/masks/index.ts b/apps/web/src/masks/index.ts
index 43871b44..3544e239 100644
--- a/apps/web/src/lib/masks/index.ts
+++ b/apps/web/src/masks/index.ts
@@ -1,4 +1,4 @@
-import type { Mask, MaskDefaultContext, MaskType } from "@/lib/masks/types";
+import type { Mask, MaskDefaultContext, MaskType } from "@/masks/types";
import { masksRegistry } from "./registry";
import { generateUUID } from "@/utils/id";
diff --git a/apps/web/src/lib/masks/param-update.ts b/apps/web/src/masks/param-update.ts
index 4f8693eb..e5c7f5b7 100644
--- a/apps/web/src/lib/masks/param-update.ts
+++ b/apps/web/src/masks/param-update.ts
@@ -1,4 +1,4 @@
-import { FEATHER_HANDLE_SCALE, MAX_FEATHER } from "@/lib/masks/feather";
+import { FEATHER_HANDLE_SCALE, MAX_FEATHER } from "@/masks/feather";
export function computeFeatherUpdate({
startFeather,
diff --git a/apps/web/src/lib/masks/registry.ts b/apps/web/src/masks/registry.ts
index ae8ddd20..00b276c8 100644
--- a/apps/web/src/lib/masks/registry.ts
+++ b/apps/web/src/masks/registry.ts
@@ -1,5 +1,5 @@
-import { MAX_FEATHER } from "@/lib/masks/feather";
-import type { ParamDefinition } from "@/lib/params";
+import { MAX_FEATHER } from "@/masks/feather";
+import type { ParamDefinition } from "@/params";
import type {
BaseMaskParams,
MaskDefaultContext,
@@ -8,9 +8,9 @@ import type {
MaskSnapArgs,
MaskSnapResult,
MaskType,
-} from "@/lib/masks/types";
+} from "@/masks/types";
import type { HugeiconsIconProps } from "@hugeicons/react";
-import { DefinitionRegistry } from "@/lib/registry";
+import { DefinitionRegistry } from "@/params/registry";
export type MaskIconProps = {
icon: HugeiconsIconProps["icon"];
diff --git a/apps/web/src/lib/masks/snap.ts b/apps/web/src/masks/snap.ts
index 7396dd82..cff62ebb 100644
--- a/apps/web/src/lib/masks/snap.ts
+++ b/apps/web/src/masks/snap.ts
@@ -1,5 +1,5 @@
-import type { ElementBounds } from "@/lib/preview/element-bounds";
-import { MIN_MASK_DIMENSION } from "@/lib/masks/dimensions";
+import type { ElementBounds } from "@/preview/element-bounds";
+import { MIN_MASK_DIMENSION } from "@/masks/dimensions";
import {
snapPosition,
snapRotation,
@@ -7,8 +7,8 @@ import {
snapScaleAxes,
type ScaleEdgePreference,
type SnapLine,
-} from "@/lib/preview/preview-snap";
-import type { RectangleMaskParams, SplitMaskParams } from "@/lib/masks/types";
+} from "@/preview/preview-snap";
+import type { RectangleMaskParams, SplitMaskParams } from "@/masks/types";
import {
isRectangleMaskParams,
getMaskSnapGeometry,
diff --git a/apps/web/src/lib/masks/types.ts b/apps/web/src/masks/types.ts
index 5a0d126d..4f6c90aa 100644
--- a/apps/web/src/lib/masks/types.ts
+++ b/apps/web/src/masks/types.ts
@@ -1,12 +1,12 @@
-import type { ElementBounds } from "@/lib/preview/element-bounds";
-import type { SnapLine } from "@/lib/preview/preview-snap";
-import type { ParamDefinition } from "@/lib/params";
-import type { CustomMaskPathPoint } from "@/lib/masks/custom-path";
+import type { ElementBounds } from "@/preview/element-bounds";
+import type { SnapLine } from "@/preview/preview-snap";
+import type { ParamDefinition } from "@/params";
+import type { CustomMaskPathPoint } from "@/masks/custom-path";
import type {
TextDecoration,
TextFontStyle,
TextFontWeight,
-} from "@/lib/text/primitives";
+} from "@/text/primitives";
export type MaskType =
| "split"
diff --git a/apps/web/src/hooks/use-mask-handles.ts b/apps/web/src/masks/use-mask-handles.ts
index 917a0677..b81de4f4 100644
--- a/apps/web/src/hooks/use-mask-handles.ts
+++ b/apps/web/src/masks/use-mask-handles.ts
@@ -1,25 +1,25 @@
import { useCallback, useEffect, useRef, useState } from "react";
-import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport";
-import { useEditor } from "@/hooks/use-editor";
+import { usePreviewViewport } from "@/preview/components/preview-viewport";
+import { useEditor } from "@/editor/use-editor";
import { useShiftKey } from "@/hooks/use-shift-key";
-import { masksRegistry } from "@/lib/masks";
+import { masksRegistry } from "@/masks";
import {
parseCustomMaskHandleId,
parseCustomMaskSegmentHandleId,
-} from "@/lib/masks/custom-path";
-import { appendPointToCustomMask } from "@/lib/masks/definitions/custom";
+} from "@/masks/custom-path";
+import { appendPointToCustomMask } from "@/masks/definitions/custom";
import {
getVisibleElementsWithBounds,
type ElementBounds,
-} from "@/lib/preview/element-bounds";
+} from "@/preview/element-bounds";
import {
SNAP_THRESHOLD_SCREEN_PIXELS,
type SnapLine,
-} from "@/lib/preview/preview-snap";
-import type { SelectedMaskPointSelection } from "@/lib/selection/editor-selection";
-import type { Mask, MaskInteractionResult } from "@/lib/masks/types";
-import type { MaskableElement } from "@/lib/timeline";
-import { registerCanceller } from "@/lib/cancel-interaction";
+} from "@/preview/preview-snap";
+import type { SelectedMaskPointSelection } from "@/selection/editor-selection";
+import type { Mask, MaskInteractionResult } from "@/masks/types";
+import type { MaskableElement } from "@/timeline";
+import { registerCanceller } from "@/editor/cancel-interaction";
interface DragState {
trackId: string;
diff --git a/apps/web/src/lib/masks/utils.ts b/apps/web/src/masks/utils.ts
index b1ac2c39..b1ac2c39 100644
--- a/apps/web/src/lib/masks/utils.ts
+++ b/apps/web/src/masks/utils.ts
diff --git a/apps/web/src/lib/media/audio-mastering.ts b/apps/web/src/media/audio-mastering.ts
index f901c3bf..f901c3bf 100644
--- a/apps/web/src/lib/media/audio-mastering.ts
+++ b/apps/web/src/media/audio-mastering.ts
diff --git a/apps/web/src/lib/media/audio.ts b/apps/web/src/media/audio.ts
index 675cc3a5..42756d49 100644
--- a/apps/web/src/lib/media/audio.ts
+++ b/apps/web/src/media/audio.ts
@@ -4,26 +4,26 @@ import type {
LibraryAudioElement,
RetimeConfig,
SceneTracks,
-} from "@/lib/timeline";
-import { shouldMaintainPitch } from "@/lib/retime/rate";
-import type { MediaAsset } from "@/lib/media/types";
-import { applyAudioMasteringToBuffer } from "@/lib/media/audio-mastering";
-import type { AudioCapableElement } from "@/lib/timeline/audio-state";
+} from "@/timeline";
+import { shouldMaintainPitch } from "@/retime/rate";
+import type { MediaAsset } from "@/media/types";
+import { applyAudioMasteringToBuffer } from "@/media/audio-mastering";
+import type { AudioCapableElement } from "@/timeline/audio-state";
import {
hasAnimatedVolume,
resolveEffectiveAudioGain,
-} from "@/lib/timeline/audio-state";
-import { doesElementHaveEnabledAudio } from "@/lib/timeline/audio-separation";
-import { canElementHaveAudio, hasMediaId } from "@/lib/timeline/element-utils";
-import { canTrackHaveAudio } from "@/lib/timeline";
-import { mediaSupportsAudio } from "@/lib/media/media-utils";
-import { getSourceTimeAtClipTime, renderRetimedBuffer } from "@/lib/retime";
+} from "@/timeline/audio-state";
+import { doesElementHaveEnabledAudio } from "@/timeline/audio-separation";
+import { canElementHaveAudio, hasMediaId } from "@/timeline/element-utils";
+import { canTrackHaveAudio } from "@/timeline";
+import { mediaSupportsAudio } from "@/media/media-utils";
+import { getSourceTimeAtClipTime, renderRetimedBuffer } from "@/retime";
import { Input, ALL_FORMATS, BlobSource, AudioBufferSink } from "mediabunny";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
+import { TICKS_PER_SECOND } from "@/wasm";
import {
computeRmsBuckets,
type SampleBucket,
-} from "@/lib/media/waveform-summary";
+} from "@/media/waveform-summary";
const MAX_AUDIO_CHANNELS = 2;
const EXPORT_SAMPLE_RATE = 44100;
diff --git a/apps/web/src/lib/media/media-utils.ts b/apps/web/src/media/media-utils.ts
index dc73da23..dd59bf31 100644
--- a/apps/web/src/lib/media/media-utils.ts
+++ b/apps/web/src/media/media-utils.ts
@@ -1,4 +1,4 @@
-import type { MediaAsset, MediaType } from "@/lib/media/types";
+import type { MediaAsset, MediaType } from "@/media/types";
export const SUPPORTS_AUDIO: readonly MediaType[] = ["audio", "video"];
diff --git a/apps/web/src/lib/media/mediabunny.ts b/apps/web/src/media/mediabunny.ts
index 1e97fc42..47ffaeb1 100644
--- a/apps/web/src/lib/media/mediabunny.ts
+++ b/apps/web/src/media/mediabunny.ts
@@ -1,8 +1,8 @@
import { Input, ALL_FORMATS, BlobSource } from "mediabunny";
-import { createTimelineAudioBuffer } from "@/lib/media/audio";
-import type { SceneTracks } from "@/lib/timeline";
-import type { MediaAsset } from "@/lib/media/types";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
+import { createTimelineAudioBuffer } from "@/media/audio";
+import type { SceneTracks } from "@/timeline";
+import type { MediaAsset } from "@/media/types";
+import { TICKS_PER_SECOND } from "@/wasm";
export async function getVideoInfo({
videoFile,
diff --git a/apps/web/src/lib/media/processing.ts b/apps/web/src/media/processing.ts
index 547e7ed8..d8d483f8 100644
--- a/apps/web/src/lib/media/processing.ts
+++ b/apps/web/src/media/processing.ts
@@ -1,9 +1,9 @@
import { Input, ALL_FORMATS, BlobSource, VideoSampleSink } from "mediabunny";
import { toast } from "sonner";
-import { getMediaTypeFromFile } from "@/lib/media/media-utils";
+import { getMediaTypeFromFile } from "@/media/media-utils";
import { formatStorageBytes } from "@/services/storage/quota";
import { storageService } from "@/services/storage/service";
-import type { MediaAsset } from "@/lib/media/types";
+import type { MediaAsset } from "@/media/types";
import { getVideoInfo } from "./mediabunny";
export interface ProcessedMediaAsset extends Omit<MediaAsset, "id"> {}
diff --git a/apps/web/src/lib/media/types.ts b/apps/web/src/media/types.ts
index 4d3e4106..4d3e4106 100644
--- a/apps/web/src/lib/media/types.ts
+++ b/apps/web/src/media/types.ts
diff --git a/apps/web/src/lib/media/upload-toast.ts b/apps/web/src/media/upload-toast.ts
index 45846206..45846206 100644
--- a/apps/web/src/lib/media/upload-toast.ts
+++ b/apps/web/src/media/upload-toast.ts
diff --git a/apps/web/src/hooks/use-file-upload.ts b/apps/web/src/media/use-file-upload.ts
index d174e002..1ee30ad9 100644
--- a/apps/web/src/hooks/use-file-upload.ts
+++ b/apps/web/src/media/use-file-upload.ts
@@ -1,5 +1,5 @@
import { useState, useRef } from "react";
-import { hasDragData } from "@/lib/drag-data";
+import { hasDragData } from "@/timeline/drag-data";
interface UseFileUploadOptions {
accept?: string;
diff --git a/apps/web/src/hooks/use-paste-media.ts b/apps/web/src/media/use-paste-media.ts
index 95020a04..3055920e 100644
--- a/apps/web/src/hooks/use-paste-media.ts
+++ b/apps/web/src/media/use-paste-media.ts
@@ -1,15 +1,15 @@
import { useEffect } from "react";
-import { useEditor } from "@/hooks/use-editor";
-import { processMediaAssets } from "@/lib/media/processing";
-import { showMediaUploadToast } from "@/lib/media/upload-toast";
-import { buildElementFromMedia } from "@/lib/timeline/element-utils";
-import { AddMediaAssetCommand } from "@/lib/commands/media";
-import { InsertElementCommand } from "@/lib/commands/timeline";
-import { BatchCommand } from "@/lib/commands";
-import { DEFAULT_NEW_ELEMENT_DURATION } from "@/lib/timeline/creation";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
+import { useEditor } from "@/editor/use-editor";
+import { processMediaAssets } from "@/media/processing";
+import { showMediaUploadToast } from "@/media/upload-toast";
+import { buildElementFromMedia } from "@/timeline/element-utils";
+import { AddMediaAssetCommand } from "@/commands/media";
+import { InsertElementCommand } from "@/commands/timeline";
+import { BatchCommand } from "@/commands";
+import { DEFAULT_NEW_ELEMENT_DURATION } from "@/timeline/creation";
+import { TICKS_PER_SECOND } from "@/wasm";
import { isTypableDOMElement } from "@/utils/browser";
-import type { MediaType } from "@/lib/media/types";
+import type { MediaType } from "@/media/types";
const MEDIA_MIME_PREFIXES: MediaType[] = ["image", "video", "audio"];
diff --git a/apps/web/src/lib/media/waveform-summary.ts b/apps/web/src/media/waveform-summary.ts
index 522d6e04..03a0059c 100644
--- a/apps/web/src/lib/media/waveform-summary.ts
+++ b/apps/web/src/media/waveform-summary.ts
@@ -1,7 +1,7 @@
"use client";
-import { getSourceTimeAtClipTime } from "@/lib/retime";
-import type { RetimeConfig } from "@/lib/timeline";
+import { getSourceTimeAtClipTime } from "@/retime";
+import type { RetimeConfig } from "@/timeline";
const RMS_ANALYSIS_WINDOW_SECONDS = 0.02;
const DEFAULT_SOURCE_WAVEFORM_BUCKET_SIZE = 128;
diff --git a/apps/web/src/lib/panels/layout.ts b/apps/web/src/panels/layout.ts
index 5a570469..5a570469 100644
--- a/apps/web/src/lib/panels/layout.ts
+++ b/apps/web/src/panels/layout.ts
diff --git a/apps/web/src/lib/params.ts b/apps/web/src/params/index.ts
index 633bfbd0..633bfbd0 100644
--- a/apps/web/src/lib/params.ts
+++ b/apps/web/src/params/index.ts
diff --git a/apps/web/src/lib/registry.ts b/apps/web/src/params/registry.ts
index f1c92f67..d9fcc266 100644
--- a/apps/web/src/lib/registry.ts
+++ b/apps/web/src/params/registry.ts
@@ -1,4 +1,4 @@
-import type { ParamDefinition, ParamValues } from "@/lib/params";
+import type { ParamDefinition, ParamValues } from "@/params";
export function buildDefaultParamValues(
params: ParamDefinition[],
diff --git a/apps/web/src/components/editor/panels/preview/context-menu.tsx b/apps/web/src/preview/components/context-menu.tsx
index 677f35db..fca55d71 100644
--- a/apps/web/src/components/editor/panels/preview/context-menu.tsx
+++ b/apps/web/src/preview/components/context-menu.tsx
@@ -1,73 +1,85 @@
-"use client";
-
-import {
- ContextMenuCheckboxItem,
- ContextMenuContent,
- ContextMenuItem,
- ContextMenuSeparator,
-} from "@/components/ui/context-menu";
-import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport";
-import { useEditor } from "@/hooks/use-editor";
-import { usePreviewStore } from "@/stores/preview-store";
-import { toast } from "sonner";
-
-export function PreviewContextMenu({
- onToggleFullscreen,
- containerRef,
-}: {
- onToggleFullscreen: () => void;
- containerRef: React.RefObject<HTMLElement | null>;
-}) {
- const editor = useEditor();
- const viewport = usePreviewViewport();
- const { overlays, setOverlayVisibility } = usePreviewStore();
-
- const handleCopySnapshot = async () => {
- const result = await editor.renderer.copySnapshot();
-
- if (!result.success) {
- toast.error("Failed to copy snapshot", {
- description: result.error ?? "Please try again",
- });
- return;
- }
- };
-
- const handleSaveSnapshot = async () => {
- const result = await editor.renderer.saveSnapshot();
-
- if (!result.success) {
- toast.error("Failed to save snapshot", {
- description: result.error ?? "Please try again",
- });
- return;
- }
- };
-
- return (
- <ContextMenuContent className="w-56" container={containerRef.current}>
- <ContextMenuItem onClick={viewport.fitToScreen} inset>
- Fit to screen
- </ContextMenuItem>
- <ContextMenuSeparator />
- <ContextMenuItem onClick={onToggleFullscreen} inset>
- Full screen
- </ContextMenuItem>
- <ContextMenuItem onClick={handleSaveSnapshot} inset>
- Save snapshot
- </ContextMenuItem>
- <ContextMenuItem onClick={handleCopySnapshot} inset>
- Copy snapshot
- </ContextMenuItem>
- <ContextMenuSeparator />
- <ContextMenuCheckboxItem
- checked={overlays.bookmarks}
- onCheckedChange={(checked) =>
- setOverlayVisibility({ overlay: "bookmarks", isVisible: !!checked })
- }
- >
- Show bookmark notes
- </ContextMenuCheckboxItem>
- </ContextMenuContent>
- );
-}
+"use client";
+
+import {
+ ContextMenuCheckboxItem,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuSeparator,
+} from "@/components/ui/context-menu";
+import { usePreviewViewport } from "@/preview/components/preview-viewport";
+import { useEditor } from "@/editor/use-editor";
+import type { PreviewOverlayControl } from "@/preview/overlays";
+import { toast } from "sonner";
+
+export function PreviewContextMenu({
+ onToggleFullscreen,
+ containerRef,
+ overlayControls,
+ onOverlayVisibilityChange,
+}: {
+ onToggleFullscreen: () => void;
+ containerRef: React.RefObject<HTMLElement | null>;
+ overlayControls: PreviewOverlayControl[];
+ onOverlayVisibilityChange: (params: {
+ overlayId: string;
+ isVisible: boolean;
+ }) => void;
+}) {
+ const editor = useEditor();
+ const viewport = usePreviewViewport();
+
+ const handleCopySnapshot = async () => {
+ const result = await editor.renderer.copySnapshot();
+
+ if (!result.success) {
+ toast.error("Failed to copy snapshot", {
+ description: result.error ?? "Please try again",
+ });
+ return;
+ }
+ };
+
+ const handleSaveSnapshot = async () => {
+ const result = await editor.renderer.saveSnapshot();
+
+ if (!result.success) {
+ toast.error("Failed to save snapshot", {
+ description: result.error ?? "Please try again",
+ });
+ return;
+ }
+ };
+
+ return (
+ <ContextMenuContent className="w-56" container={containerRef.current}>
+ <ContextMenuItem onClick={viewport.fitToScreen} inset>
+ Fit to screen
+ </ContextMenuItem>
+ <ContextMenuSeparator />
+ <ContextMenuItem onClick={onToggleFullscreen} inset>
+ Full screen
+ </ContextMenuItem>
+ <ContextMenuItem onClick={handleSaveSnapshot} inset>
+ Save snapshot
+ </ContextMenuItem>
+ <ContextMenuItem onClick={handleCopySnapshot} inset>
+ Copy snapshot
+ </ContextMenuItem>
+ {overlayControls.length > 0 ? <ContextMenuSeparator /> : null}
+ {overlayControls.map((overlayControl) => (
+ <ContextMenuCheckboxItem
+ key={overlayControl.id}
+ checked={overlayControl.isVisible}
+ onCheckedChange={(checked) =>
+ onOverlayVisibilityChange({
+ overlayId: overlayControl.id,
+ isVisible: !!checked,
+ })
+ }
+ >
+ {overlayControl.label}
+ </ContextMenuCheckboxItem>
+ ))}
+ </ContextMenuContent>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/preview/cursors.ts b/apps/web/src/preview/components/cursors.ts
index f04dbef0..f04dbef0 100644
--- a/apps/web/src/components/editor/panels/preview/cursors.ts
+++ b/apps/web/src/preview/components/cursors.ts
diff --git a/apps/web/src/components/editor/panels/preview/guide-popover.tsx b/apps/web/src/preview/components/guide-popover.tsx
index bcac8075..5bfa92ce 100644
--- a/apps/web/src/components/editor/panels/preview/guide-popover.tsx
+++ b/apps/web/src/preview/components/guide-popover.tsx
@@ -1,8 +1,8 @@
"use client";
import { AnimatePresence, motion } from "motion/react";
-import { GUIDE_REGISTRY, getGuideById } from "@/lib/guides";
-import { usePreviewStore } from "@/stores/preview-store";
+import { GUIDE_REGISTRY, getGuideById } from "@/guides";
+import { usePreviewStore } from "@/preview/preview-store";
import {
Popover,
PopoverContent,
diff --git a/apps/web/src/components/editor/panels/preview/handle-primitives.tsx b/apps/web/src/preview/components/handle-primitives.tsx
index 33baf9fc..33baf9fc 100644
--- a/apps/web/src/components/editor/panels/preview/handle-primitives.tsx
+++ b/apps/web/src/preview/components/handle-primitives.tsx
diff --git a/apps/web/src/components/editor/panels/preview/index.tsx b/apps/web/src/preview/components/index.tsx
index 369d854c..d3cb2a49 100644
--- a/apps/web/src/components/editor/panels/preview/index.tsx
+++ b/apps/web/src/preview/components/index.tsx
@@ -2,19 +2,21 @@
import { useCallback, useEffect, useMemo, useRef } from "react";
import useDeepCompareEffect from "use-deep-compare-effect";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import { useRafLoop } from "@/hooks/use-raf-loop";
import { useContainerSize } from "@/hooks/use-container-size";
import { useFullscreen } from "@/hooks/use-fullscreen";
import { CanvasRenderer } from "@/services/renderer/canvas-renderer";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
+import { TICKS_PER_SECOND } from "@/wasm";
import type { RootNode } from "@/services/renderer/nodes/root-node";
import { buildScene } from "@/services/renderer/scene-builder";
+import { PreviewOverlayLayer } from "./overlay-layer";
import { PreviewInteractionOverlay } from "./preview-interaction-overlay";
-import { BookmarkNoteOverlay } from "./bookmark-note-overlay";
-import { GuideOverlay } from "./guide-overlay";
import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu";
-import { usePreviewStore } from "@/stores/preview-store";
+import type {
+ PreviewOverlayControl,
+ PreviewOverlayInstance,
+} from "@/preview/overlays";
import { PreviewContextMenu } from "./context-menu";
import { PreviewToolbar } from "./toolbar";
import {
@@ -53,7 +55,18 @@ function normalizeWheelDelta({
return delta;
}
-export function PreviewPanel() {
+export function PreviewPanel({
+ overlayControls,
+ overlayInstances,
+ onOverlayVisibilityChange,
+}: {
+ overlayControls: PreviewOverlayControl[];
+ overlayInstances: PreviewOverlayInstance[];
+ onOverlayVisibilityChange: (params: {
+ overlayId: string;
+ isVisible: boolean;
+ }) => void;
+}) {
const containerRef = useRef<HTMLDivElement>(null);
const { toggleFullscreen } = useFullscreen({ containerRef });
@@ -65,6 +78,9 @@ export function PreviewPanel() {
<PreviewCanvas
containerRef={containerRef}
onToggleFullscreen={toggleFullscreen}
+ overlayControls={overlayControls}
+ overlayInstances={overlayInstances}
+ onOverlayVisibilityChange={onOverlayVisibilityChange}
/>
<RenderTreeController />
</div>
@@ -103,9 +119,18 @@ function RenderTreeController() {
function PreviewCanvas({
containerRef,
onToggleFullscreen,
+ overlayControls,
+ overlayInstances,
+ onOverlayVisibilityChange,
}: {
containerRef: React.RefObject<HTMLElement | null>;
onToggleFullscreen: () => void;
+ overlayControls: PreviewOverlayControl[];
+ overlayInstances: PreviewOverlayInstance[];
+ onOverlayVisibilityChange: (params: {
+ overlayId: string;
+ isVisible: boolean;
+ }) => void;
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const viewportRef = useRef<HTMLDivElement>(null);
@@ -117,7 +142,6 @@ function PreviewCanvas({
const editor = useEditor();
const activeProject = useEditor((e) => e.project.getActive());
const renderTree = useEditor((e) => e.renderer.getRenderTree());
- const { overlays } = usePreviewStore();
const viewport = usePreviewViewportState({
canvasHeight: nativeHeight,
canvasWidth: nativeWidth,
@@ -132,16 +156,18 @@ function PreviewCanvas({
height: nativeHeight,
fps: activeProject.settings.fps,
});
- }, [nativeWidth, nativeHeight, activeProject.settings.fps.numerator, activeProject.settings.fps.denominator]);
+ }, [nativeWidth, nativeHeight, activeProject.settings.fps]);
const render = useCallback(() => {
if (canvasRef.current && renderTree && !renderingRef.current) {
- const renderTime = Math.min(
- editor.playback.getCurrentTime(),
- editor.timeline.getLastFrameTime(),
- );
- const ticksPerFrame = Math.round(TICKS_PER_SECOND * renderer.fps.denominator / renderer.fps.numerator);
- const frame = Math.floor(renderTime / ticksPerFrame);
+ const renderTime = Math.min(
+ editor.playback.getCurrentTime(),
+ editor.timeline.getLastFrameTime(),
+ );
+ const ticksPerFrame = Math.round(
+ (TICKS_PER_SECOND * renderer.fps.denominator) / renderer.fps.numerator,
+ );
+ const frame = Math.floor(renderTime / ticksPerFrame);
if (
frame !== lastFrameRef.current ||
@@ -161,7 +187,7 @@ function PreviewCanvas({
});
}
}
- }, [renderer, renderTree, editor.playback]);
+ }, [renderer, renderTree, editor.playback, editor.timeline]);
useRafLoop(render);
@@ -276,14 +302,22 @@ function PreviewCanvas({
: activeProject?.settings.background.color,
}}
/>
- <GuideOverlay />
+ <PreviewOverlayLayer
+ instances={overlayInstances}
+ plane="under-interaction"
+ />
<PreviewInteractionOverlay />
- {overlays.bookmarks && <BookmarkNoteOverlay />}
+ <PreviewOverlayLayer
+ instances={overlayInstances}
+ plane="over-interaction"
+ />
</div>
</ContextMenuTrigger>
<PreviewContextMenu
onToggleFullscreen={onToggleFullscreen}
containerRef={containerRef}
+ overlayControls={overlayControls}
+ onOverlayVisibilityChange={onOverlayVisibilityChange}
/>
</ContextMenu>
</div>
diff --git a/apps/web/src/components/editor/panels/preview/mask-handles.tsx b/apps/web/src/preview/components/mask-handles.tsx
index d32b6ff9..340cab0a 100644
--- a/apps/web/src/components/editor/panels/preview/mask-handles.tsx
+++ b/apps/web/src/preview/components/mask-handles.tsx
@@ -1,9 +1,9 @@
"use client";
-import { PEN_CURSOR } from "@/components/editor/panels/preview/cursors";
-import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport";
-import { useMaskHandles } from "@/hooks/use-mask-handles";
-import type { SnapLine } from "@/lib/preview/preview-snap";
+import { PEN_CURSOR } from "@/preview/components/cursors";
+import { usePreviewViewport } from "@/preview/components/preview-viewport";
+import { useMaskHandles } from "@/masks/use-mask-handles";
+import type { SnapLine } from "@/preview/preview-snap";
import {
CornerHandle,
CircleHandle,
diff --git a/apps/web/src/preview/components/overlay-layer.tsx b/apps/web/src/preview/components/overlay-layer.tsx
new file mode 100644
index 00000000..eb730f6e
--- /dev/null
+++ b/apps/web/src/preview/components/overlay-layer.tsx
@@ -0,0 +1,197 @@
+"use client";
+
+import type {
+ PreviewOverlayHudAnchor,
+ PreviewOverlayInstance,
+ PreviewOverlayPlane,
+} from "@/preview/overlays";
+import { usePreviewViewport } from "./preview-viewport";
+
+const HUD_ANCHOR_CLASS_NAMES: Record<PreviewOverlayHudAnchor, string> = {
+ "top-left": "absolute top-2 left-2 flex flex-col items-start gap-1.5",
+ "top-right": "absolute top-2 right-2 flex flex-col items-end gap-1.5",
+ "bottom-left":
+ "absolute bottom-2 left-2 flex flex-col-reverse items-start gap-1.5",
+ "bottom-right":
+ "absolute right-2 bottom-2 flex flex-col-reverse items-end gap-1.5",
+};
+
+function getPositionedMountStyle({
+ instance,
+ baseStyle,
+ sceneLeft,
+ sceneTop,
+ sceneWidth,
+ sceneHeight,
+}: {
+ instance: PreviewOverlayInstance;
+ baseStyle: {
+ pointerEvents: "none" | "auto";
+ zIndex: number | undefined;
+ };
+ sceneLeft: number;
+ sceneTop: number;
+ sceneWidth: number;
+ sceneHeight: number;
+}) {
+ if (instance.mount.kind === "hud") {
+ return baseStyle;
+ }
+
+ const isSceneMount = instance.mount.kind === "scene";
+ const mount = instance.mount;
+ const hasExplicitBounds =
+ mount.x != null ||
+ mount.y != null ||
+ mount.width != null ||
+ mount.height != null;
+
+ return {
+ ...baseStyle,
+ left: (isSceneMount ? sceneLeft : 0) + (mount.x ?? 0),
+ top: (isSceneMount ? sceneTop : 0) + (mount.y ?? 0),
+ ...(hasExplicitBounds
+ ? {
+ ...(mount.width != null ? { width: mount.width } : {}),
+ ...(mount.height != null ? { height: mount.height } : {}),
+ }
+ : {
+ width: isSceneMount ? sceneWidth : "100%",
+ height: isSceneMount ? sceneHeight : "100%",
+ }),
+ } as const;
+}
+
+export function PreviewOverlayLayer({
+ instances,
+ plane,
+}: {
+ instances: PreviewOverlayInstance[];
+ plane: PreviewOverlayPlane;
+}) {
+ const viewport = usePreviewViewport();
+ const visibleInstances = instances
+ .filter((instance) => (instance.plane ?? "over-interaction") === plane)
+ .sort((left, right) => (left.zIndex ?? 0) - (right.zIndex ?? 0));
+
+ if (visibleInstances.length === 0) {
+ return null;
+ }
+
+ const hudInstancesByAnchor = visibleInstances.reduce<
+ Record<PreviewOverlayHudAnchor, PreviewOverlayInstance[]>
+ >(
+ (groups, instance) => {
+ if (instance.mount.kind === "hud") {
+ groups[instance.mount.anchor].push(instance);
+ }
+ return groups;
+ },
+ {
+ "top-left": [],
+ "top-right": [],
+ "bottom-left": [],
+ "bottom-right": [],
+ },
+ );
+
+ return (
+ <>
+ {visibleInstances.map((instance) => {
+ if (instance.mount.kind === "hud") {
+ return null;
+ }
+
+ const pointerEvents = instance.pointerEvents ?? "none";
+ const baseStyle = {
+ pointerEvents,
+ zIndex: instance.zIndex,
+ } as const;
+ const content = instance.render({
+ sceneHeight: viewport.sceneHeight,
+ sceneWidth: viewport.sceneWidth,
+ });
+
+ switch (instance.mount.kind) {
+ case "scene":
+ return (
+ <div
+ key={instance.id}
+ className="absolute"
+ style={getPositionedMountStyle({
+ instance,
+ baseStyle,
+ sceneLeft: viewport.sceneLeft,
+ sceneTop: viewport.sceneTop,
+ sceneWidth: viewport.sceneWidth,
+ sceneHeight: viewport.sceneHeight,
+ })}
+ >
+ {content}
+ </div>
+ );
+
+ case "viewport":
+ return (
+ <div
+ key={instance.id}
+ className="absolute"
+ style={getPositionedMountStyle({
+ instance,
+ baseStyle,
+ sceneLeft: viewport.sceneLeft,
+ sceneTop: viewport.sceneTop,
+ sceneWidth: viewport.sceneWidth,
+ sceneHeight: viewport.sceneHeight,
+ })}
+ >
+ {content}
+ </div>
+ );
+
+ default:
+ return null;
+ }
+ })}
+ {(
+ Object.entries(hudInstancesByAnchor) as Array<
+ [PreviewOverlayHudAnchor, PreviewOverlayInstance[]]
+ >
+ ).map(([anchor, anchorInstances]) => {
+ if (anchorInstances.length === 0) {
+ return null;
+ }
+
+ const sortedAnchorInstances = [...anchorInstances].sort(
+ (left, right) =>
+ (left.mount.kind === "hud" ? (left.mount.order ?? 0) : 0) -
+ (right.mount.kind === "hud" ? (right.mount.order ?? 0) : 0),
+ );
+
+ return (
+ <div key={anchor} className={HUD_ANCHOR_CLASS_NAMES[anchor]}>
+ {sortedAnchorInstances.map((instance) => {
+ const pointerEvents = instance.pointerEvents ?? "none";
+ const content = instance.render({
+ sceneHeight: viewport.sceneHeight,
+ sceneWidth: viewport.sceneWidth,
+ });
+
+ return (
+ <div
+ key={instance.id}
+ style={{
+ pointerEvents,
+ zIndex: instance.zIndex,
+ }}
+ >
+ {content}
+ </div>
+ );
+ })}
+ </div>
+ );
+ })}
+ </>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/preview/preview-interaction-overlay.tsx b/apps/web/src/preview/components/preview-interaction-overlay.tsx
index 6ee4b584..e16a31d2 100644
--- a/apps/web/src/components/editor/panels/preview/preview-interaction-overlay.tsx
+++ b/apps/web/src/preview/components/preview-interaction-overlay.tsx
@@ -1,13 +1,13 @@
import { useState } from "react";
-import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport";
-import { usePreviewInteraction } from "@/hooks/use-preview-interaction";
-import type { SnapLine } from "@/lib/preview/preview-snap";
+import { usePreviewViewport } from "@/preview/components/preview-viewport";
+import { usePreviewInteraction } from "@/preview/hooks/use-preview-interaction";
+import type { SnapLine } from "@/preview/preview-snap";
import { TransformHandles } from "./transform-handles";
import { MaskHandles } from "./mask-handles";
import { SnapGuides } from "./snap-guides";
import { TextEditOverlay } from "./text-edit-overlay";
-import { usePropertiesStore } from "../properties/stores/properties-store";
-import { useEditor } from "@/hooks/use-editor";
+import { usePropertiesStore } from "@/components/editor/panels/properties/stores/properties-store";
+import { useEditor } from "@/editor/use-editor";
export function PreviewInteractionOverlay() {
const [snapLines, setSnapLines] = useState<SnapLine[]>([]);
diff --git a/apps/web/src/components/editor/panels/preview/preview-viewport.tsx b/apps/web/src/preview/components/preview-viewport.tsx
index 2ac6bd92..6dc9f084 100644
--- a/apps/web/src/components/editor/panels/preview/preview-viewport.tsx
+++ b/apps/web/src/preview/components/preview-viewport.tsx
@@ -15,9 +15,9 @@ import {
positionToOverlay,
screenPixelsToLogicalThreshold,
screenToCanvas,
-} from "@/lib/preview/preview-coords";
+} from "@/preview/preview-coords";
import { clamp, isNearlyEqual } from "@/utils/math";
-import { PREVIEW_ZOOM } from "@/lib/preview/zoom";
+import { PREVIEW_ZOOM } from "@/preview/zoom";
const MIDDLE_MOUSE_BUTTON = 1;
const IS_AT_FIT_EPSILON = 0.001;
diff --git a/apps/web/src/components/editor/panels/preview/snap-guides.tsx b/apps/web/src/preview/components/snap-guides.tsx
index ced5e921..5c2e1b9f 100644
--- a/apps/web/src/components/editor/panels/preview/snap-guides.tsx
+++ b/apps/web/src/preview/components/snap-guides.tsx
@@ -1,7 +1,7 @@
"use client";
-import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport";
-import type { SnapLine } from "@/lib/preview/preview-snap";
+import { usePreviewViewport } from "@/preview/components/preview-viewport";
+import type { SnapLine } from "@/preview/preview-snap";
export function SnapGuides({ lines }: { lines: SnapLine[] }) {
const viewport = usePreviewViewport();
diff --git a/apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx b/apps/web/src/preview/components/text-edit-overlay.tsx
index eabbde8a..bc27c724 100644
--- a/apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx
+++ b/apps/web/src/preview/components/text-edit-overlay.tsx
@@ -1,15 +1,15 @@
"use client";
import { useCallback, useEffect, useRef } from "react";
-import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport";
-import { useEditor } from "@/hooks/use-editor";
-import type { TextElement } from "@/lib/timeline";
-import { DEFAULTS } from "@/lib/timeline/defaults";
+import { usePreviewViewport } from "@/preview/components/preview-viewport";
+import { useEditor } from "@/editor/use-editor";
+import type { TextElement } from "@/timeline";
+import { DEFAULTS } from "@/timeline/defaults";
import {
getElementLocalTime,
resolveTransformAtTime,
-} from "@/lib/animation";
-import { resolveTextLayout } from "@/lib/text/primitives";
+} from "@/animation";
+import { resolveTextLayout } from "@/text/primitives";
export function TextEditOverlay({
trackId,
diff --git a/apps/web/src/components/editor/panels/preview/toolbar.tsx b/apps/web/src/preview/components/toolbar.tsx
index 9ee6c822..b44e42ef 100644
--- a/apps/web/src/components/editor/panels/preview/toolbar.tsx
+++ b/apps/web/src/preview/components/toolbar.tsx
@@ -1,9 +1,9 @@
"use client";
import { useState, useEffect } from "react";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import { formatTimecode } from "opencut-wasm";
-import { invokeAction } from "@/lib/actions";
+import { invokeAction } from "@/actions";
import { EditableTimecode } from "@/components/editable-timecode";
import { Button } from "@/components/ui/button";
import {
@@ -13,7 +13,7 @@ import {
PlayIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
-import { getGuideById } from "@/lib/guides";
+import { getGuideById } from "@/guides";
import { Separator } from "@/components/ui/separator";
import {
Select,
@@ -22,10 +22,10 @@ import {
SelectItem,
SelectSeparator,
} from "@/components/ui/select";
-import { PREVIEW_ZOOM_PRESETS } from "@/lib/preview/zoom";
+import { PREVIEW_ZOOM_PRESETS } from "@/preview/zoom";
import { usePreviewViewport } from "./preview-viewport";
import { GridPopover } from "./guide-popover";
-import { usePreviewStore } from "@/stores/preview-store";
+import { usePreviewStore } from "@/preview/preview-store";
export function PreviewToolbar({
onToggleFullscreen,
diff --git a/apps/web/src/components/editor/panels/preview/transform-handles.tsx b/apps/web/src/preview/components/transform-handles.tsx
index 2b81b557..ad496cbc 100644
--- a/apps/web/src/components/editor/panels/preview/transform-handles.tsx
+++ b/apps/web/src/preview/components/transform-handles.tsx
@@ -1,16 +1,16 @@
"use client";
-import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport";
-import { useTransformHandles } from "@/hooks/use-transform-handles";
-import { isVisualElement } from "@/lib/timeline/element-utils";
+import { usePreviewViewport } from "@/preview/components/preview-viewport";
+import { useTransformHandles } from "@/preview/hooks/use-transform-handles";
+import { isVisualElement } from "@/timeline/element-utils";
import {
getCornerPosition,
getEdgeHandlePosition,
ROTATION_HANDLE_OFFSET,
type Corner,
type Edge,
-} from "@/lib/preview/element-bounds";
-import type { OnSnapLinesChange } from "@/hooks/use-preview-interaction";
+} from "@/preview/element-bounds";
+import type { OnSnapLinesChange } from "@/preview/hooks/use-preview-interaction";
import {
BoundingBoxOutline,
CornerHandle,
diff --git a/apps/web/src/lib/preview/element-bounds.ts b/apps/web/src/preview/element-bounds.ts
index 39595c33..ac556181 100644
--- a/apps/web/src/lib/preview/element-bounds.ts
+++ b/apps/web/src/preview/element-bounds.ts
@@ -1,12 +1,12 @@
-import type { SceneTracks, TimelineElement } from "@/lib/timeline";
-import type { MediaAsset } from "@/lib/media/types";
-import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/lib/stickers/intrinsic-size";
-import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/lib/graphics";
-import { measureTextElement } from "@/lib/text/measure-element";
+import type { SceneTracks, TimelineElement } from "@/timeline";
+import type { MediaAsset } from "@/media/types";
+import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/stickers/intrinsic-size";
+import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/graphics";
+import { measureTextElement } from "@/text/measure-element";
import {
getElementLocalTime,
resolveTransformAtTime,
-} from "@/lib/animation";
+} from "@/animation";
export interface ElementBounds {
cx: number;
diff --git a/apps/web/src/lib/preview/hit-test.ts b/apps/web/src/preview/hit-test.ts
index 2552922b..0f5fd45e 100644
--- a/apps/web/src/lib/preview/hit-test.ts
+++ b/apps/web/src/preview/hit-test.ts
@@ -1,5 +1,5 @@
import type { ElementWithBounds } from "./element-bounds";
-import type { ElementRef } from "@/lib/timeline/types";
+import type { ElementRef } from "@/timeline/types";
function pointInRotatedRect({
px,
diff --git a/apps/web/src/hooks/use-preview-interaction.ts b/apps/web/src/preview/hooks/use-preview-interaction.ts
index cfb3d527..78212e74 100644
--- a/apps/web/src/hooks/use-preview-interaction.ts
+++ b/apps/web/src/preview/hooks/use-preview-interaction.ts
@@ -1,25 +1,25 @@
import { useCallback, useEffect, useRef, useState } from "react";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import { useShiftKey } from "@/hooks/use-shift-key";
-import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport";
-import type { Transform } from "@/lib/rendering";
-import type { ElementRef, TextElement } from "@/lib/timeline";
+import { usePreviewViewport } from "@/preview/components/preview-viewport";
+import type { Transform } from "@/rendering";
+import type { ElementRef, TextElement } from "@/timeline";
import {
getVisibleElementsWithBounds,
type ElementWithBounds,
-} from "@/lib/preview/element-bounds";
+} from "@/preview/element-bounds";
import {
getHitElements,
hitTest,
resolvePreferredHit,
-} from "@/lib/preview/hit-test";
-import { isVisualElement } from "@/lib/timeline/element-utils";
+} from "@/preview/hit-test";
+import { isVisualElement } from "@/timeline/element-utils";
import {
SNAP_THRESHOLD_SCREEN_PIXELS,
snapPosition,
type SnapLine,
-} from "@/lib/preview/preview-snap";
-import { registerCanceller } from "@/lib/cancel-interaction";
+} from "@/preview/preview-snap";
+import { registerCanceller } from "@/editor/cancel-interaction";
export type OnSnapLinesChange = (lines: SnapLine[]) => void;
diff --git a/apps/web/src/hooks/use-transform-handles.ts b/apps/web/src/preview/hooks/use-transform-handles.ts
index 7495a387..030e026c 100644
--- a/apps/web/src/hooks/use-transform-handles.ts
+++ b/apps/web/src/preview/hooks/use-transform-handles.ts
@@ -1,12 +1,12 @@
import { useCallback, useEffect, useRef, useState } from "react";
-import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport";
-import type { OnSnapLinesChange } from "@/hooks/use-preview-interaction";
-import { useEditor } from "@/hooks/use-editor";
+import { usePreviewViewport } from "@/preview/components/preview-viewport";
+import type { OnSnapLinesChange } from "@/preview/hooks/use-preview-interaction";
+import { useEditor } from "@/editor/use-editor";
import { useShiftKey } from "@/hooks/use-shift-key";
import {
getVisibleElementsWithBounds,
type ElementWithBounds,
-} from "@/lib/preview/element-bounds";
+} from "@/preview/element-bounds";
import {
MIN_SCALE,
SNAP_THRESHOLD_SCREEN_PIXELS,
@@ -15,27 +15,23 @@ import {
snapScaleAxes,
type ScaleEdgePreference,
type SnapLine,
-} from "@/lib/preview/preview-snap";
-import { isVisualElement } from "@/lib/timeline/element-utils";
+} from "@/preview/preview-snap";
+import { isVisualElement } from "@/timeline/element-utils";
import {
getElementLocalTime,
hasKeyframesForPath,
resolveTransformAtTime,
setChannel,
-} from "@/lib/animation";
-import type { Transform } from "@/lib/rendering";
-import type { ElementAnimations } from "@/lib/animation/types";
-import { registerCanceller } from "@/lib/cancel-interaction";
+} from "@/animation";
+import type { Transform } from "@/rendering";
+import type { ElementAnimations } from "@/animation/types";
+import { registerCanceller } from "@/editor/cancel-interaction";
type Corner = "top-left" | "top-right" | "bottom-left" | "bottom-right";
type Edge = "right" | "left" | "bottom";
type HandleType = Corner | Edge | "rotation";
-function getPreferredEdge({
- edge,
-}: {
- edge: Edge;
-}): ScaleEdgePreference {
+function getPreferredEdge({ edge }: { edge: Edge }): ScaleEdgePreference {
return edge === "right"
? { right: true }
: edge === "left"
@@ -191,7 +187,12 @@ export function useTransformHandles({
releaseCapturedPointer();
},
});
- }, [activeHandle, clearActiveHandleState, editor.timeline, releaseCapturedPointer]);
+ }, [
+ activeHandle,
+ clearActiveHandleState,
+ editor.timeline,
+ releaseCapturedPointer,
+ ]);
const handleCornerPointerDown = useCallback(
({ event, corner }: { event: React.PointerEvent; corner: Corner }) => {
@@ -333,11 +334,10 @@ export function useTransformHandles({
edge === "right" || edge === "left"
? "transform.scaleX"
: "transform.scaleY";
- const shouldClearScaleAnimation =
- hasKeyframesForPath({
- animations: element.animations,
- propertyPath,
- });
+ const shouldClearScaleAnimation = hasKeyframesForPath({
+ animations: element.animations,
+ propertyPath,
+ });
const animationsWithoutScale = shouldClearScaleAnimation
? setChannel({
animations: element.animations,
@@ -605,18 +605,16 @@ export function useTransformHandles({
);
const handlePointerUp = useCallback(() => {
- if (
- scaleStateRef.current ||
- rotationStateRef.current ||
- edgeScaleStateRef.current
- ) {
- editor.timeline.commitPreview();
- clearActiveHandleState();
- }
- releaseCapturedPointer();
- },
- [clearActiveHandleState, editor, releaseCapturedPointer],
- );
+ if (
+ scaleStateRef.current ||
+ rotationStateRef.current ||
+ edgeScaleStateRef.current
+ ) {
+ editor.timeline.commitPreview();
+ clearActiveHandleState();
+ }
+ releaseCapturedPointer();
+ }, [clearActiveHandleState, editor, releaseCapturedPointer]);
return {
selectedWithBounds,
diff --git a/apps/web/src/preview/overlays.ts b/apps/web/src/preview/overlays.ts
new file mode 100644
index 00000000..6e5b0816
--- /dev/null
+++ b/apps/web/src/preview/overlays.ts
@@ -0,0 +1,108 @@
+import type { ReactNode } from "react";
+
+export type PreviewOverlayHudAnchor =
+ | "top-left"
+ | "top-right"
+ | "bottom-left"
+ | "bottom-right";
+
+export type PreviewOverlayMount =
+ | {
+ kind: "hud";
+ anchor: PreviewOverlayHudAnchor;
+ order?: number;
+ }
+ | {
+ kind: "scene";
+ x?: number;
+ y?: number;
+ width?: number;
+ height?: number;
+ }
+ | {
+ kind: "viewport";
+ x?: number;
+ y?: number;
+ width?: number;
+ height?: number;
+ };
+
+export type PreviewOverlayPlane = "under-interaction" | "over-interaction";
+
+export interface PreviewOverlayRenderContext {
+ sceneHeight: number;
+ sceneWidth: number;
+}
+
+export interface PreviewOverlayInstance {
+ id: string;
+ mount: PreviewOverlayMount;
+ plane?: PreviewOverlayPlane;
+ pointerEvents?: "none" | "auto";
+ zIndex?: number;
+ render: (context: PreviewOverlayRenderContext) => ReactNode;
+}
+
+export interface PreviewOverlayDefinition {
+ id: string;
+ label: string;
+ defaultVisible?: boolean;
+}
+
+export interface PreviewOverlayControl extends PreviewOverlayDefinition {
+ isVisible: boolean;
+}
+
+export interface PreviewOverlaySourceResult {
+ definitions: PreviewOverlayDefinition[];
+ instances: PreviewOverlayInstance[];
+}
+
+export const EMPTY_PREVIEW_OVERLAY_SOURCE_RESULT: PreviewOverlaySourceResult = {
+ definitions: [],
+ instances: [],
+};
+
+export function isPreviewOverlayVisible({
+ overlay,
+ overlays,
+}: {
+ overlay: PreviewOverlayDefinition;
+ overlays: Record<string, boolean>;
+}): boolean {
+ return overlays[overlay.id] ?? overlay.defaultVisible ?? true;
+}
+
+export function createPreviewOverlayControl({
+ overlay,
+ overlays,
+}: {
+ overlay: PreviewOverlayDefinition;
+ overlays: Record<string, boolean>;
+}): PreviewOverlayControl {
+ return {
+ ...overlay,
+ isVisible: isPreviewOverlayVisible({ overlay, overlays }),
+ };
+}
+
+export function mergePreviewOverlaySources({
+ sources,
+}: {
+ sources: PreviewOverlaySourceResult[];
+}): PreviewOverlaySourceResult {
+ const definitionsById = new Map<string, PreviewOverlayDefinition>();
+ const instances: PreviewOverlayInstance[] = [];
+
+ for (const source of sources) {
+ for (const definition of source.definitions) {
+ definitionsById.set(definition.id, definition);
+ }
+ instances.push(...source.instances);
+ }
+
+ return {
+ definitions: [...definitionsById.values()],
+ instances,
+ };
+}
diff --git a/apps/web/src/lib/preview/preview-coords.ts b/apps/web/src/preview/preview-coords.ts
index 64184990..64184990 100644
--- a/apps/web/src/lib/preview/preview-coords.ts
+++ b/apps/web/src/preview/preview-coords.ts
diff --git a/apps/web/src/lib/preview/preview-snap.ts b/apps/web/src/preview/preview-snap.ts
index 86d35390..86d35390 100644
--- a/apps/web/src/lib/preview/preview-snap.ts
+++ b/apps/web/src/preview/preview-snap.ts
diff --git a/apps/web/src/stores/preview-store.ts b/apps/web/src/preview/preview-store.ts
index b5957cb5..0cfcbdff 100644
--- a/apps/web/src/stores/preview-store.ts
+++ b/apps/web/src/preview/preview-store.ts
@@ -1,112 +1,104 @@
-import { create } from "zustand";
-import { persist } from "zustand/middleware";
-import { isGuideId, type GuideId } from "@/lib/guides";
-import { DEFAULT_GRID_CONFIG } from "@/lib/guides/grid";
-import type { GridConfig } from "@/lib/guides/types";
-
-interface PreviewOverlaysState {
- bookmarks: boolean;
-}
-
-interface PersistedPreviewState {
- activeGuide?: string | null;
- layoutGuide?: {
- platform?: string | null;
- };
- overlays?: PreviewOverlaysState;
- gridConfig?: GridConfig;
-}
-
-interface PreviewState {
- activeGuide: GuideId | null;
- overlays: PreviewOverlaysState;
- gridConfig: GridConfig;
- toggleGuide: (guideId: GuideId) => void;
- setGridConfig: (config: Partial<GridConfig>) => void;
- setOverlayVisibility: ({
- overlay,
- isVisible,
- }: {
- overlay: keyof PreviewOverlaysState;
- isVisible: boolean;
- }) => void;
- toggleOverlayVisibility: ({
- overlay,
- }: {
- overlay: keyof PreviewOverlaysState;
- }) => void;
-}
-
-const DEFAULT_PREVIEW_OVERLAYS: PreviewOverlaysState = {
- bookmarks: true,
-};
-
-function getPersistedActiveGuide(
- state: PersistedPreviewState | undefined,
-): GuideId | null {
- const persistedGuide =
- state?.activeGuide ?? state?.layoutGuide?.platform ?? null;
-
- if (typeof persistedGuide !== "string") {
- return null;
- }
-
- return isGuideId(persistedGuide) ? persistedGuide : null;
-}
-
-export const usePreviewStore = create<PreviewState>()(
- persist(
- (set) => ({
- activeGuide: null,
- overlays: DEFAULT_PREVIEW_OVERLAYS,
- gridConfig: DEFAULT_GRID_CONFIG,
- toggleGuide: (guideId) => {
- set((state) => ({
- activeGuide: state.activeGuide === guideId ? null : guideId,
- }));
- },
- setGridConfig: (config) => {
- set((state) => ({
- gridConfig: { ...state.gridConfig, ...config },
- }));
- },
- setOverlayVisibility: ({ overlay, isVisible }) => {
- set((state) => ({
- overlays: {
- ...state.overlays,
- [overlay]: isVisible,
- },
- }));
- },
- toggleOverlayVisibility: ({ overlay }) => {
- set((state) => ({
- overlays: {
- ...state.overlays,
- [overlay]: !state.overlays[overlay],
- },
- }));
- },
- }),
- {
- name: "preview-settings",
- version: 4,
- migrate: (persistedState) => {
- const state = persistedState as PersistedPreviewState | undefined;
-
- return {
- activeGuide: getPersistedActiveGuide(state),
- overlays: state?.overlays ?? DEFAULT_PREVIEW_OVERLAYS,
- gridConfig: {
- rows: state?.gridConfig?.rows ?? DEFAULT_GRID_CONFIG.rows,
- cols: state?.gridConfig?.cols ?? DEFAULT_GRID_CONFIG.cols,
- },
- };
- },
- partialize: (state) => ({
- activeGuide: state.activeGuide,
- overlays: state.overlays,
- gridConfig: state.gridConfig,
- }),
- },
- ),
-);
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+import { isGuideId, type GuideId } from "@/guides";
+import { DEFAULT_GRID_CONFIG } from "@/guides/grid";
+import type { GridConfig } from "@/guides/types";
+
+type PreviewOverlaysState = Record<string, boolean>;
+
+interface PersistedPreviewState {
+ activeGuide?: string | null;
+ layoutGuide?: {
+ platform?: string | null;
+ };
+ overlays?: PreviewOverlaysState;
+ gridConfig?: GridConfig;
+}
+
+interface PreviewState {
+ activeGuide: GuideId | null;
+ overlays: PreviewOverlaysState;
+ gridConfig: GridConfig;
+ toggleGuide: (guideId: GuideId) => void;
+ setGridConfig: (config: Partial<GridConfig>) => void;
+ setOverlayVisibility: ({
+ overlayId,
+ isVisible,
+ }: {
+ overlayId: string;
+ isVisible: boolean;
+ }) => void;
+ toggleOverlayVisibility: ({ overlayId }: { overlayId: string }) => void;
+}
+
+const DEFAULT_PREVIEW_OVERLAYS: PreviewOverlaysState = {};
+
+function getPersistedActiveGuide(
+ state: PersistedPreviewState | undefined,
+): GuideId | null {
+ const persistedGuide =
+ state?.activeGuide ?? state?.layoutGuide?.platform ?? null;
+
+ if (typeof persistedGuide !== "string") {
+ return null;
+ }
+
+ return isGuideId(persistedGuide) ? persistedGuide : null;
+}
+
+export const usePreviewStore = create<PreviewState>()(
+ persist(
+ (set) => ({
+ activeGuide: null,
+ overlays: DEFAULT_PREVIEW_OVERLAYS,
+ gridConfig: DEFAULT_GRID_CONFIG,
+ toggleGuide: (guideId) => {
+ set((state) => ({
+ activeGuide: state.activeGuide === guideId ? null : guideId,
+ }));
+ },
+ setGridConfig: (config) => {
+ set((state) => ({
+ gridConfig: { ...state.gridConfig, ...config },
+ }));
+ },
+ setOverlayVisibility: ({ overlayId, isVisible }) => {
+ set((state) => ({
+ overlays: {
+ ...state.overlays,
+ [overlayId]: isVisible,
+ },
+ }));
+ },
+ toggleOverlayVisibility: ({ overlayId }) => {
+ set((state) => ({
+ overlays: {
+ ...state.overlays,
+ [overlayId]: !state.overlays[overlayId],
+ },
+ }));
+ },
+ }),
+ {
+ name: "preview-settings",
+ version: 6,
+ migrate: (persistedState) => {
+ const state = persistedState as PersistedPreviewState | undefined;
+
+ return {
+ activeGuide: getPersistedActiveGuide(state),
+ overlays: DEFAULT_PREVIEW_OVERLAYS,
+ gridConfig: {
+ rows: state?.gridConfig?.rows ?? DEFAULT_GRID_CONFIG.rows,
+ cols: state?.gridConfig?.cols ?? DEFAULT_GRID_CONFIG.cols,
+ },
+ };
+ },
+ partialize: (state) => ({
+ activeGuide: state.activeGuide,
+ overlays: state.overlays,
+ gridConfig: state.gridConfig,
+ }),
+ },
+ ),
+);
diff --git a/apps/web/src/lib/preview/zoom.ts b/apps/web/src/preview/zoom.ts
index 9e6f5905..9e6f5905 100644
--- a/apps/web/src/lib/preview/zoom.ts
+++ b/apps/web/src/preview/zoom.ts
diff --git a/apps/web/src/components/editor/dialogs/delete-project-dialog.tsx b/apps/web/src/project/components/delete-project-dialog.tsx
index 0086384e..0086384e 100644
--- a/apps/web/src/components/editor/dialogs/delete-project-dialog.tsx
+++ b/apps/web/src/project/components/delete-project-dialog.tsx
diff --git a/apps/web/src/components/editor/dialogs/migration-dialog.tsx b/apps/web/src/project/components/migration-dialog.tsx
index 4c9550c2..4aac26d5 100644
--- a/apps/web/src/components/editor/dialogs/migration-dialog.tsx
+++ b/apps/web/src/project/components/migration-dialog.tsx
@@ -7,7 +7,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import { Loader2 } from "lucide-react";
export function MigrationDialog() {
diff --git a/apps/web/src/components/editor/dialogs/project-info-dialog.tsx b/apps/web/src/project/components/project-info-dialog.tsx
index ed4eb990..f0ea364e 100644
--- a/apps/web/src/components/editor/dialogs/project-info-dialog.tsx
+++ b/apps/web/src/project/components/project-info-dialog.tsx
@@ -6,7 +6,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
-import type { TProjectMetadata } from "@/lib/project/types";
+import type { TProjectMetadata } from "@/project/types";
import { formatDate } from "@/utils/date";
import { formatTimecode, mediaTimeToSeconds } from "opencut-wasm";
import { Button } from "@/components/ui/button";
diff --git a/apps/web/src/components/editor/dialogs/rename-project-dialog.tsx b/apps/web/src/project/components/rename-project-dialog.tsx
index 73024d71..73024d71 100644
--- a/apps/web/src/components/editor/dialogs/rename-project-dialog.tsx
+++ b/apps/web/src/project/components/rename-project-dialog.tsx
diff --git a/apps/web/src/lib/project/types.ts b/apps/web/src/project/types.ts
index 0a454e07..42e844f6 100644
--- a/apps/web/src/lib/project/types.ts
+++ b/apps/web/src/project/types.ts
@@ -1,5 +1,5 @@
import type { FrameRate } from "opencut-wasm";
-import type { TScene } from "@/lib/timeline/types";
+import type { TScene } from "@/timeline/types";
export type TBackground =
| {
diff --git a/apps/web/src/components/editor/panels/properties/tabs/blending-tab.tsx b/apps/web/src/rendering/components/blending-tab.tsx
index f8e5397d..3e0db46c 100644
--- a/apps/web/src/components/editor/panels/properties/tabs/blending-tab.tsx
+++ b/apps/web/src/rendering/components/blending-tab.tsx
@@ -1,9 +1,9 @@
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import { clamp } from "@/utils/math";
import { NumberField } from "@/components/ui/number-field";
import { OcCheckerboardIcon } from "@/components/icons";
import { Fragment, useRef } from "react";
-import { useMenuPreview } from "@/hooks/use-menu-preview";
+import { useMenuPreview } from "@/editor/use-menu-preview";
import {
Section,
SectionContent,
@@ -19,16 +19,16 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
-import type { BlendMode } from "@/lib/rendering";
-import type { ElementType } from "@/lib/timeline";
-import type { ElementAnimations } from "@/lib/animation/types";
+import type { BlendMode } from "@/rendering";
+import type { ElementType } from "@/timeline";
+import type { ElementAnimations } from "@/animation/types";
import { HugeiconsIcon } from "@hugeicons/react";
import { RainDropIcon } from "@hugeicons/core-free-icons";
-import { KeyframeToggle } from "../components/keyframe-toggle";
-import { useKeyframedNumberProperty } from "../hooks/use-keyframed-number-property";
-import { useElementPlayhead } from "../hooks/use-element-playhead";
-import { resolveOpacityAtTime } from "@/lib/animation";
-import { DEFAULTS } from "@/lib/timeline/defaults";
+import { KeyframeToggle } from "@/components/editor/panels/properties/components/keyframe-toggle";
+import { useKeyframedNumberProperty } from "@/components/editor/panels/properties/hooks/use-keyframed-number-property";
+import { useElementPlayhead } from "@/components/editor/panels/properties/hooks/use-element-playhead";
+import { resolveOpacityAtTime } from "@/animation";
+import { DEFAULTS } from "@/timeline/defaults";
import { isPropertyAtDefault } from "./transform-tab";
type BlendingElement = {
diff --git a/apps/web/src/components/editor/panels/properties/tabs/transform-tab.tsx b/apps/web/src/rendering/components/transform-tab.tsx
index c3cef2d6..60d4c7df 100644
--- a/apps/web/src/components/editor/panels/properties/tabs/transform-tab.tsx
+++ b/apps/web/src/rendering/components/transform-tab.tsx
@@ -1,7 +1,7 @@
import { NumberField } from "@/components/ui/number-field";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import { clamp, isNearlyEqual } from "@/utils/math";
-import type { VisualElement } from "@/lib/timeline";
+import type { VisualElement } from "@/timeline";
import {
Section,
SectionContent,
@@ -21,12 +21,12 @@ import {
getGroupKeyframesAtTime,
hasGroupKeyframeAtTime,
resolveTransformAtTime,
-} from "@/lib/animation";
-import { DEFAULTS } from "@/lib/timeline/defaults";
-import { useElementPlayhead } from "../hooks/use-element-playhead";
-import { KeyframeToggle } from "../components/keyframe-toggle";
-import { useKeyframedNumberProperty } from "../hooks/use-keyframed-number-property";
-import { usePropertiesStore } from "../stores/properties-store";
+} from "@/animation";
+import { DEFAULTS } from "@/timeline/defaults";
+import { useElementPlayhead } from "@/components/editor/panels/properties/hooks/use-element-playhead";
+import { KeyframeToggle } from "@/components/editor/panels/properties/components/keyframe-toggle";
+import { useKeyframedNumberProperty } from "@/components/editor/panels/properties/hooks/use-keyframed-number-property";
+import { usePropertiesStore } from "@/components/editor/panels/properties/stores/properties-store";
export function parseNumericInput({ input }: { input: string }): number | null {
const parsed = parseFloat(input);
diff --git a/apps/web/src/lib/rendering.ts b/apps/web/src/rendering/index.ts
index c5555371..c5555371 100644
--- a/apps/web/src/lib/rendering.ts
+++ b/apps/web/src/rendering/index.ts
diff --git a/apps/web/src/lib/retime/__tests__/resolve.test.ts b/apps/web/src/retime/__tests__/resolve.test.ts
index 2fbd6675..bb7b9a6d 100644
--- a/apps/web/src/lib/retime/__tests__/resolve.test.ts
+++ b/apps/web/src/retime/__tests__/resolve.test.ts
@@ -4,8 +4,8 @@ import {
getEffectiveRateAt,
getSourceTimeAtClipTime,
getTimelineDurationForSourceSpan,
-} from "@/lib/retime";
-import type { RetimeConfig } from "@/lib/timeline";
+} from "@/retime";
+import type { RetimeConfig } from "@/timeline";
const twoX: RetimeConfig = { rate: 2 };
const halfX: RetimeConfig = { rate: 0.5 };
diff --git a/apps/web/src/lib/retime/__tests__/split.test.ts b/apps/web/src/retime/__tests__/split.test.ts
index a00eadac..3c7bef63 100644
--- a/apps/web/src/lib/retime/__tests__/split.test.ts
+++ b/apps/web/src/retime/__tests__/split.test.ts
@@ -2,8 +2,8 @@ import { describe, expect, test } from "bun:test";
import {
getSourceSpanAtClipTime,
splitRetimeAtClipTime,
-} from "@/lib/retime";
-import type { RetimeConfig } from "@/lib/timeline";
+} from "@/retime";
+import type { RetimeConfig } from "@/timeline";
describe("retime split", () => {
test("measures source span at a clip time", () => {
diff --git a/apps/web/src/lib/retime/audio-stretch.ts b/apps/web/src/retime/audio-stretch.ts
index 92902dff..7df67f36 100644
--- a/apps/web/src/lib/retime/audio-stretch.ts
+++ b/apps/web/src/retime/audio-stretch.ts
@@ -1,6 +1,6 @@
import { PitchShifter } from "soundtouchjs";
-import { clampRetimeRate, shouldMaintainPitch } from "@/lib/retime/rate";
-import type { RetimeConfig } from "@/lib/timeline";
+import { clampRetimeRate, shouldMaintainPitch } from "@/retime/rate";
+import type { RetimeConfig } from "@/timeline";
import { getSourceTimeAtClipTime } from "./resolve";
const RATE_EPSILON = 1e-6;
diff --git a/apps/web/src/lib/retime/index.ts b/apps/web/src/retime/index.ts
index 2ab1d10f..2ab1d10f 100644
--- a/apps/web/src/lib/retime/index.ts
+++ b/apps/web/src/retime/index.ts
diff --git a/apps/web/src/lib/retime/presets.ts b/apps/web/src/retime/presets.ts
index fbd391ff..816c8675 100644
--- a/apps/web/src/lib/retime/presets.ts
+++ b/apps/web/src/retime/presets.ts
@@ -1,5 +1,5 @@
-import type { RetimeConfig } from "@/lib/timeline";
-import { clampRetimeRate } from "@/lib/retime/rate";
+import type { RetimeConfig } from "@/timeline";
+import { clampRetimeRate } from "@/retime/rate";
export function buildConstantRetime({
rate,
diff --git a/apps/web/src/lib/retime/rate.ts b/apps/web/src/retime/rate.ts
index 75ea530b..75ea530b 100644
--- a/apps/web/src/lib/retime/rate.ts
+++ b/apps/web/src/retime/rate.ts
diff --git a/apps/web/src/lib/retime/resolve.ts b/apps/web/src/retime/resolve.ts
index 5e7236d5..0dea9dd8 100644
--- a/apps/web/src/lib/retime/resolve.ts
+++ b/apps/web/src/retime/resolve.ts
@@ -1,5 +1,5 @@
-import type { RetimeConfig } from "@/lib/timeline";
-import { clampRetimeRate } from "@/lib/retime/rate";
+import type { RetimeConfig } from "@/timeline";
+import { clampRetimeRate } from "@/retime/rate";
function getSafeRate({ rate }: { rate: number }): number {
return clampRetimeRate({ rate });
diff --git a/apps/web/src/lib/retime/split.ts b/apps/web/src/retime/split.ts
index 2635590e..1b7360cc 100644
--- a/apps/web/src/lib/retime/split.ts
+++ b/apps/web/src/retime/split.ts
@@ -1,4 +1,4 @@
-import type { RetimeConfig } from "@/lib/timeline";
+import type { RetimeConfig } from "@/timeline";
import { getSourceTimeAtClipTime } from "./resolve";
export function getSourceSpanAtClipTime({
diff --git a/apps/web/src/lib/ripple/apply.ts b/apps/web/src/ripple/apply.ts
index f96e4600..4ad83529 100644
--- a/apps/web/src/lib/ripple/apply.ts
+++ b/apps/web/src/ripple/apply.ts
@@ -1,4 +1,4 @@
-import type { SceneTracks, TimelineTrack } from "@/lib/timeline/types";
+import type { SceneTracks, TimelineTrack } from "@/timeline/types";
import { rippleShiftElements } from "./shift";
export interface RippleAdjustment {
diff --git a/apps/web/src/lib/ripple/diff.ts b/apps/web/src/ripple/diff.ts
index 49238738..e450d5ae 100644
--- a/apps/web/src/lib/ripple/diff.ts
+++ b/apps/web/src/ripple/diff.ts
@@ -1,4 +1,4 @@
-import type { SceneTracks, TimelineElement, TimelineTrack } from "@/lib/timeline/types";
+import type { SceneTracks, TimelineElement, TimelineTrack } from "@/timeline/types";
import type { RippleAdjustment } from "./apply";
interface Interval {
diff --git a/apps/web/src/lib/ripple/index.ts b/apps/web/src/ripple/index.ts
index 5a14de64..5a14de64 100644
--- a/apps/web/src/lib/ripple/index.ts
+++ b/apps/web/src/ripple/index.ts
diff --git a/apps/web/src/lib/ripple/shift.ts b/apps/web/src/ripple/shift.ts
index 03ab4a67..a88d2b2d 100644
--- a/apps/web/src/lib/ripple/shift.ts
+++ b/apps/web/src/ripple/shift.ts
@@ -1,4 +1,4 @@
-import type { TimelineElement } from "@/lib/timeline/types";
+import type { TimelineElement } from "@/timeline/types";
export function rippleShiftElements<TElement extends TimelineElement>({
elements,
diff --git a/apps/web/src/lib/selection/attributes.ts b/apps/web/src/selection/attributes.ts
index a1aee842..a1aee842 100644
--- a/apps/web/src/lib/selection/attributes.ts
+++ b/apps/web/src/selection/attributes.ts
diff --git a/apps/web/src/lib/selection/context.tsx b/apps/web/src/selection/context.tsx
index 8e87aa40..8e87aa40 100644
--- a/apps/web/src/lib/selection/context.tsx
+++ b/apps/web/src/selection/context.tsx
diff --git a/apps/web/src/lib/selection/editor-selection.ts b/apps/web/src/selection/editor-selection.ts
index e9563b97..21ab6466 100644
--- a/apps/web/src/lib/selection/editor-selection.ts
+++ b/apps/web/src/selection/editor-selection.ts
@@ -1,5 +1,5 @@
-import type { SelectedKeyframeRef } from "@/lib/animation/types";
-import type { ElementRef } from "@/lib/timeline/types";
+import type { SelectedKeyframeRef } from "@/animation/types";
+import type { ElementRef } from "@/timeline/types";
export interface SelectedMaskPointSelection {
trackId: string;
diff --git a/apps/web/src/lib/selection/hit-testing.ts b/apps/web/src/selection/hit-testing.ts
index d8186238..d8186238 100644
--- a/apps/web/src/lib/selection/hit-testing.ts
+++ b/apps/web/src/selection/hit-testing.ts
diff --git a/apps/web/src/lib/selection/hooks/use-box-select.ts b/apps/web/src/selection/hooks/use-box-select.ts
index ab8464ec..90a584b5 100644
--- a/apps/web/src/lib/selection/hooks/use-box-select.ts
+++ b/apps/web/src/selection/hooks/use-box-select.ts
@@ -1,170 +1,170 @@
-"use client";
-
-import { useCallback, useEffect, useRef, useState } from "react";
-import type {
- BoxSelectionSnapshot,
- ResolveIntersections,
-} from "@/lib/selection/types";
-
-interface SelectionBoxState<TId> extends BoxSelectionSnapshot<TId> {
- startPos: { x: number; y: number };
- currentPos: { x: number; y: number };
- isActive: boolean;
- isAdditive: boolean;
-}
-
-export function useBoxSelect<TId>({
- containerRef,
- resolveIntersections,
- selectedIds,
- anchorId,
- onSelectionChange,
- shouldStartSelection,
- getIsAdditiveSelection,
- isEnabled = true,
-}: {
- containerRef: React.RefObject<HTMLElement | null>;
- resolveIntersections: ResolveIntersections<TId>;
- selectedIds: TId[];
- anchorId: TId | null;
- onSelectionChange: (state: {
- intersectedIds: TId[];
- initialSelectedIds: TId[];
- initialAnchorId: TId | null;
- isAdditive: boolean;
- }) => void;
- shouldStartSelection?: (event: React.MouseEvent<Element>) => boolean;
- getIsAdditiveSelection?: (event: React.MouseEvent<Element>) => boolean;
- isEnabled?: boolean;
-}) {
- const [selectionBox, setSelectionBox] = useState<SelectionBoxState<TId> | null>(
- null,
- );
- const justFinishedSelectingRef = useRef(false);
- const shouldStartSelectionCheck = shouldStartSelection ?? (() => true);
- const getIsAdditiveSelectionCheck =
- getIsAdditiveSelection ??
- ((event: React.MouseEvent<Element>) => event.ctrlKey || event.metaKey);
-
- const handleMouseDown = useCallback(
- (event: React.MouseEvent<Element>) => {
- const canStartSelection = shouldStartSelectionCheck(event);
- if (!isEnabled || event.button !== 0 || !canStartSelection) {
- return;
- }
-
- setSelectionBox({
- startPos: { x: event.clientX, y: event.clientY },
- currentPos: { x: event.clientX, y: event.clientY },
- isActive: false,
- isAdditive: getIsAdditiveSelectionCheck(event),
- initialSelectedIds: selectedIds,
- initialAnchorId: anchorId,
- });
- },
- [
- anchorId,
- getIsAdditiveSelectionCheck,
- isEnabled,
- selectedIds,
- shouldStartSelectionCheck,
- ],
- );
-
- const updateSelection = useCallback(
- ({
- startPos,
- currentPos,
- isAdditive,
- initialSelectedIds,
- initialAnchorId,
- }: SelectionBoxState<TId>) => {
- const intersectedIds = resolveIntersections({
- startPos,
- currentPos,
- });
- onSelectionChange({
- intersectedIds,
- initialSelectedIds,
- initialAnchorId,
- isAdditive,
- });
- },
- [onSelectionChange, resolveIntersections],
- );
-
- useEffect(() => {
- if (!selectionBox) {
- return;
- }
-
- const handleMouseMove = ({ clientX, clientY }: MouseEvent) => {
- const deltaX = Math.abs(clientX - selectionBox.startPos.x);
- const deltaY = Math.abs(clientY - selectionBox.startPos.y);
- const nextSelectionBox = {
- ...selectionBox,
- currentPos: { x: clientX, y: clientY },
- isActive: deltaX > 5 || deltaY > 5 || selectionBox.isActive,
- };
-
- setSelectionBox(nextSelectionBox);
-
- if (!nextSelectionBox.isActive) {
- return;
- }
-
- updateSelection(nextSelectionBox);
- };
-
- const handleMouseUp = () => {
- if (selectionBox.isActive) {
- justFinishedSelectingRef.current = true;
- requestAnimationFrame(() => {
- justFinishedSelectingRef.current = false;
- });
- }
-
- setSelectionBox(null);
- };
-
- window.addEventListener("mousemove", handleMouseMove);
- window.addEventListener("mouseup", handleMouseUp);
-
- return () => {
- window.removeEventListener("mousemove", handleMouseMove);
- window.removeEventListener("mouseup", handleMouseUp);
- };
- }, [selectionBox, updateSelection]);
-
- useEffect(() => {
- if (!selectionBox) {
- return;
- }
-
- const previousBodyUserSelect = document.body.style.userSelect;
- const previousContainerUserSelect = containerRef.current?.style.userSelect ?? "";
-
- document.body.style.userSelect = "none";
- if (containerRef.current) {
- containerRef.current.style.userSelect = "none";
- }
-
- return () => {
- document.body.style.userSelect = previousBodyUserSelect;
- if (containerRef.current) {
- containerRef.current.style.userSelect = previousContainerUserSelect;
- }
- };
- }, [containerRef, selectionBox]);
-
- const shouldIgnoreClick = useCallback(() => {
- return justFinishedSelectingRef.current;
- }, []);
-
- return {
- selectionBox,
- handleMouseDown,
- isSelecting: selectionBox?.isActive ?? false,
- shouldIgnoreClick,
- };
-}
+"use client";
+
+import { useCallback, useEffect, useRef, useState } from "react";
+import type {
+ BoxSelectionSnapshot,
+ ResolveIntersections,
+} from "@/selection/types";
+
+interface SelectionBoxState<TId> extends BoxSelectionSnapshot<TId> {
+ startPos: { x: number; y: number };
+ currentPos: { x: number; y: number };
+ isActive: boolean;
+ isAdditive: boolean;
+}
+
+export function useBoxSelect<TId>({
+ containerRef,
+ resolveIntersections,
+ selectedIds,
+ anchorId,
+ onSelectionChange,
+ shouldStartSelection,
+ getIsAdditiveSelection,
+ isEnabled = true,
+}: {
+ containerRef: React.RefObject<HTMLElement | null>;
+ resolveIntersections: ResolveIntersections<TId>;
+ selectedIds: TId[];
+ anchorId: TId | null;
+ onSelectionChange: (state: {
+ intersectedIds: TId[];
+ initialSelectedIds: TId[];
+ initialAnchorId: TId | null;
+ isAdditive: boolean;
+ }) => void;
+ shouldStartSelection?: (event: React.MouseEvent<Element>) => boolean;
+ getIsAdditiveSelection?: (event: React.MouseEvent<Element>) => boolean;
+ isEnabled?: boolean;
+}) {
+ const [selectionBox, setSelectionBox] =
+ useState<SelectionBoxState<TId> | null>(null);
+ const justFinishedSelectingRef = useRef(false);
+ const shouldStartSelectionCheck = shouldStartSelection ?? (() => true);
+ const getIsAdditiveSelectionCheck =
+ getIsAdditiveSelection ??
+ ((event: React.MouseEvent<Element>) => event.ctrlKey || event.metaKey);
+
+ const handleMouseDown = useCallback(
+ (event: React.MouseEvent<Element>) => {
+ const canStartSelection = shouldStartSelectionCheck(event);
+ if (!isEnabled || event.button !== 0 || !canStartSelection) {
+ return;
+ }
+
+ setSelectionBox({
+ startPos: { x: event.clientX, y: event.clientY },
+ currentPos: { x: event.clientX, y: event.clientY },
+ isActive: false,
+ isAdditive: getIsAdditiveSelectionCheck(event),
+ initialSelectedIds: selectedIds,
+ initialAnchorId: anchorId,
+ });
+ },
+ [
+ anchorId,
+ getIsAdditiveSelectionCheck,
+ isEnabled,
+ selectedIds,
+ shouldStartSelectionCheck,
+ ],
+ );
+
+ const updateSelection = useCallback(
+ ({
+ startPos,
+ currentPos,
+ isAdditive,
+ initialSelectedIds,
+ initialAnchorId,
+ }: SelectionBoxState<TId>) => {
+ const intersectedIds = resolveIntersections({
+ startPos,
+ currentPos,
+ });
+ onSelectionChange({
+ intersectedIds,
+ initialSelectedIds,
+ initialAnchorId,
+ isAdditive,
+ });
+ },
+ [onSelectionChange, resolveIntersections],
+ );
+
+ useEffect(() => {
+ if (!selectionBox) {
+ return;
+ }
+
+ const handleMouseMove = ({ clientX, clientY }: MouseEvent) => {
+ const deltaX = Math.abs(clientX - selectionBox.startPos.x);
+ const deltaY = Math.abs(clientY - selectionBox.startPos.y);
+ const nextSelectionBox = {
+ ...selectionBox,
+ currentPos: { x: clientX, y: clientY },
+ isActive: deltaX > 5 || deltaY > 5 || selectionBox.isActive,
+ };
+
+ setSelectionBox(nextSelectionBox);
+
+ if (!nextSelectionBox.isActive) {
+ return;
+ }
+
+ updateSelection(nextSelectionBox);
+ };
+
+ const handleMouseUp = () => {
+ if (selectionBox.isActive) {
+ justFinishedSelectingRef.current = true;
+ requestAnimationFrame(() => {
+ justFinishedSelectingRef.current = false;
+ });
+ }
+
+ setSelectionBox(null);
+ };
+
+ window.addEventListener("mousemove", handleMouseMove);
+ window.addEventListener("mouseup", handleMouseUp);
+
+ return () => {
+ window.removeEventListener("mousemove", handleMouseMove);
+ window.removeEventListener("mouseup", handleMouseUp);
+ };
+ }, [selectionBox, updateSelection]);
+
+ useEffect(() => {
+ if (!selectionBox) {
+ return;
+ }
+
+ const previousBodyUserSelect = document.body.style.userSelect;
+ const previousContainerUserSelect =
+ containerRef.current?.style.userSelect ?? "";
+
+ document.body.style.userSelect = "none";
+ if (containerRef.current) {
+ containerRef.current.style.userSelect = "none";
+ }
+
+ return () => {
+ document.body.style.userSelect = previousBodyUserSelect;
+ if (containerRef.current) {
+ containerRef.current.style.userSelect = previousContainerUserSelect;
+ }
+ };
+ }, [containerRef, selectionBox]);
+
+ const shouldIgnoreClick = useCallback(() => {
+ return justFinishedSelectingRef.current;
+ }, []);
+
+ return {
+ selectionBox,
+ handleMouseDown,
+ isSelecting: selectionBox?.isActive ?? false,
+ shouldIgnoreClick,
+ };
+}
diff --git a/apps/web/src/lib/selection/hooks/use-selection-scope.ts b/apps/web/src/selection/hooks/use-selection-scope.ts
index a7090c5d..ad4d2ce8 100644
--- a/apps/web/src/lib/selection/hooks/use-selection-scope.ts
+++ b/apps/web/src/selection/hooks/use-selection-scope.ts
@@ -1,6 +1,6 @@
import { useEffect, useRef } from "react";
-import { useSelectionContext } from "@/lib/selection/context";
-import { activateScope, type ScopeEntry } from "@/lib/selection/scope";
+import { useSelectionContext } from "@/selection/context";
+import { activateScope, type ScopeEntry } from "@/selection/scope";
export function useSelectionScope() {
const { selectedIds, clearSelection } = useSelectionContext();
diff --git a/apps/web/src/selection/index.ts b/apps/web/src/selection/index.ts
new file mode 100644
index 00000000..9d62731b
--- /dev/null
+++ b/apps/web/src/selection/index.ts
@@ -0,0 +1,4 @@
+export { SelectableItem } from "@/selection/selectable-item";
+export { SelectableSurface } from "@/selection/selectable-surface";
+export { useSelection } from "@/selection/context";
+export { useSelectionScope } from "@/selection/hooks/use-selection-scope";
diff --git a/apps/web/src/lib/selection/scope.ts b/apps/web/src/selection/scope.ts
index 22c7710c..22c7710c 100644
--- a/apps/web/src/lib/selection/scope.ts
+++ b/apps/web/src/selection/scope.ts
diff --git a/apps/web/src/lib/selection/selectable-item.tsx b/apps/web/src/selection/selectable-item.tsx
index 03264753..cc9e1dbf 100644
--- a/apps/web/src/lib/selection/selectable-item.tsx
+++ b/apps/web/src/selection/selectable-item.tsx
@@ -1,9 +1,9 @@
"use client";
import { forwardRef, useCallback } from "react";
-import { useSelectionContext } from "@/lib/selection/context";
-import { SELECTABLE_ITEM_ATTRIBUTE } from "@/lib/selection/attributes";
-import type { SelectableItemProps } from "@/lib/selection/types";
+import { useSelectionContext } from "@/selection/context";
+import { SELECTABLE_ITEM_ATTRIBUTE } from "@/selection/attributes";
+import type { SelectableItemProps } from "@/selection/types";
import { cn } from "@/utils/ui";
function setForwardedRef<T>(ref: React.ForwardedRef<T>, value: T | null) {
diff --git a/apps/web/src/lib/selection/selectable-surface.tsx b/apps/web/src/selection/selectable-surface.tsx
index 012bc486..24166daf 100644
--- a/apps/web/src/lib/selection/selectable-surface.tsx
+++ b/apps/web/src/selection/selectable-surface.tsx
@@ -1,10 +1,10 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { SelectionBox } from "@/lib/selection/selection-box";
-import { SelectionContext } from "@/lib/selection/context";
-import { SELECTABLE_ITEM_ATTRIBUTE } from "@/lib/selection/attributes";
-import { resolveElementIntersections } from "@/lib/selection/hit-testing";
+import { SelectionBox } from "@/selection/selection-box";
+import { SelectionContext } from "@/selection/context";
+import { SELECTABLE_ITEM_ATTRIBUTE } from "@/selection/attributes";
+import { resolveElementIntersections } from "@/selection/hit-testing";
import {
applyBoxSelection,
clearSelection,
@@ -13,12 +13,12 @@ import {
replaceSelection,
selectRange,
toggleSelection,
-} from "@/lib/selection/state";
+} from "@/selection/state";
import type {
SelectableSurfaceProps,
SelectionState,
-} from "@/lib/selection/types";
-import { useBoxSelect } from "@/lib/selection/hooks/use-box-select";
+} from "@/selection/types";
+import { useBoxSelect } from "@/selection/hooks/use-box-select";
import { cn } from "@/utils/ui";
export function SelectableSurface({
diff --git a/apps/web/src/lib/selection/selection-box.tsx b/apps/web/src/selection/selection-box.tsx
index e30f574d..e30f574d 100644
--- a/apps/web/src/lib/selection/selection-box.tsx
+++ b/apps/web/src/selection/selection-box.tsx
diff --git a/apps/web/src/lib/selection/state.ts b/apps/web/src/selection/state.ts
index b0ba3089..b44578a1 100644
--- a/apps/web/src/lib/selection/state.ts
+++ b/apps/web/src/selection/state.ts
@@ -1,7 +1,7 @@
import type {
BoxSelectionChange,
SelectionState,
-} from "@/lib/selection/types";
+} from "@/selection/types";
function dedupeIds({ ids }: { ids: string[] }) {
return [...new Set(ids)];
diff --git a/apps/web/src/lib/selection/types.ts b/apps/web/src/selection/types.ts
index 31520788..31520788 100644
--- a/apps/web/src/lib/selection/types.ts
+++ b/apps/web/src/selection/types.ts
diff --git a/apps/web/src/services/renderer/compositor/frame-descriptor.ts b/apps/web/src/services/renderer/compositor/frame-descriptor.ts
index f93eae27..bae9203f 100644
--- a/apps/web/src/services/renderer/compositor/frame-descriptor.ts
+++ b/apps/web/src/services/renderer/compositor/frame-descriptor.ts
@@ -1,5 +1,5 @@
-import { drawCssBackground } from "@/lib/gradients";
-import { masksRegistry } from "@/lib/masks";
+import { drawCssBackground } from "@/gradients";
+import { masksRegistry } from "@/masks";
import type { AnyBaseNode } from "../nodes/base-node";
import type { CanvasRenderer } from "../canvas-renderer";
import { createOffscreenCanvas } from "../canvas-utils";
@@ -19,7 +19,7 @@ import type {
LayerMaskDescriptor,
QuadTransformDescriptor,
} from "./types";
-import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/lib/graphics";
+import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/graphics";
export type TextureUploadDescriptor = {
id: string;
diff --git a/apps/web/src/services/renderer/compositor/types.ts b/apps/web/src/services/renderer/compositor/types.ts
index 5d9322e8..af5a58c2 100644
--- a/apps/web/src/services/renderer/compositor/types.ts
+++ b/apps/web/src/services/renderer/compositor/types.ts
@@ -1,5 +1,5 @@
-import type { BlendMode } from "@/lib/rendering";
-import type { EffectPass } from "@/lib/effects/types";
+import type { BlendMode } from "@/rendering";
+import type { EffectPass } from "@/effects/types";
export type FrameDescriptor = {
width: number;
diff --git a/apps/web/src/services/renderer/effect-preview.ts b/apps/web/src/services/renderer/effect-preview.ts
index 06e64d0e..c4cacde0 100644
--- a/apps/web/src/services/renderer/effect-preview.ts
+++ b/apps/web/src/services/renderer/effect-preview.ts
@@ -1,7 +1,7 @@
import { createOffscreenCanvas } from "./canvas-utils";
-import { effectsRegistry, resolveEffectPasses } from "@/lib/effects";
-import { buildDefaultParamValues } from "@/lib/registry";
-import type { ParamValues } from "@/lib/params";
+import { effectsRegistry, resolveEffectPasses } from "@/effects";
+import { buildDefaultParamValues } from "@/params/registry";
+import type { ParamValues } from "@/params";
import { gpuRenderer } from "./gpu-renderer";
const PREVIEW_SIZE = 160;
diff --git a/apps/web/src/services/renderer/gpu-renderer.ts b/apps/web/src/services/renderer/gpu-renderer.ts
index c11c3015..c61b6b98 100644
--- a/apps/web/src/services/renderer/gpu-renderer.ts
+++ b/apps/web/src/services/renderer/gpu-renderer.ts
@@ -3,7 +3,7 @@ import {
applyMaskFeather as applyMaskFeatherWasm,
initializeGpu,
} from "opencut-wasm";
-import type { EffectPass, EffectUniformValue } from "@/lib/effects/types";
+import type { EffectPass, EffectUniformValue } from "@/effects/types";
let gpuAvailable = false;
let initPromise: Promise<void> | null = null;
diff --git a/apps/web/src/services/renderer/nodes/blur-background-node.ts b/apps/web/src/services/renderer/nodes/blur-background-node.ts
index d20e2533..d83c1cf5 100644
--- a/apps/web/src/services/renderer/nodes/blur-background-node.ts
+++ b/apps/web/src/services/renderer/nodes/blur-background-node.ts
@@ -1,5 +1,5 @@
-import type { EffectPass } from "@/lib/effects/types";
-import type { RetimeConfig } from "@/lib/timeline";
+import type { EffectPass } from "@/effects/types";
+import type { RetimeConfig } from "@/timeline";
import { BaseNode } from "./base-node";
export type BlurBackgroundNodeParams = {
diff --git a/apps/web/src/services/renderer/nodes/effect-layer-node.ts b/apps/web/src/services/renderer/nodes/effect-layer-node.ts
index d6e70fcf..d1b51fa3 100644
--- a/apps/web/src/services/renderer/nodes/effect-layer-node.ts
+++ b/apps/web/src/services/renderer/nodes/effect-layer-node.ts
@@ -1,5 +1,5 @@
-import type { EffectPass } from "@/lib/effects/types";
-import type { ParamValues } from "@/lib/params";
+import type { EffectPass } from "@/effects/types";
+import type { ParamValues } from "@/params";
import { BaseNode } from "./base-node";
export type EffectLayerNodeParams = {
diff --git a/apps/web/src/services/renderer/nodes/graphic-node.ts b/apps/web/src/services/renderer/nodes/graphic-node.ts
index 494d2179..2d022510 100644
--- a/apps/web/src/services/renderer/nodes/graphic-node.ts
+++ b/apps/web/src/services/renderer/nodes/graphic-node.ts
@@ -3,8 +3,8 @@ import {
DEFAULT_GRAPHIC_SOURCE_SIZE,
getGraphicDefinition,
registerDefaultGraphics,
-} from "@/lib/graphics";
-import type { ParamValues } from "@/lib/params";
+} from "@/graphics";
+import type { ParamValues } from "@/params";
import {
VisualNode,
type ResolvedVisualNodeState,
diff --git a/apps/web/src/services/renderer/nodes/sticker-node.ts b/apps/web/src/services/renderer/nodes/sticker-node.ts
index 3c155cb6..3d22799e 100644
--- a/apps/web/src/services/renderer/nodes/sticker-node.ts
+++ b/apps/web/src/services/renderer/nodes/sticker-node.ts
@@ -1,4 +1,4 @@
-import { resolveStickerId } from "@/lib/stickers";
+import { resolveStickerId } from "@/stickers";
import {
VisualNode,
type ResolvedVisualSourceNodeState,
diff --git a/apps/web/src/services/renderer/nodes/text-node.ts b/apps/web/src/services/renderer/nodes/text-node.ts
index 86384328..d5755315 100644
--- a/apps/web/src/services/renderer/nodes/text-node.ts
+++ b/apps/web/src/services/renderer/nodes/text-node.ts
@@ -1,11 +1,11 @@
import { BaseNode } from "./base-node";
-import type { TextElement } from "@/lib/timeline";
-import type { EffectPass } from "@/lib/effects/types";
-import type { Transform } from "@/lib/rendering";
+import type { TextElement } from "@/timeline";
+import type { EffectPass } from "@/effects/types";
+import type { Transform } from "@/rendering";
import {
drawMeasuredTextLayout,
-} from "@/lib/text/primitives";
-import type { MeasuredTextElement } from "@/lib/text/measure-element";
+} from "@/text/primitives";
+import type { MeasuredTextElement } from "@/text/measure-element";
export type TextNodeParams = TextElement & {
canvasCenter: { x: number; y: number };
diff --git a/apps/web/src/services/renderer/nodes/visual-node.ts b/apps/web/src/services/renderer/nodes/visual-node.ts
index 010f146e..995e598d 100644
--- a/apps/web/src/services/renderer/nodes/visual-node.ts
+++ b/apps/web/src/services/renderer/nodes/visual-node.ts
@@ -1,8 +1,8 @@
import { BaseNode } from "./base-node";
-import type { Effect, EffectPass } from "@/lib/effects/types";
-import type { Mask } from "@/lib/masks/types";
-import type { BlendMode, Transform } from "@/lib/rendering";
-import type { RetimeConfig, VisualElement } from "@/lib/timeline";
+import type { Effect, EffectPass } from "@/effects/types";
+import type { Mask } from "@/masks/types";
+import type { BlendMode, Transform } from "@/rendering";
+import type { RetimeConfig, VisualElement } from "@/timeline";
export interface VisualNodeParams {
duration: number;
diff --git a/apps/web/src/services/renderer/resolve.ts b/apps/web/src/services/renderer/resolve.ts
index 3f30f0d5..578c9603 100644
--- a/apps/web/src/services/renderer/resolve.ts
+++ b/apps/web/src/services/renderer/resolve.ts
@@ -5,20 +5,20 @@ import {
resolveGraphicParamsAtTime,
resolveOpacityAtTime,
resolveTransformAtTime,
-} from "@/lib/animation";
-import { resolveEffectParamsAtTime } from "@/lib/animation/effect-param-channel";
+} from "@/animation";
+import { resolveEffectParamsAtTime } from "@/animation/effect-param-channel";
import {
buildGaussianBlurPasses,
intensityToSigma,
-} from "@/lib/effects/definitions/blur";
-import { effectsRegistry, resolveEffectPasses } from "@/lib/effects";
-import type { Effect, EffectPass } from "@/lib/effects/types";
-import { getSourceTimeAtClipTime } from "@/lib/retime";
-import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/lib/graphics";
+} from "@/effects/definitions/blur";
+import { effectsRegistry, resolveEffectPasses } from "@/effects";
+import type { Effect, EffectPass } from "@/effects/types";
+import { getSourceTimeAtClipTime } from "@/retime";
+import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/graphics";
import {
getTextMeasurementContext,
measureTextElement,
-} from "@/lib/text/measure-element";
+} from "@/text/measure-element";
import { videoCache } from "@/services/video-cache/service";
import type { CanvasRenderer } from "./canvas-renderer";
import type { AnyBaseNode } from "./nodes/base-node";
diff --git a/apps/web/src/services/renderer/scene-builder.ts b/apps/web/src/services/renderer/scene-builder.ts
index 47e6139c..e79c62d4 100644
--- a/apps/web/src/services/renderer/scene-builder.ts
+++ b/apps/web/src/services/renderer/scene-builder.ts
@@ -1,5 +1,5 @@
-import type { SceneTracks, TimelineTrack } from "@/lib/timeline";
-import type { MediaAsset } from "@/lib/media/types";
+import type { SceneTracks, TimelineTrack } from "@/timeline";
+import type { MediaAsset } from "@/media/types";
import { RootNode } from "./nodes/root-node";
import { VideoNode } from "./nodes/video-node";
import { ImageNode } from "./nodes/image-node";
@@ -10,8 +10,8 @@ import { ColorNode } from "./nodes/color-node";
import { BlurBackgroundNode } from "./nodes/blur-background-node";
import { EffectLayerNode } from "./nodes/effect-layer-node";
import type { AnyBaseNode } from "./nodes/base-node";
-import type { TBackground, TCanvasSize } from "@/lib/project/types";
-import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/lib/background/blur";
+import type { TBackground, TCanvasSize } from "@/project/types";
+import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur";
const PREVIEW_MAX_IMAGE_SIZE = 2048;
diff --git a/apps/web/src/services/renderer/scene-exporter.ts b/apps/web/src/services/renderer/scene-exporter.ts
index b19005cb..1b3267a7 100644
--- a/apps/web/src/services/renderer/scene-exporter.ts
+++ b/apps/web/src/services/renderer/scene-exporter.ts
@@ -14,10 +14,10 @@ import {
} from "mediabunny";
import type { FrameRate } from "opencut-wasm";
import { mediaTimeToSeconds } from "opencut-wasm";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
-import { frameRateToFloat } from "@/lib/fps/utils";
+import { TICKS_PER_SECOND } from "@/wasm";
+import { frameRateToFloat } from "@/fps/utils";
import type { RootNode } from "./nodes/root-node";
-import type { ExportFormat, ExportQuality } from "@/lib/export";
+import type { ExportFormat, ExportQuality } from "@/export";
import { CanvasRenderer } from "./canvas-renderer";
type ExportParams = {
diff --git a/apps/web/src/components/editor/dialogs/storage-persistence-dialog.tsx b/apps/web/src/services/storage/components/storage-persistence-dialog.tsx
index 0e854a93..4544ef1d 100644
--- a/apps/web/src/components/editor/dialogs/storage-persistence-dialog.tsx
+++ b/apps/web/src/services/storage/components/storage-persistence-dialog.tsx
@@ -9,7 +9,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
-import { useStoragePersistence } from "@/hooks/use-storage-persistence";
+import { useStoragePersistence } from "@/services/storage/use-storage-persistence";
export function StoragePersistenceDialog() {
const { showDialog, onConfirm, onDismiss } = useStoragePersistence();
diff --git a/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts b/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts
index 9cea9a2e..6d0c37fb 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test";
-import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/lib/background/blur";
-import { DEFAULT_BACKGROUND_COLOR } from "@/lib/background/color";
-import { DEFAULT_CANVAS_SIZE } from "@/lib/canvas/sizes";
+import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur";
+import { DEFAULT_BACKGROUND_COLOR } from "@/background/color";
+import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes";
const DEFAULT_FPS = 30;
import type { MediaAssetData } from "@/services/storage/types";
import { getProjectId, transformProjectV1ToV2 } from "../transformers/v1-to-v2";
diff --git a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
index a19269cd..c4b64e50 100644
--- a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
@@ -1,6 +1,6 @@
-import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/lib/background/blur";
-import { DEFAULT_BACKGROUND_COLOR } from "@/lib/background/color";
-import { DEFAULT_CANVAS_SIZE } from "@/lib/canvas/sizes";
+import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur";
+import { DEFAULT_BACKGROUND_COLOR } from "@/background/color";
+import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes";
const DEFAULT_FPS = 30;
import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
import type { MediaAssetData } from "@/services/storage/types";
diff --git a/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts b/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts
index 36e3d9a8..5f3c01e9 100644
--- a/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts
@@ -1,4 +1,4 @@
-import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/lib/stickers/intrinsic-size";
+import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/stickers/intrinsic-size";
import type { MigrationResult, ProjectRecord } from "./types";
import { getProjectId, isRecord } from "./utils";
diff --git a/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts b/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts
index 5dcf6feb..3ef6cde9 100644
--- a/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts
@@ -1,6 +1,6 @@
import type { MigrationResult, ProjectRecord } from "./types";
-import { VOLUME_DB_MIN } from "@/lib/timeline/audio-constants";
-import { clampDb } from "@/lib/timeline/audio-state";
+import { VOLUME_DB_MIN } from "@/timeline/audio-constants";
+import { clampDb } from "@/timeline/audio-state";
import { getProjectId, isRecord } from "./utils";
export function transformProjectV17ToV18({
diff --git a/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts b/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts
index f5194bda..6c30e5c8 100644
--- a/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts
@@ -1,4 +1,4 @@
-import { INTENSITY_TO_SIGMA_DIVISOR } from "@/lib/effects/definitions/blur";
+import { INTENSITY_TO_SIGMA_DIVISOR } from "@/effects/definitions/blur";
import type { MigrationResult, ProjectRecord } from "./types";
import { getProjectId, isRecord } from "./utils";
diff --git a/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts b/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts
index cde1294e..5e3adc33 100644
--- a/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts
@@ -1,7 +1,7 @@
import type { MigrationResult, ProjectRecord } from "./types";
import { getProjectId, isRecord } from "./utils";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
+import { TICKS_PER_SECOND } from "@/wasm";
const ARBITRARY_FPS_DENOMINATOR = 1_000_000;
const STANDARD_FRAME_RATES = [
{ value: 24_000 / 1_001, numerator: 24_000, denominator: 1_001 },
diff --git a/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts b/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts
index 186ae1e9..820b0087 100644
--- a/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts
@@ -1,4 +1,4 @@
-import { parseCustomMaskPath } from "@/lib/masks/custom-path";
+import { parseCustomMaskPath } from "@/masks/custom-path";
import type { MigrationResult, ProjectRecord } from "./types";
import { getProjectId, isRecord } from "./utils";
diff --git a/apps/web/src/services/storage/service.ts b/apps/web/src/services/storage/service.ts
index 1aea0297..8ef77846 100644
--- a/apps/web/src/services/storage/service.ts
+++ b/apps/web/src/services/storage/service.ts
@@ -1,6 +1,6 @@
-import type { TProject, TProjectMetadata } from "@/lib/project/types";
-import { getProjectDurationFromScenes } from "@/lib/scenes";
-import type { MediaAsset } from "@/lib/media/types";
+import type { TProject, TProjectMetadata } from "@/project/types";
+import { getProjectDurationFromScenes } from "@/timeline/scenes";
+import type { MediaAsset } from "@/media/types";
import { IndexedDBAdapter } from "./indexeddb-adapter";
import { OPFSAdapter } from "./opfs-adapter";
import {
@@ -16,12 +16,12 @@ import type {
SerializedProject,
SerializedScene,
} from "./types";
-import type { SavedSoundsData, SavedSound, SoundEffect } from "@/lib/sounds/types";
+import type { SavedSoundsData, SavedSound, SoundEffect } from "@/sounds/types";
import {
migrations,
runStorageMigrations,
} from "@/services/storage/migrations";
-import type { Bookmark, SceneTracks, TScene } from "@/lib/timeline";
+import type { Bookmark, SceneTracks, TScene } from "@/timeline";
function normalizeBookmarks({ raw }: { raw: unknown }): Bookmark[] {
if (!Array.isArray(raw)) return [];
diff --git a/apps/web/src/services/storage/types.ts b/apps/web/src/services/storage/types.ts
index 1dcf6545..58c14b49 100644
--- a/apps/web/src/services/storage/types.ts
+++ b/apps/web/src/services/storage/types.ts
@@ -1,10 +1,10 @@
-import type { MediaType } from "@/lib/media/types";
+import type { MediaType } from "@/media/types";
import type {
TProject,
TProjectMetadata,
TTimelineViewState,
-} from "@/lib/project/types";
-import type { TScene } from "@/lib/timeline";
+} from "@/project/types";
+import type { TScene } from "@/timeline";
export interface StorageAdapter<T> {
get(key: string): Promise<T | null>;
diff --git a/apps/web/src/hooks/storage/use-local-storage.ts b/apps/web/src/services/storage/use-local-storage.ts
index d0188ab3..d0188ab3 100644
--- a/apps/web/src/hooks/storage/use-local-storage.ts
+++ b/apps/web/src/services/storage/use-local-storage.ts
diff --git a/apps/web/src/hooks/use-storage-persistence.ts b/apps/web/src/services/storage/use-storage-persistence.ts
index a026b510..a026b510 100644
--- a/apps/web/src/hooks/use-storage-persistence.ts
+++ b/apps/web/src/services/storage/use-storage-persistence.ts
diff --git a/apps/web/src/services/transcription/service.ts b/apps/web/src/services/transcription/service.ts
index 443a4230..9fa17a87 100644
--- a/apps/web/src/services/transcription/service.ts
+++ b/apps/web/src/services/transcription/service.ts
@@ -3,11 +3,11 @@ import type {
TranscriptionResult,
TranscriptionProgress,
TranscriptionModelId,
-} from "@/lib/transcription/types";
+} from "@/transcription/types";
import {
DEFAULT_TRANSCRIPTION_MODEL,
TRANSCRIPTION_MODELS,
-} from "@/lib/transcription/models";
+} from "@/transcription/models";
import type { WorkerMessage, WorkerResponse } from "./worker";
type ProgressCallback = (progress: TranscriptionProgress) => void;
diff --git a/apps/web/src/services/transcription/worker.ts b/apps/web/src/services/transcription/worker.ts
index 928b580e..0ced6484 100644
--- a/apps/web/src/services/transcription/worker.ts
+++ b/apps/web/src/services/transcription/worker.ts
@@ -3,11 +3,11 @@ import {
type AutomaticSpeechRecognitionPipeline,
type AutomaticSpeechRecognitionOutput,
} from "@huggingface/transformers";
-import type { TranscriptionSegment } from "@/lib/transcription/types";
+import type { TranscriptionSegment } from "@/transcription/types";
import {
DEFAULT_CHUNK_LENGTH_SECONDS,
DEFAULT_STRIDE_SECONDS,
-} from "@/lib/transcription/audio";
+} from "@/transcription/audio";
export type WorkerMessage =
| { type: "init"; modelId: string }
diff --git a/apps/web/src/services/waveform-cache/service.ts b/apps/web/src/services/waveform-cache/service.ts
index 3b7d34f3..f37cf98a 100644
--- a/apps/web/src/services/waveform-cache/service.ts
+++ b/apps/web/src/services/waveform-cache/service.ts
@@ -1,10 +1,10 @@
"use client";
-import { createAudioContext } from "@/lib/media/audio";
+import { createAudioContext } from "@/media/audio";
import {
buildSourceWaveformSummary,
type SourceWaveformSummary,
-} from "@/lib/media/waveform-summary";
+} from "@/media/waveform-summary";
interface GetSourceWaveformSummaryArgs {
sourceKey: string;
diff --git a/apps/web/src/lib/site/brand.ts b/apps/web/src/site/brand.ts
index 2701608c..2701608c 100644
--- a/apps/web/src/lib/site/brand.ts
+++ b/apps/web/src/site/brand.ts
diff --git a/apps/web/src/lib/site/external-tools.ts b/apps/web/src/site/external-tools.ts
index 610ea158..610ea158 100644
--- a/apps/web/src/lib/site/external-tools.ts
+++ b/apps/web/src/site/external-tools.ts
diff --git a/apps/web/src/lib/site/social.ts b/apps/web/src/site/social.ts
index 571e45d5..571e45d5 100644
--- a/apps/web/src/lib/site/social.ts
+++ b/apps/web/src/site/social.ts
diff --git a/apps/web/src/lib/site/sponsors.ts b/apps/web/src/site/sponsors.ts
index 2bd201b7..2bd201b7 100644
--- a/apps/web/src/lib/site/sponsors.ts
+++ b/apps/web/src/site/sponsors.ts
diff --git a/apps/web/src/components/editor/panels/assets/views/sounds.tsx b/apps/web/src/sounds/components/assets-view.tsx
index c7a1029c..fc7528c8 100644
--- a/apps/web/src/components/editor/panels/assets/views/sounds.tsx
+++ b/apps/web/src/sounds/components/assets-view.tsx
@@ -22,9 +22,9 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useInfiniteScroll } from "@/hooks/use-infinite-scroll";
-import { useSoundSearch } from "@/hooks/use-sound-search";
-import { useSoundsStore } from "@/stores/sounds-store";
-import type { SavedSound, SoundEffect } from "@/lib/sounds/types";
+import { useSoundSearch } from "@/sounds/use-sound-search";
+import { useSoundsStore } from "@/sounds/sounds-store";
+import type { SavedSound, SoundEffect } from "@/sounds/types";
import { cn } from "@/utils/ui";
import {
FavouriteIcon,
diff --git a/apps/web/src/stores/sounds-store.ts b/apps/web/src/sounds/sounds-store.ts
index b4b5772b..b9b3f070 100644
--- a/apps/web/src/stores/sounds-store.ts
+++ b/apps/web/src/sounds/sounds-store.ts
@@ -1,9 +1,9 @@
import { create } from "zustand";
-import type { SoundEffect, SavedSound } from "@/lib/sounds/types";
+import type { SoundEffect, SavedSound } from "@/sounds/types";
import { storageService } from "@/services/storage/service";
import { toast } from "sonner";
import { EditorCore } from "@/core";
-import { buildLibraryAudioElement } from "@/lib/timeline/element-utils";
+import { buildLibraryAudioElement } from "@/timeline/element-utils";
interface SoundsStore {
topSoundEffects: SoundEffect[];
diff --git a/apps/web/src/lib/sounds/types.ts b/apps/web/src/sounds/types.ts
index f7526846..f7526846 100644
--- a/apps/web/src/lib/sounds/types.ts
+++ b/apps/web/src/sounds/types.ts
diff --git a/apps/web/src/hooks/use-sound-search.ts b/apps/web/src/sounds/use-sound-search.ts
index 263a01e2..79bc3110 100644
--- a/apps/web/src/hooks/use-sound-search.ts
+++ b/apps/web/src/sounds/use-sound-search.ts
@@ -1,5 +1,5 @@
import { useEffect } from "react";
-import { useSoundsStore } from "@/stores/sounds-store";
+import { useSoundsStore } from "@/sounds/sounds-store";
export function useSoundSearch({
query,
diff --git a/apps/web/src/components/editor/panels/properties/tabs/speed-tab.tsx b/apps/web/src/speed/components/speed-tab.tsx
index cb10aac3..2f9231d6 100644
--- a/apps/web/src/components/editor/panels/properties/tabs/speed-tab.tsx
+++ b/apps/web/src/speed/components/speed-tab.tsx
@@ -1,18 +1,18 @@
import { useRef } from "react";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import { NumberField } from "@/components/ui/number-field";
import { Switch } from "@/components/ui/switch";
import { HugeiconsIcon } from "@hugeicons/react";
import { DashboardSpeed02Icon } from "@hugeicons/core-free-icons";
-import { buildConstantRetime } from "@/lib/retime";
+import { buildConstantRetime } from "@/retime";
import {
DEFAULT_RETIME_RATE,
MIN_RETIME_RATE,
MAX_RETIME_RATE,
clampRetimeRate,
canMaintainPitch,
-} from "@/lib/retime/rate";
-import type { AudioElement, VideoElement } from "@/lib/timeline";
+} from "@/retime/rate";
+import type { AudioElement, VideoElement } from "@/timeline";
import {
Section,
SectionContent,
@@ -21,7 +21,7 @@ import {
SectionHeader,
SectionTitle,
} from "@/components/section";
-import { usePropertyDraft } from "../hooks/use-property-draft";
+import { usePropertyDraft } from "@/components/editor/panels/properties/hooks/use-property-draft";
import {
formatNumberForDisplay,
getFractionDigitsForStep,
diff --git a/apps/web/src/lib/stickers/__tests__/sticker-id.test.ts b/apps/web/src/stickers/__tests__/sticker-id.test.ts
index 26458a7c..26458a7c 100644
--- a/apps/web/src/lib/stickers/__tests__/sticker-id.test.ts
+++ b/apps/web/src/stickers/__tests__/sticker-id.test.ts
diff --git a/apps/web/src/lib/stickers/categories.ts b/apps/web/src/stickers/categories.ts
index e8a8b71b..e8a8b71b 100644
--- a/apps/web/src/lib/stickers/categories.ts
+++ b/apps/web/src/stickers/categories.ts
diff --git a/apps/web/src/components/editor/panels/assets/views/stickers.tsx b/apps/web/src/stickers/components/assets-view.tsx
index 318aaccb..aab84c12 100644
--- a/apps/web/src/components/editor/panels/assets/views/stickers.tsx
+++ b/apps/web/src/stickers/components/assets-view.tsx
@@ -9,22 +9,22 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/spinner";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { useEditor } from "@/hooks/use-editor";
-import { resolveStickerIntrinsicSize } from "@/lib/stickers";
+import { useEditor } from "@/editor/use-editor";
+import { resolveStickerIntrinsicSize } from "@/stickers";
import {
buildGraphicElement,
buildStickerElement,
-} from "@/lib/timeline/element-utils";
-import { STICKER_CATEGORIES } from "@/lib/stickers/categories";
-import { getRegionLabel, resolveQueryToRegions } from "@/lib/stickers";
-import { parseShapeStickerId } from "@/lib/stickers/providers/shapes";
-import type { TimelineDragData } from "@/lib/timeline/drag";
+} from "@/timeline/element-utils";
+import { STICKER_CATEGORIES } from "@/stickers/categories";
+import { getRegionLabel, resolveQueryToRegions } from "@/stickers";
+import { parseShapeStickerId } from "@/stickers/providers/shapes";
+import type { TimelineDragData } from "@/timeline/drag";
import type {
StickerBrowseSection,
StickerCategory,
StickerItem as StickerData,
-} from "@/lib/stickers";
-import { useStickersStore } from "@/stores/stickers-store";
+} from "@/stickers";
+import { useStickersStore } from "@/stickers/stickers-store";
import { cn } from "@/utils/ui";
import {
HappyIcon,
diff --git a/apps/web/src/lib/stickers/index.ts b/apps/web/src/stickers/index.ts
index 2d71d097..d41e277d 100644
--- a/apps/web/src/lib/stickers/index.ts
+++ b/apps/web/src/stickers/index.ts
@@ -1,8 +1,8 @@
import {
STICKER_CATEGORIES,
-} from "@/lib/stickers/categories";
-import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/lib/stickers/intrinsic-size";
-import type { StickerCategory } from "@/lib/stickers/types";
+} from "@/stickers/categories";
+import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/stickers/intrinsic-size";
+import type { StickerCategory } from "@/stickers/types";
import { stickersRegistry } from "./registry";
import { resolveStickerId } from "./resolver";
import { registerDefaultStickerProviders } from "./providers";
diff --git a/apps/web/src/lib/stickers/intrinsic-size.ts b/apps/web/src/stickers/intrinsic-size.ts
index 43436237..43436237 100644
--- a/apps/web/src/lib/stickers/intrinsic-size.ts
+++ b/apps/web/src/stickers/intrinsic-size.ts
diff --git a/apps/web/src/lib/stickers/providers/countries-data.ts b/apps/web/src/stickers/providers/countries-data.ts
index 05551699..05551699 100644
--- a/apps/web/src/lib/stickers/providers/countries-data.ts
+++ b/apps/web/src/stickers/providers/countries-data.ts
diff --git a/apps/web/src/lib/stickers/providers/flags.ts b/apps/web/src/stickers/providers/flags.ts
index 0cca292f..0cca292f 100644
--- a/apps/web/src/lib/stickers/providers/flags.ts
+++ b/apps/web/src/stickers/providers/flags.ts
diff --git a/apps/web/src/lib/stickers/providers/index.ts b/apps/web/src/stickers/providers/index.ts
index 830a1129..ca23963c 100644
--- a/apps/web/src/lib/stickers/providers/index.ts
+++ b/apps/web/src/stickers/providers/index.ts
@@ -1,5 +1,5 @@
import { stickersRegistry } from "../registry";
-import type { StickerProvider } from "@/lib/stickers/types";
+import type { StickerProvider } from "@/stickers/types";
import { flagsProvider } from "./flags";
import { logosProvider } from "./logos";
import { shapesProvider } from "./shapes";
diff --git a/apps/web/src/lib/stickers/providers/logos.ts b/apps/web/src/stickers/providers/logos.ts
index 474c64da..474c64da 100644
--- a/apps/web/src/lib/stickers/providers/logos.ts
+++ b/apps/web/src/stickers/providers/logos.ts
diff --git a/apps/web/src/lib/stickers/providers/shapes.ts b/apps/web/src/stickers/providers/shapes.ts
index a23f14a7..e3137425 100644
--- a/apps/web/src/lib/stickers/providers/shapes.ts
+++ b/apps/web/src/stickers/providers/shapes.ts
@@ -1,5 +1,5 @@
-import { buildGraphicPreviewUrl, buildDefaultGraphicInstance, graphicsRegistry, registerDefaultGraphics } from "@/lib/graphics";
-import type { ParamValues } from "@/lib/params";
+import { buildGraphicPreviewUrl, buildDefaultGraphicInstance, graphicsRegistry, registerDefaultGraphics } from "@/graphics";
+import type { ParamValues } from "@/params";
import { buildStickerId, parseStickerId } from "../sticker-id";
import type {
StickerBrowseResult,
diff --git a/apps/web/src/lib/stickers/registry.ts b/apps/web/src/stickers/registry.ts
index 7148c09d..a7d79df0 100644
--- a/apps/web/src/lib/stickers/registry.ts
+++ b/apps/web/src/stickers/registry.ts
@@ -1,5 +1,5 @@
-import type { StickerProvider } from "@/lib/stickers/types";
-import { DefinitionRegistry } from "@/lib/registry";
+import type { StickerProvider } from "@/stickers/types";
+import { DefinitionRegistry } from "@/params/registry";
export class StickersRegistry extends DefinitionRegistry<string, StickerProvider> {
constructor() {
diff --git a/apps/web/src/lib/stickers/resolver.ts b/apps/web/src/stickers/resolver.ts
index 0002e716..a7825bbc 100644
--- a/apps/web/src/lib/stickers/resolver.ts
+++ b/apps/web/src/stickers/resolver.ts
@@ -1,7 +1,7 @@
import { stickersRegistry } from "./registry";
import { parseStickerId } from "./sticker-id";
import { registerDefaultStickerProviders } from "./providers";
-import type { StickerResolveOptions } from "@/lib/stickers/types";
+import type { StickerResolveOptions } from "@/stickers/types";
export function resolveStickerId({
stickerId,
diff --git a/apps/web/src/lib/stickers/sticker-id.ts b/apps/web/src/stickers/sticker-id.ts
index cb56db4c..cb56db4c 100644
--- a/apps/web/src/lib/stickers/sticker-id.ts
+++ b/apps/web/src/stickers/sticker-id.ts
diff --git a/apps/web/src/stores/stickers-store.ts b/apps/web/src/stickers/stickers-store.ts
index 191050dd..aacc5a79 100644
--- a/apps/web/src/stores/stickers-store.ts
+++ b/apps/web/src/stickers/stickers-store.ts
@@ -5,13 +5,13 @@ import {
browseCategory,
searchAll,
searchStickers as searchStickersFromProviders,
-} from "@/lib/stickers";
-import type { StickerBrowseResult, StickerSearchResult } from "@/lib/stickers";
-import { STICKER_CATEGORIES } from "@/lib/stickers/categories";
-import type { StickerCategory } from "@/lib/stickers/types";
-import { registerDefaultStickerProviders } from "@/lib/stickers/providers";
-import { stickersRegistry } from "@/lib/stickers/registry";
-import { parseStickerId } from "@/lib/stickers/sticker-id";
+} from "@/stickers";
+import type { StickerBrowseResult, StickerSearchResult } from "@/stickers";
+import { STICKER_CATEGORIES } from "@/stickers/categories";
+import type { StickerCategory } from "@/stickers/types";
+import { registerDefaultStickerProviders } from "@/stickers/providers";
+import { stickersRegistry } from "@/stickers/registry";
+import { parseStickerId } from "@/stickers/sticker-id";
const MAX_RECENT_STICKERS = 50;
let browseRequestVersion = 0;
diff --git a/apps/web/src/lib/stickers/types.ts b/apps/web/src/stickers/types.ts
index bc7dd490..86aa4299 100644
--- a/apps/web/src/lib/stickers/types.ts
+++ b/apps/web/src/stickers/types.ts
@@ -1,4 +1,4 @@
-import type { STICKER_CATEGORIES } from "@/lib/stickers/categories";
+import type { STICKER_CATEGORIES } from "@/stickers/categories";
export type StickerCategory = keyof typeof STICKER_CATEGORIES;
diff --git a/apps/web/src/stores/properties-store.ts b/apps/web/src/stores/properties-store.ts
deleted file mode 100644
index f45f176d..00000000
--- a/apps/web/src/stores/properties-store.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { create } from "zustand";
-
-interface ClipEffectsTarget {
- elementId: string;
- trackId: string;
-}
-
-interface PropertiesState {
- clipEffectsTarget: ClipEffectsTarget | null;
- openClipEffects: ({ elementId, trackId }: ClipEffectsTarget) => void;
- closeClipEffects: () => void;
-}
-
-export const usePropertiesStore = create<PropertiesState>()((set) => ({
- clipEffectsTarget: null,
- openClipEffects: ({ elementId, trackId }) =>
- set({ clipEffectsTarget: { elementId, trackId } }),
- closeClipEffects: () => set({ clipEffectsTarget: null }),
-}));
diff --git a/apps/web/src/lib/subtitles/ass.ts b/apps/web/src/subtitles/ass.ts
index 030c5b36..030c5b36 100644
--- a/apps/web/src/lib/subtitles/ass.ts
+++ b/apps/web/src/subtitles/ass.ts
diff --git a/apps/web/src/lib/subtitles/build-subtitle-text-element.ts b/apps/web/src/subtitles/build-subtitle-text-element.ts
index 698310a6..c4ac21ac 100644
--- a/apps/web/src/lib/subtitles/build-subtitle-text-element.ts
+++ b/apps/web/src/subtitles/build-subtitle-text-element.ts
@@ -1,12 +1,12 @@
-import { FONT_SIZE_SCALE_REFERENCE } from "@/lib/text/typography";
+import { FONT_SIZE_SCALE_REFERENCE } from "@/text/typography";
import {
getTextVisualRect,
measureTextBlock,
setCanvasLetterSpacing,
-} from "@/lib/text/layout";
-import { DEFAULTS } from "@/lib/timeline/defaults";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
-import type { CreateTextElement } from "@/lib/timeline";
+} from "@/text/layout";
+import { DEFAULTS } from "@/timeline/defaults";
+import { TICKS_PER_SECOND } from "@/wasm";
+import type { CreateTextElement } from "@/timeline";
import type { SubtitleCue, SubtitleStyleOverrides } from "./types";
const SUBTITLE_MAX_WIDTH_RATIO = 0.8;
diff --git a/apps/web/src/components/editor/panels/assets/views/captions.tsx b/apps/web/src/subtitles/components/assets-view.tsx
index 040df749..7e2ddad9 100644
--- a/apps/web/src/components/editor/panels/assets/views/captions.tsx
+++ b/apps/web/src/subtitles/components/assets-view.tsx
@@ -8,21 +8,21 @@ import {
SelectValue,
} from "@/components/ui/select";
import { useReducer, useRef, useState } from "react";
-import { extractTimelineAudio } from "@/lib/media/mediabunny";
-import { useEditor } from "@/hooks/use-editor";
-import { TRANSCRIPTION_DIAGNOSTICS_SCOPE } from "@/lib/transcription/diagnostics";
-import { DEFAULT_TRANSCRIPTION_SAMPLE_RATE } from "@/lib/transcription/audio";
-import { TRANSCRIPTION_LANGUAGES } from "@/lib/transcription/supported-languages";
+import { extractTimelineAudio } from "@/media/mediabunny";
+import { useEditor } from "@/editor/use-editor";
+import { TRANSCRIPTION_DIAGNOSTICS_SCOPE } from "@/transcription/diagnostics";
+import { DEFAULT_TRANSCRIPTION_SAMPLE_RATE } from "@/transcription/audio";
+import { TRANSCRIPTION_LANGUAGES } from "@/transcription/supported-languages";
import type {
CaptionChunk,
TranscriptionLanguage,
TranscriptionProgress,
-} from "@/lib/transcription/types";
+} from "@/transcription/types";
import { transcriptionService } from "@/services/transcription/service";
-import { decodeAudioToFloat32 } from "@/lib/media/audio";
-import { buildCaptionChunks } from "@/lib/transcription/caption";
-import { insertCaptionChunksAsTextTrack } from "@/lib/subtitles/insert";
-import { parseSubtitleFile } from "@/lib/subtitles/parse";
+import { decodeAudioToFloat32 } from "@/media/audio";
+import { buildCaptionChunks } from "@/transcription/caption";
+import { insertCaptionChunksAsTextTrack } from "@/subtitles/insert";
+import { parseSubtitleFile } from "@/subtitles/parse";
import { Spinner } from "@/components/ui/spinner";
import {
Section,
@@ -38,7 +38,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
-import type { DiagnosticSeverity } from "@/lib/diagnostics/types";
+import type { DiagnosticSeverity } from "@/diagnostics/types";
const DIAGNOSTIC_BUTTON_VARIANT: Record<
DiagnosticSeverity,
diff --git a/apps/web/src/lib/subtitles/insert.ts b/apps/web/src/subtitles/insert.ts
index 1bb04fc8..50d6c40b 100644
--- a/apps/web/src/lib/subtitles/insert.ts
+++ b/apps/web/src/subtitles/insert.ts
@@ -3,7 +3,7 @@ import {
AddTrackCommand,
BatchCommand,
InsertElementCommand,
-} from "@/lib/commands";
+} from "@/commands";
import { buildSubtitleTextElement } from "./build-subtitle-text-element";
import type { SubtitleCue } from "./types";
diff --git a/apps/web/src/lib/subtitles/parse.ts b/apps/web/src/subtitles/parse.ts
index 7a2d2342..7a2d2342 100644
--- a/apps/web/src/lib/subtitles/parse.ts
+++ b/apps/web/src/subtitles/parse.ts
diff --git a/apps/web/src/lib/subtitles/srt.ts b/apps/web/src/subtitles/srt.ts
index 197e96f9..197e96f9 100644
--- a/apps/web/src/lib/subtitles/srt.ts
+++ b/apps/web/src/subtitles/srt.ts
diff --git a/apps/web/src/lib/subtitles/types.ts b/apps/web/src/subtitles/types.ts
index 86f6525d..e4f5db3c 100644
--- a/apps/web/src/lib/subtitles/types.ts
+++ b/apps/web/src/subtitles/types.ts
@@ -1,5 +1,5 @@
-import type { TextBackground, TextElement } from "@/lib/timeline";
-import type { CaptionChunk } from "@/lib/transcription/types";
+import type { TextBackground, TextElement } from "@/timeline";
+import type { CaptionChunk } from "@/transcription/types";
export interface SubtitlePlacementStyle {
verticalAlign?: "top" | "middle" | "bottom";
diff --git a/apps/web/src/lib/text/background.ts b/apps/web/src/text/background.ts
index be5ab6f5..be5ab6f5 100644
--- a/apps/web/src/lib/text/background.ts
+++ b/apps/web/src/text/background.ts
diff --git a/apps/web/src/components/editor/panels/assets/views/text.tsx b/apps/web/src/text/components/assets-view.tsx
index da295da3..25a49893 100644
--- a/apps/web/src/components/editor/panels/assets/views/text.tsx
+++ b/apps/web/src/text/components/assets-view.tsx
@@ -1,8 +1,8 @@
import { DraggableItem } from "@/components/editor/panels/assets/draggable-item";
import { PanelView } from "@/components/editor/panels/assets/views/base-panel";
-import { useEditor } from "@/hooks/use-editor";
-import { DEFAULTS } from "@/lib/timeline/defaults";
-import { buildTextElement } from "@/lib/timeline/element-utils";
+import { useEditor } from "@/editor/use-editor";
+import { DEFAULTS } from "@/timeline/defaults";
+import { buildTextElement } from "@/timeline/element-utils";
export function TextView() {
const editor = useEditor();
diff --git a/apps/web/src/components/editor/panels/properties/tabs/text-tab.tsx b/apps/web/src/text/components/text-tab.tsx
index d37379ea..bf14efba 100644
--- a/apps/web/src/components/editor/panels/properties/tabs/text-tab.tsx
+++ b/apps/web/src/text/components/text-tab.tsx
@@ -2,7 +2,7 @@
import { Textarea } from "@/components/ui/textarea";
import { FontPicker } from "@/components/ui/font-picker";
-import type { TextElement } from "@/lib/timeline";
+import type { TextElement } from "@/timeline";
import { NumberField } from "@/components/ui/number-field";
import { useRef } from "react";
import {
@@ -17,23 +17,20 @@ import { ColorPicker } from "@/components/ui/color-picker";
import { Button } from "@/components/ui/button";
import { uppercase } from "@/utils/string";
import { clamp, formatNumberForDisplay } from "@/utils/math";
-import { useEditor } from "@/hooks/use-editor";
-import {
- CORNER_RADIUS_MAX,
- CORNER_RADIUS_MIN,
-} from "@/lib/text/background";
+import { useEditor } from "@/editor/use-editor";
+import { CORNER_RADIUS_MAX, CORNER_RADIUS_MIN } from "@/text/background";
import {
DEFAULT_TEXT_COLOR,
MAX_FONT_SIZE,
MIN_FONT_SIZE,
-} from "@/lib/text/typography";
-import { usePropertyDraft } from "../hooks/use-property-draft";
-import { useKeyframedColorProperty } from "../hooks/use-keyframed-color-property";
-import { useKeyframedNumberProperty } from "../hooks/use-keyframed-number-property";
-import { useElementPlayhead } from "../hooks/use-element-playhead";
-import { KeyframeToggle } from "../components/keyframe-toggle";
-import { isPropertyAtDefault } from "./transform-tab";
-import { resolveColorAtTime, resolveNumberAtTime } from "@/lib/animation";
+} from "@/text/typography";
+import { usePropertyDraft } from "@/components/editor/panels/properties/hooks/use-property-draft";
+import { useKeyframedColorProperty } from "@/components/editor/panels/properties/hooks/use-keyframed-color-property";
+import { useKeyframedNumberProperty } from "@/components/editor/panels/properties/hooks/use-keyframed-number-property";
+import { useElementPlayhead } from "@/components/editor/panels/properties/hooks/use-element-playhead";
+import { KeyframeToggle } from "@/components/editor/panels/properties/components/keyframe-toggle";
+import { isPropertyAtDefault } from "@/rendering/components/transform-tab";
+import { resolveColorAtTime, resolveNumberAtTime } from "@/animation";
import { HugeiconsIcon } from "@hugeicons/react";
import {
MinusSignIcon,
@@ -41,7 +38,7 @@ import {
TextFontIcon,
} from "@hugeicons/core-free-icons";
import { OcTextHeightIcon, OcTextWidthIcon } from "@/components/icons";
-import { DEFAULTS } from "@/lib/timeline/defaults";
+import { DEFAULTS } from "@/timeline/defaults";
import { cn } from "@/utils/ui";
export function TextTab({
diff --git a/apps/web/src/lib/text/layout.ts b/apps/web/src/text/layout.ts
index a3badfdc..aabf5ca9 100644
--- a/apps/web/src/lib/text/layout.ts
+++ b/apps/web/src/text/layout.ts
@@ -1,5 +1,5 @@
-import type { TextBackground, TextElement } from "@/lib/timeline";
-import { DEFAULTS } from "@/lib/timeline/defaults";
+import type { TextBackground, TextElement } from "@/timeline";
+import { DEFAULTS } from "@/timeline/defaults";
type TextRect = {
left: number;
diff --git a/apps/web/src/lib/text/measure-element.ts b/apps/web/src/text/measure-element.ts
index 0b107587..3a49696f 100644
--- a/apps/web/src/lib/text/measure-element.ts
+++ b/apps/web/src/text/measure-element.ts
@@ -1,7 +1,7 @@
-import { CORNER_RADIUS_MIN } from "@/lib/text/background";
-import { resolveNumberAtTime } from "@/lib/animation";
-import { DEFAULTS } from "@/lib/timeline/defaults";
-import type { TextBackground, TextElement } from "@/lib/timeline";
+import { CORNER_RADIUS_MIN } from "@/text/background";
+import { resolveNumberAtTime } from "@/animation";
+import { DEFAULTS } from "@/timeline/defaults";
+import type { TextBackground, TextElement } from "@/timeline";
import {
getTextVisualRect,
} from "./layout";
diff --git a/apps/web/src/lib/text/primitives.ts b/apps/web/src/text/primitives.ts
index 20674c3c..9a9262a4 100644
--- a/apps/web/src/lib/text/primitives.ts
+++ b/apps/web/src/text/primitives.ts
@@ -1,5 +1,5 @@
-import type { TextCanvasContext, TextBlockMeasurement } from "@/lib/text/layout";
-import { DEFAULTS } from "@/lib/timeline/defaults";
+import type { TextCanvasContext, TextBlockMeasurement } from "@/text/layout";
+import { DEFAULTS } from "@/timeline/defaults";
import { clamp } from "@/utils/math";
import { CORNER_RADIUS_MAX, CORNER_RADIUS_MIN } from "./background";
import {
diff --git a/apps/web/src/lib/text/typography.ts b/apps/web/src/text/typography.ts
index 837236ab..837236ab 100644
--- a/apps/web/src/lib/text/typography.ts
+++ b/apps/web/src/text/typography.ts
diff --git a/apps/web/src/lib/timeline/audio-constants.ts b/apps/web/src/timeline/audio-constants.ts
index f5bb8a3e..f5bb8a3e 100644
--- a/apps/web/src/lib/timeline/audio-constants.ts
+++ b/apps/web/src/timeline/audio-constants.ts
diff --git a/apps/web/src/lib/timeline/audio-display.ts b/apps/web/src/timeline/audio-display.ts
index 9958c472..9958c472 100644
--- a/apps/web/src/lib/timeline/audio-display.ts
+++ b/apps/web/src/timeline/audio-display.ts
diff --git a/apps/web/src/lib/timeline/audio-separation/index.ts b/apps/web/src/timeline/audio-separation/index.ts
index 849dbda3..beb488de 100644
--- a/apps/web/src/lib/timeline/audio-separation/index.ts
+++ b/apps/web/src/timeline/audio-separation/index.ts
@@ -1,7 +1,7 @@
-import { cloneAnimations } from "@/lib/animation";
-import type { ElementAnimations } from "@/lib/animation/types";
-import type { MediaAsset } from "@/lib/media/types";
-import { DEFAULTS } from "@/lib/timeline/defaults";
+import { cloneAnimations } from "@/animation";
+import type { ElementAnimations } from "@/animation/types";
+import type { MediaAsset } from "@/media/types";
+import { DEFAULTS } from "@/timeline/defaults";
import type {
CreateUploadAudioElement,
TimelineElement,
diff --git a/apps/web/src/lib/timeline/audio-state.ts b/apps/web/src/timeline/audio-state.ts
index 165ece94..4ef559ea 100644
--- a/apps/web/src/lib/timeline/audio-state.ts
+++ b/apps/web/src/timeline/audio-state.ts
@@ -1,5 +1,5 @@
-import { hasKeyframesForPath } from "@/lib/animation/keyframe-query";
-import { resolveNumberAtTime } from "@/lib/animation/resolve";
+import { hasKeyframesForPath } from "@/animation/keyframe-query";
+import { resolveNumberAtTime } from "@/animation/resolve";
import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "./audio-constants";
import type { TimelineElement } from "./types";
const DEFAULT_STEP_SECONDS = 1 / 60;
@@ -32,7 +32,7 @@ export function hasAnimatedVolume({
});
}
-import { TICKS_PER_SECOND } from "@/lib/wasm";
+import { TICKS_PER_SECOND } from "@/wasm";
export function resolveEffectiveAudioGain({
element,
diff --git a/apps/web/src/components/editor/panels/timeline/bookmarks.tsx b/apps/web/src/timeline/bookmarks/components/bookmarks.tsx
index c432b7d4..20e22c57 100644
--- a/apps/web/src/components/editor/panels/timeline/bookmarks.tsx
+++ b/apps/web/src/timeline/bookmarks/components/bookmarks.tsx
@@ -2,20 +2,18 @@
import { useEffect, useState } from "react";
import type { EditorCore } from "@/core";
-import { useEditor } from "@/hooks/use-editor";
-import type { BookmarkDragState } from "@/hooks/timeline/use-bookmark-drag";
-import {
- DEFAULT_TIMELINE_BOOKMARK_COLOR,
-} from "./theme";
-import { TIMELINE_BOOKMARK_ROW_HEIGHT_PX } from "./layout";
-import { DEFAULT_FPS } from "@/lib/fps/defaults";
+import { useEditor } from "@/editor/use-editor";
+import type { BookmarkDragState } from "../hooks/use-bookmark-drag";
+import { DEFAULT_TIMELINE_BOOKMARK_COLOR } from "@/timeline/components/theme";
+import { TIMELINE_BOOKMARK_ROW_HEIGHT_PX } from "@/timeline/components/layout";
+import { DEFAULT_FPS } from "@/fps/defaults";
import { snappedSeekTime } from "opencut-wasm";
import {
ArrowTurnBackwardIcon,
Delete02Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
-import type { Bookmark } from "@/lib/timeline";
+import type { Bookmark } from "@/timeline";
import {
Popover,
PopoverAnchor,
@@ -27,10 +25,7 @@ import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { uppercase } from "@/utils/string";
import { clamp, formatNumberForDisplay } from "@/utils/math";
-import {
- timelineTimeToPixels,
- timelineTimeToSnappedPixels,
-} from "@/lib/timeline";
+import { timelineTimeToPixels, timelineTimeToSnappedPixels } from "@/timeline";
const MIN_BOOKMARK_WIDTH_PX = 2;
const BOOKMARK_MARKER_WIDTH_PX = 12;
@@ -360,7 +355,10 @@ function BookmarkPopoverContent({
/>
{bookmark.color &&
bookmark.color.replace(/^#/, "").toUpperCase() !==
- DEFAULT_TIMELINE_BOOKMARK_COLOR.replace(/^#/, "").toUpperCase() && (
+ DEFAULT_TIMELINE_BOOKMARK_COLOR.replace(
+ /^#/,
+ "",
+ ).toUpperCase() && (
<Button
type="button"
variant="text"
diff --git a/apps/web/src/hooks/timeline/use-bookmark-drag.ts b/apps/web/src/timeline/bookmarks/hooks/use-bookmark-drag.ts
index 3ff1cb21..1071c6dd 100644
--- a/apps/web/src/hooks/timeline/use-bookmark-drag.ts
+++ b/apps/web/src/timeline/bookmarks/hooks/use-bookmark-drag.ts
@@ -5,17 +5,22 @@ import {
useRef,
type RefObject,
} from "react";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import { useShiftKey } from "@/hooks/use-shift-key";
-import { TIMELINE_DRAG_THRESHOLD_PX } from "@/components/editor/panels/timeline/interaction";
+import { TIMELINE_DRAG_THRESHOLD_PX } from "@/timeline/components/interaction";
import { roundToFrame } from "opencut-wasm";
-import { getMouseTimeFromClientX } from "@/lib/timeline/drag-utils";
+import { getMouseTimeFromClientX } from "@/timeline/drag-utils";
import {
- findSnapPoints,
- snapToNearestPoint,
+ buildTimelineSnapPoints,
+ getTimelineSnapThresholdInTicks,
+ resolveTimelineSnap,
type SnapPoint,
-} from "@/lib/timeline/snap-utils";
-import type { Bookmark } from "@/lib/timeline";
+} from "@/timeline/snapping";
+import { getBookmarkSnapPoints } from "../snap-source";
+import { getElementEdgeSnapPoints } from "@/timeline/element-snap-source";
+import { getPlayheadSnapPoints } from "@/timeline/playhead-snap-source";
+import { getAnimationKeyframeSnapPointsForTimeline } from "@/animation/timeline-snap-points";
+import type { Bookmark } from "@/timeline";
export interface BookmarkDragState {
isDragging: boolean;
@@ -97,23 +102,32 @@ export function useBookmarkDrag({
return { snappedTime: rawTime, snapPoint: null };
}
- const snapPoints = findSnapPoints({
- tracks,
- playheadTime,
- bookmarks,
- excludeBookmarkTime,
+ const snapPoints = buildTimelineSnapPoints({
+ sources: [
+ () => getElementEdgeSnapPoints({ tracks }),
+ () => getPlayheadSnapPoints({ playheadTime }),
+ () => getBookmarkSnapPoints({ bookmarks, excludeBookmarkTime }),
+ () => getAnimationKeyframeSnapPointsForTimeline({ tracks }),
+ ],
});
- const result = snapToNearestPoint({
+ const result = resolveTimelineSnap({
targetTime: rawTime,
snapPoints,
- zoomLevel,
+ maxSnapDistance: getTimelineSnapThresholdInTicks({ zoomLevel }),
});
return {
snappedTime: result.snappedTime,
snapPoint: result.snapPoint,
};
},
- [snappingEnabled, tracks, playheadTime, bookmarks, zoomLevel, isShiftHeldRef],
+ [
+ snappingEnabled,
+ tracks,
+ playheadTime,
+ bookmarks,
+ zoomLevel,
+ isShiftHeldRef,
+ ],
);
useEffect(() => {
@@ -148,7 +162,11 @@ export function useBookmarkDrag({
zoomLevel,
scrollLeft,
});
- const frameSnappedTime = roundToFrame({ time: Math.max(0, Math.min(mouseTime, duration)), rate: activeProject.settings.fps }) ?? Math.max(0, Math.min(mouseTime, duration));
+ const frameSnappedTime =
+ roundToFrame({
+ time: Math.max(0, Math.min(mouseTime, duration)),
+ rate: activeProject.settings.fps,
+ }) ?? Math.max(0, Math.min(mouseTime, duration));
const { snappedTime: initialTime } = getSnapResult({
rawTime: frameSnappedTime,
excludeBookmarkTime: bookmarkTime,
@@ -176,9 +194,11 @@ export function useBookmarkDrag({
scrollLeft,
});
const clampedTime = Math.max(0, Math.min(mouseTime, duration));
- const frameSnappedTime = roundToFrame({ time: clampedTime, rate: activeProject.settings.fps }) ?? clampedTime;
- const snapResult = getSnapResult({
- rawTime: frameSnappedTime,
+ const frameSnappedTime =
+ roundToFrame({ time: clampedTime, rate: activeProject.settings.fps }) ??
+ clampedTime;
+ const snapResult = getSnapResult({
+ rawTime: frameSnappedTime,
excludeBookmarkTime: dragState.bookmarkTime,
});
diff --git a/apps/web/src/timeline/bookmarks/index.ts b/apps/web/src/timeline/bookmarks/index.ts
new file mode 100644
index 00000000..667979b4
--- /dev/null
+++ b/apps/web/src/timeline/bookmarks/index.ts
@@ -0,0 +1,19 @@
+export {
+ findBookmarkIndex,
+ isBookmarkAtTime,
+ toggleBookmarkInArray,
+ removeBookmarkFromArray,
+ updateBookmarkInArray,
+ moveBookmarkInArray,
+ getFrameTime,
+ getBookmarkAtTime,
+ getBookmarksActiveAtTime,
+} from "./utils";
+export { getBookmarkSnapPoints } from "./snap-source";
+export {
+ bookmarkNotesPreviewOverlay,
+ getBookmarkPreviewOverlaySource,
+} from "./preview-overlay-source";
+export { useBookmarkDrag } from "./hooks/use-bookmark-drag";
+export type { BookmarkDragState } from "./hooks/use-bookmark-drag";
+export { TimelineBookmarksRow } from "./components/bookmarks";
diff --git a/apps/web/src/timeline/bookmarks/preview-overlay-source.tsx b/apps/web/src/timeline/bookmarks/preview-overlay-source.tsx
new file mode 100644
index 00000000..8e2b4bcf
--- /dev/null
+++ b/apps/web/src/timeline/bookmarks/preview-overlay-source.tsx
@@ -0,0 +1,85 @@
+import type { Bookmark } from "@/timeline";
+import {
+ EMPTY_PREVIEW_OVERLAY_SOURCE_RESULT,
+ type PreviewOverlayDefinition,
+ type PreviewOverlaySourceResult,
+} from "@/preview/overlays";
+import { getBookmarksActiveAtTime } from "./utils";
+
+export const bookmarkNotesPreviewOverlay: PreviewOverlayDefinition = {
+ id: "bookmark-notes",
+ label: "Show bookmark notes",
+ defaultVisible: true,
+};
+
+function BookmarkNotesOverlay({
+ bookmarks,
+}: {
+ bookmarks: Array<{ time: number; note: string; color?: string }>;
+}) {
+ return (
+ <div className="flex flex-col gap-1.5" aria-live="polite">
+ {bookmarks.map((bookmark) => (
+ <div
+ key={bookmark.time}
+ className="flex max-w-[min(200px,50vw)] px-2.5 py-1.5 text-left text-white text-xs shadow-md backdrop-blur-sm"
+ style={{
+ backgroundColor: "rgb(0 0 0 / 0.5)",
+ borderLeft: bookmark.color
+ ? `3px solid ${bookmark.color}`
+ : "3px solid var(--primary)",
+ }}
+ >
+ {bookmark.note}
+ </div>
+ ))}
+ </div>
+ );
+}
+
+export function getBookmarkPreviewOverlaySource({
+ bookmarks,
+ time,
+ isVisible,
+}: {
+ bookmarks: Bookmark[];
+ time: number;
+ isVisible: boolean;
+}): PreviewOverlaySourceResult {
+ const bookmarksWithNotes = getBookmarksActiveAtTime({
+ bookmarks,
+ time,
+ }).flatMap((bookmark) => {
+ if (bookmark.note == null || bookmark.note.trim() === "") {
+ return [];
+ }
+
+ return [
+ {
+ time: bookmark.time,
+ note: bookmark.note,
+ color: bookmark.color,
+ },
+ ];
+ });
+
+ if (!isVisible || bookmarksWithNotes.length === 0) {
+ return {
+ ...EMPTY_PREVIEW_OVERLAY_SOURCE_RESULT,
+ definitions: [bookmarkNotesPreviewOverlay],
+ };
+ }
+
+ return {
+ definitions: [bookmarkNotesPreviewOverlay],
+ instances: [
+ {
+ id: bookmarkNotesPreviewOverlay.id,
+ mount: { kind: "hud", anchor: "top-left", order: 0 },
+ plane: "over-interaction",
+ pointerEvents: "none",
+ render: () => <BookmarkNotesOverlay bookmarks={bookmarksWithNotes} />,
+ },
+ ],
+ };
+}
diff --git a/apps/web/src/timeline/bookmarks/snap-source.ts b/apps/web/src/timeline/bookmarks/snap-source.ts
new file mode 100644
index 00000000..32a84baa
--- /dev/null
+++ b/apps/web/src/timeline/bookmarks/snap-source.ts
@@ -0,0 +1,20 @@
+import type { Bookmark } from "@/timeline";
+import type { SnapPoint } from "@/timeline/snapping";
+
+export function getBookmarkSnapPoints({
+ bookmarks,
+ excludeBookmarkTime,
+}: {
+ bookmarks: Bookmark[];
+ excludeBookmarkTime?: number;
+}): SnapPoint[] {
+ return bookmarks.flatMap((bookmark) => {
+ if (excludeBookmarkTime != null && bookmark.time === excludeBookmarkTime) {
+ return [];
+ }
+
+ return [
+ { time: bookmark.time, type: "bookmark" satisfies SnapPoint["type"] },
+ ];
+ });
+}
diff --git a/apps/web/src/lib/timeline/bookmarks.ts b/apps/web/src/timeline/bookmarks/utils.ts
index 1fbf17bf..d05fe3a9 100644
--- a/apps/web/src/lib/timeline/bookmarks.ts
+++ b/apps/web/src/timeline/bookmarks/utils.ts
@@ -1,4 +1,4 @@
-import type { Bookmark } from "@/lib/timeline";
+import type { Bookmark } from "@/timeline";
import type { FrameRate } from "opencut-wasm";
import { roundToFrame } from "opencut-wasm";
diff --git a/apps/web/src/components/editor/panels/properties/tabs/audio-tab.tsx b/apps/web/src/timeline/components/audio-tab.tsx
index 5ac0fbb7..1dbb3d1a 100644
--- a/apps/web/src/components/editor/panels/properties/tabs/audio-tab.tsx
+++ b/apps/web/src/timeline/components/audio-tab.tsx
@@ -1,8 +1,8 @@
import { Button } from "@/components/ui/button";
import { NumberField } from "@/components/ui/number-field";
-import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/lib/timeline/audio-constants";
-import { isSourceAudioSeparated } from "@/lib/timeline/audio-separation";
-import { DEFAULTS } from "@/lib/timeline/defaults";
+import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/timeline/audio-constants";
+import { isSourceAudioSeparated } from "@/timeline/audio-separation";
+import { DEFAULTS } from "@/timeline/defaults";
import {
clamp,
formatNumberForDisplay,
@@ -10,12 +10,12 @@ import {
isNearlyEqual,
snapToStep,
} from "@/utils/math";
-import type { AudioElement, VideoElement } from "@/lib/timeline";
-import { resolveNumberAtTime } from "@/lib/animation";
-import { useEditor } from "@/hooks/use-editor";
-import { useElementPlayhead } from "../hooks/use-element-playhead";
-import { useKeyframedNumberProperty } from "../hooks/use-keyframed-number-property";
-import { KeyframeToggle } from "../components/keyframe-toggle";
+import type { AudioElement, VideoElement } from "@/timeline";
+import { resolveNumberAtTime } from "@/animation";
+import { useEditor } from "@/editor/use-editor";
+import { useElementPlayhead } from "@/components/editor/panels/properties/hooks/use-element-playhead";
+import { useKeyframedNumberProperty } from "@/components/editor/panels/properties/hooks/use-keyframed-number-property";
+import { KeyframeToggle } from "@/components/editor/panels/properties/components/keyframe-toggle";
import { HugeiconsIcon } from "@hugeicons/react";
import { VolumeHighIcon } from "@hugeicons/core-free-icons";
import {
diff --git a/apps/web/src/components/editor/panels/timeline/audio-volume-line.tsx b/apps/web/src/timeline/components/audio-volume-line.tsx
index 208f3beb..1baac2bd 100644
--- a/apps/web/src/components/editor/panels/timeline/audio-volume-line.tsx
+++ b/apps/web/src/timeline/components/audio-volume-line.tsx
@@ -2,15 +2,15 @@
import { useCallback, useRef, useState } from "react";
import { createPortal } from "react-dom";
-import { useEditor } from "@/hooks/use-editor";
-import { DEFAULTS } from "@/lib/timeline/defaults";
+import { useEditor } from "@/editor/use-editor";
+import { DEFAULTS } from "@/timeline/defaults";
import {
getDbFromLinePos,
getLinePosFromDb,
-} from "@/lib/timeline/audio-display";
-import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/lib/timeline/audio-constants";
-import { hasAnimatedVolume } from "@/lib/timeline/audio-state";
-import type { AudioElement } from "@/lib/timeline/types";
+} from "@/timeline/audio-display";
+import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/timeline/audio-constants";
+import { hasAnimatedVolume } from "@/timeline/audio-state";
+import type { AudioElement } from "@/timeline/types";
import {
clamp,
formatNumberForDisplay,
diff --git a/apps/web/src/components/editor/panels/timeline/audio-waveform.tsx b/apps/web/src/timeline/components/audio-waveform.tsx
index a94267dd..7777bc09 100644
--- a/apps/web/src/components/editor/panels/timeline/audio-waveform.tsx
+++ b/apps/web/src/timeline/components/audio-waveform.tsx
@@ -7,9 +7,9 @@ import {
buildWaveformSampleBuckets,
sampleSourceWaveformSummary,
type SourceWaveformSummary,
-} from "@/lib/media/waveform-summary";
-import type { RetimeConfig } from "@/lib/timeline";
-import { getBarFractionFromOutputAmplitude } from "@/lib/timeline/audio-display";
+} from "@/media/waveform-summary";
+import type { RetimeConfig } from "@/timeline";
+import { getBarFractionFromOutputAmplitude } from "@/timeline/audio-display";
import { waveformCache } from "@/services/waveform-cache/service";
import { findScrollParent } from "@/utils/browser";
import { cn } from "@/utils/ui";
diff --git a/apps/web/src/components/editor/panels/timeline/drag-line.tsx b/apps/web/src/timeline/components/drag-line.tsx
index 89a4c93f..49621f5c 100644
--- a/apps/web/src/components/editor/panels/timeline/drag-line.tsx
+++ b/apps/web/src/timeline/components/drag-line.tsx
@@ -1,5 +1,5 @@
import { getDropLineY } from "./drop-target";
-import type { TimelineTrack, DropTarget } from "@/lib/timeline";
+import type { TimelineTrack, DropTarget } from "@/timeline";
import { TIMELINE_LAYERS } from "./layers";
interface DragLineProps {
diff --git a/apps/web/src/components/editor/panels/timeline/drop-target.ts b/apps/web/src/timeline/components/drop-target.ts
index ef6cbd90..be2495e4 100644
--- a/apps/web/src/components/editor/panels/timeline/drop-target.ts
+++ b/apps/web/src/timeline/components/drop-target.ts
@@ -1,9 +1,9 @@
-import type { TimelineTrack, TimelineElement } from "@/lib/timeline";
-import type { ComputeDropTargetParams, DropTarget } from "@/lib/timeline";
-import { resolveTrackPlacement } from "@/lib/timeline/placement";
+import type { TimelineTrack, TimelineElement } from "@/timeline";
+import type { ComputeDropTargetParams, DropTarget } from "@/timeline";
+import { resolveTrackPlacement } from "@/timeline/placement";
import { TIMELINE_TRACK_GAP_PX } from "./layout";
import { getTrackHeight } from "./track-layout";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
+import { TICKS_PER_SECOND } from "@/wasm";
function findElementAtPosition({
mouseX,
diff --git a/apps/web/src/components/editor/panels/timeline/expanded-layout.ts b/apps/web/src/timeline/components/expanded-layout.ts
index 6b9734a7..fae0e50c 100644
--- a/apps/web/src/components/editor/panels/timeline/expanded-layout.ts
+++ b/apps/web/src/timeline/components/expanded-layout.ts
@@ -1,9 +1,9 @@
import type {
AnimationPath,
ElementAnimations,
-} from "@/lib/animation/types";
-import type { TimelineTrack } from "@/lib/timeline";
-import { getElementKeyframes } from "@/lib/animation";
+} from "@/animation/types";
+import type { TimelineTrack } from "@/timeline";
+import { getElementKeyframes } from "@/animation";
import { KEYFRAME_LANE_HEIGHT_PX } from "./layout";
export interface ExpandedRow {
diff --git a/apps/web/src/components/editor/panels/timeline/graph-editor/bezier-graph.tsx b/apps/web/src/timeline/components/graph-editor/bezier-graph.tsx
index 3f78350b..7d77b985 100644
--- a/apps/web/src/components/editor/panels/timeline/graph-editor/bezier-graph.tsx
+++ b/apps/web/src/timeline/components/graph-editor/bezier-graph.tsx
@@ -2,8 +2,8 @@
import { useRef, useState, type PointerEvent } from "react";
import { useShiftKey } from "@/hooks/use-shift-key";
-import { getBezierPoint } from "@/lib/animation/bezier";
-import type { NormalizedCubicBezier } from "@/lib/animation/types";
+import { getBezierPoint } from "@/animation/bezier";
+import type { NormalizedCubicBezier } from "@/animation/types";
import { cn } from "@/utils/ui";
const GRAPH_WIDTH = 140;
diff --git a/apps/web/src/components/editor/panels/timeline/graph-editor/custom-presets-store.ts b/apps/web/src/timeline/components/graph-editor/custom-presets-store.ts
index 684ac7c9..4be2adfd 100644
--- a/apps/web/src/components/editor/panels/timeline/graph-editor/custom-presets-store.ts
+++ b/apps/web/src/timeline/components/graph-editor/custom-presets-store.ts
@@ -2,7 +2,7 @@
import { useSyncExternalStore } from "react";
import { generateUUID } from "@/utils/id";
-import type { NormalizedCubicBezier } from "@/lib/animation/types";
+import type { NormalizedCubicBezier } from "@/animation/types";
import type { EasingPreset } from "./easing-presets";
const STORAGE_KEY = "graph-editor-presets";
diff --git a/apps/web/src/components/editor/panels/timeline/graph-editor/easing-presets.ts b/apps/web/src/timeline/components/graph-editor/easing-presets.ts
index 6ccc9071..78a605d4 100644
--- a/apps/web/src/components/editor/panels/timeline/graph-editor/easing-presets.ts
+++ b/apps/web/src/timeline/components/graph-editor/easing-presets.ts
@@ -1,4 +1,4 @@
-import type { NormalizedCubicBezier } from "@/lib/animation/types";
+import type { NormalizedCubicBezier } from "@/animation/types";
export const PRESET_MATCH_TOLERANCE = 0.02;
diff --git a/apps/web/src/components/editor/panels/timeline/graph-editor/popover.tsx b/apps/web/src/timeline/components/graph-editor/popover.tsx
index 733195ed..1b8194c0 100644
--- a/apps/web/src/components/editor/panels/timeline/graph-editor/popover.tsx
+++ b/apps/web/src/timeline/components/graph-editor/popover.tsx
@@ -11,8 +11,8 @@ import {
Delete02Icon,
PlusSignIcon,
} from "@hugeicons/core-free-icons";
-import { getBezierPoint } from "@/lib/animation/bezier";
-import type { NormalizedCubicBezier } from "@/lib/animation/types";
+import { getBezierPoint } from "@/animation/bezier";
+import type { NormalizedCubicBezier } from "@/animation/types";
import type { GraphEditorComponentOption } from "./session";
import {
BUILTIN_PRESETS,
diff --git a/apps/web/src/components/editor/panels/timeline/graph-editor/session.ts b/apps/web/src/timeline/components/graph-editor/session.ts
index cc0fb070..d7c6b001 100644
--- a/apps/web/src/components/editor/panels/timeline/graph-editor/session.ts
+++ b/apps/web/src/timeline/components/graph-editor/session.ts
@@ -5,7 +5,7 @@ import {
getNormalizedCubicBezierForScalarSegment,
getScalarKeyframeContext,
updateScalarKeyframeCurve,
-} from "@/lib/animation";
+} from "@/animation";
import type {
AnimationPath,
ElementAnimations,
@@ -13,8 +13,8 @@ import type {
ScalarCurveKeyframePatch,
ScalarGraphKeyframeContext,
SelectedKeyframeRef,
-} from "@/lib/animation/types";
-import type { SceneTracks, TimelineElement } from "@/lib/timeline";
+} from "@/animation/types";
+import type { SceneTracks, TimelineElement } from "@/timeline";
const GRAPH_LINEAR_CURVE: NormalizedCubicBezier = [0, 0, 1, 1];
const FLAT_VALUE_EPSILON = 1e-6;
diff --git a/apps/web/src/components/editor/panels/timeline/graph-editor/use-controller.ts b/apps/web/src/timeline/components/graph-editor/use-controller.ts
index 51d5a0ff..ea091485 100644
--- a/apps/web/src/components/editor/panels/timeline/graph-editor/use-controller.ts
+++ b/apps/web/src/timeline/components/graph-editor/use-controller.ts
@@ -1,10 +1,10 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { useEditor } from "@/hooks/use-editor";
-import { registerCanceller } from "@/lib/cancel-interaction";
-import type { NormalizedCubicBezier } from "@/lib/animation/types";
-import { useKeyframeSelection } from "@/hooks/timeline/element/use-keyframe-selection";
+import { useEditor } from "@/editor/use-editor";
+import { registerCanceller } from "@/editor/cancel-interaction";
+import type { NormalizedCubicBezier } from "@/animation/types";
+import { useKeyframeSelection } from "@/timeline/hooks/element/use-keyframe-selection";
import {
applyGraphEditorCurvePreview,
buildGraphEditorCurvePatches,
diff --git a/apps/web/src/components/editor/panels/timeline/index.tsx b/apps/web/src/timeline/components/index.tsx
index 20e9faf1..e6a9df49 100644
--- a/apps/web/src/components/editor/panels/timeline/index.tsx
+++ b/apps/web/src/timeline/components/index.tsx
@@ -20,7 +20,7 @@ import {
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
-import { useTimelineZoom } from "@/hooks/timeline/use-timeline-zoom";
+import { useTimelineZoom } from "@/timeline/hooks/use-timeline-zoom";
import {
useCallback,
useEffect,
@@ -29,14 +29,14 @@ import {
useState,
type ReactNode,
} from "react";
-import type { ElementDragState, DropTarget } from "@/lib/timeline";
+import type { ElementDragState, DropTarget } from "@/timeline";
import { TimelineTrackContent } from "./timeline-track";
import { TimelinePlayhead } from "./timeline-playhead";
-import { SelectionBox } from "@/lib/selection/selection-box";
-import { useBoxSelect } from "@/lib/selection/hooks/use-box-select";
+import { SelectionBox } from "@/selection/selection-box";
+import { useBoxSelect } from "@/selection/hooks/use-box-select";
import { SnapIndicator } from "./snap-indicator";
-import type { SnapPoint } from "@/lib/timeline/snap-utils";
-import type { TimelineTrack } from "@/lib/timeline";
+import type { SnapPoint } from "@/timeline/snapping";
+import type { TimelineTrack } from "@/timeline";
import {
TIMELINE_SCROLLBAR_SIZE_PX,
TIMELINE_CONTENT_TOP_PADDING_PX,
@@ -44,14 +44,14 @@ import {
TIMELINE_TRACK_LABELS_COLUMN_WIDTH_PX,
KEYFRAME_LANE_HEIGHT_PX,
} from "./layout";
-import { useElementInteraction } from "@/hooks/timeline/element/use-element-interaction";
+import { useElementInteraction } from "@/timeline/hooks/element/use-element-interaction";
import {
canTrackHaveAudio,
canTrackBeHidden,
getTimelineZoomMin,
getTimelinePaddingPx,
-} from "@/lib/timeline";
-import { timelineTimeToPixels } from "@/lib/timeline/pixel-utils";
+} from "@/timeline";
+import { timelineTimeToPixels } from "@/timeline/pixel-utils";
import {
getTrackHeight,
getCumulativeHeightBefore,
@@ -66,20 +66,22 @@ import {
} from "./expanded-layout";
import { TIMELINE_HORIZONTAL_WHEEL_STEP_PX } from "./interaction";
import { TimelineToolbar } from "./timeline-toolbar";
-import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
-import { useTimelineSeek } from "@/hooks/timeline/use-timeline-seek";
-import { useTimelineDragDrop } from "@/hooks/timeline/use-timeline-drag-drop";
+import { useElementSelection } from "@/timeline/hooks/element/use-element-selection";
+import { useTimelineSeek } from "@/timeline/hooks/use-timeline-seek";
+import { useTimelineDragDrop } from "@/timeline/hooks/use-timeline-drag-drop";
import { TimelineRuler } from "./timeline-ruler";
-import { TimelineBookmarksRow } from "./bookmarks";
-import { useBookmarkDrag } from "@/hooks/timeline/use-bookmark-drag";
-import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
-import { useInitialScrollBottom } from "@/hooks/timeline/use-initial-scroll-bottom";
-import { useTimelineResize } from "@/hooks/timeline/use-timeline-resize";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { useEditor } from "@/hooks/use-editor";
-import { useTimelinePlayhead } from "@/hooks/timeline/use-timeline-playhead";
+import {
+ TimelineBookmarksRow,
+ useBookmarkDrag,
+} from "@/timeline/bookmarks/index";
+import { useEdgeAutoScroll } from "@/timeline/hooks/use-edge-auto-scroll";
+import { useInitialScrollBottom } from "@/timeline/hooks/use-initial-scroll-bottom";
+import { useTimelineResize } from "@/timeline/hooks/use-timeline-resize";
+import { useTimelineStore } from "@/timeline/timeline-store";
+import { useEditor } from "@/editor/use-editor";
+import { useTimelinePlayhead } from "@/timeline/hooks/use-timeline-playhead";
import { DragLine } from "./drag-line";
-import { invokeAction } from "@/lib/actions";
+import { invokeAction } from "@/actions";
import { resolveTimelineElementIntersections } from "./selection-hit-testing";
import { cn } from "@/utils/ui";
diff --git a/apps/web/src/components/editor/panels/timeline/interaction.ts b/apps/web/src/timeline/components/interaction.ts
index fe0210a4..fe0210a4 100644
--- a/apps/web/src/components/editor/panels/timeline/interaction.ts
+++ b/apps/web/src/timeline/components/interaction.ts
diff --git a/apps/web/src/components/editor/panels/timeline/layers.ts b/apps/web/src/timeline/components/layers.ts
index d59db412..d59db412 100644
--- a/apps/web/src/components/editor/panels/timeline/layers.ts
+++ b/apps/web/src/timeline/components/layers.ts
diff --git a/apps/web/src/components/editor/panels/timeline/layout.ts b/apps/web/src/timeline/components/layout.ts
index c30b7877..57a7f5df 100644
--- a/apps/web/src/components/editor/panels/timeline/layout.ts
+++ b/apps/web/src/timeline/components/layout.ts
@@ -1,4 +1,4 @@
-import type { TrackType } from "@/lib/timeline";
+import type { TrackType } from "@/timeline";
export const TIMELINE_TRACK_HEIGHTS_PX: Record<TrackType, number> = {
video: 65,
diff --git a/apps/web/src/components/editor/panels/timeline/selection-hit-testing.ts b/apps/web/src/timeline/components/selection-hit-testing.ts
index 551393e9..da5617c6 100644
--- a/apps/web/src/components/editor/panels/timeline/selection-hit-testing.ts
+++ b/apps/web/src/timeline/components/selection-hit-testing.ts
@@ -1,5 +1,5 @@
-import type { TimelineTrack } from "@/lib/timeline";
-import { timelineTimeToPixels } from "@/lib/timeline/pixel-utils";
+import type { TimelineTrack } from "@/timeline";
+import { timelineTimeToPixels } from "@/timeline/pixel-utils";
import {
TIMELINE_CONTENT_TOP_PADDING_PX,
} from "./layout";
diff --git a/apps/web/src/components/editor/panels/timeline/snap-indicator.tsx b/apps/web/src/timeline/components/snap-indicator.tsx
index a86e2a5e..6c1dbf1a 100644
--- a/apps/web/src/components/editor/panels/timeline/snap-indicator.tsx
+++ b/apps/web/src/timeline/components/snap-indicator.tsx
@@ -1,50 +1,50 @@
-"use client";
-
-import { useSnapIndicatorPosition } from "@/hooks/timeline/use-snap-indicator-position";
-import type { SnapPoint } from "@/lib/timeline/snap-utils";
-import {
- getCenteredLineLeft,
- TIMELINE_INDICATOR_LINE_WIDTH_PX,
-} from "@/lib/timeline";
-import { TIMELINE_LAYERS } from "./layers";
-interface SnapIndicatorProps {
- snapPoint: SnapPoint | null;
- zoomLevel: number;
- isVisible: boolean;
- timelineRef: React.RefObject<HTMLDivElement | null>;
- tracksScrollRef: React.RefObject<HTMLDivElement | null>;
-}
-
-export function SnapIndicator({
- snapPoint,
- zoomLevel,
- isVisible,
- timelineRef,
- tracksScrollRef,
-}: SnapIndicatorProps) {
- const { leftPosition, topPosition, height } = useSnapIndicatorPosition({
- snapPoint,
- zoomLevel,
- timelineRef,
- tracksScrollRef,
- });
-
- if (!isVisible || !snapPoint) {
- return null;
- }
-
- return (
- <div
- className="pointer-events-none absolute"
- style={{
- left: `${getCenteredLineLeft({ centerPixel: leftPosition })}px`,
- top: topPosition,
- height: `${height}px`,
- width: `${TIMELINE_INDICATOR_LINE_WIDTH_PX}px`,
- zIndex: TIMELINE_LAYERS.snapIndicator,
- }}
- >
- <div className={"bg-primary/40 h-full w-0.5 opacity-80"} />
- </div>
- );
-}
+"use client";
+
+import { useSnapIndicatorPosition } from "@/timeline/hooks/use-snap-indicator-position";
+import type { SnapPoint } from "@/timeline/snapping";
+import {
+ getCenteredLineLeft,
+ TIMELINE_INDICATOR_LINE_WIDTH_PX,
+} from "@/timeline";
+import { TIMELINE_LAYERS } from "./layers";
+interface SnapIndicatorProps {
+ snapPoint: SnapPoint | null;
+ zoomLevel: number;
+ isVisible: boolean;
+ timelineRef: React.RefObject<HTMLDivElement | null>;
+ tracksScrollRef: React.RefObject<HTMLDivElement | null>;
+}
+
+export function SnapIndicator({
+ snapPoint,
+ zoomLevel,
+ isVisible,
+ timelineRef,
+ tracksScrollRef,
+}: SnapIndicatorProps) {
+ const { leftPosition, topPosition, height } = useSnapIndicatorPosition({
+ snapPoint,
+ zoomLevel,
+ timelineRef,
+ tracksScrollRef,
+ });
+
+ if (!isVisible || !snapPoint) {
+ return null;
+ }
+
+ return (
+ <div
+ className="pointer-events-none absolute"
+ style={{
+ left: `${getCenteredLineLeft({ centerPixel: leftPosition })}px`,
+ top: topPosition,
+ height: `${height}px`,
+ width: `${TIMELINE_INDICATOR_LINE_WIDTH_PX}px`,
+ zIndex: TIMELINE_LAYERS.snapIndicator,
+ }}
+ >
+ <div className={"bg-primary/40 h-full w-0.5 opacity-80"} />
+ </div>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/timeline/theme.ts b/apps/web/src/timeline/components/theme.ts
index 0afe3eb4..9f93dc16 100644
--- a/apps/web/src/components/editor/panels/timeline/theme.ts
+++ b/apps/web/src/timeline/components/theme.ts
@@ -1,4 +1,4 @@
-import type { TrackType } from "@/lib/timeline";
+import type { TrackType } from "@/timeline";
export const TIMELINE_AUDIO_WAVEFORM_COLOR = "rgba(255, 255, 255, 0.7)";
diff --git a/apps/web/src/components/editor/panels/timeline/timeline-element.tsx b/apps/web/src/timeline/components/timeline-element.tsx
index 75a2dfa8..74c34607 100644
--- a/apps/web/src/components/editor/panels/timeline/timeline-element.tsx
+++ b/apps/web/src/timeline/components/timeline-element.tsx
@@ -1,19 +1,19 @@
"use client";
import { createContext, useContext } from "react";
-import { useEditor } from "@/hooks/use-editor";
-import { useAssetsPanelStore } from "@/stores/assets-panel-store";
+import { useEditor } from "@/editor/use-editor";
+import { useAssetsPanelStore } from "@/components/editor/panels/assets/assets-panel-store";
import { AudioWaveform, WAVEFORM_GAIN_SAMPLE_COUNT } from "./audio-waveform";
import { AudioVolumeLine } from "./audio-volume-line";
-import { useElementPreview } from "@/hooks/use-element-preview";
+import { useElementPreview } from "@/timeline/hooks/use-element-preview";
import {
useKeyframeDrag,
type KeyframeDragState,
-} from "@/hooks/timeline/element/use-keyframe-drag";
-import { useKeyframeSelection } from "@/hooks/timeline/element/use-keyframe-selection";
-import { useKeyframeBoxSelect } from "@/hooks/timeline/element/use-keyframe-box-select";
-import { SelectionBox } from "@/lib/selection/selection-box";
-import { getElementKeyframes } from "@/lib/animation";
+} from "@/timeline/hooks/element/use-keyframe-drag";
+import { useKeyframeSelection } from "@/timeline/hooks/element/use-keyframe-selection";
+import { useKeyframeBoxSelect } from "@/timeline/hooks/element/use-keyframe-box-select";
+import { SelectionBox } from "@/selection/selection-box";
+import { getElementKeyframes } from "@/animation";
import {
canElementHaveAudio,
canElementBeHidden,
@@ -21,7 +21,7 @@ import {
hasMediaId,
timelineTimeToPixels,
timelineTimeToSnappedPixels,
-} from "@/lib/timeline";
+} from "@/timeline";
import { getTrackHeight } from "./track-layout";
import { getTimelineElementClassName, TIMELINE_TRACK_THEME } from "./theme";
import {
@@ -38,27 +38,27 @@ import type {
VideoElement,
ImageElement,
AudioElement,
-} from "@/lib/timeline";
-import type { MediaAsset } from "@/lib/media/types";
-import { mediaSupportsAudio } from "@/lib/media/media-utils";
+} from "@/timeline";
+import type { MediaAsset } from "@/media/types";
+import { mediaSupportsAudio } from "@/media/media-utils";
import {
canToggleSourceAudio,
getSourceAudioActionLabel,
isSourceAudioSeparated,
-} from "@/lib/timeline/audio-separation";
-import { buildWaveformGainSamples } from "@/lib/timeline/audio-state";
-import { getTimelinePixelsPerSecond } from "@/lib/timeline";
-import { buildWaveformSourceKey } from "@/lib/media/waveform-summary";
-import { TICKS_PER_SECOND } from "@/lib/wasm/ticks";
+} from "@/timeline/audio-separation";
+import { buildWaveformGainSamples } from "@/timeline/audio-state";
+import { getTimelinePixelsPerSecond } from "@/timeline";
+import { buildWaveformSourceKey } from "@/media/waveform-summary";
+import { TICKS_PER_SECOND } from "@/wasm/ticks";
import {
getActionDefinition,
type TAction,
type TActionWithOptionalArgs,
invokeAction,
-} from "@/lib/actions";
-import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
-import { resolveStickerId } from "@/lib/stickers";
-import { buildGraphicPreviewUrl } from "@/lib/graphics";
+} from "@/actions";
+import { useElementSelection } from "@/timeline/hooks/element/use-element-selection";
+import { resolveStickerId } from "@/stickers";
+import { buildGraphicPreviewUrl } from "@/graphics";
import Image from "next/image";
import {
ScissorIcon,
@@ -77,14 +77,11 @@ import {
import { HugeiconsIcon } from "@hugeicons/react";
import { uppercase } from "@/utils/string";
import { useMemo, type ComponentProps, type ReactNode } from "react";
-import type {
- SelectedKeyframeRef,
- ElementKeyframe,
-} from "@/lib/animation/types";
+import type { SelectedKeyframeRef, ElementKeyframe } from "@/animation/types";
import { cn } from "@/utils/ui";
import { usePropertiesStore } from "@/components/editor/panels/properties/stores/properties-store";
-import { getTrackTypeForElementType } from "@/lib/timeline/placement/compatibility";
-import { useTimelineStore } from "@/stores/timeline-store";
+import { getTrackTypeForElementType } from "@/timeline/placement/compatibility";
+import { useTimelineStore } from "@/timeline/timeline-store";
import { KEYFRAME_LANE_HEIGHT_PX } from "./layout";
import {
getExpandedRows,
diff --git a/apps/web/src/components/editor/panels/timeline/timeline-playhead.tsx b/apps/web/src/timeline/components/timeline-playhead.tsx
index d9218371..4dd3329f 100644
--- a/apps/web/src/components/editor/panels/timeline/timeline-playhead.tsx
+++ b/apps/web/src/timeline/components/timeline-playhead.tsx
@@ -5,13 +5,11 @@ import {
getCenteredLineLeft,
TIMELINE_INDICATOR_LINE_WIDTH_PX,
timelineTimeToSnappedPixels,
-} from "@/lib/timeline";
-import { useTimelinePlayhead } from "@/hooks/timeline/use-timeline-playhead";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
-import { useEditor } from "@/hooks/use-editor";
-import {
- TIMELINE_SCROLLBAR_SIZE_PX,
-} from "./layout";
+} from "@/timeline";
+import { useTimelinePlayhead } from "@/timeline/hooks/use-timeline-playhead";
+import { TICKS_PER_SECOND } from "@/wasm";
+import { useEditor } from "@/editor/use-editor";
+import { TIMELINE_SCROLLBAR_SIZE_PX } from "./layout";
import { TIMELINE_LAYERS } from "./layers";
interface TimelinePlayheadProps {
@@ -74,10 +72,15 @@ export function TimelinePlayhead({
event.preventDefault();
const fps = editor.project.getActive().settings.fps;
- const ticksPerFrame = Math.round(TICKS_PER_SECOND * fps.denominator / fps.numerator);
+ const ticksPerFrame = Math.round(
+ (TICKS_PER_SECOND * fps.denominator) / fps.numerator,
+ );
const direction = event.key === "ArrowRight" ? 1 : -1;
const now = editor.playback.getCurrentTime();
- const nextTime = Math.max(0, Math.min(duration, now + direction * ticksPerFrame));
+ const nextTime = Math.max(
+ 0,
+ Math.min(duration, now + direction * ticksPerFrame),
+ );
editor.playback.seek({ time: nextTime });
};
diff --git a/apps/web/src/components/editor/panels/timeline/timeline-ruler.tsx b/apps/web/src/timeline/components/timeline-ruler.tsx
index 7b69e820..e2a379ee 100644
--- a/apps/web/src/components/editor/panels/timeline/timeline-ruler.tsx
+++ b/apps/web/src/timeline/components/timeline-ruler.tsx
@@ -1,14 +1,12 @@
import { type JSX, useLayoutEffect, useRef } from "react";
-import {
- BASE_TIMELINE_PIXELS_PER_SECOND,
-} from "@/lib/timeline/scale";
+import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale";
import { mediaTimeToSeconds } from "opencut-wasm";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
+import { TICKS_PER_SECOND } from "@/wasm";
import { TIMELINE_RULER_HEIGHT_PX } from "./layout";
-import { DEFAULT_FPS } from "@/lib/fps/defaults";
-import { useEditor } from "@/hooks/use-editor";
-import { getRulerConfig, shouldShowLabel } from "@/lib/timeline/ruler-utils";
-import { useScrollPosition } from "@/hooks/timeline/use-scroll-position";
+import { DEFAULT_FPS } from "@/fps/defaults";
+import { useEditor } from "@/editor/use-editor";
+import { getRulerConfig, shouldShowLabel } from "@/timeline/ruler-utils";
+import { useScrollPosition } from "@/timeline/hooks/use-scroll-position";
import { TimelineTick } from "./timeline-tick";
interface TimelineRulerProps {
@@ -36,14 +34,18 @@ export function TimelineRuler({
const durationSeconds = mediaTimeToSeconds({ time: durationTicks });
const pixelsPerSecond = BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel;
const visibleDurationSeconds = dynamicTimelineWidth / pixelsPerSecond;
- const effectiveDurationSeconds = Math.max(durationSeconds, visibleDurationSeconds);
+ const effectiveDurationSeconds = Math.max(
+ durationSeconds,
+ visibleDurationSeconds,
+ );
const fps =
useEditor((e) => e.project.getActiveOrNull()?.settings.fps) ?? DEFAULT_FPS;
const { labelIntervalSeconds, tickIntervalSeconds } = getRulerConfig({
zoomLevel,
fps,
});
- const tickCount = Math.ceil(effectiveDurationSeconds / tickIntervalSeconds) + 1;
+ const tickCount =
+ Math.ceil(effectiveDurationSeconds / tickIntervalSeconds) + 1;
const { scrollLeft, viewportWidth } = useScrollPosition({
scrollRef: tracksScrollRef,
@@ -90,7 +92,10 @@ export function TimelineRuler({
if (timeSeconds > effectiveDurationSeconds) break;
const timeTicks = Math.round(timeSeconds * TICKS_PER_SECOND);
- const showLabel = shouldShowLabel({ time: timeSeconds, labelIntervalSeconds });
+ const showLabel = shouldShowLabel({
+ time: timeSeconds,
+ labelIntervalSeconds,
+ });
timelineTicks.push(
<TimelineTick
key={tickIndex}
diff --git a/apps/web/src/components/editor/panels/timeline/timeline-tick.tsx b/apps/web/src/timeline/components/timeline-tick.tsx
index fca55752..1b1e46d3 100644
--- a/apps/web/src/components/editor/panels/timeline/timeline-tick.tsx
+++ b/apps/web/src/timeline/components/timeline-tick.tsx
@@ -1,8 +1,8 @@
"use client";
import type { FrameRate } from "opencut-wasm";
-import { timelineTimeToSnappedPixels } from "@/lib/timeline";
-import { formatRulerLabel } from "@/lib/timeline/ruler-utils";
+import { timelineTimeToSnappedPixels } from "@/timeline";
+import { formatRulerLabel } from "@/timeline/ruler-utils";
interface TimelineTickProps {
time: number;
diff --git a/apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx b/apps/web/src/timeline/components/timeline-toolbar.tsx
index 80ea10d0..22acd447 100644
--- a/apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx
+++ b/apps/web/src/timeline/components/timeline-toolbar.tsx
@@ -1,5 +1,5 @@
-import { useEditor } from "@/hooks/use-editor";
-import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
+import { useEditor } from "@/editor/use-editor";
+import { useElementSelection } from "@/timeline/hooks/element/use-element-selection";
import {
TooltipProvider,
Tooltip,
@@ -15,18 +15,18 @@ import {
} from "@/components/ui/split-button";
import { Slider } from "@/components/ui/slider";
import { TIMELINE_ZOOM_BUTTON_FACTOR } from "./interaction";
-import { TIMELINE_ZOOM_MAX } from "@/lib/timeline/scale";
-import { sliderToZoom, zoomToSlider } from "@/lib/timeline/zoom-utils";
+import { TIMELINE_ZOOM_MAX } from "@/timeline/scale";
+import { sliderToZoom, zoomToSlider } from "@/timeline/zoom-utils";
import { ScenesView } from "@/components/editor/scenes-view";
-import { type TActionWithOptionalArgs, invokeAction } from "@/lib/actions";
+import { type TActionWithOptionalArgs, invokeAction } from "@/actions";
import {
canToggleSourceAudio,
getSourceAudioActionLabel,
isSourceAudioSeparated,
-} from "@/lib/timeline/audio-separation";
-import { hasMediaId } from "@/lib/timeline";
+} from "@/timeline/audio-separation";
+import { hasMediaId } from "@/timeline";
import { cn } from "@/utils/ui";
-import { useTimelineStore } from "@/stores/timeline-store";
+import { useTimelineStore } from "@/timeline/timeline-store";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Bookmark02Icon,
diff --git a/apps/web/src/components/editor/panels/timeline/timeline-track.tsx b/apps/web/src/timeline/components/timeline-track.tsx
index 32b1b93d..6c40637e 100644
--- a/apps/web/src/components/editor/panels/timeline/timeline-track.tsx
+++ b/apps/web/src/timeline/components/timeline-track.tsx
@@ -1,14 +1,14 @@
"use client";
-import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
+import { useElementSelection } from "@/timeline/hooks/element/use-element-selection";
import { TimelineElement } from "./timeline-element";
-import type { TimelineTrack } from "@/lib/timeline";
-import type { TimelineElement as TimelineElementType } from "@/lib/timeline";
+import type { TimelineTrack } from "@/timeline";
+import type { TimelineElement as TimelineElementType } from "@/timeline";
import { TIMELINE_LAYERS } from "./layers";
-import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
-import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
-import type { ElementDragState } from "@/lib/timeline";
-import { useEditor } from "@/hooks/use-editor";
+import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale";
+import { useEdgeAutoScroll } from "@/timeline/hooks/use-edge-auto-scroll";
+import type { ElementDragState } from "@/timeline";
+import { useEditor } from "@/editor/use-editor";
interface TimelineTrackContentProps {
track: TimelineTrack;
diff --git a/apps/web/src/components/editor/panels/timeline/track-layout.ts b/apps/web/src/timeline/components/track-layout.ts
index 8f833a57..59f4a5f9 100644
--- a/apps/web/src/components/editor/panels/timeline/track-layout.ts
+++ b/apps/web/src/timeline/components/track-layout.ts
@@ -1,4 +1,4 @@
-import type { TrackType } from "@/lib/timeline";
+import type { TrackType } from "@/timeline";
import {
KEYFRAME_LANE_HEIGHT_PX,
TIMELINE_TRACK_GAP_PX,
diff --git a/apps/web/src/lib/timeline/creation.ts b/apps/web/src/timeline/creation.ts
index 19f46115..2ac7f246 100644
--- a/apps/web/src/lib/timeline/creation.ts
+++ b/apps/web/src/timeline/creation.ts
@@ -1,3 +1,3 @@
-import { TICKS_PER_SECOND } from "@/lib/wasm/ticks";
+import { TICKS_PER_SECOND } from "@/wasm/ticks";
export const DEFAULT_NEW_ELEMENT_DURATION = 5 * TICKS_PER_SECOND;
diff --git a/apps/web/src/lib/timeline/defaults.ts b/apps/web/src/timeline/defaults.ts
index cf452800..06639e01 100644
--- a/apps/web/src/lib/timeline/defaults.ts
+++ b/apps/web/src/timeline/defaults.ts
@@ -1,6 +1,6 @@
-import { DEFAULT_NEW_ELEMENT_DURATION } from "@/lib/timeline/creation";
-import type { TTimelineViewState } from "@/lib/project/types";
-import type { BlendMode, Transform } from "@/lib/rendering";
+import { DEFAULT_NEW_ELEMENT_DURATION } from "@/timeline/creation";
+import type { TTimelineViewState } from "@/project/types";
+import type { BlendMode, Transform } from "@/rendering";
import type { TextElement } from "./types";
const defaultTransform: Transform = {
diff --git a/apps/web/src/lib/drag-data.ts b/apps/web/src/timeline/drag-data.ts
index 5cbcfbe0..1fc9ce9a 100644
--- a/apps/web/src/lib/drag-data.ts
+++ b/apps/web/src/timeline/drag-data.ts
@@ -1,4 +1,4 @@
-import type { TimelineDragData } from "@/lib/timeline/drag";
+import type { TimelineDragData } from "@/timeline/drag";
const MIME_TYPE = "application/x-timeline-drag";
let lastDragData: TimelineDragData | null = null;
diff --git a/apps/web/src/lib/timeline/drag-utils.ts b/apps/web/src/timeline/drag-utils.ts
index 35d00613..a731d7a0 100644
--- a/apps/web/src/lib/timeline/drag-utils.ts
+++ b/apps/web/src/timeline/drag-utils.ts
@@ -1,5 +1,5 @@
-import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
+import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale";
+import { TICKS_PER_SECOND } from "@/wasm";
export function getMouseTimeFromClientX({
clientX,
diff --git a/apps/web/src/lib/timeline/drag.ts b/apps/web/src/timeline/drag.ts
index 6e74e17c..7bf99a6d 100644
--- a/apps/web/src/lib/timeline/drag.ts
+++ b/apps/web/src/timeline/drag.ts
@@ -1,5 +1,5 @@
import type { MaskableElement, VisualElement } from "./types";
-import type { ParamValues } from "@/lib/params";
+import type { ParamValues } from "@/params";
interface BaseDragData {
id: string;
diff --git a/apps/web/src/timeline/element-snap-source.ts b/apps/web/src/timeline/element-snap-source.ts
new file mode 100644
index 00000000..ec1e5f65
--- /dev/null
+++ b/apps/web/src/timeline/element-snap-source.ts
@@ -0,0 +1,38 @@
+import type { SceneTracks } from "@/timeline";
+import type { SnapPoint } from "@/timeline/snapping";
+
+export function getElementEdgeSnapPoints({
+ tracks,
+ excludeElementIds,
+}: {
+ tracks: SceneTracks;
+ excludeElementIds?: Set<string>;
+}): SnapPoint[] {
+ const snapPoints: SnapPoint[] = [];
+ const orderedTracks = [...tracks.overlay, tracks.main, ...tracks.audio];
+
+ for (const track of orderedTracks) {
+ for (const element of track.elements) {
+ if (excludeElementIds?.has(element.id)) {
+ continue;
+ }
+
+ snapPoints.push(
+ {
+ time: element.startTime,
+ type: "element-start",
+ elementId: element.id,
+ trackId: track.id,
+ },
+ {
+ time: element.startTime + element.duration,
+ type: "element-end",
+ elementId: element.id,
+ trackId: track.id,
+ },
+ );
+ }
+ }
+
+ return snapPoints;
+}
diff --git a/apps/web/src/lib/timeline/element-utils.ts b/apps/web/src/timeline/element-utils.ts
index e7ec4065..676bc823 100644
--- a/apps/web/src/lib/timeline/element-utils.ts
+++ b/apps/web/src/timeline/element-utils.ts
@@ -1,4 +1,4 @@
-import { DEFAULT_NEW_ELEMENT_DURATION } from "@/lib/timeline/creation";
+import { DEFAULT_NEW_ELEMENT_DURATION } from "@/timeline/creation";
import {
MASKABLE_ELEMENT_TYPES,
RETIMABLE_ELEMENT_TYPES,
@@ -22,12 +22,12 @@ import {
type RetimableElement,
type VisualElement,
type UploadAudioElement,
-} from "@/lib/timeline";
-import { DEFAULTS } from "@/lib/timeline/defaults";
-import type { MediaType } from "@/lib/media/types";
-import { buildDefaultEffectInstance } from "@/lib/effects";
-import { buildDefaultGraphicInstance } from "@/lib/graphics";
-import type { ParamValues } from "@/lib/params";
+} from "@/timeline";
+import { DEFAULTS } from "@/timeline/defaults";
+import type { MediaType } from "@/media/types";
+import { buildDefaultEffectInstance } from "@/effects";
+import { buildDefaultGraphicInstance } from "@/graphics";
+import type { ParamValues } from "@/params";
import { capitalizeFirstLetter } from "@/utils/string";
export function canElementHaveAudio(
diff --git a/apps/web/src/lib/timeline/group-move/build-group.ts b/apps/web/src/timeline/group-move/build-group.ts
index 16d720eb..00f39d55 100644
--- a/apps/web/src/lib/timeline/group-move/build-group.ts
+++ b/apps/web/src/timeline/group-move/build-group.ts
@@ -1,5 +1,5 @@
-import type { ElementRef, SceneTracks } from "@/lib/timeline";
-import { findTrackInSceneTracks } from "@/lib/timeline/track-element-update";
+import type { ElementRef, SceneTracks } from "@/timeline";
+import { findTrackInSceneTracks } from "@/timeline/track-element-update";
import type { GroupMember, MoveGroup } from "./types";
import { getTrackPlacementById } from "./track-placement";
diff --git a/apps/web/src/lib/timeline/group-move/index.ts b/apps/web/src/timeline/group-move/index.ts
index 82bd3895..82bd3895 100644
--- a/apps/web/src/lib/timeline/group-move/index.ts
+++ b/apps/web/src/timeline/group-move/index.ts
diff --git a/apps/web/src/lib/timeline/group-move/resolve-move.ts b/apps/web/src/timeline/group-move/resolve-move.ts
index fbe4cbda..60940315 100644
--- a/apps/web/src/lib/timeline/group-move/resolve-move.ts
+++ b/apps/web/src/timeline/group-move/resolve-move.ts
@@ -1,6 +1,6 @@
-import type { SceneTracks } from "@/lib/timeline";
-import { getTrackTypeForElementType } from "@/lib/timeline/placement/compatibility";
-import { canPlaceTimeSpansOnTrack } from "@/lib/timeline/placement/overlap";
+import type { SceneTracks } from "@/timeline";
+import { getTrackTypeForElementType } from "@/timeline/placement/compatibility";
+import { canPlaceTimeSpansOnTrack } from "@/timeline/placement/overlap";
import type {
GroupMoveResult,
MoveGroup,
diff --git a/apps/web/src/lib/timeline/group-move/snap.ts b/apps/web/src/timeline/group-move/snap.ts
index 3502ae30..32c0a2f1 100644
--- a/apps/web/src/lib/timeline/group-move/snap.ts
+++ b/apps/web/src/timeline/group-move/snap.ts
@@ -1,9 +1,13 @@
-import type { SceneTracks } from "@/lib/timeline";
+import type { SceneTracks } from "@/timeline";
import {
- findSnapPoints,
- snapToNearestPoint,
+ buildTimelineSnapPoints,
+ getTimelineSnapThresholdInTicks,
+ resolveTimelineSnap,
type SnapPoint,
-} from "@/lib/timeline/snap-utils";
+} from "@/timeline/snapping";
+import { getElementEdgeSnapPoints } from "@/timeline/element-snap-source";
+import { getPlayheadSnapPoints } from "@/timeline/playhead-snap-source";
+import { getAnimationKeyframeSnapPointsForTimeline } from "@/animation/timeline-snap-points";
import type { MoveGroup } from "./types";
export function snapGroupEdges({
@@ -22,11 +26,21 @@ export function snapGroupEdges({
snappedAnchorStartTime: number;
snapPoint: SnapPoint | null;
} {
- const snapPoints = findSnapPoints({
- tracks,
- playheadTime,
- excludeElementIds: new Set(group.members.map((member) => member.elementId)),
+ const excludeElementIds = new Set(
+ group.members.map((member) => member.elementId),
+ );
+ const snapPoints = buildTimelineSnapPoints({
+ sources: [
+ () => getElementEdgeSnapPoints({ tracks, excludeElementIds }),
+ () => getPlayheadSnapPoints({ playheadTime }),
+ () =>
+ getAnimationKeyframeSnapPointsForTimeline({
+ tracks,
+ excludeElementIds,
+ }),
+ ],
});
+ const maxSnapDistance = getTimelineSnapThresholdInTicks({ zoomLevel });
let closestSnapDistance = Infinity;
let snappedAnchorStartTime = anchorStartTime;
@@ -34,10 +48,10 @@ export function snapGroupEdges({
for (const member of group.members) {
const memberStartTime = anchorStartTime + member.timeOffset;
- const memberStartSnap = snapToNearestPoint({
+ const memberStartSnap = resolveTimelineSnap({
targetTime: memberStartTime,
snapPoints,
- zoomLevel,
+ maxSnapDistance,
});
if (
memberStartSnap.snapPoint &&
@@ -48,10 +62,10 @@ export function snapGroupEdges({
snapPoint = memberStartSnap.snapPoint;
}
- const memberEndSnap = snapToNearestPoint({
+ const memberEndSnap = resolveTimelineSnap({
targetTime: memberStartTime + member.duration,
snapPoints,
- zoomLevel,
+ maxSnapDistance,
});
if (
memberEndSnap.snapPoint &&
diff --git a/apps/web/src/lib/timeline/group-move/track-placement.ts b/apps/web/src/timeline/group-move/track-placement.ts
index d301e5ef..c4c5efb5 100644
--- a/apps/web/src/lib/timeline/group-move/track-placement.ts
+++ b/apps/web/src/timeline/group-move/track-placement.ts
@@ -1,4 +1,4 @@
-import type { SceneTracks, TimelineTrack } from "@/lib/timeline";
+import type { SceneTracks, TimelineTrack } from "@/timeline";
import type { GroupTrackSection } from "./types";
export interface TrackPlacement {
diff --git a/apps/web/src/lib/timeline/group-move/types.ts b/apps/web/src/timeline/group-move/types.ts
index 5ac1e207..daf9c6e9 100644
--- a/apps/web/src/lib/timeline/group-move/types.ts
+++ b/apps/web/src/timeline/group-move/types.ts
@@ -1,4 +1,4 @@
-import type { ElementRef, ElementType, TrackType } from "@/lib/timeline";
+import type { ElementRef, ElementType, TrackType } from "@/timeline";
export type GroupTrackSection = "overlay" | "main" | "audio";
diff --git a/apps/web/src/lib/timeline/group-resize/compute-resize.ts b/apps/web/src/timeline/group-resize/compute-resize.ts
index 5730b117..dd542967 100644
--- a/apps/web/src/lib/timeline/group-resize/compute-resize.ts
+++ b/apps/web/src/timeline/group-resize/compute-resize.ts
@@ -2,8 +2,8 @@ import { roundToFrame } from "opencut-wasm";
import {
getSourceSpanAtClipTime,
getTimelineDurationForSourceSpan,
-} from "@/lib/retime";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
+} from "@/retime";
+import { TICKS_PER_SECOND } from "@/wasm";
import type {
ComputeGroupResizeArgs,
GroupResizeMember,
diff --git a/apps/web/src/lib/timeline/group-resize/index.ts b/apps/web/src/timeline/group-resize/index.ts
index bc0a7b35..bc0a7b35 100644
--- a/apps/web/src/lib/timeline/group-resize/index.ts
+++ b/apps/web/src/timeline/group-resize/index.ts
diff --git a/apps/web/src/lib/timeline/group-resize/types.ts b/apps/web/src/timeline/group-resize/types.ts
index d5f00c1b..b07e6fe6 100644
--- a/apps/web/src/lib/timeline/group-resize/types.ts
+++ b/apps/web/src/timeline/group-resize/types.ts
@@ -1,5 +1,5 @@
import type { FrameRate } from "opencut-wasm";
-import type { ElementRef, RetimeConfig } from "@/lib/timeline/types";
+import type { ElementRef, RetimeConfig } from "@/timeline/types";
export type ResizeSide = "left" | "right";
diff --git a/apps/web/src/hooks/timeline/element/use-element-interaction.ts b/apps/web/src/timeline/hooks/element/use-element-interaction.ts
index 5458efe7..621259b9 100644
--- a/apps/web/src/hooks/timeline/element/use-element-interaction.ts
+++ b/apps/web/src/timeline/hooks/element/use-element-interaction.ts
@@ -6,25 +6,25 @@ import {
type MouseEvent as ReactMouseEvent,
type RefObject,
} from "react";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import { useShiftKey } from "@/hooks/use-shift-key";
-import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
+import { useElementSelection } from "@/timeline/hooks/element/use-element-selection";
import {
buildMoveGroup,
resolveGroupMove,
snapGroupEdges,
type GroupMoveResult,
type MoveGroup,
-} from "@/lib/timeline/group-move";
-import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
-import { TIMELINE_DRAG_THRESHOLD_PX } from "@/components/editor/panels/timeline/interaction";
+} from "@/timeline/group-move";
+import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale";
+import { TICKS_PER_SECOND } from "@/wasm";
+import { TIMELINE_DRAG_THRESHOLD_PX } from "@/timeline/components/interaction";
import { roundToFrame } from "opencut-wasm";
-import { computeDropTarget } from "@/components/editor/panels/timeline/drop-target";
-import { getMouseTimeFromClientX } from "@/lib/timeline/drag-utils";
+import { computeDropTarget } from "@/timeline/components/drop-target";
+import { getMouseTimeFromClientX } from "@/timeline/drag-utils";
import { generateUUID } from "@/utils/id";
-import type { SnapPoint } from "@/lib/timeline/snap-utils";
-import { registerCanceller } from "@/lib/cancel-interaction";
+import type { SnapPoint } from "@/timeline/snapping";
+import { registerCanceller } from "@/editor/cancel-interaction";
import type {
DropTarget,
ElementRef,
@@ -32,7 +32,7 @@ import type {
SceneTracks,
TimelineElement,
TimelineTrack,
-} from "@/lib/timeline";
+} from "@/timeline";
interface UseElementInteractionProps {
zoomLevel: number;
diff --git a/apps/web/src/hooks/timeline/element/use-element-selection.ts b/apps/web/src/timeline/hooks/element/use-element-selection.ts
index 42187b7f..19037e10 100644
--- a/apps/web/src/hooks/timeline/element/use-element-selection.ts
+++ b/apps/web/src/timeline/hooks/element/use-element-selection.ts
@@ -1,6 +1,6 @@
import { useCallback } from "react";
-import { useEditor } from "@/hooks/use-editor";
-import type { ElementRef } from "@/lib/timeline/types";
+import { useEditor } from "@/editor/use-editor";
+import type { ElementRef } from "@/timeline/types";
export function useElementSelection() {
const editor = useEditor();
diff --git a/apps/web/src/hooks/timeline/element/use-keyframe-box-select.ts b/apps/web/src/timeline/hooks/element/use-keyframe-box-select.ts
index 0fc46372..e471e2d4 100644
--- a/apps/web/src/hooks/timeline/element/use-keyframe-box-select.ts
+++ b/apps/web/src/timeline/hooks/element/use-keyframe-box-select.ts
@@ -1,19 +1,16 @@
import { useCallback, useRef, useMemo } from "react";
-import { useBoxSelect } from "@/lib/selection/hooks/use-box-select";
+import { useBoxSelect } from "@/selection/hooks/use-box-select";
import {
useKeyframeSelection,
getSelectedKeyframeId,
} from "./use-keyframe-selection";
-import type {
- SelectedKeyframeRef,
- ElementKeyframe,
-} from "@/lib/animation/types";
-import type { ExpandedRow } from "@/components/editor/panels/timeline/expanded-layout";
-import { timelineTimeToSnappedPixels } from "@/lib/timeline";
+import type { SelectedKeyframeRef, ElementKeyframe } from "@/animation/types";
+import type { ExpandedRow } from "@/timeline/components/expanded-layout";
+import { timelineTimeToSnappedPixels } from "@/timeline";
import {
KEYFRAME_LANE_HEIGHT_PX,
KEYFRAME_DIAMOND_SIZE_PX,
-} from "@/components/editor/panels/timeline/layout";
+} from "@/timeline/components/layout";
export function useKeyframeBoxSelect({
trackId,
@@ -96,7 +93,8 @@ export function useKeyframeBoxSelect({
}, [keyframeEntries]);
const selectedIds = useMemo(
- () => selectedKeyframes.map((keyframe) => getSelectedKeyframeId({ keyframe })),
+ () =>
+ selectedKeyframes.map((keyframe) => getSelectedKeyframeId({ keyframe })),
[selectedKeyframes],
);
@@ -168,10 +166,7 @@ export function useKeyframeBoxSelect({
if (isAdditive) {
setKeyframeSelection({
- keyframes: [
- ...initialKeyframesRef.current,
- ...intersectedRefs,
- ],
+ keyframes: [...initialKeyframesRef.current, ...intersectedRefs],
});
} else {
setKeyframeSelection({ keyframes: intersectedRefs });
diff --git a/apps/web/src/hooks/timeline/element/use-keyframe-drag.ts b/apps/web/src/timeline/hooks/element/use-keyframe-drag.ts
index 24bff26a..92b6f0d8 100644
--- a/apps/web/src/hooks/timeline/element/use-keyframe-drag.ts
+++ b/apps/web/src/timeline/hooks/element/use-keyframe-drag.ts
@@ -5,20 +5,20 @@ import {
useRef,
type MouseEvent as ReactMouseEvent,
} from "react";
-import { useEditor } from "@/hooks/use-editor";
-import { getKeyframeById } from "@/lib/animation";
+import { useEditor } from "@/editor/use-editor";
+import { getKeyframeById } from "@/animation";
import { useKeyframeSelection } from "./use-keyframe-selection";
import { roundToFrame, snappedSeekTime } from "opencut-wasm";
-import { timelineTimeToSnappedPixels } from "@/lib/timeline";
-import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
-import { TIMELINE_DRAG_THRESHOLD_PX } from "@/components/editor/panels/timeline/interaction";
-import { RetimeKeyframeCommand } from "@/lib/commands/timeline/element/keyframes/retime-keyframe";
-import { BatchCommand } from "@/lib/commands";
-import type { SelectedKeyframeRef } from "@/lib/animation/types";
-import type { TimelineElement } from "@/lib/timeline";
-import type { Command } from "@/lib/commands/base-command";
-import { registerCanceller } from "@/lib/cancel-interaction";
+import { timelineTimeToSnappedPixels } from "@/timeline";
+import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale";
+import { TICKS_PER_SECOND } from "@/wasm";
+import { TIMELINE_DRAG_THRESHOLD_PX } from "@/timeline/components/interaction";
+import { RetimeKeyframeCommand } from "@/commands/timeline/element/keyframes/retime-keyframe";
+import { BatchCommand } from "@/commands";
+import type { SelectedKeyframeRef } from "@/animation/types";
+import type { TimelineElement } from "@/timeline";
+import type { Command } from "@/commands/base-command";
+import { registerCanceller } from "@/editor/cancel-interaction";
export interface KeyframeDragState {
isDragging: boolean;
draggingKeyframeIds: Set<string>;
@@ -145,9 +145,12 @@ export function useKeyframeDrag({
if (!dragState.isDragging) return;
- const startX = mouseDownXRef.current ?? clientX;
- const rawDelta = Math.round(((clientX - startX) / pixelsPerSecond) * TICKS_PER_SECOND);
- const snappedDelta = roundToFrame({ time: rawDelta, rate: fps }) ?? rawDelta;
+ const startX = mouseDownXRef.current ?? clientX;
+ const rawDelta = Math.round(
+ ((clientX - startX) / pixelsPerSecond) * TICKS_PER_SECOND,
+ );
+ const snappedDelta =
+ roundToFrame({ time: rawDelta, rate: fps }) ?? rawDelta;
setDragState((previous) => ({ ...previous, deltaTime: snappedDelta }));
};
@@ -211,27 +214,27 @@ export function useKeyframeDrag({
event.preventDefault();
event.stopPropagation();
- mouseDownXRef.current = event.clientX;
+ mouseDownXRef.current = event.clientX;
- const anySelected = keyframes.some((keyframe) =>
- isKeyframeSelected({ keyframe }),
- );
+ const anySelected = keyframes.some((keyframe) =>
+ isKeyframeSelected({ keyframe }),
+ );
- const isModifierKey = event.shiftKey || event.metaKey || event.ctrlKey;
- if (!anySelected && !isModifierKey) {
- setKeyframeSelection({ keyframes });
- }
+ const isModifierKey = event.shiftKey || event.metaKey || event.ctrlKey;
+ if (!anySelected && !isModifierKey) {
+ setKeyframeSelection({ keyframes });
+ }
- const keyframeRefsToTrack = anySelected ? selectedKeyframes : keyframes;
+ const keyframeRefsToTrack = anySelected ? selectedKeyframes : keyframes;
- pendingDragRef.current = {
- keyframeRefs: keyframeRefsToTrack,
- startMouseX: event.clientX,
- };
- setIsPendingDrag(true);
- },
- [isKeyframeSelected, selectedKeyframes, setKeyframeSelection],
-);
+ pendingDragRef.current = {
+ keyframeRefs: keyframeRefsToTrack,
+ startMouseX: event.clientX,
+ };
+ setIsPendingDrag(true);
+ },
+ [isKeyframeSelected, selectedKeyframes, setKeyframeSelection],
+ );
const handleKeyframeClick = useCallback(
({
@@ -250,13 +253,18 @@ export function useKeyframeDrag({
const wasDrag =
mouseDownXRef.current !== null &&
Math.abs(event.clientX - mouseDownXRef.current) >
- TIMELINE_DRAG_THRESHOLD_PX;
+ TIMELINE_DRAG_THRESHOLD_PX;
mouseDownXRef.current = null;
if (wasDrag) return;
const duration = editor.timeline.getTotalDuration();
- const seekTime = snappedSeekTime({ time: displayedStartTime + indicatorTime, duration, rate: fps }) ?? displayedStartTime + indicatorTime;
+ const seekTime =
+ snappedSeekTime({
+ time: displayedStartTime + indicatorTime,
+ duration,
+ rate: fps,
+ }) ?? displayedStartTime + indicatorTime;
editor.playback.seek({ time: seekTime });
if (event.shiftKey) {
@@ -273,7 +281,13 @@ export function useKeyframeDrag({
isMultiKey: event.metaKey || event.ctrlKey,
});
},
- [toggleKeyframeSelection, selectKeyframeRange, editor, displayedStartTime, fps],
+ [
+ toggleKeyframeSelection,
+ selectKeyframeRange,
+ editor,
+ displayedStartTime,
+ fps,
+ ],
);
const getVisualOffsetPx = useCallback(
diff --git a/apps/web/src/hooks/timeline/element/use-keyframe-selection.ts b/apps/web/src/timeline/hooks/element/use-keyframe-selection.ts
index 2b5a0566..c5d3ffc9 100644
--- a/apps/web/src/hooks/timeline/element/use-keyframe-selection.ts
+++ b/apps/web/src/timeline/hooks/element/use-keyframe-selection.ts
@@ -1,6 +1,6 @@
import { useCallback, useSyncExternalStore } from "react";
-import { useEditor } from "@/hooks/use-editor";
-import type { SelectedKeyframeRef } from "@/lib/animation/types";
+import { useEditor } from "@/editor/use-editor";
+import type { SelectedKeyframeRef } from "@/animation/types";
export function getSelectedKeyframeId({
keyframe,
diff --git a/apps/web/src/hooks/timeline/use-edge-auto-scroll.ts b/apps/web/src/timeline/hooks/use-edge-auto-scroll.ts
index 509031bb..509031bb 100644
--- a/apps/web/src/hooks/timeline/use-edge-auto-scroll.ts
+++ b/apps/web/src/timeline/hooks/use-edge-auto-scroll.ts
diff --git a/apps/web/src/hooks/use-element-preview.ts b/apps/web/src/timeline/hooks/use-element-preview.ts
index 4f28be83..67213094 100644
--- a/apps/web/src/hooks/use-element-preview.ts
+++ b/apps/web/src/timeline/hooks/use-element-preview.ts
@@ -1,5 +1,5 @@
-import { useEditor } from "@/hooks/use-editor";
-import { findTrackInSceneTracks, type TimelineElement } from "@/lib/timeline";
+import { useEditor } from "@/editor/use-editor";
+import { findTrackInSceneTracks, type TimelineElement } from "@/timeline";
/**
* Subscribes to render tracks and returns the live (preview-aware) version of
diff --git a/apps/web/src/hooks/timeline/use-initial-scroll-bottom.ts b/apps/web/src/timeline/hooks/use-initial-scroll-bottom.ts
index 0b1cf92a..0b1cf92a 100644
--- a/apps/web/src/hooks/timeline/use-initial-scroll-bottom.ts
+++ b/apps/web/src/timeline/hooks/use-initial-scroll-bottom.ts
diff --git a/apps/web/src/hooks/timeline/use-scroll-position.ts b/apps/web/src/timeline/hooks/use-scroll-position.ts
index ad2c6374..ad2c6374 100644
--- a/apps/web/src/hooks/timeline/use-scroll-position.ts
+++ b/apps/web/src/timeline/hooks/use-scroll-position.ts
diff --git a/apps/web/src/hooks/timeline/use-scroll-sync.ts b/apps/web/src/timeline/hooks/use-scroll-sync.ts
index e147c106..e147c106 100644
--- a/apps/web/src/hooks/timeline/use-scroll-sync.ts
+++ b/apps/web/src/timeline/hooks/use-scroll-sync.ts
diff --git a/apps/web/src/hooks/timeline/use-snap-indicator-position.ts b/apps/web/src/timeline/hooks/use-snap-indicator-position.ts
index b941dcb5..2081b939 100644
--- a/apps/web/src/hooks/timeline/use-snap-indicator-position.ts
+++ b/apps/web/src/timeline/hooks/use-snap-indicator-position.ts
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
-import { timelineTimeToSnappedPixels } from "@/lib/timeline";
-import { TIMELINE_TRACK_LABELS_COLUMN_WIDTH_PX } from "@/components/editor/panels/timeline/layout";
+import { timelineTimeToSnappedPixels } from "@/timeline";
+import { TIMELINE_TRACK_LABELS_COLUMN_WIDTH_PX } from "@/timeline/components/layout";
interface UseSnapIndicatorPositionParams {
snapPoint: { time: number } | null;
zoomLevel: number;
diff --git a/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts b/apps/web/src/timeline/hooks/use-timeline-drag-drop.ts
index 25c24b8b..775b7337 100644
--- a/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts
+++ b/apps/web/src/timeline/hooks/use-timeline-drag-drop.ts
@@ -1,11 +1,11 @@
import { useState, useCallback, type RefObject } from "react";
-import { useEditor } from "@/hooks/use-editor";
-import { processMediaAssets } from "@/lib/media/processing";
+import { useEditor } from "@/editor/use-editor";
+import { processMediaAssets } from "@/media/processing";
import { toast } from "sonner";
-import { showMediaUploadToast } from "@/lib/media/upload-toast";
-import { DEFAULT_NEW_ELEMENT_DURATION } from "@/lib/timeline/creation";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
-import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
+import { showMediaUploadToast } from "@/media/upload-toast";
+import { DEFAULT_NEW_ELEMENT_DURATION } from "@/timeline/creation";
+import { TICKS_PER_SECOND } from "@/wasm";
+import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale";
import { roundToFrame } from "opencut-wasm";
import {
buildTextElement,
@@ -13,18 +13,18 @@ import {
buildStickerElement,
buildElementFromMedia,
buildEffectElement,
-} from "@/lib/timeline/element-utils";
-import { AddTrackCommand, InsertElementCommand } from "@/lib/commands/timeline";
-import { BatchCommand } from "@/lib/commands";
-import { computeDropTarget } from "@/components/editor/panels/timeline/drop-target";
-import { getDragData, hasDragData } from "@/lib/drag-data";
-import type { TrackType, DropTarget, ElementType } from "@/lib/timeline";
+} from "@/timeline/element-utils";
+import { AddTrackCommand, InsertElementCommand } from "@/commands/timeline";
+import { BatchCommand } from "@/commands";
+import { computeDropTarget } from "@/timeline/components/drop-target";
+import { getDragData, hasDragData } from "@/timeline/drag-data";
+import type { TrackType, DropTarget, ElementType } from "@/timeline";
import type {
MediaDragData,
GraphicDragData,
StickerDragData,
EffectDragData,
-} from "@/lib/timeline/drag";
+} from "@/timeline/drag";
interface UseTimelineDragDropProps {
containerRef: RefObject<HTMLDivElement | null>;
diff --git a/apps/web/src/hooks/timeline/use-timeline-playhead.ts b/apps/web/src/timeline/hooks/use-timeline-playhead.ts
index 1886222f..7d9cf447 100644
--- a/apps/web/src/hooks/timeline/use-timeline-playhead.ts
+++ b/apps/web/src/timeline/hooks/use-timeline-playhead.ts
@@ -1,16 +1,23 @@
import { snappedSeekTime } from "opencut-wasm";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
+import { TICKS_PER_SECOND } from "@/wasm";
import { useEffect, useCallback, useRef } from "react";
-import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
-import { useEditor } from "../use-editor";
+import { useEdgeAutoScroll } from "@/timeline/hooks/use-edge-auto-scroll";
+import { useEditor } from "@/editor/use-editor";
import { useShiftKey } from "@/hooks/use-shift-key";
-import { findSnapPoints, snapToNearestPoint } from "@/lib/timeline/snap-utils";
+import {
+ buildTimelineSnapPoints,
+ getTimelineSnapThresholdInTicks,
+ resolveTimelineSnap,
+} from "@/timeline/snapping";
+import { getBookmarkSnapPoints } from "@/timeline/bookmarks/index";
+import { getElementEdgeSnapPoints } from "@/timeline/element-snap-source";
+import { getAnimationKeyframeSnapPointsForTimeline } from "@/animation/timeline-snap-points";
import {
getCenteredLineLeft,
timelineTimeToPixels,
timelineTimeToSnappedPixels,
-} from "@/lib/timeline";
-import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
+} from "@/timeline";
+import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale";
interface UseTimelinePlayheadProps {
zoomLevel: number;
@@ -68,40 +75,45 @@ export function useTimelinePlayhead({
const rulerRect = ruler.getBoundingClientRect();
const relativeMouseX = event.clientX - rulerRect.left;
- const timelineContentWidth = timelineTimeToPixels({ time: duration, zoomLevel });
+ const timelineContentWidth = timelineTimeToPixels({
+ time: duration,
+ zoomLevel,
+ });
- const clampedMouseX = Math.max(
- 0,
- Math.min(timelineContentWidth, relativeMouseX),
- );
+ const clampedMouseX = Math.max(
+ 0,
+ Math.min(timelineContentWidth, relativeMouseX),
+ );
- const rawTimeSeconds = Math.max(
- 0,
- Math.min(
- duration / TICKS_PER_SECOND,
- clampedMouseX / (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel),
- ),
- );
- const rawTime = Math.round(rawTimeSeconds * TICKS_PER_SECOND);
+ const rawTimeSeconds = Math.max(
+ 0,
+ Math.min(
+ duration / TICKS_PER_SECOND,
+ clampedMouseX / (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel),
+ ),
+ );
+ const rawTime = Math.round(rawTimeSeconds * TICKS_PER_SECOND);
- const rate = activeProject.settings.fps;
- const frameTime = snappedSeekTime({ time: rawTime, duration, rate }) ?? rawTime;
+ const rate = activeProject.settings.fps;
+ const frameTime =
+ snappedSeekTime({ time: rawTime, duration, rate }) ?? rawTime;
const shouldSnap = snappingEnabled && !isShiftHeldRef.current;
const time = (() => {
if (!shouldSnap) return frameTime;
const tracks = editor.scenes.getActiveScene().tracks;
const bookmarks = editor.scenes.getActiveScene()?.bookmarks ?? [];
- const snapPoints = findSnapPoints({
- tracks,
- playheadTime: frameTime,
- bookmarks,
- enablePlayheadSnapping: false,
+ const snapPoints = buildTimelineSnapPoints({
+ sources: [
+ () => getElementEdgeSnapPoints({ tracks }),
+ () => getBookmarkSnapPoints({ bookmarks }),
+ () => getAnimationKeyframeSnapPointsForTimeline({ tracks }),
+ ],
});
- const snapResult = snapToNearestPoint({
+ const snapResult = resolveTimelineSnap({
targetTime: frameTime,
snapPoints,
- zoomLevel,
+ maxSnapDistance: getTimelineSnapThresholdInTicks({ zoomLevel }),
});
return snapResult.snapPoint ? snapResult.snappedTime : frameTime;
})();
@@ -119,7 +131,6 @@ export function useTimelinePlayhead({
activeProject.settings.fps,
isShiftHeldRef,
editor.scenes,
- editor.timeline,
],
);
@@ -249,10 +260,10 @@ export function useTimelinePlayhead({
const tracksViewport = tracksScrollRef.current;
if (!rulerViewport || !tracksViewport) return;
- const playheadPixels = timelineTimeToPixels({
- time,
- zoomLevel: zoomLevelRef.current,
- });
+ const playheadPixels = timelineTimeToPixels({
+ time,
+ zoomLevel: zoomLevelRef.current,
+ });
const viewportWidth = rulerViewport.clientWidth;
const scrollMinimum = 0;
const scrollMaximum = rulerViewport.scrollWidth - viewportWidth;
diff --git a/apps/web/src/hooks/timeline/use-timeline-resize.ts b/apps/web/src/timeline/hooks/use-timeline-resize.ts
index f246261a..fb8da291 100644
--- a/apps/web/src/hooks/timeline/use-timeline-resize.ts
+++ b/apps/web/src/timeline/hooks/use-timeline-resize.ts
@@ -1,29 +1,29 @@
import { useCallback, useEffect, useRef, useState } from "react";
import type { EditorCore } from "@/core";
-import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
-import { useEditor } from "@/hooks/use-editor";
-import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
+import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale";
+import { TICKS_PER_SECOND } from "@/wasm";
+import { useEditor } from "@/editor/use-editor";
+import { useElementSelection } from "@/timeline/hooks/element/use-element-selection";
import { useShiftKey } from "@/hooks/use-shift-key";
-import { useTimelineStore } from "@/stores/timeline-store";
+import { useTimelineStore } from "@/timeline/timeline-store";
import {
computeGroupResize,
type GroupResizeMember,
type GroupResizeResult,
type ResizeSide,
-} from "@/lib/timeline/group-resize";
+} from "@/timeline/group-resize";
import {
- findSnapPoints,
- snapToNearestPoint,
+ buildTimelineSnapPoints,
+ getTimelineSnapThresholdInTicks,
+ resolveTimelineSnap,
type SnapPoint,
-} from "@/lib/timeline/snap-utils";
-import type {
- SceneTracks,
- TimelineElement,
- TimelineTrack,
-} from "@/lib/timeline";
-import { isRetimableElement } from "@/lib/timeline";
-import { registerCanceller } from "@/lib/cancel-interaction";
+} from "@/timeline/snapping";
+import { getElementEdgeSnapPoints } from "@/timeline/element-snap-source";
+import { getPlayheadSnapPoints } from "@/timeline/playhead-snap-source";
+import type { SceneTracks, TimelineElement, TimelineTrack } from "@/timeline";
+import { isRetimableElement } from "@/timeline";
+import { registerCanceller } from "@/editor/cancel-interaction";
+import { getAnimationKeyframeSnapPointsForTimeline } from "@/animation/timeline-snap-points";
interface ResizeInteractionState {
side: ResizeSide;
@@ -269,13 +269,23 @@ function getSnappedResizeDelta({
return { deltaTime: rawDeltaTime };
}
- const snapPoints = findSnapPoints({
- tracks: editor.scenes.getActiveScene().tracks,
- playheadTime: editor.playback.getCurrentTime(),
- excludeElementIds: new Set(
- resizeState.members.map((member) => member.elementId),
- ),
+ const tracks = editor.scenes.getActiveScene().tracks;
+ const playheadTime = editor.playback.getCurrentTime();
+ const excludeElementIds = new Set(
+ resizeState.members.map((member) => member.elementId),
+ );
+ const snapPoints = buildTimelineSnapPoints({
+ sources: [
+ () => getElementEdgeSnapPoints({ tracks, excludeElementIds }),
+ () => getPlayheadSnapPoints({ playheadTime }),
+ () =>
+ getAnimationKeyframeSnapPointsForTimeline({
+ tracks,
+ excludeElementIds,
+ }),
+ ],
});
+ const maxSnapDistance = getTimelineSnapThresholdInTicks({ zoomLevel });
let closestSnapPoint: SnapPoint | null = null;
let closestSnapDistance = Infinity;
let deltaTime = rawDeltaTime;
@@ -285,10 +295,10 @@ function getSnappedResizeDelta({
resizeState.side === "left"
? member.startTime
: member.startTime + member.duration;
- const snapResult = snapToNearestPoint({
+ const snapResult = resolveTimelineSnap({
targetTime: baseEdgeTime + rawDeltaTime,
snapPoints,
- zoomLevel,
+ maxSnapDistance,
});
if (snapResult.snapPoint && snapResult.snapDistance < closestSnapDistance) {
closestSnapDistance = snapResult.snapDistance;
diff --git a/apps/web/src/hooks/timeline/use-timeline-seek.ts b/apps/web/src/timeline/hooks/use-timeline-seek.ts
index 756ec7ee..889177c9 100644
--- a/apps/web/src/hooks/timeline/use-timeline-seek.ts
+++ b/apps/web/src/timeline/hooks/use-timeline-seek.ts
@@ -1,9 +1,9 @@
import { useCallback, useRef } from "react";
import type { MutableRefObject, RefObject } from "react";
-import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
+import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale";
import { snappedSeekTime } from "opencut-wasm";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
-import { useEditor } from "../use-editor";
+import { TICKS_PER_SECOND } from "@/wasm";
+import { useEditor } from "@/editor/use-editor";
interface UseTimelineSeekProps {
playheadRef: RefObject<HTMLDivElement | null>;
@@ -128,18 +128,19 @@ export function useTimelineSeek({
const mouseX = event.clientX - rect.left;
const scrollLeft = scrollContainer.scrollLeft;
- const rawTimeSeconds = Math.max(
- 0,
- Math.min(
- duration / TICKS_PER_SECOND,
- (mouseX + scrollLeft) /
- (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel),
- ),
- );
- const rawTime = Math.round(rawTimeSeconds * TICKS_PER_SECOND);
+ const rawTimeSeconds = Math.max(
+ 0,
+ Math.min(
+ duration / TICKS_PER_SECOND,
+ (mouseX + scrollLeft) / (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel),
+ ),
+ );
+ const rawTime = Math.round(rawTimeSeconds * TICKS_PER_SECOND);
- const rate = activeProject?.settings.fps;
- const time = rate ? (snappedSeekTime({ time: rawTime, duration, rate }) ?? rawTime) : rawTime;
+ const rate = activeProject?.settings.fps;
+ const time = rate
+ ? (snappedSeekTime({ time: rawTime, duration, rate }) ?? rawTime)
+ : rawTime;
seek(time);
editor.project.setTimelineViewState({
viewState: {
@@ -157,7 +158,7 @@ export function useTimelineSeek({
seek,
editor,
activeProject?.settings.fps.numerator,
- activeProject?.settings.fps.denominator,
+ activeProject?.settings.fps.denominator,
],
);
diff --git a/apps/web/src/hooks/timeline/use-timeline-zoom.ts b/apps/web/src/timeline/hooks/use-timeline-zoom.ts
index 1f79a5b2..7929d495 100644
--- a/apps/web/src/hooks/timeline/use-timeline-zoom.ts
+++ b/apps/web/src/timeline/hooks/use-timeline-zoom.ts
@@ -7,16 +7,11 @@ import {
useRef,
useState,
} from "react";
-import {
- TIMELINE_ZOOM_ANCHOR_PLAYHEAD_THRESHOLD,
-} from "@/components/editor/panels/timeline/interaction";
-import {
- TIMELINE_ZOOM_MAX,
- TIMELINE_ZOOM_MIN,
-} from "@/lib/timeline/scale";
-import { timelineTimeToPixels } from "@/lib/timeline/pixel-utils";
-import { useEditor } from "@/hooks/use-editor";
-import { zoomToSlider } from "@/lib/timeline/zoom-utils";
+import { TIMELINE_ZOOM_ANCHOR_PLAYHEAD_THRESHOLD } from "@/timeline/components/interaction";
+import { TIMELINE_ZOOM_MAX, TIMELINE_ZOOM_MIN } from "@/timeline/scale";
+import { timelineTimeToPixels } from "@/timeline/pixel-utils";
+import { useEditor } from "@/editor/use-editor";
+import { zoomToSlider } from "@/timeline/zoom-utils";
interface UseTimelineZoomProps {
containerRef: RefObject<HTMLDivElement | null>;
@@ -54,10 +49,7 @@ export function useTimelineZoom({
const [zoomLevel, setZoomLevelRaw] = useState(() => {
if (initialZoom !== undefined) {
hasInitializedRef.current = true;
- return Math.max(
- minZoom,
- Math.min(TIMELINE_ZOOM_MAX, initialZoom),
- );
+ return Math.max(minZoom, Math.min(TIMELINE_ZOOM_MAX, initialZoom));
}
return minZoom;
});
@@ -88,22 +80,22 @@ export function useTimelineZoom({
return;
}
- // pinch-zoom (ctrl/meta + wheel)
- if (isZoomGesture) {
- const normalizedDelta =
- event.deltaMode === 1 ? event.deltaY * 16 : event.deltaY;
- const cappedDelta =
- Math.sign(normalizedDelta) * Math.min(Math.abs(normalizedDelta), 30);
- const zoomFactor = Math.exp(-cappedDelta / 300);
- setZoomLevel((prev) => {
- const nextZoom = Math.max(
- minZoom,
- Math.min(TIMELINE_ZOOM_MAX, prev * zoomFactor),
- );
- return nextZoom;
- });
- return;
- }
+ // pinch-zoom (ctrl/meta + wheel)
+ if (isZoomGesture) {
+ const normalizedDelta =
+ event.deltaMode === 1 ? event.deltaY * 16 : event.deltaY;
+ const cappedDelta =
+ Math.sign(normalizedDelta) * Math.min(Math.abs(normalizedDelta), 30);
+ const zoomFactor = Math.exp(-cappedDelta / 300);
+ setZoomLevel((prev) => {
+ const nextZoom = Math.max(
+ minZoom,
+ Math.min(TIMELINE_ZOOM_MAX, prev * zoomFactor),
+ );
+ return nextZoom;
+ });
+ return;
+ }
},
[minZoom, setZoomLevel],
);
@@ -111,9 +103,7 @@ export function useTimelineZoom({
useEffect(() => {
if (initialZoom !== undefined && !hasInitializedRef.current) {
hasInitializedRef.current = true;
- setZoomLevel(
- Math.max(minZoom, Math.min(TIMELINE_ZOOM_MAX, initialZoom)),
- );
+ setZoomLevel(Math.max(minZoom, Math.min(TIMELINE_ZOOM_MAX, initialZoom)));
return;
}
setZoomLevel((prev) => {
@@ -159,12 +149,10 @@ export function useTimelineZoom({
minZoom,
});
const isCrossingThresholdUp =
- previousSliderPercent <
- TIMELINE_ZOOM_ANCHOR_PLAYHEAD_THRESHOLD &&
+ previousSliderPercent < TIMELINE_ZOOM_ANCHOR_PLAYHEAD_THRESHOLD &&
sliderPercent >= TIMELINE_ZOOM_ANCHOR_PLAYHEAD_THRESHOLD;
const isCrossingThresholdDown =
- previousSliderPercent >=
- TIMELINE_ZOOM_ANCHOR_PLAYHEAD_THRESHOLD &&
+ previousSliderPercent >= TIMELINE_ZOOM_ANCHOR_PLAYHEAD_THRESHOLD &&
sliderPercent < TIMELINE_ZOOM_ANCHOR_PLAYHEAD_THRESHOLD;
const syncScroll = (scrollLeft: number) => {
@@ -186,8 +174,14 @@ export function useTimelineZoom({
}
if (sliderPercent >= TIMELINE_ZOOM_ANCHOR_PLAYHEAD_THRESHOLD) {
- const playheadPixelsBefore = timelineTimeToPixels({ time: playheadTime, zoomLevel: previousZoom });
- const playheadPixelsAfter = timelineTimeToPixels({ time: playheadTime, zoomLevel });
+ const playheadPixelsBefore = timelineTimeToPixels({
+ time: playheadTime,
+ zoomLevel: previousZoom,
+ });
+ const playheadPixelsAfter = timelineTimeToPixels({
+ time: playheadTime,
+ zoomLevel,
+ });
const viewportOffset = playheadPixelsBefore - currentScrollLeft;
const newScrollLeft = playheadPixelsAfter - viewportOffset;
diff --git a/apps/web/src/lib/timeline/index.ts b/apps/web/src/timeline/index.ts
index 5ee079b1..5ee079b1 100644
--- a/apps/web/src/lib/timeline/index.ts
+++ b/apps/web/src/timeline/index.ts
diff --git a/apps/web/src/lib/timeline/pixel-utils.ts b/apps/web/src/timeline/pixel-utils.ts
index 10f258aa..71e8466a 100644
--- a/apps/web/src/lib/timeline/pixel-utils.ts
+++ b/apps/web/src/timeline/pixel-utils.ts
@@ -1,5 +1,5 @@
-import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
+import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale";
+import { TICKS_PER_SECOND } from "@/wasm";
export const TIMELINE_INDICATOR_LINE_WIDTH_PX = 2;
diff --git a/apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts b/apps/web/src/timeline/placement/__tests__/resolve.test.ts
index 7cadb9b7..ebbfe578 100644
--- a/apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts
+++ b/apps/web/src/timeline/placement/__tests__/resolve.test.ts
@@ -11,9 +11,9 @@ import type {
TimelineTrack,
VideoElement,
VideoTrack,
-} from "@/lib/timeline";
-import type { Transform } from "@/lib/rendering";
-import { resolveTrackPlacement } from "@/lib/timeline/placement";
+} from "@/timeline";
+import type { Transform } from "@/rendering";
+import { resolveTrackPlacement } from "@/timeline/placement";
function buildTransform(): Transform {
return {
diff --git a/apps/web/src/lib/timeline/placement/apply.ts b/apps/web/src/timeline/placement/apply.ts
index 639b1b42..214b0611 100644
--- a/apps/web/src/lib/timeline/placement/apply.ts
+++ b/apps/web/src/timeline/placement/apply.ts
@@ -8,11 +8,11 @@ import type {
TimelineElement,
TimelineTrack,
VideoTrack,
-} from "@/lib/timeline";
+} from "@/timeline";
import { generateUUID } from "@/utils/id";
import { buildEmptyTrack } from "./track-factory";
import type { PlacementResult } from "./types";
-import { updateTrackInSceneTracks } from "@/lib/timeline/track-element-update";
+import { updateTrackInSceneTracks } from "@/timeline/track-element-update";
export function applyPlacement({
tracks,
diff --git a/apps/web/src/lib/timeline/placement/compatibility.ts b/apps/web/src/timeline/placement/compatibility.ts
index de1baff3..b1af547c 100644
--- a/apps/web/src/lib/timeline/placement/compatibility.ts
+++ b/apps/web/src/timeline/placement/compatibility.ts
@@ -1,4 +1,4 @@
-import type { ElementType, TrackType } from "@/lib/timeline";
+import type { ElementType, TrackType } from "@/timeline";
const ELEMENT_TRACK_MAP: Record<ElementType, TrackType> = {
audio: "audio",
diff --git a/apps/web/src/lib/timeline/placement/index.ts b/apps/web/src/timeline/placement/index.ts
index 530cb69f..530cb69f 100644
--- a/apps/web/src/lib/timeline/placement/index.ts
+++ b/apps/web/src/timeline/placement/index.ts
diff --git a/apps/web/src/lib/timeline/placement/insert-index.ts b/apps/web/src/timeline/placement/insert-index.ts
index 04fa5a4a..5e2786dc 100644
--- a/apps/web/src/lib/timeline/placement/insert-index.ts
+++ b/apps/web/src/timeline/placement/insert-index.ts
@@ -1,4 +1,4 @@
-import type { SceneTracks, TrackType } from "@/lib/timeline";
+import type { SceneTracks, TrackType } from "@/timeline";
export function getDefaultInsertIndexForTrack({
tracks,
diff --git a/apps/web/src/lib/timeline/placement/main-track.ts b/apps/web/src/timeline/placement/main-track.ts
index 7e206517..ab2ab7bd 100644
--- a/apps/web/src/lib/timeline/placement/main-track.ts
+++ b/apps/web/src/timeline/placement/main-track.ts
@@ -1,4 +1,4 @@
-import type { SceneTracks, TimelineElement, VideoTrack } from "@/lib/timeline";
+import type { SceneTracks, TimelineElement, VideoTrack } from "@/timeline";
export const MAIN_TRACK_NAME = "Main Track";
diff --git a/apps/web/src/lib/timeline/placement/overlap.ts b/apps/web/src/timeline/placement/overlap.ts
index 5e37c052..daf3e627 100644
--- a/apps/web/src/lib/timeline/placement/overlap.ts
+++ b/apps/web/src/timeline/placement/overlap.ts
@@ -1,4 +1,4 @@
-import type { TimelineElement } from "@/lib/timeline";
+import type { TimelineElement } from "@/timeline";
import type { PlacementTimeSpan } from "./types";
interface TrackWithElements {
diff --git a/apps/web/src/lib/timeline/placement/resolve.ts b/apps/web/src/timeline/placement/resolve.ts
index 67943912..26275392 100644
--- a/apps/web/src/lib/timeline/placement/resolve.ts
+++ b/apps/web/src/timeline/placement/resolve.ts
@@ -1,4 +1,4 @@
-import type { SceneTracks, TrackType, TimelineTrack } from "@/lib/timeline";
+import type { SceneTracks, TrackType, TimelineTrack } from "@/timeline";
import {
getDefaultInsertIndexForTrack,
getHighestInsertIndexForTrack,
diff --git a/apps/web/src/lib/timeline/placement/track-factory.ts b/apps/web/src/timeline/placement/track-factory.ts
index b73c5155..9c69ea8c 100644
--- a/apps/web/src/lib/timeline/placement/track-factory.ts
+++ b/apps/web/src/timeline/placement/track-factory.ts
@@ -1,4 +1,4 @@
-import { DEFAULT_TRACK_NAMES } from "@/lib/timeline/tracks";
+import { DEFAULT_TRACK_NAMES } from "@/timeline/tracks";
import type {
AudioTrack,
EffectTrack,
@@ -7,7 +7,7 @@ import type {
TrackType,
TimelineTrack,
VideoTrack,
-} from "@/lib/timeline";
+} from "@/timeline";
export function buildEmptyTrack({
id,
diff --git a/apps/web/src/lib/timeline/placement/types.ts b/apps/web/src/timeline/placement/types.ts
index 559e85cc..6847ab36 100644
--- a/apps/web/src/lib/timeline/placement/types.ts
+++ b/apps/web/src/timeline/placement/types.ts
@@ -1,4 +1,4 @@
-import type { ElementType, TrackType } from "@/lib/timeline";
+import type { ElementType, TrackType } from "@/timeline";
export interface PlacementTimeSpan {
startTime: number;
diff --git a/apps/web/src/timeline/playhead-snap-source.ts b/apps/web/src/timeline/playhead-snap-source.ts
new file mode 100644
index 00000000..88e21eba
--- /dev/null
+++ b/apps/web/src/timeline/playhead-snap-source.ts
@@ -0,0 +1,9 @@
+import type { SnapPoint } from "@/timeline/snapping";
+
+export function getPlayheadSnapPoints({
+ playheadTime,
+}: {
+ playheadTime: number;
+}): SnapPoint[] {
+ return [{ time: playheadTime, type: "playhead" }];
+}
diff --git a/apps/web/src/lib/timeline/ruler-utils.ts b/apps/web/src/timeline/ruler-utils.ts
index ea2982aa..d355ef31 100644
--- a/apps/web/src/lib/timeline/ruler-utils.ts
+++ b/apps/web/src/timeline/ruler-utils.ts
@@ -1,6 +1,6 @@
import type { FrameRate } from "opencut-wasm";
-import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
-import { frameRateToFloat } from "@/lib/fps/utils";
+import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale";
+import { frameRateToFloat } from "@/fps/utils";
/**
* frame intervals for labels - starts at 2 so there's always at least
diff --git a/apps/web/src/lib/timeline/scale.ts b/apps/web/src/timeline/scale.ts
index 7770f59f..7770f59f 100644
--- a/apps/web/src/lib/timeline/scale.ts
+++ b/apps/web/src/timeline/scale.ts
diff --git a/apps/web/src/lib/scenes.ts b/apps/web/src/timeline/scenes.ts
index 91285275..d48c943c 100644
--- a/apps/web/src/lib/scenes.ts
+++ b/apps/web/src/timeline/scenes.ts
@@ -1,7 +1,7 @@
-import type { TScene } from "@/lib/timeline";
+import type { TScene } from "@/timeline";
import { generateUUID } from "@/utils/id";
-import { calculateTotalDuration } from "@/lib/timeline";
-import { MAIN_TRACK_NAME } from "@/lib/timeline/placement/main-track";
+import { calculateTotalDuration } from "@/timeline";
+import { MAIN_TRACK_NAME } from "@/timeline/placement/main-track";
export function getMainScene({ scenes }: { scenes: TScene[] }): TScene | null {
return scenes.find((scene) => scene.isMain) || null;
diff --git a/apps/web/src/timeline/snapping/build.ts b/apps/web/src/timeline/snapping/build.ts
new file mode 100644
index 00000000..87d3db2e
--- /dev/null
+++ b/apps/web/src/timeline/snapping/build.ts
@@ -0,0 +1,17 @@
+import type { SnapPoint, TimelineSnapPointSource } from "./types";
+
+export function buildTimelineSnapPoints({
+ sources,
+}: {
+ sources: TimelineSnapPointSource[];
+}): SnapPoint[] {
+ const snapPoints: SnapPoint[] = [];
+
+ for (const source of sources) {
+ for (const snapPoint of source()) {
+ snapPoints.push(snapPoint);
+ }
+ }
+
+ return snapPoints;
+}
diff --git a/apps/web/src/timeline/snapping/index.ts b/apps/web/src/timeline/snapping/index.ts
new file mode 100644
index 00000000..73f49272
--- /dev/null
+++ b/apps/web/src/timeline/snapping/index.ts
@@ -0,0 +1,9 @@
+export { buildTimelineSnapPoints } from "./build";
+export { resolveTimelineSnap } from "./resolve";
+export { getTimelineSnapThresholdInTicks } from "./threshold";
+export type {
+ SnapPoint,
+ SnapPointType,
+ SnapResult,
+ TimelineSnapPointSource,
+} from "./types";
diff --git a/apps/web/src/timeline/snapping/resolve.ts b/apps/web/src/timeline/snapping/resolve.ts
new file mode 100644
index 00000000..2f00d081
--- /dev/null
+++ b/apps/web/src/timeline/snapping/resolve.ts
@@ -0,0 +1,28 @@
+import type { SnapPoint, SnapResult } from "./types";
+
+export function resolveTimelineSnap({
+ targetTime,
+ snapPoints,
+ maxSnapDistance,
+}: {
+ targetTime: number;
+ snapPoints: SnapPoint[];
+ maxSnapDistance: number;
+}): SnapResult {
+ let closestSnapPoint: SnapPoint | null = null;
+ let closestDistance = Infinity;
+
+ for (const snapPoint of snapPoints) {
+ const distance = Math.abs(targetTime - snapPoint.time);
+ if (distance <= maxSnapDistance && distance < closestDistance) {
+ closestDistance = distance;
+ closestSnapPoint = snapPoint;
+ }
+ }
+
+ return {
+ snappedTime: closestSnapPoint ? closestSnapPoint.time : targetTime,
+ snapPoint: closestSnapPoint,
+ snapDistance: closestDistance,
+ };
+}
diff --git a/apps/web/src/timeline/snapping/threshold.ts b/apps/web/src/timeline/snapping/threshold.ts
new file mode 100644
index 00000000..f03111dc
--- /dev/null
+++ b/apps/web/src/timeline/snapping/threshold.ts
@@ -0,0 +1,15 @@
+import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale";
+import { TICKS_PER_SECOND } from "@/wasm";
+
+const DEFAULT_TIMELINE_SNAP_THRESHOLD_PX = 10;
+
+export function getTimelineSnapThresholdInTicks({
+ zoomLevel,
+ snapThresholdPx = DEFAULT_TIMELINE_SNAP_THRESHOLD_PX,
+}: {
+ zoomLevel: number;
+ snapThresholdPx?: number;
+}): number {
+ const pixelsPerSecond = BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel;
+ return (snapThresholdPx / pixelsPerSecond) * TICKS_PER_SECOND;
+}
diff --git a/apps/web/src/timeline/snapping/types.ts b/apps/web/src/timeline/snapping/types.ts
new file mode 100644
index 00000000..2a6d0903
--- /dev/null
+++ b/apps/web/src/timeline/snapping/types.ts
@@ -0,0 +1,21 @@
+export type SnapPointType =
+ | "element-start"
+ | "element-end"
+ | "playhead"
+ | "bookmark"
+ | "keyframe";
+
+export interface SnapPoint {
+ time: number;
+ type: SnapPointType;
+ elementId?: string;
+ trackId?: string;
+}
+
+export interface SnapResult {
+ snappedTime: number;
+ snapPoint: SnapPoint | null;
+ snapDistance: number;
+}
+
+export type TimelineSnapPointSource = () => Iterable<SnapPoint>;
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/timeline/timeline-store.ts
index b7ddb885..b7ddb885 100644
--- a/apps/web/src/stores/timeline-store.ts
+++ b/apps/web/src/timeline/timeline-store.ts
diff --git a/apps/web/src/lib/timeline/track-capabilities.ts b/apps/web/src/timeline/track-capabilities.ts
index 08ee841c..e6ff3945 100644
--- a/apps/web/src/lib/timeline/track-capabilities.ts
+++ b/apps/web/src/timeline/track-capabilities.ts
@@ -5,7 +5,7 @@ import type {
GraphicTrack,
TextTrack,
EffectTrack,
-} from "@/lib/timeline";
+} from "@/timeline";
export function canTrackHaveAudio(
track: TimelineTrack,
diff --git a/apps/web/src/lib/timeline/track-element-update.ts b/apps/web/src/timeline/track-element-update.ts
index f1c2028e..7d86a4e8 100644
--- a/apps/web/src/lib/timeline/track-element-update.ts
+++ b/apps/web/src/timeline/track-element-update.ts
@@ -1,4 +1,4 @@
-import type { SceneTracks, TimelineElement, TimelineTrack } from "@/lib/timeline";
+import type { SceneTracks, TimelineElement, TimelineTrack } from "@/timeline";
export function findTrackInSceneTracks({
tracks,
diff --git a/apps/web/src/lib/timeline/tracks.ts b/apps/web/src/timeline/tracks.ts
index 181b9fcf..9ce5511d 100644
--- a/apps/web/src/lib/timeline/tracks.ts
+++ b/apps/web/src/timeline/tracks.ts
@@ -1,4 +1,4 @@
-import type { TrackType } from "@/lib/timeline";
+import type { TrackType } from "@/timeline";
export const DEFAULT_TRACK_NAMES: Record<TrackType, string> = {
video: "Video track",
diff --git a/apps/web/src/lib/timeline/types.ts b/apps/web/src/timeline/types.ts
index ca9ac999..cd543cdf 100644
--- a/apps/web/src/lib/timeline/types.ts
+++ b/apps/web/src/timeline/types.ts
@@ -1,8 +1,8 @@
-import type { ElementAnimations } from "@/lib/animation/types";
-import type { Effect } from "@/lib/effects/types";
-import type { Mask } from "@/lib/masks/types";
-import type { ParamValues } from "@/lib/params";
-import type { BlendMode, Transform } from "@/lib/rendering";
+import type { ElementAnimations } from "@/animation/types";
+import type { Effect } from "@/effects/types";
+import type { Mask } from "@/masks/types";
+import type { ParamValues } from "@/params";
+import type { BlendMode, Transform } from "@/rendering";
export type ElementRef = {
trackId: string;
diff --git a/apps/web/src/lib/timeline/update-pipeline.ts b/apps/web/src/timeline/update-pipeline.ts
index 2be73441..beacf897 100644
--- a/apps/web/src/lib/timeline/update-pipeline.ts
+++ b/apps/web/src/timeline/update-pipeline.ts
@@ -1,11 +1,11 @@
-import { clampAnimationsToDuration } from "@/lib/animation";
+import { clampAnimationsToDuration } from "@/animation";
import {
clampRetimeRate,
getSourceSpanAtClipTime,
getTimelineDurationForSourceSpan,
-} from "@/lib/retime";
-import type { RetimeConfig, SceneTracks, TimelineElement } from "@/lib/timeline";
-import { isRetimableElement } from "@/lib/timeline";
+} from "@/retime";
+import type { RetimeConfig, SceneTracks, TimelineElement } from "@/timeline";
+import { isRetimableElement } from "@/timeline";
type ElementUpdateField = keyof TimelineElement | string;
diff --git a/apps/web/src/lib/timeline/zoom-utils.ts b/apps/web/src/timeline/zoom-utils.ts
index 680f8d0d..39090d92 100644
--- a/apps/web/src/lib/timeline/zoom-utils.ts
+++ b/apps/web/src/timeline/zoom-utils.ts
@@ -1,8 +1,8 @@
import {
BASE_TIMELINE_PIXELS_PER_SECOND,
TIMELINE_ZOOM_MAX,
-} from "@/lib/timeline/scale";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
+} from "@/timeline/scale";
+import { TICKS_PER_SECOND } from "@/wasm";
const PADDING_MAX_RATIO = 0.75;
const PADDING_MIN_RATIO = 0.15;
diff --git a/apps/web/src/lib/transcription/audio.ts b/apps/web/src/transcription/audio.ts
index e2dc96d9..e2dc96d9 100644
--- a/apps/web/src/lib/transcription/audio.ts
+++ b/apps/web/src/transcription/audio.ts
diff --git a/apps/web/src/lib/transcription/caption-defaults.ts b/apps/web/src/transcription/caption-defaults.ts
index fc277158..fc277158 100644
--- a/apps/web/src/lib/transcription/caption-defaults.ts
+++ b/apps/web/src/transcription/caption-defaults.ts
diff --git a/apps/web/src/lib/transcription/caption.ts b/apps/web/src/transcription/caption.ts
index 5e23bad8..8e6ae864 100644
--- a/apps/web/src/lib/transcription/caption.ts
+++ b/apps/web/src/transcription/caption.ts
@@ -1,8 +1,8 @@
-import type { TranscriptionSegment, CaptionChunk } from "@/lib/transcription/types";
+import type { TranscriptionSegment, CaptionChunk } from "@/transcription/types";
import {
DEFAULT_WORDS_PER_CAPTION,
MIN_CAPTION_DURATION_SECONDS,
-} from "@/lib/transcription/caption-defaults";
+} from "@/transcription/caption-defaults";
export function buildCaptionChunks({
segments,
diff --git a/apps/web/src/lib/transcription/diagnostics.ts b/apps/web/src/transcription/diagnostics.ts
index a1921699..6765d07f 100644
--- a/apps/web/src/lib/transcription/diagnostics.ts
+++ b/apps/web/src/transcription/diagnostics.ts
@@ -1,5 +1,5 @@
import type { DiagnosticsManager } from "@/core/managers/diagnostics-manager";
-import { timelineHasAudio } from "@/lib/media/audio";
+import { timelineHasAudio } from "@/media/audio";
export const TRANSCRIPTION_DIAGNOSTICS_SCOPE = "transcription";
diff --git a/apps/web/src/lib/transcription/languages.ts b/apps/web/src/transcription/languages.ts
index e5678145..e5678145 100644
--- a/apps/web/src/lib/transcription/languages.ts
+++ b/apps/web/src/transcription/languages.ts
diff --git a/apps/web/src/lib/transcription/models.ts b/apps/web/src/transcription/models.ts
index ee9047e7..ee9047e7 100644
--- a/apps/web/src/lib/transcription/models.ts
+++ b/apps/web/src/transcription/models.ts
diff --git a/apps/web/src/lib/transcription/supported-languages.ts b/apps/web/src/transcription/supported-languages.ts
index 0adc1332..0adc1332 100644
--- a/apps/web/src/lib/transcription/supported-languages.ts
+++ b/apps/web/src/transcription/supported-languages.ts
diff --git a/apps/web/src/lib/transcription/types.ts b/apps/web/src/transcription/types.ts
index 7ab0da7f..7ab0da7f 100644
--- a/apps/web/src/lib/transcription/types.ts
+++ b/apps/web/src/transcription/types.ts
diff --git a/apps/web/src/lib/wasm/index.ts b/apps/web/src/wasm/index.ts
index 60031a0e..60031a0e 100644
--- a/apps/web/src/lib/wasm/index.ts
+++ b/apps/web/src/wasm/index.ts
diff --git a/apps/web/src/lib/wasm/ticks.ts b/apps/web/src/wasm/ticks.ts
index ababe6b3..ababe6b3 100644
--- a/apps/web/src/lib/wasm/ticks.ts
+++ b/apps/web/src/wasm/ticks.ts