summaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorMaze <167211895+mazeincoding@users.noreply.github.com>2026-01-31 00:20:04 +0100
committerGitHub <noreply@github.com>2026-01-31 00:20:04 +0100
commit7bf0984698e43b9aa2dd879f65951318ed410390 (patch)
treeccf2cd590a47d550152afc07129cb55c71a9ec35 /apps
parent0173db99448d1c4c6415dcf15272e17551b48b04 (diff)
downloadopencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz
codebase overhaul (#697)
Diffstat (limited to 'apps')
-rw-r--r--apps/transcription/README.md86
-rw-r--r--apps/transcription/requirements.txt5
-rw-r--r--apps/transcription/transcription.py143
-rw-r--r--apps/web/.env.example41
-rw-r--r--apps/web/.gitignore8
-rw-r--r--apps/web/Dockerfile65
-rw-r--r--apps/web/drizzle.config.ts12
-rw-r--r--apps/web/migrations/0000_brainy_saracen.sql (renamed from apps/web/migrations/0000_hot_the_fallen.sql)8
-rw-r--r--apps/web/migrations/0001_tricky_jackpot.sql8
-rw-r--r--apps/web/migrations/0002_cuddly_pretty_boy.sql1
-rw-r--r--apps/web/migrations/0003_long_polaris.sql10
-rw-r--r--apps/web/migrations/meta/0000_snapshot.json39
-rw-r--r--apps/web/migrations/meta/0001_snapshot.json344
-rw-r--r--apps/web/migrations/meta/0002_snapshot.json345
-rw-r--r--apps/web/migrations/meta/0003_snapshot.json365
-rw-r--r--apps/web/migrations/meta/_journal.json27
-rw-r--r--apps/web/next-env.d.ts6
-rw-r--r--apps/web/package.json33
-rw-r--r--apps/web/public/frame.svg10
-rw-r--r--apps/web/public/logo.pngbin1789 -> 0 bytes
-rw-r--r--apps/web/public/logos/opencut/1k/logo-black-with-text.pngbin0 -> 10331 bytes
-rw-r--r--apps/web/public/logos/opencut/1k/logo-black.pngbin0 -> 2101 bytes
-rw-r--r--apps/web/public/logos/opencut/1k/logo-white-with-text.pngbin0 -> 10839 bytes
-rw-r--r--apps/web/public/logos/opencut/1k/logo-white.pngbin0 -> 2149 bytes
-rw-r--r--apps/web/public/logos/opencut/2k/logo-black-with-text.pngbin0 -> 22647 bytes
-rw-r--r--apps/web/public/logos/opencut/2k/logo-black.pngbin0 -> 4815 bytes
-rw-r--r--apps/web/public/logos/opencut/2k/logo-white-with-text.pngbin0 -> 22670 bytes
-rw-r--r--apps/web/public/logos/opencut/2k/logo-white.pngbin0 -> 4153 bytes
-rw-r--r--apps/web/public/logos/opencut/4k/logo-black-with-text.pngbin0 -> 52288 bytes
-rw-r--r--apps/web/public/logos/opencut/4k/logo-black.pngbin0 -> 11217 bytes
-rw-r--r--apps/web/public/logos/opencut/4k/logo-white-with-text.pngbin0 -> 54360 bytes
-rw-r--r--apps/web/public/logos/opencut/4k/logo-white.pngbin0 -> 10259 bytes
-rw-r--r--apps/web/public/logos/opencut/svg/logo.svg (renamed from apps/web/public/logo.svg)0
-rw-r--r--apps/web/public/logos/others/fal.svg3
-rw-r--r--apps/web/public/logos/others/vercel.svg3
-rw-r--r--apps/web/src/app/(auth)/login/page.tsx146
-rw-r--r--apps/web/src/app/(auth)/signup/page.tsx162
-rw-r--r--apps/web/src/app/animation/page.tsx24
-rw-r--r--apps/web/src/app/api/auth/[...all]/route.ts2
-rw-r--r--apps/web/src/app/api/get-upload-url/route.ts128
-rw-r--r--apps/web/src/app/api/health/route.ts6
-rw-r--r--apps/web/src/app/api/sounds/search/route.ts467
-rw-r--r--apps/web/src/app/api/transcribe/route.ts189
-rw-r--r--apps/web/src/app/api/waitlist/export/route.ts83
-rw-r--r--apps/web/src/app/base-page.tsx53
-rw-r--r--apps/web/src/app/blog/[slug]/page.tsx246
-rw-r--r--apps/web/src/app/blog/page.tsx151
-rw-r--r--apps/web/src/app/contributors/page.tsx518
-rw-r--r--apps/web/src/app/editor/[project_id]/layout.tsx9
-rw-r--r--apps/web/src/app/editor/[project_id]/page.tsx527
-rw-r--r--apps/web/src/app/globals.css394
-rw-r--r--apps/web/src/app/layout.tsx86
-rw-r--r--apps/web/src/app/metadata.ts164
-rw-r--r--apps/web/src/app/page.tsx22
-rw-r--r--apps/web/src/app/privacy/page.tsx512
-rw-r--r--apps/web/src/app/projects/page.tsx1498
-rw-r--r--apps/web/src/app/projects/store.ts129
-rw-r--r--apps/web/src/app/roadmap/page.tsx371
-rw-r--r--apps/web/src/app/robots.ts18
-rw-r--r--apps/web/src/app/rss.xml/route.ts78
-rw-r--r--apps/web/src/app/sitemap.ts110
-rw-r--r--apps/web/src/app/sponsors/page.tsx82
-rw-r--r--apps/web/src/app/terms/page.tsx580
-rw-r--r--apps/web/src/app/why-not-capcut/page.tsx191
-rw-r--r--apps/web/src/components/delete-project-dialog.tsx68
-rw-r--r--apps/web/src/components/editable-timecode.tsx154
-rw-r--r--apps/web/src/components/editor/audio-waveform.tsx162
-rw-r--r--apps/web/src/components/editor/dialogs/delete-project-dialog.tsx84
-rw-r--r--apps/web/src/components/editor/dialogs/migration-dialog.tsx44
-rw-r--r--apps/web/src/components/editor/dialogs/project-info-dialog.tsx83
-rw-r--r--apps/web/src/components/editor/dialogs/rename-project-dialog.tsx72
-rw-r--r--apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx229
-rw-r--r--apps/web/src/components/editor/editor-header.tsx279
-rw-r--r--apps/web/src/components/editor/export-button.tsx588
-rw-r--r--apps/web/src/components/editor/layout-guide-overlay.tsx30
-rw-r--r--apps/web/src/components/editor/media-panel/drag-overlay.tsx57
-rw-r--r--apps/web/src/components/editor/media-panel/index.tsx52
-rw-r--r--apps/web/src/components/editor/media-panel/store.ts86
-rw-r--r--apps/web/src/components/editor/media-panel/tabbar.tsx99
-rw-r--r--apps/web/src/components/editor/media-panel/views/captions.tsx303
-rw-r--r--apps/web/src/components/editor/media-panel/views/media.tsx545
-rw-r--r--apps/web/src/components/editor/media-panel/views/settings.tsx332
-rw-r--r--apps/web/src/components/editor/media-panel/views/sounds.tsx562
-rw-r--r--apps/web/src/components/editor/media-panel/views/stickers.tsx612
-rw-r--r--apps/web/src/components/editor/media-panel/views/text.tsx36
-rw-r--r--apps/web/src/components/editor/onboarding.tsx227
-rw-r--r--apps/web/src/components/editor/panel-base-view.tsx85
-rw-r--r--apps/web/src/components/editor/panel-preset-selector.tsx91
-rw-r--r--apps/web/src/components/editor/panels/assets/drag-overlay.tsx61
-rw-r--r--apps/web/src/components/editor/panels/assets/draggable-item.tsx244
-rw-r--r--apps/web/src/components/editor/panels/assets/index.tsx52
-rw-r--r--apps/web/src/components/editor/panels/assets/tabbar.tsx116
-rw-r--r--apps/web/src/components/editor/panels/assets/views/captions.tsx152
-rw-r--r--apps/web/src/components/editor/panels/assets/views/media.tsx675
-rw-r--r--apps/web/src/components/editor/panels/assets/views/settings.tsx394
-rw-r--r--apps/web/src/components/editor/panels/assets/views/sounds.tsx574
-rw-r--r--apps/web/src/components/editor/panels/assets/views/stickers.tsx615
-rw-r--r--apps/web/src/components/editor/panels/assets/views/text.tsx46
-rw-r--r--apps/web/src/components/editor/panels/panel-base-view.tsx85
-rw-r--r--apps/web/src/components/editor/panels/preview/index.tsx124
-rw-r--r--apps/web/src/components/editor/panels/properties/audio-properties.tsx5
-rw-r--r--apps/web/src/components/editor/panels/properties/index.tsx68
-rw-r--r--apps/web/src/components/editor/panels/properties/property-item.tsx91
-rw-r--r--apps/web/src/components/editor/panels/properties/text-properties.tsx410
-rw-r--r--apps/web/src/components/editor/panels/properties/video-properties.tsx9
-rw-r--r--apps/web/src/components/editor/preview-panel.tsx1134
-rw-r--r--apps/web/src/components/editor/properties-panel/audio-properties.tsx5
-rw-r--r--apps/web/src/components/editor/properties-panel/index.tsx70
-rw-r--r--apps/web/src/components/editor/properties-panel/media-properties.tsx5
-rw-r--r--apps/web/src/components/editor/properties-panel/property-item.tsx86
-rw-r--r--apps/web/src/components/editor/properties-panel/text-properties.tsx370
-rw-r--r--apps/web/src/components/editor/scenes-view.tsx367
-rw-r--r--apps/web/src/components/editor/selection-box.tsx77
-rw-r--r--apps/web/src/components/editor/snap-indicator.tsx77
-rw-r--r--apps/web/src/components/editor/speed-control.tsx46
-rw-r--r--apps/web/src/components/editor/timeline/audio-waveform.tsx176
-rw-r--r--apps/web/src/components/editor/timeline/bookmarks.tsx111
-rw-r--r--apps/web/src/components/editor/timeline/drag-line.tsx28
-rw-r--r--apps/web/src/components/editor/timeline/index.tsx1385
-rw-r--r--apps/web/src/components/editor/timeline/snap-indicator.tsx52
-rw-r--r--apps/web/src/components/editor/timeline/timeline-cache-indicator.tsx126
-rw-r--r--apps/web/src/components/editor/timeline/timeline-element.tsx861
-rw-r--r--apps/web/src/components/editor/timeline/timeline-marker.tsx67
-rw-r--r--apps/web/src/components/editor/timeline/timeline-playhead.tsx228
-rw-r--r--apps/web/src/components/editor/timeline/timeline-ruler.tsx83
-rw-r--r--apps/web/src/components/editor/timeline/timeline-tick.tsx39
-rw-r--r--apps/web/src/components/editor/timeline/timeline-toolbar.tsx696
-rw-r--r--apps/web/src/components/editor/timeline/timeline-track.tsx1290
-rw-r--r--apps/web/src/components/footer.tsx234
-rw-r--r--apps/web/src/components/gitHub-contribute-section.tsx47
-rw-r--r--apps/web/src/components/header-base.tsx41
-rw-r--r--apps/web/src/components/header.tsx180
-rw-r--r--apps/web/src/components/icons.tsx233
-rw-r--r--apps/web/src/components/keyboard-shortcuts-help.tsx245
-rw-r--r--apps/web/src/components/landing/handlebars.tsx238
-rw-r--r--apps/web/src/components/landing/hero.tsx103
-rw-r--r--apps/web/src/components/language-select.tsx204
-rw-r--r--apps/web/src/components/providers/editor-provider.tsx157
-rw-r--r--apps/web/src/components/providers/migrators/scenes-migrator.tsx177
-rw-r--r--apps/web/src/components/rename-project-dialog.tsx73
-rw-r--r--apps/web/src/components/storage-provider.tsx111
-rw-r--r--apps/web/src/components/theme-toggle.tsx41
-rw-r--r--apps/web/src/components/ui/accordion.tsx64
-rw-r--r--apps/web/src/components/ui/alert-dialog.tsx172
-rw-r--r--apps/web/src/components/ui/alert.tsx83
-rw-r--r--apps/web/src/components/ui/audio-player.tsx127
-rw-r--r--apps/web/src/components/ui/avatar.tsx56
-rw-r--r--apps/web/src/components/ui/badge.tsx48
-rw-r--r--apps/web/src/components/ui/breadcrumb.tsx146
-rw-r--r--apps/web/src/components/ui/button.tsx94
-rw-r--r--apps/web/src/components/ui/calendar.tsx114
-rw-r--r--apps/web/src/components/ui/card.tsx90
-rw-r--r--apps/web/src/components/ui/carousel.tsx388
-rw-r--r--apps/web/src/components/ui/chart.tsx590
-rw-r--r--apps/web/src/components/ui/checkbox.tsx34
-rw-r--r--apps/web/src/components/ui/color-picker.tsx579
-rw-r--r--apps/web/src/components/ui/command.tsx191
-rw-r--r--apps/web/src/components/ui/context-menu.tsx323
-rw-r--r--apps/web/src/components/ui/dialog.tsx166
-rw-r--r--apps/web/src/components/ui/draggable-item.tsx247
-rw-r--r--apps/web/src/components/ui/drawer.tsx138
-rw-r--r--apps/web/src/components/ui/dropdown-menu.tsx326
-rw-r--r--apps/web/src/components/ui/editable-timecode.tsx138
-rw-r--r--apps/web/src/components/ui/font-picker.tsx64
-rw-r--r--apps/web/src/components/ui/form.tsx216
-rw-r--r--apps/web/src/components/ui/hover-card.tsx26
-rw-r--r--apps/web/src/components/ui/image-timeline-treatment.tsx102
-rw-r--r--apps/web/src/components/ui/input-otp.tsx92
-rw-r--r--apps/web/src/components/ui/input-with-back.tsx142
-rw-r--r--apps/web/src/components/ui/input.tsx225
-rw-r--r--apps/web/src/components/ui/label.tsx20
-rw-r--r--apps/web/src/components/ui/menubar.tsx324
-rw-r--r--apps/web/src/components/ui/navigation-menu.tsx166
-rw-r--r--apps/web/src/components/ui/pagination.tsx148
-rw-r--r--apps/web/src/components/ui/phone-input.tsx246
-rw-r--r--apps/web/src/components/ui/popover.tsx30
-rw-r--r--apps/web/src/components/ui/progress.tsx32
-rw-r--r--apps/web/src/components/ui/prose.tsx26
-rw-r--r--apps/web/src/components/ui/radio-group.tsx52
-rw-r--r--apps/web/src/components/ui/react-markdown-wrapper.tsx26
-rw-r--r--apps/web/src/components/ui/resizable.tsx42
-rw-r--r--apps/web/src/components/ui/scroll-area.tsx20
-rw-r--r--apps/web/src/components/ui/select.tsx249
-rw-r--r--apps/web/src/components/ui/separator.tsx38
-rw-r--r--apps/web/src/components/ui/sheet.tsx182
-rw-r--r--apps/web/src/components/ui/sidebar.tsx1352
-rw-r--r--apps/web/src/components/ui/skeleton.tsx18
-rw-r--r--apps/web/src/components/ui/slider.tsx32
-rw-r--r--apps/web/src/components/ui/sonner.tsx46
-rw-r--r--apps/web/src/components/ui/spinner.tsx17
-rw-r--r--apps/web/src/components/ui/split-button.tsx120
-rw-r--r--apps/web/src/components/ui/sponsor-button.tsx39
-rw-r--r--apps/web/src/components/ui/switch.tsx34
-rw-r--r--apps/web/src/components/ui/table.tsx150
-rw-r--r--apps/web/src/components/ui/tabs.tsx62
-rw-r--r--apps/web/src/components/ui/textarea.tsx30
-rw-r--r--apps/web/src/components/ui/toast.tsx160
-rw-r--r--apps/web/src/components/ui/toaster.tsx31
-rw-r--r--apps/web/src/components/ui/toggle-group.tsx72
-rw-r--r--apps/web/src/components/ui/toggle.tsx56
-rw-r--r--apps/web/src/components/ui/tooltip.tsx111
-rw-r--r--apps/web/src/components/ui/video-player.tsx133
-rw-r--r--apps/web/src/constants/actions.ts295
-rw-r--r--apps/web/src/constants/editor-constants.ts15
-rw-r--r--apps/web/src/constants/export-constants.ts12
-rw-r--r--apps/web/src/constants/font-constants.ts122
-rw-r--r--apps/web/src/constants/language-constants.ts11
-rw-r--r--apps/web/src/constants/project-constants.ts27
-rw-r--r--apps/web/src/constants/site-constants.ts66
-rw-r--r--apps/web/src/constants/site.ts33
-rw-r--r--apps/web/src/constants/stickers-constants.ts16
-rw-r--r--apps/web/src/constants/text-constants.ts50
-rw-r--r--apps/web/src/constants/timeline-constants.ts107
-rw-r--r--apps/web/src/constants/timeline-constants.tsx72
-rw-r--r--apps/web/src/constants/transcription-constants.ts58
-rw-r--r--apps/web/src/core/index.ts50
-rw-r--r--apps/web/src/core/managers/audio-manager.ts335
-rw-r--r--apps/web/src/core/managers/commands.ts44
-rw-r--r--apps/web/src/core/managers/media-manager.ts162
-rw-r--r--apps/web/src/core/managers/playback-manager.ts162
-rw-r--r--apps/web/src/core/managers/project-manager.ts648
-rw-r--r--apps/web/src/core/managers/renderer-manager.ts129
-rw-r--r--apps/web/src/core/managers/save-manager.ts107
-rw-r--r--apps/web/src/core/managers/scenes-manager.ts316
-rw-r--r--apps/web/src/core/managers/selection-manager.ts35
-rw-r--r--apps/web/src/core/managers/timeline-manager.ts275
-rw-r--r--apps/web/src/data/colors/pattern-craft.ts76
-rw-r--r--apps/web/src/data/colors/solid.ts484
-rw-r--r--apps/web/src/data/colors/syntax-ui.tsx42
-rw-r--r--apps/web/src/env.ts45
-rw-r--r--apps/web/src/hooks/actions/use-action-handler.ts52
-rw-r--r--apps/web/src/hooks/actions/use-editor-actions.ts312
-rw-r--r--apps/web/src/hooks/auth/useLogin.ts60
-rw-r--r--apps/web/src/hooks/auth/useSignUp.ts65
-rw-r--r--apps/web/src/hooks/storage/use-local-storage.ts58
-rw-r--r--apps/web/src/hooks/timeline/element/use-element-interaction.ts619
-rw-r--r--apps/web/src/hooks/timeline/element/use-element-resize.ts331
-rw-r--r--apps/web/src/hooks/timeline/element/use-element-selection.ts114
-rw-r--r--apps/web/src/hooks/timeline/use-edge-auto-scroll.ts100
-rw-r--r--apps/web/src/hooks/timeline/use-scroll-sync.ts173
-rw-r--r--apps/web/src/hooks/timeline/use-selection-box.ts243
-rw-r--r--apps/web/src/hooks/timeline/use-snap-indicator-position.ts62
-rw-r--r--apps/web/src/hooks/timeline/use-timeline-drag-drop.ts460
-rw-r--r--apps/web/src/hooks/timeline/use-timeline-playhead.ts218
-rw-r--r--apps/web/src/hooks/timeline/use-timeline-seek.ts197
-rw-r--r--apps/web/src/hooks/timeline/use-timeline-snapping.ts159
-rw-r--r--apps/web/src/hooks/timeline/use-timeline-zoom.ts224
-rw-r--r--apps/web/src/hooks/use-aspect-ratio.ts97
-rw-r--r--apps/web/src/hooks/use-drag-drop.ts85
-rw-r--r--apps/web/src/hooks/use-edge-auto-scroll.ts101
-rw-r--r--apps/web/src/hooks/use-editor-actions.ts241
-rw-r--r--apps/web/src/hooks/use-editor.ts39
-rw-r--r--apps/web/src/hooks/use-file-upload.ts103
-rw-r--r--apps/web/src/hooks/use-frame-cache.ts348
-rw-r--r--apps/web/src/hooks/use-highlight-scroll.ts36
-rw-r--r--apps/web/src/hooks/use-infinite-scroll.ts46
-rw-r--r--apps/web/src/hooks/use-keybinding-conflicts.ts59
-rw-r--r--apps/web/src/hooks/use-keybindings.ts102
-rw-r--r--apps/web/src/hooks/use-keyboard-shortcuts-help.ts189
-rw-r--r--apps/web/src/hooks/use-mobile.ts19
-rw-r--r--apps/web/src/hooks/use-mobile.tsx21
-rw-r--r--apps/web/src/hooks/use-playback-controls.ts109
-rw-r--r--apps/web/src/hooks/use-raf-loop.ts20
-rw-r--r--apps/web/src/hooks/use-reveal-item.ts36
-rw-r--r--apps/web/src/hooks/use-selection-box.ts215
-rw-r--r--apps/web/src/hooks/use-sound-search.ts270
-rw-r--r--apps/web/src/hooks/use-timeline-element-resize.ts257
-rw-r--r--apps/web/src/hooks/use-timeline-playhead.ts218
-rw-r--r--apps/web/src/hooks/use-timeline-snapping.ts178
-rw-r--r--apps/web/src/hooks/use-timeline-zoom.ts60
-rw-r--r--apps/web/src/hooks/use-toast.ts191
-rw-r--r--apps/web/src/lib/actions/definitions.ts162
-rw-r--r--apps/web/src/lib/actions/index.ts3
-rw-r--r--apps/web/src/lib/actions/registry.ts61
-rw-r--r--apps/web/src/lib/actions/types.ts42
-rw-r--r--apps/web/src/lib/auth/client.ts6
-rw-r--r--apps/web/src/lib/auth/server.ts43
-rw-r--r--apps/web/src/lib/blog-query.ts64
-rw-r--r--apps/web/src/lib/blog/query.ts72
-rw-r--r--apps/web/src/lib/canvas-gradients.ts219
-rw-r--r--apps/web/src/lib/commands/base-command.ts11
-rw-r--r--apps/web/src/lib/commands/index.ts6
-rw-r--r--apps/web/src/lib/commands/media/add-media-asset.ts61
-rw-r--r--apps/web/src/lib/commands/media/index.ts2
-rw-r--r--apps/web/src/lib/commands/media/remove-media-asset.ts85
-rw-r--r--apps/web/src/lib/commands/project/index.ts1
-rw-r--r--apps/web/src/lib/commands/project/update-project-settings.ts46
-rw-r--r--apps/web/src/lib/commands/scene/create-scene.ts40
-rw-r--r--apps/web/src/lib/commands/scene/delete-scene.ts59
-rw-r--r--apps/web/src/lib/commands/scene/index.ts5
-rw-r--r--apps/web/src/lib/commands/scene/remove-bookmark.ts59
-rw-r--r--apps/web/src/lib/commands/scene/rename-scene.ts46
-rw-r--r--apps/web/src/lib/commands/scene/toggle-bookmark.ts52
-rw-r--r--apps/web/src/lib/commands/timeline/clipboard/index.ts1
-rw-r--r--apps/web/src/lib/commands/timeline/clipboard/paste.ts263
-rw-r--r--apps/web/src/lib/commands/timeline/element/delete-elements.ts48
-rw-r--r--apps/web/src/lib/commands/timeline/element/duplicate-elements.ts114
-rw-r--r--apps/web/src/lib/commands/timeline/element/index.ts11
-rw-r--r--apps/web/src/lib/commands/timeline/element/insert-element.ts311
-rw-r--r--apps/web/src/lib/commands/timeline/element/move-elements.ts127
-rw-r--r--apps/web/src/lib/commands/timeline/element/split-elements.ts133
-rw-r--r--apps/web/src/lib/commands/timeline/element/toggle-elements-muted.ts57
-rw-r--r--apps/web/src/lib/commands/timeline/element/toggle-elements-visibility.ts47
-rw-r--r--apps/web/src/lib/commands/timeline/element/update-element-duration.ts37
-rw-r--r--apps/web/src/lib/commands/timeline/element/update-element-start-time.ts48
-rw-r--r--apps/web/src/lib/commands/timeline/element/update-element-trim.ts48
-rw-r--r--apps/web/src/lib/commands/timeline/element/update-text-element.ts52
-rw-r--r--apps/web/src/lib/commands/timeline/index.ts3
-rw-r--r--apps/web/src/lib/commands/timeline/track/add-track.ts53
-rw-r--r--apps/web/src/lib/commands/timeline/track/index.ts4
-rw-r--r--apps/web/src/lib/commands/timeline/track/remove-track.ts35
-rw-r--r--apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts39
-rw-r--r--apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts40
-rw-r--r--apps/web/src/lib/db/index.ts19
-rw-r--r--apps/web/src/lib/db/schema.ts59
-rw-r--r--apps/web/src/lib/drag-data.ts48
-rw-r--r--apps/web/src/lib/editor-utils.ts46
-rw-r--r--apps/web/src/lib/export.ts311
-rw-r--r--apps/web/src/lib/font-config.ts39
-rw-r--r--apps/web/src/lib/gradients/canvas.ts923
-rw-r--r--apps/web/src/lib/gradients/index.ts2
-rw-r--r--apps/web/src/lib/gradients/parser.ts652
-rw-r--r--apps/web/src/lib/iconify-api.ts357
-rw-r--r--apps/web/src/lib/media-processing.ts89
-rw-r--r--apps/web/src/lib/media/audio.ts459
-rw-r--r--apps/web/src/lib/media/media-utils.ts32
-rw-r--r--apps/web/src/lib/media/mediabunny.ts226
-rw-r--r--apps/web/src/lib/media/processing.ts282
-rw-r--r--apps/web/src/lib/mediabunny-utils.ts311
-rw-r--r--apps/web/src/lib/rate-limit.ts21
-rw-r--r--apps/web/src/lib/scenes.ts103
-rw-r--r--apps/web/src/lib/schemas/waitlist.ts15
-rw-r--r--apps/web/src/lib/storage/indexeddb-adapter.ts89
-rw-r--r--apps/web/src/lib/storage/opfs-adapter.ts73
-rw-r--r--apps/web/src/lib/storage/storage-service.ts480
-rw-r--r--apps/web/src/lib/storage/types.ts69
-rw-r--r--apps/web/src/lib/time.ts325
-rw-r--r--apps/web/src/lib/timeline-renderer.ts279
-rw-r--r--apps/web/src/lib/timeline.ts54
-rw-r--r--apps/web/src/lib/timeline/bookmarks.ts61
-rw-r--r--apps/web/src/lib/timeline/drop-utils.ts234
-rw-r--r--apps/web/src/lib/timeline/element-utils.ts304
-rw-r--r--apps/web/src/lib/timeline/index.ts23
-rw-r--r--apps/web/src/lib/timeline/ruler-utils.ts228
-rw-r--r--apps/web/src/lib/timeline/track-utils.ts244
-rw-r--r--apps/web/src/lib/timeline/zoom-utils.ts53
-rw-r--r--apps/web/src/lib/transcription-utils.ts13
-rw-r--r--apps/web/src/lib/transcription/caption.ts49
-rw-r--r--apps/web/src/lib/utils.ts80
-rw-r--r--apps/web/src/lib/video-cache.ts186
-rw-r--r--apps/web/src/lib/zk-encryption.ts71
-rw-r--r--apps/web/src/middleware.ts30
-rw-r--r--apps/web/src/services/media/video-cache.ts314
-rw-r--r--apps/web/src/services/renderer/canvas-renderer.ts87
-rw-r--r--apps/web/src/services/renderer/nodes/base-node.ts35
-rw-r--r--apps/web/src/services/renderer/nodes/blur-background-node.ts75
-rw-r--r--apps/web/src/services/renderer/nodes/color-node.ts31
-rw-r--r--apps/web/src/services/renderer/nodes/image-node.ts93
-rw-r--r--apps/web/src/services/renderer/nodes/root-node.ts11
-rw-r--r--apps/web/src/services/renderer/nodes/sticker-node.ts87
-rw-r--r--apps/web/src/services/renderer/nodes/text-node.ts74
-rw-r--r--apps/web/src/services/renderer/nodes/video-node.ts84
-rw-r--r--apps/web/src/services/renderer/scene-builder.ts130
-rw-r--r--apps/web/src/services/renderer/scene-exporter.ts152
-rw-r--r--apps/web/src/services/storage/indexeddb-adapter.ts101
-rw-r--r--apps/web/src/services/storage/migrations/base.ts5
-rw-r--r--apps/web/src/services/storage/migrations/index.ts14
-rw-r--r--apps/web/src/services/storage/migrations/runner.ts134
-rw-r--r--apps/web/src/services/storage/migrations/v0-to-v1.ts93
-rw-r--r--apps/web/src/services/storage/migrations/v1-to-v2.ts339
-rw-r--r--apps/web/src/services/storage/migrations/v2-to-v3.ts92
-rw-r--r--apps/web/src/services/storage/migrations/version-manager.ts73
-rw-r--r--apps/web/src/services/storage/opfs-adapter.ts73
-rw-r--r--apps/web/src/services/storage/storage-service.ts475
-rw-r--r--apps/web/src/services/storage/types.ts66
-rw-r--r--apps/web/src/services/transcription/index.ts186
-rw-r--r--apps/web/src/services/transcription/worker.ts176
-rw-r--r--apps/web/src/stores/assets-panel-store.tsx107
-rw-r--r--apps/web/src/stores/editor-store.ts129
-rw-r--r--apps/web/src/stores/keybindings-store.ts409
-rw-r--r--apps/web/src/stores/keybindings/migrations/index.ts28
-rw-r--r--apps/web/src/stores/keybindings/migrations/v2-to-v3.ts26
-rw-r--r--apps/web/src/stores/media-store.ts296
-rw-r--r--apps/web/src/stores/panel-store.ts292
-rw-r--r--apps/web/src/stores/playback-store.ts174
-rw-r--r--apps/web/src/stores/project-store.ts571
-rw-r--r--apps/web/src/stores/scene-store.ts318
-rw-r--r--apps/web/src/stores/sounds-store.ts475
-rw-r--r--apps/web/src/stores/stickers-store.ts343
-rw-r--r--apps/web/src/stores/text-properties-store.ts30
-rw-r--r--apps/web/src/stores/timeline-store.ts1913
-rw-r--r--apps/web/src/types/assets.ts9
-rw-r--r--apps/web/src/types/blog.ts92
-rw-r--r--apps/web/src/types/drag.ts21
-rw-r--r--apps/web/src/types/editor.ts29
-rw-r--r--apps/web/src/types/export.ts33
-rw-r--r--apps/web/src/types/keybinding.ts124
-rw-r--r--apps/web/src/types/language.ts4
-rw-r--r--apps/web/src/types/media.ts17
-rw-r--r--apps/web/src/types/playback.ts21
-rw-r--r--apps/web/src/types/post.ts92
-rw-r--r--apps/web/src/types/project.ts74
-rw-r--r--apps/web/src/types/sounds.ts62
-rw-r--r--apps/web/src/types/stickers.ts3
-rw-r--r--apps/web/src/types/time.ts1
-rw-r--r--apps/web/src/types/timeline.ts286
-rw-r--r--apps/web/src/types/transcription.ts47
-rw-r--r--apps/web/src/utils/browser.ts17
-rw-r--r--apps/web/src/utils/date.ts7
-rw-r--r--apps/web/src/utils/geometry.ts13
-rw-r--r--apps/web/src/utils/id.ts28
-rw-r--r--apps/web/src/utils/math.ts11
-rw-r--r--apps/web/src/utils/platform.ts11
-rw-r--r--apps/web/src/utils/string.ts7
-rw-r--r--apps/web/src/utils/ui.ts6
-rw-r--r--apps/web/tsconfig.json7
-rw-r--r--apps/web/tsconfig.tsbuildinfo2
417 files changed, 33235 insertions, 31353 deletions
diff --git a/apps/transcription/README.md b/apps/transcription/README.md
deleted file mode 100644
index 07be6790..00000000
--- a/apps/transcription/README.md
+++ /dev/null
@@ -1,86 +0,0 @@
-Before you follow anything in this guide, please make sure you've followed the steps in the [README](../../README.md) (under "Optional: Auto-captions (Transcription) Setup").
-
-Open your terminal and make sure you're in the `apps/transcription` directory.
-
-1. Create virtual environment
-
-```bash
-python -m venv env
-```
-
-2. Activate it
-
-**Windows:**
-
-```bash
-env\Scripts\activate
-```
-
-**macOS/Linux:**
-
-```bash
-source env/bin/activate
-```
-
-> Note: if you're using VS Code/Cursor and you're seeing errors with the imports about the modules not being found,
-> You might have to press CTRL + Shift + P -> Python: Select Interpreter -> Enter interpreter path -> Find -> env -> scripts -> python.exe
-
-3. Install libraries/packages/whatever you wanna call them
-
-```bash
-pip install -r requirements.txt
-```
-
-4. Make sure you have a Modal account. If you don't: [create one](https://modal.com/)
-
-> If you don't know what Modal is: it allows us to process the actual audio and transcribe with Whisper by providing the infra to run Python code with a lot of RAM, generally affordable.
-
-5. Once you've got a Modal accoumt, run this:
-
-```bash
-python -m modal setup
-```
-
-It's gonna open a browser so you can authenticate.
-
-6. Test it if you want to make sure it actually works:
-
-```bash
-modal run transcription.py
-```
-
-6. Deploy the function!
-
-```bash
-modal deploy transcription.py
-```
-
-7. Set the required secrets in Modal
-
-So the script we just deployed interacts with Cloudflare to do two things:
-
-- Download the audio (so it can be transcribed with Whisper)
-- Delete the file after processing (privacy)
-
-To do those things, the script needs access to these environment variables:
-```bash
-CLOUDFLARE_ACCOUNT_ID=your-account-id
-R2_ACCESS_KEY_ID=your-access-key-id
-R2_SECRET_ACCESS_KEY=your-secret-access-key
-R2_BUCKET_NAME=opencut-transcription
-```
-
-Remember, we set these earlier in `.env.local`.
-
-So let's do it:
-
- - Go to [Modal Secrets](https://modal.com/secrets/mazewinther/main)
- - Click "Custom" and enter "opencut-r2-secrets" for the name.
- - Now you can just click "Import .env" and copy/paste the 4 variables from your `.env.local` file. Copy and paste these only:
- ```bash
- CLOUDFLARE_ACCOUNT_ID=your-account-id
- R2_ACCESS_KEY_ID=your-access-key-id
- R2_SECRET_ACCESS_KEY=your-secret-access-key
- R2_BUCKET_NAME=opencut-transcription
- ```
- - Click "Done" and you should see some cool particles! \ No newline at end of file
diff --git a/apps/transcription/requirements.txt b/apps/transcription/requirements.txt
deleted file mode 100644
index c002e683..00000000
--- a/apps/transcription/requirements.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-modal
-openai-whisper
-boto3
-pydantic
-cryptography \ No newline at end of file
diff --git a/apps/transcription/transcription.py b/apps/transcription/transcription.py
deleted file mode 100644
index ad55f6d4..00000000
--- a/apps/transcription/transcription.py
+++ /dev/null
@@ -1,143 +0,0 @@
-import modal
-from pydantic import BaseModel
-
-app = modal.App("opencut-transcription")
-
-class TranscribeRequest(BaseModel):
- filename: str
- language: str = "auto"
- decryptionKey: str = None
- iv: str = None
-
-@app.function(
- image=modal.Image.debian_slim()
- .apt_install(["ffmpeg"])
- .pip_install(["openai-whisper", "boto3", "fastapi[standard]", "pydantic", "cryptography"]),
- gpu="A10G",
- timeout=300, # 5m
- secrets=[modal.Secret.from_name("opencut-r2-secrets")]
-)
-@modal.fastapi_endpoint(method="POST")
-def transcribe_audio(request: TranscribeRequest):
- import whisper
- import boto3
- import tempfile
- import os
- import json
-
- try:
- filename = request.filename
- language = request.language
- decryption_key = request.decryptionKey
- iv = request.iv
-
- if not filename:
- return {
- "error": "Missing filename parameter"
- }
-
- # Initialize R2 client
- s3_client = boto3.client(
- 's3',
- endpoint_url=f'https://{os.environ["CLOUDFLARE_ACCOUNT_ID"]}.r2.cloudflarestorage.com',
- aws_access_key_id=os.environ["R2_ACCESS_KEY_ID"],
- aws_secret_access_key=os.environ["R2_SECRET_ACCESS_KEY"],
- region_name='auto'
- )
-
- # Create temporary file for audio
- with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as temp_file:
- temp_path = temp_file.name
-
- try:
- # Download audio from R2
- s3_client.download_file(
- os.environ["R2_BUCKET_NAME"],
- filename,
- temp_path
- )
-
- # If decryption key provided, decrypt the file directly (zero-knowledge)
- if decryption_key and iv:
- import base64
- from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
- from cryptography.hazmat.backends import default_backend
-
- # Read the encrypted file
- with open(temp_path, 'rb') as f:
- encrypted_data = f.read()
-
- # Decode the key and IV from base64
- key_bytes = base64.b64decode(decryption_key)
- iv_bytes = base64.b64decode(iv)
-
- # Decrypt the data using AES-GCM
- # Extract the tag (last 16 bytes) and ciphertext
- tag = encrypted_data[-16:]
- ciphertext = encrypted_data[:-16]
-
- cipher = Cipher(
- algorithms.AES(key_bytes),
- modes.GCM(iv_bytes, tag),
- backend=default_backend()
- )
- decryptor = cipher.decryptor()
- decrypted_data = decryptor.update(ciphertext) + decryptor.finalize()
-
- # Write decrypted audio back to temp file
- with open(temp_path, 'wb') as f:
- f.write(decrypted_data)
-
- # Load Whisper model
- model = whisper.load_model("base")
-
- # Transcribe audio
- if language == "auto":
- result = model.transcribe(temp_path)
- else:
- result = model.transcribe(temp_path, language=language.lower())
-
- # Delete audio file from R2 (cleanup)
- s3_client.delete_object(
- Bucket=os.environ["R2_BUCKET_NAME"],
- Key=filename
- )
-
- # Adjust segment timing - Whisper is consistently late by ~500ms
- adjusted_segments = []
- for segment in result["segments"]:
- adjusted_segment = segment.copy()
- # Shift start/end times earlier by 500ms, don't go below 0
- adjusted_segment["start"] = max(0, segment["start"] - 0.5)
- adjusted_segment["end"] = max(0.5, segment["end"] - 0.5) # Ensure duration is at least 0.5s
- adjusted_segments.append(adjusted_segment)
-
- return {
- "text": result["text"],
- "segments": adjusted_segments,
- "language": result["language"]
- }
-
- finally:
- # Clean up temporary file
- if os.path.exists(temp_path):
- os.unlink(temp_path)
-
- except Exception as e:
- import traceback
- print(f"Transcription error: {str(e)}")
- print(f"Traceback: {traceback.format_exc()}")
-
- # Return error response that matches expected format
- return {
- "error": str(e),
- "text": "",
- "segments": [],
- "language": "unknown"
- }
-
-@app.local_entrypoint()
-def main():
- # Test function - you can call this with modal run transcription.py
- print("Transcription service is ready to deploy!")
- print("Deploy with: modal deploy transcription.py") \ No newline at end of file
diff --git a/apps/web/.env.example b/apps/web/.env.example
index e77d3c5a..b321f09a 100644
--- a/apps/web/.env.example
+++ b/apps/web/.env.example
@@ -1,33 +1,28 @@
-# Environment Variables Example
+# Environment variables example
# Copy this file to .env.local and update the values as needed
-DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"
+# Node
+NODE_ENV=development
-# Better Auth
-NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000
-BETTER_AUTH_SECRET=your-secret-key-here
+# Public
+NEXT_PUBLIC_SITE_URL=http://localhost:3000
+NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com
-# Development Environment
-NODE_ENV=development
+# Server
+DATABASE_URL="postgresql://opencut:opencut@localhost:5432/opencut"
+BETTER_AUTH_SECRET=your_better_auth_secret
-# Redis
UPSTASH_REDIS_REST_URL=http://localhost:8079
-UPSTASH_REDIS_REST_TOKEN=example_token
+UPSTASH_REDIS_REST_TOKEN=example_token_here
-# Marble Blog
-MARBLE_WORKSPACE_KEY=cm6ytuq9x0000i803v0isidst # example organization key
-NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com
+MARBLE_WORKSPACE_KEY=your_workspace_key_here
-# Freesound (generate at https://freesound.org/apiv2/apply/)
-FREESOUND_CLIENT_ID=...
-FREESOUND_API_KEY=...
+FREESOUND_CLIENT_ID=your_client_id_here
+FREESOUND_API_KEY=your_api_key_here
-# Cloudflare R2 (for auto-captions/transcription)
-# Get these from Cloudflare Dashboard > R2 > Manage R2 API tokens
-CLOUDFLARE_ACCOUNT_ID=your-account-id
-R2_ACCESS_KEY_ID=your-access-key-id
-R2_SECRET_ACCESS_KEY=your-secret-access-key
-R2_BUCKET_NAME=opencut-transcription
+CLOUDFLARE_ACCOUNT_ID=your_account_id_here
+R2_ACCESS_KEY_ID=your_access_key_here
+R2_SECRET_ACCESS_KEY=your_secret_key_here
+R2_BUCKET_NAME=opencut-transcription # whatever you named your r2 bucket
-# Modal transcription endpoint (from modal deploy transcription.py)
-MODAL_TRANSCRIPTION_URL=https://your-username--opencut-transcription-transcribe-audio.modal.run \ No newline at end of file
+MODAL_TRANSCRIPTION_URL=your_modal_url_here \ No newline at end of file
diff --git a/apps/web/.gitignore b/apps/web/.gitignore
index 36b42034..dc069c57 100644
--- a/apps/web/.gitignore
+++ b/apps/web/.gitignore
@@ -1,2 +1,8 @@
# Turborepo
-.turbo \ No newline at end of file
+.turbo
+
+# Env vars
+.env*
+!.env.example
+
+.next/ \ No newline at end of file
diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile
deleted file mode 100644
index 6c42e708..00000000
--- a/apps/web/Dockerfile
+++ /dev/null
@@ -1,65 +0,0 @@
-FROM oven/bun:alpine AS base
-
-# Install dependencies and build the application
-FROM base AS builder
-
-WORKDIR /app
-
-ARG FREESOUND_CLIENT_ID
-ARG FREESOUND_API_KEY
-
-COPY package.json package.json
-COPY bun.lock bun.lock
-COPY turbo.json turbo.json
-
-COPY apps/web/package.json apps/web/package.json
-COPY packages/db/package.json packages/db/package.json
-COPY packages/auth/package.json packages/auth/package.json
-
-RUN bun install
-
-COPY apps/web/ apps/web/
-COPY packages/db/ packages/db/
-COPY packages/auth/ packages/auth/
-
-ENV NODE_ENV production
-ENV NEXT_TELEMETRY_DISABLED 1
-# Set build-time environment variables for validation
-ENV DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"
-ENV BETTER_AUTH_SECRET="build-time-secret"
-ENV UPSTASH_REDIS_REST_URL="http://localhost:8079"
-ENV UPSTASH_REDIS_REST_TOKEN="example_token"
-ENV NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:3000"
-
-ENV FREESOUND_CLIENT_ID=$FREESOUND_CLIENT_ID
-ENV FREESOUND_API_KEY=$FREESOUND_API_KEY
-
-WORKDIR /app/apps/web
-RUN bun run build
-
-# Production image
-FROM base AS runner
-WORKDIR /app
-
-ENV NODE_ENV production
-ENV NEXT_TELEMETRY_DISABLED 1
-
-RUN addgroup --system --gid 1001 nodejs
-RUN adduser --system --uid 1001 nextjs
-
-COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
-COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
-COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
-
-RUN chown nextjs:nodejs apps
-
-USER nextjs
-
-EXPOSE 3000
-
-ENV PORT=3000
-ENV HOSTNAME="0.0.0.0"
-
-
-CMD ["bun", "apps/web/server.js"]
-
diff --git a/apps/web/drizzle.config.ts b/apps/web/drizzle.config.ts
index a0602a74..2d53d6b9 100644
--- a/apps/web/drizzle.config.ts
+++ b/apps/web/drizzle.config.ts
@@ -1,19 +1,23 @@
import type { Config } from "drizzle-kit";
import * as dotenv from "dotenv";
+import { webEnv } from "@opencut/env/web";
// Load the right env file based on environment
-if (process.env.NODE_ENV === "production") {
+if (webEnv.NODE_ENV === "production") {
dotenv.config({ path: ".env.production" });
} else {
dotenv.config({ path: ".env.local" });
}
export default {
- schema: "../../packages/db/src/schema.ts",
+ schema: "./src/schema.ts",
dialect: "postgresql",
+ migrations: {
+ table: "drizzle_migrations",
+ },
dbCredentials: {
- url: process.env.DATABASE_URL!,
+ url: webEnv.DATABASE_URL,
},
out: "./migrations",
- strict: process.env.NODE_ENV === "production",
+ strict: webEnv.NODE_ENV === "production",
} satisfies Config;
diff --git a/apps/web/migrations/0000_hot_the_fallen.sql b/apps/web/migrations/0000_brainy_saracen.sql
index 321043f0..2c6ab2b2 100644
--- a/apps/web/migrations/0000_hot_the_fallen.sql
+++ b/apps/web/migrations/0000_brainy_saracen.sql
@@ -50,5 +50,13 @@ CREATE TABLE "verifications" (
);
--> statement-breakpoint
ALTER TABLE "verifications" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
+CREATE TABLE "waitlist" (
+ "id" text PRIMARY KEY NOT NULL,
+ "email" text NOT NULL,
+ "created_at" timestamp NOT NULL,
+ CONSTRAINT "waitlist_email_unique" UNIQUE("email")
+);
+--> statement-breakpoint
+ALTER TABLE "waitlist" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file
diff --git a/apps/web/migrations/0001_tricky_jackpot.sql b/apps/web/migrations/0001_tricky_jackpot.sql
deleted file mode 100644
index 6eaeb56e..00000000
--- a/apps/web/migrations/0001_tricky_jackpot.sql
+++ /dev/null
@@ -1,8 +0,0 @@
-CREATE TABLE "waitlist" (
- "id" text PRIMARY KEY NOT NULL,
- "email" text NOT NULL,
- "created_at" timestamp NOT NULL,
- CONSTRAINT "waitlist_email_unique" UNIQUE("email")
-);
---> statement-breakpoint
-ALTER TABLE "waitlist" ENABLE ROW LEVEL SECURITY; \ No newline at end of file
diff --git a/apps/web/migrations/0002_cuddly_pretty_boy.sql b/apps/web/migrations/0002_cuddly_pretty_boy.sql
deleted file mode 100644
index fc8aed79..00000000
--- a/apps/web/migrations/0002_cuddly_pretty_boy.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE "users" ALTER COLUMN "email_verified" SET DEFAULT false; \ No newline at end of file
diff --git a/apps/web/migrations/0003_long_polaris.sql b/apps/web/migrations/0003_long_polaris.sql
deleted file mode 100644
index b2b74064..00000000
--- a/apps/web/migrations/0003_long_polaris.sql
+++ /dev/null
@@ -1,10 +0,0 @@
-CREATE TABLE "export_waitlist" (
- "id" text PRIMARY KEY NOT NULL,
- "email" text NOT NULL,
- "created_at" timestamp NOT NULL,
- "updated_at" timestamp NOT NULL,
- CONSTRAINT "export_waitlist_email_unique" UNIQUE("email")
-);
---> statement-breakpoint
-ALTER TABLE "export_waitlist" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
-DROP TABLE "waitlist" CASCADE; \ No newline at end of file
diff --git a/apps/web/migrations/meta/0000_snapshot.json b/apps/web/migrations/meta/0000_snapshot.json
index 2da63d5c..8d505274 100644
--- a/apps/web/migrations/meta/0000_snapshot.json
+++ b/apps/web/migrations/meta/0000_snapshot.json
@@ -1,5 +1,5 @@
{
- "id": "4440fb90-cf0e-4cb2-afad-08250ce3dc1e",
+ "id": "33a6742f-89da-4ac5-958f-421aa1cf9bd6",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
@@ -291,6 +291,43 @@
"policies": {},
"checkConstraints": {},
"isRLSEnabled": true
+ },
+ "public.waitlist": {
+ "name": "waitlist",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "waitlist_email_unique": {
+ "name": "waitlist_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
}
},
"enums": {},
diff --git a/apps/web/migrations/meta/0001_snapshot.json b/apps/web/migrations/meta/0001_snapshot.json
deleted file mode 100644
index 008540ad..00000000
--- a/apps/web/migrations/meta/0001_snapshot.json
+++ /dev/null
@@ -1,344 +0,0 @@
-{
- "id": "b7d920ca-6dd0-430f-8ee6-1d38fdf3e80f",
- "prevId": "4440fb90-cf0e-4cb2-afad-08250ce3dc1e",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.accounts": {
- "name": "accounts",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "account_id": {
- "name": "account_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "provider_id": {
- "name": "provider_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "access_token": {
- "name": "access_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "refresh_token": {
- "name": "refresh_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "id_token": {
- "name": "id_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_token_expires_at": {
- "name": "access_token_expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- },
- "refresh_token_expires_at": {
- "name": "refresh_token_expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- },
- "scope": {
- "name": "scope",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "password": {
- "name": "password",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {
- "accounts_user_id_users_id_fk": {
- "name": "accounts_user_id_users_id_fk",
- "tableFrom": "accounts",
- "tableTo": "users",
- "columnsFrom": ["user_id"],
- "columnsTo": ["id"],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.sessions": {
- "name": "sessions",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "expires_at": {
- "name": "expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "token": {
- "name": "token",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "ip_address": {
- "name": "ip_address",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_agent": {
- "name": "user_agent",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {
- "sessions_user_id_users_id_fk": {
- "name": "sessions_user_id_users_id_fk",
- "tableFrom": "sessions",
- "tableTo": "users",
- "columnsFrom": ["user_id"],
- "columnsTo": ["id"],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "sessions_token_unique": {
- "name": "sessions_token_unique",
- "nullsNotDistinct": false,
- "columns": ["token"]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true
- },
- "image": {
- "name": "image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": ["email"]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.verifications": {
- "name": "verifications",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "identifier": {
- "name": "identifier",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "value": {
- "name": "value",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "expires_at": {
- "name": "expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "waitlist_email_unique": {
- "name": "waitlist_email_unique",
- "nullsNotDistinct": false,
- "columns": ["email"]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "roles": {},
- "policies": {},
- "views": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-}
diff --git a/apps/web/migrations/meta/0002_snapshot.json b/apps/web/migrations/meta/0002_snapshot.json
deleted file mode 100644
index 42512564..00000000
--- a/apps/web/migrations/meta/0002_snapshot.json
+++ /dev/null
@@ -1,345 +0,0 @@
-{
- "id": "1b9148ed-497b-4e53-8cf6-f556c8fe7f7f",
- "prevId": "b7d920ca-6dd0-430f-8ee6-1d38fdf3e80f",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.accounts": {
- "name": "accounts",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "account_id": {
- "name": "account_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "provider_id": {
- "name": "provider_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "access_token": {
- "name": "access_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "refresh_token": {
- "name": "refresh_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "id_token": {
- "name": "id_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_token_expires_at": {
- "name": "access_token_expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- },
- "refresh_token_expires_at": {
- "name": "refresh_token_expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- },
- "scope": {
- "name": "scope",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "password": {
- "name": "password",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {
- "accounts_user_id_users_id_fk": {
- "name": "accounts_user_id_users_id_fk",
- "tableFrom": "accounts",
- "tableTo": "users",
- "columnsFrom": ["user_id"],
- "columnsTo": ["id"],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.sessions": {
- "name": "sessions",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "expires_at": {
- "name": "expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "token": {
- "name": "token",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "ip_address": {
- "name": "ip_address",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_agent": {
- "name": "user_agent",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {
- "sessions_user_id_users_id_fk": {
- "name": "sessions_user_id_users_id_fk",
- "tableFrom": "sessions",
- "tableTo": "users",
- "columnsFrom": ["user_id"],
- "columnsTo": ["id"],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "sessions_token_unique": {
- "name": "sessions_token_unique",
- "nullsNotDistinct": false,
- "columns": ["token"]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "image": {
- "name": "image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": ["email"]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.verifications": {
- "name": "verifications",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "identifier": {
- "name": "identifier",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "value": {
- "name": "value",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "expires_at": {
- "name": "expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "waitlist_email_unique": {
- "name": "waitlist_email_unique",
- "nullsNotDistinct": false,
- "columns": ["email"]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "roles": {},
- "policies": {},
- "views": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-}
diff --git a/apps/web/migrations/meta/0003_snapshot.json b/apps/web/migrations/meta/0003_snapshot.json
deleted file mode 100644
index 2c5d986e..00000000
--- a/apps/web/migrations/meta/0003_snapshot.json
+++ /dev/null
@@ -1,365 +0,0 @@
-{
- "id": "71b84015-78f8-4d07-9115-8d56ea550459",
- "prevId": "1b9148ed-497b-4e53-8cf6-f556c8fe7f7f",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.accounts": {
- "name": "accounts",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "account_id": {
- "name": "account_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "provider_id": {
- "name": "provider_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "access_token": {
- "name": "access_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "refresh_token": {
- "name": "refresh_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "id_token": {
- "name": "id_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_token_expires_at": {
- "name": "access_token_expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- },
- "refresh_token_expires_at": {
- "name": "refresh_token_expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- },
- "scope": {
- "name": "scope",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "password": {
- "name": "password",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {
- "accounts_user_id_users_id_fk": {
- "name": "accounts_user_id_users_id_fk",
- "tableFrom": "accounts",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.export_waitlist": {
- "name": "export_waitlist",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "export_waitlist_email_unique": {
- "name": "export_waitlist_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.sessions": {
- "name": "sessions",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "expires_at": {
- "name": "expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "token": {
- "name": "token",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "ip_address": {
- "name": "ip_address",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_agent": {
- "name": "user_agent",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {
- "sessions_user_id_users_id_fk": {
- "name": "sessions_user_id_users_id_fk",
- "tableFrom": "sessions",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "sessions_token_unique": {
- "name": "sessions_token_unique",
- "nullsNotDistinct": false,
- "columns": [
- "token"
- ]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "image": {
- "name": "image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.verifications": {
- "name": "verifications",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "identifier": {
- "name": "identifier",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "value": {
- "name": "value",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "expires_at": {
- "name": "expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "roles": {},
- "policies": {},
- "views": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/web/migrations/meta/_journal.json b/apps/web/migrations/meta/_journal.json
index c172952e..c7d30f16 100644
--- a/apps/web/migrations/meta/_journal.json
+++ b/apps/web/migrations/meta/_journal.json
@@ -5,30 +5,9 @@
{
"idx": 0,
"version": "7",
- "when": 1750581188229,
- "tag": "0000_hot_the_fallen",
- "breakpoints": true
- },
- {
- "idx": 1,
- "version": "7",
- "when": 1750689835736,
- "tag": "0001_tricky_jackpot",
- "breakpoints": true
- },
- {
- "idx": 2,
- "version": "7",
- "when": 1752569400809,
- "tag": "0002_cuddly_pretty_boy",
- "breakpoints": true
- },
- {
- "idx": 3,
- "version": "7",
- "when": 1755377469662,
- "tag": "0003_long_polaris",
+ "when": 1750753385927,
+ "tag": "0000_brainy_saracen",
"breakpoints": true
}
]
-} \ No newline at end of file
+}
diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts
new file mode 100644
index 00000000..c4b7818f
--- /dev/null
+++ b/apps/web/next-env.d.ts
@@ -0,0 +1,6 @@
+/// <reference types="next" />
+/// <reference types="next/image-types/global" />
+import "./.next/dev/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/apps/web/package.json b/apps/web/package.json
index 25dc9aee..aa752a81 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,5 +1,5 @@
{
- "name": "opencut",
+ "name": "@opencut/web",
"version": "0.1.0",
"private": true,
"packageManager": "bun@1.2.18",
@@ -21,13 +21,18 @@
"@ffmpeg/util": "^0.12.2",
"@hello-pangea/dnd": "^18.0.1",
"@hookform/resolvers": "^3.9.1",
- "@opencut/auth": "workspace:*",
- "@opencut/db": "workspace:*",
- "@radix-ui/react-separator": "^1.1.7",
- "@t3-oss/env-core": "^0.13.8",
- "@t3-oss/env-nextjs": "^0.13.8",
- "@upstash/ratelimit": "^2.0.5",
- "@upstash/redis": "^1.35.0",
+ "@hugeicons/core-free-icons": "^3.1.1",
+ "@hugeicons/react": "^1.1.4",
+ "@huggingface/transformers": "^3.8.1",
+ "@opencut/env": "workspace:*",
+ "@opencut/ui": "workspace:*",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "@upstash/ratelimit": "^2.0.6",
+ "@upstash/redis": "^1.35.4",
"@vercel/analytics": "^1.4.1",
"aws4fetch": "^1.0.20",
"better-auth": "^1.2.7",
@@ -36,18 +41,19 @@
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"dayjs": "^1.11.13",
- "dotenv": "^16.5.0",
"drizzle-orm": "^0.44.2",
"embla-carousel-react": "^8.5.1",
+ "eventemitter3": "^5.0.1",
"feed": "^5.1.0",
- "framer-motion": "^11.13.1",
"input-otp": "^1.4.1",
- "lucide-react": "^0.468.0",
+ "lucide-react": "^0.562.0",
+ "mediabunny": "^1.29.1",
"motion": "^12.18.1",
"nanoid": "^5.1.5",
- "next": "^15.5.7",
+ "next": "16.1.3",
"next-themes": "^0.4.4",
"pg": "^8.16.2",
+ "postgres": "^3.4.5",
"radix-ui": "^1.4.2",
"react": "^18.2.0",
"react-day-picker": "^8.10.1",
@@ -67,7 +73,9 @@
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"unified": "^11.0.5",
+ "use-deep-compare-effect": "^1.8.1",
"vaul": "^1.1.1",
+ "wavesurfer.js": "^7.9.8",
"zod": "^3.25.67",
"zustand": "^5.0.2"
},
@@ -81,6 +89,7 @@
"@types/react-dom": "^18.2.18",
"cross-env": "^7.0.3",
"drizzle-kit": "^0.31.4",
+ "dotenv": "^16.5.0",
"postcss": "^8",
"tailwindcss": "^4.1.11",
"tsx": "^4.7.1",
diff --git a/apps/web/public/frame.svg b/apps/web/public/frame.svg
deleted file mode 100644
index 4dac68e9..00000000
--- a/apps/web/public/frame.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-<svg width="459" height="77" viewBox="0 0 459 77" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect x="0.5" y="1.15625" width="458" height="75" rx="15.5" fill="#101010"/>
-<rect x="0.5" y="1.15625" width="458" height="75" rx="15.5" stroke="#FFCC00"/>
-<rect x="0.5" y="1.15625" width="31" height="75" rx="15.5" fill="#2A2A2A"/>
-<rect x="0.5" y="1.15625" width="31" height="75" rx="15.5" stroke="#FFCC00"/>
-<rect x="13" y="20.6562" width="6" height="36" rx="3" fill="#FFCC00"/>
-<rect x="427.5" y="1.15625" width="31" height="75" rx="15.5" fill="#2A2A2A"/>
-<rect x="427.5" y="1.15625" width="31" height="75" rx="15.5" stroke="#FFCC00"/>
-<rect x="440" y="20.6562" width="6" height="36" rx="3" fill="#FFCC00"/>
-</svg>
diff --git a/apps/web/public/logo.png b/apps/web/public/logo.png
deleted file mode 100644
index fc9e7902..00000000
--- a/apps/web/public/logo.png
+++ /dev/null
Binary files differ
diff --git a/apps/web/public/logos/opencut/1k/logo-black-with-text.png b/apps/web/public/logos/opencut/1k/logo-black-with-text.png
new file mode 100644
index 00000000..08ceb702
--- /dev/null
+++ b/apps/web/public/logos/opencut/1k/logo-black-with-text.png
Binary files differ
diff --git a/apps/web/public/logos/opencut/1k/logo-black.png b/apps/web/public/logos/opencut/1k/logo-black.png
new file mode 100644
index 00000000..6b21beb9
--- /dev/null
+++ b/apps/web/public/logos/opencut/1k/logo-black.png
Binary files differ
diff --git a/apps/web/public/logos/opencut/1k/logo-white-with-text.png b/apps/web/public/logos/opencut/1k/logo-white-with-text.png
new file mode 100644
index 00000000..a3151060
--- /dev/null
+++ b/apps/web/public/logos/opencut/1k/logo-white-with-text.png
Binary files differ
diff --git a/apps/web/public/logos/opencut/1k/logo-white.png b/apps/web/public/logos/opencut/1k/logo-white.png
new file mode 100644
index 00000000..f4f6ae4c
--- /dev/null
+++ b/apps/web/public/logos/opencut/1k/logo-white.png
Binary files differ
diff --git a/apps/web/public/logos/opencut/2k/logo-black-with-text.png b/apps/web/public/logos/opencut/2k/logo-black-with-text.png
new file mode 100644
index 00000000..7493bed6
--- /dev/null
+++ b/apps/web/public/logos/opencut/2k/logo-black-with-text.png
Binary files differ
diff --git a/apps/web/public/logos/opencut/2k/logo-black.png b/apps/web/public/logos/opencut/2k/logo-black.png
new file mode 100644
index 00000000..1336d172
--- /dev/null
+++ b/apps/web/public/logos/opencut/2k/logo-black.png
Binary files differ
diff --git a/apps/web/public/logos/opencut/2k/logo-white-with-text.png b/apps/web/public/logos/opencut/2k/logo-white-with-text.png
new file mode 100644
index 00000000..29bf0e6f
--- /dev/null
+++ b/apps/web/public/logos/opencut/2k/logo-white-with-text.png
Binary files differ
diff --git a/apps/web/public/logos/opencut/2k/logo-white.png b/apps/web/public/logos/opencut/2k/logo-white.png
new file mode 100644
index 00000000..9ee83d3c
--- /dev/null
+++ b/apps/web/public/logos/opencut/2k/logo-white.png
Binary files differ
diff --git a/apps/web/public/logos/opencut/4k/logo-black-with-text.png b/apps/web/public/logos/opencut/4k/logo-black-with-text.png
new file mode 100644
index 00000000..edaf5358
--- /dev/null
+++ b/apps/web/public/logos/opencut/4k/logo-black-with-text.png
Binary files differ
diff --git a/apps/web/public/logos/opencut/4k/logo-black.png b/apps/web/public/logos/opencut/4k/logo-black.png
new file mode 100644
index 00000000..b5f0ff13
--- /dev/null
+++ b/apps/web/public/logos/opencut/4k/logo-black.png
Binary files differ
diff --git a/apps/web/public/logos/opencut/4k/logo-white-with-text.png b/apps/web/public/logos/opencut/4k/logo-white-with-text.png
new file mode 100644
index 00000000..90596e87
--- /dev/null
+++ b/apps/web/public/logos/opencut/4k/logo-white-with-text.png
Binary files differ
diff --git a/apps/web/public/logos/opencut/4k/logo-white.png b/apps/web/public/logos/opencut/4k/logo-white.png
new file mode 100644
index 00000000..794e360b
--- /dev/null
+++ b/apps/web/public/logos/opencut/4k/logo-white.png
Binary files differ
diff --git a/apps/web/public/logo.svg b/apps/web/public/logos/opencut/svg/logo.svg
index f4079a5e..f4079a5e 100644
--- a/apps/web/public/logo.svg
+++ b/apps/web/public/logos/opencut/svg/logo.svg
diff --git a/apps/web/public/logos/others/fal.svg b/apps/web/public/logos/others/fal.svg
new file mode 100644
index 00000000..abc56513
--- /dev/null
+++ b/apps/web/public/logos/others/fal.svg
@@ -0,0 +1,3 @@
+<svg width="512" height="513" viewBox="0 0 512 513" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M330.144 0.740479C339.014 0.740479 346.122 7.95256 346.971 16.7816C354.534 95.534 417.207 158.206 495.959 165.769C504.788 166.618 512 173.726 512 182.596V330.886C512 339.755 504.788 346.863 495.959 347.712C417.207 355.275 354.534 417.947 346.971 496.7C346.122 505.529 339.014 512.74 330.144 512.74H181.856C172.986 512.74 165.878 505.529 165.029 496.7C157.467 417.947 94.7934 355.275 16.041 347.712C7.21212 346.863 0 339.755 0 330.886V182.596C0 173.726 7.21216 166.618 16.0411 165.769C94.7935 158.206 157.467 95.534 165.029 16.7816C165.878 7.95256 172.986 0.740479 181.856 0.740479H330.144ZM102.8 256.307C102.8 341.495 171.781 410.554 256.874 410.554C341.966 410.554 410.948 341.495 410.948 256.307C410.948 171.119 341.966 102.06 256.874 102.06C171.781 102.06 102.8 171.119 102.8 256.307Z" fill="black"/>
+</svg>
diff --git a/apps/web/public/logos/others/vercel.svg b/apps/web/public/logos/others/vercel.svg
new file mode 100644
index 00000000..824018a3
--- /dev/null
+++ b/apps/web/public/logos/others/vercel.svg
@@ -0,0 +1,3 @@
+<svg width="588" height="512" viewBox="0 0 588 512" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M294 0L588 512H0L294 0Z" fill="black"/>
+</svg>
diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx
deleted file mode 100644
index f28cd191..00000000
--- a/apps/web/src/app/(auth)/login/page.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-"use client";
-
-import { useRouter } from "next/navigation";
-import { Button } from "@/components/ui/button";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-import { memo, Suspense } from "react";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Separator } from "@/components/ui/separator";
-import Link from "next/link";
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
-import { ArrowLeft, Loader2 } from "lucide-react";
-import { GoogleIcon } from "@/components/icons";
-import { useLogin } from "@/hooks/auth/useLogin";
-
-const LoginPage = () => {
- const router = useRouter();
- const {
- email,
- setEmail,
- password,
- setPassword,
- error,
- isAnyLoading,
- isEmailLoading,
- isGoogleLoading,
- handleLogin,
- handleGoogleLogin,
- } = useLogin();
-
- return (
- <div className="flex h-screen items-center justify-center relative">
- <Button
- variant="text"
- onClick={() => router.back()}
- className="absolute top-6 left-6"
- >
- <ArrowLeft className="h-5 w-5" /> Back
- </Button>
- <Card className="w-[400px] shadow-lg border-0">
- <CardHeader className="text-center pb-4">
- <CardTitle className="text-2xl font-semibold">Welcome back</CardTitle>
- <CardDescription className="text-base">
- Sign in to your account to continue
- </CardDescription>
- </CardHeader>
- <CardContent className="pt-0">
- <Suspense
- fallback={
- <div className="text-center">
- <Loader2 className="animate-spin" />
- </div>
- }
- >
- <div className="flex flex-col space-y-6">
- {error && (
- <Alert variant="destructive">
- <AlertTitle>Error</AlertTitle>
- <AlertDescription>{error}</AlertDescription>
- </Alert>
- )}
-
- <Button
- onClick={handleGoogleLogin}
- variant="outline"
- size="lg"
- disabled={isAnyLoading}
- >
- {isGoogleLoading ? (
- <Loader2 className="animate-spin" />
- ) : (
- <GoogleIcon />
- )}{" "}
- Continue with Google
- </Button>
- <div className="relative">
- <div className="absolute inset-0 flex items-center">
- <Separator className="w-full" />
- </div>
- <div className="relative flex justify-center text-xs uppercase">
- <span className="bg-background px-2 text-muted-foreground">
- Or continue with
- </span>
- </div>
- </div>
- <div className="space-y-4">
- <div className="space-y-2">
- <Label htmlFor="email">Email</Label>
- <Input
- id="email"
- type="email"
- placeholder="m@example.com"
- value={email}
- onChange={(e) => setEmail(e.target.value)}
- disabled={isAnyLoading}
- className="h-11"
- />
- </div>
- <div className="space-y-2">
- <Label htmlFor="password">Password</Label>
- <Input
- id="password"
- type="password"
- value={password}
- onChange={(e) => setPassword(e.target.value)}
- disabled={isAnyLoading}
- className="h-11"
- />
- </div>
- <Button
- onClick={handleLogin}
- disabled={isAnyLoading || !email || !password}
- className="w-full h-11"
- size="lg"
- >
- {isEmailLoading ? (
- <Loader2 className="animate-spin" />
- ) : (
- "Sign in"
- )}
- </Button>
- </div>
- </div>
- <div className="mt-6 text-center text-sm">
- Don't have an account?{" "}
- <Link
- href="/signup"
- className="font-medium text-primary underline-offset-4 hover:underline"
- >
- Sign up
- </Link>
- </div>
- </Suspense>
- </CardContent>
- </Card>
- </div>
- );
-};
-
-export default memo(LoginPage);
diff --git a/apps/web/src/app/(auth)/signup/page.tsx b/apps/web/src/app/(auth)/signup/page.tsx
deleted file mode 100644
index c3a0f899..00000000
--- a/apps/web/src/app/(auth)/signup/page.tsx
+++ /dev/null
@@ -1,162 +0,0 @@
-"use client";
-
-import { useRouter } from "next/navigation";
-import { Button } from "@/components/ui/button";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-import { memo, Suspense } from "react";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Separator } from "@/components/ui/separator";
-import Link from "next/link";
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
-import { ArrowLeft, Loader2 } from "lucide-react";
-import { GoogleIcon } from "@/components/icons";
-import { useSignUp } from "@/hooks/auth/useSignUp";
-
-const SignUpPage = () => {
- const router = useRouter();
- const {
- name,
- setName,
- email,
- setEmail,
- password,
- setPassword,
- error,
- isAnyLoading,
- isEmailLoading,
- isGoogleLoading,
- handleSignUp,
- handleGoogleSignUp,
- } = useSignUp();
-
- return (
- <div className="flex h-screen items-center justify-center relative">
- <Button
- variant="text"
- onClick={() => router.back()}
- className="absolute top-6 left-6"
- >
- <ArrowLeft className="h-5 w-5" /> Back
- </Button>
- <Card className="w-[400px] shadow-lg border-0">
- <CardHeader className="text-center pb-4">
- <CardTitle className="text-2xl font-semibold">
- Create your account
- </CardTitle>
- <CardDescription className="text-base">
- Get started with your free account today
- </CardDescription>
- </CardHeader>
- <CardContent className="pt-0">
- <Suspense
- fallback={
- <div className="text-center">
- <Loader2 className="animate-spin" />
- </div>
- }
- >
- <div className="flex flex-col space-y-6">
- {error && (
- <Alert variant="destructive">
- <AlertTitle>Error</AlertTitle>
- <AlertDescription>{error}</AlertDescription>
- </Alert>
- )}
- <Button
- onClick={handleGoogleSignUp}
- variant="outline"
- size="lg"
- disabled={isAnyLoading}
- >
- {isGoogleLoading ? (
- <Loader2 className="animate-spin" />
- ) : (
- <GoogleIcon />
- )}{" "}
- Continue with Google
- </Button>
- <div className="relative">
- <div className="absolute inset-0 flex items-center">
- <Separator className="w-full" />
- </div>
- <div className="relative flex justify-center text-xs uppercase">
- <span className="bg-background px-2 text-muted-foreground">
- Or continue with
- </span>
- </div>
- </div>
- <div className="space-y-4">
- <div className="space-y-2">
- <Label htmlFor="name">Full Name</Label>
- <Input
- id="name"
- type="text"
- placeholder="John Doe"
- value={name}
- onChange={(e) => setName(e.target.value)}
- disabled={isAnyLoading}
- className="h-11"
- />
- </div>
- <div className="space-y-2">
- <Label htmlFor="email">Email</Label>
- <Input
- id="email"
- type="email"
- placeholder="m@example.com"
- value={email}
- onChange={(e) => setEmail(e.target.value)}
- disabled={isAnyLoading}
- className="h-11"
- />
- </div>
- <div className="space-y-2">
- <Label htmlFor="password">Password</Label>
- <Input
- id="password"
- type="password"
- placeholder="Create a strong password"
- value={password}
- onChange={(e) => setPassword(e.target.value)}
- disabled={isAnyLoading}
- className="h-11"
- />
- </div>
- <Button
- onClick={handleSignUp}
- disabled={isAnyLoading || !name || !email || !password}
- className="w-full h-11"
- size="lg"
- >
- {isEmailLoading ? (
- <Loader2 className="animate-spin" />
- ) : (
- "Create account"
- )}
- </Button>
- </div>
- </div>
- <div className="mt-6 text-center text-sm">
- Already have an account?{" "}
- <Link
- href="/login"
- className="font-medium text-primary underline-offset-4 hover:underline"
- >
- Sign in
- </Link>
- </div>
- </Suspense>
- </CardContent>
- </Card>
- </div>
- );
-};
-
-export default memo(SignUpPage);
diff --git a/apps/web/src/app/animation/page.tsx b/apps/web/src/app/animation/page.tsx
deleted file mode 100644
index 37be1ef0..00000000
--- a/apps/web/src/app/animation/page.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-"use client";
-
-import { InputWithBack } from "@/components/ui/input-with-back";
-import { useState } from "react";
-
-export default function AnimationPage() {
- const [isExpanded, setIsExpanded] = useState(false);
-
- return (
- <div className="h-dvh flex">
- <div className="bg-panel p-6 w-[20rem]">
- <InputWithBack isExpanded={isExpanded} setIsExpanded={setIsExpanded} />
- </div>
- <div className="p-6 flex items-end justify-end">
- <p
- onClick={() => setIsExpanded(!isExpanded)}
- className="cursor-pointer hover:opacity-75 transition-opacity"
- >
- {isExpanded ? "Collapse" : "Expand"}
- </p>
- </div>
- </div>
- );
-}
diff --git a/apps/web/src/app/api/auth/[...all]/route.ts b/apps/web/src/app/api/auth/[...all]/route.ts
index 5deef204..e17d0cce 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 "@opencut/auth";
+import { auth } from "@/lib/auth/server";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
diff --git a/apps/web/src/app/api/get-upload-url/route.ts b/apps/web/src/app/api/get-upload-url/route.ts
deleted file mode 100644
index dc5b7328..00000000
--- a/apps/web/src/app/api/get-upload-url/route.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-import { NextRequest, NextResponse } from "next/server";
-import { z } from "zod";
-import { AwsClient } from "aws4fetch";
-import { nanoid } from "nanoid";
-import { env } from "@/env";
-import { baseRateLimit } from "@/lib/rate-limit";
-import { isTranscriptionConfigured } from "@/lib/transcription-utils";
-
-const uploadRequestSchema = z.object({
- fileExtension: z.enum(["wav", "mp3", "m4a", "flac"], {
- errorMap: () => ({
- message: "File extension must be wav, mp3, m4a, or flac",
- }),
- }),
-});
-
-const apiResponseSchema = z.object({
- uploadUrl: z.string().url(),
- fileName: z.string().min(1),
-});
-
-export async function POST(request: NextRequest) {
- try {
- // Rate limiting
- const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
- const { success } = await baseRateLimit.limit(ip);
-
- if (!success) {
- return NextResponse.json({ error: "Too many requests" }, { status: 429 });
- }
-
- // Check transcription configuration
- const transcriptionCheck = isTranscriptionConfigured();
- if (!transcriptionCheck.configured) {
- console.error(
- "Missing environment variables:",
- JSON.stringify(transcriptionCheck.missingVars)
- );
-
- return NextResponse.json(
- {
- error: "Transcription not configured",
- message: `Auto-captions require environment variables: ${transcriptionCheck.missingVars.join(", ")}. Check README for setup instructions.`,
- },
- { status: 503 }
- );
- }
-
- // Parse and validate request body
- const rawBody = await request.json().catch(() => null);
- if (!rawBody) {
- return NextResponse.json(
- { error: "Invalid JSON in request body" },
- { status: 400 }
- );
- }
-
- const validationResult = uploadRequestSchema.safeParse(rawBody);
- if (!validationResult.success) {
- return NextResponse.json(
- {
- error: "Invalid request parameters",
- details: validationResult.error.flatten().fieldErrors,
- },
- { status: 400 }
- );
- }
-
- const { fileExtension } = validationResult.data;
-
- // Initialize R2 client
- const client = new AwsClient({
- accessKeyId: env.R2_ACCESS_KEY_ID,
- secretAccessKey: env.R2_SECRET_ACCESS_KEY,
- });
-
- // Generate unique filename with timestamp
- const timestamp = Date.now();
- const fileName = `audio/${timestamp}-${nanoid()}.${fileExtension}`;
-
- // Create presigned URL
- const url = new URL(
- `https://${env.R2_BUCKET_NAME}.${env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com/${fileName}`
- );
-
- url.searchParams.set("X-Amz-Expires", "3600"); // 1 hour expiry
-
- const signed = await client.sign(new Request(url, { method: "PUT" }), {
- aws: { signQuery: true },
- });
-
- if (!signed.url) {
- throw new Error("Failed to generate presigned URL");
- }
-
- // Prepare and validate response
- const responseData = {
- uploadUrl: signed.url,
- fileName,
- };
-
- const responseValidation = apiResponseSchema.safeParse(responseData);
- if (!responseValidation.success) {
- console.error(
- "Invalid API response structure:",
- responseValidation.error
- );
- return NextResponse.json(
- { error: "Internal response formatting error" },
- { status: 500 }
- );
- }
-
- return NextResponse.json(responseValidation.data);
- } catch (error) {
- console.error("Error generating upload URL:", error);
- return NextResponse.json(
- {
- error: "Failed to generate upload URL",
- message:
- error instanceof Error
- ? error.message
- : "An unexpected error occurred",
- },
- { status: 500 }
- );
- }
-}
diff --git a/apps/web/src/app/api/health/route.ts b/apps/web/src/app/api/health/route.ts
index ecb9ce04..edb40a0d 100644
--- a/apps/web/src/app/api/health/route.ts
+++ b/apps/web/src/app/api/health/route.ts
@@ -1,5 +1,3 @@
-import { NextRequest } from "next/server";
-
-export async function GET(request: NextRequest) {
- return new Response("OK", { status: 200 });
+export async function GET() {
+ return new Response("OK", { status: 200 });
}
diff --git a/apps/web/src/app/api/sounds/search/route.ts b/apps/web/src/app/api/sounds/search/route.ts
index c89bc76c..3070d152 100644
--- a/apps/web/src/app/api/sounds/search/route.ts
+++ b/apps/web/src/app/api/sounds/search/route.ts
@@ -1,265 +1,280 @@
-import { NextRequest, NextResponse } from "next/server";
+import { webEnv } from "@opencut/env/web";
+import { type NextRequest, NextResponse } from "next/server";
import { z } from "zod";
-import { env } from "@/env";
-import { baseRateLimit } from "@/lib/rate-limit";
+import { checkRateLimit } from "@/lib/rate-limit";
const searchParamsSchema = z.object({
- q: z.string().max(500, "Query too long").optional(),
- type: z.enum(["songs", "effects"]).optional(),
- page: z.coerce.number().int().min(1).max(1000).default(1),
- page_size: z.coerce.number().int().min(1).max(150).default(20),
- sort: z
- .enum(["downloads", "rating", "created", "score"])
- .default("downloads"),
- min_rating: z.coerce.number().min(0).max(5).default(3),
- commercial_only: z.coerce.boolean().default(true),
+ q: z.string().max(500, "Query too long").optional(),
+ type: z.enum(["songs", "effects"]).optional(),
+ page: z.coerce.number().int().min(1).max(1000).default(1),
+ page_size: z.coerce.number().int().min(1).max(150).default(20),
+ sort: z
+ .enum(["downloads", "rating", "created", "score"])
+ .default("downloads"),
+ min_rating: z.coerce.number().min(0).max(5).default(3),
+ commercial_only: z.coerce.boolean().default(true),
});
const freesoundResultSchema = z.object({
- id: z.number(),
- name: z.string(),
- description: z.string(),
- url: z.string().url(),
- previews: z
- .object({
- "preview-hq-mp3": z.string().url(),
- "preview-lq-mp3": z.string().url(),
- "preview-hq-ogg": z.string().url(),
- "preview-lq-ogg": z.string().url(),
- })
- .optional(),
- download: z.string().url().optional(),
- duration: z.number(),
- filesize: z.number(),
- type: z.string(),
- channels: z.number(),
- bitrate: z.number(),
- bitdepth: z.number(),
- samplerate: z.number(),
- username: z.string(),
- tags: z.array(z.string()),
- license: z.string(),
- created: z.string(),
- num_downloads: z.number().optional(),
- avg_rating: z.number().optional(),
- num_ratings: z.number().optional(),
+ id: z.number(),
+ name: z.string(),
+ description: z.string(),
+ url: z.string().url(),
+ previews: z
+ .object({
+ "preview-hq-mp3": z.string().url(),
+ "preview-lq-mp3": z.string().url(),
+ "preview-hq-ogg": z.string().url(),
+ "preview-lq-ogg": z.string().url(),
+ })
+ .optional(),
+ download: z.string().url().optional(),
+ duration: z.number(),
+ filesize: z.number(),
+ type: z.string(),
+ channels: z.number(),
+ bitrate: z.number(),
+ bitdepth: z.number(),
+ samplerate: z.number(),
+ username: z.string(),
+ tags: z.array(z.string()),
+ license: z.string(),
+ created: z.string(),
+ num_downloads: z.number().optional(),
+ avg_rating: z.number().optional(),
+ num_ratings: z.number().optional(),
});
const freesoundResponseSchema = z.object({
- count: z.number(),
- next: z.string().url().nullable(),
- previous: z.string().url().nullable(),
- results: z.array(freesoundResultSchema),
+ count: z.number(),
+ next: z.string().url().nullable(),
+ previous: z.string().url().nullable(),
+ results: z.array(freesoundResultSchema),
});
const transformedResultSchema = z.object({
- id: z.number(),
- name: z.string(),
- description: z.string(),
- url: z.string(),
- previewUrl: z.string().optional(),
- downloadUrl: z.string().optional(),
- duration: z.number(),
- filesize: z.number(),
- type: z.string(),
- channels: z.number(),
- bitrate: z.number(),
- bitdepth: z.number(),
- samplerate: z.number(),
- username: z.string(),
- tags: z.array(z.string()),
- license: z.string(),
- created: z.string(),
- downloads: z.number().optional(),
- rating: z.number().optional(),
- ratingCount: z.number().optional(),
+ id: z.number(),
+ name: z.string(),
+ description: z.string(),
+ url: z.string(),
+ previewUrl: z.string().optional(),
+ downloadUrl: z.string().optional(),
+ duration: z.number(),
+ filesize: z.number(),
+ type: z.string(),
+ channels: z.number(),
+ bitrate: z.number(),
+ bitdepth: z.number(),
+ samplerate: z.number(),
+ username: z.string(),
+ tags: z.array(z.string()),
+ license: z.string(),
+ created: z.string(),
+ downloads: z.number().optional(),
+ rating: z.number().optional(),
+ ratingCount: z.number().optional(),
});
const apiResponseSchema = z.object({
- count: z.number(),
- next: z.string().nullable(),
- previous: z.string().nullable(),
- results: z.array(transformedResultSchema),
- query: z.string().optional(),
- type: z.string(),
- page: z.number(),
- pageSize: z.number(),
- sort: z.string(),
- minRating: z.number().optional(),
+ count: z.number(),
+ next: z.string().nullable(),
+ previous: z.string().nullable(),
+ results: z.array(transformedResultSchema),
+ query: z.string().optional(),
+ type: z.string(),
+ page: z.number(),
+ pageSize: z.number(),
+ sort: z.string(),
+ minRating: z.number().optional(),
});
-export async function GET(request: NextRequest) {
- try {
- const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
- const { success } = await baseRateLimit.limit(ip);
+function buildSortParameter({ query, sort }: { query?: string; sort: string }) {
+ if (!query) return `${sort}_desc`;
+ return sort === "score" ? "score" : `${sort}_desc`;
+}
- if (!success) {
- return NextResponse.json({ error: "Too many requests" }, { status: 429 });
- }
+function applyEffectsFilters({
+ params,
+ min_rating,
+ commercial_only,
+}: {
+ params: URLSearchParams;
+ min_rating: number;
+ commercial_only: boolean;
+}) {
+ params.append("filter", "duration:[* TO 30.0]");
+ params.append("filter", `avg_rating:[${min_rating} TO *]`);
- const { searchParams } = new URL(request.url);
+ if (commercial_only) {
+ params.append(
+ "filter",
+ 'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")',
+ );
+ }
+
+ params.append(
+ "filter",
+ "tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion",
+ );
+}
- const validationResult = searchParamsSchema.safeParse({
- q: searchParams.get("q") || undefined,
- type: searchParams.get("type") || undefined,
- page: searchParams.get("page") || undefined,
- page_size: searchParams.get("page_size") || undefined,
- sort: searchParams.get("sort") || undefined,
- min_rating: searchParams.get("min_rating") || undefined,
- });
+function transformFreesoundResult(
+ result: z.infer<typeof freesoundResultSchema>,
+) {
+ return {
+ id: result.id,
+ name: result.name,
+ description: result.description,
+ url: result.url,
+ previewUrl:
+ result.previews?.["preview-hq-mp3"] ||
+ result.previews?.["preview-lq-mp3"],
+ downloadUrl: result.download,
+ duration: result.duration,
+ filesize: result.filesize,
+ type: result.type,
+ channels: result.channels,
+ bitrate: result.bitrate,
+ bitdepth: result.bitdepth,
+ samplerate: result.samplerate,
+ username: result.username,
+ tags: result.tags,
+ license: result.license,
+ created: result.created,
+ downloads: result.num_downloads || 0,
+ rating: result.avg_rating || 0,
+ ratingCount: result.num_ratings || 0,
+ };
+}
+
+export async function GET(request: NextRequest) {
+ try {
+ const { limited } = await checkRateLimit({ request });
+ if (limited) {
+ return NextResponse.json({ error: "Too many requests" }, { status: 429 });
+ }
- if (!validationResult.success) {
- return NextResponse.json(
- {
- error: "Invalid parameters",
- details: validationResult.error.flatten().fieldErrors,
- },
- { status: 400 }
- );
- }
+ const { searchParams } = new URL(request.url);
- const {
- q: query,
- type,
- page,
- page_size: pageSize,
- sort,
- min_rating,
- commercial_only,
- } = validationResult.data;
+ const validationResult = searchParamsSchema.safeParse({
+ q: searchParams.get("q") || undefined,
+ type: searchParams.get("type") || undefined,
+ page: searchParams.get("page") || undefined,
+ page_size: searchParams.get("page_size") || undefined,
+ sort: searchParams.get("sort") || undefined,
+ min_rating: searchParams.get("min_rating") || undefined,
+ });
- if (type === "songs") {
- return NextResponse.json(
- {
- error: "Songs are not available yet",
- message:
- "Song search functionality is coming soon. Try searching for sound effects instead.",
- },
- { status: 501 }
- );
- }
+ if (!validationResult.success) {
+ return NextResponse.json(
+ {
+ error: "Invalid parameters",
+ details: validationResult.error.flatten().fieldErrors,
+ },
+ { status: 400 },
+ );
+ }
- const baseUrl = "https://freesound.org/apiv2/search/text/";
+ const {
+ q: query,
+ type,
+ page,
+ page_size: pageSize,
+ sort,
+ min_rating,
+ commercial_only,
+ } = validationResult.data;
- // Use score sorting for search queries, downloads for top sounds
- const sortParam = query
- ? sort === "score"
- ? "score"
- : `${sort}_desc`
- : `${sort}_desc`;
+ if (type === "songs") {
+ return NextResponse.json(
+ {
+ error: "Songs are not available yet",
+ message:
+ "Song search functionality is coming soon. Try searching for sound effects instead.",
+ },
+ { status: 501 },
+ );
+ }
- const params = new URLSearchParams({
- query: query || "",
- token: env.FREESOUND_API_KEY,
- page: page.toString(),
- page_size: pageSize.toString(),
- sort: sortParam,
- fields:
- "id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings",
- });
+ const baseUrl = "https://freesound.org/apiv2/search/text/";
- // Always apply sound effect filters (since we're primarily a sound effects search)
- if (type === "effects" || !type) {
- params.append("filter", "duration:[* TO 30.0]");
- params.append("filter", `avg_rating:[${min_rating} TO *]`);
+ const sortParam = buildSortParameter({ query, sort });
- // Filter by license if commercial_only is true
- if (commercial_only) {
- params.append(
- "filter",
- 'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")'
- );
- }
+ const params = new URLSearchParams({
+ query: query || "",
+ token: webEnv.FREESOUND_API_KEY,
+ page: page.toString(),
+ page_size: pageSize.toString(),
+ sort: sortParam,
+ fields:
+ "id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings",
+ });
- params.append(
- "filter",
- "tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion"
- );
- }
+ const isEffectsSearch = type === "effects" || !type;
+ if (isEffectsSearch) {
+ applyEffectsFilters({ params, min_rating, commercial_only });
+ }
- const response = await fetch(`${baseUrl}?${params.toString()}`);
+ const response = await fetch(`${baseUrl}?${params.toString()}`);
- if (!response.ok) {
- const errorText = await response.text();
- console.error("Freesound API error:", response.status, errorText);
- return NextResponse.json(
- { error: "Failed to search sounds" },
- { status: response.status }
- );
- }
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error("Freesound API error:", response.status, errorText);
+ return NextResponse.json(
+ { error: "Failed to search sounds" },
+ { status: response.status },
+ );
+ }
- const rawData = await response.json();
+ const rawData = await response.json();
- const freesoundValidation = freesoundResponseSchema.safeParse(rawData);
- if (!freesoundValidation.success) {
- console.error(
- "Invalid Freesound API response:",
- freesoundValidation.error
- );
- return NextResponse.json(
- { error: "Invalid response from Freesound API" },
- { status: 502 }
- );
- }
+ const freesoundValidation = freesoundResponseSchema.safeParse(rawData);
+ if (!freesoundValidation.success) {
+ console.error(
+ "Invalid Freesound API response:",
+ freesoundValidation.error,
+ );
+ return NextResponse.json(
+ { error: "Invalid response from Freesound API" },
+ { status: 502 },
+ );
+ }
- const data = freesoundValidation.data;
+ const data = freesoundValidation.data;
- const transformedResults = data.results.map((result) => ({
- id: result.id,
- name: result.name,
- description: result.description,
- url: result.url,
- previewUrl:
- result.previews?.["preview-hq-mp3"] ||
- result.previews?.["preview-lq-mp3"],
- downloadUrl: result.download,
- duration: result.duration,
- filesize: result.filesize,
- type: result.type,
- channels: result.channels,
- bitrate: result.bitrate,
- bitdepth: result.bitdepth,
- samplerate: result.samplerate,
- username: result.username,
- tags: result.tags,
- license: result.license,
- created: result.created,
- downloads: result.num_downloads || 0,
- rating: result.avg_rating || 0,
- ratingCount: result.num_ratings || 0,
- }));
+ const transformedResults = data.results.map(transformFreesoundResult);
- const responseData = {
- count: data.count,
- next: data.next,
- previous: data.previous,
- results: transformedResults,
- query: query || "",
- type: type || "effects",
- page,
- pageSize,
- sort,
- minRating: min_rating,
- };
+ const responseData = {
+ count: data.count,
+ next: data.next,
+ previous: data.previous,
+ results: transformedResults,
+ query: query || "",
+ type: type || "effects",
+ page,
+ pageSize,
+ sort,
+ minRating: min_rating,
+ };
- const responseValidation = apiResponseSchema.safeParse(responseData);
- if (!responseValidation.success) {
- console.error(
- "Invalid API response structure:",
- responseValidation.error
- );
- return NextResponse.json(
- { error: "Internal response formatting error" },
- { status: 500 }
- );
- }
+ const responseValidation = apiResponseSchema.safeParse(responseData);
+ if (!responseValidation.success) {
+ console.error(
+ "Invalid API response structure:",
+ responseValidation.error,
+ );
+ return NextResponse.json(
+ { error: "Internal response formatting error" },
+ { status: 500 },
+ );
+ }
- return NextResponse.json(responseValidation.data);
- } catch (error) {
- console.error("Error searching sounds:", error);
- return NextResponse.json(
- { error: "Internal server error" },
- { status: 500 }
- );
- }
+ return NextResponse.json(responseValidation.data);
+ } catch (error) {
+ console.error("Error searching sounds:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 },
+ );
+ }
}
diff --git a/apps/web/src/app/api/transcribe/route.ts b/apps/web/src/app/api/transcribe/route.ts
deleted file mode 100644
index 9a497f65..00000000
--- a/apps/web/src/app/api/transcribe/route.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-import { NextRequest, NextResponse } from "next/server";
-import { z } from "zod";
-import { env } from "@/env";
-import { baseRateLimit } from "@/lib/rate-limit";
-import { isTranscriptionConfigured } from "@/lib/transcription-utils";
-
-const transcribeRequestSchema = z.object({
- filename: z.string().min(1, "Filename is required"),
- language: z.string().optional().default("auto"),
- decryptionKey: z.string().min(1, "Decryption key is required").optional(),
- iv: z.string().min(1, "IV is required").optional(),
-});
-
-const modalResponseSchema = z.object({
- text: z.string(),
- segments: z.array(
- z.object({
- id: z.number(),
- seek: z.number(),
- start: z.number(),
- end: z.number(),
- text: z.string(),
- tokens: z.array(z.number()),
- temperature: z.number(),
- avg_logprob: z.number(),
- compression_ratio: z.number(),
- no_speech_prob: z.number(),
- })
- ),
- language: z.string(),
-});
-
-const apiResponseSchema = z.object({
- text: z.string(),
- segments: z.array(
- z.object({
- id: z.number(),
- seek: z.number(),
- start: z.number(),
- end: z.number(),
- text: z.string(),
- tokens: z.array(z.number()),
- temperature: z.number(),
- avg_logprob: z.number(),
- compression_ratio: z.number(),
- no_speech_prob: z.number(),
- })
- ),
- language: z.string(),
-});
-
-export async function POST(request: NextRequest) {
- try {
- // Rate limiting
- const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
- const { success } = await baseRateLimit.limit(ip);
- const origin = request.headers.get("origin");
-
- if (!success) {
- return NextResponse.json({ error: "Too many requests" }, { status: 429 });
- }
-
- // Check transcription configuration
- const transcriptionCheck = isTranscriptionConfigured();
- if (!transcriptionCheck.configured) {
- console.error(
- "Missing environment variables:",
- JSON.stringify(transcriptionCheck.missingVars)
- );
-
- return NextResponse.json(
- {
- error: "Transcription not configured",
- message: `Auto-captions require environment variables: ${transcriptionCheck.missingVars.join(", ")}. Check README for setup instructions.`,
- },
- { status: 503 }
- );
- }
-
- // Parse and validate request body
- const rawBody = await request.json().catch(() => null);
- if (!rawBody) {
- return NextResponse.json(
- { error: "Invalid JSON in request body" },
- { status: 400 }
- );
- }
-
- const validationResult = transcribeRequestSchema.safeParse(rawBody);
- if (!validationResult.success) {
- return NextResponse.json(
- {
- error: "Invalid request parameters",
- details: validationResult.error.flatten().fieldErrors,
- },
- { status: 400 }
- );
- }
-
- const { filename, language, decryptionKey, iv } = validationResult.data;
-
- // Prepare request body for Modal
- const modalRequestBody: any = {
- filename,
- language,
- };
-
- // Add encryption parameters if provided (zero-knowledge)
- if (decryptionKey && iv) {
- modalRequestBody.decryptionKey = decryptionKey;
- modalRequestBody.iv = iv;
- }
-
- // Call Modal transcription service
- const response = await fetch(env.MODAL_TRANSCRIPTION_URL, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(modalRequestBody),
- });
-
- if (!response.ok) {
- const errorText = await response.text();
- console.error("Modal API error:", response.status, errorText);
-
- let errorMessage = "Transcription service unavailable";
- try {
- const errorData = JSON.parse(errorText);
- errorMessage = errorData.error || errorMessage;
- } catch {
- // Use default message if parsing fails
- }
-
- return NextResponse.json(
- {
- error: errorMessage,
- message: "Failed to process transcription request",
- },
- { status: response.status >= 500 ? 502 : response.status }
- );
- }
-
- const rawResult = await response.json();
- console.log("Raw Modal response:", JSON.stringify(rawResult, null, 2));
-
- // Validate Modal response
- const modalValidation = modalResponseSchema.safeParse(rawResult);
- if (!modalValidation.success) {
- console.error("Invalid Modal API response:", modalValidation.error);
- return NextResponse.json(
- { error: "Invalid response from transcription service" },
- { status: 502 }
- );
- }
-
- const result = modalValidation.data;
-
- // Prepare and validate API response
- const responseData = {
- text: result.text,
- segments: result.segments,
- language: result.language,
- };
-
- const responseValidation = apiResponseSchema.safeParse(responseData);
- if (!responseValidation.success) {
- console.error(
- "Invalid API response structure:",
- responseValidation.error
- );
- return NextResponse.json(
- { error: "Internal response formatting error" },
- { status: 500 }
- );
- }
-
- return NextResponse.json(responseValidation.data);
- } catch (error) {
- console.error("Transcription API error:", error);
- return NextResponse.json(
- {
- error: "Internal server error",
- message: "An unexpected error occurred during transcription",
- },
- { status: 500 }
- );
- }
-}
diff --git a/apps/web/src/app/api/waitlist/export/route.ts b/apps/web/src/app/api/waitlist/export/route.ts
deleted file mode 100644
index 0200e255..00000000
--- a/apps/web/src/app/api/waitlist/export/route.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { NextRequest, NextResponse } from "next/server";
-import { baseRateLimit } from "@/lib/rate-limit";
-import { db, exportWaitlist, eq } from "@opencut/db";
-import { randomUUID } from "crypto";
-import {
- exportWaitlistSchema,
- exportWaitlistResponseSchema,
-} from "@/lib/schemas/waitlist";
-
-const requestSchema = exportWaitlistSchema;
-const responseSchema = exportWaitlistResponseSchema;
-
-export async function POST(request: NextRequest) {
- try {
- const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
- const { success } = await baseRateLimit.limit(ip);
- if (!success) {
- return NextResponse.json({ error: "Too many requests" }, { status: 429 });
- }
-
- const body = await request.json().catch(() => null);
- if (!body) {
- return NextResponse.json(
- { error: "Invalid JSON in request body" },
- { status: 400 }
- );
- }
-
- const parsed = requestSchema.safeParse(body);
- if (!parsed.success) {
- return NextResponse.json(
- {
- error: "Invalid request parameters",
- details: parsed.error.flatten().fieldErrors,
- },
- { status: 400 }
- );
- }
-
- const { email } = parsed.data;
-
- const existing = await db
- .select({ id: exportWaitlist.id })
- .from(exportWaitlist)
- .where(eq(exportWaitlist.email, email))
- .limit(1);
-
- if (existing.length > 0) {
- const responseData = { success: true, alreadySubscribed: true } as const;
- const validated = responseSchema.safeParse(responseData);
- if (!validated.success) {
- return NextResponse.json(
- { error: "Internal response formatting error" },
- { status: 500 }
- );
- }
- return NextResponse.json(validated.data);
- }
-
- await db.insert(exportWaitlist).values({
- id: randomUUID(),
- email,
- createdAt: new Date(),
- updatedAt: new Date(),
- });
-
- const responseData = { success: true } as const;
- const validated = responseSchema.safeParse(responseData);
- if (!validated.success) {
- return NextResponse.json(
- { error: "Internal response formatting error" },
- { status: 500 }
- );
- }
- return NextResponse.json(validated.data);
- } catch (error) {
- console.error("Waitlist API error:", error);
- return NextResponse.json(
- { error: "Internal server error" },
- { status: 500 }
- );
- }
-}
diff --git a/apps/web/src/app/base-page.tsx b/apps/web/src/app/base-page.tsx
new file mode 100644
index 00000000..a3d56fe5
--- /dev/null
+++ b/apps/web/src/app/base-page.tsx
@@ -0,0 +1,53 @@
+import { Header } from "@/components/header";
+import { Footer } from "@/components/footer";
+import { cn } from "@/utils/ui";
+
+interface BasePageProps {
+ children: React.ReactNode;
+ className?: string;
+ mainClassName?: string;
+ maxWidth?: "3xl" | "6xl" | "full";
+ title?: string;
+ description?: string;
+}
+
+export function BasePage({
+ children,
+ className = "",
+ mainClassName = "",
+ maxWidth = "3xl",
+ title,
+ description,
+}: BasePageProps) {
+ const maxWidthClass = {
+ "3xl": "max-w-3xl",
+ "6xl": "max-w-6xl",
+ full: "max-w-full",
+ }[maxWidth];
+
+ return (
+ <section className={cn("bg-background min-h-screen", className)}>
+ <Header />
+ <main
+ className={cn(
+ "relative container mx-auto flex flex-col gap-12 px-6 pt-12 pb-24 md:pt-24",
+ maxWidthClass,
+ mainClassName,
+ )}
+ >
+ {title && description && (
+ <div className="flex flex-col gap-8 text-center">
+ <h1 className="text-5xl font-bold tracking-tight md:text-6xl">
+ {title}
+ </h1>
+ <p className="text-muted-foreground mx-auto max-w-2xl text-xl leading-relaxed">
+ {description}
+ </p>
+ </div>
+ )}
+ {children}
+ </main>
+ <Footer />
+ </section>
+ );
+}
diff --git a/apps/web/src/app/blog/[slug]/page.tsx b/apps/web/src/app/blog/[slug]/page.tsx
index d759050b..46c6163d 100644
--- a/apps/web/src/app/blog/[slug]/page.tsx
+++ b/apps/web/src/app/blog/[slug]/page.tsx
@@ -1,144 +1,152 @@
-import { Header } from "@/components/header";
-import Prose from "@/components/ui/prose";
-import { Separator } from "@/components/ui/separator";
-import { getPosts, getSinglePost, processHtmlContent } from "@/lib/blog-query";
-import { Metadata } from "next";
+import type { Metadata } from "next";
import Image from "next/image";
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 "@/types/blog";
type PageProps = {
- params: Promise<{ slug: string }>;
- searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+ params: Promise<{ slug: string }>;
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export async function generateMetadata({
- params,
+ params,
}: PageProps): Promise<Metadata> {
- const slug = (await params).slug;
+ const slug = (await params).slug;
- const data = await getSinglePost(slug);
+ const data = await getSinglePost({ slug });
- if (!data || !data.post) return {};
+ if (!data || !data.post) return {};
- return {
- title: data.post.title,
- description: data.post.description,
- twitter: {
- title: `${data.post.title}`,
- description: `${data.post.description}`,
- card: "summary_large_image",
- images: [
- {
- url: data.post.coverImage,
- width: "1200",
- height: "630",
- alt: data.post.title,
- },
- ],
- },
- openGraph: {
- type: "article",
- images: [
- {
- url: data.post.coverImage,
- width: "1200",
- height: "630",
- alt: data.post.title,
- },
- ],
- title: data.post.title,
- description: data.post.description,
- publishedTime: new Date(data.post.publishedAt).toISOString(),
- authors: [
- ...data.post.authors.map((author: { name: string }) => author.name),
- ],
- },
- };
+ return {
+ title: data.post.title,
+ description: data.post.description,
+ twitter: {
+ title: `${data.post.title}`,
+ description: `${data.post.description}`,
+ card: "summary_large_image",
+ images: [
+ {
+ url: data.post.coverImage,
+ width: "1200",
+ height: "630",
+ alt: data.post.title,
+ },
+ ],
+ },
+ openGraph: {
+ type: "article",
+ images: [
+ {
+ url: data.post.coverImage,
+ width: "1200",
+ height: "630",
+ alt: data.post.title,
+ },
+ ],
+ title: data.post.title,
+ description: data.post.description,
+ publishedTime: new Date(data.post.publishedAt).toISOString(),
+ authors: data.post.authors.map((author: Author) => author.name),
+ },
+ };
}
export async function generateStaticParams() {
- const data = await getPosts();
- if (!data || !data.posts.length) return [];
+ const data = await getPosts();
+ if (!data || !data.posts.length) return [];
- return data.posts.map((post) => ({
- slug: post.slug,
- }));
+ return data.posts.map((post) => ({
+ slug: post.slug,
+ }));
}
-async function Page({ params }: PageProps) {
- const slug = (await params).slug;
- const data = await getSinglePost(slug);
- if (!data || !data.post) return notFound();
+export default async function BlogPostPage({ params }: PageProps) {
+ const slug = (await params).slug;
+ const data = await getSinglePost({ slug });
+ if (!data || !data.post) return notFound();
- const html = await processHtmlContent(data.post.content);
+ const html = await processHtmlContent({ html: data.post.content });
- const formattedDate = new Date(data.post.publishedAt).toLocaleDateString(
- "en-US",
- {
- day: "numeric",
- month: "long",
- year: "numeric",
- }
- );
+ return (
+ <BasePage>
+ <PostHeader post={data.post} />
+ <Separator />
+ <PostContent html={html} />
+ </BasePage>
+ );
+}
- return (
- <div className="min-h-screen bg-background">
- <Header />
+function PostHeader({ post }: { post: Post }) {
+ const formattedDate = new Date(post.publishedAt).toLocaleDateString("en-US", {
+ day: "numeric",
+ month: "long",
+ year: "numeric",
+ });
- <main className="relative">
- <div className="absolute inset-0 overflow-hidden pointer-events-none">
- <div className="absolute -top-40 -right-40 w-96 h-96 bg-linear-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
- <div className="absolute top-1/2 -left-40 w-80 h-80 bg-linear-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
- </div>
+ return (
+ <>
+ {post.coverImage && <PostCoverImage post={post} />}
+ <PostMeta date={formattedDate} publishedAt={post.publishedAt} />
+ <PostTitle title={post.title} />
+ <PostAuthor author={post.authors[0]} />
+ </>
+ );
+}
- <div className="relative container max-w-3xl mx-auto px-4 py-16">
- <div className="text-center mb-6">
- {data.post.coverImage && (
- <div className="relative aspect-video rounded-lg overflow-hidden mb-6">
- <Image
- src={data.post.coverImage}
- alt={data.post.title}
- loading="eager"
- fill
- className="object-cover rounded-lg"
- />
- </div>
- )}
- <div className="flex items-center justify-center mb-6">
- <time dateTime={data.post.publishedAt.toString()}>
- {formattedDate}
- </time>
- </div>
+function PostCoverImage({ post }: { post: Post }) {
+ return (
+ <div className="relative aspect-video overflow-hidden rounded-lg">
+ <Image
+ src={post.coverImage}
+ alt={post.title}
+ loading="eager"
+ fill
+ className="rounded-lg object-cover"
+ />
+ </div>
+ );
+}
- <h1 className="text-5xl md:text-4xl font-bold tracking-tight mb-6">
- {data.post.title}
- </h1>
- <div className="flex items-center justify-center gap-2">
- {data.post.authors[0] && (
- <>
- <Image
- src={data.post.authors[0].image}
- alt={data.post.authors[0].name}
- width={36}
- height={36}
- loading="eager"
- className="aspect-square shrink-0 size-8 rounded-full"
- />
- <p className="text-muted-foreground">
- {data.post.authors[0].name}
- </p>
- </>
- )}
- </div>
- </div>
- <Separator />
- <section className="mt-14">
- <Prose html={html} />
- </section>
- </div>
- </main>
- </div>
- );
+function PostMeta({ date, publishedAt }: { date: string; publishedAt: Date }) {
+ return (
+ <div className="flex items-center justify-center">
+ <time dateTime={publishedAt.toString()}>{date}</time>
+ </div>
+ );
}
-export default Page;
+function PostTitle({ title }: { title: string }) {
+ return (
+ <h1 className="text-5xl font-bold tracking-tight md:text-4xl">{title}</h1>
+ );
+}
+
+function PostAuthor({ author }: { author?: Author }) {
+ if (!author) return null;
+
+ return (
+ <div className="flex items-center justify-center gap-2">
+ <Image
+ src={author.image}
+ alt={author.name}
+ width={36}
+ height={36}
+ loading="eager"
+ className="aspect-square size-8 shrink-0 rounded-full"
+ />
+ <p className="text-muted-foreground">{author.name}</p>
+ </div>
+ );
+}
+
+function PostContent({ html }: { html: string }) {
+ return (
+ <section className="pt-8">
+ <Prose html={html} />
+ </section>
+ );
+}
diff --git a/apps/web/src/app/blog/page.tsx b/apps/web/src/app/blog/page.tsx
index 08da9fd7..9e3b6d11 100644
--- a/apps/web/src/app/blog/page.tsx
+++ b/apps/web/src/app/blog/page.tsx
@@ -1,98 +1,73 @@
-import { Metadata } from "next";
-import { Header } from "@/components/header";
-import { Card, CardContent } from "@/components/ui/card";
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import type { Metadata } from "next";
import Link from "next/link";
-import { getPosts } from "@/lib/blog-query";
-import Image from "next/image";
+import { BasePage } from "@/app/base-page";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Separator } from "@/components/ui/separator";
+import { getPosts } from "@/lib/blog/query";
+import type { Author, Post } from "@/types/blog";
export const metadata: Metadata = {
- title: "Blog - OpenCut",
- description:
- "Read the latest news and updates about OpenCut, the free and open-source video editor.",
- openGraph: {
- title: "Blog - OpenCut",
- description:
- "Read the latest news and updates about OpenCut, the free and open-source video editor.",
- type: "website",
- },
+ title: "Blog - OpenCut",
+ description:
+ "Read the latest news and updates about OpenCut, the free and open-source video editor.",
+ openGraph: {
+ title: "Blog - OpenCut",
+ description:
+ "Read the latest news and updates about OpenCut, the free and open-source video editor.",
+ type: "website",
+ },
};
export default async function BlogPage() {
- const data = await getPosts();
- if (!data || !data.posts) return <div>No posts yet</div>;
-
- return (
- <div className="min-h-screen bg-background">
- <Header />
+ const data = await getPosts();
+ if (!data || !data.posts) return <div>No posts yet</div>;
- <main className="relative">
- <div className="absolute inset-0 overflow-hidden pointer-events-none">
- <div className="absolute -top-40 -right-40 w-96 h-96 bg-linear-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
- <div className="absolute top-1/2 -left-40 w-80 h-80 bg-linear-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
- </div>
+ return (
+ <BasePage
+ title="Blog"
+ description="Read the latest news and updates about OpenCut, the free and open-source video editor."
+ >
+ <div className="flex flex-col">
+ {data.posts.map((post) => (
+ <div key={post.id} className="flex flex-col">
+ <BlogPostItem post={post} />
+ <Separator />
+ </div>
+ ))}
+ </div>
+ </BasePage>
+ );
+}
- <div className="relative container max-w-3xl mx-auto px-4 py-16">
- <div className="text-center mb-20">
- <h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
- Blog
- </h1>
- <p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
- Read the latest news and updates about OpenCut, the free and
- open-source video editor.
- </p>
- </div>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- {data.posts.map((post) => (
- <Link key={post.id} href={`/blog/${post.slug}`}>
- <Card className="h-full hover:shadow-lg transition-shadow overflow-hidden">
- {post.coverImage && (
- <div className="relative aspect-video">
- <Image
- src={post.coverImage}
- alt={post.title}
- fill
- className="object-cover rounded-xl"
- />
- </div>
- )}
+function BlogPostItem({ post }: { post: Post }) {
+ return (
+ <Link href={`/blog/${post.slug}`}>
+ <div className="flex h-auto w-full items-center justify-between py-6 opacity-100 hover:opacity-75">
+ <div className="flex flex-col gap-2">
+ <h2 className="text-xl font-semibold">{post.title}</h2>
+ <p className="text-muted-foreground">{post.description}</p>
+ </div>
+ {post.authors && post.authors.length > 0 && (
+ <AuthorList authors={post.authors} />
+ )}
+ </div>
+ </Link>
+ );
+}
- <CardContent className="p-6">
- {post.authors && post.authors.length > 0 && (
- <div className="flex items-center gap-2 mb-4">
- {post.authors.map((author, index) => (
- <div
- key={author.id}
- className="flex items-center gap-2"
- >
- <Avatar className="w-6 h-6">
- <AvatarImage
- src={author.image}
- alt={author.name}
- />
- <AvatarFallback className="text-xs">
- {author.name.charAt(0).toUpperCase()}
- </AvatarFallback>
- </Avatar>
- <span className="text-sm text-muted-foreground">
- {author.name}
- </span>
- {index < post.authors.length - 1 && (
- <span className="text-muted-foreground">•</span>
- )}
- </div>
- ))}
- </div>
- )}
- <h2 className="text-xl font-semibold mb-2">{post.title}</h2>
- <p className="text-muted-foreground">{post.description}</p>
- </CardContent>
- </Card>
- </Link>
- ))}
- </div>
- </div>
- </main>
- </div>
- );
+function AuthorList({ authors }: { authors: Author[] }) {
+ return (
+ <div className="flex items-center gap-2">
+ {authors.map((author) => (
+ <div key={author.id} className="flex items-center gap-2">
+ <Avatar className="size-6 shadow-sm">
+ <AvatarImage src={author.image} alt={author.name} />
+ <AvatarFallback className="text-xs">
+ {author.name.charAt(0).toUpperCase()}
+ </AvatarFallback>
+ </Avatar>
+ </div>
+ ))}
+ </div>
+ );
}
diff --git a/apps/web/src/app/contributors/page.tsx b/apps/web/src/app/contributors/page.tsx
index 3ad99023..21703852 100644
--- a/apps/web/src/app/contributors/page.tsx
+++ b/apps/web/src/app/contributors/page.tsx
@@ -1,330 +1,242 @@
-import { Metadata } from "next";
-import { Header } from "@/components/header";
-import { Card, CardContent } from "@/components/ui/card";
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-import { Button } from "@/components/ui/button";
-import { ExternalLink } from "lucide-react";
+import type { Metadata } from "next";
import Link from "next/link";
-import {
- GithubIcon,
- MarbleIcon,
- VercelIcon,
- DataBuddyIcon,
-} from "@/components/icons";
-import { Badge } from "@/components/ui/badge";
-import { EXTERNAL_TOOLS } from "@/constants/site";
+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 "@/constants/site-constants";
+import { BasePage } from "../base-page";
export const metadata: Metadata = {
- title: "Contributors - OpenCut",
- description:
- "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.",
- openGraph: {
- title: "Contributors - OpenCut",
- description:
- "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.",
- type: "website",
- },
+ title: "Contributors - OpenCut",
+ description:
+ "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.",
+ openGraph: {
+ title: "Contributors - OpenCut",
+ description:
+ "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.",
+ type: "website",
+ },
};
interface Contributor {
- id: number;
- login: string;
- avatar_url: string;
- html_url: string;
- contributions: number;
- type: string;
+ id: number;
+ login: string;
+ avatar_url: string;
+ html_url: string;
+ contributions: number;
+ type: string;
}
async function getContributors(): Promise<Contributor[]> {
- try {
- const response = await fetch(
- "https://api.github.com/repos/OpenCut-app/OpenCut/contributors?per_page=100",
- {
- headers: {
- Accept: "application/vnd.github.v3+json",
- "User-Agent": "OpenCut-Web-App",
- },
- next: { revalidate: 600 }, // 10 minutes
- } as RequestInit
- );
+ try {
+ const response = await fetch(
+ "https://api.github.com/repos/OpenCut-app/OpenCut/contributors?per_page=100",
+ {
+ headers: {
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "OpenCut-Web-App",
+ },
+ next: { revalidate: 600 }, // 10 minutes
+ },
+ );
- if (!response.ok) {
- console.error("Failed to fetch contributors");
- return [];
- }
+ if (!response.ok) {
+ console.error("Failed to fetch contributors");
+ return [];
+ }
- const contributors = (await response.json()) as Contributor[];
+ const contributors = (await response.json()) as Contributor[];
- const filteredContributors = contributors.filter(
- (contributor: Contributor) => contributor.type === "User"
- );
+ const filteredContributors = contributors.filter(
+ (contributor) => contributor.type === "User",
+ );
- return filteredContributors;
- } catch (error) {
- console.error("Error fetching contributors:", error);
- return [];
- }
+ return filteredContributors;
+ } catch (error) {
+ console.error("Error fetching contributors:", error);
+ return [];
+ }
}
export default async function ContributorsPage() {
- const contributors = await getContributors();
- const topContributors = contributors.slice(0, 2);
- const otherContributors = contributors.slice(2);
-
- return (
- <div className="min-h-screen bg-background">
- <Header />
-
- <main className="relative">
- <div className="absolute inset-0 overflow-hidden pointer-events-none">
- <div className="absolute -top-40 -right-40 w-96 h-96 bg-linear-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
- <div className="absolute top-1/2 -left-40 w-80 h-80 bg-linear-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
- </div>
-
- <div className="relative container mx-auto px-4 py-16">
- <div className="max-w-6xl mx-auto">
- <div className="text-center mb-20">
- <Link
- href={"https://github.com/OpenCut-app/OpenCut"}
- target="_blank"
- >
- <Badge variant="secondary" className="gap-2 mb-6">
- <GithubIcon className="h-3 w-3" />
- Open Source
- </Badge>
- </Link>
- <h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
- Contributors
- </h1>
- <p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
- Meet the amazing developers who are building the future of video
- editing
- </p>
+ const contributors = await getContributors();
+ const topContributors = contributors.slice(0, 2);
+ const otherContributors = contributors.slice(2);
+ const totalContributions = contributors.reduce(
+ (sum, c) => sum + c.contributions,
+ 0,
+ );
- <div className="flex items-center justify-center gap-8 text-sm">
- <div className="flex items-center gap-2">
- <div className="w-2 h-2 bg-foreground rounded-full" />
- <span className="font-medium">{contributors.length}</span>
- <span className="text-muted-foreground">contributors</span>
- </div>
- <div className="flex items-center gap-2">
- <div className="w-2 h-2 bg-foreground rounded-full" />
- <span className="font-medium">
- {contributors.reduce((sum, c) => sum + c.contributions, 0)}
- </span>
- <span className="text-muted-foreground">contributions</span>
- </div>
- </div>
- </div>
+ return (
+ <BasePage
+ title="Contributors"
+ description="Meet the amazing people who contribute to OpenCut, the free and open-source video editor."
+ >
+ <div className="-mt-4 flex items-center justify-center gap-8 text-sm">
+ <StatItem value={contributors.length} label="contributors" />
+ <StatItem value={totalContributions} label="contributions" />
+ </div>
- {topContributors.length > 0 && (
- <div className="mb-20">
- <div className="text-center mb-12">
- <h2 className="text-2xl font-semibold mb-2">
- Top Contributors
- </h2>
- <p className="text-muted-foreground">
- Leading the way in contributions
- </p>
- </div>
-
- <div className="flex flex-col md:flex-row gap-6 justify-center max-w-4xl mx-auto">
- {topContributors.map((contributor, index) => (
- <Link
- key={contributor.id}
- href={contributor.html_url}
- target="_blank"
- rel="noopener noreferrer"
- className="group block flex-1"
- >
- <div className="relative mx-auto max-w-md">
- <div className="absolute inset-0 bg-linear-to-r from-muted/50 to-muted/30 rounded-2xl blur-sm group-hover:blur-md transition-all duration-300" />
- <Card className="relative bg-background/80 backdrop-blur-xs border-2 group-hover:border-muted-foreground/20 transition-all duration-300 group-hover:shadow-xl">
- <CardContent className="p-8 text-center">
- <div className="relative mb-6">
- <Avatar className="h-24 w-24 mx-auto ring-4 ring-background shadow-2xl">
- <AvatarImage
- src={contributor.avatar_url}
- alt={`${contributor.login}'s avatar`}
- />
- <AvatarFallback className="text-lg font-semibold">
- {contributor.login.charAt(0).toUpperCase()}
- </AvatarFallback>
- </Avatar>
- </div>
- <h3 className="text-xl font-semibold mb-2 group-hover:text-foreground/80 transition-colors">
- {contributor.login}
- </h3>
- <div className="flex items-center justify-center gap-2 text-muted-foreground">
- <span className="font-medium text-foreground">
- {contributor.contributions}
- </span>
- <span>contributions</span>
- </div>
- </CardContent>
- </Card>
- </div>
- </Link>
- ))}
- </div>
- </div>
- )}
+ <div className="mx-auto flex max-w-6xl flex-col gap-20">
+ {topContributors.length > 0 && (
+ <TopContributorsSection contributors={topContributors} />
+ )}
+ {otherContributors.length > 0 && (
+ <AllContributorsSection contributors={otherContributors} />
+ )}
+ <ExternalToolsSection />
+ <GitHubContributeSection
+ title="Join the community"
+ description="OpenCut is built by developers like you. Every contribution, no matter how small, helps make video editing more accessible for everyone."
+ />
+ </div>
+ </BasePage>
+ );
+}
- {otherContributors.length > 0 && (
- <div>
- <div className="text-center mb-12">
- <h2 className="text-2xl font-semibold mb-2">
- All Contributors
- </h2>
- <p className="text-muted-foreground">
- Everyone who makes OpenCut better
- </p>
- </div>
+function StatItem({ value, label }: { value: number; label: string }) {
+ return (
+ <div className="flex items-center gap-2">
+ <div className="bg-foreground size-2 rounded-full" />
+ <span className="font-medium">{value}</span>
+ <span className="text-muted-foreground">{label}</span>
+ </div>
+ );
+}
- <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
- {otherContributors.map((contributor, index) => (
- <Link
- key={contributor.id}
- href={contributor.html_url}
- target="_blank"
- rel="noopener noreferrer"
- className="group block"
- style={{
- animationDelay: `${index * 50}ms`,
- }}
- >
- <div className="text-center p-2 rounded-xl transition-all duration-300 hover:opacity-50">
- <Avatar className="h-16 w-16 mx-auto mb-3">
- <AvatarImage
- src={contributor.avatar_url}
- alt={`${contributor.login}'s avatar`}
- />
- <AvatarFallback className="font-medium">
- {contributor.login.charAt(0).toUpperCase()}
- </AvatarFallback>
- </Avatar>
- <h3 className="font-medium text-sm truncate group-hover:text-foreground transition-colors mb-1">
- {contributor.login}
- </h3>
- <p className="text-xs text-muted-foreground">
- {contributor.contributions}
- </p>
- </div>
- </Link>
- ))}
- </div>
- </div>
- )}
+function TopContributorsSection({
+ contributors,
+}: {
+ contributors: Contributor[];
+}) {
+ return (
+ <div className="flex flex-col gap-10">
+ <div className="flex flex-col gap-2 text-center">
+ <h2 className="text-2xl font-semibold">Top contributors</h2>
+ <p className="text-muted-foreground">
+ Leading the way in contributions
+ </p>
+ </div>
- {contributors.length === 0 && (
- <div className="text-center py-20">
- <div className="w-20 h-20 mx-auto mb-6 rounded-full bg-muted/50 flex items-center justify-center">
- <GithubIcon className="h-8 w-8 text-muted-foreground" />
- </div>
- <h3 className="text-xl font-medium mb-3">
- No contributors found
- </h3>
- <p className="text-muted-foreground mb-8 max-w-md mx-auto">
- Unable to load contributors at the moment. Check back later or
- view on GitHub.
- </p>
- <Link
- href="https://github.com/OpenCut-app/OpenCut/graphs/contributors"
- target="_blank"
- rel="noopener noreferrer"
- >
- <Button variant="outline" className="gap-2">
- <GithubIcon className="h-4 w-4" />
- View on GitHub
- <ExternalLink className="h-4 w-4" />
- </Button>
- </Link>
- </div>
- )}
+ <div className="mx-auto flex w-full max-w-xl flex-col justify-center gap-6 md:flex-row">
+ {contributors.map((contributor) => (
+ <TopContributorCard key={contributor.id} contributor={contributor} />
+ ))}
+ </div>
+ </div>
+ );
+}
- <div className="mt-6 mb-20">
- <div className="text-center mb-12">
- <h2 className="text-2xl font-semibold mb-2">External Tools</h2>
- <p className="text-muted-foreground">
- Tools we use to build OpenCut
- </p>
- </div>
+function TopContributorCard({ contributor }: { contributor: Contributor }) {
+ return (
+ <Link
+ href={contributor.html_url}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="w-full"
+ >
+ <Card>
+ <CardContent className="flex flex-col gap-6 p-8 text-center">
+ <Avatar className="mx-auto size-28">
+ <AvatarImage
+ src={contributor.avatar_url}
+ alt={`${contributor.login}'s avatar`}
+ />
+ <AvatarFallback className="text-lg font-semibold">
+ {contributor.login.charAt(0).toUpperCase()}
+ </AvatarFallback>
+ </Avatar>
+ <div className="flex flex-col gap-2">
+ <h3 className="text-xl font-semibold">{contributor.login}</h3>
+ <div className="flex items-center justify-center gap-2">
+ <span className="font-medium">{contributor.contributions}</span>
+ <span className="text-muted-foreground">contributions</span>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ </Link>
+ );
+}
- <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 max-w-4xl mx-auto">
- {EXTERNAL_TOOLS.map((tool, index) => {
- const IconComponent = {
- MarbleIcon,
- VercelIcon,
- DataBuddyIcon,
- }[tool.icon];
+function AllContributorsSection({
+ contributors,
+}: {
+ contributors: Contributor[];
+}) {
+ return (
+ <div className="flex flex-col gap-12">
+ <div className="flex flex-col gap-2 text-center">
+ <h2 className="text-2xl font-semibold">All contributors</h2>
+ <p className="text-muted-foreground">
+ Everyone who makes OpenCut better
+ </p>
+ </div>
- return (
- <Link
- key={tool.name}
- href={tool.url}
- target="_blank"
- className="group block"
- style={{
- animationDelay: `${index * 100}ms`,
- }}
- >
- <Card className="h-full bg-background/80 backdrop-blur-xs border-2 group-hover:border-muted-foreground/20 transition-all duration-300 group-hover:shadow-xl">
- <CardContent className="p-6 text-center h-full flex flex-col">
- <div className="mb-4">
- <div className="w-12 h-12 mx-auto rounded-full bg-muted/50 flex items-center justify-center group-hover:bg-muted/70 transition-colors">
- <IconComponent className="h-6 w-6" size={24} />
- </div>
- </div>
- <h3 className="font-semibold text-lg mb-2 group-hover:text-foreground/80 transition-colors">
- {tool.name}
- </h3>
- <p className="text-sm text-muted-foreground flex-1">
- {tool.description}
- </p>
- </CardContent>
- </Card>
- </Link>
- );
- })}
- </div>
- </div>
+ <div className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6">
+ {contributors.map((contributor) => (
+ <Link
+ key={contributor.id}
+ href={contributor.html_url}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="opacity-100 hover:opacity-70"
+ >
+ <div className="flex flex-col items-center gap-2 p-2">
+ <Avatar className="size-16">
+ <AvatarImage
+ src={contributor.avatar_url}
+ alt={`${contributor.login}'s avatar`}
+ />
+ <AvatarFallback>
+ {contributor.login.charAt(0).toUpperCase()}
+ </AvatarFallback>
+ </Avatar>
+ <div className="text-center">
+ <h3 className="text-sm font-medium">{contributor.login}</h3>
+ <p className="text-muted-foreground text-xs">
+ {contributor.contributions}
+ </p>
+ </div>
+ </div>
+ </Link>
+ ))}
+ </div>
+ </div>
+ );
+}
- <div className="mt-32 text-center">
- <div className="max-w-2xl mx-auto">
- <h2 className="text-3xl font-bold mb-4">Join the community</h2>
- <p className="text-lg text-muted-foreground mb-10 leading-relaxed">
- OpenCut is built by developers like you. Every contribution,
- no matter how small, helps make video editing more accessible
- for everyone.
- </p>
+function ExternalToolsSection() {
+ return (
+ <div className="flex flex-col gap-10">
+ <div className="flex flex-col gap-2 text-center">
+ <h2 className="text-2xl font-semibold">External tools</h2>
+ <p className="text-muted-foreground">Tools we use to build OpenCut</p>
+ </div>
- <div className="flex flex-col sm:flex-row gap-4 justify-center">
- <Link
- href="https://github.com/OpenCut-app/OpenCut/blob/main/.github/CONTRIBUTING.md"
- target="_blank"
- rel="noopener noreferrer"
- >
- <Button size="lg" className="gap-2 group">
- <GithubIcon className="h-4 w-4 group-hover:scale-110 transition-transform" />
- Start Contributing
- </Button>
- </Link>
- <Link
- href="https://github.com/OpenCut-app/OpenCut/issues"
- target="_blank"
- rel="noopener noreferrer"
- >
- <Button variant="outline" size="lg" className="gap-2 group">
- Browse Issues
- <ExternalLink className="h-4 w-4 group-hover:scale-110 transition-transform" />
- </Button>
- </Link>
- </div>
- </div>
- </div>
- </div>
- </div>
- </main>
- </div>
- );
+ <div className="mx-auto grid max-w-4xl grid-cols-1 gap-6 sm:grid-cols-2">
+ {EXTERNAL_TOOLS.map((tool, index) => (
+ <Link
+ key={tool.url}
+ href={tool.url}
+ target="_blank"
+ className="block"
+ style={{ animationDelay: `${index * 100}ms` }}
+ >
+ <Card className="h-full">
+ <CardContent className="flex items-center justify-center h-full flex-col gap-4 p-6 text-center">
+ <tool.icon className="size-8" />
+ <div className="flex flex-1 flex-col gap-2">
+ <h3 className="text-lg font-semibold">{tool.name}</h3>
+ <p className="text-muted-foreground text-sm">
+ {tool.description}
+ </p>
+ </div>
+ </CardContent>
+ </Card>
+ </Link>
+ ))}
+ </div>
+ </div>
+ );
}
diff --git a/apps/web/src/app/editor/[project_id]/layout.tsx b/apps/web/src/app/editor/[project_id]/layout.tsx
deleted file mode 100644
index c37e78b7..00000000
--- a/apps/web/src/app/editor/[project_id]/layout.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-"use client";
-
-export default function EditorLayout({
- children,
-}: {
- children: React.ReactNode;
-}) {
- return <div>{children}</div>;
-}
diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx
index 1b53b6b5..e652e928 100644
--- a/apps/web/src/app/editor/[project_id]/page.tsx
+++ b/apps/web/src/app/editor/[project_id]/page.tsx
@@ -1,459 +1,108 @@
"use client";
-import { useEffect, useRef } from "react";
-import { useParams, useRouter } from "next/navigation";
+import { useParams } from "next/navigation";
import {
- ResizablePanelGroup,
- ResizablePanel,
- ResizableHandle,
+ ResizablePanelGroup,
+ ResizablePanel,
+ ResizableHandle,
} from "@/components/ui/resizable";
-import { MediaPanel } from "@/components/editor/media-panel";
-import { PropertiesPanel } from "@/components/editor/properties-panel";
+import { AssetsPanel } from "@/components/editor/panels/assets";
+import { PropertiesPanel } from "@/components/editor/panels/properties";
import { Timeline } from "@/components/editor/timeline";
-import { PreviewPanel } from "@/components/editor/preview-panel";
+import { PreviewPanel } from "@/components/editor/panels/preview";
import { EditorHeader } from "@/components/editor/editor-header";
-import { usePanelStore } from "@/stores/panel-store";
-import { useProjectStore } from "@/stores/project-store";
import { EditorProvider } from "@/components/providers/editor-provider";
-import { usePlaybackControls } from "@/hooks/use-playback-controls";
import { Onboarding } from "@/components/editor/onboarding";
+import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
+import { usePanelStore } from "@/stores/panel-store";
export default function Editor() {
- const {
- toolsPanel,
- previewPanel,
- mainContent,
- timeline,
- setToolsPanel,
- setPreviewPanel,
- setMainContent,
- setTimeline,
- propertiesPanel,
- setPropertiesPanel,
- activePreset,
- resetCounter,
- } = usePanelStore();
-
- const {
- activeProject,
- loadProject,
- createNewProject,
- isInvalidProjectId,
- markProjectIdAsInvalid,
- } = useProjectStore();
- const params = useParams();
- const router = useRouter();
- const projectId = params.project_id as string;
- const handledProjectIds = useRef<Set<string>>(new Set());
- const isInitializingRef = useRef<boolean>(false);
-
- usePlaybackControls();
-
- useEffect(() => {
- let isCancelled = false;
-
- const initProject = async () => {
- if (!projectId) {
- return;
- }
-
- // Prevent duplicate initialization
- if (isInitializingRef.current) {
- return;
- }
-
- // Check if project is already loaded
- if (activeProject?.id === projectId) {
- return;
- }
-
- // Check global invalid tracking first (most important for preventing duplicates)
- if (isInvalidProjectId(projectId)) {
- return;
- }
-
- // Check if we've already handled this project ID locally
- if (handledProjectIds.current.has(projectId)) {
- return;
- }
-
- // Mark as initializing to prevent race conditions
- isInitializingRef.current = true;
- handledProjectIds.current.add(projectId);
-
- try {
- await loadProject(projectId);
-
- // Check if component was unmounted during async operation
- if (isCancelled) {
- return;
- }
-
- // Project loaded successfully
- isInitializingRef.current = false;
- } catch (error) {
- // Check if component was unmounted during async operation
- if (isCancelled) {
- return;
- }
-
- // More specific error handling - only create new project for actual "not found" errors
- const isProjectNotFound =
- error instanceof Error &&
- (error.message.includes("not found") ||
- error.message.includes("does not exist") ||
- error.message.includes("Project not found"));
-
- if (isProjectNotFound) {
- // Mark this project ID as invalid globally BEFORE creating project
- markProjectIdAsInvalid(projectId);
-
- try {
- const newProjectId = await createNewProject("Untitled Project");
-
- // Check again if component was unmounted
- if (isCancelled) {
- return;
- }
-
- router.replace(`/editor/${newProjectId}`);
- } catch (createError) {
- console.error("Failed to create new project:", createError);
- }
- } else {
- // For other errors (storage issues, corruption, etc.), don't create new project
- console.error(
- "Project loading failed with recoverable error:",
- error
- );
- // Remove from handled set so user can retry
- handledProjectIds.current.delete(projectId);
- }
-
- isInitializingRef.current = false;
- }
- };
+ const params = useParams();
+ const projectId = params.project_id as string;
- initProject();
-
- // Cleanup function to cancel async operations
- return () => {
- isCancelled = true;
- isInitializingRef.current = false;
- };
- }, [
- projectId,
- loadProject,
- createNewProject,
- router,
- isInvalidProjectId,
- markProjectIdAsInvalid,
- ]);
-
- return (
- <EditorProvider>
- <div className="h-screen w-screen flex flex-col bg-background overflow-hidden">
- <EditorHeader />
- <div className="flex-1 min-h-0 min-w-0">
- {activePreset === "media" ? (
- <ResizablePanelGroup
- key={`media-${activePreset}-${resetCounter}`}
- direction="horizontal"
- className="h-full w-full gap-[0.18rem] px-3 pb-3"
- >
- <ResizablePanel
- defaultSize={toolsPanel}
- minSize={15}
- maxSize={40}
- onResize={setToolsPanel}
- className="min-w-0 rounded-sm"
- >
- <MediaPanel />
- </ResizablePanel>
-
- <ResizableHandle withHandle />
-
- <ResizablePanel
- defaultSize={100 - toolsPanel}
- minSize={60}
- className="min-w-0 min-h-0"
- >
- <ResizablePanelGroup
- direction="vertical"
- className="h-full w-full gap-[0.18rem]"
- >
- <ResizablePanel
- defaultSize={mainContent}
- minSize={30}
- maxSize={85}
- onResize={setMainContent}
- className="min-h-0"
- >
- <ResizablePanelGroup
- direction="horizontal"
- className="h-full w-full gap-[0.19rem]"
- >
- <ResizablePanel
- defaultSize={previewPanel}
- minSize={30}
- onResize={setPreviewPanel}
- className="min-w-0 min-h-0 flex-1"
- >
- <PreviewPanel />
- </ResizablePanel>
-
- <ResizableHandle withHandle />
-
- <ResizablePanel
- defaultSize={propertiesPanel}
- minSize={15}
- maxSize={40}
- onResize={setPropertiesPanel}
- className="min-w-0"
- >
- <PropertiesPanel />
- </ResizablePanel>
- </ResizablePanelGroup>
- </ResizablePanel>
-
- <ResizableHandle withHandle />
-
- <ResizablePanel
- defaultSize={timeline}
- minSize={15}
- maxSize={70}
- onResize={setTimeline}
- className="min-h-0"
- >
- <Timeline />
- </ResizablePanel>
- </ResizablePanelGroup>
- </ResizablePanel>
- </ResizablePanelGroup>
- ) : activePreset === "inspector" ? (
- <ResizablePanelGroup
- key={`inspector-${activePreset}-${resetCounter}`}
- direction="horizontal"
- className="h-full w-full gap-[0.18rem] px-3 pb-3"
- >
- <ResizablePanel
- defaultSize={100 - propertiesPanel}
- minSize={30}
- onResize={(size) => setPropertiesPanel(100 - size)}
- className="min-w-0 min-h-0"
- >
- <ResizablePanelGroup
- direction="vertical"
- className="h-full w-full gap-[0.18rem]"
- >
- <ResizablePanel
- defaultSize={mainContent}
- minSize={30}
- maxSize={85}
- onResize={setMainContent}
- className="min-h-0"
- >
- <ResizablePanelGroup
- direction="horizontal"
- className="h-full w-full gap-[0.19rem]"
- >
- <ResizablePanel
- defaultSize={toolsPanel}
- minSize={15}
- maxSize={40}
- onResize={setToolsPanel}
- className="min-w-0 rounded-sm"
- >
- <MediaPanel />
- </ResizablePanel>
-
- <ResizableHandle withHandle />
-
- <ResizablePanel
- defaultSize={previewPanel}
- minSize={30}
- onResize={setPreviewPanel}
- className="min-w-0 min-h-0 flex-1"
- >
- <PreviewPanel />
- </ResizablePanel>
- </ResizablePanelGroup>
- </ResizablePanel>
-
- <ResizableHandle withHandle />
-
- <ResizablePanel
- defaultSize={timeline}
- minSize={15}
- maxSize={70}
- onResize={setTimeline}
- className="min-h-0"
- >
- <Timeline />
- </ResizablePanel>
- </ResizablePanelGroup>
- </ResizablePanel>
-
- <ResizableHandle withHandle />
-
- <ResizablePanel
- defaultSize={propertiesPanel}
- minSize={15}
- maxSize={40}
- onResize={setPropertiesPanel}
- className="min-w-0 min-h-0"
- >
- <PropertiesPanel />
- </ResizablePanel>
- </ResizablePanelGroup>
- ) : activePreset === "vertical-preview" ? (
- <ResizablePanelGroup
- key={`vertical-preview-${activePreset}-${resetCounter}`}
- direction="horizontal"
- className="h-full w-full gap-[0.18rem] px-3 pb-3"
- >
- <ResizablePanel
- defaultSize={100 - previewPanel}
- minSize={30}
- onResize={(size) => setPreviewPanel(100 - size)}
- className="min-w-0 min-h-0"
- >
- <ResizablePanelGroup
- direction="vertical"
- className="h-full w-full gap-[0.18rem]"
- >
- <ResizablePanel
- defaultSize={mainContent}
- minSize={30}
- maxSize={85}
- onResize={setMainContent}
- className="min-h-0"
- >
- <ResizablePanelGroup
- direction="horizontal"
- className="h-full w-full gap-[0.19rem]"
- >
- <ResizablePanel
- defaultSize={toolsPanel}
- minSize={15}
- maxSize={40}
- onResize={setToolsPanel}
- className="min-w-0 rounded-sm"
- >
- <MediaPanel />
- </ResizablePanel>
-
- <ResizableHandle withHandle />
-
- <ResizablePanel
- defaultSize={propertiesPanel}
- minSize={15}
- maxSize={40}
- onResize={setPropertiesPanel}
- className="min-w-0"
- >
- <PropertiesPanel />
- </ResizablePanel>
- </ResizablePanelGroup>
- </ResizablePanel>
-
- <ResizableHandle withHandle />
-
- <ResizablePanel
- defaultSize={timeline}
- minSize={15}
- maxSize={70}
- onResize={setTimeline}
- className="min-h-0"
- >
- <Timeline />
- </ResizablePanel>
- </ResizablePanelGroup>
- </ResizablePanel>
+ return (
+ <EditorProvider projectId={projectId}>
+ <div className="bg-background flex h-screen w-screen flex-col overflow-hidden">
+ <EditorHeader />
+ <div className="min-h-0 min-w-0 flex-1">
+ <EditorLayout />
+ </div>
+ <Onboarding />
+ <MigrationDialog />
+ </div>
+ </EditorProvider>
+ );
+}
- <ResizableHandle withHandle />
+function EditorLayout() {
+ const { panels, setPanel } = usePanelStore();
- <ResizablePanel
- defaultSize={previewPanel}
- minSize={30}
- onResize={setPreviewPanel}
- className="min-w-0 min-h-0"
- >
- <PreviewPanel />
- </ResizablePanel>
- </ResizablePanelGroup>
- ) : (
- <ResizablePanelGroup
- key={`default-${activePreset}-${resetCounter}`}
- direction="vertical"
- className="h-full w-full gap-[0.18rem]"
- >
- <ResizablePanel
- defaultSize={mainContent}
- minSize={30}
- maxSize={85}
- onResize={setMainContent}
- className="min-h-0"
- >
- {/* Main content area */}
- <ResizablePanelGroup
- direction="horizontal"
- className="h-full w-full gap-[0.19rem] px-3"
- >
- {/* Tools Panel */}
- <ResizablePanel
- defaultSize={toolsPanel}
- minSize={15}
- maxSize={40}
- onResize={setToolsPanel}
- className="min-w-0 rounded-sm"
- >
- <MediaPanel />
- </ResizablePanel>
+ return (
+ <ResizablePanelGroup
+ direction="vertical"
+ className="size-full gap-[0.18rem]"
+ onLayout={(sizes) => {
+ setPanel("mainContent", sizes[0] ?? panels.mainContent);
+ setPanel("timeline", sizes[1] ?? panels.timeline);
+ }}
+ >
+ <ResizablePanel
+ defaultSize={panels.mainContent}
+ minSize={30}
+ maxSize={85}
+ className="min-h-0"
+ >
+ <ResizablePanelGroup
+ direction="horizontal"
+ className="size-full gap-[0.19rem] px-3"
+ onLayout={(sizes) => {
+ setPanel("tools", sizes[0] ?? panels.tools);
+ setPanel("preview", sizes[1] ?? panels.preview);
+ setPanel("properties", sizes[2] ?? panels.properties);
+ }}
+ >
+ <ResizablePanel
+ defaultSize={panels.tools}
+ minSize={15}
+ maxSize={40}
+ className="min-w-0 rounded-sm"
+ >
+ <AssetsPanel />
+ </ResizablePanel>
- <ResizableHandle withHandle />
+ <ResizableHandle withHandle />
- {/* Preview Area */}
- <ResizablePanel
- defaultSize={previewPanel}
- minSize={30}
- onResize={setPreviewPanel}
- className="min-w-0 min-h-0 flex-1"
- >
- <PreviewPanel />
- </ResizablePanel>
+ <ResizablePanel
+ defaultSize={panels.preview}
+ minSize={30}
+ className="min-h-0 min-w-0 flex-1"
+ >
+ <PreviewPanel />
+ </ResizablePanel>
- <ResizableHandle withHandle />
+ <ResizableHandle withHandle />
- <ResizablePanel
- defaultSize={propertiesPanel}
- minSize={15}
- maxSize={40}
- onResize={setPropertiesPanel}
- className="min-w-0 rounded-sm"
- >
- <PropertiesPanel />
- </ResizablePanel>
- </ResizablePanelGroup>
- </ResizablePanel>
+ <ResizablePanel
+ defaultSize={panels.properties}
+ minSize={15}
+ maxSize={40}
+ className="min-w-0 rounded-sm"
+ >
+ <PropertiesPanel />
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ </ResizablePanel>
- <ResizableHandle withHandle />
+ <ResizableHandle withHandle />
- {/* Timeline */}
- <ResizablePanel
- defaultSize={timeline}
- minSize={15}
- maxSize={70}
- onResize={setTimeline}
- className="min-h-0 px-3 pb-3"
- >
- <Timeline />
- </ResizablePanel>
- </ResizablePanelGroup>
- )}
- </div>
- <Onboarding />
- </div>
- </EditorProvider>
- );
+ <ResizablePanel
+ defaultSize={panels.timeline}
+ minSize={15}
+ maxSize={70}
+ className="min-h-0 px-3 pb-3"
+ >
+ <Timeline />
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ );
}
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index ca78ee26..d0da1f51 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -8,85 +8,88 @@
@plugin "tailwindcss-animate";
:root {
- /* Custom colors - light mode (default) */
- --background: hsl(0, 0%, 100%);
- --foreground: hsl(0 0% 11%);
- --card: hsl(216, 8%, 86%);
- --card-foreground: hsl(0 0% 2%);
- --popover: hsl(0, 0%, 100%);
- --popover-foreground: hsl(0 0% 2%);
- --primary: hsl(205, 84%, 47%);
- --primary-foreground: hsl(0 0% 91%);
- --secondary: hsl(216, 13%, 92%);
- --secondary-foreground: hsl(0 0% 2%);
- --muted: hsl(0 0% 85.1%);
- --muted-foreground: hsl(0 0% 50%);
- --accent: hsl(216, 13%, 92%);
- --accent-foreground: hsl(0 0% 2%);
- --destructive: hsl(0, 83%, 50%);
- --destructive-foreground: hsl(0, 0%, 100%);
- --border: hsl(0 0% 83%);
- --input: hsl(0 0% 85.1%);
- --ring: hsl(0, 0%, 55%);
- --chart-1: hsl(220 70% 50%);
- --chart-2: hsl(160 60% 45%);
- --chart-3: hsl(30 80% 55%);
- --chart-4: hsl(280 65% 60%);
- --chart-5: hsl(340 75% 55%);
- --sidebar-background: hsl(0 0% 96.1%);
- --sidebar-foreground: hsl(0 0% 2%);
- --sidebar-primary: hsl(0 0% 2%);
- --sidebar-primary-foreground: hsl(0 0% 91%);
- --sidebar-accent: hsl(0 0% 85.1%);
- --sidebar-accent-foreground: hsl(0 0% 2%);
- --sidebar-border: hsl(0 0% 85.1%);
- --sidebar-ring: hsl(0 0% 16.9%);
- --panel-background: hsl(216 13% 92%);
- --panel-accent: hsl(216, 8%, 86%);
-
- /* Radius base */
- --radius: 1rem;
+ --background: hsl(0, 0%, 100%);
+ --foreground: hsl(0 0% 11%);
+ --card: hsl(0, 0%, 100%);
+ --card-foreground: hsl(0 0% 11%);
+ --popover: hsl(0, 0%, 100%);
+ --popover-foreground: hsl(0 0% 2%);
+ --primary: hsl(203, 100%, 50%);
+ --primary-hover: hsl(203, 100%, 45%);
+ --primary-foreground: hsl(0, 0%, 100%);
+ --secondary: hsl(216 13% 94%);
+ --secondary-foreground: hsl(0 0% 2%);
+ --muted: hsl(0 0% 85.1%);
+ --muted-foreground: hsl(0 0% 50%);
+ --accent: hsl(216, 13%, 88%);
+ --accent-foreground: hsl(0 0% 2%);
+ --destructive: hsl(0, 83%, 50%);
+ --destructive-foreground: hsl(0, 0%, 100%);
+ --constructive: hsl(141, 71%, 48%);
+ --constructive-foreground: hsl(0, 0%, 100%);
+ --border: hsl(0 0% 88%);
+ --input: hsl(0 0% 85.1%);
+ --ring: hsl(0, 0%, 55%);
+ --chart-1: hsl(220 70% 50%);
+ --chart-2: hsl(160 60% 45%);
+ --chart-3: hsl(30 80% 55%);
+ --chart-4: hsl(280 65% 60%);
+ --chart-5: hsl(340 75% 55%);
+ --sidebar-background: hsl(0 0% 96.1%);
+ --sidebar-foreground: hsl(0 0% 2%);
+ --sidebar-primary: hsl(0 0% 2%);
+ --sidebar-primary-foreground: hsl(0 0% 91%);
+ --sidebar-accent: hsl(0 0% 85.1%);
+ --sidebar-accent-foreground: hsl(0 0% 2%);
+ --sidebar-border: hsl(0 0% 85.1%);
+ --sidebar-ring: hsl(0 0% 16.9%);
+ --panel-background: hsl(216 13% 94%);
+ --panel-accent: hsl(216, 8%, 88%);
+ --sidebar: hsl(0 0% 98%);
}
.dark {
- /* Custom colors - dark mode */
- --background: hsl(0 0% 4%);
- --foreground: hsl(0 0% 89%);
- --card: hsl(0 0% 14.9%);
- --card-foreground: hsl(0 0% 98%);
- --popover: hsl(0 0% 14.9%);
- --popover-foreground: hsl(0 0% 98%);
- --primary: hsl(205, 84%, 53%);
- --primary-foreground: hsl(0 0% 9%);
- --secondary: hsl(0 0% 14.9%);
- --secondary-foreground: hsl(0 0% 98%);
- --muted: hsl(0 0% 14.9%);
- --muted-foreground: hsl(0 0% 63.9%);
- --accent: hsl(0 0% 14.9%);
- --accent-foreground: hsl(0 0% 98%);
- --destructive: hsl(0 100% 60%);
- --destructive-foreground: hsl(0 0% 98%);
- --border: hsl(0 0% 17%);
- --input: hsl(0 0% 14.9%);
- --ring: hsl(0 0% 83.1%);
- --chart-1: hsl(220 70% 50%);
- --chart-2: hsl(160 60% 45%);
- --chart-3: hsl(30 80% 55%);
- --chart-4: hsl(280 65% 60%);
- --chart-5: hsl(340 75% 55%);
- --sidebar-background: hsl(0 0% 3.9%);
- --sidebar-foreground: hsl(0 0% 98%);
- --sidebar-primary: hsl(0 0% 98%);
- --sidebar-primary-foreground: hsl(0 0% 9%);
- --sidebar-accent: hsl(0 0% 14.9%);
- --sidebar-accent-foreground: hsl(0 0% 98%);
- --sidebar-border: hsl(0 0% 14.9%);
- --sidebar-ring: hsl(0 0% 83.1%);
- --panel-background: hsl(0 0% 11%);
- --panel-accent: hsl(0 0% 15%);
+ --background: hsl(0 0% 4%);
+ --foreground: hsl(0 0% 89%);
+ --card: hsl(0 0% 4%);
+ --card-foreground: hsl(0 0% 89%);
+ --popover: hsl(0 0% 14.9%);
+ --popover-foreground: hsl(0 0% 98%);
+ --primary: hsl(203, 100%, 50%);
+ --primary-hover: hsl(203, 100%, 45%);
+ --primary-foreground: hsl(0 0% 9%);
+ --secondary: hsl(0 0% 14.9%);
+ --secondary-foreground: hsl(0 0% 98%);
+ --muted: hsl(0 0% 14.9%);
+ --muted-foreground: hsl(0 0% 63.9%);
+ --accent: hsl(0, 0%, 28%);
+ --accent-foreground: hsl(0 0% 98%);
+ --destructive: hsl(0 83%, 55%);
+ --destructive-foreground: hsl(0 0% 98%);
+ --constructive: hsl(141, 71%, 48%);
+ --constructive-foreground: hsl(0 0% 100%);
+ --border: hsl(0 0% 17%);
+ --input: hsl(0 0% 14.9%);
+ --ring: hsl(0 0% 83.1%);
+ --chart-1: hsl(220 70% 50%);
+ --chart-2: hsl(160 60% 45%);
+ --chart-3: hsl(30 80% 55%);
+ --chart-4: hsl(280 65% 60%);
+ --chart-5: hsl(340 75% 55%);
+ --sidebar-background: hsl(0 0% 3.9%);
+ --sidebar-foreground: hsl(0 0% 98%);
+ --sidebar-primary: hsl(0 0% 98%);
+ --sidebar-primary-foreground: hsl(0 0% 9%);
+ --sidebar-accent: hsl(0 0% 14.9%);
+ --sidebar-accent-foreground: hsl(0 0% 98%);
+ --sidebar-border: hsl(0 0% 14.9%);
+ --sidebar-ring: hsl(0 0% 83.1%);
+ --panel-background: hsl(0 0% 11%);
+ --panel-accent: hsl(0 0% 15%);
+ --sidebar: hsl(240 5.9% 10%);
}
@layer base {
- /*
+ /*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
@@ -94,152 +97,165 @@
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
- *,
- ::after,
- ::before,
- ::backdrop,
- ::file-selector-button {
- border-color: var(--color-gray-200, currentcolor);
- }
- /* Other default base styles */
- * {
- @apply border-border;
- }
- body {
- @apply bg-background text-foreground;
- /* Prevent back/forward swipe */
- overscroll-behavior-x: contain;
- }
+ *,
+ ::after,
+ ::before,
+ ::backdrop,
+ ::file-selector-button {
+ border-color: var(--color-gray-200, currentcolor);
+ }
+ /* Other default base styles */
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ /* Prevent back/forward swipe */
+ overscroll-behavior-x: contain;
+ }
}
@theme inline {
- /* Responsive breakpoints */
- --breakpoint-xs: 30rem;
+ /* Responsive breakpoints */
+ --breakpoint-xs: 30rem;
- /* Typography */
- --font-sans: var(--font-inter), sans-serif;
+ /* Typography */
+ --font-sans: var(--font-inter), sans-serif;
- /* Font sizes */
- --text-base: 0.95rem;
- --text-base--line-height: calc(1.5 / 0.95);
- --text-xs: 0.8rem;
- --text-xs--line-height: calc(1 / 0.8);
+ /* Font sizes */
+ --text-xl: 1.20rem;
+ --text-base: 0.92rem;
+ --text-base--line-height: calc(1.5 / 0.95);
+ --text-xs: 0.75rem;
+ --text-xs--line-height: calc(1 / 0.8);
- /* Border radius */
- --radius-lg: var(--radius);
- --radius-md: calc(var(--radius) - 2px);
- --radius-sm: calc(var(--radius) - 8px);
+ /* Border radius */
+ --radius-lg: 0.82rem;
+ --radius-md: 0.65rem;
+ --radius-sm: 0.35rem;
- /* Palette mapped to root design tokens */
- --color-background: var(--background);
- --color-foreground: var(--foreground);
+ /* Palette mapped to root design tokens */
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
- --color-card: var(--card);
- --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
- --color-popover: var(--popover);
- --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
- --color-primary: var(--primary);
- --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary-hover: var(--primary-hover);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
- --color-secondary: var(--secondary);
- --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
- --color-muted: var(--muted);
- --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
- --color-accent: var(--accent);
- --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
- --color-destructive: var(--destructive);
- --color-destructive-foreground: var(--destructive-foreground);
+ --color-constructive: var(--constructive);
+ --color-constructive-foreground: var(--constructive-foreground);
- --color-border: var(--border);
- --color-input: var(--input);
- --color-ring: var(--ring);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
- /* Chart colors */
- --color-chart-1: var(--chart-1);
- --color-chart-2: var(--chart-2);
- --color-chart-3: var(--chart-3);
- --color-chart-4: var(--chart-4);
- --color-chart-5: var(--chart-5);
+ /* Chart colors */
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
- /* Sidebar */
- --color-sidebar: var(--sidebar-background);
- --color-sidebar-foreground: var(--sidebar-foreground);
- --color-sidebar-primary: var(--sidebar-primary);
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
- --color-sidebar-accent: var(--sidebar-accent);
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
- --color-sidebar-border: var(--sidebar-border);
- --color-sidebar-ring: var(--sidebar-ring);
+ /* Sidebar */
+ --color-sidebar: var(--sidebar-background);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
- /* Panel */
- --color-panel: var(--panel-background);
- --color-panel-accent: var(--panel-accent);
+ /* Panel */
+ --color-panel: var(--panel-background);
+ --color-panel-accent: var(--panel-accent);
- /* Animations */
- --animate-accordion-down: accordion-down 0.2s ease-out;
- --animate-accordion-up: accordion-up 0.2s ease-out;
+ /* Animations */
+ --animate-accordion-down: accordion-down 0.2s ease-out;
+ --animate-accordion-up: accordion-up 0.2s ease-out;
- @keyframes accordion-down {
- from {
- height: 0;
- }
- to {
- height: var(--radix-accordion-content-height);
- }
- }
+ @keyframes accordion-down {
+ from {
+ height: 0;
+ }
+ to {
+ height: var(--radix-accordion-content-height);
+ }
+ }
- @keyframes accordion-up {
- from {
- height: var(--radix-accordion-content-height);
- }
- to {
- height: 0;
- }
- }
+ @keyframes accordion-up {
+ from {
+ height: var(--radix-accordion-content-height);
+ }
+ to {
+ height: 0;
+ }
+ }
}
@utility scrollbar-hidden {
- -ms-overflow-style: none;
- scrollbar-width: none;
- &::-webkit-scrollbar {
- display: none;
- }
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
}
@utility scrollbar-x-hidden {
- -ms-overflow-style: none;
- scrollbar-width: none;
- &::-webkit-scrollbar:horizontal {
- display: none;
- }
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ &::-webkit-scrollbar:horizontal {
+ display: none;
+ }
}
@utility scrollbar-y-hidden {
- -ms-overflow-style: none;
- scrollbar-width: none;
- &::-webkit-scrollbar:vertical {
- display: none;
- }
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ &::-webkit-scrollbar:vertical {
+ display: none;
+ }
}
@utility scrollbar-thin {
- &::-webkit-scrollbar {
- width: 6px;
- height: 8px;
- }
- &::-webkit-scrollbar-track {
- background: transparent;
- }
- &::-webkit-scrollbar-thumb {
- background: var(--border);
- border-radius: 4px;
- }
- &::-webkit-scrollbar-thumb:hover {
- background: var(--muted-foreground);
- }
+ &::-webkit-scrollbar {
+ width: 6px;
+ height: 8px;
+ }
+ &::-webkit-scrollbar-track {
+ background: transparent;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: 4px;
+ }
+ &::-webkit-scrollbar-thumb:hover {
+ background: var(--muted-foreground);
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
}
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 7e32ea22..2b3d0c30 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -1,62 +1,58 @@
import { ThemeProvider } from "next-themes";
-import { Analytics } from "@vercel/analytics/react";
import Script from "next/script";
import "./globals.css";
import { Toaster } from "../components/ui/sonner";
import { TooltipProvider } from "../components/ui/tooltip";
-import { StorageProvider } from "../components/storage-provider";
-import { ScenesMigrator } from "../components/providers/migrators/scenes-migrator";
import { baseMetaData } from "./metadata";
-import { defaultFont } from "../lib/font-config";
import { BotIdClient } from "botid/client";
-import { env } from "@/env";
+import { webEnv } from "@opencut/env/web";
+import { Inter } from "next/font/google";
+
+const siteFont = Inter({ subsets: ["latin"] });
export const metadata = baseMetaData;
const protectedRoutes = [
- {
- path: "/none",
- method: "GET",
- },
- {
- path: "/api/waitlist/export",
- method: "POST",
- },
+ {
+ path: "/none",
+ method: "GET",
+ },
];
export default function RootLayout({
- children,
+ children,
}: Readonly<{
- children: React.ReactNode;
+ children: React.ReactNode;
}>) {
- return (
- <html lang="en" suppressHydrationWarning>
- <head>
- <BotIdClient protect={protectedRoutes} />
- </head>
- <body className={`${defaultFont.className} font-sans antialiased`}>
- <ThemeProvider attribute="class" defaultTheme="dark">
- <TooltipProvider>
- <StorageProvider>
- <ScenesMigrator>{children}</ScenesMigrator>
- </StorageProvider>
- <Analytics />
- <Toaster />
- <Script
- src="https://cdn.databuddy.cc/databuddy.js"
- strategy="afterInteractive"
- async
- data-client-id="UP-Wcoy5arxFeK7oyjMMZ"
- data-disabled={env.NODE_ENV === "development"}
- data-track-attributes={false}
- data-track-errors={true}
- data-track-outgoing-links={false}
- data-track-web-vitals={false}
- data-track-sessions={false}
- />
- </TooltipProvider>
- </ThemeProvider>
- </body>
- </html>
- );
+ return (
+ <html lang="en" suppressHydrationWarning>
+ <head>
+ <BotIdClient protect={protectedRoutes} />
+ </head>
+ <body className={`${siteFont.className} font-sans antialiased`}>
+ <ThemeProvider
+ attribute="class"
+ defaultTheme="system"
+ disableTransitionOnChange={true}
+ >
+ <TooltipProvider>
+ <Toaster />
+ <Script
+ src="https://cdn.databuddy.cc/databuddy.js"
+ strategy="afterInteractive"
+ async
+ data-client-id="UP-Wcoy5arxFeK7oyjMMZ"
+ data-disabled={webEnv.NODE_ENV === "development"}
+ data-track-attributes={false}
+ data-track-errors={true}
+ data-track-outgoing-links={false}
+ data-track-web-vitals={false}
+ data-track-sessions={false}
+ />
+ {children}
+ </TooltipProvider>
+ </ThemeProvider>
+ </body>
+ </html>
+ );
}
diff --git a/apps/web/src/app/metadata.ts b/apps/web/src/app/metadata.ts
index 0608974d..7c192d2b 100644
--- a/apps/web/src/app/metadata.ts
+++ b/apps/web/src/app/metadata.ts
@@ -1,86 +1,86 @@
import type { Metadata } from "next";
-import { SITE_INFO, SITE_URL } from "@/constants/site";
+import { SITE_INFO, SITE_URL } from "@/constants/site-constants";
export const baseMetaData: Metadata = {
- metadataBase: new URL(SITE_URL),
- title: SITE_INFO.title,
- description: SITE_INFO.description,
- openGraph: {
- title: SITE_INFO.title,
- description: SITE_INFO.description,
- url: SITE_URL,
- siteName: SITE_INFO.title,
- locale: "en_US",
- type: "website",
- images: [
- {
- url: SITE_INFO.openGraphImage,
- width: 1200,
- height: 630,
- alt: "OpenCut Wordmark",
- },
- ],
- },
- twitter: {
- card: "summary_large_image",
- title: SITE_INFO.title,
- description: SITE_INFO.description,
- creator: "@opencutapp",
- images: [SITE_INFO.twitterImage],
- },
- pinterest: {
- richPin: false,
- },
- robots: {
- index: true,
- follow: true,
- },
- icons: {
- icon: [
- { url: "/favicon.ico" },
- { url: "/icons/favicon-16x16.png", sizes: "16x16", type: "image/png" },
- { url: "/icons/favicon-32x32.png", sizes: "32x32", type: "image/png" },
- { url: "/icons/favicon-96x96.png", sizes: "96x96", type: "image/png" },
- ],
- apple: [
- { url: "/icons/apple-icon-57x57.png", sizes: "57x57", type: "image/png" },
- { url: "/icons/apple-icon-60x60.png", sizes: "60x60", type: "image/png" },
- { url: "/icons/apple-icon-72x72.png", sizes: "72x72", type: "image/png" },
- { url: "/icons/apple-icon-76x76.png", sizes: "76x76", type: "image/png" },
- {
- url: "/icons/apple-icon-114x114.png",
- sizes: "114x114",
- type: "image/png",
- },
- {
- url: "/icons/apple-icon-120x120.png",
- sizes: "120x120",
- type: "image/png",
- },
- {
- url: "/icons/apple-icon-144x144.png",
- sizes: "144x144",
- type: "image/png",
- },
- {
- url: "/icons/apple-icon-152x152.png",
- sizes: "152x152",
- type: "image/png",
- },
- {
- url: "/icons/apple-icon-180x180.png",
- sizes: "180x180",
- type: "image/png",
- },
- ],
- shortcut: ["/favicon.ico"],
- },
- appleWebApp: {
- capable: true,
- title: SITE_INFO.title,
- },
- manifest: "/manifest.json",
- other: {
- "msapplication-config": "/browserconfig.xml",
- },
+ metadataBase: new URL(SITE_URL),
+ title: SITE_INFO.title,
+ description: SITE_INFO.description,
+ openGraph: {
+ title: SITE_INFO.title,
+ description: SITE_INFO.description,
+ url: SITE_URL,
+ siteName: SITE_INFO.title,
+ locale: "en_US",
+ type: "website",
+ images: [
+ {
+ url: SITE_INFO.openGraphImage,
+ width: 1200,
+ height: 630,
+ alt: "OpenCut Wordmark",
+ },
+ ],
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: SITE_INFO.title,
+ description: SITE_INFO.description,
+ creator: "@opencutapp",
+ images: [SITE_INFO.twitterImage],
+ },
+ pinterest: {
+ richPin: false,
+ },
+ robots: {
+ index: true,
+ follow: true,
+ },
+ icons: {
+ icon: [
+ { url: "/favicon.ico" },
+ { url: "/icons/favicon-16x16.png", sizes: "16x16", type: "image/png" },
+ { url: "/icons/favicon-32x32.png", sizes: "32x32", type: "image/png" },
+ { url: "/icons/favicon-96x96.png", sizes: "96x96", type: "image/png" },
+ ],
+ apple: [
+ { url: "/icons/apple-icon-57x57.png", sizes: "57x57", type: "image/png" },
+ { url: "/icons/apple-icon-60x60.png", sizes: "60x60", type: "image/png" },
+ { url: "/icons/apple-icon-72x72.png", sizes: "72x72", type: "image/png" },
+ { url: "/icons/apple-icon-76x76.png", sizes: "76x76", type: "image/png" },
+ {
+ url: "/icons/apple-icon-114x114.png",
+ sizes: "114x114",
+ type: "image/png",
+ },
+ {
+ url: "/icons/apple-icon-120x120.png",
+ sizes: "120x120",
+ type: "image/png",
+ },
+ {
+ url: "/icons/apple-icon-144x144.png",
+ sizes: "144x144",
+ type: "image/png",
+ },
+ {
+ url: "/icons/apple-icon-152x152.png",
+ sizes: "152x152",
+ type: "image/png",
+ },
+ {
+ url: "/icons/apple-icon-180x180.png",
+ sizes: "180x180",
+ type: "image/png",
+ },
+ ],
+ shortcut: ["/favicon.ico"],
+ },
+ appleWebApp: {
+ capable: true,
+ title: SITE_INFO.title,
+ },
+ manifest: "/manifest.json",
+ other: {
+ "msapplication-config": "/browserconfig.xml",
+ },
};
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index 818884ed..d97c9a54 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -2,20 +2,20 @@ 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 "@/constants/site";
+import { SITE_URL } from "@/constants/site-constants";
export const metadata: Metadata = {
- alternates: {
- canonical: SITE_URL,
- },
+ alternates: {
+ canonical: SITE_URL,
+ },
};
export default async function Home() {
- return (
- <div>
- <Header />
- <Hero />
- <Footer />
- </div>
- );
+ return (
+ <div>
+ <Header />
+ <Hero />
+ <Footer />
+ </div>
+ );
}
diff --git a/apps/web/src/app/privacy/page.tsx b/apps/web/src/app/privacy/page.tsx
index 4bb1e023..b71b8d30 100644
--- a/apps/web/src/app/privacy/page.tsx
+++ b/apps/web/src/app/privacy/page.tsx
@@ -1,253 +1,287 @@
-import { Metadata } from "next";
-import { Card, CardContent } from "@/components/ui/card";
-import { Badge } from "@/components/ui/badge";
-import { GithubIcon } from "@/components/icons";
-import Link from "next/link";
-import { Footer } from "@/components/footer";
-import { Header } from "@/components/header";
+import type { Metadata } from "next";
+import { BasePage } from "@/app/base-page";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import { Separator } from "@/components/ui/separator";
+import { SOCIAL_LINKS } from "@/constants/site-constants";
export const metadata: Metadata = {
- title: "Privacy Policy - OpenCut",
- description:
- "Learn how OpenCut handles your data and privacy. Our commitment to protecting your information while you edit videos.",
- openGraph: {
- title: "Privacy Policy - OpenCut",
- description:
- "Learn how OpenCut handles your data and privacy. Our commitment to protecting your information while you edit videos.",
- type: "website",
- },
+ title: "Privacy Policy - OpenCut",
+ description:
+ "Learn how OpenCut handles your data and privacy. Our commitment to protecting your information while you edit videos.",
+ openGraph: {
+ title: "Privacy Policy - OpenCut",
+ description:
+ "Learn how OpenCut handles your data and privacy. Our commitment to protecting your information while you edit videos.",
+ type: "website",
+ },
};
export default function PrivacyPage() {
- return (
- <div className="min-h-screen bg-background">
- <Header />
- <main className="relative">
- <div className="absolute inset-0 overflow-hidden pointer-events-none">
- <div className="absolute -top-40 -right-40 w-96 h-96 bg-linear-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
- <div className="absolute top-1/2 -left-40 w-80 h-80 bg-linear-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
- </div>
- <div className="relative container mx-auto px-4 py-16">
- <div className="max-w-4xl mx-auto">
- <div className="text-center mb-10">
- <Link
- href="https://github.com/OpenCut-app/OpenCut"
- target="_blank"
- >
- <Badge variant="secondary" className="gap-2 mb-6">
- <GithubIcon className="h-3 w-3" />
- Open Source
- </Badge>
- </Link>
- <h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
- Privacy Policy
- </h1>
- <p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
- Learn how we handle your data and privacy. Contact us if you
- have any questions.
- </p>
- </div>
- <Card className="bg-background/80 backdrop-blur-xs border-2 border-muted/30">
- <CardContent className="p-8 text-base leading-relaxed space-y-8">
- <section>
- <h2 className="text-2xl font-semibold mb-4">
- Your Videos Stay Private
- </h2>
- <p className="mb-4">
- <strong>
- OpenCut processes all videos locally on your device.
- </strong>{" "}
- We never upload, store, or have access to your video files.
- Your content remains completely private and under your
- control at all times.
- </p>
- <p>
- All video editing, rendering, and processing happens in your
- browser using WebAssembly and local storage. No video data
- is transmitted to our servers.
- </p>
- </section>
+ return (
+ <BasePage
+ title="Privacy policy"
+ description="Learn how we handle your data and privacy. Contact us if you have any questions."
+ >
+ <Accordion type="single" collapsible className="w-full">
+ <AccordionItem
+ value="quick-summary"
+ className="rounded-2xl border px-5"
+ >
+ <AccordionTrigger className="no-underline!">
+ Quick summary
+ </AccordionTrigger>
+ <AccordionContent>
+ <h3 className="mb-3 text-lg font-medium">
+ Your content stays private and encrypted.
+ </h3>
+ <ol className="list-decimal space-y-2 pl-6">
+ <li>
+ Basic editing happens locally in your browser - we never see
+ your files
+ </li>
+ <li>
+ AI features require encrypted uploads - your content is
+ encrypted before leaving your device
+ </li>
+ <li>
+ We only collect your email and basic profile info for your
+ account
+ </li>
+ <li>Project data stays on your device, not our servers</li>
+ <li>
+ We use analytics to improve the app, but no personal video
+ content is tracked
+ </li>
+ <li>
+ You can delete your account anytime and all data gets removed
+ </li>
+ <li>We don't sell your data or share it with advertisers</li>
+ </ol>
+ <p className="mt-4">
+ Questions? Email us at{" "}
+ <a
+ href="mailto:oss@opencut.app"
+ className="text-primary hover:underline"
+ >
+ oss@opencut.app
+ </a>
+ </p>
+ </AccordionContent>
+ </AccordionItem>
+ </Accordion>
- <section>
- <h2 className="text-2xl font-semibold mb-4">
- Account Information
- </h2>
- <p className="mb-4">
- When you create an account, we only collect:
- </p>
- <ul className="list-disc pl-6 mb-4 space-y-2">
- <li>Email address (for account access)</li>
- <li>
- Profile information from Google OAuth (if you choose to
- sign in with Google)
- </li>
- </ul>
- <p className="mb-4">
- <strong>
- We do NOT store your projects on our servers.
- </strong>{" "}
- All project data, including names, thumbnails, and creation
- dates, is stored locally in your browser using IndexedDB.
- </p>
- <p>
- We use{" "}
- <a
- href="https://www.better-auth.com"
- target="_blank"
- rel="noopener"
- className="text-primary hover:underline"
- >
- Better Auth
- </a>{" "}
- for secure authentication and follow industry-standard
- security practices.
- </p>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">How We Handle Your Content</h2>
+ <p>
+ <strong>Basic video editing happens locally on your device.</strong>{" "}
+ For standard editing features, we never upload, store, or have access
+ to your video files. Your content remains completely private and under
+ your control.
+ </p>
+ <p>
+ <strong>AI features require secure processing:</strong> When you
+ choose to use AI features like auto captions, your audio/video content
+ is encrypted on your device before being uploaded to our servers for
+ processing. We use zero-knowledge encryption, meaning we cannot
+ decrypt or view your content.
+ </p>
+ <p>
+ After AI processing is complete, the encrypted content is immediately
+ deleted from our servers. Only the results (like generated captions)
+ are returned to your device.
+ </p>
+ </section>
- <section>
- <h2 className="text-2xl font-semibold mb-4">Analytics</h2>
- <p className="mb-4">
- We use{" "}
- <a
- href="https://www.databuddy.cc"
- target="_blank"
- rel="noopener"
- className="text-primary hover:underline"
- >
- Databuddy
- </a>{" "}
- for completely anonymized and non-invasive analytics to
- understand how people use OpenCut.
- </p>
- <p>
- This helps us improve the editor, but we never collect
- personal information, track individual users, or store any
- data that could identify you.
- </p>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">Account Information</h2>
+ <p>When you create an account, we only collect:</p>
+ <ul className="list-disc space-y-2 pl-6">
+ <li>Email address (for account access)</li>
+ <li>
+ Profile information from Google OAuth (if you choose to sign in with
+ Google)
+ </li>
+ </ul>
+ <p>
+ <strong>We do NOT store your projects on our servers.</strong> All
+ project data, including names, thumbnails, and creation dates, is
+ stored locally in your browser using IndexedDB.
+ </p>
+ <p>
+ We use{" "}
+ <a
+ href="https://www.better-auth.com"
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ Better Auth
+ </a>{" "}
+ for secure authentication and follow industry-standard security
+ practices.
+ </p>
+ </section>
- <section>
- <h2 className="text-2xl font-semibold mb-4">
- Local Storage & Cookies
- </h2>
- <p className="mb-4">
- We use browser local storage and IndexedDB to:
- </p>
- <ul className="list-disc pl-6 mb-4 space-y-2">
- <li>Save your projects locally on your device</li>
- <li>Remember your editor preferences and settings</li>
- <li>Keep you logged in across browser sessions</li>
- </ul>
- <p>
- All data stays on your device and can be cleared at any time
- through your browser settings.
- </p>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">AI Features & Encryption</h2>
+ <p>
+ When you use AI-powered features (like auto captions, content
+ analysis, or enhancement tools), your content needs to be processed on
+ our servers. Here's how we protect your privacy:
+ </p>
+ <ul className="list-disc space-y-2 pl-6">
+ <li>
+ <strong>Client-side encryption:</strong> Your content is encrypted
+ on your device before upload
+ </li>
+ <li>
+ <strong>Zero-knowledge processing:</strong> We cannot decrypt or
+ view your original content
+ </li>
+ <li>
+ <strong>Temporary processing:</strong> Encrypted content is deleted
+ immediately after processing
+ </li>
+ <li>
+ <strong>Opt-in only:</strong> AI features are optional - basic
+ editing remains fully local
+ </li>
+ </ul>
+ <p>
+ Different AI features may process different types of content (audio
+ for captions, video for analysis, etc.), but all follow the same
+ zero-knowledge encryption approach.
+ </p>
+ </section>
- <section>
- <h2 className="text-2xl font-semibold mb-4">
- Third-Party Services
- </h2>
- <p className="mb-4">
- OpenCut integrates with these services:
- </p>
- <ul className="list-disc pl-6 mb-4 space-y-2">
- <li>
- <strong>Google OAuth:</strong> For optional Google sign-in
- (governed by Google's privacy policy)
- </li>
- <li>
- <strong>Vercel:</strong> For hosting and content delivery
- </li>
- <li>
- <strong>Databuddy:</strong> For anonymized analytics
- </li>
- </ul>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">Analytics</h2>
+ <p>
+ We use{" "}
+ <a
+ href="https://www.databuddy.cc"
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ Databuddy
+ </a>{" "}
+ for completely anonymized and non-invasive analytics to understand how
+ people use OpenCut.
+ </p>
+ <p>
+ This helps us improve the editor, but we never collect personal
+ information, track individual users, or store any data that could
+ identify you.
+ </p>
+ </section>
- <section>
- <h2 className="text-2xl font-semibold mb-4">Your Rights</h2>
- <p className="mb-4">
- You have complete control over your data:
- </p>
- <ul className="list-disc pl-6 mb-4 space-y-2">
- <li>
- Delete your account and all associated data at any time
- </li>
- <li>Export your project data</li>
- <li>Clear local storage to remove all saved projects</li>
- <li>Contact us with any privacy concerns</li>
- </ul>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">Local Storage & Cookies</h2>
+ <p>We use browser local storage and IndexedDB to:</p>
+ <ul className="list-disc space-y-2 pl-6">
+ <li>Save your projects locally on your device</li>
+ <li>Remember your editor preferences and settings</li>
+ <li>Keep you logged in across browser sessions</li>
+ </ul>
+ <p>
+ All data stays on your device and can be cleared at any time through
+ your browser settings.
+ </p>
+ </section>
- <section>
- <h2 className="text-2xl font-semibold mb-4">
- Open Source Transparency
- </h2>
- <p className="mb-4">
- OpenCut is completely open source. You can review our code,
- see exactly how we handle data, and even self-host the
- application if you prefer.
- </p>
- <p>
- View our source code on{" "}
- <a
- href="https://github.com/OpenCut-app/OpenCut"
- target="_blank"
- rel="noopener"
- className="text-primary hover:underline"
- >
- GitHub
- </a>
- .
- </p>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">Third-Party Services</h2>
+ <p>OpenCut integrates with these services:</p>
+ <ul className="list-disc space-y-2 pl-6">
+ <li>
+ <strong>Google OAuth:</strong> For optional Google sign-in (governed
+ by Google's privacy policy)
+ </li>
+ <li>
+ <strong>Vercel:</strong> For hosting and content delivery
+ </li>
+ <li>
+ <strong>Databuddy:</strong> For anonymized analytics
+ </li>
+ </ul>
+ </section>
- <section>
- <h2 className="text-2xl font-semibold mb-4">Contact Us</h2>
- <p className="mb-4">
- Questions about this privacy policy or how we handle your
- data?
- </p>
- <p>
- Open an issue on our{" "}
- <a
- href="https://github.com/OpenCut-app/OpenCut/issues"
- target="_blank"
- rel="noopener"
- className="text-primary hover:underline"
- >
- GitHub repository
- </a>
- , email us at{" "}
- <a
- href="mailto:oss@opencut.app"
- className="text-primary hover:underline"
- >
- oss@opencut.app
- </a>
- , or reach out on{" "}
- <a
- href="https://x.com/opencutapp"
- target="_blank"
- rel="noopener"
- className="text-primary hover:underline"
- >
- X (Twitter)
- </a>
- .
- </p>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">Your Rights</h2>
+ <p>You have complete control over your data:</p>
+ <ul className="list-disc space-y-2 pl-6">
+ <li>Delete your account and all associated data at any time</li>
+ <li>Export your project data</li>
+ <li>Clear local storage to remove all saved projects</li>
+ <li>Contact us with any privacy concerns</li>
+ </ul>
+ </section>
- <p className="text-sm text-muted-foreground mt-8 pt-8 border-t border-muted/20">
- Last updated: July 14, 2025
- </p>
- </CardContent>
- </Card>
- </div>
- </div>
- </main>
- <Footer />
- </div>
- );
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">Open Source Transparency</h2>
+ <p>
+ OpenCut is completely open source. You can review our code, see
+ exactly how we handle data, and even self-host the application if you
+ prefer.
+ </p>
+ <p>
+ View our source code on{" "}
+ <a
+ href={SOCIAL_LINKS.github}
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ GitHub
+ </a>
+ .
+ </p>
+ </section>
+
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">Contact Us</h2>
+ <p>Questions about this privacy policy or how we handle your data?</p>
+ <p>
+ Open an issue on our{" "}
+ <a
+ href={`${SOCIAL_LINKS.github}/issues`}
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ GitHub repository
+ </a>
+ , email us at{" "}
+ <a
+ href="mailto:oss@opencut.app"
+ className="text-primary hover:underline"
+ >
+ oss@opencut.app
+ </a>
+ , or reach out on{" "}
+ <a
+ href={SOCIAL_LINKS.x}
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ X (Twitter)
+ </a>
+ .
+ </p>
+ </section>
+
+ <Separator />
+
+ <p className="text-muted-foreground text-sm">
+ Last updated: July 14, 2025
+ </p>
+ </BasePage>
+ );
}
diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx
index 3d66a3c5..d58c3aac 100644
--- a/apps/web/src/app/projects/page.tsx
+++ b/apps/web/src/app/projects/page.tsx
@@ -1,646 +1,956 @@
"use client";
-import {
- Calendar,
- ChevronLeft,
- Loader2,
- MoreHorizontal,
- ArrowDown01,
- Plus,
- Search,
- Trash2,
- Video,
- X,
-} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
-import { useCallback, useEffect, useState } from "react";
-import { DeleteProjectDialog } from "@/components/delete-project-dialog";
-import { RenameProjectDialog } from "@/components/rename-project-dialog";
+import type { KeyboardEvent, MouseEvent } from "react";
+import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
import { Skeleton } from "@/components/ui/skeleton";
-import { useProjectStore } from "@/stores/project-store";
-import { useTimelineStore } from "@/stores/timeline-store";
-import type { TProject } from "@/types/project";
+import { useEditor } from "@/hooks/use-editor";
+import { useProjectsStore } from "./store";
+import type {
+ TProjectMetadata,
+ TProjectSortKey,
+ TProjectSortOption,
+} from "@/types/project";
+import { formatTimeCode } from "@/lib/time";
+import { formatDate } from "@/utils/date";
+import { HugeiconsIcon } from "@hugeicons/react";
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+} from "@/components/ui/breadcrumb";
+import {
+ Calendar04Icon,
+ GridViewIcon,
+ LeftToRightListDashIcon,
+ PlusSignIcon,
+ Search01Icon,
+ Video01Icon,
+ MoreHorizontalIcon,
+ Delete02Icon,
+ Copy01Icon,
+ Edit03Icon,
+ ArrowDown02Icon,
+ InformationCircleIcon,
+} from "@hugeicons/core-free-icons";
+import { OcVideoIcon } from "@opencut/ui/icons";
+import { Label } from "@/components/ui/label";
+import {
+ DropdownMenu,
+ DropdownMenuCheckboxItem,
+ DropdownMenuContent,
+ 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 { cn } from "@/utils/ui";
+
+const formatProjectDuration = ({
+ duration,
+}: {
+ duration: number | undefined;
+}): string | null => {
+ if (duration === undefined) {
+ return null;
+ }
+
+ const format = duration >= 3600 ? "HH:MM:SS" : "MM:SS";
+ return formatTimeCode({ timeInSeconds: duration, format });
+};
export default function ProjectsPage() {
- const {
- savedProjects,
- isLoading,
- isInitialized,
- deleteProject,
- createNewProject,
- getFilteredAndSortedProjects,
- } = useProjectStore();
- const [thumbnailCache, setThumbnailCache] = useState<
- Record<string, string | null>
- >({});
- const [_loadingThumbnails, setLoadingThumbnails] = useState<Set<string>>(
- new Set()
- );
- const [isSelectionMode, setIsSelectionMode] = useState(false);
- const [selectedProjects, setSelectedProjects] = useState<Set<string>>(
- new Set()
- );
- const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
- const [searchQuery, setSearchQuery] = useState("");
- const [sortOption, setSortOption] = useState("createdAt-desc");
- const router = useRouter();
+ const { searchQuery, sortKey, sortOrder, viewMode } = useProjectsStore();
+ const editor = useEditor();
- const getProjectThumbnail = useCallback(
- async (projectId: string): Promise<string | null> => {
- if (thumbnailCache[projectId] !== undefined) {
- return thumbnailCache[projectId];
- }
+ useEffect(() => {
+ if (!editor.project.getIsInitialized()) {
+ editor.project.loadAllProjects();
+ }
+ }, [editor.project]);
- setLoadingThumbnails((prev) => new Set(prev).add(projectId));
+ const sortOption: TProjectSortOption = `${sortKey}-${sortOrder}`;
+ const projectsToDisplay = editor.project.getFilteredAndSortedProjects({
+ searchQuery,
+ sortOption,
+ });
- try {
- const thumbnail = await useTimelineStore
- .getState()
- .getProjectThumbnail(projectId);
- setThumbnailCache((prev) => ({ ...prev, [projectId]: thumbnail }));
- return thumbnail;
- } finally {
- setLoadingThumbnails((prev) => {
- const newSet = new Set(prev);
- newSet.delete(projectId);
- return newSet;
- });
- }
- },
- []
- );
+ const isLoading = editor.project.getIsLoading();
+ const isInitialized = editor.project.getIsInitialized();
- const handleCreateProject = async () => {
- const projectId = await createNewProject("New Project");
- console.log("projectId", projectId);
- router.push(`/editor/${projectId}`);
- };
+ return (
+ <div className="bg-background min-h-screen">
+ <MigrationDialog />
+ <ProjectsHeader />
+ <ProjectsToolbar projectIds={projectsToDisplay.map((p) => p.id)} />
+ <main className="mx-auto px-4 pt-2 pb-6 flex flex-col gap-4">
+ {isLoading || !isInitialized ? (
+ <ProjectsSkeleton />
+ ) : projectsToDisplay.length === 0 ? (
+ <EmptyState />
+ ) : (
+ <div
+ className={
+ viewMode === "grid"
+ ? "xs:grid-cols-2 grid grid-cols-1 gap-6 sm:grid-cols-3 lg:grid-cols-4 px-4"
+ : "flex flex-col"
+ }
+ >
+ {projectsToDisplay.map((project) => (
+ <ProjectItem
+ key={project.id}
+ project={project}
+ allProjectIds={projectsToDisplay.map((p) => p.id)}
+ />
+ ))}
+ </div>
+ )}
+ </main>
+ </div>
+ );
+}
- const handleSelectProject = (projectId: string, checked: boolean) => {
- const newSelected = new Set(selectedProjects);
- if (checked) {
- newSelected.add(projectId);
- } else {
- newSelected.delete(projectId);
- }
- setSelectedProjects(newSelected);
- };
+function ProjectsHeader() {
+ const { viewMode, isHydrated, setViewMode } = useProjectsStore();
- const handleSelectAll = (checked: boolean) => {
- if (checked) {
- setSelectedProjects(new Set(sortedProjects.map((p) => p.id)));
- } else {
- setSelectedProjects(new Set());
- }
- };
+ return (
+ <header className="sticky top-0 z-20 px-8 bg-background flex flex-col gap-2">
+ <div className="flex items-center justify-between h-16 pt-2">
+ <div className="flex items-center gap-5">
+ <Breadcrumb>
+ <BreadcrumbList>
+ <BreadcrumbItem>
+ <BreadcrumbLink asChild>
+ <Link href="/" className="text-sm sm:text-base">
+ Home
+ </Link>
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+ <BreadcrumbSeparator />
+ <BreadcrumbItem>
+ <BreadcrumbPage className="text-sm sm:text-base font-medium">
+ All projects
+ </BreadcrumbPage>
+ </BreadcrumbItem>
+ </BreadcrumbList>
+ </Breadcrumb>
- const handleCancelSelection = () => {
- setIsSelectionMode(false);
- setSelectedProjects(new Set());
- };
+ <div className="hidden md:flex rounded-md border p-1 h-10">
+ <button
+ type="button"
+ className={`p-2 rounded-sm cursor-pointer ${isHydrated && viewMode === "grid" ? "bg-accent/75" : ""}`}
+ onClick={() => setViewMode({ viewMode: "grid" })}
+ onKeyDown={(event) =>
+ event.key === "Enter" && setViewMode({ viewMode: "grid" })
+ }
+ aria-label="Grid view"
+ aria-pressed={isHydrated && viewMode === "grid"}
+ >
+ <HugeiconsIcon icon={GridViewIcon} className="size-4" />
+ </button>
+ <button
+ type="button"
+ className={`p-2 rounded-sm cursor-pointer ${isHydrated && viewMode === "list" ? "bg-accent/75" : ""}`}
+ onClick={() => setViewMode({ viewMode: "list" })}
+ onKeyDown={(event) =>
+ event.key === "Enter" && setViewMode({ viewMode: "list" })
+ }
+ aria-label="List view"
+ aria-pressed={isHydrated && viewMode === "list"}
+ >
+ <HugeiconsIcon
+ icon={LeftToRightListDashIcon}
+ className="size-4"
+ />
+ </button>
+ </div>
+ </div>
+
+ <div className="flex items-center gap-3 md:gap-4">
+ <SearchBar className="hidden md:block" />
+ <NewProjectButton />
+ </div>
+ </div>
+ <SearchBar className="block md:hidden mb-4" />
+ </header>
+ );
+}
- const handleBulkDelete = async () => {
- await Promise.all(
- Array.from(selectedProjects).map((projectId) => deleteProject(projectId))
- );
- setSelectedProjects(new Set());
- setIsSelectionMode(false);
- setIsBulkDeleteDialogOpen(false);
- };
+const SORT_LABELS: Record<TProjectSortKey, string> = {
+ createdAt: "Created",
+ updatedAt: "Modified",
+ name: "Name",
+ duration: "Duration",
+};
- const sortedProjects = getFilteredAndSortedProjects(searchQuery, sortOption);
+function ProjectsToolbar({ projectIds }: { projectIds: string[] }) {
+ const {
+ selectedProjectIds,
+ sortKey,
+ sortOrder,
+ setSortOrder,
+ setSelectedProjects,
+ clearSelectedProjects,
+ viewMode,
+ setViewMode,
+ } = useProjectsStore();
- const allSelected =
- sortedProjects.length > 0 &&
- selectedProjects.size === sortedProjects.length;
- const someSelected =
- selectedProjects.size > 0 && selectedProjects.size < sortedProjects.length;
+ const selectedProjectCount = selectedProjectIds.length;
+ const isAllSelected =
+ projectIds.length > 0 && selectedProjectCount === projectIds.length;
+ const hasSomeSelected =
+ selectedProjectCount > 0 && selectedProjectCount < projectIds.length;
- return (
- <div className="min-h-screen bg-background">
- <div className="pt-6 px-6 flex items-center justify-between w-full h-16">
- <Link
- href="/"
- className="flex items-center gap-1 hover:text-muted-foreground transition-colors"
- >
- <ChevronLeft className="size-5! shrink-0" />
- <span className="text-sm font-medium">Back</span>
- </Link>
- <div className="block md:hidden">
- {isSelectionMode ? (
- <div className="flex items-center gap-2">
- <Button
- variant="outline"
- size="sm"
- onClick={handleCancelSelection}
- >
- <X className="size-4!" />
- Cancel
- </Button>
- {selectedProjects.size > 0 && (
- <Button
- variant="destructive"
- size="sm"
- onClick={() => setIsBulkDeleteDialogOpen(true)}
- >
- <Trash2 className="size-4!" />
- Delete ({selectedProjects.size})
- </Button>
- )}
- </div>
- ) : (
- <CreateButton onClick={handleCreateProject} />
- )}
- </div>
- </div>
- <main className="max-w-6xl mx-auto px-6 pt-6 pb-6">
- <div className="mb-8 flex items-center justify-between">
- <div className="flex flex-col gap-3">
- <h1 className="text-2xl md:text-3xl font-bold tracking-tight">
- Your Projects
- </h1>
- <p className="text-muted-foreground">
- {savedProjects.length}{" "}
- {savedProjects.length === 1 ? "project" : "projects"}
- {isSelectionMode && selectedProjects.size > 0 && (
- <span className="ml-2 text-primary">
- • {selectedProjects.size} selected
- </span>
- )}
- </p>
- </div>
- <div className="hidden md:block">
- {isSelectionMode ? (
- <div className="flex items-center gap-2">
- <Button variant="outline" onClick={handleCancelSelection}>
- <X className="size-4!" />
- Cancel
- </Button>
- {selectedProjects.size > 0 && (
- <Button
- variant="destructive"
- onClick={() => setIsBulkDeleteDialogOpen(true)}
- >
- <Trash2 className="size-4!" />
- Delete Selected ({selectedProjects.size})
- </Button>
- )}
- </div>
- ) : (
- <div className="flex items-center gap-2">
- <Button
- variant="outline"
- onClick={() => setIsSelectionMode(true)}
- disabled={savedProjects.length === 0}
- >
- Select Projects
- </Button>
- <CreateButton onClick={handleCreateProject} />
- </div>
- )}
- </div>
- </div>
+ const handleSelectAll = ({ checked }: { checked: boolean }) => {
+ if (checked) {
+ setSelectedProjects({ projectIds });
+ return;
+ }
+ clearSelectedProjects();
+ };
- <div className="mb-4 flex items-center justify-between gap-4">
- <div className="flex-1 max-w-72">
- <Input
- placeholder="Search projects..."
- value={searchQuery}
- onChange={(e) => setSearchQuery(e.target.value)}
- />
- </div>
- <div className="flex items-center gap-0">
- <TooltipProvider>
- <Tooltip>
- <DropdownMenu>
- <TooltipTrigger asChild>
- <DropdownMenuTrigger asChild>
- <Button
- size="icon"
- variant="secondary"
- className="justify-center items-center w-9 h-9"
- >
- <ArrowDown01
- strokeWidth={1.5}
- className="!size-[1.05rem]"
- />
- </Button>
- </DropdownMenuTrigger>
- </TooltipTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuItem
- onClick={() => {
- if (sortOption.startsWith("createdAt")) {
- setSortOption(
- sortOption.endsWith("asc")
- ? "createdAt-desc"
- : "createdAt-asc"
- );
- } else {
- setSortOption("createdAt-asc");
- }
- }}
- >
- Created{" "}
- {sortOption.startsWith("createdAt") &&
- (sortOption.endsWith("asc") ? "↑" : "↓")}
- </DropdownMenuItem>
- <DropdownMenuItem
- onClick={() => {
- if (sortOption.startsWith("name")) {
- setSortOption(
- sortOption.endsWith("asc")
- ? "name-desc"
- : "name-asc"
- );
- } else {
- setSortOption("name-asc");
- }
- }}
- >
- Name{" "}
- {sortOption.startsWith("name") &&
- (sortOption.endsWith("asc") ? "↑" : "↓")}
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- <TooltipContent>
- <p>
- Sort by{" "}
- {sortOption.startsWith("createdAt") ? "date" : "name"} (
- {sortOption.endsWith("asc") ? "ascending" : "descending"})
- </p>
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- </div>
- </div>
+ return (
+ <div className="sticky top-16 z-10 flex items-center justify-between px-6 h-14 pt-2 bg-background">
+ <div className="flex items-center gap-2">
+ <Label
+ className="flex items-center gap-3 cursor-pointer px-2"
+ htmlFor="select-all-projects"
+ >
+ <Checkbox
+ className="size-5"
+ id="select-all-projects"
+ checked={
+ isAllSelected ? true : hasSomeSelected ? "indeterminate" : false
+ }
+ onCheckedChange={(checked) =>
+ handleSelectAll({ checked: checked === true })
+ }
+ />
+ <span className="text-muted-foreground hidden md:block">
+ Select all
+ </span>
+ </Label>
- {isSelectionMode && sortedProjects.length > 0 && (
- <button
- type="button"
- onClick={() => handleSelectAll(!allSelected)}
- onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
- handleSelectAll(!allSelected);
- }
- }}
- className="w-full hover:cursor-pointer gap-2 mb-6 p-4 bg-muted/30 rounded-lg border items-center flex"
- tabIndex={0}
- >
- <Checkbox checked={someSelected ? "indeterminate" : allSelected} />
- <span className="text-sm font-medium">
- {allSelected ? "Deselect All" : "Select All"}
- </span>
- <span className="text-sm text-muted-foreground">
- ({selectedProjects.size} of {sortedProjects.length} selected)
- </span>
- </button>
- )}
+ <div className="h-4 w-px bg-border/50" />
- {isLoading || !isInitialized ? (
- <div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6">
- {Array.from({ length: 8 }, (_, index) => (
- <div
- key={`skeleton-${index}-${Date.now()}`}
- className="overflow-hidden bg-background border-none p-0"
- >
- <Skeleton className="aspect-square w-full bg-muted/50" />
- <div className="px-0 pt-5 flex flex-col gap-1">
- <Skeleton className="h-4 w-3/4 bg-muted/50" />
- <div className="flex items-center gap-1.5">
- <Skeleton className="h-4 w-4 bg-muted/50" />
- <Skeleton className="h-4 w-24 bg-muted/50" />
- </div>
- </div>
- </div>
- ))}
- </div>
- ) : savedProjects.length === 0 ? (
- <NoProjects onCreateProject={handleCreateProject} />
- ) : sortedProjects.length === 0 ? (
- <NoResults
- searchQuery={searchQuery}
- onClearSearch={() => setSearchQuery("")}
- />
- ) : (
- <div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6">
- {sortedProjects.map((project) => (
- <ProjectCard
- key={project.id}
- project={project}
- isSelectionMode={isSelectionMode}
- isSelected={selectedProjects.has(project.id)}
- onSelect={handleSelectProject}
- getProjectThumbnail={getProjectThumbnail}
- />
- ))}
- </div>
- )}
- </main>
+ <SortDropdown>
+ <Button variant="text" className="text-muted-foreground pl-2">
+ {SORT_LABELS[sortKey]}
+ </Button>
+ </SortDropdown>
+ <Button
+ type="button"
+ variant="text"
+ className="text-muted-foreground"
+ onClick={() =>
+ setSortOrder({
+ sortOrder: sortOrder === "asc" ? "desc" : "asc",
+ })
+ }
+ onKeyDown={(event) => {
+ if (event.key === "Enter" || event.key === " ") {
+ setSortOrder({
+ sortOrder: sortOrder === "asc" ? "desc" : "asc",
+ });
+ }
+ }}
+ aria-label={`Sort ${sortOrder === "asc" ? "ascending" : "descending"}`}
+ >
+ <HugeiconsIcon
+ icon={ArrowDown02Icon}
+ className={sortOrder === "asc" ? "rotate-180" : ""}
+ />
+ </Button>
- <DeleteProjectDialog
- isOpen={isBulkDeleteDialogOpen}
- onOpenChange={setIsBulkDeleteDialogOpen}
- onConfirm={handleBulkDelete}
- />
- </div>
- );
+ <div className="h-4 w-px bg-border/50 block md:hidden" />
+
+ <div className="flex md:hidden items-center gap-4">
+ <Button
+ variant="text"
+ onClick={() => setViewMode({ viewMode: "grid" })}
+ >
+ <HugeiconsIcon
+ icon={GridViewIcon}
+ className={cn(
+ viewMode === "grid" ? "text-primary" : "text-muted-foreground",
+ )}
+ />
+ </Button>
+ <Button
+ variant="text"
+ onClick={() => setViewMode({ viewMode: "list" })}
+ >
+ <HugeiconsIcon
+ icon={LeftToRightListDashIcon}
+ className={cn(
+ viewMode === "list" ? "text-primary" : "text-muted-foreground",
+ )}
+ />
+ </Button>
+ </div>
+ </div>
+ {selectedProjectCount > 0 ? <ProjectActions /> : null}
+ </div>
+ );
}
-interface ProjectCardProps {
- project: TProject;
- isSelectionMode?: boolean;
- isSelected?: boolean;
- onSelect?: (projectId: string, checked: boolean) => void;
- getProjectThumbnail: (projectId: string) => Promise<string | null>;
+function SearchBar({
+ className,
+ collapsed,
+}: {
+ className?: string;
+ collapsed?: boolean;
+}) {
+ const { searchQuery, setSearchQuery } = useProjectsStore();
+
+ return (
+ <>
+ {collapsed ? (
+ <div className="block md:hidden">
+ <Button
+ size="icon"
+ variant="outline"
+ className="size-10.5 rounded-full"
+ >
+ <HugeiconsIcon icon={Search01Icon} />
+ </Button>
+ </div>
+ ) : (
+ <div className={cn("relative", className)}>
+ <HugeiconsIcon
+ icon={Search01Icon}
+ className="text-muted-foreground pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2"
+ aria-hidden="true"
+ />
+ <Input
+ placeholder="Search..."
+ value={searchQuery}
+ onChange={(event) => setSearchQuery({ query: event.target.value })}
+ size="lg"
+ className="pl-9"
+ />
+ </div>
+ )}
+ </>
+ );
}
-function ProjectCard({
- project,
- isSelectionMode = false,
- isSelected = false,
- onSelect,
- getProjectThumbnail,
-}: ProjectCardProps) {
- const [isDropdownOpen, setIsDropdownOpen] = useState(false);
- const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
- const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
- const [dynamicThumbnail, setDynamicThumbnail] = useState<string | null>(null);
- const [isLoadingThumbnail, setIsLoadingThumbnail] = useState(true);
- const { deleteProject, renameProject, duplicateProject } = useProjectStore();
+const PROJECT_ACTIONS = [
+ {
+ id: "duplicate",
+ label: "Duplicate",
+ icon: Copy01Icon,
+ variant: "outline" as const,
+ },
+ {
+ id: "delete",
+ label: "Delete",
+ icon: Delete02Icon,
+ variant: "destructive-foreground" as const,
+ },
+] as const;
- useEffect(() => {
- const loadThumbnail = async () => {
- setIsLoadingThumbnail(true);
- try {
- const thumbnail = await getProjectThumbnail(project.id);
- setDynamicThumbnail(thumbnail);
- } finally {
- setIsLoadingThumbnail(false);
- }
- };
- loadThumbnail();
- }, [project.id, getProjectThumbnail]);
+async function deleteProjects({
+ editor,
+ ids,
+}: {
+ editor: ReturnType<typeof useEditor>;
+ ids: string[];
+}) {
+ await editor.project.deleteProjects({ ids });
+}
- const formatDate = (date: Date): string => {
- return date.toLocaleDateString("en-US", {
- month: "short",
- day: "numeric",
- year: "numeric",
- });
- };
+async function duplicateProjects({
+ editor,
+ ids,
+}: {
+ editor: ReturnType<typeof useEditor>;
+ ids: string[];
+}) {
+ await editor.project.duplicateProjects({ ids });
+}
- const handleDeleteProject = async () => {
- await deleteProject(project.id);
- setIsDropdownOpen(false);
- };
+async function renameProject({
+ editor,
+ id,
+ name,
+}: {
+ editor: ReturnType<typeof useEditor>;
+ id: string;
+ name: string;
+}) {
+ await editor.project.renameProject({ id, name });
+}
- const handleRenameProject = async (newName: string) => {
- await renameProject(project.id, newName);
- setIsRenameDialogOpen(false);
- };
+function ProjectActions() {
+ const editor = useEditor();
+ const { selectedProjectIds, clearSelectedProjects } = useProjectsStore();
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
- const handleDuplicateProject = async () => {
- setIsDropdownOpen(false);
- await duplicateProject(project.id);
- };
+ const savedProjects = editor.project.getSavedProjects();
+ const selectedProjectNames = savedProjects
+ .filter((project) => selectedProjectIds.includes(project.id))
+ .map((project) => project.name);
- const handleCardClick = (e: React.MouseEvent) => {
- if (isSelectionMode) {
- e.preventDefault();
- onSelect?.(project.id, !isSelected);
- }
- };
+ const handleDuplicate = async () => {
+ await duplicateProjects({ editor, ids: selectedProjectIds });
+ clearSelectedProjects();
+ };
- const handleCardKeyDown = (e: React.KeyboardEvent) => {
- if (isSelectionMode && (e.key === "Enter" || e.key === " ")) {
- e.preventDefault();
- onSelect?.(project.id, !isSelected);
- }
- };
+ const handleDeleteClick = () => {
+ setIsDeleteDialogOpen(true);
+ };
- const cardContent = (
- <Card
- className={`overflow-hidden bg-background border-none p-0 transition-all ${
- isSelectionMode && isSelected ? "ring-2 ring-primary" : ""
- }`}
- >
- <div
- className={`relative aspect-square bg-muted transition-opacity ${
- isDropdownOpen ? "opacity-65" : "opacity-100 group-hover:opacity-65"
- }`}
- >
- {isSelectionMode && (
- <div className="absolute top-3 left-3 z-10">
- <div className="w-5 h-5 rounded-full bg-background/80 backdrop-blur-xs border flex items-center justify-center">
- <Checkbox
- checked={isSelected}
- onCheckedChange={(checked) =>
- onSelect?.(project.id, checked as boolean)
- }
- onClick={(e) => e.stopPropagation()}
- className="w-4 h-4"
- />
- </div>
- </div>
- )}
+ const handleDeleteConfirm = async () => {
+ await deleteProjects({ editor, ids: selectedProjectIds });
+ clearSelectedProjects();
+ setIsDeleteDialogOpen(false);
+ };
- <div className="absolute inset-0">
- {isLoadingThumbnail ? (
- <div className="w-full h-full bg-muted/50 flex items-center justify-center">
- <Loader2 className="h-12 w-12 text-muted-foreground animate-spin" />
- </div>
- ) : dynamicThumbnail ? (
- <Image
- src={dynamicThumbnail}
- alt="Project thumbnail"
- fill
- className="object-cover"
- />
- ) : (
- <div className="w-full h-full bg-muted/50 flex items-center justify-center">
- <Video className="h-12 w-12 shrink-0 text-muted-foreground" />
- </div>
- )}
- </div>
- </div>
+ const actionHandlers: Record<string, () => void> = {
+ duplicate: handleDuplicate,
+ delete: handleDeleteClick,
+ };
- <CardContent className="px-0 pt-5 flex flex-col gap-1">
- <div className="flex items-start justify-between">
- <h3 className="font-medium text-sm leading-snug group-hover:text-foreground/90 transition-colors line-clamp-2">
- {project.name}
- </h3>
- {!isSelectionMode && (
- <DropdownMenu
- open={isDropdownOpen}
- onOpenChange={setIsDropdownOpen}
- >
- <DropdownMenuTrigger asChild>
- <Button
- variant="text"
- size="sm"
- className={`size-6 p-0 transition-all shrink-0 ml-2 ${
- isDropdownOpen
- ? "opacity-100"
- : "opacity-0 group-hover:opacity-100"
- }`}
- onClick={(e) => e.preventDefault()}
- >
- <MoreHorizontal />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent
- align="end"
- onCloseAutoFocus={(e) => {
- e.preventDefault();
- e.stopPropagation();
- }}
- >
- <DropdownMenuItem
- onClick={(e) => {
- e.preventDefault();
- e.stopPropagation();
- setIsDropdownOpen(false);
- setIsRenameDialogOpen(true);
- }}
- >
- Rename
- </DropdownMenuItem>
- <DropdownMenuItem
- onClick={(e) => {
- e.preventDefault();
- e.stopPropagation();
- handleDuplicateProject();
- }}
- >
- Duplicate
- </DropdownMenuItem>
- <DropdownMenuSeparator />
- <DropdownMenuItem
- variant="destructive"
- onClick={(e) => {
- e.preventDefault();
- e.stopPropagation();
- setIsDropdownOpen(false);
- setIsDeleteDialogOpen(true);
- }}
- >
- Delete
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- )}
- </div>
+ return (
+ <>
+ <div className="flex items-center gap-2.5 px-3">
+ <div className="hidden sm:flex items-center gap-2.5">
+ {PROJECT_ACTIONS.map((action) => (
+ <Button
+ key={action.id}
+ size="icon"
+ variant={action.variant}
+ className="size-9"
+ onClick={actionHandlers[action.id]}
+ >
+ <HugeiconsIcon icon={action.icon} />
+ </Button>
+ ))}
+ </div>
- <div className="space-y-1">
- <div className="flex items-center gap-1.5 text-sm text-muted-foreground">
- <Calendar className="size-4!" />
- <span>Created {formatDate(project.createdAt)}</span>
- </div>
- </div>
- </CardContent>
- </Card>
- );
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild className="sm:hidden">
+ <Button size="icon" variant="outline" className="size-9">
+ <HugeiconsIcon icon={MoreHorizontalIcon} />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end">
+ {PROJECT_ACTIONS.map((action) => (
+ <DropdownMenuItem
+ key={action.id}
+ variant={action.id === "delete" ? "destructive" : undefined}
+ onClick={actionHandlers[action.id]}
+ >
+ <HugeiconsIcon icon={action.icon} />
+ {action.label}
+ </DropdownMenuItem>
+ ))}
+ </DropdownMenuContent>
+ </DropdownMenu>
+ </div>
- return (
- <>
- {isSelectionMode ? (
- <button
- type="button"
- onClick={handleCardClick}
- onKeyDown={handleCardKeyDown}
- className="block group cursor-pointer w-full text-left"
- >
- {cardContent}
- </button>
- ) : (
- <Link href={`/editor/${project.id}`} className="block group">
- {cardContent}
- </Link>
- )}
- <DeleteProjectDialog
- isOpen={isDeleteDialogOpen}
- onOpenChange={setIsDeleteDialogOpen}
- onConfirm={handleDeleteProject}
- />
- <RenameProjectDialog
- isOpen={isRenameDialogOpen}
- onOpenChange={setIsRenameDialogOpen}
- onConfirm={handleRenameProject}
- projectName={project.name}
- />
- </>
- );
+ <DeleteProjectDialog
+ isOpen={isDeleteDialogOpen}
+ onOpenChange={setIsDeleteDialogOpen}
+ projectNames={selectedProjectNames}
+ onConfirm={handleDeleteConfirm}
+ />
+ </>
+ );
}
-function CreateButton({ onClick }: { onClick?: () => void }) {
- return (
- <Button className="flex" onClick={onClick}>
- <Plus className="size-4!" />
- <span className="text-sm font-medium">New project</span>
- </Button>
- );
+function SortDropdown({ children }: { children: React.ReactNode }) {
+ const { sortKey, setSortKey } = useProjectsStore();
+
+ return (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
+ <DropdownMenuContent className="w-48" align="center">
+ <DropdownMenuCheckboxItem
+ checked={sortKey === "createdAt"}
+ onCheckedChange={() => setSortKey({ sortKey: "createdAt" })}
+ >
+ Created
+ </DropdownMenuCheckboxItem>
+ <DropdownMenuCheckboxItem
+ checked={sortKey === "updatedAt"}
+ onCheckedChange={() => setSortKey({ sortKey: "updatedAt" })}
+ >
+ Modified
+ </DropdownMenuCheckboxItem>
+ <DropdownMenuCheckboxItem
+ checked={sortKey === "name"}
+ onCheckedChange={() => setSortKey({ sortKey: "name" })}
+ >
+ Name
+ </DropdownMenuCheckboxItem>
+ <DropdownMenuCheckboxItem
+ checked={sortKey === "duration"}
+ onCheckedChange={() => setSortKey({ sortKey: "duration" })}
+ >
+ Duration
+ </DropdownMenuCheckboxItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ );
+}
+
+function NewProjectButton() {
+ const editor = useEditor();
+ const router = useRouter();
+
+ const handleCreateProject = async () => {
+ const projectId = await editor.project.createNewProject({
+ name: "New project",
+ });
+ router.push(`/editor/${projectId}`);
+ };
+
+ return (
+ <Button
+ size="lg"
+ className="flex px-5 md:px-6"
+ onClick={handleCreateProject}
+ >
+ <span className="text-sm font-medium hidden md:block">New project</span>
+ <span className="text-sm font-medium block md:hidden">New</span>
+ </Button>
+ );
}
-function NoProjects({ onCreateProject }: { onCreateProject: () => void }) {
- return (
- <div className="flex flex-col items-center justify-center py-16 text-center">
- <div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4">
- <Video className="h-8 w-8 text-muted-foreground" />
- </div>
- <h3 className="text-lg font-medium mb-2">No projects yet</h3>
- <p className="text-muted-foreground mb-6 max-w-md">
- Start creating your first video project. Import media, edit, and export
- professional videos.
- </p>
- <Button size="lg" className="gap-2" onClick={onCreateProject}>
- <Plus className="h-4 w-4" />
- Create Your First Project
- </Button>
- </div>
- );
+function ProjectItem({
+ project,
+ allProjectIds,
+}: {
+ project: TProjectMetadata;
+ allProjectIds: string[];
+}) {
+ const {
+ selectedProjectIds,
+ viewMode,
+ setProjectSelected,
+ selectProjectRange,
+ } = useProjectsStore();
+ const selectedProjectIdSet = new Set(selectedProjectIds);
+ const isSelected = selectedProjectIdSet.has(project.id);
+ const selectedProjectCount = selectedProjectIds.length;
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+ const durationLabel = formatProjectDuration({ duration: project.duration });
+ const isMultiSelect = selectedProjectCount > 1;
+ const isGridView = viewMode === "grid";
+
+ const handleCheckboxChange = ({
+ checked,
+ shiftKey,
+ }: {
+ checked: boolean;
+ shiftKey: boolean;
+ }) => {
+ if (shiftKey && checked) {
+ selectProjectRange({ projectId: project.id, allProjectIds });
+ return;
+ }
+ setProjectSelected({ projectId: project.id, isSelected: checked });
+ };
+
+ const gridContent = (
+ <Card className="bg-background overflow-hidden border-none p-0">
+ <div className="bg-muted relative aspect-video">
+ <div className="absolute inset-0">
+ {project.thumbnail ? (
+ <Image
+ src={project.thumbnail}
+ alt="Project thumbnail"
+ fill
+ className="object-cover"
+ />
+ ) : (
+ <div className="flex size-full items-center justify-center">
+ <OcVideoIcon className="text-muted-foreground size-12 shrink-0" />
+ </div>
+ )}
+ </div>
+
+ {durationLabel && (
+ <div className="absolute bottom-2 right-2 bg-black/60 text-white text-xs font-semibold px-2 py-1 rounded-lg">
+ {durationLabel}
+ </div>
+ )}
+ </div>
+
+ <CardContent className="flex flex-col gap-2 px-0 pt-4">
+ <h3 className="group-hover:text-foreground/90 line-clamp-2 text-sm leading-snug font-medium">
+ {project.name}
+ </h3>
+ <div className="text-muted-foreground flex items-center gap-1.5 text-sm">
+ <HugeiconsIcon icon={Calendar04Icon} className="size-4" />
+ <span>Created {formatDate({ date: project.createdAt })}</span>
+ </div>
+ </CardContent>
+ </Card>
+ );
+
+ const listRowContent = (
+ <div className="flex items-center gap-3 flex-1 min-w-0">
+ <div className="bg-muted relative size-10 rounded overflow-hidden shrink-0">
+ {project.thumbnail ? (
+ <Image
+ src={project.thumbnail}
+ alt="Project thumbnail"
+ fill
+ className="object-cover"
+ />
+ ) : (
+ <div className="flex size-full items-center justify-center">
+ <OcVideoIcon className="text-muted-foreground size-5 shrink-0" />
+ </div>
+ )}
+ </div>
+
+ <h3 className="group-hover:text-foreground/90 text-sm font-medium truncate flex-1 min-w-0">
+ {project.name}
+ </h3>
+
+ <span className="text-muted-foreground text-sm shrink-0 hidden sm:block">
+ {durationLabel ?? "—"}
+ </span>
+
+ <span className="text-muted-foreground text-sm shrink-0 w-auto pl-8 text-right hidden xs:block">
+ {formatDate({ date: project.createdAt })}
+ </span>
+ </div>
+ );
+
+ const listContent = (
+ <div
+ className={`flex items-center gap-4 py-2 px-4 border-b border-border/50 ${
+ isSelected ? "bg-primary/5" : ""
+ }`}
+ >
+ <Checkbox
+ checked={isSelected}
+ onMouseDown={(event) => event.preventDefault()}
+ onClick={(event) => {
+ handleCheckboxChange({
+ checked: !isSelected,
+ shiftKey: event.shiftKey,
+ });
+ }}
+ onCheckedChange={() => {}}
+ className="size-5 shrink-0"
+ />
+
+ <Link href={`/editor/${project.id}`} className="flex-1 min-w-0">
+ {listRowContent}
+ </Link>
+
+ {!isMultiSelect && (
+ <ProjectMenu
+ isOpen={isDropdownOpen}
+ onOpenChange={setIsDropdownOpen}
+ project={project}
+ variant="list"
+ />
+ )}
+ </div>
+ );
+
+ const cardContent = isGridView ? gridContent : listContent;
+
+ if (!isGridView) {
+ return <div className="group relative">{listContent}</div>;
+ }
+
+ return (
+ <div className="group relative">
+ <Link href={`/editor/${project.id}`} className="block">
+ {cardContent}
+ </Link>
+
+ {isGridView && (
+ <>
+ <Checkbox
+ checked={isSelected}
+ onMouseDown={(event) => event.preventDefault()}
+ onClick={(event) => {
+ handleCheckboxChange({
+ checked: !isSelected,
+ shiftKey: event.shiftKey,
+ });
+ }}
+ onCheckedChange={() => {}}
+ className={`absolute z-10 size-5 top-3 left-3 ${
+ isSelected || isDropdownOpen
+ ? "opacity-100"
+ : "opacity-0 group-hover:opacity-100"
+ }`}
+ />
+
+ {!isMultiSelect && (
+ <ProjectMenu
+ isOpen={isDropdownOpen}
+ onOpenChange={setIsDropdownOpen}
+ project={project}
+ />
+ )}
+ </>
+ )}
+ </div>
+ );
}
-function NoResults({
- searchQuery,
- onClearSearch,
+function ProjectMenu({
+ isOpen,
+ onOpenChange,
+ project,
+ variant = "grid",
}: {
- searchQuery: string;
- onClearSearch: () => void;
+ isOpen: boolean;
+ onOpenChange: (open: boolean) => void;
+ project: TProjectMetadata;
+ variant?: "grid" | "list";
}) {
- return (
- <div className="flex flex-col items-center justify-center py-16 text-center">
- <div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4">
- <Search className="h-8 w-8 text-muted-foreground" />
- </div>
- <h3 className="text-lg font-medium mb-2">No results found</h3>
- <p className="text-muted-foreground mb-6 max-w-md">
- Your search for "{searchQuery}" did not return any results.
- </p>
- <Button onClick={onClearSearch} variant="outline">
- Clear Search
- </Button>
- </div>
- );
+ const editor = useEditor();
+ const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
+ const [isInfoDialogOpen, setIsInfoDialogOpen] = useState(false);
+
+ const handleMenuClick = ({
+ event,
+ }: {
+ event: MouseEvent<HTMLButtonElement>;
+ }) => {
+ event.preventDefault();
+ event.stopPropagation();
+ };
+
+ const handleMenuKeyDown = ({
+ event,
+ }: {
+ event: KeyboardEvent<HTMLButtonElement>;
+ }) => {
+ if (event.key !== "Enter" && event.key !== " ") {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ };
+
+ const handleRename = () => {
+ setIsRenameDialogOpen(true);
+ onOpenChange(false);
+ };
+
+ const handleDuplicate = async () => {
+ await duplicateProjects({ editor, ids: [project.id] });
+ onOpenChange(false);
+ };
+
+ const handleDeleteClick = () => {
+ setIsDeleteDialogOpen(true);
+ onOpenChange(false);
+ };
+
+ const handleDeleteConfirm = async () => {
+ await deleteProjects({ editor, ids: [project.id] });
+ setIsDeleteDialogOpen(false);
+ };
+
+ const handleInfoClick = () => {
+ setIsInfoDialogOpen(true);
+ onOpenChange(false);
+ };
+
+ const isGrid = variant === "grid";
+
+ return (
+ <>
+ <DropdownMenu open={isOpen} onOpenChange={onOpenChange}>
+ <DropdownMenuTrigger asChild>
+ <Button
+ type="button"
+ variant="background"
+ className={
+ isGrid
+ ? `absolute z-10 top-3 right-3 ${isOpen ? "opacity-100" : "opacity-0 group-hover:opacity-100"}`
+ : "!bg-transparent !shadow-none"
+ }
+ size="icon"
+ aria-label="Project menu"
+ onClick={(event) =>
+ handleMenuClick({
+ event: event as unknown as MouseEvent<HTMLButtonElement>,
+ })
+ }
+ onMouseDown={(event) => event.stopPropagation()}
+ onKeyDown={(event) =>
+ handleMenuKeyDown({
+ event: event as unknown as KeyboardEvent<HTMLButtonElement>,
+ })
+ }
+ >
+ <HugeiconsIcon
+ icon={MoreHorizontalIcon}
+ className="text-foreground"
+ aria-hidden="true"
+ />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent className="w-48" align="end">
+ <DropdownMenuItem onClick={handleRename}>
+ <HugeiconsIcon icon={Edit03Icon} />
+ Rename
+ </DropdownMenuItem>
+ <DropdownMenuItem onClick={handleDuplicate}>
+ <HugeiconsIcon icon={Copy01Icon} />
+ Duplicate
+ </DropdownMenuItem>
+ <DropdownMenuItem onClick={handleInfoClick}>
+ <HugeiconsIcon icon={InformationCircleIcon} />
+ Info
+ </DropdownMenuItem>
+ <DropdownMenuItem variant="destructive" onClick={handleDeleteClick}>
+ <HugeiconsIcon icon={Delete02Icon} />
+ Delete
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+
+ <RenameProjectDialog
+ isOpen={isRenameDialogOpen}
+ onOpenChange={setIsRenameDialogOpen}
+ projectName={project.name}
+ onConfirm={async (newName) => {
+ await renameProject({ editor, id: project.id, name: newName });
+ setIsRenameDialogOpen(false);
+ }}
+ />
+
+ <DeleteProjectDialog
+ isOpen={isDeleteDialogOpen}
+ onOpenChange={setIsDeleteDialogOpen}
+ projectNames={[project.name]}
+ onConfirm={handleDeleteConfirm}
+ />
+
+ <ProjectInfoDialog
+ isOpen={isInfoDialogOpen}
+ onOpenChange={setIsInfoDialogOpen}
+ project={project}
+ />
+ </>
+ );
+}
+
+function ProjectsSkeleton() {
+ const skeletonIds = Array.from(
+ { length: 24 },
+ (_, index) => `skeleton-${index}`,
+ );
+
+ return (
+ <div className="xs:grid-cols-2 grid grid-cols-1 gap-6 sm:grid-cols-3 lg:grid-cols-4">
+ {skeletonIds.map((skeletonId) => (
+ <Card
+ key={skeletonId}
+ className="bg-background overflow-hidden border-none p-0"
+ >
+ <div className="bg-muted relative aspect-video">
+ <div className="absolute inset-0">
+ <Skeleton className="bg-muted/50 size-full" />
+ </div>
+ </div>
+ <CardContent className="flex flex-col gap-2 px-0 pt-4">
+ <Skeleton className="bg-muted/50 h-4 w-3/4" />
+ <div className="text-muted-foreground flex items-center gap-1.5">
+ <Skeleton className="bg-muted/50 size-4" />
+ <Skeleton className="bg-muted/50 h-4 w-24" />
+ </div>
+ </CardContent>
+ </Card>
+ ))}
+ </div>
+ );
+}
+
+function EmptyState() {
+ const { searchQuery, setSearchQuery } = useProjectsStore();
+ const router = useRouter();
+ const editor = useEditor();
+ const savedProjects = editor.project.getSavedProjects();
+
+ const handleCreateProject = async () => {
+ try {
+ const projectId = await editor.project.createNewProject({
+ name: "New project",
+ });
+ router.push(`/editor/${projectId}`);
+ } catch (error) {
+ toast.error("Failed to create project", {
+ description:
+ error instanceof Error ? error.message : "Please try again",
+ });
+ }
+ };
+
+ if (savedProjects.length > 0) {
+ return (
+ <div className="flex flex-col items-center justify-center gap-5 py-16 text-center">
+ <div className="flex flex-col items-center gap-8">
+ <HugeiconsIcon
+ icon={Search01Icon}
+ className="text-muted-foreground size-16 bg-accent/35 border rounded-md p-4"
+ />
+ <div className="flex flex-col items-center gap-3">
+ <h3 className="text-lg font-medium">No results found</h3>
+ <p className="text-muted-foreground max-w-md">
+ Your search for "{searchQuery}" did not return any results.
+ </p>
+ </div>
+ </div>
+ <Button
+ onClick={() => setSearchQuery({ query: "" })}
+ variant="outline"
+ size="lg"
+ >
+ Clear search
+ </Button>
+ </div>
+ );
+ }
+
+ return (
+ <div className="flex flex-col items-center justify-center gap-6 py-16 text-center">
+ <div className="flex flex-col items-center gap-2">
+ <div className="bg-muted/30 flex size-16 items-center justify-center rounded-full">
+ <HugeiconsIcon
+ icon={Video01Icon}
+ className="text-muted-foreground size-8"
+ />
+ </div>
+ <h3 className="text-lg font-medium">No projects yet</h3>
+ <p className="text-muted-foreground max-w-md">
+ Start creating your first project. Import media, edit, and export your
+ videos. All privately.
+ </p>
+ </div>
+ <Button size="lg" className="gap-2" onClick={handleCreateProject}>
+ <HugeiconsIcon icon={PlusSignIcon} />
+ Create your first project
+ </Button>
+ </div>
+ );
}
diff --git a/apps/web/src/app/projects/store.ts b/apps/web/src/app/projects/store.ts
new file mode 100644
index 00000000..eaf8f947
--- /dev/null
+++ b/apps/web/src/app/projects/store.ts
@@ -0,0 +1,129 @@
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+import type { TProjectSortKey } from "@/types/project";
+
+export type ProjectsViewMode = "grid" | "list";
+
+interface ProjectsState {
+ searchQuery: string;
+ sortKey: TProjectSortKey;
+ sortOrder: "asc" | "desc";
+ viewMode: ProjectsViewMode;
+ selectedProjectIds: string[];
+ lastSelectedProjectId: string | null;
+ isHydrated: boolean;
+ setIsHydrated: ({ isHydrated }: { isHydrated: boolean }) => void;
+ setSearchQuery: ({ query }: { query: string }) => void;
+ setSortKey: ({ sortKey }: { sortKey: TProjectSortKey }) => void;
+ setSortOrder: ({ sortOrder }: { sortOrder: "asc" | "desc" }) => void;
+ toggleSortOrder: () => void;
+ setViewMode: ({ viewMode }: { viewMode: ProjectsViewMode }) => void;
+ setSelectedProjects: ({ projectIds }: { projectIds: string[] }) => void;
+ clearSelectedProjects: () => void;
+ setProjectSelected: ({
+ projectId,
+ isSelected,
+ }: {
+ projectId: string;
+ isSelected: boolean;
+ }) => void;
+ selectProjectRange: ({
+ projectId,
+ allProjectIds,
+ }: {
+ projectId: string;
+ allProjectIds: string[];
+ }) => void;
+}
+
+const getNextSelectedProjectIds = ({
+ selectedProjectIds,
+ projectId,
+ isSelected,
+}: {
+ selectedProjectIds: string[];
+ projectId: string;
+ isSelected: boolean;
+}): string[] => {
+ const selectedProjectIdSet = new Set(selectedProjectIds);
+
+ if (isSelected) {
+ selectedProjectIdSet.add(projectId);
+ return Array.from(selectedProjectIdSet);
+ }
+
+ selectedProjectIdSet.delete(projectId);
+ return Array.from(selectedProjectIdSet);
+};
+
+export const useProjectsStore = create<ProjectsState>()(
+ persist(
+ (set) => ({
+ searchQuery: "",
+ sortKey: "createdAt",
+ sortOrder: "desc",
+ viewMode: "grid",
+ selectedProjectIds: [],
+ lastSelectedProjectId: null,
+ isHydrated: false,
+ setIsHydrated: ({ isHydrated }) => set({ isHydrated }),
+ setSearchQuery: ({ query }) => set({ searchQuery: query }),
+ setSortKey: ({ sortKey }) => set({ sortKey }),
+ setSortOrder: ({ sortOrder }) => set({ sortOrder }),
+ toggleSortOrder: () =>
+ set((state) => ({
+ sortOrder: state.sortOrder === "asc" ? "desc" : "asc",
+ })),
+ setViewMode: ({ viewMode }) => set({ viewMode }),
+ setSelectedProjects: ({ projectIds }) =>
+ set({ selectedProjectIds: projectIds }),
+ clearSelectedProjects: () =>
+ set({ selectedProjectIds: [], lastSelectedProjectId: null }),
+ setProjectSelected: ({ projectId, isSelected }) =>
+ set((state) => ({
+ selectedProjectIds: getNextSelectedProjectIds({
+ selectedProjectIds: state.selectedProjectIds,
+ projectId,
+ isSelected,
+ }),
+ lastSelectedProjectId: isSelected ? projectId : state.lastSelectedProjectId,
+ })),
+ selectProjectRange: ({ projectId, allProjectIds }) =>
+ set((state) => {
+ const anchorId = state.lastSelectedProjectId;
+ if (!anchorId) {
+ return {
+ selectedProjectIds: [projectId],
+ lastSelectedProjectId: projectId,
+ };
+ }
+
+ const anchorIndex = allProjectIds.indexOf(anchorId);
+ const targetIndex = allProjectIds.indexOf(projectId);
+
+ if (anchorIndex === -1 || targetIndex === -1) {
+ return {
+ selectedProjectIds: [projectId],
+ lastSelectedProjectId: projectId,
+ };
+ }
+
+ const startIndex = Math.min(anchorIndex, targetIndex);
+ const endIndex = Math.max(anchorIndex, targetIndex);
+ const rangeIds = allProjectIds.slice(startIndex, endIndex + 1);
+
+ const merged = new Set([...state.selectedProjectIds, ...rangeIds]);
+ return {
+ selectedProjectIds: Array.from(merged),
+ };
+ }),
+ }),
+ {
+ name: "projects-view-mode",
+ partialize: (state) => ({ viewMode: state.viewMode }),
+ onRehydrateStorage: () => (state) => {
+ state?.setIsHydrated({ isHydrated: true });
+ },
+ },
+ ),
+);
diff --git a/apps/web/src/app/roadmap/page.tsx b/apps/web/src/app/roadmap/page.tsx
index b5397b63..5227f808 100644
--- a/apps/web/src/app/roadmap/page.tsx
+++ b/apps/web/src/app/roadmap/page.tsx
@@ -1,247 +1,144 @@
-import { Metadata } from "next";
+import type { Metadata } from "next";
+import { BasePage } from "@/app/base-page";
+import { GitHubContributeSection } from "@/components/gitHub-contribute-section";
import { Badge } from "@/components/ui/badge";
-import { GithubIcon } from "@/components/icons";
-import Link from "next/link";
-import { Footer } from "@/components/footer";
-import { Header } from "@/components/header";
-import ReactMarkdown from "react-markdown";
-import { cn } from "@/lib/utils";
+import { ReactMarkdownWrapper } from "@/components/ui/react-markdown-wrapper";
+import { cn } from "@/utils/ui";
-const roadmapItems: {
- title: string;
- description: string;
- status: {
- text: string;
- type: "complete" | "pending" | "default" | "info";
- };
-}[] = [
- {
- title: "Start",
- description:
- "This is where it all started. Repository created, initial project structure, and the vision for a free, open-source video editor. [Check out the first tweet](https://x.com/mazeincoding/status/1936706642512388188) to see where it started.",
- status: {
- text: "Completed",
- type: "complete",
- },
- },
- {
- title: "Core UI",
- description:
- "Built the foundation - main layout, header, sidebar, timeline container, and basic component structure. Not all functionality yet, but the UI framework that everything else builds on.",
- status: {
- text: "Completed",
- type: "complete",
- },
- },
- {
- title: "Basic Functionality",
- description:
- "The heart of any video editor. Timeline zoom in/out, making clips longer/shorter, dragging elements around, selection, playhead scrubbing. **This part has to be fucking perfect** because it's what users interact with 99% of the time.",
- status: {
- text: "In Progress",
- type: "pending",
- },
- },
- {
- title: "Export/Preview Logic",
- description:
- "The foundation that enables everything else. Real-time preview, video rendering, export functionality. Once this works, we can add effects, filters, transitions - basically everything that makes a video editor powerful.",
- status: {
- text: "Completed",
- type: "complete",
- },
- },
- {
- title: "Text",
- description:
- "After media, text is the next most important thing. Font selection with custom font imports, text stroke, colors. All the text essential text properties.",
- status: {
- text: "Not Started",
- type: "default",
- },
- },
- {
- title: "Effects",
- description:
- "Adding visual effects to both text and media. Blur, brightness, contrast, saturation, filters, and all the creative tools that make videos pop. This is where the magic happens.",
- status: {
- text: "Not Started",
- type: "default",
- },
- },
- {
- title: "Transitions",
- description:
- "Smooth transitions between clips. Fade in/out, slide, zoom, dissolve, and custom transition effects.",
- status: {
- text: "Not Started",
- type: "default",
- },
- },
- {
- title: "Refine from Here",
- description:
- "Once we nail the above, we have a **solid foundation** to build anything. Advanced features, performance optimizations, mobile support, desktop app.",
- status: {
- text: "Future",
- type: "info",
- },
- },
+type StatusType = "complete" | "pending" | "default" | "info";
+
+interface Status {
+ text: string;
+ type: StatusType;
+}
+
+interface RoadmapItem {
+ title: string;
+ description: string;
+ status: Status;
+}
+
+const roadmapItems: RoadmapItem[] = [
+ {
+ title: "Start",
+ description:
+ "This is where it all started. Repository created, initial project structure, and the vision for a free, open-source video editor. [Check out the first tweet](https://x.com/mazeincoding/status/1936706642512388188) to see where it started.",
+ status: {
+ text: "Completed",
+ type: "complete",
+ },
+ },
+ {
+ title: "Core UI",
+ description:
+ "Build the foundation - main layout, header, sidebar, timeline container, and basic component structure. Not all functionality yet, but the UI framework that everything else builds on.",
+ status: {
+ text: "Completed",
+ type: "complete",
+ },
+ },
+ {
+ title: "Essential functionality",
+ description:
+ "Everything that makes a video editor **useful**. Timeline interactivity, storage, effects, transitions, etc.",
+ status: {
+ text: "In progress",
+ type: "pending",
+ },
+ },
+ {
+ title: "Badge (potentially)",
+ description:
+ 'An "Edit with OpenCut" badge web apps can integrate. Shows on video players.',
+ status: {
+ text: "Not started",
+ type: "default",
+ },
+ },
];
export const metadata: Metadata = {
- title: "Roadmap - OpenCut",
- description:
- "See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.",
- openGraph: {
- title: "OpenCut Roadmap - What's Coming Next",
- description:
- "See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.",
- type: "website",
- images: [
- {
- url: "/open-graph/roadmap.jpg",
- width: 1200,
- height: 630,
- alt: "OpenCut Roadmap",
- },
- ],
- },
- twitter: {
- card: "summary_large_image",
- title: "OpenCut Roadmap - What's Coming Next",
- description:
- "See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.",
- images: ["/open-graph/roadmap.jpg"],
- },
+ title: "Roadmap - OpenCut",
+ description:
+ "See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.",
+ openGraph: {
+ title: "OpenCut Roadmap - What's Coming Next",
+ description:
+ "See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.",
+ type: "website",
+ images: [
+ {
+ url: "/open-graph/roadmap.jpg",
+ width: 1200,
+ height: 630,
+ alt: "OpenCut Roadmap",
+ },
+ ],
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: "OpenCut Roadmap - What's Coming Next",
+ description:
+ "See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.",
+ images: ["/open-graph/roadmap.jpg"],
+ },
};
export default function RoadmapPage() {
- return (
- <div className="min-h-screen bg-background">
- <Header />
- <main className="relative">
- <div className="absolute inset-0 overflow-hidden pointer-events-none">
- <div className="absolute -top-40 -right-40 w-96 h-96 bg-linear-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
- <div className="absolute top-1/2 -left-40 w-80 h-80 bg-linear-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
- </div>
- <div className="relative container mx-auto px-4 py-16">
- <div className="max-w-4xl mx-auto">
- <div className="text-center mb-10">
- <Link
- href="https://github.com/OpenCut-app/OpenCut"
- target="_blank"
- >
- <Badge variant="secondary" className="gap-2 mb-6">
- <GithubIcon className="h-3 w-3" />
- Open Source
- </Badge>
- </Link>
- <h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
- Roadmap
- </h1>
- <p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
- What's coming next for OpenCut (last updated: July 14, 2025)
- </p>
- </div>
- <div className="space-y-6">
- {roadmapItems.map((item, index) => (
- <div key={index} className="relative">
- <div className="flex items-start gap-2">
- <span className="text-lg font-medium text-muted-foreground select-none leading-normal">
- {index + 1}.
- </span>
- <div className="flex-1 pt-[2px]">
- <div className="flex items-center gap-2 mb-2">
- <h3 className="font-medium text-lg">{item.title}</h3>
- <Badge
- className={cn("shadow-none", {
- "bg-green-500! text-white":
- item.status.type === "complete",
- "bg-yellow-500! text-white":
- item.status.type === "pending",
- "bg-blue-500! text-white":
- item.status.type === "info",
- "bg-foreground/10! text-accent-foreground":
- item.status.type === "default",
- })}
- >
- {item.status.text}
- </Badge>
- </div>
- <div className="text-foreground/70 leading-relaxed">
- <ReactMarkdown
- components={{
- a: ({ className, children, ...props }) => (
- <a
- className={cn(
- "text-primary hover:underline",
- className
- )}
- target="_blank"
- rel="noopener noreferrer"
- {...props}
- >
- {children}
- </a>
- ),
- strong: ({ children }) => (
- <strong className="font-semibold text-foreground">
- {children}
- </strong>
- ),
- }}
- >
- {item.description}
- </ReactMarkdown>
- </div>
- </div>
- </div>
- </div>
- ))}
- </div>
+ return (
+ <BasePage
+ title="Roadmap"
+ description="What's coming next for OpenCut (last updated: July 14, 2025)"
+ >
+ <div className="mx-auto flex max-w-4xl flex-col gap-16">
+ <div className="flex flex-col gap-6">
+ {roadmapItems.map((item, index) => (
+ <RoadmapItem key={item.title} item={item} index={index} />
+ ))}
+ </div>
+ <GitHubContributeSection
+ title="Want to help?"
+ description="OpenCut is open source and built by the community. Every contribution,
+ no matter how small, helps us build the best free video editor
+ possible."
+ />
+ </div>
+ </BasePage>
+ );
+}
+
+function RoadmapItem({ item, index }: { item: RoadmapItem; index: number }) {
+ return (
+ <div className="flex flex-col gap-2">
+ <div className="flex items-center gap-2 text-lg font-medium">
+ <span className="leading-normal select-none">{index + 1}</span>
+ <h3>{item.title}</h3>
+ <StatusBadge status={item.status} className="ml-1" />
+ </div>
+ <div className="text-foreground/70 leading-relaxed">
+ <ReactMarkdownWrapper>{item.description}</ReactMarkdownWrapper>
+ </div>
+ </div>
+ );
+}
- <div className="mt-12 pt-8 border-t border-muted/20">
- <div className="text-center space-y-4">
- <h3 className="text-xl font-semibold">Want to Help?</h3>
- <p className="text-muted-foreground max-w-2xl mx-auto">
- OpenCut is open source and built by the community. Every
- contribution, no matter how small, helps us build the best
- free video editor possible.
- </p>
- <div className="flex flex-col sm:flex-row gap-4 justify-center mt-6">
- <Link
- href="https://github.com/OpenCut-app/OpenCut/blob/main/.github/CONTRIBUTING.md"
- target="_blank"
- rel="noopener noreferrer"
- >
- <Badge
- variant="outline"
- className="text-sm px-4 py-2 hover:bg-muted/50 transition-colors"
- >
- <GithubIcon className="h-4 w-4 mr-2" />
- Start Contributing
- </Badge>
- </Link>
- <Link
- href="https://github.com/OpenCut-app/OpenCut/issues"
- target="_blank"
- rel="noopener noreferrer"
- >
- <Badge
- variant="outline"
- className="text-sm px-4 py-2 hover:bg-muted/50 transition-colors"
- >
- Report Issues
- </Badge>
- </Link>
- </div>
- </div>
- </div>
- </div>
- </div>
- </main>
- <Footer />
- </div>
- );
+function StatusBadge({
+ status,
+ className,
+}: {
+ status: Status;
+ className?: string;
+}) {
+ return (
+ <Badge
+ className={cn("shadow-none", className, {
+ "bg-green-500! text-white": status.type === "complete",
+ "bg-yellow-500! text-white": status.type === "pending",
+ "bg-blue-500! text-white": status.type === "info",
+ "bg-foreground/10! text-accent-foreground": status.type === "default",
+ })}
+ >
+ {status.text}
+ </Badge>
+ );
}
diff --git a/apps/web/src/app/robots.ts b/apps/web/src/app/robots.ts
index ad6c31c8..60c13829 100644
--- a/apps/web/src/app/robots.ts
+++ b/apps/web/src/app/robots.ts
@@ -1,13 +1,13 @@
import type { MetadataRoute } from "next";
-import { SITE_URL } from "@/constants/site";
+import { SITE_URL } from "@/constants/site-constants";
export default function robots(): MetadataRoute.Robots {
- return {
- rules: {
- userAgent: "*",
- allow: "/",
- disallow: ["/_next/", "/projects/", "/editor/"],
- },
- sitemap: `${SITE_URL}/sitemap.xml`,
- };
+ return {
+ rules: {
+ userAgent: "*",
+ allow: "/",
+ disallow: ["/_next/", "/projects/", "/editor/"],
+ },
+ sitemap: `${SITE_URL}/sitemap.xml`,
+ };
}
diff --git a/apps/web/src/app/rss.xml/route.ts b/apps/web/src/app/rss.xml/route.ts
index ab645655..909d5a1a 100644
--- a/apps/web/src/app/rss.xml/route.ts
+++ b/apps/web/src/app/rss.xml/route.ts
@@ -1,46 +1,46 @@
import { Feed } from "feed";
-import { getPosts } from "@/lib/blog-query";
-import { SITE_INFO, SITE_URL } from "@/constants/site";
+import { getPosts } from "@/lib/blog/query";
+import { SITE_INFO, SITE_URL } from "@/constants/site-constants";
export async function GET() {
- try {
- const { posts } = await getPosts();
+ try {
+ const { posts } = await getPosts();
- const feed = new Feed({
- title: `${SITE_INFO.title} Blog`,
- description: SITE_INFO.description,
- id: `${SITE_URL}`,
- link: `${SITE_URL}/blog/`,
- language: "en",
- image: `${SITE_INFO.openGraphImage}`,
- favicon: `${SITE_INFO.favicon}`,
- copyright: `All rights reserved ${new Date().getFullYear()}, ${
- SITE_INFO.title
- }`,
- });
+ const feed = new Feed({
+ title: `${SITE_INFO.title} Blog`,
+ description: SITE_INFO.description,
+ id: `${SITE_URL}`,
+ link: `${SITE_URL}/blog/`,
+ language: "en",
+ image: `${SITE_INFO.openGraphImage}`,
+ favicon: `${SITE_INFO.favicon}`,
+ copyright: `All rights reserved ${new Date().getFullYear()}, ${
+ SITE_INFO.title
+ }`,
+ });
- for (const post of posts) {
- feed.addItem({
- title: post.title,
- id: `${SITE_URL}/blog/${post.slug}`,
- link: `${SITE_URL}/blog/${post.slug}`,
- description: post.description,
- author: post.authors.map((author) => ({
- name: author.name,
- })),
- date: new Date(post.publishedAt),
- image: post.coverImage || SITE_INFO.openGraphImage,
- });
- }
+ for (const post of posts) {
+ feed.addItem({
+ title: post.title,
+ id: `${SITE_URL}/blog/${post.slug}`,
+ link: `${SITE_URL}/blog/${post.slug}`,
+ description: post.description,
+ author: post.authors.map((author) => ({
+ name: author.name,
+ })),
+ date: new Date(post.publishedAt),
+ image: post.coverImage || SITE_INFO.openGraphImage,
+ });
+ }
- return new Response(feed.rss2(), {
- headers: {
- "Content-Type": "text/xml",
- "Cache-Control": "public, max-age=86400, stale-while-revalidate",
- },
- });
- } catch (error) {
- console.error("Error generating RSS feed", error);
- return new Response("Internal Server Error", { status: 500 });
- }
+ return new Response(feed.rss2(), {
+ headers: {
+ "Content-Type": "text/xml",
+ "Cache-Control": "public, max-age=86400, stale-while-revalidate",
+ },
+ });
+ } catch (error) {
+ console.error("Error generating RSS feed", error);
+ return new Response("Internal Server Error", { status: 500 });
+ }
}
diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts
index 81a5ebf4..fed1aed6 100644
--- a/apps/web/src/app/sitemap.ts
+++ b/apps/web/src/app/sitemap.ts
@@ -1,61 +1,61 @@
-import { SITE_URL } from "@/constants/site";
-import { getPosts } from "@/lib/blog-query";
+import { SITE_URL } from "@/constants/site-constants";
+import { getPosts } from "@/lib/blog/query";
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
- const data = await getPosts();
+ const data = await getPosts();
- const postPages: MetadataRoute.Sitemap =
- data?.posts?.map((post) => ({
- url: `${SITE_URL}/blog/${post.slug}`,
- lastModified: new Date(post.publishedAt),
- changeFrequency: "weekly",
- priority: 0.8,
- })) ?? [];
+ const postPages: MetadataRoute.Sitemap =
+ data?.posts?.map((post) => ({
+ url: `${SITE_URL}/blog/${post.slug}`,
+ lastModified: new Date(post.publishedAt),
+ changeFrequency: "weekly",
+ priority: 0.8,
+ })) ?? [];
- return [
- {
- url: SITE_URL,
- lastModified: new Date(),
- changeFrequency: "weekly",
- priority: 1,
- },
- {
- url: `${SITE_URL}/contributors`,
- lastModified: new Date(),
- changeFrequency: "daily",
- priority: 0.5,
- },
- {
- url: `${SITE_URL}/roadmap`,
- lastModified: new Date(),
- changeFrequency: "weekly",
- priority: 1,
- },
- {
- url: `${SITE_URL}/privacy`,
- lastModified: new Date(),
- changeFrequency: "monthly",
- priority: 0.5,
- },
- {
- url: `${SITE_URL}/terms`,
- lastModified: new Date(),
- changeFrequency: "monthly",
- priority: 0.5,
- },
- {
- url: `${SITE_URL}/why-not-capcut`,
- lastModified: new Date(),
- changeFrequency: "yearly",
- priority: 1,
- },
- {
- url: `${SITE_URL}/blog`,
- lastModified: new Date(),
- changeFrequency: "weekly",
- priority: 1,
- },
- ...postPages,
- ];
+ return [
+ {
+ url: SITE_URL,
+ lastModified: new Date(),
+ changeFrequency: "weekly",
+ priority: 1,
+ },
+ {
+ url: `${SITE_URL}/contributors`,
+ lastModified: new Date(),
+ changeFrequency: "daily",
+ priority: 0.5,
+ },
+ {
+ url: `${SITE_URL}/roadmap`,
+ lastModified: new Date(),
+ changeFrequency: "weekly",
+ priority: 1,
+ },
+ {
+ url: `${SITE_URL}/privacy`,
+ lastModified: new Date(),
+ changeFrequency: "monthly",
+ priority: 0.5,
+ },
+ {
+ url: `${SITE_URL}/terms`,
+ lastModified: new Date(),
+ changeFrequency: "monthly",
+ priority: 0.5,
+ },
+ {
+ url: `${SITE_URL}/why-not-capcut`,
+ lastModified: new Date(),
+ changeFrequency: "yearly",
+ priority: 1,
+ },
+ {
+ url: `${SITE_URL}/blog`,
+ lastModified: new Date(),
+ changeFrequency: "weekly",
+ priority: 1,
+ },
+ ...postPages,
+ ];
}
diff --git a/apps/web/src/app/sponsors/page.tsx b/apps/web/src/app/sponsors/page.tsx
new file mode 100644
index 00000000..5d8b4e6d
--- /dev/null
+++ b/apps/web/src/app/sponsors/page.tsx
@@ -0,0 +1,82 @@
+import type { Metadata } from "next";
+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 "@/constants/site-constants";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { LinkSquare02Icon } from "@hugeicons/core-free-icons";
+
+export const metadata: Metadata = {
+ title: "Sponsors - OpenCut",
+ description:
+ "Support OpenCut and help us build the future of free and open-source video editing.",
+ openGraph: {
+ title: "Sponsors - OpenCut",
+ description:
+ "Support OpenCut and help us build the future of free and open-source video editing.",
+ type: "website",
+ },
+};
+
+export default function SponsorsPage() {
+ return (
+ <BasePage>
+ <div className="flex flex-col gap-8 text-center">
+ <h1 className="text-5xl font-bold tracking-tight md:text-6xl">
+ Sponsors
+ </h1>
+ <p className="text-muted-foreground mx-auto max-w-2xl text-xl leading-relaxed text-pretty">
+ Support OpenCut and help us build the future of privacy-first video
+ editing.
+ </p>
+ </div>
+ <SponsorsGrid />
+ </BasePage>
+ );
+}
+
+function SponsorsGrid() {
+ return (
+ <div className="grid gap-6 sm:grid-cols-2">
+ {SPONSORS.map((sponsor) => (
+ <SponsorCard key={sponsor.name} sponsor={sponsor} />
+ ))}
+ </div>
+ );
+}
+
+function SponsorCard({ sponsor }: { sponsor: Sponsor }) {
+ return (
+ <Link
+ href={sponsor.url}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="size-full"
+ >
+ <Card className="h-full">
+ <CardContent className="flex h-full flex-col justify-center gap-8 p-8">
+ <Image
+ src={sponsor.logo}
+ alt={`${sponsor.name} logo`}
+ width={50}
+ height={50}
+ className="object-contain"
+ />
+ <div className="flex flex-col gap-2">
+ <div className="flex items-center gap-2">
+ <h3 className="text-xl font-semibold group-hover:underline">
+ {sponsor.name}
+ </h3>
+ <HugeiconsIcon
+ icon={LinkSquare02Icon}
+ className="text-muted-foreground size-4"
+ />
+ </div>
+ <p className="text-muted-foreground">{sponsor.description}</p>
+ </div>
+ </CardContent>
+ </Card>
+ </Link>
+ );
+}
diff --git a/apps/web/src/app/terms/page.tsx b/apps/web/src/app/terms/page.tsx
index ee202d8b..d5d43ba7 100644
--- a/apps/web/src/app/terms/page.tsx
+++ b/apps/web/src/app/terms/page.tsx
@@ -1,317 +1,295 @@
-import { Metadata } from "next";
-import { Card, CardContent } from "@/components/ui/card";
-import { Badge } from "@/components/ui/badge";
-import { GithubIcon } from "@/components/icons";
-import Link from "next/link";
-import { Footer } from "@/components/footer";
-import { Header } from "@/components/header";
+import type { Metadata } from "next";
+import { BasePage } from "@/app/base-page";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import { Separator } from "@/components/ui/separator";
+import { SOCIAL_LINKS } from "@/constants/site-constants";
export const metadata: Metadata = {
- title: "Terms of Service - OpenCut",
- description:
- "OpenCut's Terms of Service. Fair, transparent terms for our free and open-source video editor.",
- openGraph: {
- title: "Terms of Service - OpenCut",
- description:
- "OpenCut's Terms of Service. Fair, transparent terms for our free and open-source video editor.",
- type: "website",
- },
+ title: "Terms of Service - OpenCut",
+ description:
+ "OpenCut's Terms of Service. Fair, transparent terms for our free and open-source video editor.",
+ openGraph: {
+ title: "Terms of Service - OpenCut",
+ description:
+ "OpenCut's Terms of Service. Fair, transparent terms for our free and open-source video editor.",
+ type: "website",
+ },
};
export default function TermsPage() {
- return (
- <div className="min-h-screen bg-background">
- <Header />
- <main className="relative">
- <div className="absolute inset-0 overflow-hidden pointer-events-none">
- <div className="absolute -top-40 -right-40 w-96 h-96 bg-linear-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
- <div className="absolute top-1/2 -left-40 w-80 h-80 bg-linear-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
- </div>
- <div className="relative container mx-auto px-4 py-16">
- <div className="max-w-4xl mx-auto">
- <div className="text-center mb-10">
- <Link
- href="https://github.com/OpenCut-app/OpenCut"
- target="_blank"
- >
- <Badge variant="secondary" className="gap-2 mb-6">
- <GithubIcon className="h-3 w-3" />
- Open Source
- </Badge>
- </Link>
- <h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
- Terms of Service
- </h1>
- <p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
- Fair and transparent terms for our free, open-source video
- editor.
- </p>
- </div>
- <Card className="bg-background/80 backdrop-blur-xs border-2 border-muted/30">
- <CardContent className="p-8 text-base leading-relaxed space-y-8">
- <section>
- <h2 className="text-2xl font-semibold mb-4">
- Welcome to OpenCut
- </h2>
- <p className="mb-4">
- OpenCut is a free, open-source video editor that runs in
- your browser. By using our service, you agree to these
- terms. We've designed these terms to be fair and protect
- both you and our project.
- </p>
- <p>
- <strong>Key principle:</strong> Your content stays on your
- device. We never claim ownership of your videos or projects.
- </p>
- </section>
+ return (
+ <BasePage
+ title="Terms of service"
+ description="Fair and transparent terms for our free, open-source video editor. Contact us if you have any questions."
+ >
+ <Accordion type="single" collapsible className="w-full">
+ <AccordionItem
+ value="quick-summary"
+ className="rounded-2xl border px-5"
+ >
+ <AccordionTrigger className="no-underline!">
+ Quick summary
+ </AccordionTrigger>
+ <AccordionContent>
+ <h3 className="mb-3 text-lg font-medium">
+ You own your content, we own nothing.
+ </h3>
+ <ol className="list-decimal space-y-2 pl-6">
+ <li>
+ Your content stays private - basic editing is local, AI features
+ use encrypted uploads
+ </li>
+ <li>
+ We never claim ownership of your content, even when processing
+ AI features
+ </li>
+ <li>
+ Free for personal and commercial use with no watermarks or
+ restrictions
+ </li>
+ <li>Don't use OpenCut for illegal activities or harassment</li>
+ <li>
+ Service provided "as is" - we can't guarantee perfect uptime
+ </li>
+ <li>
+ Open source means you can review our code and self-host if
+ needed
+ </li>
+ <li>
+ You can delete your account anytime and keep using your exported
+ videos
+ </li>
+ </ol>
+ <p className="mt-4">
+ Questions? Email us at{" "}
+ <a
+ href="mailto:oss@opencut.app"
+ className="text-primary hover:underline"
+ >
+ oss@opencut.app
+ </a>
+ </p>
+ </AccordionContent>
+ </AccordionItem>
+ </Accordion>
- <section>
- <h2 className="text-2xl font-semibold mb-4">
- Your Content, Your Rights
- </h2>
- <p className="mb-4">
- <strong>You own everything you create.</strong> OpenCut
- processes your videos locally on your device, so we never
- have access to your content. We make no claims to ownership,
- licensing, or rights over your videos, projects, or any
- content you create using OpenCut.
- </p>
- <ul className="list-disc pl-6 mb-4 space-y-2">
- <li>
- Your videos remain completely private and under your
- control
- </li>
- <li>
- You retain all intellectual property rights to your
- content
- </li>
- <li>
- You can export and use your content however you choose
- </li>
- <li>
- No watermarks, no licensing restrictions from OpenCut
- </li>
- </ul>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">Your Content, Your Rights</h2>
+ <p>
+ <strong>You own everything you create.</strong> OpenCut processes
+ basic editing locally on your device. For AI features, content is
+ encrypted before upload and we cannot access your original files. We
+ make no claims to ownership, licensing, or rights over your videos,
+ projects, or any content you create using OpenCut.
+ </p>
+ <ul className="list-disc space-y-2 pl-6">
+ <li>
+ Your content remains private and under your control at all times
+ </li>
+ <li>You retain all intellectual property rights to your content</li>
+ <li>
+ Even when using AI features, we cannot access your unencrypted
+ content
+ </li>
+ <li>You can export and use your content however you choose</li>
+ <li>No watermarks, no licensing restrictions from OpenCut</li>
+ </ul>
+ </section>
- <section>
- <h2 className="text-2xl font-semibold mb-4">
- How You Can Use OpenCut
- </h2>
- <p className="mb-4">
- OpenCut is free for personal and commercial use. You can:
- </p>
- <ul className="list-disc pl-6 mb-4 space-y-2">
- <li>
- Create videos for personal, educational, or commercial
- purposes
- </li>
- <li>Use OpenCut for client work and paid projects</li>
- <li>Share and distribute videos created with OpenCut</li>
- <li>
- Modify and distribute the OpenCut software (under MIT
- license)
- </li>
- </ul>
- <p>
- <strong>What we ask:</strong> Don't use OpenCut for illegal
- activities, harassment, or creating harmful content. Be
- respectful of others and follow applicable laws.
- </p>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">How You Can Use OpenCut</h2>
+ <p>OpenCut is free for personal and commercial use. You can:</p>
+ <ul className="list-disc space-y-2 pl-6">
+ <li>
+ Create videos for personal, educational, or commercial purposes
+ </li>
+ <li>Use OpenCut for client work and paid projects</li>
+ <li>Share and distribute videos created with OpenCut</li>
+ <li>
+ Modify and distribute the OpenCut software (under MIT license)
+ </li>
+ </ul>
+ <p>
+ <strong>What we ask:</strong> Don't use OpenCut for illegal
+ activities, harassment, or creating harmful content. Be respectful of
+ others and follow applicable laws.
+ </p>
+ </section>
- <section>
- <h2 className="text-2xl font-semibold mb-4">
- Account and Service
- </h2>
- <p className="mb-4">
- To use certain features, you may create an account:
- </p>
- <ul className="list-disc pl-6 mb-4 space-y-2">
- <li>Provide accurate information when signing up</li>
- <li>
- Keep your account secure and don't share credentials
- </li>
- <li>You're responsible for activity under your account</li>
- <li>You can delete your account at any time</li>
- </ul>
- <p>
- OpenCut is provided "as is" without warranties. While we
- strive for reliability, we can't guarantee uninterrupted
- service.
- </p>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">
+ AI Features and Data Processing
+ </h2>
+ <p>
+ OpenCut offers optional AI-powered features that require server
+ processing:
+ </p>
+ <ul className="list-disc space-y-2 pl-6">
+ <li>
+ AI features (auto captions, content analysis, etc.) are completely
+ optional
+ </li>
+ <li>Your content is encrypted on your device before any upload</li>
+ <li>
+ We use zero-knowledge encryption - we cannot decrypt your content
+ </li>
+ <li>Encrypted content is deleted immediately after processing</li>
+ <li>
+ You maintain full ownership and control of your content throughout
+ </li>
+ </ul>
+ <p>
+ By using AI features, you consent to the temporary, encrypted
+ processing of your content as described in our Privacy Policy. You can
+ always choose to use only local editing features.
+ </p>
+ </section>
- <section>
- <h2 className="text-2xl font-semibold mb-4">
- Open Source Benefits
- </h2>
- <p className="mb-4">
- Because OpenCut is open source, you have additional rights:
- </p>
- <ul className="list-disc pl-6 mb-4 space-y-2">
- <li>
- Review our code to see exactly how we handle your data
- </li>
- <li>Self-host OpenCut on your own servers</li>
- <li>Modify the software to suit your needs</li>
- <li>Contribute improvements back to the community</li>
- </ul>
- <p>
- View our source code and license on{" "}
- <a
- href="https://github.com/OpenCut-app/OpenCut"
- target="_blank"
- rel="noopener"
- className="text-primary hover:underline"
- >
- GitHub
- </a>
- .
- </p>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">Account and Service</h2>
+ <p>To use certain features, you may create an account:</p>
+ <ul className="list-disc space-y-2 pl-6">
+ <li>Provide accurate information when signing up</li>
+ <li>Keep your account secure and don't share credentials</li>
+ <li>You're responsible for activity under your account</li>
+ <li>You can delete your account at any time</li>
+ </ul>
+ <p>
+ OpenCut is provided "as is" without warranties. While we strive for
+ reliability, we can't guarantee uninterrupted service.
+ </p>
+ </section>
- <section>
- <h2 className="text-2xl font-semibold mb-4">
- Third-Party Content
- </h2>
- <p className="mb-4">
- When using OpenCut, make sure you have the right to use any
- content you import:
- </p>
- <ul className="list-disc pl-6 mb-4 space-y-2">
- <li>
- Only upload content you own or have permission to use
- </li>
- <li>
- Respect copyright, trademarks, and other intellectual
- property
- </li>
- <li>
- Don't use copyrighted music, images, or videos without
- permission
- </li>
- <li>
- You're responsible for any claims related to your content
- </li>
- </ul>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">Open Source Benefits</h2>
+ <p>Because OpenCut is open source, you have additional rights:</p>
+ <ul className="list-disc space-y-2 pl-6">
+ <li>Review our code to see exactly how we handle your data</li>
+ <li>Self-host OpenCut on your own servers</li>
+ <li>Modify the software to suit your needs</li>
+ <li>Contribute improvements back to the community</li>
+ </ul>
+ <p>
+ View our source code and license on{" "}
+ <a
+ href={SOCIAL_LINKS.github}
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ GitHub
+ </a>
+ .
+ </p>
+ </section>
- <section>
- <h2 className="text-2xl font-semibold mb-4">
- Limitations and Liability
- </h2>
- <p className="mb-4">
- OpenCut is provided free of charge. To the extent permitted
- by law:
- </p>
- <ul className="list-disc pl-6 mb-4 space-y-2">
- <li>We're not liable for any loss of data or content</li>
- <li>
- Projects are stored in your browser and may be lost if you
- clear browser data
- </li>
- <li>We're not responsible for how you use the service</li>
- <li>
- Our liability is limited to the maximum extent allowed by
- law
- </li>
- </ul>
- <p>
- Since your content stays on your device, we have no way to
- recover lost projects. Consider exporting important videos
- when finished editing.
- </p>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">Third-Party Content</h2>
+ <p>
+ When using OpenCut, make sure you have the right to use any content
+ you import:
+ </p>
+ <ul className="list-disc space-y-2 pl-6">
+ <li>Only upload content you own or have permission to use</li>
+ <li>
+ Respect copyright, trademarks, and other intellectual property
+ </li>
+ <li>
+ Don't use copyrighted music, images, or videos without permission
+ </li>
+ <li>You're responsible for any claims related to your content</li>
+ </ul>
+ </section>
- <section>
- <h2 className="text-2xl font-semibold mb-4">
- Service Changes
- </h2>
- <p className="mb-4">We may update OpenCut and these terms:</p>
- <ul className="list-disc pl-6 mb-4 space-y-2">
- <li>
- We'll notify you of significant changes to these terms
- </li>
- <li>Continued use means you accept any updates</li>
- <li>
- You can always self-host an older version if you prefer
- </li>
- <li>
- Major changes will be discussed with the community on
- GitHub
- </li>
- </ul>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">Limitations and Liability</h2>
+ <p>
+ OpenCut is provided free of charge. To the extent permitted by law:
+ </p>
+ <ul className="list-disc space-y-2 pl-6">
+ <li>We're not liable for any loss of data or content</li>
+ <li>
+ Projects are stored in your browser and may be lost if you clear
+ browser data
+ </li>
+ <li>We're not responsible for how you use the service</li>
+ <li>Our liability is limited to the maximum extent allowed by law</li>
+ </ul>
+ <p>
+ Since your content stays on your device, we have no way to recover
+ lost projects. Consider exporting important videos when finished
+ editing.
+ </p>
+ </section>
- <section>
- <h2 className="text-2xl font-semibold mb-4">Termination</h2>
- <p className="mb-4">
- You can stop using OpenCut at any time:
- </p>
- <ul className="list-disc pl-6 mb-4 space-y-2">
- <li>Delete your account through your profile settings</li>
- <li>Clear your browser data to remove local projects</li>
- <li>
- Your content remains yours even if you stop using OpenCut
- </li>
- <li>
- We may suspend accounts for violations of these terms
- </li>
- </ul>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">Service Changes</h2>
+ <p>We may update OpenCut and these terms:</p>
+ <ul className="list-disc space-y-2 pl-6">
+ <li>We'll notify you of significant changes to these terms</li>
+ <li>Continued use means you accept any updates</li>
+ <li>You can always self-host an older version if you prefer</li>
+ <li>Major changes will be discussed with the community on GitHub</li>
+ </ul>
+ </section>
- <section>
- <h2 className="text-2xl font-semibold mb-4">
- Contact and Disputes
- </h2>
- <p className="mb-4">
- Questions about these terms or need to report an issue?
- </p>
- <p className="mb-4">
- Contact us through our{" "}
- <a
- href="https://github.com/OpenCut-app/OpenCut/issues"
- target="_blank"
- rel="noopener"
- className="text-primary hover:underline"
- >
- GitHub repository
- </a>
- , email us at{" "}
- <a
- href="mailto:oss@opencut.app"
- className="text-primary hover:underline"
- >
- oss@opencut.app
- </a>
- , or reach out on{" "}
- <a
- href="https://x.com/opencutapp"
- target="_blank"
- rel="noopener"
- className="text-primary hover:underline"
- >
- X (Twitter)
- </a>
- .
- </p>
- <p>
- These terms are governed by applicable law in your
- jurisdiction. We prefer to resolve disputes through friendly
- discussion in our open-source community.
- </p>
- </section>
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">Termination</h2>
+ <p>You can stop using OpenCut at any time:</p>
+ <ul className="list-disc space-y-2 pl-6">
+ <li>Delete your account through your profile settings</li>
+ <li>Clear your browser data to remove local projects</li>
+ <li>Your content remains yours even if you stop using OpenCut</li>
+ <li>We may suspend accounts for violations of these terms</li>
+ </ul>
+ </section>
- <p className="text-sm text-muted-foreground mt-8 pt-8 border-t border-muted/20">
- Last updated: July 14, 2025
- </p>
- </CardContent>
- </Card>
- </div>
- </div>
- </main>
- <Footer />
- </div>
- );
+ <section className="flex flex-col gap-3">
+ <h2 className="text-2xl font-semibold">Contact Us</h2>
+ <p>Questions about these terms or need to report an issue?</p>
+ <p>
+ Contact us through our{" "}
+ <a
+ href={`${SOCIAL_LINKS.github}/issues`}
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ GitHub repository
+ </a>
+ , email us at{" "}
+ <a
+ href="mailto:oss@opencut.app"
+ className="text-primary hover:underline"
+ >
+ oss@opencut.app
+ </a>
+ , or reach out on{" "}
+ <a
+ href={SOCIAL_LINKS.x}
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ X (Twitter)
+ </a>
+ .
+ </p>
+ <p>
+ These terms are governed by applicable law in your jurisdiction. We
+ prefer to resolve disputes through friendly discussion in our
+ open-source community.
+ </p>
+ </section>
+ <Separator />
+ <p className="text-muted-foreground text-sm">
+ Last updated: July 14, 2025
+ </p>
+ </BasePage>
+ );
}
diff --git a/apps/web/src/app/why-not-capcut/page.tsx b/apps/web/src/app/why-not-capcut/page.tsx
deleted file mode 100644
index b05860a6..00000000
--- a/apps/web/src/app/why-not-capcut/page.tsx
+++ /dev/null
@@ -1,191 +0,0 @@
-import { Header } from "@/components/header";
-
-export default function WhyNotCapcut() {
- return (
- <div className="min-h-screen bg-background px-5">
- <Header />
-
- <main className="relative mt-12">
- <div className="absolute inset-0 overflow-hidden pointer-events-none">
- <div className="absolute -top-40 -right-40 w-96 h-96 bg-linear-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
- <div className="absolute top-1/2 -left-40 w-80 h-80 bg-linear-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
- </div>
-
- <div className="relative container mx-auto px-4 py-16">
- <div className="max-w-6xl mx-auto">
- <div className="text-center mb-20">
- <h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
- Fuck CapCut
- </h1>
- <p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
- Roasting time, so get ready motherfucker.
- </p>
- </div>
-
- <div className="max-w-4xl mx-auto space-y-12">
- <div>
- <h2 className="text-3xl font-bold mb-6">
- Seriously, what the fuck else do you want?
- </h2>
- <p className="text-lg mb-6">
- You probably use CapCut and think your video editing is
- special. You think your fucking TikTok with 47 transitions and
- 12 different fonts is going to get you some viral fame. You
- think loading up every goddamn effect in their library makes
- your content better. Wrong, motherfucker. Let me describe what
- CapCut actually gives you:
- </p>
- <ul className="text-lg space-y-2 mb-6 list-disc list-inside">
- <li>A paywall every time you breathe</li>
- <li>Terms of service that steal your shit</li>
- <li>
- More "Get Pro" dialogs than a Windows 95 error message
- </li>
- <li>
- Features that disappear behind paywalls while you're fucking
- using them
- </li>
- <li>Bugs disguised as "premium features"</li>
- </ul>
- <p className="text-lg mb-6">
- <strong>Well guess what, motherfucker:</strong>
- </p>
- <p className="text-lg mb-6">
- You. Are. Getting. Scammed. Look at this shit. It's a fucking
- video editor. Why the fuck do you need to pay $20/month just
- to remove a goddamn watermark? You spent hours editing your
- video and they slap their logo on it like they fucking made
- it.
- </p>
- </div>
-
- <div>
- <h2 className="text-3xl font-bold mb-6">
- The "Get Pro" dialog is everywhere
- </h2>
- <p className="text-lg mb-6">
- This motherfucking dialog pops up more than ads on a pirated
- movie site. Want to add a transition? Get Pro. Want to export
- without their watermark? Get Pro. Want to use more than 2
- fonts? Get fucking Pro, peasant.
- </p>
- <p className="text-lg mb-6">
- Did you seriously think you could edit a video without seeing
- this dialog 47 times? You click one button and BAM - there it
- is again, asking for your credit card like a desperate ex
- asking for money.
- </p>
- </div>
-
- <div>
- <h2 className="text-3xl font-bold mb-6">
- Everything costs money now
- </h2>
- <p className="text-lg mb-6">
- You dumbass. You thought CapCut was free, but no. Free means
- they let you open the app. Everything else costs money. Basic
- shake effect? That'll be $20/month. A decent transition that
- isn't "fade"? Pay up, motherfucker.
- </p>
- <p className="text-lg mb-6">
- Here's my favorite piece of bullshit: You import an MP3 file -
- you know, AUDIO - and try to export. "Sorry, can't export
- because you're using our premium extract audio feature!"
- </p>
- <p className="text-lg mb-6">
- <strong>
- My MP3 was already fucking audio, you absolute morons.
- </strong>
- </p>
- <p className="text-lg mb-6">
- But wait, there's more! If you drag that same MP3 to their
- media panel first, then to the timeline, it magically works.
- This isn't a bug, it's a fucking scam disguised as software
- engineering.
- </p>
- </div>
-
- <div>
- <h2 className="text-3xl font-bold mb-6">
- Their Terms of Service are insane
- </h2>
- <p className="text-lg mb-6">
- Look at this shit. You upload your content and they basically
- say "thanks for the free content, we own it now, but if Disney
- sues anyone, that's your problem."
- </p>
- <p className="text-lg mb-6">
- <strong>CapCut's Terms of Service:</strong> We get full rights
- to use, modify, distribute, and monetize everything you upload
- - permanently and without paying you shit. But you're still
- responsible if anything goes wrong.
- </p>
- <p className="text-lg mb-6">
- Translation: "We'll make money off your viral video, you
- handle the lawsuits." Brilliant legal strategy, you fucks.
- </p>
- </div>
-
- <div>
- <h2 className="text-3xl font-bold mb-6">
- The editor is actually good
- </h2>
- <p className="text-lg mb-6">
- Here's the thing that makes me want to punch my monitor: the
- actual video editor is fucking good. It's intuitive, powerful,
- and anyone can figure it out. When it's not begging for money
- every 30 seconds, it actually works well.
- </p>
- <p className="text-lg mb-6">
- Which makes everything else so much worse. They built
- something people want to use, then turned it into a digital
- slot machine. Every click might trigger a payment request.
- </p>
- </div>
-
- <div>
- <h2 className="text-3xl font-bold mb-6">
- This is a video editor. Look at it. You've never seen one
- before.
- </h2>
- <p className="text-lg mb-6">
- Like the person who's never used software that doesn't
- constantly beg for money, you have no fucking idea what a
- video editor should be. All you've ever seen are predatory
- apps disguised as creative tools.
- </p>
- <p className="text-lg mb-6">
- A real video editor lets you edit videos. It doesn't steal
- your content. It doesn't pop up payment dialogs every 5
- seconds. It doesn't charge you separately for basic features
- that should be free.
- </p>
- </div>
-
- <div>
- <h2 className="text-3xl font-bold mb-6">
- Yes, this is fucking satire, you fuck
- </h2>
- <p className="text-lg mb-6">
- I'm not actually saying all video editors should be basic as
- shit. What I'm saying is that all the problems we have with
- video editing apps are{" "}
- <strong>ones they create themselves</strong>. Video editors
- aren't broken by default - they edit videos, export them, and
- let you use basic features without constantly begging for
- money. CapCut breaks them. They turn them into payment
- processors with video editing as a side feature.
- </p>
- <p className="text-lg">
- <em>"Good software gets out of your way."</em>
- <br />- Some smart motherfucker who definitely wasn't working
- at CapCut
- </p>
- </div>
- </div>
- </div>
- </div>
- </main>
- </div>
- );
-}
diff --git a/apps/web/src/components/delete-project-dialog.tsx b/apps/web/src/components/delete-project-dialog.tsx
deleted file mode 100644
index e8f8703c..00000000
--- a/apps/web/src/components/delete-project-dialog.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { cn } from "@/lib/utils";
-
-export function DeleteProjectDialog({
- isOpen,
- onOpenChange,
- onConfirm,
- projectName,
-}: {
- isOpen: boolean;
- onOpenChange: (open: boolean) => void;
- onConfirm: () => void;
- projectName?: string;
-}) {
- return (
- <Dialog open={isOpen} onOpenChange={onOpenChange}>
- <DialogContent
- onOpenAutoFocus={(e) => {
- e.preventDefault();
- e.stopPropagation();
- }}
- >
- <DialogHeader>
- <DialogTitle>
- {projectName ? (
- <>
- {"Delete '"}
- <span className="inline-block max-w-[300px] truncate align-bottom">
- {projectName}
- </span>
- {"'?"}
- </>
- ) : (
- "Delete Project?"
- )}
- </DialogTitle>
- <DialogDescription>
- Are you sure you want to delete this project? This action cannot be
- undone.
- </DialogDescription>
- </DialogHeader>
- <DialogFooter>
- <Button
- variant="text"
- onClick={(e) => {
- e.preventDefault();
- e.stopPropagation();
- onOpenChange(false);
- }}
- >
- Cancel
- </Button>
- <Button variant="destructive" onClick={onConfirm}>
- Delete
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/apps/web/src/components/editable-timecode.tsx b/apps/web/src/components/editable-timecode.tsx
new file mode 100644
index 00000000..d32f067a
--- /dev/null
+++ b/apps/web/src/components/editable-timecode.tsx
@@ -0,0 +1,154 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { formatTimeCode, parseTimeCode } from "@/lib/time";
+import type { TTimeCode } from "@/types/time";
+import { cn } from "@/utils/ui";
+
+interface EditableTimecodeProps {
+ time: number;
+ duration: number;
+ format?: TTimeCode;
+ fps: number;
+ onTimeChange?: ({ time }: { time: number }) => void;
+ className?: string;
+ disabled?: boolean;
+}
+
+export function EditableTimecode({
+ time,
+ duration,
+ format = "HH:MM:SS:FF",
+ fps,
+ onTimeChange,
+ className,
+ disabled = false,
+}: EditableTimecodeProps) {
+ const [isEditing, setIsEditing] = useState(false);
+ const [inputValue, setInputValue] = useState("");
+ const [hasError, setHasError] = useState(false);
+ const inputRef = useRef<HTMLInputElement>(null);
+ const enterPressedRef = useRef(false);
+ const formattedTime = formatTimeCode({ timeInSeconds: time, format, fps });
+
+ const startEditing = () => {
+ if (disabled) return;
+ setIsEditing(true);
+ setInputValue(formattedTime);
+ setHasError(false);
+ enterPressedRef.current = false;
+ };
+
+ const cancelEditing = () => {
+ setIsEditing(false);
+ setInputValue("");
+ setHasError(false);
+ enterPressedRef.current = false;
+ };
+
+ const applyEdit = () => {
+ const parsedTime = parseTimeCode({ timeCode: inputValue, format, fps });
+
+ if (parsedTime === null) {
+ setHasError(true);
+ return;
+ }
+
+ const clampedTime = Math.max(
+ 0,
+ duration ? Math.min(duration, parsedTime) : parsedTime,
+ );
+
+ onTimeChange?.({ time: clampedTime });
+ setIsEditing(false);
+ setInputValue("");
+ setHasError(false);
+ enterPressedRef.current = false;
+ };
+
+ const handleKeyDown = ({
+ key,
+ preventDefault,
+ }: React.KeyboardEvent<HTMLInputElement>) => {
+ if (key === "Enter") {
+ preventDefault();
+ enterPressedRef.current = true;
+ applyEdit();
+ } else if (key === "Escape") {
+ preventDefault();
+ cancelEditing();
+ }
+ };
+
+ const handleInputChange = ({
+ target,
+ }: React.ChangeEvent<HTMLInputElement>) => {
+ setInputValue(target.value);
+ setHasError(false);
+ };
+
+ const handleBlur = () => {
+ if (!enterPressedRef.current && isEditing) {
+ applyEdit();
+ }
+ };
+
+ const handleDisplayKeyDown = ({
+ key,
+ preventDefault,
+ }: React.KeyboardEvent<HTMLButtonElement>) => {
+ if (disabled) return;
+
+ if (key === "Enter" || key === " ") {
+ preventDefault();
+ startEditing();
+ }
+ };
+
+ useEffect(() => {
+ if (isEditing && inputRef.current) {
+ inputRef.current.focus();
+ inputRef.current.select();
+ }
+ }, [isEditing]);
+
+ if (isEditing) {
+ return (
+ <input
+ ref={inputRef}
+ type="text"
+ value={inputValue}
+ onChange={handleInputChange}
+ onKeyDown={handleKeyDown}
+ onBlur={handleBlur}
+ className={cn(
+ "border-none bg-transparent font-mono text-xs outline-none",
+ "focus:bg-background focus:border-primary focus:rounded focus:border focus:px-1",
+ "text-primary tabular-nums",
+ hasError && "text-destructive focus:border-destructive",
+ className,
+ )}
+ style={{ width: `${formattedTime.length + 1}ch` }}
+ placeholder={formattedTime}
+ />
+ );
+ }
+
+ return (
+ <button
+ type="button"
+ onClick={startEditing}
+ onKeyDown={handleDisplayKeyDown}
+ disabled={disabled}
+ className={cn(
+ "text-primary cursor-pointer font-mono text-xs tabular-nums",
+ "hover:bg-muted/50 -mx-1 px-1 hover:rounded",
+ disabled && "cursor-default hover:bg-transparent",
+ className,
+ )}
+ title={disabled ? undefined : "Click to edit time"}
+ >
+ {formattedTime}
+ </button>
+ );
+}
diff --git a/apps/web/src/components/editor/audio-waveform.tsx b/apps/web/src/components/editor/audio-waveform.tsx
deleted file mode 100644
index 60406f61..00000000
--- a/apps/web/src/components/editor/audio-waveform.tsx
+++ /dev/null
@@ -1,162 +0,0 @@
-import React, { useEffect, useRef, useState } from "react";
-import WaveSurfer from "wavesurfer.js";
-
-interface AudioWaveformProps {
- audioUrl: string;
- height?: number;
- className?: string;
-}
-
-const AudioWaveform: React.FC<AudioWaveformProps> = ({
- audioUrl,
- height = 32,
- className = "",
-}) => {
- const waveformRef = useRef<HTMLDivElement>(null);
- const wavesurfer = useRef<WaveSurfer | null>(null);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(false);
-
- useEffect(() => {
- let mounted = true;
- let ws = wavesurfer.current;
-
- const initWaveSurfer = async () => {
- if (!waveformRef.current || !audioUrl) return;
-
- try {
- // Clear any existing instance safely
- if (ws) {
- // Instead of immediately destroying, just set to null
- // We'll destroy it outside this function
- wavesurfer.current = null;
- }
-
- // Create a fresh instance
- const newWaveSurfer = WaveSurfer.create({
- container: waveformRef.current,
- waveColor: "rgba(255, 255, 255, 0.6)",
- progressColor: "rgba(255, 255, 255, 0.9)",
- cursorColor: "transparent",
- barWidth: 2,
- barGap: 1,
- height,
- normalize: true,
- interact: false,
- });
-
- // Assign to ref only if component is still mounted
- if (mounted) {
- wavesurfer.current = newWaveSurfer;
- } else {
- // Component unmounted during initialization, clean up
- try {
- newWaveSurfer.destroy();
- } catch (e) {
- // Ignore destroy errors
- }
- return;
- }
-
- // Event listeners
- newWaveSurfer.on("ready", () => {
- if (mounted) {
- setIsLoading(false);
- setError(false);
- }
- });
-
- newWaveSurfer.on("error", (err) => {
- if (mounted) {
- console.error("WaveSurfer error:", err);
- setError(true);
- setIsLoading(false);
- }
- });
-
- await newWaveSurfer.load(audioUrl);
- } catch (err) {
- if (mounted) {
- console.error("Failed to initialize WaveSurfer:", err);
- setError(true);
- setIsLoading(false);
- }
- }
- };
-
- // First safely destroy previous instance if it exists
- if (ws) {
- // Use this pattern to safely destroy the previous instance
- const wsToDestroy = ws;
- // Detach from ref immediately
- wavesurfer.current = null;
-
- // Wait a tick to destroy so any pending operations can complete
- requestAnimationFrame(() => {
- try {
- wsToDestroy.destroy();
- } catch (e) {
- // Ignore errors during destroy
- }
- // Only initialize new instance after destroying the old one
- if (mounted) {
- initWaveSurfer();
- }
- });
- } else {
- // No previous instance to clean up, initialize directly
- initWaveSurfer();
- }
-
- return () => {
- // Mark component as unmounted
- mounted = false;
-
- // Store reference to current wavesurfer instance
- const wsToDestroy = wavesurfer.current;
-
- // Immediately clear the ref to prevent accessing it after unmount
- wavesurfer.current = null;
-
- // If we have an instance to clean up, do it safely
- if (wsToDestroy) {
- // Delay destruction to avoid race conditions
- requestAnimationFrame(() => {
- try {
- wsToDestroy.destroy();
- } catch (e) {
- // Ignore destroy errors - they're expected
- }
- });
- }
- };
- }, [audioUrl, height]);
-
- if (error) {
- return (
- <div
- className={`flex items-center justify-center ${className}`}
- style={{ height }}
- >
- <span className="text-xs text-foreground/60">Audio unavailable</span>
- </div>
- );
- }
-
- return (
- <div className={`relative ${className}`}>
- {isLoading && (
- <div className="absolute inset-0 flex items-center justify-center">
- <span className="text-xs text-foreground/60">Loading...</span>
- </div>
- )}
- <div
- ref={waveformRef}
- className={`w-full transition-opacity duration-200 ${isLoading ? "opacity-0" : "opacity-100"}`}
- style={{ height }}
- />
- </div>
- );
-};
-
-export default AudioWaveform;
diff --git a/apps/web/src/components/editor/dialogs/delete-project-dialog.tsx b/apps/web/src/components/editor/dialogs/delete-project-dialog.tsx
new file mode 100644
index 00000000..0086384e
--- /dev/null
+++ b/apps/web/src/components/editor/dialogs/delete-project-dialog.tsx
@@ -0,0 +1,84 @@
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogBody,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Label } from "@/components/ui/label";
+import { Input } from "@/components/ui/input";
+
+export function DeleteProjectDialog({
+ isOpen,
+ onOpenChange,
+ onConfirm,
+ projectNames,
+}: {
+ isOpen: boolean;
+ onOpenChange: (open: boolean) => void;
+ onConfirm: () => void;
+ projectNames: string[];
+}) {
+ const count = projectNames.length;
+ const isSingle = count === 1;
+ const singleName = isSingle ? projectNames[0] : null;
+
+ return (
+ <Dialog open={isOpen} onOpenChange={onOpenChange}>
+ <DialogContent
+ onOpenAutoFocus={(event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ }}
+ >
+ <DialogHeader>
+ <DialogTitle>
+ {singleName ? (
+ <>
+ {"Delete '"}
+ <span className="inline-block max-w-[300px] truncate align-bottom">
+ {singleName}
+ </span>
+ {"'?"}
+ </>
+ ) : (
+ `Delete ${count} projects?`
+ )}
+ </DialogTitle>
+ </DialogHeader>
+ <DialogBody>
+ <Alert variant="destructive">
+ <AlertTitle>Warning</AlertTitle>
+ <AlertDescription>
+ This will permanently delete{" "}
+ {singleName ? `"${singleName}"` : `${count} projects`} and all
+ associated files.
+ </AlertDescription>
+ </Alert>
+ <div className="flex flex-col gap-3">
+ <Label className="text-xs font-semibold text-slate-500">
+ Type "DELETE" to confirm
+ </Label>
+ <Input
+ type="text"
+ placeholder="DELETE"
+ size="lg"
+ variant="destructive"
+ />
+ </div>
+ </DialogBody>
+ <DialogFooter>
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
+ Cancel
+ </Button>
+ <Button variant="destructive" onClick={onConfirm}>
+ Delete project
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
diff --git a/apps/web/src/components/editor/dialogs/migration-dialog.tsx b/apps/web/src/components/editor/dialogs/migration-dialog.tsx
new file mode 100644
index 00000000..4c9550c2
--- /dev/null
+++ b/apps/web/src/components/editor/dialogs/migration-dialog.tsx
@@ -0,0 +1,44 @@
+"use client";
+
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { useEditor } from "@/hooks/use-editor";
+import { Loader2 } from "lucide-react";
+
+export function MigrationDialog() {
+ const editor = useEditor();
+ const migrationState = editor.project.getMigrationState();
+
+ if (!migrationState.isMigrating) return null;
+
+ const title = migrationState.projectName
+ ? "Updating project"
+ : "Updating projects";
+ const description = migrationState.projectName
+ ? `Upgrading "${migrationState.projectName}" from v${migrationState.fromVersion} to v${migrationState.toVersion}`
+ : `Upgrading projects from v${migrationState.fromVersion} to v${migrationState.toVersion}`;
+
+ return (
+ <Dialog open={true}>
+ <DialogContent
+ className="sm:max-w-md"
+ onPointerDownOutside={(event) => event.preventDefault()}
+ onEscapeKeyDown={(event) => event.preventDefault()}
+ >
+ <DialogHeader>
+ <DialogTitle>{title}</DialogTitle>
+ <DialogDescription>{description}</DialogDescription>
+ </DialogHeader>
+
+ <div className="flex items-center justify-center py-4">
+ <Loader2 className="text-muted-foreground size-8 animate-spin" />
+ </div>
+ </DialogContent>
+ </Dialog>
+ );
+}
diff --git a/apps/web/src/components/editor/dialogs/project-info-dialog.tsx b/apps/web/src/components/editor/dialogs/project-info-dialog.tsx
new file mode 100644
index 00000000..7b736b23
--- /dev/null
+++ b/apps/web/src/components/editor/dialogs/project-info-dialog.tsx
@@ -0,0 +1,83 @@
+import {
+ Dialog,
+ DialogBody,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import type { TProjectMetadata } from "@/types/project";
+import { formatDate } from "@/utils/date";
+import { formatTimeCode } from "@/lib/time";
+import { Button } from "@/components/ui/button";
+
+function InfoRow({
+ label,
+ value,
+}: {
+ label: string;
+ value: string | React.ReactNode;
+}) {
+ return (
+ <div className="flex justify-between items-center py-0 last:pb-0">
+ <span className="text-muted-foreground text-sm">{label}</span>
+ <span className="text-sm font-medium">{value}</span>
+ </div>
+ );
+}
+
+export function ProjectInfoDialog({
+ isOpen,
+ onOpenChange,
+ project,
+}: {
+ isOpen: boolean;
+ onOpenChange: (open: boolean) => void;
+ project: TProjectMetadata;
+}) {
+ const durationFormatted =
+ project.duration > 0
+ ? formatTimeCode({
+ timeInSeconds: project.duration,
+ format: project.duration >= 3600 ? "HH:MM:SS" : "MM:SS",
+ })
+ : "0:00";
+
+ return (
+ <Dialog open={isOpen} onOpenChange={onOpenChange}>
+ <DialogContent onOpenAutoFocus={(event) => event.preventDefault()}>
+ <DialogHeader>
+ <DialogTitle className="truncate max-w-[350px]">
+ {project.name}
+ </DialogTitle>
+ </DialogHeader>
+
+ <DialogBody className="flex flex-col">
+ <InfoRow label="Duration" value={durationFormatted} />
+ <InfoRow
+ label="Created"
+ value={formatDate({ date: project.createdAt })}
+ />
+ <InfoRow
+ label="Modified"
+ value={formatDate({ date: project.updatedAt })}
+ />
+ <InfoRow
+ label="Project ID"
+ value={
+ <code className="text-xs bg-muted px-1.5 py-0.5 rounded">
+ {project.id.slice(0, 8)}
+ </code>
+ }
+ />
+ </DialogBody>
+ <DialogFooter>
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
+ Close
+ </Button>
+ <Button onClick={() => onOpenChange(false)}>Done</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
diff --git a/apps/web/src/components/editor/dialogs/rename-project-dialog.tsx b/apps/web/src/components/editor/dialogs/rename-project-dialog.tsx
new file mode 100644
index 00000000..73024d71
--- /dev/null
+++ b/apps/web/src/components/editor/dialogs/rename-project-dialog.tsx
@@ -0,0 +1,72 @@
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogBody,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { useState } from "react";
+import { Label } from "@/components/ui/label";
+
+export function RenameProjectDialog({
+ isOpen,
+ onOpenChange,
+ onConfirm,
+ projectName,
+}: {
+ isOpen: boolean;
+ onOpenChange: (open: boolean) => void;
+ onConfirm: (newName: string) => void;
+ projectName: string;
+}) {
+ const [name, setName] = useState(projectName);
+
+ const handleOpenChange = (open: boolean) => {
+ if (open) {
+ setName(projectName);
+ }
+ onOpenChange(open);
+ };
+
+ return (
+ <Dialog open={isOpen} onOpenChange={handleOpenChange}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Rename project</DialogTitle>
+ </DialogHeader>
+
+ <DialogBody className="gap-3">
+ <Label>New name</Label>
+ <Input
+ value={name}
+ onChange={(e) => setName(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ onConfirm(name);
+ }
+ }}
+ placeholder="Enter a new name"
+ />
+ </DialogBody>
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onOpenChange(false);
+ }}
+ >
+ Cancel
+ </Button>
+ <Button onClick={() => onConfirm(name)}>Rename</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
diff --git a/apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx b/apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx
new file mode 100644
index 00000000..565fe705
--- /dev/null
+++ b/apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx
@@ -0,0 +1,229 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import {
+ type KeyboardShortcut,
+ useKeyboardShortcutsHelp,
+} from "@/hooks/use-keyboard-shortcuts-help";
+import { useKeybindingsStore } from "@/stores/keybindings-store";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogBody,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+
+export function ShortcutsDialog({
+ isOpen,
+ onOpenChange,
+}: {
+ isOpen: boolean;
+ onOpenChange: (open: boolean) => void;
+}) {
+ const [recordingShortcut, setRecordingShortcut] =
+ useState<KeyboardShortcut | null>(null);
+
+ const {
+ updateKeybinding,
+ removeKeybinding,
+ getKeybindingString,
+ validateKeybinding,
+ getKeybindingsForAction,
+ setIsRecording,
+ resetToDefaults,
+ isRecording,
+ } = useKeybindingsStore();
+
+ const { shortcuts } = useKeyboardShortcutsHelp();
+
+ const categories = Array.from(new Set(shortcuts.map((s) => s.category)));
+
+ useEffect(() => {
+ if (!isRecording || !recordingShortcut) return;
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const keyString = getKeybindingString(e);
+ if (keyString) {
+ const conflict = validateKeybinding(
+ keyString,
+ recordingShortcut.action,
+ );
+ if (conflict) {
+ toast.error(
+ `Key "${keyString}" is already bound to "${conflict.existingAction}"`,
+ );
+ setRecordingShortcut(null);
+ return;
+ }
+
+ const oldKeys = getKeybindingsForAction(recordingShortcut.action);
+ for (const key of oldKeys) {
+ removeKeybinding(key);
+ }
+
+ updateKeybinding(keyString, recordingShortcut.action);
+
+ setIsRecording(false);
+ setRecordingShortcut(null);
+ }
+ };
+
+ const handleClickOutside = () => {
+ setRecordingShortcut(null);
+ setIsRecording(false);
+ };
+
+ document.addEventListener("keydown", handleKeyDown);
+ document.addEventListener("click", handleClickOutside);
+
+ return () => {
+ document.removeEventListener("keydown", handleKeyDown);
+ document.removeEventListener("click", handleClickOutside);
+ };
+ }, [
+ recordingShortcut,
+ getKeybindingString,
+ updateKeybinding,
+ removeKeybinding,
+ validateKeybinding,
+ getKeybindingsForAction,
+ setIsRecording,
+ isRecording,
+ ]);
+
+ const handleStartRecording = (shortcut: KeyboardShortcut) => {
+ setRecordingShortcut(shortcut);
+ setIsRecording(true);
+ };
+
+ return (
+ <Dialog open={isOpen} onOpenChange={onOpenChange}>
+ <DialogContent className="flex max-h-[80vh] max-w-2xl flex-col p-0">
+ <DialogHeader>
+ <DialogTitle>Keyboard shortcuts</DialogTitle>
+ </DialogHeader>
+
+ <DialogBody className="scrollbar-thin flex-grow overflow-y-auto">
+ <div className="flex flex-col gap-6">
+ {categories.map((category) => (
+ <div key={category} className="flex flex-col gap-1">
+ <h3 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
+ {category}
+ </h3>
+ <div className="flex flex-col gap-1">
+ {shortcuts
+ .filter((shortcut) => shortcut.category === category)
+ .map((shortcut) => (
+ <ShortcutItem
+ key={shortcut.action}
+ shortcut={shortcut}
+ isRecording={
+ shortcut.action === recordingShortcut?.action
+ }
+ onStartRecording={() => handleStartRecording(shortcut)}
+ />
+ ))}
+ </div>
+ </div>
+ ))}
+ </div>
+ </DialogBody>
+ <DialogFooter>
+ <Button variant="destructive" onClick={resetToDefaults}>
+ Reset to default
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
+
+function ShortcutItem({
+ shortcut,
+ isRecording,
+ onStartRecording,
+}: {
+ shortcut: KeyboardShortcut;
+ isRecording: boolean;
+ onStartRecording: (params: { shortcut: KeyboardShortcut }) => void;
+}) {
+ const displayKeys = shortcut.keys.filter((key: string) => {
+ if (
+ key.includes("Cmd") &&
+ shortcut.keys.includes(key.replace("Cmd", "Ctrl"))
+ )
+ return false;
+
+ return true;
+ });
+
+ return (
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ {shortcut.icon && (
+ <div className="text-muted-foreground">{shortcut.icon}</div>
+ )}
+ <span className="text-sm">{shortcut.description}</span>
+ </div>
+ <div className="flex items-center gap-2">
+ {displayKeys.map((key: string, index: number) => (
+ <div key={key} className="flex items-center gap-2">
+ <div className="flex items-center gap-1">
+ {key.split("+").map((keyPart: string, partIndex: number) => {
+ const keyId = `${shortcut.id}-${index}-${partIndex}`;
+ return (
+ <EditableShortcutKey
+ key={keyId}
+ isRecording={isRecording}
+ onStartRecording={() => onStartRecording({ shortcut })}
+ >
+ {keyPart}
+ </EditableShortcutKey>
+ );
+ })}
+ </div>
+ {index < displayKeys.length - 1 && (
+ <span className="text-muted-foreground text-xs">or</span>
+ )}
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+}
+
+function EditableShortcutKey({
+ children,
+ isRecording,
+ onStartRecording,
+}: {
+ children: React.ReactNode;
+ isRecording: boolean;
+ onStartRecording: () => void;
+}) {
+ const handleClick = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onStartRecording();
+ };
+
+ return (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleClick}
+ title={
+ isRecording ? "Press any key combination..." : "Click to edit shortcut"
+ }
+ >
+ {children}
+ </Button>
+ );
+}
diff --git a/apps/web/src/components/editor/editor-header.tsx b/apps/web/src/components/editor/editor-header.tsx
index 5363d0c7..d74d78d7 100644
--- a/apps/web/src/components/editor/editor-header.tsx
+++ b/apps/web/src/components/editor/editor-header.tsx
@@ -1,130 +1,187 @@
"use client";
import { Button } from "../ui/button";
-import { ChevronDown, ArrowLeft, SquarePen, Trash } from "lucide-react";
-import { HeaderBase } from "../header-base";
-import { useProjectStore } from "@/stores/project-store";
-import { KeyboardShortcutsHelp } from "../keyboard-shortcuts-help";
+import { ChevronDown } from "lucide-react";
import { useState } from "react";
import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import Link from "next/link";
-import { RenameProjectDialog } from "../rename-project-dialog";
-import { DeleteProjectDialog } from "../delete-project-dialog";
+import { RenameProjectDialog } from "./dialogs/rename-project-dialog";
+import { DeleteProjectDialog } from "./dialogs/delete-project-dialog";
import { useRouter } from "next/navigation";
import { FaDiscord } from "react-icons/fa6";
-import { PanelPresetSelector } from "./panel-preset-selector";
import { ExportButton } from "./export-button";
import { ThemeToggle } from "../theme-toggle";
+import { SOCIAL_LINKS } from "@/constants/site-constants";
+import { toast } from "sonner";
+import { useEditor } from "@/hooks/use-editor";
+import {
+ ArrowLeft02Icon,
+ Edit03Icon,
+ Delete02Icon,
+ CommandIcon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { ShortcutsDialog } from "./dialogs/shortcuts-dialog";
export function EditorHeader() {
- const { activeProject, renameProject, deleteProject } = useProjectStore();
- const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
- const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
- const router = useRouter();
+ return (
+ <header className="bg-background flex h-[3.2rem] items-center justify-between px-3 pt-0.5">
+ <div className="flex items-center gap-2">
+ <ProjectDropdown />
+ </div>
+ <nav className="flex items-center gap-2">
+ <ExportButton />
+ <ThemeToggle />
+ </nav>
+ </header>
+ );
+}
+
+function ProjectDropdown() {
+ const [openDialog, setOpenDialog] = useState<
+ "delete" | "rename" | "shortcuts" | null
+ >(null);
+ const [isExiting, setIsExiting] = useState(false);
+ const router = useRouter();
+ const editor = useEditor();
+ const activeProject = editor.project.getActive();
- const handleNameSave = async (newName: string) => {
- console.log("handleNameSave", newName);
- if (activeProject && newName.trim() && newName !== activeProject.name) {
- try {
- await renameProject(activeProject.id, newName.trim());
- setIsRenameDialogOpen(false);
- } catch (error) {
- console.error("Failed to rename project:", error);
- }
- }
- };
+ const handleExit = async () => {
+ if (isExiting) return;
+ setIsExiting(true);
- const handleDelete = () => {
- if (activeProject) {
- deleteProject(activeProject.id);
- setIsDeleteDialogOpen(false);
- router.push("/projects");
- }
- };
+ try {
+ await editor.project.prepareExit();
+ editor.project.closeProject();
+ } catch (error) {
+ console.error("Failed to prepare project exit:", error);
+ } finally {
+ editor.project.closeProject();
+ router.push("/projects");
+ }
+ };
- const leftContent = (
- <div className="flex items-center gap-2">
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- variant="secondary"
- className="h-auto py-1.5 px-2.5 flex items-center justify-center"
- >
- <ChevronDown className="text-muted-foreground" />
- <span className="text-[0.85rem] mr-2">{activeProject?.name}</span>
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="start" className="w-40 z-100">
- <Link href="/projects">
- <DropdownMenuItem className="flex items-center gap-1.5">
- <ArrowLeft className="h-4 w-4" />
- Projects
- </DropdownMenuItem>
- </Link>
- <DropdownMenuItem
- className="flex items-center gap-1.5"
- onClick={() => setIsRenameDialogOpen(true)}
- >
- <SquarePen className="h-4 w-4" />
- Rename project
- </DropdownMenuItem>
- <DropdownMenuItem
- variant="destructive"
- className="flex items-center gap-1.5"
- onClick={() => setIsDeleteDialogOpen(true)}
- >
- <Trash className="h-4 w-4" />
- Delete Project
- </DropdownMenuItem>
- <DropdownMenuSeparator />
- <DropdownMenuItem asChild>
- <Link
- href="https://discord.gg/zmR9N35cjK"
- target="_blank"
- rel="noopener noreferrer"
- className="flex items-center gap-1.5"
- >
- <FaDiscord className="h-4 w-4" />
- Discord
- </Link>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- <RenameProjectDialog
- isOpen={isRenameDialogOpen}
- onOpenChange={setIsRenameDialogOpen}
- onConfirm={handleNameSave}
- projectName={activeProject?.name || ""}
- />
- <DeleteProjectDialog
- isOpen={isDeleteDialogOpen}
- onOpenChange={setIsDeleteDialogOpen}
- onConfirm={handleDelete}
- projectName={activeProject?.name || ""}
- />
- </div>
- );
+ const handleSaveProjectName = async (newName: string) => {
+ if (
+ activeProject &&
+ newName.trim() &&
+ newName !== activeProject.metadata.name
+ ) {
+ try {
+ await editor.project.renameProject({
+ id: activeProject.metadata.id,
+ name: newName.trim(),
+ });
+ } catch (error) {
+ toast.error("Failed to rename project", {
+ description:
+ error instanceof Error ? error.message : "Please try again",
+ });
+ } finally {
+ setOpenDialog(null);
+ }
+ }
+ };
- const rightContent = (
- <nav className="flex items-center gap-2">
- <PanelPresetSelector />
- <KeyboardShortcutsHelp />
- <ExportButton />
- <ThemeToggle />
- </nav>
- );
+ const handleDeleteProject = async () => {
+ if (activeProject) {
+ try {
+ await editor.project.deleteProjects({
+ ids: [activeProject.metadata.id],
+ });
+ router.push("/projects");
+ } catch (error) {
+ toast.error("Failed to delete project", {
+ description:
+ error instanceof Error ? error.message : "Please try again",
+ });
+ } finally {
+ setOpenDialog(null);
+ }
+ }
+ };
- return (
- <HeaderBase
- leftContent={leftContent}
- rightContent={rightContent}
- className="bg-background h-[3.2rem] px-3 items-center mt-0.5"
- />
- );
+ return (
+ <>
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ variant="secondary"
+ className="flex h-auto items-center justify-center px-2.5 py-1.5"
+ >
+ <ChevronDown className="text-muted-foreground" />
+ <span className="mr-2 text-[0.85rem]">
+ {activeProject?.metadata.name}
+ </span>
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="start" className="z-100 w-52">
+ <DropdownMenuItem
+ className="flex items-center gap-1.5"
+ onClick={handleExit}
+ disabled={isExiting}
+ >
+ <HugeiconsIcon icon={ArrowLeft02Icon} className="size-4" />
+ Exit project
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ className="flex items-center gap-1.5"
+ onClick={() => setOpenDialog("rename")}
+ >
+ <HugeiconsIcon icon={Edit03Icon} className="size-4" />
+ Rename project
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ variant="destructive"
+ className="flex items-center gap-1.5"
+ onClick={() => setOpenDialog("delete")}
+ >
+ <HugeiconsIcon icon={Delete02Icon} className="size-4" />
+ Delete project
+ </DropdownMenuItem>
+ <DropdownMenuSeparator />
+ <DropdownMenuItem
+ className="flex items-center gap-1.5"
+ onClick={() => setOpenDialog("shortcuts")}
+ >
+ <HugeiconsIcon icon={CommandIcon} className="size-4" />
+ Keyboard shortcuts
+ </DropdownMenuItem>
+ <DropdownMenuItem asChild>
+ <Link
+ href={SOCIAL_LINKS.discord}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="flex items-center gap-1.5"
+ >
+ <FaDiscord className="size-4" />
+ Discord
+ </Link>
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ <RenameProjectDialog
+ isOpen={openDialog === "rename"}
+ onOpenChange={(isOpen) => setOpenDialog(isOpen ? "rename" : null)}
+ onConfirm={(newName) => handleSaveProjectName(newName)}
+ projectName={activeProject?.metadata.name || ""}
+ />
+ <DeleteProjectDialog
+ isOpen={openDialog === "delete"}
+ onOpenChange={(isOpen) => setOpenDialog(isOpen ? "delete" : null)}
+ onConfirm={handleDeleteProject}
+ projectNames={[activeProject?.metadata.name || ""]}
+ />
+ <ShortcutsDialog
+ isOpen={openDialog === "shortcuts"}
+ onOpenChange={(isOpen) => setOpenDialog(isOpen ? "shortcuts" : null)}
+ />
+ </>
+ );
}
diff --git a/apps/web/src/components/editor/export-button.tsx b/apps/web/src/components/editor/export-button.tsx
index 246e8b6f..f79710bf 100644
--- a/apps/web/src/components/editor/export-button.tsx
+++ b/apps/web/src/components/editor/export-button.tsx
@@ -1,317 +1,347 @@
"use client";
-import { useState } from "react";
-import { TransitionUpIcon } from "../icons";
-import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
-import { Button } from "../ui/button";
-import { Label } from "../ui/label";
-import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
-import { Progress } from "../ui/progress";
-import { Checkbox } from "../ui/checkbox";
-import { cn } from "@/lib/utils";
+import { useState, useRef } from "react";
+import { TransitionTopIcon } from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
import {
- exportProject,
- getExportMimeType,
- getExportFileExtension,
- DEFAULT_EXPORT_OPTIONS,
-} from "@/lib/export";
-import { useProjectStore } from "@/stores/project-store";
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import { Progress } from "@/components/ui/progress";
+import { Checkbox } from "@/components/ui/checkbox";
+import { cn } from "@/utils/ui";
+import { getExportMimeType, getExportFileExtension } from "@/lib/export";
import { Check, Copy, Download, RotateCcw, X } from "lucide-react";
-import { ExportFormat, ExportQuality, ExportResult } from "@/types/export";
-import { PropertyGroup } from "./properties-panel/property-item";
+import {
+ EXPORT_FORMAT_VALUES,
+ EXPORT_QUALITY_VALUES,
+ type ExportFormat,
+ type ExportQuality,
+ type ExportResult,
+} from "@/types/export";
+import { PropertyGroup } from "@/components/editor/panels/properties/property-item";
+import { useEditor } from "@/hooks/use-editor";
+import { DEFAULT_EXPORT_OPTIONS } from "@/constants/export-constants";
export function ExportButton() {
- const [isExportPopoverOpen, setIsExportPopoverOpen] = useState(false);
- const { activeProject } = useProjectStore();
+ const [isExportPopoverOpen, setIsExportPopoverOpen] = useState(false);
+ const editor = useEditor();
- const handleExport = () => {
- setIsExportPopoverOpen(true);
- };
+ const handleExport = () => {
+ setIsExportPopoverOpen(true);
+ };
- const hasProject = !!activeProject;
+ const hasProject = !!editor.project.getActive();
- return (
- <Popover open={isExportPopoverOpen} onOpenChange={setIsExportPopoverOpen}>
- <PopoverTrigger asChild>
- <button
- type="button"
- className={cn(
- "flex items-center gap-1.5 bg-[#38BDF8] text-white rounded-md px-[0.12rem] py-[0.12rem] transition-all duration-200",
- hasProject
- ? "cursor-pointer hover:brightness-95"
- : "cursor-not-allowed opacity-50"
- )}
- onClick={hasProject ? handleExport : undefined}
- disabled={!hasProject}
- onKeyDown={(event) => {
- if (hasProject && (event.key === "Enter" || event.key === " ")) {
- event.preventDefault();
- handleExport();
- }
- }}
- >
- <div className="flex items-center gap-1.5 bg-linear-270 from-[#2567EC] to-[#37B6F7] rounded-[0.8rem] px-4 py-1 relative shadow-[0_1px_3px_0px_rgba(0,0,0,0.65)]">
- <TransitionUpIcon className="z-50" />
- <span className="text-[0.875rem] z-50">Export</span>
- <div className="absolute w-full h-full left-0 top-0 bg-linear-to-t from-white/0 to-white/50 z-10 rounded-[0.8rem] flex items-center justify-center">
- <div className="absolute w-[calc(100%-2px)] h-[calc(100%-2px)] top-[0.08rem] bg-linear-270 from-[#2567EC] to-[#37B6F7] z-50 rounded-[0.8rem]"></div>
- </div>
- </div>
- </button>
- </PopoverTrigger>
- {hasProject && <ExportPopover onOpenChange={setIsExportPopoverOpen} />}
- </Popover>
- );
+ return (
+ <Popover open={isExportPopoverOpen} onOpenChange={setIsExportPopoverOpen}>
+ <PopoverTrigger asChild>
+ <button
+ type="button"
+ className={cn(
+ "flex items-center gap-1.5 rounded-md bg-[#38BDF8] px-[0.12rem] py-[0.12rem] text-white",
+ hasProject
+ ? "cursor-pointer hover:brightness-105"
+ : "cursor-not-allowed opacity-50",
+ )}
+ onClick={hasProject ? handleExport : undefined}
+ disabled={!hasProject}
+ onKeyDown={(event) => {
+ if (hasProject && (event.key === "Enter" || event.key === " ")) {
+ event.preventDefault();
+ handleExport();
+ }
+ }}
+ >
+ <div className="relative flex items-center gap-1.5 rounded-[0.6rem] bg-linear-270 from-[#2567EC] to-[#37B6F7] px-4 py-1 shadow-[0_1px_3px_0px_rgba(0,0,0,0.65)]">
+ <HugeiconsIcon icon={TransitionTopIcon} className="z-50 size-4" />
+ <span className="z-50 text-[0.875rem]">Export</span>
+ <div className="absolute top-0 left-0 z-10 flex size-full items-center justify-center rounded-[0.6rem] bg-linear-to-t from-white/0 to-white/50">
+ <div className="absolute top-[0.08rem] z-50 h-[calc(100%-2px)] w-[calc(100%-2px)] rounded-[0.6rem] bg-linear-270 from-[#2567EC] to-[#37B6F7]"></div>
+ </div>
+ </div>
+ </button>
+ </PopoverTrigger>
+ {hasProject && <ExportPopover onOpenChange={setIsExportPopoverOpen} />}
+ </Popover>
+ );
}
function ExportPopover({
- onOpenChange,
+ onOpenChange,
}: {
- onOpenChange: (open: boolean) => void;
+ onOpenChange: (open: boolean) => void;
}) {
- const { activeProject } = useProjectStore();
- const [format, setFormat] = useState<ExportFormat>(
- DEFAULT_EXPORT_OPTIONS.format
- );
- const [quality, setQuality] = useState<ExportQuality>(
- DEFAULT_EXPORT_OPTIONS.quality
- );
- const [includeAudio, setIncludeAudio] = useState<boolean>(
- DEFAULT_EXPORT_OPTIONS.includeAudio || true
- );
- const [isExporting, setIsExporting] = useState(false);
- const [progress, setProgress] = useState(0);
- const [exportResult, setExportResult] = useState<ExportResult | null>(null);
+ const editor = useEditor();
+ const activeProject = editor.project.getActive();
+ const [format, setFormat] = useState<ExportFormat>(
+ DEFAULT_EXPORT_OPTIONS.format,
+ );
+ const [quality, setQuality] = useState<ExportQuality>(
+ DEFAULT_EXPORT_OPTIONS.quality,
+ );
+ const [includeAudio, setIncludeAudio] = useState<boolean>(
+ DEFAULT_EXPORT_OPTIONS.includeAudio || true,
+ );
+ const [isExporting, setIsExporting] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const [exportResult, setExportResult] = useState<ExportResult | null>(null);
+ const cancelRequestedRef = useRef(false);
+
+ const handleExport = async () => {
+ if (!activeProject) return;
+
+ cancelRequestedRef.current = false;
+ setIsExporting(true);
+ setProgress(0);
+ setExportResult(null);
+
+ const result = await editor.project.export({
+ options: {
+ format,
+ quality,
+ fps: activeProject.settings.fps,
+ includeAudio,
+ onProgress: ({ progress }) => setProgress(progress),
+ onCancel: () => cancelRequestedRef.current,
+ },
+ });
- const handleExport = async () => {
- if (!activeProject) return;
+ setIsExporting(false);
- setIsExporting(true);
- setProgress(0);
- setExportResult(null);
+ if (result.cancelled) {
+ setExportResult(null);
+ setProgress(0);
+ return;
+ }
- const result = await exportProject({
- format,
- quality,
- fps: activeProject.fps,
- includeAudio,
- onProgress: setProgress,
- onCancel: () => false, // TODO: Add cancel functionality
- });
+ setExportResult(result);
- setIsExporting(false);
- setExportResult(result);
+ if (result.success && result.buffer) {
+ const mimeType = getExportMimeType({ format });
+ const extension = getExportFileExtension({ format });
+ const blob = new Blob([result.buffer], { type: mimeType });
+ const url = URL.createObjectURL(blob);
- if (result.success && result.buffer) {
- // Download the file
- const mimeType = getExportMimeType(format);
- const extension = getExportFileExtension(format);
- const blob = new Blob([result.buffer], { type: mimeType });
- const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `${activeProject.metadata.name}${extension}`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
- const a = document.createElement("a");
- a.href = url;
- a.download = `${activeProject.name}${extension}`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
+ onOpenChange(false);
+ setExportResult(null);
+ setProgress(0);
+ }
+ };
- onOpenChange(false);
- setExportResult(null);
- setProgress(0);
- }
- };
+ const handleClose = () => {
+ if (!isExporting) {
+ onOpenChange(false);
+ setExportResult(null);
+ setProgress(0);
+ }
+ };
- const handleClose = () => {
- if (!isExporting) {
- onOpenChange(false);
- setExportResult(null);
- setProgress(0);
- }
- };
+ const handleCancel = () => {
+ cancelRequestedRef.current = true;
+ };
- return (
- <PopoverContent className="w-80 mr-4 flex flex-col gap-3 bg-background">
- <>
- {exportResult && !exportResult.success ? (
- <ExportError
- error={exportResult.error || "Unknown error occurred"}
- onRetry={handleExport}
- />
- ) : (
- <>
- <div className="flex items-center justify-between">
- <h3 className=" font-medium">
- {isExporting ? "Exporting project" : "Export project"}
- </h3>
- <Button variant="text" size="icon" onClick={handleClose}>
- <X className="!size-5 text-foreground/85" />
- </Button>
- </div>
+ return (
+ <PopoverContent className="bg-background mr-4 flex w-80 flex-col gap-3">
+ {exportResult && !exportResult.success ? (
+ <ExportError
+ error={exportResult.error || "Unknown error occurred"}
+ onRetry={handleExport}
+ />
+ ) : (
+ <>
+ <div className="flex items-center justify-between">
+ <h3 className="font-medium">
+ {isExporting ? "Exporting project" : "Export project"}
+ </h3>
+ <Button variant="text" size="icon" onClick={handleClose}>
+ <X className="text-foreground/85 !size-5" />
+ </Button>
+ </div>
- <div className="flex flex-col gap-4">
- {!isExporting && (
- <>
- <div className="flex flex-col gap-3">
- <PropertyGroup
- title="Format"
- titleClassName="text-sm"
- defaultExpanded={false}
- >
- <RadioGroup
- value={format}
- onValueChange={(value) =>
- setFormat(value as ExportFormat)
- }
- >
- <div className="flex items-center space-x-2">
- <RadioGroupItem value="mp4" id="mp4" />
- <Label htmlFor="mp4">
- MP4 (H.264) - Better compatibility
- </Label>
- </div>
- <div className="flex items-center space-x-2">
- <RadioGroupItem value="webm" id="webm" />
- <Label htmlFor="webm">
- WebM (VP9) - Smaller file size
- </Label>
- </div>
- </RadioGroup>
- </PropertyGroup>
+ <div className="flex flex-col gap-4">
+ {!isExporting && (
+ <>
+ <div className="flex flex-col gap-3">
+ <PropertyGroup
+ title="Format"
+ titleClassName="text-sm"
+ defaultExpanded={false}
+ >
+ <RadioGroup
+ value={format}
+ onValueChange={(value) => {
+ if (isExportFormat(value)) {
+ setFormat(value);
+ }
+ }}
+ >
+ <div className="flex items-center space-x-2">
+ <RadioGroupItem value="mp4" id="mp4" />
+ <Label htmlFor="mp4">
+ MP4 (H.264) - Better compatibility
+ </Label>
+ </div>
+ <div className="flex items-center space-x-2">
+ <RadioGroupItem value="webm" id="webm" />
+ <Label htmlFor="webm">
+ WebM (VP9) - Smaller file size
+ </Label>
+ </div>
+ </RadioGroup>
+ </PropertyGroup>
- <PropertyGroup
- title="Quality"
- titleClassName="text-sm"
- defaultExpanded={false}
- >
- <RadioGroup
- value={quality}
- onValueChange={(value) =>
- setQuality(value as ExportQuality)
- }
- >
- <div className="flex items-center space-x-2">
- <RadioGroupItem value="low" id="low" />
- <Label htmlFor="low">Low - Smallest file size</Label>
- </div>
- <div className="flex items-center space-x-2">
- <RadioGroupItem value="medium" id="medium" />
- <Label htmlFor="medium">Medium - Balanced</Label>
- </div>
- <div className="flex items-center space-x-2">
- <RadioGroupItem value="high" id="high" />
- <Label htmlFor="high">High - Recommended</Label>
- </div>
- <div className="flex items-center space-x-2">
- <RadioGroupItem value="very_high" id="very_high" />
- <Label htmlFor="very_high">
- Very High - Largest file size
- </Label>
- </div>
- </RadioGroup>
- </PropertyGroup>
+ <PropertyGroup
+ title="Quality"
+ titleClassName="text-sm"
+ defaultExpanded={false}
+ >
+ <RadioGroup
+ value={quality}
+ onValueChange={(value) => {
+ if (isExportQuality(value)) {
+ setQuality(value);
+ }
+ }}
+ >
+ <div className="flex items-center space-x-2">
+ <RadioGroupItem value="low" id="low" />
+ <Label htmlFor="low">Low - Smallest file size</Label>
+ </div>
+ <div className="flex items-center space-x-2">
+ <RadioGroupItem value="medium" id="medium" />
+ <Label htmlFor="medium">Medium - Balanced</Label>
+ </div>
+ <div className="flex items-center space-x-2">
+ <RadioGroupItem value="high" id="high" />
+ <Label htmlFor="high">High - Recommended</Label>
+ </div>
+ <div className="flex items-center space-x-2">
+ <RadioGroupItem value="very_high" id="very_high" />
+ <Label htmlFor="very_high">
+ Very High - Largest file size
+ </Label>
+ </div>
+ </RadioGroup>
+ </PropertyGroup>
- <PropertyGroup
- title="Audio"
- titleClassName="text-sm"
- defaultExpanded={false}
- >
- <div className="flex items-center space-x-2">
- <Checkbox
- id="include-audio"
- checked={includeAudio}
- onCheckedChange={(checked) =>
- setIncludeAudio(!!checked)
- }
- />
- <Label htmlFor="include-audio">
- Include audio in export
- </Label>
- </div>
- </PropertyGroup>
- </div>
+ <PropertyGroup
+ title="Audio"
+ titleClassName="text-sm"
+ defaultExpanded={false}
+ >
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id="include-audio"
+ checked={includeAudio}
+ onCheckedChange={(checked) =>
+ setIncludeAudio(!!checked)
+ }
+ />
+ <Label htmlFor="include-audio">
+ Include audio in export
+ </Label>
+ </div>
+ </PropertyGroup>
+ </div>
- <Button onClick={handleExport} className="w-full gap-2">
- <Download className="w-4 h-4" />
- Export
- </Button>
- </>
- )}
+ <Button onClick={handleExport} className="w-full gap-2">
+ <Download className="size-4" />
+ Export
+ </Button>
+ </>
+ )}
- {isExporting && (
- <div className="space-y-4">
- <div className="flex flex-col">
- <div className="text-center flex items-center justify-between">
- <p className="text-sm text-muted-foreground mb-2">
- {Math.round(progress * 100)}%
- </p>
- <p className="text-sm text-muted-foreground mb-2">100%</p>
- </div>
- <Progress value={progress * 100} className="w-full" />
- </div>
+ {isExporting && (
+ <div className="space-y-4">
+ <div className="flex flex-col">
+ <div className="flex items-center justify-between text-center">
+ <p className="text-muted-foreground mb-2 text-sm">
+ {Math.round(progress * 100)}%
+ </p>
+ <p className="text-muted-foreground mb-2 text-sm">100%</p>
+ </div>
+ <Progress value={progress * 100} className="w-full" />
+ </div>
+
+ <Button
+ variant="outline"
+ className="w-full rounded-md"
+ onClick={handleCancel}
+ >
+ Cancel
+ </Button>
+ </div>
+ )}
+ </div>
+ </>
+ )}
+ </PopoverContent>
+ );
+}
+
+function isExportFormat(value: string): value is ExportFormat {
+ return EXPORT_FORMAT_VALUES.some((formatValue) => formatValue === value);
+}
- <Button
- variant="outline"
- className="rounded-md w-full"
- onClick={() => {}}
- >
- Cancel
- </Button>
- </div>
- )}
- </div>
- </>
- )}
- </>
- </PopoverContent>
- );
+function isExportQuality(value: string): value is ExportQuality {
+ return EXPORT_QUALITY_VALUES.some((qualityValue) => qualityValue === value);
}
function ExportError({
- error,
- onRetry,
+ error,
+ onRetry,
}: {
- error: string;
- onRetry: () => void;
+ error: string;
+ onRetry: () => void;
}) {
- const [copied, setCopied] = useState(false);
+ const [copied, setCopied] = useState(false);
- const handleCopy = async () => {
- await navigator.clipboard.writeText(error);
- setCopied(true);
- setTimeout(() => setCopied(false), 1000);
- };
+ const handleCopy = async () => {
+ await navigator.clipboard.writeText(error);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1000);
+ };
- return (
- <div className="space-y-4">
- <div className="flex flex-col gap-1.5">
- <p className="text-sm font-medium text-red-400">Export failed</p>
- <p className="text-xs text-muted-foreground">
- {error}
- </p>
- </div>
+ return (
+ <div className="space-y-4">
+ <div className="flex flex-col gap-1.5">
+ <p className="text-destructive text-sm font-medium">Export failed</p>
+ <p className="text-muted-foreground text-xs">{error}</p>
+ </div>
- <div className="flex gap-2">
- <Button
- variant="outline"
- size="sm"
- className="flex-1 text-xs h-8"
- onClick={handleCopy}
- >
- {copied ? <Check className="text-green-500" /> : <Copy />}
- Copy
- </Button>
- <Button
- variant="outline"
- size="sm"
- className="flex-1 text-xs h-8"
- onClick={onRetry}
- >
- <RotateCcw />
- Retry
- </Button>
- </div>
- </div>
- );
+ <div className="flex gap-2">
+ <Button
+ variant="outline"
+ size="sm"
+ className="h-8 flex-1 text-xs"
+ onClick={handleCopy}
+ >
+ {copied ? <Check className="text-constructive" /> : <Copy />}
+ Copy
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ className="h-8 flex-1 text-xs"
+ onClick={onRetry}
+ >
+ <RotateCcw />
+ Retry
+ </Button>
+ </div>
+ </div>
+ );
}
diff --git a/apps/web/src/components/editor/layout-guide-overlay.tsx b/apps/web/src/components/editor/layout-guide-overlay.tsx
index ff4cde57..0f02da1c 100644
--- a/apps/web/src/components/editor/layout-guide-overlay.tsx
+++ b/apps/web/src/components/editor/layout-guide-overlay.tsx
@@ -4,24 +4,24 @@ import { useEditorStore } from "@/stores/editor-store";
import Image from "next/image";
function TikTokGuide() {
- return (
- <div className="absolute inset-0 pointer-events-none">
- <Image
- src="/platform-guides/tiktok-blueprint.png"
- alt="TikTok layout guide"
- className="absolute inset-0 w-full h-full object-contain"
- draggable={false}
- fill
- />
- </div>
- );
+ return (
+ <div className="pointer-events-none absolute inset-0">
+ <Image
+ src="/platform-guides/tiktok-blueprint.png"
+ alt="TikTok layout guide"
+ className="absolute inset-0 size-full object-contain"
+ draggable={false}
+ fill
+ />
+ </div>
+ );
}
export function LayoutGuideOverlay() {
- const { layoutGuide } = useEditorStore();
+ const { layoutGuide } = useEditorStore();
- if (layoutGuide.platform === null) return null;
- if (layoutGuide.platform === "tiktok") return <TikTokGuide />;
+ if (layoutGuide.platform === null) return null;
+ if (layoutGuide.platform === "tiktok") return <TikTokGuide />;
- return null;
+ return null;
}
diff --git a/apps/web/src/components/editor/media-panel/drag-overlay.tsx b/apps/web/src/components/editor/media-panel/drag-overlay.tsx
deleted file mode 100644
index e1fd257e..00000000
--- a/apps/web/src/components/editor/media-panel/drag-overlay.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Upload, Plus, Image } from "lucide-react";
-import { Button } from "@/components/ui/button";
-
-interface MediaDragOverlayProps {
- isVisible: boolean;
- isProcessing?: boolean;
- progress?: number;
- onClick?: () => void;
- isEmptyState?: boolean;
-}
-
-export function MediaDragOverlay({
- isVisible,
- isProcessing = false,
- progress = 0,
- onClick,
- isEmptyState = false,
-}: MediaDragOverlayProps) {
- if (!isVisible) return null;
-
- const handleClick = (e: React.MouseEvent) => {
- if (isProcessing || !onClick) return;
- e.preventDefault();
- e.stopPropagation();
- onClick();
- };
-
- return (
- <div
- className="flex flex-col items-center justify-center gap-4 h-full text-center rounded-lg bg-foreground/5 hover:bg-foreground/10 transition-all duration-200 p-8"
- onClick={handleClick}
- >
- <div className="flex items-center justify-center">
- <Upload className="h-10 w-10 text-foreground" />
- </div>
-
- <div className="space-y-2">
- <p className="text-xs text-muted-foreground max-w-sm">
- {isProcessing
- ? `Processing your files (${progress}%)`
- : "Drag and drop videos, photos, and audio files here"}
- </p>
- </div>
-
- {isProcessing && (
- <div className="w-full max-w-xs">
- <div className="w-full bg-muted/50 rounded-full h-2">
- <div
- className="bg-primary h-2 rounded-full transition-all duration-300"
- style={{ width: `${progress}%` }}
- />
- </div>
- </div>
- )}
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/media-panel/index.tsx b/apps/web/src/components/editor/media-panel/index.tsx
deleted file mode 100644
index 89b53b64..00000000
--- a/apps/web/src/components/editor/media-panel/index.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-"use client";
-
-import { TabBar } from "./tabbar";
-import { MediaView } from "./views/media";
-import { useMediaPanelStore, Tab } from "./store";
-import { TextView } from "./views/text";
-import { SoundsView } from "./views/sounds";
-import { StickersView } from "./views/stickers";
-import { Separator } from "@/components/ui/separator";
-import { SettingsView } from "./views/settings";
-import { Captions } from "./views/captions";
-
-export function MediaPanel() {
- const { activeTab } = useMediaPanelStore();
-
- const viewMap: Record<Tab, React.ReactNode> = {
- media: <MediaView />,
- sounds: <SoundsView />,
- text: <TextView />,
- stickers: <StickersView />,
- effects: (
- <div className="p-4 text-muted-foreground">
- Effects view coming soon...
- </div>
- ),
- transitions: (
- <div className="p-4 text-muted-foreground">
- Transitions view coming soon...
- </div>
- ),
- captions: <Captions />,
- filters: (
- <div className="p-4 text-muted-foreground">
- Filters view coming soon...
- </div>
- ),
- adjustment: (
- <div className="p-4 text-muted-foreground">
- Adjustment view coming soon...
- </div>
- ),
- settings: <SettingsView />,
- };
-
- return (
- <div className="h-full flex bg-panel">
- <TabBar />
- <Separator orientation="vertical" />
- <div className="flex-1 overflow-hidden">{viewMap[activeTab]}</div>
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/media-panel/store.ts b/apps/web/src/components/editor/media-panel/store.ts
deleted file mode 100644
index eb645ec3..00000000
--- a/apps/web/src/components/editor/media-panel/store.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import {
- CaptionsIcon,
- ArrowLeftRightIcon,
- SparklesIcon,
- StickerIcon,
- MusicIcon,
- VideoIcon,
- BlendIcon,
- SlidersHorizontalIcon,
- LucideIcon,
- TypeIcon,
- SettingsIcon,
-} from "lucide-react";
-import { create } from "zustand";
-
-export type Tab =
- | "media"
- | "sounds"
- | "text"
- | "stickers"
- | "effects"
- | "transitions"
- | "captions"
- | "filters"
- | "adjustment"
- | "settings";
-
-export const tabs: { [key in Tab]: { icon: LucideIcon; label: string } } = {
- media: {
- icon: VideoIcon,
- label: "Media",
- },
- sounds: {
- icon: MusicIcon,
- label: "Sounds",
- },
- text: {
- icon: TypeIcon,
- label: "Text",
- },
- stickers: {
- icon: StickerIcon,
- label: "Stickers",
- },
- effects: {
- icon: SparklesIcon,
- label: "Effects",
- },
- transitions: {
- icon: ArrowLeftRightIcon,
- label: "Transitions",
- },
- captions: {
- icon: CaptionsIcon,
- label: "Captions",
- },
- filters: {
- icon: BlendIcon,
- label: "Filters",
- },
- adjustment: {
- icon: SlidersHorizontalIcon,
- label: "Adjustment",
- },
- settings: {
- icon: SettingsIcon,
- label: "Settings",
- },
-};
-
-interface MediaPanelStore {
- activeTab: Tab;
- setActiveTab: (tab: Tab) => void;
- highlightMediaId: string | null;
- requestRevealMedia: (mediaId: string) => void;
- clearHighlight: () => void;
-}
-
-export const useMediaPanelStore = create<MediaPanelStore>((set) => ({
- activeTab: "media",
- setActiveTab: (tab) => set({ activeTab: tab }),
- highlightMediaId: null,
- requestRevealMedia: (mediaId) =>
- set({ activeTab: "media", highlightMediaId: mediaId }),
- clearHighlight: () => set({ highlightMediaId: null }),
-}));
diff --git a/apps/web/src/components/editor/media-panel/tabbar.tsx b/apps/web/src/components/editor/media-panel/tabbar.tsx
deleted file mode 100644
index e2bfdff7..00000000
--- a/apps/web/src/components/editor/media-panel/tabbar.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-"use client";
-
-import { cn } from "@/lib/utils";
-import { Tab, tabs, useMediaPanelStore } from "./store";
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
-import { useEffect, useRef, useState } from "react";
-
-export function TabBar() {
- const { activeTab, setActiveTab } = useMediaPanelStore();
- const scrollRef = useRef<HTMLDivElement>(null);
- const [showTopFade, setShowTopFade] = useState(false);
- const [showBottomFade, setShowBottomFade] = useState(false);
-
- const checkScrollPosition = () => {
- const element = scrollRef.current;
- if (!element) return;
-
- const { scrollTop, scrollHeight, clientHeight } = element;
- setShowTopFade(scrollTop > 0);
- setShowBottomFade(scrollTop < scrollHeight - clientHeight - 1);
- };
-
- useEffect(() => {
- const element = scrollRef.current;
- if (!element) return;
-
- checkScrollPosition();
- element.addEventListener("scroll", checkScrollPosition);
-
- const resizeObserver = new ResizeObserver(checkScrollPosition);
- resizeObserver.observe(element);
-
- return () => {
- element.removeEventListener("scroll", checkScrollPosition);
- resizeObserver.disconnect();
- };
- }, []);
-
- return (
- <div className="flex relative">
- <div
- ref={scrollRef}
- className="h-full px-4 flex flex-col justify-start items-center gap-5 overflow-y-auto scrollbar-hidden relative w-full py-4"
- >
- {(Object.keys(tabs) as Tab[]).map((tabKey) => {
- const tab = tabs[tabKey];
- return (
- <div
- className={cn(
- "flex z-[100] flex-col gap-0.5 items-center cursor-pointer",
- activeTab === tabKey
- ? "text-primary !opacity-100"
- : "text-muted-foreground"
- )}
- onClick={() => setActiveTab(tabKey)}
- key={tabKey}
- >
- <Tooltip delayDuration={10}>
- <TooltipTrigger asChild>
- <tab.icon className="size-[1.1rem]! opacity-100 hover:opacity-75" />
- </TooltipTrigger>
- <TooltipContent
- side="right"
- align="center"
- variant="sidebar"
- sideOffset={8}
- >
- <div className="dark:text-base-gray-950 text-black text-sm font-medium leading-none dark:text-white">
- {tab.label}
- </div>
- </TooltipContent>
- </Tooltip>
- </div>
- );
- })}
- </div>
-
- <FadeOverlay direction="top" show={showTopFade} />
- <FadeOverlay direction="bottom" show={showBottomFade} />
- </div>
- );
-}
-
-function FadeOverlay({ direction, show }: { direction: "top" | "bottom", show: boolean }) {
- return (
- <div
- className={cn(
- "absolute left-0 right-0 h-6 pointer-events-none z-[101] transition-opacity duration-200",
- direction === "top" && show
- ? "top-0 bg-gradient-to-b from-panel to-transparent"
- : "bottom-0 bg-gradient-to-t from-panel to-transparent"
- )}
- />
- );
-} \ No newline at end of file
diff --git a/apps/web/src/components/editor/media-panel/views/captions.tsx b/apps/web/src/components/editor/media-panel/views/captions.tsx
deleted file mode 100644
index fbe5b418..00000000
--- a/apps/web/src/components/editor/media-panel/views/captions.tsx
+++ /dev/null
@@ -1,303 +0,0 @@
-import { Button } from "@/components/ui/button";
-import { PropertyGroup } from "../../properties-panel/property-item";
-import { PanelBaseView as BaseView } from "@/components/editor/panel-base-view";
-import { Language, LanguageSelect } from "@/components/language-select";
-import { useState, useRef, useEffect } from "react";
-import { extractTimelineAudio } from "@/lib/mediabunny-utils";
-import { encryptWithRandomKey, arrayBufferToBase64 } from "@/lib/zk-encryption";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
-import { Loader2, Shield, Trash2, Upload } from "lucide-react";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { TextElement } from "@/types/timeline";
-
-export const languages: Language[] = [
- { code: "US", name: "English" },
- { code: "ES", name: "Spanish" },
- { code: "IT", name: "Italian" },
- { code: "FR", name: "French" },
- { code: "DE", name: "German" },
- { code: "PT", name: "Portuguese" },
- { code: "RU", name: "Russian" },
- { code: "JP", name: "Japanese" },
- { code: "CN", name: "Chinese" },
-];
-
-const PRIVACY_DIALOG_KEY = "opencut-transcription-privacy-accepted";
-
-export function Captions() {
- const [selectedCountry, setSelectedCountry] = useState("auto");
- const [isProcessing, setIsProcessing] = useState(false);
- const [processingStep, setProcessingStep] = useState<string>("");
- const [error, setError] = useState<string | null>(null);
- const [showPrivacyDialog, setShowPrivacyDialog] = useState(false);
- const [hasAcceptedPrivacy, setHasAcceptedPrivacy] = useState(false);
- const containerRef = useRef<HTMLDivElement>(null);
- const { insertTrackAt, addElementToTrack } = useTimelineStore();
-
- // Check if user has already accepted privacy on mount
- useEffect(() => {
- const hasAccepted = localStorage.getItem(PRIVACY_DIALOG_KEY) === "true";
- setHasAcceptedPrivacy(hasAccepted);
- }, []);
-
- const handleGenerateTranscript = async () => {
- try {
- setIsProcessing(true);
- setError(null);
- setProcessingStep("Extracting audio...");
-
- const audioBlob = await extractTimelineAudio();
-
- setProcessingStep("Encrypting audio...");
-
- // Encrypt the audio with a random key (zero-knowledge)
- const audioBuffer = await audioBlob.arrayBuffer();
- const encryptionResult = await encryptWithRandomKey(audioBuffer);
-
- // Convert encrypted data to blob for upload
- const encryptedBlob = new Blob([encryptionResult.encryptedData]);
-
- setProcessingStep("Uploading...");
- const uploadResponse = await fetch("/api/get-upload-url", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ fileExtension: "wav" }),
- });
-
- if (!uploadResponse.ok) {
- const error = await uploadResponse.json();
- throw new Error(error.message || "Failed to get upload URL");
- }
-
- const { uploadUrl, fileName } = await uploadResponse.json();
-
- // Upload to R2
- await fetch(uploadUrl, {
- method: "PUT",
- body: encryptedBlob,
- });
-
- setProcessingStep("Transcribing...");
-
- // Call Modal transcription API with encryption parameters
- const transcriptionResponse = await fetch("/api/transcribe", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- filename: fileName,
- language:
- selectedCountry === "auto" ? "auto" : selectedCountry.toLowerCase(),
- // Send the raw encryption key and IV (zero-knowledge)
- decryptionKey: arrayBufferToBase64(encryptionResult.key),
- iv: arrayBufferToBase64(encryptionResult.iv),
- }),
- });
-
- if (!transcriptionResponse.ok) {
- const error = await transcriptionResponse.json();
- throw new Error(error.message || "Transcription failed");
- }
-
- const { text, segments } = await transcriptionResponse.json();
-
- console.log("Transcription completed:", { text, segments });
-
- const shortCaptions: Array<{
- text: string;
- startTime: number;
- duration: number;
- }> = [];
-
- let globalEndTime = 0; // Track the end time of the last caption globally
-
- segments.forEach((segment: any) => {
- const words = segment.text.trim().split(/\s+/);
- const segmentDuration = segment.end - segment.start;
- const wordsPerSecond = words.length / segmentDuration;
-
- // Split into chunks of 2-4 words
- const chunks: string[] = [];
- for (let i = 0; i < words.length; i += 3) {
- chunks.push(words.slice(i, i + 3).join(" "));
- }
-
- // Calculate timing for each chunk to place them sequentially
- let chunkStartTime = segment.start;
- chunks.forEach((chunk) => {
- const chunkWords = chunk.split(/\s+/).length;
- const chunkDuration = Math.max(0.8, chunkWords / wordsPerSecond); // Minimum 0.8s per chunk
-
- let adjustedStartTime = chunkStartTime;
-
- // Prevent overlapping: if this caption would start before the last one ends,
- // start it right after the last one ends
- if (adjustedStartTime < globalEndTime) {
- adjustedStartTime = globalEndTime;
- }
-
- shortCaptions.push({
- text: chunk,
- startTime: adjustedStartTime,
- duration: chunkDuration,
- });
-
- // Update global end time
- globalEndTime = adjustedStartTime + chunkDuration;
-
- // Next chunk starts when this one ends (for within-segment timing)
- chunkStartTime += chunkDuration;
- });
- });
-
- // Create a single track for all captions
- const captionTrackId = insertTrackAt("text", 0);
-
- // Add all caption elements to the same track
- shortCaptions.forEach((caption, index) => {
- addElementToTrack(captionTrackId, {
- ...DEFAULT_TEXT_ELEMENT,
- name: `Caption ${index + 1}`,
- content: caption.text,
- duration: caption.duration,
- startTime: caption.startTime,
- fontSize: 65, // Larger for captions
- fontWeight: "bold", // Bold for captions
- } as TextElement);
- });
-
- console.log(
- `✅ ${shortCaptions.length} short-form caption chunks added to timeline!`
- );
- } catch (error) {
- console.error("Transcription failed:", error);
- setError(
- error instanceof Error ? error.message : "An unexpected error occurred"
- );
- } finally {
- setIsProcessing(false);
- setProcessingStep("");
- }
- };
-
- return (
- <BaseView ref={containerRef} className="flex flex-col justify-between h-full">
- <PropertyGroup title="Language">
- <LanguageSelect
- selectedCountry={selectedCountry}
- onSelect={setSelectedCountry}
- containerRef={containerRef}
- languages={languages}
- />
- </PropertyGroup>
-
- <div className="flex flex-col gap-4">
- {error && (
- <div className="p-3 bg-destructive/10 border border-destructive/20 rounded-md">
- <p className="text-sm text-destructive">{error}</p>
- </div>
- )}
-
- <Button
- className="w-full"
- onClick={() => {
- if (hasAcceptedPrivacy) {
- handleGenerateTranscript();
- } else {
- setShowPrivacyDialog(true);
- }
- }}
- disabled={isProcessing}
- >
- {isProcessing && <Loader2 className="mr-1 h-4 w-4 animate-spin" />}
- {isProcessing ? processingStep : "Generate transcript"}
- </Button>
-
- <Dialog open={showPrivacyDialog} onOpenChange={setShowPrivacyDialog}>
- <DialogContent className="sm:max-w-md">
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2">
- <Shield className="h-5 w-5" />
- Audio Processing Notice
- </DialogTitle>
- <DialogDescription className="space-y-3">
- <p>
- To generate captions, we need to process your timeline audio
- using speech-to-text technology.
- </p>
-
- <div className="space-y-2 pt-2">
- <div className="flex items-start gap-2">
- <Shield className="h-4 w-4 flex-shrink-0" />
- <span className="text-sm">
- Zero-knowledge encryption - we cannot decrypt your files
- even if we wanted to
- </span>
- </div>
-
- <div className="flex items-start gap-2">
- <Shield className="h-4 w-4 flex-shrink-0" />
- <span className="text-sm">
- Encryption keys generated randomly in your browser, never
- stored anywhere
- </span>
- </div>
-
- <div className="flex items-start gap-2">
- <Upload className="h-4 w-4 flex-shrink-0" />
- <span className="text-sm">
- Audio encrypted before upload - raw audio never leaves
- your device
- </span>
- </div>
-
- <div className="flex items-start gap-2">
- <Trash2 className="h-4 w-4 flex-shrink-0" />
- <span className="text-sm">
- Everything permanently deleted within seconds after
- transcription
- </span>
- </div>
- </div>
-
- <p className="text-xs text-muted-foreground">
- <strong>True zero-knowledge privacy:</strong> Encryption keys
- are generated randomly in your browser and never stored
- anywhere. It's cryptographically impossible for us, our cloud
- providers, or anyone else to decrypt your audio files.
- </p>
- </DialogDescription>
- </DialogHeader>
- <DialogFooter className="gap-2">
- <Button
- variant="outline"
- onClick={() => setShowPrivacyDialog(false)}
- disabled={isProcessing}
- >
- Cancel
- </Button>
- <Button
- onClick={() => {
- localStorage.setItem(PRIVACY_DIALOG_KEY, "true");
- setHasAcceptedPrivacy(true);
- setShowPrivacyDialog(false);
- handleGenerateTranscript();
- }}
- disabled={isProcessing}
- >
- Continue & Generate Captions
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- </div>
- </BaseView>
- );
-}
diff --git a/apps/web/src/components/editor/media-panel/views/media.tsx b/apps/web/src/components/editor/media-panel/views/media.tsx
deleted file mode 100644
index 07a44a7c..00000000
--- a/apps/web/src/components/editor/media-panel/views/media.tsx
+++ /dev/null
@@ -1,545 +0,0 @@
-"use client";
-
-import { useDragDrop } from "@/hooks/use-drag-drop";
-import { processMediaFiles } from "@/lib/media-processing";
-import { useMediaStore } from "@/stores/media-store";
-import { MediaFile } from "@/types/media";
-import {
- ArrowDown01,
- CloudUpload,
- Grid2X2,
- Image,
- List,
- Loader2,
- Music,
- Video,
-} from "lucide-react";
-import { useRef, useState, useMemo } from "react";
-import { useHighlightScroll } from "@/hooks/use-highlight-scroll";
-import { toast } from "sonner";
-import { Button } from "@/components/ui/button";
-import { MediaDragOverlay } from "@/components/editor/media-panel/drag-overlay";
-import {
- ContextMenu,
- ContextMenuContent,
- ContextMenuItem,
- ContextMenuTrigger,
-} from "@/components/ui/context-menu";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { DraggableMediaItem } from "@/components/ui/draggable-item";
-import { useProjectStore } from "@/stores/project-store";
-import { useTimelineStore } from "@/stores/timeline-store";
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
-import { usePanelStore } from "@/stores/panel-store";
-import { useMediaPanelStore } from "../store";
-
-function MediaItemWithContextMenu({
- item,
- children,
- onRemove,
-}: {
- item: MediaFile;
- children: React.ReactNode;
- onRemove: (e: React.MouseEvent, id: string) => Promise<void>;
-}) {
- return (
- <ContextMenu>
- <ContextMenuTrigger>{children}</ContextMenuTrigger>
- <ContextMenuContent>
- <ContextMenuItem>Export clips</ContextMenuItem>
- <ContextMenuItem
- variant="destructive"
- onClick={(e) => onRemove(e, item.id)}
- >
- Delete
- </ContextMenuItem>
- </ContextMenuContent>
- </ContextMenu>
- );
-}
-
-export function MediaView() {
- const { mediaFiles, addMediaFile, removeMediaFile } = useMediaStore();
- const { activeProject } = useProjectStore();
- const { mediaViewMode, setMediaViewMode } = usePanelStore();
- const fileInputRef = useRef<HTMLInputElement>(null);
- const [isProcessing, setIsProcessing] = useState(false);
- const [progress, setProgress] = useState(0);
- const [sortBy, setSortBy] = useState<"name" | "type" | "duration" | "size">(
- "name"
- );
- const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
- const { highlightMediaId, clearHighlight } = useMediaPanelStore();
- const { highlightedId, registerElement } = useHighlightScroll(
- highlightMediaId,
- clearHighlight
- );
-
- const processFiles = async (files: FileList | File[]) => {
- if (!files || files.length === 0) return;
- if (!activeProject) {
- toast.error("No active project");
- return;
- }
-
- setIsProcessing(true);
- setProgress(0);
- try {
- const processedItems = await processMediaFiles(files, (p) =>
- setProgress(p)
- );
- for (const item of processedItems) {
- await addMediaFile(activeProject.id, item);
- }
- } catch (error) {
- console.error("Error processing files:", error);
- toast.error("Failed to process files");
- } finally {
- setIsProcessing(false);
- setProgress(0);
- }
- };
-
- const { isDragOver, dragProps } = useDragDrop({
- // When files are dropped, process them
- onDrop: processFiles,
- });
-
- const handleFileSelect = () => fileInputRef.current?.click(); // Open file picker
-
- const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- if (e.target.files) processFiles(e.target.files);
- e.target.value = ""; // Reset input
- };
-
- const handleRemove = async (e: React.MouseEvent, id: string) => {
- e.stopPropagation();
-
- if (!activeProject) {
- toast.error("No active project");
- return;
- }
-
- await removeMediaFile(activeProject.id, id);
- };
-
- const formatDuration = (duration: number) => {
- // Format seconds as mm:ss
- const min = Math.floor(duration / 60);
- const sec = Math.floor(duration % 60);
- return `${min}:${sec.toString().padStart(2, "0")}`;
- };
-
- const filteredMediaItems = useMemo(() => {
- let filtered = mediaFiles.filter((item) => {
- if (item.ephemeral) return false;
- return true;
- });
-
- filtered.sort((a, b) => {
- let valueA: string | number;
- let valueB: string | number;
-
- switch (sortBy) {
- case "name":
- valueA = a.name.toLowerCase();
- valueB = b.name.toLowerCase();
- break;
- case "type":
- valueA = a.type;
- valueB = b.type;
- break;
- case "duration":
- valueA = a.duration || 0;
- valueB = b.duration || 0;
- break;
- case "size":
- valueA = a.file.size;
- valueB = b.file.size;
- break;
- default:
- return 0;
- }
-
- if (valueA < valueB) return sortOrder === "asc" ? -1 : 1;
- if (valueA > valueB) return sortOrder === "asc" ? 1 : -1;
- return 0;
- });
-
- return filtered;
- }, [mediaFiles, sortBy, sortOrder]);
-
- const previewComponents = useMemo(() => {
- const previews = new Map<string, React.ReactNode>();
-
- filteredMediaItems.forEach((item) => {
- let preview: React.ReactNode;
-
- if (item.type === "image") {
- preview = (
- <div className="w-full h-full flex items-center justify-center">
- <img
- src={item.url}
- alt={item.name}
- className="w-full max-h-full object-cover"
- loading="lazy"
- />
- </div>
- );
- } else if (item.type === "video") {
- if (item.thumbnailUrl) {
- preview = (
- <div className="relative w-full h-full">
- <img
- src={item.thumbnailUrl}
- alt={item.name}
- className="w-full h-full object-cover rounded"
- loading="lazy"
- />
- <div className="absolute inset-0 flex items-center justify-center bg-black/20 rounded">
- <Video className="h-6 w-6 text-white drop-shadow-md" />
- </div>
- {item.duration && (
- <div className="absolute bottom-1 right-1 bg-black/70 text-white text-xs px-1 rounded">
- {formatDuration(item.duration)}
- </div>
- )}
- </div>
- );
- } else {
- preview = (
- <div className="w-full h-full bg-muted/30 flex flex-col items-center justify-center text-muted-foreground rounded">
- <Video className="h-6 w-6 mb-1" />
- <span className="text-xs">Video</span>
- {item.duration && (
- <span className="text-xs opacity-70">
- {formatDuration(item.duration)}
- </span>
- )}
- </div>
- );
- }
- } else if (item.type === "audio") {
- preview = (
- <div className="w-full h-full bg-linear-to-br from-green-500/20 to-emerald-500/20 flex flex-col items-center justify-center text-muted-foreground rounded border border-green-500/20">
- <Music className="h-6 w-6 mb-1" />
- <span className="text-xs">Audio</span>
- {item.duration && (
- <span className="text-xs opacity-70">
- {formatDuration(item.duration)}
- </span>
- )}
- </div>
- );
- } else {
- preview = (
- <div className="w-full h-full bg-muted/30 flex flex-col items-center justify-center text-muted-foreground rounded">
- <Image className="h-6 w-6" />
- <span className="text-xs mt-1">Unknown</span>
- </div>
- );
- }
-
- previews.set(item.id, preview);
- });
-
- return previews;
- }, [filteredMediaItems]);
-
- const renderPreview = (item: MediaFile) => previewComponents.get(item.id);
-
- return (
- <>
- {/* Hidden file input for uploading media */}
- <input
- ref={fileInputRef}
- type="file"
- accept="image/*,video/*,audio/*"
- multiple
- className="hidden"
- onChange={handleFileChange}
- />
-
- <div
- className={`h-full flex flex-col gap-1 transition-colors relative ${isDragOver ? "bg-accent/30" : ""}`}
- {...dragProps}
- >
- <div className="p-3 pb-2 bg-panel">
- {/* Search and filter controls */}
- <div className="flex items-center gap-2">
- <Button
- variant="outline"
- size="lg"
- onClick={handleFileSelect}
- disabled={isProcessing}
- className="!bg-background px-4 flex-1 justify-center items-center h-9 opacity-100 hover:opacity-75 transition-opacity"
- >
- {isProcessing ? (
- <Loader2 className="h-4 w-4 animate-spin" />
- ) : (
- <CloudUpload className="h-4 w-4" />
- )}
- <span>Upload</span>
- </Button>
- <div className="flex items-center gap-0">
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- size="icon"
- variant="text"
- onClick={() =>
- setMediaViewMode(
- mediaViewMode === "grid" ? "list" : "grid"
- )
- }
- disabled={isProcessing}
- className="justify-center items-center"
- >
- {mediaViewMode === "grid" ? (
- <List strokeWidth={1.5} className="!size-[1.05rem]" />
- ) : (
- <Grid2X2
- strokeWidth={1.5}
- className="!size-[1.05rem]"
- />
- )}
- </Button>
- </TooltipTrigger>
- <TooltipContent>
- <p>
- {mediaViewMode === "grid"
- ? "Switch to list view"
- : "Switch to grid view"}
- </p>
- </TooltipContent>
- <Tooltip>
- <DropdownMenu>
- <TooltipTrigger asChild>
- <DropdownMenuTrigger asChild>
- <Button
- size="icon"
- variant="text"
- disabled={isProcessing}
- className="justify-center items-center"
- >
- <ArrowDown01
- strokeWidth={1.5}
- className="!size-[1.05rem]"
- />
- </Button>
- </DropdownMenuTrigger>
- </TooltipTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuItem
- onClick={() => {
- if (sortBy === "name") {
- setSortOrder(
- sortOrder === "asc" ? "desc" : "asc"
- );
- } else {
- setSortBy("name");
- setSortOrder("asc");
- }
- }}
- >
- Name{" "}
- {sortBy === "name" &&
- (sortOrder === "asc" ? "↑" : "↓")}
- </DropdownMenuItem>
- <DropdownMenuItem
- onClick={() => {
- if (sortBy === "type") {
- setSortOrder(
- sortOrder === "asc" ? "desc" : "asc"
- );
- } else {
- setSortBy("type");
- setSortOrder("asc");
- }
- }}
- >
- Type{" "}
- {sortBy === "type" &&
- (sortOrder === "asc" ? "↑" : "↓")}
- </DropdownMenuItem>
- <DropdownMenuItem
- onClick={() => {
- if (sortBy === "duration") {
- setSortOrder(
- sortOrder === "asc" ? "desc" : "asc"
- );
- } else {
- setSortBy("duration");
- setSortOrder("asc");
- }
- }}
- >
- Duration{" "}
- {sortBy === "duration" &&
- (sortOrder === "asc" ? "↑" : "↓")}
- </DropdownMenuItem>
- <DropdownMenuItem
- onClick={() => {
- if (sortBy === "size") {
- setSortOrder(
- sortOrder === "asc" ? "desc" : "asc"
- );
- } else {
- setSortBy("size");
- setSortOrder("asc");
- }
- }}
- >
- File Size{" "}
- {sortBy === "size" &&
- (sortOrder === "asc" ? "↑" : "↓")}
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- <TooltipContent>
- <p>
- Sort by {sortBy} (
- {sortOrder === "asc" ? "ascending" : "descending"})
- </p>
- </TooltipContent>
- </Tooltip>
- </Tooltip>
- </TooltipProvider>
- </div>
- </div>
- </div>
-
- <div className="h-full w-full overflow-y-auto scrollbar-thin pt-1">
- <div className="flex-1 p-3 pt-0 w-full">
- {isDragOver || filteredMediaItems.length === 0 ? (
- <MediaDragOverlay
- isVisible={true}
- isProcessing={isProcessing}
- progress={progress}
- onClick={handleFileSelect}
- isEmptyState={filteredMediaItems.length === 0 && !isDragOver}
- />
- ) : mediaViewMode === "grid" ? (
- <GridView
- filteredMediaItems={filteredMediaItems}
- renderPreview={renderPreview}
- handleRemove={handleRemove}
- highlightedId={highlightedId}
- registerElement={registerElement}
- />
- ) : (
- <ListView
- filteredMediaItems={filteredMediaItems}
- renderPreview={renderPreview}
- handleRemove={handleRemove}
- highlightedId={highlightedId}
- registerElement={registerElement}
- />
- )}
- </div>
- </div>
- </div>
- </>
- );
-}
-
-function GridView({
- filteredMediaItems,
- renderPreview,
- handleRemove,
- highlightedId,
- registerElement,
-}: {
- filteredMediaItems: MediaFile[];
- renderPreview: (item: MediaFile) => React.ReactNode;
- handleRemove: (e: React.MouseEvent, id: string) => Promise<void>;
- highlightedId: string | null;
- registerElement: (id: string, element: HTMLElement | null) => void;
-}) {
- const { addElementAtTime } = useTimelineStore();
-
- return (
- <div
- className="grid gap-2"
- style={{
- gridTemplateColumns: "repeat(auto-fill, 160px)",
- }}
- >
- {filteredMediaItems.map((item) => (
- <div key={item.id} ref={(el) => registerElement(item.id, el)}>
- <MediaItemWithContextMenu item={item} onRemove={handleRemove}>
- <DraggableMediaItem
- name={item.name}
- preview={renderPreview(item)}
- dragData={{
- id: item.id,
- type: item.type,
- name: item.name,
- }}
- showPlusOnDrag={false}
- onAddToTimeline={(currentTime) =>
- addElementAtTime(item, currentTime)
- }
- rounded={false}
- variant="card"
- isHighlighted={highlightedId === item.id}
- />
- </MediaItemWithContextMenu>
- </div>
- ))}
- </div>
- );
-}
-
-function ListView({
- filteredMediaItems,
- renderPreview,
- handleRemove,
- highlightedId,
- registerElement,
-}: {
- filteredMediaItems: MediaFile[];
- renderPreview: (item: MediaFile) => React.ReactNode;
- handleRemove: (e: React.MouseEvent, id: string) => Promise<void>;
- highlightedId: string | null;
- registerElement: (id: string, element: HTMLElement | null) => void;
-}) {
- const { addElementAtTime } = useTimelineStore();
-
- return (
- <div className="space-y-1">
- {filteredMediaItems.map((item) => (
- <div key={item.id} ref={(el) => registerElement(item.id, el)}>
- <MediaItemWithContextMenu item={item} onRemove={handleRemove}>
- <DraggableMediaItem
- name={item.name}
- preview={renderPreview(item)}
- dragData={{
- id: item.id,
- type: item.type,
- name: item.name,
- }}
- showPlusOnDrag={false}
- onAddToTimeline={(currentTime) =>
- addElementAtTime(item, currentTime)
- }
- variant="compact"
- isHighlighted={highlightedId === item.id}
- />
- </MediaItemWithContextMenu>
- </div>
- ))}
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/media-panel/views/settings.tsx b/apps/web/src/components/editor/media-panel/views/settings.tsx
deleted file mode 100644
index be54a9c4..00000000
--- a/apps/web/src/components/editor/media-panel/views/settings.tsx
+++ /dev/null
@@ -1,332 +0,0 @@
-"use client";
-
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { PanelBaseView as BaseView } from "@/components/editor/panel-base-view";
-import {
- PropertyItem,
- PropertyItemLabel,
- PropertyItemValue,
- PropertyGroup,
-} from "../../properties-panel/property-item";
-import { FPS_PRESETS } from "@/constants/timeline-constants";
-import { useProjectStore } from "@/stores/project-store";
-import type { BlurIntensity } from "@/types/project";
-import { useEditorStore } from "@/stores/editor-store";
-import { useAspectRatio } from "@/hooks/use-aspect-ratio";
-import Image from "next/image";
-import { cn } from "@/lib/utils";
-import { colors } from "@/data/colors/solid";
-import { patternCraftGradients } from "@/data/colors/pattern-craft";
-import { PipetteIcon, PlusIcon } from "lucide-react";
-import { useMemo, memo, useCallback } from "react";
-import { syntaxUIGradients } from "@/data/colors/syntax-ui";
-import { Button } from "@/components/ui/button";
-import { Separator } from "@/components/ui/separator";
-
-export function SettingsView() {
- return <ProjectSettingsTabs />;
-}
-
-function ProjectSettingsTabs() {
- return (
- <BaseView
- defaultTab="project-info"
- tabs={[
- {
- value: "project-info",
- label: "Project info",
- content: (
- <div className="p-5">
- <ProjectInfoView />
- </div>
- ),
- },
- {
- value: "background",
- label: "Background",
- content: (
- <div className="flex flex-col justify-between h-full">
- <div className="flex-1 p-5">
- <BackgroundView />
- </div>
- <div className="flex flex-col sticky -bottom-0 bg-panel/85 backdrop-blur-lg">
- <Separator />
- <Button className="w-fit h-auto p-5 py-4 !bg-transparent shadow-none text-muted-foreground hover:text-foreground/85 text-xs">
- Custom background
- <PlusIcon />
- </Button>
- </div>
-
- {/* Another UI, looks so beautiful i don't wanna remove it */}
- {/* <div className="flex flex-col justify-center items-center pb-5 sticky bottom-0">
- <Button className="w-fit h-auto gap-1.5 px-3.5 py-1.5 bg-foreground hover:bg-foreground/85 text-background rounded-full">
- <span className="text-sm">Custom</span>
- <PlusIcon className="" />
- </Button>
- </div> */}
- </div>
- ),
- },
- ]}
- className="flex flex-col justify-between h-full p-0"
- />
- );
-}
-
-function ProjectInfoView() {
- const { activeProject, updateProjectFps, updateCanvasSize } =
- useProjectStore();
- const { canvasPresets } = useEditorStore();
- const { getDisplayName } = useAspectRatio();
-
- const handleAspectRatioChange = (value: string) => {
- const preset = canvasPresets.find((p) => p.name === value);
- if (preset) {
- updateCanvasSize(
- { width: preset.width, height: preset.height },
- "preset"
- );
- }
- };
-
- const handleFpsChange = (value: string) => {
- const fps = parseFloat(value);
- updateProjectFps(fps);
- };
-
- return (
- <div className="flex flex-col gap-4">
- <PropertyItem direction="column">
- <PropertyItemLabel>Name</PropertyItemLabel>
- <PropertyItemValue>
- {activeProject?.name || "Untitled project"}
- </PropertyItemValue>
- </PropertyItem>
-
- <PropertyItem direction="column">
- <PropertyItemLabel>Aspect ratio</PropertyItemLabel>
- <PropertyItemValue>
- <Select
- value={getDisplayName()}
- onValueChange={handleAspectRatioChange}
- >
- <SelectTrigger className="bg-panel-accent">
- <SelectValue placeholder="Select an aspect ratio" />
- </SelectTrigger>
- <SelectContent>
- {canvasPresets.map((preset) => (
- <SelectItem key={preset.name} value={preset.name}>
- {preset.name}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </PropertyItemValue>
- </PropertyItem>
-
- <PropertyItem direction="column">
- <PropertyItemLabel>Frame rate</PropertyItemLabel>
- <PropertyItemValue>
- <Select
- value={(activeProject?.fps || 30).toString()}
- onValueChange={handleFpsChange}
- >
- <SelectTrigger className="bg-panel-accent">
- <SelectValue placeholder="Select a frame rate" />
- </SelectTrigger>
- <SelectContent>
- {FPS_PRESETS.map((preset) => (
- <SelectItem key={preset.value} value={preset.value}>
- {preset.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </PropertyItemValue>
- </PropertyItem>
- </div>
- );
-}
-
-const BlurPreview = memo(
- ({
- blur,
- isSelected,
- onSelect,
- }: {
- blur: { label: string; value: number };
- isSelected: boolean;
- onSelect: () => void;
- }) => (
- <div
- className={cn(
- "w-full aspect-square rounded-sm cursor-pointer border border-foreground/15 hover:border-primary relative overflow-hidden",
- isSelected && "border-2 border-primary"
- )}
- onClick={onSelect}
- >
- <Image
- src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
- alt={`Blur preview ${blur.label}`}
- fill
- className="object-cover"
- style={{ filter: `blur(${blur.value}px)` }}
- loading="eager"
- />
- <div className="absolute bottom-1 left-1 right-1 text-center">
- <span className="text-xs text-white bg-black/50 px-1 rounded">
- {blur.label}
- </span>
- </div>
- </div>
- )
-);
-
-BlurPreview.displayName = "BlurPreview";
-
-const BackgroundPreviews = memo(
- ({
- backgrounds,
- currentBackgroundColor,
- isColorBackground,
- handleColorSelect,
- useBackgroundColor = false,
- }: {
- backgrounds: string[];
- currentBackgroundColor: string;
- isColorBackground: boolean;
- handleColorSelect: (bg: string) => void;
- useBackgroundColor?: boolean;
- }) => {
- return useMemo(
- () =>
- backgrounds.map((bg, index) => (
- <div
- key={`${index}-${bg}`}
- className={cn(
- "w-full aspect-square rounded-sm cursor-pointer border border-foreground/15 hover:border-primary",
- isColorBackground &&
- bg === currentBackgroundColor &&
- "border-2 border-primary"
- )}
- style={
- useBackgroundColor
- ? { backgroundColor: bg }
- : {
- background: bg,
- backgroundSize: "cover",
- backgroundPosition: "center",
- backgroundRepeat: "no-repeat",
- }
- }
- onClick={() => handleColorSelect(bg)}
- />
- )),
- [
- backgrounds,
- isColorBackground,
- currentBackgroundColor,
- handleColorSelect,
- useBackgroundColor,
- ]
- );
- }
-);
-
-BackgroundPreviews.displayName = "BackgroundPreviews";
-
-function BackgroundView() {
- const { activeProject, updateBackgroundType } = useProjectStore();
-
- const blurLevels = useMemo<Array<{ label: string; value: BlurIntensity }>>(
- () => [
- { label: "Light", value: 4 },
- { label: "Medium", value: 8 },
- { label: "Heavy", value: 18 },
- ],
- []
- );
-
- const handleBlurSelect = useCallback(
- async (blurIntensity: BlurIntensity) => {
- await updateBackgroundType("blur", { blurIntensity });
- },
- [updateBackgroundType]
- );
-
- const handleColorSelect = useCallback(
- async (color: string) => {
- await updateBackgroundType("color", { backgroundColor: color });
- },
- [updateBackgroundType]
- );
-
- const currentBlurIntensity = activeProject?.blurIntensity || 8;
- const isBlurBackground = activeProject?.backgroundType === "blur";
- const currentBackgroundColor = activeProject?.backgroundColor || "#000000";
- const isColorBackground = activeProject?.backgroundType === "color";
-
- const blurPreviews = useMemo(
- () =>
- blurLevels.map((blur) => (
- <BlurPreview
- key={blur.value}
- blur={blur}
- isSelected={isBlurBackground && currentBlurIntensity === blur.value}
- onSelect={() => handleBlurSelect(blur.value)}
- />
- )),
- [blurLevels, isBlurBackground, currentBlurIntensity, handleBlurSelect]
- );
-
- return (
- <div className="flex flex-col gap-4 h-full">
- <PropertyGroup title="Blur" defaultExpanded={false}>
- <div className="grid grid-cols-4 gap-2 w-full">{blurPreviews}</div>
- </PropertyGroup>
-
- <PropertyGroup title="Colors" defaultExpanded={false}>
- <div className="grid grid-cols-4 gap-2 w-full">
- <div className="w-full aspect-square rounded-sm cursor-pointer border border-foreground/15 hover:border-primary flex items-center justify-center">
- <PipetteIcon className="size-4" />
- </div>
- <BackgroundPreviews
- backgrounds={colors}
- currentBackgroundColor={currentBackgroundColor}
- isColorBackground={isColorBackground}
- handleColorSelect={handleColorSelect}
- useBackgroundColor={true}
- />
- </div>
- </PropertyGroup>
-
- <PropertyGroup title="Pattern craft" defaultExpanded={false}>
- <div className="grid grid-cols-4 gap-2 w-full">
- <BackgroundPreviews
- backgrounds={patternCraftGradients}
- currentBackgroundColor={currentBackgroundColor}
- isColorBackground={isColorBackground}
- handleColorSelect={handleColorSelect}
- />
- </div>
- </PropertyGroup>
-
- <PropertyGroup title="Syntax UI" defaultExpanded={false}>
- <div className="grid grid-cols-4 gap-2 w-full">
- <BackgroundPreviews
- backgrounds={syntaxUIGradients}
- currentBackgroundColor={currentBackgroundColor}
- isColorBackground={isColorBackground}
- handleColorSelect={handleColorSelect}
- />
- </div>
- </PropertyGroup>
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/media-panel/views/sounds.tsx b/apps/web/src/components/editor/media-panel/views/sounds.tsx
deleted file mode 100644
index 700eaea7..00000000
--- a/apps/web/src/components/editor/media-panel/views/sounds.tsx
+++ /dev/null
@@ -1,562 +0,0 @@
-"use client";
-
-import { Input } from "@/components/ui/input";
-import { useState, useMemo, useEffect } from "react";
-import { Separator } from "@/components/ui/separator";
-import { ScrollArea } from "@/components/ui/scroll-area";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import {
- PlayIcon,
- PauseIcon,
- HeartIcon,
- PlusIcon,
- ListFilter,
-} from "lucide-react";
-import { useSoundsStore } from "@/stores/sounds-store";
-import { useSoundSearch } from "@/hooks/use-sound-search";
-import type { SoundEffect, SavedSound } from "@/types/sounds";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuCheckboxItem,
-} from "@/components/ui/dropdown-menu";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { cn } from "@/lib/utils";
-import { useInfiniteScroll } from "@/hooks/use-infinite-scroll";
-
-export function SoundsView() {
- return (
- <div className="h-full flex flex-col">
- <Tabs defaultValue="sound-effects" className="flex flex-col h-full">
- <div className="px-3 pt-4 pb-0">
- <TabsList>
- <TabsTrigger value="sound-effects">Sound effects</TabsTrigger>
- <TabsTrigger value="songs">Songs</TabsTrigger>
- <TabsTrigger value="saved">Saved</TabsTrigger>
- </TabsList>
- </div>
- <Separator className="my-4" />
- <TabsContent
- value="sound-effects"
- className="p-5 pt-0 mt-0 flex-1 flex flex-col min-h-0"
- >
- <SoundEffectsView />
- </TabsContent>
- <TabsContent
- value="saved"
- className="p-5 pt-0 mt-0 flex-1 flex flex-col min-h-0"
- >
- <SavedSoundsView />
- </TabsContent>
- <TabsContent
- value="songs"
- className="p-5 pt-0 mt-0 flex-1 flex flex-col min-h-0"
- >
- <SongsView />
- </TabsContent>
- </Tabs>
- </div>
- );
-}
-
-function SoundEffectsView() {
- const {
- topSoundEffects,
- isLoading,
- searchQuery,
- setSearchQuery,
- scrollPosition,
- setScrollPosition,
- loadSavedSounds,
- isSoundSaved,
- toggleSavedSound,
- showCommercialOnly,
- toggleCommercialFilter,
- hasLoaded,
- setTopSoundEffects,
- setLoading,
- setError,
- setHasLoaded,
- setCurrentPage,
- setHasNextPage,
- setTotalCount,
- } = useSoundsStore();
- const {
- results: searchResults,
- isLoading: isSearching,
- loadMore,
- hasNextPage,
- isLoadingMore,
- } = useSoundSearch(searchQuery, showCommercialOnly);
-
- // Audio playback state
- const [playingId, setPlayingId] = useState<number | null>(null);
- const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(
- null
- );
-
- const { scrollAreaRef, handleScroll } = useInfiniteScroll({
- onLoadMore: loadMore,
- hasMore: hasNextPage,
- isLoading: isLoadingMore || isSearching,
- });
-
- useEffect(() => {
- loadSavedSounds();
-
- if (!hasLoaded) {
- let ignore = false;
-
- const fetchTopSounds = async () => {
- try {
- if (!ignore) {
- setLoading(true);
- setError(null);
- }
-
- const response = await fetch(
- "/api/sounds/search?page_size=50&sort=downloads"
- );
-
- if (!ignore) {
- if (!response.ok) {
- throw new Error(`Failed to fetch: ${response.status}`);
- }
-
- const data = await response.json();
- setTopSoundEffects(data.results);
- setHasLoaded(true);
-
- setCurrentPage(1);
- setHasNextPage(!!data.next);
- setTotalCount(data.count);
- }
- } catch (error) {
- if (!ignore) {
- console.error("Failed to fetch top sounds:", error);
- setError(
- error instanceof Error ? error.message : "Failed to load sounds"
- );
- }
- } finally {
- if (!ignore) {
- setLoading(false);
- }
- }
- };
-
- const timeoutId = setTimeout(fetchTopSounds, 100);
-
- return () => {
- clearTimeout(timeoutId);
- ignore = true;
- };
- }
-
- if (scrollAreaRef.current && scrollPosition > 0) {
- const timeoutId = setTimeout(() => {
- scrollAreaRef.current?.scrollTo({ top: scrollPosition });
- }, 100);
-
- return () => clearTimeout(timeoutId);
- }
- }, [
- hasLoaded,
- setTopSoundEffects,
- setLoading,
- setError,
- setHasLoaded,
- setCurrentPage,
- setHasNextPage,
- setTotalCount,
- ]);
-
- const handleScrollWithPosition = (event: React.UIEvent<HTMLDivElement>) => {
- const { scrollTop } = event.currentTarget;
- setScrollPosition(scrollTop);
- handleScroll(event);
- };
-
- const displayedSounds = useMemo(() => {
- const sounds = searchQuery ? searchResults : topSoundEffects;
- return sounds;
- }, [searchQuery, searchResults, topSoundEffects]);
-
- const playSound = (sound: SoundEffect) => {
- if (playingId === sound.id) {
- audioElement?.pause();
- setPlayingId(null);
- return;
- }
-
- // Stop previous sound
- audioElement?.pause();
-
- if (sound.previewUrl) {
- const audio = new Audio(sound.previewUrl);
- audio.addEventListener("ended", () => {
- setPlayingId(null);
- });
- audio.addEventListener("error", (e) => {
- setPlayingId(null);
- });
- audio.play().catch((error) => {
- setPlayingId(null);
- });
-
- setAudioElement(audio);
- setPlayingId(sound.id);
- }
- };
-
- return (
- <div className="flex flex-col gap-5 mt-1 h-full">
- <div className="flex items-center gap-3">
- <Input
- placeholder="Search sound effects"
- className="bg-panel-accent w-full"
- containerClassName="w-full"
- value={searchQuery}
- onChange={(e) => setSearchQuery(e.target.value)}
- showClearIcon
- onClear={() => setSearchQuery("")}
- />
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- variant="text"
- size="icon"
- className={cn(showCommercialOnly && "text-primary")}
- >
- <ListFilter className="w-4 h-4" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end" className="w-56">
- <DropdownMenuCheckboxItem
- checked={showCommercialOnly}
- onCheckedChange={toggleCommercialFilter}
- >
- Show only commercially licensed
- </DropdownMenuCheckboxItem>
- <div className="px-2 py-1.5 text-xs text-muted-foreground">
- {showCommercialOnly
- ? "Only showing sounds licensed for commercial use"
- : "Showing all sounds regardless of license"}
- </div>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
-
- <div className="relative h-full overflow-hidden">
- <ScrollArea
- className="flex-1 h-full"
- ref={scrollAreaRef}
- onScrollCapture={handleScrollWithPosition}
- >
- <div className="flex flex-col gap-4">
- {isLoading && !searchQuery && (
- <div className="text-muted-foreground text-sm">
- Loading sounds...
- </div>
- )}
- {isSearching && searchQuery && (
- <div className="text-muted-foreground text-sm">Searching...</div>
- )}
- {displayedSounds.map((sound) => (
- <AudioItem
- key={sound.id}
- sound={sound}
- isPlaying={playingId === sound.id}
- onPlay={() => playSound(sound)}
- isSaved={isSoundSaved(sound.id)}
- onToggleSaved={() => toggleSavedSound(sound)}
- />
- ))}
- {!isLoading && !isSearching && displayedSounds.length === 0 && (
- <div className="text-muted-foreground text-sm">
- {searchQuery ? "No sounds found" : "No sounds available"}
- </div>
- )}
- {isLoadingMore && (
- <div className="text-muted-foreground text-sm text-center py-4">
- Loading more sounds...
- </div>
- )}
- </div>
- </ScrollArea>
- </div>
- </div>
- );
-}
-
-function SavedSoundsView() {
- const {
- savedSounds,
- isLoadingSavedSounds,
- savedSoundsError,
- loadSavedSounds,
- isSoundSaved,
- toggleSavedSound,
- clearSavedSounds,
- } = useSoundsStore();
-
- // Audio playback state
- const [playingId, setPlayingId] = useState<number | null>(null);
- const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(
- null
- );
-
- // Clear confirmation dialog state
- const [showClearDialog, setShowClearDialog] = useState(false);
-
- // Load saved sounds when tab becomes active
- useEffect(() => {
- loadSavedSounds();
- }, [loadSavedSounds]);
-
- const playSound = (sound: SavedSound) => {
- if (playingId === sound.id) {
- audioElement?.pause();
- setPlayingId(null);
- return;
- }
-
- // Stop previous sound
- audioElement?.pause();
-
- if (sound.previewUrl) {
- const audio = new Audio(sound.previewUrl);
- audio.addEventListener("ended", () => {
- setPlayingId(null);
- });
- audio.addEventListener("error", (e) => {
- setPlayingId(null);
- });
- audio.play().catch((error) => {
- setPlayingId(null);
- });
-
- setAudioElement(audio);
- setPlayingId(sound.id);
- }
- };
-
- // Convert SavedSound to SoundEffect for compatibility with AudioItem
- const convertToSoundEffect = (savedSound: SavedSound): SoundEffect => ({
- id: savedSound.id,
- name: savedSound.name,
- description: "",
- url: "",
- previewUrl: savedSound.previewUrl,
- downloadUrl: savedSound.downloadUrl,
- duration: savedSound.duration,
- filesize: 0,
- type: "audio",
- channels: 0,
- bitrate: 0,
- bitdepth: 0,
- samplerate: 0,
- username: savedSound.username,
- tags: savedSound.tags,
- license: savedSound.license,
- created: savedSound.savedAt,
- downloads: 0,
- rating: 0,
- ratingCount: 0,
- });
-
- if (isLoadingSavedSounds) {
- return (
- <div className="flex items-center justify-center h-full">
- <div className="text-muted-foreground text-sm">
- Loading saved sounds...
- </div>
- </div>
- );
- }
-
- if (savedSoundsError) {
- return (
- <div className="flex items-center justify-center h-full">
- <div className="text-destructive text-sm">
- Error: {savedSoundsError}
- </div>
- </div>
- );
- }
-
- if (savedSounds.length === 0) {
- return (
- <div className="bg-panel h-full p-4 flex flex-col items-center justify-center gap-3">
- <HeartIcon
- className="w-10 h-10 text-muted-foreground"
- strokeWidth={1.5}
- />
- <div className="flex flex-col gap-2 text-center">
- <p className="text-lg font-medium">No saved sounds</p>
- <p className="text-sm text-muted-foreground text-balance">
- Click the heart icon on any sound to save it here
- </p>
- </div>
- </div>
- );
- }
-
- return (
- <div className="flex flex-col gap-5 mt-1 h-full">
- <div className="flex items-center justify-between">
- <p className="text-sm text-muted-foreground">
- {savedSounds.length} saved{" "}
- {savedSounds.length === 1 ? "sound" : "sounds"}
- </p>
- <Dialog open={showClearDialog} onOpenChange={setShowClearDialog}>
- <DialogTrigger asChild>
- <Button
- variant="text"
- size="sm"
- className="h-auto text-muted-foreground hover:text-destructive !opacity-100"
- >
- Clear all
- </Button>
- </DialogTrigger>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Clear all saved sounds?</DialogTitle>
- <DialogDescription>
- This will permanently remove all {savedSounds.length} saved
- sounds from your collection. This action cannot be undone.
- </DialogDescription>
- </DialogHeader>
- <DialogFooter>
- <Button variant="text" onClick={() => setShowClearDialog(false)}>
- Cancel
- </Button>
- <Button
- variant="destructive"
- onClick={async () => {
- await clearSavedSounds();
- setShowClearDialog(false);
- }}
- >
- Clear all sounds
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- </div>
-
- <div className="relative h-full overflow-hidden">
- <ScrollArea className="flex-1 h-full">
- <div className="flex flex-col gap-4">
- {savedSounds.map((sound) => (
- <AudioItem
- key={sound.id}
- sound={convertToSoundEffect(sound)}
- isPlaying={playingId === sound.id}
- onPlay={() => playSound(sound)}
- isSaved={isSoundSaved(sound.id)}
- onToggleSaved={() =>
- toggleSavedSound(convertToSoundEffect(sound))
- }
- />
- ))}
- </div>
- </ScrollArea>
- </div>
- </div>
- );
-}
-
-function SongsView() {
- return <div>Songs</div>;
-}
-
-interface AudioItemProps {
- sound: SoundEffect;
- isPlaying: boolean;
- onPlay: () => void;
- isSaved: boolean;
- onToggleSaved: () => void;
-}
-
-function AudioItem({
- sound,
- isPlaying,
- onPlay,
- isSaved,
- onToggleSaved,
-}: AudioItemProps) {
- const { addSoundToTimeline } = useSoundsStore();
-
- const handleClick = () => {
- onPlay();
- };
-
- const handleSaveClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- onToggleSaved();
- };
-
- const handleAddToTimeline = async (e: React.MouseEvent) => {
- e.stopPropagation();
- await addSoundToTimeline(sound);
- };
-
- return (
- <div
- className="group flex items-center gap-3 opacity-100 hover:opacity-75 transition-opacity cursor-pointer"
- onClick={handleClick}
- >
- <div className="relative w-12 h-12 bg-accent rounded-md flex items-center justify-center overflow-hidden shrink-0">
- <div className="absolute inset-0 bg-gradient-to-br from-primary/20 to-transparent" />
- {isPlaying ? (
- <PauseIcon className="w-5 h-5" />
- ) : (
- <PlayIcon className="w-5 h-5" />
- )}
- </div>
-
- <div className="flex-1 min-w-0 overflow-hidden">
- <p className="font-medium truncate text-sm">{sound.name}</p>
- <span className="text-xs text-muted-foreground truncate block">
- {sound.username}
- </span>
- </div>
-
- <div className="flex items-center gap-3 pr-2">
- <Button
- variant="text"
- size="icon"
- className="text-muted-foreground hover:text-foreground !opacity-100 w-auto"
- onClick={handleAddToTimeline}
- title="Add to timeline"
- >
- <PlusIcon className="w-4 h-4" />
- </Button>
- <Button
- variant="text"
- size="icon"
- className={`hover:text-foreground !opacity-100 w-auto ${
- isSaved
- ? "text-red-500 hover:text-red-600"
- : "text-muted-foreground"
- }`}
- onClick={handleSaveClick}
- title={isSaved ? "Remove from saved" : "Save sound"}
- >
- <HeartIcon className={`w-4 h-4 ${isSaved ? "fill-current" : ""}`} />
- </Button>
- </div>
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/media-panel/views/stickers.tsx b/apps/web/src/components/editor/media-panel/views/stickers.tsx
deleted file mode 100644
index f67450cc..00000000
--- a/apps/web/src/components/editor/media-panel/views/stickers.tsx
+++ /dev/null
@@ -1,612 +0,0 @@
-"use client";
-
-import { useEffect, useState, useMemo } from "react";
-import { useStickersStore } from "@/stores/stickers-store";
-import { useMediaStore } from "@/stores/media-store";
-import { useProjectStore } from "@/stores/project-store";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { usePlaybackStore } from "@/stores/playback-store";
-import {
- Loader2,
- Grid3X3,
- Hash,
- Smile,
- Clock,
- X,
- Sparkles,
- ArrowRight,
- StickerIcon,
-} from "lucide-react";
-import { toast } from "sonner";
-import { Button } from "@/components/ui/button";
-import { ScrollArea } from "@/components/ui/scroll-area";
-import { PanelBaseView as BaseView } from "@/components/editor/panel-base-view";
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
-
-import {
- getIconSvgUrl,
- buildIconSvgUrl,
- ICONIFY_HOSTS,
- POPULAR_COLLECTIONS,
-} from "@/lib/iconify-api";
-import { cn } from "@/lib/utils";
-import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
-import Image from "next/image";
-import type { MediaFile } from "@/types/media";
-import { DraggableMediaItem } from "@/components/ui/draggable-item";
-import { InputWithBack } from "@/components/ui/input-with-back";
-import { StickerCategory } from "@/stores/stickers-store";
-import { useInfiniteScroll } from "@/hooks/use-infinite-scroll";
-
-export function StickersView() {
- const { selectedCategory, setSelectedCategory } = useStickersStore();
-
- return (
- <BaseView
- value={selectedCategory}
- onValueChange={(v) => {
- if (["all", "general", "brands", "emoji"].includes(v)) {
- setSelectedCategory(v as StickerCategory);
- }
- }}
- tabs={[
- {
- value: "all",
- label: "All",
- icon: <Grid3X3 className="h-3 w-3" />,
- content: <StickersContentView category="all" />,
- },
- {
- value: "general",
- label: "Icons",
- icon: <Sparkles className="h-3 w-3" />,
- content: <StickersContentView category="general" />,
- },
- {
- value: "brands",
- label: "Brands",
- icon: <Hash className="h-3 w-3" />,
- content: <StickersContentView category="brands" />,
- },
- {
- value: "emoji",
- label: "Emoji",
- icon: <Smile className="h-3 w-3" />,
- content: <StickersContentView category="emoji" />,
- },
- ]}
- className="flex flex-col h-full p-0 overflow-hidden"
- />
- );
-}
-
-function StickerGrid({
- icons,
- onAdd,
- addingSticker,
- capSize = false,
-}: {
- icons: string[];
- onAdd: (iconName: string) => void;
- addingSticker: string | null;
- capSize?: boolean;
-}) {
- return (
- <div
- className="grid gap-2"
- style={{
- gridTemplateColumns: capSize
- ? "repeat(auto-fill, minmax(var(--sticker-min, 96px), var(--sticker-max, 160px)))"
- : "repeat(auto-fit, minmax(var(--sticker-min, 96px), 1fr))",
- ["--sticker-min" as any]: "96px",
- ...(capSize ? ({ ["--sticker-max"]: "160px" } as any) : {}),
- }}
- >
- {icons.map((iconName) => (
- <StickerItem
- key={iconName}
- iconName={iconName}
- onAdd={onAdd}
- isAdding={addingSticker === iconName}
- capSize={capSize}
- />
- ))}
- </div>
- );
-}
-
-function CollectionGrid({
- collections,
- onSelectCollection,
-}: {
- collections: Array<{
- prefix: string;
- name: string;
- total: number;
- category?: string;
- }>;
- onSelectCollection: (prefix: string) => void;
-}) {
- return (
- <div className="grid grid-cols-1 gap-2">
- {collections.map((collection) => (
- <CollectionItem
- key={collection.prefix}
- title={collection.name}
- subtitle={`${collection.total.toLocaleString()} icons${collection.category ? ` • ${collection.category}` : ""}`}
- onClick={() => onSelectCollection(collection.prefix)}
- />
- ))}
- </div>
- );
-}
-
-function EmptyView({ message }: { message: string }) {
- return (
- <div className="bg-panel h-full p-4 flex flex-col items-center justify-center gap-3">
- <StickerIcon
- className="w-10 h-10 text-muted-foreground"
- strokeWidth={1.5}
- />
- <div className="flex flex-col gap-2 text-center">
- <p className="text-lg font-medium">No stickers found</p>
- <p className="text-sm text-muted-foreground text-balance">{message}</p>
- </div>
- </div>
- );
-}
-
-function StickersContentView({ category }: { category: StickerCategory }) {
- const { activeProject } = useProjectStore();
- const { addElementAtTime } = useTimelineStore();
- const { currentTime } = usePlaybackStore();
- const { addMediaFile } = useMediaStore();
- const {
- searchQuery,
- selectedCollection,
- viewMode,
- collections,
- currentCollection,
- searchResults,
- recentStickers,
- isLoadingCollections,
- isLoadingCollection,
- isSearching,
- setSearchQuery,
- setSelectedCollection,
- loadCollections,
- searchStickers,
- addStickerToTimeline,
- clearRecentStickers,
- setSelectedCategory,
- addingSticker,
- } = useStickersStore();
-
- const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery);
- const [collectionsToShow, setCollectionsToShow] = useState(20);
- const [showCollectionItems, setShowCollectionItems] = useState(false);
-
- const filteredCollections = useMemo(() => {
- if (category === "all") {
- return Object.entries(collections).map(([prefix, collection]) => ({
- prefix,
- name: collection.name,
- total: collection.total,
- category: collection.category,
- }));
- }
-
- const collectionList =
- POPULAR_COLLECTIONS[category as keyof typeof POPULAR_COLLECTIONS];
- if (!collectionList) return [];
-
- return collectionList
- .map((c) => {
- const collection = collections[c.prefix];
- return collection
- ? {
- prefix: c.prefix,
- name: c.name,
- total: collection.total,
- }
- : null;
- })
- .filter(Boolean) as Array<{
- prefix: string;
- name: string;
- total: number;
- }>;
- }, [collections, category]);
-
- const { scrollAreaRef, handleScroll } = useInfiniteScroll({
- onLoadMore: () => setCollectionsToShow((prev) => prev + 20),
- hasMore: filteredCollections.length > collectionsToShow,
- isLoading: isLoadingCollections,
- enabled: viewMode === "browse" && !selectedCollection && category === "all",
- });
-
- useEffect(() => {
- if (Object.keys(collections).length === 0) {
- loadCollections();
- }
- }, []);
-
- useEffect(() => {
- const timer = setTimeout(() => {
- if (localSearchQuery !== searchQuery) {
- setSearchQuery(localSearchQuery);
- if (localSearchQuery.trim()) {
- searchStickers(localSearchQuery);
- }
- }
- }, 500);
-
- return () => clearTimeout(timer);
- }, [localSearchQuery]);
-
- const handleAddSticker = async (iconName: string) => {
- try {
- await addStickerToTimeline(iconName);
- } catch (error) {
- console.error("Failed to add sticker:", error);
- toast.error("Failed to add sticker to timeline");
- }
- };
-
- const iconsToDisplay = useMemo(() => {
- if (viewMode === "search" && searchResults) {
- return searchResults.icons;
- }
-
- if (viewMode === "collection" && currentCollection) {
- const icons: string[] = [];
-
- if (currentCollection.uncategorized) {
- icons.push(
- ...currentCollection.uncategorized.map(
- (name) => `${currentCollection.prefix}:${name}`
- )
- );
- }
-
- if (currentCollection.categories) {
- Object.values(currentCollection.categories).forEach((categoryIcons) => {
- icons.push(
- ...categoryIcons.map(
- (name) => `${currentCollection.prefix}:${name}`
- )
- );
- });
- }
-
- return icons.slice(0, 100);
- }
-
- return [];
- }, [viewMode, searchResults, currentCollection]);
-
- const isInCollection = viewMode === "collection" && !!selectedCollection;
-
- useEffect(() => {
- if (isInCollection) {
- setShowCollectionItems(false);
- const timer = setTimeout(() => setShowCollectionItems(true), 350);
- return () => clearTimeout(timer);
- } else {
- setShowCollectionItems(false);
- }
- }, [isInCollection]);
-
- return (
- <div className="flex flex-col gap-5 mt-1 h-full p-4">
- <div className="space-y-3">
- <InputWithBack
- isExpanded={isInCollection}
- setIsExpanded={(expanded) => {
- if (!expanded && isInCollection) {
- setSelectedCollection(null);
- }
- }}
- placeholder={
- category === "all"
- ? "Search all stickers"
- : category === "general"
- ? "Search icons"
- : category === "brands"
- ? "Search brands"
- : "Search Emojis"
- }
- value={localSearchQuery}
- onChange={setLocalSearchQuery}
- disableAnimation={true}
- />
- </div>
-
- <div className="relative h-full overflow-hidden">
- <ScrollArea
- className="flex-1 h-full"
- ref={scrollAreaRef}
- onScrollCapture={handleScroll}
- >
- <div className="flex flex-col gap-4 h-full">
- {recentStickers.length > 0 && viewMode === "browse" && (
- <div className="h-full">
- <div className="flex items-center gap-2 mb-2">
- <Clock className="h-4 w-4 text-muted-foreground" />
- <span className="text-sm font-medium">Recent</span>
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger asChild>
- <button
- onClick={clearRecentStickers}
- className="ml-auto h-5 w-5 p-0 rounded hover:bg-accent flex items-center justify-center"
- >
- <X className="h-3 w-3 text-muted-foreground" />
- </button>
- </TooltipTrigger>
- <TooltipContent>
- <p>Clear recent stickers</p>
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- </div>
- <StickerGrid
- icons={recentStickers.slice(0, 12)}
- onAdd={handleAddSticker}
- addingSticker={addingSticker}
- capSize
- />
- </div>
- )}
-
- {viewMode === "collection" && selectedCollection && (
- <div className="h-full">
- {isLoadingCollection ? (
- <div className="flex items-center justify-center py-8">
- <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
- </div>
- ) : showCollectionItems ? (
- <StickerGrid
- icons={iconsToDisplay}
- onAdd={handleAddSticker}
- addingSticker={addingSticker}
- />
- ) : (
- <div className="flex items-center justify-center py-8">
- <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
- </div>
- )}
- </div>
- )}
-
- {viewMode === "search" && (
- <div className="h-full">
- {isSearching ? (
- <div className="flex items-center justify-center py-8">
- <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
- </div>
- ) : searchResults?.icons.length ? (
- <>
- <div className="flex items-center justify-between mb-3">
- <span className="text-sm text-muted-foreground">
- {searchResults.total} results
- </span>
- </div>
- <StickerGrid
- icons={iconsToDisplay}
- onAdd={handleAddSticker}
- addingSticker={addingSticker}
- capSize
- />
- </>
- ) : searchQuery ? (
- <div className="flex flex-col items-center justify-center py-8 gap-3">
- <EmptyView
- message={`No stickers found for "${searchQuery}"`}
- />
- {category !== "all" && (
- <Button
- variant="outline"
- onClick={() => {
- const q = localSearchQuery || searchQuery;
- if (q) {
- setSearchQuery(q);
- }
- setSelectedCategory("all");
- if (q) {
- searchStickers(q);
- }
- }}
- >
- Search in all icons
- </Button>
- )}
- </div>
- ) : null}
- </div>
- )}
-
- {viewMode === "browse" && !selectedCollection && (
- <div className="space-y-4 h-full">
- {isLoadingCollections ? (
- <div className="flex items-center justify-center py-8">
- <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
- </div>
- ) : (
- <>
- {category !== "all" && (
- <div className="h-full">
- <h3 className="text-sm font-medium mb-2">
- Popular{" "}
- {category === "general"
- ? "Icon Sets"
- : category === "brands"
- ? "Brand Icons"
- : "Emoji Sets"}
- </h3>
- <CollectionGrid
- collections={filteredCollections}
- onSelectCollection={setSelectedCollection}
- />
- </div>
- )}
-
- {category === "all" && filteredCollections.length > 0 && (
- <div className="h-full">
- <CollectionGrid
- collections={filteredCollections.slice(
- 0,
- collectionsToShow
- )}
- onSelectCollection={setSelectedCollection}
- />
- </div>
- )}
- </>
- )}
- </div>
- )}
- </div>
- </ScrollArea>
- </div>
- </div>
- );
-}
-
-interface CollectionItemProps {
- title: string;
- subtitle: string;
- onClick: () => void;
-}
-
-function CollectionItem({ title, subtitle, onClick }: CollectionItemProps) {
- return (
- <Button
- variant="outline"
- className="justify-between h-auto py-2 "
- onClick={onClick}
- >
- <div className="text-left">
- <p className="font-medium">{title}</p>
- <p className="text-xs text-muted-foreground">{subtitle}</p>
- </div>
- <ArrowRight className="h-4 w-4" />
- </Button>
- );
-}
-
-interface StickerItemProps {
- iconName: string;
- onAdd: (iconName: string) => void;
- isAdding?: boolean;
- capSize?: boolean;
-}
-
-function StickerItem({
- iconName,
- onAdd,
- isAdding,
- capSize = false,
-}: StickerItemProps) {
- const [imageError, setImageError] = useState(false);
- const [hostIndex, setHostIndex] = useState(0);
-
- useEffect(() => {
- setImageError(false);
- setHostIndex(0);
- }, [iconName]);
-
- const displayName = iconName.split(":")[1] || iconName;
- const collectionPrefix = iconName.split(":")[0];
-
- const preview = imageError ? (
- <div className="w-full h-full flex items-center justify-center p-2">
- <span className="text-xs text-muted-foreground text-center break-all">
- {displayName}
- </span>
- </div>
- ) : (
- <div className="w-full h-full p-4 flex items-center justify-center">
- <Image
- src={
- hostIndex === 0
- ? getIconSvgUrl(iconName, { width: 64, height: 64 })
- : buildIconSvgUrl(
- ICONIFY_HOSTS[Math.min(hostIndex, ICONIFY_HOSTS.length - 1)],
- iconName,
- { width: 64, height: 64 }
- )
- }
- alt={displayName}
- width={64}
- height={64}
- className="w-full h-full object-contain"
- style={
- capSize
- ? {
- maxWidth: "var(--sticker-max, 160px)",
- maxHeight: "var(--sticker-max, 160px)",
- }
- : undefined
- }
- onError={() => {
- const next = hostIndex + 1;
- if (next < ICONIFY_HOSTS.length) {
- setHostIndex(next);
- } else {
- setImageError(true);
- }
- }}
- loading="lazy"
- unoptimized
- />
- </div>
- );
-
- return (
- <Tooltip>
- <TooltipTrigger asChild>
- <div
- className={cn(
- "relative",
- isAdding && "opacity-50 pointer-events-none"
- )}
- >
- <DraggableMediaItem
- name={displayName}
- preview={preview}
- dragData={{
- id: "sticker-placeholder",
- type: "image",
- name: displayName,
- }}
- onAddToTimeline={() => onAdd(iconName)}
- aspectRatio={1}
- showLabel={false}
- rounded={true}
- variant="card"
- className=""
- containerClassName="w-full"
- isDraggable={false}
- />
- {isAdding && (
- <div className="absolute inset-0 bg-black/60 flex items-center justify-center rounded-md z-10">
- <Loader2 className="h-6 w-6 animate-spin text-white" />
- </div>
- )}
- </div>
- </TooltipTrigger>
- <TooltipContent>
- <div className="space-y-1">
- <p className="font-medium">{displayName}</p>
- <p className="text-xs text-muted-foreground">{collectionPrefix}</p>
- </div>
- </TooltipContent>
- </Tooltip>
- );
-}
diff --git a/apps/web/src/components/editor/media-panel/views/text.tsx b/apps/web/src/components/editor/media-panel/views/text.tsx
deleted file mode 100644
index 22fc2413..00000000
--- a/apps/web/src/components/editor/media-panel/views/text.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { DraggableMediaItem } from "@/components/ui/draggable-item";
-import { PanelBaseView as BaseView } from "@/components/editor/panel-base-view";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
-
-export function TextView() {
- return (
- <BaseView>
- <DraggableMediaItem
- name="Default text"
- preview={
- <div className="flex items-center justify-center w-full h-full bg-panel-accent rounded">
- <span className="text-xs select-none">Default text</span>
- </div>
- }
- dragData={{
- id: "temp-text-id",
- type: DEFAULT_TEXT_ELEMENT.type,
- name: DEFAULT_TEXT_ELEMENT.name,
- content: DEFAULT_TEXT_ELEMENT.content,
- }}
- aspectRatio={1}
- onAddToTimeline={(currentTime) =>
- useTimelineStore.getState().addElementAtTime(
- {
- ...DEFAULT_TEXT_ELEMENT,
- id: "temp-text-id",
- },
- currentTime
- )
- }
- showLabel={false}
- />
- </BaseView>
- );
-}
diff --git a/apps/web/src/components/editor/onboarding.tsx b/apps/web/src/components/editor/onboarding.tsx
index 8ee60690..c60c2de5 100644
--- a/apps/web/src/components/editor/onboarding.tsx
+++ b/apps/web/src/components/editor/onboarding.tsx
@@ -1,138 +1,137 @@
"use client";
-import { Dialog, DialogContent, DialogTitle } from "../ui/dialog";
-import { Button } from "../ui/button";
import { ArrowRightIcon } from "lucide-react";
-import { useState, useEffect } from "react";
+import { useState } from "react";
import ReactMarkdown from "react-markdown";
+import { SOCIAL_LINKS } from "@/constants/site-constants";
+import { useLocalStorage } from "@/hooks/storage/use-local-storage";
+import { Button } from "../ui/button";
+import { Dialog, DialogBody, DialogContent, DialogTitle } from "../ui/dialog";
export function Onboarding() {
- const [step, setStep] = useState(0);
- const [isOpen, setIsOpen] = useState(false);
+ const [step, setStep] = useState(0);
+ const [hasSeenOnboarding, setHasSeenOnboarding] = useLocalStorage({
+ key: "hasSeenOnboarding",
+ defaultValue: false,
+ });
- useEffect(() => {
- const hasSeenOnboarding = localStorage.getItem("hasSeenOnboarding");
- if (!hasSeenOnboarding) {
- setIsOpen(true);
- }
- }, []);
+ const isOpen = !hasSeenOnboarding;
- const handleNext = () => {
- setStep(step + 1);
- };
+ const handleNext = () => {
+ setStep(step + 1);
+ };
- const handleClose = () => {
- setIsOpen(false);
- localStorage.setItem("hasSeenOnboarding", "true");
- };
+ const handleClose = () => {
+ setHasSeenOnboarding({ value: true });
+ };
- const getStepTitle = () => {
- switch (step) {
- case 0:
- return "Welcome to OpenCut Beta! 🎉";
- case 1:
- return "⚠️ This is a super early beta!";
- case 2:
- return "🦋 Have fun testing!";
- default:
- return "OpenCut Onboarding";
- }
- };
+ const getStepTitle = () => {
+ switch (step) {
+ case 0:
+ return "Welcome to OpenCut Beta! 🎉";
+ case 1:
+ return "⚠️ This is a super early beta!";
+ case 2:
+ return "🦋 Have fun testing!";
+ default:
+ return "OpenCut Onboarding";
+ }
+ };
- const renderStepContent = () => {
- switch (step) {
- case 0:
- return (
- <div className="space-y-5">
- <div className="space-y-3">
- <Title title="Welcome to OpenCut Beta! 🎉" />
- <Description description="You're among the first to try OpenCut - the fully open source CapCut alternative." />
- </div>
- <NextButton onClick={handleNext}>Next</NextButton>
- </div>
- );
- case 1:
- return (
- <div className="space-y-5">
- <div className="space-y-3">
- <Title title={getStepTitle()} />
- <Description description="OpenCut started just one month ago. There's still a ton of things to do to make this editor amazing." />
- <Description description="A lot of features are still missing, like the export functionality. We're working hard to build them out!" />
- <Description description="If you're curious, check out our roadmap [here](https://opencut.app/roadmap)" />
- </div>
- <NextButton onClick={handleNext}>Next</NextButton>
- </div>
- );
- case 2:
- return (
- <div className="space-y-5">
- <div className="space-y-3">
- <Title title={getStepTitle()} />
- <Description description="Join our [Discord](https://discord.gg/zmR9N35cjK), chat with cool people and share feedback to help make OpenCut the best editor ever." />
- </div>
- <NextButton onClick={handleClose}>Finish</NextButton>
- </div>
- );
- default:
- return null;
- }
- };
+ const renderStepContent = () => {
+ switch (step) {
+ case 0:
+ return (
+ <div className="space-y-5">
+ <div className="space-y-3">
+ <Title title="Welcome to OpenCut Beta! 🎉" />
+ <Description description="You're among the first to try OpenCut - the fully open source CapCut alternative." />
+ </div>
+ <NextButton onClick={handleNext}>Next</NextButton>
+ </div>
+ );
+ case 1:
+ return (
+ <div className="space-y-5">
+ <div className="space-y-3">
+ <Title title={getStepTitle()} />
+ <Description description="OpenCut started just one month ago. There's still a ton of things to do to make this editor amazing." />
+ <Description description="A lot of features are still missing, like the export functionality. We're working hard to build them out!" />
+ <Description description="If you're curious, check out our roadmap [here](https://opencut.app/roadmap)" />
+ </div>
+ <NextButton onClick={handleNext}>Next</NextButton>
+ </div>
+ );
+ case 2:
+ return (
+ <div className="space-y-5">
+ <div className="space-y-3">
+ <Title title={getStepTitle()} />
+ <Description
+ description={`Join our [Discord](${SOCIAL_LINKS.discord}), chat with cool people and share feedback to help make OpenCut the best editor ever.`}
+ />
+ </div>
+ <NextButton onClick={handleClose}>Finish</NextButton>
+ </div>
+ );
+ default:
+ return null;
+ }
+ };
- return (
- <Dialog open={isOpen} onOpenChange={handleClose}>
- <DialogContent className="sm:max-w-[425px] !outline-none pt-2">
- <DialogTitle>
- <span className="sr-only">{getStepTitle()}</span>
- </DialogTitle>
- {renderStepContent()}
- </DialogContent>
- </Dialog>
- );
+ return (
+ <Dialog open={isOpen} onOpenChange={handleClose}>
+ <DialogContent className="sm:max-w-[425px]">
+ <DialogTitle>
+ <span className="sr-only">{getStepTitle()}</span>
+ </DialogTitle>
+ <DialogBody>
+ {renderStepContent()}
+ </DialogBody>
+ </DialogContent>
+ </Dialog>
+ );
}
function Title({ title }: { title: string }) {
- return <h2 className="text-lg md:text-xl font-bold">{title}</h2>;
-}
-
-function Subtitle({ subtitle }: { subtitle: string }) {
- return <h3 className="text-lg font-medium">{subtitle}</h3>;
+ return <h2 className="text-lg font-bold md:text-xl">{title}</h2>;
}
function Description({ description }: { description: string }) {
- return (
- <div className="text-muted-foreground">
- <ReactMarkdown
- components={{
- p: ({ children }) => <p className="mb-0">{children}</p>,
- a: ({ href, children }) => (
- <a
- href={href}
- target="_blank"
- rel="noopener noreferrer"
- className="text-foreground hover:text-foreground/80 underline"
- >
- {children}
- </a>
- ),
- }}
- >
- {description}
- </ReactMarkdown>
- </div>
- );
+ return (
+ <div className="text-muted-foreground">
+ <ReactMarkdown
+ components={{
+ p: ({ children }) => <p className="mb-0">{children}</p>,
+ a: ({ href, children }) => (
+ <a
+ href={href}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="text-foreground hover:text-foreground/80 underline"
+ >
+ {children}
+ </a>
+ ),
+ }}
+ >
+ {description}
+ </ReactMarkdown>
+ </div>
+ );
}
function NextButton({
- children,
- onClick,
+ children,
+ onClick,
}: {
- children: React.ReactNode;
- onClick: () => void;
+ children: React.ReactNode;
+ onClick: () => void;
}) {
- return (
- <Button onClick={onClick} variant="default" className="w-full">
- {children}
- <ArrowRightIcon className="w-4 h-4" />
- </Button>
- );
+ return (
+ <Button onClick={onClick} variant="default" className="w-full">
+ {children}
+ <ArrowRightIcon className="size-4" />
+ </Button>
+ );
}
diff --git a/apps/web/src/components/editor/panel-base-view.tsx b/apps/web/src/components/editor/panel-base-view.tsx
deleted file mode 100644
index 6e92c616..00000000
--- a/apps/web/src/components/editor/panel-base-view.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import { ScrollArea } from "@/components/ui/scroll-area";
-import { Separator } from "@/components/ui/separator";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { cn } from "@/lib/utils";
-
-interface PanelBaseViewProps {
- children?: React.ReactNode;
- defaultTab?: string;
- value?: string;
- onValueChange?: (value: string) => void;
- tabs?: {
- value: string;
- label: string;
- icon?: React.ReactNode;
- content: React.ReactNode;
- }[];
- className?: string;
- ref?: React.RefObject<HTMLDivElement>;
-}
-
-function ViewContent({
- children,
- className,
-}: {
- children: React.ReactNode;
- className?: string;
-}) {
- return (
- <ScrollArea className="flex-1">
- <div className={cn("p-5", className)}>{children}</div>
- </ScrollArea>
- );
-}
-
-export function PanelBaseView({
- children,
- defaultTab,
- value,
- onValueChange,
- tabs,
- className = "",
- ref,
-}: PanelBaseViewProps) {
- return (
- <div className={cn("h-full flex flex-col", className)} ref={ref}>
- {!tabs || tabs.length === 0 ? (
- <ViewContent className={className}>{children}</ViewContent>
- ) : (
- <Tabs
- defaultValue={defaultTab}
- value={value}
- onValueChange={onValueChange}
- className="flex flex-col h-full"
- >
- <div className="sticky top-0 z-10 bg-panel">
- <div className="px-3 pt-3.5 pb-0">
- <TabsList>
- {tabs.map((tab) => (
- <TabsTrigger key={tab.value} value={tab.value}>
- {tab.icon ? (
- <span className="inline-flex items-center mr-1">
- {tab.icon}
- </span>
- ) : null}
- {tab.label}
- </TabsTrigger>
- ))}
- </TabsList>
- </div>
- <Separator className="mt-3.5" />
- </div>
- {tabs.map((tab) => (
- <TabsContent
- key={tab.value}
- value={tab.value}
- className="mt-0 flex-1 flex flex-col min-h-0"
- >
- <ViewContent className={className}>{tab.content}</ViewContent>
- </TabsContent>
- ))}
- </Tabs>
- )}
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/panel-preset-selector.tsx b/apps/web/src/components/editor/panel-preset-selector.tsx
deleted file mode 100644
index af22a171..00000000
--- a/apps/web/src/components/editor/panel-preset-selector.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-"use client";
-
-import { Button } from "../ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "../ui/dropdown-menu";
-import { ChevronDown, RotateCcw, LayoutPanelTop } from "lucide-react";
-import { usePanelStore, type PanelPreset } from "@/stores/panel-store";
-
-const PRESET_LABELS: Record<PanelPreset, string> = {
- default: "Default",
- media: "Media",
- inspector: "Inspector",
- "vertical-preview": "Vertical Preview",
-};
-
-const PRESET_DESCRIPTIONS: Record<PanelPreset, string> = {
- default: "Media, preview, and inspector on top row, timeline on bottom",
- media: "Full height media on left, preview and inspector on top row",
- inspector: "Full height inspector on right, media and preview on top row",
- "vertical-preview": "Full height preview on right for vertical videos",
-};
-
-export function PanelPresetSelector() {
- const { activePreset, setActivePreset, resetPreset } = usePanelStore();
-
- const handlePresetChange = (preset: PanelPreset) => {
- setActivePreset(preset);
- };
-
- const handleResetPreset = (preset: PanelPreset, event: React.MouseEvent) => {
- event.stopPropagation();
- resetPreset(preset);
- };
-
- return (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- variant="secondary"
- size="sm"
- className="h-8 px-2 flex items-center gap-1 text-xs"
- title="Panel Presets"
- >
- <LayoutPanelTop className="h-4 w-4" />
- <ChevronDown className="h-3 w-3" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="start" className="w-64">
- <div className="px-3 py-2 text-xs font-medium text-muted-foreground">
- Panel Presets
- </div>
- <DropdownMenuSeparator />
- {(Object.keys(PRESET_LABELS) as PanelPreset[]).map((preset) => (
- <DropdownMenuItem
- key={preset}
- onClick={() => handlePresetChange(preset)}
- className="flex items-start justify-between gap-2 py-2 px-3 cursor-pointer"
- >
- <div className="flex-1 min-w-0">
- <div className="flex items-center gap-2">
- <span className="font-medium text-sm">
- {PRESET_LABELS[preset]}
- </span>
- {activePreset === preset && (
- <div className="h-2 w-2 rounded-full bg-primary" />
- )}
- </div>
- <p className="text-xs text-muted-foreground mt-0.5 leading-tight">
- {PRESET_DESCRIPTIONS[preset]}
- </p>
- </div>
- <Button
- variant="secondary"
- size="icon"
- className="h-6 w-6 shrink-0 opacity-60 hover:opacity-100"
- onClick={(e) => handleResetPreset(preset, e)}
- title={`Reset ${PRESET_LABELS[preset]} preset`}
- >
- <RotateCcw className="h-3 w-3" />
- </Button>
- </DropdownMenuItem>
- ))}
- </DropdownMenuContent>
- </DropdownMenu>
- );
-}
diff --git a/apps/web/src/components/editor/panels/assets/drag-overlay.tsx b/apps/web/src/components/editor/panels/assets/drag-overlay.tsx
new file mode 100644
index 00000000..c6b0861c
--- /dev/null
+++ b/apps/web/src/components/editor/panels/assets/drag-overlay.tsx
@@ -0,0 +1,61 @@
+import { HugeiconsIcon } from "@hugeicons/react";
+import { UploadIcon } from "@hugeicons/core-free-icons";
+
+interface MediaDragOverlayProps {
+ isVisible: boolean;
+ isProcessing?: boolean;
+ progress?: number;
+ onClick?: () => void;
+}
+
+export function MediaDragOverlay({
+ isVisible,
+ isProcessing = false,
+ progress = 0,
+ onClick,
+}: MediaDragOverlayProps) {
+ if (!isVisible) return null;
+
+ const handleClick = ({
+ event,
+ }: {
+ event: React.MouseEvent<HTMLButtonElement>;
+ }) => {
+ if (isProcessing || !onClick) return;
+ event.preventDefault();
+ event.stopPropagation();
+ onClick();
+ };
+
+ return (
+ <button
+ className="bg-foreground/5 hover:bg-foreground/10 flex size-full flex-col items-center justify-center gap-4 rounded-lg p-8 text-center"
+ type="button"
+ disabled={isProcessing || !onClick}
+ onClick={(event) => handleClick({ event })}
+ >
+ <div className="flex items-center justify-center">
+ <HugeiconsIcon icon={UploadIcon} className="text-foreground size-10" />
+ </div>
+
+ <div className="space-y-2">
+ <p className="text-muted-foreground max-w-sm text-xs">
+ {isProcessing
+ ? `Processing your files (${progress}%)`
+ : "Drag and drop videos, photos, and audio files here"}
+ </p>
+ </div>
+
+ {isProcessing && (
+ <div className="w-full max-w-xs">
+ <div className="bg-muted/50 h-2 w-full rounded-full">
+ <div
+ className="bg-primary h-2 rounded-full"
+ style={{ width: `${progress}%` }}
+ />
+ </div>
+ </div>
+ )}
+ </button>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/assets/draggable-item.tsx b/apps/web/src/components/editor/panels/assets/draggable-item.tsx
new file mode 100644
index 00000000..10cb1f8c
--- /dev/null
+++ b/apps/web/src/components/editor/panels/assets/draggable-item.tsx
@@ -0,0 +1,244 @@
+"use client";
+
+import { Plus } from "lucide-react";
+import { type ReactNode, useEffect, useRef, useState } from "react";
+import { createPortal } from "react-dom";
+import { AspectRatio } from "@/components/ui/aspect-ratio";
+import { Button } from "@/components/ui/button";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { useEditor } from "@/hooks/use-editor";
+import { clearDragData, setDragData } from "@/lib/drag-data";
+import type { TimelineDragData } from "@/types/drag";
+import { cn } from "@/utils/ui";
+
+export interface DraggableItemProps {
+ name: string;
+ preview: ReactNode;
+ dragData: TimelineDragData;
+ onDragStart?: ({ e }: { e: React.DragEvent }) => void;
+ onAddToTimeline?: ({ currentTime }: { currentTime: number }) => void;
+ aspectRatio?: number;
+ className?: string;
+ containerClassName?: string;
+ shouldShowPlusOnDrag?: boolean;
+ shouldShowLabel?: boolean;
+ isRounded?: boolean;
+ variant?: "card" | "compact";
+ isDraggable?: boolean;
+ isHighlighted?: boolean;
+}
+
+export function DraggableItem({
+ name,
+ preview,
+ dragData,
+ onDragStart,
+ onAddToTimeline,
+ aspectRatio = 16 / 9,
+ className = "",
+ containerClassName,
+ shouldShowPlusOnDrag = true,
+ shouldShowLabel = true,
+ isRounded = true,
+ variant = "card",
+ isDraggable = true,
+ isHighlighted = false,
+}: DraggableItemProps) {
+ const [isDragging, setIsDragging] = useState(false);
+ const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 });
+ const dragRef = useRef<HTMLDivElement>(null);
+ const editor = useEditor();
+ const highlightClassName = `ring-2 ring-primary bg-primary/10 ${isRounded ? "rounded-sm" : ""}`;
+
+ const handleAddToTimeline = () => {
+ onAddToTimeline?.({ currentTime: editor.playback.getCurrentTime() });
+ };
+
+ const emptyImg = new window.Image();
+ emptyImg.src =
+ "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=";
+
+ useEffect(() => {
+ if (!isDragging) return;
+
+ const handleDragOver = (e: DragEvent) => {
+ setDragPosition({ x: e.clientX, y: e.clientY });
+ };
+
+ document.addEventListener("dragover", handleDragOver);
+
+ return () => {
+ document.removeEventListener("dragover", handleDragOver);
+ };
+ }, [isDragging]);
+
+ const handleDragStart = (e: React.DragEvent) => {
+ e.dataTransfer.setDragImage(emptyImg, 0, 0);
+
+ setDragData({ dataTransfer: e.dataTransfer, dragData });
+ e.dataTransfer.effectAllowed = "copy";
+
+ setDragPosition({ x: e.clientX, y: e.clientY });
+ setIsDragging(true);
+
+ onDragStart?.({ e });
+ };
+
+ const handleDragEnd = () => {
+ setIsDragging(false);
+ clearDragData();
+ };
+
+ return (
+ <>
+ {variant === "card" ? (
+ <div
+ ref={dragRef}
+ className={cn("group relative", containerClassName ?? "size-28")}
+ >
+ <div
+ className={cn(
+ "relative flex h-auto w-full cursor-default flex-col gap-1 p-1",
+ className,
+ isHighlighted && highlightClassName,
+ )}
+ >
+ <AspectRatio
+ ratio={aspectRatio}
+ className={cn(
+ "bg-panel-accent relative overflow-hidden",
+ isRounded && "rounded-sm",
+ isDraggable && "[&::-webkit-drag-ghost]:opacity-0",
+ )}
+ draggable={isDraggable}
+ onDragStart={isDraggable ? handleDragStart : undefined}
+ onDragEnd={isDraggable ? handleDragEnd : undefined}
+ >
+ {preview}
+ {!isDragging && (
+ <PlusButton
+ className="opacity-0 group-hover:opacity-100"
+ onClick={handleAddToTimeline}
+ />
+ )}
+ </AspectRatio>
+ {shouldShowLabel && (
+ <span
+ className="text-muted-foreground w-full truncate text-left text-[0.7rem]"
+ title={name}
+ >
+ <span className="sr-only">{name}</span>
+ <span aria-hidden="true">
+ {name.length > 8
+ ? `${name.slice(0, 16)}...${name.slice(-3)}`
+ : name}
+ </span>
+ </span>
+ )}
+ </div>
+ </div>
+ ) : (
+ <div
+ ref={dragRef}
+ className={cn(
+ "group relative w-full",
+ isHighlighted && highlightClassName,
+ )}
+ >
+ <button
+ type="button"
+ className={cn(
+ "flex h-8 w-full cursor-default items-center gap-3 px-1",
+ isDraggable && "[&::-webkit-drag-ghost]:opacity-0",
+ className,
+ )}
+ draggable={isDraggable}
+ onDragStart={isDraggable ? handleDragStart : undefined}
+ onDragEnd={isDraggable ? handleDragEnd : undefined}
+ >
+ <div className="size-6 flex-shrink-0 overflow-hidden rounded-[0.35rem]">
+ {preview}
+ </div>
+ <span className="w-full flex-1 truncate text-sm text-left">{name}</span>
+ </button>
+ </div>
+ )}
+
+ {isDraggable &&
+ isDragging &&
+ typeof document !== "undefined" &&
+ createPortal(
+ <div
+ className="pointer-events-none fixed z-9999"
+ style={{
+ left: dragPosition.x - 40,
+ top: dragPosition.y - 40,
+ }}
+ >
+ <div className="w-[80px]">
+ <AspectRatio
+ ratio={1}
+ className="ring-primary relative overflow-hidden rounded-md shadow-2xl ring-3"
+ >
+ <div className="size-full [&_img]:size-full [&_img]:rounded-none [&_img]:object-cover">
+ {preview}
+ </div>
+ {shouldShowPlusOnDrag && (
+ <PlusButton
+ onClick={handleAddToTimeline}
+ tooltipText="Add to timeline or drag to position"
+ />
+ )}
+ </AspectRatio>
+ </div>
+ </div>,
+ document.body,
+ )}
+ </>
+ );
+}
+
+function PlusButton({
+ className,
+ onClick,
+ tooltipText,
+}: {
+ className?: string;
+ onClick?: () => void;
+ tooltipText?: string;
+}) {
+ const button = (
+ <Button
+ size="icon"
+ className={cn(
+ "bg-background hover:bg-panel text-foreground absolute right-2 bottom-2 size-5",
+ className,
+ )}
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onClick?.();
+ }}
+ title={tooltipText}
+ >
+ <Plus />
+ </Button>
+ );
+
+ if (tooltipText) {
+ return (
+ <Tooltip>
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
+ <TooltipContent>
+ <p>{tooltipText}</p>
+ </TooltipContent>
+ </Tooltip>
+ );
+ }
+
+ return button;
+}
diff --git a/apps/web/src/components/editor/panels/assets/index.tsx b/apps/web/src/components/editor/panels/assets/index.tsx
new file mode 100644
index 00000000..9161cb1d
--- /dev/null
+++ b/apps/web/src/components/editor/panels/assets/index.tsx
@@ -0,0 +1,52 @@
+"use client";
+
+import { Separator } from "@/components/ui/separator";
+import { type Tab, useAssetsPanelStore } from "@/stores/assets-panel-store";
+import { TabBar } from "./tabbar";
+import { Captions } from "./views/captions";
+import { MediaView } from "./views/media";
+import { SettingsView } from "./views/settings";
+import { SoundsView } from "./views/sounds";
+import { StickersView } from "./views/stickers";
+import { TextView } from "./views/text";
+
+export function AssetsPanel() {
+ const { activeTab } = useAssetsPanelStore();
+
+ const viewMap: Record<Tab, React.ReactNode> = {
+ media: <MediaView />,
+ sounds: <SoundsView />,
+ text: <TextView />,
+ stickers: <StickersView />,
+ effects: (
+ <div className="text-muted-foreground p-4">
+ Effects view coming soon...
+ </div>
+ ),
+ transitions: (
+ <div className="text-muted-foreground p-4">
+ Transitions view coming soon...
+ </div>
+ ),
+ captions: <Captions />,
+ filters: (
+ <div className="text-muted-foreground p-4">
+ Filters view coming soon...
+ </div>
+ ),
+ adjustment: (
+ <div className="text-muted-foreground p-4">
+ Adjustment view coming soon...
+ </div>
+ ),
+ settings: <SettingsView />,
+ };
+
+ return (
+ <div className="bg-panel flex h-full">
+ <TabBar />
+ <Separator orientation="vertical" />
+ <div className="flex-1 overflow-hidden">{viewMap[activeTab]}</div>
+ </div>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/assets/tabbar.tsx b/apps/web/src/components/editor/panels/assets/tabbar.tsx
new file mode 100644
index 00000000..2c950fdc
--- /dev/null
+++ b/apps/web/src/components/editor/panels/assets/tabbar.tsx
@@ -0,0 +1,116 @@
+"use client";
+
+import { useCallback, useEffect, useRef, useState } from "react";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { cn } from "@/utils/ui";
+import {
+ TAB_KEYS,
+ tabs,
+ useAssetsPanelStore,
+} from "@/stores/assets-panel-store";
+
+export function TabBar() {
+ const { activeTab, setActiveTab } = useAssetsPanelStore();
+ const scrollRef = useRef<HTMLDivElement>(null);
+ const [showTopFade, setShowTopFade] = useState(false);
+ const [showBottomFade, setShowBottomFade] = useState(false);
+
+ const checkScrollPosition = useCallback(() => {
+ const element = scrollRef.current;
+ if (!element) return;
+
+ const { scrollTop, scrollHeight, clientHeight } = element;
+ setShowTopFade(scrollTop > 0);
+ setShowBottomFade(scrollTop < scrollHeight - clientHeight - 1);
+ }, []);
+
+ useEffect(() => {
+ const element = scrollRef.current;
+ if (!element) return;
+
+ checkScrollPosition();
+ element.addEventListener("scroll", checkScrollPosition);
+
+ const resizeObserver = new ResizeObserver(checkScrollPosition);
+ resizeObserver.observe(element);
+
+ return () => {
+ element.removeEventListener("scroll", checkScrollPosition);
+ resizeObserver.disconnect();
+ };
+ }, [checkScrollPosition]);
+
+ return (
+ <div className="relative flex">
+ <div
+ ref={scrollRef}
+ className="scrollbar-hidden relative flex size-full flex-col items-center justify-start gap-5 overflow-y-auto px-4 py-4"
+ >
+ {TAB_KEYS.map((tabKey) => {
+ const tab = tabs[tabKey];
+ return (
+ <Tooltip key={tabKey} delayDuration={10}>
+ <TooltipTrigger asChild>
+ <button
+ type="button"
+ aria-label={tab.label}
+ className={cn(
+ "flex cursor-pointer flex-col items-center gap-0.5 [&>svg]:size-4.5! opacity-100 hover:opacity-75",
+ activeTab === tabKey
+ ? "text-primary !opacity-100"
+ : "text-muted-foreground",
+ )}
+ onClick={() => setActiveTab(tabKey)}
+ onKeyDown={(event) => {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ setActiveTab(tabKey);
+ }
+ }}
+ >
+ <tab.icon className=" " />
+ </button>
+ </TooltipTrigger>
+ <TooltipContent
+ side="right"
+ align="center"
+ variant="sidebar"
+ sideOffset={8}
+ >
+ <div className="text-foreground text-sm leading-none font-medium">
+ {tab.label}
+ </div>
+ </TooltipContent>
+ </Tooltip>
+ );
+ })}
+ </div>
+
+ <FadeOverlay direction="top" show={showTopFade} />
+ <FadeOverlay direction="bottom" show={showBottomFade} />
+ </div>
+ );
+}
+
+function FadeOverlay({
+ direction,
+ show,
+}: {
+ direction: "top" | "bottom";
+ show: boolean;
+}) {
+ return (
+ <div
+ className={cn(
+ "pointer-events-none absolute right-0 left-0 h-6",
+ direction === "top" && show
+ ? "from-panel top-0 bg-gradient-to-b to-transparent"
+ : "from-panel bottom-0 bg-gradient-to-t to-transparent",
+ )}
+ />
+ );
+}
diff --git a/apps/web/src/components/editor/panels/assets/views/captions.tsx b/apps/web/src/components/editor/panels/assets/views/captions.tsx
new file mode 100644
index 00000000..306bad2a
--- /dev/null
+++ b/apps/web/src/components/editor/panels/assets/views/captions.tsx
@@ -0,0 +1,152 @@
+import { Button } from "@/components/ui/button";
+import { PropertyGroup } from "@/components/editor/panels/properties/property-item";
+import { PanelBaseView as BaseView } from "@/components/editor/panels/panel-base-view";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { useState, useRef } from "react";
+import { extractTimelineAudio } from "@/lib/media/mediabunny";
+import { useEditor } from "@/hooks/use-editor";
+import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
+import { TRANSCRIPTION_LANGUAGES } from "@/constants/transcription-constants";
+import type {
+ TranscriptionLanguage,
+ TranscriptionProgress,
+} from "@/types/transcription";
+import { transcriptionService } from "@/services/transcription";
+import { decodeAudioToFloat32 } from "@/lib/media/audio";
+import { buildCaptionChunks } from "@/lib/transcription/caption";
+import { Spinner } from "@/components/ui/spinner";
+
+export function Captions() {
+ const [selectedLanguage, setSelectedLanguage] =
+ useState<TranscriptionLanguage>("auto");
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [processingStep, setProcessingStep] = useState("");
+ const [error, setError] = useState<string | null>(null);
+ const containerRef = useRef<HTMLDivElement>(null);
+ const editor = useEditor();
+
+ const handleProgress = (progress: TranscriptionProgress) => {
+ if (progress.status === "loading-model") {
+ setProcessingStep(`Loading model ${Math.round(progress.progress)}%`);
+ } else if (progress.status === "transcribing") {
+ setProcessingStep("Transcribing...");
+ }
+ };
+
+ const handleGenerateTranscript = async () => {
+ try {
+ setIsProcessing(true);
+ setError(null);
+ setProcessingStep("Extracting audio...");
+
+ const audioBlob = await extractTimelineAudio({
+ tracks: editor.timeline.getTracks(),
+ mediaAssets: editor.media.getAssets(),
+ totalDuration: editor.timeline.getTotalDuration(),
+ });
+
+ setProcessingStep("Preparing audio...");
+ const { samples } = await decodeAudioToFloat32({ audioBlob });
+
+ const result = await transcriptionService.transcribe({
+ audioData: samples,
+ language: selectedLanguage === "auto" ? undefined : selectedLanguage,
+ onProgress: handleProgress,
+ });
+
+ setProcessingStep("Generating captions...");
+ const captionChunks = buildCaptionChunks({ segments: result.segments });
+
+ const captionTrackId = editor.timeline.addTrack({
+ type: "text",
+ index: 0,
+ });
+
+ for (let i = 0; i < captionChunks.length; i++) {
+ const caption = captionChunks[i];
+ editor.timeline.insertElement({
+ placement: { mode: "explicit", trackId: captionTrackId },
+ element: {
+ ...DEFAULT_TEXT_ELEMENT,
+ name: `Caption ${i + 1}`,
+ content: caption.text,
+ duration: caption.duration,
+ startTime: caption.startTime,
+ fontSize: 65,
+ fontWeight: "bold",
+ },
+ });
+ }
+ } catch (error) {
+ console.error("Transcription failed:", error);
+ setError(
+ error instanceof Error ? error.message : "An unexpected error occurred",
+ );
+ } finally {
+ setIsProcessing(false);
+ setProcessingStep("");
+ }
+ };
+
+ const handleLanguageChange = ({ value }: { value: string }) => {
+ if (value === "auto") {
+ setSelectedLanguage("auto");
+ return;
+ }
+
+ const matchedLanguage = TRANSCRIPTION_LANGUAGES.find(
+ (language) => language.code === value,
+ );
+ if (!matchedLanguage) return;
+ setSelectedLanguage(matchedLanguage.code);
+ };
+
+ return (
+ <BaseView
+ ref={containerRef}
+ className="flex h-full flex-col justify-between"
+ >
+ <PropertyGroup title="Language">
+ <Select
+ value={selectedLanguage}
+ onValueChange={(value) => handleLanguageChange({ value })}
+ >
+ <SelectTrigger className="bg-panel-accent h-8 w-full text-xs">
+ <SelectValue placeholder="Select a language" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="auto">Auto detect</SelectItem>
+ {TRANSCRIPTION_LANGUAGES.map((language) => (
+ <SelectItem key={language.code} value={language.code}>
+ {language.name}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </PropertyGroup>
+
+ <div className="flex flex-col gap-4">
+ {error && (
+ <div className="bg-destructive/10 border-destructive/20 rounded-md border p-3">
+ <p className="text-destructive text-sm">{error}</p>
+ </div>
+ )}
+
+ <Button
+ className="w-full"
+ onClick={handleGenerateTranscript}
+ disabled={isProcessing}
+ >
+ {isProcessing && <Spinner className="mr-1" />}
+ {isProcessing ? processingStep : "Generate transcript"}
+ </Button>
+ </div>
+ </BaseView>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/assets/views/media.tsx b/apps/web/src/components/editor/panels/assets/views/media.tsx
new file mode 100644
index 00000000..76ae0064
--- /dev/null
+++ b/apps/web/src/components/editor/panels/assets/views/media.tsx
@@ -0,0 +1,675 @@
+"use client";
+
+import Image from "next/image";
+import { useMemo, useState } from "react";
+import { toast } from "sonner";
+import { MediaDragOverlay } from "@/components/editor/panels/assets/drag-overlay";
+import { DraggableItem } from "@/components/editor/panels/assets/draggable-item";
+import { Button } from "@/components/ui/button";
+import {
+ ContextMenu,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuTrigger,
+} from "@/components/ui/context-menu";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import { useEditor } from "@/hooks/use-editor";
+import { useFileUpload } from "@/hooks/use-file-upload";
+import { useRevealItem } from "@/hooks/use-reveal-item";
+import { processMediaAssets } from "@/lib/media/processing";
+import {
+ buildImageElement,
+ buildUploadAudioElement,
+ buildVideoElement,
+} from "@/lib/timeline/element-utils";
+import { useAssetsPanelStore } from "@/stores/assets-panel-store";
+import type { MediaAsset } from "@/types/assets";
+import type { CreateTimelineElement } from "@/types/timeline";
+import { cn } from "@/utils/ui";
+import {
+ CloudUploadIcon,
+ GridViewIcon,
+ LeftToRightListDashIcon,
+ SortingOneNineIcon,
+ Image02Icon,
+ MusicNote03Icon,
+ Video01Icon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react";
+
+export function MediaView() {
+ const editor = useEditor();
+ const mediaFiles = editor.media.getAssets();
+ const activeProject = editor.project.getActive();
+
+ const { mediaViewMode, setMediaViewMode, highlightMediaId, clearHighlight } =
+ useAssetsPanelStore();
+ const { highlightedId, registerElement } = useRevealItem(
+ highlightMediaId,
+ clearHighlight,
+ );
+
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const [sortBy, setSortBy] = useState<"name" | "type" | "duration" | "size">(
+ "name",
+ );
+ const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
+
+ const processFiles = async ({ files }: { files: FileList }) => {
+ if (!files || files.length === 0) return;
+ if (!activeProject) {
+ toast.error("No active project");
+ return;
+ }
+
+ setIsProcessing(true);
+ setProgress(0);
+ try {
+ const processedAssets = await processMediaAssets({
+ files,
+ onProgress: (progress: { progress: number }) =>
+ setProgress(progress.progress),
+ });
+ for (const asset of processedAssets) {
+ await editor.media.addMediaAsset({
+ projectId: activeProject.metadata.id,
+ asset,
+ });
+ }
+ } catch (error) {
+ console.error("Error processing files:", error);
+ toast.error("Failed to process files");
+ } finally {
+ setIsProcessing(false);
+ setProgress(0);
+ }
+ };
+
+ const { isDragOver, dragProps, openFilePicker, fileInputProps } =
+ useFileUpload({
+ accept: "image/*,video/*,audio/*",
+ multiple: true,
+ onFilesSelected: (files) => processFiles({ files }),
+ });
+
+ const handleRemove = async ({
+ event,
+ id,
+ }: {
+ event: React.MouseEvent;
+ id: string;
+ }) => {
+ event.stopPropagation();
+
+ if (!activeProject) {
+ toast.error("No active project");
+ return;
+ }
+
+ await editor.media.removeMediaAsset({
+ projectId: activeProject.metadata.id,
+ id,
+ });
+ };
+
+ const addElementAtTime = ({
+ asset,
+ startTime,
+ }: {
+ asset: MediaAsset;
+ startTime: number;
+ }): boolean => {
+ const element = createElementFromMedia({ asset, startTime });
+ editor.timeline.insertElement({
+ element,
+ placement: { mode: "auto" },
+ });
+ return true;
+ };
+
+ const filteredMediaItems = useMemo(() => {
+ const filtered = mediaFiles.filter((item) => !item.ephemeral);
+
+ filtered.sort((a, b) => {
+ let valueA: string | number;
+ let valueB: string | number;
+
+ switch (sortBy) {
+ case "name":
+ valueA = a.name.toLowerCase();
+ valueB = b.name.toLowerCase();
+ break;
+ case "type":
+ valueA = a.type;
+ valueB = b.type;
+ break;
+ case "duration":
+ valueA = a.duration || 0;
+ valueB = b.duration || 0;
+ break;
+ case "size":
+ valueA = a.file.size;
+ valueB = b.file.size;
+ break;
+ default:
+ return 0;
+ }
+
+ if (valueA < valueB) return sortOrder === "asc" ? -1 : 1;
+ if (valueA > valueB) return sortOrder === "asc" ? 1 : -1;
+ return 0;
+ });
+
+ return filtered;
+ }, [mediaFiles, sortBy, sortOrder]);
+
+ const previewComponents = useMemo(() => {
+ const previews = new Map<string, React.ReactNode>();
+
+ filteredMediaItems.forEach((item) => {
+ previews.set(item.id, <MediaPreview item={item} />);
+ previews.set(
+ `compact-${item.id}`,
+ <MediaPreview item={item} variant="compact" />,
+ );
+ });
+
+ return previews;
+ }, [filteredMediaItems]);
+
+ const renderPreview = (item: MediaAsset) => previewComponents.get(item.id);
+ const renderCompactPreview = (item: MediaAsset) =>
+ previewComponents.get(`compact-${item.id}`);
+
+ return (
+ <>
+ <input {...fileInputProps} />
+
+ <div
+ className={`relative flex h-full flex-col gap-1 ${isDragOver ? "bg-accent/30" : ""}`}
+ {...dragProps}
+ >
+ <div className="bg-panel py-2 px-4 flex items-center justify-between border-b">
+ <span className="text-muted-foreground text-sm">Assets</span>
+ <div className="flex items-center gap-0">
+ <TooltipProvider>
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button
+ size="icon"
+ variant="text"
+ onClick={() =>
+ setMediaViewMode(
+ mediaViewMode === "grid" ? "list" : "grid",
+ )
+ }
+ disabled={isProcessing}
+ className="items-center justify-center"
+ >
+ {mediaViewMode === "grid" ? (
+ <HugeiconsIcon icon={LeftToRightListDashIcon} />
+ ) : (
+ <HugeiconsIcon icon={GridViewIcon} />
+ )}
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>
+ <p>
+ {mediaViewMode === "grid"
+ ? "Switch to list view"
+ : "Switch to grid view"}
+ </p>
+ </TooltipContent>
+ <Tooltip>
+ <DropdownMenu>
+ <TooltipTrigger asChild>
+ <DropdownMenuTrigger asChild>
+ <Button
+ size="icon"
+ variant="text"
+ disabled={isProcessing}
+ className="items-center justify-center"
+ >
+ <HugeiconsIcon icon={SortingOneNineIcon} />
+ </Button>
+ </DropdownMenuTrigger>
+ </TooltipTrigger>
+ <DropdownMenuContent align="end">
+ <SortMenuItem
+ label="Name"
+ sortKey="name"
+ currentSortBy={sortBy}
+ currentSortOrder={sortOrder}
+ onSort={({ key }) => {
+ if (sortBy === key) {
+ setSortOrder(sortOrder === "asc" ? "desc" : "asc");
+ } else {
+ setSortBy(key);
+ setSortOrder("asc");
+ }
+ }}
+ />
+ <SortMenuItem
+ label="Type"
+ sortKey="type"
+ currentSortBy={sortBy}
+ currentSortOrder={sortOrder}
+ onSort={({ key }) => {
+ if (sortBy === key) {
+ setSortOrder(sortOrder === "asc" ? "desc" : "asc");
+ } else {
+ setSortBy(key);
+ setSortOrder("asc");
+ }
+ }}
+ />
+ <SortMenuItem
+ label="Duration"
+ sortKey="duration"
+ currentSortBy={sortBy}
+ currentSortOrder={sortOrder}
+ onSort={({ key }) => {
+ if (sortBy === key) {
+ setSortOrder(sortOrder === "asc" ? "desc" : "asc");
+ } else {
+ setSortBy(key);
+ setSortOrder("asc");
+ }
+ }}
+ />
+ <SortMenuItem
+ label="File size"
+ sortKey="size"
+ currentSortBy={sortBy}
+ currentSortOrder={sortOrder}
+ onSort={({ key }) => {
+ if (sortBy === key) {
+ setSortOrder(sortOrder === "asc" ? "desc" : "asc");
+ } else {
+ setSortBy(key);
+ setSortOrder("asc");
+ }
+ }}
+ />
+ </DropdownMenuContent>
+ </DropdownMenu>
+ <TooltipContent>
+ <p>
+ Sort by {sortBy} (
+ {sortOrder === "asc" ? "ascending" : "descending"})
+ </p>
+ </TooltipContent>
+ </Tooltip>
+ </Tooltip>
+ </TooltipProvider>
+ <Button
+ variant="outline"
+ onClick={openFilePicker}
+ disabled={isProcessing}
+ size="sm"
+ className="items-center justify-center gap-1.5 ml-1.5"
+ >
+ <HugeiconsIcon icon={CloudUploadIcon} />
+ Upload
+ </Button>
+ </div>
+ </div>
+
+ <div className="scrollbar-thin size-full overflow-y-auto pt-1">
+ <div className="w-full flex-1 p-3 pt-0">
+ {isDragOver || filteredMediaItems.length === 0 ? (
+ <MediaDragOverlay
+ isVisible={true}
+ isProcessing={isProcessing}
+ progress={progress}
+ onClick={openFilePicker}
+ />
+ ) : mediaViewMode === "grid" ? (
+ <GridView
+ items={filteredMediaItems}
+ renderPreview={renderPreview}
+ onRemove={handleRemove}
+ onAddToTimeline={addElementAtTime}
+ highlightedId={highlightedId}
+ registerElement={registerElement}
+ />
+ ) : (
+ <ListView
+ items={filteredMediaItems}
+ renderPreview={renderCompactPreview}
+ onRemove={handleRemove}
+ onAddToTimeline={addElementAtTime}
+ highlightedId={highlightedId}
+ registerElement={registerElement}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+ </>
+ );
+}
+
+function MediaItemWithContextMenu({
+ item,
+ children,
+ onRemove,
+}: {
+ item: MediaAsset;
+ children: React.ReactNode;
+ onRemove: ({ event, id }: { event: React.MouseEvent; id: string }) => void;
+}) {
+ return (
+ <ContextMenu>
+ <ContextMenuTrigger>{children}</ContextMenuTrigger>
+ <ContextMenuContent>
+ <ContextMenuItem>Export clips</ContextMenuItem>
+ <ContextMenuItem
+ variant="destructive"
+ onClick={(event) => onRemove({ event, id: item.id })}
+ >
+ Delete
+ </ContextMenuItem>
+ </ContextMenuContent>
+ </ContextMenu>
+ );
+}
+
+function GridView({
+ items,
+ renderPreview,
+ onRemove,
+ onAddToTimeline,
+ highlightedId,
+ registerElement,
+}: {
+ items: MediaAsset[];
+ renderPreview: (item: MediaAsset) => React.ReactNode;
+ onRemove: ({ event, id }: { event: React.MouseEvent; id: string }) => void;
+ onAddToTimeline: ({
+ asset,
+ startTime,
+ }: {
+ asset: MediaAsset;
+ startTime: number;
+ }) => boolean;
+ highlightedId: string | null;
+ registerElement: (id: string, element: HTMLElement | null) => void;
+}) {
+ return (
+ <div
+ className="grid gap-2"
+ style={{
+ gridTemplateColumns: "repeat(auto-fill, 160px)",
+ }}
+ >
+ {items.map((item) => (
+ <div key={item.id} ref={(el) => registerElement(item.id, el)}>
+ <MediaItemWithContextMenu item={item} onRemove={onRemove}>
+ <DraggableItem
+ name={item.name}
+ preview={renderPreview(item)}
+ dragData={{
+ id: item.id,
+ type: "media",
+ mediaType: item.type,
+ name: item.name,
+ }}
+ shouldShowPlusOnDrag={false}
+ onAddToTimeline={({ currentTime }) =>
+ onAddToTimeline({ asset: item, startTime: currentTime })
+ }
+ isRounded={false}
+ variant="card"
+ isHighlighted={highlightedId === item.id}
+ />
+ </MediaItemWithContextMenu>
+ </div>
+ ))}
+ </div>
+ );
+}
+
+function ListView({
+ items,
+ renderPreview,
+ onRemove,
+ onAddToTimeline,
+ highlightedId,
+ registerElement,
+}: {
+ items: MediaAsset[];
+ renderPreview: (item: MediaAsset) => React.ReactNode;
+ onRemove: ({ event, id }: { event: React.MouseEvent; id: string }) => void;
+ onAddToTimeline: ({
+ asset,
+ startTime,
+ }: {
+ asset: MediaAsset;
+ startTime: number;
+ }) => boolean;
+ highlightedId: string | null;
+ registerElement: (id: string, element: HTMLElement | null) => void;
+}) {
+ return (
+ <div className="space-y-1">
+ {items.map((item) => (
+ <div key={item.id} ref={(el) => registerElement(item.id, el)}>
+ <MediaItemWithContextMenu item={item} onRemove={onRemove}>
+ <DraggableItem
+ name={item.name}
+ preview={renderPreview(item)}
+ dragData={{
+ id: item.id,
+ type: "media",
+ mediaType: item.type,
+ name: item.name,
+ }}
+ shouldShowPlusOnDrag={false}
+ onAddToTimeline={({ currentTime }) =>
+ onAddToTimeline({ asset: item, startTime: currentTime })
+ }
+ variant="compact"
+ isHighlighted={highlightedId === item.id}
+ />
+ </MediaItemWithContextMenu>
+ </div>
+ ))}
+ </div>
+ );
+}
+
+const formatDuration = ({ duration }: { duration: number }) => {
+ const min = Math.floor(duration / 60);
+ const sec = Math.floor(duration % 60);
+ return `${min}:${sec.toString().padStart(2, "0")}`;
+};
+
+function MediaDurationBadge({ duration }: { duration?: number }) {
+ if (!duration) return null;
+
+ return (
+ <div className="absolute right-1 bottom-1 rounded bg-black/70 px-1 text-xs text-white">
+ {formatDuration({ duration })}
+ </div>
+ );
+}
+
+function MediaDurationLabel({ duration }: { duration?: number }) {
+ if (!duration) return null;
+
+ return (
+ <span className="text-xs opacity-70">{formatDuration({ duration })}</span>
+ );
+}
+
+function MediaTypePlaceholder({
+ icon,
+ label,
+ duration,
+ variant,
+}: {
+ icon: IconSvgElement;
+ label: string;
+ duration?: number;
+ variant: "muted" | "bordered";
+}) {
+ const iconClassName = cn("size-6", variant === "bordered" && "mb-1");
+
+ return (
+ <div
+ className={cn(
+ "text-muted-foreground flex size-full flex-col items-center justify-center rounded",
+ variant === "muted" ? "bg-muted/30" : "border",
+ )}
+ >
+ <HugeiconsIcon icon={icon} className={iconClassName} />
+ <span className="text-xs">{label}</span>
+ <MediaDurationLabel duration={duration} />
+ </div>
+ );
+}
+
+function MediaPreview({
+ item,
+ variant = "grid",
+}: {
+ item: MediaAsset;
+ variant?: "grid" | "compact";
+}) {
+ const shouldShowDurationBadge = variant === "grid";
+
+ if (item.type === "image") {
+ return (
+ <div className="relative flex size-full items-center justify-center">
+ <Image
+ src={item.url ?? ""}
+ alt={item.name}
+ fill
+ sizes="100vw"
+ className="object-cover"
+ loading="lazy"
+ unoptimized
+ />
+ </div>
+ );
+ }
+
+ if (item.type === "video") {
+ if (item.thumbnailUrl) {
+ return (
+ <div className="relative size-full">
+ <Image
+ src={item.thumbnailUrl}
+ alt={item.name}
+ fill
+ sizes="100vw"
+ className="rounded object-cover"
+ loading="lazy"
+ unoptimized
+ />
+ {shouldShowDurationBadge ? (
+ <MediaDurationBadge duration={item.duration} />
+ ) : null}
+ </div>
+ );
+ }
+
+ return (
+ <MediaTypePlaceholder
+ icon={Video01Icon}
+ label="Video"
+ duration={item.duration}
+ variant="muted"
+ />
+ );
+ }
+
+ if (item.type === "audio") {
+ return (
+ <MediaTypePlaceholder
+ icon={MusicNote03Icon}
+ label="Audio"
+ duration={item.duration}
+ variant="bordered"
+ />
+ );
+ }
+
+ return (
+ <MediaTypePlaceholder icon={Image02Icon} label="Unknown" variant="muted" />
+ );
+}
+
+function SortMenuItem({
+ label,
+ sortKey,
+ currentSortBy,
+ currentSortOrder,
+ onSort,
+}: {
+ label: string;
+ sortKey: "name" | "type" | "duration" | "size";
+ currentSortBy: string;
+ currentSortOrder: "asc" | "desc";
+ onSort: ({ key }: { key: "name" | "type" | "duration" | "size" }) => void;
+}) {
+ const isActive = currentSortBy === sortKey;
+ const arrow = isActive ? (currentSortOrder === "asc" ? "↑" : "↓") : "";
+
+ return (
+ <DropdownMenuItem onClick={() => onSort({ key: sortKey })}>
+ {label} {arrow}
+ </DropdownMenuItem>
+ );
+}
+
+function createElementFromMedia({
+ asset,
+ startTime,
+}: {
+ asset: MediaAsset;
+ startTime: number;
+}): CreateTimelineElement {
+ const duration =
+ asset.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
+
+ switch (asset.type) {
+ case "video":
+ return buildVideoElement({
+ mediaId: asset.id,
+ name: asset.name,
+ duration,
+ startTime,
+ });
+ case "image":
+ return buildImageElement({
+ mediaId: asset.id,
+ name: asset.name,
+ duration,
+ startTime,
+ });
+ case "audio":
+ return buildUploadAudioElement({
+ mediaId: asset.id,
+ name: asset.name,
+ duration,
+ startTime,
+ });
+ default:
+ throw new Error(`Unsupported media type: ${asset.type}`);
+ }
+}
diff --git a/apps/web/src/components/editor/panels/assets/views/settings.tsx b/apps/web/src/components/editor/panels/assets/views/settings.tsx
new file mode 100644
index 00000000..b8c54510
--- /dev/null
+++ b/apps/web/src/components/editor/panels/assets/views/settings.tsx
@@ -0,0 +1,394 @@
+"use client";
+
+import Image from "next/image";
+import { memo, useCallback, useMemo } from "react";
+import { PanelBaseView as BaseView } from "@/components/editor/panels/panel-base-view";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ BLUR_INTENSITY_PRESETS,
+ DEFAULT_BLUR_INTENSITY,
+ DEFAULT_COLOR,
+ FPS_PRESETS,
+} from "@/constants/project-constants";
+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 { useEditorStore } from "@/stores/editor-store";
+import type { TProject } from "@/types/project";
+import { dimensionToAspectRatio } from "@/utils/geometry";
+import { cn } from "@/utils/ui";
+import {
+ PropertyGroup,
+ PropertyItem,
+ PropertyItemLabel,
+ PropertyItemValue,
+} from "@/components/editor/panels/properties/property-item";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { DropperIcon } from "@hugeicons/core-free-icons";
+
+export function SettingsView() {
+ return <ProjectSettingsTabs />;
+}
+
+function ProjectSettingsTabs() {
+ return (
+ <BaseView
+ defaultTab="project-info"
+ tabs={[
+ {
+ value: "project-info",
+ label: "Project info",
+ content: (
+ <div className="p-5">
+ <ProjectInfoView />
+ </div>
+ ),
+ },
+ {
+ value: "background",
+ label: "Background",
+ content: (
+ <div className="flex h-full flex-col justify-between">
+ <div className="flex-1 p-5">
+ <BackgroundView />
+ </div>
+ {/* <div className="bg-panel/85 sticky -bottom-0 flex flex-col backdrop-blur-lg">
+ <Separator />
+ <Button className="text-muted-foreground hover:text-foreground/85 h-auto w-fit !bg-transparent p-5 py-4 text-xs shadow-none">
+ Custom background
+ <PlusIcon />
+ </Button>
+ </div> */}
+
+ {/* another ui */}
+ {/* <div className="flex flex-col justify-center items-center pb-5 sticky bottom-0">
+ <Button className="w-fit h-auto gap-1.5 px-3.5 py-1.5 bg-foreground hover:bg-foreground/85 text-background rounded-full">
+ <span className="text-sm">Custom</span>
+ <PlusIcon className="" />
+ </Button>
+ </div> */}
+ </div>
+ ),
+ },
+ ]}
+ className="flex h-full flex-col justify-between p-0"
+ />
+ );
+}
+
+function getCurrentCanvasSize({ activeProject }: { activeProject: TProject }) {
+ const { canvasSize } = activeProject.settings;
+
+ return {
+ width: canvasSize.width,
+ height: canvasSize.height,
+ };
+}
+
+function ProjectInfoView() {
+ const editor = useEditor();
+ const activeProject = editor.project.getActive();
+ const { canvasPresets } = useEditorStore();
+
+ const findPresetIndexByAspectRatio = ({
+ presets,
+ targetAspectRatio,
+ }: {
+ presets: Array<{ width: number; height: number }>;
+ targetAspectRatio: string;
+ }) => {
+ for (let index = 0; index < presets.length; index++) {
+ const preset = presets[index];
+ const presetAspectRatio = dimensionToAspectRatio({
+ width: preset.width,
+ height: preset.height,
+ });
+ if (presetAspectRatio === targetAspectRatio) {
+ return index;
+ }
+ }
+ return -1;
+ };
+
+ const currentCanvasSize = getCurrentCanvasSize({ activeProject });
+ const currentAspectRatio = dimensionToAspectRatio(currentCanvasSize);
+ const originalCanvasSize = activeProject.settings.originalCanvasSize ?? null;
+ const presetIndex = findPresetIndexByAspectRatio({
+ presets: canvasPresets,
+ targetAspectRatio: currentAspectRatio,
+ });
+ const originalPresetValue = "original";
+ const selectedPresetValue =
+ presetIndex !== -1 ? presetIndex.toString() : originalPresetValue;
+
+ const handleAspectRatioChange = ({ value }: { value: string }) => {
+ if (value === originalPresetValue) {
+ const canvasSize = originalCanvasSize ?? currentCanvasSize;
+ editor.project.updateSettings({
+ settings: { canvasSize },
+ });
+ return;
+ }
+ const index = parseInt(value, 10);
+ const preset = canvasPresets[index];
+ if (preset) {
+ editor.project.updateSettings({ settings: { canvasSize: preset } });
+ }
+ };
+
+ const handleFpsChange = (value: string) => {
+ const fps = parseFloat(value);
+ editor.project.updateSettings({ settings: { fps } });
+ };
+
+ return (
+ <div className="flex flex-col gap-4">
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Name</PropertyItemLabel>
+ <PropertyItemValue>{activeProject.metadata.name}</PropertyItemValue>
+ </PropertyItem>
+
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Aspect ratio</PropertyItemLabel>
+ <PropertyItemValue>
+ <Select
+ value={selectedPresetValue}
+ onValueChange={(value) => handleAspectRatioChange({ value })}
+ >
+ <SelectTrigger className="bg-panel-accent">
+ <SelectValue placeholder="Select an aspect ratio" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value={originalPresetValue}>Original</SelectItem>
+ {canvasPresets.map((preset, index) => {
+ const label = dimensionToAspectRatio({
+ width: preset.width,
+ height: preset.height,
+ });
+ return (
+ <SelectItem key={label} value={index.toString()}>
+ {label}
+ </SelectItem>
+ );
+ })}
+ </SelectContent>
+ </Select>
+ </PropertyItemValue>
+ </PropertyItem>
+
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Frame rate</PropertyItemLabel>
+ <PropertyItemValue>
+ <Select
+ value={activeProject.settings.fps.toString()}
+ onValueChange={handleFpsChange}
+ >
+ <SelectTrigger className="bg-panel-accent">
+ <SelectValue placeholder="Select a frame rate" />
+ </SelectTrigger>
+ <SelectContent>
+ {FPS_PRESETS.map((preset) => (
+ <SelectItem key={preset.value} value={preset.value}>
+ {preset.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </PropertyItemValue>
+ </PropertyItem>
+ </div>
+ );
+}
+
+const BlurPreview = memo(
+ ({
+ blur,
+ isSelected,
+ onSelect,
+ }: {
+ blur: { label: string; value: number };
+ isSelected: boolean;
+ onSelect: () => void;
+ }) => (
+ <button
+ className={cn(
+ "border-foreground/15 hover:border-primary relative aspect-square w-full cursor-pointer overflow-hidden rounded-sm border",
+ isSelected && "border-primary border-2",
+ )}
+ onClick={onSelect}
+ type="button"
+ aria-label={`Select ${blur.label} blur`}
+ >
+ <Image
+ src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
+ alt={`Blur preview ${blur.label}`}
+ fill
+ className="object-cover"
+ style={{ filter: `blur(${blur.value}px)` }}
+ loading="eager"
+ />
+ <div className="absolute right-1 bottom-1 left-1 text-center">
+ <span className="rounded bg-black/50 px-1 text-xs text-white">
+ {blur.label}
+ </span>
+ </div>
+ </button>
+ ),
+);
+
+BlurPreview.displayName = "BlurPreview";
+
+const BackgroundPreviews = memo(
+ ({
+ backgrounds,
+ currentBackgroundColor,
+ isColorBackground,
+ handleColorSelect,
+ useBackgroundColor = false,
+ }: {
+ backgrounds: string[];
+ currentBackgroundColor: string;
+ isColorBackground: boolean;
+ handleColorSelect: ({ bg }: { bg: string }) => void;
+ useBackgroundColor?: boolean;
+ }) => {
+ return useMemo(
+ () =>
+ backgrounds.map((bg, index) => (
+ <button
+ key={`${index}-${bg}`}
+ className={cn(
+ "border-foreground/15 hover:border-primary aspect-square w-full cursor-pointer rounded-sm border",
+ isColorBackground &&
+ bg === currentBackgroundColor &&
+ "border-primary border-2",
+ )}
+ style={
+ useBackgroundColor
+ ? { backgroundColor: bg }
+ : {
+ background: bg,
+ backgroundSize: "cover",
+ backgroundPosition: "center",
+ backgroundRepeat: "no-repeat",
+ }
+ }
+ onClick={() => handleColorSelect({ bg })}
+ type="button"
+ aria-label={`Select background ${useBackgroundColor ? bg : index + 1}`}
+ />
+ )),
+ [
+ backgrounds,
+ isColorBackground,
+ currentBackgroundColor,
+ handleColorSelect,
+ useBackgroundColor,
+ ],
+ );
+ },
+);
+
+BackgroundPreviews.displayName = "BackgroundPreviews";
+
+function BackgroundView() {
+ const editor = useEditor();
+ const activeProject = editor.project.getActive();
+ const blurLevels = useMemo(() => BLUR_INTENSITY_PRESETS, []);
+
+ const handleBlurSelect = useCallback(
+ async ({ blurIntensity }: { blurIntensity: number }) => {
+ await editor.project.updateSettings({
+ settings: { background: { type: "blur", blurIntensity } },
+ });
+ },
+ [editor.project],
+ );
+
+ const handleColorSelect = useCallback(
+ async ({ color }: { color: string }) => {
+ await editor.project.updateSettings({
+ settings: { background: { type: "color", color } },
+ });
+ },
+ [editor.project],
+ );
+
+ const currentBlurIntensity =
+ activeProject.settings.background.type === "blur"
+ ? activeProject.settings.background.blurIntensity
+ : DEFAULT_BLUR_INTENSITY;
+
+ const currentBackgroundColor =
+ activeProject.settings.background.type === "color"
+ ? activeProject.settings.background.color
+ : DEFAULT_COLOR;
+
+ const isBlurBackground = activeProject.settings.background.type === "blur";
+ const isColorBackground = activeProject.settings.background.type === "color";
+
+ const blurPreviews = useMemo(
+ () =>
+ blurLevels.map((blur) => (
+ <BlurPreview
+ key={blur.value}
+ blur={blur}
+ isSelected={isBlurBackground && currentBlurIntensity === blur.value}
+ onSelect={() => handleBlurSelect({ blurIntensity: blur.value })}
+ />
+ )),
+ [blurLevels, isBlurBackground, currentBlurIntensity, handleBlurSelect],
+ );
+
+ return (
+ <div className="flex h-full flex-col gap-4">
+ <PropertyGroup title="Blur" defaultExpanded={false}>
+ <div className="grid w-full grid-cols-4 gap-2">{blurPreviews}</div>
+ </PropertyGroup>
+
+ <PropertyGroup title="Colors" defaultExpanded={false}>
+ <div className="grid w-full grid-cols-4 gap-2">
+ <div className="border-foreground/15 hover:border-primary flex aspect-square w-full cursor-pointer items-center justify-center rounded-sm border">
+ <HugeiconsIcon icon={DropperIcon} className="size-4" />
+ </div>
+ <BackgroundPreviews
+ backgrounds={colors}
+ currentBackgroundColor={currentBackgroundColor}
+ isColorBackground={isColorBackground}
+ handleColorSelect={({ bg }) => handleColorSelect({ color: bg })}
+ useBackgroundColor={true}
+ />
+ </div>
+ </PropertyGroup>
+
+ <PropertyGroup title="Pattern craft" defaultExpanded={false}>
+ <div className="grid w-full grid-cols-4 gap-2">
+ <BackgroundPreviews
+ backgrounds={patternCraftGradients}
+ currentBackgroundColor={currentBackgroundColor}
+ isColorBackground={isColorBackground}
+ handleColorSelect={({ bg }) => handleColorSelect({ color: bg })}
+ />
+ </div>
+ </PropertyGroup>
+
+ <PropertyGroup title="Syntax UI" defaultExpanded={false}>
+ <div className="grid w-full grid-cols-4 gap-2">
+ <BackgroundPreviews
+ backgrounds={syntaxUIGradients}
+ currentBackgroundColor={currentBackgroundColor}
+ isColorBackground={isColorBackground}
+ handleColorSelect={({ bg }) => handleColorSelect({ color: bg })}
+ />
+ </div>
+ </PropertyGroup>
+ </div>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/assets/views/sounds.tsx b/apps/web/src/components/editor/panels/assets/views/sounds.tsx
new file mode 100644
index 00000000..84a5d0bf
--- /dev/null
+++ b/apps/web/src/components/editor/panels/assets/views/sounds.tsx
@@ -0,0 +1,574 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import {
+ DropdownMenu,
+ DropdownMenuCheckboxItem,
+ DropdownMenuContent,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Input } from "@/components/ui/input";
+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 "@/types/sounds";
+import { cn } from "@/utils/ui";
+import {
+ FavouriteIcon,
+ FilterMailIcon,
+ PauseIcon,
+ PlayIcon,
+ PlusSignIcon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
+
+export function SoundsView() {
+ return (
+ <div className="flex h-full flex-col">
+ <Tabs defaultValue="sound-effects" className="flex h-full flex-col">
+ <div className="px-3 pt-4 pb-0">
+ <TabsList>
+ <TabsTrigger value="sound-effects">Sound effects</TabsTrigger>
+ <TabsTrigger value="songs">Songs</TabsTrigger>
+ <TabsTrigger value="saved">Saved</TabsTrigger>
+ </TabsList>
+ </div>
+ <Separator className="my-4" />
+ <TabsContent
+ value="sound-effects"
+ className="mt-0 flex min-h-0 flex-1 flex-col p-5 pt-0"
+ >
+ <SoundEffectsView />
+ </TabsContent>
+ <TabsContent
+ value="saved"
+ className="mt-0 flex min-h-0 flex-1 flex-col p-5 pt-0"
+ >
+ <SavedSoundsView />
+ </TabsContent>
+ <TabsContent
+ value="songs"
+ className="mt-0 flex min-h-0 flex-1 flex-col p-5 pt-0"
+ >
+ <SongsView />
+ </TabsContent>
+ </Tabs>
+ </div>
+ );
+}
+
+function SoundEffectsView() {
+ const {
+ topSoundEffects,
+ isLoading,
+ searchQuery,
+ setSearchQuery,
+ scrollPosition,
+ setScrollPosition,
+ loadSavedSounds,
+ showCommercialOnly,
+ toggleCommercialFilter,
+ hasLoaded,
+ setTopSoundEffects,
+ setLoading,
+ setError,
+ setHasLoaded,
+ setCurrentPage,
+ setHasNextPage,
+ setTotalCount,
+ } = useSoundsStore();
+ const {
+ results: searchResults,
+ isLoading: isSearching,
+ loadMore,
+ hasNextPage,
+ isLoadingMore,
+ } = useSoundSearch({
+ query: searchQuery,
+ commercialOnly: showCommercialOnly,
+ });
+
+ const [playingId, setPlayingId] = useState<number | null>(null);
+ const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(
+ null,
+ );
+
+ const { scrollAreaRef, handleScroll } = useInfiniteScroll({
+ onLoadMore: loadMore,
+ hasMore: hasNextPage,
+ isLoading: isLoadingMore || isSearching,
+ });
+
+ useEffect(() => {
+ loadSavedSounds();
+ }, [loadSavedSounds]);
+
+ useEffect(() => {
+ if (hasLoaded) {
+ return;
+ }
+
+ let shouldIgnore = false;
+
+ const fetchTopSounds = async () => {
+ try {
+ if (!shouldIgnore) {
+ setLoading({ loading: true });
+ setError({ error: null });
+ }
+
+ const response = await fetch(
+ "/api/sounds/search?page_size=50&sort=downloads",
+ );
+
+ if (!shouldIgnore) {
+ if (!response.ok) {
+ throw new Error(`Failed to fetch: ${response.status}`);
+ }
+
+ const data = await response.json();
+ setTopSoundEffects({ sounds: data.results });
+ setHasLoaded({ loaded: true });
+
+ setCurrentPage({ page: 1 });
+ setHasNextPage({ hasNext: !!data.next });
+ setTotalCount({ count: data.count });
+ }
+ } catch (error) {
+ if (!shouldIgnore) {
+ console.error("Failed to fetch top sounds:", error);
+ setError({
+ error:
+ error instanceof Error ? error.message : "Failed to load sounds",
+ });
+ }
+ } finally {
+ if (!shouldIgnore) {
+ setLoading({ loading: false });
+ }
+ }
+ };
+
+ const timeoutId = setTimeout(fetchTopSounds, 100, {});
+
+ return () => {
+ shouldIgnore = true;
+ clearTimeout(timeoutId);
+ };
+ }, [
+ hasLoaded,
+ setTopSoundEffects,
+ setLoading,
+ setError,
+ setHasLoaded,
+ setCurrentPage,
+ setHasNextPage,
+ setTotalCount,
+ ]);
+
+ useEffect(() => {
+ if (!scrollAreaRef.current || scrollPosition <= 0) {
+ return;
+ }
+
+ const restoreScrollPosition = () => {
+ scrollAreaRef.current?.scrollTo({ top: scrollPosition });
+ };
+
+ const timeoutId = setTimeout(restoreScrollPosition, 100, {});
+
+ return () => clearTimeout(timeoutId);
+ }, [scrollPosition, scrollAreaRef]);
+
+ const handleScrollWithPosition = ({
+ currentTarget,
+ }: React.UIEvent<HTMLDivElement>) => {
+ const { scrollTop } = currentTarget;
+ setScrollPosition({ position: scrollTop });
+ handleScroll({ currentTarget } as React.UIEvent<HTMLDivElement>);
+ };
+
+ const displayedSounds = searchQuery ? searchResults : topSoundEffects;
+
+ const playSound = ({ sound }: { sound: SoundEffect }) => {
+ if (playingId === sound.id) {
+ audioElement?.pause();
+ setPlayingId(null);
+ return;
+ }
+
+ audioElement?.pause();
+
+ if (sound.previewUrl) {
+ const audio = new Audio(sound.previewUrl);
+ audio.addEventListener("ended", () => {
+ setPlayingId(null);
+ });
+ audio.addEventListener("error", () => {
+ setPlayingId(null);
+ });
+ audio.play().catch((error) => {
+ console.error("Failed to play sound preview:", error);
+ setPlayingId(null);
+ });
+
+ setAudioElement(audio);
+ setPlayingId(sound.id);
+ }
+ };
+
+ return (
+ <div className="mt-1 flex h-full flex-col gap-5">
+ <div className="flex items-center gap-3">
+ <Input
+ placeholder="Search sound effects"
+ className="bg-panel-accent w-full"
+ containerClassName="w-full"
+ value={searchQuery}
+ onChange={({ currentTarget }) =>
+ setSearchQuery({ query: currentTarget.value })
+ }
+ showClearIcon
+ onClear={() => setSearchQuery({ query: "" })}
+ />
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ variant="text"
+ size="icon"
+ className={cn(showCommercialOnly && "text-primary")}
+ >
+ <HugeiconsIcon icon={FilterMailIcon} />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end" className="w-56">
+ <DropdownMenuCheckboxItem
+ checked={showCommercialOnly}
+ onCheckedChange={() => toggleCommercialFilter()}
+ >
+ Show only commercially licensed
+ </DropdownMenuCheckboxItem>
+ <div className="text-muted-foreground px-2 py-1.5 text-xs">
+ {showCommercialOnly
+ ? "Only showing sounds licensed for commercial use"
+ : "Showing all sounds regardless of license"}
+ </div>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ </div>
+
+ <div className="relative h-full overflow-hidden">
+ <ScrollArea
+ className="h-full flex-1"
+ ref={scrollAreaRef}
+ onScrollCapture={handleScrollWithPosition}
+ >
+ <div className="flex flex-col gap-4">
+ {isLoading && !searchQuery && (
+ <div className="text-muted-foreground text-sm">
+ Loading sounds...
+ </div>
+ )}
+ {isSearching && searchQuery && (
+ <div className="text-muted-foreground text-sm">Searching...</div>
+ )}
+ {displayedSounds.map((sound) => (
+ <AudioItem
+ key={sound.id}
+ sound={sound}
+ isPlaying={playingId === sound.id}
+ onPlay={playSound}
+ />
+ ))}
+ {!isLoading && !isSearching && displayedSounds.length === 0 && (
+ <div className="text-muted-foreground text-sm">
+ {searchQuery ? "No sounds found" : "No sounds available"}
+ </div>
+ )}
+ {isLoadingMore && (
+ <div className="text-muted-foreground py-4 text-center text-sm">
+ Loading more sounds...
+ </div>
+ )}
+ </div>
+ </ScrollArea>
+ </div>
+ </div>
+ );
+}
+
+function SavedSoundsView() {
+ const {
+ savedSounds,
+ isLoadingSavedSounds,
+ savedSoundsError,
+ loadSavedSounds,
+ clearSavedSounds,
+ } = useSoundsStore();
+
+ const [playingId, setPlayingId] = useState<number | null>(null);
+ const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(
+ null,
+ );
+
+ const [showClearDialog, setShowClearDialog] = useState(false);
+
+ useEffect(() => {
+ loadSavedSounds();
+ }, [loadSavedSounds]);
+
+ const playSound = ({ sound }: { sound: SoundEffect }) => {
+ if (playingId === sound.id) {
+ audioElement?.pause();
+ setPlayingId(null);
+ return;
+ }
+
+ audioElement?.pause();
+
+ if (sound.previewUrl) {
+ const audio = new Audio(sound.previewUrl);
+ audio.addEventListener("ended", () => {
+ setPlayingId(null);
+ });
+ audio.addEventListener("error", () => {
+ setPlayingId(null);
+ });
+ audio.play().catch((error) => {
+ console.error("Failed to play sound preview:", error);
+ setPlayingId(null);
+ });
+
+ setAudioElement(audio);
+ setPlayingId(sound.id);
+ }
+ };
+
+ const convertToSoundEffect = ({
+ savedSound,
+ }: {
+ savedSound: SavedSound;
+ }): SoundEffect => ({
+ id: savedSound.id,
+ name: savedSound.name,
+ description: "",
+ url: "",
+ previewUrl: savedSound.previewUrl,
+ downloadUrl: savedSound.downloadUrl,
+ duration: savedSound.duration,
+ filesize: 0,
+ type: "audio",
+ channels: 0,
+ bitrate: 0,
+ bitdepth: 0,
+ samplerate: 0,
+ username: savedSound.username,
+ tags: savedSound.tags,
+ license: savedSound.license,
+ created: savedSound.savedAt,
+ downloads: 0,
+ rating: 0,
+ ratingCount: 0,
+ });
+
+ if (isLoadingSavedSounds) {
+ return (
+ <div className="flex h-full items-center justify-center">
+ <div className="text-muted-foreground text-sm">
+ Loading saved sounds...
+ </div>
+ </div>
+ );
+ }
+
+ if (savedSoundsError) {
+ return (
+ <div className="flex h-full items-center justify-center">
+ <div className="text-destructive text-sm">
+ Error: {savedSoundsError}
+ </div>
+ </div>
+ );
+ }
+
+ if (savedSounds.length === 0) {
+ return (
+ <div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4">
+ <HugeiconsIcon
+ icon={FavouriteIcon}
+ className="text-muted-foreground size-10"
+ />
+ <div className="flex flex-col gap-2 text-center">
+ <p className="text-lg font-medium">No saved sounds</p>
+ <p className="text-muted-foreground text-sm text-balance">
+ Click the heart icon on any sound to save it here
+ </p>
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className="mt-1 flex h-full flex-col gap-5">
+ <div className="flex items-center justify-between">
+ <p className="text-muted-foreground text-sm">
+ {savedSounds.length} saved{" "}
+ {savedSounds.length === 1 ? "sound" : "sounds"}
+ </p>
+ <Dialog open={showClearDialog} onOpenChange={setShowClearDialog}>
+ <DialogTrigger asChild>
+ <Button
+ variant="text"
+ size="sm"
+ className="text-muted-foreground hover:text-destructive h-auto !opacity-100"
+ >
+ Clear all
+ </Button>
+ </DialogTrigger>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Clear all saved sounds?</DialogTitle>
+ <DialogDescription>
+ This will permanently remove all {savedSounds.length} saved
+ sounds from your collection. This action cannot be undone.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter>
+ <Button variant="text" onClick={() => setShowClearDialog(false)}>
+ Cancel
+ </Button>
+ <Button
+ variant="destructive"
+ onClick={async ({
+ stopPropagation,
+ }: React.MouseEvent<HTMLButtonElement>) => {
+ stopPropagation();
+ await clearSavedSounds();
+ setShowClearDialog(false);
+ }}
+ >
+ Clear all sounds
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ </div>
+
+ <div className="relative h-full overflow-hidden">
+ <ScrollArea className="h-full flex-1">
+ <div className="flex flex-col gap-4">
+ {savedSounds.map((sound) => (
+ <AudioItem
+ key={sound.id}
+ sound={convertToSoundEffect({ savedSound: sound })}
+ isPlaying={playingId === sound.id}
+ onPlay={playSound}
+ />
+ ))}
+ </div>
+ </ScrollArea>
+ </div>
+ </div>
+ );
+}
+
+function SongsView() {
+ return <div>Songs</div>;
+}
+
+interface AudioItemProps {
+ sound: SoundEffect;
+ isPlaying: boolean;
+ onPlay: ({ sound }: { sound: SoundEffect }) => void;
+}
+
+function AudioItem({ sound, isPlaying, onPlay }: AudioItemProps) {
+ const { addSoundToTimeline, isSoundSaved, toggleSavedSound } =
+ useSoundsStore();
+ const isSaved = isSoundSaved({ soundId: sound.id });
+
+ const handleClick = () => {
+ onPlay({ sound });
+ };
+
+ const handleSaveClick = ({
+ stopPropagation,
+ }: React.MouseEvent<HTMLButtonElement>) => {
+ stopPropagation();
+ toggleSavedSound({ soundEffect: sound });
+ };
+
+ const handleAddToTimeline = async ({
+ stopPropagation,
+ }: React.MouseEvent<HTMLButtonElement>) => {
+ stopPropagation();
+ await addSoundToTimeline({ sound });
+ };
+
+ return (
+ <div className="group flex items-center gap-3 opacity-100 hover:opacity-75">
+ <button
+ type="button"
+ className="flex min-w-0 flex-1 items-center gap-3 text-left"
+ onClick={handleClick}
+ >
+ <div className="bg-accent relative flex size-12 shrink-0 items-center justify-center overflow-hidden rounded-md">
+ <div className="from-primary/20 absolute inset-0 bg-gradient-to-br to-transparent" />
+ {isPlaying ? (
+ <HugeiconsIcon icon={PauseIcon} className="size-5" />
+ ) : (
+ <HugeiconsIcon icon={PlayIcon} className="size-5" />
+ )}
+ </div>
+
+ <div className="min-w-0 flex-1 overflow-hidden">
+ <p className="truncate text-sm font-medium">{sound.name}</p>
+ <span className="text-muted-foreground block truncate text-xs">
+ {sound.username}
+ </span>
+ </div>
+ </button>
+
+ <div className="flex items-center gap-3 pr-2">
+ <Button
+ variant="text"
+ size="icon"
+ className="text-muted-foreground hover:text-foreground w-auto !opacity-100"
+ onClick={handleAddToTimeline}
+ title="Add to timeline"
+ >
+ <HugeiconsIcon icon={PlusSignIcon} />
+ </Button>
+ <Button
+ variant="text"
+ size="icon"
+ className={`hover:text-foreground w-auto !opacity-100 ${
+ isSaved
+ ? "text-red-500 hover:text-red-600"
+ : "text-muted-foreground"
+ }`}
+ onClick={handleSaveClick}
+ title={isSaved ? "Remove from saved" : "Save sound"}
+ >
+ <HugeiconsIcon
+ icon={FavouriteIcon}
+ className={`${isSaved ? "fill-current" : ""}`}
+ />
+ </Button>
+ </div>
+ </div>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/assets/views/stickers.tsx b/apps/web/src/components/editor/panels/assets/views/stickers.tsx
new file mode 100644
index 00000000..6b48e775
--- /dev/null
+++ b/apps/web/src/components/editor/panels/assets/views/stickers.tsx
@@ -0,0 +1,615 @@
+"use client";
+
+import Image from "next/image";
+import type { CSSProperties } from "react";
+import { useEffect, useMemo, useState } from "react";
+import { toast } from "sonner";
+import { DraggableItem } from "@/components/editor/panels/assets/draggable-item";
+import { PanelBaseView as BaseView } from "@/components/editor/panels/panel-base-view";
+import { Button } from "@/components/ui/button";
+import { InputWithBack } from "@/components/ui/input-with-back";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { STICKER_CATEGORIES } from "@/constants/stickers-constants";
+import { useInfiniteScroll } from "@/hooks/use-infinite-scroll";
+import {
+ buildIconSvgUrl,
+ getIconSvgUrl,
+ ICONIFY_HOSTS,
+ POPULAR_COLLECTIONS,
+} from "@/lib/iconify-api";
+import { useStickersStore } from "@/stores/stickers-store";
+import type { StickerCategory } from "@/types/stickers";
+import { cn } from "@/utils/ui";
+import {
+ ArrowRightIcon,
+ HappyIcon,
+ ClockIcon,
+ LayoutGridIcon,
+ MultiplicationSignIcon,
+ SparklesIcon,
+ HashtagIcon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { Spinner } from "@/components/ui/spinner";
+
+function isStickerCategory(value: string): value is StickerCategory {
+ return STICKER_CATEGORIES.includes(value as StickerCategory);
+}
+
+export function StickersView() {
+ const { selectedCategory, setSelectedCategory } = useStickersStore();
+
+ return (
+ <BaseView
+ value={selectedCategory}
+ onValueChange={(v) => {
+ if (isStickerCategory(v)) {
+ setSelectedCategory({ category: v });
+ }
+ }}
+ tabs={[
+ {
+ value: "all",
+ label: "All",
+ icon: <HugeiconsIcon icon={LayoutGridIcon} className="size-3" />,
+ content: <StickersContentView category="all" />,
+ },
+ {
+ value: "general",
+ label: "Icons",
+ icon: <HugeiconsIcon icon={SparklesIcon} className="size-3" />,
+ content: <StickersContentView category="general" />,
+ },
+ {
+ value: "brands",
+ label: "Brands",
+ icon: <HugeiconsIcon icon={HashtagIcon} className="size-3" />,
+ content: <StickersContentView category="brands" />,
+ },
+ {
+ value: "emoji",
+ label: "Emoji",
+ icon: <HugeiconsIcon icon={HappyIcon} className="size-3" />,
+ content: <StickersContentView category="emoji" />,
+ },
+ ]}
+ className="flex h-full flex-col overflow-hidden p-0"
+ />
+ );
+}
+
+function StickerGrid({
+ icons,
+ onAdd,
+ addingSticker,
+ capSize = false,
+}: {
+ icons: string[];
+ onAdd: (iconName: string) => void;
+ addingSticker: string | null;
+ capSize?: boolean;
+}) {
+ const gridStyle: CSSProperties & {
+ "--sticker-min": string;
+ "--sticker-max"?: string;
+ } = {
+ gridTemplateColumns: capSize
+ ? "repeat(auto-fill, minmax(var(--sticker-min, 96px), var(--sticker-max, 160px)))"
+ : "repeat(auto-fit, minmax(var(--sticker-min, 96px), 1fr))",
+ "--sticker-min": "96px",
+ ...(capSize ? { "--sticker-max": "160px" } : {}),
+ };
+
+ return (
+ <div className="grid gap-2" style={gridStyle}>
+ {icons.map((iconName) => (
+ <StickerItem
+ key={iconName}
+ iconName={iconName}
+ onAdd={onAdd}
+ isAdding={addingSticker === iconName}
+ capSize={capSize}
+ />
+ ))}
+ </div>
+ );
+}
+
+function CollectionGrid({
+ collections,
+ onSelectCollection,
+}: {
+ collections: Array<{
+ prefix: string;
+ name: string;
+ total: number;
+ category?: string;
+ }>;
+ onSelectCollection: ({ prefix }: { prefix: string }) => void;
+}) {
+ return (
+ <div className="grid grid-cols-1 gap-2">
+ {collections.map((collection) => (
+ <CollectionItem
+ key={collection.prefix}
+ title={collection.name}
+ subtitle={`${collection.total.toLocaleString()} icons${collection.category ? ` • ${collection.category}` : ""}`}
+ onClick={() => onSelectCollection({ prefix: collection.prefix })}
+ />
+ ))}
+ </div>
+ );
+}
+
+function EmptyView({ message }: { message: string }) {
+ return (
+ <div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4">
+ <HugeiconsIcon
+ icon={HappyIcon}
+ className="text-muted-foreground size-10"
+ />
+ <div className="flex flex-col gap-2 text-center">
+ <p className="text-lg font-medium">No stickers found</p>
+ <p className="text-muted-foreground text-sm text-balance">{message}</p>
+ </div>
+ </div>
+ );
+}
+
+function StickersContentView({ category }: { category: StickerCategory }) {
+ const {
+ searchQuery,
+ selectedCollection,
+ viewMode,
+ collections,
+ currentCollection,
+ searchResults,
+ recentStickers,
+ isLoadingCollections,
+ isLoadingCollection,
+ isSearching,
+ setSearchQuery,
+ setSelectedCollection,
+ loadCollections,
+ searchStickers,
+ addStickerToTimeline,
+ clearRecentStickers,
+ setSelectedCategory,
+ addingSticker,
+ } = useStickersStore();
+
+ const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery);
+ const [collectionsToShow, setCollectionsToShow] = useState(20);
+ const [showCollectionItems, setShowCollectionItems] = useState(false);
+
+ const filteredCollections = useMemo(() => {
+ if (category === "all") {
+ return Object.entries(collections).map(([prefix, collection]) => ({
+ prefix,
+ name: collection.name,
+ total: collection.total,
+ category: collection.category,
+ }));
+ }
+
+ const collectionList =
+ POPULAR_COLLECTIONS[category as keyof typeof POPULAR_COLLECTIONS];
+ if (!collectionList) return [];
+
+ return collectionList
+ .map((c) => {
+ const collection = collections[c.prefix];
+ return collection
+ ? {
+ prefix: c.prefix,
+ name: c.name,
+ total: collection.total,
+ }
+ : null;
+ })
+ .filter(Boolean) as Array<{
+ prefix: string;
+ name: string;
+ total: number;
+ }>;
+ }, [collections, category]);
+
+ const { scrollAreaRef, handleScroll } = useInfiniteScroll({
+ onLoadMore: () => setCollectionsToShow((prev) => prev + 20),
+ hasMore: filteredCollections.length > collectionsToShow,
+ isLoading: isLoadingCollections,
+ enabled: viewMode === "browse" && !selectedCollection && category === "all",
+ });
+
+ useEffect(() => {
+ if (Object.keys(collections).length === 0) {
+ loadCollections();
+ }
+ }, [collections, loadCollections]);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ if (localSearchQuery !== searchQuery) {
+ setSearchQuery({ query: localSearchQuery });
+ if (localSearchQuery.trim()) {
+ searchStickers({ query: localSearchQuery });
+ }
+ }
+ }, 500);
+
+ return () => clearTimeout(timer);
+ }, [localSearchQuery, searchQuery, searchStickers, setSearchQuery]);
+
+ const handleAddSticker = async (iconName: string) => {
+ try {
+ await addStickerToTimeline({ iconName });
+ } catch (error) {
+ console.error("Failed to add sticker:", error);
+ toast.error("Failed to add sticker to timeline");
+ }
+ };
+
+ const iconsToDisplay = useMemo(() => {
+ if (viewMode === "search" && searchResults) {
+ return searchResults.icons;
+ }
+
+ if (viewMode === "collection" && currentCollection) {
+ const icons: string[] = [];
+
+ if (currentCollection.uncategorized) {
+ icons.push(
+ ...currentCollection.uncategorized.map(
+ (name) => `${currentCollection.prefix}:${name}`,
+ ),
+ );
+ }
+
+ if (currentCollection.categories) {
+ Object.values(currentCollection.categories).forEach((categoryIcons) => {
+ icons.push(
+ ...categoryIcons.map(
+ (name) => `${currentCollection.prefix}:${name}`,
+ ),
+ );
+ });
+ }
+
+ return icons.slice(0, 100);
+ }
+
+ return [];
+ }, [viewMode, searchResults, currentCollection]);
+
+ const isInCollection = viewMode === "collection" && !!selectedCollection;
+
+ useEffect(() => {
+ if (isInCollection) {
+ setShowCollectionItems(false);
+ const timer = setTimeout(() => setShowCollectionItems(true), 350);
+ return () => clearTimeout(timer);
+ } else {
+ setShowCollectionItems(false);
+ }
+ }, [isInCollection]);
+
+ return (
+ <div className="mt-1 flex h-full flex-col gap-5 p-4">
+ <div className="space-y-3">
+ <InputWithBack
+ isExpanded={isInCollection}
+ setIsExpanded={(expanded) => {
+ if (!expanded && isInCollection) {
+ setSelectedCollection({ collection: null });
+ }
+ }}
+ placeholder={
+ category === "all"
+ ? "Search all stickers"
+ : category === "general"
+ ? "Search icons"
+ : category === "brands"
+ ? "Search brands"
+ : "Search Emojis"
+ }
+ value={localSearchQuery}
+ onChange={setLocalSearchQuery}
+ disableAnimation={true}
+ />
+ </div>
+
+ <div className="relative h-full overflow-hidden">
+ <ScrollArea
+ className="h-full flex-1"
+ ref={scrollAreaRef}
+ onScrollCapture={handleScroll}
+ >
+ <div className="flex h-full flex-col gap-4">
+ {recentStickers.length > 0 && viewMode === "browse" && (
+ <div className="h-full">
+ <div className="mb-2 flex items-center gap-2">
+ <HugeiconsIcon
+ icon={ClockIcon}
+ className="text-muted-foreground size-4"
+ />
+ <span className="text-sm font-medium">Recent</span>
+ <TooltipProvider>
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <button
+ type="button"
+ onClick={clearRecentStickers}
+ className="hover:bg-accent ml-auto flex size-5 items-center justify-center rounded p-0"
+ >
+ <HugeiconsIcon
+ icon={MultiplicationSignIcon}
+ className="text-muted-foreground size-3"
+ />
+ </button>
+ </TooltipTrigger>
+ <TooltipContent>
+ <p>Clear recent stickers</p>
+ </TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ </div>
+ <StickerGrid
+ icons={recentStickers.slice(0, 12)}
+ onAdd={handleAddSticker}
+ addingSticker={addingSticker}
+ capSize
+ />
+ </div>
+ )}
+
+ {viewMode === "collection" && selectedCollection && (
+ <div className="h-full">
+ {isLoadingCollection ? (
+ <div className="flex items-center justify-center py-8">
+ <Spinner className="text-muted-foreground size-6" />
+ </div>
+ ) : showCollectionItems ? (
+ <StickerGrid
+ icons={iconsToDisplay}
+ onAdd={handleAddSticker}
+ addingSticker={addingSticker}
+ />
+ ) : (
+ <div className="flex items-center justify-center py-8">
+ <Spinner className="text-muted-foreground size-6" />
+ </div>
+ )}
+ </div>
+ )}
+
+ {viewMode === "search" && (
+ <div className="h-full">
+ {isSearching ? (
+ <div className="flex items-center justify-center py-8">
+ <Spinner className="text-muted-foreground size-6" />
+ </div>
+ ) : searchResults?.icons.length ? (
+ <>
+ <div className="mb-3 flex items-center justify-between">
+ <span className="text-muted-foreground text-sm">
+ {searchResults.total} results
+ </span>
+ </div>
+ <StickerGrid
+ icons={iconsToDisplay}
+ onAdd={handleAddSticker}
+ addingSticker={addingSticker}
+ capSize
+ />
+ </>
+ ) : searchQuery ? (
+ <div className="flex flex-col items-center justify-center gap-3 py-8">
+ <EmptyView
+ message={`No stickers found for "${searchQuery}"`}
+ />
+ {category !== "all" && (
+ <Button
+ variant="outline"
+ onClick={() => {
+ const q = localSearchQuery || searchQuery;
+ if (q) {
+ setSearchQuery({ query: q });
+ }
+ setSelectedCategory({ category: "all" });
+ if (q) {
+ searchStickers({ query: q });
+ }
+ }}
+ >
+ Search in all icons
+ </Button>
+ )}
+ </div>
+ ) : null}
+ </div>
+ )}
+
+ {viewMode === "browse" && !selectedCollection && (
+ <div className="h-full space-y-4">
+ {isLoadingCollections ? (
+ <div className="flex items-center justify-center py-8">
+ <Spinner className="text-muted-foreground size-6" />
+ </div>
+ ) : (
+ <>
+ {category !== "all" && (
+ <div className="h-full">
+ <CollectionGrid
+ collections={filteredCollections}
+ onSelectCollection={({ prefix }) =>
+ setSelectedCollection({ collection: prefix })
+ }
+ />
+ </div>
+ )}
+
+ {category === "all" && filteredCollections.length > 0 && (
+ <div className="h-full">
+ <CollectionGrid
+ collections={filteredCollections.slice(
+ 0,
+ collectionsToShow,
+ )}
+ onSelectCollection={({ prefix }) =>
+ setSelectedCollection({ collection: prefix })
+ }
+ />
+ </div>
+ )}
+ </>
+ )}
+ </div>
+ )}
+ </div>
+ </ScrollArea>
+ </div>
+ </div>
+ );
+}
+
+interface CollectionItemProps {
+ title: string;
+ subtitle: string;
+ onClick: () => void;
+}
+
+function CollectionItem({ title, subtitle, onClick }: CollectionItemProps) {
+ return (
+ <Button
+ variant="outline"
+ className="h-auto justify-between rounded-md py-2"
+ onClick={onClick}
+ >
+ <div className="text-left">
+ <p className="font-medium">{title}</p>
+ <p className="text-muted-foreground text-xs">{subtitle}</p>
+ </div>
+ <HugeiconsIcon icon={ArrowRightIcon} className="size-4" />
+ </Button>
+ );
+}
+
+interface StickerItemProps {
+ iconName: string;
+ onAdd: (iconName: string) => void;
+ isAdding?: boolean;
+ capSize?: boolean;
+}
+
+function StickerItem({
+ iconName,
+ onAdd,
+ isAdding,
+ capSize = false,
+}: StickerItemProps) {
+ const [imageError, setImageError] = useState(false);
+ const [hostIndex, setHostIndex] = useState(0);
+
+ useEffect(() => {
+ if (!iconName) {
+ return;
+ }
+ setImageError(false);
+ setHostIndex(0);
+ }, [iconName]);
+
+ const displayName = iconName.split(":")[1] || iconName;
+ const collectionPrefix = iconName.split(":")[0];
+
+ const preview = imageError ? (
+ <div className="flex size-full items-center justify-center p-2">
+ <span className="text-muted-foreground text-center text-xs break-all">
+ {displayName}
+ </span>
+ </div>
+ ) : (
+ <div className="flex size-full items-center justify-center p-4">
+ <Image
+ src={
+ hostIndex === 0
+ ? getIconSvgUrl(iconName, { width: 64, height: 64 })
+ : buildIconSvgUrl(
+ ICONIFY_HOSTS[Math.min(hostIndex, ICONIFY_HOSTS.length - 1)],
+ iconName,
+ { width: 64, height: 64 },
+ )
+ }
+ alt={displayName}
+ width={64}
+ height={64}
+ className="size-full object-contain"
+ style={
+ capSize
+ ? {
+ maxWidth: "var(--sticker-max, 160px)",
+ maxHeight: "var(--sticker-max, 160px)",
+ }
+ : undefined
+ }
+ onError={() => {
+ const next = hostIndex + 1;
+ if (next < ICONIFY_HOSTS.length) {
+ setHostIndex(next);
+ } else {
+ setImageError(true);
+ }
+ }}
+ loading="lazy"
+ unoptimized
+ />
+ </div>
+ );
+
+ return (
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <div
+ className={cn(
+ "relative",
+ isAdding && "pointer-events-none opacity-50",
+ )}
+ >
+ <DraggableItem
+ name={displayName}
+ preview={preview}
+ dragData={{
+ id: iconName,
+ type: "sticker",
+ name: displayName,
+ iconName,
+ }}
+ onAddToTimeline={() => onAdd(iconName)}
+ aspectRatio={1}
+ shouldShowLabel={false}
+ isRounded={true}
+ variant="card"
+ className=""
+ containerClassName="w-full"
+ />
+ {isAdding && (
+ <div className="absolute inset-0 z-10 flex items-center justify-center rounded-md bg-black/60">
+ <Spinner className="size-6 text-white" />
+ </div>
+ )}
+ </div>
+ </TooltipTrigger>
+ <TooltipContent>
+ <div className="space-y-1">
+ <p className="font-medium">{displayName}</p>
+ <p className="text-muted-foreground text-xs">{collectionPrefix}</p>
+ </div>
+ </TooltipContent>
+ </Tooltip>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/assets/views/text.tsx b/apps/web/src/components/editor/panels/assets/views/text.tsx
new file mode 100644
index 00000000..6ccf5d98
--- /dev/null
+++ b/apps/web/src/components/editor/panels/assets/views/text.tsx
@@ -0,0 +1,46 @@
+import { DraggableItem } from "@/components/editor/panels/assets/draggable-item";
+import { PanelBaseView as BaseView } from "@/components/editor/panels/panel-base-view";
+import { useEditor } from "@/hooks/use-editor";
+import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
+import { buildTextElement } from "@/lib/timeline/element-utils";
+
+export function TextView() {
+ const editor = useEditor();
+
+ const handleAddToTimeline = ({ currentTime }: { currentTime: number }) => {
+ const activeScene = editor.scenes.getActiveScene();
+ if (!activeScene) return;
+
+ const element = buildTextElement({
+ raw: DEFAULT_TEXT_ELEMENT,
+ startTime: currentTime,
+ });
+
+ editor.timeline.insertElement({
+ element,
+ placement: { mode: "auto" },
+ });
+ };
+
+ return (
+ <BaseView>
+ <DraggableItem
+ name="Default text"
+ preview={
+ <div className="bg-panel-accent flex size-full items-center justify-center rounded">
+ <span className="text-xs select-none">Default text</span>
+ </div>
+ }
+ dragData={{
+ id: "temp-text-id",
+ type: DEFAULT_TEXT_ELEMENT.type,
+ name: DEFAULT_TEXT_ELEMENT.name,
+ content: DEFAULT_TEXT_ELEMENT.content,
+ }}
+ aspectRatio={1}
+ onAddToTimeline={handleAddToTimeline}
+ shouldShowLabel={false}
+ />
+ </BaseView>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/panel-base-view.tsx b/apps/web/src/components/editor/panels/panel-base-view.tsx
new file mode 100644
index 00000000..b16d1db6
--- /dev/null
+++ b/apps/web/src/components/editor/panels/panel-base-view.tsx
@@ -0,0 +1,85 @@
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Separator } from "@/components/ui/separator";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { cn } from "@/utils/ui";
+
+interface PanelBaseViewProps {
+ children?: React.ReactNode;
+ defaultTab?: string;
+ value?: string;
+ onValueChange?: (value: string) => void;
+ tabs?: {
+ value: string;
+ label: string;
+ icon?: React.ReactNode;
+ content: React.ReactNode;
+ }[];
+ className?: string;
+ ref?: React.RefObject<HTMLDivElement>;
+}
+
+function ViewContent({
+ children,
+ className,
+}: {
+ children: React.ReactNode;
+ className?: string;
+}) {
+ return (
+ <ScrollArea className="flex-1">
+ <div className={cn("p-5", className)}>{children}</div>
+ </ScrollArea>
+ );
+}
+
+export function PanelBaseView({
+ children,
+ defaultTab,
+ value,
+ onValueChange,
+ tabs,
+ className = "",
+ ref,
+}: PanelBaseViewProps) {
+ return (
+ <div className={cn("flex h-full flex-col", className)} ref={ref}>
+ {!tabs || tabs.length === 0 ? (
+ <ViewContent className={className}>{children}</ViewContent>
+ ) : (
+ <Tabs
+ defaultValue={defaultTab}
+ value={value}
+ onValueChange={onValueChange}
+ className="flex h-full flex-col"
+ >
+ <div className="bg-panel sticky top-0 z-10">
+ <div className="px-3 pt-3 pb-0">
+ <TabsList>
+ {tabs.map((tab) => (
+ <TabsTrigger key={tab.value} value={tab.value}>
+ {tab.icon ? (
+ <span className="mr-1 inline-flex items-center">
+ {tab.icon}
+ </span>
+ ) : null}
+ {tab.label}
+ </TabsTrigger>
+ ))}
+ </TabsList>
+ </div>
+ <Separator className="mt-3" />
+ </div>
+ {tabs.map((tab) => (
+ <TabsContent
+ key={tab.value}
+ value={tab.value}
+ className="mt-0 flex min-h-0 flex-1 flex-col"
+ >
+ <ViewContent className={className}>{tab.content}</ViewContent>
+ </TabsContent>
+ ))}
+ </Tabs>
+ )}
+ </div>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/preview/index.tsx b/apps/web/src/components/editor/panels/preview/index.tsx
new file mode 100644
index 00000000..5ed9db00
--- /dev/null
+++ b/apps/web/src/components/editor/panels/preview/index.tsx
@@ -0,0 +1,124 @@
+"use client";
+
+import { useCallback, useMemo, useRef } from "react";
+import useDeepCompareEffect from "use-deep-compare-effect";
+import { useEditor } from "@/hooks/use-editor";
+import { useRafLoop } from "@/hooks/use-raf-loop";
+import { CanvasRenderer } from "@/services/renderer/canvas-renderer";
+import type { RootNode } from "@/services/renderer/nodes/root-node";
+import { buildScene } from "@/services/renderer/scene-builder";
+import { getLastFrameTime } from "@/lib/time";
+
+function usePreviewSize() {
+ const editor = useEditor();
+ const activeProject = editor.project.getActive();
+
+ return {
+ width: activeProject?.settings.canvasSize.width,
+ height: activeProject?.settings.canvasSize.height,
+ };
+}
+
+function RenderTreeController() {
+ const editor = useEditor();
+ const tracks = editor.timeline.getTracks();
+ const mediaAssets = editor.media.getAssets();
+ const activeProject = editor.project.getActive();
+
+ const { width, height } = usePreviewSize();
+
+ useDeepCompareEffect(() => {
+ if (!activeProject) return;
+
+ const duration = editor.timeline.getTotalDuration();
+ const renderTree = buildScene({
+ tracks,
+ mediaAssets,
+ duration,
+ canvasSize: { width, height },
+ background: activeProject.settings.background,
+ });
+
+ editor.renderer.setRenderTree({ renderTree });
+ }, [tracks, mediaAssets, activeProject?.settings.background, width, height]);
+
+ return null;
+}
+
+export function PreviewPanel() {
+ return (
+ <div className="bg-panel relative flex h-full min-h-0 w-full min-w-0 flex-col rounded-sm">
+ <div className="flex min-h-0 min-w-0 flex-1 items-center justify-center p-2">
+ <PreviewCanvas />
+ <RenderTreeController />
+ </div>
+ </div>
+ );
+}
+
+function PreviewCanvas() {
+ const ref = useRef<HTMLCanvasElement>(null);
+ const lastFrameRef = useRef(-1);
+ const lastSceneRef = useRef<RootNode | null>(null);
+ const renderingRef = useRef(false);
+ const { width, height } = usePreviewSize();
+ const editor = useEditor();
+ const activeProject = editor.project.getActive();
+
+ const renderer = useMemo(() => {
+ return new CanvasRenderer({
+ width,
+ height,
+ fps: activeProject.settings.fps,
+ });
+ }, [width, height, activeProject.settings.fps]);
+
+ const renderTree = editor.renderer.getRenderTree();
+
+ const render = useCallback(() => {
+ if (ref.current && renderTree && !renderingRef.current) {
+ const time = editor.playback.getCurrentTime();
+ const lastFrameTime = getLastFrameTime({
+ duration: renderTree.duration,
+ fps: renderer.fps,
+ });
+ const renderTime = Math.min(time, lastFrameTime);
+ const frame = Math.floor(renderTime * renderer.fps);
+
+ if (
+ frame !== lastFrameRef.current ||
+ renderTree !== lastSceneRef.current
+ ) {
+ renderingRef.current = true;
+ lastSceneRef.current = renderTree;
+ lastFrameRef.current = frame;
+ renderer
+ .renderToCanvas({
+ node: renderTree,
+ time: renderTime,
+ targetCanvas: ref.current,
+ })
+ .then(() => {
+ renderingRef.current = false;
+ });
+ }
+ }
+ }, [renderer, renderTree, editor.playback]);
+
+ useRafLoop(render);
+
+ return (
+ <canvas
+ ref={ref}
+ width={width}
+ height={height}
+ className="block max-h-full max-w-full border"
+ style={{
+ background:
+ activeProject.settings.background.type === "blur"
+ ? "transparent"
+ : activeProject?.settings.background.color,
+ }}
+ />
+ );
+}
diff --git a/apps/web/src/components/editor/panels/properties/audio-properties.tsx b/apps/web/src/components/editor/panels/properties/audio-properties.tsx
new file mode 100644
index 00000000..0b83a4b6
--- /dev/null
+++ b/apps/web/src/components/editor/panels/properties/audio-properties.tsx
@@ -0,0 +1,5 @@
+import type { AudioElement } from "@/types/timeline";
+
+export function AudioProperties({ _element }: { _element: AudioElement }) {
+ return <div className="space-y-4 p-5">Audio properties</div>;
+}
diff --git a/apps/web/src/components/editor/panels/properties/index.tsx b/apps/web/src/components/editor/panels/properties/index.tsx
new file mode 100644
index 00000000..3a552b93
--- /dev/null
+++ b/apps/web/src/components/editor/panels/properties/index.tsx
@@ -0,0 +1,68 @@
+"use client";
+
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { AudioProperties } from "./audio-properties";
+import { VideoProperties } from "./video-properties";
+import { TextProperties } from "./text-properties";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { Settings05Icon } from "@hugeicons/core-free-icons";
+import { useEditor } from "@/hooks/use-editor";
+import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
+
+export function PropertiesPanel() {
+ const editor = useEditor();
+ const { selectedElements } = useElementSelection();
+
+ const elementsWithTracks = editor.timeline.getElementsWithTracks({
+ elements: selectedElements,
+ });
+
+ return (
+ <>
+ {selectedElements.length > 0 ? (
+ <ScrollArea className="bg-panel h-full rounded-sm">
+ {elementsWithTracks.map(({ track, element }) => {
+ if (element.type === "text") {
+ return (
+ <div key={element.id}>
+ <TextProperties element={element} trackId={track.id} />
+ </div>
+ );
+ }
+ if (element.type === "audio") {
+ return <AudioProperties key={element.id} _element={element} />;
+ }
+ if (element.type === "video" || element.type === "image") {
+ return (
+ <div key={element.id}>
+ <VideoProperties _element={element} />
+ </div>
+ );
+ }
+ return null;
+ })}
+ </ScrollArea>
+ ) : (
+ <EmptyView />
+ )}
+ </>
+ );
+}
+
+function EmptyView() {
+ return (
+ <div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4">
+ <HugeiconsIcon
+ icon={Settings05Icon}
+ className="text-muted-foreground/75 size-10"
+ strokeWidth={1}
+ />
+ <div className="flex flex-col gap-2 text-center">
+ <p className="text-lg font-medium">It's empty here</p>
+ <p className="text-muted-foreground text-sm text-balance">
+ Click an element on the timeline to edit its properties
+ </p>
+ </div>
+ </div>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/properties/property-item.tsx b/apps/web/src/components/editor/panels/properties/property-item.tsx
new file mode 100644
index 00000000..6d2fe444
--- /dev/null
+++ b/apps/web/src/components/editor/panels/properties/property-item.tsx
@@ -0,0 +1,91 @@
+import { useState } from "react";
+import { cn } from "@/utils/ui";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { ArrowDownIcon } from "@hugeicons/core-free-icons";
+
+interface PropertyItemProps {
+ direction?: "row" | "column";
+ children: React.ReactNode;
+ className?: string;
+}
+
+export function PropertyItem({
+ direction = "row",
+ children,
+ className,
+}: PropertyItemProps) {
+ return (
+ <div
+ className={cn(
+ "flex gap-2",
+ direction === "row"
+ ? "items-center justify-between gap-6"
+ : "flex-col gap-1.5",
+ className,
+ )}
+ >
+ {children}
+ </div>
+ );
+}
+
+export function PropertyItemLabel({
+ children,
+ className,
+}: {
+ children: React.ReactNode;
+ className?: string;
+}) {
+ return (
+ <span className={cn("text-muted-foreground text-xs", className)}>
+ {children}
+ </span>
+ );
+}
+
+export function PropertyItemValue({
+ children,
+ className,
+}: {
+ children: React.ReactNode;
+ className?: string;
+}) {
+ return <div className={cn("flex-1 text-sm", className)}>{children}</div>;
+}
+
+interface PropertyGroupProps {
+ title: string;
+ children: React.ReactNode;
+ defaultExpanded?: boolean;
+ className?: string;
+ titleClassName?: string;
+}
+
+export function PropertyGroup({
+ title,
+ children,
+ defaultExpanded = true,
+ className,
+ titleClassName,
+}: PropertyGroupProps) {
+ const [isExpanded, setIsExpanded] = useState(defaultExpanded);
+
+ return (
+ <PropertyItem direction="column" className={cn("gap-3", className)}>
+ <button
+ type="button"
+ className="flex items-center gap-1.5 cursor-pointer"
+ onClick={() => setIsExpanded(!isExpanded)}
+ >
+ <PropertyItemLabel className={cn(titleClassName)}>
+ {title}
+ </PropertyItemLabel>
+ <HugeiconsIcon
+ icon={ArrowDownIcon}
+ className={cn("size-3", !isExpanded && "-rotate-90")}
+ />
+ </button>
+ {isExpanded && <PropertyItemValue>{children}</PropertyItemValue>}
+ </PropertyItem>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/properties/text-properties.tsx b/apps/web/src/components/editor/panels/properties/text-properties.tsx
new file mode 100644
index 00000000..f8c9bf87
--- /dev/null
+++ b/apps/web/src/components/editor/panels/properties/text-properties.tsx
@@ -0,0 +1,410 @@
+import { Textarea } from "@/components/ui/textarea";
+import { FontPicker } from "@/components/ui/font-picker";
+import type { FontFamily } from "@/constants/font-constants";
+import type { TextElement } from "@/types/timeline";
+import { Slider } from "@/components/ui/slider";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { useState, useRef } from "react";
+import { PanelBaseView } from "@/components/editor/panels/panel-base-view";
+import {
+ TEXT_PROPERTIES_TABS,
+ isTextPropertiesTab,
+ useTextPropertiesStore,
+} from "@/stores/text-properties-store";
+import {
+ PropertyItem,
+ PropertyItemLabel,
+ PropertyItemValue,
+} from "./property-item";
+import { ColorPicker } from "@/components/ui/color-picker";
+import { cn } from "@/utils/ui";
+import { capitalizeFirstLetter, uppercase } from "@/utils/string";
+import { clamp } from "@/utils/math";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { LayoutGridIcon } from "@hugeicons/core-free-icons";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { useEditor } from "@/hooks/use-editor";
+import { DEFAULT_COLOR } from "@/constants/project-constants";
+
+export function TextProperties({
+ element,
+ trackId,
+}: {
+ element: TextElement;
+ trackId: string;
+}) {
+ const editor = useEditor();
+ const { activeTab, setActiveTab } = useTextPropertiesStore();
+ const containerRef = useRef<HTMLDivElement>(null);
+ const [fontSizeInput, setFontSizeInput] = useState(
+ element.fontSize.toString(),
+ );
+ const [opacityInput, setOpacityInput] = useState(
+ Math.round(element.opacity * 100).toString(),
+ );
+
+ const lastSelectedColor = useRef(DEFAULT_COLOR);
+
+ const handleFontSizeChange = ({ value }: { value: string }) => {
+ setFontSizeInput(value);
+
+ if (value.trim() !== "") {
+ const parsed = parseInt(value, 10);
+ const fontSize = Number.isNaN(parsed)
+ ? element.fontSize
+ : clamp({ value: parsed, min: 8, max: 300 });
+ editor.timeline.updateTextElement({
+ trackId,
+ elementId: element.id,
+ updates: { fontSize },
+ });
+ }
+ };
+
+ const handleFontSizeBlur = () => {
+ const parsed = parseInt(fontSizeInput, 10);
+ const fontSize = Number.isNaN(parsed)
+ ? element.fontSize
+ : clamp({ value: parsed, min: 8, max: 300 });
+ setFontSizeInput(fontSize.toString());
+ editor.timeline.updateTextElement({
+ trackId,
+ elementId: element.id,
+ updates: { fontSize },
+ });
+ };
+
+ const handleOpacityChange = ({ value }: { value: string }) => {
+ setOpacityInput(value);
+
+ if (value.trim() !== "") {
+ const parsed = parseInt(value, 10);
+ const opacityPercent = Number.isNaN(parsed)
+ ? Math.round(element.opacity * 100)
+ : clamp({ value: parsed, min: 0, max: 100 });
+ editor.timeline.updateTextElement({
+ trackId,
+ elementId: element.id,
+ updates: { opacity: opacityPercent / 100 },
+ });
+ }
+ };
+
+ const handleOpacityBlur = () => {
+ const parsed = parseInt(opacityInput, 10);
+ const opacityPercent = Number.isNaN(parsed)
+ ? Math.round(element.opacity * 100)
+ : clamp({ value: parsed, min: 0, max: 100 });
+ setOpacityInput(opacityPercent.toString());
+ editor.timeline.updateTextElement({
+ trackId,
+ elementId: element.id,
+ updates: { opacity: opacityPercent / 100 },
+ });
+ };
+
+ const handleColorChange = ({ color }: { color: string }) => {
+ if (color !== "transparent") {
+ lastSelectedColor.current = color;
+ }
+ editor.timeline.updateTextElement({
+ trackId,
+ elementId: element.id,
+ updates: { backgroundColor: color },
+ });
+ };
+
+ const handleTransparentToggle = ({
+ isTransparent,
+ }: {
+ isTransparent: boolean;
+ }) => {
+ const newColor = isTransparent ? "transparent" : lastSelectedColor.current;
+ editor.timeline.updateTextElement({
+ trackId,
+ elementId: element.id,
+ updates: { backgroundColor: newColor },
+ });
+ };
+
+ return (
+ <PanelBaseView
+ defaultTab="transform"
+ value={activeTab}
+ onValueChange={(v) => {
+ if (isTextPropertiesTab(v)) setActiveTab(v);
+ }}
+ ref={containerRef}
+ tabs={TEXT_PROPERTIES_TABS.map((t) => ({
+ value: t.value,
+ label: t.label,
+ content:
+ t.value === "transform" ? (
+ <div className="space-y-6"></div>
+ ) : (
+ <div className="space-y-6">
+ <Textarea
+ placeholder="Name"
+ defaultValue={element.content}
+ className="bg-panel-accent min-h-20"
+ onChange={(e) =>
+ editor.timeline.updateTextElement({
+ trackId,
+ elementId: element.id,
+ updates: { content: e.target.value },
+ })
+ }
+ />
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Font</PropertyItemLabel>
+ <PropertyItemValue>
+ <FontPicker
+ defaultValue={element.fontFamily}
+ onValueChange={(value: FontFamily) =>
+ editor.timeline.updateTextElement({
+ trackId,
+ elementId: element.id,
+ updates: { fontFamily: value },
+ })
+ }
+ />
+ </PropertyItemValue>
+ </PropertyItem>
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Style</PropertyItemLabel>
+ <PropertyItemValue>
+ <div className="flex items-center gap-2">
+ <Button
+ variant={
+ element.fontWeight === "bold" ? "default" : "outline"
+ }
+ size="sm"
+ onClick={() =>
+ editor.timeline.updateTextElement({
+ trackId,
+ elementId: element.id,
+ updates: {
+ fontWeight:
+ element.fontWeight === "bold" ? "normal" : "bold",
+ },
+ })
+ }
+ className="h-8 px-3 font-bold"
+ >
+ B
+ </Button>
+ <Button
+ variant={
+ element.fontStyle === "italic" ? "default" : "outline"
+ }
+ size="sm"
+ onClick={() =>
+ editor.timeline.updateTextElement({
+ trackId,
+ elementId: element.id,
+ updates: {
+ fontStyle:
+ element.fontStyle === "italic"
+ ? "normal"
+ : "italic",
+ },
+ })
+ }
+ className="h-8 px-3 italic"
+ >
+ I
+ </Button>
+ <Button
+ variant={
+ element.textDecoration === "underline"
+ ? "default"
+ : "outline"
+ }
+ size="sm"
+ onClick={() =>
+ editor.timeline.updateTextElement({
+ trackId,
+ elementId: element.id,
+ updates: {
+ textDecoration:
+ element.textDecoration === "underline"
+ ? "none"
+ : "underline",
+ },
+ })
+ }
+ className="h-8 px-3 underline"
+ >
+ U
+ </Button>
+ <Button
+ variant={
+ element.textDecoration === "line-through"
+ ? "default"
+ : "outline"
+ }
+ size="sm"
+ onClick={() =>
+ editor.timeline.updateTextElement({
+ trackId,
+ elementId: element.id,
+ updates: {
+ textDecoration:
+ element.textDecoration === "line-through"
+ ? "none"
+ : "line-through",
+ },
+ })
+ }
+ className="h-8 px-3 line-through"
+ >
+ S
+ </Button>
+ </div>
+ </PropertyItemValue>
+ </PropertyItem>
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Font size</PropertyItemLabel>
+ <PropertyItemValue>
+ <div className="flex items-center gap-2">
+ <Slider
+ value={[element.fontSize]}
+ min={8}
+ max={300}
+ step={1}
+ onValueChange={([value]) => {
+ editor.timeline.updateTextElement({
+ trackId,
+ elementId: element.id,
+ updates: { fontSize: value },
+ });
+ setFontSizeInput(value.toString());
+ }}
+ className="w-full"
+ />
+ <Input
+ type="number"
+ value={fontSizeInput}
+ min={8}
+ max={300}
+ onChange={(e) =>
+ handleFontSizeChange({ value: e.target.value })
+ }
+ onBlur={handleFontSizeBlur}
+ className="bg-panel-accent h-7 w-12 [appearance:textfield] rounded-sm px-2 text-center !text-xs [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
+ />
+ </div>
+ </PropertyItemValue>
+ </PropertyItem>
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Color</PropertyItemLabel>
+ <PropertyItemValue>
+ <ColorPicker
+ value={uppercase({
+ string: (element.color || "FFFFFF").replace("#", ""),
+ })}
+ onChange={(color) => {
+ editor.timeline.updateTextElement({
+ trackId,
+ elementId: element.id,
+ updates: { color: `#${color}` },
+ });
+ }}
+ containerRef={containerRef}
+ />
+ </PropertyItemValue>
+ </PropertyItem>
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Opacity</PropertyItemLabel>
+ <PropertyItemValue>
+ <div className="flex items-center gap-2">
+ <Slider
+ value={[element.opacity * 100]}
+ min={0}
+ max={100}
+ step={1}
+ onValueChange={([value]) => {
+ editor.timeline.updateTextElement({
+ trackId,
+ elementId: element.id,
+ updates: { opacity: value / 100 },
+ });
+ setOpacityInput(value.toString());
+ }}
+ className="w-full"
+ />
+ <Input
+ type="number"
+ value={opacityInput}
+ min={0}
+ max={100}
+ onChange={(e) =>
+ handleOpacityChange({ value: e.target.value })
+ }
+ onBlur={handleOpacityBlur}
+ className="bg-panel-accent h-7 w-12 [appearance:textfield] rounded-sm text-center !text-xs [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
+ />
+ </div>
+ </PropertyItemValue>
+ </PropertyItem>
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Background</PropertyItemLabel>
+ <PropertyItemValue>
+ <div className="flex items-center gap-2">
+ <ColorPicker
+ value={capitalizeFirstLetter({
+ string:
+ element.backgroundColor === "transparent"
+ ? lastSelectedColor.current.replace("#", "")
+ : element.backgroundColor.replace("#", ""),
+ })}
+ onChange={(color) =>
+ handleColorChange({ color: `#${color}` })
+ }
+ containerRef={containerRef}
+ className={
+ element.backgroundColor === "transparent"
+ ? "pointer-events-none opacity-50"
+ : ""
+ }
+ />
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button
+ variant="outline"
+ size="icon"
+ onClick={() =>
+ handleTransparentToggle({
+ isTransparent:
+ element.backgroundColor !== "transparent",
+ })
+ }
+ className="bg-panel-accent size-9 overflow-hidden rounded-full p-0"
+ >
+ <HugeiconsIcon
+ icon={LayoutGridIcon}
+ className={cn(
+ "text-foreground",
+ element.backgroundColor === "transparent" &&
+ "text-primary",
+ )}
+ />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Transparent background</TooltipContent>
+ </Tooltip>
+ </div>
+ </PropertyItemValue>
+ </PropertyItem>
+ </div>
+ ),
+ }))}
+ />
+ );
+}
diff --git a/apps/web/src/components/editor/panels/properties/video-properties.tsx b/apps/web/src/components/editor/panels/properties/video-properties.tsx
new file mode 100644
index 00000000..bf6d92d8
--- /dev/null
+++ b/apps/web/src/components/editor/panels/properties/video-properties.tsx
@@ -0,0 +1,9 @@
+import type { ImageElement, VideoElement } from "@/types/timeline";
+
+export function VideoProperties({
+ _element,
+}: {
+ _element: VideoElement | ImageElement;
+}) {
+ return <div className="space-y-4 p-5">Video properties</div>;
+}
diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx
deleted file mode 100644
index 36f6a2ba..00000000
--- a/apps/web/src/components/editor/preview-panel.tsx
+++ /dev/null
@@ -1,1134 +0,0 @@
-"use client";
-
-import { useTimelineStore } from "@/stores/timeline-store";
-import { TimelineElement, TimelineTrack } from "@/types/timeline";
-import { useMediaStore } from "@/stores/media-store";
-import { MediaFile } from "@/types/media";
-import { usePlaybackStore } from "@/stores/playback-store";
-import { useEditorStore } from "@/stores/editor-store";
-import { Button } from "@/components/ui/button";
-import { Play, Pause, Expand, SkipBack, SkipForward } from "lucide-react";
-import { useState, useRef, useEffect, useCallback } from "react";
-import { renderTimelineFrame } from "@/lib/timeline-renderer";
-import { cn } from "@/lib/utils";
-import { formatTimeCode } from "@/lib/time";
-import { EditableTimecode } from "@/components/ui/editable-timecode";
-import { useFrameCache } from "@/hooks/use-frame-cache";
-import { useSceneStore } from "@/stores/scene-store";
-import {
- DEFAULT_CANVAS_SIZE,
- DEFAULT_FPS,
- useProjectStore,
-} from "@/stores/project-store";
-import { TextElementDragState } from "@/types/editor";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { Checkbox } from "@/components/ui/checkbox";
-import { LayoutGuideOverlay } from "./layout-guide-overlay";
-import { Label } from "../ui/label";
-import { SocialsIcon } from "../icons";
-import { PLATFORM_LAYOUTS, type PlatformLayout } from "@/stores/editor-store";
-
-interface ActiveElement {
- element: TimelineElement;
- track: TimelineTrack;
- mediaItem: MediaFile | null;
-}
-
-export function PreviewPanel() {
- const { tracks, getTotalDuration, updateTextElement } = useTimelineStore();
- const { mediaFiles } = useMediaStore();
- const { currentTime, toggle, setCurrentTime } = usePlaybackStore();
- const { isPlaying, volume, muted } = usePlaybackStore();
- const { activeProject } = useProjectStore();
- const { currentScene } = useSceneStore();
- const previewRef = useRef<HTMLDivElement>(null);
- const canvasRef = useRef<HTMLCanvasElement>(null);
- const { getCachedFrame, cacheFrame, invalidateCache, preRenderNearbyFrames } =
- useFrameCache();
- const lastFrameTimeRef = useRef(0);
- const renderSeqRef = useRef(0);
- const offscreenCanvasRef = useRef<OffscreenCanvas | HTMLCanvasElement | null>(
- null
- );
-
- const audioContextRef = useRef<AudioContext | null>(null);
- const audioGainRef = useRef<GainNode | null>(null);
- const audioBuffersRef = useRef<Map<string, AudioBuffer>>(new Map());
- const playingSourcesRef = useRef<Set<AudioBufferSourceNode>>(new Set());
- const containerRef = useRef<HTMLDivElement>(null);
- const [previewDimensions, setPreviewDimensions] = useState({
- width: 0,
- height: 0,
- });
- const [isExpanded, setIsExpanded] = useState(false);
-
- const canvasSize = activeProject?.canvasSize || DEFAULT_CANVAS_SIZE;
- const [dragState, setDragState] = useState<TextElementDragState>({
- isDragging: false,
- elementId: null,
- trackId: null,
- startX: 0,
- startY: 0,
- initialElementX: 0,
- initialElementY: 0,
- currentX: 0,
- currentY: 0,
- elementWidth: 0,
- elementHeight: 0,
- });
-
- useEffect(() => {
- const updatePreviewSize = () => {
- if (!containerRef.current) return;
-
- let availableWidth, availableHeight;
-
- if (isExpanded) {
- const controlsHeight = 80;
- const marginSpace = 24;
- availableWidth = window.innerWidth - marginSpace;
- availableHeight = window.innerHeight - controlsHeight - marginSpace;
- } else {
- const container = containerRef.current.getBoundingClientRect();
- const computedStyle = getComputedStyle(containerRef.current);
- const paddingTop = parseFloat(computedStyle.paddingTop);
- const paddingBottom = parseFloat(computedStyle.paddingBottom);
- const paddingLeft = parseFloat(computedStyle.paddingLeft);
- const paddingRight = parseFloat(computedStyle.paddingRight);
- const gap = parseFloat(computedStyle.gap) || 16;
- const toolbar = containerRef.current.querySelector("[data-toolbar]");
- const toolbarHeight = toolbar
- ? toolbar.getBoundingClientRect().height
- : 0;
-
- availableWidth = container.width - paddingLeft - paddingRight;
- availableHeight =
- container.height -
- paddingTop -
- paddingBottom -
- toolbarHeight -
- (toolbarHeight > 0 ? gap : 0);
- }
-
- const targetRatio = canvasSize.width / canvasSize.height;
- const containerRatio = availableWidth / availableHeight;
- let width, height;
-
- if (containerRatio > targetRatio) {
- height = availableHeight * (isExpanded ? 0.95 : 1);
- width = height * targetRatio;
- } else {
- width = availableWidth * (isExpanded ? 0.95 : 1);
- height = width / targetRatio;
- }
-
- setPreviewDimensions({ width, height });
- };
-
- updatePreviewSize();
- const resizeObserver = new ResizeObserver(updatePreviewSize);
- if (containerRef.current) {
- resizeObserver.observe(containerRef.current);
- }
- if (isExpanded) {
- window.addEventListener("resize", updatePreviewSize);
- }
-
- return () => {
- resizeObserver.disconnect();
- if (isExpanded) {
- window.removeEventListener("resize", updatePreviewSize);
- }
- };
- }, [canvasSize.width, canvasSize.height, isExpanded]);
-
- useEffect(() => {
- const handleEscapeKey = (event: KeyboardEvent) => {
- if (event.key === "Escape" && isExpanded) {
- setIsExpanded(false);
- }
- };
-
- if (isExpanded) {
- document.addEventListener("keydown", handleEscapeKey);
- document.body.style.overflow = "hidden";
- } else {
- document.body.style.overflow = "";
- }
-
- return () => {
- document.removeEventListener("keydown", handleEscapeKey);
- document.body.style.overflow = "";
- };
- }, [isExpanded]);
-
- useEffect(() => {
- const handleMouseMove = (e: MouseEvent) => {
- if (!dragState.isDragging) return;
-
- const deltaX = e.clientX - dragState.startX;
- const deltaY = e.clientY - dragState.startY;
-
- const scaleRatio = previewDimensions.width / canvasSize.width;
- const newX = dragState.initialElementX + deltaX / scaleRatio;
- const newY = dragState.initialElementY + deltaY / scaleRatio;
-
- const halfWidth = dragState.elementWidth / scaleRatio / 2;
- const halfHeight = dragState.elementHeight / scaleRatio / 2;
-
- const constrainedX = Math.max(
- -canvasSize.width / 2 + halfWidth,
- Math.min(canvasSize.width / 2 - halfWidth, newX)
- );
- const constrainedY = Math.max(
- -canvasSize.height / 2 + halfHeight,
- Math.min(canvasSize.height / 2 - halfHeight, newY)
- );
-
- setDragState((prev) => ({
- ...prev,
- currentX: constrainedX,
- currentY: constrainedY,
- }));
- };
-
- const handleMouseUp = () => {
- if (dragState.isDragging && dragState.trackId && dragState.elementId) {
- updateTextElement(dragState.trackId, dragState.elementId, {
- x: dragState.currentX,
- y: dragState.currentY,
- });
- }
- setDragState((prev) => ({ ...prev, isDragging: false }));
- };
-
- if (dragState.isDragging) {
- document.addEventListener("mousemove", handleMouseMove);
- document.addEventListener("mouseup", handleMouseUp);
- document.body.style.cursor = "grabbing";
- document.body.style.userSelect = "none";
- }
-
- return () => {
- document.removeEventListener("mousemove", handleMouseMove);
- document.removeEventListener("mouseup", handleMouseUp);
- document.body.style.cursor = "";
- document.body.style.userSelect = "";
- };
- }, [dragState, previewDimensions, canvasSize, updateTextElement]);
-
- // Clear the frame cache when background settings change since they affect rendering
- useEffect(() => {
- invalidateCache();
- }, [
- mediaFiles,
- activeProject?.backgroundColor,
- activeProject?.backgroundType,
- invalidateCache,
- ]);
-
- const handleTextMouseDown = (
- e: React.MouseEvent<HTMLDivElement>,
- element: any,
- trackId: string
- ) => {
- e.preventDefault();
- e.stopPropagation();
-
- const rect = e.currentTarget.getBoundingClientRect();
-
- setDragState({
- isDragging: true,
- elementId: element.id,
- trackId,
- startX: e.clientX,
- startY: e.clientY,
- initialElementX: element.x,
- initialElementY: element.y,
- currentX: element.x,
- currentY: element.y,
- elementWidth: rect.width,
- elementHeight: rect.height,
- });
- };
-
- const toggleExpanded = useCallback(() => {
- setIsExpanded((prev) => !prev);
- }, []);
-
- const hasAnyElements = tracks.some((track) => track.elements.length > 0);
- const shouldRenderPreview = hasAnyElements || activeProject?.backgroundType;
- const getActiveElements = (): ActiveElement[] => {
- const activeElements: ActiveElement[] = [];
-
- // Iterate tracks from bottom to top so topmost track renders last (on top)
- [...tracks].reverse().forEach((track) => {
- track.elements.forEach((element) => {
- if (element.hidden) return;
- const elementStart = element.startTime;
- const elementEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
-
- if (currentTime >= elementStart && currentTime < elementEnd) {
- let mediaItem = null;
- if (element.type === "media") {
- mediaItem =
- element.mediaId === "test"
- ? null
- : mediaFiles.find((item) => item.id === element.mediaId) ||
- null;
- }
- activeElements.push({ element, track, mediaItem });
- }
- });
- });
-
- return activeElements;
- };
-
- const activeElements = getActiveElements();
-
- // Ensure first frame after mount/seek renders immediately
- useEffect(() => {
- const onSeek = () => {
- lastFrameTimeRef.current = -Infinity;
- renderSeqRef.current++;
- };
- window.addEventListener("playback-seek", onSeek as EventListener);
- lastFrameTimeRef.current = -Infinity;
- return () => {
- window.removeEventListener("playback-seek", onSeek as EventListener);
- };
- }, []);
-
- // Web Audio: schedule only on play/pause/seek/volume/mute changes
- useEffect(() => {
- const stopAll = () => {
- for (const src of playingSourcesRef.current) {
- try {
- src.stop();
- } catch {}
- }
- playingSourcesRef.current.clear();
- };
-
- type WebAudioWindow = Window & {
- AudioContext?: typeof AudioContext;
- webkitAudioContext?: typeof AudioContext;
- };
- const ensureAudioGraph = async () => {
- if (!audioContextRef.current) {
- const win = window as WebAudioWindow;
- const Ctx = win.AudioContext ?? win.webkitAudioContext;
- if (!Ctx) return;
- audioContextRef.current = new Ctx();
- }
- if (!audioGainRef.current) {
- audioGainRef.current = audioContextRef.current!.createGain();
- audioGainRef.current.connect(audioContextRef.current!.destination);
- }
- if (audioContextRef.current!.state === "suspended") {
- try {
- await audioContextRef.current!.resume();
- } catch {}
- }
- const gainValue = muted ? 0 : Math.max(0, Math.min(1, volume));
- audioGainRef.current!.gain.setValueAtTime(
- gainValue,
- audioContextRef.current!.currentTime
- );
- };
-
- const scheduleNow = async () => {
- await ensureAudioGraph();
- const audioCtx = audioContextRef.current!;
- const gain = audioGainRef.current!;
-
- const tracksSnapshot = useTimelineStore.getState().tracks;
- const mediaList = mediaFiles;
- const idToMedia = new Map(mediaList.map((m) => [m.id, m] as const));
- const playbackNow = usePlaybackStore.getState().currentTime;
-
- const audible: Array<{
- id: string;
- elementStart: number;
- trimStart: number;
- trimEnd: number;
- duration: number;
- muted: boolean;
- trackMuted: boolean;
- }> = [];
- const uniqueIds = new Set<string>();
- for (const track of tracksSnapshot) {
- for (const element of track.elements) {
- if (element.type !== "media") continue;
- const media = idToMedia.get(element.mediaId);
- if (!media || media.type !== "audio") continue;
- const visibleDuration =
- element.duration - element.trimStart - element.trimEnd;
- if (visibleDuration <= 0) continue;
- const localTime = playbackNow - element.startTime + element.trimStart;
- if (localTime < 0 || localTime >= visibleDuration) continue;
- audible.push({
- id: media.id,
- elementStart: element.startTime,
- trimStart: element.trimStart,
- trimEnd: element.trimEnd,
- duration: element.duration,
- muted: !!element.muted,
- trackMuted: !!track.muted,
- });
- uniqueIds.add(media.id);
- }
- }
-
- if (audible.length === 0) return;
-
- // Decode buffers as needed
- const decodePromises: Array<Promise<void>> = [];
- for (const id of uniqueIds) {
- if (!audioBuffersRef.current.has(id)) {
- const mediaItem = idToMedia.get(id);
- if (!mediaItem) continue;
- const p = (async () => {
- const arr = await mediaItem.file.arrayBuffer();
- const buf = await audioCtx.decodeAudioData(arr.slice(0));
- audioBuffersRef.current.set(id, buf);
- })();
- decodePromises.push(p);
- }
- }
- await Promise.all(decodePromises);
-
- const startAt = audioCtx.currentTime + 0.02;
- for (const entry of audible) {
- if (entry.muted || entry.trackMuted) continue;
- const buffer = audioBuffersRef.current.get(entry.id);
- if (!buffer) continue;
- const visibleDuration =
- entry.duration - entry.trimStart - entry.trimEnd;
- const localTime = Math.max(
- 0,
- playbackNow - entry.elementStart + entry.trimStart
- );
- const playDuration = Math.max(0, visibleDuration - localTime);
- if (playDuration <= 0) continue;
- const src = audioCtx.createBufferSource();
- src.buffer = buffer;
- src.connect(gain);
- try {
- src.start(startAt, localTime, playDuration);
- playingSourcesRef.current.add(src);
- } catch {}
- }
- };
-
- const onSeek = () => {
- if (!isPlaying) return;
- for (const src of playingSourcesRef.current) {
- try {
- src.stop();
- } catch {}
- }
- playingSourcesRef.current.clear();
- void scheduleNow();
- };
-
- // Apply volume/mute changes immediately
- void ensureAudioGraph();
-
- // Start/stop on play state changes
- for (const src of playingSourcesRef.current) {
- try {
- src.stop();
- } catch {}
- }
- playingSourcesRef.current.clear();
- if (isPlaying) {
- void scheduleNow();
- }
-
- window.addEventListener("playback-seek", onSeek as EventListener);
- return () => {
- window.removeEventListener("playback-seek", onSeek as EventListener);
- for (const src of playingSourcesRef.current) {
- try {
- src.stop();
- } catch {}
- }
- playingSourcesRef.current.clear();
- };
- }, [isPlaying, volume, muted, mediaFiles]);
-
- // Canvas: draw current frame with caching
- useEffect(() => {
- const draw = async () => {
- const canvas = canvasRef.current;
- if (!canvas) return;
- const mainCtx = canvas.getContext("2d");
- if (!mainCtx) return;
-
- // Set canvas internal resolution to avoid blurry scaling
- const displayWidth = Math.max(1, Math.floor(previewDimensions.width));
- const displayHeight = Math.max(1, Math.floor(previewDimensions.height));
- if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
- canvas.width = displayWidth;
- canvas.height = displayHeight;
- }
-
- // Throttle rendering to project FPS during playback only
- const fps = activeProject?.fps || DEFAULT_FPS;
- const minDelta = 1 / fps;
- if (isPlaying) {
- if (currentTime - lastFrameTimeRef.current < minDelta) {
- return;
- }
- lastFrameTimeRef.current = currentTime;
- }
-
- const cachedFrame = getCachedFrame(
- currentTime,
- tracks,
- mediaFiles,
- activeProject,
- currentScene?.id
- );
- if (cachedFrame) {
- mainCtx.putImageData(cachedFrame, 0, 0);
-
- // Pre-render nearby frames in background
- if (!isPlaying) {
- // Only during scrubbing to avoid interfering with playback
- preRenderNearbyFrames(
- currentTime,
- tracks,
- mediaFiles,
- activeProject,
- async (time: number) => {
- const tempCanvas = document.createElement("canvas");
- tempCanvas.width = displayWidth;
- tempCanvas.height = displayHeight;
- const tempCtx = tempCanvas.getContext("2d");
- if (!tempCtx)
- throw new Error("Failed to create temp canvas context");
-
- await renderTimelineFrame({
- ctx: tempCtx,
- time,
- canvasWidth: displayWidth,
- canvasHeight: displayHeight,
- tracks,
- mediaFiles,
- backgroundType: activeProject?.backgroundType,
- blurIntensity: activeProject?.blurIntensity,
- backgroundColor:
- activeProject?.backgroundType === "blur"
- ? undefined
- : activeProject?.backgroundColor || "#000000",
- projectCanvasSize: canvasSize,
- });
-
- return tempCtx.getImageData(0, 0, displayWidth, displayHeight);
- },
- currentScene?.id,
- 3
- );
- } else {
- // Small lookahead while playing
- preRenderNearbyFrames(
- currentTime,
- tracks,
- mediaFiles,
- activeProject,
- async (time: number) => {
- const tempCanvas = document.createElement("canvas");
- tempCanvas.width = displayWidth;
- tempCanvas.height = displayHeight;
- const tempCtx = tempCanvas.getContext("2d");
- if (!tempCtx)
- throw new Error("Failed to create temp canvas context");
-
- await renderTimelineFrame({
- ctx: tempCtx,
- time,
- canvasWidth: displayWidth,
- canvasHeight: displayHeight,
- tracks,
- mediaFiles,
- backgroundType: activeProject?.backgroundType,
- blurIntensity: activeProject?.blurIntensity,
- backgroundColor:
- activeProject?.backgroundType === "blur"
- ? undefined
- : activeProject?.backgroundColor || "#000000",
- projectCanvasSize: canvasSize,
- });
-
- return tempCtx.getImageData(0, 0, displayWidth, displayHeight);
- },
- currentScene?.id,
- 1
- );
- }
- return;
- }
-
- // Cache miss - render from scratch
- if (!offscreenCanvasRef.current) {
- const hasOffscreen =
- typeof (globalThis as unknown as { OffscreenCanvas?: unknown })
- .OffscreenCanvas !== "undefined";
- if (hasOffscreen) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- offscreenCanvasRef.current = new (globalThis as any).OffscreenCanvas(
- displayWidth,
- displayHeight
- ) as OffscreenCanvas;
- } else {
- const c = document.createElement("canvas");
- c.width = displayWidth;
- c.height = displayHeight;
- offscreenCanvasRef.current = c;
- }
- }
- // Ensure size matches
- if (
- offscreenCanvasRef.current &&
- (offscreenCanvasRef.current as HTMLCanvasElement).getContext
- ) {
- const c = offscreenCanvasRef.current as HTMLCanvasElement;
- if (c.width !== displayWidth || c.height !== displayHeight) {
- c.width = displayWidth;
- c.height = displayHeight;
- }
- } else {
- const c = offscreenCanvasRef.current as OffscreenCanvas;
- // @ts-ignore width/height exist on OffscreenCanvas in modern browsers
- if (
- (c as unknown as { width: number }).width !== displayWidth ||
- (c as unknown as { height: number }).height !== displayHeight
- ) {
- // @ts-ignore
- (c as unknown as { width: number }).width = displayWidth;
- // @ts-ignore
- (c as unknown as { height: number }).height = displayHeight;
- }
- }
- const offscreenCanvas = offscreenCanvasRef.current as
- | HTMLCanvasElement
- | OffscreenCanvas;
- const offCtx = (offscreenCanvas as HTMLCanvasElement).getContext
- ? (offscreenCanvas as HTMLCanvasElement).getContext("2d")
- : (offscreenCanvas as OffscreenCanvas).getContext("2d");
- if (!offCtx) return;
-
- await renderTimelineFrame({
- ctx: offCtx as CanvasRenderingContext2D,
- time: currentTime,
- canvasWidth: displayWidth,
- canvasHeight: displayHeight,
- tracks,
- mediaFiles,
- backgroundType: activeProject?.backgroundType,
- blurIntensity: activeProject?.blurIntensity,
- backgroundColor:
- activeProject?.backgroundType === "blur"
- ? undefined
- : activeProject?.backgroundColor || "#000000",
- projectCanvasSize: canvasSize,
- });
-
- const imageData = (offCtx as CanvasRenderingContext2D).getImageData(
- 0,
- 0,
- displayWidth,
- displayHeight
- );
- cacheFrame(
- currentTime,
- imageData,
- tracks,
- mediaFiles,
- activeProject,
- currentScene?.id
- );
-
- // Blit offscreen to visible canvas
- mainCtx.clearRect(0, 0, displayWidth, displayHeight);
- if ((offscreenCanvas as HTMLCanvasElement).getContext) {
- mainCtx.drawImage(offscreenCanvas as HTMLCanvasElement, 0, 0);
- } else {
- mainCtx.drawImage(
- offscreenCanvas as unknown as CanvasImageSource,
- 0,
- 0
- );
- }
- };
-
- void draw();
- }, [
- activeElements,
- currentTime,
- previewDimensions.width,
- previewDimensions.height,
- canvasSize.width,
- canvasSize.height,
- activeProject?.backgroundType,
- activeProject?.backgroundColor,
- getCachedFrame,
- cacheFrame,
- preRenderNearbyFrames,
- isPlaying,
- ]);
-
- // Get media elements for blur background (video/image only)
- const getBlurBackgroundElements = (): ActiveElement[] => {
- return activeElements.filter(
- ({ element, mediaItem }) =>
- element.type === "media" &&
- mediaItem &&
- (mediaItem.type === "video" || mediaItem.type === "image") &&
- element.mediaId !== "test" // Exclude test elements
- );
- };
-
- const blurBackgroundElements = getBlurBackgroundElements();
-
- // Render blur background layer (handled by canvas now)
- const renderBlurBackground = () => null;
-
- // Render an element (canvas handles visuals now). Audio playback to be implemented via Web Audio.
- const renderElement = (_elementData: ActiveElement) => null;
-
- return (
- <>
- <div className="h-full w-full flex flex-col min-h-0 min-w-0 bg-panel rounded-sm relative">
- <div
- ref={containerRef}
- className="flex-1 flex flex-col items-center justify-center min-h-0 min-w-0"
- >
- <div className="flex-1" />
- {shouldRenderPreview ? (
- <div
- ref={previewRef}
- className="relative overflow-hidden border"
- style={{
- width: previewDimensions.width,
- height: previewDimensions.height,
- background:
- activeProject?.backgroundType === "blur"
- ? "transparent"
- : activeProject?.backgroundColor || "#000000",
- }}
- >
- {renderBlurBackground()}
- <canvas
- ref={canvasRef}
- style={{
- position: "absolute",
- left: 0,
- top: 0,
- width: previewDimensions.width,
- height: previewDimensions.height,
- }}
- aria-label="Video preview canvas"
- />
- {activeElements.length === 0 ? (
- <></>
- ) : (
- activeElements.map((elementData) => renderElement(elementData))
- )}
- <LayoutGuideOverlay />
- </div>
- ) : null}
-
- <div className="flex-1" />
-
- <PreviewToolbar
- hasAnyElements={hasAnyElements}
- onToggleExpanded={toggleExpanded}
- isExpanded={isExpanded}
- currentTime={currentTime}
- setCurrentTime={setCurrentTime}
- toggle={toggle}
- getTotalDuration={getTotalDuration}
- />
- </div>
- </div>
-
- {isExpanded && (
- <FullscreenPreview
- previewDimensions={previewDimensions}
- activeProject={activeProject}
- renderBlurBackground={renderBlurBackground}
- activeElements={activeElements}
- renderElement={renderElement}
- blurBackgroundElements={blurBackgroundElements}
- hasAnyElements={hasAnyElements}
- toggleExpanded={toggleExpanded}
- currentTime={currentTime}
- setCurrentTime={setCurrentTime}
- toggle={toggle}
- getTotalDuration={getTotalDuration}
- />
- )}
- </>
- );
-}
-
-function FullscreenToolbar({
- hasAnyElements,
- onToggleExpanded,
- currentTime,
- setCurrentTime,
- toggle,
- getTotalDuration,
-}: {
- hasAnyElements: boolean;
- onToggleExpanded: () => void;
- currentTime: number;
- setCurrentTime: (time: number) => void;
- toggle: () => void;
- getTotalDuration: () => number;
-}) {
- const { isPlaying, seek } = usePlaybackStore();
- const { activeProject } = useProjectStore();
- const [isDragging, setIsDragging] = useState(false);
-
- const totalDuration = getTotalDuration();
- const progress = totalDuration > 0 ? (currentTime / totalDuration) * 100 : 0;
-
- const handleTimelineClick = (e: React.MouseEvent<HTMLDivElement>) => {
- if (!hasAnyElements) return;
- const rect = e.currentTarget.getBoundingClientRect();
- const clickX = e.clientX - rect.left;
- const percentage = Math.max(0, Math.min(1, clickX / rect.width));
- const newTime = percentage * totalDuration;
- setCurrentTime(Math.max(0, Math.min(newTime, totalDuration)));
- };
-
- const handleTimelineDrag = (e: React.MouseEvent<HTMLDivElement>) => {
- if (!hasAnyElements) return;
- e.preventDefault();
- e.stopPropagation();
- const rect = e.currentTarget.getBoundingClientRect();
- setIsDragging(true);
-
- const handleMouseMove = (moveEvent: MouseEvent) => {
- moveEvent.preventDefault();
- const dragX = moveEvent.clientX - rect.left;
- const percentage = Math.max(0, Math.min(1, dragX / rect.width));
- const newTime = percentage * totalDuration;
- setCurrentTime(Math.max(0, Math.min(newTime, totalDuration)));
- };
-
- const handleMouseUp = () => {
- setIsDragging(false);
- document.removeEventListener("mousemove", handleMouseMove);
- document.removeEventListener("mouseup", handleMouseUp);
- document.body.style.userSelect = "";
- };
-
- document.body.style.userSelect = "none";
- document.addEventListener("mousemove", handleMouseMove);
- document.addEventListener("mouseup", handleMouseUp);
- handleMouseMove(e.nativeEvent);
- };
-
- const skipBackward = () => {
- const newTime = Math.max(0, currentTime - 1);
- setCurrentTime(newTime);
- };
-
- const skipForward = () => {
- const newTime = Math.min(totalDuration, currentTime + 1);
- setCurrentTime(newTime);
- };
-
- return (
- <div
- data-toolbar
- className="flex items-center gap-2 p-1 pt-2 w-full text-foreground relative"
- >
- <div className="flex items-center gap-1 text-[0.70rem] tabular-nums text-foreground/90">
- <EditableTimecode
- time={currentTime}
- duration={totalDuration}
- format="HH:MM:SS:FF"
- fps={activeProject?.fps || DEFAULT_FPS}
- onTimeChange={seek}
- disabled={!hasAnyElements}
- className="text-foreground/90 hover:bg-white/10"
- />
- <span className="opacity-50">/</span>
- <span>
- {formatTimeCode(
- totalDuration,
- "HH:MM:SS:FF",
- activeProject?.fps || DEFAULT_FPS
- )}
- </span>
- </div>
-
- <div className="flex items-center gap-1">
- <Button
- variant="text"
- size="icon"
- onClick={skipBackward}
- disabled={!hasAnyElements}
- className="h-auto p-0 text-foreground"
- title="Skip backward 1s"
- >
- <SkipBack className="h-3 w-3" />
- </Button>
- <Button
- variant="text"
- size="icon"
- onClick={toggle}
- disabled={!hasAnyElements}
- className="h-auto p-0 text-foreground hover:text-foreground/80"
- >
- {isPlaying ? (
- <Pause className="h-3 w-3" />
- ) : (
- <Play className="h-3 w-3" />
- )}
- </Button>
- <Button
- variant="text"
- size="icon"
- onClick={skipForward}
- disabled={!hasAnyElements}
- className="h-auto p-0 text-foreground hover:text-foreground/80"
- title="Skip forward 1s"
- >
- <SkipForward className="h-3 w-3" />
- </Button>
- </div>
-
- <div className="flex-1 flex items-center gap-2">
- <div
- className={cn(
- "relative h-1 rounded-full cursor-pointer flex-1 bg-foreground/20",
- !hasAnyElements && "opacity-50 cursor-not-allowed"
- )}
- onClick={hasAnyElements ? handleTimelineClick : undefined}
- onMouseDown={hasAnyElements ? handleTimelineDrag : undefined}
- style={{ userSelect: "none" }}
- >
- <div
- className={cn(
- "absolute top-0 left-0 h-full rounded-full bg-foreground",
- !isDragging && "duration-100"
- )}
- style={{ width: `${progress}%` }}
- />
- <div
- className="absolute top-1/2 w-3 h-3 rounded-full -translate-y-1/2 -translate-x-1/2 shadow-xs bg-foreground border border-black/20"
- style={{ left: `${progress}%` }}
- />
- </div>
- </div>
-
- <Button
- variant="text"
- size="icon"
- className="size-4! text-foreground/80 hover:text-foreground"
- onClick={onToggleExpanded}
- title="Exit fullscreen (Esc)"
- >
- <Expand className="size-4!" />
- </Button>
- </div>
- );
-}
-
-function FullscreenPreview({
- previewDimensions,
- activeProject,
- renderBlurBackground,
- activeElements,
- renderElement,
- blurBackgroundElements,
- hasAnyElements,
- toggleExpanded,
- currentTime,
- setCurrentTime,
- toggle,
- getTotalDuration,
-}: {
- previewDimensions: { width: number; height: number };
- activeProject: any;
- renderBlurBackground: () => React.ReactNode;
- activeElements: ActiveElement[];
- renderElement: (elementData: ActiveElement, index: number) => React.ReactNode;
- blurBackgroundElements: ActiveElement[];
- hasAnyElements: boolean;
- toggleExpanded: () => void;
- currentTime: number;
- setCurrentTime: (time: number) => void;
- toggle: () => void;
- getTotalDuration: () => number;
-}) {
- return (
- <div className="fixed inset-0 z-9999 flex flex-col">
- <div className="flex-1 flex items-center justify-center bg-background">
- <div
- className="relative overflow-hidden border border-border m-3"
- style={{
- width: previewDimensions.width,
- height: previewDimensions.height,
- background:
- activeProject?.backgroundType === "blur"
- ? "#1a1a1a"
- : activeProject?.backgroundColor || "#1a1a1a",
- }}
- >
- {renderBlurBackground()}
- {activeElements.length === 0 ? (
- <div className="absolute inset-0 flex items-center justify-center text-white/60">
- No elements at current time
- </div>
- ) : (
- activeElements.map((elementData, index) =>
- renderElement(elementData, index)
- )
- )}
- <LayoutGuideOverlay />
- </div>
- </div>
- <div className="p-4 bg-background">
- <FullscreenToolbar
- hasAnyElements={hasAnyElements}
- onToggleExpanded={toggleExpanded}
- currentTime={currentTime}
- setCurrentTime={setCurrentTime}
- toggle={toggle}
- getTotalDuration={getTotalDuration}
- />
- </div>
- </div>
- );
-}
-
-function PreviewToolbar({
- hasAnyElements,
- onToggleExpanded,
- isExpanded,
- currentTime,
- setCurrentTime,
- toggle,
- getTotalDuration,
-}: {
- hasAnyElements: boolean;
- onToggleExpanded: () => void;
- isExpanded: boolean;
- currentTime: number;
- setCurrentTime: (time: number) => void;
- toggle: () => void;
- getTotalDuration: () => number;
-}) {
- const { isPlaying } = usePlaybackStore();
- const { layoutGuide, toggleLayoutGuide } = useEditorStore();
-
- if (isExpanded) {
- return (
- <FullscreenToolbar
- {...{
- hasAnyElements,
- onToggleExpanded,
- currentTime,
- setCurrentTime,
- toggle,
- getTotalDuration,
- }}
- />
- );
- }
-
- return (
- <div
- data-toolbar
- className="flex justify-end gap-2 h-auto pb-5 pr-5 pt-4 w-full"
- >
- <div className="flex items-center gap-2">
- <Button
- variant="text"
- size="icon"
- onClick={toggle}
- disabled={!hasAnyElements}
- className="h-auto p-0"
- >
- {isPlaying ? (
- <Pause className="h-3 w-3" />
- ) : (
- <Play className="h-3 w-3" />
- )}
- </Button>
- <Popover>
- <PopoverTrigger asChild>
- <Button
- variant="text"
- size="icon"
- className="h-auto p-0 mr-1"
- title="Toggle layout guide"
- >
- <SocialsIcon className="!size-6" />
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-80">
- <div className="grid gap-4">
- <div className="space-y-2">
- <h4 className="font-medium leading-none">Layout guide</h4>
- <p className="text-sm text-muted-foreground">
- Show platform-specific layout guides to help align your
- content with interface elements like profile pictures,
- usernames, and interaction buttons.
- </p>
- </div>
- <div className="grid gap-2">
- <div className="flex items-center space-x-2">
- <Checkbox
- id="none"
- checked={layoutGuide.platform === null}
- onCheckedChange={() =>
- toggleLayoutGuide(layoutGuide.platform || "tiktok")
- }
- />
- <Label htmlFor="none">None</Label>
- </div>
- {Object.entries(PLATFORM_LAYOUTS).map(([platform, label]) => (
- <div key={platform} className="flex items-center space-x-2">
- <Checkbox
- id={platform}
- checked={layoutGuide.platform === platform}
- onCheckedChange={() =>
- toggleLayoutGuide(platform as PlatformLayout)
- }
- />
- <Label htmlFor={platform}>{label}</Label>
- </div>
- ))}
- </div>
- </div>
- </PopoverContent>
- </Popover>
- <Button
- variant="text"
- size="icon"
- className="size-4!"
- onClick={onToggleExpanded}
- title="Enter fullscreen"
- >
- <Expand className="size-4!" />
- </Button>
- </div>
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/properties-panel/audio-properties.tsx b/apps/web/src/components/editor/properties-panel/audio-properties.tsx
deleted file mode 100644
index 8d69e36a..00000000
--- a/apps/web/src/components/editor/properties-panel/audio-properties.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { MediaElement } from "@/types/timeline";
-
-export function AudioProperties({ element }: { element: MediaElement }) {
- return <div className="space-y-4 p-5">Audio properties</div>;
-}
diff --git a/apps/web/src/components/editor/properties-panel/index.tsx b/apps/web/src/components/editor/properties-panel/index.tsx
deleted file mode 100644
index 150f9512..00000000
--- a/apps/web/src/components/editor/properties-panel/index.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-"use client";
-
-import { useMediaStore } from "@/stores/media-store";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { ScrollArea } from "../../ui/scroll-area";
-import { AudioProperties } from "./audio-properties";
-import { MediaProperties } from "./media-properties";
-import { TextProperties } from "./text-properties";
-import { SquareSlashIcon } from "lucide-react";
-
-export function PropertiesPanel() {
- const { selectedElements, tracks } = useTimelineStore();
- const { mediaFiles } = useMediaStore();
-
- return (
- <>
- {selectedElements.length > 0 ? (
- <ScrollArea className="h-full bg-panel rounded-sm">
- {selectedElements.map(({ trackId, elementId }) => {
- const track = tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((e) => e.id === elementId);
-
- if (element?.type === "text") {
- return (
- <div key={elementId}>
- <TextProperties element={element} trackId={trackId} />
- </div>
- );
- }
- if (element?.type === "media") {
- const mediaFile = mediaFiles.find(
- (file) => file.id === element.mediaId
- );
-
- if (mediaFile?.type === "audio") {
- return <AudioProperties key={elementId} element={element} />;
- }
-
- return (
- <div key={elementId}>
- <MediaProperties element={element} />
- </div>
- );
- }
- return null;
- })}
- </ScrollArea>
- ) : (
- <EmptyView />
- )}
- </>
- );
-}
-
-function EmptyView() {
- return (
- <div className="bg-panel h-full p-4 flex flex-col items-center justify-center gap-3">
- <SquareSlashIcon
- className="w-10 h-10 text-muted-foreground"
- strokeWidth={1.5}
- />
- <div className="flex flex-col gap-2 text-center">
- <p className="text-lg font-medium">It’s empty here</p>
- <p className="text-sm text-muted-foreground text-balance">
- Click an element on the timeline to edit its properties
- </p>
- </div>
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/properties-panel/media-properties.tsx b/apps/web/src/components/editor/properties-panel/media-properties.tsx
deleted file mode 100644
index 4ea28bb2..00000000
--- a/apps/web/src/components/editor/properties-panel/media-properties.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { MediaElement } from "@/types/timeline";
-
-export function MediaProperties({ element }: { element: MediaElement }) {
- return <div className="space-y-4 p-5">Media properties</div>;
-}
diff --git a/apps/web/src/components/editor/properties-panel/property-item.tsx b/apps/web/src/components/editor/properties-panel/property-item.tsx
deleted file mode 100644
index f7d5dbda..00000000
--- a/apps/web/src/components/editor/properties-panel/property-item.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { cn } from "@/lib/utils";
-import { ChevronDown } from "lucide-react";
-import { useState } from "react";
-
-interface PropertyItemProps {
- direction?: "row" | "column";
- children: React.ReactNode;
- className?: string;
-}
-
-export function PropertyItem({
- direction = "row",
- children,
- className,
-}: PropertyItemProps) {
- return (
- <div
- className={cn(
- "flex gap-2",
- direction === "row"
- ? "items-center justify-between gap-6"
- : "flex-col gap-1.5",
- className
- )}
- >
- {children}
- </div>
- );
-}
-
-export function PropertyItemLabel({
- children,
- className,
-}: {
- children: React.ReactNode;
- className?: string;
-}) {
- return (
- <label className={cn("text-xs text-muted-foreground", className)}>
- {children}
- </label>
- );
-}
-
-export function PropertyItemValue({
- children,
- className,
-}: {
- children: React.ReactNode;
- className?: string;
-}) {
- return <div className={cn("flex-1 text-sm", className)}>{children}</div>;
-}
-
-interface PropertyGroupProps {
- title: string;
- children: React.ReactNode;
- defaultExpanded?: boolean;
- className?: string;
- titleClassName?: string;
-}
-
-export function PropertyGroup({
- title,
- children,
- defaultExpanded = true,
- className,
- titleClassName,
-}: PropertyGroupProps) {
- const [isExpanded, setIsExpanded] = useState(defaultExpanded);
-
- return (
- <PropertyItem direction="column" className={cn("gap-3", className)}>
- <div
- className="flex items-center gap-1.5 cursor-pointer"
- onClick={() => setIsExpanded(!isExpanded)}
- >
- <PropertyItemLabel className={cn("cursor-pointer", titleClassName)}>
- {title}
- </PropertyItemLabel>
- <ChevronDown className={cn("size-3", !isExpanded && "-rotate-90")} />
- </div>
- {isExpanded && <PropertyItemValue>{children}</PropertyItemValue>}
- </PropertyItem>
- );
-}
diff --git a/apps/web/src/components/editor/properties-panel/text-properties.tsx b/apps/web/src/components/editor/properties-panel/text-properties.tsx
deleted file mode 100644
index deaee157..00000000
--- a/apps/web/src/components/editor/properties-panel/text-properties.tsx
+++ /dev/null
@@ -1,370 +0,0 @@
-import { Textarea } from "@/components/ui/textarea";
-import { FontPicker } from "@/components/ui/font-picker";
-import { FontFamily } from "@/constants/font-constants";
-import { TextElement } from "@/types/timeline";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { Slider } from "@/components/ui/slider";
-import { Input } from "@/components/ui/input";
-import { Button } from "@/components/ui/button";
-import { useState, useRef } from "react";
-import { PanelBaseView } from "@/components/editor/panel-base-view";
-import {
- TEXT_PROPERTIES_TABS,
- isTextPropertiesTab,
- useTextPropertiesStore,
-} from "@/stores/text-properties-store";
-import {
- PropertyItem,
- PropertyItemLabel,
- PropertyItemValue,
-} from "./property-item";
-import { ColorPicker } from "@/components/ui/color-picker";
-import { cn, uppercase } from "@/lib/utils";
-import { Grid2x2 } from "lucide-react";
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
-
-export function TextProperties({
- element,
- trackId,
-}: {
- element: TextElement;
- trackId: string;
-}) {
- const { updateTextElement } = useTimelineStore();
- const { activeTab, setActiveTab } = useTextPropertiesStore();
- const containerRef = useRef<HTMLDivElement>(null);
- // Local state for input values to allow temporary empty/invalid states
- const [fontSizeInput, setFontSizeInput] = useState(
- element.fontSize.toString()
- );
- const [opacityInput, setOpacityInput] = useState(
- Math.round(element.opacity * 100).toString()
- );
-
- // Track the last selected color for toggling
- const lastSelectedColor = useRef("#000000");
-
- const parseAndValidateNumber = (
- value: string,
- min: number,
- max: number,
- fallback: number
- ): number => {
- const parsed = parseInt(value, 10);
- if (isNaN(parsed)) return fallback;
- return Math.max(min, Math.min(max, parsed));
- };
-
- const handleFontSizeChange = (value: string) => {
- setFontSizeInput(value);
-
- if (value.trim() !== "") {
- const fontSize = parseAndValidateNumber(value, 8, 300, element.fontSize);
- updateTextElement(trackId, element.id, { fontSize });
- }
- };
-
- const handleFontSizeBlur = () => {
- const fontSize = parseAndValidateNumber(
- fontSizeInput,
- 8,
- 300,
- element.fontSize
- );
- setFontSizeInput(fontSize.toString());
- updateTextElement(trackId, element.id, { fontSize });
- };
-
- const handleOpacityChange = (value: string) => {
- setOpacityInput(value);
-
- if (value.trim() !== "") {
- const opacityPercent = parseAndValidateNumber(
- value,
- 0,
- 100,
- Math.round(element.opacity * 100)
- );
- updateTextElement(trackId, element.id, { opacity: opacityPercent / 100 });
- }
- };
-
- const handleOpacityBlur = () => {
- const opacityPercent = parseAndValidateNumber(
- opacityInput,
- 0,
- 100,
- Math.round(element.opacity * 100)
- );
- setOpacityInput(opacityPercent.toString());
- updateTextElement(trackId, element.id, { opacity: opacityPercent / 100 });
- };
-
- // Update last selected color when a new color is picked
- const handleColorChange = (color: string) => {
- if (color !== "transparent") {
- lastSelectedColor.current = color;
- }
- updateTextElement(trackId, element.id, { backgroundColor: color });
- };
-
- // Toggle between transparent and last selected color
- const handleTransparentToggle = (isTransparent: boolean) => {
- const newColor = isTransparent ? "transparent" : lastSelectedColor.current;
- updateTextElement(trackId, element.id, { backgroundColor: newColor });
- };
-
- return (
- <PanelBaseView
- defaultTab="transform"
- value={activeTab}
- onValueChange={(v) => {
- if (isTextPropertiesTab(v)) setActiveTab(v);
- }}
- ref={containerRef}
- tabs={TEXT_PROPERTIES_TABS.map((t) => ({
- value: t.value,
- label: t.label,
- content:
- t.value === "transform" ? (
- <div className="space-y-6"></div>
- ) : (
- <div className="space-y-6">
- <Textarea
- placeholder="Name"
- defaultValue={element.content}
- className="min-h-18 resize-none bg-panel-accent"
- onChange={(e) =>
- updateTextElement(trackId, element.id, {
- content: e.target.value,
- })
- }
- />
- <PropertyItem direction="column">
- <PropertyItemLabel>Font</PropertyItemLabel>
- <PropertyItemValue>
- <FontPicker
- defaultValue={element.fontFamily}
- onValueChange={(value: FontFamily) =>
- updateTextElement(trackId, element.id, {
- fontFamily: value,
- })
- }
- />
- </PropertyItemValue>
- </PropertyItem>
- <PropertyItem direction="column">
- <PropertyItemLabel>Style</PropertyItemLabel>
- <PropertyItemValue>
- <div className="flex items-center gap-2">
- <Button
- variant={
- element.fontWeight === "bold" ? "default" : "outline"
- }
- size="sm"
- onClick={() =>
- updateTextElement(trackId, element.id, {
- fontWeight:
- element.fontWeight === "bold" ? "normal" : "bold",
- })
- }
- className="h-8 px-3 font-bold"
- >
- B
- </Button>
- <Button
- variant={
- element.fontStyle === "italic" ? "default" : "outline"
- }
- size="sm"
- onClick={() =>
- updateTextElement(trackId, element.id, {
- fontStyle:
- element.fontStyle === "italic"
- ? "normal"
- : "italic",
- })
- }
- className="h-8 px-3 italic"
- >
- I
- </Button>
- <Button
- variant={
- element.textDecoration === "underline"
- ? "default"
- : "outline"
- }
- size="sm"
- onClick={() =>
- updateTextElement(trackId, element.id, {
- textDecoration:
- element.textDecoration === "underline"
- ? "none"
- : "underline",
- })
- }
- className="h-8 px-3 underline"
- >
- U
- </Button>
- <Button
- variant={
- element.textDecoration === "line-through"
- ? "default"
- : "outline"
- }
- size="sm"
- onClick={() =>
- updateTextElement(trackId, element.id, {
- textDecoration:
- element.textDecoration === "line-through"
- ? "none"
- : "line-through",
- })
- }
- className="h-8 px-3 line-through"
- >
- S
- </Button>
- </div>
- </PropertyItemValue>
- </PropertyItem>
- <PropertyItem direction="column">
- <PropertyItemLabel>Font size</PropertyItemLabel>
- <PropertyItemValue>
- <div className="flex items-center gap-2">
- <Slider
- value={[element.fontSize]}
- min={8}
- max={300}
- step={1}
- onValueChange={([value]) => {
- updateTextElement(trackId, element.id, {
- fontSize: value,
- });
- setFontSizeInput(value.toString());
- }}
- className="w-full"
- />
- <Input
- type="number"
- value={fontSizeInput}
- min={8}
- max={300}
- onChange={(e) => handleFontSizeChange(e.target.value)}
- onBlur={handleFontSizeBlur}
- className="w-12 px-2 !text-xs h-7 rounded-sm text-center bg-panel-accent
- [appearance:textfield]
- [&::-webkit-outer-spin-button]:appearance-none
- [&::-webkit-inner-spin-button]:appearance-none"
- />
- </div>
- </PropertyItemValue>
- </PropertyItem>
- <PropertyItem direction="column">
- <PropertyItemLabel>Color</PropertyItemLabel>
- <PropertyItemValue>
- <ColorPicker
- value={uppercase(
- (element.color || "FFFFFF").replace("#", "")
- )}
- onChange={(color) => {
- updateTextElement(trackId, element.id, {
- color: `#${color}`,
- });
- }}
- containerRef={containerRef}
- />
- </PropertyItemValue>
- </PropertyItem>
- <PropertyItem direction="column">
- <PropertyItemLabel>Opacity</PropertyItemLabel>
- <PropertyItemValue>
- <div className="flex items-center gap-2">
- <Slider
- value={[element.opacity * 100]}
- min={0}
- max={100}
- step={1}
- onValueChange={([value]) => {
- updateTextElement(trackId, element.id, {
- opacity: value / 100,
- });
- setOpacityInput(value.toString());
- }}
- className="w-full"
- />
- <Input
- type="number"
- value={opacityInput}
- min={0}
- max={100}
- onChange={(e) => handleOpacityChange(e.target.value)}
- onBlur={handleOpacityBlur}
- className="w-12 !text-xs h-7 rounded-sm text-center bg-panel-accent
- [appearance:textfield]
- [&::-webkit-outer-spin-button]:appearance-none
- [&::-webkit-inner-spin-button]:appearance-none"
- />
- </div>
- </PropertyItemValue>
- </PropertyItem>
- <PropertyItem direction="column">
- <PropertyItemLabel>Background</PropertyItemLabel>
- <PropertyItemValue>
- <div className="flex items-center gap-2">
- <ColorPicker
- value={uppercase(
- element.backgroundColor === "transparent"
- ? lastSelectedColor.current.replace("#", "")
- : (element.backgroundColor || "#000000").replace(
- "#",
- ""
- )
- )}
- onChange={(color) => handleColorChange(`#${color}`)}
- containerRef={containerRef}
- className={
- element.backgroundColor === "transparent"
- ? "opacity-50 pointer-events-none"
- : ""
- }
- />
-
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="outline"
- size="icon"
- onClick={() =>
- handleTransparentToggle(
- element.backgroundColor !== "transparent"
- )
- }
- className="size-9 rounded-full bg-panel-accent p-0 overflow-hidden"
- >
- <Grid2x2
- className={cn(
- "text-foreground",
- element.backgroundColor === "transparent" &&
- "text-primary"
- )}
- />
- </Button>
- </TooltipTrigger>
- <TooltipContent>Transparent background</TooltipContent>
- </Tooltip>
- </div>
- </PropertyItemValue>
- </PropertyItem>
- </div>
- ),
- }))}
- />
- );
-}
diff --git a/apps/web/src/components/editor/scenes-view.tsx b/apps/web/src/components/editor/scenes-view.tsx
index b5432b53..f7576377 100644
--- a/apps/web/src/components/editor/scenes-view.tsx
+++ b/apps/web/src/components/editor/scenes-view.tsx
@@ -1,198 +1,219 @@
"use client";
import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetHeader,
- SheetTitle,
- SheetTrigger,
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
-import { useSceneStore } from "@/stores/scene-store";
import { Check, ListCheck, Trash2 } from "lucide-react";
-import { cn } from "@/lib/utils";
+import { cn } from "@/utils/ui";
import { useState } from "react";
import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogDescription,
- DialogFooter,
- DialogTrigger,
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+ DialogTrigger,
} from "@/components/ui/dialog";
+import { canDeleteScene, getMainScene } from "@/lib/scenes";
+import { toast } from "sonner";
+import { useEditor } from "@/hooks/use-editor";
export function ScenesView({ children }: { children: React.ReactNode }) {
- const { scenes, currentScene, switchToScene, deleteScene } = useSceneStore();
- const [isSelectMode, setIsSelectMode] = useState(false);
- const [selectedScenes, setSelectedScenes] = useState<Set<string>>(new Set());
+ const editor = useEditor();
+ const scenes = editor.scenes.getScenes();
+ const currentScene = editor.scenes.getActiveScene();
+ const [isSelectMode, setIsSelectMode] = useState(false);
+ const [selectedScenes, setSelectedScenes] = useState<Set<string>>(new Set());
- const handleSceneSwitch = async (sceneId: string) => {
- if (isSelectMode) {
- toggleSceneSelection(sceneId);
- return;
- }
+ const handleSceneSwitch = async (sceneId: string) => {
+ if (isSelectMode) {
+ toggleSceneSelection({ sceneId });
+ return;
+ }
- try {
- await switchToScene({ sceneId });
- } catch (error) {
- console.error("Failed to switch scene:", error);
- }
- };
+ try {
+ await editor.scenes.switchToScene({ sceneId });
+ } catch (error) {
+ console.error("Failed to switch scene:", error);
+ }
+ };
- const toggleSceneSelection = (sceneId: string) => {
- setSelectedScenes((prev) => {
- const newSet = new Set(prev);
- if (newSet.has(sceneId)) {
- newSet.delete(sceneId);
- } else {
- newSet.add(sceneId);
- }
- return newSet;
- });
- };
+ const toggleSceneSelection = ({ sceneId }: { sceneId: string }) => {
+ setSelectedScenes((prev) => {
+ const newSet = new Set(prev);
+ if (newSet.has(sceneId)) {
+ newSet.delete(sceneId);
+ } else {
+ newSet.add(sceneId);
+ }
+ return newSet;
+ });
+ };
- const handleSelectMode = () => {
- setIsSelectMode(!isSelectMode);
- setSelectedScenes(new Set());
- };
+ const handleSelectMode = () => {
+ setIsSelectMode(!isSelectMode);
+ setSelectedScenes(new Set());
+ };
- const handleDeleteSelected = async () => {
- for (const sceneId of selectedScenes) {
- const scene = scenes.find((s) => s.id === sceneId);
- if (scene && !scene.isMain) {
- try {
- await deleteScene({ sceneId });
- } catch (error) {
- console.error("Failed to delete scene:", error);
- }
- }
- }
- setSelectedScenes(new Set());
- setIsSelectMode(false);
- };
+ const handleDeleteSelected = async () => {
+ for (const sceneId of selectedScenes) {
+ const scene = scenes.find((scene) => scene.id === sceneId);
+ if (!scene) {
+ continue;
+ }
- return (
- <Sheet>
- <SheetTrigger asChild>{children}</SheetTrigger>
- <SheetContent>
- <SheetHeader>
- <SheetTitle>
- {isSelectMode ? `Select scenes (${selectedScenes.size})` : "Scenes"}
- </SheetTitle>
- <SheetDescription>
- {isSelectMode
- ? "Select scenes to delete"
- : "Switch between scenes in your project"}
- </SheetDescription>
- </SheetHeader>
- <div className="py-4 flex flex-col gap-4">
- <div className="flex items-center gap-2">
- <Button
- className="rounded-md"
- variant={isSelectMode ? "default" : "outline"}
- size="sm"
- onClick={handleSelectMode}
- >
- <ListCheck />
- {isSelectMode ? "Cancel" : "Select"}
- </Button>
- {isSelectMode && (
- <DeleteDialog
- count={selectedScenes.size}
- onDelete={handleDeleteSelected}
- disabled={Array.from(selectedScenes).some(
- (id) => scenes.find((s) => s.id === id)?.isMain
- )}
- >
- <Button className="rounded-md" variant="destructive" size="sm">
- <Trash2 />
- Delete ({selectedScenes.size})
- </Button>
- </DeleteDialog>
- )}
- </div>
- {scenes.length === 0 ? (
- <div className="text-sm text-muted-foreground">
- No scenes available
- </div>
- ) : (
- <div className="space-y-2">
- {scenes.map((scene) => (
- <Button
- key={scene.id}
- variant="outline"
- className={cn(
- "w-full justify-between font-normal",
- currentScene?.id === scene.id &&
- !isSelectMode &&
- "border-primary !text-primary",
- isSelectMode &&
- selectedScenes.has(scene.id) &&
- "bg-accent border-foreground/30"
- )}
- onClick={() => handleSceneSwitch(scene.id)}
- >
- <span>{scene.name}</span>
- <div className="flex items-center gap-2">
- {((isSelectMode && selectedScenes.has(scene.id)) ||
- (!isSelectMode && currentScene?.id === scene.id)) && (
- <Check className="h-4 w-4" />
- )}
- </div>
- </Button>
- ))}
- </div>
- )}
- </div>
- </SheetContent>
- </Sheet>
- );
+ const { canDelete, reason } = canDeleteScene({ scene });
+ if (!canDelete) {
+ toast.error(reason || "Failed to delete scene");
+ continue;
+ }
+
+ try {
+ await editor.scenes.deleteScene({ sceneId });
+ } catch (error) {
+ console.error("Failed to delete scene:", error);
+ }
+ }
+ setSelectedScenes(new Set());
+ setIsSelectMode(false);
+ };
+
+ const isMainSceneSelected = (() => {
+ const mainScene = getMainScene({ scenes });
+ return Boolean(mainScene?.id && selectedScenes.has(mainScene.id));
+ })();
+
+ return (
+ <Sheet>
+ <SheetTrigger asChild>{children}</SheetTrigger>
+ <SheetContent>
+ <SheetHeader>
+ <SheetTitle>
+ {isSelectMode ? `Select scenes (${selectedScenes.size})` : "Scenes"}
+ </SheetTitle>
+ <SheetDescription>
+ {isSelectMode
+ ? "Select scenes to delete"
+ : "Switch between scenes in your project"}
+ </SheetDescription>
+ </SheetHeader>
+ <div className="flex flex-col gap-4 py-4">
+ <div className="flex items-center gap-2">
+ <Button
+ className="rounded-md"
+ variant={isSelectMode ? "default" : "outline"}
+ size="sm"
+ onClick={handleSelectMode}
+ >
+ <ListCheck />
+ {isSelectMode ? "Cancel" : "Select"}
+ </Button>
+ {isSelectMode && (
+ <DeleteDialog
+ count={selectedScenes.size}
+ onDelete={handleDeleteSelected}
+ disabled={isMainSceneSelected}
+ trigger={
+ <Button
+ className="rounded-md"
+ variant="destructive"
+ disabled={isMainSceneSelected}
+ size="sm"
+ >
+ <Trash2 />
+ Delete ({selectedScenes.size})
+ </Button>
+ }
+ />
+ )}
+ </div>
+ {scenes.length === 0 ? (
+ <div className="text-muted-foreground text-sm">
+ No scenes available
+ </div>
+ ) : (
+ <div className="space-y-2">
+ {scenes.map((scene) => (
+ <Button
+ key={scene.id}
+ variant="outline"
+ className={cn(
+ "w-full justify-between font-normal",
+ currentScene?.id === scene.id &&
+ !isSelectMode &&
+ "border-primary !text-primary",
+ isSelectMode &&
+ selectedScenes.has(scene.id) &&
+ "bg-accent border-foreground/30",
+ )}
+ onClick={() => handleSceneSwitch(scene.id)}
+ >
+ <span>{scene.name}</span>
+ <div className="flex items-center gap-2">
+ {((isSelectMode && selectedScenes.has(scene.id)) ||
+ (!isSelectMode && currentScene?.id === scene.id)) && (
+ <Check className="size-4" />
+ )}
+ </div>
+ </Button>
+ ))}
+ </div>
+ )}
+ </div>
+ </SheetContent>
+ </Sheet>
+ );
}
function DeleteDialog({
- count,
- onDelete,
- disabled,
- children,
+ count,
+ onDelete,
+ disabled,
+ trigger,
}: {
- count: number;
- onDelete: () => void;
- disabled?: boolean;
- children: React.ReactNode;
+ count: number;
+ onDelete: () => void;
+ disabled?: boolean;
+ trigger: React.ReactNode;
}) {
- const [open, setOpen] = useState(false);
+ const [open, setOpen] = useState(false);
- const handleDelete = () => {
- onDelete();
- setOpen(false);
- };
+ const handleDelete = () => {
+ onDelete();
+ setOpen(false);
+ };
- return (
- <Dialog open={open} onOpenChange={setOpen}>
- <DialogTrigger asChild>{children}</DialogTrigger>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Delete Scenes</DialogTitle>
- <DialogDescription>
- Are you sure you want to delete {count} scene
- {count === 1 ? "" : "s"}? This action cannot be undone.
- </DialogDescription>
- </DialogHeader>
- <DialogFooter>
- <Button variant="outline" onClick={() => setOpen(false)}>
- Cancel
- </Button>
- <Button
- variant="destructive"
- onClick={handleDelete}
- disabled={disabled}
- >
- Delete
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
+ return (
+ <Dialog open={open} onOpenChange={setOpen}>
+ <DialogTrigger asChild>{trigger}</DialogTrigger>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Delete Scenes</DialogTitle>
+ <DialogDescription>
+ Are you sure you want to delete {count} scene
+ {count === 1 ? "" : "s"}? This action cannot be undone.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter>
+ <Button variant="outline" onClick={() => setOpen(false)}>
+ Cancel
+ </Button>
+ <Button
+ variant="destructive"
+ onClick={handleDelete}
+ disabled={disabled}
+ >
+ Delete
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
}
diff --git a/apps/web/src/components/editor/selection-box.tsx b/apps/web/src/components/editor/selection-box.tsx
index ca9d799f..32adbfd8 100644
--- a/apps/web/src/components/editor/selection-box.tsx
+++ b/apps/web/src/components/editor/selection-box.tsx
@@ -1,55 +1,50 @@
"use client";
-import { useEffect, useRef } from "react";
+import { useMemo } from "react";
interface SelectionBoxProps {
- startPos: { x: number; y: number } | null;
- currentPos: { x: number; y: number } | null;
- containerRef: React.RefObject<HTMLElement>;
- isActive: boolean;
+ startPos: { x: number; y: number } | null;
+ currentPos: { x: number; y: number } | null;
+ containerRef: React.RefObject<HTMLElement>;
+ isActive: boolean;
}
export function SelectionBox({
- startPos,
- currentPos,
- containerRef,
- isActive,
+ startPos,
+ currentPos,
+ containerRef,
+ isActive,
}: SelectionBoxProps) {
- const selectionBoxRef = useRef<HTMLDivElement>(null);
+ const selectionBoxStyle = useMemo(() => {
+ if (!isActive || !startPos || !currentPos || !containerRef.current) {
+ return null;
+ }
- useEffect(() => {
- if (!isActive || !startPos || !currentPos || !containerRef.current) return;
+ const containerRect = containerRef.current.getBoundingClientRect();
+ const startX = startPos.x - containerRect.left;
+ const startY = startPos.y - containerRect.top;
+ const currentX = currentPos.x - containerRect.left;
+ const currentY = currentPos.y - containerRect.top;
- const container = containerRef.current;
- const containerRect = container.getBoundingClientRect();
+ const left = Math.min(startX, currentX);
+ const top = Math.min(startY, currentY);
+ const width = Math.abs(currentX - startX);
+ const height = Math.abs(currentY - startY);
- // Calculate relative positions within the container
- const startX = startPos.x - containerRect.left;
- const startY = startPos.y - containerRect.top;
- const currentX = currentPos.x - containerRect.left;
- const currentY = currentPos.y - containerRect.top;
+ return {
+ left: `${left}px`,
+ top: `${top}px`,
+ width: `${width}px`,
+ height: `${height}px`,
+ };
+ }, [containerRef, currentPos, isActive, startPos]);
- // Calculate the selection rectangle bounds
- const left = Math.min(startX, currentX);
- const top = Math.min(startY, currentY);
- const width = Math.abs(currentX - startX);
- const height = Math.abs(currentY - startY);
+ if (!selectionBoxStyle) return null;
- // Update the selection box position and size
- if (selectionBoxRef.current) {
- selectionBoxRef.current.style.left = `${left}px`;
- selectionBoxRef.current.style.top = `${top}px`;
- selectionBoxRef.current.style.width = `${width}px`;
- selectionBoxRef.current.style.height = `${height}px`;
- }
- }, [startPos, currentPos, isActive, containerRef]);
-
- if (!isActive || !startPos || !currentPos) return null;
-
- return (
- <div
- ref={selectionBoxRef}
- className="absolute pointer-events-none z-50 bg-foreground/10"
- />
- );
+ return (
+ <div
+ style={selectionBoxStyle}
+ className="border-foreground/50 bg-foreground/5 pointer-events-none absolute z-50 border"
+ />
+ );
}
diff --git a/apps/web/src/components/editor/snap-indicator.tsx b/apps/web/src/components/editor/snap-indicator.tsx
deleted file mode 100644
index ac884b71..00000000
--- a/apps/web/src/components/editor/snap-indicator.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-"use client";
-
-import { SnapPoint } from "@/hooks/use-timeline-snapping";
-import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
-import type { TimelineTrack } from "@/types/timeline";
-import { useState, useEffect } from "react";
-
-interface SnapIndicatorProps {
- snapPoint: SnapPoint | null;
- zoomLevel: number;
- isVisible: boolean;
- tracks: TimelineTrack[];
- timelineRef: React.RefObject<HTMLDivElement>;
- trackLabelsRef?: React.RefObject<HTMLDivElement>;
- tracksScrollRef: React.RefObject<HTMLDivElement>;
-}
-
-export function SnapIndicator({
- snapPoint,
- zoomLevel,
- isVisible,
- tracks,
- timelineRef,
- trackLabelsRef,
- tracksScrollRef,
-}: SnapIndicatorProps) {
- const [scrollLeft, setScrollLeft] = useState(0);
-
- // Track scroll position to lock snap indicator to frame
- useEffect(() => {
- const tracksViewport = tracksScrollRef.current;
-
- if (!tracksViewport) return;
-
- const handleScroll = () => {
- setScrollLeft(tracksViewport.scrollLeft);
- };
-
- // Set initial scroll position
- setScrollLeft(tracksViewport.scrollLeft);
-
- tracksViewport.addEventListener("scroll", handleScroll);
- return () => tracksViewport.removeEventListener("scroll", handleScroll);
- }, [tracksScrollRef]);
-
- if (!isVisible || !snapPoint) {
- return null;
- }
-
- const timelineContainerHeight = timelineRef.current?.offsetHeight || 400;
- const totalHeight = timelineContainerHeight - 8; // 8px padding from edges
-
- // Get dynamic track labels width, fallback to 0 if no tracks or no ref
- const trackLabelsWidth =
- tracks.length > 0 && trackLabelsRef?.current
- ? trackLabelsRef.current.offsetWidth
- : 0;
-
- // Calculate position locked to timeline content (accounting for scroll)
- const timelinePosition =
- snapPoint.time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
- const leftPosition = trackLabelsWidth + timelinePosition - scrollLeft;
-
- return (
- <div
- className="absolute pointer-events-none z-90"
- style={{
- left: `${leftPosition}px`,
- top: 0,
- height: `${totalHeight}px`,
- width: "2px",
- }}
- >
- <div className={"w-0.5 h-full bg-primary/40 opacity-80"} />
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/speed-control.tsx b/apps/web/src/components/editor/speed-control.tsx
deleted file mode 100644
index f2b0568d..00000000
--- a/apps/web/src/components/editor/speed-control.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { Slider } from "../ui/slider";
-import { Label } from "../ui/label";
-import { Button } from "../ui/button";
-import { usePlaybackStore } from "@/stores/playback-store";
-
-const SPEED_PRESETS = [
- { label: "0.5x", value: 0.5 },
- { label: "1x", value: 1.0 },
- { label: "1.5x", value: 1.5 },
- { label: "2x", value: 2.0 },
-];
-
-export function SpeedControl() {
- const { speed, setSpeed } = usePlaybackStore();
-
- return (
- <div className="space-y-4">
- <h3 className="text-sm font-medium">Playback Speed</h3>
- <div className="space-y-4">
- <div className="flex gap-2">
- {SPEED_PRESETS.map((preset) => (
- <Button
- key={preset.value}
- variant={speed === preset.value ? "default" : "outline"}
- className="flex-1"
- onClick={() => setSpeed(preset.value)}
- >
- {preset.label}
- </Button>
- ))}
- </div>
- <div className="space-y-1">
- <Label>Custom ({speed.toFixed(1)}x)</Label>
- <Slider
- value={[speed]}
- min={0.1}
- max={2.0}
- step={0.1}
- onValueChange={(value) => setSpeed(value[0])}
- className="mt-2"
- />
- </div>
- </div>
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/timeline/audio-waveform.tsx b/apps/web/src/components/editor/timeline/audio-waveform.tsx
new file mode 100644
index 00000000..bbc5cb83
--- /dev/null
+++ b/apps/web/src/components/editor/timeline/audio-waveform.tsx
@@ -0,0 +1,176 @@
+import { useEffect, useRef, useState } from "react";
+import WaveSurfer from "wavesurfer.js";
+
+interface AudioWaveformProps {
+ audioUrl?: string;
+ audioBuffer?: AudioBuffer;
+ height?: number;
+ className?: string;
+}
+
+function extractPeaks({
+ buffer,
+ length = 512,
+}: {
+ buffer: AudioBuffer;
+ length?: number;
+}): number[][] {
+ const channels = buffer.numberOfChannels;
+ const peaks: number[][] = [];
+
+ for (let c = 0; c < channels; c++) {
+ const data = buffer.getChannelData(c);
+ const step = Math.floor(data.length / length);
+ const channelPeaks: number[] = [];
+
+ for (let i = 0; i < length; i++) {
+ const start = i * step;
+ const end = Math.min(start + step, data.length);
+ let max = 0;
+ for (let j = start; j < end; j++) {
+ const abs = Math.abs(data[j]);
+ if (abs > max) max = abs;
+ }
+ channelPeaks.push(max);
+ }
+ peaks.push(channelPeaks);
+ }
+
+ return peaks;
+}
+
+export function AudioWaveform({
+ audioUrl,
+ audioBuffer,
+ height = 32,
+ className = "",
+}: AudioWaveformProps) {
+ const waveformRef = useRef<HTMLDivElement>(null);
+ const wavesurfer = useRef<WaveSurfer | null>(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(false);
+
+ useEffect(() => {
+ let mounted = true;
+ const ws = wavesurfer.current;
+
+ const initWaveSurfer = async () => {
+ if (!waveformRef.current || (!audioUrl && !audioBuffer)) return;
+
+ try {
+ if (ws) {
+ wavesurfer.current = null;
+ }
+
+ const newWaveSurfer = WaveSurfer.create({
+ container: waveformRef.current,
+ waveColor: "rgba(255, 255, 255, 0.6)",
+ progressColor: "rgba(255, 255, 255, 0.9)",
+ cursorColor: "transparent",
+ barWidth: 2,
+ barGap: 1,
+ height,
+ normalize: true,
+ interact: false,
+ });
+
+ if (mounted) {
+ wavesurfer.current = newWaveSurfer;
+ } else {
+ try {
+ newWaveSurfer.destroy();
+ } catch {}
+ return;
+ }
+
+ newWaveSurfer.on("ready", () => {
+ if (mounted) {
+ setIsLoading(false);
+ setError(false);
+ }
+ });
+
+ newWaveSurfer.on("error", (err) => {
+ if (mounted) {
+ console.error("WaveSurfer error:", err);
+ setError(true);
+ setIsLoading(false);
+ }
+ });
+
+ if (audioBuffer) {
+ const peaks = extractPeaks({ buffer: audioBuffer });
+ newWaveSurfer.load("", peaks, audioBuffer.duration);
+ } else if (audioUrl) {
+ await newWaveSurfer.load(audioUrl);
+ }
+ } catch (err) {
+ if (mounted) {
+ console.error("Failed to initialize WaveSurfer:", err);
+ setError(true);
+ setIsLoading(false);
+ }
+ }
+ };
+
+ if (ws) {
+ const wsToDestroy = ws;
+ wavesurfer.current = null;
+
+ requestAnimationFrame(() => {
+ try {
+ wsToDestroy.destroy();
+ } catch {}
+ if (mounted) {
+ initWaveSurfer();
+ }
+ });
+ } else {
+ initWaveSurfer();
+ }
+
+ return () => {
+ mounted = false;
+
+ const wsToDestroy = wavesurfer.current;
+
+ wavesurfer.current = null;
+
+ if (wsToDestroy) {
+ requestAnimationFrame(() => {
+ try {
+ wsToDestroy.destroy();
+ } catch {}
+ });
+ }
+ };
+ }, [audioUrl, audioBuffer, height]);
+
+ if (error) {
+ return (
+ <div
+ className={`flex items-center justify-center ${className}`}
+ style={{ height }}
+ >
+ <span className="text-foreground/60 text-xs">Audio unavailable</span>
+ </div>
+ );
+ }
+
+ return (
+ <div className={`relative ${className}`}>
+ {isLoading && (
+ <div className="absolute inset-0 flex items-center justify-center">
+ <span className="text-foreground/60 text-xs">Loading...</span>
+ </div>
+ )}
+ <div
+ ref={waveformRef}
+ className={`w-full ${isLoading ? "opacity-0" : "opacity-100"}`}
+ style={{ height }}
+ />
+ </div>
+ );
+}
+
+export default AudioWaveform;
diff --git a/apps/web/src/components/editor/timeline/bookmarks.tsx b/apps/web/src/components/editor/timeline/bookmarks.tsx
new file mode 100644
index 00000000..cfcb1e23
--- /dev/null
+++ b/apps/web/src/components/editor/timeline/bookmarks.tsx
@@ -0,0 +1,111 @@
+import { useEditor } from "@/hooks/use-editor";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import { getSnappedSeekTime } from "@/lib/time";
+import { Bookmark02Icon } from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
+
+interface TimelineBookmarksRowProps {
+ zoomLevel: number;
+ dynamicTimelineWidth: number;
+ handleWheel: (e: React.WheelEvent) => void;
+ handleTimelineContentClick: (e: React.MouseEvent) => void;
+ handleRulerTrackingMouseDown: (e: React.MouseEvent) => void;
+ handleRulerMouseDown: (e: React.MouseEvent) => void;
+}
+
+export function TimelineBookmarksRow({
+ zoomLevel,
+ dynamicTimelineWidth,
+ handleWheel,
+ handleTimelineContentClick,
+ handleRulerTrackingMouseDown,
+ handleRulerMouseDown,
+}: TimelineBookmarksRowProps) {
+ const editor = useEditor();
+ const activeScene = editor.scenes.getActiveScene();
+
+ return (
+ <div className="relative h-4 flex-1 overflow-hidden">
+ <button
+ className="relative h-4 w-full cursor-default select-none border-0 bg-transparent p-0"
+ style={{
+ width: `${dynamicTimelineWidth}px`,
+ }}
+ aria-label="Timeline ruler"
+ type="button"
+ onWheel={handleWheel}
+ onClick={handleTimelineContentClick}
+ onMouseDown={(event) => {
+ handleRulerMouseDown(event);
+ handleRulerTrackingMouseDown(event);
+ }}
+ >
+ {activeScene.bookmarks.map((time: number) => (
+ <TimelineBookmark
+ key={`bookmark-row-${time}`}
+ time={time}
+ zoomLevel={zoomLevel}
+ />
+ ))}
+ </button>
+ </div>
+ );
+}
+
+export function TimelineBookmark({
+ time,
+ zoomLevel,
+}: {
+ time: number;
+ zoomLevel: number;
+}) {
+ const editor = useEditor();
+ const activeProject = editor.project.getActive();
+ const duration = editor.timeline.getTotalDuration();
+
+ const handleBookmarkActivate = ({
+ event,
+ }: {
+ event:
+ | React.MouseEvent<HTMLButtonElement>
+ | React.KeyboardEvent<HTMLButtonElement>;
+ }) => {
+ event.stopPropagation();
+ const framesPerSecond = activeProject?.settings.fps ?? 30;
+ const snappedTime = getSnappedSeekTime({
+ rawTime: time,
+ duration,
+ fps: framesPerSecond,
+ });
+ editor.playback.seek({ time: snappedTime });
+ };
+
+ return (
+ <button
+ className="absolute top-0 h-10 w-0.5 cursor-pointer border-0 bg-transparent p-0"
+ style={{
+ left: `${time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
+ }}
+ aria-label={`Seek to bookmark at ${time}s`}
+ type="button"
+ onMouseDown={(event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ }}
+ onClick={(event) => handleBookmarkActivate({ event })}
+ onKeyDown={(event) => {
+ if (event.key !== "Enter" && event.key !== " ") return;
+ event.preventDefault();
+ handleBookmarkActivate({ event });
+ }}
+ >
+ <div className="text-primary absolute top-[-1px] left-[-5px]">
+ <HugeiconsIcon
+ icon={Bookmark02Icon}
+ aria-hidden="true"
+ className="fill-primary size-3"
+ />
+ </div>
+ </button>
+ );
+}
diff --git a/apps/web/src/components/editor/timeline/drag-line.tsx b/apps/web/src/components/editor/timeline/drag-line.tsx
new file mode 100644
index 00000000..f31c6e28
--- /dev/null
+++ b/apps/web/src/components/editor/timeline/drag-line.tsx
@@ -0,0 +1,28 @@
+import { getDropLineY } from "@/lib/timeline/drop-utils";
+import type { TimelineTrack, DropTarget } from "@/types/timeline";
+
+interface DragLineProps {
+ dropTarget: DropTarget | null;
+ tracks: TimelineTrack[];
+ isVisible: boolean;
+ headerHeight?: number;
+}
+
+export function DragLine({
+ dropTarget,
+ tracks,
+ isVisible,
+ headerHeight = 0,
+}: DragLineProps) {
+ if (!isVisible || !dropTarget) return null;
+
+ const y = getDropLineY({ dropTarget, tracks });
+ const lineTop = y + headerHeight;
+
+ return (
+ <div
+ className="bg-primary pointer-events-none absolute right-0 left-0 z-50 h-0.5"
+ style={{ top: `${lineTop}px` }}
+ />
+ );
+}
diff --git a/apps/web/src/components/editor/timeline/index.tsx b/apps/web/src/components/editor/timeline/index.tsx
index 4a59fd21..7e2b2b66 100644
--- a/apps/web/src/components/editor/timeline/index.tsx
+++ b/apps/web/src/components/editor/timeline/index.tsx
@@ -1,925 +1,530 @@
"use client";
import { ScrollArea } from "@/components/ui/scroll-area";
-import { Button } from "../../ui/button";
import {
- Scissors,
- ArrowLeftToLine,
- ArrowRightToLine,
- Trash2,
- Snowflake,
- Copy,
- SplitSquareHorizontal,
- Pause,
- Play,
- SkipBack,
- Video,
- Music,
- TypeIcon,
- Magnet,
- Link,
- ZoomIn,
- ZoomOut,
- Bookmark,
- Eye,
- VolumeOff,
- Volume2,
-} from "lucide-react";
+ Delete02Icon,
+ TaskAdd02Icon,
+ ViewIcon,
+ ViewOffSlashIcon,
+ VolumeHighIcon,
+ VolumeOffIcon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react";
import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
- TooltipProvider,
-} from "../../ui/tooltip";
-import {
- ContextMenu,
- ContextMenuContent,
- ContextMenuItem,
- ContextMenuTrigger,
+ ContextMenu,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuTrigger,
} from "../../ui/context-menu";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { useMediaStore } from "@/stores/media-store";
-import { usePlaybackStore } from "@/stores/playback-store";
-import { DEFAULT_FPS, useProjectStore } from "@/stores/project-store";
-
-import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
-import { processMediaFiles } from "@/lib/media-processing";
-
-import { toast } from "sonner";
-import { useState, useRef, useEffect, useCallback } from "react";
+import { useTimelineZoom } from "@/hooks/timeline/use-timeline-zoom";
+import { useState, useRef, useCallback } from "react";
import { TimelineTrackContent } from "./timeline-track";
-import {
- TimelinePlayhead,
- useTimelinePlayheadRuler,
-} from "./timeline-playhead";
+import { TimelinePlayhead } from "./timeline-playhead";
import { SelectionBox } from "../selection-box";
-import { useSelectionBox } from "@/hooks/use-selection-box";
-import { SnapIndicator } from "../snap-indicator";
-import { SnapPoint } from "@/hooks/use-timeline-snapping";
-import type { DragData, TimelineTrack, TrackType } from "@/types/timeline";
-import { TimelineCacheIndicator } from "./timeline-cache-indicator";
-import { TimelineMarker } from "./timeline-marker";
-import { useFrameCache } from "@/hooks/use-frame-cache";
+import { useSelectionBox } from "@/hooks/timeline/use-selection-box";
+import { SnapIndicator } from "./snap-indicator";
+import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
+import type { TimelineTrack } from "@/types/timeline";
import {
- getTrackHeight,
- getCumulativeHeightBefore,
- getTotalTracksHeight,
- TIMELINE_CONSTANTS,
- snapTimeToFrame,
+ TIMELINE_CONSTANTS,
+ TRACK_ICONS,
} from "@/constants/timeline-constants";
-import { Slider } from "@/components/ui/slider";
-import { formatTimeCode } from "@/lib/time";
-import { EditableTimecode } from "@/components/ui/editable-timecode";
+import { useElementInteraction } from "@/hooks/timeline/element/use-element-interaction";
+import {
+ getTrackHeight,
+ getCumulativeHeightBefore,
+ getTotalTracksHeight,
+ canTracktHaveAudio,
+ canTrackBeHidden,
+ getTimelineZoomMin,
+ getTimelinePaddingPx,
+ isMainTrack,
+} from "@/lib/timeline";
import { TimelineToolbar } from "./timeline-toolbar";
+import { useScrollSync } from "@/hooks/timeline/use-scroll-sync";
+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 { TimelineRuler } from "./timeline-ruler";
+import { TimelineBookmarksRow } from "./bookmarks";
+import { useTimelineStore } from "@/stores/timeline-store";
+import { useEditor } from "@/hooks/use-editor";
+import { useTimelinePlayhead } from "@/hooks/timeline/use-timeline-playhead";
+import { DragLine } from "./drag-line";
+import { invokeAction } from "@/lib/actions";
export function Timeline() {
- // Timeline shows all tracks (video, audio, effects) and their elements.
- // You can drag media here to add it to your project.
- // elements can be trimmed, deleted, and moved.
-
- const {
- tracks,
- getTotalDuration,
- clearSelectedElements,
- snappingEnabled,
- setSelectedElements,
- toggleTrackMute,
- dragState,
- } = useTimelineStore();
- const { mediaFiles, addMediaFile } = useMediaStore();
- const { activeProject } = useProjectStore();
- const { currentTime, duration, seek, setDuration } = usePlaybackStore();
- const { getRenderStatus } = useFrameCache();
- const [isDragOver, setIsDragOver] = useState(false);
- const { addElementToNewTrack } = useTimelineStore();
- const dragCounterRef = useRef(0);
- const timelineRef = useRef<HTMLDivElement>(null);
- const rulerRef = useRef<HTMLDivElement>(null);
- const [isInTimeline, setIsInTimeline] = useState(false);
-
- // Track mouse down/up for distinguishing clicks from drag/resize ends
- const mouseTrackingRef = useRef({
- isMouseDown: false,
- downX: 0,
- downY: 0,
- downTime: 0,
- });
-
- // Timeline zoom functionality
- const { zoomLevel, setZoomLevel, handleWheel } = useTimelineZoom({
- containerRef: timelineRef,
- isInTimeline,
- });
-
- // Old marquee selection removed - using new SelectionBox component instead
-
- // Dynamic timeline width calculation based on playhead position and duration
- const dynamicTimelineWidth = Math.max(
- (duration || 0) * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel, // Base width from duration
- (currentTime + 30) * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel, // Width to show current time + 30 seconds buffer
- timelineRef.current?.clientWidth || 1000 // Minimum width
- );
-
- // Scroll synchronization and auto-scroll to playhead
- const rulerScrollRef = useRef<HTMLDivElement>(null);
- const tracksScrollRef = useRef<HTMLDivElement>(null);
- const trackLabelsRef = useRef<HTMLDivElement>(null);
- const playheadRef = useRef<HTMLDivElement>(null);
- const trackLabelsScrollRef = useRef<HTMLDivElement>(null);
- const isUpdatingRef = useRef(false);
- const lastRulerSync = useRef(0);
- const lastTracksSync = useRef(0);
- const lastVerticalSync = useRef(0);
-
- // Timeline playhead ruler handlers
- const { handleRulerMouseDown } = useTimelinePlayheadRuler({
- currentTime,
- duration,
- zoomLevel,
- seek,
- rulerRef,
- rulerScrollRef,
- tracksScrollRef,
- playheadRef,
- });
-
- // Selection box functionality
- const tracksContainerRef = useRef<HTMLDivElement>(null);
- const {
- selectionBox,
- handleMouseDown: handleSelectionMouseDown,
- isSelecting,
- justFinishedSelecting,
- } = useSelectionBox({
- containerRef: tracksContainerRef,
- playheadRef,
- onSelectionComplete: (elements) => {
- console.log(JSON.stringify({ onSelectionComplete: elements.length }));
- setSelectedElements(elements);
- },
- });
-
- // Calculate snap indicator state
- const [currentSnapPoint, setCurrentSnapPoint] = useState<SnapPoint | null>(
- null
- );
- const showSnapIndicator =
- dragState.isDragging && snappingEnabled && currentSnapPoint !== null;
-
- // Callback to handle snap point changes from TimelineTrackContent
- const handleSnapPointChange = useCallback((snapPoint: SnapPoint | null) => {
- setCurrentSnapPoint(snapPoint);
- }, []);
-
- // Track mouse down to distinguish real clicks from drag/resize ends
- const handleTimelineMouseDown = useCallback((e: React.MouseEvent) => {
- // Only track mouse down on timeline background areas (not elements)
- const target = e.target as HTMLElement;
- console.log(
- JSON.stringify({
- debug_mousedown: "START",
- target_class: target.className,
- target_parent_class: target.parentElement?.className,
- clientX: e.clientX,
- clientY: e.clientY,
- timeStamp: e.timeStamp,
- })
- );
-
- const isTimelineBackground =
- !target.closest(".timeline-element") &&
- !playheadRef.current?.contains(target) &&
- !target.closest("[data-track-labels]");
-
- console.log(
- JSON.stringify({
- debug_mousedown: "CHECK",
- isTimelineBackground,
- hasTimelineElement: !!target.closest(".timeline-element"),
- hasPlayhead: !!playheadRef.current?.contains(target),
- hasTrackLabels: !!target.closest("[data-track-labels]"),
- })
- );
-
- if (isTimelineBackground) {
- mouseTrackingRef.current = {
- isMouseDown: true,
- downX: e.clientX,
- downY: e.clientY,
- downTime: e.timeStamp,
- };
- console.log(
- JSON.stringify({
- debug_mousedown: "TRACKED",
- mouseTracking: mouseTrackingRef.current,
- })
- );
- } else {
- console.log(
- JSON.stringify({
- debug_mousedown: "IGNORED - not timeline background",
- })
- );
- }
- }, []);
-
- // Timeline content click to seek handler
- const handleTimelineContentClick = useCallback(
- (e: React.MouseEvent) => {
- console.log(
- JSON.stringify({
- debug_click: "START",
- target: (e.target as HTMLElement).className,
- target_parent: (e.target as HTMLElement).parentElement?.className,
- mouseTracking: mouseTrackingRef.current,
- isSelecting,
- justFinishedSelecting,
- clickX: e.clientX,
- clickY: e.clientY,
- timeStamp: e.timeStamp,
- })
- );
-
- const { isMouseDown, downX, downY, downTime } = mouseTrackingRef.current;
-
- // Reset mouse tracking
- mouseTrackingRef.current = {
- isMouseDown: false,
- downX: 0,
- downY: 0,
- downTime: 0,
- };
-
- // Only process as click if we tracked a mouse down on timeline background
- if (!isMouseDown) {
- console.log(
- JSON.stringify({
- debug_click: "REJECTED - no mousedown",
- mouseTracking: mouseTrackingRef.current,
- })
- );
- return;
- }
-
- // Check if mouse moved significantly (indicates drag, not click)
- const deltaX = Math.abs(e.clientX - downX);
- const deltaY = Math.abs(e.clientY - downY);
- const deltaTime = e.timeStamp - downTime;
-
- if (deltaX > 5 || deltaY > 5 || deltaTime > 500) {
- console.log(
- JSON.stringify({
- debug_click: "REJECTED - movement too large",
- deltaX,
- deltaY,
- deltaTime,
- })
- );
- return;
- }
-
- // Don't seek if this was a selection box operation
- if (isSelecting || justFinishedSelecting) {
- console.log(
- JSON.stringify({
- debug_click: "REJECTED - selection operation",
- isSelecting,
- justFinishedSelecting,
- })
- );
- return;
- }
+ const tracksContainerHeight = { min: 0, max: 800 };
+ const { snappingEnabled } = useTimelineStore();
+ const { clearElementSelection, setElementSelection } = useElementSelection();
+ const editor = useEditor();
+ const timeline = editor.timeline;
+ const tracks = timeline.getTracks();
+ const seek = (time: number) => editor.playback.seek({ time });
- // Don't seek if clicking on timeline elements, but still deselect
- if ((e.target as HTMLElement).closest(".timeline-element")) {
- console.log(
- JSON.stringify({
- debug_click: "REJECTED - clicked timeline element",
- })
- );
- return;
- }
+ // refs
+ const timelineRef = useRef<HTMLDivElement>(null);
+ const timelineHeaderRef = useRef<HTMLDivElement>(null);
+ const rulerRef = useRef<HTMLDivElement>(null);
+ const tracksContainerRef = useRef<HTMLDivElement>(null);
+ const tracksScrollRef = useRef<HTMLDivElement>(null);
+ const trackLabelsRef = useRef<HTMLDivElement>(null);
+ const playheadRef = useRef<HTMLDivElement>(null);
+ const trackLabelsScrollRef = useRef<HTMLDivElement>(null);
- // Don't seek if clicking on playhead
- if (playheadRef.current?.contains(e.target as Node)) {
- console.log(
- JSON.stringify({
- debug_click: "REJECTED - clicked playhead",
- })
- );
- return;
- }
+ // state
+ const [isResizing, setIsResizing] = useState(false);
+ const [currentSnapPoint, setCurrentSnapPoint] = useState<SnapPoint | null>(
+ null,
+ );
- // Don't seek if clicking on track labels
- if ((e.target as HTMLElement).closest("[data-track-labels]")) {
- console.log(
- JSON.stringify({
- debug_click: "REJECTED - clicked track labels",
- })
- );
- clearSelectedElements();
- return;
- }
+ const handleSnapPointChange = useCallback((snapPoint: SnapPoint | null) => {
+ setCurrentSnapPoint(snapPoint);
+ }, []);
+ const handleResizeStateChange = useCallback(
+ ({ isResizing: nextIsResizing }: { isResizing: boolean }) => {
+ setIsResizing(nextIsResizing);
+ if (!nextIsResizing) {
+ setCurrentSnapPoint(null);
+ }
+ },
+ [],
+ );
- // Clear selected elements when clicking empty timeline area
- console.log(
- JSON.stringify({
- debug_click: "PROCEEDING - clearing elements",
- clearingSelectedElements: true,
- })
- );
- clearSelectedElements();
+ const timelineDuration = timeline.getTotalDuration() || 0;
+ const minZoomLevel = getTimelineZoomMin({
+ duration: timelineDuration,
+ containerWidth: tracksContainerRef.current?.clientWidth,
+ });
- // Determine if we're clicking in ruler or tracks area
- const isRulerClick = (e.target as HTMLElement).closest(
- "[data-ruler-area]"
- );
+ const savedViewState = editor.project.getTimelineViewState();
- console.log(
- JSON.stringify({
- debug_click: "CALCULATING POSITION",
- isRulerClick,
- clientX: e.clientX,
- clientY: e.clientY,
- target_element: (e.target as HTMLElement).tagName,
- target_class: (e.target as HTMLElement).className,
- })
- );
+ const { zoomLevel, setZoomLevel, handleWheel, saveScrollPosition } =
+ useTimelineZoom({
+ containerRef: timelineRef,
+ minZoom: minZoomLevel,
+ initialZoom: savedViewState?.zoomLevel,
+ initialScrollLeft: savedViewState?.scrollLeft,
+ initialPlayheadTime: savedViewState?.playheadTime,
+ tracksScrollRef,
+ rulerScrollRef: tracksScrollRef,
+ });
- let mouseX: number;
- let scrollLeft = 0;
+ const {
+ dragState,
+ dragDropTarget,
+ handleElementMouseDown,
+ handleElementClick,
+ lastMouseXRef,
+ } = useElementInteraction({
+ zoomLevel,
+ timelineRef,
+ tracksContainerRef,
+ tracksScrollRef,
+ headerRef: timelineHeaderRef,
+ snappingEnabled,
+ onSnapPointChange: handleSnapPointChange,
+ });
- if (isRulerClick) {
- // Calculate based on ruler position
- const rulerContent = rulerScrollRef.current;
- if (!rulerContent) {
- console.log(
- JSON.stringify({
- debug_click: "ERROR - no ruler container found",
- })
- );
- return;
- }
- const rect = rulerContent.getBoundingClientRect();
- mouseX = e.clientX - rect.left;
- scrollLeft = rulerContent.scrollLeft;
- } else {
- const tracksContent = tracksScrollRef.current;
- if (!tracksContent) {
- return;
- }
- const rect = tracksContent.getBoundingClientRect();
- mouseX = e.clientX - rect.left;
- scrollLeft = tracksContent.scrollLeft;
- }
+ const { handleRulerMouseDown: handlePlayheadRulerMouseDown } =
+ useTimelinePlayhead({
+ zoomLevel,
+ rulerRef,
+ rulerScrollRef: tracksScrollRef,
+ tracksScrollRef,
+ playheadRef,
+ });
- const rawTime = Math.max(
- 0,
- Math.min(
- duration,
- (mouseX + scrollLeft) /
- (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel)
- )
- );
+ const { isDragOver, dropTarget, dragProps } = useTimelineDragDrop({
+ containerRef: tracksContainerRef,
+ headerRef: timelineHeaderRef,
+ zoomLevel,
+ });
- // Use frame snapping for timeline clicking
- const projectFps = activeProject?.fps || 30;
- const time = snapTimeToFrame(rawTime, projectFps);
- seek(time);
- },
- [
- duration,
- zoomLevel,
- seek,
- rulerScrollRef,
- tracksScrollRef,
- clearSelectedElements,
- isSelecting,
- justFinishedSelecting,
- ]
- );
+ const {
+ selectionBox,
+ handleMouseDown: handleSelectionMouseDown,
+ isSelecting,
+ shouldIgnoreClick,
+ } = useSelectionBox({
+ containerRef: tracksContainerRef,
+ onSelectionComplete: (elements) => {
+ setElementSelection({ elements });
+ },
+ tracksScrollRef,
+ zoomLevel,
+ });
- // Update timeline duration when tracks change
- useEffect(() => {
- const totalDuration = getTotalDuration();
- setDuration(Math.max(totalDuration, 10)); // Minimum 10 seconds for empty timeline
- }, [tracks, setDuration, getTotalDuration]);
+ const containerWidth = tracksContainerRef.current?.clientWidth || 1000;
+ const contentWidth =
+ timelineDuration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
+ const paddingPx = getTimelinePaddingPx({
+ containerWidth,
+ zoomLevel,
+ minZoom: minZoomLevel,
+ });
+ const dynamicTimelineWidth = Math.max(
+ contentWidth + paddingPx,
+ containerWidth,
+ );
- // Old marquee system removed - using new SelectionBox component instead
+ const showSnapIndicator =
+ snappingEnabled &&
+ currentSnapPoint !== null &&
+ (dragState.isDragging || isResizing);
- const handleDragEnter = (e: React.DragEvent) => {
- // When something is dragged over the timeline, show overlay
- e.preventDefault();
- // Don't show overlay for timeline elements - they're handled by tracks
- if (e.dataTransfer.types.includes("application/x-timeline-element")) {
- return;
- }
- dragCounterRef.current += 1;
- if (!isDragOver) {
- setIsDragOver(true);
- }
- };
+ const {
+ handleTracksMouseDown,
+ handleTracksClick,
+ handleRulerMouseDown,
+ handleRulerClick,
+ } = useTimelineSeek({
+ playheadRef,
+ trackLabelsRef,
+ rulerScrollRef: tracksScrollRef,
+ tracksScrollRef,
+ zoomLevel,
+ duration: timeline.getTotalDuration(),
+ isSelecting,
+ clearSelectedElements: clearElementSelection,
+ seek,
+ });
- const handleDragOver = (e: React.DragEvent) => {
- e.preventDefault();
- };
+ useScrollSync({
+ tracksScrollRef,
+ trackLabelsScrollRef,
+ });
- const handleDragLeave = (e: React.DragEvent) => {
- e.preventDefault();
+ const timelineHeaderHeight =
+ timelineHeaderRef.current?.getBoundingClientRect().height ?? 0;
- // Don't update state for timeline elements - they're handled by tracks
- if (e.dataTransfer.types.includes("application/x-timeline-element")) {
- return;
- }
+ return (
+ <section
+ className={
+ "bg-panel relative flex h-full flex-col overflow-hidden rounded-sm"
+ }
+ {...dragProps}
+ aria-label="Timeline"
+ >
+ <TimelineToolbar
+ zoomLevel={zoomLevel}
+ minZoom={minZoomLevel}
+ setZoomLevel={({ zoom }) => setZoomLevel(zoom)}
+ />
- dragCounterRef.current -= 1;
- if (dragCounterRef.current === 0) {
- setIsDragOver(false);
- }
- };
+ <div
+ className="relative flex flex-1 flex-col overflow-hidden"
+ ref={timelineRef}
+ >
+ <SnapIndicator
+ snapPoint={currentSnapPoint}
+ zoomLevel={zoomLevel}
+ tracks={tracks}
+ timelineRef={timelineRef}
+ trackLabelsRef={trackLabelsRef}
+ tracksScrollRef={tracksScrollRef}
+ isVisible={showSnapIndicator}
+ />
+ <div className="flex flex-1 overflow-hidden">
+ <div className="bg-panel flex w-28 shrink-0 flex-col border-r">
+ <div className="bg-panel flex h-4 items-center justify-between px-3">
+ <span className="opacity-0">.</span>
+ </div>
+ <div className="bg-panel flex h-4 items-center justify-between px-3">
+ <span className="opacity-0">.</span>
+ </div>
+ {tracks.length > 0 && (
+ <div
+ ref={trackLabelsRef}
+ className="bg-panel flex-1 overflow-y-auto"
+ style={{ paddingTop: TIMELINE_CONSTANTS.PADDING_TOP_PX }}
+ >
+ <ScrollArea className="size-full" ref={trackLabelsScrollRef}>
+ <div className="flex flex-col gap-1">
+ {tracks.map((track) => (
+ <div
+ key={track.id}
+ className="group flex items-center px-3"
+ style={{
+ height: `${getTrackHeight({ type: track.type })}px`,
+ }}
+ >
+ <div className="flex min-w-0 flex-1 items-center justify-end gap-2">
+ {process.env.NODE_ENV === "development" &&
+ isMainTrack(track) && (
+ <div className="bg-red-500 size-1.5 rounded-full" />
+ )}
+ {canTracktHaveAudio(track) && (
+ <TrackToggleIcon
+ isOff={track.muted}
+ icons={{
+ on: VolumeHighIcon,
+ off: VolumeOffIcon,
+ }}
+ onClick={() =>
+ editor.timeline.toggleTrackMute({
+ trackId: track.id,
+ })
+ }
+ />
+ )}
+ {canTrackBeHidden(track) && (
+ <TrackToggleIcon
+ isOff={track.hidden}
+ icons={{
+ on: ViewIcon,
+ off: ViewOffSlashIcon,
+ }}
+ onClick={() =>
+ editor.timeline.toggleTrackVisibility({
+ trackId: track.id,
+ })
+ }
+ />
+ )}
+ <TrackIcon track={track} />
+ </div>
+ </div>
+ ))}
+ </div>
+ </ScrollArea>
+ </div>
+ )}
+ </div>
- const handleDrop = async (e: React.DragEvent) => {
- // When media is dropped, add it as a new track/element
- e.preventDefault();
- setIsDragOver(false);
- dragCounterRef.current = 0;
-
- // Ignore timeline element drags - they're handled by track-specific handlers
- const hasTimelineElement = e.dataTransfer.types.includes(
- "application/x-timeline-element"
- );
- if (hasTimelineElement) {
- return;
- }
-
- const itemData = e.dataTransfer.getData("application/x-media-item");
- if (itemData) {
- try {
- const dragData: DragData = JSON.parse(itemData);
-
- if (dragData.type === "text") {
- // Always create new text track to avoid overlaps
- addElementToNewTrack(dragData);
- } else {
- // Handle media items
- const mediaItem = mediaFiles.find(
- (item: any) => item.id === dragData.id
- );
- if (!mediaItem) {
- toast.error("Media item not found");
- return;
- }
-
- addElementToNewTrack(mediaItem);
- }
- } catch (error) {
- console.error("Error parsing dropped item data:", error);
- toast.error("Failed to add item to timeline");
- }
- } else if (e.dataTransfer.files?.length > 0) {
- // Handle file drops by creating new tracks
- if (!activeProject) {
- toast.error("No active project");
- return;
- }
-
- try {
- const processedItems = await processMediaFiles(e.dataTransfer.files);
- for (const processedItem of processedItems) {
- await addMediaFile(activeProject.id, processedItem);
- const currentMediaFiles = mediaFiles;
- const addedItem = currentMediaFiles.find(
- (item) =>
- item.name === processedItem.name && item.url === processedItem.url
- );
- if (addedItem) {
- const trackType: TrackType =
- addedItem.type === "audio" ? "audio" : "media";
- const targetTrackId = useTimelineStore
- .getState()
- .insertTrackAt(trackType, 0);
-
- useTimelineStore.getState().addElementToTrack(targetTrackId, {
- type: "media",
- mediaId: addedItem.id,
- name: addedItem.name,
- duration: addedItem.duration || 5,
- startTime: currentTime,
- trimStart: 0,
- trimEnd: 0,
- });
- }
- }
- } catch (error) {
- // Show error if file processing fails
- console.error("Error processing external files:", error);
- toast.error("Failed to process dropped files");
- }
- }
- };
-
- const dragProps = {
- onDragEnter: handleDragEnter,
- onDragOver: handleDragOver,
- onDragLeave: handleDragLeave,
- onDrop: handleDrop,
- };
-
- // --- Scroll synchronization effect ---
- useEffect(() => {
- const rulerViewport = rulerScrollRef.current;
- const tracksViewport = tracksScrollRef.current;
- const trackLabelsViewport = trackLabelsScrollRef.current;
-
- if (!rulerViewport || !tracksViewport) return;
-
- // Horizontal scroll synchronization between ruler and tracks
- const handleRulerScroll = () => {
- const now = Date.now();
- if (isUpdatingRef.current || now - lastRulerSync.current < 16) return;
- lastRulerSync.current = now;
- isUpdatingRef.current = true;
- tracksViewport.scrollLeft = rulerViewport.scrollLeft;
- isUpdatingRef.current = false;
- };
- const handleTracksScroll = () => {
- const now = Date.now();
- if (isUpdatingRef.current || now - lastTracksSync.current < 16) return;
- lastTracksSync.current = now;
- isUpdatingRef.current = true;
- rulerViewport.scrollLeft = tracksViewport.scrollLeft;
- isUpdatingRef.current = false;
- };
-
- rulerViewport.addEventListener("scroll", handleRulerScroll);
- tracksViewport.addEventListener("scroll", handleTracksScroll);
-
- // Vertical scroll synchronization between track labels and tracks content
- if (trackLabelsViewport) {
- const handleTrackLabelsScroll = () => {
- const now = Date.now();
- if (isUpdatingRef.current || now - lastVerticalSync.current < 16)
- return;
- lastVerticalSync.current = now;
- isUpdatingRef.current = true;
- tracksViewport.scrollTop = trackLabelsViewport.scrollTop;
- isUpdatingRef.current = false;
- };
- const handleTracksVerticalScroll = () => {
- const now = Date.now();
- if (isUpdatingRef.current || now - lastVerticalSync.current < 16)
- return;
- lastVerticalSync.current = now;
- isUpdatingRef.current = true;
- trackLabelsViewport.scrollTop = tracksViewport.scrollTop;
- isUpdatingRef.current = false;
- };
-
- trackLabelsViewport.addEventListener("scroll", handleTrackLabelsScroll);
- tracksViewport.addEventListener("scroll", handleTracksVerticalScroll);
-
- return () => {
- rulerViewport.removeEventListener("scroll", handleRulerScroll);
- tracksViewport.removeEventListener("scroll", handleTracksScroll);
- trackLabelsViewport.removeEventListener(
- "scroll",
- handleTrackLabelsScroll
- );
- tracksViewport.removeEventListener(
- "scroll",
- handleTracksVerticalScroll
- );
- };
- }
-
- return () => {
- rulerViewport.removeEventListener("scroll", handleRulerScroll);
- tracksViewport.removeEventListener("scroll", handleTracksScroll);
- };
- }, []);
-
- return (
- <div
- className={
- "h-full flex flex-col transition-colors duration-200 relative bg-panel rounded-sm overflow-hidden"
- }
- {...dragProps}
- onMouseEnter={() => setIsInTimeline(true)}
- onMouseLeave={() => setIsInTimeline(false)}
- >
- <TimelineToolbar zoomLevel={zoomLevel} setZoomLevel={setZoomLevel} />
-
- {/* Timeline Container */}
- <div
- className="flex-1 flex flex-col overflow-hidden relative"
- ref={timelineRef}
- >
- <TimelinePlayhead
- currentTime={currentTime}
- duration={duration}
- zoomLevel={zoomLevel}
- tracks={tracks}
- seek={seek}
- rulerRef={rulerRef}
- rulerScrollRef={rulerScrollRef}
- tracksScrollRef={tracksScrollRef}
- trackLabelsRef={trackLabelsRef}
- timelineRef={timelineRef}
- playheadRef={playheadRef}
- isSnappingToPlayhead={
- showSnapIndicator && currentSnapPoint?.type === "playhead"
- }
- />
- <SnapIndicator
- snapPoint={currentSnapPoint}
- zoomLevel={zoomLevel}
- tracks={tracks}
- timelineRef={timelineRef}
- trackLabelsRef={trackLabelsRef}
- tracksScrollRef={tracksScrollRef}
- isVisible={showSnapIndicator}
- />
- {/* Timeline Header with Ruler */}
- <div className="flex bg-panel sticky top-0 z-10">
- {/* Track Labels Header */}
- <div className="w-28 shrink-0 bg-panel border-r flex items-center justify-between px-3 py-2">
- {/* Empty space */}
- <span className="text-sm font-medium text-muted-foreground opacity-0">
- .
- </span>
- </div>
-
- {/* Timeline Ruler */}
- <div
- className="flex-1 relative overflow-hidden h-10"
- onWheel={(e) => {
- // Check if this is horizontal scrolling - if so, don't handle it here
- if (e.shiftKey || Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
- return; // Let ScrollArea handle horizontal scrolling
- }
- handleWheel(e);
- }}
- onMouseDown={handleSelectionMouseDown}
- onClick={handleTimelineContentClick}
- data-ruler-area
- >
- <ScrollArea
- className="w-full"
- ref={rulerScrollRef}
- onScroll={(e) => {
- if (isUpdatingRef.current) return;
- isUpdatingRef.current = true;
- const tracksViewport = tracksScrollRef.current;
- if (tracksViewport) {
- tracksViewport.scrollLeft = (
- e.currentTarget as HTMLDivElement
- ).scrollLeft;
- }
- isUpdatingRef.current = false;
- }}
- >
- <div
- ref={rulerRef}
- className="relative h-10 select-none cursor-default"
- style={{
- width: `${dynamicTimelineWidth}px`,
- }}
- onMouseDown={handleRulerMouseDown}
- >
- <TimelineCacheIndicator
- duration={duration}
- zoomLevel={zoomLevel}
- tracks={tracks}
- mediaFiles={mediaFiles}
- activeProject={activeProject}
- getRenderStatus={getRenderStatus}
- />
- {/* Time markers */}
- {(() => {
- // Calculate appropriate time interval based on zoom level
- const getTimeInterval = (zoom: number) => {
- const pixelsPerSecond =
- TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoom;
- if (pixelsPerSecond >= 200) return 0.1; // Every 0.1s when very zoomed in
- if (pixelsPerSecond >= 100) return 0.5; // Every 0.5s when zoomed in
- if (pixelsPerSecond >= 50) return 1; // Every 1s at normal zoom
- if (pixelsPerSecond >= 25) return 2; // Every 2s when zoomed out
- if (pixelsPerSecond >= 12) return 5; // Every 5s when more zoomed out
- if (pixelsPerSecond >= 6) return 10; // Every 10s when very zoomed out
- return 30; // Every 30s when extremely zoomed out
- };
-
- const interval = getTimeInterval(zoomLevel);
- const markerCount = Math.ceil(duration / interval) + 1;
-
- return Array.from({ length: markerCount }, (_, i) => {
- const time = i * interval;
- if (time > duration) return null;
-
- const isMainMarker =
- time % (interval >= 1 ? Math.max(1, interval) : 1) === 0;
-
- return (
- <TimelineMarker
- key={i}
- time={time}
- zoomLevel={zoomLevel}
- interval={interval}
- isMainMarker={isMainMarker}
- />
- );
- }).filter(Boolean);
- })()}
-
- {/* Bookmark markers */}
- {(() => {
- const { activeProject } = useProjectStore.getState();
- if (!activeProject?.bookmarks?.length) return null;
-
- return activeProject.bookmarks.map((bookmarkTime, i) => (
- <div
- key={`bookmark-${i}`}
- className="absolute top-0 h-10 w-0.5 !bg-primary cursor-pointer"
- style={{
- left: `${bookmarkTime * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
- }}
- onClick={(e) => {
- e.stopPropagation();
- usePlaybackStore.getState().seek(bookmarkTime);
- }}
- >
- <div className="absolute top-[-1px] left-[-5px] text-primary">
- <Bookmark className="h-3 w-3 fill-primary" />
- </div>
- </div>
- ));
- })()}
- </div>
- </ScrollArea>
- </div>
- </div>
-
- {/* Tracks Area */}
- <div className="flex-1 flex overflow-hidden">
- {/* Track Labels */}
- {tracks.length > 0 && (
- <div
- ref={trackLabelsRef}
- className="w-28 shrink-0 border-r overflow-y-auto z-100 bg-panel"
- data-track-labels
- >
- <ScrollArea className="w-full h-full" ref={trackLabelsScrollRef}>
- <div className="flex flex-col gap-1">
- {tracks.map((track) => (
- <div
- key={track.id}
- className="flex items-center px-3 group"
- style={{ height: `${getTrackHeight(track.type)}px` }}
- >
- <div className="flex items-center justify-end flex-1 min-w-0 gap-2">
- {track.muted ? (
- <VolumeOff
- className="h-4 w-4 text-destructive cursor-pointer"
- onClick={() => toggleTrackMute(track.id)}
- />
- ) : (
- <Volume2
- className="h-4 w-4 text-muted-foreground cursor-pointer"
- onClick={() => toggleTrackMute(track.id)}
- />
- )}
- <Eye className="h-4 w-4 text-muted-foreground" />
- <TrackIcon track={track} />
- </div>
- </div>
- ))}
- </div>
- </ScrollArea>
- </div>
- )}
-
- {/* Timeline Tracks Content */}
- <div
- className="flex-1 relative overflow-hidden"
- onWheel={(e) => {
- // Check if this is horizontal scrolling - if so, don't handle it here
- if (e.shiftKey || Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
- return; // Let ScrollArea handle horizontal scrolling
- }
- handleWheel(e);
- }}
- onMouseDown={(e) => {
- handleTimelineMouseDown(e);
- handleSelectionMouseDown(e);
- }}
- onClick={handleTimelineContentClick}
- ref={tracksContainerRef}
- >
- <SelectionBox
- startPos={selectionBox?.startPos || null}
- currentPos={selectionBox?.currentPos || null}
- containerRef={tracksContainerRef}
- isActive={selectionBox?.isActive || false}
- />
- <ScrollArea
- className="w-full h-full"
- ref={tracksScrollRef}
- onScroll={(e) => {
- if (isUpdatingRef.current) return;
- isUpdatingRef.current = true;
- const rulerViewport = rulerScrollRef.current;
- if (rulerViewport) {
- rulerViewport.scrollLeft = (
- e.currentTarget as HTMLDivElement
- ).scrollLeft;
- }
- isUpdatingRef.current = false;
- }}
- >
- <div
- className="relative flex-1"
- style={{
- height: `${Math.max(
- 200,
- Math.min(800, getTotalTracksHeight(tracks))
- )}px`,
- width: `${dynamicTimelineWidth}px`,
- }}
- >
- {tracks.length === 0 ? (
- <div />
- ) : (
- <>
- {tracks.map((track, index) => (
- <ContextMenu key={track.id}>
- <ContextMenuTrigger asChild>
- <div
- className="absolute left-0 right-0"
- style={{
- top: `${getCumulativeHeightBefore(
- tracks,
- index
- )}px`,
- height: `${getTrackHeight(track.type)}px`,
- }}
- onClick={(e) => {
- // If clicking empty area (not on a element), deselect all elements
- if (
- !(e.target as HTMLElement).closest(
- ".timeline-element"
- )
- ) {
- clearSelectedElements();
- }
- }}
- >
- <TimelineTrackContent
- track={track}
- zoomLevel={zoomLevel}
- onSnapPointChange={handleSnapPointChange}
- rulerScrollRef={rulerScrollRef}
- tracksScrollRef={tracksScrollRef}
- />
- </div>
- </ContextMenuTrigger>
- <ContextMenuContent className="z-200">
- <ContextMenuItem
- onClick={(e) => {
- e.stopPropagation();
- toggleTrackMute(track.id);
- }}
- >
- {track.muted ? "Unmute Track" : "Mute Track"}
- </ContextMenuItem>
- <ContextMenuItem onClick={(e) => e.stopPropagation()}>
- Track settings (soon)
- </ContextMenuItem>
- </ContextMenuContent>
- </ContextMenu>
- ))}
- </>
- )}
- </div>
- </ScrollArea>
- </div>
- </div>
- </div>
- </div>
- );
+ <div
+ className="relative flex flex-1 flex-col overflow-hidden"
+ ref={tracksContainerRef}
+ >
+ <SelectionBox
+ startPos={selectionBox?.startPos || null}
+ currentPos={selectionBox?.currentPos || null}
+ containerRef={tracksContainerRef}
+ isActive={selectionBox?.isActive || false}
+ />
+ <DragLine
+ dropTarget={dropTarget}
+ tracks={timeline.getTracks()}
+ isVisible={isDragOver}
+ headerHeight={timelineHeaderHeight}
+ />
+ <DragLine
+ dropTarget={dragDropTarget}
+ tracks={timeline.getTracks()}
+ isVisible={dragState.isDragging}
+ headerHeight={timelineHeaderHeight}
+ />
+ <ScrollArea
+ className="size-full overflow-y-hidden"
+ ref={tracksScrollRef}
+ onMouseDown={(event) => {
+ const isDirectTarget = event.target === event.currentTarget;
+ if (!isDirectTarget) return;
+ event.stopPropagation();
+ handleTracksMouseDown(event);
+ handleSelectionMouseDown(event);
+ }}
+ onClick={(event) => {
+ const isDirectTarget = event.target === event.currentTarget;
+ if (!isDirectTarget) return;
+ event.stopPropagation();
+ handleTracksClick(event);
+ }}
+ onWheel={(event) => {
+ if (
+ event.shiftKey ||
+ Math.abs(event.deltaX) > Math.abs(event.deltaY)
+ ) {
+ return;
+ }
+ handleWheel(event);
+ }}
+ onScroll={() => {
+ saveScrollPosition();
+ }}
+ >
+ <div
+ className="relative"
+ style={{
+ width: `${dynamicTimelineWidth}px`,
+ }}
+ >
+ <div
+ ref={timelineHeaderRef}
+ className="bg-panel sticky top-0 z-30 flex flex-col"
+ >
+ <TimelineRuler
+ zoomLevel={zoomLevel}
+ dynamicTimelineWidth={dynamicTimelineWidth}
+ rulerRef={rulerRef}
+ handleWheel={handleWheel}
+ handleTimelineContentClick={handleRulerClick}
+ handleRulerTrackingMouseDown={handleRulerMouseDown}
+ handleRulerMouseDown={handlePlayheadRulerMouseDown}
+ />
+ <TimelineBookmarksRow
+ zoomLevel={zoomLevel}
+ dynamicTimelineWidth={dynamicTimelineWidth}
+ handleWheel={handleWheel}
+ handleTimelineContentClick={handleRulerClick}
+ handleRulerTrackingMouseDown={handleRulerMouseDown}
+ handleRulerMouseDown={handlePlayheadRulerMouseDown}
+ />
+ </div>
+ <TimelinePlayhead
+ zoomLevel={zoomLevel}
+ rulerRef={rulerRef}
+ rulerScrollRef={tracksScrollRef}
+ tracksScrollRef={tracksScrollRef}
+ timelineRef={timelineRef}
+ playheadRef={playheadRef}
+ isSnappingToPlayhead={
+ showSnapIndicator && currentSnapPoint?.type === "playhead"
+ }
+ />
+ <div
+ className="relative"
+ style={{
+ height: `${Math.max(
+ tracksContainerHeight.min,
+ Math.min(
+ tracksContainerHeight.max,
+ getTotalTracksHeight({ tracks }),
+ ),
+ )}px`,
+ }}
+ >
+ {tracks.length === 0 ? (
+ <div />
+ ) : (
+ tracks.map((track, index) => (
+ <ContextMenu key={track.id}>
+ <ContextMenuTrigger asChild>
+ <div
+ className="absolute right-0 left-0"
+ style={{
+ top: `${getCumulativeHeightBefore({
+ tracks,
+ trackIndex: index,
+ })}px`,
+ height: `${getTrackHeight({
+ type: track.type,
+ })}px`,
+ }}
+ >
+ <TimelineTrackContent
+ track={track}
+ zoomLevel={zoomLevel}
+ dragState={dragState}
+ rulerScrollRef={tracksScrollRef}
+ tracksScrollRef={tracksScrollRef}
+ lastMouseXRef={lastMouseXRef}
+ onSnapPointChange={handleSnapPointChange}
+ onResizeStateChange={handleResizeStateChange}
+ onElementMouseDown={handleElementMouseDown}
+ onElementClick={handleElementClick}
+ onTrackMouseDown={handleSelectionMouseDown}
+ shouldIgnoreClick={shouldIgnoreClick}
+ />
+ </div>
+ </ContextMenuTrigger>
+ <ContextMenuContent className="z-200 w-40">
+ <ContextMenuItem
+ icon={<HugeiconsIcon icon={TaskAdd02Icon} />}
+ onClick={(e) => {
+ e.stopPropagation();
+ invokeAction("paste-copied");
+ }}
+ >
+ Paste elements
+ </ContextMenuItem>
+ <ContextMenuItem
+ onClick={(e) => {
+ e.stopPropagation();
+ timeline.toggleTrackMute({
+ trackId: track.id,
+ });
+ }}
+ >
+ <HugeiconsIcon icon={VolumeHighIcon} />
+ <span>
+ {canTracktHaveAudio(track) && track.muted
+ ? "Unmute track"
+ : "Mute track"}
+ </span>
+ </ContextMenuItem>
+ <ContextMenuItem
+ onClick={(e) => {
+ e.stopPropagation();
+ timeline.toggleTrackVisibility({
+ trackId: track.id,
+ });
+ }}
+ >
+ <HugeiconsIcon icon={ViewIcon} />
+ <span>
+ {canTrackBeHidden(track) && track.hidden
+ ? "Show track"
+ : "Hide track"}
+ </span>
+ </ContextMenuItem>
+ <ContextMenuItem
+ onClick={(e) => {
+ e.stopPropagation();
+ timeline.removeTrack({
+ trackId: track.id,
+ });
+ }}
+ variant="destructive"
+ >
+ <HugeiconsIcon icon={Delete02Icon} />
+ Delete track
+ </ContextMenuItem>
+ </ContextMenuContent>
+ </ContextMenu>
+ ))
+ )}
+ </div>
+ </div>
+ </ScrollArea>
+ </div>
+ </div>
+ </div>
+ </section>
+ );
}
function TrackIcon({ track }: { track: TimelineTrack }) {
- return (
- <>
- {track.type === "media" && (
- <Video className="w-4 h-4 shrink-0 text-muted-foreground" />
- )}
- {track.type === "text" && (
- <TypeIcon className="w-4 h-4 shrink-0 text-muted-foreground" />
- )}
- {track.type === "audio" && (
- <Music className="w-4 h-4 shrink-0 text-muted-foreground" />
- )}
- </>
- );
+ return <>{TRACK_ICONS[track.type]}</>;
+}
+
+function TrackToggleIcon({
+ isOff,
+ icons,
+ onClick,
+}: {
+ isOff: boolean;
+ icons: {
+ on: IconSvgElement;
+ off: IconSvgElement;
+ };
+ onClick: () => void;
+}) {
+ return (
+ <>
+ {isOff ? (
+ <HugeiconsIcon
+ icon={icons.off}
+ className="text-destructive size-4 cursor-pointer"
+ onClick={onClick}
+ />
+ ) : (
+ <HugeiconsIcon
+ icon={icons.on}
+ className="text-muted-foreground size-4 cursor-pointer"
+ onClick={onClick}
+ />
+ )}
+ </>
+ );
}
diff --git a/apps/web/src/components/editor/timeline/snap-indicator.tsx b/apps/web/src/components/editor/timeline/snap-indicator.tsx
new file mode 100644
index 00000000..6bfdf086
--- /dev/null
+++ b/apps/web/src/components/editor/timeline/snap-indicator.tsx
@@ -0,0 +1,52 @@
+"use client";
+
+import { useSnapIndicatorPosition } from "@/hooks/timeline/use-snap-indicator-position";
+import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
+import type { TimelineTrack } from "@/types/timeline";
+
+interface SnapIndicatorProps {
+ snapPoint: SnapPoint | null;
+ zoomLevel: number;
+ isVisible: boolean;
+ tracks: TimelineTrack[];
+ timelineRef: React.RefObject<HTMLDivElement>;
+ trackLabelsRef?: React.RefObject<HTMLDivElement>;
+ tracksScrollRef: React.RefObject<HTMLDivElement>;
+}
+
+export function SnapIndicator({
+ snapPoint,
+ zoomLevel,
+ isVisible,
+ tracks,
+ timelineRef,
+ trackLabelsRef,
+ tracksScrollRef,
+}: SnapIndicatorProps) {
+ const { leftPosition, topPosition, height } = useSnapIndicatorPosition({
+ snapPoint,
+ zoomLevel,
+ tracks,
+ timelineRef,
+ trackLabelsRef,
+ tracksScrollRef,
+ });
+
+ if (!isVisible || !snapPoint) {
+ return null;
+ }
+
+ return (
+ <div
+ className="pointer-events-none absolute z-90"
+ style={{
+ left: `${leftPosition}px`,
+ top: topPosition,
+ height: `${height}px`,
+ width: "2px",
+ }}
+ >
+ <div className={"bg-primary/40 h-full w-0.5 opacity-80"} />
+ </div>
+ );
+}
diff --git a/apps/web/src/components/editor/timeline/timeline-cache-indicator.tsx b/apps/web/src/components/editor/timeline/timeline-cache-indicator.tsx
deleted file mode 100644
index 7def2985..00000000
--- a/apps/web/src/components/editor/timeline/timeline-cache-indicator.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-"use client";
-
-import { cn } from "@/lib/utils";
-import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
-import { TimelineTrack } from "@/types/timeline";
-import { MediaFile } from "@/types/media";
-import { TProject } from "@/types/project";
-import { useSceneStore } from "@/stores/scene-store";
-
-interface CacheSegment {
- startTime: number;
- endTime: number;
- cached: boolean;
-}
-
-interface TimelineCacheIndicatorProps {
- duration: number;
- zoomLevel: number;
- tracks: TimelineTrack[];
- mediaFiles: MediaFile[];
- activeProject: TProject | null;
- getRenderStatus: (
- time: number,
- tracks: TimelineTrack[],
- mediaFiles: MediaFile[],
- activeProject: TProject | null,
- sceneId?: string
- ) => "cached" | "not-cached";
-}
-
-export function TimelineCacheIndicator({
- duration,
- zoomLevel,
- tracks,
- mediaFiles,
- activeProject,
- getRenderStatus,
-}: TimelineCacheIndicatorProps) {
- const { currentScene } = useSceneStore();
-
- // Calculate cache segments by sampling the timeline
- const calculateCacheSegments = (): CacheSegment[] => {
- const segments: CacheSegment[] = [];
- const sampleRate = 10; // Sample every 0.1 seconds
- const totalSamples = Math.ceil(duration * sampleRate);
-
- if (totalSamples === 0) {
- return [{ startTime: 0, endTime: duration, cached: false }];
- }
-
- let currentSegment: CacheSegment | null = null;
-
- for (let i = 0; i <= totalSamples; i++) {
- const time = i / sampleRate;
- const cached =
- getRenderStatus(
- time,
- tracks,
- mediaFiles,
- activeProject,
- currentScene?.id
- ) === "cached";
-
- if (!currentSegment) {
- // Start first segment
- currentSegment = {
- startTime: time,
- endTime: time,
- cached,
- };
- } else if (currentSegment.cached === cached) {
- // Extend current segment
- currentSegment.endTime = time;
- } else {
- // Finish current segment and start new one
- segments.push(currentSegment);
- currentSegment = {
- startTime: time,
- endTime: time,
- cached,
- };
- }
- }
-
- // Add the last segment
- if (currentSegment) {
- currentSegment.endTime = duration;
- segments.push(currentSegment);
- }
-
- return segments;
- };
-
- const cacheSegments = calculateCacheSegments();
-
- return (
- <div className="absolute top-0 left-0 right-0 h-px z-10 pointer-events-none">
- {cacheSegments.map((segment, index) => {
- const startX =
- segment.startTime * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
- const endX =
- segment.endTime * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
- const width = endX - startX;
-
- return (
- <div
- key={index}
- className={cn(
- "absolute top-0 h-px",
- segment.cached ? "bg-primary" : "bg-transparent"
- )}
- style={{
- left: `${startX}px`,
- width: `${width}px`,
- }}
- title={
- segment.cached
- ? "Cached (fast playback)"
- : "Not cached (will render)"
- }
- />
- );
- })}
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/timeline/timeline-element.tsx b/apps/web/src/components/editor/timeline/timeline-element.tsx
index ca47d3ca..fd292ad8 100644
--- a/apps/web/src/components/editor/timeline/timeline-element.tsx
+++ b/apps/web/src/components/editor/timeline/timeline-element.tsx
@@ -1,387 +1,554 @@
"use client";
+import { useEditor } from "@/hooks/use-editor";
+import { useAssetsPanelStore } from "@/stores/assets-panel-store";
+import AudioWaveform from "./audio-waveform";
+import { useTimelineElementResize } from "@/hooks/timeline/element/use-element-resize";
+import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import {
- Scissors,
- Trash2,
- Copy,
- Search,
- RefreshCw,
- EyeOff,
- Eye,
- Volume2,
- VolumeX,
-} from "lucide-react";
-import { useMediaStore } from "@/stores/media-store";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { usePlaybackStore } from "@/stores/playback-store";
-import AudioWaveform from "../audio-waveform";
-import { toast } from "sonner";
-import { TimelineElementProps, MediaElement } from "@/types/timeline";
-import { useTimelineElementResize } from "@/hooks/use-timeline-element-resize";
+ getTrackClasses,
+ getTrackHeight,
+ canElementHaveAudio,
+ canElementBeHidden,
+ hasMediaId,
+} from "@/lib/timeline";
import {
- getTrackElementClasses,
- TIMELINE_CONSTANTS,
- getTrackHeight,
-} from "@/constants/timeline-constants";
-import {
- ContextMenu,
- ContextMenuContent,
- ContextMenuItem,
- ContextMenuSeparator,
- ContextMenuTrigger,
+ ContextMenu,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuSeparator,
+ ContextMenuTrigger,
} from "../../ui/context-menu";
-import { useMediaPanelStore } from "../media-panel/store";
+import type {
+ TimelineElement as TimelineElementType,
+ TimelineTrack,
+ ElementDragState,
+} from "@/types/timeline";
+import type { MediaAsset } from "@/types/assets";
+import { mediaSupportsAudio } from "@/lib/media/media-utils";
+import { getActionDefinition, type TAction, invokeAction } from "@/lib/actions";
+import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
+import Image from "next/image";
+import {
+ ScissorIcon,
+ Delete02Icon,
+ Copy01Icon,
+ ViewIcon,
+ ViewOffSlashIcon,
+ VolumeHighIcon,
+ VolumeOffIcon,
+ VolumeMute02Icon,
+ Search01Icon,
+ Exchange01Icon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { uppercase } from "@/utils/string";
+import type { ComponentProps } from "react";
-export function TimelineElement({
- element,
- track,
- zoomLevel,
- isSelected,
- onElementMouseDown,
- onElementClick,
-}: TimelineElementProps) {
- const { mediaFiles } = useMediaStore();
- const {
- dragState,
- copySelected,
- selectedElements,
- deleteSelected,
- splitSelected,
- toggleSelectedHidden,
- toggleSelectedMuted,
- duplicateElement,
- revealElementInMedia,
- replaceElementWithFile,
- getContextMenuState,
- } = useTimelineStore();
- const { currentTime } = usePlaybackStore();
+function getDisplayShortcut(action: TAction) {
+ const { defaultShortcuts } = getActionDefinition(action);
+ if (!defaultShortcuts?.length) {
+ return "";
+ }
- const mediaItem =
- element.type === "media"
- ? mediaFiles.find((file) => file.id === element.mediaId)
- : null;
- const hasAudio = mediaItem?.type === "audio" || mediaItem?.type === "video";
+ return uppercase({
+ string: defaultShortcuts[0].replace("+", " "),
+ });
+}
- const { resizing, handleResizeStart, handleResizeMove, handleResizeEnd } =
- useTimelineElementResize({
- element,
- track,
- zoomLevel,
- });
+interface TimelineElementProps {
+ element: TimelineElementType;
+ track: TimelineTrack;
+ zoomLevel: number;
+ isSelected: boolean;
+ onSnapPointChange?: (snapPoint: SnapPoint | null) => void;
+ onResizeStateChange?: (params: { isResizing: boolean }) => void;
+ onElementMouseDown: (
+ e: React.MouseEvent,
+ element: TimelineElementType,
+ ) => void;
+ onElementClick: (e: React.MouseEvent, element: TimelineElementType) => void;
+ dragState: ElementDragState;
+}
- const {
- isMultipleSelected,
- isCurrentElementSelected,
- hasAudioElements,
- canSplitSelected,
- } = getContextMenuState(track.id, element.id);
+export function TimelineElement({
+ element,
+ track,
+ zoomLevel,
+ isSelected,
+ onSnapPointChange,
+ onResizeStateChange,
+ onElementMouseDown,
+ onElementClick,
+ dragState,
+}: TimelineElementProps) {
+ const editor = useEditor();
+ const { selectedElements } = useElementSelection();
+ const { requestRevealMedia } = useAssetsPanelStore();
- const effectiveDuration =
- element.duration - element.trimStart - element.trimEnd;
- const elementWidth = Math.max(
- TIMELINE_CONSTANTS.ELEMENT_MIN_WIDTH,
- effectiveDuration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel
- );
+ const mediaAssets = editor.media.getAssets();
+ let mediaAsset: MediaAsset | null = null;
- const isBeingDragged = dragState.elementId === element.id;
- const elementStartTime =
- isBeingDragged && dragState.isDragging
- ? dragState.currentTime
- : element.startTime;
+ if (hasMediaId(element)) {
+ mediaAsset =
+ mediaAssets.find((asset) => asset.id === element.mediaId) ?? null;
+ }
- const elementLeft = elementStartTime * 50 * zoomLevel;
+ const hasAudio = mediaSupportsAudio({ media: mediaAsset });
- const handleElementSplitContext = (e: React.MouseEvent) => {
- e.stopPropagation();
- splitSelected(
- currentTime,
- isMultipleSelected && isCurrentElementSelected ? undefined : track.id,
- isMultipleSelected && isCurrentElementSelected ? undefined : element.id
- );
- };
+ const { handleResizeStart, isResizing, currentStartTime, currentDuration } =
+ useTimelineElementResize({
+ element,
+ track,
+ zoomLevel,
+ onSnapPointChange,
+ onResizeStateChange,
+ });
- const handleElementDuplicateContext = (e: React.MouseEvent) => {
- e.stopPropagation();
- duplicateElement(track.id, element.id);
- };
+ const isCurrentElementSelected = selectedElements.some(
+ (selected) =>
+ selected.elementId === element.id && selected.trackId === track.id,
+ );
- const handleElementCopyContext = (e: React.MouseEvent) => {
- e.stopPropagation();
- copySelected();
- };
+ const isBeingDragged = dragState.elementId === element.id;
+ const dragOffsetY =
+ isBeingDragged && dragState.isDragging
+ ? dragState.currentMouseY - dragState.startMouseY
+ : 0;
+ const elementStartTime =
+ isBeingDragged && dragState.isDragging
+ ? dragState.currentTime
+ : element.startTime;
+ const displayedStartTime = isResizing ? currentStartTime : elementStartTime;
+ const displayedDuration = isResizing ? currentDuration : element.duration;
+ const elementWidth =
+ displayedDuration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
+ const elementLeft = displayedStartTime * 50 * zoomLevel;
- const handleElementDeleteContext = (e: React.MouseEvent) => {
- e.stopPropagation();
- deleteSelected(
- isMultipleSelected && isCurrentElementSelected ? undefined : track.id,
- isMultipleSelected && isCurrentElementSelected ? undefined : element.id
- );
- };
+ const handleRevealInMedia = ({ event }: { event: React.MouseEvent }) => {
+ event.stopPropagation();
+ if (hasMediaId(element)) {
+ requestRevealMedia(element.mediaId);
+ }
+ };
- const handleToggleElementContext = (e: React.MouseEvent) => {
- e.stopPropagation();
+ const isMuted = canElementHaveAudio(element) && element.muted === true;
- if (hasAudio && element.type === "media") {
- toggleSelectedMuted(
- isMultipleSelected && isCurrentElementSelected ? undefined : track.id,
- isMultipleSelected && isCurrentElementSelected ? undefined : element.id
- );
- } else {
- toggleSelectedHidden(
- isMultipleSelected && isCurrentElementSelected ? undefined : track.id,
- isMultipleSelected && isCurrentElementSelected ? undefined : element.id
- );
- }
- };
+ return (
+ <ContextMenu>
+ <ContextMenuTrigger asChild>
+ <div
+ className={`absolute top-0 h-full select-none ${
+ isBeingDragged ? "z-30" : "z-10"
+ }`}
+ style={{
+ left: `${elementLeft}px`,
+ width: `${elementWidth}px`,
+ transform:
+ isBeingDragged && dragState.isDragging
+ ? `translate3d(0, ${dragOffsetY}px, 0)`
+ : undefined,
+ }}
+ >
+ <ElementInner
+ element={element}
+ track={track}
+ isSelected={isSelected}
+ isBeingDragged={isBeingDragged}
+ hasAudio={hasAudio}
+ isMuted={isMuted}
+ mediaAssets={mediaAssets}
+ onElementClick={onElementClick}
+ onElementMouseDown={onElementMouseDown}
+ handleResizeStart={handleResizeStart}
+ />
+ </div>
+ </ContextMenuTrigger>
+ <ContextMenuContent className="z-200 w-64">
+ <ActionMenuItem action="split" icon={<HugeiconsIcon icon={ScissorIcon} />}>
+ Split
+ </ActionMenuItem>
+ <CopyMenuItem />
+ {canElementHaveAudio(element) && hasAudio && (
+ <MuteMenuItem
+ isMultipleSelected={selectedElements.length > 1}
+ isCurrentElementSelected={isCurrentElementSelected}
+ isMuted={isMuted}
+ />
+ )}
+ {canElementBeHidden(element) && (
+ <VisibilityMenuItem
+ element={element}
+ isMultipleSelected={selectedElements.length > 1}
+ isCurrentElementSelected={isCurrentElementSelected}
+ />
+ )}
+ {selectedElements.length === 1 && (
+ <ActionMenuItem action="duplicate-selected" icon={<HugeiconsIcon icon={Copy01Icon} />}>
+ Duplicate
+ </ActionMenuItem>
+ )}
+ {selectedElements.length === 1 && hasMediaId(element) && (
+ <>
+ <ContextMenuItem
+ icon={<HugeiconsIcon icon={Search01Icon} />}
+ onClick={(event) => handleRevealInMedia({ event })}
+ >
+ Reveal media
+ </ContextMenuItem>
+ <ContextMenuItem
+ icon={<HugeiconsIcon icon={Exchange01Icon} />}
+ disabled
+ >
+ Replace media
+ </ContextMenuItem>
+ </>
+ )}
+ <ContextMenuSeparator />
+ <DeleteMenuItem
+ isMultipleSelected={selectedElements.length > 1}
+ isCurrentElementSelected={isCurrentElementSelected}
+ elementType={element.type}
+ selectedCount={selectedElements.length}
+ />
+ </ContextMenuContent>
+ </ContextMenu>
+ );
+}
- const handleReplaceClip = (e: React.MouseEvent) => {
- e.stopPropagation();
+function ElementInner({
+ element,
+ track,
+ isSelected,
+ isBeingDragged,
+ hasAudio,
+ isMuted,
+ mediaAssets,
+ onElementClick,
+ onElementMouseDown,
+ handleResizeStart,
+}: {
+ element: TimelineElementType;
+ track: TimelineTrack;
+ isSelected: boolean;
+ isBeingDragged: boolean;
+ hasAudio: boolean;
+ isMuted: boolean;
+ mediaAssets: MediaAsset[];
+ onElementClick: (e: React.MouseEvent, element: TimelineElementType) => void;
+ onElementMouseDown: (
+ e: React.MouseEvent,
+ element: TimelineElementType,
+ ) => void;
+ handleResizeStart: (params: {
+ e: React.MouseEvent;
+ elementId: string;
+ side: "left" | "right";
+ }) => void;
+}) {
+ return (
+ <div
+ className={`relative h-full cursor-pointer overflow-hidden rounded-[0.5rem] ${getTrackClasses(
+ {
+ type: track.type,
+ },
+ )} ${isBeingDragged ? "z-30" : "z-10"} ${canElementBeHidden(element) && element.hidden ? "opacity-50" : ""}`}
+ >
+ <button
+ type="button"
+ className="absolute inset-0 size-full cursor-pointer"
+ onClick={(e) => onElementClick(e, element)}
+ onMouseDown={(e) => onElementMouseDown(e, element)}
+ >
+ <div className="absolute inset-0 flex h-full items-center">
+ <ElementContent
+ element={element}
+ track={track}
+ isSelected={isSelected}
+ mediaAssets={mediaAssets}
+ />
+ </div>
- const input = document.createElement("input");
- input.type = "file";
- input.accept = "video/*,audio/*,image/*";
- input.onchange = async (e) => {
- const file = (e.target as HTMLInputElement).files?.[0];
- if (file) {
- await replaceElementWithFile(track.id, element.id, file);
- }
- };
- input.click();
- };
+ {(hasAudio
+ ? isMuted
+ : canElementBeHidden(element) && element.hidden) && (
+ <div className="bg-opacity-50 pointer-events-none absolute inset-0 flex items-center justify-center bg-black">
+ {hasAudio ? (
+ <HugeiconsIcon
+ icon={VolumeHighIcon}
+ className="size-6 text-white"
+ />
+ ) : (
+ <HugeiconsIcon
+ icon={VolumeOffIcon}
+ className="size-6 text-white"
+ />
+ )}
+ </div>
+ )}
+ </button>
- const handleRevealInMedia = (e: React.MouseEvent) => {
- e.stopPropagation();
- revealElementInMedia(element.id);
- };
+ {isSelected && (
+ <>
+ <ResizeHandle
+ side="left"
+ elementId={element.id}
+ handleResizeStart={handleResizeStart}
+ />
+ <ResizeHandle
+ side="right"
+ elementId={element.id}
+ handleResizeStart={handleResizeStart}
+ />
+ </>
+ )}
+ </div>
+ );
+}
+
+function ResizeHandle({
+ side,
+ elementId,
+ handleResizeStart,
+}: {
+ side: "left" | "right";
+ elementId: string;
+ handleResizeStart: (params: {
+ e: React.MouseEvent;
+ elementId: string;
+ side: "left" | "right";
+ }) => void;
+}) {
+ const isLeft = side === "left";
+ return (
+ <button
+ type="button"
+ className={`bg-primary absolute top-0 bottom-0 z-50 flex w-[0.6rem] items-center justify-center ${isLeft ? "left-0 cursor-w-resize" : "right-0 cursor-e-resize"}`}
+ onMouseDown={(e) => handleResizeStart({ e, elementId, side })}
+ aria-label={`${isLeft ? "Left" : "Right"} resize handle`}
+ >
+ <div className="bg-foreground h-[1.5rem] w-[0.2rem] rounded-full" />
+ </button>
+ );
+}
- const renderElementContent = () => {
- if (element.type === "text") {
- return (
- <div className="w-full h-full flex items-center justify-start pl-2">
- <span className="text-xs text-white truncate">{element.content}</span>
- </div>
- );
- }
+function ElementContent({
+ element,
+ track,
+ isSelected,
+ mediaAssets,
+}: {
+ element: TimelineElementType;
+ track: TimelineTrack;
+ isSelected: boolean;
+ mediaAssets: MediaAsset[];
+}) {
+ if (element.type === "text") {
+ return (
+ <div className="flex size-full items-center justify-start pl-2">
+ <span className="truncate text-xs text-white">{element.content}</span>
+ </div>
+ );
+ }
- const mediaItem = mediaFiles.find((file) => file.id === element.mediaId);
- if (!mediaItem) {
- return (
- <span className="text-xs text-foreground/80 truncate">
- {element.name}
- </span>
- );
- }
+ if (element.type === "sticker") {
+ return (
+ <div className="flex size-full items-center gap-2 pl-2">
+ <Image
+ src={`https://api.iconify.design/${element.iconName}.svg?width=20&height=20`}
+ alt={element.name}
+ className="size-5 shrink-0"
+ width={20}
+ height={20}
+ unoptimized
+ />
+ <span className="truncate text-xs text-white">{element.name}</span>
+ </div>
+ );
+ }
- if (
- mediaItem.type === "image" ||
- (mediaItem.type === "video" && mediaItem.thumbnailUrl)
- ) {
- const trackHeight = getTrackHeight(track.type);
- const tileWidth = trackHeight * (16 / 9);
+ if (element.type === "audio") {
+ const audioBuffer =
+ element.sourceType === "library" ? element.buffer : undefined;
- const imageUrl =
- mediaItem.type === "image" ? mediaItem.url : mediaItem.thumbnailUrl;
+ const audioUrl =
+ element.sourceType === "library"
+ ? element.sourceUrl
+ : mediaAssets.find((asset) => asset.id === element.mediaId)?.url;
- return (
- <div className="w-full h-full flex items-center justify-center">
- <div
- className={`w-full h-full relative ${
- isSelected ? "bg-primary" : "bg-transparent"
- }`}
- >
- <div
- className={`absolute top-[0.25rem] bottom-[0.25rem] left-0 right-0`}
- style={{
- backgroundImage: imageUrl ? `url(${imageUrl})` : "none",
- backgroundRepeat: "repeat-x",
- backgroundSize: `${tileWidth}px ${trackHeight}px`,
- backgroundPosition: "left center",
- pointerEvents: "none",
- }}
- aria-label={`Tiled ${mediaItem.type === "image" ? "background" : "thumbnail"} of ${mediaItem.name}`}
- />
- </div>
- </div>
- );
- }
+ if (audioBuffer || audioUrl) {
+ return (
+ <div className="flex size-full items-center gap-2">
+ <div className="min-w-0 flex-1">
+ <AudioWaveform
+ audioBuffer={audioBuffer}
+ audioUrl={audioUrl}
+ height={24}
+ className="w-full"
+ />
+ </div>
+ </div>
+ );
+ }
- if (mediaItem.type === "audio") {
- return (
- <div className="w-full h-full flex items-center gap-2">
- <div className="flex-1 min-w-0">
- <AudioWaveform
- audioUrl={mediaItem.url || ""}
- height={24}
- className="w-full"
- />
- </div>
- </div>
- );
- }
+ return (
+ <span className="text-foreground/80 truncate text-xs">
+ {element.name}
+ </span>
+ );
+ }
- return (
- <span className="text-xs text-foreground/80 truncate">
- {element.name}
- </span>
- );
- };
+ const mediaAsset = mediaAssets.find((asset) => asset.id === element.mediaId);
+ if (!mediaAsset) {
+ return (
+ <span className="text-foreground/80 truncate text-xs">
+ {element.name}
+ </span>
+ );
+ }
- const handleElementMouseDown = (e: React.MouseEvent) => {
- if (onElementMouseDown) {
- onElementMouseDown(e, element);
- }
- };
+ if (
+ mediaAsset.type === "image" ||
+ (mediaAsset.type === "video" && mediaAsset.thumbnailUrl)
+ ) {
+ const trackHeight = getTrackHeight({ type: track.type });
+ const tileWidth = trackHeight * (16 / 9);
+ const imageUrl =
+ mediaAsset.type === "image" ? mediaAsset.url : mediaAsset.thumbnailUrl;
- const isMuted = element.type === "media" && element.muted;
+ return (
+ <div className="flex size-full items-center justify-center">
+ <div
+ className={`relative size-full ${isSelected ? "bg-primary" : "bg-transparent"}`}
+ >
+ <div
+ className="absolute right-0 left-0"
+ style={{
+ backgroundImage: imageUrl ? `url(${imageUrl})` : "none",
+ backgroundRepeat: "repeat-x",
+ backgroundSize: `${tileWidth}px ${trackHeight}px`,
+ backgroundPosition: "left center",
+ pointerEvents: "none",
+ top: isSelected ? "0.25rem" : "0rem",
+ bottom: isSelected ? "0.25rem" : "0rem",
+ }}
+ />
+ </div>
+ </div>
+ );
+ }
- return (
- <ContextMenu>
- <ContextMenuTrigger asChild>
- <div
- className={`absolute top-0 h-full select-none timeline-element ${
- isBeingDragged ? "z-50" : "z-10"
- }`}
- style={{
- left: `${elementLeft}px`,
- width: `${elementWidth}px`,
- }}
- data-element-id={element.id}
- data-track-id={track.id}
- onMouseMove={resizing ? handleResizeMove : undefined}
- onMouseUp={resizing ? handleResizeEnd : undefined}
- onMouseLeave={resizing ? handleResizeEnd : undefined}
- >
- <div
- className={`relative h-full rounded-[0.5rem] cursor-pointer overflow-hidden ${getTrackElementClasses(
- track.type
- )} ${isSelected ? "" : ""} ${
- isBeingDragged ? "z-50" : "z-10"
- } ${element.hidden ? "opacity-50" : ""}`}
- onClick={(e) => onElementClick && onElementClick(e, element)}
- onMouseDown={handleElementMouseDown}
- onContextMenu={(e) =>
- onElementMouseDown && onElementMouseDown(e, element)
- }
- >
- <div className="absolute inset-0 flex items-center h-full">
- {renderElementContent()}
- </div>
+ return (
+ <span className="text-foreground/80 truncate text-xs">{element.name}</span>
+ );
+}
- {(hasAudio ? isMuted : element.hidden) && (
- <div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center pointer-events-none">
- {hasAudio ? (
- <VolumeX className="h-6 w-6 text-white" />
- ) : (
- <EyeOff className="h-6 w-6 text-white" />
- )}
- </div>
- )}
+function CopyMenuItem() {
+ return (
+ <ActionMenuItem action="copy-selected" icon={<HugeiconsIcon icon={Copy01Icon} />}>
+ Copy
+ </ActionMenuItem>
+ );
+}
- {isSelected && (
- <>
- <div
- className="absolute left-0 top-0 bottom-0 w-[0.6rem] cursor-w-resize bg-primary z-50 flex items-center justify-center"
- onMouseDown={(e) => handleResizeStart(e, element.id, "left")}
- >
- <div className="w-[0.2rem] h-[1.5rem] bg-foreground/75 rounded-full" />
- </div>
- <div
- className="absolute right-0 top-0 bottom-0 w-[0.6rem] cursor-e-resize bg-primary z-50 flex items-center justify-center"
- onMouseDown={(e) => handleResizeStart(e, element.id, "right")}
- >
- <div className="w-[0.2rem] h-[1.5rem] bg-foreground/75 rounded-full" />
- </div>
- </>
- )}
- </div>
- </div>
- </ContextMenuTrigger>
- <ContextMenuContent className="z-200">
- {(!isMultipleSelected ||
- (isMultipleSelected &&
- isCurrentElementSelected &&
- canSplitSelected)) && (
- <ContextMenuItem onClick={handleElementSplitContext}>
- <Scissors className="h-4 w-4 mr-2" />
- {isMultipleSelected && isCurrentElementSelected
- ? `Split ${selectedElements.length} elements at playhead`
- : "Split at playhead"}
- </ContextMenuItem>
- )}
+function MuteMenuItem({
+ isMultipleSelected,
+ isCurrentElementSelected,
+ isMuted,
+}: {
+ isMultipleSelected: boolean;
+ isCurrentElementSelected: boolean;
+ isMuted: boolean;
+}) {
+ const getIcon = () => {
+ if (isMultipleSelected && isCurrentElementSelected) {
+ return <HugeiconsIcon icon={VolumeMute02Icon} />;
+ }
+ return isMuted ? (
+ <HugeiconsIcon icon={VolumeHighIcon} />
+ ) : (
+ <HugeiconsIcon icon={VolumeOffIcon} />
+ );
+ };
- <ContextMenuItem onClick={handleElementCopyContext}>
- <Copy className="h-4 w-4 mr-2" />
- {isMultipleSelected && isCurrentElementSelected
- ? `Copy ${selectedElements.length} elements`
- : "Copy element"}
- </ContextMenuItem>
+ return (
+ <ActionMenuItem action="toggle-elements-muted-selected" icon={getIcon()}>
+ {isMuted ? "Unmute" : "Mute"}
+ </ActionMenuItem>
+ );
+}
- <ContextMenuItem onClick={handleToggleElementContext}>
- {isMultipleSelected && isCurrentElementSelected ? (
- hasAudioElements ? (
- <VolumeX className="h-4 w-4 mr-2" />
- ) : (
- <EyeOff className="h-4 w-4 mr-2" />
- )
- ) : hasAudio ? (
- isMuted ? (
- <Volume2 className="h-4 w-4 mr-2" />
- ) : (
- <VolumeX className="h-4 w-4 mr-2" />
- )
- ) : element.hidden ? (
- <Eye className="h-4 w-4 mr-2" />
- ) : (
- <EyeOff className="h-4 w-4 mr-2" />
- )}
- <span>
- {isMultipleSelected && isCurrentElementSelected
- ? hasAudioElements
- ? `Toggle mute ${selectedElements.length} elements`
- : `Toggle visibility ${selectedElements.length} elements`
- : hasAudio
- ? isMuted
- ? "Unmute"
- : "Mute"
- : element.hidden
- ? "Show"
- : "Hide"}{" "}
- {!isMultipleSelected && (element.type === "text" ? "text" : "clip")}
- </span>
- </ContextMenuItem>
+function VisibilityMenuItem({
+ element,
+ isMultipleSelected,
+ isCurrentElementSelected,
+}: {
+ element: TimelineElementType;
+ isMultipleSelected: boolean;
+ isCurrentElementSelected: boolean;
+}) {
+ const isHidden = canElementBeHidden(element) && element.hidden;
- {!isMultipleSelected && (
- <ContextMenuItem onClick={handleElementDuplicateContext}>
- <Copy className="h-4 w-4 mr-2" />
- Duplicate {element.type === "text" ? "text" : "clip"}
- </ContextMenuItem>
- )}
+ const getIcon = () => {
+ if (isMultipleSelected && isCurrentElementSelected) {
+ return <HugeiconsIcon icon={ViewOffSlashIcon} />;
+ }
+ return isHidden ? (
+ <HugeiconsIcon icon={ViewIcon} />
+ ) : (
+ <HugeiconsIcon icon={ViewOffSlashIcon} />
+ );
+ };
- {!isMultipleSelected && element.type === "media" && (
- <>
- <ContextMenuItem onClick={handleRevealInMedia}>
- <Search className="h-4 w-4 mr-2" />
- Reveal in media
- </ContextMenuItem>
- <ContextMenuItem onClick={handleReplaceClip}>
- <RefreshCw className="h-4 w-4 mr-2" />
- Replace clip
- </ContextMenuItem>
- </>
- )}
+ return (
+ <ActionMenuItem action="toggle-elements-visibility-selected" icon={getIcon()}>
+ {isHidden ? "Show" : "Hide"}
+ </ActionMenuItem>
+ );
+}
- <ContextMenuSeparator />
+function DeleteMenuItem({
+ isMultipleSelected,
+ isCurrentElementSelected,
+ elementType,
+ selectedCount,
+}: {
+ isMultipleSelected: boolean;
+ isCurrentElementSelected: boolean;
+ elementType: TimelineElementType["type"];
+ selectedCount: number;
+}) {
+ return (
+ <ActionMenuItem
+ action="delete-selected"
+ variant="destructive"
+ icon={<HugeiconsIcon icon={Delete02Icon} />}
+ >
+ {isMultipleSelected && isCurrentElementSelected
+ ? `Delete ${selectedCount} elements`
+ : `Delete ${elementType === "text" ? "text" : "clip"}`}
+ </ActionMenuItem>
+ );
+}
- <ContextMenuItem
- onClick={handleElementDeleteContext}
- className="text-destructive focus:text-destructive"
- >
- <Trash2 className="h-4 w-4 mr-2" />
- {isMultipleSelected && isCurrentElementSelected
- ? `Delete ${selectedElements.length} elements`
- : `Delete ${element.type === "text" ? "text" : "clip"}`}
- </ContextMenuItem>
- </ContextMenuContent>
- </ContextMenu>
- );
+function ActionMenuItem({
+ action,
+ children,
+ ...props
+}: Omit<ComponentProps<typeof ContextMenuItem>, "onClick" | "textRight"> & {
+ action: TAction;
+}) {
+ return (
+ <ContextMenuItem
+ onClick={(event) => {
+ event.stopPropagation();
+ invokeAction(action);
+ }}
+ textRight={getDisplayShortcut(action)}
+ {...props}
+ >
+ {children}
+ </ContextMenuItem>
+ );
}
diff --git a/apps/web/src/components/editor/timeline/timeline-marker.tsx b/apps/web/src/components/editor/timeline/timeline-marker.tsx
deleted file mode 100644
index 1351a457..00000000
--- a/apps/web/src/components/editor/timeline/timeline-marker.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-"use client";
-
-import { cn } from "@/lib/utils";
-import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
-
-interface TimelineMarkerProps {
- time: number;
- zoomLevel: number;
- interval: number;
- isMainMarker: boolean;
-}
-
-export function TimelineMarker({
- time,
- zoomLevel,
- interval,
- isMainMarker,
-}: TimelineMarkerProps) {
- return (
- <div
- className={cn(
- "absolute top-0 h-4",
- isMainMarker
- ? "border-l border-muted-foreground/40"
- : "border-l border-muted-foreground/20"
- )}
- style={{
- left: `${time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
- }}
- >
- <span
- className={cn(
- "absolute top-1 left-1 text-[0.6rem]",
- isMainMarker
- ? "text-muted-foreground font-medium"
- : "text-muted-foreground/70"
- )}
- >
- {(() => {
- const formatTime = (seconds: number) => {
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
- const secs = seconds % 60;
-
- if (hours > 0) {
- return `${hours}:${minutes
- .toString()
- .padStart(2, "0")}:${Math.floor(secs)
- .toString()
- .padStart(2, "0")}`;
- }
- if (minutes > 0) {
- return `${minutes}:${Math.floor(secs)
- .toString()
- .padStart(2, "0")}`;
- }
- if (interval >= 1) {
- return `${Math.floor(secs)}s`;
- }
- return `${secs.toFixed(1)}s`;
- };
- return formatTime(time);
- })()}
- </span>
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/timeline/timeline-playhead.tsx b/apps/web/src/components/editor/timeline/timeline-playhead.tsx
index a1be5b26..58d0adb0 100644
--- a/apps/web/src/components/editor/timeline/timeline-playhead.tsx
+++ b/apps/web/src/components/editor/timeline/timeline-playhead.tsx
@@ -1,172 +1,92 @@
"use client";
-import { useRef, useState, useEffect } from "react";
-import { TimelineTrack } from "@/types/timeline";
+import { useRef } from "react";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
-import { useTimelinePlayhead } from "@/hooks/use-timeline-playhead";
+import { useTimelinePlayhead } from "@/hooks/timeline/use-timeline-playhead";
+import { useEditor } from "@/hooks/use-editor";
interface TimelinePlayheadProps {
- currentTime: number;
- duration: number;
- zoomLevel: number;
- tracks: TimelineTrack[];
- seek: (time: number) => void;
- rulerRef: React.RefObject<HTMLDivElement>;
- rulerScrollRef: React.RefObject<HTMLDivElement>;
- tracksScrollRef: React.RefObject<HTMLDivElement>;
- trackLabelsRef?: React.RefObject<HTMLDivElement>;
- timelineRef: React.RefObject<HTMLDivElement>;
- playheadRef?: React.RefObject<HTMLDivElement>;
- isSnappingToPlayhead?: boolean;
+ zoomLevel: number;
+ rulerRef: React.RefObject<HTMLDivElement>;
+ rulerScrollRef: React.RefObject<HTMLDivElement>;
+ tracksScrollRef: React.RefObject<HTMLDivElement>;
+ timelineRef: React.RefObject<HTMLDivElement>;
+ playheadRef?: React.RefObject<HTMLDivElement>;
+ isSnappingToPlayhead?: boolean;
}
export function TimelinePlayhead({
- currentTime,
- duration,
- zoomLevel,
- tracks,
- seek,
- rulerRef,
- rulerScrollRef,
- tracksScrollRef,
- trackLabelsRef,
- timelineRef,
- playheadRef: externalPlayheadRef,
- isSnappingToPlayhead = false,
+ zoomLevel,
+ rulerRef,
+ rulerScrollRef,
+ tracksScrollRef,
+ timelineRef,
+ playheadRef: externalPlayheadRef,
+ isSnappingToPlayhead = false,
}: TimelinePlayheadProps) {
- const internalPlayheadRef = useRef<HTMLDivElement>(null);
- const playheadRef = externalPlayheadRef || internalPlayheadRef;
- const [scrollLeft, setScrollLeft] = useState(0);
+ const editor = useEditor();
+ const duration = editor.timeline.getTotalDuration();
+ const internalPlayheadRef = useRef<HTMLDivElement>(null);
+ const playheadRef = externalPlayheadRef || internalPlayheadRef;
- const { playheadPosition, handlePlayheadMouseDown } = useTimelinePlayhead({
- currentTime,
- duration,
- zoomLevel,
- seek,
- rulerRef,
- rulerScrollRef,
- tracksScrollRef,
- playheadRef,
- });
+ const { playheadPosition, handlePlayheadMouseDown } = useTimelinePlayhead({
+ zoomLevel,
+ rulerRef,
+ rulerScrollRef,
+ tracksScrollRef,
+ playheadRef,
+ });
- // Track scroll position to lock playhead to frame
- useEffect(() => {
- const tracksViewport = tracksScrollRef.current;
+ const timelineContainerHeight =
+ tracksScrollRef.current?.clientHeight ??
+ timelineRef.current?.clientHeight ??
+ 400;
+ const totalHeight = Math.max(0, timelineContainerHeight - 4);
- if (!tracksViewport) return;
+ const timelinePosition =
+ playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
+ const leftPosition = timelinePosition;
- const handleScroll = () => {
- setScrollLeft(tracksViewport.scrollLeft);
- };
+ const handlePlayheadKeyDown = (
+ event: React.KeyboardEvent<HTMLDivElement>,
+ ) => {
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
- // Set initial scroll position
- setScrollLeft(tracksViewport.scrollLeft);
+ event.preventDefault();
+ const step = 1 / Math.max(1, editor.project.getActive().settings.fps);
+ const direction = event.key === "ArrowRight" ? 1 : -1;
+ const nextTime = Math.max(
+ 0,
+ Math.min(duration, playheadPosition + direction * step),
+ );
- tracksViewport.addEventListener("scroll", handleScroll);
- return () => tracksViewport.removeEventListener("scroll", handleScroll);
- }, [tracksScrollRef]);
+ editor.playback.seek({ time: nextTime });
+ };
- // Use timeline container height minus a few pixels for breathing room
- const timelineContainerHeight = timelineRef.current?.offsetHeight || 400;
- const totalHeight = timelineContainerHeight - 4;
+ return (
+ <div
+ ref={playheadRef}
+ role="slider"
+ aria-label="Timeline playhead"
+ aria-valuemin={0}
+ aria-valuemax={duration}
+ aria-valuenow={playheadPosition}
+ tabIndex={0}
+ className="pointer-events-auto absolute z-60"
+ style={{
+ left: `${leftPosition}px`,
+ top: 0,
+ height: `${totalHeight}px`,
+ width: "2px",
+ }}
+ onMouseDown={handlePlayheadMouseDown}
+ onKeyDown={handlePlayheadKeyDown}
+ >
+ <div className="bg-foreground absolute left-0 h-full w-0.5 cursor-col-resize" />
- // Get dynamic track labels width, fallback to 0 if no tracks or no ref
- const trackLabelsWidth =
- tracks.length > 0 && trackLabelsRef?.current
- ? trackLabelsRef.current.offsetWidth
- : 0;
-
- // Calculate position locked to timeline content (accounting for scroll)
- const timelinePosition =
- playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
- const rawLeftPosition = trackLabelsWidth + timelinePosition - scrollLeft;
-
- // Get the timeline content width and viewport width for right boundary
- const timelineContentWidth =
- duration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
- const tracksViewport = tracksScrollRef.current;
- const viewportWidth = tracksViewport?.clientWidth || 1000;
-
- // Constrain playhead to never appear outside the timeline area
- const leftBoundary = trackLabelsWidth;
- const rightBoundary = Math.min(
- trackLabelsWidth + timelineContentWidth - scrollLeft, // Don't go beyond timeline content
- trackLabelsWidth + viewportWidth // Don't go beyond viewport
- );
-
- const leftPosition = Math.max(
- leftBoundary,
- Math.min(rightBoundary, rawLeftPosition)
- );
-
- // Debug logging when playhead might go outside
- if (rawLeftPosition < leftBoundary || rawLeftPosition > rightBoundary) {
- console.log(
- "PLAYHEAD VISUAL DEBUG:",
- JSON.stringify({
- playheadPosition,
- timelinePosition,
- trackLabelsWidth,
- scrollLeft,
- rawLeftPosition,
- constrainedLeftPosition: leftPosition,
- leftBoundary,
- rightBoundary,
- timelineContentWidth,
- viewportWidth,
- zoomLevel,
- })
- );
- }
-
- return (
- <div
- ref={playheadRef}
- className="absolute pointer-events-auto z-40"
- style={{
- left: `${leftPosition}px`,
- top: 0,
- height: `${totalHeight}px`,
- width: "2px", // Slightly wider for better click target
- }}
- onMouseDown={handlePlayheadMouseDown}
- >
- {/* The playhead line spanning full height */}
- <div
- className={`absolute left-0 w-0.5 cursor-col-resize h-full ${isSnappingToPlayhead ? "bg-foreground" : "bg-foreground"}`}
- />
-
- {/* Playhead dot indicator at the top (in ruler area) */}
- <div
- className={`absolute top-1 left-1/2 transform -translate-x-1/2 w-3 h-3 rounded-full border-2 shadow-xs ${isSnappingToPlayhead ? "bg-foreground border-foreground" : "bg-foreground border-foreground/50"}`}
- />
- </div>
- );
+ <div
+ className={`absolute top-1 left-1/2 size-3 -translate-x-1/2 transform rounded-full border-2 shadow-xs ${isSnappingToPlayhead ? "bg-foreground border-foreground" : "bg-foreground border-foreground/50"}`}
+ />
+ </div>
+ );
}
-
-// Also export a hook for getting ruler handlers
-export function useTimelinePlayheadRuler({
- currentTime,
- duration,
- zoomLevel,
- seek,
- rulerRef,
- rulerScrollRef,
- tracksScrollRef,
- playheadRef,
-}: Omit<TimelinePlayheadProps, "tracks" | "trackLabelsRef" | "timelineRef">) {
- const { handleRulerMouseDown, isDraggingRuler } = useTimelinePlayhead({
- currentTime,
- duration,
- zoomLevel,
- seek,
- rulerRef,
- rulerScrollRef,
- tracksScrollRef,
- playheadRef,
- });
-
- return { handleRulerMouseDown, isDraggingRuler };
-}
-
-export { TimelinePlayhead as default };
diff --git a/apps/web/src/components/editor/timeline/timeline-ruler.tsx b/apps/web/src/components/editor/timeline/timeline-ruler.tsx
new file mode 100644
index 00000000..87702a4f
--- /dev/null
+++ b/apps/web/src/components/editor/timeline/timeline-ruler.tsx
@@ -0,0 +1,83 @@
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import { DEFAULT_FPS } from "@/constants/project-constants";
+import { useEditor } from "@/hooks/use-editor";
+import { getRulerConfig, shouldShowLabel } from "@/lib/timeline/ruler-utils";
+import { TimelineTick } from "./timeline-tick";
+
+interface TimelineRulerProps {
+ zoomLevel: number;
+ dynamicTimelineWidth: number;
+ rulerRef: React.RefObject<HTMLDivElement>;
+ handleWheel: (e: React.WheelEvent) => void;
+ handleTimelineContentClick: (e: React.MouseEvent) => void;
+ handleRulerTrackingMouseDown: (e: React.MouseEvent) => void;
+ handleRulerMouseDown: (e: React.MouseEvent) => void;
+}
+
+export function TimelineRuler({
+ zoomLevel,
+ dynamicTimelineWidth,
+ rulerRef,
+ handleWheel,
+ handleTimelineContentClick,
+ handleRulerTrackingMouseDown,
+ handleRulerMouseDown,
+}: TimelineRulerProps) {
+ const editor = useEditor();
+ const duration = editor.timeline.getTotalDuration();
+ const pixelsPerSecond = TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
+ const visibleDuration = dynamicTimelineWidth / pixelsPerSecond;
+ const effectiveDuration = Math.max(duration, visibleDuration);
+ const project = editor.project.getActive();
+ const fps = project?.settings.fps ?? DEFAULT_FPS;
+ const { labelIntervalSeconds, tickIntervalSeconds } = getRulerConfig({
+ zoomLevel,
+ fps,
+ });
+ const tickCount = Math.ceil(effectiveDuration / tickIntervalSeconds) + 1;
+
+ const timelineTicks: Array<JSX.Element> = [];
+ for (let tickIndex = 0; tickIndex < tickCount; tickIndex += 1) {
+ const time = tickIndex * tickIntervalSeconds;
+ if (time > effectiveDuration) break;
+
+ const showLabel = shouldShowLabel({ time, labelIntervalSeconds });
+ timelineTicks.push(
+ <TimelineTick
+ key={tickIndex}
+ time={time}
+ zoomLevel={zoomLevel}
+ fps={fps}
+ showLabel={showLabel}
+ />,
+ );
+ }
+
+ return (
+ <div
+ role="slider"
+ tabIndex={0}
+ aria-label="Timeline ruler"
+ aria-valuemin={0}
+ aria-valuemax={effectiveDuration}
+ aria-valuenow={0}
+ className="relative h-4 flex-1 overflow-x-visible"
+ onWheel={handleWheel}
+ onClick={handleTimelineContentClick}
+ onMouseDown={handleRulerTrackingMouseDown}
+ onKeyDown={() => {}}
+ >
+ <div
+ role="none"
+ ref={rulerRef}
+ className="relative h-4 cursor-default select-none"
+ style={{
+ width: `${dynamicTimelineWidth}px`,
+ }}
+ onMouseDown={handleRulerMouseDown}
+ >
+ {timelineTicks}
+ </div>
+ </div>
+ );
+}
diff --git a/apps/web/src/components/editor/timeline/timeline-tick.tsx b/apps/web/src/components/editor/timeline/timeline-tick.tsx
new file mode 100644
index 00000000..9402d25e
--- /dev/null
+++ b/apps/web/src/components/editor/timeline/timeline-tick.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import { formatRulerLabel } from "@/lib/timeline/ruler-utils";
+
+interface TimelineTickProps {
+ time: number;
+ zoomLevel: number;
+ fps: number;
+ showLabel: boolean;
+}
+
+export function TimelineTick({
+ time,
+ zoomLevel,
+ fps,
+ showLabel,
+}: TimelineTickProps) {
+ const leftPosition = time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
+
+ if (showLabel) {
+ const label = formatRulerLabel({ timeInSeconds: time, fps });
+ return (
+ <span
+ className="text-muted-foreground/85 absolute bottom-0 select-none text-[10px] leading-none"
+ style={{ left: `${leftPosition}px` }}
+ >
+ {label}
+ </span>
+ );
+ }
+
+ return (
+ <div
+ className="border-muted-foreground/25 absolute bottom-0.5 h-1.5 border-l"
+ style={{ left: `${leftPosition}px` }}
+ />
+ );
+}
diff --git a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx b/apps/web/src/components/editor/timeline/timeline-toolbar.tsx
index 80c6ce08..822bb438 100644
--- a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx
+++ b/apps/web/src/components/editor/timeline/timeline-toolbar.tsx
@@ -1,397 +1,355 @@
-import { usePlaybackStore } from "@/stores/playback-store";
-import { useProjectStore } from "@/stores/project-store";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { useSceneStore } from "@/stores/scene-store";
-import { toast } from "sonner";
+import { useEditor } from "@/hooks/use-editor";
import {
- TooltipProvider,
- Tooltip,
- TooltipTrigger,
- TooltipContent,
+ TooltipProvider,
+ Tooltip,
+ TooltipTrigger,
+ TooltipContent,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
+import { SkipBack, SplitSquareHorizontal } from "lucide-react";
import {
- Pause,
- Play,
- SkipBack,
- Bookmark,
- Magnet,
- Link,
- ZoomOut,
- ZoomIn,
- Copy,
- Trash2,
- Snowflake,
- ArrowLeftToLine,
- ArrowRightToLine,
- SplitSquareHorizontal,
- Scissors,
- LayersIcon,
-} from "lucide-react";
-import {
- SplitButton,
- SplitButtonLeft,
- SplitButtonRight,
- SplitButtonSeparator,
+ SplitButton,
+ SplitButtonLeft,
+ SplitButtonRight,
+ SplitButtonSeparator,
} from "@/components/ui/split-button";
import { Slider } from "@/components/ui/slider";
-import { DEFAULT_FPS } from "@/stores/project-store";
import { formatTimeCode } from "@/lib/time";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
-import { EditableTimecode } from "@/components/ui/editable-timecode";
+import { EditableTimecode } from "@/components/editable-timecode";
import { ScenesView } from "../scenes-view";
+import { type TAction, invokeAction } from "@/lib/actions";
+import { cn } from "@/utils/ui";
+import { useTimelineStore } from "@/stores/timeline-store";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import {
+ Bookmark02Icon,
+ Delete02Icon,
+ SnowIcon,
+ ScissorIcon,
+ MagnetIcon,
+ Link04Icon,
+ SearchAddIcon,
+ SearchMinusIcon,
+ PauseIcon,
+ PlayIcon,
+ Copy01Icon,
+ AlignLeftIcon,
+ AlignRightIcon,
+ Layers01Icon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
export function TimelineToolbar({
- zoomLevel,
- setZoomLevel,
+ zoomLevel,
+ minZoom,
+ setZoomLevel,
}: {
- zoomLevel: number;
- setZoomLevel: (zoom: number) => void;
+ zoomLevel: number;
+ minZoom: number;
+ setZoomLevel: ({ zoom }: { zoom: number }) => void;
}) {
- const {
- tracks,
- addTrack,
- addElementToTrack,
- selectedElements,
- clearSelectedElements,
- deleteSelected,
- splitSelected,
- splitAndKeepLeft,
- splitAndKeepRight,
- separateAudio,
- snappingEnabled,
- toggleSnapping,
- rippleEditingEnabled,
- toggleRippleEditing,
- } = useTimelineStore();
- const { currentTime, duration, isPlaying, toggle, seek } = usePlaybackStore();
- const { toggleBookmark, isBookmarked, activeProject } = useProjectStore();
- const { scenes, currentScene } = useSceneStore();
+ const handleZoom = ({ direction }: { direction: "in" | "out" }) => {
+ const newZoomLevel =
+ direction === "in"
+ ? Math.min(
+ TIMELINE_CONSTANTS.ZOOM_MAX,
+ zoomLevel + TIMELINE_CONSTANTS.ZOOM_STEP,
+ )
+ : Math.max(minZoom, zoomLevel - TIMELINE_CONSTANTS.ZOOM_STEP);
+ setZoomLevel({ zoom: newZoomLevel });
+ };
+
+ return (
+ <ScrollArea className="scrollbar-hidden">
+ <div className="flex h-10 items-center justify-between border-b px-2 py-1">
+ <ToolbarLeftSection />
+
+ <SceneSelector />
+
+ <ToolbarRightSection
+ zoomLevel={zoomLevel}
+ minZoom={minZoom}
+ onZoomChange={(zoom) => setZoomLevel({ zoom })}
+ onZoom={handleZoom}
+ />
+ </div>
+ </ScrollArea>
+ );
+}
+
+function ToolbarLeftSection() {
+ const editor = useEditor();
+ const currentTime = editor.playback.getCurrentTime();
+ const isPlaying = editor.playback.getIsPlaying();
+ const currentBookmarked = editor.scenes.isBookmarked({ time: currentTime });
+
+ const handleAction = ({
+ action,
+ event,
+ }: {
+ action: TAction;
+ event: React.MouseEvent;
+ }) => {
+ event.stopPropagation();
+ invokeAction(action);
+ };
+
+ return (
+ <div className="flex items-center gap-1">
+ <TooltipProvider delayDuration={500}>
+ <ToolbarButton
+ icon={
+ isPlaying ? (
+ <HugeiconsIcon icon={PauseIcon} />
+ ) : (
+ <HugeiconsIcon icon={PlayIcon} />
+ )
+ }
+ tooltip={isPlaying ? "Pause" : "Play"}
+ onClick={({ event }) =>
+ handleAction({ action: "toggle-play", event })
+ }
+ />
+
+ <ToolbarButton
+ icon={<SkipBack />}
+ tooltip="Go to start"
+ onClick={({ event }) => handleAction({ action: "goto-start", event })}
+ />
+
+ <div className="bg-border mx-1 h-6 w-px" />
+
+ <TimeDisplay />
- const handleSplitSelected = () => {
- splitSelected(currentTime);
- };
+ <div className="bg-border mx-1 h-6 w-px" />
- const handleDuplicateSelected = () => {
- if (selectedElements.length === 0) return;
- const canDuplicate = selectedElements.length === 1;
- if (!canDuplicate) return;
+ <ToolbarButton
+ icon={<HugeiconsIcon icon={ScissorIcon} />}
+ tooltip="Split element"
+ onClick={({ event }) =>
+ handleAction({ action: "split", event })
+ }
+ />
- selectedElements.forEach(({ trackId, elementId }) => {
- const track = tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((el) => el.id === elementId);
- if (element) {
- const newStartTime =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd) +
- 0.1;
- const { id, ...elementWithoutId } = element;
- addElementToTrack(trackId, {
- ...elementWithoutId,
- startTime: newStartTime,
- });
- }
- });
- clearSelectedElements();
- };
+ <ToolbarButton
+ icon={<HugeiconsIcon icon={AlignLeftIcon} />}
+ tooltip="Split left"
+ onClick={({ event }) =>
+ handleAction({ action: "split-left", event })
+ }
+ />
- const handleFreezeSelected = () => {
- toast.info("Freeze frame functionality coming soon!");
- };
+ <ToolbarButton
+ icon={<HugeiconsIcon icon={AlignRightIcon} />}
+ tooltip="Split right"
+ onClick={({ event }) =>
+ handleAction({ action: "split-right", event })
+ }
+ />
- const handleSplitAndKeepLeft = () => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one element");
- return;
- }
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((c) => c.id === elementId);
- if (!element) return;
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
- if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
- toast.error("Playhead must be within selected element");
- return;
- }
- splitAndKeepLeft(trackId, elementId, currentTime);
- };
+ <ToolbarButton
+ icon={<SplitSquareHorizontal />}
+ tooltip="Coming soon" /* separate audio */
+ disabled={true}
+ onClick={({ event: _event }) => {}}
+ />
- const handleSplitAndKeepRight = () => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one element");
- return;
- }
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((c) => c.id === elementId);
- if (!element) return;
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
- if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
- toast.error("Playhead must be within selected element");
- return;
- }
- splitAndKeepRight(trackId, elementId, currentTime);
- };
+ <ToolbarButton
+ icon={<HugeiconsIcon icon={Copy01Icon} />}
+ tooltip="Duplicate element"
+ onClick={({ event }) =>
+ handleAction({ action: "duplicate-selected", event })
+ }
+ />
- const handleSeparateAudio = () => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one media element to separate audio");
- return;
- }
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t) => t.id === trackId);
- if (!track || track.type !== "media") {
- toast.error("Select a media element to separate audio");
- return;
- }
- separateAudio(trackId, elementId);
- };
+ <ToolbarButton
+ icon={<HugeiconsIcon icon={SnowIcon} />}
+ tooltip="Coming soon" /* freeze frame */
+ disabled={true}
+ onClick={({ event: _event }) => {}}
+ />
- const handleDeleteSelected = () => {
- deleteSelected();
- };
+ <ToolbarButton
+ icon={<HugeiconsIcon icon={Delete02Icon} />}
+ tooltip="Delete element"
+ onClick={({ event }) =>
+ handleAction({ action: "delete-selected", event })
+ }
+ />
- const handleZoomIn = () => {
- setZoomLevel(Math.min(4, zoomLevel + 0.25));
- };
+ <div className="bg-border mx-1 h-6 w-px" />
- const handleZoomOut = () => {
- setZoomLevel(Math.max(0.25, zoomLevel - 0.25));
- };
+ <Tooltip>
+ <ToolbarButton
+ icon={
+ <HugeiconsIcon
+ icon={Bookmark02Icon}
+ className={currentBookmarked ? "fill-primary text-primary" : ""}
+ />
+ }
+ tooltip={currentBookmarked ? "Remove bookmark" : "Add bookmark"}
+ onClick={({ event }) =>
+ handleAction({ action: "toggle-bookmark", event })
+ }
+ />
+ </Tooltip>
+ </TooltipProvider>
+ </div>
+ );
+}
+
+function TimeDisplay() {
+ const editor = useEditor();
+ const currentTime = editor.playback.getCurrentTime();
+ const totalDuration = editor.timeline.getTotalDuration();
+ const fps = editor.project.getActive().settings.fps;
+
+ return (
+ <div className="flex flex-row items-center justify-center px-2">
+ <EditableTimecode
+ time={currentTime}
+ duration={totalDuration}
+ format="HH:MM:SS:FF"
+ fps={fps}
+ onTimeChange={({ time }) => editor.playback.seek({ time })}
+ className="text-center"
+ />
+ <div className="text-muted-foreground px-2 font-mono text-xs">/</div>
+ <div className="text-muted-foreground text-center font-mono text-xs">
+ {formatTimeCode({
+ timeInSeconds: totalDuration,
+ format: "HH:MM:SS:FF",
+ fps,
+ })}
+ </div>
+ </div>
+ );
+}
+
+function SceneSelector() {
+ const editor = useEditor();
+ const currentScene = editor.scenes.getActiveScene();
+
+ return (
+ <div>
+ <SplitButton className="border-foreground/10 border">
+ <SplitButtonLeft>{currentScene?.name || "No Scene"}</SplitButtonLeft>
+ <SplitButtonSeparator />
+ <ScenesView>
+ <SplitButtonRight onClick={() => {}} type="button">
+ <HugeiconsIcon icon={Layers01Icon} className="size-4" />
+ </SplitButtonRight>
+ </ScenesView>
+ </SplitButton>
+ </div>
+ );
+}
- const handleZoomSliderChange = (values: number[]) => {
- setZoomLevel(values[0]);
- };
+function ToolbarRightSection({
+ zoomLevel,
+ minZoom,
+ onZoomChange,
+ onZoom,
+}: {
+ zoomLevel: number;
+ minZoom: number;
+ onZoomChange: (zoom: number) => void;
+ onZoom: (options: { direction: "in" | "out" }) => void;
+}) {
+ const {
+ snappingEnabled,
+ rippleEditingEnabled,
+ toggleSnapping,
+ toggleRippleEditing,
+ } = useTimelineStore();
- const handleToggleBookmark = async () => {
- await toggleBookmark(currentTime);
- };
+ return (
+ <div className="flex items-center gap-1">
+ <TooltipProvider delayDuration={500}>
+ <ToolbarButton
+ icon={
+ <HugeiconsIcon
+ icon={MagnetIcon}
+ className={cn(snappingEnabled ? "text-primary" : "")}
+ />
+ }
+ tooltip="Auto snapping"
+ onClick={() => toggleSnapping()}
+ />
- const currentBookmarked = isBookmarked(currentTime);
- return (
- <div className="flex items-center justify-between px-2 py-1 border-b h-10">
- <div className="flex items-center gap-1">
- <TooltipProvider delayDuration={500}>
- <Tooltip>
- <TooltipTrigger asChild>
- <Button variant="text" size="icon" onClick={toggle}>
- {isPlaying ? (
- <Pause className="h-4 w-4" />
- ) : (
- <Play className="h-4 w-4" />
- )}
- </Button>
- </TooltipTrigger>
- <TooltipContent>
- {isPlaying ? "Pause (Space)" : "Play (Space)"}
- </TooltipContent>
- </Tooltip>
- <Tooltip>
- <TooltipTrigger asChild>
- <Button variant="text" size="icon" onClick={() => seek(0)}>
- <SkipBack className="h-4 w-4" />
- </Button>
- </TooltipTrigger>
- <TooltipContent>Return to Start (Home / Enter)</TooltipContent>
- </Tooltip>
- <div className="w-px h-6 bg-border mx-1" />
- {/* Time Display */}
- <div className="flex flex-row items-center justify-center px-2">
- <EditableTimecode
- time={currentTime}
- duration={duration}
- format="HH:MM:SS:FF"
- fps={activeProject?.fps ?? DEFAULT_FPS}
- onTimeChange={seek}
- className="text-center"
- />
- <div className="text-xs text-muted-foreground font-mono px-2">
- /
- </div>
- <div className="text-xs text-muted-foreground font-mono text-center">
- {formatTimeCode(duration, "HH:MM:SS:FF")}
- </div>
- </div>
- {tracks.length === 0 && (
- <>
- <div className="w-px h-6 bg-border mx-1" />
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="outline"
- size="sm"
- onClick={() => {
- const trackId = addTrack("media");
- addElementToTrack(trackId, {
- type: "media",
- mediaId: "test",
- name: "Test Clip",
- duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
- startTime: 0,
- trimStart: 0,
- trimEnd: 0,
- });
- }}
- className="text-xs"
- >
- Add Test Clip
- </Button>
- </TooltipTrigger>
- <TooltipContent>Add a test clip to try playback</TooltipContent>
- </Tooltip>
- </>
- )}
- <div className="w-px h-6 bg-border mx-1" />
- <Tooltip>
- <TooltipTrigger asChild>
- <Button variant="text" size="icon" onClick={handleSplitSelected}>
- <Scissors className="h-4 w-4" />
- </Button>
- </TooltipTrigger>
- <TooltipContent>Split element (Ctrl+S)</TooltipContent>
- </Tooltip>
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="text"
- size="icon"
- onClick={handleSplitAndKeepLeft}
- >
- <ArrowLeftToLine className="h-4 w-4" />
- </Button>
- </TooltipTrigger>
- <TooltipContent>Split and keep left (Ctrl+Q)</TooltipContent>
- </Tooltip>
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="text"
- size="icon"
- onClick={handleSplitAndKeepRight}
- >
- <ArrowRightToLine className="h-4 w-4" />
- </Button>
- </TooltipTrigger>
- <TooltipContent>Split and keep right (Ctrl+W)</TooltipContent>
- </Tooltip>
- <Tooltip>
- <TooltipTrigger asChild>
- <Button variant="text" size="icon" onClick={handleSeparateAudio}>
- <SplitSquareHorizontal className="h-4 w-4" />
- </Button>
- </TooltipTrigger>
- <TooltipContent>Separate audio (Ctrl+D)</TooltipContent>
- </Tooltip>
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="text"
- size="icon"
- onClick={handleDuplicateSelected}
- >
- <Copy className="h-4 w-4" />
- </Button>
- </TooltipTrigger>
- <TooltipContent>Duplicate element (Ctrl+D)</TooltipContent>
- </Tooltip>
- <Tooltip>
- <TooltipTrigger asChild>
- <Button variant="text" size="icon" onClick={handleFreezeSelected}>
- <Snowflake className="h-4 w-4" />
- </Button>
- </TooltipTrigger>
- <TooltipContent>Freeze frame (F)</TooltipContent>
- </Tooltip>
- <Tooltip>
- <TooltipTrigger asChild>
- <Button variant="text" size="icon" onClick={handleDeleteSelected}>
- <Trash2 className="h-4 w-4" />
- </Button>
- </TooltipTrigger>
- <TooltipContent>Delete element (Delete)</TooltipContent>
- </Tooltip>
- <div className="w-px h-6 bg-border mx-1" />
- <Tooltip>
- <TooltipTrigger asChild>
- <Button variant="text" size="icon" onClick={handleToggleBookmark}>
- <Bookmark
- className={`h-4 w-4 ${currentBookmarked ? "fill-primary text-primary" : ""}`}
- />
- </Button>
- </TooltipTrigger>
- <TooltipContent>
- {currentBookmarked ? "Remove bookmark" : "Add bookmark"}
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- </div>
- <div>
- <SplitButton className="border border-foreground/10">
- <SplitButtonLeft>{currentScene?.name || "No Scene"}</SplitButtonLeft>
- <SplitButtonSeparator />
- <ScenesView>
- <SplitButtonRight disabled={scenes.length === 1} onClick={() => {}}>
- <LayersIcon />
- </SplitButtonRight>
- </ScenesView>
- </SplitButton>
- </div>
- <div className="flex items-center gap-1">
- <TooltipProvider delayDuration={500}>
- <Tooltip>
- <TooltipTrigger asChild>
- <Button variant="text" size="icon" onClick={toggleSnapping}>
- {snappingEnabled ? (
- <Magnet className="h-4 w-4 text-primary" />
- ) : (
- <Magnet className="h-4 w-4" />
- )}
- </Button>
- </TooltipTrigger>
- <TooltipContent>Auto snapping</TooltipContent>
- </Tooltip>
- <Tooltip>
- <TooltipTrigger asChild>
- <Button variant="text" size="icon" onClick={toggleRippleEditing}>
- <Link
- className={`h-4 w-4 ${
- rippleEditingEnabled ? "text-primary" : ""
- }`}
- />
- </Button>
- </TooltipTrigger>
- <TooltipContent>
- {rippleEditingEnabled
- ? "Disable Ripple Editing"
- : "Enable Ripple Editing"}
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
+ <ToolbarButton
+ icon={
+ <HugeiconsIcon
+ icon={Link04Icon}
+ className={cn(
+ rippleEditingEnabled ? "text-primary" : "",
+ "scale-110",
+ )}
+ />
+ }
+ tooltip="Ripple editing"
+ onClick={() => toggleRippleEditing()}
+ />
+ </TooltipProvider>
- <div className="h-6 w-px bg-border mx-1" />
- <div className="flex items-center gap-1">
- <Button variant="text" size="icon" onClick={handleZoomOut}>
- <ZoomOut className="h-4 w-4" />
- </Button>
- <Slider
- className="w-24"
- value={[zoomLevel]}
- onValueChange={handleZoomSliderChange}
- min={0.25}
- max={4}
- step={0.25}
- />
- <Button variant="text" size="icon" onClick={handleZoomIn}>
- <ZoomIn className="h-4 w-4" />
- </Button>
- </div>
- </div>
- </div>
- );
+ <div className="bg-border mx-1 h-6 w-px" />
+
+ <div className="flex items-center gap-1">
+ <Button
+ variant="text"
+ size="icon"
+ type="button"
+ onClick={() => onZoom({ direction: "out" })}
+ >
+ <HugeiconsIcon icon={SearchAddIcon} />
+ </Button>
+ <Slider
+ className="w-24"
+ value={[zoomLevel]}
+ onValueChange={(values) => onZoomChange(values[0])}
+ min={minZoom}
+ max={TIMELINE_CONSTANTS.ZOOM_MAX}
+ step={TIMELINE_CONSTANTS.ZOOM_STEP}
+ />
+ <Button
+ variant="text"
+ size="icon"
+ type="button"
+ onClick={() => onZoom({ direction: "in" })}
+ >
+ <HugeiconsIcon icon={SearchMinusIcon} />
+ </Button>
+ </div>
+ </div>
+ );
+}
+
+function ToolbarButton({
+ icon,
+ tooltip,
+ onClick,
+ disabled,
+}: {
+ icon: React.ReactNode;
+ tooltip: string;
+ onClick: ({ event }: { event: React.MouseEvent }) => void;
+ disabled?: boolean;
+}) {
+ return (
+ <Tooltip delayDuration={200}>
+ <TooltipTrigger asChild>
+ <Button
+ variant="text"
+ size="icon"
+ type="button"
+ onClick={(event) => onClick({ event })}
+ className={disabled ? "cursor-not-allowed opacity-50" : ""}
+ >
+ {icon}
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>{tooltip}</TooltipContent>
+ </Tooltip>
+ );
}
diff --git a/apps/web/src/components/editor/timeline/timeline-track.tsx b/apps/web/src/components/editor/timeline/timeline-track.tsx
index 7cfde931..29d58315 100644
--- a/apps/web/src/components/editor/timeline/timeline-track.tsx
+++ b/apps/web/src/components/editor/timeline/timeline-track.tsx
@@ -1,1199 +1,113 @@
"use client";
-import { useRef, useState, useEffect } from "react";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { useMediaStore } from "@/stores/media-store";
-import { toast } from "sonner";
-import { processMediaFiles } from "@/lib/media-processing";
+import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
import { TimelineElement } from "./timeline-element";
-import {
- TimelineTrack,
- getMainTrack,
- canElementGoOnTrack,
-} from "@/types/timeline";
-import { usePlaybackStore } from "@/stores/playback-store";
-import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
-import type {
- TimelineElement as TimelineElementType,
- DragData,
- TrackType,
-} from "@/types/timeline";
-import {
- snapTimeToFrame,
- TIMELINE_CONSTANTS,
-} from "@/constants/timeline-constants";
-import { DEFAULT_FPS, useProjectStore } from "@/stores/project-store";
-import { useTimelineSnapping, SnapPoint } from "@/hooks/use-timeline-snapping";
-import { useEdgeAutoScroll } from "@/hooks/use-edge-auto-scroll";
+import type { TimelineTrack } from "@/types/timeline";
+import type { TimelineElement as TimelineElementType } from "@/types/timeline";
+import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
+import type { ElementDragState } from "@/types/timeline";
+import { useEditor } from "@/hooks/use-editor";
+import { cn } from "@/utils/ui";
-export function TimelineTrackContent({
- track,
- zoomLevel,
- onSnapPointChange,
- rulerScrollRef,
- tracksScrollRef,
-}: {
- track: TimelineTrack;
- zoomLevel: number;
- onSnapPointChange?: (snapPoint: SnapPoint | null) => void;
- rulerScrollRef: React.RefObject<HTMLDivElement>;
- tracksScrollRef: React.RefObject<HTMLDivElement>;
-}) {
- const { mediaFiles } = useMediaStore();
- const {
- tracks,
- addTrack,
- moveElementToTrack,
- updateElementStartTime,
- updateElementStartTimeWithRipple,
- addElementToTrack,
- selectedElements,
- selectElement,
- dragState,
- startDrag: startDragAction,
- updateDragTime,
- endDrag: endDragAction,
- clearSelectedElements,
- insertTrackAt,
- snappingEnabled,
- rippleEditingEnabled,
- } = useTimelineStore();
-
- const { currentTime, duration } = usePlaybackStore();
-
- // Initialize snapping hook
- const { snapElementPosition, snapElementEdge } = useTimelineSnapping({
- snapThreshold: 10,
- enableElementSnapping: snappingEnabled,
- enablePlayheadSnapping: snappingEnabled,
- });
-
- // Helper function for drop snapping that tries both edges
- const getDropSnappedTime = (
- dropTime: number,
- elementDuration: number,
- excludeElementId?: string
- ) => {
- // Always apply frame snapping first
- const projectStore = useProjectStore.getState();
- const projectFps = projectStore.activeProject?.fps || DEFAULT_FPS;
- let finalTime = snapTimeToFrame(dropTime, projectFps);
-
- // Additionally apply element snapping if enabled
- if (snappingEnabled) {
- // Try snapping both start and end edges for drops
- const startSnapResult = snapElementEdge(
- dropTime,
- elementDuration,
- tracks,
- currentTime,
- zoomLevel,
- excludeElementId,
- true // snap to start edge
- );
-
- const endSnapResult = snapElementEdge(
- dropTime,
- elementDuration,
- tracks,
- currentTime,
- zoomLevel,
- excludeElementId,
- false // snap to end edge
- );
-
- // Choose the snap result with the smaller distance (closer snap)
- let bestSnapResult = startSnapResult;
- if (
- endSnapResult.snapPoint &&
- (!startSnapResult.snapPoint ||
- endSnapResult.snapDistance < startSnapResult.snapDistance)
- ) {
- bestSnapResult = endSnapResult;
- }
-
- // Only use element snapping if it found a snap point, otherwise keep frame-snapped time
- if (bestSnapResult.snapPoint) {
- finalTime = bestSnapResult.snappedTime;
- }
- }
-
- return finalTime;
- };
-
- const timelineRef = useRef<HTMLDivElement>(null);
- const [isDropping, setIsDropping] = useState(false);
- const [dropPosition, setDropPosition] = useState<number | null>(null);
- const [wouldOverlap, setWouldOverlap] = useState(false);
- const dragCounterRef = useRef(0);
- const [mouseDownLocation, setMouseDownLocation] = useState<{
- x: number;
- y: number;
- } | null>(null);
-
- const lastMouseXRef = useRef(0);
-
- // Set up mouse event listeners for drag
- useEffect(() => {
- if (!dragState.isDragging) return;
-
- const handleMouseMove = (e: MouseEvent) => {
- if (!timelineRef.current) return;
- lastMouseXRef.current = e.clientX;
-
- // On first mouse move during drag, ensure the element is selected
- if (dragState.elementId && dragState.trackId) {
- const isSelected = selectedElements.some(
- (c) =>
- c.trackId === dragState.trackId &&
- c.elementId === dragState.elementId
- );
-
- if (!isSelected) {
- // Select this element (replacing other selections) since we're dragging it
- selectElement(dragState.trackId, dragState.elementId, false);
- }
- }
-
- const timelineRect = timelineRef.current.getBoundingClientRect();
- const mouseX = e.clientX - timelineRect.left;
- const mouseTime = Math.max(
- 0,
- mouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel)
- );
- const adjustedTime = Math.max(0, mouseTime - dragState.clickOffsetTime);
-
- // Always apply frame snapping first
- const projectStore = useProjectStore.getState();
- const projectFps = projectStore.activeProject?.fps || DEFAULT_FPS;
- let finalTime = snapTimeToFrame(adjustedTime, projectFps);
- let snapPoint = null;
-
- // Additionally apply element snapping if enabled
- if (snappingEnabled) {
- // Find the element being dragged to get its duration
- let elementDuration = 5; // fallback duration
- if (dragState.elementId && dragState.trackId) {
- const sourceTrack = tracks.find((t) => t.id === dragState.trackId);
- const element = sourceTrack?.elements.find(
- (e) => e.id === dragState.elementId
- );
- if (element) {
- elementDuration =
- element.duration - element.trimStart - element.trimEnd;
- }
- }
-
- // Try snapping both start and end edges
- const startSnapResult = snapElementEdge(
- adjustedTime,
- elementDuration,
- tracks,
- currentTime,
- zoomLevel,
- dragState.elementId || undefined,
- true // snap to start edge
- );
-
- const endSnapResult = snapElementEdge(
- adjustedTime,
- elementDuration,
- tracks,
- currentTime,
- zoomLevel,
- dragState.elementId || undefined,
- false // snap to end edge
- );
-
- // Choose the snap result with the smaller distance (closer snap)
- let bestSnapResult = startSnapResult;
- if (
- endSnapResult.snapPoint &&
- (!startSnapResult.snapPoint ||
- endSnapResult.snapDistance < startSnapResult.snapDistance)
- ) {
- bestSnapResult = endSnapResult;
- }
-
- // Only use element snapping if it found a snap point, otherwise keep frame-snapped time
- if (bestSnapResult.snapPoint) {
- finalTime = bestSnapResult.snappedTime;
- snapPoint = bestSnapResult.snapPoint;
- }
-
- // Notify parent component about snap point change
- onSnapPointChange?.(snapPoint);
- } else {
- // Clear snap point when element snapping is disabled
- onSnapPointChange?.(null);
- }
-
- updateDragTime(finalTime);
- };
-
- const handleMouseUp = (e: MouseEvent) => {
- if (!dragState.elementId || !dragState.trackId) return;
-
- // If this track initiated the drag, we should handle the mouse up regardless of where it occurs
- const isTrackThatStartedDrag = dragState.trackId === track.id;
-
- const timelineRect = timelineRef.current?.getBoundingClientRect();
- if (!timelineRect) {
- if (isTrackThatStartedDrag) {
- if (rippleEditingEnabled) {
- updateElementStartTimeWithRipple(
- track.id,
- dragState.elementId,
- dragState.currentTime
- );
- } else {
- updateElementStartTime(
- track.id,
- dragState.elementId,
- dragState.currentTime
- );
- }
- endDragAction();
- // Clear snap point when drag ends
- onSnapPointChange?.(null);
- }
- return;
- }
-
- const isMouseOverThisTrack =
- e.clientY >= timelineRect.top && e.clientY <= timelineRect.bottom;
-
- if (!isMouseOverThisTrack && !isTrackThatStartedDrag) return;
-
- const finalTime = dragState.currentTime;
-
- if (isMouseOverThisTrack) {
- const sourceTrack = tracks.find((t) => t.id === dragState.trackId);
- const movingElement = sourceTrack?.elements.find(
- (c) => c.id === dragState.elementId
- );
-
- if (movingElement) {
- const movingElementDuration =
- movingElement.duration -
- movingElement.trimStart -
- movingElement.trimEnd;
- const movingElementEnd = finalTime + movingElementDuration;
-
- const targetTrack = tracks.find((t) => t.id === track.id);
- const hasOverlap = targetTrack?.elements.some((existingElement) => {
- if (
- dragState.trackId === track.id &&
- existingElement.id === dragState.elementId
- ) {
- return false;
- }
- const existingStart = existingElement.startTime;
- const existingEnd =
- existingElement.startTime +
- (existingElement.duration -
- existingElement.trimStart -
- existingElement.trimEnd);
- return finalTime < existingEnd && movingElementEnd > existingStart;
- });
-
- if (!hasOverlap) {
- if (dragState.trackId === track.id) {
- if (rippleEditingEnabled) {
- updateElementStartTimeWithRipple(
- track.id,
- dragState.elementId,
- finalTime
- );
- } else {
- updateElementStartTime(
- track.id,
- dragState.elementId,
- finalTime
- );
- }
- } else {
- moveElementToTrack(
- dragState.trackId,
- track.id,
- dragState.elementId
- );
- requestAnimationFrame(() => {
- if (rippleEditingEnabled) {
- updateElementStartTimeWithRipple(
- track.id,
- dragState.elementId!,
- finalTime
- );
- } else {
- updateElementStartTime(
- track.id,
- dragState.elementId!,
- finalTime
- );
- }
- });
- }
- }
- }
- } else if (isTrackThatStartedDrag) {
- // Mouse is not over this track, but this track started the drag
- // This means user released over ruler/outside - update position within same track
- const sourceTrack = tracks.find((t) => t.id === dragState.trackId);
- const movingElement = sourceTrack?.elements.find(
- (c) => c.id === dragState.elementId
- );
-
- if (movingElement) {
- const movingElementDuration =
- movingElement.duration -
- movingElement.trimStart -
- movingElement.trimEnd;
- const movingElementEnd = finalTime + movingElementDuration;
-
- const hasOverlap = track.elements.some((existingElement) => {
- if (existingElement.id === dragState.elementId) {
- return false;
- }
- const existingStart = existingElement.startTime;
- const existingEnd =
- existingElement.startTime +
- (existingElement.duration -
- existingElement.trimStart -
- existingElement.trimEnd);
- return finalTime < existingEnd && movingElementEnd > existingStart;
- });
-
- if (!hasOverlap) {
- if (rippleEditingEnabled) {
- updateElementStartTimeWithRipple(
- track.id,
- dragState.elementId,
- finalTime
- );
- } else {
- updateElementStartTime(track.id, dragState.elementId, finalTime);
- }
- }
- }
- }
-
- if (isTrackThatStartedDrag) {
- endDragAction();
- // Clear snap point when drag ends
- onSnapPointChange?.(null);
- }
- };
-
- document.addEventListener("mousemove", handleMouseMove);
- document.addEventListener("mouseup", handleMouseUp);
-
- return () => {
- document.removeEventListener("mousemove", handleMouseMove);
- document.removeEventListener("mouseup", handleMouseUp);
- };
- }, [
- dragState.isDragging,
- dragState.clickOffsetTime,
- dragState.elementId,
- dragState.trackId,
- dragState.currentTime,
- zoomLevel,
- tracks,
- track.id,
- updateDragTime,
- updateElementStartTime,
- moveElementToTrack,
- endDragAction,
- selectedElements,
- selectElement,
- onSnapPointChange,
- ]);
-
- useEdgeAutoScroll({
- isActive: dragState.isDragging,
- getMouseClientX: () => lastMouseXRef.current,
- rulerScrollRef,
- tracksScrollRef,
- contentWidth: duration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel,
- });
-
- const handleElementMouseDown = (
- e: React.MouseEvent,
- element: TimelineElementType
- ) => {
- setMouseDownLocation({ x: e.clientX, y: e.clientY });
-
- // Detect right-click (button 2) and handle selection without starting drag
- const isRightClick = e.button === 2;
- const isMultiSelect = e.metaKey || e.ctrlKey || e.shiftKey;
-
- if (isRightClick) {
- // Handle right-click selection
- const isSelected = selectedElements.some(
- (c) => c.trackId === track.id && c.elementId === element.id
- );
-
- // If element is not selected, select it (keep other selections if multi-select)
- if (!isSelected) {
- selectElement(track.id, element.id, isMultiSelect);
- }
- // If element is already selected, keep it selected
-
- // Don't start drag action for right-clicks
- return;
- }
-
- // Handle multi-selection for left-click with modifiers
- if (isMultiSelect) {
- selectElement(track.id, element.id, true);
- }
-
- // Calculate the offset from the left edge of the element to where the user clicked
- const elementElement = e.currentTarget as HTMLElement;
- const elementRect = elementElement.getBoundingClientRect();
- const clickOffsetX = e.clientX - elementRect.left;
- const clickOffsetTime =
- clickOffsetX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel);
-
- startDragAction(
- element.id,
- track.id,
- e.clientX,
- element.startTime,
- clickOffsetTime
- );
- };
-
- const handleElementClick = (
- e: React.MouseEvent,
- element: TimelineElementType
- ) => {
- e.stopPropagation();
-
- // Check if mouse moved significantly
- if (mouseDownLocation) {
- const deltaX = Math.abs(e.clientX - mouseDownLocation.x);
- const deltaY = Math.abs(e.clientY - mouseDownLocation.y);
- // If it moved more than a few pixels, consider it a drag and not a click.
- if (deltaX > 5 || deltaY > 5) {
- setMouseDownLocation(null); // Reset for next interaction
- return;
- }
- }
-
- // Skip selection logic for multi-selection (handled in mousedown)
- if (e.metaKey || e.ctrlKey || e.shiftKey) {
- return;
- }
-
- // Handle single selection
- const isSelected = selectedElements.some(
- (c) => c.trackId === track.id && c.elementId === element.id
- );
-
- if (!isSelected) {
- // If element is not selected, select it (replacing other selections)
- selectElement(track.id, element.id, false);
- }
- // If element is already selected, keep it selected (do nothing)
- };
-
- const handleTrackDragOver = (e: React.DragEvent) => {
- e.preventDefault();
-
- // Handle both timeline elements and media items
- const hasTimelineElement = e.dataTransfer.types.includes(
- "application/x-timeline-element"
- );
- const hasMediaItem = e.dataTransfer.types.includes(
- "application/x-media-item"
- );
-
- if (!hasTimelineElement && !hasMediaItem) return;
-
- // Calculate drop position for overlap checking
- const trackContainer = e.currentTarget.querySelector(
- ".track-elements-container"
- ) as HTMLElement;
- let dropTime = 0;
- if (trackContainer) {
- const rect = trackContainer.getBoundingClientRect();
- const mouseX = Math.max(0, e.clientX - rect.left);
- dropTime = mouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel);
- }
-
- // Check for potential overlaps and show appropriate feedback
- let wouldOverlap = false;
-
- if (hasMediaItem) {
- try {
- const mediaItemData = e.dataTransfer.getData(
- "application/x-media-item"
- );
- if (mediaItemData) {
- const dragData: DragData = JSON.parse(mediaItemData);
-
- if (dragData.type === "text") {
- // Text elements have default duration of 5 seconds
- const newElementDuration = 5;
- const snappedTime = getDropSnappedTime(
- dropTime,
- newElementDuration
- );
- const newElementEnd = snappedTime + newElementDuration;
-
- wouldOverlap = track.elements.some((existingElement) => {
- const existingStart = existingElement.startTime;
- const existingEnd =
- existingElement.startTime +
- (existingElement.duration -
- existingElement.trimStart -
- existingElement.trimEnd);
- return snappedTime < existingEnd && newElementEnd > existingStart;
- });
- } else {
- // Media elements
- const mediaItem = mediaFiles.find(
- (item) => item.id === dragData.id
- );
- if (mediaItem) {
- const newElementDuration = mediaItem.duration || 5;
- const snappedTime = getDropSnappedTime(
- dropTime,
- newElementDuration
- );
- const newElementEnd = snappedTime + newElementDuration;
-
- wouldOverlap = track.elements.some((existingElement) => {
- const existingStart = existingElement.startTime;
- const existingEnd =
- existingElement.startTime +
- (existingElement.duration -
- existingElement.trimStart -
- existingElement.trimEnd);
- return (
- snappedTime < existingEnd && newElementEnd > existingStart
- );
- });
- }
- }
- }
- } catch (error) {
- // Continue with default behavior
- }
- } else if (hasTimelineElement) {
- try {
- const timelineElementData = e.dataTransfer.getData(
- "application/x-timeline-element"
- );
- if (timelineElementData) {
- const { elementId, trackId: fromTrackId } =
- JSON.parse(timelineElementData);
- const sourceTrack = tracks.find(
- (t: TimelineTrack) => t.id === fromTrackId
- );
- const movingElement = sourceTrack?.elements.find(
- (c: any) => c.id === elementId
- );
-
- if (movingElement) {
- const movingElementDuration =
- movingElement.duration -
- movingElement.trimStart -
- movingElement.trimEnd;
- const snappedTime = getDropSnappedTime(
- dropTime,
- movingElementDuration,
- elementId
- );
- const movingElementEnd = snappedTime + movingElementDuration;
-
- wouldOverlap = track.elements.some((existingElement) => {
- if (fromTrackId === track.id && existingElement.id === elementId)
- return false;
-
- const existingStart = existingElement.startTime;
- const existingEnd =
- existingElement.startTime +
- (existingElement.duration -
- existingElement.trimStart -
- existingElement.trimEnd);
- return (
- snappedTime < existingEnd && movingElementEnd > existingStart
- );
- });
- }
- }
- } catch (error) {
- // Continue with default behavior
- }
- }
-
- if (wouldOverlap) {
- e.dataTransfer.dropEffect = "none";
- setWouldOverlap(true);
- // Use default duration for position indicator
- setDropPosition(getDropSnappedTime(dropTime, 5));
- return;
- }
-
- e.dataTransfer.dropEffect = hasTimelineElement ? "move" : "copy";
- setWouldOverlap(false);
- // Use default duration for position indicator
- setDropPosition(getDropSnappedTime(dropTime, 5));
- };
-
- const handleTrackDragEnter = (e: React.DragEvent) => {
- e.preventDefault();
-
- const hasTimelineElement = e.dataTransfer.types.includes(
- "application/x-timeline-element"
- );
- const hasMediaItem = e.dataTransfer.types.includes(
- "application/x-media-item"
- );
-
- if (!hasTimelineElement && !hasMediaItem) return;
-
- dragCounterRef.current++;
- setIsDropping(true);
- };
-
- const handleTrackDragLeave = (e: React.DragEvent) => {
- e.preventDefault();
-
- const hasTimelineElement = e.dataTransfer.types.includes(
- "application/x-timeline-element"
- );
- const hasMediaItem = e.dataTransfer.types.includes(
- "application/x-media-item"
- );
-
- if (!hasTimelineElement && !hasMediaItem) return;
-
- dragCounterRef.current--;
-
- if (dragCounterRef.current === 0) {
- setIsDropping(false);
- setWouldOverlap(false);
- setDropPosition(null);
- }
- };
-
- const handleTrackDrop = (e: React.DragEvent) => {
- e.preventDefault();
- e.stopPropagation();
-
- // Debug logging
- console.log(
- JSON.stringify({
- message: "Drop event started in timeline track",
- dataTransferTypes: Array.from(e.dataTransfer.types),
- trackId: track.id,
- trackType: track.type,
- })
- );
-
- // Reset all drag states
- dragCounterRef.current = 0;
- setIsDropping(false);
- setWouldOverlap(false);
-
- const hasTimelineElement = e.dataTransfer.types.includes(
- "application/x-timeline-element"
- );
- const hasMediaItem = e.dataTransfer.types.includes(
- "application/x-media-item"
- );
- const hasFiles = e.dataTransfer.files?.length > 0;
-
- if (!hasTimelineElement && !hasMediaItem && !hasFiles) return;
-
- const trackContainer = e.currentTarget.querySelector(
- ".track-elements-container"
- ) as HTMLElement;
- if (!trackContainer) return;
-
- const rect = trackContainer.getBoundingClientRect();
- const mouseX = Math.max(0, e.clientX - rect.left);
- const mouseY = e.clientY - rect.top; // Get Y position relative to this track
- const newStartTime =
- mouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel);
- const projectStore = useProjectStore.getState();
- const projectFps = projectStore.activeProject?.fps || DEFAULT_FPS;
- const snappedTime = snapTimeToFrame(newStartTime, projectFps);
-
- // Calculate drop position relative to tracks
- const currentTrackIndex = tracks.findIndex((t) => t.id === track.id);
-
- // Determine drop zone within the track (top 20px, middle 20px, bottom 20px)
- let dropPosition: "above" | "on" | "below";
- if (mouseY < 20) {
- dropPosition = "above";
- } else if (mouseY > 40) {
- dropPosition = "below";
- } else {
- dropPosition = "on";
- }
-
- try {
- if (hasTimelineElement) {
- // Handle timeline element movement
- const timelineElementData = e.dataTransfer.getData(
- "application/x-timeline-element"
- );
- if (!timelineElementData) return;
-
- const {
- elementId,
- trackId: fromTrackId,
- clickOffsetTime = 0,
- } = JSON.parse(timelineElementData);
-
- // Find the element being moved
- const sourceTrack = tracks.find(
- (t: TimelineTrack) => t.id === fromTrackId
- );
- const movingElement = sourceTrack?.elements.find(
- (c: TimelineElementType) => c.id === elementId
- );
-
- if (!movingElement) {
- toast.error("Element not found");
- return;
- }
-
- // Check for overlaps with existing elements (excluding the moving element itself)
- const movingElementDuration =
- movingElement.duration -
- movingElement.trimStart -
- movingElement.trimEnd;
-
- // Adjust position based on where user clicked on the element
- const adjustedStartTime = newStartTime - clickOffsetTime;
- const snappedStartTime = getDropSnappedTime(
- adjustedStartTime,
- movingElementDuration,
- elementId
- );
- const finalStartTime = Math.max(0, snappedStartTime);
- const movingElementEnd = finalStartTime + movingElementDuration;
-
- const hasOverlap = track.elements.some((existingElement) => {
- // Skip the element being moved if it's on the same track
- if (fromTrackId === track.id && existingElement.id === elementId)
- return false;
-
- const existingStart = existingElement.startTime;
- const existingEnd =
- existingElement.startTime +
- (existingElement.duration -
- existingElement.trimStart -
- existingElement.trimEnd);
-
- // Check if elements overlap
- return (
- finalStartTime < existingEnd && movingElementEnd > existingStart
- );
- });
-
- if (hasOverlap) {
- toast.error(
- "Cannot move element here - it would overlap with existing elements"
- );
- return;
- }
-
- if (fromTrackId === track.id) {
- // Moving within same track
- if (rippleEditingEnabled) {
- updateElementStartTimeWithRipple(
- track.id,
- elementId,
- finalStartTime
- );
- } else {
- updateElementStartTime(track.id, elementId, finalStartTime);
- }
- } else {
- // Moving to different track
- moveElementToTrack(fromTrackId, track.id, elementId);
- requestAnimationFrame(() => {
- if (rippleEditingEnabled) {
- updateElementStartTimeWithRipple(
- track.id,
- elementId,
- finalStartTime
- );
- } else {
- updateElementStartTime(track.id, elementId, finalStartTime);
- }
- });
- }
- } else if (hasMediaItem) {
- // Handle media item drop
- const mediaItemData = e.dataTransfer.getData(
- "application/x-media-item"
- );
- if (!mediaItemData) return;
-
- const dragData: DragData = JSON.parse(mediaItemData);
-
- if (dragData.type === "text") {
- let targetTrackId = track.id;
- let targetTrack = track;
-
- // Handle position-aware track creation for text
- if (track.type !== "text" || dropPosition !== "on") {
- // Text tracks should go above the main track
- const mainTrack = getMainTrack(tracks);
- let insertIndex: number;
-
- if (dropPosition === "above") {
- insertIndex = currentTrackIndex;
- } else if (dropPosition === "below") {
- insertIndex = currentTrackIndex + 1;
- } else {
- // dropPosition === "on" but track is not text type
- // Insert above main track if main track exists, otherwise at top
- if (mainTrack) {
- const mainTrackIndex = tracks.findIndex(
- (t) => t.id === mainTrack.id
- );
- insertIndex = mainTrackIndex;
- } else {
- insertIndex = 0; // Top of timeline
- }
- }
-
- targetTrackId = insertTrackAt("text", insertIndex);
- // Get the updated tracks array after creating the new track
- const updatedTracks = useTimelineStore.getState().tracks;
- const newTargetTrack = updatedTracks.find(
- (t) => t.id === targetTrackId
- );
- if (!newTargetTrack) return;
- targetTrack = newTargetTrack;
- }
-
- // Check for overlaps with existing elements in target track
- const newElementDuration = 5; // Default text duration
- const textSnappedTime = getDropSnappedTime(
- newStartTime,
- newElementDuration
- );
- const newElementEnd = textSnappedTime + newElementDuration;
-
- const hasOverlap = targetTrack.elements.some((existingElement) => {
- const existingStart = existingElement.startTime;
- const existingEnd =
- existingElement.startTime +
- (existingElement.duration -
- existingElement.trimStart -
- existingElement.trimEnd);
-
- // Check if elements overlap
- return (
- textSnappedTime < existingEnd && newElementEnd > existingStart
- );
- });
-
- if (hasOverlap) {
- toast.error(
- "Cannot place element here - it would overlap with existing elements"
- );
- return;
- }
-
- addElementToTrack(targetTrackId, {
- ...DEFAULT_TEXT_ELEMENT,
- name: dragData.name || DEFAULT_TEXT_ELEMENT.name,
- content: dragData.content || DEFAULT_TEXT_ELEMENT.content,
- startTime: textSnappedTime,
- });
- } else {
- // Handle media items
- const mediaItem = mediaFiles.find((item) => item.id === dragData.id);
-
- if (!mediaItem) {
- toast.error("Media item not found");
- return;
- }
-
- let targetTrackId = track.id;
-
- // Check if track type is compatible
- const isVideoOrImage =
- dragData.type === "video" || dragData.type === "image";
- const isAudio = dragData.type === "audio";
- const isCompatible = isVideoOrImage
- ? canElementGoOnTrack("media", track.type)
- : isAudio
- ? canElementGoOnTrack("media", track.type)
- : false;
-
- let targetTrack = tracks.find((t) => t.id === targetTrackId);
-
- // Handle position-aware track creation for media elements
- if (!isCompatible || dropPosition !== "on") {
- if (isVideoOrImage) {
- // For video/image, check if we need a main track or additional media track
- const mainTrack = getMainTrack(tracks);
-
- if (!mainTrack) {
- // No main track exists, create it
- targetTrackId = addTrack("media");
- const updatedTracks = useTimelineStore.getState().tracks;
- const newTargetTrack = updatedTracks.find(
- (t) => t.id === targetTrackId
- );
- if (!newTargetTrack) return;
- targetTrack = newTargetTrack;
- } else if (
- mainTrack.elements.length === 0 &&
- dropPosition === "on"
- ) {
- // Main track exists and is empty, use it
- targetTrackId = mainTrack.id;
- targetTrack = mainTrack;
- } else {
- // Create new media track
- let insertIndex: number;
-
- if (dropPosition === "above") {
- insertIndex = currentTrackIndex;
- } else if (dropPosition === "below") {
- insertIndex = currentTrackIndex + 1;
- } else {
- // Insert above main track
- const mainTrackIndex = tracks.findIndex(
- (t) => t.id === mainTrack.id
- );
- insertIndex = mainTrackIndex;
- }
-
- targetTrackId = insertTrackAt("media", insertIndex);
- const updatedTracks = useTimelineStore.getState().tracks;
- const newTargetTrack = updatedTracks.find(
- (t) => t.id === targetTrackId
- );
- if (!newTargetTrack) return;
- targetTrack = newTargetTrack;
- }
- } else if (isAudio) {
- // Audio tracks go at the bottom
- const mainTrack = getMainTrack(tracks);
- let insertIndex: number;
-
- if (dropPosition === "above") {
- insertIndex = currentTrackIndex;
- } else if (dropPosition === "below") {
- insertIndex = currentTrackIndex + 1;
- } else {
- // Insert after main track (bottom area)
- if (mainTrack) {
- const mainTrackIndex = tracks.findIndex(
- (t) => t.id === mainTrack.id
- );
- insertIndex = mainTrackIndex + 1;
- } else {
- insertIndex = tracks.length; // Bottom of timeline
- }
- }
-
- targetTrackId = insertTrackAt("audio", insertIndex);
- const updatedTracks = useTimelineStore.getState().tracks;
- const newTargetTrack = updatedTracks.find(
- (t) => t.id === targetTrackId
- );
- if (!newTargetTrack) return;
- targetTrack = newTargetTrack;
- }
- }
-
- if (!targetTrack) return;
-
- // Check for overlaps with existing elements in target track
- const newElementDuration = mediaItem.duration || 5;
- const mediaSnappedTime = getDropSnappedTime(
- newStartTime,
- newElementDuration
- );
- const newElementEnd = mediaSnappedTime + newElementDuration;
-
- const hasOverlap = targetTrack.elements.some((existingElement) => {
- const existingStart = existingElement.startTime;
- const existingEnd =
- existingElement.startTime +
- (existingElement.duration -
- existingElement.trimStart -
- existingElement.trimEnd);
-
- // Check if elements overlap
- return (
- mediaSnappedTime < existingEnd && newElementEnd > existingStart
- );
- });
-
- if (hasOverlap) {
- toast.error(
- "Cannot place element here - it would overlap with existing elements"
- );
- return;
- }
-
- addElementToTrack(targetTrackId, {
- type: "media",
- mediaId: mediaItem.id,
- name: mediaItem.name,
- duration: mediaItem.duration || 5,
- startTime: mediaSnappedTime,
- trimStart: 0,
- trimEnd: 0,
- });
- }
- } else if (hasFiles) {
- // External file drops
- const { activeProject } = useProjectStore.getState();
- const { addMediaFile } = useMediaStore.getState();
- const { addElementToTrack } = useTimelineStore.getState();
-
- if (!activeProject) {
- toast.error("No active project");
- return;
- }
-
- // Process and add files to new timeline tracks at playhead position
- processMediaFiles(e.dataTransfer.files)
- .then(async (processedItems) => {
- for (const processedItem of processedItems) {
- await addMediaFile(activeProject.id, processedItem);
- const currentMediaFiles = mediaFiles;
- const addedItem = currentMediaFiles.find(
- (item) =>
- item.name === processedItem.name &&
- item.url === processedItem.url
- );
-
- if (addedItem) {
- const trackType: TrackType =
- addedItem.type === "audio" ? "audio" : "media";
- const targetTrackId = insertTrackAt(trackType, 0);
-
- addElementToTrack(targetTrackId, {
- type: "media",
- mediaId: addedItem.id,
- name: addedItem.name,
- duration: addedItem.duration || 5,
- startTime: currentTime,
- trimStart: 0,
- trimEnd: 0,
- });
- }
- }
- })
- .catch((error) => {
- console.error("Error processing external files:", error);
- toast.error("Failed to process dropped files");
- });
- }
- } catch (error) {
- console.error("Error handling drop:", error);
- toast.error("Failed to add media to track");
- }
- };
+interface TimelineTrackContentProps {
+ track: TimelineTrack;
+ zoomLevel: number;
+ dragState: ElementDragState;
+ rulerScrollRef: React.RefObject<HTMLDivElement>;
+ tracksScrollRef: React.RefObject<HTMLDivElement>;
+ lastMouseXRef: React.RefObject<number>;
+ onSnapPointChange?: (snapPoint: SnapPoint | null) => void;
+ onResizeStateChange?: (params: { isResizing: boolean }) => void;
+ onElementMouseDown: (params: {
+ event: React.MouseEvent;
+ element: TimelineElementType;
+ track: TimelineTrack;
+ }) => void;
+ onElementClick: (params: {
+ event: React.MouseEvent;
+ element: TimelineElementType;
+ track: TimelineTrack;
+ }) => void;
+ onTrackMouseDown?: (event: React.MouseEvent) => void;
+ shouldIgnoreClick?: () => boolean;
+}
- return (
- <div
- className="w-full h-full hover:bg-muted/20"
- onClick={(e) => {
- // If clicking empty area (not on an element), deselect all elements
- if (!(e.target as HTMLElement).closest(".timeline-element")) {
- clearSelectedElements();
- }
- }}
- onDragOver={handleTrackDragOver}
- onDragEnter={handleTrackDragEnter}
- onDragLeave={handleTrackDragLeave}
- onDrop={handleTrackDrop}
- >
- <div
- ref={timelineRef}
- className="h-full relative track-elements-container min-w-full"
- >
- {track.elements.length === 0 ? (
- <div
- className={`h-full w-full rounded-sm border-2 border-dashed flex items-center justify-center text-xs text-muted-foreground transition-colors ${
- isDropping
- ? wouldOverlap
- ? "border-red-500 bg-red-500/10 text-red-600"
- : "border-blue-500 bg-blue-500/10 text-blue-600"
- : "border-muted/30"
- }`}
- >
- {isDropping
- ? wouldOverlap
- ? "Cannot drop - would overlap"
- : "Drop element here"
- : ""}
- </div>
- ) : (
- <>
- {track.elements.map((element) => {
- const isSelected = selectedElements.some(
- (c) => c.trackId === track.id && c.elementId === element.id
- );
+export function TimelineTrackContent({
+ track,
+ zoomLevel,
+ dragState,
+ rulerScrollRef,
+ tracksScrollRef,
+ lastMouseXRef,
+ onSnapPointChange,
+ onResizeStateChange,
+ onElementMouseDown,
+ onElementClick,
+ onTrackMouseDown,
+ shouldIgnoreClick,
+}: TimelineTrackContentProps) {
+ const editor = useEditor();
+ const { isElementSelected, clearElementSelection } = useElementSelection();
- const handleElementSplit = () => {
- const { currentTime } = usePlaybackStore();
- const { splitSelected } = useTimelineStore();
- const splitTime = currentTime;
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
+ const duration = editor.timeline.getTotalDuration();
- if (splitTime > effectiveStart && splitTime < effectiveEnd) {
- splitSelected(splitTime, track.id, element.id);
- } else {
- toast.error("Playhead must be within element to split");
- }
- };
+ useEdgeAutoScroll({
+ isActive: dragState.isDragging,
+ getMouseClientX: () => lastMouseXRef.current ?? 0,
+ rulerScrollRef,
+ tracksScrollRef,
+ contentWidth: duration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel,
+ });
- const handleElementDuplicate = () => {
- const { addElementToTrack } = useTimelineStore.getState();
- const { id, ...elementWithoutId } = element;
- addElementToTrack(track.id, {
- ...elementWithoutId,
- name: element.name + " (copy)",
- startTime:
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd) +
- 0.1,
- });
- };
+ const hasSelectedElements = track.elements.some((element) =>
+ isElementSelected({ trackId: track.id, elementId: element.id }),
+ );
- const handleElementDelete = () => {
- const { deleteSelected } = useTimelineStore.getState();
- deleteSelected(track.id, element.id);
- };
+ return (
+ <button
+ className={cn("size-full", hasSelectedElements && "bg-panel-accent/35")}
+ onClick={() => {
+ if (shouldIgnoreClick?.()) return;
+ clearElementSelection();
+ }}
+ onMouseDown={(event) => {
+ onTrackMouseDown?.(event);
+ }}
+ type="button"
+ >
+ <div className="relative h-full min-w-full">
+ {track.elements.length === 0 ? (
+ <div className="text-muted-foreground border-muted/30 flex size-full items-center justify-center rounded-sm border-2 border-dashed text-xs" />
+ ) : (
+ track.elements.map((element) => {
+ const isSelected = isElementSelected({
+ trackId: track.id,
+ elementId: element.id,
+ });
- return (
- <TimelineElement
- key={element.id}
- element={element}
- track={track}
- zoomLevel={zoomLevel}
- isSelected={isSelected}
- onElementMouseDown={handleElementMouseDown}
- onElementClick={handleElementClick}
- />
- );
- })}
- </>
- )}
- </div>
- </div>
- );
+ return (
+ <TimelineElement
+ key={element.id}
+ element={element}
+ track={track}
+ zoomLevel={zoomLevel}
+ isSelected={isSelected}
+ onSnapPointChange={onSnapPointChange}
+ onResizeStateChange={onResizeStateChange}
+ onElementMouseDown={(event, element) =>
+ onElementMouseDown({ event, element, track })
+ }
+ onElementClick={(event, element) =>
+ onElementClick({ event, element, track })
+ }
+ dragState={dragState}
+ />
+ );
+ })
+ )}
+ </div>
+ </button>
+ );
}
diff --git a/apps/web/src/components/footer.tsx b/apps/web/src/components/footer.tsx
index 176452cd..7c78fc57 100644
--- a/apps/web/src/components/footer.tsx
+++ b/apps/web/src/components/footer.tsx
@@ -1,130 +1,122 @@
-"use client";
-
-import { motion } from "motion/react";
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, SOCIAL_LINKS } from "@/constants/site-constants";
+import { capitalizeFirstLetter } from "@/utils/string";
-export function Footer() {
- return (
- <motion.footer
- className="bg-background border-t"
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{ delay: 0.8, duration: 0.8 }}
- >
- <div className="max-w-5xl mx-auto px-8 py-10">
- <div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-8">
- {/* Brand Section */}
- <div className="md:col-span-1 max-w-sm">
- <div className="flex justify-start items-center gap-2 mb-4">
- <Image
- src="/logo.svg"
- alt="OpenCut"
- width={24}
- height={24}
- className="invert dark:invert-0"
- />
- <span className="font-bold text-lg">OpenCut</span>
- </div>
- <p className="text-sm md:text-left text-muted-foreground mb-5">
- The open source video editor that gets the job done. Simple,
- powerful, and works on any platform.
- </p>
- <div className="flex justify-start gap-3">
- <Link
- href="https://github.com/OpenCut-app/OpenCut"
- className="text-muted-foreground hover:text-foreground transition-colors"
- target="_blank"
- rel="noopener noreferrer"
- >
- <FaGithub className="h-5 w-5" />
- </Link>
- <Link
- href="https://x.com/OpenCutApp"
- className="text-muted-foreground hover:text-foreground transition-colors"
- target="_blank"
- rel="noopener noreferrer"
- >
- <RiTwitterXLine className="h-5 w-5" />
- </Link>
- <Link
- href="https://discord.com/invite/Mu3acKZvCp"
- className="text-muted-foreground hover:text-foreground transition-colors"
- target="_blank"
- rel="noopener noreferrer"
- >
- <RiDiscordFill className="h-5 w-5" />
- </Link>
- </div>
- </div>
+type Category = "resources" | "company";
+
+interface FooterLink {
+ label: string;
+ href: string;
+}
- <div className="flex gap-12 justify-start items-start py-2">
- <div>
- <h3 className="font-semibold text-foreground mb-4">Resources</h3>
- <ul className="space-y-2 text-sm">
- <li>
- <Link
- href="/roadmap"
- className="text-muted-foreground hover:text-foreground transition-colors"
- >
- Roadmap
- </Link>
- </li>
- <li>
- <Link
- href="/privacy"
- className="text-muted-foreground hover:text-foreground transition-colors"
- >
- Privacy policy
- </Link>
- </li>
- <li>
- <Link
- href="/terms"
- className="text-muted-foreground hover:text-foreground transition-colors"
- >
- Terms of use
- </Link>
- </li>
- </ul>
- </div>
+type CategoryLinks = Record<Category, FooterLink[]>;
+
+const links: CategoryLinks = {
+ resources: [
+ { label: "Roadmap", href: "/roadmap" },
+ { label: "Blog", href: "/blog" },
+ { label: "Privacy", href: "/privacy" },
+ { label: "Terms of use", href: "/terms" },
+ ],
+ company: [
+ { label: "Contributors", href: "/contributors" },
+ { label: "Sponsors", href: "/sponsors" },
+ { label: "Branding", href: "/branding" },
+ { label: "About", href: `${SOCIAL_LINKS.github}/blob/main/README.md` },
+ ],
+};
+
+export function Footer() {
+ return (
+ <footer className="bg-background border-t">
+ <div className="mx-auto max-w-5xl px-8 py-10">
+ <div className="mb-8 grid grid-cols-1 gap-12 md:grid-cols-2">
+ {/* Brand Section */}
+ <div className="max-w-sm md:col-span-1">
+ <div className="mb-4 flex items-center justify-start gap-2">
+ <Image
+ src={DEFAULT_LOGO_URL}
+ alt="OpenCut"
+ width={24}
+ height={24}
+ className="invert dark:invert-0"
+ />
+ <span className="text-lg font-bold">OpenCut</span>
+ </div>
+ <p className="text-muted-foreground mb-5 text-sm md:text-left">
+ The privacy-first video editor that feels simple to use.
+ </p>
+ <div className="flex justify-start gap-3">
+ <Link
+ href={SOCIAL_LINKS.github}
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <FaGithub className="size-5" />
+ </Link>
+ <Link
+ href={SOCIAL_LINKS.x}
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <RiTwitterXLine className="size-5" />
+ </Link>
+ <Link
+ href={SOCIAL_LINKS.discord}
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <RiDiscordFill className="size-5" />
+ </Link>
+ </div>
+ </div>
- {/* Company Links */}
- <div>
- <h3 className="font-semibold text-foreground mb-4">Company</h3>
- <ul className="space-y-2 text-sm">
- <li>
- <Link
- href="/contributors"
- className="text-muted-foreground hover:text-foreground transition-colors"
- >
- Contributors
- </Link>
- </li>
- <li>
- <Link
- href="https://github.com/OpenCut-app/OpenCut/blob/main/README.md"
- className="text-muted-foreground hover:text-foreground transition-colors"
- target="_blank"
- rel="noopener noreferrer"
- >
- About
- </Link>
- </li>
- </ul>
- </div>
- </div>
- </div>
+ <div className="flex items-start justify-start gap-12 py-2">
+ {(Object.keys(links) as Category[]).map((category) => (
+ <div key={category} className="flex flex-col gap-2">
+ <h3 className="text-foreground font-semibold">
+ {capitalizeFirstLetter({ string: category })}
+ </h3>
+ <ul className="space-y-2 text-sm">
+ {links[category].map((link) => (
+ <li key={link.href}>
+ <Link
+ href={link.href}
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ target={
+ link.href.startsWith("http") ? "_blank" : undefined
+ }
+ rel={
+ link.href.startsWith("http")
+ ? "noopener noreferrer"
+ : undefined
+ }
+ >
+ {link.label}
+ </Link>
+ </li>
+ ))}
+ </ul>
+ </div>
+ ))}
+ </div>
+ </div>
- {/* Bottom Section */}
- <div className="pt-2 flex flex-col md:flex-row justify-between items-start gap-4">
- <div className="flex items-center gap-4 text-sm text-muted-foreground">
- <span>© 2025 OpenCut, All Rights Reserved</span>
- </div>
- </div>
- </div>
- </motion.footer>
- );
+ {/* Bottom Section */}
+ <div className="flex flex-col items-start justify-between gap-4 pt-2 md:flex-row">
+ <div className="text-muted-foreground flex items-center gap-4 text-sm">
+ <span>
+ © {new Date().getFullYear()} OpenCut, All Rights Reserved
+ </span>
+ </div>
+ </div>
+ </div>
+ </footer>
+ );
}
diff --git a/apps/web/src/components/gitHub-contribute-section.tsx b/apps/web/src/components/gitHub-contribute-section.tsx
new file mode 100644
index 00000000..f96def4a
--- /dev/null
+++ b/apps/web/src/components/gitHub-contribute-section.tsx
@@ -0,0 +1,47 @@
+import { Button } from "./ui/button";
+import Link from "next/link";
+import { SOCIAL_LINKS } from "@/constants/site-constants";
+import {
+ GithubIcon,
+ Link04Icon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
+
+export function GitHubContributeSection({
+ title,
+ description,
+}: {
+ title: string;
+ description: string;
+}) {
+ return (
+ <div className="flex flex-col gap-6">
+ <div className="flex flex-col gap-4 text-center">
+ <h3 className="text-2xl font-semibold">{title}</h3>
+ <p className="text-muted-foreground">{description}</p>
+ </div>
+ <div className="flex flex-col justify-center gap-4 sm:flex-row">
+ <Link
+ href={`${SOCIAL_LINKS.github}/blob/main/.github/CONTRIBUTING.md`}
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <Button className="w-full" size="lg">
+ <HugeiconsIcon icon={GithubIcon} />
+ Start contributing
+ </Button>
+ </Link>
+ <Link
+ href={`${SOCIAL_LINKS.github}/issues`}
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <Button variant="outline" className="w-full" size="lg">
+ <HugeiconsIcon icon={Link04Icon} />
+ Report issues
+ </Button>
+ </Link>
+ </div>
+ </div>
+ );
+}
diff --git a/apps/web/src/components/header-base.tsx b/apps/web/src/components/header-base.tsx
deleted file mode 100644
index 98f1c431..00000000
--- a/apps/web/src/components/header-base.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-"use client";
-
-import { cn } from "@/lib/utils";
-import { ReactNode } from "react";
-
-interface HeaderBaseProps {
- leftContent?: ReactNode;
- centerContent?: ReactNode;
- rightContent?: ReactNode;
- className?: string;
- children?: ReactNode;
-}
-
-export function HeaderBase({
- leftContent,
- centerContent,
- rightContent,
- className,
- children,
-}: HeaderBaseProps) {
- // If children is provided, render it directly without the grid layout
- if (children) {
- return (
- <header className={cn("px-6 h-16 flex items-center", className)}>
- {children}
- </header>
- );
- }
-
- return (
- <header
- className={cn("px-6 h-14 flex justify-between items-center", className)}
- >
- {leftContent && <div className="flex items-center">{leftContent}</div>}
- {centerContent && (
- <div className="flex items-center">{centerContent}</div>
- )}
- {rightContent && <div className="flex items-center">{rightContent}</div>}
- </header>
- );
-}
diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx
index f8877330..95f08304 100644
--- a/apps/web/src/components/header.tsx
+++ b/apps/web/src/components/header.tsx
@@ -1,57 +1,147 @@
"use client";
+import { useState } from "react";
import Link from "next/link";
+import { motion } from "motion/react";
import { Button } from "./ui/button";
import { ArrowRight } from "lucide-react";
-import { HeaderBase } from "./header-base";
import Image from "next/image";
import { ThemeToggle } from "./theme-toggle";
+import { GithubIcon, Menu02Icon } from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { cn } from "@/utils/ui";
+import { DEFAULT_LOGO_URL, SOCIAL_LINKS } from "@/constants/site-constants";
export function Header() {
- const leftContent = (
- <Link href="/" className="flex items-center gap-3">
- <Image
- src="/logo.svg"
- alt="OpenCut Logo"
- className="invert dark:invert-0"
- width={32}
- height={32}
- />
- <span className="text-xl font-medium hidden md:block">OpenCut</span>
- </Link>
- );
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+ const closeMenu = () => setIsMenuOpen(false);
- const rightContent = (
- <nav className="flex items-center gap-2">
- <div className="flex items-center gap-4">
- <Link href="/blog">
- <Button variant="text" className="text-sm p-0">
- Blog
- </Button>
- </Link>
- <Link href="/contributors">
- <Button variant="text" className="text-sm p-0">
- Contributors
- </Button>
- </Link>
- </div>
- <Link href="/projects">
- <Button size="sm" className="text-sm ml-2">
- Projects
- <ArrowRight className="h-4 w-4" />
- </Button>
- </Link>
- <ThemeToggle className="mr-2" />
- </nav>
- );
+ const links = [
+ {
+ label: "Contributors",
+ href: "/contributors",
+ },
+ {
+ label: "Sponsors",
+ href: "/sponsors",
+ },
+ {
+ label: "Blog",
+ href: "/blog",
+ },
+ ];
- return (
- <div className="sticky top-4 z-50 mx-4 md:mx-0">
- <HeaderBase
- className="bg-background border rounded-2xl max-w-3xl mx-auto mt-4 pl-4 pr-[11px]"
- leftContent={leftContent}
- rightContent={rightContent}
- />
- </div>
- );
+ return (
+ <header className="bg-background shadow-background/85 sticky top-0 z-10 shadow-[0_30px_35px_15px_rgba(0,0,0,1)]">
+ <div className="relative flex w-full items-center justify-between px-6 pt-4">
+ <div className="relative z-10 flex items-center gap-6">
+ <Link href="/" className="flex items-center gap-3">
+ <Image
+ src={DEFAULT_LOGO_URL}
+ alt="OpenCut Logo"
+ className="invert dark:invert-0"
+ width={32}
+ height={32}
+ />
+ </Link>
+ <nav className="hidden items-center gap-4 md:flex">
+ {links.map((link) => (
+ <Link key={link.href} href={link.href}>
+ <Button variant="text" className="p-0 text-sm">
+ {link.label}
+ </Button>
+ </Link>
+ ))}
+ </nav>
+ </div>
+
+ <div className="relative z-10">
+ <div className="flex items-center gap-3 md:hidden">
+ <Button
+ variant="text"
+ size="icon"
+ className="flex items-center justify-center p-0"
+ onClick={() => setIsMenuOpen(!isMenuOpen)}
+ >
+ <HugeiconsIcon icon={Menu02Icon} size={30} />
+ </Button>
+ </div>
+ <div className="hidden items-center gap-3 md:flex">
+ <Link href={SOCIAL_LINKS.github}>
+ <Button className="bg-background text-sm" variant="outline">
+ <HugeiconsIcon icon={GithubIcon} className="size-4" />
+ 40k+
+ </Button>
+ </Link>
+ <Link href="/projects">
+ <Button variant="foreground" className="text-sm">
+ Projects
+ <ArrowRight className="size-4" />
+ </Button>
+ </Link>
+ <ThemeToggle />
+ </div>
+ </div>
+ <div
+ className={cn(
+ "bg-background/20 pointer-events-none fixed inset-0 opacity-0 backdrop-blur-3xl",
+ "transition-opacity duration-150",
+ isMenuOpen && "pointer-events-auto opacity-100",
+ )}
+ >
+ <div className="relative h-full">
+ <button
+ type="button"
+ aria-label="Close menu"
+ className="absolute inset-0"
+ onClick={closeMenu}
+ onKeyDown={(event) => {
+ if (
+ event.key === "Enter" ||
+ event.key === " " ||
+ event.key === "Escape"
+ ) {
+ event.preventDefault();
+ closeMenu();
+ }
+ }}
+ />
+ <nav className="flex flex-col gap-3 px-6 pt-[5rem]">
+ {links.map((link, index) => (
+ <motion.div
+ key={link.href}
+ initial={{ scale: 0.98, opacity: 0 }}
+ animate={{
+ scale: isMenuOpen ? 1 : 0.98,
+ opacity: isMenuOpen ? 1 : 0,
+ }}
+ transition={{
+ duration: 0.4,
+ delay: isMenuOpen ? index * 0.1 : 0,
+ ease: [0.25, 0.46, 0.45, 0.94],
+ }}
+ >
+ <Link
+ href={link.href}
+ className="text-2xl font-semibold"
+ onClick={() => setIsMenuOpen(false)}
+ >
+ {link.label}
+ </Link>
+ </motion.div>
+ ))}
+ </nav>
+ <ThemeToggle
+ className="absolute right-8 bottom-8 size-10"
+ iconClassName="!size-[1.2rem]"
+ onToggle={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ />
+ </div>
+ </div>
+ </div>
+ </header>
+ );
}
diff --git a/apps/web/src/components/icons.tsx b/apps/web/src/components/icons.tsx
deleted file mode 100644
index b24687a6..00000000
--- a/apps/web/src/components/icons.tsx
+++ /dev/null
@@ -1,233 +0,0 @@
-export function GoogleIcon({ className }: { className?: string }) {
- return (
- <svg className={className} viewBox="0 0 24 24">
- <path
- fill="#4285F4"
- d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
- />
- <path
- fill="#34A853"
- d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
- />
- <path
- fill="#FBBC05"
- d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
- />
- <path
- fill="#EA4335"
- d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
- />
- </svg>
- );
-}
-
-export function GithubIcon({ className }: { className?: string }) {
- return (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- className={className}
- viewBox="0 -3.5 256 256"
- preserveAspectRatio="xMinYMin meet"
- >
- <g fill="currentColor">
- <path d="M127.505 0C57.095 0 0 57.085 0 127.505c0 56.336 36.534 104.13 87.196 120.99 6.372 1.18 8.712-2.766 8.712-6.134 0-3.04-.119-13.085-.173-23.739-35.473 7.713-42.958-15.044-42.958-15.044-5.8-14.738-14.157-18.656-14.157-18.656-11.568-7.914.872-7.752.872-7.752 12.804.9 19.546 13.14 19.546 13.14 11.372 19.493 29.828 13.857 37.104 10.6 1.144-8.242 4.449-13.866 8.095-17.05-28.32-3.225-58.092-14.158-58.092-63.014 0-13.92 4.981-25.295 13.138-34.224-1.324-3.212-5.688-16.18 1.235-33.743 0 0 10.707-3.427 35.073 13.07 10.17-2.826 21.078-4.242 31.914-4.29 10.836.048 21.752 1.464 31.942 4.29 24.337-16.497 35.029-13.07 35.029-13.07 6.94 17.563 2.574 30.531 1.25 33.743 8.175 8.929 13.122 20.303 13.122 34.224 0 48.972-29.828 59.756-58.22 62.912 4.573 3.957 8.648 11.717 8.648 23.612 0 17.06-.148 30.791-.148 34.991 0 3.393 2.295 7.369 8.759 6.117 50.634-16.879 87.122-64.656 87.122-120.973C255.009 57.085 197.922 0 127.505 0" />
-
- <path d="M47.755 181.634c-.28.633-1.278.823-2.185.389-.925-.416-1.445-1.28-1.145-1.916.275-.652 1.273-.834 2.196-.396.927.415 1.455 1.287 1.134 1.923M54.027 187.23c-.608.564-1.797.302-2.604-.589-.834-.889-.99-2.077-.373-2.65.627-.563 1.78-.3 2.616.59.834.899.996 2.08.36 2.65M58.33 194.39c-.782.543-2.06.034-2.849-1.1-.781-1.133-.781-2.493.017-3.038.792-.545 2.05-.055 2.85 1.07.78 1.153.78 2.513-.019 3.069M65.606 202.683c-.699.77-2.187.564-3.277-.488-1.114-1.028-1.425-2.487-.724-3.258.707-.772 2.204-.555 3.302.488 1.107 1.026 1.445 2.496.7 3.258M75.01 205.483c-.307.998-1.741 1.452-3.185 1.028-1.442-.437-2.386-1.607-2.095-2.616.3-1.005 1.74-1.478 3.195-1.024 1.44.435 2.386 1.596 2.086 2.612M85.714 206.67c.036 1.052-1.189 1.924-2.705 1.943-1.525.033-2.758-.818-2.774-1.852 0-1.062 1.197-1.926 2.721-1.951 1.516-.03 2.758.815 2.758 1.86M96.228 206.267c.182 1.026-.872 2.08-2.377 2.36-1.48.27-2.85-.363-3.039-1.38-.184-1.052.89-2.105 2.367-2.378 1.508-.262 2.857.355 3.049 1.398" />
- </g>
- </svg>
- );
-}
-
-export function VercelIcon({ className }: { className?: string }) {
- return (
- <svg
- className={className}
- width="20"
- height="18"
- viewBox="0 0 76 65"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- >
- <path d="M37.5274 0L75.0548 65H0L37.5274 0Z" fill="currentColor" />
- </svg>
- );
-}
-
-export function BackgroundIcon({ className }: { className?: string }) {
- return (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="353"
- height="353"
- viewBox="0 0 353 353"
- fill="none"
- className={className}
- >
- <g clipPath="url(#clip0_1_3)">
- <rect
- x="-241.816"
- y="233.387"
- width="592.187"
- height="17.765"
- transform="rotate(-37 -241.816 233.387)"
- fill="white"
- />
- <rect
- x="-189.907"
- y="306.804"
- width="592.187"
- height="17.765"
- transform="rotate(-37 -189.907 306.804)"
- fill="white"
- />
- <rect
- x="-146.928"
- y="389.501"
- width="592.187"
- height="17.765"
- transform="rotate(-37 -146.928 389.501)"
- fill="white"
- />
- <rect
- x="-103.144"
- y="477.904"
- width="592.187"
- height="17.765"
- transform="rotate(-37 -103.144 477.904)"
- fill="white"
- />
- <rect
- x="-57.169"
- y="570.714"
- width="592.187"
- height="17.765"
- transform="rotate(-37 -57.169 570.714)"
- fill="white"
- />
- </g>
- <defs>
- <clipPath id="clip0_1_3">
- <rect width="353" height="353" fill="white" />
- </clipPath>
- </defs>
- </svg>
- );
-}
-
-export function MarbleIcon({
- className = "",
- size = 32,
-}: {
- className?: string;
- size?: number;
-}) {
- return (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width={size}
- height={size}
- className={className}
- fill="none"
- viewBox="0 0 256 256"
- >
- <path fill="#202027" d="M0 0h256v256H0z" />
- <path
- fill="#fff"
- d="M116.032 94.016q1.408.256 6.272 9.856 4.856 9.472 11.904 24.576l4.096 8.576a42.4 42.4 0 0 0 4.992-6.784q2.304-3.96 5.76-10.624 3.96-7.56 5.504-9.728 6.144-9.344 13.312-19.84.896-1.408 3.2-1.92a4.8 4.8 0 0 1 1.28-.128q3.072 0 6.784 2.56 3.84 2.56 4.992 5.248a4.8 4.8 0 0 1 .256 1.92q0 1.28-.128 1.92l-.64 2.944q-.256 1.664-.512 3.968a3200 3200 0 0 0-5.888 46.848 56 56 0 0 0-.512 8.064l-.256 4.096a32 32 0 0 0-.128 2.944q0 1.672-.384 2.304-.384.64-1.408.64-.768 0-2.304-.384-4.096-.896-6.272-3.2-2.176-2.432-2.432-6.656-.128-2.176-.128-6.784 0-4.736.256-14.464l.128-7.04.256-10.112q-1.152 1.024-5.888 8.064a328 328 0 0 0-9.088 14.464q-4.48 7.56-5.888 11.648-1.28 3.2-2.56 4.736-1.152 1.536-2.816 1.536-1.536 0-3.968-1.408-6.912-4.224-8.448-11.648a336 336 0 0 0-6.016-23.68 40 40 0 0 0-2.56-6.272q-1.792-3.968-2.304-4.992l-1.664-3.328q-18.68 24.832-25.344 45.568-1.28 4.352-2.432 6.272-1.152 1.792-2.688 1.792-2.432 0-6.912-4.352Q72 158.144 72 154.304q0-2.04.768-4.096a108 108 0 0 1 16-30.08l3.968-5.248a432 432 0 0 0 12.16-16.64q3.072-4.48 8.192-4.48.896 0 2.944.256"
- />
- </svg>
- );
-}
-
-export function DataBuddyIcon({
- className = "",
- size = 32,
-}: {
- className?: string;
- size?: number;
-}) {
- return (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width={size}
- height={size}
- className={className}
- viewBox="0 0 8 8"
- shapeRendering="crispEdges"
- >
- <path d="M0 0h8v8H0z" />
- <path
- fill="#fff"
- d="M1 1h1v6H1zm1 0h4v1H2zm4 1h1v1H6zm0 1h1v1H6zm0 1h1v1H6zm0 1h1v1H6zM2 6h4v1H2zm1-3h1v1H3zm1 1h1v1H4z"
- />
- </svg>
- );
-}
-
-export function SocialsIcon({
- className = "",
- size = 32,
-}: {
- className?: string;
- size?: number;
-}) {
- return (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width={size}
- height={size}
- viewBox="0 0 345 243"
- fill="none"
- className={className}
- >
- <g opacity="0.5">
- <path d="M203.75 4H39.25C19.782 4 4 19.782 4 39.25V203.75C4 223.218 19.782 239 39.25 239H203.75C223.218 239 239 223.218 239 203.75V39.25C239 19.782 223.218 4 203.75 4Z" fill="#FFFC00"/>
- <path d="M97.1738 194.02C88.9121 188.053 82.4863 184.84 66.8809 187.594C64.5859 188.053 60.4551 189.43 59.9961 185.299C59.0781 182.086 59.0781 177.037 56.7832 176.578C42.5547 174.742 37.5059 171.988 35.2109 169.234C34.293 168.316 33.834 166.021 35.6699 165.562C59.9961 160.973 71.4707 137.564 73.7656 132.975C76.5195 126.09 71.9297 121.959 63.209 119.205C59.0781 117.828 52.1934 115.992 52.1934 111.402C52.1934 109.107 54.4883 107.73 56.7832 106.812C58.6191 106.354 60.4551 105.895 62.291 106.812C67.7988 109.107 72.8477 110.025 75.6016 107.73C75.6016 95.3379 72.3887 79.7324 77.4375 66.8809C83.4043 52.6523 98.0918 39.8008 121.5 39.8008C144.908 39.8008 159.596 52.6523 165.562 66.8809C170.611 79.7324 167.398 95.3379 167.398 107.73C170.152 110.025 175.201 109.107 180.709 106.812C182.545 105.895 184.381 106.354 186.217 106.812C188.512 107.73 190.807 109.107 190.807 111.402C190.807 115.992 183.922 117.828 179.791 119.205C171.07 121.959 166.48 126.09 169.234 132.975C171.529 137.564 183.004 160.973 207.33 165.562C209.166 166.021 208.707 168.316 207.789 169.234C205.494 171.988 200.445 174.742 186.217 176.578C183.922 177.037 183.922 182.086 183.004 185.299C182.545 189.43 178.414 188.053 176.119 187.594C160.514 184.84 154.088 188.053 145.826 194.02C139.065 199.872 130.442 203.126 121.5 203.199C111.861 203.658 104.059 199.527 97.1738 194.02Z" fill="white"/>
- <path d="M203.75 4H39.25C19.782 4 4 19.782 4 39.25V203.75C4 223.218 19.782 239 39.25 239H203.75C223.218 239 239 223.218 239 203.75V39.25C239 19.782 223.218 4 203.75 4Z" stroke="black" strokeWidth="7"/>
- <path d="M97.1738 194.02C88.9121 188.053 82.4863 184.84 66.8809 187.594C64.5859 188.053 60.4551 189.43 59.9961 185.299C59.0781 182.086 59.0781 177.037 56.7832 176.578C42.5547 174.742 37.5059 171.988 35.2109 169.234C34.293 168.316 33.834 166.021 35.6699 165.562C59.9961 160.973 71.4707 137.564 73.7656 132.975C76.5195 126.09 71.9297 121.959 63.209 119.205C59.0781 117.828 52.1934 115.992 52.1934 111.402C52.1934 109.107 54.4883 107.73 56.7832 106.812C58.6191 106.354 60.4551 105.895 62.291 106.812C67.7988 109.107 72.8477 110.025 75.6016 107.73C75.6016 95.3379 72.3887 79.7324 77.4375 66.8809C83.4043 52.6523 98.0918 39.8008 121.5 39.8008C144.908 39.8008 159.596 52.6523 165.562 66.8809C170.611 79.7324 167.398 95.3379 167.398 107.73C170.152 110.025 175.201 109.107 180.709 106.812C182.545 105.895 184.381 106.354 186.217 106.812C188.512 107.73 190.807 109.107 190.807 111.402C190.807 115.992 183.922 117.828 179.791 119.205C171.07 121.959 166.48 126.09 169.234 132.975C171.529 137.564 183.004 160.973 207.33 165.562C209.166 166.021 208.707 168.316 207.789 169.234C205.494 171.988 200.445 174.742 186.217 176.578C183.922 177.037 183.922 182.086 183.004 185.299C182.545 189.43 178.414 188.053 176.119 187.594C160.514 184.84 154.088 188.053 145.826 194.02C139.065 199.872 130.442 203.126 121.5 203.199C111.861 203.658 104.059 199.527 97.1738 194.02Z" stroke="black" strokeWidth="7"/>
- </g>
- <path fillRule="evenodd" clipRule="evenodd" d="M133.5 4H321.5C334.48 4 345 14.5205 345 27.5V215.5C345 228.48 334.48 239 321.5 239H133.5C120.52 239 110 228.48 110 215.5V27.5C110 14.5205 120.52 4 133.5 4Z" fill="#010101"/>
- <path fillRule="evenodd" clipRule="evenodd" d="M261.497 99.4086C271.784 106.758 284.388 111.083 297.999 111.083V84.9035C295.423 84.9045 292.854 84.6357 290.333 84.1016V104.709C276.723 104.709 264.121 100.384 253.831 93.0345V146.459C253.831 173.185 232.155 194.85 205.417 194.85C195.44 194.85 186.167 191.835 178.464 186.665C187.256 195.65 199.516 201.224 213.08 201.224C239.821 201.224 261.498 179.56 261.498 152.833L261.497 99.4086ZM270.953 72.9965C265.696 67.2559 262.244 59.8365 261.497 51.634V48.267H254.233C256.061 58.6916 262.298 67.5981 270.953 72.9965ZM195.376 166.157C192.438 162.308 190.851 157.598 190.858 152.756C190.858 140.533 200.773 130.622 213.005 130.622C215.285 130.621 217.551 130.97 219.723 131.659V104.894C217.185 104.547 214.622 104.399 212.061 104.454V125.286C209.887 124.597 207.62 124.247 205.34 124.249C193.107 124.249 183.193 134.159 183.193 146.384C183.193 155.027 188.149 162.512 195.376 166.157Z" fill="#EE1D52"/>
- <path fillRule="evenodd" clipRule="evenodd" d="M253.831 93.0345C264.121 100.384 276.723 104.709 290.333 104.709V84.1016C282.736 82.4848 276.011 78.5162 270.953 72.9965C262.298 67.5981 256.061 58.6916 254.233 48.267H235.152V152.832C235.108 165.022 225.21 174.892 213.004 174.892C205.811 174.892 199.422 171.465 195.376 166.157C188.149 162.512 183.193 155.027 183.193 146.384C183.193 134.159 193.107 124.249 205.34 124.249C207.683 124.249 209.942 124.614 212.061 125.286V104.454C185.792 104.996 164.665 126.449 164.665 152.833C164.665 166.003 169.926 177.942 178.464 186.665C186.167 191.835 195.44 194.85 205.417 194.85C232.155 194.85 253.831 173.185 253.831 146.459V93.0345Z" fill="white"/>
- <path fillRule="evenodd" clipRule="evenodd" d="M290.333 84.1016V78.5293C283.482 78.5396 276.766 76.6229 270.953 72.9965C276.099 78.6269 282.874 82.5087 290.333 84.1016ZM254.233 48.267C254.058 47.2708 253.924 46.2679 253.831 45.2608V41.8938H227.485V146.459C227.443 158.648 217.545 168.518 205.339 168.518C201.754 168.518 198.372 167.669 195.376 166.157C199.422 171.465 205.811 174.892 213.004 174.892C225.21 174.892 235.108 165.022 235.152 152.832V48.267H254.233ZM212.061 104.454L212.061 98.5212C209.859 98.2202 207.64 98.0698 205.418 98.071C178.676 98.071 157 119.736 157 146.459C157 163.214 165.518 177.979 178.464 186.665C169.926 177.942 164.666 166.002 164.666 152.832C164.666 126.449 185.791 104.996 212.061 104.454Z" fill="#69C9D0"/>
- </svg>
- );
-}
-
-export function TransitionUpIcon({
- className = "",
- size = 16,
-}: {
- className?: string;
- size?: number;
-}) {
- return (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width={size}
- height={size}
- viewBox="0 0 16 16"
- fill="none"
- className={className}
- >
- <path
- fillRule="evenodd"
- clipRule="evenodd"
- d="M2.5 12.6667C2.5 13.4951 3.17157 14.1667 4 14.1667H12C12.8284 14.1667 13.5 13.4951 13.5 12.6667V11.3333C13.5 10.5049 12.8284 9.83333 12 9.83333H4C3.17157 9.83333 2.5 10.5049 2.5 11.3333V12.6667ZM4 15.1667C2.61929 15.1667 1.5 14.0474 1.5 12.6667V11.3333C1.5 9.95262 2.61929 8.83333 4 8.83333H12C13.3807 8.83333 14.5 9.95262 14.5 11.3333V12.6667C14.5 14.0474 13.3807 15.1667 12 15.1667H4Z"
- fill="white"
- />
- <path
- fillRule="evenodd"
- clipRule="evenodd"
- d="M2 5.83333C1.72386 5.83333 1.5 5.60947 1.5 5.33333L1.5 4C1.5 2.2511 2.91777 0.833332 4.66667 0.833332L11.3333 0.833332C13.0822 0.833332 14.5 2.2511 14.5 4V5.33333C14.5 5.60947 14.2761 5.83333 14 5.83333C13.7239 5.83333 13.5 5.60947 13.5 5.33333V4C13.5 2.80338 12.53 1.83333 11.3333 1.83333L4.66667 1.83333C3.47005 1.83333 2.5 2.80338 2.5 4V5.33333C2.5 5.60947 2.27614 5.83333 2 5.83333Z"
- fill="white"
- />
- <path
- fillRule="evenodd"
- clipRule="evenodd"
- d="M8.35355 3.64645C8.15829 3.45118 7.84171 3.45118 7.64645 3.64645L5.64645 5.64645C5.45118 5.84171 5.45118 6.15829 5.64645 6.35355C5.84171 6.54882 6.15829 6.54882 6.35355 6.35355L7.5 5.20711L7.5 9.33333C7.5 9.60948 7.72386 9.83333 8 9.83333C8.27614 9.83333 8.5 9.60948 8.5 9.33333V5.20711L9.64645 6.35355C9.84171 6.54882 10.1583 6.54882 10.3536 6.35355C10.5488 6.15829 10.5488 5.84171 10.3536 5.64645L8.35355 3.64645Z"
- fill="white"
- />
- </svg>
- );
-} \ No newline at end of file
diff --git a/apps/web/src/components/keyboard-shortcuts-help.tsx b/apps/web/src/components/keyboard-shortcuts-help.tsx
deleted file mode 100644
index 689c3ee3..00000000
--- a/apps/web/src/components/keyboard-shortcuts-help.tsx
+++ /dev/null
@@ -1,245 +0,0 @@
-"use client";
-
-import { Keyboard } from "lucide-react";
-import { useEffect, useState } from "react";
-import { toast } from "sonner";
-import {
- type KeyboardShortcut,
- useKeyboardShortcutsHelp,
-} from "@/hooks/use-keyboard-shortcuts-help";
-import { useKeybindingsStore } from "@/stores/keybindings-store";
-import { Button } from "./ui/button";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "./ui/dialog";
-
-export function KeyboardShortcutsHelp() {
- const [open, setOpen] = useState(false);
- const [recordingShortcut, setRecordingShortcut] =
- useState<KeyboardShortcut | null>(null);
-
- const {
- updateKeybinding,
- removeKeybinding,
- getKeybindingString,
- validateKeybinding,
- getKeybindingsForAction,
- setIsRecording,
- resetToDefaults,
- isRecording,
- } = useKeybindingsStore();
-
- // Get shortcuts from centralized hook
- const { shortcuts } = useKeyboardShortcutsHelp();
-
- const categories = Array.from(new Set(shortcuts.map((s) => s.category)));
-
- useEffect(() => {
- if (!isRecording || !recordingShortcut) return;
-
- const handleKeyDown = (e: KeyboardEvent) => {
- e.preventDefault();
- e.stopPropagation();
-
- const keyString = getKeybindingString(e);
- if (keyString) {
- // Auto-save the new keybinding
- const conflict = validateKeybinding(
- keyString,
- recordingShortcut.action
- );
- if (conflict) {
- toast.error(
- `Key "${keyString}" is already bound to "${conflict.existingAction}"`
- );
- setRecordingShortcut(null);
- return;
- }
-
- // Remove old keybindings for this action
- const oldKeys = getKeybindingsForAction(recordingShortcut.action);
- oldKeys.forEach((key) => removeKeybinding(key));
-
- // Add new keybinding
- updateKeybinding(keyString, recordingShortcut.action);
-
- setIsRecording(false);
- setRecordingShortcut(null);
- }
- };
-
- const handleClickOutside = () => {
- setRecordingShortcut(null);
- setIsRecording(false);
- };
-
- document.addEventListener("keydown", handleKeyDown);
- document.addEventListener("click", handleClickOutside);
-
- return () => {
- document.removeEventListener("keydown", handleKeyDown);
- document.removeEventListener("click", handleClickOutside);
- };
- }, [
- recordingShortcut,
- getKeybindingString,
- updateKeybinding,
- removeKeybinding,
- validateKeybinding,
- getKeybindingsForAction,
- setIsRecording,
- isRecording,
- ]);
-
- const handleStartRecording = (shortcut: KeyboardShortcut) => {
- setRecordingShortcut(shortcut);
- setIsRecording(true);
- };
-
- return (
- <Dialog open={open} onOpenChange={setOpen}>
- <DialogTrigger asChild>
- <Button variant="text" size="sm" className="gap-2">
- <Keyboard className="w-4 h-4" />
- Shortcuts
- </Button>
- </DialogTrigger>
- <DialogContent className="max-w-2xl max-h-[80vh] flex flex-col p-0">
- <DialogHeader className="flex-shrink-0 p-6 pb-0">
- <DialogTitle className="flex items-center gap-2">
- <Keyboard className="w-5 h-5" />
- Keyboard Shortcuts
- </DialogTitle>
- <DialogDescription>
- Speed up your video editing workflow with these keyboard shortcuts.
- Click any shortcut key to edit it.
- </DialogDescription>
- </DialogHeader>
-
- <div className="overflow-y-auto flex-grow scrollbar-thin">
- <div className="space-y-6 p-6 pt-2">
- {categories.map((category) => (
- <div key={category} className="flex flex-col gap-1">
- <h3 className="text-xs text-muted-foreground uppercase tracking-wide font-medium">
- {category}
- </h3>
- <div className="space-y-0.5">
- {shortcuts
- .filter((shortcut) => shortcut.category === category)
- .map((shortcut) => (
- <ShortcutItem
- key={shortcut.action}
- shortcut={shortcut}
- isRecording={
- shortcut.action === recordingShortcut?.action
- }
- onStartRecording={handleStartRecording}
- />
- ))}
- </div>
- </div>
- ))}
- </div>
- </div>
- <DialogFooter className="flex-shrink-0 p-6 pt-4">
- <Button size="sm" variant="destructive" onClick={resetToDefaults}>
- Reset to Default
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-}
-
-function ShortcutItem({
- shortcut,
- isRecording,
- onStartRecording,
-}: {
- shortcut: KeyboardShortcut;
- isRecording: boolean;
- onStartRecording: (shortcut: KeyboardShortcut) => void;
-}) {
- // Filter out lowercase duplicates for display - if both "j" and "J" exist, only show "J"
- const displayKeys = shortcut.keys.filter((key: string) => {
- if (
- key.includes("Cmd") &&
- shortcut.keys.includes(key.replace("Cmd", "Ctrl"))
- )
- return false;
-
- return true;
- });
-
- return (
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-3">
- {shortcut.icon && (
- <div className="text-muted-foreground">{shortcut.icon}</div>
- )}
- <span className="text-sm">{shortcut.description}</span>
- </div>
- <div className="flex items-center gap-1">
- {displayKeys.map((key: string, index: number) => (
- <div key={key} className="flex items-center gap-1">
- <div className="flex items-center">
- {key.split("+").map((keyPart: string, partIndex: number) => {
- const keyId = `${shortcut.id}-${index}-${partIndex}`;
- return (
- <EditableShortcutKey
- key={keyId}
- isRecording={isRecording}
- onStartRecording={() => onStartRecording(shortcut)}
- >
- {keyPart}
- </EditableShortcutKey>
- );
- })}
- </div>
- {index < displayKeys.length - 1 && (
- <span className="text-xs text-muted-foreground">or</span>
- )}
- </div>
- ))}
- </div>
- </div>
- );
-}
-
-function EditableShortcutKey({
- children,
- isRecording,
- onStartRecording,
-}: {
- children: React.ReactNode;
- isRecording: boolean;
- onStartRecording: () => void;
-}) {
- const handleClick = (e: React.MouseEvent) => {
- e.preventDefault();
- e.stopPropagation();
- onStartRecording();
- };
-
- return (
- <Button
- variant="outline"
- size="sm"
- className={`font-sans px-2 min-w-6 min-h-6 leading-none mr-1 hover:bg-opacity-80 ${
- isRecording ? "border-primary bg-primary/10" : "border bg-accent/50"
- }`}
- onClick={handleClick}
- title={
- isRecording ? "Press any key combination..." : "Click to edit shortcut"
- }
- >
- {children}
- </Button>
- );
-} \ No newline at end of file
diff --git a/apps/web/src/components/landing/handlebars.tsx b/apps/web/src/components/landing/handlebars.tsx
index 758717e5..da0d895d 100644
--- a/apps/web/src/components/landing/handlebars.tsx
+++ b/apps/web/src/components/landing/handlebars.tsx
@@ -1,105 +1,173 @@
"use client";
-import {
- motion,
- useMotionTemplate,
- useMotionValue,
- useMotionValueEvent,
- useTransform,
-} from "motion/react";
import { type PropsWithChildren, useEffect, useRef, useState } from "react";
type HandlebarsProps = PropsWithChildren;
export function Handlebars({ children }: HandlebarsProps) {
- const containerRef = useRef<HTMLDivElement>(null);
+ const containerRef = useRef<HTMLDivElement>(null);
+ const leftHandleRef = useRef<HTMLDivElement>(null);
+ const rightHandleRef = useRef<HTMLDivElement>(null);
- const [width, setWidth] = useState(0);
+ const [width, setWidth] = useState(0);
+ const [leftHandle, setLeftHandle] = useState(0);
+ const [rightHandle, setRightHandle] = useState(0);
- const [leftHandle, setLeftHandle] = useState(0);
- const [rightHandle, setRightHandle] = useState(width);
+ const widthRef = useRef(0);
+ const leftHandlePositionRef = useRef(0);
+ const rightHandlePositionRef = useRef(0);
- const leftHandleX = useMotionValue(0);
- const rightHandleX = useMotionValue(width);
+ const dragRef = useRef<{
+ isDragging: boolean;
+ side: "left" | "right" | null;
+ pointerId: number | null;
+ startX: number;
+ initialPosition: number;
+ }>({
+ isDragging: false,
+ side: null,
+ pointerId: null,
+ startX: 0,
+ initialPosition: 0,
+ });
- useEffect(() => {
- const el = containerRef.current;
- if (!el) return;
+ useEffect(() => {
+ const el = containerRef.current;
+ if (!el) return;
- const observer = new ResizeObserver(() => {
- setWidth(el.offsetWidth);
- setRightHandle(el.offsetWidth);
- rightHandleX.set(el.offsetWidth);
- });
+ const updateWidth = () => {
+ const newWidth = el.offsetWidth;
+ setWidth(newWidth);
+ setRightHandle(newWidth);
+ };
- observer.observe(el);
- return () => observer.disconnect();
- }, [rightHandleX.set]);
+ const observer = new ResizeObserver(updateWidth);
+ observer.observe(el);
+ updateWidth();
- useMotionValueEvent(leftHandleX, "change", () => {
- setLeftHandle(leftHandleX.get());
- });
- useMotionValueEvent(rightHandleX, "change", () => {
- setRightHandle(rightHandleX.get());
- });
- const leftGradient = useTransform(leftHandleX, [0, width - 10], [0, 100]);
- const rightGradient = useTransform(rightHandleX, [0, width + 10], [0, 100]);
+ return () => observer.disconnect();
+ }, []);
- return (
- <div className="flex justify-center gap-4 leading-16">
- <div ref={containerRef} className="relative -rotate-[2.76deg] mt-0.5">
- <div className="absolute inset-0 w-full h-full rounded-2xl border border-yellow-500 flex justify-between z-1">
- <motion.div
- className="absolute z-10 left-0 h-full border border-yellow-500 w-7 rounded-full bg-background flex items-center justify-center select-none"
- style={{
- x: leftHandleX,
- }}
- drag="x"
- dragConstraints={{ left: 0, right: rightHandle - 60 }}
- dragElastic={0}
- dragMomentum={false}
- whileHover={{ scale: 1.05, cursor: "grab" }}
- whileDrag={{ scale: 1.1, cursor: "grabbing" }}
- transition={{ type: "spring", stiffness: 400, damping: 30 }}
- >
- <div className="w-2 h-8 rounded-full bg-yellow-500" />
- </motion.div>
+ useEffect(() => {
+ widthRef.current = width;
+ leftHandlePositionRef.current = leftHandle;
+ rightHandlePositionRef.current = rightHandle;
+ }, [leftHandle, rightHandle, width]);
- <motion.div
- className="absolute z-10 -left-[30px] h-full border border-yellow-500 w-7 rounded-full bg-background flex items-center justify-center select-none"
- style={{
- x: rightHandleX,
- }}
- drag="x"
- dragConstraints={{
- left: leftHandle + 60,
- right: width,
- }}
- dragElastic={0}
- dragMomentum={false}
- whileHover={{ scale: 1.05, cursor: "grab" }}
- whileDrag={{ scale: 1.1, cursor: "grabbing" }}
- transition={{ type: "spring", stiffness: 400, damping: 30 }}
- >
- <div className="w-2 h-8 rounded-full bg-yellow-500" />
- </motion.div>
- </div>
+ useEffect(() => {
+ const handlePointerMove = (event: PointerEvent) => {
+ const { isDragging, side, pointerId, startX, initialPosition } =
+ dragRef.current;
- <motion.span
- className="inline-flex items-center justify-center w-full h-full px-9 relative rounded-2xl will-change-auto"
- style={{
- mask: useMotionTemplate`linear-gradient(90deg,
+ if (!isDragging) return;
+ if (pointerId !== null && event.pointerId !== pointerId) return;
+ if (!side) return;
+
+ const deltaX = event.clientX - startX;
+
+ if (side === "left") {
+ const maxLeft = Math.max(0, rightHandlePositionRef.current - 60);
+ const nextLeftHandle = Math.max(
+ 0,
+ Math.min(maxLeft, initialPosition + deltaX),
+ );
+ setLeftHandle(nextLeftHandle);
+ return;
+ }
+
+ const minRight = Math.min(
+ widthRef.current,
+ leftHandlePositionRef.current + 60,
+ );
+ const nextRightHandle = Math.max(
+ minRight,
+ Math.min(widthRef.current, initialPosition + deltaX),
+ );
+ setRightHandle(nextRightHandle);
+ };
+
+ const handlePointerEnd = (event: PointerEvent) => {
+ const { pointerId } = dragRef.current;
+ if (pointerId !== null && event.pointerId !== pointerId) return;
+
+ dragRef.current.isDragging = false;
+ dragRef.current.side = null;
+ dragRef.current.pointerId = null;
+ };
+
+ window.addEventListener("pointermove", handlePointerMove);
+ window.addEventListener("pointerup", handlePointerEnd);
+ window.addEventListener("pointercancel", handlePointerEnd);
+
+ return () => {
+ window.removeEventListener("pointermove", handlePointerMove);
+ window.removeEventListener("pointerup", handlePointerEnd);
+ window.removeEventListener("pointercancel", handlePointerEnd);
+ };
+ }, []);
+
+ const leftGradientPercent = width > 0 ? (leftHandle / (width - 10)) * 100 : 0;
+ const rightGradientPercent =
+ width > 0 ? (rightHandle / (width + 10)) * 100 : 0;
+
+ return (
+ <div className="flex justify-center gap-4 leading-16">
+ <div ref={containerRef} className="relative mt-0.5 -rotate-[2.76deg]">
+ <div className="absolute inset-0 z-10 flex size-full justify-between rounded-2xl border border-yellow-500">
+ <div
+ ref={leftHandleRef}
+ className="bg-background absolute left-0 z-20 flex h-full w-7 cursor-ew-resize touch-none items-center justify-center rounded-full border border-yellow-500 select-none"
+ style={{
+ translate: `${leftHandle}px 0`,
+ }}
+ onPointerDown={(event) => {
+ event.preventDefault();
+ leftHandleRef.current?.setPointerCapture(event.pointerId);
+ dragRef.current.isDragging = true;
+ dragRef.current.side = "left";
+ dragRef.current.pointerId = event.pointerId;
+ dragRef.current.startX = event.clientX;
+ dragRef.current.initialPosition = leftHandlePositionRef.current;
+ }}
+ >
+ <div className="h-8 w-2 rounded-full bg-yellow-500" />
+ </div>
+
+ <div
+ ref={rightHandleRef}
+ className="bg-background absolute -left-[30px] z-20 flex h-full w-7 cursor-ew-resize touch-none items-center justify-center rounded-full border border-yellow-500 select-none"
+ style={{
+ translate: `${rightHandle}px 0`,
+ }}
+ onPointerDown={(event) => {
+ event.preventDefault();
+ rightHandleRef.current?.setPointerCapture(event.pointerId);
+ dragRef.current.isDragging = true;
+ dragRef.current.side = "right";
+ dragRef.current.pointerId = event.pointerId;
+ dragRef.current.startX = event.clientX;
+ dragRef.current.initialPosition = rightHandlePositionRef.current;
+ }}
+ >
+ <div className="h-8 w-2 rounded-full bg-yellow-500" />
+ </div>
+ </div>
+
+ <span
+ className="relative z-0 inline-flex size-full items-center justify-center rounded-2xl px-9 will-change-auto"
+ style={{
+ mask: `linear-gradient(90deg,
rgba(255, 255, 255, 0) 0%,
- rgba(255, 255, 255, 0) ${leftGradient}%,
- rgba(0, 0, 0) ${leftGradient}%,
- rgba(0, 0, 0) ${rightGradient}%,
- rgba(255, 255, 255, 0) ${rightGradient}%,
+ rgba(255, 255, 255, 0) ${leftGradientPercent}%,
+ rgba(0, 0, 0) ${leftGradientPercent}%,
+ rgba(0, 0, 0) ${rightGradientPercent}%,
+ rgba(255, 255, 255, 0) ${rightGradientPercent}%,
rgba(255, 255, 255, 0) 100%)`,
- }}
- >
- {children}
- </motion.span>
- </div>
- </div>
- );
+ }}
+ >
+ {children}
+ </span>
+ </div>
+ </div>
+ );
}
diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx
index 1b557412..6f11ad7c 100644
--- a/apps/web/src/components/landing/hero.tsx
+++ b/apps/web/src/components/landing/hero.tsx
@@ -1,81 +1,46 @@
"use client";
-import { motion } from "motion/react";
import { Button } from "../ui/button";
-import { SponsorButton } from "../ui/sponsor-button";
-import { VercelIcon } from "../icons";
import { ArrowRight } from "lucide-react";
-
import Image from "next/image";
import { Handlebars } from "./handlebars";
import Link from "next/link";
export function Hero() {
- return (
- <div className="min-h-[calc(100svh-4.5rem)] flex flex-col justify-between items-center text-center px-4">
- <Image
- className="absolute top-0 left-0 -z-50 size-full object-cover invert dark:invert-0 opacity-85"
- src="/landing-page-dark.png"
- height={1903.5}
- width={1269}
- alt="landing-page.bg"
- />
- <motion.div
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{ duration: 1 }}
- className="max-w-3xl mx-auto w-full flex-1 flex flex-col justify-center"
- >
- <motion.div
- initial={{ opacity: 0, y: 20 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ delay: 0.6, duration: 0.8 }}
- className="mb-4 flex justify-center"
- >
- <SponsorButton
- href="https://vercel.com/home?utm_source=opencut"
- logo={VercelIcon}
- companyName="Vercel"
- />
- </motion.div>
- <motion.div
- initial={{ opacity: 0, y: 20 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ delay: 0.2, duration: 0.8 }}
- className="inline-block font-bold tracking-tighter text-4xl md:text-[4rem]"
- >
- <h1>The Open Source</h1>
- <Handlebars>Video Editor</Handlebars>
- </motion.div>
+ return (
+ <div className="flex min-h-[calc(100svh-4.5rem)] flex-col items-center justify-between px-4 text-center">
+ <Image
+ className="absolute top-0 left-0 -z-50 size-full object-cover opacity-85 invert dark:invert-0"
+ src="/landing-page-dark.png"
+ height={1903.5}
+ width={1269}
+ alt="landing-page.bg"
+ />
+ <div className="mx-auto flex w-full max-w-3xl flex-1 flex-col justify-center">
+ <div className="inline-block text-4xl font-bold tracking-tighter md:text-[4rem]">
+ <h1>The open source</h1>
+ <Handlebars>Video editor</Handlebars>
+ </div>
- <motion.p
- className="mt-10 text-base sm:text-xl text-muted-foreground font-light tracking-wide max-w-xl mx-auto"
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{ delay: 0.4, duration: 0.8 }}
- >
- A simple but powerful video editor that gets the job done. Works on
- any platform.
- </motion.p>
+ <p className="text-muted-foreground mx-auto mt-10 max-w-xl text-base font-light tracking-wide sm:text-xl">
+ A simple but powerful video editor that gets the job done. Works on
+ any platform.
+ </p>
- <motion.div
- className="mt-8 flex gap-8 justify-center"
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{ delay: 0.6, duration: 0.8 }}
- >
- <Link href="/projects">
- <Button
- type="submit"
- size="lg"
- className="px-6 h-11 text-base bg-foreground"
- >
- Try early beta
- <ArrowRight className="relative z-10 ml-0.5 h-4 w-4 inline-block" />
- </Button>
- </Link>
- </motion.div>
- </motion.div>
- </div>
- );
+ <div className="mt-8 flex justify-center gap-8">
+ <Link href="/projects">
+ <Button
+ variant="foreground"
+ type="submit"
+ size="lg"
+ className="h-11 text-base"
+ >
+ Try early beta
+ <ArrowRight className="ml-0.5" />
+ </Button>
+ </Link>
+ </div>
+ </div>
+ </div>
+ );
}
diff --git a/apps/web/src/components/language-select.tsx b/apps/web/src/components/language-select.tsx
deleted file mode 100644
index 43ed632d..00000000
--- a/apps/web/src/components/language-select.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-import { useState, useRef, useEffect } from "react";
-import { ChevronDown, Globe } from "lucide-react";
-import { cn } from "@/lib/utils";
-import { motion } from "framer-motion";
-import ReactCountryFlag from "react-country-flag";
-
-export interface Language {
- code: string;
- name: string;
- flag?: string;
-}
-
-interface LanguageSelectProps {
- selectedCountry: string;
- onSelect: (country: string) => void;
- containerRef: React.RefObject<HTMLDivElement>;
- languages: Language[];
-}
-
-function FlagPreloader({ languages }: { languages: Language[] }) {
- return (
- <div className="absolute -top-[9999px] left-0 pointer-events-none">
- {languages.map((language) => (
- <ReactCountryFlag
- key={language.code}
- countryCode={language.code}
- svg
- style={{ width: "1.05rem", height: "1.05rem" }}
- />
- ))}
- </div>
- );
-}
-
-export function LanguageSelect({
- selectedCountry,
- onSelect,
- containerRef,
- languages,
-}: LanguageSelectProps) {
- const [expanded, setExpanded] = useState(false);
- const [isTapping, setIsTapping] = useState(false);
- const [isClosing, setIsClosing] = useState(false);
- const collapsedHeight = "2.5rem";
- const expandHeight = "12rem";
- const buttonRef = useRef<HTMLButtonElement>(null);
-
- const expand = () => {
- setIsTapping(true);
- setTimeout(() => setIsTapping(false), 600);
- setExpanded(true);
- buttonRef.current?.focus();
- };
-
- useEffect(() => {
- if (!expanded) return;
-
- const handleClickOutside = (event: MouseEvent) => {
- if (
- buttonRef.current &&
- !buttonRef.current.contains(event.target as Node)
- ) {
- setIsClosing(true);
- setTimeout(() => setIsClosing(false), 600);
- setExpanded(false);
- buttonRef.current?.blur();
- }
- };
-
- document.addEventListener("mousedown", handleClickOutside);
- return () => {
- document.removeEventListener("mousedown", handleClickOutside);
- };
- }, [expanded]);
-
- const selectedLanguage = languages.find(
- (lang) => lang.code === selectedCountry
- );
-
- const handleSelect = ({
- code,
- e,
- }: {
- code: string;
- e: React.MouseEvent<HTMLButtonElement>;
- }) => {
- e.stopPropagation();
- e.preventDefault();
- onSelect(code);
- setExpanded(false);
- };
-
- return (
- <div className="relative w-full h-9">
- <FlagPreloader languages={languages} />
- <motion.button
- type="button"
- className={cn(
- "absolute w-full h-full flex flex-col overflow-hidden items-start justify-between z-10 rounded-lg px-3 cursor-pointer",
- "!bg-foreground/10 backdrop-blur-md text-foreground py-0",
- "transition-[color,box-shadow] focus:border-ring focus:ring-ring/50 focus:ring-[1px]"
- )}
- initial={{
- height: collapsedHeight,
- scale: 1,
- }}
- animate={{
- height: expanded ? expandHeight : collapsedHeight,
- scale: isTapping ? [1, 0.985, 1] : 1,
- }}
- transition={{
- height: { duration: 0.25, ease: [0.4, 0, 0.2, 1] },
- scale: { duration: 0.6, ease: "easeOut" },
- }}
- onClick={expand}
- ref={buttonRef}
- >
- {!expanded ? (
- <div
- className="flex items-center justify-between w-full"
- style={{
- height: collapsedHeight,
- }}
- >
- <div className="flex items-center gap-2">
- {selectedCountry === "auto" ? (
- <Globe className="!size-[1.05rem]" />
- ) : (
- <ReactCountryFlag
- countryCode={selectedCountry}
- svg
- style={{ width: "1.05rem", height: "1.05rem" }}
- />
- )}
- <span className="pt-[0.05rem]">
- {selectedCountry === "auto" ? "Auto" : selectedLanguage?.name}
- </span>
- </div>
- </div>
- ) : (
- <div className="flex flex-col gap-2 my-2.5 w-full overflow-y-auto scrollbar-hidden">
- <LanguageButton
- language={{ code: "auto", name: "Auto" }}
- onSelect={handleSelect}
- selectedCountry={selectedCountry}
- />
- {languages.map((language) => (
- <LanguageButton
- key={language.code}
- language={language}
- onSelect={handleSelect}
- selectedCountry={selectedCountry}
- />
- ))}
- </div>
- )}
- </motion.button>
-
- <motion.div
- className="absolute top-1/2 right-3 -translate-y-1/2 pointer-events-none z-20 mt-0.5"
- initial={{ opacity: 1 }}
- animate={{ opacity: expanded ? 0 : 1 }}
- transition={{ duration: 0.2, ease: "easeInOut" }}
- >
- <ChevronDown className="text-muted-foreground size-4" />
- </motion.div>
- </div>
- );
-}
-
-function LanguageButton({
- language,
- onSelect,
- selectedCountry,
-}: {
- language: Language;
- onSelect: ({
- code,
- e,
- }: {
- code: string;
- e: React.MouseEvent<HTMLButtonElement>;
- }) => void;
- selectedCountry: string;
-}) {
- return (
- <button
- type="button"
- className="flex items-center gap-2 cursor-pointer text-foreground hover:text-foreground/75"
- onClick={(e) => onSelect({ code: language.code, e })}
- >
- {language.code === "auto" ? (
- <Globe className="!size-[1.0rem]" />
- ) : (
- <ReactCountryFlag
- countryCode={language.code}
- svg
- style={{ width: "1.05rem", height: "1.05rem" }}
- />
- )}
- <span className="pt-[0.05rem]">{language.name}</span>
- </button>
- );
-}
diff --git a/apps/web/src/components/providers/editor-provider.tsx b/apps/web/src/components/providers/editor-provider.tsx
index b8e9ae1d..b1b5ab3a 100644
--- a/apps/web/src/components/providers/editor-provider.tsx
+++ b/apps/web/src/components/providers/editor-provider.tsx
@@ -1,53 +1,136 @@
"use client";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
import { Loader2 } from "lucide-react";
-import { useEditorStore } from "@/stores/editor-store";
+import { useEditor } from "@/hooks/use-editor";
import {
- useKeybindingsListener,
- useKeybindingDisabler,
+ useKeybindingsListener,
+ useKeybindingDisabler,
} from "@/hooks/use-keybindings";
-import { useEditorActions } from "@/hooks/use-editor-actions";
+import { useEditorActions } from "@/hooks/actions/use-editor-actions";
interface EditorProviderProps {
- children: React.ReactNode;
+ projectId: string;
+ children: React.ReactNode;
}
-export function EditorProvider({ children }: EditorProviderProps) {
- const { isInitializing, isPanelsReady, initializeApp } = useEditorStore();
- const { disableKeybindings, enableKeybindings } = useKeybindingDisabler();
+export function EditorProvider({ projectId, children }: EditorProviderProps) {
+ const editor = useEditor();
+ const router = useRouter();
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState<string | null>(null);
+ const { disableKeybindings, enableKeybindings } = useKeybindingDisabler();
+ const activeProject = editor.project.getActiveOrNull();
- // Set up action handlers
- useEditorActions();
+ useEffect(() => {
+ if (isLoading) {
+ disableKeybindings();
+ } else {
+ enableKeybindings();
+ }
+ }, [isLoading, disableKeybindings, enableKeybindings]);
- // Set up keybinding listener
- useKeybindingsListener();
+ useEffect(() => {
+ let cancelled = false;
- // Disable keybindings when initializing
- useEffect(() => {
- if (isInitializing || !isPanelsReady) {
- disableKeybindings();
- } else {
- enableKeybindings();
- }
- }, [isInitializing, isPanelsReady, disableKeybindings, enableKeybindings]);
+ const loadProject = async () => {
+ try {
+ setIsLoading(true);
+ await editor.project.loadProject({ id: projectId });
- useEffect(() => {
- initializeApp();
- }, [initializeApp]);
+ if (cancelled) return;
- // Show loading screen while initializing
- if (isInitializing || !isPanelsReady) {
- return (
- <div className="h-screen w-screen flex items-center justify-center bg-background">
- <div className="flex flex-col items-center gap-4">
- <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
- <p className="text-sm text-muted-foreground">Loading editor...</p>
- </div>
- </div>
- );
- }
+ setIsLoading(false);
+ } catch (err) {
+ if (cancelled) return;
- // App is ready, render children
- return <>{children}</>;
+ const isNotFound =
+ err instanceof Error &&
+ (err.message.includes("not found") ||
+ err.message.includes("does not exist"));
+
+ if (isNotFound) {
+ try {
+ const newProjectId = await editor.project.createNewProject({
+ name: "Untitled Project",
+ });
+ router.replace(`/editor/${newProjectId}`);
+ } catch (_createErr) {
+ setError("Failed to create project");
+ setIsLoading(false);
+ }
+ } else {
+ setError(
+ err instanceof Error ? err.message : "Failed to load project",
+ );
+ setIsLoading(false);
+ }
+ }
+ };
+
+ loadProject();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [projectId, editor, router]);
+
+ if (error) {
+ return (
+ <div className="bg-background flex h-screen w-screen items-center justify-center">
+ <div className="flex flex-col items-center gap-4">
+ <p className="text-destructive text-sm">{error}</p>
+ </div>
+ </div>
+ );
+ }
+
+ if (isLoading) {
+ return (
+ <div className="bg-background flex h-screen w-screen items-center justify-center">
+ <div className="flex flex-col items-center gap-4">
+ <Loader2 className="text-muted-foreground size-8 animate-spin" />
+ <p className="text-muted-foreground text-sm">Loading project...</p>
+ </div>
+ </div>
+ );
+ }
+
+ if (!activeProject) {
+ return (
+ <div className="bg-background flex h-screen w-screen items-center justify-center">
+ <div className="flex flex-col items-center gap-4">
+ <Loader2 className="text-muted-foreground size-8 animate-spin" />
+ <p className="text-muted-foreground text-sm">Exiting project...</p>
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <>
+ <EditorRuntimeBindings />
+ {children}
+ </>
+ );
+}
+
+function EditorRuntimeBindings() {
+ const editor = useEditor();
+
+ useEffect(() => {
+ const handleBeforeUnload = (event: BeforeUnloadEvent) => {
+ if (!editor.save.getIsDirty()) return;
+ event.preventDefault();
+ (event as unknown as { returnValue: string }).returnValue = "";
+ };
+
+ window.addEventListener("beforeunload", handleBeforeUnload);
+ return () => window.removeEventListener("beforeunload", handleBeforeUnload);
+ }, [editor]);
+
+ useEditorActions();
+ useKeybindingsListener();
+ return null;
}
diff --git a/apps/web/src/components/providers/migrators/scenes-migrator.tsx b/apps/web/src/components/providers/migrators/scenes-migrator.tsx
deleted file mode 100644
index b984a7f4..00000000
--- a/apps/web/src/components/providers/migrators/scenes-migrator.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import { usePathname } from "next/navigation";
-import { storageService } from "@/lib/storage/storage-service";
-import { TProject, Scene } from "@/types/project";
-import { generateUUID } from "@/lib/utils";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { Progress } from "@/components/ui/progress";
-import { createMainScene } from "@/stores/project-store";
-
-interface MigrationProgress {
- current: number;
- total: number;
- currentProjectName: string;
-}
-
-export function ScenesMigrator({ children }: { children: React.ReactNode }) {
- const pathname = usePathname();
- const [isMigrating, setIsMigrating] = useState(false);
- const [progress, setProgress] = useState<MigrationProgress>({
- current: 0,
- total: 0,
- currentProjectName: "",
- });
-
- const shouldCheckMigration =
- pathname.startsWith("/editor") || pathname.startsWith("/projects");
-
- useEffect(() => {
- if (!shouldCheckMigration) return;
-
- checkAndMigrateProjects();
- }, [shouldCheckMigration]);
-
- const checkAndMigrateProjects = async () => {
- try {
- const projects = await storageService.loadAllProjects();
- const legacyProjects = projects.filter(
- (project) => !project.scenes || project.scenes.length === 0
- );
-
- if (legacyProjects.length === 0) {
- // No migration needed
- return;
- }
-
- setIsMigrating(true);
- setProgress({
- current: 0,
- total: legacyProjects.length,
- currentProjectName: "",
- });
-
- // Migrate each legacy project
- for (let i = 0; i < legacyProjects.length; i++) {
- const project = legacyProjects[i];
-
- setProgress({
- current: i,
- total: legacyProjects.length,
- currentProjectName: project.name,
- });
-
- await migrateLegacyProject(project);
- }
-
- setProgress({
- current: legacyProjects.length,
- total: legacyProjects.length,
- currentProjectName: "Complete!",
- });
-
- setTimeout(() => {
- setIsMigrating(false);
- }, 1000);
- } catch (error) {
- console.error("Migration failed:", error);
- setIsMigrating(false);
- }
- };
-
- const migrateLegacyProject = async (project: TProject) => {
- try {
- const mainScene: Scene = {
- id: generateUUID(),
- name: "Main Scene",
- isMain: true,
- createdAt: new Date(),
- updatedAt: new Date(),
- };
-
- const migratedProject: TProject = {
- ...project,
- scenes: [mainScene],
- currentSceneId: mainScene.id,
- updatedAt: new Date(),
- };
-
- // Load existing timeline data (legacy format)
- const legacyTimeline = await storageService.loadTimeline({
- projectId: project.id,
- });
-
- await storageService.saveProject({ project: migratedProject });
-
- // If timeline data, migrate it to the main scene
- if (legacyTimeline && legacyTimeline.length > 0) {
- await storageService.saveTimeline({
- projectId: project.id,
- tracks: legacyTimeline,
- sceneId: mainScene.id,
- });
- }
-
- // Clean up legacy timeline storage
- await storageService.deleteProjectTimeline({ projectId: project.id });
- } catch (error) {
- console.error(`Failed to migrate project ${project.name}:`, error);
- throw error;
- }
- };
-
- if (!shouldCheckMigration) {
- return children;
- }
-
- if (isMigrating) {
- const progressPercent =
- progress.total > 0 ? (progress.current / progress.total) * 100 : 0;
-
- return (
- <Dialog open={true}>
- <DialogContent
- className="sm:max-w-md"
- onPointerDownOutside={(e) => e.preventDefault()}
- >
- <DialogHeader>
- <DialogTitle>Updating Projects</DialogTitle>
- <DialogDescription>
- We're adding scene support to your projects. This will only take a
- moment.
- </DialogDescription>
- </DialogHeader>
-
- <div className="space-y-4">
- <div className="space-y-2">
- <div className="flex justify-between text-sm">
- <span>Progress</span>
- <span>
- {progress.current} of {progress.total}
- </span>
- </div>
- <Progress value={progressPercent} className="w-full" />
- </div>
-
- {progress.currentProjectName && (
- <div className="text-sm text-muted-foreground">
- {progress.current < progress.total
- ? `Updating: ${progress.currentProjectName}`
- : progress.currentProjectName}
- </div>
- )}
- </div>
- </DialogContent>
- </Dialog>
- );
- }
-
- return children;
-}
diff --git a/apps/web/src/components/rename-project-dialog.tsx b/apps/web/src/components/rename-project-dialog.tsx
deleted file mode 100644
index dc576b00..00000000
--- a/apps/web/src/components/rename-project-dialog.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { Input } from "@/components/ui/input";
-import { useState } from "react";
-
-export function RenameProjectDialog({
- isOpen,
- onOpenChange,
- onConfirm,
- projectName,
-}: {
- isOpen: boolean;
- onOpenChange: (open: boolean) => void;
- onConfirm: (name: string) => void;
- projectName: string;
-}) {
- const [name, setName] = useState(projectName);
-
- // Reset the name when dialog opens - this is better UX than syncing with every prop change
- const handleOpenChange = (open: boolean) => {
- if (open) {
- setName(projectName);
- }
- onOpenChange(open);
- };
-
- return (
- <Dialog open={isOpen} onOpenChange={handleOpenChange}>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Rename Project</DialogTitle>
- <DialogDescription>
- Enter a new name for your project.
- </DialogDescription>
- </DialogHeader>
-
- <Input
- value={name}
- onChange={(e) => setName(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter") {
- e.preventDefault();
- onConfirm(name);
- }
- }}
- placeholder="Enter a new name"
- className="mt-0 bg-background border-2 border-border"
- />
-
- <DialogFooter>
- <Button
- variant="outline"
- onClick={(e) => {
- e.preventDefault();
- e.stopPropagation();
- onOpenChange(false);
- }}
- >
- Cancel
- </Button>
- <Button onClick={() => onConfirm(name)}>Rename</Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/apps/web/src/components/storage-provider.tsx b/apps/web/src/components/storage-provider.tsx
index 69105d0f..144790dc 100644
--- a/apps/web/src/components/storage-provider.tsx
+++ b/apps/web/src/components/storage-provider.tsx
@@ -1,80 +1,81 @@
"use client";
-import { createContext, useContext, useEffect, useState } from "react";
-import { useProjectStore } from "@/stores/project-store";
-import { useMediaStore } from "@/stores/media-store";
-import { storageService } from "@/lib/storage/storage-service";
+import { createContext, useContext, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
+import { useEditor } from "@/hooks/use-editor";
+import { storageService } from "@/services/storage/storage-service";
interface StorageContextType {
- isInitialized: boolean;
- isLoading: boolean;
- hasSupport: boolean;
- error: string | null;
+ isInitialized: boolean;
+ isLoading: boolean;
+ hasSupport: boolean;
+ error: string | null;
}
const StorageContext = createContext<StorageContextType | null>(null);
export function useStorage() {
- const context = useContext(StorageContext);
- if (!context) {
- throw new Error("useStorage must be used within StorageProvider");
- }
- return context;
+ const context = useContext(StorageContext);
+ if (!context) {
+ throw new Error("useStorage must be used within StorageProvider");
+ }
+ return context;
}
interface StorageProviderProps {
- children: React.ReactNode;
+ children: React.ReactNode;
}
export function StorageProvider({ children }: StorageProviderProps) {
- const [status, setStatus] = useState<StorageContextType>({
- isInitialized: false,
- isLoading: true,
- hasSupport: false,
- error: null,
- });
+ const [status, setStatus] = useState<StorageContextType>({
+ isInitialized: false,
+ isLoading: true,
+ hasSupport: false,
+ error: null,
+ });
- const loadAllProjects = useProjectStore((state) => state.loadAllProjects);
+ const editor = useEditor();
+ const hasInitialized = useRef(false);
- useEffect(() => {
- const initializeStorage = async () => {
- setStatus((prev) => ({ ...prev, isLoading: true }));
+ useEffect(() => {
+ if (hasInitialized.current) return;
+ hasInitialized.current = true;
- try {
- // Check browser support
- const hasSupport = storageService.isFullySupported();
+ const initializeStorage = async () => {
+ setStatus((prev) => ({ ...prev, isLoading: true }));
- if (!hasSupport) {
- toast.warning(
- "Storage not fully supported. Some features may not work."
- );
- }
+ try {
+ const hasSupport = storageService.isFullySupported();
- // Load saved projects (media will be loaded when a project is loaded)
- await loadAllProjects();
+ if (!hasSupport) {
+ toast.warning(
+ "Storage not fully supported. Some features may not work.",
+ );
+ }
- setStatus({
- isInitialized: true,
- isLoading: false,
- hasSupport,
- error: null,
- });
- } catch (error) {
- console.error("Failed to initialize storage:", error);
- setStatus({
- isInitialized: false,
- isLoading: false,
- hasSupport: storageService.isFullySupported(),
- error: error instanceof Error ? error.message : "Unknown error",
- });
- }
- };
+ await editor.project.loadAllProjects();
- initializeStorage();
- }, [loadAllProjects]);
+ setStatus({
+ isInitialized: true,
+ isLoading: false,
+ hasSupport,
+ error: null,
+ });
+ } catch (error) {
+ console.error("Failed to initialize storage:", error);
+ setStatus({
+ isInitialized: false,
+ isLoading: false,
+ hasSupport: storageService.isFullySupported(),
+ error: error instanceof Error ? error.message : "Unknown error",
+ });
+ }
+ };
- return (
- <StorageContext.Provider value={status}>{children}</StorageContext.Provider>
- );
+ initializeStorage();
+ }, [editor.project.loadAllProjects]);
+
+ return (
+ <StorageContext.Provider value={status}>{children}</StorageContext.Provider>
+ );
}
diff --git a/apps/web/src/components/theme-toggle.tsx b/apps/web/src/components/theme-toggle.tsx
index 8302a19c..394d75b2 100644
--- a/apps/web/src/components/theme-toggle.tsx
+++ b/apps/web/src/components/theme-toggle.tsx
@@ -1,25 +1,36 @@
"use client";
import { Button } from "./ui/button";
-import { Sun, Moon } from "lucide-react";
import { useTheme } from "next-themes";
+import { cn } from "@/utils/ui";
+import { Sun03Icon } from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
interface ThemeToggleProps {
- className?: string;
+ className?: string;
+ iconClassName?: string;
+ onToggle?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}
-export function ThemeToggle({ className }: ThemeToggleProps) {
- const { theme, setTheme } = useTheme();
+export function ThemeToggle({
+ className,
+ iconClassName,
+ onToggle,
+}: ThemeToggleProps) {
+ const { theme, setTheme } = useTheme();
- return (
- <Button
- size="icon"
- variant="text"
- className="h-7"
- onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
- >
- <Sun className="!size-[1.1rem]" />
- <span className="sr-only">{theme === "dark" ? "Light" : "Dark"}</span>
- </Button>
- );
+ return (
+ <Button
+ size="icon"
+ variant="text"
+ className={cn("h-7", className)}
+ onClick={(e) => {
+ setTheme(theme === "dark" ? "light" : "dark");
+ onToggle?.(e);
+ }}
+ >
+ <HugeiconsIcon icon={Sun03Icon} className={cn("!size-[1.1rem]", iconClassName)} />
+ <span className="sr-only">{theme === "dark" ? "Light" : "Dark"}</span>
+ </Button>
+ );
}
diff --git a/apps/web/src/components/ui/accordion.tsx b/apps/web/src/components/ui/accordion.tsx
index 68edd869..95a1c2df 100644
--- a/apps/web/src/components/ui/accordion.tsx
+++ b/apps/web/src/components/ui/accordion.tsx
@@ -4,53 +4,53 @@ import * as React from "react";
import { Accordion as AccordionPrimitive } from "radix-ui";
import { ChevronDown } from "lucide-react";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
- React.ElementRef<typeof AccordionPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
+ React.ElementRef<typeof AccordionPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
- <AccordionPrimitive.Item
- ref={ref}
- className={cn("border-b", className)}
- {...props}
- />
+ <AccordionPrimitive.Item
+ ref={ref}
+ className={cn("border-b", className)}
+ {...props}
+ />
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
- React.ElementRef<typeof AccordionPrimitive.Trigger>,
- React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
- <AccordionPrimitive.Header className="flex">
- <AccordionPrimitive.Trigger
- ref={ref}
- className={cn(
- "flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
- className
- )}
- {...props}
- >
- {children}
- <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
- </AccordionPrimitive.Trigger>
- </AccordionPrimitive.Header>
+ <AccordionPrimitive.Header className="flex">
+ <AccordionPrimitive.Trigger
+ ref={ref}
+ className={cn(
+ "flex flex-1 cursor-pointer items-center justify-between py-4 text-left text-sm font-medium hover:underline [&[data-state=open]>svg]:rotate-180",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+ <ChevronDown className="text-muted-foreground size-4 shrink-0" />
+ </AccordionPrimitive.Trigger>
+ </AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
- React.ElementRef<typeof AccordionPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
+ React.ElementRef<typeof AccordionPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
- <AccordionPrimitive.Content
- ref={ref}
- className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
- {...props}
- >
- <div className={cn("pb-4 pt-0", className)}>{children}</div>
- </AccordionPrimitive.Content>
+ <AccordionPrimitive.Content
+ ref={ref}
+ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
+ {...props}
+ >
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
+ </AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
diff --git a/apps/web/src/components/ui/alert-dialog.tsx b/apps/web/src/components/ui/alert-dialog.tsx
index 2cce235c..e7295429 100644
--- a/apps/web/src/components/ui/alert-dialog.tsx
+++ b/apps/web/src/components/ui/alert-dialog.tsx
@@ -3,7 +3,7 @@
import * as React from "react";
import { AlertDialog as AlertDialogPrimitive } from "radix-ui";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
import { buttonVariants } from "./button";
const AlertDialog = AlertDialogPrimitive.Root;
@@ -13,129 +13,129 @@ const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
- React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
- React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
- <AlertDialogPrimitive.Overlay
- className={cn(
- "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
- className
- )}
- {...props}
- ref={ref}
- />
+ <AlertDialogPrimitive.Overlay
+ className={cn(
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
+ className,
+ )}
+ {...props}
+ ref={ref}
+ />
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
- React.ElementRef<typeof AlertDialogPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
- <AlertDialogPortal>
- <AlertDialogOverlay />
- <AlertDialogPrimitive.Content
- ref={ref}
- className={cn(
- "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
- className
- )}
- {...props}
- />
- </AlertDialogPortal>
+ <AlertDialogPortal>
+ <AlertDialogOverlay />
+ <AlertDialogPrimitive.Content
+ ref={ref}
+ className={cn(
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
+ className,
+ )}
+ {...props}
+ />
+ </AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
- <div
- className={cn(
- "flex flex-col space-y-2 text-center sm:text-left",
- className
- )}
- {...props}
- />
+ <div
+ className={cn(
+ "flex flex-col space-y-2 text-center sm:text-left",
+ className,
+ )}
+ {...props}
+ />
);
AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
- <div
- className={cn(
- "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
- className
- )}
- {...props}
- />
+ <div
+ className={cn(
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4",
+ className,
+ )}
+ {...props}
+ />
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
- React.ElementRef<typeof AlertDialogPrimitive.Title>,
- React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
- <AlertDialogPrimitive.Title
- ref={ref}
- className={cn("text-lg font-semibold", className)}
- {...props}
- />
+ <AlertDialogPrimitive.Title
+ ref={ref}
+ className={cn("text-lg font-semibold", className)}
+ {...props}
+ />
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
- React.ElementRef<typeof AlertDialogPrimitive.Description>,
- React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
- <AlertDialogPrimitive.Description
- ref={ref}
- className={cn("text-sm text-muted-foreground", className)}
- {...props}
- />
+ <AlertDialogPrimitive.Description
+ ref={ref}
+ className={cn("text-muted-foreground text-sm", className)}
+ {...props}
+ />
));
AlertDialogDescription.displayName =
- AlertDialogPrimitive.Description.displayName;
+ AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
- React.ElementRef<typeof AlertDialogPrimitive.Action>,
- React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
- <AlertDialogPrimitive.Action
- ref={ref}
- className={cn(buttonVariants(), className)}
- {...props}
- />
+ <AlertDialogPrimitive.Action
+ ref={ref}
+ className={cn(buttonVariants(), className)}
+ {...props}
+ />
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
- React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
- React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
- <AlertDialogPrimitive.Cancel
- ref={ref}
- className={cn(
- buttonVariants({ variant: "outline" }),
- "mt-2 sm:mt-0",
- className
- )}
- {...props}
- />
+ <AlertDialogPrimitive.Cancel
+ ref={ref}
+ className={cn(
+ buttonVariants({ variant: "outline" }),
+ "mt-2 sm:mt-0",
+ className,
+ )}
+ {...props}
+ />
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
- AlertDialog,
- AlertDialogPortal,
- AlertDialogOverlay,
- AlertDialogTrigger,
- AlertDialogContent,
- AlertDialogHeader,
- AlertDialogFooter,
- AlertDialogTitle,
- AlertDialogDescription,
- AlertDialogAction,
- AlertDialogCancel,
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
};
diff --git a/apps/web/src/components/ui/alert.tsx b/apps/web/src/components/ui/alert.tsx
index cef19199..e941ca43 100644
--- a/apps/web/src/components/ui/alert.tsx
+++ b/apps/web/src/components/ui/alert.tsx
@@ -1,58 +1,65 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
-
-import { cn } from "../../lib/utils";
+import { AlertTriangle } from "lucide-react";
+import { cn } from "@/utils/ui";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { Alert02Icon } from "@hugeicons/core-free-icons";
const alertVariants = cva(
- "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
- {
- variants: {
- variant: {
- default: "bg-background text-foreground",
- destructive:
- "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
+ "relative w-full rounded-lg border p-5 py-4.5 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-destructive [&>svg~*]:pl-7",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "bg-destructive/5 border-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
);
const Alert = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
->(({ className, variant, ...props }, ref) => (
- <div
- ref={ref}
- role="alert"
- className={cn(alertVariants({ variant }), className)}
- {...props}
- />
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
+>(({ className, variant, children, ...props }, ref) => (
+ <div
+ ref={ref}
+ role="alert"
+ className={cn(alertVariants({ variant }), className)}
+ {...props}
+ >
+ {variant === "destructive" && (
+ <HugeiconsIcon icon={Alert02Icon} className="size-5 text-destructive mt-0.5" />
+ )}
+ {children}
+ </div>
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes<HTMLHeadingElement>
+ HTMLParagraphElement,
+ React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
- <h5
- ref={ref}
- className={cn("mb-1 font-medium leading-none tracking-tight", className)}
- {...props}
- />
+ <h5
+ ref={ref}
+ className={cn("mb-2 text-base leading-none font-semibold tracking-tight", className)}
+ {...props}
+ />
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes<HTMLParagraphElement>
+ HTMLParagraphElement,
+ React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn("text-sm [&_p]:leading-relaxed", className)}
- {...props}
- />
+ <div
+ ref={ref}
+ className={cn("text-sm [&_p]:leading-relaxed", className)}
+ {...props}
+ />
));
AlertDescription.displayName = "AlertDescription";
diff --git a/apps/web/src/components/ui/audio-player.tsx b/apps/web/src/components/ui/audio-player.tsx
deleted file mode 100644
index 37bf8ca9..00000000
--- a/apps/web/src/components/ui/audio-player.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-"use client";
-
-import { useRef, useEffect } from "react";
-import { usePlaybackStore } from "@/stores/playback-store";
-
-interface AudioPlayerProps {
- src: string;
- className?: string;
- clipStartTime: number;
- trimStart: number;
- trimEnd: number;
- clipDuration: number;
- trackMuted?: boolean;
-}
-
-export function AudioPlayer({
- src,
- className = "",
- clipStartTime,
- trimStart,
- trimEnd,
- clipDuration,
- trackMuted = false,
-}: AudioPlayerProps) {
- const audioRef = useRef<HTMLAudioElement>(null);
- const { isPlaying, currentTime, volume, speed, muted } = usePlaybackStore();
-
- // Calculate if we're within this clip's timeline range
- const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd);
- const isInClipRange =
- currentTime >= clipStartTime && currentTime < clipEndTime;
-
- // Sync playback events
- useEffect(() => {
- const audio = audioRef.current;
- if (!audio || !isInClipRange) return;
-
- const handleSeekEvent = (e: CustomEvent) => {
- // Always update audio time, even if outside clip range
- const timelineTime = e.detail.time;
- const audioTime = Math.max(
- trimStart,
- Math.min(
- clipDuration - trimEnd,
- timelineTime - clipStartTime + trimStart
- )
- );
- audio.currentTime = audioTime;
- };
-
- const handleUpdateEvent = (e: CustomEvent) => {
- // Always update audio time, even if outside clip range
- const timelineTime = e.detail.time;
- const targetTime = Math.max(
- trimStart,
- Math.min(
- clipDuration - trimEnd,
- timelineTime - clipStartTime + trimStart
- )
- );
-
- if (Math.abs(audio.currentTime - targetTime) > 0.5) {
- audio.currentTime = targetTime;
- }
- };
-
- const handleSpeed = (e: CustomEvent) => {
- audio.playbackRate = e.detail.speed;
- };
-
- window.addEventListener("playback-seek", handleSeekEvent as EventListener);
- window.addEventListener(
- "playback-update",
- handleUpdateEvent as EventListener
- );
- window.addEventListener("playback-speed", handleSpeed as EventListener);
-
- return () => {
- window.removeEventListener(
- "playback-seek",
- handleSeekEvent as EventListener
- );
- window.removeEventListener(
- "playback-update",
- handleUpdateEvent as EventListener
- );
- window.removeEventListener(
- "playback-speed",
- handleSpeed as EventListener
- );
- };
- }, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]);
-
- // Sync playback state
- useEffect(() => {
- const audio = audioRef.current;
- if (!audio) return;
-
- if (isPlaying && isInClipRange && !trackMuted) {
- audio.play().catch(() => {});
- } else {
- audio.pause();
- }
- }, [isPlaying, isInClipRange, trackMuted]);
-
- // Sync volume and speed
- useEffect(() => {
- const audio = audioRef.current;
- if (!audio) return;
-
- audio.volume = volume;
- audio.muted = muted || trackMuted;
- audio.playbackRate = speed;
- }, [volume, speed, muted, trackMuted]);
-
- return (
- <audio
- ref={audioRef}
- src={src}
- className={className}
- preload="auto"
- controls={false}
- style={{ display: "none" }} // Audio elements don't need visual representation
- onContextMenu={(e) => e.preventDefault()}
- />
- );
-}
diff --git a/apps/web/src/components/ui/avatar.tsx b/apps/web/src/components/ui/avatar.tsx
index 7979c415..0c2fb4c4 100644
--- a/apps/web/src/components/ui/avatar.tsx
+++ b/apps/web/src/components/ui/avatar.tsx
@@ -3,47 +3,47 @@
import * as React from "react";
import { Avatar as AvatarPrimitive } from "radix-ui";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const Avatar = React.forwardRef<
- React.ElementRef<typeof AvatarPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
+ React.ElementRef<typeof AvatarPrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
- <AvatarPrimitive.Root
- ref={ref}
- className={cn(
- "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
- className
- )}
- {...props}
- />
+ <AvatarPrimitive.Root
+ ref={ref}
+ className={cn(
+ "relative flex size-10 shrink-0 overflow-hidden rounded-full",
+ className,
+ )}
+ {...props}
+ />
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
- React.ElementRef<typeof AvatarPrimitive.Image>,
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
+ React.ElementRef<typeof AvatarPrimitive.Image>,
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
- <AvatarPrimitive.Image
- ref={ref}
- className={cn("aspect-square h-full w-full", className)}
- {...props}
- />
+ <AvatarPrimitive.Image
+ ref={ref}
+ className={cn("aspect-square size-full", className)}
+ {...props}
+ />
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
- React.ElementRef<typeof AvatarPrimitive.Fallback>,
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
- <AvatarPrimitive.Fallback
- ref={ref}
- className={cn(
- "flex h-full w-full items-center justify-center rounded-full bg-muted",
- className
- )}
- {...props}
- />
+ <AvatarPrimitive.Fallback
+ ref={ref}
+ className={cn(
+ "bg-muted flex size-full items-center justify-center rounded-full",
+ className,
+ )}
+ {...props}
+ />
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx
index 02b8532b..21240ba0 100644
--- a/apps/web/src/components/ui/badge.tsx
+++ b/apps/web/src/components/ui/badge.tsx
@@ -1,36 +1,36 @@
-import * as React from "react";
+import type * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const badgeVariants = cva(
- "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
- {
- variants: {
- variant: {
- default:
- "border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary/80",
- secondary:
- "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
- destructive:
- "border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/80",
- outline: "text-foreground",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
);
export interface BadgeProps
- extends React.HTMLAttributes<HTMLDivElement>,
- VariantProps<typeof badgeVariants> {}
+ extends React.HTMLAttributes<HTMLDivElement>,
+ VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
- return (
- <div className={cn(badgeVariants({ variant }), className)} {...props} />
- );
+ return (
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
+ );
}
export { Badge, badgeVariants };
diff --git a/apps/web/src/components/ui/breadcrumb.tsx b/apps/web/src/components/ui/breadcrumb.tsx
index d9df9bf5..e95f2965 100644
--- a/apps/web/src/components/ui/breadcrumb.tsx
+++ b/apps/web/src/components/ui/breadcrumb.tsx
@@ -2,114 +2,114 @@ import * as React from "react";
import { Slot as SlotPrimitive } from "radix-ui";
import { ChevronRight, MoreHorizontal } from "lucide-react";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const Breadcrumb = React.forwardRef<
- HTMLElement,
- React.ComponentPropsWithoutRef<"nav"> & {
- separator?: React.ReactNode;
- }
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode;
+ }
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<
- HTMLOListElement,
- React.ComponentPropsWithoutRef<"ol">
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
- <ol
- ref={ref}
- className={cn(
- "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
- className
- )}
- {...props}
- />
+ <ol
+ ref={ref}
+ className={cn(
+ "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
+ className,
+ )}
+ {...props}
+ />
));
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<
- HTMLLIElement,
- React.ComponentPropsWithoutRef<"li">
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
- <li
- ref={ref}
- className={cn("inline-flex items-center gap-1.5", className)}
- {...props}
- />
+ <li
+ ref={ref}
+ className={cn("inline-flex items-center gap-1.5", className)}
+ {...props}
+ />
));
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
- HTMLAnchorElement,
- React.ComponentPropsWithoutRef<"a"> & {
- asChild?: boolean;
- }
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean;
+ }
>(({ asChild, className, ...props }, ref) => {
- const Comp = asChild ? SlotPrimitive.Slot : "a";
+ const Comp = asChild ? SlotPrimitive.Slot : "a";
- return (
- <Comp
- ref={ref}
- className={cn("transition-colors hover:text-foreground", className)}
- {...props}
- />
- );
+ return (
+ <Comp
+ ref={ref}
+ className={cn("hover:text-foreground", className)}
+ {...props}
+ />
+ );
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<
- HTMLSpanElement,
- React.ComponentPropsWithoutRef<"span">
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
- <span
- ref={ref}
- role="link"
- aria-disabled="true"
- aria-current="page"
- className={cn("font-normal text-foreground", className)}
- {...props}
- />
+ <span
+ ref={ref}
+ role="link"
+ aria-disabled="true"
+ aria-current="page"
+ className={cn("text-foreground font-normal", className)}
+ {...props}
+ />
));
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({
- children,
- className,
- ...props
+ children,
+ className,
+ ...props
}: React.ComponentProps<"li">) => (
- <li
- role="presentation"
- aria-hidden="true"
- className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
- {...props}
- >
- {children ?? <ChevronRight />}
- </li>
+ <li
+ role="presentation"
+ aria-hidden="true"
+ className={cn("[&>svg]:size-3.5", className)}
+ {...props}
+ >
+ {children ?? <ChevronRight />}
+ </li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({
- className,
- ...props
+ className,
+ ...props
}: React.ComponentProps<"span">) => (
- <span
- role="presentation"
- aria-hidden="true"
- className={cn("flex h-9 w-9 items-center justify-center", className)}
- {...props}
- >
- <MoreHorizontal className="h-4 w-4" />
- <span className="sr-only">More</span>
- </span>
+ <span
+ role="presentation"
+ aria-hidden="true"
+ className={cn("flex size-9 items-center justify-center", className)}
+ {...props}
+ >
+ <MoreHorizontal className="size-4" />
+ <span className="sr-only">More</span>
+ </span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
- Breadcrumb,
- BreadcrumbList,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbPage,
- BreadcrumbSeparator,
- BreadcrumbEllipsis,
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
};
diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx
index 56a10f44..6c063eff 100644
--- a/apps/web/src/components/ui/button.tsx
+++ b/apps/web/src/components/ui/button.tsx
@@ -2,59 +2,63 @@ import * as React from "react";
import { Slot as SlotPrimitive } from "radix-ui";
import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const buttonVariants = cva(
- "inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
- {
- variants: {
- variant: {
- default:
- "bg-foreground text-background shadow-sm hover:bg-foreground/90",
- primary:
- "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
- "primary-gradient":
- "bg-gradient-to-r from-cyan-400 to-blue-500 text-white hover:opacity-85 transition-opacity",
- destructive:
- "bg-destructive/0 border border-destructive/25 text-destructive shadow-xs hover:bg-destructive hover:text-destructive-foreground",
- outline:
- "border border-input bg-transparent shadow-xs hover:opacity-75 transition-opacity hover:text-accent-foreground",
- secondary:
- "bg-secondary text-secondary-foreground shadow-xs hover:bg-foreground/15 border border-input",
- text: "bg-transparent p-0 rounded-none opacity-100 hover:opacity-50 transition-opacity", // Instead of ghost (matches app better)
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- default: "h-9 px-4 py-2",
- sm: "h-8 rounded-sm px-3 text-xs",
- lg: "h-10 rounded-md px-8",
- icon: "h-7 w-7",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
+ "inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground hover:bg-primary-hover",
+ background:
+ "bg-background text-foreground hover:bg-background/90",
+ foreground:
+ "bg-foreground text-background hover:bg-foreground/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ "destructive-foreground":
+ "border bg-background hover:bg-destructive/15 text-destructive",
+ outline:
+ "border border-border bg-transparent hover:bg-accent/50",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-foreground/15 border border-input",
+ text: "bg-transparent rounded-none opacity-100 hover:opacity-75",
+ link: "text-primary underline-offset-4 hover:underline !p-0 !h-auto",
+ },
+ size: {
+ default: "h-9.5 px-4 py-2",
+ sm: "h-8 px-3 text-xs",
+ lg: "h-10 p-5 px-6",
+ icon: "size-7",
+ text: "p-0",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
);
export interface ButtonProps
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
- VariantProps<typeof buttonVariants> {
- asChild?: boolean;
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
+ VariantProps<typeof buttonVariants> {
+ asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
- ({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? SlotPrimitive.Slot : "button";
- return (
- <Comp
- className={cn(buttonVariants({ variant, size, className }))}
- ref={ref}
- {...props}
- />
- );
- }
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? SlotPrimitive.Slot : "button";
+ const effectiveSize = size ?? (variant === "text" ? "text" : "default");
+ return (
+ <Comp
+ className={cn(buttonVariants({ variant, size: effectiveSize, className }))}
+ ref={ref}
+ {...props}
+ />
+ );
+ },
);
Button.displayName = "Button";
diff --git a/apps/web/src/components/ui/calendar.tsx b/apps/web/src/components/ui/calendar.tsx
index 192643cb..a0664cc3 100644
--- a/apps/web/src/components/ui/calendar.tsx
+++ b/apps/web/src/components/ui/calendar.tsx
@@ -1,71 +1,71 @@
"use client";
-import * as React from "react";
+import type * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
import { buttonVariants } from "./button";
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({
- className,
- classNames,
- showOutsideDays = true,
- ...props
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
}: CalendarProps) {
- return (
- <DayPicker
- showOutsideDays={showOutsideDays}
- className={cn("p-3", className)}
- classNames={{
- months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
- month: "space-y-4",
- caption: "flex justify-center pt-1 relative items-center",
- caption_label: "text-sm font-medium",
- nav: "space-x-1 flex items-center",
- nav_button: cn(
- buttonVariants({ variant: "outline" }),
- "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
- ),
- nav_button_previous: "absolute left-1",
- nav_button_next: "absolute right-1",
- table: "w-full border-collapse space-y-1",
- head_row: "flex",
- head_cell:
- "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
- row: "flex w-full mt-2",
- cell: cn(
- "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
- props.mode === "range"
- ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
- : "[&:has([aria-selected])]:rounded-md"
- ),
- day: cn(
- buttonVariants({ variant: "text" }),
- "h-8 w-8 p-0 font-normal aria-selected:opacity-100"
- ),
- day_range_start: "day-range-start",
- day_range_end: "day-range-end",
- day_selected:
- "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
- day_today: "bg-accent text-accent-foreground",
- day_outside:
- "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
- day_disabled: "text-muted-foreground opacity-50",
- day_range_middle:
- "aria-selected:bg-accent aria-selected:text-accent-foreground",
- day_hidden: "invisible",
- ...classNames,
- }}
- components={{
- IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
- IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
- }}
- {...props}
- />
- );
+ return (
+ <DayPicker
+ showOutsideDays={showOutsideDays}
+ className={cn("p-3", className)}
+ classNames={{
+ months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
+ month: "space-y-4",
+ caption: "flex justify-center pt-1 relative items-center",
+ caption_label: "text-sm font-medium",
+ nav: "space-x-1 flex items-center",
+ nav_button: cn(
+ buttonVariants({ variant: "outline" }),
+ "size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
+ ),
+ nav_button_previous: "absolute left-1",
+ nav_button_next: "absolute right-1",
+ table: "w-full border-collapse space-y-1",
+ head_row: "flex",
+ head_cell:
+ "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
+ row: "flex w-full mt-2",
+ cell: cn(
+ "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
+ props.mode === "range"
+ ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
+ : "[&:has([aria-selected])]:rounded-md",
+ ),
+ day: cn(
+ buttonVariants({ variant: "text" }),
+ "size-8 p-0 font-normal aria-selected:opacity-100",
+ ),
+ day_range_start: "day-range-start",
+ day_range_end: "day-range-end",
+ day_selected:
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
+ day_today: "bg-accent text-accent-foreground",
+ day_outside:
+ "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
+ day_disabled: "text-muted-foreground opacity-50",
+ day_range_middle:
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
+ day_hidden: "invisible",
+ ...classNames,
+ }}
+ components={{
+ IconLeft: ({ ...props }) => <ChevronLeft className="size-4" />,
+ IconRight: ({ ...props }) => <ChevronRight className="size-4" />,
+ }}
+ {...props}
+ />
+ );
}
Calendar.displayName = "Calendar";
diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx
index 6d0b37cc..c3fc45f0 100644
--- a/apps/web/src/components/ui/card.tsx
+++ b/apps/web/src/components/ui/card.tsx
@@ -1,80 +1,80 @@
import * as React from "react";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const Card = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn("rounded-xl border bg-card text-card-foreground", className)}
- {...props}
- />
+ <div
+ ref={ref}
+ className={cn("bg-card text-card-foreground rounded-2xl border", className)}
+ {...props}
+ />
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn("flex flex-col space-y-1.5 p-6", className)}
- {...props}
- />
+ <div
+ ref={ref}
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
+ {...props}
+ />
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn("font-semibold leading-none tracking-tight", className)}
- {...props}
- />
+ <div
+ ref={ref}
+ className={cn("leading-none font-semibold tracking-tight", className)}
+ {...props}
+ />
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn("text-sm text-muted-foreground", className)}
- {...props}
- />
+ <div
+ ref={ref}
+ className={cn("text-muted-foreground text-sm", className)}
+ {...props}
+ />
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
- <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn("flex items-center p-6 pt-0", className)}
- {...props}
- />
+ <div
+ ref={ref}
+ className={cn("flex items-center p-6 pt-0", className)}
+ {...props}
+ />
));
CardFooter.displayName = "CardFooter";
export {
- Card,
- CardHeader,
- CardFooter,
- CardTitle,
- CardDescription,
- CardContent,
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardDescription,
+ CardContent,
};
diff --git a/apps/web/src/components/ui/carousel.tsx b/apps/web/src/components/ui/carousel.tsx
index a9c98b67..9018b9ea 100644
--- a/apps/web/src/components/ui/carousel.tsx
+++ b/apps/web/src/components/ui/carousel.tsx
@@ -1,12 +1,12 @@
"use client";
-import * as React from "react";
import useEmblaCarousel, {
- type UseEmblaCarouselType,
+ type UseEmblaCarouselType,
} from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
+import * as React from "react";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
import { Button } from "./button";
type CarouselApi = UseEmblaCarouselType[1];
@@ -15,248 +15,248 @@ type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
- opts?: CarouselOptions;
- plugins?: CarouselPlugin;
- orientation?: "horizontal" | "vertical";
- setApi?: (api: CarouselApi) => void;
+ opts?: CarouselOptions;
+ plugins?: CarouselPlugin;
+ orientation?: "horizontal" | "vertical";
+ setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
- carouselRef: ReturnType<typeof useEmblaCarousel>[0];
- api: ReturnType<typeof useEmblaCarousel>[1];
- scrollPrev: () => void;
- scrollNext: () => void;
- canScrollPrev: boolean;
- canScrollNext: boolean;
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0];
+ api: ReturnType<typeof useEmblaCarousel>[1];
+ scrollPrev: () => void;
+ scrollNext: () => void;
+ canScrollPrev: boolean;
+ canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
- const context = React.useContext(CarouselContext);
+ const context = React.useContext(CarouselContext);
- if (!context) {
- throw new Error("useCarousel must be used within a <Carousel />");
- }
+ if (!context) {
+ throw new Error("useCarousel must be used within a <Carousel />");
+ }
- return context;
+ return context;
}
const Carousel = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement> & CarouselProps
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
- (
- {
- orientation = "horizontal",
- opts,
- setApi,
- plugins,
- className,
- children,
- ...props
- },
- ref
- ) => {
- const [carouselRef, api] = useEmblaCarousel(
- {
- ...opts,
- axis: orientation === "horizontal" ? "x" : "y",
- },
- plugins
- );
- const [canScrollPrev, setCanScrollPrev] = React.useState(false);
- const [canScrollNext, setCanScrollNext] = React.useState(false);
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref,
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins,
+ );
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
- const onSelect = React.useCallback((api: CarouselApi) => {
- if (!api) {
- return;
- }
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return;
+ }
- setCanScrollPrev(api.canScrollPrev());
- setCanScrollNext(api.canScrollNext());
- }, []);
+ setCanScrollPrev(api.canScrollPrev());
+ setCanScrollNext(api.canScrollNext());
+ }, []);
- const scrollPrev = React.useCallback(() => {
- api?.scrollPrev();
- }, [api]);
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev();
+ }, [api]);
- const scrollNext = React.useCallback(() => {
- api?.scrollNext();
- }, [api]);
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext();
+ }, [api]);
- const handleKeyDown = React.useCallback(
- (event: React.KeyboardEvent<HTMLDivElement>) => {
- if (event.key === "ArrowLeft") {
- event.preventDefault();
- scrollPrev();
- } else if (event.key === "ArrowRight") {
- event.preventDefault();
- scrollNext();
- }
- },
- [scrollPrev, scrollNext]
- );
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault();
+ scrollPrev();
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault();
+ scrollNext();
+ }
+ },
+ [scrollPrev, scrollNext],
+ );
- React.useEffect(() => {
- if (!api || !setApi) {
- return;
- }
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return;
+ }
- setApi(api);
- }, [api, setApi]);
+ setApi(api);
+ }, [api, setApi]);
- React.useEffect(() => {
- if (!api) {
- return;
- }
+ React.useEffect(() => {
+ if (!api) {
+ return;
+ }
- onSelect(api);
- api.on("reInit", onSelect);
- api.on("select", onSelect);
+ onSelect(api);
+ api.on("reInit", onSelect);
+ api.on("select", onSelect);
- return () => {
- api?.off("select", onSelect);
- };
- }, [api, onSelect]);
+ return () => {
+ api?.off("select", onSelect);
+ };
+ }, [api, onSelect]);
- return (
- <CarouselContext.Provider
- value={{
- carouselRef,
- api,
- opts,
- orientation:
- orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
- scrollPrev,
- scrollNext,
- canScrollPrev,
- canScrollNext,
- }}
- >
- <div
- ref={ref}
- onKeyDownCapture={handleKeyDown}
- className={cn("relative", className)}
- role="region"
- aria-roledescription="carousel"
- {...props}
- >
- {children}
- </div>
- </CarouselContext.Provider>
- );
- }
+ return (
+ <CarouselContext.Provider
+ value={{
+ carouselRef,
+ api,
+ opts,
+ orientation:
+ orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
+ scrollPrev,
+ scrollNext,
+ canScrollPrev,
+ canScrollNext,
+ }}
+ >
+ <div
+ ref={ref}
+ onKeyDownCapture={handleKeyDown}
+ className={cn("relative", className)}
+ role="region"
+ aria-roledescription="carousel"
+ {...props}
+ >
+ {children}
+ </div>
+ </CarouselContext.Provider>
+ );
+ },
);
Carousel.displayName = "Carousel";
const CarouselContent = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
- const { carouselRef, orientation } = useCarousel();
+ const { carouselRef, orientation } = useCarousel();
- return (
- <div ref={carouselRef} className="overflow-hidden">
- <div
- ref={ref}
- className={cn(
- "flex",
- orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
- className
- )}
- {...props}
- />
- </div>
- );
+ return (
+ <div ref={carouselRef} className="overflow-hidden">
+ <div
+ ref={ref}
+ className={cn(
+ "flex",
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
+ className,
+ )}
+ {...props}
+ />
+ </div>
+ );
});
CarouselContent.displayName = "CarouselContent";
const CarouselItem = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
- const { orientation } = useCarousel();
+ const { orientation } = useCarousel();
- return (
- <div
- ref={ref}
- role="group"
- aria-roledescription="slide"
- className={cn(
- "min-w-0 shrink-0 grow-0 basis-full",
- orientation === "horizontal" ? "pl-4" : "pt-4",
- className
- )}
- {...props}
- />
- );
+ return (
+ <div
+ ref={ref}
+ role="group"
+ aria-roledescription="slide"
+ className={cn(
+ "min-w-0 shrink-0 grow-0 basis-full",
+ orientation === "horizontal" ? "pl-4" : "pt-4",
+ className,
+ )}
+ {...props}
+ />
+ );
});
CarouselItem.displayName = "CarouselItem";
const CarouselPrevious = React.forwardRef<
- HTMLButtonElement,
- React.ComponentProps<typeof Button>
+ HTMLButtonElement,
+ React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
- const { orientation, scrollPrev, canScrollPrev } = useCarousel();
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
- return (
- <Button
- ref={ref}
- variant={variant}
- size={size}
- className={cn(
- "absolute h-8 w-8 rounded-full",
- orientation === "horizontal"
- ? "-left-12 top-1/2 -translate-y-1/2"
- : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
- className
- )}
- disabled={!canScrollPrev}
- onClick={scrollPrev}
- {...props}
- >
- <ArrowLeft className="h-4 w-4" />
- <span className="sr-only">Previous slide</span>
- </Button>
- );
+ return (
+ <Button
+ ref={ref}
+ variant={variant}
+ size={size}
+ className={cn(
+ "absolute size-8 rounded-full",
+ orientation === "horizontal"
+ ? "top-1/2 -left-12 -translate-y-1/2"
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
+ className,
+ )}
+ disabled={!canScrollPrev}
+ onClick={scrollPrev}
+ {...props}
+ >
+ <ArrowLeft className="size-4" />
+ <span className="sr-only">Previous slide</span>
+ </Button>
+ );
});
CarouselPrevious.displayName = "CarouselPrevious";
const CarouselNext = React.forwardRef<
- HTMLButtonElement,
- React.ComponentProps<typeof Button>
+ HTMLButtonElement,
+ React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
- const { orientation, scrollNext, canScrollNext } = useCarousel();
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
- return (
- <Button
- ref={ref}
- variant={variant}
- size={size}
- className={cn(
- "absolute h-8 w-8 rounded-full",
- orientation === "horizontal"
- ? "-right-12 top-1/2 -translate-y-1/2"
- : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
- className
- )}
- disabled={!canScrollNext}
- onClick={scrollNext}
- {...props}
- >
- <ArrowRight className="h-4 w-4" />
- <span className="sr-only">Next slide</span>
- </Button>
- );
+ return (
+ <Button
+ ref={ref}
+ variant={variant}
+ size={size}
+ className={cn(
+ "absolute size-8 rounded-full",
+ orientation === "horizontal"
+ ? "top-1/2 -right-12 -translate-y-1/2"
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
+ className,
+ )}
+ disabled={!canScrollNext}
+ onClick={scrollNext}
+ {...props}
+ >
+ <ArrowRight className="size-4" />
+ <span className="sr-only">Next slide</span>
+ </Button>
+ );
});
CarouselNext.displayName = "CarouselNext";
export {
- type CarouselApi,
- Carousel,
- CarouselContent,
- CarouselItem,
- CarouselPrevious,
- CarouselNext,
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
};
diff --git a/apps/web/src/components/ui/chart.tsx b/apps/web/src/components/ui/chart.tsx
index a469c87a..2bd04b20 100644
--- a/apps/web/src/components/ui/chart.tsx
+++ b/apps/web/src/components/ui/chart.tsx
@@ -3,368 +3,368 @@
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import {
- NameType,
- Payload,
- ValueType,
+ NameType,
+ Payload,
+ ValueType,
} from "recharts/types/component/DefaultTooltipContent";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
- [k in string]: {
- label?: React.ReactNode;
- icon?: React.ComponentType;
- } & (
- | { color?: string; theme?: never }
- | { color?: never; theme: Record<keyof typeof THEMES, string> }
- );
+ [k in string]: {
+ label?: React.ReactNode;
+ icon?: React.ComponentType;
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
+ );
};
type ChartContextProps = {
- config: ChartConfig;
+ config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
- const context = React.useContext(ChartContext);
+ const context = React.useContext(ChartContext);
- if (!context) {
- throw new Error("useChart must be used within a <ChartContainer />");
- }
+ if (!context) {
+ throw new Error("useChart must be used within a <ChartContainer />");
+ }
- return context;
+ return context;
}
const ChartContainer = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div"> & {
- config: ChartConfig;
- children: React.ComponentProps<
- typeof RechartsPrimitive.ResponsiveContainer
- >["children"];
- }
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig;
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"];
+ }
>(({ id, className, children, config, ...props }, ref) => {
- const uniqueId = React.useId();
- const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
+ const uniqueId = React.useId();
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
- return (
- <ChartContext.Provider value={{ config }}>
- <div
- data-chart={chartId}
- ref={ref}
- className={cn(
- "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden",
- className
- )}
- {...props}
- >
- <ChartStyle id={chartId} config={config} />
- <RechartsPrimitive.ResponsiveContainer>
- {children}
- </RechartsPrimitive.ResponsiveContainer>
- </div>
- </ChartContext.Provider>
- );
+ return (
+ <ChartContext.Provider value={{ config }}>
+ <div
+ data-chart={chartId}
+ ref={ref}
+ className={cn(
+ "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
+ className,
+ )}
+ {...props}
+ >
+ <ChartStyle id={chartId} config={config} />
+ <RechartsPrimitive.ResponsiveContainer>
+ {children}
+ </RechartsPrimitive.ResponsiveContainer>
+ </div>
+ </ChartContext.Provider>
+ );
});
ChartContainer.displayName = "Chart";
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
- const colorConfig = Object.entries(config).filter(
- ([_, config]) => config.theme || config.color
- );
+ const colorConfig = Object.entries(config).filter(
+ ([_, config]) => config.theme || config.color,
+ );
- if (!colorConfig.length) {
- return null;
- }
+ if (!colorConfig.length) {
+ return null;
+ }
- return (
- <style
- dangerouslySetInnerHTML={{
- __html: Object.entries(THEMES)
- .map(
- ([theme, prefix]) => `
+ return (
+ <style
+ dangerouslySetInnerHTML={{
+ __html: Object.entries(THEMES)
+ .map(
+ ([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
- .map(([key, itemConfig]) => {
- const color =
- itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
- itemConfig.color;
- return color ? ` --color-${key}: ${color};` : null;
- })
- .join("\n")}
+ .map(([key, itemConfig]) => {
+ const color =
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
+ itemConfig.color;
+ return color ? ` --color-${key}: ${color};` : null;
+ })
+ .join("\n")}
}
-`
- )
- .join("\n"),
- }}
- />
- );
+`,
+ )
+ .join("\n"),
+ }}
+ />
+ );
};
const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
- React.ComponentProps<"div"> & {
- hideLabel?: boolean;
- hideIndicator?: boolean;
- indicator?: "line" | "dot" | "dashed";
- nameKey?: string;
- labelKey?: string;
- }
+ HTMLDivElement,
+ React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
+ React.ComponentProps<"div"> & {
+ hideLabel?: boolean;
+ hideIndicator?: boolean;
+ indicator?: "line" | "dot" | "dashed";
+ nameKey?: string;
+ labelKey?: string;
+ }
>(
- (
- {
- active,
- payload,
- className,
- indicator = "dot",
- hideLabel = false,
- hideIndicator = false,
- label,
- labelFormatter,
- labelClassName,
- formatter,
- color,
- nameKey,
- labelKey,
- },
- ref
- ) => {
- const { config } = useChart();
+ (
+ {
+ active,
+ payload,
+ className,
+ indicator = "dot",
+ hideLabel = false,
+ hideIndicator = false,
+ label,
+ labelFormatter,
+ labelClassName,
+ formatter,
+ color,
+ nameKey,
+ labelKey,
+ },
+ ref,
+ ) => {
+ const { config } = useChart();
- const tooltipLabel = React.useMemo(() => {
- if (hideLabel || !payload?.length) {
- return null;
- }
+ const tooltipLabel = React.useMemo(() => {
+ if (hideLabel || !payload?.length) {
+ return null;
+ }
- const [item] = payload;
- const key = `${labelKey || item.dataKey || item.name || "value"}`;
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
- const value =
- !labelKey && typeof label === "string"
- ? config[label as keyof typeof config]?.label || label
- : itemConfig?.label;
+ const [item] = payload;
+ const key = `${labelKey || item.dataKey || item.name || "value"}`;
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
+ const value =
+ !labelKey && typeof label === "string"
+ ? config[label as keyof typeof config]?.label || label
+ : itemConfig?.label;
- if (labelFormatter) {
- return (
- <div className={cn("font-medium", labelClassName)}>
- {labelFormatter(value, payload)}
- </div>
- );
- }
+ if (labelFormatter) {
+ return (
+ <div className={cn("font-medium", labelClassName)}>
+ {labelFormatter(value, payload)}
+ </div>
+ );
+ }
- if (!value) {
- return null;
- }
+ if (!value) {
+ return null;
+ }
- return <div className={cn("font-medium", labelClassName)}>{value}</div>;
- }, [
- label,
- labelFormatter,
- payload,
- hideLabel,
- labelClassName,
- config,
- labelKey,
- ]);
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>;
+ }, [
+ label,
+ labelFormatter,
+ payload,
+ hideLabel,
+ labelClassName,
+ config,
+ labelKey,
+ ]);
- if (!active || !payload?.length) {
- return null;
- }
+ if (!active || !payload?.length) {
+ return null;
+ }
- const nestLabel = payload.length === 1 && indicator !== "dot";
+ const nestLabel = payload.length === 1 && indicator !== "dot";
- return (
- <div
- ref={ref}
- className={cn(
- "grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
- className
- )}
- >
- {nestLabel ? null : tooltipLabel}
- <div className="grid gap-1.5">
- {payload.map((item, index) => {
- const key = `${nameKey || item.name || item.dataKey || "value"}`;
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
- const indicatorColor = color || item.payload.fill || item.color;
+ return (
+ <div
+ ref={ref}
+ className={cn(
+ "border-border/50 bg-background grid min-w-32 items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
+ className,
+ )}
+ >
+ {nestLabel ? null : tooltipLabel}
+ <div className="grid gap-1.5">
+ {payload.map((item, index) => {
+ const key = `${nameKey || item.name || item.dataKey || "value"}`;
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
+ const indicatorColor = color || item.payload.fill || item.color;
- return (
- <div
- key={item.dataKey}
- className={cn(
- "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
- indicator === "dot" && "items-center"
- )}
- >
- {formatter && item?.value !== undefined && item.name ? (
- formatter(item.value, item.name, item, index, item.payload)
- ) : (
- <>
- {itemConfig?.icon ? (
- <itemConfig.icon />
- ) : (
- !hideIndicator && (
- <div
- className={cn(
- "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
- {
- "h-2.5 w-2.5": indicator === "dot",
- "w-1": indicator === "line",
- "w-0 border-[1.5px] border-dashed bg-transparent":
- indicator === "dashed",
- "my-0.5": nestLabel && indicator === "dashed",
- }
- )}
- style={
- {
- "--color-bg": indicatorColor,
- "--color-border": indicatorColor,
- } as React.CSSProperties
- }
- />
- )
- )}
- <div
- className={cn(
- "flex flex-1 justify-between leading-none",
- nestLabel ? "items-end" : "items-center"
- )}
- >
- <div className="grid gap-1.5">
- {nestLabel ? tooltipLabel : null}
- <span className="text-muted-foreground">
- {itemConfig?.label || item.name}
- </span>
- </div>
- {item.value && (
- <span className="font-mono font-medium tabular-nums text-foreground">
- {item.value.toLocaleString()}
- </span>
- )}
- </div>
- </>
- )}
- </div>
- );
- })}
- </div>
- </div>
- );
- }
+ return (
+ <div
+ key={item.dataKey}
+ className={cn(
+ "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5",
+ indicator === "dot" && "items-center",
+ )}
+ >
+ {formatter && item?.value !== undefined && item.name ? (
+ formatter(item.value, item.name, item, index, item.payload)
+ ) : (
+ <>
+ {itemConfig?.icon ? (
+ <itemConfig.icon />
+ ) : (
+ !hideIndicator && (
+ <div
+ className={cn(
+ "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
+ {
+ "size-2.5": indicator === "dot",
+ "w-1": indicator === "line",
+ "w-0 border-[1.5px] border-dashed bg-transparent":
+ indicator === "dashed",
+ "my-0.5": nestLabel && indicator === "dashed",
+ },
+ )}
+ style={
+ {
+ "--color-bg": indicatorColor,
+ "--color-border": indicatorColor,
+ } as React.CSSProperties
+ }
+ />
+ )
+ )}
+ <div
+ className={cn(
+ "flex flex-1 justify-between leading-none",
+ nestLabel ? "items-end" : "items-center",
+ )}
+ >
+ <div className="grid gap-1.5">
+ {nestLabel ? tooltipLabel : null}
+ <span className="text-muted-foreground">
+ {itemConfig?.label || item.name}
+ </span>
+ </div>
+ {item.value && (
+ <span className="text-foreground font-mono font-medium tabular-nums">
+ {item.value.toLocaleString()}
+ </span>
+ )}
+ </div>
+ </>
+ )}
+ </div>
+ );
+ })}
+ </div>
+ </div>
+ );
+ },
);
ChartTooltipContent.displayName = "ChartTooltip";
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div"> &
- Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
- hideIcon?: boolean;
- nameKey?: string;
- }
+ HTMLDivElement,
+ React.ComponentProps<"div"> &
+ Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
+ hideIcon?: boolean;
+ nameKey?: string;
+ }
>(
- (
- { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
- ref
- ) => {
- const { config } = useChart();
+ (
+ { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
+ ref,
+ ) => {
+ const { config } = useChart();
- if (!payload?.length) {
- return null;
- }
+ if (!payload?.length) {
+ return null;
+ }
- return (
- <div
- ref={ref}
- className={cn(
- "flex items-center justify-center gap-4",
- verticalAlign === "top" ? "pb-3" : "pt-3",
- className
- )}
- >
- {payload.map((item) => {
- const key = `${nameKey || item.dataKey || "value"}`;
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
+ return (
+ <div
+ ref={ref}
+ className={cn(
+ "flex items-center justify-center gap-4",
+ verticalAlign === "top" ? "pb-3" : "pt-3",
+ className,
+ )}
+ >
+ {payload.map((item) => {
+ const key = `${nameKey || item.dataKey || "value"}`;
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
- return (
- <div
- key={item.value}
- className={cn(
- "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
- )}
- >
- {itemConfig?.icon && !hideIcon ? (
- <itemConfig.icon />
- ) : (
- <div
- className="h-2 w-2 shrink-0 rounded-[2px]"
- style={{
- backgroundColor: item.color,
- }}
- />
- )}
- {itemConfig?.label}
- </div>
- );
- })}
- </div>
- );
- }
+ return (
+ <div
+ key={item.value}
+ className={cn(
+ "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:size-3",
+ )}
+ >
+ {itemConfig?.icon && !hideIcon ? (
+ <itemConfig.icon />
+ ) : (
+ <div
+ className="size-2 shrink-0 rounded-[2px]"
+ style={{
+ backgroundColor: item.color,
+ }}
+ />
+ )}
+ {itemConfig?.label}
+ </div>
+ );
+ })}
+ </div>
+ );
+ },
);
ChartLegendContent.displayName = "ChartLegend";
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
- config: ChartConfig,
- payload: unknown,
- key: string
+ config: ChartConfig,
+ payload: unknown,
+ key: string,
) {
- if (typeof payload !== "object" || payload === null) {
- return;
- }
+ if (typeof payload !== "object" || payload === null) {
+ return;
+ }
- const payloadPayload =
- "payload" in payload &&
- typeof payload.payload === "object" &&
- payload.payload !== null
- ? payload.payload
- : undefined;
+ const payloadPayload =
+ "payload" in payload &&
+ typeof payload.payload === "object" &&
+ payload.payload !== null
+ ? payload.payload
+ : undefined;
- let configLabelKey: string = key;
+ let configLabelKey: string = key;
- if (
- key in payload &&
- typeof payload[key as keyof typeof payload] === "string"
- ) {
- configLabelKey = payload[key as keyof typeof payload] as string;
- } else if (
- payloadPayload &&
- key in payloadPayload &&
- typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
- ) {
- configLabelKey = payloadPayload[
- key as keyof typeof payloadPayload
- ] as string;
- }
+ if (
+ key in payload &&
+ typeof payload[key as keyof typeof payload] === "string"
+ ) {
+ configLabelKey = payload[key as keyof typeof payload] as string;
+ } else if (
+ payloadPayload &&
+ key in payloadPayload &&
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
+ ) {
+ configLabelKey = payloadPayload[
+ key as keyof typeof payloadPayload
+ ] as string;
+ }
- return configLabelKey in config
- ? config[configLabelKey]
- : config[key as keyof typeof config];
+ return configLabelKey in config
+ ? config[configLabelKey]
+ : config[key as keyof typeof config];
}
export {
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
- ChartLegend,
- ChartLegendContent,
- ChartStyle,
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+ ChartLegend,
+ ChartLegendContent,
+ ChartStyle,
};
diff --git a/apps/web/src/components/ui/checkbox.tsx b/apps/web/src/components/ui/checkbox.tsx
index 5e429994..af33b5b3 100644
--- a/apps/web/src/components/ui/checkbox.tsx
+++ b/apps/web/src/components/ui/checkbox.tsx
@@ -4,26 +4,26 @@ import * as React from "react";
import { Checkbox as CheckboxPrimitive } from "radix-ui";
import { Check } from "lucide-react";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const Checkbox = React.forwardRef<
- React.ElementRef<typeof CheckboxPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
- <CheckboxPrimitive.Root
- ref={ref}
- className={cn(
- "peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow-sm focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
- className
- )}
- {...props}
- >
- <CheckboxPrimitive.Indicator
- className={cn("flex items-center justify-center text-current")}
- >
- <Check className="h-4 w-4" />
- </CheckboxPrimitive.Indicator>
- </CheckboxPrimitive.Root>
+ <CheckboxPrimitive.Root
+ ref={ref}
+ className={cn(
+ "cursor-pointer bg-background peer focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary size-4 shrink-0 shadow-xs rounded-sm border focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
+ className,
+ )}
+ {...props}
+ >
+ <CheckboxPrimitive.Indicator
+ className={cn("flex items-center justify-center text-current")}
+ >
+ <Check className="size-4" />
+ </CheckboxPrimitive.Indicator>
+ </CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
diff --git a/apps/web/src/components/ui/color-picker.tsx b/apps/web/src/components/ui/color-picker.tsx
index b94c1f98..ad350e0e 100644
--- a/apps/web/src/components/ui/color-picker.tsx
+++ b/apps/web/src/components/ui/color-picker.tsx
@@ -1,336 +1,343 @@
-import * as React from "react";
+import { forwardRef, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
import { Input } from "./input";
interface ColorPickerProps {
- value?: string;
- onChange?: (value: string) => void;
- className?: string;
- containerRef?: React.RefObject<HTMLDivElement>;
+ value?: string;
+ onChange?: (value: string) => void;
+ className?: string;
+ containerRef?: React.RefObject<HTMLDivElement>;
}
const hexToHsv = (hex: string) => {
- const r = parseInt(hex.slice(0, 2), 16) / 255;
- const g = parseInt(hex.slice(2, 4), 16) / 255;
- const b = parseInt(hex.slice(4, 6), 16) / 255;
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
- const max = Math.max(r, g, b);
- const min = Math.min(r, g, b);
- const diff = max - min;
+ const max = Math.max(r, g, b);
+ const min = Math.min(r, g, b);
+ const diff = max - min;
- let h = 0;
- let s = max === 0 ? 0 : diff / max;
- let v = max;
+ let h = 0;
+ const s = max === 0 ? 0 : diff / max;
+ const v = max;
- if (diff !== 0) {
- switch (max) {
- case r:
- h = ((g - b) / diff) % 6;
- break;
- case g:
- h = (b - r) / diff + 2;
- break;
- case b:
- h = (r - g) / diff + 4;
- break;
- }
- }
+ if (diff !== 0) {
+ switch (max) {
+ case r:
+ h = ((g - b) / diff) % 6;
+ break;
+ case g:
+ h = (b - r) / diff + 2;
+ break;
+ case b:
+ h = (r - g) / diff + 4;
+ break;
+ }
+ }
- h = (h * 60 + 360) % 360;
- if (isNaN(h)) h = 0;
+ h = (h * 60 + 360) % 360;
+ if (Number.isNaN(h)) h = 0;
- return [h, s, v];
+ return [h, s, v];
};
const hsvToHex = (h: number, s: number, v: number) => {
- const c = v * s;
- const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
- const m = v - c;
+ const c = v * s;
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
+ const m = v - c;
- let r = 0,
- g = 0,
- b = 0;
+ let r = 0,
+ g = 0,
+ b = 0;
- if (h >= 0 && h < 60) {
- r = c;
- g = x;
- b = 0;
- } else if (h >= 60 && h < 120) {
- r = x;
- g = c;
- b = 0;
- } else if (h >= 120 && h < 180) {
- r = 0;
- g = c;
- b = x;
- } else if (h >= 180 && h < 240) {
- r = 0;
- g = x;
- b = c;
- } else if (h >= 240 && h < 300) {
- r = x;
- g = 0;
- b = c;
- } else if (h >= 300 && h < 360) {
- r = c;
- g = 0;
- b = x;
- }
+ if (h >= 0 && h < 60) {
+ r = c;
+ g = x;
+ b = 0;
+ } else if (h >= 60 && h < 120) {
+ r = x;
+ g = c;
+ b = 0;
+ } else if (h >= 120 && h < 180) {
+ r = 0;
+ g = c;
+ b = x;
+ } else if (h >= 180 && h < 240) {
+ r = 0;
+ g = x;
+ b = c;
+ } else if (h >= 240 && h < 300) {
+ r = x;
+ g = 0;
+ b = c;
+ } else if (h >= 300 && h < 360) {
+ r = c;
+ g = 0;
+ b = x;
+ }
- r = Math.round((r + m) * 255);
- g = Math.round((g + m) * 255);
- b = Math.round((b + m) * 255);
+ r = Math.round((r + m) * 255);
+ g = Math.round((g + m) * 255);
+ b = Math.round((b + m) * 255);
- return [r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("");
+ return [r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("");
};
-const ColorPicker = React.forwardRef<HTMLDivElement, ColorPickerProps>(
- ({ className, value = "FFFFFF", onChange, containerRef, ...props }, ref) => {
- const [isOpen, setIsOpen] = React.useState(false);
- const [isDragging, setIsDragging] = React.useState<
- "saturation" | "hue" | null
- >(null);
- const [pickerPosition, setPickerPosition] = React.useState({
- right: 0,
- bottom: 0,
- });
- const [internalHue, setInternalHue] = React.useState(0);
- const [inputValue, setInputValue] = React.useState(value);
+const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
+ ({ className, value = "FFFFFF", onChange, containerRef, ...props }, ref) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [isDragging, setIsDragging] = useState<"saturation" | "hue" | null>(
+ null,
+ );
+ const [pickerPosition, setPickerPosition] = useState({
+ right: 0,
+ bottom: 0,
+ });
+ const [internalHue, setInternalHue] = useState(0);
+ const [inputValue, setInputValue] = useState(value);
- const pickerRef = React.useRef<HTMLDivElement>(null);
- const saturationRef = React.useRef<HTMLDivElement>(null);
- const hueRef = React.useRef<HTMLDivElement>(null);
- const triggerRef = React.useRef<HTMLDivElement>(null);
+ const pickerRef = useRef<HTMLDivElement>(null);
+ const saturationRef = useRef<HTMLButtonElement>(null);
+ const hueRef = useRef<HTMLButtonElement>(null);
+ const triggerRef = useRef<HTMLButtonElement>(null);
- const [h, s, v] = hexToHsv(value);
- const displayHue = s > 0 ? h : internalHue;
+ const [h, s, v] = hexToHsv(value);
+ const displayHue = s > 0 ? h : internalHue;
- React.useEffect(() => {
- setInputValue(value);
- }, [value]);
+ useEffect(() => {
+ setInputValue(value);
+ }, [value]);
- React.useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (
- pickerRef.current &&
- !pickerRef.current.contains(event.target as Node)
- ) {
- setIsOpen(false);
- }
- };
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ pickerRef.current &&
+ !pickerRef.current.contains(event.target as Node)
+ ) {
+ setIsOpen(false);
+ }
+ };
- if (isOpen) {
- document.addEventListener("mousedown", handleClickOutside);
- return () =>
- document.removeEventListener("mousedown", handleClickOutside);
- }
- }, [isOpen]);
+ if (isOpen) {
+ document.addEventListener("mousedown", handleClickOutside);
+ return () =>
+ document.removeEventListener("mousedown", handleClickOutside);
+ }
+ }, [isOpen]);
- React.useEffect(() => {
- const handleMouseMove = (e: MouseEvent) => {
- if (!isDragging) return;
+ useEffect(() => {
+ const handleMouseMove = (e: MouseEvent) => {
+ if (!isDragging) return;
- if (isDragging === "saturation" && saturationRef.current) {
- const rect = saturationRef.current.getBoundingClientRect();
- const x = Math.max(
- 0,
- Math.min(1, (e.clientX - rect.left) / rect.width)
- );
- const y = Math.max(
- 0,
- Math.min(1, (e.clientY - rect.top) / rect.height)
- );
- const newS = x;
- const newV = 1 - y;
- const newHex = hsvToHex(displayHue, newS, newV);
- onChange?.(newHex);
- }
+ if (isDragging === "saturation" && saturationRef.current) {
+ const rect = saturationRef.current.getBoundingClientRect();
+ const x = Math.max(
+ 0,
+ Math.min(1, (e.clientX - rect.left) / rect.width),
+ );
+ const y = Math.max(
+ 0,
+ Math.min(1, (e.clientY - rect.top) / rect.height),
+ );
+ const newS = x;
+ const newV = 1 - y;
+ const newHex = hsvToHex(displayHue, newS, newV);
+ onChange?.(newHex);
+ }
- if (isDragging === "hue" && hueRef.current) {
- const rect = hueRef.current.getBoundingClientRect();
- const x = Math.max(
- 0,
- Math.min(1, (e.clientX - rect.left) / rect.width)
- );
- const newH = x * 360;
- setInternalHue(newH);
- if (s > 0) {
- const newHex = hsvToHex(newH, s, v);
- onChange?.(newHex);
- }
- }
- };
+ if (isDragging === "hue" && hueRef.current) {
+ const rect = hueRef.current.getBoundingClientRect();
+ const x = Math.max(
+ 0,
+ Math.min(1, (e.clientX - rect.left) / rect.width),
+ );
+ const newH = x * 360;
+ setInternalHue(newH);
+ if (s > 0) {
+ const newHex = hsvToHex(newH, s, v);
+ onChange?.(newHex);
+ }
+ }
+ };
- const handleMouseUp = () => {
- setIsDragging(null);
- };
+ const handleMouseUp = () => {
+ setIsDragging(null);
+ };
- if (isDragging) {
- document.addEventListener("mousemove", handleMouseMove);
- document.addEventListener("mouseup", handleMouseUp);
- return () => {
- document.removeEventListener("mousemove", handleMouseMove);
- document.removeEventListener("mouseup", handleMouseUp);
- };
- }
- }, [isDragging, displayHue, s, v, onChange]);
+ if (isDragging) {
+ document.addEventListener("mousemove", handleMouseMove);
+ document.addEventListener("mouseup", handleMouseUp);
+ return () => {
+ document.removeEventListener("mousemove", handleMouseMove);
+ document.removeEventListener("mouseup", handleMouseUp);
+ };
+ }
+ }, [isDragging, displayHue, s, v, onChange]);
- const handleSaturationMouseDown = (e: React.MouseEvent) => {
- e.preventDefault();
- setIsDragging("saturation");
- const rect = saturationRef.current!.getBoundingClientRect();
- const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
- const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
- const newS = x;
- const newV = 1 - y;
- const newHex = hsvToHex(displayHue, newS, newV);
- onChange?.(newHex);
- };
+ const handleSaturationMouseDown = (e: React.MouseEvent) => {
+ e.preventDefault();
+ const saturationElement = saturationRef.current;
+ if (!saturationElement) return;
+ setIsDragging("saturation");
+ const rect = saturationElement.getBoundingClientRect();
+ const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
+ const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
+ const newS = x;
+ const newV = 1 - y;
+ const newHex = hsvToHex(displayHue, newS, newV);
+ onChange?.(newHex);
+ };
- const handleHueMouseDown = (e: React.MouseEvent) => {
- e.preventDefault();
- setIsDragging("hue");
- const rect = hueRef.current!.getBoundingClientRect();
- const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
- const newH = x * 360;
- setInternalHue(newH);
- if (s > 0) {
- const newHex = hsvToHex(newH, s, v);
- onChange?.(newHex);
- }
- };
+ const handleHueMouseDown = (e: React.MouseEvent) => {
+ e.preventDefault();
+ const hueElement = hueRef.current;
+ if (!hueElement) return;
+ setIsDragging("hue");
+ const rect = hueElement.getBoundingClientRect();
+ const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
+ const newH = x * 360;
+ setInternalHue(newH);
+ if (s > 0) {
+ const newHex = hsvToHex(newH, s, v);
+ onChange?.(newHex);
+ }
+ };
- const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- const hex = e.target.value.replace("#", "");
- setInputValue(hex);
- };
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const hex = e.target.value.replace("#", "");
+ setInputValue(hex);
+ };
- const handleInputBlur = () => {
- onChange?.(inputValue);
- };
+ const handleInputBlur = () => {
+ onChange?.(inputValue);
+ };
- const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if (e.key === "Enter") {
- onChange?.(inputValue);
- e.currentTarget.blur();
- }
- };
+ const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (e.key === "Enter") {
+ onChange?.(inputValue);
+ e.currentTarget.blur();
+ }
+ };
- const saturationStyle = {
- background: `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(${displayHue}, 100%, 50%))`,
- };
+ const saturationStyle = {
+ background: `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(${displayHue}, 100%, 50%))`,
+ };
- const hueStyle = {
- background:
- "linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)",
- };
+ const hueStyle = {
+ background:
+ "linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)",
+ };
- return (
- <div className="relative">
- <div
- ref={ref}
- className={cn(
- "bg-panel-accent h-9 rounded-full flex items-center gap-2 px-[0.45rem]",
- className
- )}
- {...props}
- >
- <div
- ref={triggerRef}
- className="size-6 rounded-full cursor-pointer hover:ring-2 hover:ring-white/20 transition-all"
- style={{ backgroundColor: `#${value}` }}
- onClick={() => {
- if (!isOpen && triggerRef.current && containerRef?.current) {
- const containerRect =
- containerRef.current.getBoundingClientRect();
- setPickerPosition({
- right: window.innerWidth - containerRect.left - 8,
- bottom: window.innerHeight - containerRect.bottom,
- });
- }
- setIsOpen(!isOpen);
- }}
- />
- <div className="flex-1 flex items-center">
- <Input
- className="bg-transparent p-0 !ring-0 !ring-offset-0 !border-0"
- containerClassName="w-full"
- value={inputValue}
- onChange={handleInputChange}
- onBlur={handleInputBlur}
- onKeyDown={handleInputKeyDown}
- />
- </div>
- </div>
+ return (
+ <div className="relative flex-1">
+ <div
+ ref={ref}
+ className={cn(
+ "bg-panel-accent flex h-9 items-center gap-2 rounded-full px-[0.45rem]",
+ className,
+ )}
+ {...props}
+ >
+ <button
+ ref={triggerRef}
+ className="size-6 cursor-pointer rounded-full hover:ring-2 hover:ring-white/20"
+ style={{ backgroundColor: `#${value}` }}
+ type="button"
+ onClick={() => {
+ if (!isOpen && triggerRef.current && containerRef?.current) {
+ const containerRect =
+ containerRef.current.getBoundingClientRect();
+ setPickerPosition({
+ right: window.innerWidth - containerRect.left - 8,
+ bottom: window.innerHeight - containerRect.bottom,
+ });
+ }
+ setIsOpen(!isOpen);
+ }}
+ />
+ <div className="flex flex-1 items-center">
+ <Input
+ className="!border-0 bg-transparent p-0 !ring-0 !ring-offset-0"
+ containerClassName="w-full"
+ value={inputValue}
+ onChange={handleInputChange}
+ onBlur={handleInputBlur}
+ onKeyDown={handleInputKeyDown}
+ />
+ </div>
+ </div>
- {isOpen &&
- createPortal(
- <div
- ref={pickerRef}
- className="fixed z-50 p-4 bg-popover border border-border rounded-lg shadow-lg select-none"
- style={{
- right: pickerPosition.right,
- bottom: pickerPosition.bottom,
- }}
- >
- <div
- ref={saturationRef}
- className="relative w-48 h-32 cursor-crosshair mb-3"
- style={saturationStyle}
- onMouseDown={handleSaturationMouseDown}
- >
- <ColorCircle
- size="sm"
- position={{ left: `${s * 100}%`, top: `${(1 - v) * 100}%` }}
- color={`#${value}`}
- />
- </div>
+ {isOpen &&
+ createPortal(
+ <div
+ ref={pickerRef}
+ className="bg-popover border-border fixed z-50 rounded-lg border p-4 shadow-lg select-none"
+ style={{
+ right: pickerPosition.right,
+ bottom: pickerPosition.bottom,
+ }}
+ >
+ <button
+ ref={saturationRef}
+ className="relative mb-3 h-32 w-48 cursor-crosshair appearance-none border-0 bg-transparent p-0"
+ style={saturationStyle}
+ type="button"
+ onMouseDown={handleSaturationMouseDown}
+ >
+ <ColorCircle
+ size="sm"
+ position={{ left: `${s * 100}%`, top: `${(1 - v) * 100}%` }}
+ color={`#${value}`}
+ />
+ </button>
- <div
- ref={hueRef}
- className="relative w-48 h-4 rounded-lg cursor-pointer"
- style={hueStyle}
- onMouseDown={handleHueMouseDown}
- >
- <ColorCircle
- size="md"
- position={{
- left: `${(displayHue / 360) * 100}%`,
- top: "50%",
- }}
- color={`#${value}`}
- />
- </div>
- </div>,
- document.body
- )}
- </div>
- );
- }
+ <button
+ ref={hueRef}
+ className="relative h-4 w-48 cursor-pointer rounded-lg appearance-none border-0 bg-transparent p-0"
+ style={hueStyle}
+ type="button"
+ onMouseDown={handleHueMouseDown}
+ >
+ <ColorCircle
+ size="md"
+ position={{
+ left: `${(displayHue / 360) * 100}%`,
+ top: "50%",
+ }}
+ color={`#${value}`}
+ />
+ </button>
+ </div>,
+ document.body,
+ )}
+ </div>
+ );
+ },
);
ColorPicker.displayName = "ColorPicker";
const ColorCircle = ({
- size,
- position,
- color,
+ size,
+ position,
+ color,
}: {
- size: "sm" | "md";
- position: { left: string; top: string };
- color: string;
+ size: "sm" | "md";
+ position: { left: string; top: string };
+ color: string;
}) => (
- <div
- className={`absolute border-3 border-white rounded-full shadow-lg pointer-events-none ${
- size === "sm" ? "w-3 h-3" : "w-4 h-4"
- }`}
- style={{
- left: position.left,
- top: position.top,
- transform: "translate(-50%, -50%)",
- backgroundColor: color,
- }}
- />
+ <div
+ className={`pointer-events-none absolute rounded-full border-3 border-white shadow-lg ${
+ size === "sm" ? "size-3" : "size-4"
+ }`}
+ style={{
+ left: position.left,
+ top: position.top,
+ transform: "translate(-50%, -50%)",
+ backgroundColor: color,
+ }}
+ />
);
export { ColorPicker };
diff --git a/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx
index 887c1ab7..0f77a4ce 100644
--- a/apps/web/src/components/ui/command.tsx
+++ b/apps/web/src/components/ui/command.tsx
@@ -1,153 +1,152 @@
"use client";
import * as React from "react";
-import { DialogProps } from "@radix-ui/react-dialog";
+import type { DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
-
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
import { Dialog, DialogContent } from "./dialog";
const Command = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive>
+ React.ElementRef<typeof CommandPrimitive>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
- <CommandPrimitive
- ref={ref}
- className={cn(
- "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
- className
- )}
- {...props}
- />
+ <CommandPrimitive
+ ref={ref}
+ className={cn(
+ "bg-popover text-popover-foreground flex size-full flex-col overflow-hidden rounded-md",
+ className,
+ )}
+ {...props}
+ />
));
Command.displayName = CommandPrimitive.displayName;
const CommandDialog = ({ children, ...props }: DialogProps) => {
- return (
- <Dialog {...props}>
- <DialogContent className="overflow-hidden p-0">
- <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
- {children}
- </Command>
- </DialogContent>
- </Dialog>
- );
+ return (
+ <Dialog {...props}>
+ <DialogContent className="overflow-hidden p-0">
+ <Command className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:size-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:size-5">
+ {children}
+ </Command>
+ </DialogContent>
+ </Dialog>
+ );
};
const CommandInput = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.Input>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
+ React.ElementRef<typeof CommandPrimitive.Input>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
- <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
- <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
- <CommandPrimitive.Input
- ref={ref}
- className={cn(
- "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
- className
- )}
- {...props}
- />
- </div>
+ <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
+ <Search className="mr-2 size-4 shrink-0 opacity-50" />
+ <CommandPrimitive.Input
+ ref={ref}
+ className={cn(
+ "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
+ className,
+ )}
+ {...props}
+ />
+ </div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.List>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
+ React.ElementRef<typeof CommandPrimitive.List>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
- <CommandPrimitive.List
- ref={ref}
- className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
- {...props}
- />
+ <CommandPrimitive.List
+ ref={ref}
+ className={cn("max-h-[300px] overflow-x-hidden overflow-y-auto", className)}
+ {...props}
+ />
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.Empty>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
+ React.ElementRef<typeof CommandPrimitive.Empty>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
- <CommandPrimitive.Empty
- ref={ref}
- className="py-6 text-center text-sm"
- {...props}
- />
+ <CommandPrimitive.Empty
+ ref={ref}
+ className="py-6 text-center text-sm"
+ {...props}
+ />
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.Group>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
+ React.ElementRef<typeof CommandPrimitive.Group>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
- <CommandPrimitive.Group
- ref={ref}
- className={cn(
- "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
- className
- )}
- {...props}
- />
+ <CommandPrimitive.Group
+ ref={ref}
+ className={cn(
+ "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
+ className,
+ )}
+ {...props}
+ />
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.Separator>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
+ React.ElementRef<typeof CommandPrimitive.Separator>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
- <CommandPrimitive.Separator
- ref={ref}
- className={cn("-mx-1 h-px bg-border", className)}
- {...props}
- />
+ <CommandPrimitive.Separator
+ ref={ref}
+ className={cn("bg-border -mx-1 h-px", className)}
+ {...props}
+ />
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
+ React.ElementRef<typeof CommandPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
- <CommandPrimitive.Item
- ref={ref}
- className={cn(
- "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
- className
- )}
- {...props}
- />
+ <CommandPrimitive.Item
+ ref={ref}
+ className={cn(
+ "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ className,
+ )}
+ {...props}
+ />
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
- return (
- <span
- className={cn(
- "ml-auto text-xs tracking-widest text-muted-foreground",
- className
- )}
- {...props}
- />
- );
+ return (
+ <span
+ className={cn(
+ "text-muted-foreground ml-auto text-xs tracking-widest",
+ className,
+ )}
+ {...props}
+ />
+ );
};
CommandShortcut.displayName = "CommandShortcut";
export {
- Command,
- CommandDialog,
- CommandInput,
- CommandList,
- CommandEmpty,
- CommandGroup,
- CommandItem,
- CommandShortcut,
- CommandSeparator,
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
};
diff --git a/apps/web/src/components/ui/context-menu.tsx b/apps/web/src/components/ui/context-menu.tsx
index a1a28fa6..4af92764 100644
--- a/apps/web/src/components/ui/context-menu.tsx
+++ b/apps/web/src/components/ui/context-menu.tsx
@@ -2,10 +2,10 @@
import * as React from "react";
import { ContextMenu as ContextMenuPrimitive } from "radix-ui";
-import { Check, ChevronRight, Circle } from "lucide-react";
import { cva, type VariantProps } from "class-variance-authority";
-
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { Tick02Icon, ArrowRightIcon, CircleIcon } from "@hugeicons/core-free-icons"
const ContextMenu = ContextMenuPrimitive.Root;
@@ -20,196 +20,215 @@ const ContextMenuSub = ContextMenuPrimitive.Sub;
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
const contextMenuItemVariants = cva(
- "relative flex cursor-pointer select-none items-center gap-2 px-2 py-1.5 text-sm outline-hidden transition-opacity data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
- {
- variants: {
- variant: {
- default: "focus:opacity-65 focus:text-accent-foreground",
- destructive: "text-destructive focus:text-destructive/80",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
+ "relative flex cursor-pointer select-none items-center gap-2.5 px-4 py-1.5 last:pb-1 text-base outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "focus:bg-accent/35 focus:text-accent-foreground [&_svg]:text-muted-foreground",
+ destructive: "text-destructive focus:bg-destructive/5 focus:text-destructive [&_svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
);
const ContextMenuSubTrigger = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
- inset?: boolean;
- variant?: VariantProps<typeof contextMenuItemVariants>["variant"];
- }
->(({ className, inset, children, variant = "default", ...props }, ref) => (
- <ContextMenuPrimitive.SubTrigger
- ref={ref}
- className={cn(
- contextMenuItemVariants({ variant }),
- "data-[state=open]:bg-accent data-[state=open]:opacity-65",
- inset && "pl-8",
- className
- )}
- {...props}
- >
- {children}
- <ChevronRight className="ml-auto" />
- </ContextMenuPrimitive.SubTrigger>
+ React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
+ inset?: boolean;
+ variant?: VariantProps<typeof contextMenuItemVariants>["variant"];
+ icon?: React.ReactNode;
+ }
+>(({ className, inset, children, variant = "default", icon, ...props }, ref) => (
+ <ContextMenuPrimitive.SubTrigger
+ ref={ref}
+ className={cn(
+ contextMenuItemVariants({ variant }),
+ "data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
+ inset && "pl-8",
+ className,
+ )}
+ {...props}
+ >
+ {icon && <span className="size-4 shrink-0 text-muted-foreground">{icon}</span>}
+ {children}
+ <HugeiconsIcon icon={ArrowRightIcon} className="ml-auto text-muted-foreground/80" />
+ </ContextMenuPrimitive.SubTrigger>
));
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
const ContextMenuSubContent = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
+ React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
- <ContextMenuPrimitive.SubContent
- ref={ref}
- className={cn(
- "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
- className
- )}
- {...props}
- />
+ <ContextMenuPrimitive.SubContent
+ ref={ref}
+ className={cn(
+ "bg-popover text-popover-foreground z-50 min-w-48 overflow-hidden rounded-lg border shadow-xl py-2.5",
+ className,
+ )}
+ {...props}
+ />
));
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
const ContextMenuContent = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
+ React.ElementRef<typeof ContextMenuPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
- <ContextMenuPrimitive.Portal>
- <ContextMenuPrimitive.Content
- ref={ref}
- className={cn(
- "z-50 min-w-32 overflow-hidden rounded-md border p-1 bg-popover text-popover-foreground shadow-md",
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
- className
- )}
- {...props}
- />
- </ContextMenuPrimitive.Portal>
+ <ContextMenuPrimitive.Portal>
+ <ContextMenuPrimitive.Content
+ ref={ref}
+ className={cn(
+ "bg-popover text-popover-foreground z-50 min-w-48 overflow-hidden rounded-lg border shadow-xl py-2.5",
+ className,
+ )}
+ {...props}
+ />
+ </ContextMenuPrimitive.Portal>
));
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
const ContextMenuItem = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
- inset?: boolean;
- variant?: VariantProps<typeof contextMenuItemVariants>["variant"];
- }
->(({ className, inset, variant = "default", ...props }, ref) => (
- <ContextMenuPrimitive.Item
- ref={ref}
- className={cn(
- contextMenuItemVariants({ variant }),
- inset && "pl-8",
- className
- )}
- {...props}
- />
+ React.ElementRef<typeof ContextMenuPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
+ inset?: boolean;
+ variant?: VariantProps<typeof contextMenuItemVariants>["variant"];
+ icon?: React.ReactNode;
+ textRight?: string;
+ }
+>(({ className, inset, variant = "default", icon, children, textRight, ...props }, ref) => (
+ <ContextMenuPrimitive.Item
+ ref={ref}
+ className={cn(
+ contextMenuItemVariants({ variant }),
+ inset && "pl-8",
+ className,
+ )}
+ {...props}
+ >
+ {icon && <span className="[&_svg]:size-4 [&_svg]:shrink-0">{icon}</span>}
+ {children}
+ {textRight && (
+ <span className="ml-auto text-[0.60rem] tracking-widest text-muted-foreground/80 mb-0.5">
+ {textRight}
+ </span>
+ )}
+ </ContextMenuPrimitive.Item>
));
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
const ContextMenuCheckboxItem = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem> & {
- variant?: VariantProps<typeof contextMenuItemVariants>["variant"];
- }
->(({ className, children, checked, variant = "default", ...props }, ref) => (
- <ContextMenuPrimitive.CheckboxItem
- ref={ref}
- className={cn(contextMenuItemVariants({ variant }), "pl-8 pr-2", className)}
- checked={checked}
- {...props}
- >
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
- <ContextMenuPrimitive.ItemIndicator>
- <Check className="h-4 w-4" />
- </ContextMenuPrimitive.ItemIndicator>
- </span>
- {children}
- </ContextMenuPrimitive.CheckboxItem>
+ React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem> & {
+ variant?: VariantProps<typeof contextMenuItemVariants>["variant"];
+ icon?: React.ReactNode;
+ }
+>(({ className, children, checked, variant = "default", icon, ...props }, ref) => (
+ <ContextMenuPrimitive.CheckboxItem
+ ref={ref}
+ className={cn(contextMenuItemVariants({ variant }), "pr-2 pl-8", className)}
+ checked={checked}
+ {...props}
+ >
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
+ <ContextMenuPrimitive.ItemIndicator>
+ <HugeiconsIcon icon={Tick02Icon} className="size-4" />
+ </ContextMenuPrimitive.ItemIndicator>
+ </span>
+ {icon && <span className="size-4 shrink-0 text-muted-foreground">{icon}</span>}
+ {children}
+ </ContextMenuPrimitive.CheckboxItem>
));
ContextMenuCheckboxItem.displayName =
- ContextMenuPrimitive.CheckboxItem.displayName;
+ ContextMenuPrimitive.CheckboxItem.displayName;
const ContextMenuRadioItem = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem> & {
- variant?: VariantProps<typeof contextMenuItemVariants>["variant"];
- }
->(({ className, children, variant = "default", ...props }, ref) => (
- <ContextMenuPrimitive.RadioItem
- ref={ref}
- className={cn(contextMenuItemVariants({ variant }), "pl-8 pr-2", className)}
- {...props}
- >
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
- <ContextMenuPrimitive.ItemIndicator>
- <Circle className="h-2 w-2 fill-current" />
- </ContextMenuPrimitive.ItemIndicator>
- </span>
- {children}
- </ContextMenuPrimitive.RadioItem>
+ React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem> & {
+ variant?: VariantProps<typeof contextMenuItemVariants>["variant"];
+ icon?: React.ReactNode;
+ }
+>(({ className, children, variant = "default", icon, ...props }, ref) => (
+ <ContextMenuPrimitive.RadioItem
+ ref={ref}
+ className={cn(contextMenuItemVariants({ variant }), "pr-2 pl-8", className)}
+ {...props}
+ >
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
+ <ContextMenuPrimitive.ItemIndicator>
+ <HugeiconsIcon icon={CircleIcon} className="size-2 fill-current" />
+ </ContextMenuPrimitive.ItemIndicator>
+ </span>
+ {icon && <span className="size-4 shrink-0 text-muted-foreground">{icon}</span>}
+ {children}
+ </ContextMenuPrimitive.RadioItem>
));
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
const ContextMenuLabel = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.Label>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
- inset?: boolean;
- }
->(({ className, inset, ...props }, ref) => (
- <ContextMenuPrimitive.Label
- ref={ref}
- className={cn(
- "px-2 py-1.5 text-sm font-semibold",
- inset && "pl-8",
- className
- )}
- {...props}
- />
+ React.ElementRef<typeof ContextMenuPrimitive.Label>,
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
+ inset?: boolean;
+ icon?: React.ReactNode;
+ }
+>(({ className, inset, icon, children, ...props }, ref) => (
+ <ContextMenuPrimitive.Label
+ ref={ref}
+ className={cn(
+ "flex items-center gap-2.5 px-4 py-1.5 text-sm font-semibold text-foreground",
+ inset && "pl-8",
+ className,
+ )}
+ {...props}
+ >
+ {icon && <span className="size-4 shrink-0 text-muted-foreground">{icon}</span>}
+ {children}
+ </ContextMenuPrimitive.Label>
));
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
const ContextMenuSeparator = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.Separator>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
+ React.ElementRef<typeof ContextMenuPrimitive.Separator>,
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
- <ContextMenuPrimitive.Separator
- ref={ref}
- className={cn("-mx-1 my-1 h-px bg-foreground/10", className)}
- {...props}
- />
+ <ContextMenuPrimitive.Separator
+ ref={ref}
+ className={cn("bg-border my-2 h-px", className)}
+ {...props}
+ />
));
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
const ContextMenuShortcut = ({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
- return (
- <span
- className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
- {...props}
- />
- );
+ return (
+ <span
+ className={cn("ml-auto text-xs tracking-widest text-muted-foreground opacity-60", className)}
+ {...props}
+ />
+ );
};
ContextMenuShortcut.displayName = "ContextMenuShortcut";
export {
- ContextMenu,
- ContextMenuTrigger,
- ContextMenuContent,
- ContextMenuItem,
- ContextMenuCheckboxItem,
- ContextMenuRadioItem,
- ContextMenuLabel,
- ContextMenuSeparator,
- ContextMenuShortcut,
- ContextMenuGroup,
- ContextMenuPortal,
- ContextMenuSub,
- ContextMenuSubContent,
- ContextMenuSubTrigger,
- ContextMenuRadioGroup,
+ ContextMenu,
+ ContextMenuTrigger,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuCheckboxItem,
+ ContextMenuRadioItem,
+ ContextMenuLabel,
+ ContextMenuSeparator,
+ ContextMenuShortcut,
+ ContextMenuGroup,
+ ContextMenuPortal,
+ ContextMenuSub,
+ ContextMenuSubContent,
+ ContextMenuSubTrigger,
+ ContextMenuRadioGroup,
};
diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx
index 42d0f80d..3577f796 100644
--- a/apps/web/src/components/ui/dialog.tsx
+++ b/apps/web/src/components/ui/dialog.tsx
@@ -3,9 +3,7 @@
import * as React from "react";
import { Dialog as DialogPrimitive } from "radix-ui";
import { X } from "lucide-react";
-
-import { cn } from "../../lib/utils";
-import { ScrollArea } from "./scroll-area";
+import { cn } from "@/utils/ui";
const Dialog = DialogPrimitive.Root;
@@ -16,112 +14,118 @@ const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Overlay>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
- <DialogPrimitive.Overlay
- ref={ref}
- className={cn(
- "fixed inset-0 z-250 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
- className
- )}
- {...props}
- />
+ <DialogPrimitive.Overlay
+ ref={ref}
+ className={cn(
+ "fixed inset-0 z-250 backdrop-blur-sm bg-black/10",
+ className,
+ )}
+ {...props}
+ />
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
+ React.ElementRef<typeof DialogPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
- <DialogPortal>
- <DialogOverlay />
- <DialogPrimitive.Content
- ref={ref}
- className={cn(
- "fixed left-[50%] top-[50%] p-6 z-250 grid w-[calc(100%-2rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-popover shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 rounded-lg",
- className
- )}
- onCloseAutoFocus={(e) => {
- e.stopPropagation();
- e.preventDefault();
- }}
- {...props}
- >
- {children}
- <DialogPrimitive.Close className="absolute right-4 top-4 opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground cursor-pointer">
- <X className="h-4 w-4" />
- <span className="sr-only">Close</span>
- </DialogPrimitive.Close>
- </DialogPrimitive.Content>
- </DialogPortal>
+ <DialogPortal>
+ <DialogOverlay />
+ <DialogPrimitive.Content
+ ref={ref}
+ className={cn(
+ "bg-popover fixed top-[50%] left-[50%] z-250 grid w-[calc(100%-2rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200",
+ className,
+ )}
+ onCloseAutoFocus={(e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ {...props}
+ >
+ {children}
+ <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-6 right-6 cursor-pointer opacity-70 hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
+ <X className="size-5 text-muted-foreground" />
+ <span className="sr-only">Close</span>
+ </DialogPrimitive.Close>
+ </DialogPrimitive.Content>
+ </DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
- <div
- className={cn(
- "flex flex-col space-y-2 text-center sm:text-left",
- className
- )}
- {...props}
- />
+ <div
+ className={cn("flex flex-col space-y-2 text-left border-b p-6", className)}
+ {...props}
+ />
);
DialogHeader.displayName = "DialogHeader";
+const DialogBody = ({
+ className,
+ ...props
+}: React.HTMLAttributes<HTMLDivElement>) => (
+ <div className={cn("p-6 flex flex-col gap-6", className)} {...props} />
+);
+DialogBody.displayName = "DialogBody";
+
const DialogFooter = ({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
- <div
- className={cn(
- "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
- className
- )}
- {...props}
- />
+ <div
+ className={cn(
+ "flex gap-3 flex-col-reverse sm:flex-row sm:justify-end p-6 py-5 border-t",
+ className,
+ )}
+ {...props}
+ />
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Title>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
+ React.ElementRef<typeof DialogPrimitive.Title>,
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
- <DialogPrimitive.Title
- ref={ref}
- className={cn(
- "text-lg font-semibold leading-none tracking-tight",
- className
- )}
- {...props}
- />
+ <DialogPrimitive.Title
+ ref={ref}
+ className={cn(
+ "text-lg leading-none font-semibold tracking-tight",
+ className,
+ )}
+ {...props}
+ />
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Description>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
+ React.ElementRef<typeof DialogPrimitive.Description>,
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
- <DialogPrimitive.Description
- ref={ref}
- className={cn("text-sm text-muted-foreground", className)}
- {...props}
- />
+ <DialogPrimitive.Description
+ ref={ref}
+ className={cn("text-muted-foreground text-sm", className)}
+ {...props}
+ />
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
- Dialog,
- DialogPortal,
- DialogOverlay,
- DialogTrigger,
- DialogClose,
- DialogContent,
- DialogHeader,
- DialogFooter,
- DialogTitle,
- DialogDescription,
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogTrigger,
+ DialogClose,
+ DialogContent,
+ DialogHeader,
+ DialogBody,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
};
diff --git a/apps/web/src/components/ui/draggable-item.tsx b/apps/web/src/components/ui/draggable-item.tsx
deleted file mode 100644
index 9f5c7887..00000000
--- a/apps/web/src/components/ui/draggable-item.tsx
+++ /dev/null
@@ -1,247 +0,0 @@
-"use client";
-
-import { AspectRatio } from "@/components/ui/aspect-ratio";
-import { Button } from "@/components/ui/button";
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
-import { ReactNode, useState, useRef, useEffect } from "react";
-import { createPortal } from "react-dom";
-import { Plus } from "lucide-react";
-import { cn } from "@/lib/utils";
-import { usePlaybackStore } from "@/stores/playback-store";
-import { DragData } from "@/types/timeline";
-
-export interface DraggableMediaItemProps {
- name: string;
- preview: ReactNode;
- dragData: DragData;
- onDragStart?: (e: React.DragEvent) => void;
- onAddToTimeline?: (currentTime: number) => void;
- aspectRatio?: number;
- className?: string;
- containerClassName?: string;
- showPlusOnDrag?: boolean;
- showLabel?: boolean;
- rounded?: boolean;
- variant?: "card" | "compact";
- isDraggable?: boolean;
- isHighlighted?: boolean;
-}
-
-export function DraggableMediaItem({
- name,
- preview,
- dragData,
- onDragStart,
- onAddToTimeline,
- aspectRatio = 16 / 9,
- className = "",
- containerClassName,
- showPlusOnDrag = true,
- showLabel = true,
- rounded = true,
- variant = "card",
- isDraggable = true,
- isHighlighted = false,
-}: DraggableMediaItemProps) {
- const [isDragging, setIsDragging] = useState(false);
- const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 });
- const dragRef = useRef<HTMLDivElement>(null);
- const currentTime = isDraggable
- ? usePlaybackStore((state) => state.currentTime)
- : 0;
- const highlightClassName = "ring-2 ring-primary rounded-sm bg-primary/10";
-
- const handleAddToTimeline = () => {
- onAddToTimeline?.(currentTime);
- };
-
- const emptyImg = new window.Image();
- emptyImg.src =
- "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=";
-
- useEffect(() => {
- if (!isDragging) return;
-
- const handleDragOver = (e: DragEvent) => {
- setDragPosition({ x: e.clientX, y: e.clientY });
- };
-
- document.addEventListener("dragover", handleDragOver);
-
- return () => {
- document.removeEventListener("dragover", handleDragOver);
- };
- }, [isDragging]);
-
- const handleDragStart = (e: React.DragEvent) => {
- e.dataTransfer.setDragImage(emptyImg, 0, 0);
-
- // Set drag data
- e.dataTransfer.setData(
- "application/x-media-item",
- JSON.stringify(dragData)
- );
- e.dataTransfer.effectAllowed = "copy";
-
- // Set initial position and show custom drag preview
- setDragPosition({ x: e.clientX, y: e.clientY });
- setIsDragging(true);
-
- onDragStart?.(e);
- };
-
- const handleDragEnd = () => {
- setIsDragging(false);
- };
-
- return (
- <>
- {variant === "card" ? (
- <div
- ref={dragRef}
- className={cn("relative group", containerClassName ?? "w-28 h-28")}
- >
- <div
- className={cn(
- "flex flex-col gap-1 p-1 h-auto w-full relative cursor-default",
- className,
- isHighlighted && highlightClassName
- )}
- >
- <AspectRatio
- ratio={aspectRatio}
- className={cn(
- "bg-panel-accent relative overflow-hidden",
- rounded && "rounded-md",
- isDraggable && "[&::-webkit-drag-ghost]:opacity-0" // Webkit-specific ghost hiding
- )}
- draggable={isDraggable}
- onDragStart={isDraggable ? handleDragStart : undefined}
- onDragEnd={isDraggable ? handleDragEnd : undefined}
- >
- {preview}
- {!isDragging && (
- <PlusButton
- className="opacity-0 group-hover:opacity-100"
- onClick={handleAddToTimeline}
- />
- )}
- </AspectRatio>
- {showLabel && (
- <span
- className="text-[0.7rem] text-muted-foreground truncate w-full text-left"
- aria-label={name}
- title={name}
- >
- {name.length > 8
- ? `${name.slice(0, 16)}...${name.slice(-3)}`
- : name}
- </span>
- )}
- </div>
- </div>
- ) : (
- <div
- ref={dragRef}
- className={cn(
- "relative group w-full",
- isHighlighted && highlightClassName
- )}
- >
- <div
- className={cn(
- "h-8 flex items-center gap-3 cursor-default w-full px-1",
- isDraggable && "[&::-webkit-drag-ghost]:opacity-0",
- className
- )}
- draggable={isDraggable}
- onDragStart={isDraggable ? handleDragStart : undefined}
- onDragEnd={isDraggable ? handleDragEnd : undefined}
- >
- <div className="w-6 h-6 flex-shrink-0 rounded-[0.35rem] overflow-hidden">
- {preview}
- </div>
- <span className="text-sm truncate flex-1 w-full">{name}</span>
- </div>
- </div>
- )}
-
- {/* Custom drag preview */}
- {isDraggable &&
- isDragging &&
- typeof document !== "undefined" &&
- createPortal(
- <div
- className="fixed pointer-events-none z-9999"
- style={{
- left: dragPosition.x - 40, // Center the preview (half of 80px)
- top: dragPosition.y - 40, // Center the preview (half of 80px)
- }}
- >
- <div className="w-[80px]">
- <AspectRatio
- ratio={1}
- className="relative rounded-md overflow-hidden shadow-2xl ring-3 ring-primary"
- >
- <div className="w-full h-full [&_img]:w-full [&_img]:h-full [&_img]:object-cover [&_img]:rounded-none">
- {preview}
- </div>
- {showPlusOnDrag && (
- <PlusButton
- onClick={handleAddToTimeline}
- tooltipText="Add to timeline or drag to position"
- />
- )}
- </AspectRatio>
- </div>
- </div>,
- document.body
- )}
- </>
- );
-}
-
-function PlusButton({
- className,
- onClick,
- tooltipText,
-}: {
- className?: string;
- onClick?: () => void;
- tooltipText?: string;
-}) {
- const button = (
- <Button
- size="icon"
- className={cn(
- "absolute bottom-2 right-2 size-5 bg-background hover:bg-panel text-foreground",
- className
- )}
- onClick={(e) => {
- e.preventDefault();
- e.stopPropagation();
- onClick?.();
- }}
- title={tooltipText}
- >
- <Plus className="size-4!" />
- </Button>
- );
-
- if (tooltipText) {
- return (
- <Tooltip>
- <TooltipTrigger asChild>{button}</TooltipTrigger>
- <TooltipContent>
- <p>{tooltipText}</p>
- </TooltipContent>
- </Tooltip>
- );
- }
-
- return button;
-}
diff --git a/apps/web/src/components/ui/drawer.tsx b/apps/web/src/components/ui/drawer.tsx
index 7e065e5c..d5752d62 100644
--- a/apps/web/src/components/ui/drawer.tsx
+++ b/apps/web/src/components/ui/drawer.tsx
@@ -3,16 +3,16 @@
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const Drawer = ({
- shouldScaleBackground = true,
- ...props
+ shouldScaleBackground = true,
+ ...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
- <DrawerPrimitive.Root
- shouldScaleBackground={shouldScaleBackground}
- {...props}
- />
+ <DrawerPrimitive.Root
+ shouldScaleBackground={shouldScaleBackground}
+ {...props}
+ />
);
Drawer.displayName = "Drawer";
@@ -23,96 +23,96 @@ const DrawerPortal = DrawerPrimitive.Portal;
const DrawerClose = DrawerPrimitive.Close;
const DrawerOverlay = React.forwardRef<
- React.ElementRef<typeof DrawerPrimitive.Overlay>,
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
+ React.ElementRef<typeof DrawerPrimitive.Overlay>,
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
- <DrawerPrimitive.Overlay
- ref={ref}
- className={cn("fixed inset-0 z-50 bg-black/80", className)}
- {...props}
- />
+ <DrawerPrimitive.Overlay
+ ref={ref}
+ className={cn("fixed inset-0 z-50 bg-black/80", className)}
+ {...props}
+ />
));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef<
- React.ElementRef<typeof DrawerPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
+ React.ElementRef<typeof DrawerPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
- <DrawerPortal>
- <DrawerOverlay />
- <DrawerPrimitive.Content
- ref={ref}
- className={cn(
- "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
- className
- )}
- {...props}
- >
- <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
- {children}
- </DrawerPrimitive.Content>
- </DrawerPortal>
+ <DrawerPortal>
+ <DrawerOverlay />
+ <DrawerPrimitive.Content
+ ref={ref}
+ className={cn(
+ "bg-background fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border",
+ className,
+ )}
+ {...props}
+ >
+ <div className="bg-muted mx-auto mt-4 h-2 w-[100px] rounded-full" />
+ {children}
+ </DrawerPrimitive.Content>
+ </DrawerPortal>
));
DrawerContent.displayName = "DrawerContent";
const DrawerHeader = ({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
- <div
- className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
- {...props}
- />
+ <div
+ className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
+ {...props}
+ />
);
DrawerHeader.displayName = "DrawerHeader";
const DrawerFooter = ({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
- <div
- className={cn("mt-auto flex flex-col gap-2 p-4", className)}
- {...props}
- />
+ <div
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
+ {...props}
+ />
);
DrawerFooter.displayName = "DrawerFooter";
const DrawerTitle = React.forwardRef<
- React.ElementRef<typeof DrawerPrimitive.Title>,
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
+ React.ElementRef<typeof DrawerPrimitive.Title>,
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
- <DrawerPrimitive.Title
- ref={ref}
- className={cn(
- "text-lg font-semibold leading-none tracking-tight",
- className
- )}
- {...props}
- />
+ <DrawerPrimitive.Title
+ ref={ref}
+ className={cn(
+ "text-lg leading-none font-semibold tracking-tight",
+ className,
+ )}
+ {...props}
+ />
));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef<
- React.ElementRef<typeof DrawerPrimitive.Description>,
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
+ React.ElementRef<typeof DrawerPrimitive.Description>,
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
- <DrawerPrimitive.Description
- ref={ref}
- className={cn("text-sm text-muted-foreground", className)}
- {...props}
- />
+ <DrawerPrimitive.Description
+ ref={ref}
+ className={cn("text-muted-foreground text-sm", className)}
+ {...props}
+ />
));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export {
- Drawer,
- DrawerPortal,
- DrawerOverlay,
- DrawerTrigger,
- DrawerClose,
- DrawerContent,
- DrawerHeader,
- DrawerFooter,
- DrawerTitle,
- DrawerDescription,
+ Drawer,
+ DrawerPortal,
+ DrawerOverlay,
+ DrawerTrigger,
+ DrawerClose,
+ DrawerContent,
+ DrawerHeader,
+ DrawerFooter,
+ DrawerTitle,
+ DrawerDescription,
};
diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx
index 823d48a3..a7736e6e 100644
--- a/apps/web/src/components/ui/dropdown-menu.tsx
+++ b/apps/web/src/components/ui/dropdown-menu.tsx
@@ -5,7 +5,7 @@ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const DropdownMenu = DropdownMenuPrimitive.Root;
@@ -20,215 +20,215 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const dropdownMenuItemVariants = cva(
- "relative flex cursor-pointer select-none items-center gap-2 px-2 py-1.5 text-sm outline-hidden transition-opacity data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
- {
- variants: {
- variant: {
- default: "focus:opacity-65 focus:text-accent-foreground",
- destructive: "text-destructive focus:text-destructive/80",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
+ "relative flex cursor-pointer select-none items-center gap-2 rounded-lg px-2.5 py-2 text-sm text-foreground/85 outline-hidden data-[highlighted]:bg-accent/35 data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "",
+ destructive:
+ "text-destructive data-[highlighted]:bg-destructive/5 data-[highlighted]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
);
const DropdownMenuSubTrigger = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
- inset?: boolean;
- variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"];
- }
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
+ inset?: boolean;
+ variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"];
+ }
>(({ className, inset, children, variant = "default", ...props }, ref) => (
- <DropdownMenuPrimitive.SubTrigger
- ref={ref}
- className={cn(
- dropdownMenuItemVariants({ variant }),
- "data-[state=open]:bg-accent data-[state=open]:opacity-65",
- inset && "pl-8",
- className
- )}
- {...props}
- >
- {children}
- <ChevronRight className="ml-auto" />
- </DropdownMenuPrimitive.SubTrigger>
+ <DropdownMenuPrimitive.SubTrigger
+ ref={ref}
+ className={cn(
+ dropdownMenuItemVariants({ variant }),
+ "data-[state=open]:bg-muted data-[state=open]:text-foreground",
+ inset && "pl-8",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+ <ChevronRight className="ml-auto" />
+ </DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
- DropdownMenuPrimitive.SubTrigger.displayName;
+ DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
- <DropdownMenuPrimitive.SubContent
- ref={ref}
- className={cn(
- "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
- className
- )}
- {...props}
- />
+ <DropdownMenuPrimitive.SubContent
+ ref={ref}
+ className={cn(
+ "bg-popover text-popover-foreground z-50 min-w-32 overflow-hidden rounded-2xl border p-2 shadow-lg",
+ className,
+ )}
+ {...props}
+ />
));
DropdownMenuSubContent.displayName =
- DropdownMenuPrimitive.SubContent.displayName;
+ DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
- <DropdownMenuPrimitive.Portal>
- <DropdownMenuPrimitive.Content
- ref={ref}
- sideOffset={sideOffset}
- onCloseAutoFocus={(e) => {
- e.stopPropagation();
- e.preventDefault();
- }}
- className={cn(
- "z-50 min-w-32 overflow-hidden rounded-md border p-1 bg-popover text-popover-foreground shadow-md",
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
- className
- )}
- {...props}
- />
- </DropdownMenuPrimitive.Portal>
+ <DropdownMenuPrimitive.Portal>
+ <DropdownMenuPrimitive.Content
+ ref={ref}
+ sideOffset={sideOffset}
+ onCloseAutoFocus={(e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ className={cn(
+ "bg-popover text-popover-foreground z-50 min-w-32 overflow-hidden rounded-lg border p-2 shadow-lg",
+ className,
+ )}
+ {...props}
+ />
+ </DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
- inset?: boolean;
- variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"];
- }
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
+ inset?: boolean;
+ variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"];
+ }
>(({ className, inset, variant = "default", ...props }, ref) => (
- <DropdownMenuPrimitive.Item
- ref={ref}
- className={cn(
- dropdownMenuItemVariants({ variant }),
- inset && "pl-8",
- className
- )}
- {...props}
- />
+ <DropdownMenuPrimitive.Item
+ ref={ref}
+ className={cn(
+ dropdownMenuItemVariants({ variant }),
+ inset && "pl-8",
+ className,
+ )}
+ {...props}
+ />
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> & {
- variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"];
- }
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> & {
+ variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"];
+ }
>(({ className, children, checked, variant = "default", ...props }, ref) => (
- <DropdownMenuPrimitive.CheckboxItem
- ref={ref}
- className={cn(
- dropdownMenuItemVariants({ variant }),
- "pl-2 pr-8",
- className
- )}
- checked={checked}
- onSelect={(e) => {
- e.preventDefault();
- }}
- {...props}
- >
- {children}
- <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
- <DropdownMenuPrimitive.ItemIndicator>
- <Check className="h-4 w-4" />
- </DropdownMenuPrimitive.ItemIndicator>
- </span>
- </DropdownMenuPrimitive.CheckboxItem>
+ <DropdownMenuPrimitive.CheckboxItem
+ ref={ref}
+ className={cn(
+ dropdownMenuItemVariants({ variant }),
+ "pr-8 pl-2",
+ className,
+ )}
+ checked={checked}
+ onSelect={(e) => {
+ e.preventDefault();
+ }}
+ {...props}
+ >
+ {children}
+ <span className="absolute right-2 flex size-3.5 items-center justify-center">
+ <DropdownMenuPrimitive.ItemIndicator>
+ <Check className="size-4" />
+ </DropdownMenuPrimitive.ItemIndicator>
+ </span>
+ </DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
- DropdownMenuPrimitive.CheckboxItem.displayName;
+ DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> & {
- variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"];
- }
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> & {
+ variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"];
+ }
>(({ className, children, variant = "default", ...props }, ref) => (
- <DropdownMenuPrimitive.RadioItem
- ref={ref}
- className={cn(
- dropdownMenuItemVariants({ variant }),
- "pl-8 pr-2",
- className
- )}
- {...props}
- >
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
- <DropdownMenuPrimitive.ItemIndicator>
- <Circle className="h-2 w-2 fill-current" />
- </DropdownMenuPrimitive.ItemIndicator>
- </span>
- {children}
- </DropdownMenuPrimitive.RadioItem>
+ <DropdownMenuPrimitive.RadioItem
+ ref={ref}
+ className={cn(
+ dropdownMenuItemVariants({ variant }),
+ "pr-2 pl-8",
+ className,
+ )}
+ {...props}
+ >
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
+ <DropdownMenuPrimitive.ItemIndicator>
+ <Circle className="size-2 fill-current" />
+ </DropdownMenuPrimitive.ItemIndicator>
+ </span>
+ {children}
+ </DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Label>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
- inset?: boolean;
- }
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
+ inset?: boolean;
+ }
>(({ className, inset, ...props }, ref) => (
- <DropdownMenuPrimitive.Label
- ref={ref}
- className={cn(
- "px-2 py-1.5 text-sm font-semibold",
- inset && "pl-8",
- className
- )}
- {...props}
- />
+ <DropdownMenuPrimitive.Label
+ ref={ref}
+ className={cn(
+ "px-3 pb-2 pt-1 text-[11px] font-bold uppercase tracking-wider text-muted-foreground",
+ inset && "pl-8",
+ className,
+ )}
+ {...props}
+ />
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
- <DropdownMenuPrimitive.Separator
- ref={ref}
- className={cn("-mx-1 my-1 h-px bg-foreground/10", className)}
- {...props}
- />
+ <DropdownMenuPrimitive.Separator
+ ref={ref}
+ className={cn("bg-border/60 mx-1 my-2 h-px", className)}
+ {...props}
+ />
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
- return (
- <span
- className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
- {...props}
- />
- );
+ return (
+ <span
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
+ {...props}
+ />
+ );
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
- DropdownMenu,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuCheckboxItem,
- DropdownMenuRadioItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuShortcut,
- DropdownMenuGroup,
- DropdownMenuPortal,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuRadioGroup,
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
};
diff --git a/apps/web/src/components/ui/editable-timecode.tsx b/apps/web/src/components/ui/editable-timecode.tsx
deleted file mode 100644
index 404831f6..00000000
--- a/apps/web/src/components/ui/editable-timecode.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-"use client";
-
-import { useState, useRef, useEffect } from "react";
-import { cn } from "@/lib/utils";
-import { formatTimeCode, parseTimeCode, TimeCode } from "@/lib/time";
-import { DEFAULT_FPS } from "@/stores/project-store";
-
-interface EditableTimecodeProps {
- time: number;
- duration?: number;
- format?: TimeCode;
- fps?: number;
- onTimeChange?: (time: number) => void;
- className?: string;
- disabled?: boolean;
-}
-
-export function EditableTimecode({
- time,
- duration,
- format = "HH:MM:SS:FF",
- fps = DEFAULT_FPS,
- onTimeChange,
- className,
- disabled = false,
-}: EditableTimecodeProps) {
- const [isEditing, setIsEditing] = useState(false);
- const [inputValue, setInputValue] = useState("");
- const [hasError, setHasError] = useState(false);
- const inputRef = useRef<HTMLInputElement>(null);
- const enterPressedRef = useRef(false);
-
- const formattedTime = formatTimeCode(time, format, fps);
-
- const startEditing = () => {
- if (disabled) return;
- setIsEditing(true);
- setInputValue(formattedTime);
- setHasError(false);
- enterPressedRef.current = false;
- };
-
- const cancelEditing = () => {
- setIsEditing(false);
- setInputValue("");
- setHasError(false);
- enterPressedRef.current = false;
- };
-
- const applyEdit = () => {
- const parsedTime = parseTimeCode(inputValue, format, fps);
-
- if (parsedTime === null) {
- setHasError(true);
- return;
- }
-
- // Clamp time to valid range
- const clampedTime = Math.max(
- 0,
- duration ? Math.min(duration, parsedTime) : parsedTime
- );
-
- onTimeChange?.(clampedTime);
- setIsEditing(false);
- setInputValue("");
- setHasError(false);
- enterPressedRef.current = false;
- };
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Enter") {
- e.preventDefault();
- enterPressedRef.current = true;
- applyEdit();
- } else if (e.key === "Escape") {
- e.preventDefault();
- cancelEditing();
- }
- };
-
- const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- setInputValue(e.target.value);
- setHasError(false);
- };
-
- const handleBlur = () => {
- // Only apply edit if Enter wasn't pressed (to avoid double processing)
- if (!enterPressedRef.current && isEditing) {
- applyEdit();
- }
- };
-
- // Focus input when entering edit mode
- useEffect(() => {
- if (isEditing && inputRef.current) {
- inputRef.current.focus();
- inputRef.current.select();
- }
- }, [isEditing]);
-
- if (isEditing) {
- return (
- <input
- ref={inputRef}
- type="text"
- value={inputValue}
- onChange={handleInputChange}
- onKeyDown={handleKeyDown}
- onBlur={handleBlur}
- className={cn(
- "text-xs font-mono bg-transparent border-none outline-none",
- "focus:bg-background focus:border focus:border-primary focus:px-1 focus:rounded",
- "tabular-nums text-primary",
- hasError && "text-destructive focus:border-destructive",
- className
- )}
- style={{ width: `${formattedTime.length + 1}ch` }}
- placeholder={formattedTime}
- />
- );
- }
-
- return (
- <span
- onClick={startEditing}
- className={cn(
- "text-xs font-mono tabular-nums text-primary cursor-pointer",
- "hover:bg-muted/50 hover:rounded px-1 -mx-1 transition-colors",
- disabled && "cursor-default hover:bg-transparent",
- className
- )}
- title={disabled ? undefined : "Click to edit time"}
- >
- {formattedTime}
- </span>
- );
-}
diff --git a/apps/web/src/components/ui/font-picker.tsx b/apps/web/src/components/ui/font-picker.tsx
index 7bb11274..1806b48a 100644
--- a/apps/web/src/components/ui/font-picker.tsx
+++ b/apps/web/src/components/ui/font-picker.tsx
@@ -1,40 +1,42 @@
import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
} from "@/components/ui/select";
-import { FONT_OPTIONS, FontFamily } from "@/constants/font-constants";
+import { FONT_OPTIONS, type FontFamily } from "@/constants/font-constants";
interface FontPickerProps {
- defaultValue?: FontFamily;
- onValueChange?: (value: FontFamily) => void;
- className?: string;
+ defaultValue?: FontFamily;
+ onValueChange?: (value: FontFamily) => void;
+ className?: string;
}
export function FontPicker({
- defaultValue,
- onValueChange,
- className,
+ defaultValue,
+ onValueChange,
+ className,
}: FontPickerProps) {
- return (
- <Select defaultValue={defaultValue} onValueChange={onValueChange}>
- <SelectTrigger className={`w-full bg-panel-accent h-8 text-xs ${className || ""}`}>
- <SelectValue placeholder="Select a font" />
- </SelectTrigger>
- <SelectContent>
- {FONT_OPTIONS.map((font) => (
- <SelectItem
- key={font.value}
- value={font.value}
- className="text-xs"
- style={{ fontFamily: font.value }}
- >
- {font.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- );
+ return (
+ <Select defaultValue={defaultValue} onValueChange={onValueChange}>
+ <SelectTrigger
+ className={`bg-panel-accent h-9 w-full text-base ${className || ""}`}
+ >
+ <SelectValue placeholder="Select a font" className="text-sm" />
+ </SelectTrigger>
+ <SelectContent>
+ {FONT_OPTIONS.map((font) => (
+ <SelectItem
+ key={font.value}
+ value={font.value}
+ className="text-sn"
+ style={{ fontFamily: font.value }}
+ >
+ {font.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ );
}
diff --git a/apps/web/src/components/ui/form.tsx b/apps/web/src/components/ui/form.tsx
index cab151e1..a6a18752 100644
--- a/apps/web/src/components/ui/form.tsx
+++ b/apps/web/src/components/ui/form.tsx
@@ -1,177 +1,177 @@
"use client";
import * as React from "react";
-import { Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui";
+import { type Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui";
import {
- Controller,
- ControllerProps,
- FieldPath,
- FieldValues,
- FormProvider,
- useFormContext,
+ Controller,
+ type ControllerProps,
+ type FieldPath,
+ type FieldValues,
+ FormProvider,
+ useFormContext,
} from "react-hook-form";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
import { Label } from "./label";
const Form = FormProvider;
type FormFieldContextValue<
- TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
- name: TName;
+ name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
- {} as FormFieldContextValue
+ {} as FormFieldContextValue,
);
const FormField = <
- TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
- ...props
+ ...props
}: ControllerProps<TFieldValues, TName>) => {
- return (
- <FormFieldContext.Provider value={{ name: props.name }}>
- <Controller {...props} />
- </FormFieldContext.Provider>
- );
+ return (
+ <FormFieldContext.Provider value={{ name: props.name }}>
+ <Controller {...props} />
+ </FormFieldContext.Provider>
+ );
};
const useFormField = () => {
- const fieldContext = React.useContext(FormFieldContext);
- const itemContext = React.useContext(FormItemContext);
- const { getFieldState, formState } = useFormContext();
+ const fieldContext = React.useContext(FormFieldContext);
+ const itemContext = React.useContext(FormItemContext);
+ const { getFieldState, formState } = useFormContext();
- const fieldState = getFieldState(fieldContext.name, formState);
+ const fieldState = getFieldState(fieldContext.name, formState);
- if (!fieldContext) {
- throw new Error("useFormField should be used within <FormField>");
- }
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within <FormField>");
+ }
- const { id } = itemContext;
+ const { id } = itemContext;
- return {
- id,
- name: fieldContext.name,
- formItemId: `${id}-form-item`,
- formDescriptionId: `${id}-form-item-description`,
- formMessageId: `${id}-form-item-message`,
- ...fieldState,
- };
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ };
};
type FormItemContextValue = {
- id: string;
+ id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
- {} as FormItemContextValue
+ {} as FormItemContextValue,
);
const FormItem = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
- const id = React.useId();
+ const id = React.useId();
- return (
- <FormItemContext.Provider value={{ id }}>
- <div ref={ref} className={cn("space-y-2", className)} {...props} />
- </FormItemContext.Provider>
- );
+ return (
+ <FormItemContext.Provider value={{ id }}>
+ <div ref={ref} className={cn("space-y-2", className)} {...props} />
+ </FormItemContext.Provider>
+ );
});
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
- React.ElementRef<typeof LabelPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
+ React.ElementRef<typeof LabelPrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
- const { error, formItemId } = useFormField();
+ const { error, formItemId } = useFormField();
- return (
- <Label
- ref={ref}
- className={cn(error && "text-destructive", className)}
- htmlFor={formItemId}
- {...props}
- />
- );
+ return (
+ <Label
+ ref={ref}
+ className={cn(error && "text-destructive", className)}
+ htmlFor={formItemId}
+ {...props}
+ />
+ );
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
- React.ElementRef<typeof SlotPrimitive.Slot>,
- React.ComponentPropsWithoutRef<typeof SlotPrimitive.Slot>
+ React.ElementRef<typeof SlotPrimitive.Slot>,
+ React.ComponentPropsWithoutRef<typeof SlotPrimitive.Slot>
>(({ ...props }, ref) => {
- const { error, formItemId, formDescriptionId, formMessageId } =
- useFormField();
+ const { error, formItemId, formDescriptionId, formMessageId } =
+ useFormField();
- return (
- <SlotPrimitive.Slot
- ref={ref}
- id={formItemId}
- aria-describedby={
- error ? `${formDescriptionId} ${formMessageId}` : `${formDescriptionId}`
- }
- aria-invalid={!!error}
- {...props}
- />
- );
+ return (
+ <SlotPrimitive.Slot
+ ref={ref}
+ id={formItemId}
+ aria-describedby={
+ error ? `${formDescriptionId} ${formMessageId}` : `${formDescriptionId}`
+ }
+ aria-invalid={!!error}
+ {...props}
+ />
+ );
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes<HTMLParagraphElement>
+ HTMLParagraphElement,
+ React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
- const { formDescriptionId } = useFormField();
+ const { formDescriptionId } = useFormField();
- return (
- <p
- ref={ref}
- id={formDescriptionId}
- className={cn("text-[0.8rem] text-muted-foreground", className)}
- {...props}
- />
- );
+ return (
+ <p
+ ref={ref}
+ id={formDescriptionId}
+ className={cn("text-muted-foreground text-[0.8rem]", className)}
+ {...props}
+ />
+ );
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes<HTMLParagraphElement>
+ HTMLParagraphElement,
+ React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
- const { error, formMessageId } = useFormField();
- const body = error ? String(error?.message) : children;
+ const { error, formMessageId } = useFormField();
+ const body = error ? String(error?.message) : children;
- if (!body) {
- return null;
- }
+ if (!body) {
+ return null;
+ }
- return (
- <p
- ref={ref}
- id={formMessageId}
- className={cn("text-[0.8rem] font-medium text-destructive", className)}
- {...props}
- >
- {body}
- </p>
- );
+ return (
+ <p
+ ref={ref}
+ id={formMessageId}
+ className={cn("text-destructive text-[0.8rem] font-medium", className)}
+ {...props}
+ >
+ {body}
+ </p>
+ );
});
FormMessage.displayName = "FormMessage";
export {
- useFormField,
- Form,
- FormItem,
- FormLabel,
- FormControl,
- FormDescription,
- FormMessage,
- FormField,
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
};
diff --git a/apps/web/src/components/ui/hover-card.tsx b/apps/web/src/components/ui/hover-card.tsx
index 92700e09..6f2c34e1 100644
--- a/apps/web/src/components/ui/hover-card.tsx
+++ b/apps/web/src/components/ui/hover-card.tsx
@@ -3,26 +3,26 @@
import * as React from "react";
import { HoverCard as HoverCardPrimitive } from "radix-ui";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const HoverCard = HoverCardPrimitive.Root;
const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardContent = React.forwardRef<
- React.ElementRef<typeof HoverCardPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
+ React.ElementRef<typeof HoverCardPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
- <HoverCardPrimitive.Content
- ref={ref}
- align={align}
- sideOffset={sideOffset}
- className={cn(
- "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
- className
- )}
- {...props}
- />
+ <HoverCardPrimitive.Content
+ ref={ref}
+ align={align}
+ sideOffset={sideOffset}
+ className={cn(
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 rounded-md border p-4 shadow-md outline-hidden",
+ className,
+ )}
+ {...props}
+ />
));
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
diff --git a/apps/web/src/components/ui/image-timeline-treatment.tsx b/apps/web/src/components/ui/image-timeline-treatment.tsx
deleted file mode 100644
index 7a64be18..00000000
--- a/apps/web/src/components/ui/image-timeline-treatment.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { cn } from "@/lib/utils";
-import type { BackgroundType } from "@/types/editor";
-
-interface ImageTimelineTreatmentProps {
- src: string;
- alt: string;
- targetAspectRatio?: number; // Default to 16:9 for video
- className?: string;
- backgroundType?: BackgroundType;
- backgroundColor?: string;
-}
-
-export function ImageTimelineTreatment({
- src,
- alt,
- targetAspectRatio = 16 / 9,
- className,
- backgroundType = "blur",
- backgroundColor = "#000000",
-}: ImageTimelineTreatmentProps) {
- const [imageLoaded, setImageLoaded] = useState(false);
- const [imageDimensions, setImageDimensions] = useState<{
- width: number;
- height: number;
- } | null>(null);
-
- const handleImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
- const img = e.currentTarget;
- setImageDimensions({
- width: img.naturalWidth,
- height: img.naturalHeight,
- });
- setImageLoaded(true);
- };
-
- const imageAspectRatio = imageDimensions
- ? imageDimensions.width / imageDimensions.height
- : 1;
-
- const needsAspectRatioTreatment = imageAspectRatio !== targetAspectRatio;
-
- return (
- <div
- className={cn("relative overflow-hidden", className)}
- style={{ aspectRatio: targetAspectRatio }}
- >
- {/* Background Layer */}
- {needsAspectRatioTreatment && imageLoaded && (
- <>
- {backgroundType === "blur" && (
- <div className="absolute inset-0">
- <img
- src={src}
- alt=""
- className="w-full h-full object-cover filter blur-xl scale-110 opacity-60"
- aria-hidden="true"
- />
- <div className="absolute inset-0 bg-black/20" />
- </div>
- )}
-
- {backgroundType === "mirror" && (
- <div className="absolute inset-0">
- <img
- src={src}
- alt=""
- className="w-full h-full object-cover opacity-30"
- aria-hidden="true"
- />
- </div>
- )}
-
- {backgroundType === "color" && (
- <div className="absolute inset-0" style={{ backgroundColor }} />
- )}
- </>
- )}
-
- {/* Main Image Layer */}
- <div className="absolute inset-0">
- <img
- src={src}
- alt={alt}
- className="w-full h-full object-cover"
- onLoad={handleImageLoad}
- />
- </div>
-
- {/* Loading state */}
- {!imageLoaded && (
- <div className="absolute inset-0 flex items-center justify-center bg-muted/30">
- <div className="animate-pulse text-xs text-muted-foreground">
- Loading...
- </div>
- </div>
- )}
- </div>
- );
-}
diff --git a/apps/web/src/components/ui/input-otp.tsx b/apps/web/src/components/ui/input-otp.tsx
index 0d2cc002..ec71f96a 100644
--- a/apps/web/src/components/ui/input-otp.tsx
+++ b/apps/web/src/components/ui/input-otp.tsx
@@ -4,71 +4,71 @@ import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { Minus } from "lucide-react";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const InputOTP = React.forwardRef<
- React.ElementRef<typeof OTPInput>,
- React.ComponentPropsWithoutRef<typeof OTPInput>
+ React.ElementRef<typeof OTPInput>,
+ React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
- <OTPInput
- ref={ref}
- containerClassName={cn(
- "flex items-center justify-between gap-2 w-full has-disabled:opacity-50",
- containerClassName
- )}
- className={cn("disabled:cursor-not-allowed", className)}
- {...props}
- />
+ <OTPInput
+ ref={ref}
+ containerClassName={cn(
+ "flex items-center justify-between gap-2 w-full has-disabled:opacity-50",
+ containerClassName,
+ )}
+ className={cn("disabled:cursor-not-allowed", className)}
+ {...props}
+ />
));
InputOTP.displayName = "InputOTP";
const InputOTPGroup = React.forwardRef<
- React.ElementRef<"div">,
- React.ComponentPropsWithoutRef<"div">
+ React.ElementRef<"div">,
+ React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn("flex items-center flex-1 gap-2", className)}
- {...props}
- />
+ <div
+ ref={ref}
+ className={cn("flex flex-1 items-center gap-2", className)}
+ {...props}
+ />
));
InputOTPGroup.displayName = "InputOTPGroup";
const InputOTPSlot = React.forwardRef<
- React.ElementRef<"div">,
- React.ComponentPropsWithoutRef<"div"> & { index: number }
+ React.ElementRef<"div">,
+ React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
- const inputOTPContext = React.useContext(OTPInputContext);
- const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
+ const inputOTPContext = React.useContext(OTPInputContext);
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
- return (
- <div
- ref={ref}
- className={cn(
- "relative flex aspect-square flex-1 min-w-[36px] items-center justify-center border border-input text-lg shadow-xs transition-all first:rounded-l-md first:border-l last:rounded-r-md",
- isActive && "z-10 ring-1 ring-ring",
- className
- )}
- {...props}
- >
- {char}
- {hasFakeCaret && (
- <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
- <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
- </div>
- )}
- </div>
- );
+ return (
+ <div
+ ref={ref}
+ className={cn(
+ "border-input relative flex aspect-square min-w-[36px] flex-1 items-center justify-center border text-lg shadow-xs first:rounded-l-md first:border-l last:rounded-r-md",
+ isActive && "ring-ring z-10 ring-1",
+ className,
+ )}
+ {...props}
+ >
+ {char}
+ {hasFakeCaret && (
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
+ <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
+ </div>
+ )}
+ </div>
+ );
});
InputOTPSlot.displayName = "InputOTPSlot";
const InputOTPSeparator = React.forwardRef<
- React.ElementRef<"div">,
- React.ComponentPropsWithoutRef<"div">
+ React.ElementRef<"div">,
+ React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
- <div ref={ref} role="separator" {...props}>
- <Minus />
- </div>
+ <div ref={ref} role="separator" {...props}>
+ <Minus />
+ </div>
));
InputOTPSeparator.displayName = "InputOTPSeparator";
diff --git a/apps/web/src/components/ui/input-with-back.tsx b/apps/web/src/components/ui/input-with-back.tsx
index cae9f8c9..392ab3c3 100644
--- a/apps/web/src/components/ui/input-with-back.tsx
+++ b/apps/web/src/components/ui/input-with-back.tsx
@@ -7,82 +7,82 @@ import { motion } from "motion/react";
import { useState, useEffect } from "react";
interface InputWithBackProps {
- isExpanded: boolean;
- setIsExpanded: (isExpanded: boolean) => void;
- placeholder?: string;
- value?: string;
- onChange?: (value: string) => void;
- disableAnimation?: boolean;
+ isExpanded: boolean;
+ setIsExpanded: (isExpanded: boolean) => void;
+ placeholder?: string;
+ value?: string;
+ onChange?: (value: string) => void;
+ disableAnimation?: boolean;
}
export function InputWithBack({
- isExpanded,
- setIsExpanded,
- placeholder = "Search anything",
- value,
- onChange,
- disableAnimation = false,
+ isExpanded,
+ setIsExpanded,
+ placeholder = "Search anything",
+ value,
+ onChange,
+ disableAnimation = false,
}: InputWithBackProps) {
- const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null);
- const [buttonOffset, setButtonOffset] = useState(-60);
+ const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null);
+ const [buttonOffset, setButtonOffset] = useState(-60);
- const smoothTransition = {
- duration: disableAnimation ? 0 : 0.35,
- ease: [0.25, 0.1, 0.25, 1] as const,
- };
+ const smoothTransition = {
+ duration: disableAnimation ? 0 : 0.35,
+ ease: [0.25, 0.1, 0.25, 1] as const,
+ };
- useEffect(() => {
- if (containerRef) {
- const rect = containerRef.getBoundingClientRect();
- setButtonOffset(-rect.left - 48);
- }
- }, [containerRef]);
+ useEffect(() => {
+ if (containerRef) {
+ const rect = containerRef.getBoundingClientRect();
+ setButtonOffset(-rect.left - 48);
+ }
+ }, [containerRef]);
- return (
- <div ref={setContainerRef} className="relative w-full">
- <motion.div
- className="absolute left-0 top-1/2 -translate-y-1/2 cursor-pointer hover:opacity-75 transition-opacity z-10"
- initial={{
- x: isExpanded ? 0 : buttonOffset,
- opacity: isExpanded ? 1 : 0.5,
- }}
- animate={{
- x: isExpanded ? 0 : buttonOffset,
- opacity: isExpanded ? 1 : 0.5,
- }}
- transition={smoothTransition}
- onClick={() => setIsExpanded(!isExpanded)}
- >
- <Button
- variant="outline"
- className="!size-9 rounded-full bg-panel-accent"
- >
- <ArrowLeft />
- </Button>
- </motion.div>
- <div
- className="relative flex-1"
- style={{ marginLeft: "0px", paddingLeft: "0px" }}
- >
- <motion.div
- className="relative"
- initial={{
- marginLeft: isExpanded ? 50 : 0,
- }}
- animate={{
- marginLeft: isExpanded ? 50 : 0,
- }}
- transition={smoothTransition}
- >
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
- <Input
- placeholder={placeholder}
- className="pl-9 bg-panel-accent w-full"
- value={value}
- onChange={(e) => onChange?.(e.target.value)}
- />
- </motion.div>
- </div>
- </div>
- );
+ return (
+ <div ref={setContainerRef} className="relative w-full">
+ <motion.div
+ className="absolute top-1/2 left-0 z-10 -translate-y-1/2 cursor-pointer hover:opacity-75"
+ initial={{
+ x: isExpanded ? 0 : buttonOffset,
+ opacity: isExpanded ? 1 : 0.5,
+ }}
+ animate={{
+ x: isExpanded ? 0 : buttonOffset,
+ opacity: isExpanded ? 1 : 0.5,
+ }}
+ transition={smoothTransition}
+ onClick={() => setIsExpanded(!isExpanded)}
+ >
+ <Button
+ variant="outline"
+ className="bg-panel-accent !size-9 rounded-full"
+ >
+ <ArrowLeft />
+ </Button>
+ </motion.div>
+ <div
+ className="relative flex-1"
+ style={{ marginLeft: "0px", paddingLeft: "0px" }}
+ >
+ <motion.div
+ className="relative"
+ initial={{
+ marginLeft: isExpanded ? 50 : 0,
+ }}
+ animate={{
+ marginLeft: isExpanded ? 50 : 0,
+ }}
+ transition={smoothTransition}
+ >
+ <Search className="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" />
+ <Input
+ placeholder={placeholder}
+ className="bg-panel-accent w-full pl-9"
+ value={value}
+ onChange={(e) => onChange?.(e.target.value)}
+ />
+ </motion.div>
+ </div>
+ </div>
+ );
}
diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx
index 8f195a50..9be4bca2 100644
--- a/apps/web/src/components/ui/input.tsx
+++ b/apps/web/src/components/ui/input.tsx
@@ -1,113 +1,142 @@
"use client";
import { Eye, EyeOff, X } from "lucide-react";
-import { cn } from "../../lib/utils";
+import { cva, type VariantProps } from "class-variance-authority";
+import { cn } from "@/utils/ui";
import { Button } from "./button";
-import { forwardRef, ComponentProps } from "react";
+import { forwardRef, type ComponentProps } from "react";
import { useState } from "react";
-interface InputProps extends ComponentProps<"input"> {
- showPassword?: boolean;
- onShowPasswordChange?: (show: boolean) => void;
- showClearIcon?: boolean;
- onClear?: () => void;
- containerClassName?: string;
+const inputVariants = cva(
+ "file:text-foreground placeholder:text-muted-foreground border-border bg-background flex w-full min-w-0 rounded-md border shadow-xs outline-none file:inline-flex file:border-0 file:bg-transparent file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 focus-visible:ring-4 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default: "selection:bg-primary selection:text-primary-foreground focus-visible:border-primary focus-visible:ring-primary/10",
+ destructive: "selection:bg-destructive selection:text-destructive-foreground focus-visible:border-destructive focus-visible:ring-destructive/10",
+ },
+ size: {
+ default:
+ "h-9 px-3 py-1 text-base file:h-7 file:text-sm md:text-sm",
+ sm: "h-8 px-3 text-xs file:h-6 file:text-xs",
+ lg: "h-10 px-4 text-base file:h-8 file:text-sm md:text-sm",
+ },
+ },
+ defaultVariants: {
+ size: "default",
+ variant: "default",
+ },
+ },
+);
+
+interface InputProps
+ extends Omit<ComponentProps<"input">, "size">,
+ VariantProps<typeof inputVariants> {
+ showPassword?: boolean;
+ onShowPasswordChange?: (show: boolean) => void;
+ showClearIcon?: boolean;
+ onClear?: () => void;
+ containerClassName?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
- (
- {
- className,
- type,
- containerClassName,
- showPassword,
- onShowPasswordChange,
- showClearIcon,
- onClear,
- value,
- onFocus,
- onBlur,
- ...props
- },
- ref
- ) => {
- const [isFocused, setIsFocused] = useState(false);
+ (
+ {
+ className,
+ type,
+ size,
+ variant,
+ containerClassName,
+ showPassword,
+ onShowPasswordChange,
+ showClearIcon,
+ onClear,
+ value,
+ onFocus,
+ onBlur,
+ ...props
+ },
+ ref,
+ ) => {
+ const [isFocused, setIsFocused] = useState(false);
- const isPassword = type === "password";
- const showPasswordToggle = isPassword && onShowPasswordChange;
- const showClear =
- showClearIcon &&
- onClear &&
- value &&
- String(value).length > 0 &&
- isFocused;
- const inputType = isPassword && showPassword ? "text" : type;
+ const isPassword = type === "password";
+ const showPasswordToggle = isPassword && onShowPasswordChange;
+ const showClear =
+ showClearIcon &&
+ onClear &&
+ value &&
+ String(value).length > 0 &&
+ isFocused;
+ const inputType = isPassword && showPassword ? "text" : type;
- const hasIcons = showPasswordToggle || showClear;
- const iconCount = Number(showPasswordToggle) + Number(showClear);
- const paddingRight =
- iconCount === 2 ? "pr-20" : iconCount === 1 ? "pr-10" : "";
+ const hasIcons = showPasswordToggle || showClear;
+ const iconCount = Number(showPasswordToggle) + Number(showClear);
+ const paddingRight =
+ iconCount === 2 ? "pr-20" : iconCount === 1 ? "pr-10" : "";
- return (
- <div className={cn(hasIcons ? "relative w-full" : "", containerClassName)}>
- <input
- type={inputType}
- className={cn(
- "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-accent/50 px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px]",
- "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
- paddingRight,
- className
- )}
- ref={ref}
- value={value}
- onFocus={(e) => {
- setIsFocused(true);
- onFocus?.(e);
- }}
- onBlur={(e) => {
- setIsFocused(false);
- onBlur?.(e);
- }}
- {...props}
- />
- {showClear && (
- <Button
- type="button"
- variant="text"
- size="icon"
- onMouseDown={(e) => {
- e.preventDefault();
- onClear?.();
- }}
- className="absolute right-0 top-0 h-full px-3 text-muted-foreground !opacity-100"
- aria-label="Clear input"
- >
- <X className="!size-[0.85]" />
- </Button>
- )}
- {showPasswordToggle && (
- <Button
- type="button"
- variant="text"
- size="icon"
- onClick={() => onShowPasswordChange?.(!showPassword)}
- className={cn(
- "absolute top-0 h-full px-3 text-muted-foreground hover:text-foreground",
- showClear ? "right-10" : "right-0"
- )}
- aria-label={showPassword ? "Hide password" : "Show password"}
- >
- {showPassword ? (
- <Eye className="h-4 w-4" />
- ) : (
- <EyeOff className="h-4 w-4" />
- )}
- </Button>
- )}
- </div>
- );
- }
+ return (
+ <div
+ className={cn(hasIcons ? "relative w-full" : "", containerClassName)}
+ >
+ <input
+ type={inputType}
+ className={cn(
+ inputVariants({
+ size,
+ className: cn(paddingRight, className),
+ variant,
+ }),
+ )}
+ ref={ref}
+ value={value}
+ onFocus={(e) => {
+ setIsFocused(true);
+ onFocus?.(e);
+ }}
+ onBlur={(e) => {
+ setIsFocused(false);
+ onBlur?.(e);
+ }}
+ {...props}
+ />
+ {showClear && (
+ <Button
+ type="button"
+ variant="text"
+ size="icon"
+ onMouseDown={(e) => {
+ e.preventDefault();
+ onClear?.();
+ }}
+ className="text-muted-foreground absolute top-0 right-0 h-full px-3 !opacity-100"
+ aria-label="Clear input"
+ >
+ <X className="!size-[0.85]" />
+ </Button>
+ )}
+ {showPasswordToggle && (
+ <Button
+ type="button"
+ variant="text"
+ size="icon"
+ onClick={() => onShowPasswordChange?.(!showPassword)}
+ className={cn(
+ "text-muted-foreground hover:text-foreground absolute top-0 h-full px-3",
+ showClear ? "right-10" : "right-0",
+ )}
+ aria-label={showPassword ? "Hide password" : "Show password"}
+ >
+ {showPassword ? (
+ <Eye className="size-4" />
+ ) : (
+ <EyeOff className="size-4" />
+ )}
+ </Button>
+ )}
+ </div>
+ );
+ },
);
Input.displayName = "Input";
diff --git a/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx
index 877af15b..0ea7be03 100644
--- a/apps/web/src/components/ui/label.tsx
+++ b/apps/web/src/components/ui/label.tsx
@@ -4,22 +4,22 @@ import * as React from "react";
import { Label as LabelPrimitive } from "radix-ui";
import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const labelVariants = cva(
- "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+ "text-sm text-muted-foreground font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<
- React.ElementRef<typeof LabelPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
- VariantProps<typeof labelVariants>
+ React.ElementRef<typeof LabelPrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
+ VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
- <LabelPrimitive.Root
- ref={ref}
- className={cn(labelVariants(), className)}
- {...props}
- />
+ <LabelPrimitive.Root
+ ref={ref}
+ className={cn(labelVariants(), className)}
+ {...props}
+ />
));
Label.displayName = LabelPrimitive.Root.displayName;
diff --git a/apps/web/src/components/ui/menubar.tsx b/apps/web/src/components/ui/menubar.tsx
index 96e66c8a..17a93839 100644
--- a/apps/web/src/components/ui/menubar.tsx
+++ b/apps/web/src/components/ui/menubar.tsx
@@ -4,7 +4,7 @@ import * as React from "react";
import { Menubar as MenubarPrimitive } from "radix-ui";
import { Check, ChevronRight, Circle } from "lucide-react";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const MenubarMenu = MenubarPrimitive.Menu;
@@ -17,220 +17,220 @@ const MenubarSub = MenubarPrimitive.Sub;
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
const Menubar = React.forwardRef<
- React.ElementRef<typeof MenubarPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
+ React.ElementRef<typeof MenubarPrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
- <MenubarPrimitive.Root
- ref={ref}
- className={cn(
- "flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-xs",
- className
- )}
- {...props}
- />
+ <MenubarPrimitive.Root
+ ref={ref}
+ className={cn(
+ "bg-background flex h-9 items-center space-x-1 rounded-md border p-1 shadow-xs",
+ className,
+ )}
+ {...props}
+ />
));
Menubar.displayName = MenubarPrimitive.Root.displayName;
const MenubarTrigger = React.forwardRef<
- React.ElementRef<typeof MenubarPrimitive.Trigger>,
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
+ React.ElementRef<typeof MenubarPrimitive.Trigger>,
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
- <MenubarPrimitive.Trigger
- ref={ref}
- className={cn(
- "flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-hidden focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
- className
- )}
- {...props}
- />
+ <MenubarPrimitive.Trigger
+ ref={ref}
+ className={cn(
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-3 py-1 text-sm font-medium outline-hidden select-none",
+ className,
+ )}
+ {...props}
+ />
));
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
const MenubarSubTrigger = React.forwardRef<
- React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
- inset?: boolean;
- }
+ React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
+ inset?: boolean;
+ }
>(({ className, inset, children, ...props }, ref) => (
- <MenubarPrimitive.SubTrigger
- ref={ref}
- className={cn(
- "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
- inset && "pl-8",
- className
- )}
- {...props}
- >
- {children}
- <ChevronRight className="ml-auto h-4 w-4" />
- </MenubarPrimitive.SubTrigger>
+ <MenubarPrimitive.SubTrigger
+ ref={ref}
+ className={cn(
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none",
+ inset && "pl-8",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+ <ChevronRight className="ml-auto size-4" />
+ </MenubarPrimitive.SubTrigger>
));
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
const MenubarSubContent = React.forwardRef<
- React.ElementRef<typeof MenubarPrimitive.SubContent>,
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
+ React.ElementRef<typeof MenubarPrimitive.SubContent>,
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
- <MenubarPrimitive.SubContent
- ref={ref}
- className={cn(
- "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
- className
- )}
- {...props}
- />
+ <MenubarPrimitive.SubContent
+ ref={ref}
+ className={cn(
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-lg",
+ className,
+ )}
+ {...props}
+ />
));
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
const MenubarContent = React.forwardRef<
- React.ElementRef<typeof MenubarPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
+ React.ElementRef<typeof MenubarPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(
- (
- { className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
- ref
- ) => (
- <MenubarPrimitive.Portal>
- <MenubarPrimitive.Content
- ref={ref}
- align={align}
- alignOffset={alignOffset}
- sideOffset={sideOffset}
- className={cn(
- "z-50 min-w-48 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
- className
- )}
- {...props}
- />
- </MenubarPrimitive.Portal>
- )
+ (
+ { className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
+ ref,
+ ) => (
+ <MenubarPrimitive.Portal>
+ <MenubarPrimitive.Content
+ ref={ref}
+ align={align}
+ alignOffset={alignOffset}
+ sideOffset={sideOffset}
+ className={cn(
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-48 overflow-hidden rounded-md border p-1 shadow-md",
+ className,
+ )}
+ {...props}
+ />
+ </MenubarPrimitive.Portal>
+ ),
);
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
const MenubarItem = React.forwardRef<
- React.ElementRef<typeof MenubarPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
- inset?: boolean;
- }
+ React.ElementRef<typeof MenubarPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
+ inset?: boolean;
+ }
>(({ className, inset, ...props }, ref) => (
- <MenubarPrimitive.Item
- ref={ref}
- className={cn(
- "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
- inset && "pl-8",
- className
- )}
- {...props}
- />
+ <MenubarPrimitive.Item
+ ref={ref}
+ className={cn(
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50",
+ inset && "pl-8",
+ className,
+ )}
+ {...props}
+ />
));
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
const MenubarCheckboxItem = React.forwardRef<
- React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
+ React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
- <MenubarPrimitive.CheckboxItem
- ref={ref}
- className={cn(
- "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
- className
- )}
- checked={checked}
- {...props}
- >
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
- <MenubarPrimitive.ItemIndicator>
- <Check className="h-4 w-4" />
- </MenubarPrimitive.ItemIndicator>
- </span>
- {children}
- </MenubarPrimitive.CheckboxItem>
+ <MenubarPrimitive.CheckboxItem
+ ref={ref}
+ className={cn(
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50",
+ className,
+ )}
+ checked={checked}
+ {...props}
+ >
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
+ <MenubarPrimitive.ItemIndicator>
+ <Check className="size-4" />
+ </MenubarPrimitive.ItemIndicator>
+ </span>
+ {children}
+ </MenubarPrimitive.CheckboxItem>
));
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
const MenubarRadioItem = React.forwardRef<
- React.ElementRef<typeof MenubarPrimitive.RadioItem>,
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
+ React.ElementRef<typeof MenubarPrimitive.RadioItem>,
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
- <MenubarPrimitive.RadioItem
- ref={ref}
- className={cn(
- "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
- className
- )}
- {...props}
- >
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
- <MenubarPrimitive.ItemIndicator>
- <Circle className="h-4 w-4 fill-current" />
- </MenubarPrimitive.ItemIndicator>
- </span>
- {children}
- </MenubarPrimitive.RadioItem>
+ <MenubarPrimitive.RadioItem
+ ref={ref}
+ className={cn(
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50",
+ className,
+ )}
+ {...props}
+ >
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
+ <MenubarPrimitive.ItemIndicator>
+ <Circle className="size-4 fill-current" />
+ </MenubarPrimitive.ItemIndicator>
+ </span>
+ {children}
+ </MenubarPrimitive.RadioItem>
));
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
const MenubarLabel = React.forwardRef<
- React.ElementRef<typeof MenubarPrimitive.Label>,
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
- inset?: boolean;
- }
+ React.ElementRef<typeof MenubarPrimitive.Label>,
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
+ inset?: boolean;
+ }
>(({ className, inset, ...props }, ref) => (
- <MenubarPrimitive.Label
- ref={ref}
- className={cn(
- "px-2 py-1.5 text-sm font-semibold",
- inset && "pl-8",
- className
- )}
- {...props}
- />
+ <MenubarPrimitive.Label
+ ref={ref}
+ className={cn(
+ "px-2 py-1.5 text-sm font-semibold",
+ inset && "pl-8",
+ className,
+ )}
+ {...props}
+ />
));
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
const MenubarSeparator = React.forwardRef<
- React.ElementRef<typeof MenubarPrimitive.Separator>,
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
+ React.ElementRef<typeof MenubarPrimitive.Separator>,
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
- <MenubarPrimitive.Separator
- ref={ref}
- className={cn("-mx-1 my-1 h-px bg-muted", className)}
- {...props}
- />
+ <MenubarPrimitive.Separator
+ ref={ref}
+ className={cn("bg-muted -mx-1 my-1 h-px", className)}
+ {...props}
+ />
));
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
const MenubarShortcut = ({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
- return (
- <span
- className={cn(
- "ml-auto text-xs tracking-widest text-muted-foreground",
- className
- )}
- {...props}
- />
- );
+ return (
+ <span
+ className={cn(
+ "text-muted-foreground ml-auto text-xs tracking-widest",
+ className,
+ )}
+ {...props}
+ />
+ );
};
MenubarShortcut.displayname = "MenubarShortcut";
export {
- Menubar,
- MenubarMenu,
- MenubarTrigger,
- MenubarContent,
- MenubarItem,
- MenubarSeparator,
- MenubarLabel,
- MenubarCheckboxItem,
- MenubarRadioGroup,
- MenubarRadioItem,
- MenubarPortal,
- MenubarSubContent,
- MenubarSubTrigger,
- MenubarGroup,
- MenubarSub,
- MenubarShortcut,
+ Menubar,
+ MenubarMenu,
+ MenubarTrigger,
+ MenubarContent,
+ MenubarItem,
+ MenubarSeparator,
+ MenubarLabel,
+ MenubarCheckboxItem,
+ MenubarRadioGroup,
+ MenubarRadioItem,
+ MenubarPortal,
+ MenubarSubContent,
+ MenubarSubTrigger,
+ MenubarGroup,
+ MenubarSub,
+ MenubarShortcut,
};
diff --git a/apps/web/src/components/ui/navigation-menu.tsx b/apps/web/src/components/ui/navigation-menu.tsx
index 288b5d88..f619d575 100644
--- a/apps/web/src/components/ui/navigation-menu.tsx
+++ b/apps/web/src/components/ui/navigation-menu.tsx
@@ -3,126 +3,126 @@ import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const NavigationMenu = React.forwardRef<
- React.ElementRef<typeof NavigationMenuPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
+ React.ElementRef<typeof NavigationMenuPrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
- <NavigationMenuPrimitive.Root
- ref={ref}
- className={cn(
- "relative z-10 flex max-w-max flex-1 items-center justify-center",
- className
- )}
- {...props}
- >
- {children}
- <NavigationMenuViewport />
- </NavigationMenuPrimitive.Root>
+ <NavigationMenuPrimitive.Root
+ ref={ref}
+ className={cn(
+ "relative z-10 flex max-w-max flex-1 items-center justify-center",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+ <NavigationMenuViewport />
+ </NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
- React.ElementRef<typeof NavigationMenuPrimitive.List>,
- React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
+ React.ElementRef<typeof NavigationMenuPrimitive.List>,
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
- <NavigationMenuPrimitive.List
- ref={ref}
- className={cn(
- "group flex flex-1 list-none items-center justify-center space-x-1",
- className
- )}
- {...props}
- />
+ <NavigationMenuPrimitive.List
+ ref={ref}
+ className={cn(
+ "group flex flex-1 list-none items-center justify-center space-x-1",
+ className,
+ )}
+ {...props}
+ />
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
- "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent/50 data-[state=open]:bg-accent/50"
+ "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent/50 data-[state=open]:bg-accent/50",
);
const NavigationMenuTrigger = React.forwardRef<
- React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
- React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
+ React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
- <NavigationMenuPrimitive.Trigger
- ref={ref}
- className={cn(navigationMenuTriggerStyle(), "group", className)}
- {...props}
- >
- {children}{" "}
- <ChevronDown
- className="relative top-px ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
- aria-hidden="true"
- />
- </NavigationMenuPrimitive.Trigger>
+ <NavigationMenuPrimitive.Trigger
+ ref={ref}
+ className={cn(navigationMenuTriggerStyle(), "group", className)}
+ {...props}
+ >
+ {children}{" "}
+ <ChevronDown
+ className="relative top-px ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
+ aria-hidden="true"
+ />
+ </NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
- React.ElementRef<typeof NavigationMenuPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
+ React.ElementRef<typeof NavigationMenuPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
- <NavigationMenuPrimitive.Content
- ref={ref}
- className={cn(
- "left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
- className
- )}
- {...props}
- />
+ <NavigationMenuPrimitive.Content
+ ref={ref}
+ className={cn(
+ "data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full md:absolute md:w-auto",
+ className,
+ )}
+ {...props}
+ />
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
- React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
- React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
+ React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
- <div className={cn("absolute left-0 top-full flex justify-center")}>
- <NavigationMenuPrimitive.Viewport
- className={cn(
- "origin-top-center relative mt-1.5 h-(--radix-navigation-menu-viewport-height) w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-(--radix-navigation-menu-viewport-width)",
- className
- )}
- ref={ref}
- {...props}
- />
- </div>
+ <div className={cn("absolute top-full left-0 flex justify-center")}>
+ <NavigationMenuPrimitive.Viewport
+ className={cn(
+ "origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-(--radix-navigation-menu-viewport-height) w-full overflow-hidden rounded-md border shadow-sm md:w-(--radix-navigation-menu-viewport-width)",
+ className,
+ )}
+ ref={ref}
+ {...props}
+ />
+ </div>
));
NavigationMenuViewport.displayName =
- NavigationMenuPrimitive.Viewport.displayName;
+ NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
- React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
- React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
+ React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
- <NavigationMenuPrimitive.Indicator
- ref={ref}
- className={cn(
- "top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
- className
- )}
- {...props}
- >
- <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
- </NavigationMenuPrimitive.Indicator>
+ <NavigationMenuPrimitive.Indicator
+ ref={ref}
+ className={cn(
+ "data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-1 flex h-1.5 items-end justify-center overflow-hidden",
+ className,
+ )}
+ {...props}
+ >
+ <div className="bg-border relative top-[60%] size-2 rotate-45 rounded-tl-sm shadow-md" />
+ </NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName =
- NavigationMenuPrimitive.Indicator.displayName;
+ NavigationMenuPrimitive.Indicator.displayName;
export {
- navigationMenuTriggerStyle,
- NavigationMenu,
- NavigationMenuList,
- NavigationMenuItem,
- NavigationMenuContent,
- NavigationMenuTrigger,
- NavigationMenuLink,
- NavigationMenuIndicator,
- NavigationMenuViewport,
+ navigationMenuTriggerStyle,
+ NavigationMenu,
+ NavigationMenuList,
+ NavigationMenuItem,
+ NavigationMenuContent,
+ NavigationMenuTrigger,
+ NavigationMenuLink,
+ NavigationMenuIndicator,
+ NavigationMenuViewport,
};
diff --git a/apps/web/src/components/ui/pagination.tsx b/apps/web/src/components/ui/pagination.tsx
index 61766cec..96eec969 100644
--- a/apps/web/src/components/ui/pagination.tsx
+++ b/apps/web/src/components/ui/pagination.tsx
@@ -1,117 +1,117 @@
import * as React from "react";
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
-import { cn } from "../../lib/utils";
-import { ButtonProps, buttonVariants } from "./button";
+import { cn } from "@/utils/ui";
+import { type ButtonProps, buttonVariants } from "./button";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
- <nav
- role="navigation"
- aria-label="pagination"
- className={cn("mx-auto flex w-full justify-center", className)}
- {...props}
- />
+ <nav
+ role="navigation"
+ aria-label="pagination"
+ className={cn("mx-auto flex w-full justify-center", className)}
+ {...props}
+ />
);
Pagination.displayName = "Pagination";
const PaginationContent = React.forwardRef<
- HTMLUListElement,
- React.ComponentProps<"ul">
+ HTMLUListElement,
+ React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
- <ul
- ref={ref}
- className={cn("flex flex-row items-center gap-1", className)}
- {...props}
- />
+ <ul
+ ref={ref}
+ className={cn("flex flex-row items-center gap-1", className)}
+ {...props}
+ />
));
PaginationContent.displayName = "PaginationContent";
const PaginationItem = React.forwardRef<
- HTMLLIElement,
- React.ComponentProps<"li">
+ HTMLLIElement,
+ React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
- <li ref={ref} className={cn("", className)} {...props} />
+ <li ref={ref} className={cn("", className)} {...props} />
));
PaginationItem.displayName = "PaginationItem";
type PaginationLinkProps = {
- isActive?: boolean;
+ isActive?: boolean;
} & Pick<ButtonProps, "size"> &
- React.ComponentProps<"a">;
+ React.ComponentProps<"a">;
const PaginationLink = ({
- className,
- isActive,
- size = "icon",
- ...props
+ className,
+ isActive,
+ size = "icon",
+ ...props
}: PaginationLinkProps) => (
- <a
- aria-current={isActive ? "page" : undefined}
- className={cn(
- buttonVariants({
- variant: isActive ? "outline" : "text",
- size,
- }),
- className
- )}
- {...props}
- />
+ <a
+ aria-current={isActive ? "page" : undefined}
+ className={cn(
+ buttonVariants({
+ variant: isActive ? "outline" : "text",
+ size,
+ }),
+ className,
+ )}
+ {...props}
+ />
);
PaginationLink.displayName = "PaginationLink";
const PaginationPrevious = ({
- className,
- ...props
+ className,
+ ...props
}: React.ComponentProps<typeof PaginationLink>) => (
- <PaginationLink
- aria-label="Go to previous page"
- size="default"
- className={cn("gap-1 pl-2.5", className)}
- {...props}
- >
- <ChevronLeft className="h-4 w-4" />
- <span>Previous</span>
- </PaginationLink>
+ <PaginationLink
+ aria-label="Go to previous page"
+ size="default"
+ className={cn("gap-1 pl-2.5", className)}
+ {...props}
+ >
+ <ChevronLeft className="size-4" />
+ <span>Previous</span>
+ </PaginationLink>
);
PaginationPrevious.displayName = "PaginationPrevious";
const PaginationNext = ({
- className,
- ...props
+ className,
+ ...props
}: React.ComponentProps<typeof PaginationLink>) => (
- <PaginationLink
- aria-label="Go to next page"
- size="default"
- className={cn("gap-1 pr-2.5", className)}
- {...props}
- >
- <span>Next</span>
- <ChevronRight className="h-4 w-4" />
- </PaginationLink>
+ <PaginationLink
+ aria-label="Go to next page"
+ size="default"
+ className={cn("gap-1 pr-2.5", className)}
+ {...props}
+ >
+ <span>Next</span>
+ <ChevronRight className="size-4" />
+ </PaginationLink>
);
PaginationNext.displayName = "PaginationNext";
const PaginationEllipsis = ({
- className,
- ...props
+ className,
+ ...props
}: React.ComponentProps<"span">) => (
- <span
- aria-hidden
- className={cn("flex h-9 w-9 items-center justify-center", className)}
- {...props}
- >
- <MoreHorizontal className="h-4 w-4" />
- <span className="sr-only">More pages</span>
- </span>
+ <span
+ aria-hidden
+ className={cn("flex size-9 items-center justify-center", className)}
+ {...props}
+ >
+ <MoreHorizontal className="size-4" />
+ <span className="sr-only">More pages</span>
+ </span>
);
PaginationEllipsis.displayName = "PaginationEllipsis";
export {
- Pagination,
- PaginationContent,
- PaginationLink,
- PaginationItem,
- PaginationPrevious,
- PaginationNext,
- PaginationEllipsis,
+ Pagination,
+ PaginationContent,
+ PaginationLink,
+ PaginationItem,
+ PaginationPrevious,
+ PaginationNext,
+ PaginationEllipsis,
};
diff --git a/apps/web/src/components/ui/phone-input.tsx b/apps/web/src/components/ui/phone-input.tsx
index ca85df8f..1d0eeb07 100644
--- a/apps/web/src/components/ui/phone-input.tsx
+++ b/apps/web/src/components/ui/phone-input.tsx
@@ -5,163 +5,163 @@ import flags from "react-phone-number-input/flags";
import { Button } from "./button";
import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
} from "./command";
import { Input } from "./input";
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
import { ScrollArea } from "./scroll-area";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
type PhoneInputProps = Omit<
- React.ComponentProps<"input">,
- "onChange" | "value" | "ref"
+ React.ComponentProps<"input">,
+ "onChange" | "value" | "ref"
> &
- Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & {
- onChange?: (value: RPNInput.Value) => void;
- };
+ Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & {
+ onChange?: (value: RPNInput.Value) => void;
+ };
const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =
- React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>(
- ({ className, onChange, defaultCountry = "US", ...props }, ref) => {
- return (
- <RPNInput.default
- ref={ref}
- className={cn("flex", className)}
- flagComponent={FlagComponent}
- countrySelectComponent={CountrySelect}
- inputComponent={InputComponent}
- defaultCountry={defaultCountry}
- international
- addInternationalOption
- smartCaret={false}
- /**
- * Handles the onChange event.
- *
- * react-phone-number-input might trigger the onChange event as undefined
- * when a valid phone number is not entered. To prevent this,
- * the value is coerced to an empty string.
- *
- * @param {E164Number | undefined} value - The entered value
- */
- onChange={(value) => onChange?.(value || ("" as RPNInput.Value))}
- {...props}
- />
- );
- }
- );
+ React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>(
+ ({ className, onChange, defaultCountry = "US", ...props }, ref) => {
+ return (
+ <RPNInput.default
+ ref={ref}
+ className={cn("flex", className)}
+ flagComponent={FlagComponent}
+ countrySelectComponent={CountrySelect}
+ inputComponent={InputComponent}
+ defaultCountry={defaultCountry}
+ international
+ addInternationalOption
+ smartCaret={false}
+ /**
+ * Handles the onChange event.
+ *
+ * react-phone-number-input might trigger the onChange event as undefined
+ * when a valid phone number is not entered. To prevent this,
+ * the value is coerced to an empty string.
+ *
+ * @param {E164Number | undefined} value - The entered value
+ */
+ onChange={(value) => onChange?.(value || ("" as RPNInput.Value))}
+ {...props}
+ />
+ );
+ },
+ );
PhoneInput.displayName = "PhoneInput";
const InputComponent = React.forwardRef<
- HTMLInputElement,
- React.ComponentProps<"input">
+ HTMLInputElement,
+ React.ComponentProps<"input">
>(({ className, ...props }, ref) => (
- <Input
- className={cn("rounded-e-lg rounded-s-none", className)}
- {...props}
- ref={ref}
- />
+ <Input
+ className={cn("rounded-s-none rounded-e-lg", className)}
+ {...props}
+ ref={ref}
+ />
));
InputComponent.displayName = "InputComponent";
type CountryEntry = { label: string; value: RPNInput.Country | undefined };
type CountrySelectProps = {
- disabled?: boolean;
- value: RPNInput.Country;
- options: CountryEntry[];
- onChange: (country: RPNInput.Country) => void;
+ disabled?: boolean;
+ value: RPNInput.Country;
+ options: CountryEntry[];
+ onChange: (country: RPNInput.Country) => void;
};
const CountrySelect = ({
- disabled,
- value: selectedCountry,
- options: countryList,
- onChange,
+ disabled,
+ value: selectedCountry,
+ options: countryList,
+ onChange,
}: CountrySelectProps) => {
- return (
- <Popover>
- <PopoverTrigger asChild>
- <Button
- type="button"
- variant="outline"
- className="flex gap-1 rounded-e-none rounded-s-lg border-r-0 px-3 focus:z-10"
- disabled={disabled}
- >
- <FlagComponent
- country={selectedCountry}
- countryName={selectedCountry}
- />
- <ChevronsUpDown
- className={cn(
- "-mr-2 size-4 opacity-50",
- disabled ? "hidden" : "opacity-100"
- )}
- />
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-[300px] p-0">
- <Command>
- <CommandInput placeholder="Search country..." />
- <CommandList>
- <ScrollArea className="h-72">
- <CommandEmpty>No country found.</CommandEmpty>
- <CommandGroup>
- {countryList.map(({ value, label }) =>
- value ? (
- <CountrySelectOption
- key={value}
- country={value}
- countryName={label}
- selectedCountry={selectedCountry}
- onChange={onChange}
- />
- ) : null
- )}
- </CommandGroup>
- </ScrollArea>
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- );
+ return (
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button
+ type="button"
+ variant="outline"
+ className="flex gap-1 rounded-s-lg rounded-e-none border-r-0 px-3 focus:z-10"
+ disabled={disabled}
+ >
+ <FlagComponent
+ country={selectedCountry}
+ countryName={selectedCountry}
+ />
+ <ChevronsUpDown
+ className={cn(
+ "-mr-2 opacity-50",
+ disabled ? "hidden" : "opacity-100",
+ )}
+ />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-[300px] p-0">
+ <Command>
+ <CommandInput placeholder="Search country..." />
+ <CommandList>
+ <ScrollArea className="h-72">
+ <CommandEmpty>No country found.</CommandEmpty>
+ <CommandGroup>
+ {countryList.map(({ value, label }) =>
+ value ? (
+ <CountrySelectOption
+ key={value}
+ country={value}
+ countryName={label}
+ selectedCountry={selectedCountry}
+ onChange={onChange}
+ />
+ ) : null,
+ )}
+ </CommandGroup>
+ </ScrollArea>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ );
};
interface CountrySelectOptionProps extends RPNInput.FlagProps {
- selectedCountry: RPNInput.Country;
- onChange: (country: RPNInput.Country) => void;
+ selectedCountry: RPNInput.Country;
+ onChange: (country: RPNInput.Country) => void;
}
const CountrySelectOption = ({
- country,
- countryName,
- selectedCountry,
- onChange,
+ country,
+ countryName,
+ selectedCountry,
+ onChange,
}: CountrySelectOptionProps) => {
- return (
- <CommandItem className="gap-2" onSelect={() => onChange(country)}>
- <FlagComponent country={country} countryName={countryName} />
- <span className="flex-1 text-sm">{countryName}</span>
- <span className="text-sm text-foreground/50">{`+${RPNInput.getCountryCallingCode(country)}`}</span>
- <CheckIcon
- className={`ml-auto size-4 ${country === selectedCountry ? "opacity-100" : "opacity-0"}`}
- />
- </CommandItem>
- );
+ return (
+ <CommandItem className="gap-2" onSelect={() => onChange(country)}>
+ <FlagComponent country={country} countryName={countryName} />
+ <span className="flex-1 text-sm">{countryName}</span>
+ <span className="text-foreground/50 text-sm">{`+${RPNInput.getCountryCallingCode(country)}`}</span>
+ <CheckIcon
+ className={`ml-auto size-4 ${country === selectedCountry ? "opacity-100" : "opacity-0"}`}
+ />
+ </CommandItem>
+ );
};
const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => {
- const Flag = flags[country];
+ const Flag = flags[country];
- return (
- <span className="flex h-4 w-6 overflow-hidden rounded-sm bg-foreground/20 [&_svg]:size-full">
- {Flag && <Flag title={countryName} />}
- </span>
- );
+ return (
+ <span className="bg-foreground/20 flex h-4 w-6 overflow-hidden rounded-sm [&_svg]:size-full">
+ {Flag && <Flag title={countryName} />}
+ </span>
+ );
};
export { PhoneInput };
diff --git a/apps/web/src/components/ui/popover.tsx b/apps/web/src/components/ui/popover.tsx
index 406ff8eb..34738303 100644
--- a/apps/web/src/components/ui/popover.tsx
+++ b/apps/web/src/components/ui/popover.tsx
@@ -3,7 +3,7 @@
import * as React from "react";
import { Popover as PopoverPrimitive } from "radix-ui";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const Popover = PopoverPrimitive.Root;
@@ -12,21 +12,21 @@ const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
- React.ElementRef<typeof PopoverPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
+ React.ElementRef<typeof PopoverPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
- <PopoverPrimitive.Portal>
- <PopoverPrimitive.Content
- ref={ref}
- align={align}
- sideOffset={sideOffset}
- className={cn(
- "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-[0_0_10px_rgba(0,0,0,0.15)] outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
- className
- )}
- {...props}
- />
- </PopoverPrimitive.Portal>
+ <PopoverPrimitive.Portal>
+ <PopoverPrimitive.Content
+ ref={ref}
+ align={align}
+ sideOffset={sideOffset}
+ className={cn(
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-[0_0_10px_rgba(0,0,0,0.15)] outline-hidden",
+ className,
+ )}
+ {...props}
+ />
+ </PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
diff --git a/apps/web/src/components/ui/progress.tsx b/apps/web/src/components/ui/progress.tsx
index 6b785a58..8bf5e939 100644
--- a/apps/web/src/components/ui/progress.tsx
+++ b/apps/web/src/components/ui/progress.tsx
@@ -3,25 +3,25 @@
import * as React from "react";
import { Progress as ProgressPrimitive } from "radix-ui";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const Progress = React.forwardRef<
- React.ElementRef<typeof ProgressPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
+ React.ElementRef<typeof ProgressPrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
- <ProgressPrimitive.Root
- ref={ref}
- className={cn(
- "relative h-2 w-full overflow-hidden rounded-full bg-accent",
- className
- )}
- {...props}
- >
- <ProgressPrimitive.Indicator
- className="h-full w-full flex-1 bg-foreground transition-all"
- style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
- />
- </ProgressPrimitive.Root>
+ <ProgressPrimitive.Root
+ ref={ref}
+ className={cn(
+ "bg-accent relative h-2 w-full overflow-hidden rounded-full",
+ className,
+ )}
+ {...props}
+ >
+ <ProgressPrimitive.Indicator
+ className="bg-primary size-full flex-1"
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
+ />
+ </ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
diff --git a/apps/web/src/components/ui/prose.tsx b/apps/web/src/components/ui/prose.tsx
index c25a5a65..7586344c 100644
--- a/apps/web/src/components/ui/prose.tsx
+++ b/apps/web/src/components/ui/prose.tsx
@@ -1,22 +1,22 @@
-import { cn } from "@/lib/utils";
+import { cn } from "@/utils/ui";
import type React from "react";
type ProseProps = React.HTMLAttributes<HTMLElement> & {
- as?: "article";
- html: string;
+ as?: "article";
+ html: string;
};
function Prose({ children, html, className }: ProseProps) {
- return (
- <article
- className={cn(
- "prose prose-h2:font-semibold max-w-none prose-h1:text-xl prose-a:text-blue-600 prose-p:text-justify dark:prose-invert mx-auto",
- className
- )}
- >
- {html ? <div dangerouslySetInnerHTML={{ __html: html }} /> : children}
- </article>
- );
+ return (
+ <article
+ className={cn(
+ "prose prose-h2:font-semibold prose-h1:text-xl prose-a:text-blue-600 prose-p:text-justify dark:prose-invert mx-auto max-w-none",
+ className,
+ )}
+ >
+ {html ? <div dangerouslySetInnerHTML={{ __html: html }} /> : children}
+ </article>
+ );
}
export default Prose;
diff --git a/apps/web/src/components/ui/radio-group.tsx b/apps/web/src/components/ui/radio-group.tsx
index 859b8b5f..fe2045dc 100644
--- a/apps/web/src/components/ui/radio-group.tsx
+++ b/apps/web/src/components/ui/radio-group.tsx
@@ -4,40 +4,40 @@ import * as React from "react";
import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
import { Circle } from "lucide-react";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const RadioGroup = React.forwardRef<
- React.ElementRef<typeof RadioGroupPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
+ React.ElementRef<typeof RadioGroupPrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
- return (
- <RadioGroupPrimitive.Root
- className={cn("grid gap-2", className)}
- {...props}
- ref={ref}
- />
- );
+ return (
+ <RadioGroupPrimitive.Root
+ className={cn("grid gap-2", className)}
+ {...props}
+ ref={ref}
+ />
+ );
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
- React.ElementRef<typeof RadioGroupPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
+ React.ElementRef<typeof RadioGroupPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
- return (
- <RadioGroupPrimitive.Item
- ref={ref}
- className={cn(
- "aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow-sm focus:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
- className
- )}
- {...props}
- >
- <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
- <Circle className="h-3.5 w-3.5 fill-primary" />
- </RadioGroupPrimitive.Indicator>
- </RadioGroupPrimitive.Item>
- );
+ return (
+ <RadioGroupPrimitive.Item
+ ref={ref}
+ className={cn(
+ "border-primary text-primary focus-visible:ring-ring aspect-square size-4 rounded-full border shadow-sm focus:outline-hidden focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
+ className,
+ )}
+ {...props}
+ >
+ <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
+ <Circle className="fill-primary size-3.5" />
+ </RadioGroupPrimitive.Indicator>
+ </RadioGroupPrimitive.Item>
+ );
});
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
diff --git a/apps/web/src/components/ui/react-markdown-wrapper.tsx b/apps/web/src/components/ui/react-markdown-wrapper.tsx
new file mode 100644
index 00000000..181ae8ac
--- /dev/null
+++ b/apps/web/src/components/ui/react-markdown-wrapper.tsx
@@ -0,0 +1,26 @@
+import ReactMarkdown from "react-markdown";
+import { cn } from "@/utils/ui";
+
+export function ReactMarkdownWrapper({ children }: { children: string }) {
+ return (
+ <ReactMarkdown
+ components={{
+ a: ({ className: linkClassName, children, ...props }) => (
+ <a
+ className={cn("text-primary hover:underline", linkClassName)}
+ target="_blank"
+ rel="noopener noreferrer"
+ {...props}
+ >
+ {children}
+ </a>
+ ),
+ strong: ({ children }) => (
+ <strong className="text-foreground font-semibold">{children}</strong>
+ ),
+ }}
+ >
+ {children}
+ </ReactMarkdown>
+ );
+}
diff --git a/apps/web/src/components/ui/resizable.tsx b/apps/web/src/components/ui/resizable.tsx
index fc9d8b81..9287797d 100644
--- a/apps/web/src/components/ui/resizable.tsx
+++ b/apps/web/src/components/ui/resizable.tsx
@@ -2,37 +2,37 @@
import * as ResizablePrimitive from "react-resizable-panels";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const ResizablePanelGroup = ({
- className,
- ...props
+ className,
+ ...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
- <ResizablePrimitive.PanelGroup
- className={cn(
- "flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
- className
- )}
- {...props}
- />
+ <ResizablePrimitive.PanelGroup
+ className={cn(
+ "flex size-full data-[panel-group-direction=vertical]:flex-col",
+ className,
+ )}
+ {...props}
+ />
);
const ResizablePanel = ResizablePrimitive.Panel;
const ResizableHandle = ({
- withHandle,
- className,
- ...props
+ withHandle,
+ className,
+ ...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
- withHandle?: boolean;
+ withHandle?: boolean;
}) => (
- <ResizablePrimitive.PanelResizeHandle
- className={cn(
- "relative flex w-px items-center justify-center bg-transparent after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
- className
- )}
- {...props}
- />
+ <ResizablePrimitive.PanelResizeHandle
+ className={cn(
+ "focus-visible:ring-ring relative flex w-px items-center justify-center bg-transparent after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
+ className,
+ )}
+ {...props}
+ />
);
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
diff --git a/apps/web/src/components/ui/scroll-area.tsx b/apps/web/src/components/ui/scroll-area.tsx
index 1fc227b0..9019fae5 100644
--- a/apps/web/src/components/ui/scroll-area.tsx
+++ b/apps/web/src/components/ui/scroll-area.tsx
@@ -1,17 +1,17 @@
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "@/utils/ui";
const ScrollArea = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement>
>(({ className, children, ...props }, ref) => (
- <div
- ref={ref}
- className={cn("overflow-auto scrollbar-thin", className)}
- {...props}
- >
- {children}
- </div>
+ <div
+ ref={ref}
+ className={cn("scrollbar-thin overflow-auto", className)}
+ {...props}
+ >
+ {children}
+ </div>
));
ScrollArea.displayName = "ScrollArea";
diff --git a/apps/web/src/components/ui/select.tsx b/apps/web/src/components/ui/select.tsx
index 53ca27cb..79462d4a 100644
--- a/apps/web/src/components/ui/select.tsx
+++ b/apps/web/src/components/ui/select.tsx
@@ -5,7 +5,7 @@ import { Select as SelectPrimitive } from "radix-ui";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const Select = SelectPrimitive.Root;
@@ -14,165 +14,166 @@ const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const selectItemVariants = cva(
- "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-hidden transition-opacity data-disabled:pointer-events-none data-disabled:opacity-50",
- {
- variants: {
- variant: {
- default: "focus:opacity-65 focus:text-accent-foreground",
- destructive: "text-destructive focus:text-destructive/80",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
+ "relative flex cursor-pointer select-none items-center gap-2 rounded-xl px-2.5 py-2 text-sm text-foreground/85 outline-hidden data-[highlighted]:bg-accent/35 data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "",
+ destructive: "text-destructive data-[highlighted]:bg-destructive/5 data-[highlighted]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
);
const SelectTrigger = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.Trigger>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
- <SelectPrimitive.Trigger
- ref={ref}
- className={cn(
- "flex gap-1 h-7 cursor-pointer w-auto items-center justify-between whitespace-nowrap rounded-md bg-accent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
- className
- )}
- {...props}
- >
- {children}
- <SelectPrimitive.Icon asChild>
- <ChevronDown className="size-3 opacity-50" />
- </SelectPrimitive.Icon>
- </SelectPrimitive.Trigger>
+ <SelectPrimitive.Trigger
+ ref={ref}
+ className={cn(
+ "bg-accent ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-7 w-auto cursor-pointer items-center justify-between gap-1 rounded-md px-3 py-2 text-sm whitespace-nowrap focus:ring-1 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
+ "focus:border-primary focus:ring-4 focus:ring-primary/10 border-transparent transition-none",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+ <SelectPrimitive.Icon asChild>
+ <ChevronDown className="size-3 opacity-50" />
+ </SelectPrimitive.Icon>
+ </SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
- <SelectPrimitive.ScrollUpButton
- ref={ref}
- className={cn(
- "flex cursor-default items-center justify-center py-1",
- className
- )}
- {...props}
- >
- <ChevronUp className="h-4 w-4" />
- </SelectPrimitive.ScrollUpButton>
+ <SelectPrimitive.ScrollUpButton
+ ref={ref}
+ className={cn(
+ "flex cursor-default items-center justify-center py-1",
+ className,
+ )}
+ {...props}
+ >
+ <ChevronUp className="size-4" />
+ </SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
- <SelectPrimitive.ScrollDownButton
- ref={ref}
- className={cn(
- "flex cursor-default items-center justify-center py-1",
- className
- )}
- {...props}
- >
- <ChevronDown className="h-4 w-4" />
- </SelectPrimitive.ScrollDownButton>
+ <SelectPrimitive.ScrollDownButton
+ ref={ref}
+ className={cn(
+ "flex cursor-default items-center justify-center py-1",
+ className,
+ )}
+ {...props}
+ >
+ <ChevronDown className="size-4" />
+ </SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
- SelectPrimitive.ScrollDownButton.displayName;
+ SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
+ React.ElementRef<typeof SelectPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
- <SelectPrimitive.Portal>
- <SelectPrimitive.Content
- ref={ref}
- className={cn(
- "relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
- position === "popper" &&
- "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
- className
- )}
- position={position}
- onCloseAutoFocus={(e) => {
- e.preventDefault();
- e.stopPropagation();
- }}
- {...props}
- >
- <SelectScrollUpButton />
- <SelectPrimitive.Viewport
- className={cn(
- "p-1",
- position === "popper" &&
- "h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)"
- )}
- >
- {children}
- </SelectPrimitive.Viewport>
- <SelectScrollDownButton />
- </SelectPrimitive.Content>
- </SelectPrimitive.Portal>
+ <SelectPrimitive.Portal>
+ <SelectPrimitive.Content
+ ref={ref}
+ className={cn(
+ "bg-popover text-popover-foreground z-50 max-h-96 min-w-32 overflow-hidden rounded-2xl border p-2 shadow-lg",
+ className,
+ )}
+ position={position}
+ onCloseAutoFocus={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ {...props}
+ >
+ <SelectScrollUpButton />
+ <SelectPrimitive.Viewport
+ className={cn(
+ position === "popper" &&
+ "h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)",
+ )}
+ >
+ {children}
+ </SelectPrimitive.Viewport>
+ <SelectScrollDownButton />
+ </SelectPrimitive.Content>
+ </SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.Label>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
+ React.ElementRef<typeof SelectPrimitive.Label>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
- <SelectPrimitive.Label
- ref={ref}
- className={cn("px-2 py-1.5 text-sm font-semibold", className)}
- {...props}
- />
+ <SelectPrimitive.Label
+ ref={ref}
+ className={cn(
+ "px-3 pb-2 pt-1 text-[11px] font-bold uppercase tracking-wider text-muted-foreground",
+ className,
+ )}
+ {...props}
+ />
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & {
- variant?: VariantProps<typeof selectItemVariants>["variant"];
- }
+ React.ElementRef<typeof SelectPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & {
+ variant?: VariantProps<typeof selectItemVariants>["variant"];
+ }
>(({ className, children, variant = "default", ...props }, ref) => (
- <SelectPrimitive.Item
- ref={ref}
- className={cn(selectItemVariants({ variant }), className)}
- {...props}
- >
- <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
- <SelectPrimitive.ItemIndicator>
- <Check className="h-4 w-4" />
- </SelectPrimitive.ItemIndicator>
- </span>
- <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
- </SelectPrimitive.Item>
+ <SelectPrimitive.Item
+ ref={ref}
+ className={cn(selectItemVariants({ variant }), "pr-8 pl-2", className)}
+ {...props}
+ >
+ <span className="absolute right-2 flex size-3.5 items-center justify-center">
+ <SelectPrimitive.ItemIndicator>
+ <Check className="size-4" />
+ </SelectPrimitive.ItemIndicator>
+ </span>
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
+ </SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.Separator>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
+ React.ElementRef<typeof SelectPrimitive.Separator>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
- <SelectPrimitive.Separator
- ref={ref}
- className={cn("-mx-1 my-1 h-px bg-foreground/10", className)}
- {...props}
- />
+ <SelectPrimitive.Separator
+ ref={ref}
+ className={cn("bg-border/60 mx-1 my-2 h-px", className)}
+ {...props}
+ />
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
- Select,
- SelectGroup,
- SelectValue,
- SelectTrigger,
- SelectContent,
- SelectLabel,
- SelectItem,
- SelectSeparator,
- SelectScrollUpButton,
- SelectScrollDownButton,
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
};
diff --git a/apps/web/src/components/ui/separator.tsx b/apps/web/src/components/ui/separator.tsx
index f0040a52..f3f93ad7 100644
--- a/apps/web/src/components/ui/separator.tsx
+++ b/apps/web/src/components/ui/separator.tsx
@@ -3,28 +3,28 @@
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
-import { cn } from "@/lib/utils";
+import { cn } from "@/utils/ui";
const Separator = React.forwardRef<
- React.ElementRef<typeof SeparatorPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
- (
- { className, orientation = "horizontal", decorative = true, ...props },
- ref
- ) => (
- <SeparatorPrimitive.Root
- ref={ref}
- decorative={decorative}
- orientation={orientation}
- className={cn(
- "shrink-0 bg-border",
- orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
- className
- )}
- {...props}
- />
- )
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref,
+ ) => (
+ <SeparatorPrimitive.Root
+ ref={ref}
+ decorative={decorative}
+ orientation={orientation}
+ className={cn(
+ "bg-border shrink-0",
+ orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
+ className,
+ )}
+ {...props}
+ />
+ ),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
diff --git a/apps/web/src/components/ui/sheet.tsx b/apps/web/src/components/ui/sheet.tsx
index f97bd438..3155787d 100644
--- a/apps/web/src/components/ui/sheet.tsx
+++ b/apps/web/src/components/ui/sheet.tsx
@@ -5,7 +5,7 @@ import { Dialog as SheetPrimitive } from "radix-ui";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const Sheet = SheetPrimitive.Root;
@@ -16,129 +16,129 @@ const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
- React.ElementRef<typeof SheetPrimitive.Overlay>,
- React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
+ React.ElementRef<typeof SheetPrimitive.Overlay>,
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
- <SheetPrimitive.Overlay
- className={cn(
- "fixed inset-0 z-200 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
- className
- )}
- {...props}
- ref={ref}
- />
+ <SheetPrimitive.Overlay
+ className={cn(
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-200 bg-black/50",
+ className,
+ )}
+ {...props}
+ ref={ref}
+ />
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
- "fixed z-250 gap-4 bg-background p-6 shadow-lg transition ease data-[state=closed]:duration-250 data-[state=open]:duration-250 data-[state=open]:animate-in data-[state=closed]:animate-out",
- {
- variants: {
- side: {
- top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
- bottom:
- "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
- left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
- right:
- "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
- },
- },
- defaultVariants: {
- side: "right",
- },
- }
+ "fixed z-250 gap-4 bg-background p-6 shadow-lg transition ease data-[state=closed]:duration-250 data-[state=open]:duration-250 data-[state=open]:animate-in data-[state=closed]:animate-out",
+ {
+ variants: {
+ side: {
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+ bottom:
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+ right:
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+ },
+ },
+ defaultVariants: {
+ side: "right",
+ },
+ },
);
interface SheetContentProps
- extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
- VariantProps<typeof sheetVariants> {}
+ extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
+ VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
- React.ElementRef<typeof SheetPrimitive.Content>,
- SheetContentProps
+ React.ElementRef<typeof SheetPrimitive.Content>,
+ SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
- <SheetPortal>
- <SheetOverlay />
- <SheetPrimitive.Content
- ref={ref}
- className={cn(sheetVariants({ side }), className)}
- onOpenAutoFocus={(e) => {
- e.preventDefault();
- e.stopPropagation();
- }}
- {...props}
- >
- <SheetPrimitive.Close className="absolute cursor-pointer right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
- <X className="size-5" />
- <span className="sr-only">Close</span>
- </SheetPrimitive.Close>
- {children}
- </SheetPrimitive.Content>
- </SheetPortal>
+ <SheetPortal>
+ <SheetOverlay />
+ <SheetPrimitive.Content
+ ref={ref}
+ className={cn(sheetVariants({ side }), className)}
+ onOpenAutoFocus={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ {...props}
+ >
+ <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 cursor-pointer rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
+ <X className="size-5" />
+ <span className="sr-only">Close</span>
+ </SheetPrimitive.Close>
+ {children}
+ </SheetPrimitive.Content>
+ </SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
- <div
- className={cn(
- "flex flex-col space-y-2 text-center sm:text-left",
- className
- )}
- {...props}
- />
+ <div
+ className={cn(
+ "flex flex-col space-y-2 text-center sm:text-left",
+ className,
+ )}
+ {...props}
+ />
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
- <div
- className={cn(
- "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
- className
- )}
- {...props}
- />
+ <div
+ className={cn(
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4",
+ className,
+ )}
+ {...props}
+ />
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
- React.ElementRef<typeof SheetPrimitive.Title>,
- React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
+ React.ElementRef<typeof SheetPrimitive.Title>,
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
- <SheetPrimitive.Title
- ref={ref}
- className={cn("text-lg font-semibold text-foreground", className)}
- {...props}
- />
+ <SheetPrimitive.Title
+ ref={ref}
+ className={cn("text-foreground text-lg font-semibold", className)}
+ {...props}
+ />
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
- React.ElementRef<typeof SheetPrimitive.Description>,
- React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
+ React.ElementRef<typeof SheetPrimitive.Description>,
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
- <SheetPrimitive.Description
- ref={ref}
- className={cn("text-sm text-muted-foreground", className)}
- {...props}
- />
+ <SheetPrimitive.Description
+ ref={ref}
+ className={cn("text-muted-foreground text-sm", className)}
+ {...props}
+ />
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
- Sheet,
- SheetPortal,
- SheetOverlay,
- SheetTrigger,
- SheetClose,
- SheetContent,
- SheetHeader,
- SheetFooter,
- SheetTitle,
- SheetDescription,
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
};
diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx
index 819116c3..3ac9d93a 100644
--- a/apps/web/src/components/ui/sidebar.tsx
+++ b/apps/web/src/components/ui/sidebar.tsx
@@ -1,763 +1,725 @@
"use client";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import { PanelLeftIcon } from "lucide-react";
import * as React from "react";
-import { Slot as SlotPrimitive } from "radix-ui";
-import { VariantProps, cva } from "class-variance-authority";
-import { PanelLeft } from "lucide-react";
-
-import { useIsMobile } from "../../hooks/use-mobile";
-import { cn } from "../../lib/utils";
-import { Button } from "./button";
-import { Input } from "./input";
-import { Separator } from "./separator";
-import { Sheet, SheetContent, SheetTitle } from "./sheet";
-import { Skeleton } from "./skeleton";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Separator } from "@/components/ui/separator";
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet";
+import { Skeleton } from "@/components/ui/skeleton";
import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "./tooltip";
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { useIsMobile } from "@/hooks/use-mobile";
+import { cn } from "@/utils/ui";
-const SIDEBAR_COOKIE_NAME = "sidebar:state";
+const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
-type SidebarContext = {
- state: "expanded" | "collapsed";
- open: boolean;
- setOpen: (open: boolean) => void;
- openMobile: boolean;
- setOpenMobile: (open: boolean) => void;
- isMobile: boolean;
- toggleSidebar: () => void;
+type SidebarContextProps = {
+ state: "expanded" | "collapsed";
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ openMobile: boolean;
+ setOpenMobile: (open: boolean) => void;
+ isMobile: boolean;
+ toggleSidebar: () => void;
};
-const SidebarContext = React.createContext<SidebarContext | null>(null);
+const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() {
- const context = React.useContext(SidebarContext);
- if (!context) {
- throw new Error("useSidebar must be used within a SidebarProvider.");
- }
+ const context = React.useContext(SidebarContext);
+ if (!context) {
+ throw new Error("useSidebar must be used within a SidebarProvider.");
+ }
- return context;
+ return context;
}
-const SidebarProvider = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div"> & {
- defaultOpen?: boolean;
- open?: boolean;
- onOpenChange?: (open: boolean) => void;
- }
->(
- (
- {
- defaultOpen = true,
- open: openProp,
- onOpenChange: setOpenProp,
- className,
- style,
- children,
- ...props
- },
- ref
- ) => {
- const isMobile = useIsMobile();
- const [openMobile, setOpenMobile] = React.useState(false);
+function SidebarProvider({
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & {
+ defaultOpen?: boolean;
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+}) {
+ const isMobile = useIsMobile();
+ const [openMobile, setOpenMobile] = React.useState(false);
- // This is the internal state of the sidebar.
- // We use openProp and setOpenProp for control from outside the component.
- const [_open, _setOpen] = React.useState(defaultOpen);
- const open = openProp ?? _open;
- const setOpen = React.useCallback(
- (value: boolean | ((value: boolean) => boolean)) => {
- const openState = typeof value === "function" ? value(open) : value;
- if (setOpenProp) {
- setOpenProp(openState);
- } else {
- _setOpen(openState);
- }
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen);
+ const open = openProp ?? _open;
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === "function" ? value(open) : value;
+ if (setOpenProp) {
+ setOpenProp(openState);
+ } else {
+ _setOpen(openState);
+ }
- // This sets the cookie to keep the sidebar state.
- document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
- },
- [setOpenProp, open]
- );
+ // This sets the cookie to keep the sidebar state.
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
+ },
+ [setOpenProp, open],
+ );
- // Helper to toggle the sidebar.
- const toggleSidebar = React.useCallback(() => {
- return isMobile
- ? setOpenMobile((open) => !open)
- : setOpen((open) => !open);
- }, [isMobile, setOpen, setOpenMobile]);
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
+ }, [isMobile, setOpen, setOpenMobile]);
- // Adds a keyboard shortcut to toggle the sidebar.
- React.useEffect(() => {
- const handleKeyDown = (event: KeyboardEvent) => {
- if (
- event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
- (event.metaKey || event.ctrlKey)
- ) {
- event.preventDefault();
- toggleSidebar();
- }
- };
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+ (event.metaKey || event.ctrlKey)
+ ) {
+ event.preventDefault();
+ toggleSidebar();
+ }
+ };
- window.addEventListener("keydown", handleKeyDown);
- return () => window.removeEventListener("keydown", handleKeyDown);
- }, [toggleSidebar]);
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [toggleSidebar]);
- // We add a state so that we can do data-state="expanded" or "collapsed".
- // This makes it easier to style the sidebar with Tailwind classes.
- const state = open ? "expanded" : "collapsed";
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? "expanded" : "collapsed";
- const contextValue = React.useMemo<SidebarContext>(
- () => ({
- state,
- open,
- setOpen,
- isMobile,
- openMobile,
- setOpenMobile,
- toggleSidebar,
- }),
- [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
- );
+ const contextValue = React.useMemo<SidebarContextProps>(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
+ );
- return (
- <SidebarContext.Provider value={contextValue}>
- <TooltipProvider delayDuration={0}>
- <div
- style={
- {
- "--sidebar-width": SIDEBAR_WIDTH,
- "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
- ...style,
- } as React.CSSProperties
- }
- className={cn(
- "group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
- className
- )}
- ref={ref}
- {...props}
- >
- {children}
- </div>
- </TooltipProvider>
- </SidebarContext.Provider>
- );
- }
-);
-SidebarProvider.displayName = "SidebarProvider";
+ return (
+ <SidebarContext.Provider value={contextValue}>
+ <TooltipProvider delayDuration={0}>
+ <div
+ data-slot="sidebar-wrapper"
+ style={
+ {
+ "--sidebar-width": SIDEBAR_WIDTH,
+ "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
+ ...style,
+ } as React.CSSProperties
+ }
+ className={cn(
+ "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+ </div>
+ </TooltipProvider>
+ </SidebarContext.Provider>
+ );
+}
-const Sidebar = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div"> & {
- side?: "left" | "right";
- variant?: "sidebar" | "floating" | "inset";
- collapsible?: "offcanvas" | "icon" | "none";
- }
->(
- (
- {
- side = "left",
- variant = "sidebar",
- collapsible = "offcanvas",
- className,
- children,
- ...props
- },
- ref
- ) => {
- const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
+function Sidebar({
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & {
+ side?: "left" | "right";
+ variant?: "sidebar" | "floating" | "inset";
+ collapsible?: "offcanvas" | "icon" | "none";
+}) {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
- if (collapsible === "none") {
- return (
- <div
- className={cn(
- "flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
- className
- )}
- ref={ref}
- {...props}
- >
- {children}
- </div>
- );
- }
+ if (collapsible === "none") {
+ return (
+ <div
+ data-slot="sidebar"
+ className={cn(
+ "bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+ </div>
+ );
+ }
- if (isMobile) {
- return (
- <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
- <SheetContent
- data-sidebar="sidebar"
- data-mobile="true"
- className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
- style={
- {
- "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
- } as React.CSSProperties
- }
- side={side}
- >
- <SheetTitle className="sr-only">Navigation Menu</SheetTitle>
- <div className="flex h-full w-full flex-col">{children}</div>
- </SheetContent>
- </Sheet>
- );
- }
+ if (isMobile) {
+ return (
+ <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
+ <SheetContent
+ data-sidebar="sidebar"
+ data-slot="sidebar"
+ data-mobile="true"
+ className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
+ style={
+ {
+ "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
+ } as React.CSSProperties
+ }
+ side={side}
+ >
+ <SheetHeader className="sr-only">
+ <SheetTitle>Sidebar</SheetTitle>
+ <SheetDescription>Displays the mobile sidebar.</SheetDescription>
+ </SheetHeader>
+ <div className="flex size-full flex-col">{children}</div>
+ </SheetContent>
+ </Sheet>
+ );
+ }
- return (
- <div
- ref={ref}
- className="group peer hidden md:block text-sidebar-foreground"
- data-state={state}
- data-collapsible={state === "collapsed" ? collapsible : ""}
- data-variant={variant}
- data-side={side}
- >
- {/* This is what handles the sidebar gap on desktop */}
- <div
- className={cn(
- "duration-200 relative h-full w-(--sidebar-width) bg-transparent transition-[width] ease-linear",
- "group-data-[collapsible=offcanvas]:w-0",
- "group-data-[side=right]:rotate-180",
- variant === "floating" || variant === "inset"
- ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
- : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
- )}
- />
- <div
- className={cn(
- "duration-200 fixed h-[calc(100vh-4rem)] top-16 z-10 hidden w-(--sidebar-width) transition-[left,right,width] ease-linear md:flex",
- side === "left"
- ? "left-0 border-r group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
- : "right-0 border-l group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
- variant === "floating" || variant === "inset"
- ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
- : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
- className
- )}
- {...props}
- >
- <div
- data-sidebar="sidebar"
- className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm"
- >
- {children}
- </div>
- </div>
- </div>
- );
- }
-);
-Sidebar.displayName = "Sidebar";
+ return (
+ <div
+ className="group peer text-sidebar-foreground hidden md:block"
+ data-state={state}
+ data-collapsible={state === "collapsed" ? collapsible : ""}
+ data-variant={variant}
+ data-side={side}
+ data-slot="sidebar"
+ >
+ {/* This is what handles the sidebar gap on desktop */}
+ <div
+ data-slot="sidebar-gap"
+ className={cn(
+ "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
+ "group-data-[collapsible=offcanvas]:w-0",
+ "group-data-[side=right]:rotate-180",
+ variant === "floating" || variant === "inset"
+ ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
+ : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
+ )}
+ />
+ <div
+ data-slot="sidebar-container"
+ className={cn(
+ "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
+ side === "left"
+ ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
+ : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
+ // Adjust the padding for floating and inset variants.
+ variant === "floating" || variant === "inset"
+ ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
+ : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
+ className,
+ )}
+ {...props}
+ >
+ <div
+ data-sidebar="sidebar"
+ data-slot="sidebar-inner"
+ className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex size-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
+ >
+ {children}
+ </div>
+ </div>
+ </div>
+ );
+}
-const SidebarTrigger = React.forwardRef<
- React.ElementRef<typeof Button>,
- React.ComponentProps<typeof Button>
->(({ className, onClick, ...props }, ref) => {
- const { toggleSidebar } = useSidebar();
+function SidebarTrigger({
+ className,
+ onClick,
+ ...props
+}: React.ComponentProps<typeof Button>) {
+ const { toggleSidebar } = useSidebar();
- return (
- <Button
- ref={ref}
- data-sidebar="trigger"
- variant="text"
- size="icon"
- className={cn("h-8 w-8", className)}
- onClick={(event) => {
- onClick?.(event);
- toggleSidebar();
- }}
- {...props}
- >
- <PanelLeft />
- <span className="sr-only">Toggle Sidebar</span>
- </Button>
- );
-});
-SidebarTrigger.displayName = "SidebarTrigger";
+ return (
+ <Button
+ data-sidebar="trigger"
+ data-slot="sidebar-trigger"
+ variant="ghost"
+ size="icon"
+ className={cn("size-7", className)}
+ onClick={(event) => {
+ onClick?.(event);
+ toggleSidebar();
+ }}
+ {...props}
+ >
+ <PanelLeftIcon />
+ <span className="sr-only">Toggle Sidebar</span>
+ </Button>
+ );
+}
-const SidebarRail = React.forwardRef<
- HTMLButtonElement,
- React.ComponentProps<"button">
->(({ className, ...props }, ref) => {
- const { toggleSidebar } = useSidebar();
+function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
+ const { toggleSidebar } = useSidebar();
- return (
- <button
- ref={ref}
- data-sidebar="rail"
- aria-label="Toggle Sidebar"
- tabIndex={-1}
- onClick={toggleSidebar}
- title="Toggle Sidebar"
- className={cn(
- "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
- "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
- "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
- "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",
- "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
- "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
- className
- )}
- {...props}
- />
- );
-});
-SidebarRail.displayName = "SidebarRail";
+ return (
+ <button
+ data-sidebar="rail"
+ data-slot="sidebar-rail"
+ aria-label="Toggle Sidebar"
+ tabIndex={-1}
+ onClick={toggleSidebar}
+ title="Toggle Sidebar"
+ className={cn(
+ "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
+ "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
+ "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
+ "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
+ "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
+ "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
-const SidebarInset = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"main">
->(({ className, ...props }, ref) => {
- return (
- <main
- ref={ref}
- className={cn(
- "relative flex min-h-svh flex-1 flex-col bg-background",
- "peer-data-[variant=inset]:min-h-[calc(100svh-(--spacing(4)))] md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm",
- className
- )}
- {...props}
- />
- );
-});
-SidebarInset.displayName = "SidebarInset";
+function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
+ return (
+ <main
+ data-slot="sidebar-inset"
+ className={cn(
+ "bg-background relative flex w-full flex-1 flex-col",
+ "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
-const SidebarInput = React.forwardRef<
- React.ElementRef<typeof Input>,
- React.ComponentProps<typeof Input>
->(({ className, ...props }, ref) => {
- return (
- <Input
- ref={ref}
- data-sidebar="input"
- className={cn(
- "h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
- className
- )}
- {...props}
- />
- );
-});
-SidebarInput.displayName = "SidebarInput";
+function SidebarInput({
+ className,
+ ...props
+}: React.ComponentProps<typeof Input>) {
+ return (
+ <Input
+ data-slot="sidebar-input"
+ data-sidebar="input"
+ className={cn("bg-background h-8 w-full shadow-none", className)}
+ {...props}
+ />
+ );
+}
-const SidebarHeader = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div">
->(({ className, ...props }, ref) => {
- return (
- <div
- ref={ref}
- data-sidebar="header"
- className={cn("flex flex-col gap-2 p-2", className)}
- {...props}
- />
- );
-});
-SidebarHeader.displayName = "SidebarHeader";
+function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="sidebar-header"
+ data-sidebar="header"
+ className={cn("flex flex-col gap-2 p-2", className)}
+ {...props}
+ />
+ );
+}
-const SidebarFooter = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div">
->(({ className, ...props }, ref) => {
- return (
- <div
- ref={ref}
- data-sidebar="footer"
- className={cn("flex flex-col gap-2 p-2", className)}
- {...props}
- />
- );
-});
-SidebarFooter.displayName = "SidebarFooter";
+function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="sidebar-footer"
+ data-sidebar="footer"
+ className={cn("flex flex-col gap-2 p-2", className)}
+ {...props}
+ />
+ );
+}
-const SidebarSeparator = React.forwardRef<
- React.ElementRef<typeof Separator>,
- React.ComponentProps<typeof Separator>
->(({ className, ...props }, ref) => {
- return (
- <Separator
- ref={ref}
- data-sidebar="separator"
- className={cn("mx-2 w-auto bg-sidebar-border", className)}
- {...props}
- />
- );
-});
-SidebarSeparator.displayName = "SidebarSeparator";
+function SidebarSeparator({
+ className,
+ ...props
+}: React.ComponentProps<typeof Separator>) {
+ return (
+ <Separator
+ data-slot="sidebar-separator"
+ data-sidebar="separator"
+ className={cn("bg-sidebar-border mx-2 w-auto", className)}
+ {...props}
+ />
+ );
+}
-const SidebarContent = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div">
->(({ className, ...props }, ref) => {
- return (
- <div
- ref={ref}
- data-sidebar="content"
- className={cn(
- "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
- className
- )}
- {...props}
- />
- );
-});
-SidebarContent.displayName = "SidebarContent";
+function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="sidebar-content"
+ data-sidebar="content"
+ className={cn(
+ "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
-const SidebarGroup = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div">
->(({ className, ...props }, ref) => {
- return (
- <div
- ref={ref}
- data-sidebar="group"
- className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
- {...props}
- />
- );
-});
-SidebarGroup.displayName = "SidebarGroup";
+function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="sidebar-group"
+ data-sidebar="group"
+ className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
+ {...props}
+ />
+ );
+}
-const SidebarGroupLabel = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div"> & { asChild?: boolean }
->(({ className, asChild = false, ...props }, ref) => {
- const Comp = asChild ? SlotPrimitive.Slot : "div";
+function SidebarGroupLabel({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"div"> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "div";
- return (
- <Comp
- ref={ref as any}
- data-sidebar="group-label"
- className={cn(
- "duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-hidden ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
- "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
- className
- )}
- {...props}
- />
- );
-});
-SidebarGroupLabel.displayName = "SidebarGroupLabel";
+ return (
+ <Comp
+ data-slot="sidebar-group-label"
+ data-sidebar="group-label"
+ className={cn(
+ "text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
-const SidebarGroupAction = React.forwardRef<
- HTMLButtonElement,
- React.ComponentProps<"button"> & { asChild?: boolean }
->(({ className, asChild = false, ...props }, ref) => {
- const Comp = asChild ? SlotPrimitive.Slot : "button";
+function SidebarGroupAction({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "button";
- return (
- <Comp
- ref={ref as any}
- data-sidebar="group-action"
- className={cn(
- "absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
- // Increases the hit area of the button on mobile.
- "after:absolute after:-inset-2 md:after:hidden",
- "group-data-[collapsible=icon]:hidden",
- className
- )}
- {...props}
- />
- );
-});
-SidebarGroupAction.displayName = "SidebarGroupAction";
+ return (
+ <Comp
+ data-slot="sidebar-group-action"
+ data-sidebar="group-action"
+ className={cn(
+ "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 md:after:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
-const SidebarGroupContent = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div">
->(({ className, ...props }, ref) => (
- <div
- ref={ref}
- data-sidebar="group-content"
- className={cn("w-full text-sm", className)}
- {...props}
- />
-));
-SidebarGroupContent.displayName = "SidebarGroupContent";
+function SidebarGroupContent({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="sidebar-group-content"
+ data-sidebar="group-content"
+ className={cn("w-full text-sm", className)}
+ {...props}
+ />
+ );
+}
-const SidebarMenu = React.forwardRef<
- HTMLUListElement,
- React.ComponentProps<"ul">
->(({ className, ...props }, ref) => (
- <ul
- ref={ref}
- data-sidebar="menu"
- className={cn("flex w-full min-w-0 flex-col gap-1", className)}
- {...props}
- />
-));
-SidebarMenu.displayName = "SidebarMenu";
+function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+ <ul
+ data-slot="sidebar-menu"
+ data-sidebar="menu"
+ className={cn("flex w-full min-w-0 flex-col gap-1", className)}
+ {...props}
+ />
+ );
+}
-const SidebarMenuItem = React.forwardRef<
- HTMLLIElement,
- React.ComponentProps<"li">
->(({ className, ...props }, ref) => (
- <li
- ref={ref}
- data-sidebar="menu-item"
- className={cn("group/menu-item relative", className)}
- {...props}
- />
-));
-SidebarMenuItem.displayName = "SidebarMenuItem";
+function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
+ return (
+ <li
+ data-slot="sidebar-menu-item"
+ data-sidebar="menu-item"
+ className={cn("group/menu-item relative", className)}
+ {...props}
+ />
+ );
+}
const sidebarMenuButtonVariants = cva(
- "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
- {
- variants: {
- variant: {
- default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
- outline:
- "bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]",
- },
- size: {
- default: "h-8 text-sm",
- sm: "h-7 text-xs",
- lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
);
-const SidebarMenuButton = React.forwardRef<
- HTMLButtonElement,
- React.ComponentProps<"button"> & {
- asChild?: boolean;
- isActive?: boolean;
- tooltip?: string | React.ComponentProps<typeof TooltipContent>;
- } & VariantProps<typeof sidebarMenuButtonVariants>
->(
- (
- {
- asChild = false,
- isActive = false,
- variant = "default",
- size = "default",
- tooltip,
- className,
- ...props
- },
- ref
- ) => {
- const Comp = asChild ? SlotPrimitive.Slot : "button";
- const { isMobile, state } = useSidebar();
+function SidebarMenuButton({
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+}: React.ComponentProps<"button"> & {
+ asChild?: boolean;
+ isActive?: boolean;
+ tooltip?: string | React.ComponentProps<typeof TooltipContent>;
+} & VariantProps<typeof sidebarMenuButtonVariants>) {
+ const Comp = asChild ? Slot : "button";
+ const { isMobile, state } = useSidebar();
- const button = (
- <Comp
- ref={ref as any}
- data-sidebar="menu-button"
- data-size={size}
- data-active={isActive}
- className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
- {...props}
- />
- );
+ const button = (
+ <Comp
+ data-slot="sidebar-menu-button"
+ data-sidebar="menu-button"
+ data-size={size}
+ data-active={isActive}
+ className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
+ {...props}
+ />
+ );
- if (!tooltip) {
- return button;
- }
+ if (!tooltip) {
+ return button;
+ }
- if (typeof tooltip === "string") {
- tooltip = {
- children: tooltip,
- };
- }
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ };
+ }
- return (
- <Tooltip>
- <TooltipTrigger asChild>{button}</TooltipTrigger>
- <TooltipContent
- side="right"
- align="center"
- hidden={state !== "collapsed" || isMobile}
- {...tooltip}
- />
- </Tooltip>
- );
- }
-);
-SidebarMenuButton.displayName = "SidebarMenuButton";
+ return (
+ <Tooltip>
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
+ <TooltipContent
+ side="right"
+ align="center"
+ hidden={state !== "collapsed" || isMobile}
+ {...tooltip}
+ />
+ </Tooltip>
+ );
+}
-const SidebarMenuAction = React.forwardRef<
- HTMLButtonElement,
- React.ComponentProps<"button"> & {
- asChild?: boolean;
- showOnHover?: boolean;
- }
->(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
- const Comp = asChild ? SlotPrimitive.Slot : "button";
+function SidebarMenuAction({
+ className,
+ asChild = false,
+ showOnHover = false,
+ ...props
+}: React.ComponentProps<"button"> & {
+ asChild?: boolean;
+ showOnHover?: boolean;
+}) {
+ const Comp = asChild ? Slot : "button";
- return (
- <Comp
- ref={ref as any}
- data-sidebar="menu-action"
- className={cn(
- "absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
- // Increases the hit area of the button on mobile.
- "after:absolute after:-inset-2 md:after:hidden",
- "peer-data-[size=sm]/menu-button:top-1",
- "peer-data-[size=default]/menu-button:top-1.5",
- "peer-data-[size=lg]/menu-button:top-2.5",
- "group-data-[collapsible=icon]:hidden",
- showOnHover &&
- "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
- className
- )}
- {...props}
- />
- );
-});
-SidebarMenuAction.displayName = "SidebarMenuAction";
+ return (
+ <Comp
+ data-slot="sidebar-menu-action"
+ data-sidebar="menu-action"
+ className={cn(
+ "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 md:after:hidden",
+ "peer-data-[size=sm]/menu-button:top-1",
+ "peer-data-[size=default]/menu-button:top-1.5",
+ "peer-data-[size=lg]/menu-button:top-2.5",
+ "group-data-[collapsible=icon]:hidden",
+ showOnHover &&
+ "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
-const SidebarMenuBadge = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div">
->(({ className, ...props }, ref) => (
- <div
- ref={ref}
- data-sidebar="menu-badge"
- className={cn(
- "absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none",
- "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
- "peer-data-[size=sm]/menu-button:top-1",
- "peer-data-[size=default]/menu-button:top-1.5",
- "peer-data-[size=lg]/menu-button:top-2.5",
- "group-data-[collapsible=icon]:hidden",
- className
- )}
- {...props}
- />
-));
-SidebarMenuBadge.displayName = "SidebarMenuBadge";
+function SidebarMenuBadge({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="sidebar-menu-badge"
+ data-sidebar="menu-badge"
+ className={cn(
+ "text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
+ "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
+ "peer-data-[size=sm]/menu-button:top-1",
+ "peer-data-[size=default]/menu-button:top-1.5",
+ "peer-data-[size=lg]/menu-button:top-2.5",
+ "group-data-[collapsible=icon]:hidden",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
-const SidebarMenuSkeleton = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div"> & {
- showIcon?: boolean;
- }
->(({ className, showIcon = false, ...props }, ref) => {
- // Random width between 50 to 90%.
- const width = React.useMemo(() => {
- return `${Math.floor(Math.random() * 40) + 50}%`;
- }, []);
+function SidebarMenuSkeleton({
+ className,
+ showIcon = false,
+ ...props
+}: React.ComponentProps<"div"> & {
+ showIcon?: boolean;
+}) {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`;
+ }, []);
- return (
- <div
- ref={ref}
- data-sidebar="menu-skeleton"
- className={cn("rounded-md h-8 flex gap-2 px-2 items-center", className)}
- {...props}
- >
- {showIcon && (
- <Skeleton
- className="size-4 rounded-md"
- data-sidebar="menu-skeleton-icon"
- />
- )}
- <Skeleton
- className="h-4 flex-1 max-w-(--skeleton-width)"
- data-sidebar="menu-skeleton-text"
- style={
- {
- "--skeleton-width": width,
- } as React.CSSProperties
- }
- />
- </div>
- );
-});
-SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
+ return (
+ <div
+ data-slot="sidebar-menu-skeleton"
+ data-sidebar="menu-skeleton"
+ className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
+ {...props}
+ >
+ {showIcon && (
+ <Skeleton
+ className="size-4 rounded-md"
+ data-sidebar="menu-skeleton-icon"
+ />
+ )}
+ <Skeleton
+ className="h-4 max-w-(--skeleton-width) flex-1"
+ data-sidebar="menu-skeleton-text"
+ style={
+ {
+ "--skeleton-width": width,
+ } as React.CSSProperties
+ }
+ />
+ </div>
+ );
+}
-const SidebarMenuSub = React.forwardRef<
- HTMLUListElement,
- React.ComponentProps<"ul">
->(({ className, ...props }, ref) => (
- <ul
- ref={ref}
- data-sidebar="menu-sub"
- className={cn(
- "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
- "group-data-[collapsible=icon]:hidden",
- className
- )}
- {...props}
- />
-));
-SidebarMenuSub.displayName = "SidebarMenuSub";
+function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+ <ul
+ data-slot="sidebar-menu-sub"
+ data-sidebar="menu-sub"
+ className={cn(
+ "border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
+ "group-data-[collapsible=icon]:hidden",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
-const SidebarMenuSubItem = React.forwardRef<
- HTMLLIElement,
- React.ComponentProps<"li">
->(({ ...props }, ref) => <li ref={ref} {...props} />);
-SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
+function SidebarMenuSubItem({
+ className,
+ ...props
+}: React.ComponentProps<"li">) {
+ return (
+ <li
+ data-slot="sidebar-menu-sub-item"
+ data-sidebar="menu-sub-item"
+ className={cn("group/menu-sub-item relative", className)}
+ {...props}
+ />
+ );
+}
-const SidebarMenuSubButton = React.forwardRef<
- HTMLAnchorElement,
- React.ComponentProps<"a"> & {
- asChild?: boolean;
- size?: "sm" | "md";
- isActive?: boolean;
- }
->(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
- const Comp = asChild ? SlotPrimitive.Slot : "a";
+function SidebarMenuSubButton({
+ asChild = false,
+ size = "md",
+ isActive = false,
+ className,
+ ...props
+}: React.ComponentProps<"a"> & {
+ asChild?: boolean;
+ size?: "sm" | "md";
+ isActive?: boolean;
+}) {
+ const Comp = asChild ? Slot : "a";
- return (
- <Comp
- ref={ref as any}
- data-sidebar="menu-sub-button"
- data-size={size}
- data-active={isActive}
- className={cn(
- "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
- "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
- size === "sm" && "text-xs",
- size === "md" && "text-sm",
- "group-data-[collapsible=icon]:hidden",
- className
- )}
- {...props}
- />
- );
-});
-SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
+ return (
+ <Comp
+ data-slot="sidebar-menu-sub-button"
+ data-sidebar="menu-sub-button"
+ data-size={size}
+ data-active={isActive}
+ className={cn(
+ "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+ size === "sm" && "text-xs",
+ size === "md" && "text-sm",
+ "group-data-[collapsible=icon]:hidden",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
export {
- Sidebar,
- SidebarContent,
- SidebarFooter,
- SidebarGroup,
- SidebarGroupAction,
- SidebarGroupContent,
- SidebarGroupLabel,
- SidebarHeader,
- SidebarInput,
- SidebarInset,
- SidebarMenu,
- SidebarMenuAction,
- SidebarMenuBadge,
- SidebarMenuButton,
- SidebarMenuItem,
- SidebarMenuSkeleton,
- SidebarMenuSub,
- SidebarMenuSubButton,
- SidebarMenuSubItem,
- SidebarProvider,
- SidebarRail,
- SidebarSeparator,
- SidebarTrigger,
- useSidebar,
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
};
diff --git a/apps/web/src/components/ui/skeleton.tsx b/apps/web/src/components/ui/skeleton.tsx
index 4525c3a0..bf59fec2 100644
--- a/apps/web/src/components/ui/skeleton.tsx
+++ b/apps/web/src/components/ui/skeleton.tsx
@@ -1,15 +1,15 @@
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
function Skeleton({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes<HTMLDivElement>) {
- return (
- <div
- className={cn("animate-pulse rounded-md bg-primary/10", className)}
- {...props}
- />
- );
+ return (
+ <div
+ className={cn("bg-primary/10 animate-pulse rounded-md", className)}
+ {...props}
+ />
+ );
}
export { Skeleton };
diff --git a/apps/web/src/components/ui/slider.tsx b/apps/web/src/components/ui/slider.tsx
index d4afdd06..994159a1 100644
--- a/apps/web/src/components/ui/slider.tsx
+++ b/apps/web/src/components/ui/slider.tsx
@@ -3,25 +3,25 @@
import * as React from "react";
import { Slider as SliderPrimitive } from "radix-ui";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const Slider = React.forwardRef<
- React.ElementRef<typeof SliderPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
+ React.ElementRef<typeof SliderPrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
- <SliderPrimitive.Root
- ref={ref}
- className={cn(
- "relative flex w-full touch-none select-none items-center",
- className
- )}
- {...props}
- >
- <SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
- <SliderPrimitive.Range className="absolute h-full bg-primary" />
- </SliderPrimitive.Track>
- <SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow-sm transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
- </SliderPrimitive.Root>
+ <SliderPrimitive.Root
+ ref={ref}
+ className={cn(
+ "relative flex w-full touch-none items-center select-none",
+ className,
+ )}
+ {...props}
+ >
+ <SliderPrimitive.Track className="bg-primary/20 relative h-1.5 w-full grow overflow-hidden rounded-full">
+ <SliderPrimitive.Range className="bg-primary absolute h-full" />
+ </SliderPrimitive.Track>
+ <SliderPrimitive.Thumb className="border-primary/50 bg-background focus-visible:ring-ring block size-4 rounded-full border shadow-sm focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" />
+ </SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
diff --git a/apps/web/src/components/ui/sonner.tsx b/apps/web/src/components/ui/sonner.tsx
index 13694b43..4bae2fad 100644
--- a/apps/web/src/components/ui/sonner.tsx
+++ b/apps/web/src/components/ui/sonner.tsx
@@ -6,30 +6,30 @@ import { Toaster as Sonner } from "sonner";
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
- const { theme = "system" } = useTheme();
+ const { theme = "system" } = useTheme();
- return (
- <Sonner
- theme={theme as ToasterProps["theme"]}
- className="toaster group"
- position="top-center"
- offset={20}
- toastOptions={{
- classNames: {
- toast:
- "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
- description: "group-[.toast]:text-muted-foreground",
- actionButton:
- "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
- cancelButton:
- "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
- },
- }}
- expand={false}
- richColors
- {...props}
- />
- );
+ return (
+ <Sonner
+ theme={theme as ToasterProps["theme"]}
+ className="toaster group"
+ position="top-center"
+ offset={20}
+ toastOptions={{
+ classNames: {
+ toast:
+ "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
+ description: "group-[.toast]:text-muted-foreground",
+ actionButton:
+ "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
+ cancelButton:
+ "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
+ },
+ }}
+ expand={false}
+ richColors
+ {...props}
+ />
+ );
};
export { Toaster };
diff --git a/apps/web/src/components/ui/spinner.tsx b/apps/web/src/components/ui/spinner.tsx
new file mode 100644
index 00000000..a46792f7
--- /dev/null
+++ b/apps/web/src/components/ui/spinner.tsx
@@ -0,0 +1,17 @@
+import { HugeiconsIcon, type HugeiconsIconProps } from "@hugeicons/react";
+import { Loading03Icon } from "@hugeicons/core-free-icons";
+import { cn } from "@/utils/ui";
+
+function Spinner({ className, ...props }: Omit<HugeiconsIconProps, "icon">) {
+ return (
+ <HugeiconsIcon
+ icon={Loading03Icon}
+ role="status"
+ aria-label="Loading"
+ className={cn("size-4 animate-spin", className)}
+ {...props}
+ />
+ )
+}
+
+export { Spinner }
diff --git a/apps/web/src/components/ui/split-button.tsx b/apps/web/src/components/ui/split-button.tsx
index ca3b518c..26e54cab 100644
--- a/apps/web/src/components/ui/split-button.tsx
+++ b/apps/web/src/components/ui/split-button.tsx
@@ -1,89 +1,89 @@
-import { Button, ButtonProps } from "@/components/ui/button";
+import { Button, type ButtonProps } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
-import { ReactNode, forwardRef } from "react";
-import { cn } from "@/lib/utils";
+import { type ReactNode, forwardRef } from "react";
+import { cn } from "@/utils/ui";
interface SplitButtonProps {
- children: ReactNode;
- className?: string;
+ children: ReactNode;
+ className?: string;
}
interface SplitButtonSideProps extends Omit<ButtonProps, "variant" | "size"> {
- children: ReactNode;
+ children: ReactNode;
}
const SplitButton = forwardRef<HTMLDivElement, SplitButtonProps>(
- ({ children, className, ...props }, ref) => {
- return (
- <div
- ref={ref}
- className={cn(
- "inline-flex rounded-full h-7 border border-input bg-panel-accent overflow-hidden",
- className
- )}
- {...props}
- >
- {children}
- </div>
- );
- }
+ ({ children, className, ...props }, ref) => {
+ return (
+ <div
+ ref={ref}
+ className={cn(
+ "border-input bg-panel-accent inline-flex h-7 overflow-hidden rounded-lg border",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+ </div>
+ );
+ },
);
SplitButton.displayName = "SplitButton";
const SplitButtonSide = forwardRef<
- HTMLButtonElement,
- SplitButtonSideProps & { paddingClass: string }
+ HTMLButtonElement,
+ SplitButtonSideProps & { paddingClass: string }
>(({ children, className, paddingClass, onClick, ...props }, ref) => {
- return (
- <Button
- ref={ref}
- variant="text"
- className={cn(
- "h-full rounded-none bg-panel-accent !opacity-100 border-0 gap-0 font-normal transition-colors disabled:text-muted-foreground",
- onClick
- ? "hover:bg-foreground/10 hover:opacity-100 cursor-pointer"
- : "cursor-default select-text",
- paddingClass,
- className
- )}
- onClick={onClick}
- {...props}
- >
- {typeof children === "string" ? (
- <span className="font-normal cursor-text">{children}</span>
- ) : (
- children
- )}
- </Button>
- );
+ return (
+ <Button
+ ref={ref}
+ variant="text"
+ className={cn(
+ "bg-panel-accent disabled:text-muted-foreground h-full gap-0 rounded-none border-0 font-normal !opacity-100",
+ onClick
+ ? "hover:bg-foreground/10 cursor-pointer hover:opacity-100"
+ : "cursor-default select-text",
+ paddingClass,
+ className,
+ )}
+ onClick={onClick}
+ {...props}
+ >
+ {typeof children === "string" ? (
+ <span className="cursor-text font-normal">{children}</span>
+ ) : (
+ children
+ )}
+ </Button>
+ );
});
SplitButtonSide.displayName = "SplitButtonSide";
const SplitButtonLeft = forwardRef<HTMLButtonElement, SplitButtonSideProps>(
- ({ ...props }, ref) => {
- return <SplitButtonSide ref={ref} paddingClass="pl-3 pr-2" {...props} />;
- }
+ ({ ...props }, ref) => {
+ return <SplitButtonSide ref={ref} paddingClass="pl-3 pr-2" {...props} />;
+ },
);
SplitButtonLeft.displayName = "SplitButtonLeft";
const SplitButtonRight = forwardRef<HTMLButtonElement, SplitButtonSideProps>(
- ({ ...props }, ref) => {
- return <SplitButtonSide ref={ref} paddingClass="pl-2 pr-3" {...props} />;
- }
+ ({ ...props }, ref) => {
+ return <SplitButtonSide ref={ref} paddingClass="pl-2 pr-3" {...props} />;
+ },
);
SplitButtonRight.displayName = "SplitButtonRight";
const SplitButtonSeparator = forwardRef<HTMLDivElement, { className?: string }>(
- ({ className, ...props }, ref) => {
- return (
- <Separator
- ref={ref}
- orientation="vertical"
- className={cn("h-full bg-foreground/15", className)}
- {...props}
- />
- );
- }
+ ({ className, ...props }, ref) => {
+ return (
+ <Separator
+ ref={ref}
+ orientation="vertical"
+ className={cn("bg-foreground/15 h-full", className)}
+ {...props}
+ />
+ );
+ },
);
SplitButtonSeparator.displayName = "SplitButtonSeparator";
diff --git a/apps/web/src/components/ui/sponsor-button.tsx b/apps/web/src/components/ui/sponsor-button.tsx
deleted file mode 100644
index 146566a6..00000000
--- a/apps/web/src/components/ui/sponsor-button.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { motion } from "motion/react";
-import { ComponentType } from "react";
-
-interface SponsorButtonProps {
- href: string;
- logo: ComponentType<{ className?: string }>;
- companyName: string;
- className?: string;
-}
-
-export function SponsorButton({
- href,
- logo: Logo,
- companyName,
- className = "",
-}: SponsorButtonProps) {
- return (
- <motion.a
- href={href}
- target="_blank"
- rel="noopener noreferrer"
- className={`inline-flex items-center gap-2 px-3 py-2 rounded-md border border-border bg-background/5 backdrop-blur-xs hover:bg-background/10 transition-all duration-200 group shadow-lg ${className}`}
- >
- <span className="text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors">
- Sponsored by
- </span>
-
- <div className="flex items-center gap-1.5">
- <div className="text-foreground/90 group-hover:text-foreground transition-colors">
- <Logo className="w-4 h-4" />
- </div>
-
- <span className="text-xs font-medium text-foreground/90 group-hover:text-foreground transition-colors">
- {companyName}
- </span>
- </div>
- </motion.a>
- );
-}
diff --git a/apps/web/src/components/ui/switch.tsx b/apps/web/src/components/ui/switch.tsx
index f210afdf..c0207804 100644
--- a/apps/web/src/components/ui/switch.tsx
+++ b/apps/web/src/components/ui/switch.tsx
@@ -3,26 +3,26 @@
import * as React from "react";
import { Switch as SwitchPrimitives } from "radix-ui";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const Switch = React.forwardRef<
- React.ElementRef<typeof SwitchPrimitives.Root>,
- React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
+ React.ElementRef<typeof SwitchPrimitives.Root>,
+ React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
- <SwitchPrimitives.Root
- className={cn(
- "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-xs transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
- className
- )}
- {...props}
- ref={ref}
- >
- <SwitchPrimitives.Thumb
- className={cn(
- "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
- )}
- />
- </SwitchPrimitives.Root>
+ <SwitchPrimitives.Root
+ className={cn(
+ "peer focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-xs focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
+ className,
+ )}
+ {...props}
+ ref={ref}
+ >
+ <SwitchPrimitives.Thumb
+ className={cn(
+ "bg-background pointer-events-none block size-4 rounded-full shadow-lg ring-0 data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
+ )}
+ />
+ </SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
diff --git a/apps/web/src/components/ui/table.tsx b/apps/web/src/components/ui/table.tsx
index 7481cabb..7e8541ce 100644
--- a/apps/web/src/components/ui/table.tsx
+++ b/apps/web/src/components/ui/table.tsx
@@ -1,120 +1,120 @@
import * as React from "react";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const Table = React.forwardRef<
- HTMLTableElement,
- React.HTMLAttributes<HTMLTableElement>
+ HTMLTableElement,
+ React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
- <div className="relative w-full overflow-auto">
- <table
- ref={ref}
- className={cn("w-full caption-bottom text-sm", className)}
- {...props}
- />
- </div>
+ <div className="relative w-full overflow-auto">
+ <table
+ ref={ref}
+ className={cn("w-full caption-bottom text-sm", className)}
+ {...props}
+ />
+ </div>
));
Table.displayName = "Table";
const TableHeader = React.forwardRef<
- HTMLTableSectionElement,
- React.HTMLAttributes<HTMLTableSectionElement>
+ HTMLTableSectionElement,
+ React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
- <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
+ <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<
- HTMLTableSectionElement,
- React.HTMLAttributes<HTMLTableSectionElement>
+ HTMLTableSectionElement,
+ React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
- <tbody
- ref={ref}
- className={cn("[&_tr:last-child]:border-0", className)}
- {...props}
- />
+ <tbody
+ ref={ref}
+ className={cn("[&_tr:last-child]:border-0", className)}
+ {...props}
+ />
));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<
- HTMLTableSectionElement,
- React.HTMLAttributes<HTMLTableSectionElement>
+ HTMLTableSectionElement,
+ React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
- <tfoot
- ref={ref}
- className={cn(
- "border-t bg-muted/50 font-medium last:[&>tr]:border-b-0",
- className
- )}
- {...props}
- />
+ <tfoot
+ ref={ref}
+ className={cn(
+ "bg-muted/50 border-t font-medium last:[&>tr]:border-b-0",
+ className,
+ )}
+ {...props}
+ />
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
- HTMLTableRowElement,
- React.HTMLAttributes<HTMLTableRowElement>
+ HTMLTableRowElement,
+ React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
- <tr
- ref={ref}
- className={cn(
- "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
- className
- )}
- {...props}
- />
+ <tr
+ ref={ref}
+ className={cn(
+ "hover:bg-muted/50 data-[state=selected]:bg-muted border-b",
+ className,
+ )}
+ {...props}
+ />
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
- HTMLTableCellElement,
- React.ThHTMLAttributes<HTMLTableCellElement>
+ HTMLTableCellElement,
+ React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
- <th
- ref={ref}
- className={cn(
- "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
- className
- )}
- {...props}
- />
+ <th
+ ref={ref}
+ className={cn(
+ "text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
+ className,
+ )}
+ {...props}
+ />
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
- HTMLTableCellElement,
- React.TdHTMLAttributes<HTMLTableCellElement>
+ HTMLTableCellElement,
+ React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
- <td
- ref={ref}
- className={cn(
- "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
- className
- )}
- {...props}
- />
+ <td
+ ref={ref}
+ className={cn(
+ "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
+ className,
+ )}
+ {...props}
+ />
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
- HTMLTableCaptionElement,
- React.HTMLAttributes<HTMLTableCaptionElement>
+ HTMLTableCaptionElement,
+ React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
- <caption
- ref={ref}
- className={cn("mt-4 text-sm text-muted-foreground", className)}
- {...props}
- />
+ <caption
+ ref={ref}
+ className={cn("text-muted-foreground mt-4 text-sm", className)}
+ {...props}
+ />
));
TableCaption.displayName = "TableCaption";
export {
- Table,
- TableHeader,
- TableBody,
- TableFooter,
- TableHead,
- TableRow,
- TableCell,
- TableCaption,
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
};
diff --git a/apps/web/src/components/ui/tabs.tsx b/apps/web/src/components/ui/tabs.tsx
index ea628959..3668477c 100644
--- a/apps/web/src/components/ui/tabs.tsx
+++ b/apps/web/src/components/ui/tabs.tsx
@@ -3,52 +3,52 @@
import * as React from "react";
import { Tabs as TabsPrimitive } from "radix-ui";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
- React.ElementRef<typeof TabsPrimitive.List>,
- React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
+ React.ElementRef<typeof TabsPrimitive.List>,
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
- <TabsPrimitive.List
- ref={ref}
- className={cn(
- "inline-flex h-auto items-center justify-center gap-2 rounded-lg bg-transparent p-0 text-muted-foreground",
- className
- )}
- {...props}
- />
+ <TabsPrimitive.List
+ ref={ref}
+ className={cn(
+ "text-muted-foreground inline-flex h-auto items-center justify-center gap-2 rounded-lg bg-transparent p-0",
+ className,
+ )}
+ {...props}
+ />
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
- React.ElementRef<typeof TabsPrimitive.Trigger>,
- React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
- <TabsPrimitive.Trigger
- ref={ref}
- className={cn(
- "inline-flex items-center cursor-pointer justify-center whitespace-nowrap rounded-lg px-3 py-1 text-sm font-medium ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-card data-[state=active]:text-foreground",
- className
- )}
- {...props}
- />
+ <TabsPrimitive.Trigger
+ ref={ref}
+ className={cn(
+ "ring-offset-background focus-visible:ring-ring data-[state=active]:bg-panel-accent data-[state=active]:text-foreground inline-flex cursor-pointer items-center justify-center rounded-lg px-3 py-1 text-sm font-medium whitespace-nowrap focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50",
+ className,
+ )}
+ {...props}
+ />
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
- React.ElementRef<typeof TabsPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
+ React.ElementRef<typeof TabsPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
- <TabsPrimitive.Content
- ref={ref}
- className={cn(
- "mt-2 ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
- className
- )}
- {...props}
- />
+ <TabsPrimitive.Content
+ ref={ref}
+ className={cn(
+ "ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden",
+ className,
+ )}
+ {...props}
+ />
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
diff --git a/apps/web/src/components/ui/textarea.tsx b/apps/web/src/components/ui/textarea.tsx
index d9ae9ea6..9c098475 100644
--- a/apps/web/src/components/ui/textarea.tsx
+++ b/apps/web/src/components/ui/textarea.tsx
@@ -1,23 +1,23 @@
import * as React from "react";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const Textarea = React.forwardRef<
- HTMLTextAreaElement,
- React.ComponentProps<"textarea">
+ HTMLTextAreaElement,
+ React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
- return (
- <textarea
- className={cn(
- "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex min-h-[60px] w-full rounded-md border bg-accent/50 px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px]",
- "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
- className
- )}
- ref={ref}
- {...props}
- />
- );
+ return (
+ <textarea
+ className={cn(
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input bg-accent/50 flex min-h-[60px] w-full rounded-md border px-3 py-2 text-base shadow-xs outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+ "focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/10",
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ className,
+ )}
+ ref={ref}
+ {...props}
+ />
+ );
});
Textarea.displayName = "Textarea";
diff --git a/apps/web/src/components/ui/toast.tsx b/apps/web/src/components/ui/toast.tsx
index bd733f63..44dc790f 100644
--- a/apps/web/src/components/ui/toast.tsx
+++ b/apps/web/src/components/ui/toast.tsx
@@ -5,110 +5,110 @@ import { Toast as ToastPrimitives } from "radix-ui";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
- React.ElementRef<typeof ToastPrimitives.Viewport>,
- React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
+ React.ElementRef<typeof ToastPrimitives.Viewport>,
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
- <ToastPrimitives.Viewport
- ref={ref}
- className={cn(
- "fixed top-0 z-100 flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
- className
- )}
- {...props}
- />
+ <ToastPrimitives.Viewport
+ ref={ref}
+ className={cn(
+ "fixed top-0 z-100 flex max-h-screen w-full flex-col-reverse p-4 sm:top-auto sm:right-0 sm:bottom-0 sm:flex-col md:max-w-[420px]",
+ className,
+ )}
+ {...props}
+ />
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
- "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x) data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full sm:data-[state=open]:slide-in-from-bottom-full",
- {
- variants: {
- variant: {
- default: "border bg-background text-foreground",
- destructive:
- "destructive group border-destructive/35 bg-destructive/15 text-destructive-foreground backdrop-blur-lg",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x) data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full sm:data-[state=open]:slide-in-from-bottom-full",
+ {
+ variants: {
+ variant: {
+ default: "border bg-background text-foreground",
+ destructive:
+ "destructive group border-destructive/35 bg-destructive/15 text-destructive-foreground backdrop-blur-lg",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
);
const Toast = React.forwardRef<
- React.ElementRef<typeof ToastPrimitives.Root>,
- React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
- VariantProps<typeof toastVariants>
+ React.ElementRef<typeof ToastPrimitives.Root>,
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
+ VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
- return (
- <ToastPrimitives.Root
- ref={ref}
- className={cn(toastVariants({ variant }), className)}
- {...props}
- />
- );
+ return (
+ <ToastPrimitives.Root
+ ref={ref}
+ className={cn(toastVariants({ variant }), className)}
+ {...props}
+ />
+ );
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
- React.ElementRef<typeof ToastPrimitives.Action>,
- React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
+ React.ElementRef<typeof ToastPrimitives.Action>,
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
- <ToastPrimitives.Action
- ref={ref}
- className={cn(
- "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus-visible:outline-hidden focus-visible:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 hover:group-[.destructive]:border-destructive/30 hover:group-[.destructive]:bg-destructive hover:group-[.destructive]:text-destructive-foreground group-[.destructive]:focus-visible::ring-destructive",
- className
- )}
- {...props}
- />
+ <ToastPrimitives.Action
+ ref={ref}
+ className={cn(
+ "hover:bg-secondary focus:ring-ring group-[.destructive]:border-muted/40 hover:group-[.destructive]:border-destructive/30 hover:group-[.destructive]:bg-destructive hover:group-[.destructive]:text-destructive-foreground group-[.destructive]:focus-visible::ring-destructive inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50",
+ className,
+ )}
+ {...props}
+ />
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
- React.ElementRef<typeof ToastPrimitives.Close>,
- React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
+ React.ElementRef<typeof ToastPrimitives.Close>,
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
- <ToastPrimitives.Close
- ref={ref}
- className={cn(
- "absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-hidden focus-visible::ring-1 group-hover:opacity-100 group-[.destructive]:text-muted-foreground hover:group-[.destructive]:text-red-50 focus-visible:group-[.destructive]:ring-red-400 focus-visible:group-[.destructive]:ring-offset-red-600",
- className
- )}
- toast-close=""
- {...props}
- >
- <X className="h-4 w-4" />
- </ToastPrimitives.Close>
+ <ToastPrimitives.Close
+ ref={ref}
+ className={cn(
+ "text-foreground/50 hover:text-foreground focus-visible::ring-1 group-[.destructive]:text-muted-foreground absolute top-1 right-1 rounded-md p-1 opacity-0 group-hover:opacity-100 hover:group-[.destructive]:text-red-50 focus:opacity-100 focus:outline-hidden focus-visible:group-[.destructive]:ring-red-400 focus-visible:group-[.destructive]:ring-offset-red-600",
+ className,
+ )}
+ toast-close=""
+ {...props}
+ >
+ <X className="size-4" />
+ </ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
- React.ElementRef<typeof ToastPrimitives.Title>,
- React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
+ React.ElementRef<typeof ToastPrimitives.Title>,
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
- <ToastPrimitives.Title
- ref={ref}
- className={cn("text-sm font-semibold [&+div]:text-xs", className)}
- {...props}
- />
+ <ToastPrimitives.Title
+ ref={ref}
+ className={cn("text-sm font-semibold [&+div]:text-xs", className)}
+ {...props}
+ />
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
- React.ElementRef<typeof ToastPrimitives.Description>,
- React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
+ React.ElementRef<typeof ToastPrimitives.Description>,
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
- <ToastPrimitives.Description
- ref={ref}
- className={cn("text-sm opacity-90", className)}
- {...props}
- />
+ <ToastPrimitives.Description
+ ref={ref}
+ className={cn("text-sm opacity-90", className)}
+ {...props}
+ />
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
@@ -117,13 +117,13 @@ type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
- type ToastProps,
- type ToastActionElement,
- ToastProvider,
- ToastViewport,
- Toast,
- ToastTitle,
- ToastDescription,
- ToastClose,
- ToastAction,
+ type ToastProps,
+ type ToastActionElement,
+ ToastProvider,
+ ToastViewport,
+ Toast,
+ ToastTitle,
+ ToastDescription,
+ ToastClose,
+ ToastAction,
};
diff --git a/apps/web/src/components/ui/toaster.tsx b/apps/web/src/components/ui/toaster.tsx
deleted file mode 100644
index baf623f0..00000000
--- a/apps/web/src/components/ui/toaster.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-"use client";
-
-import { useToast } from "../../hooks/use-toast";
-import {
- Toast,
- ToastClose,
- ToastDescription,
- ToastProvider,
- ToastTitle,
- ToastViewport,
-} from "./toast";
-
-export function Toaster() {
- const { toasts } = useToast();
-
- return (
- <ToastProvider>
- {toasts.map(({ id, title, description, action, ...props }) => (
- <Toast key={id} {...props}>
- <div className="grid gap-1">
- {title && <ToastTitle>{title}</ToastTitle>}
- {description && <ToastDescription>{description}</ToastDescription>}
- </div>
- {action}
- <ToastClose />
- </Toast>
- ))}
- <ToastViewport />
- </ToastProvider>
- );
-}
diff --git a/apps/web/src/components/ui/toggle-group.tsx b/apps/web/src/components/ui/toggle-group.tsx
index 5842a07d..66368d50 100644
--- a/apps/web/src/components/ui/toggle-group.tsx
+++ b/apps/web/src/components/ui/toggle-group.tsx
@@ -2,58 +2,58 @@
import * as React from "react";
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui";
-import { type VariantProps } from "class-variance-authority";
+import type { VariantProps } from "class-variance-authority";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
import { toggleVariants } from "./toggle";
const ToggleGroupContext = React.createContext<
- VariantProps<typeof toggleVariants>
+ VariantProps<typeof toggleVariants>
>({
- size: "default",
- variant: "default",
+ size: "default",
+ variant: "default",
});
const ToggleGroup = React.forwardRef<
- React.ElementRef<typeof ToggleGroupPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
- VariantProps<typeof toggleVariants>
+ React.ElementRef<typeof ToggleGroupPrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
+ VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
- <ToggleGroupPrimitive.Root
- ref={ref}
- className={cn("flex items-center justify-center gap-1", className)}
- {...props}
- >
- <ToggleGroupContext.Provider value={{ variant, size }}>
- {children}
- </ToggleGroupContext.Provider>
- </ToggleGroupPrimitive.Root>
+ <ToggleGroupPrimitive.Root
+ ref={ref}
+ className={cn("flex items-center justify-center gap-1", className)}
+ {...props}
+ >
+ <ToggleGroupContext.Provider value={{ variant, size }}>
+ {children}
+ </ToggleGroupContext.Provider>
+ </ToggleGroupPrimitive.Root>
));
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
const ToggleGroupItem = React.forwardRef<
- React.ElementRef<typeof ToggleGroupPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
- VariantProps<typeof toggleVariants>
+ React.ElementRef<typeof ToggleGroupPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
+ VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
- const context = React.useContext(ToggleGroupContext);
+ const context = React.useContext(ToggleGroupContext);
- return (
- <ToggleGroupPrimitive.Item
- ref={ref}
- className={cn(
- toggleVariants({
- variant: context.variant || variant,
- size: context.size || size,
- }),
- className
- )}
- {...props}
- >
- {children}
- </ToggleGroupPrimitive.Item>
- );
+ return (
+ <ToggleGroupPrimitive.Item
+ ref={ref}
+ className={cn(
+ toggleVariants({
+ variant: context.variant || variant,
+ size: context.size || size,
+ }),
+ className,
+ )}
+ {...props}
+ >
+ {children}
+ </ToggleGroupPrimitive.Item>
+ );
});
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
diff --git a/apps/web/src/components/ui/toggle.tsx b/apps/web/src/components/ui/toggle.tsx
index 460aa9f2..836d81ec 100644
--- a/apps/web/src/components/ui/toggle.tsx
+++ b/apps/web/src/components/ui/toggle.tsx
@@ -4,40 +4,40 @@ import * as React from "react";
import { Toggle as TogglePrimitive } from "radix-ui";
import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "../../lib/utils";
+import { cn } from "@/utils/ui";
const toggleVariants = cva(
- "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
- {
- variants: {
- variant: {
- default: "bg-transparent",
- outline:
- "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
- },
- size: {
- default: "h-9 px-2 min-w-9",
- sm: "h-8 px-1.5 min-w-8",
- lg: "h-10 px-2.5 min-w-10",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-transparent",
+ outline:
+ "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
+ },
+ size: {
+ default: "h-9 px-2 min-w-9",
+ sm: "h-8 px-1.5 min-w-8",
+ lg: "h-10 px-2.5 min-w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
);
const Toggle = React.forwardRef<
- React.ElementRef<typeof TogglePrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
- VariantProps<typeof toggleVariants>
+ React.ElementRef<typeof TogglePrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
+ VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
- <TogglePrimitive.Root
- ref={ref}
- className={cn(toggleVariants({ variant, size, className }))}
- {...props}
- />
+ <TogglePrimitive.Root
+ ref={ref}
+ className={cn(toggleVariants({ variant, size, className }))}
+ {...props}
+ />
));
Toggle.displayName = TogglePrimitive.Root.displayName;
diff --git a/apps/web/src/components/ui/tooltip.tsx b/apps/web/src/components/ui/tooltip.tsx
index bfb75e2e..769ac360 100644
--- a/apps/web/src/components/ui/tooltip.tsx
+++ b/apps/web/src/components/ui/tooltip.tsx
@@ -1,8 +1,8 @@
-import { cva, type VariantProps } from 'class-variance-authority';
-import { Tooltip as TooltipPrimitive } from 'radix-ui';
-import * as React from 'react';
+import { cva, type VariantProps } from "class-variance-authority";
+import { Tooltip as TooltipPrimitive } from "radix-ui";
+import * as React from "react";
-import { cn } from '@/lib/utils';
+import { cn } from "@/utils/ui";
const TooltipProvider = TooltipPrimitive.Provider;
@@ -11,62 +11,65 @@ const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const tooltipVariants = cva(
- 'z-50 overflow-visible rounded-sm text-sm shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
- {
- variants: {
- variant: {
- default: 'bg-popover text-popover-foreground border px-3 py-1.5',
- destructive:
- 'bg-destructive/10 text-destructive dark:bg-destructive/20 border-destructive [border-width:0.5px]',
- outline: 'border-border',
- important:
- 'bg-amber-100/90 text-amber-900 dark:bg-amber-900/20 dark:text-amber-300 border-amber-900 [border-width:0.5px]',
- promotions:
- 'bg-red-100/90 text-red-900 dark:bg-red-900/20 dark:text-red-300 border-red-900 [border-width:0.5px]',
- personal:
- 'bg-green-100/90 text-green-900 dark:bg-green-900/20 dark:text-green-300 border-green-900 [border-width:0.5px]',
- updates:
- 'bg-purple-100/90 text-purple-900 dark:bg-purple-900/20 dark:text-purple-300 border-purple-900 [border-width:0.5px]',
- forums:
- 'bg-blue-100/90 text-blue-900 dark:bg-blue-900/20 dark:text-blue-300 border-blue-900 [border-width:0.5px]',
- sidebar: 'bg-white dark:bg-[#413F3E] p-2.5 flex flex-col gap-2',
- },
- },
- defaultVariants: {
- variant: 'default',
- },
- },
+ "z-50 overflow-visible rounded-sm text-sm shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+ {
+ variants: {
+ variant: {
+ default: "bg-popover text-popover-foreground border px-3 py-1.5",
+ destructive:
+ "bg-destructive/10 text-destructive dark:bg-destructive/20 border-destructive [border-width:0.5px]",
+ outline: "border-border",
+ important:
+ "bg-amber-100/90 text-amber-900 dark:bg-amber-900/20 dark:text-amber-300 border-amber-900 [border-width:0.5px]",
+ promotions:
+ "bg-red-100/90 text-red-900 dark:bg-red-900/20 dark:text-red-300 border-red-900 [border-width:0.5px]",
+ personal:
+ "bg-green-100/90 text-green-900 dark:bg-green-900/20 dark:text-green-300 border-green-900 [border-width:0.5px]",
+ updates:
+ "bg-purple-100/90 text-purple-900 dark:bg-purple-900/20 dark:text-purple-300 border-purple-900 [border-width:0.5px]",
+ forums:
+ "bg-blue-100/90 text-blue-900 dark:bg-blue-900/20 dark:text-blue-300 border-blue-900 [border-width:0.5px]",
+ sidebar: "bg-white dark:bg-[#413F3E] p-2.5 flex flex-col gap-2",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
);
interface TooltipContentProps
- extends React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>,
- VariantProps<typeof tooltipVariants> {}
+ extends React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>,
+ VariantProps<typeof tooltipVariants> {}
const TooltipContent = React.forwardRef<
- React.ElementRef<typeof TooltipPrimitive.Content>,
- TooltipContentProps
+ React.ElementRef<typeof TooltipPrimitive.Content>,
+ TooltipContentProps
>(({ className, sideOffset = 4, variant, ...props }, ref) => (
- <TooltipPrimitive.Content
- ref={ref}
- sideOffset={sideOffset}
- className={cn(tooltipVariants({ variant }), className)}
- {...props}
- >
- {variant === 'sidebar' && (
- <svg
- width="6"
- height="10"
- viewBox="0 0 6 10"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- className="absolute left-[-6px] top-1/2 -translate-y-1/2"
- >
- <path d="M6 0L0 5L6 10V0Z" className="fill-white/80 dark:fill-[#413F3E]" />
- </svg>
- )}
- {props.children}
- </TooltipPrimitive.Content>
+ <TooltipPrimitive.Content
+ ref={ref}
+ sideOffset={sideOffset}
+ className={cn(tooltipVariants({ variant }), className)}
+ {...props}
+ >
+ {variant === "sidebar" && (
+ <svg
+ width="6"
+ height="10"
+ viewBox="0 0 6 10"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ className="absolute top-1/2 left-[-6px] -translate-y-1/2"
+ >
+ <path
+ d="M6 0L0 5L6 10V0Z"
+ className="fill-white/80 dark:fill-[#413F3E]"
+ />
+ </svg>
+ )}
+ {props.children}
+ </TooltipPrimitive.Content>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
-export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; \ No newline at end of file
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
diff --git a/apps/web/src/components/ui/video-player.tsx b/apps/web/src/components/ui/video-player.tsx
deleted file mode 100644
index 99e787ef..00000000
--- a/apps/web/src/components/ui/video-player.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-"use client";
-
-import { useRef, useEffect } from "react";
-import { usePlaybackStore } from "@/stores/playback-store";
-
-interface VideoPlayerProps {
- src: string;
- poster?: string;
- className?: string;
- clipStartTime: number;
- trimStart: number;
- trimEnd: number;
- clipDuration: number;
- trackMuted?: boolean;
-}
-
-export function VideoPlayer({
- src,
- poster,
- className = "",
- clipStartTime,
- trimStart,
- trimEnd,
- clipDuration,
- trackMuted = false,
-}: VideoPlayerProps) {
- const videoRef = useRef<HTMLVideoElement>(null);
- const { isPlaying, currentTime, volume, speed, muted } = usePlaybackStore();
-
- // Calculate if we're within this clip's timeline range
- const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd);
- const isInClipRange =
- currentTime >= clipStartTime && currentTime < clipEndTime;
-
- // Sync playback events
- useEffect(() => {
- const video = videoRef.current;
- if (!video || !isInClipRange) return;
-
- const handleSeekEvent = (e: CustomEvent) => {
- // Always update video time, even if outside clip range
- const timelineTime = e.detail.time;
- const videoTime = Math.max(
- trimStart,
- Math.min(
- clipDuration - trimEnd,
- timelineTime - clipStartTime + trimStart
- )
- );
- video.currentTime = videoTime;
- };
-
- const handleUpdateEvent = (e: CustomEvent) => {
- // Always update video time, even if outside clip range
- const timelineTime = e.detail.time;
- const targetTime = Math.max(
- trimStart,
- Math.min(
- clipDuration - trimEnd,
- timelineTime - clipStartTime + trimStart
- )
- );
-
- if (Math.abs(video.currentTime - targetTime) > 0.5) {
- video.currentTime = targetTime;
- }
- };
-
- const handleSpeed = (e: CustomEvent) => {
- video.playbackRate = e.detail.speed;
- };
-
- window.addEventListener("playback-seek", handleSeekEvent as EventListener);
- window.addEventListener(
- "playback-update",
- handleUpdateEvent as EventListener
- );
- window.addEventListener("playback-speed", handleSpeed as EventListener);
-
- return () => {
- window.removeEventListener(
- "playback-seek",
- handleSeekEvent as EventListener
- );
- window.removeEventListener(
- "playback-update",
- handleUpdateEvent as EventListener
- );
- window.removeEventListener(
- "playback-speed",
- handleSpeed as EventListener
- );
- };
- }, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]);
-
- // Sync playback state
- useEffect(() => {
- const video = videoRef.current;
- if (!video) return;
-
- if (isPlaying && isInClipRange) {
- video.play().catch(() => {});
- } else {
- video.pause();
- }
- }, [isPlaying, isInClipRange]);
-
- // Sync volume and speed
- useEffect(() => {
- const video = videoRef.current;
- if (!video) return;
-
- video.volume = volume;
- video.muted = muted || trackMuted;
- video.playbackRate = speed;
- }, [volume, speed, muted, trackMuted]);
-
- return (
- <video
- ref={videoRef}
- src={src}
- poster={poster}
- className={`max-w-full max-h-full object-contain ${className}`}
- playsInline
- preload="auto"
- controls={false}
- disablePictureInPicture
- disableRemotePlayback
- style={{ pointerEvents: "none" }}
- onContextMenu={(e) => e.preventDefault()}
- />
- );
-}
diff --git a/apps/web/src/constants/actions.ts b/apps/web/src/constants/actions.ts
deleted file mode 100644
index 50e8ae5d..00000000
--- a/apps/web/src/constants/actions.ts
+++ /dev/null
@@ -1,295 +0,0 @@
-/* An `action` is a unique verb that is associated with certain thing that can be done on OpenCut.
- * For example, toggling playback or seeking.
- */
-
-import {
- useEffect,
- useRef,
- useState,
- useCallback,
- MutableRefObject,
-} from "react";
-
-// Simple event emitter for action changes
-class ActionEmitter {
- private listeners: Array<(actions: Action[]) => void> = [];
-
- subscribe(listener: (actions: Action[]) => void) {
- this.listeners.push(listener);
- return () => {
- this.listeners = this.listeners.filter((l) => l !== listener);
- };
- }
-
- emit(actions: Action[]) {
- this.listeners.forEach((listener) => listener(actions));
- }
-}
-
-const actionEmitter = new ActionEmitter();
-
-export type Action =
- | "toggle-play" // Toggle play/pause state
- | "stop-playback" // Stop playback
- | "seek-forward" // Seek forward in playback
- | "seek-backward" // Seek backward in playback
- | "frame-step-forward" // Step forward by one frame
- | "frame-step-backward" // Step backward by one frame
- | "jump-forward" // Jump forward by 5 seconds
- | "jump-backward" // Jump backward by 5 seconds
- | "goto-start" // Go to timeline start
- | "goto-end" // Go to timeline end
- | "split-element" // Split element at current time
- | "delete-selected" // Delete selected elements
- | "select-all" // Select all elements
- | "duplicate-selected" // Duplicate selected element
- | "toggle-snapping" // Toggle snapping
- | "undo" // Undo last action
- | "redo" // Redo last undone action
- | "copy-selected" // Copy selected elements to clipboard
- | "paste-selected"; // Paste elements from clipboard at playhead
-
-/**
- * Defines the arguments, if present for a given type that is required to be passed on
- * invocation and will be passed to action handlers.
- *
- * This type is supposed to be an object with the key being one of the actions mentioned above.
- * The value to the key can be anything.
- * If an action has no argument, you do not need to add it to this type.
- *
- * NOTE: We can't enforce type checks to make sure the key is Action, you
- * will know if you got something wrong if there is a type error in this file
- */
-type ActionArgsMap = {
- "seek-forward": { seconds: number } | undefined; // Args needed for seeking forward (default: 1)
- "seek-backward": { seconds: number } | undefined; // Args needed for seeking backward (default: 1)
- "jump-forward": { seconds: number } | undefined; // Args needed for jumping forward (default: 5)
- "jump-backward": { seconds: number } | undefined; // Args needed for jumping backward (default: 5)
-};
-
-type KeysWithValueUndefined<T> = {
- [K in keyof T]: undefined extends T[K] ? K : never;
-}[keyof T];
-
-/**
- * Actions which require arguments for their invocation
- */
-export type ActionWithArgs = keyof ActionArgsMap;
-
-/**
- * Actions which optionally takes in arguments for their invocation
- */
-
-export type ActionWithOptionalArgs =
- | ActionWithNoArgs
- | KeysWithValueUndefined<ActionArgsMap>;
-
-/**
- * Actions which do not require arguments for their invocation
- */
-export type ActionWithNoArgs = Exclude<Action, ActionWithArgs>;
-
-/**
- * Resolves the argument type for a given Action
- */
-type ArgOfHoppAction<A extends Action> = A extends ActionWithArgs
- ? ActionArgsMap[A]
- : undefined;
-
-/**
- * Resolves the action function for a given Action, used by action handler function defs
- */
-type ActionFunc<A extends Action> = A extends ActionWithArgs
- ? (arg: ArgOfHoppAction<A>, trigger?: InvocationTriggers) => void
- : (_?: undefined, trigger?: InvocationTriggers) => void;
-
-type BoundActionList = {
- [A in Action]?: Array<ActionFunc<A>>;
-};
-
-const boundActions: BoundActionList = {};
-
-let currentActiveActions: Action[] = [];
-
-function updateActiveActions() {
- const newActions = Object.keys(boundActions) as Action[];
- currentActiveActions = newActions;
- actionEmitter.emit(newActions);
-}
-
-export function bindAction<A extends Action>(
- action: A,
- handler: ActionFunc<A>
-) {
- if (boundActions[action]) {
- boundActions[action]?.push(handler);
- } else {
- // 'any' assertion because TypeScript doesn't seem to be able to figure out the links.
- boundActions[action] = [handler] as any;
- }
-
- updateActiveActions();
-}
-
-export type InvocationTriggers = "keypress" | "mouseclick";
-
-type InvokeActionFunc = {
- (
- action: ActionWithOptionalArgs,
- args?: undefined,
- trigger?: InvocationTriggers
- ): void;
- <A extends ActionWithArgs>(action: A, args: ActionArgsMap[A]): void;
-};
-
-/**
- * Invokes an action, triggering action handlers if any registered.
- * The second and third arguments are optional
- * @param action The action to fire
- * @param args The argument passed to the action handler. Optional if action has no args required
- * @param trigger Optionally supply the trigger that invoked the action (keypress/mouseclick)
- */
-export const invokeAction: InvokeActionFunc = <A extends Action>(
- action: A,
- args?: ArgOfHoppAction<A>,
- trigger?: InvocationTriggers
-) => {
- boundActions[action]?.forEach((handler) => (handler as any)(args, trigger));
-};
-
-export function unbindAction<A extends Action>(
- action: A,
- handler: ActionFunc<A>
-) {
- // 'any' assertion because TypeScript doesn't seem to be able to figure out the links.
- boundActions[action] = boundActions[action]?.filter(
- (x) => x !== handler
- ) as any;
-
- if (boundActions[action]?.length === 0) {
- delete boundActions[action];
- }
-
- updateActiveActions();
-}
-
-/**
- * Returns whether a given action is bound at a given time
- *
- * @param action The action to check
- */
-export function isActionBound(action: Action): boolean {
- return !!boundActions[action];
-}
-
-/**
- * A React hook that defines a component can handle a given
- * Action. The handler will be bound when the component is mounted
- * and unbound when the component is unmounted.
- * @param action The action to be bound
- * @param handler The function to be called when the action is invoked
- * @param isActive A ref that indicates whether the action is active
- */
-export function useActionHandler<A extends Action>(
- action: A,
- handler: ActionFunc<A>,
- isActive: MutableRefObject<boolean> | boolean | undefined
-) {
- const handlerRef = useRef(handler);
- const [isBound, setIsBound] = useState(false);
-
- // Update handler ref when handler changes
- useEffect(() => {
- handlerRef.current = handler;
- }, [handler]);
-
- // Create a stable handler wrapper
- const stableHandler = useCallback(
- (args: any, trigger?: InvocationTriggers) => {
- (handlerRef.current as any)(args, trigger);
- },
- []
- ) as ActionFunc<A>;
-
- useEffect(() => {
- const shouldBind =
- isActive === undefined ||
- (typeof isActive === "boolean" ? isActive : isActive.current);
-
- if (shouldBind && !isBound) {
- bindAction(action, stableHandler);
- setIsBound(true);
- } else if (!shouldBind && isBound) {
- unbindAction(action, stableHandler);
- setIsBound(false);
- }
-
- return () => {
- if (isBound) {
- unbindAction(action, stableHandler);
- setIsBound(false);
- }
- };
- }, [action, stableHandler, isActive, isBound]);
-
- // Handle ref-based isActive changes
- useEffect(() => {
- if (isActive && typeof isActive === "object" && "current" in isActive) {
- // Poll for ref changes
- const interval = setInterval(() => {
- const shouldBind = isActive.current;
- if (shouldBind !== isBound) {
- if (shouldBind) {
- bindAction(action, stableHandler);
- } else {
- unbindAction(action, stableHandler);
- }
- setIsBound(shouldBind);
- }
- }, 100);
- return () => clearInterval(interval);
- }
- }, [action, stableHandler, isActive, isBound]);
-}
-
-/**
- * A React hook that returns the current list of active actions
- * and re-renders when the list changes
- */
-export function useActiveActions(): Action[] {
- const [activeActions, setActiveActions] = useState<Action[]>([]);
-
- useEffect(() => {
- // Set initial value
- setActiveActions(currentActiveActions);
-
- // Subscribe to changes
- const unsubscribe = actionEmitter.subscribe(setActiveActions);
- return unsubscribe;
- }, []);
-
- return activeActions;
-}
-
-/**
- * A React hook that returns whether a specific action is currently bound
- * and re-renders when the binding state changes
- */
-export function useIsActionBound(action: Action): boolean {
- const [isBound, setIsBound] = useState(() => isActionBound(action));
-
- useEffect(() => {
- const updateBoundState = () => {
- setIsBound(isActionBound(action));
- };
-
- // Set initial value
- updateBoundState();
-
- // Subscribe to changes
- const unsubscribe = actionEmitter.subscribe(updateBoundState);
- return unsubscribe;
- }, [action]);
-
- return isBound;
-}
diff --git a/apps/web/src/constants/editor-constants.ts b/apps/web/src/constants/editor-constants.ts
new file mode 100644
index 00000000..d9570825
--- /dev/null
+++ b/apps/web/src/constants/editor-constants.ts
@@ -0,0 +1,15 @@
+import type { TPlatformLayout } from "@/types/editor";
+
+export const PLATFORM_LAYOUTS: Record<TPlatformLayout, string> = {
+ tiktok: "TikTok",
+};
+
+export const PANEL_CONFIG = {
+ panels: {
+ tools: 25,
+ preview: 50,
+ properties: 25,
+ mainContent: 70,
+ timeline: 30,
+ },
+};
diff --git a/apps/web/src/constants/export-constants.ts b/apps/web/src/constants/export-constants.ts
new file mode 100644
index 00000000..4e27aae5
--- /dev/null
+++ b/apps/web/src/constants/export-constants.ts
@@ -0,0 +1,12 @@
+import type { ExportOptions } from "@/types/export";
+
+export const DEFAULT_EXPORT_OPTIONS = {
+ format: "mp4",
+ quality: "high",
+ includeAudio: true,
+} satisfies ExportOptions;
+
+export const EXPORT_MIME_TYPES = {
+ webm: "video/webm",
+ mp4: "video/mp4",
+} as const;
diff --git a/apps/web/src/constants/font-constants.ts b/apps/web/src/constants/font-constants.ts
index a1c4e4d5..46f84848 100644
--- a/apps/web/src/constants/font-constants.ts
+++ b/apps/web/src/constants/font-constants.ts
@@ -1,66 +1,66 @@
export interface FontOption {
- value: string;
- label: string;
- category: "system" | "google" | "custom";
- weights?: number[];
- hasClassName?: boolean;
+ value: string;
+ label: string;
+ category: "system" | "google" | "custom";
+ weights?: number[];
+ hasClassName?: boolean;
}
export const FONT_OPTIONS: FontOption[] = [
- // System fonts (always available)
- { value: "Arial", label: "Arial", category: "system", hasClassName: false },
- {
- value: "Helvetica",
- label: "Helvetica",
- category: "system",
- hasClassName: false,
- },
- {
- value: "Times New Roman",
- label: "Times New Roman",
- category: "system",
- hasClassName: false,
- },
- {
- value: "Georgia",
- label: "Georgia",
- category: "system",
- hasClassName: false,
- },
+ // System fonts (always available)
+ { value: "Arial", label: "Arial", category: "system", hasClassName: false },
+ {
+ value: "Helvetica",
+ label: "Helvetica",
+ category: "system",
+ hasClassName: false,
+ },
+ {
+ value: "Times New Roman",
+ label: "Times New Roman",
+ category: "system",
+ hasClassName: false,
+ },
+ {
+ value: "Georgia",
+ label: "Georgia",
+ category: "system",
+ hasClassName: false,
+ },
- // Google Fonts (loaded in layout.tsx)
- {
- value: "Inter",
- label: "Inter",
- category: "google",
- weights: [400, 700],
- hasClassName: true,
- },
- {
- value: "Roboto",
- label: "Roboto",
- category: "google",
- weights: [400, 700],
- hasClassName: true,
- },
- {
- value: "Open Sans",
- label: "Open Sans",
- category: "google",
- hasClassName: true,
- },
- {
- value: "Playfair Display",
- label: "Playfair Display",
- category: "google",
- hasClassName: true,
- },
- {
- value: "Comic Neue",
- label: "Comic Neue",
- category: "google",
- hasClassName: false,
- },
+ // Google Fonts (loaded in layout.tsx)
+ {
+ value: "Inter",
+ label: "Inter",
+ category: "google",
+ weights: [400, 700],
+ hasClassName: true,
+ },
+ {
+ value: "Roboto",
+ label: "Roboto",
+ category: "google",
+ weights: [400, 700],
+ hasClassName: true,
+ },
+ {
+ value: "Open Sans",
+ label: "Open Sans",
+ category: "google",
+ hasClassName: true,
+ },
+ {
+ value: "Playfair Display",
+ label: "Playfair Display",
+ category: "google",
+ hasClassName: true,
+ },
+ {
+ value: "Comic Neue",
+ label: "Comic Neue",
+ category: "google",
+ hasClassName: false,
+ },
] as const;
export const DEFAULT_FONT = "Arial";
@@ -70,10 +70,10 @@ export type FontFamily = (typeof FONT_OPTIONS)[number]["value"];
// Helper functions
export const getFontByValue = (value: string): FontOption | undefined =>
- FONT_OPTIONS.find((font) => font.value === value);
+ FONT_OPTIONS.find((font) => font.value === value);
export const getGoogleFonts = (): FontOption[] =>
- FONT_OPTIONS.filter((font) => font.category === "google");
+ FONT_OPTIONS.filter((font) => font.category === "google");
export const getSystemFonts = (): FontOption[] =>
- FONT_OPTIONS.filter((font) => font.category === "system");
+ FONT_OPTIONS.filter((font) => font.category === "system");
diff --git a/apps/web/src/constants/language-constants.ts b/apps/web/src/constants/language-constants.ts
new file mode 100644
index 00000000..3da36661
--- /dev/null
+++ b/apps/web/src/constants/language-constants.ts
@@ -0,0 +1,11 @@
+export const LANGUAGES = [
+ { code: "en", name: "English" },
+ { code: "es", name: "Spanish" },
+ { code: "it", name: "Italian" },
+ { code: "fr", name: "French" },
+ { code: "de", name: "German" },
+ { code: "pt", name: "Portuguese" },
+ { code: "ru", name: "Russian" },
+ { code: "ja", name: "Japanese" },
+ { code: "zh", name: "Chinese" },
+] as const;
diff --git a/apps/web/src/constants/project-constants.ts b/apps/web/src/constants/project-constants.ts
new file mode 100644
index 00000000..7e3f06b6
--- /dev/null
+++ b/apps/web/src/constants/project-constants.ts
@@ -0,0 +1,27 @@
+import type { TCanvasSize } from "@/types/project";
+
+export const DEFAULT_CANVAS_PRESETS: TCanvasSize[] = [
+ { width: 1920, height: 1080 },
+ { width: 1080, height: 1920 },
+ { width: 1080, height: 1080 },
+ { width: 1440, height: 1080 },
+];
+
+export const FPS_PRESETS = [
+ { value: "24", label: "24 fps" },
+ { value: "25", label: "25 fps" },
+ { value: "30", label: "30 fps" },
+ { value: "60", label: "60 fps" },
+ { value: "120", label: "120 fps" },
+] as const;
+
+export const BLUR_INTENSITY_PRESETS: { label: string; value: number }[] = [
+ { label: "Light", value: 4 },
+ { label: "Medium", value: 8 },
+ { label: "Heavy", value: 18 },
+] as const;
+
+export const DEFAULT_CANVAS_SIZE: TCanvasSize = { width: 1920, height: 1080 };
+export const DEFAULT_FPS = 30;
+export const DEFAULT_BLUR_INTENSITY = 8;
+export const DEFAULT_COLOR = "#000000";
diff --git a/apps/web/src/constants/site-constants.ts b/apps/web/src/constants/site-constants.ts
new file mode 100644
index 00000000..ac6cd2cd
--- /dev/null
+++ b/apps/web/src/constants/site-constants.ts
@@ -0,0 +1,66 @@
+import { OcDataBuddyIcon, OcMarbleIcon, } from "@opencut/ui/icons";
+
+export const SITE_URL = "https://opencut.app";
+
+export const SITE_INFO = {
+ title: "OpenCut",
+ description:
+ "A simple but powerful video editor that gets the job done. In your browser.",
+ url: SITE_URL,
+ openGraphImage: "/open-graph/default.jpg",
+ twitterImage: "/open-graph/default.jpg",
+ favicon: "/favicon.ico",
+};
+
+export type ExternalTool = {
+ name: string;
+ description: string;
+ url: string;
+ icon: React.ElementType;
+};
+
+export const EXTERNAL_TOOLS: ExternalTool[] = [
+ {
+ name: "Marble",
+ description:
+ "Modern headless CMS for content management and the blog for OpenCut",
+ url: "https://marblecms.com?utm_source=opencut",
+ icon: OcMarbleIcon,
+ },
+ {
+ name: "Databuddy",
+ description: "GDPR compliant analytics and user insights for OpenCut",
+ url: "https://databuddy.cc?utm_source=opencut",
+ icon: OcDataBuddyIcon,
+ },
+];
+
+export const DEFAULT_LOGO_URL = "/logos/opencut/svg/logo.svg";
+
+export const SOCIAL_LINKS = {
+ x: "https://x.com/opencutapp",
+ github: "https://github.com/OpenCut-app/OpenCut",
+ discord: "https://discord.com/invite/Mu3acKZvCp",
+};
+
+export type Sponsor = {
+ name: string;
+ url: string;
+ logo: string;
+ description: string;
+};
+
+export const SPONSORS: Sponsor[] = [
+ {
+ name: "Fal.ai",
+ url: "https://fal.ai?utm_source=opencut",
+ logo: "/logos/others/fal.svg",
+ description: "Generative image, video, and audio models all in one place.",
+ },
+ {
+ name: "Vercel",
+ url: "https://vercel.com?utm_source=opencut",
+ logo: "/logos/others/vercel.svg",
+ description: "Platform where we deploy and host OpenCut.",
+ },
+];
diff --git a/apps/web/src/constants/site.ts b/apps/web/src/constants/site.ts
deleted file mode 100644
index 00bd649f..00000000
--- a/apps/web/src/constants/site.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-export const SITE_URL = "https://opencut.app";
-
-export const SITE_INFO = {
- title: "OpenCut",
- description:
- "A simple but powerful video editor that gets the job done. In your browser.",
- url: SITE_URL,
- openGraphImage: "/open-graph/default.jpg",
- twitterImage: "/open-graph/default.jpg",
- favicon: "/favicon.ico",
-};
-
-export const EXTERNAL_TOOLS = [
- {
- name: "Marble",
- description:
- "Modern headless CMS for content management and the blog for OpenCut",
- url: "https://marblecms.com?utm_source=opencut",
- icon: "MarbleIcon" as const,
- },
- {
- name: "Vercel",
- description: "Platform where we deploy and host OpenCut",
- url: "https://vercel.com?utm_source=opencut",
- icon: "VercelIcon" as const,
- },
- {
- name: "Databuddy",
- description: "GDPR compliant analytics and user insights for OpenCut",
- url: "https://databuddy.cc?utm_source=opencut",
- icon: "DataBuddyIcon" as const,
- },
-];
diff --git a/apps/web/src/constants/stickers-constants.ts b/apps/web/src/constants/stickers-constants.ts
new file mode 100644
index 00000000..3c98d4f7
--- /dev/null
+++ b/apps/web/src/constants/stickers-constants.ts
@@ -0,0 +1,16 @@
+export const STICKER_CATEGORIES = [
+ "all",
+ "general",
+ "brands",
+ "emoji",
+] as const;
+
+export const STICKER_CATEGORY_CONFIG: Record<
+ (typeof STICKER_CATEGORIES)[number],
+ string | undefined
+> = {
+ all: undefined,
+ general: "General",
+ brands: "Brands / Social",
+ emoji: "Emoji",
+};
diff --git a/apps/web/src/constants/text-constants.ts b/apps/web/src/constants/text-constants.ts
index e3fcce38..c8a23655 100644
--- a/apps/web/src/constants/text-constants.ts
+++ b/apps/web/src/constants/text-constants.ts
@@ -1,27 +1,29 @@
-import { TextElement } from "@/types/timeline";
+import type { TextElement } from "@/types/timeline";
import { TIMELINE_CONSTANTS } from "./timeline-constants";
-export const DEFAULT_TEXT_ELEMENT: Omit<
- TextElement,
- "id"
-> = {
- type: "text",
- name: "Text",
- content: "Default Text",
- fontSize: 48,
- fontFamily: "Arial",
- color: "#ffffff",
- backgroundColor: "transparent",
- textAlign: "center",
- fontWeight: "normal",
- fontStyle: "normal",
- textDecoration: "none",
- x: 0,
- y: 0,
- rotation: 0,
- opacity: 1,
- duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
- startTime: 0,
- trimStart: 0,
- trimEnd: 0,
+export const DEFAULT_TEXT_ELEMENT: Omit<TextElement, "id"> = {
+ type: "text",
+ name: "Text",
+ content: "Default Text",
+ fontSize: 48,
+ fontFamily: "Arial",
+ color: "#ffffff",
+ backgroundColor: "transparent",
+ textAlign: "center",
+ fontWeight: "normal",
+ fontStyle: "normal",
+ textDecoration: "none",
+ duration: TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION,
+ startTime: 0,
+ trimStart: 0,
+ trimEnd: 0,
+ transform: {
+ scale: 1,
+ position: {
+ x: 0,
+ y: 0,
+ },
+ rotate: 0,
+ },
+ opacity: 1,
};
diff --git a/apps/web/src/constants/timeline-constants.ts b/apps/web/src/constants/timeline-constants.ts
deleted file mode 100644
index 4516775f..00000000
--- a/apps/web/src/constants/timeline-constants.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import type { TrackType } from "@/types/timeline";
-
-// Track color definitions
-export const TRACK_COLORS: Record<
- TrackType,
- { solid: string; background: string; border: string }
-> = {
- media: {
- solid: "bg-blue-500",
- background: "",
- border: "",
- },
- text: {
- solid: "bg-[#5DBAA0]",
- background: "bg-[#5DBAA0]",
- border: "",
- },
- audio: {
- solid: "bg-green-500",
- background: "bg-[#915DBE]",
- border: "",
- },
-} as const;
-
-// Utility functions
-export function getTrackColors(type: TrackType) {
- return TRACK_COLORS[type];
-}
-
-export function getTrackElementClasses(type: TrackType) {
- const colors = getTrackColors(type);
- return `${colors.background} ${colors.border}`;
-}
-
-// Track height definitions
-export const TRACK_HEIGHTS: Record<TrackType, number> = {
- media: 60,
- text: 25,
- audio: 50,
-} as const;
-
-// Utility function for track heights
-export function getTrackHeight(type: TrackType): number {
- return TRACK_HEIGHTS[type];
-}
-
-// Calculate cumulative height up to (but not including) a track index
-export function getCumulativeHeightBefore(
- tracks: Array<{ type: TrackType }>,
- trackIndex: number
-): number {
- const GAP = 4; // 4px gap between tracks (equivalent to Tailwind's gap-1)
- return tracks
- .slice(0, trackIndex)
- .reduce((sum, track) => sum + getTrackHeight(track.type) + GAP, 0);
-}
-
-// Calculate total height of all tracks
-export function getTotalTracksHeight(
- tracks: Array<{ type: TrackType }>
-): number {
- const GAP = 4; // 4px gap between tracks (equivalent to Tailwind's gap-1)
- const tracksHeight = tracks.reduce(
- (sum, track) => sum + getTrackHeight(track.type),
- 0
- );
- const gapsHeight = Math.max(0, tracks.length - 1) * GAP; // n-1 gaps for n tracks
- return tracksHeight + gapsHeight;
-}
-
-// Other timeline constants
-export const TIMELINE_CONSTANTS = {
- ELEMENT_MIN_WIDTH: 80,
- PIXELS_PER_SECOND: 50,
- TRACK_HEIGHT: 60, // Default fallback
- DEFAULT_TEXT_DURATION: 5,
- DEFAULT_IMAGE_DURATION: 5,
- ZOOM_LEVELS: [0.25, 0.5, 1, 1.5, 2, 3, 4],
-} as const;
-
-// FPS presets for project settings
-export const FPS_PRESETS = [
- { value: "24", label: "24 fps" },
- { value: "25", label: "25 fps" },
- { value: "30", label: "30 fps" },
- { value: "60", label: "60 fps" },
- { value: "120", label: "120 fps" },
-] as const;
-
-// Frame snapping utilities
-export function timeToFrame(time: number, fps: number): number {
- return Math.round(time * fps);
-}
-
-export function frameToTime(frame: number, fps: number): number {
- return frame / fps;
-}
-
-export function snapTimeToFrame(time: number, fps: number): number {
- if (fps <= 0) return time; // Fallback for invalid FPS
- const frame = timeToFrame(time, fps);
- return frameToTime(frame, fps);
-}
-
-export function getFrameDuration(fps: number): number {
- return 1 / fps;
-}
diff --git a/apps/web/src/constants/timeline-constants.tsx b/apps/web/src/constants/timeline-constants.tsx
new file mode 100644
index 00000000..692ccd65
--- /dev/null
+++ b/apps/web/src/constants/timeline-constants.tsx
@@ -0,0 +1,72 @@
+import type { TTimelineViewState } from "@/types/project";
+import type { TrackType } from "@/types/timeline";
+import {
+ Happy01Icon,
+ MusicNote03Icon,
+ TextIcon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { OcVideoIcon } from "@opencut/ui/icons";
+
+export const TRACK_COLORS: Record<TrackType, { background: string }> = {
+ video: {
+ background: "transparent",
+ },
+ text: {
+ background: "bg-[#5DBAA0]",
+ },
+ audio: {
+ background: "bg-[#915DBE]",
+ },
+ sticker: {
+ background: "bg-amber-500",
+ },
+} as const;
+
+export const TRACK_HEIGHTS: Record<TrackType, number> = {
+ video: 60,
+ text: 25,
+ audio: 50,
+ sticker: 50,
+} as const;
+
+export const TRACK_GAP = 4;
+
+export const TIMELINE_CONSTANTS = {
+ PIXELS_PER_SECOND: 50,
+ DEFAULT_ELEMENT_DURATION: 5,
+ PLAYHEAD_LOOKAHEAD_SECONDS: 30, // padding ahead
+ PADDING_TOP_PX: 0,
+ ZOOM_LEVELS: [0.1, 0.25, 0.5, 1, 1.5, 2, 3, 4, 6, 8, 10, 15, 20, 30, 50],
+ ZOOM_MIN: 0.1,
+ ZOOM_MAX: 100,
+ ZOOM_STEP: 0.1,
+} as const;
+
+export const DEFAULT_TIMELINE_VIEW_STATE: TTimelineViewState = {
+ zoomLevel: 1,
+ scrollLeft: 0,
+ playheadTime: 0,
+};
+
+export const TRACK_ICONS: Record<TrackType, React.ReactNode> = {
+ video: <OcVideoIcon className="text-muted-foreground size-4 shrink-0" />,
+ text: (
+ <HugeiconsIcon
+ icon={TextIcon}
+ className="text-muted-foreground size-4 shrink-0"
+ />
+ ),
+ audio: (
+ <HugeiconsIcon
+ icon={MusicNote03Icon}
+ className="text-muted-foreground size-4 shrink-0"
+ />
+ ),
+ sticker: (
+ <HugeiconsIcon
+ icon={Happy01Icon}
+ className="text-muted-foreground size-4 shrink-0"
+ />
+ ),
+} as const;
diff --git a/apps/web/src/constants/transcription-constants.ts b/apps/web/src/constants/transcription-constants.ts
new file mode 100644
index 00000000..3fa914f2
--- /dev/null
+++ b/apps/web/src/constants/transcription-constants.ts
@@ -0,0 +1,58 @@
+import { LANGUAGES } from "@/constants/language-constants";
+import type {
+ TranscriptionModel,
+ TranscriptionModelId,
+} from "@/types/transcription";
+import type { LanguageCode } from "@/types/language";
+
+const SUPPORTED_TRANSCRIPTION_LANGS: ReadonlyArray<LanguageCode> = [
+ "en",
+ "es",
+ "it",
+ "fr",
+ "de",
+ "pt",
+ "ru",
+ "ja",
+ "zh",
+];
+
+export const TRANSCRIPTION_LANGUAGES = LANGUAGES.filter((language) =>
+ SUPPORTED_TRANSCRIPTION_LANGS.includes(language.code),
+);
+
+export const TRANSCRIPTION_MODELS: TranscriptionModel[] = [
+ {
+ id: "whisper-tiny",
+ name: "Tiny",
+ huggingFaceId: "onnx-community/whisper-tiny",
+ description: "Fastest, lower accuracy",
+ },
+ {
+ id: "whisper-small",
+ name: "Small",
+ huggingFaceId: "onnx-community/whisper-small",
+ description: "Good balance of speed and accuracy",
+ },
+ {
+ id: "whisper-medium",
+ name: "Medium",
+ huggingFaceId: "onnx-community/whisper-medium",
+ description: "Higher accuracy, slower",
+ },
+ {
+ id: "whisper-large-v3-turbo",
+ name: "Large v3 Turbo",
+ huggingFaceId: "onnx-community/whisper-large-v3-turbo",
+ description: "Best accuracy, requires WebGPU for good performance",
+ },
+];
+
+export const DEFAULT_TRANSCRIPTION_MODEL: TranscriptionModelId =
+ "whisper-small";
+
+export const DEFAULT_CHUNK_LENGTH_SECONDS = 30;
+export const DEFAULT_STRIDE_SECONDS = 5;
+
+export const DEFAULT_WORDS_PER_CAPTION = 3;
+export const MIN_CAPTION_DURATION_SECONDS = 0.8;
diff --git a/apps/web/src/core/index.ts b/apps/web/src/core/index.ts
new file mode 100644
index 00000000..4a8813f8
--- /dev/null
+++ b/apps/web/src/core/index.ts
@@ -0,0 +1,50 @@
+import { PlaybackManager } from "./managers/playback-manager";
+import { TimelineManager } from "./managers/timeline-manager";
+import { ScenesManager } from "./managers/scenes-manager";
+import { ProjectManager } from "./managers/project-manager";
+import { MediaManager } from "./managers/media-manager";
+import { RendererManager } from "./managers/renderer-manager";
+import { CommandManager } from "./managers/commands";
+import { SaveManager } from "./managers/save-manager";
+import { AudioManager } from "./managers/audio-manager";
+import { SelectionManager } from "./managers/selection-manager";
+
+export class EditorCore {
+ private static instance: EditorCore | null = null;
+
+ public readonly command: CommandManager;
+ public readonly playback: PlaybackManager;
+ public readonly timeline: TimelineManager;
+ public readonly scenes: ScenesManager;
+ public readonly project: ProjectManager;
+ public readonly media: MediaManager;
+ public readonly renderer: RendererManager;
+ public readonly save: SaveManager;
+ public readonly audio: AudioManager;
+ public readonly selection: SelectionManager;
+
+ private constructor() {
+ this.command = new CommandManager();
+ this.playback = new PlaybackManager(this);
+ this.timeline = new TimelineManager(this);
+ this.scenes = new ScenesManager(this);
+ this.project = new ProjectManager(this);
+ this.media = new MediaManager(this);
+ this.renderer = new RendererManager(this);
+ this.save = new SaveManager(this);
+ this.audio = new AudioManager(this);
+ this.selection = new SelectionManager(this);
+ this.save.start();
+ }
+
+ static getInstance(): EditorCore {
+ if (!EditorCore.instance) {
+ EditorCore.instance = new EditorCore();
+ }
+ return EditorCore.instance;
+ }
+
+ static reset(): void {
+ EditorCore.instance = null;
+ }
+}
diff --git a/apps/web/src/core/managers/audio-manager.ts b/apps/web/src/core/managers/audio-manager.ts
new file mode 100644
index 00000000..621cca18
--- /dev/null
+++ b/apps/web/src/core/managers/audio-manager.ts
@@ -0,0 +1,335 @@
+import type { EditorCore } from "@/core";
+import type { AudioClipSource } from "@/lib/media/audio";
+import { createAudioContext, collectAudioClips } from "@/lib/media/audio";
+import {
+ ALL_FORMATS,
+ AudioBufferSink,
+ BlobSource,
+ Input,
+ type WrappedAudioBuffer,
+} from "mediabunny";
+
+export class AudioManager {
+ private audioContext: AudioContext | null = null;
+ private masterGain: GainNode | null = null;
+ private playbackStartTime = 0;
+ private playbackStartContextTime = 0;
+ private scheduleTimer: number | null = null;
+ private lookaheadSeconds = 2;
+ private scheduleIntervalMs = 500;
+ private clips: AudioClipSource[] = [];
+ private sinks = new Map<string, AudioBufferSink>();
+ private inputs = new Map<string, Input>();
+ private activeClipIds = new Set<string>();
+ private clipIterators = new Map<
+ string,
+ AsyncGenerator<WrappedAudioBuffer, void, unknown>
+ >();
+ private queuedSources = new Set<AudioBufferSourceNode>();
+ private playbackSessionId = 0;
+ private lastIsPlaying = false;
+ private lastVolume = 1;
+ private unsubscribers: Array<() => void> = [];
+
+ constructor(private editor: EditorCore) {
+ this.lastVolume = this.editor.playback.getVolume();
+
+ this.unsubscribers.push(
+ this.editor.playback.subscribe(this.handlePlaybackChange),
+ this.editor.timeline.subscribe(this.handleTimelineChange),
+ this.editor.media.subscribe(this.handleTimelineChange),
+ );
+ if (typeof window !== "undefined") {
+ window.addEventListener("playback-seek", this.handleSeek);
+ }
+ }
+
+ dispose(): void {
+ this.stopPlayback();
+ for (const unsub of this.unsubscribers) {
+ unsub();
+ }
+ this.unsubscribers = [];
+ if (typeof window !== "undefined") {
+ window.removeEventListener("playback-seek", this.handleSeek);
+ }
+ this.disposeSinks();
+ if (this.audioContext) {
+ void this.audioContext.close();
+ this.audioContext = null;
+ this.masterGain = null;
+ }
+ }
+
+ private handlePlaybackChange = (): void => {
+ const isPlaying = this.editor.playback.getIsPlaying();
+ const volume = this.editor.playback.getVolume();
+
+ if (volume !== this.lastVolume) {
+ this.lastVolume = volume;
+ this.updateGain();
+ }
+
+ if (isPlaying !== this.lastIsPlaying) {
+ this.lastIsPlaying = isPlaying;
+ if (isPlaying) {
+ void this.startPlayback({
+ time: this.editor.playback.getCurrentTime(),
+ });
+ } else {
+ this.stopPlayback();
+ }
+ }
+ };
+
+ private handleSeek = (event: Event): void => {
+ const detail = (event as CustomEvent<{ time: number }>).detail;
+ if (!detail) return;
+
+ if (this.editor.playback.getIsPlaying()) {
+ void this.startPlayback({ time: detail.time });
+ return;
+ }
+
+ this.stopPlayback();
+ };
+
+ private handleTimelineChange = (): void => {
+ this.disposeSinks();
+
+ if (!this.editor.playback.getIsPlaying()) return;
+
+ void this.startPlayback({ time: this.editor.playback.getCurrentTime() });
+ };
+
+ private ensureAudioContext(): AudioContext | null {
+ if (this.audioContext) return this.audioContext;
+ if (typeof window === "undefined") return null;
+
+ this.audioContext = createAudioContext();
+ this.masterGain = this.audioContext.createGain();
+ this.masterGain.gain.value = this.lastVolume;
+ this.masterGain.connect(this.audioContext.destination);
+ return this.audioContext;
+ }
+
+ private updateGain(): void {
+ if (!this.masterGain) return;
+ this.masterGain.gain.value = this.lastVolume;
+ }
+
+ private getPlaybackTime(): number {
+ if (!this.audioContext) return this.playbackStartTime;
+ const elapsed = this.audioContext.currentTime - this.playbackStartContextTime;
+ return this.playbackStartTime + elapsed;
+ }
+
+ private async startPlayback({ time }: { time: number }): Promise<void> {
+ const audioContext = this.ensureAudioContext();
+ if (!audioContext) return;
+
+ this.stopPlayback();
+ this.playbackSessionId++;
+
+ const tracks = this.editor.timeline.getTracks();
+ const mediaAssets = this.editor.media.getAssets();
+ const duration = this.editor.timeline.getTotalDuration();
+
+ if (duration <= 0) return;
+
+ if (audioContext.state === "suspended") {
+ await audioContext.resume();
+ }
+
+ this.clips = await collectAudioClips({ tracks, mediaAssets });
+ if (!this.editor.playback.getIsPlaying()) return;
+
+ this.playbackStartTime = time;
+ this.playbackStartContextTime = audioContext.currentTime;
+
+ this.scheduleUpcomingClips();
+
+ if (typeof window !== "undefined") {
+ this.scheduleTimer = window.setInterval(() => {
+ this.scheduleUpcomingClips();
+ }, this.scheduleIntervalMs);
+ }
+ }
+
+ private scheduleUpcomingClips(): void {
+ if (!this.editor.playback.getIsPlaying()) return;
+
+ const currentTime = this.getPlaybackTime();
+ const windowEnd = currentTime + this.lookaheadSeconds;
+
+ for (const clip of this.clips) {
+ if (clip.muted) continue;
+ if (this.activeClipIds.has(clip.id)) continue;
+
+ const clipEnd = clip.startTime + clip.duration;
+ if (clipEnd <= currentTime) continue;
+ if (clip.startTime > windowEnd) continue;
+
+ this.activeClipIds.add(clip.id);
+ void this.runClipIterator({ clip, startTime: currentTime, sessionId: this.playbackSessionId });
+ }
+ }
+
+ private stopPlayback(): void {
+ if (this.scheduleTimer && typeof window !== "undefined") {
+ window.clearInterval(this.scheduleTimer);
+ }
+ this.scheduleTimer = null;
+
+ for (const iterator of this.clipIterators.values()) {
+ void iterator.return();
+ }
+ this.clipIterators.clear();
+ this.activeClipIds.clear();
+
+ for (const source of this.queuedSources) {
+ try {
+ source.stop();
+ } catch {}
+ source.disconnect();
+ }
+ this.queuedSources.clear();
+ }
+
+ private async runClipIterator({
+ clip,
+ startTime,
+ sessionId,
+ }: {
+ clip: AudioClipSource;
+ startTime: number;
+ sessionId: number;
+ }): Promise<void> {
+ const audioContext = this.ensureAudioContext();
+ if (!audioContext) return;
+
+ const sink = await this.getAudioSink({ clip });
+ if (!sink || !this.editor.playback.getIsPlaying()) return;
+ if (sessionId !== this.playbackSessionId) return;
+
+ const clipStart = clip.startTime;
+ const clipEnd = clip.startTime + clip.duration;
+
+ const iteratorStartTime = Math.max(startTime, clipStart);
+ const sourceStartTime =
+ clip.trimStart + (iteratorStartTime - clip.startTime);
+
+ const iterator = sink.buffers(sourceStartTime);
+ this.clipIterators.set(clip.id, iterator);
+
+ for await (const { buffer, timestamp } of iterator) {
+ if (!this.editor.playback.getIsPlaying()) return;
+ if (sessionId !== this.playbackSessionId) return;
+
+ const timelineTime = clip.startTime + (timestamp - clip.trimStart);
+ if (timelineTime >= clipEnd) break;
+
+ const node = audioContext.createBufferSource();
+ node.buffer = buffer;
+ node.connect(this.masterGain ?? audioContext.destination);
+
+ const startTimestamp =
+ this.playbackStartContextTime +
+ (timelineTime - this.playbackStartTime);
+
+ if (startTimestamp >= audioContext.currentTime) {
+ node.start(startTimestamp);
+ } else {
+ const offset = audioContext.currentTime - startTimestamp;
+ if (offset < buffer.duration) {
+ node.start(audioContext.currentTime, offset);
+ } else {
+ continue;
+ }
+ }
+
+ this.queuedSources.add(node);
+ node.addEventListener("ended", () => {
+ node.disconnect();
+ this.queuedSources.delete(node);
+ });
+
+ const aheadTime = timelineTime - this.getPlaybackTime();
+ if (aheadTime >= 1) {
+ await this.waitUntilCaughtUp({ timelineTime, targetAhead: 1 });
+ if (sessionId !== this.playbackSessionId) return;
+ }
+ }
+
+ this.clipIterators.delete(clip.id);
+ // don't remove from activeClipIds - prevents scheduler from restarting this clip
+ // the set is cleared on stopPlayback anyway
+ }
+
+ private waitUntilCaughtUp({
+ timelineTime,
+ targetAhead,
+ }: {
+ timelineTime: number;
+ targetAhead: number;
+ }): Promise<void> {
+ return new Promise((resolve) => {
+ const checkInterval = setInterval(() => {
+ if (!this.editor.playback.getIsPlaying()) {
+ clearInterval(checkInterval);
+ resolve();
+ return;
+ }
+
+ const playbackTime = this.getPlaybackTime();
+ if (timelineTime - playbackTime < targetAhead) {
+ clearInterval(checkInterval);
+ resolve();
+ }
+ }, 100);
+ });
+ }
+
+ private disposeSinks(): void {
+ for (const iterator of this.clipIterators.values()) {
+ void iterator.return();
+ }
+ this.clipIterators.clear();
+ this.activeClipIds.clear();
+
+ for (const input of this.inputs.values()) {
+ input.dispose();
+ }
+ this.inputs.clear();
+ this.sinks.clear();
+ }
+
+ private async getAudioSink({
+ clip,
+ }: {
+ clip: AudioClipSource;
+ }): Promise<AudioBufferSink | null> {
+ const existingSink = this.sinks.get(clip.sourceKey);
+ if (existingSink) return existingSink;
+
+ try {
+ const input = new Input({
+ source: new BlobSource(clip.file),
+ formats: ALL_FORMATS,
+ });
+ const audioTrack = await input.getPrimaryAudioTrack();
+ if (!audioTrack) {
+ input.dispose();
+ return null;
+ }
+
+ const sink = new AudioBufferSink(audioTrack);
+ this.inputs.set(clip.sourceKey, input);
+ this.sinks.set(clip.sourceKey, sink);
+ return sink;
+ } catch (error) {
+ console.warn("Failed to initialize audio sink:", error);
+ return null;
+ }
+ }
+}
diff --git a/apps/web/src/core/managers/commands.ts b/apps/web/src/core/managers/commands.ts
new file mode 100644
index 00000000..d996a30e
--- /dev/null
+++ b/apps/web/src/core/managers/commands.ts
@@ -0,0 +1,44 @@
+import type { Command } from "@/lib/commands";
+
+export class CommandManager {
+ private history: Command[] = [];
+ private redoStack: Command[] = [];
+
+ execute({ command }: { command: Command }): Command {
+ command.execute();
+ this.history.push(command);
+ this.redoStack = [];
+ return command;
+ }
+
+ undo(): void {
+ if (this.history.length === 0) return;
+ const command = this.history.pop();
+ command?.undo();
+ if (command) {
+ this.redoStack.push(command);
+ }
+ }
+
+ redo(): void {
+ if (this.redoStack.length === 0) return;
+ const command = this.redoStack.pop();
+ command?.redo();
+ if (command) {
+ this.history.push(command);
+ }
+ }
+
+ canUndo(): boolean {
+ return this.history.length > 0;
+ }
+
+ canRedo(): boolean {
+ return this.redoStack.length > 0;
+ }
+
+ clear(): void {
+ this.history = [];
+ this.redoStack = [];
+ }
+}
diff --git a/apps/web/src/core/managers/media-manager.ts b/apps/web/src/core/managers/media-manager.ts
new file mode 100644
index 00000000..7b9db0d5
--- /dev/null
+++ b/apps/web/src/core/managers/media-manager.ts
@@ -0,0 +1,162 @@
+import type { EditorCore } from "@/core";
+import type { MediaAsset } from "@/types/assets";
+import { storageService } from "@/services/storage/storage-service";
+import { generateUUID } from "@/utils/id";
+import { videoCache } from "@/services/media/video-cache";
+import { hasMediaId } from "@/lib/timeline/element-utils";
+
+export class MediaManager {
+ private assets: MediaAsset[] = [];
+ private isLoading = false;
+ private listeners = new Set<() => void>();
+
+ constructor(private editor: EditorCore) {}
+
+ async addMediaAsset({
+ projectId,
+ asset,
+ }: {
+ projectId: string;
+ asset: Omit<MediaAsset, "id">;
+ }): Promise<void> {
+ const newAsset: MediaAsset = {
+ ...asset,
+ id: generateUUID(),
+ };
+
+ this.assets = [...this.assets, newAsset];
+ this.notify();
+
+ try {
+ await storageService.saveMediaAsset({ projectId, mediaAsset: newAsset });
+ } catch (error) {
+ console.error("Failed to save media asset:", error);
+ this.assets = this.assets.filter((asset) => asset.id !== newAsset.id);
+ this.notify();
+ }
+ }
+
+ async removeMediaAsset({
+ projectId,
+ id,
+ }: {
+ projectId: string;
+ id: string;
+ }): Promise<void> {
+ const asset = this.assets.find((asset) => asset.id === id);
+
+ videoCache.clearVideo({ mediaId: id });
+
+ if (asset?.url) {
+ URL.revokeObjectURL(asset.url);
+ if (asset.thumbnailUrl) {
+ URL.revokeObjectURL(asset.thumbnailUrl);
+ }
+ }
+
+ this.assets = this.assets.filter((asset) => asset.id !== id);
+ this.notify();
+
+ const tracks = this.editor.timeline.getTracks();
+ const elementsToRemove: Array<{ trackId: string; elementId: string }> = [];
+
+ for (const track of tracks) {
+ for (const element of track.elements) {
+ if (hasMediaId(element) && element.mediaId === id) {
+ elementsToRemove.push({ trackId: track.id, elementId: element.id });
+ }
+ }
+ }
+
+ if (elementsToRemove.length > 0) {
+ this.editor.timeline.deleteElements({ elements: elementsToRemove });
+ }
+
+ try {
+ await storageService.deleteMediaAsset({ projectId, id });
+ } catch (error) {
+ console.error("Failed to delete media asset:", error);
+ }
+ }
+
+ async loadProjectMedia({ projectId }: { projectId: string }): Promise<void> {
+ this.isLoading = true;
+ this.notify();
+
+ try {
+ const mediaAssets = await storageService.loadAllMediaAssets({
+ projectId,
+ });
+ this.assets = mediaAssets;
+ this.notify();
+ } catch (error) {
+ console.error("Failed to load media assets:", error);
+ } finally {
+ this.isLoading = false;
+ this.notify();
+ }
+ }
+
+ async clearProjectMedia({ projectId }: { projectId: string }): Promise<void> {
+ this.assets.forEach((asset) => {
+ if (asset.url) {
+ URL.revokeObjectURL(asset.url);
+ }
+ if (asset.thumbnailUrl) {
+ URL.revokeObjectURL(asset.thumbnailUrl);
+ }
+ });
+
+ const mediaIds = this.assets.map((asset) => asset.id);
+ this.assets = [];
+ this.notify();
+
+ try {
+ await Promise.all(
+ mediaIds.map((id) =>
+ storageService.deleteMediaAsset({ projectId, id }),
+ ),
+ );
+ } catch (error) {
+ console.error("Failed to clear media assets from storage:", error);
+ }
+ }
+
+ clearAllAssets(): void {
+ videoCache.clearAll();
+
+ this.assets.forEach((asset) => {
+ if (asset.url) {
+ URL.revokeObjectURL(asset.url);
+ }
+ if (asset.thumbnailUrl) {
+ URL.revokeObjectURL(asset.thumbnailUrl);
+ }
+ });
+
+ this.assets = [];
+ this.notify();
+ }
+
+ getAssets(): MediaAsset[] {
+ return this.assets;
+ }
+
+ setAssets({ assets }: { assets: MediaAsset[] }): void {
+ this.assets = assets;
+ this.notify();
+ }
+
+ isLoadingMedia(): boolean {
+ return this.isLoading;
+ }
+
+ subscribe(listener: () => void): () => void {
+ this.listeners.add(listener);
+ return () => this.listeners.delete(listener);
+ }
+
+ private notify(): void {
+ this.listeners.forEach((fn) => fn());
+ }
+}
diff --git a/apps/web/src/core/managers/playback-manager.ts b/apps/web/src/core/managers/playback-manager.ts
new file mode 100644
index 00000000..ca6f4e43
--- /dev/null
+++ b/apps/web/src/core/managers/playback-manager.ts
@@ -0,0 +1,162 @@
+import type { EditorCore } from "@/core";
+
+export class PlaybackManager {
+ private isPlaying = false;
+ private currentTime = 0;
+ private volume = 1;
+ private muted = false;
+ private previousVolume = 1;
+ private listeners = new Set<() => void>();
+ private playbackTimer: number | null = null;
+ private lastUpdate = 0;
+
+ constructor(private editor: EditorCore) {}
+
+ play(): void {
+ const duration = this.editor.timeline.getTotalDuration();
+
+ if (duration > 0) {
+ if (this.currentTime >= duration) {
+ this.seek({ time: 0 });
+ }
+ }
+
+ this.isPlaying = true;
+ this.startTimer();
+ this.notify();
+ }
+
+ pause(): void {
+ this.isPlaying = false;
+ this.stopTimer();
+ this.notify();
+ }
+
+ toggle(): void {
+ if (this.isPlaying) {
+ this.pause();
+ } else {
+ this.play();
+ }
+ }
+
+ seek({ time }: { time: number }): void {
+ const duration = this.editor.timeline.getTotalDuration();
+ this.currentTime = Math.max(0, Math.min(duration, time));
+ this.notify();
+
+ window.dispatchEvent(
+ new CustomEvent("playback-seek", {
+ detail: { time: this.currentTime },
+ }),
+ );
+ }
+
+ setVolume({ volume }: { volume: number }): void {
+ const clampedVolume = Math.max(0, Math.min(1, volume));
+ this.volume = clampedVolume;
+ this.muted = clampedVolume === 0;
+ if (clampedVolume > 0) {
+ this.previousVolume = clampedVolume;
+ }
+ this.notify();
+ }
+
+ mute(): void {
+ if (this.volume > 0) {
+ this.previousVolume = this.volume;
+ }
+ this.muted = true;
+ this.volume = 0;
+ this.notify();
+ }
+
+ unmute(): void {
+ this.muted = false;
+ this.volume = this.previousVolume;
+ this.notify();
+ }
+
+ toggleMute(): void {
+ if (this.muted) {
+ this.unmute();
+ } else {
+ this.mute();
+ }
+ }
+
+ getIsPlaying(): boolean {
+ return this.isPlaying;
+ }
+
+ getCurrentTime(): number {
+ return this.currentTime;
+ }
+
+ getVolume(): number {
+ return this.volume;
+ }
+
+ isMuted(): boolean {
+ return this.muted;
+ }
+
+ subscribe(listener: () => void): () => void {
+ this.listeners.add(listener);
+ return () => this.listeners.delete(listener);
+ }
+
+ private notify(): void {
+ this.listeners.forEach((fn) => fn());
+ }
+
+ private startTimer(): void {
+ if (this.playbackTimer) {
+ cancelAnimationFrame(this.playbackTimer);
+ }
+
+ this.lastUpdate = performance.now();
+ this.updateTime();
+ }
+
+ private stopTimer(): void {
+ if (this.playbackTimer) {
+ cancelAnimationFrame(this.playbackTimer);
+ this.playbackTimer = null;
+ }
+ }
+
+ private updateTime = (): void => {
+ if (!this.isPlaying) return;
+
+ const now = performance.now();
+ const delta = (now - this.lastUpdate) / 1000;
+ this.lastUpdate = now;
+
+ const newTime = this.currentTime + delta;
+ const duration = this.editor.timeline.getTotalDuration();
+
+ if (duration > 0 && newTime >= duration) {
+ this.pause();
+ this.currentTime = duration;
+ this.notify();
+
+ window.dispatchEvent(
+ new CustomEvent("playback-seek", {
+ detail: { time: duration },
+ }),
+ );
+ } else {
+ this.currentTime = newTime;
+ this.notify();
+
+ window.dispatchEvent(
+ new CustomEvent("playback-update", {
+ detail: { time: newTime },
+ }),
+ );
+ }
+
+ this.playbackTimer = requestAnimationFrame(this.updateTime);
+ };
+}
diff --git a/apps/web/src/core/managers/project-manager.ts b/apps/web/src/core/managers/project-manager.ts
new file mode 100644
index 00000000..db9e3fad
--- /dev/null
+++ b/apps/web/src/core/managers/project-manager.ts
@@ -0,0 +1,648 @@
+import type { EditorCore } from "@/core";
+import type {
+ TProject,
+ TProjectMetadata,
+ TProjectSortKey,
+ TProjectSortOption,
+ TProjectSettings,
+ TTimelineViewState,
+} from "@/types/project";
+import type { ExportOptions, ExportResult } from "@/types/export";
+import { storageService } from "@/services/storage/storage-service";
+import { toast } from "sonner";
+import { generateUUID } from "@/utils/id";
+import { UpdateProjectSettingsCommand } from "@/lib/commands/project";
+import {
+ DEFAULT_FPS,
+ DEFAULT_CANVAS_SIZE,
+ DEFAULT_COLOR,
+} from "@/constants/project-constants";
+import { buildDefaultScene, getProjectDurationFromScenes } from "@/lib/scenes";
+import { buildScene } from "@/services/renderer/scene-builder";
+import { CanvasRenderer } from "@/services/renderer/canvas-renderer";
+import {
+ CURRENT_STORAGE_VERSION,
+ migrations,
+ runStorageMigrations,
+} from "@/services/storage/migrations";
+import { DEFAULT_TIMELINE_VIEW_STATE } from "@/constants/timeline-constants";
+
+export interface MigrationState {
+ isMigrating: boolean;
+ fromVersion: number | null;
+ toVersion: number | null;
+ projectName: string | null;
+}
+
+export class ProjectManager {
+ private active: TProject | null = null;
+ private savedProjects: TProjectMetadata[] = [];
+ private isLoading = true;
+ private isInitialized = false;
+ private invalidProjectIds = new Set<string>();
+ private storageMigrationPromise: Promise<void> | null = null;
+ private listeners = new Set<() => void>();
+ private migrationState: MigrationState = {
+ isMigrating: false,
+ fromVersion: null,
+ toVersion: null,
+ projectName: null,
+ };
+
+ constructor(private editor: EditorCore) {}
+
+ private async ensureStorageMigrations(): Promise<void> {
+ if (this.storageMigrationPromise) {
+ await this.storageMigrationPromise;
+ return;
+ }
+
+ this.storageMigrationPromise = (async () => {
+ let hasShownState = false;
+
+ await runStorageMigrations({
+ migrations,
+ callbacks: {
+ onMigrationStart: ({ fromVersion, toVersion }) => {
+ hasShownState = true;
+ this.setMigrationState({
+ isMigrating: true,
+ fromVersion,
+ toVersion,
+ projectName: null,
+ });
+ },
+ },
+ });
+
+ if (hasShownState) {
+ this.setMigrationState({
+ isMigrating: false,
+ fromVersion: null,
+ toVersion: null,
+ projectName: null,
+ });
+ }
+ })();
+
+ await this.storageMigrationPromise;
+ }
+
+ async createNewProject({ name }: { name: string }): Promise<string> {
+ const mainScene = buildDefaultScene({ name: "Main scene", isMain: true });
+ const newProject: TProject = {
+ metadata: {
+ id: generateUUID(),
+ name,
+ duration: getProjectDurationFromScenes({ scenes: [mainScene] }),
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ scenes: [mainScene],
+ currentSceneId: mainScene.id,
+ settings: {
+ fps: DEFAULT_FPS,
+ canvasSize: DEFAULT_CANVAS_SIZE,
+ originalCanvasSize: null,
+ background: {
+ type: "color",
+ color: DEFAULT_COLOR,
+ },
+ },
+ version: CURRENT_STORAGE_VERSION,
+ };
+
+ this.active = newProject;
+ this.notify();
+
+ this.editor.media.clearAllAssets();
+ this.editor.scenes.initializeScenes({
+ scenes: newProject.scenes,
+ currentSceneId: newProject.currentSceneId,
+ });
+
+ try {
+ await storageService.saveProject({ project: newProject });
+ this.updateMetadata(newProject);
+
+ return newProject.metadata.id;
+ } catch (error) {
+ toast.error("Failed to save new project");
+ throw error;
+ }
+ }
+
+ async loadProject({ id }: { id: string }): Promise<void> {
+ if (!this.isInitialized) {
+ this.isLoading = true;
+ this.notify();
+ }
+
+ this.editor.save.pause();
+ await this.ensureStorageMigrations();
+ this.editor.media.clearAllAssets();
+ this.editor.scenes.clearScenes();
+
+ try {
+ const result = await storageService.loadProject({ id });
+ if (!result) {
+ throw new Error(`Project with id ${id} not found`);
+ }
+
+ const project = result.project;
+
+ this.active = project;
+ this.notify();
+
+ if (project.scenes && project.scenes.length > 0) {
+ this.editor.scenes.initializeScenes({
+ scenes: project.scenes,
+ currentSceneId: project.currentSceneId,
+ });
+ }
+
+ await this.editor.media.loadProjectMedia({ projectId: id });
+
+ if (!project.metadata.thumbnail) {
+ const didUpdateThumbnail = await this.updateThumbnailFromTimeline();
+ if (didUpdateThumbnail) {
+ await this.saveCurrentProject();
+ }
+ }
+ } catch (error) {
+ console.error("Failed to load project:", error);
+ throw error;
+ } finally {
+ this.isLoading = false;
+ this.notify();
+ this.editor.save.resume();
+ }
+ }
+
+ async saveCurrentProject(): Promise<void> {
+ if (!this.active) return;
+
+ try {
+ const scenes = this.editor.scenes.getScenes();
+ const updatedProject = {
+ ...this.active,
+ scenes,
+ metadata: {
+ ...this.active.metadata,
+ duration: getProjectDurationFromScenes({ scenes }),
+ updatedAt: new Date(),
+ },
+ };
+
+ await storageService.saveProject({ project: updatedProject });
+ this.active = updatedProject;
+ this.updateMetadata(updatedProject);
+ } catch (error) {
+ console.error("Failed to save project:", error);
+ }
+ }
+
+ async export({ options }: { options: ExportOptions }): Promise<ExportResult> {
+ return this.editor.renderer.exportProject({ options });
+ }
+
+ async loadAllProjects(): Promise<void> {
+ if (!this.isInitialized) {
+ this.isLoading = true;
+ this.notify();
+ }
+
+ await this.ensureStorageMigrations();
+ try {
+ const metadata = await storageService.loadAllProjectsMetadata();
+ this.savedProjects = metadata;
+ this.notify();
+ } catch (error) {
+ console.error("Failed to load projects:", error);
+ } finally {
+ this.isLoading = false;
+ this.isInitialized = true;
+ this.notify();
+ }
+ }
+
+ async deleteProjects({ ids }: { ids: string[] }): Promise<void> {
+ const uniqueIds = Array.from(new Set(ids));
+ if (uniqueIds.length === 0) return;
+
+ try {
+ await Promise.all(
+ uniqueIds.map((id) =>
+ Promise.all([
+ storageService.deleteProjectMedia({ projectId: id }),
+ storageService.deleteProject({ id }),
+ ]),
+ ),
+ );
+
+ const idSet = new Set(uniqueIds);
+ this.savedProjects = this.savedProjects.filter(
+ (project) => !idSet.has(project.id),
+ );
+
+ const shouldClearActive =
+ this.active && idSet.has(this.active.metadata.id);
+
+ if (shouldClearActive) {
+ this.active = null;
+ this.editor.media.clearAllAssets();
+ this.editor.scenes.clearScenes();
+ }
+
+ this.notify();
+ } catch (error) {
+ console.error("Failed to delete projects:", error);
+ }
+ }
+
+ closeProject(): void {
+ this.active = null;
+ this.notify();
+
+ this.editor.media.clearAllAssets();
+ this.editor.scenes.clearScenes();
+ }
+
+ async renameProject({
+ id,
+ name,
+ }: {
+ id: string;
+ name: string;
+ }): Promise<void> {
+ try {
+ const result = await storageService.loadProject({ id });
+ if (!result) {
+ toast.error("Project not found", {
+ description: "Please try again",
+ });
+ return;
+ }
+
+ const updatedProject: TProject = {
+ ...result.project,
+ metadata: {
+ ...result.project.metadata,
+ name,
+ updatedAt: new Date(),
+ },
+ };
+
+ await storageService.saveProject({ project: updatedProject });
+
+ if (this.active?.metadata.id === id) {
+ this.active = updatedProject;
+ this.notify();
+ }
+
+ this.updateMetadata(updatedProject);
+ } catch (error) {
+ console.error("Failed to rename project:", error);
+ toast.error("Failed to rename project", {
+ description:
+ error instanceof Error ? error.message : "Please try again",
+ });
+ }
+ }
+
+ async duplicateProjects({ ids }: { ids: string[] }): Promise<string[]> {
+ const uniqueIds = Array.from(new Set(ids));
+ if (uniqueIds.length === 0) return [];
+
+ try {
+ const getDuplicateBaseName = ({ name }: { name: string }) => {
+ const match = name.match(/^\((\d+)\)\s+(.+)$/);
+ const number = match ? Number.parseInt(match[1], 10) : null;
+ const baseName = match ? match[2] : name;
+ return { baseName, number };
+ };
+
+ const loadResults = await Promise.all(
+ uniqueIds.map(async (projectId) => {
+ const result = await storageService.loadProject({ id: projectId });
+ return { projectId, project: result?.project ?? null };
+ }),
+ );
+
+ const missingProjectIds = loadResults
+ .filter((result) => !result.project)
+ .map((result) => result.projectId);
+
+ if (missingProjectIds.length > 0) {
+ toast.error(
+ missingProjectIds.length === 1
+ ? "Project not found"
+ : "Projects not found",
+ {
+ description:
+ missingProjectIds.length === 1
+ ? "Please try again"
+ : "Some projects could not be found",
+ },
+ );
+ throw new Error(`Projects not found: ${missingProjectIds.join(", ")}`);
+ }
+
+ const projectsToDuplicate = loadResults.flatMap((result) =>
+ result.project ? [result.project] : [],
+ );
+
+ const maxNumberByBaseName = new Map<string, number>();
+
+ for (const project of this.savedProjects) {
+ const { baseName, number } = getDuplicateBaseName({
+ name: project.name,
+ });
+
+ if (number === null) continue;
+
+ const currentMax = maxNumberByBaseName.get(baseName);
+ if (currentMax === undefined || number > currentMax) {
+ maxNumberByBaseName.set(baseName, number);
+ }
+ }
+
+ const nextNumberByBaseName = new Map<string, number>();
+ for (const [baseName, maxNumber] of maxNumberByBaseName) {
+ nextNumberByBaseName.set(baseName, maxNumber + 1);
+ }
+
+ const duplicationPlans = projectsToDuplicate.map((project) => {
+ const { baseName } = getDuplicateBaseName({
+ name: project.metadata.name,
+ });
+ const nextNumber = nextNumberByBaseName.get(baseName) ?? 1;
+ nextNumberByBaseName.set(baseName, nextNumber + 1);
+
+ const newProjectId = generateUUID();
+ const newProject: TProject = {
+ ...project,
+ metadata: {
+ ...project.metadata,
+ id: newProjectId,
+ name: `(${nextNumber}) ${baseName}`,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ };
+
+ return {
+ newProjectId,
+ newProject,
+ sourceProjectId: project.metadata.id,
+ };
+ });
+
+ await Promise.all(
+ duplicationPlans.map(({ newProject }) =>
+ storageService.saveProject({ project: newProject }),
+ ),
+ );
+
+ await Promise.all(
+ duplicationPlans.map(async ({ sourceProjectId, newProjectId }) => {
+ const sourceMediaAssets = await storageService.loadAllMediaAssets({
+ projectId: sourceProjectId,
+ });
+
+ await Promise.all(
+ sourceMediaAssets.map((mediaAsset) =>
+ storageService.saveMediaAsset({
+ projectId: newProjectId,
+ mediaAsset,
+ }),
+ ),
+ );
+ }),
+ );
+
+ for (const { newProject } of duplicationPlans) {
+ this.updateMetadata(newProject);
+ }
+
+ return duplicationPlans.map((plan) => plan.newProjectId);
+ } catch (error) {
+ console.error("Failed to duplicate projects:", error);
+ toast.error("Failed to duplicate projects", {
+ description:
+ error instanceof Error ? error.message : "Please try again",
+ });
+ throw error;
+ }
+ }
+
+ async updateSettings({
+ settings,
+ pushHistory = true,
+ }: {
+ settings: Partial<TProjectSettings>;
+ pushHistory?: boolean;
+ }): Promise<void> {
+ if (!this.active) return;
+
+ const command = new UpdateProjectSettingsCommand(settings);
+ if (pushHistory) {
+ this.editor.command.execute({ command });
+ return;
+ }
+
+ command.execute();
+ }
+
+ async updateThumbnail({ thumbnail }: { thumbnail: string }): Promise<void> {
+ if (!this.active) return;
+
+ const updatedProject: TProject = {
+ ...this.active,
+ metadata: { ...this.active.metadata, thumbnail, updatedAt: new Date() },
+ };
+ this.active = updatedProject;
+ this.notify();
+ this.updateMetadata(updatedProject);
+ this.editor.save.markDirty();
+ }
+
+ async prepareExit(): Promise<void> {
+ if (!this.active) return;
+
+ try {
+ const didUpdateThumbnail = await this.updateThumbnailFromTimeline();
+ if (didUpdateThumbnail) {
+ await this.editor.save.flush();
+ }
+ } catch (error) {
+ console.error("Failed to generate project thumbnail on exit:", error);
+ }
+ }
+
+ getFilteredAndSortedProjects({
+ searchQuery,
+ sortOption,
+ }: {
+ searchQuery: string;
+ sortOption: TProjectSortOption;
+ }): TProjectMetadata[] {
+ const filteredProjects = this.savedProjects.filter((project) =>
+ project.name.toLowerCase().includes(searchQuery.toLowerCase()),
+ );
+
+ const [key, order] = sortOption.split("-") as [
+ TProjectSortKey,
+ "asc" | "desc",
+ ];
+
+ const sortedProjects = [...filteredProjects].sort((a, b) => {
+ const aValue = a[key];
+ const bValue = b[key];
+
+ if (order === "asc") {
+ if (aValue < bValue) return -1;
+ if (aValue > bValue) return 1;
+ return 0;
+ }
+ if (aValue > bValue) return -1;
+ if (aValue < bValue) return 1;
+ return 0;
+ });
+
+ return sortedProjects;
+ }
+
+ isInvalidProjectId({ id }: { id: string }): boolean {
+ return this.invalidProjectIds.has(id);
+ }
+
+ markProjectIdAsInvalid({ id }: { id: string }): void {
+ this.invalidProjectIds.add(id);
+ this.notify();
+ }
+
+ clearInvalidProjectIds(): void {
+ this.invalidProjectIds.clear();
+ this.notify();
+ }
+
+ getActive(): TProject {
+ if (!this.active) {
+ throw new Error("No active project");
+ }
+ return this.active;
+ }
+
+ /**
+ * for agents:
+ * in most cases, the project is guaranteed to be active, in which getActive() should be used instead.
+ * for very rare cases, this function may be used.
+ */
+ getActiveOrNull(): TProject | null {
+ return this.active;
+ }
+
+ getTimelineViewState(): TTimelineViewState {
+ return this.active?.timelineViewState ?? DEFAULT_TIMELINE_VIEW_STATE;
+ }
+
+ setTimelineViewState({ viewState }: { viewState: TTimelineViewState }): void {
+ if (!this.active) return;
+ this.active = {
+ ...this.active,
+ timelineViewState: viewState ?? undefined,
+ };
+ this.editor.save.markDirty();
+ }
+
+ getSavedProjects(): TProjectMetadata[] {
+ return this.savedProjects;
+ }
+
+ getIsLoading(): boolean {
+ return this.isLoading;
+ }
+
+ getIsInitialized(): boolean {
+ return this.isInitialized;
+ }
+
+ getMigrationState(): MigrationState {
+ return this.migrationState;
+ }
+
+ private setMigrationState(state: Partial<MigrationState>): void {
+ this.migrationState = { ...this.migrationState, ...state };
+ this.notify();
+ }
+
+ setActiveProject({ project }: { project: TProject }): void {
+ this.active = project;
+ this.notify();
+ }
+
+ subscribe(listener: () => void): () => void {
+ this.listeners.add(listener);
+ return () => this.listeners.delete(listener);
+ }
+
+ private async updateThumbnailFromTimeline(): Promise<boolean> {
+ if (!this.active) return false;
+
+ const tracks = this.editor.timeline.getTracks();
+ const mediaAssets = this.editor.media.getAssets();
+ const duration = this.editor.timeline.getTotalDuration();
+
+ if (duration === 0) return false;
+
+ const { canvasSize, background } = this.active.settings;
+
+ const scene = buildScene({
+ tracks,
+ mediaAssets,
+ duration,
+ canvasSize,
+ background,
+ });
+
+ const renderer = new CanvasRenderer({
+ width: canvasSize.width,
+ height: canvasSize.height,
+ fps: this.active.settings.fps,
+ });
+
+ const tempCanvas = document.createElement("canvas");
+ tempCanvas.width = canvasSize.width;
+ tempCanvas.height = canvasSize.height;
+
+ await renderer.renderToCanvas({
+ node: scene,
+ time: 0,
+ targetCanvas: tempCanvas,
+ });
+
+ const thumbnailDataUrl = tempCanvas.toDataURL("image/png");
+
+ await this.updateThumbnail({ thumbnail: thumbnailDataUrl });
+ return true;
+ }
+
+ private updateMetadata(project: TProject): void {
+ const index = this.savedProjects.findIndex(
+ (p) => p.id === project.metadata.id,
+ );
+
+ if (index !== -1) {
+ this.savedProjects[index] = project.metadata;
+ } else {
+ this.savedProjects = [project.metadata, ...this.savedProjects];
+ }
+
+ this.notify();
+ }
+
+ private notify(): void {
+ this.listeners.forEach((fn) => fn());
+ }
+}
diff --git a/apps/web/src/core/managers/renderer-manager.ts b/apps/web/src/core/managers/renderer-manager.ts
new file mode 100644
index 00000000..22f1ebb8
--- /dev/null
+++ b/apps/web/src/core/managers/renderer-manager.ts
@@ -0,0 +1,129 @@
+import type { EditorCore } from "@/core";
+import type { RootNode } from "@/services/renderer/nodes/root-node";
+import type { ExportOptions, ExportResult } from "@/types/export";
+import { SceneExporter } from "@/services/renderer/scene-exporter";
+import { buildScene } from "@/services/renderer/scene-builder";
+import { createTimelineAudioBuffer } from "@/lib/media/audio";
+
+export class RendererManager {
+ private renderTree: RootNode | null = null;
+ private listeners = new Set<() => void>();
+
+ constructor(private editor: EditorCore) {}
+
+ setRenderTree({ renderTree }: { renderTree: RootNode | null }): void {
+ this.renderTree = renderTree;
+ this.notify();
+ }
+
+ getRenderTree(): RootNode | null {
+ return this.renderTree;
+ }
+
+ async exportProject({
+ options,
+ }: {
+ options: ExportOptions;
+ }): Promise<ExportResult> {
+ const { format, quality, fps, includeAudio, onProgress, onCancel } =
+ options;
+
+ try {
+ const tracks = this.editor.timeline.getTracks();
+ const mediaAssets = this.editor.media.getAssets();
+ const activeProject = this.editor.project.getActive();
+
+ if (!activeProject) {
+ return { success: false, error: "No active project" };
+ }
+
+ const duration = this.editor.timeline.getTotalDuration();
+ if (duration === 0) {
+ return { success: false, error: "Project is empty" };
+ }
+
+ const exportFps = fps || activeProject.settings.fps;
+ const canvasSize = activeProject.settings.canvasSize;
+
+ let audioBuffer: AudioBuffer | null = null;
+ if (includeAudio) {
+ onProgress?.({ progress: 0.05 });
+ audioBuffer = await createTimelineAudioBuffer({
+ tracks,
+ mediaAssets,
+ duration,
+ });
+ }
+
+ const scene = buildScene({
+ tracks,
+ mediaAssets,
+ duration,
+ canvasSize,
+ background: activeProject.settings.background,
+ });
+
+ const exporter = new SceneExporter({
+ width: canvasSize.width,
+ height: canvasSize.height,
+ fps: exportFps,
+ format,
+ quality,
+ shouldIncludeAudio: !!includeAudio,
+ audioBuffer: audioBuffer || undefined,
+ });
+
+ exporter.on("progress", (progress) => {
+ const adjustedProgress = includeAudio
+ ? 0.05 + progress * 0.95
+ : progress;
+ onProgress?.({ progress: adjustedProgress });
+ });
+
+ let cancelled = false;
+ const checkCancel = () => {
+ if (onCancel?.()) {
+ cancelled = true;
+ exporter.cancel();
+ }
+ };
+
+ const cancelInterval = setInterval(checkCancel, 100);
+
+ try {
+ const buffer = await exporter.export({ rootNode: scene });
+ clearInterval(cancelInterval);
+
+ if (cancelled) {
+ return { success: false, cancelled: true };
+ }
+
+ if (!buffer) {
+ return { success: false, error: "Export failed to produce buffer" };
+ }
+
+ return {
+ success: true,
+ buffer,
+ };
+ } finally {
+ clearInterval(cancelInterval);
+ }
+ } catch (error) {
+ console.error("Export failed:", error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "Unknown export error",
+ };
+ }
+ }
+
+ subscribe(listener: () => void): () => void {
+ this.listeners.add(listener);
+ return () => this.listeners.delete(listener);
+ }
+
+ private notify(): void {
+ this.listeners.forEach((fn) => fn());
+ }
+}
diff --git a/apps/web/src/core/managers/save-manager.ts b/apps/web/src/core/managers/save-manager.ts
new file mode 100644
index 00000000..d5881b6b
--- /dev/null
+++ b/apps/web/src/core/managers/save-manager.ts
@@ -0,0 +1,107 @@
+import type { EditorCore } from "@/core";
+
+type SaveManagerOptions = {
+ debounceMs?: number;
+};
+
+export class SaveManager {
+ private debounceMs: number;
+ private isPaused = false;
+ private isSaving = false;
+ private hasPendingSave = false;
+ private saveTimer: ReturnType<typeof setTimeout> | null = null;
+ private unsubscribeHandlers: Array<() => void> = [];
+
+ constructor(
+ private editor: EditorCore,
+ { debounceMs = 800 }: SaveManagerOptions = {},
+ ) {
+ this.debounceMs = debounceMs;
+ }
+
+ start(): void {
+ if (this.unsubscribeHandlers.length > 0) return;
+
+ this.unsubscribeHandlers = [
+ this.editor.scenes.subscribe(() => {
+ this.markDirty();
+ }),
+ this.editor.timeline.subscribe(() => {
+ this.markDirty();
+ }),
+ ];
+ }
+
+ stop(): void {
+ for (const unsubscribe of this.unsubscribeHandlers) {
+ unsubscribe();
+ }
+ this.unsubscribeHandlers = [];
+ this.clearTimer();
+ }
+
+ pause(): void {
+ this.isPaused = true;
+ }
+
+ resume(): void {
+ this.isPaused = false;
+ if (this.hasPendingSave) {
+ this.queueSave();
+ }
+ }
+
+ markDirty({ force = false }: { force?: boolean } = {}): void {
+ if (this.isPaused && !force) return;
+ this.hasPendingSave = true;
+ this.queueSave();
+ }
+
+ async flush(): Promise<void> {
+ this.hasPendingSave = true;
+ await this.saveNow();
+ }
+
+ getIsDirty(): boolean {
+ return this.hasPendingSave || this.isSaving;
+ }
+
+ private queueSave(): void {
+ if (this.isSaving) return;
+ if (this.saveTimer) {
+ clearTimeout(this.saveTimer);
+ }
+ this.saveTimer = setTimeout(() => {
+ void this.saveNow();
+ }, this.debounceMs);
+ }
+
+ private async saveNow(): Promise<void> {
+ if (this.isSaving) return;
+ if (!this.hasPendingSave) return;
+
+ const activeProject = this.editor.project.getActive();
+ if (!activeProject) return;
+ if (this.editor.project.getIsLoading()) return;
+ if (this.editor.project.getMigrationState().isMigrating) return;
+
+ this.isSaving = true;
+ this.hasPendingSave = false;
+ this.clearTimer();
+
+ try {
+ await this.editor.project.saveCurrentProject();
+ } finally {
+ this.isSaving = false;
+ if (this.hasPendingSave) {
+ this.queueSave();
+ }
+ }
+ }
+
+ private clearTimer(): void {
+ if (!this.saveTimer) return;
+ clearTimeout(this.saveTimer);
+ this.saveTimer = null;
+ }
+}
diff --git a/apps/web/src/core/managers/scenes-manager.ts b/apps/web/src/core/managers/scenes-manager.ts
new file mode 100644
index 00000000..447ec820
--- /dev/null
+++ b/apps/web/src/core/managers/scenes-manager.ts
@@ -0,0 +1,316 @@
+import type { EditorCore } from "@/core";
+import type { TimelineTrack, TScene } from "@/types/timeline";
+import { storageService } from "@/services/storage/storage-service";
+import {
+ getMainScene,
+ ensureMainScene,
+ canDeleteScene,
+ findCurrentScene,
+} from "@/lib/scenes";
+import { getFrameTime, isBookmarkAtTime } from "@/lib/timeline/bookmarks";
+import { ensureMainTrack } from "@/lib/timeline/track-utils";
+import {
+ CreateSceneCommand,
+ DeleteSceneCommand,
+ RemoveBookmarkCommand,
+ RenameSceneCommand,
+ ToggleBookmarkCommand,
+} 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 loadProjectScenes({ projectId }: { projectId: string }): Promise<void> {
+ try {
+ const result = await storageService.loadProject({ id: projectId });
+ if (result?.project.scenes) {
+ const { scenes: ensuredScenes, hasAddedMainTrack } =
+ this.ensureScenesHaveMainTrack({
+ scenes: result.project.scenes ?? [],
+ });
+ const currentScene = findCurrentScene({
+ scenes: ensuredScenes,
+ currentSceneId: result.project.currentSceneId,
+ });
+
+ this.list = ensuredScenes;
+ this.active = currentScene;
+ this.notify();
+
+ if (hasAddedMainTrack) {
+ 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 });
+ }
+ }
+ }
+ } 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 { scenes: scenesWithMainTracks, hasAddedMainTrack } =
+ this.ensureScenesHaveMainTrack({ scenes: ensuredScenes });
+ const currentScene = currentSceneId
+ ? scenesWithMainTracks.find((s) => s.id === currentSceneId)
+ : null;
+
+ const fallbackScene = getMainScene({ scenes: scenesWithMainTracks });
+
+ this.list = scenesWithMainTracks;
+ this.active = currentScene || fallbackScene;
+ this.notify();
+
+ const hasAddedMainScene = ensuredScenes.length > scenes.length;
+ if (hasAddedMainScene || hasAddedMainTrack) {
+ const activeProject = this.editor.project.getActive();
+
+ if (activeProject) {
+ const updatedProject = {
+ ...activeProject,
+ scenes: scenesWithMainTracks,
+ 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;
+ }
+
+ 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: TimelineTrack[] }): 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 });
+ }
+ }
+
+ private ensureScenesHaveMainTrack({ scenes }: { scenes: TScene[] }): {
+ scenes: TScene[];
+ hasAddedMainTrack: boolean;
+ } {
+ let hasAddedMainTrack = false;
+ const ensuredScenes: TScene[] = [];
+
+ for (const scene of scenes) {
+ const existingTracks = scene.tracks ?? [];
+ const updatedTracks = ensureMainTrack({ tracks: existingTracks });
+ if (updatedTracks !== existingTracks) {
+ hasAddedMainTrack = true;
+ ensuredScenes.push({
+ ...scene,
+ tracks: updatedTracks,
+ updatedAt: new Date(),
+ });
+ } else {
+ ensuredScenes.push(scene);
+ }
+ }
+
+ return { scenes: ensuredScenes, hasAddedMainTrack };
+ }
+}
diff --git a/apps/web/src/core/managers/selection-manager.ts b/apps/web/src/core/managers/selection-manager.ts
new file mode 100644
index 00000000..fa6a02bf
--- /dev/null
+++ b/apps/web/src/core/managers/selection-manager.ts
@@ -0,0 +1,35 @@
+import type { EditorCore } from "@/core";
+
+type ElementRef = { trackId: string; elementId: string };
+
+export class SelectionManager {
+ private selectedElements: ElementRef[] = [];
+ private listeners = new Set<() => void>();
+
+ constructor(editor: EditorCore) {
+ void editor;
+ }
+
+ getSelectedElements(): ElementRef[] {
+ return this.selectedElements;
+ }
+
+ setSelectedElements({ elements }: { elements: ElementRef[] }): void {
+ this.selectedElements = elements;
+ this.notify();
+ }
+
+ clearSelection(): void {
+ this.selectedElements = [];
+ this.notify();
+ }
+
+ subscribe(listener: () => void): () => void {
+ this.listeners.add(listener);
+ return () => this.listeners.delete(listener);
+ }
+
+ private notify(): void {
+ this.listeners.forEach((fn) => fn());
+ }
+}
diff --git a/apps/web/src/core/managers/timeline-manager.ts b/apps/web/src/core/managers/timeline-manager.ts
new file mode 100644
index 00000000..867df3a8
--- /dev/null
+++ b/apps/web/src/core/managers/timeline-manager.ts
@@ -0,0 +1,275 @@
+import type { EditorCore } from "@/core";
+import type {
+ TrackType,
+ TimelineTrack,
+ TextElement,
+ TimelineElement,
+ ClipboardItem,
+} from "@/types/timeline";
+import { calculateTotalDuration } from "@/lib/timeline";
+import {
+ AddTrackCommand,
+ RemoveTrackCommand,
+ ToggleTrackMuteCommand,
+ ToggleTrackVisibilityCommand,
+ InsertElementCommand,
+ UpdateElementTrimCommand,
+ UpdateElementDurationCommand,
+ DeleteElementsCommand,
+ DuplicateElementsCommand,
+ ToggleElementsVisibilityCommand,
+ ToggleElementsMutedCommand,
+ UpdateTextElementCommand,
+ SplitElementsCommand,
+ PasteCommand,
+ UpdateElementStartTimeCommand,
+ MoveElementCommand,
+} from "@/lib/commands/timeline";
+import type { InsertElementParams } from "@/lib/commands/timeline/element/insert-element";
+
+export class TimelineManager {
+ private listeners = new Set<() => void>();
+
+ constructor(private editor: EditorCore) {}
+
+ addTrack({ type, index }: { type: TrackType; index?: number }): string {
+ const command = new AddTrackCommand(type, index);
+ this.editor.command.execute({ command });
+ return command.getTrackId();
+ }
+
+ removeTrack({ trackId }: { trackId: string }): void {
+ const command = new RemoveTrackCommand(trackId);
+ this.editor.command.execute({ command });
+ }
+
+ insertElement({ element, placement }: InsertElementParams): void {
+ const command = new InsertElementCommand({ element, placement });
+ this.editor.command.execute({ command });
+ }
+
+ updateElementTrim({
+ elementId,
+ trimStart,
+ trimEnd,
+ pushHistory = true,
+ }: {
+ elementId: string;
+ trimStart: number;
+ trimEnd: number;
+ pushHistory?: boolean;
+ }): void {
+ const command = new UpdateElementTrimCommand(elementId, trimStart, trimEnd);
+ if (pushHistory) {
+ this.editor.command.execute({ command });
+ } else {
+ command.execute();
+ }
+ }
+
+ updateElementDuration({
+ trackId,
+ elementId,
+ duration,
+ pushHistory = true,
+ }: {
+ trackId: string;
+ elementId: string;
+ duration: number;
+ pushHistory?: boolean;
+ }): void {
+ const command = new UpdateElementDurationCommand(
+ trackId,
+ elementId,
+ duration,
+ );
+ if (pushHistory) {
+ this.editor.command.execute({ command });
+ } else {
+ command.execute();
+ }
+ }
+
+ updateElementStartTime({
+ elements,
+ startTime,
+ }: {
+ elements: { trackId: string; elementId: string }[];
+ startTime: number;
+ }): void {
+ const command = new UpdateElementStartTimeCommand(elements, startTime);
+ this.editor.command.execute({ command });
+ }
+
+ moveElement({
+ sourceTrackId,
+ targetTrackId,
+ elementId,
+ newStartTime,
+ createTrack,
+ }: {
+ sourceTrackId: string;
+ targetTrackId: string;
+ elementId: string;
+ newStartTime: number;
+ createTrack?: { type: TrackType; index: number };
+ }): void {
+ const command = new MoveElementCommand(
+ sourceTrackId,
+ targetTrackId,
+ elementId,
+ newStartTime,
+ createTrack,
+ );
+ this.editor.command.execute({ command });
+ }
+
+ toggleTrackMute({ trackId }: { trackId: string }): void {
+ const command = new ToggleTrackMuteCommand(trackId);
+ this.editor.command.execute({ command });
+ }
+
+ toggleTrackVisibility({ trackId }: { trackId: string }): void {
+ const command = new ToggleTrackVisibilityCommand(trackId);
+ this.editor.command.execute({ command });
+ }
+
+ splitElements({
+ elements,
+ splitTime,
+ retainSide = "both",
+ }: {
+ elements: { trackId: string; elementId: string }[];
+ splitTime: number;
+ retainSide?: "both" | "left" | "right";
+ }): { trackId: string; elementId: string }[] {
+ const command = new SplitElementsCommand(elements, splitTime, retainSide);
+ this.editor.command.execute({ command });
+ return command.getRightSideElements();
+ }
+
+ getTotalDuration(): number {
+ return calculateTotalDuration({ tracks: this.getTracks() });
+ }
+
+ getTrackById({ trackId }: { trackId: string }): TimelineTrack | null {
+ return this.getTracks().find((track) => track.id === trackId) ?? null;
+ }
+
+ getElementsWithTracks({
+ elements,
+ }: {
+ elements: { trackId: string; elementId: string }[];
+ }): Array<{ track: TimelineTrack; element: TimelineElement }> {
+ const result: Array<{ track: TimelineTrack; element: TimelineElement }> =
+ [];
+
+ for (const { trackId, elementId } of elements) {
+ const track = this.getTrackById({ trackId });
+ const element = track?.elements.find(
+ (trackElement) => trackElement.id === elementId,
+ );
+
+ if (track && element) {
+ result.push({ track, element });
+ }
+ }
+
+ return result;
+ }
+
+ pasteAtTime({
+ time,
+ clipboardItems,
+ }: {
+ time: number;
+ clipboardItems: ClipboardItem[];
+ }): { trackId: string; elementId: string }[] {
+ const command = new PasteCommand(time, clipboardItems);
+ this.editor.command.execute({ command });
+ return command.getPastedElements();
+ }
+
+ deleteElements({
+ elements,
+ }: {
+ elements: { trackId: string; elementId: string }[];
+ }): void {
+ const command = new DeleteElementsCommand(elements);
+ this.editor.command.execute({ command });
+ }
+
+ updateTextElement({
+ trackId,
+ elementId,
+ updates,
+ }: {
+ trackId: string;
+ elementId: string;
+ updates: Partial<
+ Pick<
+ TextElement,
+ | "content"
+ | "fontSize"
+ | "fontFamily"
+ | "color"
+ | "backgroundColor"
+ | "textAlign"
+ | "fontWeight"
+ | "fontStyle"
+ | "textDecoration"
+ | "transform"
+ | "opacity"
+ >
+ >;
+ }): void {
+ const command = new UpdateTextElementCommand(trackId, elementId, updates);
+ this.editor.command.execute({ command });
+ }
+
+ duplicateElements({
+ elements,
+ }: {
+ elements: { trackId: string; elementId: string }[];
+ }): { trackId: string; elementId: string }[] {
+ const command = new DuplicateElementsCommand({ elements });
+ this.editor.command.execute({ command });
+ return command.getDuplicatedElements();
+ }
+
+ toggleElementsVisibility({
+ elements,
+ }: {
+ elements: { trackId: string; elementId: string }[];
+ }): void {
+ const command = new ToggleElementsVisibilityCommand(elements);
+ this.editor.command.execute({ command });
+ }
+
+ toggleElementsMuted({
+ elements,
+ }: {
+ elements: { trackId: string; elementId: string }[];
+ }): void {
+ const command = new ToggleElementsMutedCommand(elements);
+ this.editor.command.execute({ command });
+ }
+
+ getTracks(): TimelineTrack[] {
+ return this.editor.scenes.getActiveScene()?.tracks ?? [];
+ }
+
+ subscribe(listener: () => void): () => void {
+ this.listeners.add(listener);
+ return () => this.listeners.delete(listener);
+ }
+
+ private notify(): void {
+ this.listeners.forEach((fn) => fn());
+ }
+
+ updateTracks(newTracks: TimelineTrack[]): void {
+ this.editor.scenes.updateSceneTracks({ tracks: newTracks });
+ this.notify();
+ }
+}
diff --git a/apps/web/src/data/colors/pattern-craft.ts b/apps/web/src/data/colors/pattern-craft.ts
index f7a5fc5b..0ede8cec 100644
--- a/apps/web/src/data/colors/pattern-craft.ts
+++ b/apps/web/src/data/colors/pattern-craft.ts
@@ -1,33 +1,69 @@
// These are the gradients from Pattern Craft (https://patterncraft.fun/)
export const patternCraftGradients = [
- // Dreamy Sky Pink Glow
- "radial-gradient(circle at 30% 70%, rgba(173, 216, 230, 0.35), transparent 60%), radial-gradient(circle at 70% 30%, rgba(255, 182, 193, 0.4), transparent 60%)",
+ // Dreamy Sky Pink Glow
+ "radial-gradient(circle at 30% 70%, rgba(173, 216, 230, 0.35), transparent 60%), radial-gradient(circle at 70% 30%, rgba(255, 182, 193, 0.4), transparent 60%), white",
- // Soft Warm Pastel Texture
- "radial-gradient(circle at 20% 80%, rgba(255, 182, 153, 0.3) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255, 244, 214, 0.5) 0%, transparent 50%), radial-gradient(circle at 40% 40%, rgba(255, 182, 153, 0.1) 0%, transparent 50%), #fff8f0",
+ // Soft Warm Pastel Texture
+ "radial-gradient(circle at 20% 80%, rgba(255, 182, 153, 0.3) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255, 244, 214, 0.5) 0%, transparent 50%), radial-gradient(circle at 40% 40%, rgba(255, 182, 153, 0.1) 0%, transparent 50%), #fff8f0",
- // Warm Soft Coral & Cream
- "radial-gradient(circle at 20% 80%, rgba(255, 160, 146, 0.25) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255, 244, 228, 0.3) 0%, transparent 50%), radial-gradient(circle at 40% 40%, rgba(255, 160, 146, 0.15) 0%, transparent 50%), #fef9f7",
+ // Warm Soft Coral & Cream
+ "radial-gradient(circle at 20% 80%, rgba(255, 160, 146, 0.25) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255, 244, 228, 0.3) 0%, transparent 50%), radial-gradient(circle at 40% 40%, rgba(255, 160, 146, 0.15) 0%, transparent 50%), #fef9f7",
- // Soft Green Glow
- "radial-gradient(circle at center, #8FFFB0, transparent), white",
+ // Soft Green Glow
+ "radial-gradient(circle at center, #8FFFB0, transparent), white",
- // Purple Glow Right
- "radial-gradient(circle at top right, rgba(173, 109, 244, 0.5), transparent 70%)",
+ // Purple Glow Right
+ "radial-gradient(circle at top right, rgba(173, 109, 244, 0.5), transparent 70%), white",
- // Teal Glow Right
- "radial-gradient(circle at top right, rgba(56, 193, 182, 0.5), transparent 70%)",
+ // Teal Glow Right
+ "radial-gradient(circle at top right, rgba(56, 193, 182, 0.5), transparent 70%), white",
- // Warm Orange Glow Right
- "radial-gradient(circle at top right, rgba(255, 140, 60, 0.5), transparent 70%)",
+ // Warm Orange Glow Right
+ "radial-gradient(circle at top right, rgba(255, 140, 60, 0.5), transparent 70%), white",
- // Cool Blue Glow Right
- "radial-gradient(circle at top right, rgba(70, 130, 180, 0.5), transparent 70%)",
+ // Cool Blue Glow Right
+ "radial-gradient(circle at top right, rgba(70, 130, 180, 0.5), transparent 70%), white",
- // Purple Glow Left
- "radial-gradient(circle at top left, rgba(173, 109, 244, 0.5), transparent 70%)",
+ // Purple Glow Left
+ "radial-gradient(circle at top left, rgba(173, 109, 244, 0.5), transparent 70%), white",
- // Pastel Wave
- "linear-gradient(120deg, #d5c5ff 0%, #a7f3d0 50%, #f0f0f0 100%)",
+ // Pastel Wave
+ "linear-gradient(120deg, #d5c5ff 0%, #a7f3d0 50%, #f0f0f0 100%)",
+
+ // Aurora Dream Corner Whispers
+ "radial-gradient(ellipse 85% 65% at 8% 8%, rgba(175, 109, 255, 0.42), transparent 60%), radial-gradient(ellipse 75% 60% at 75% 35%, rgba(255, 235, 170, 0.55), transparent 62%), radial-gradient(ellipse 70% 60% at 15% 80%, rgba(255, 100, 180, 0.40), transparent 62%), radial-gradient(ellipse 70% 60% at 92% 92%, rgba(120, 190, 255, 0.45), transparent 62%), linear-gradient(180deg, #f7eaff 0%, #fde2ea 100%)",
+
+ // Aurora Dream Vivid Bloom
+ "radial-gradient(ellipse 80% 60% at 70% 20%, rgba(175, 109, 255, 0.85), transparent 68%), radial-gradient(ellipse 70% 60% at 20% 80%, rgba(255, 100, 180, 0.75), transparent 68%), radial-gradient(ellipse 60% 50% at 60% 65%, rgba(255, 235, 170, 0.98), transparent 68%), radial-gradient(ellipse 65% 40% at 50% 60%, rgba(120, 190, 255, 0.3), transparent 68%), linear-gradient(180deg, #f7eaff 0%, #fde2ea 100%)",
+
+ // Soft Pastel Dream Gradient
+ "linear-gradient(135deg, #F8BBD9 0%, #FDD5B4 25%, #FFF2CC 50%, #E1F5FE 75%, #BBDEFB 100%)",
+
+ // Dreamy Sunset Gradient Background
+ "linear-gradient(180deg, rgba(245,245,220,1) 0%, rgba(255,223,186,0.8) 25%, rgba(255,182,193,0.6) 50%, rgba(147,112,219,0.7) 75%, rgba(72,61,139,0.9) 100%), radial-gradient(circle at 30% 20%, rgba(255,255,224,0.4) 0%, transparent 50%), radial-gradient(circle at 70% 80%, rgba(72,61,139,0.6) 0%, transparent 70%), radial-gradient(circle at 50% 60%, rgba(147,112,219,0.3) 0%, transparent 60%)",
+
+ // Cotton Candy Sky Gradient
+ "linear-gradient(45deg, #FFB3D9 0%, #FFD1DC 20%, #FFF0F5 40%, #E6F3FF 60%, #D1E7FF 80%, #C7E9F1 100%)",
+
+ // Rose Gold Whisper Gradient
+ "linear-gradient(270deg, #FFECB3 0%, #FFE0B2 20%, #FFCDD2 40%, #F8BBD9 60%, #E1BEE7 80%, #D1C4E9 100%)",
+
+ // Ember Glow Background
+ "radial-gradient(circle at 50% 100%, rgba(255, 69, 0, 0.6) 0%, transparent 60%), radial-gradient(circle at 50% 100%, rgba(255, 140, 0, 0.4) 0%, transparent 70%), radial-gradient(circle at 50% 100%, rgba(255, 215, 0, 0.3) 0%, transparent 80%)",
+
+ // Cosmic Aurora
+ "radial-gradient(ellipse at 20% 30%, rgba(56, 189, 248, 0.4) 0%, transparent 60%), radial-gradient(ellipse at 80% 70%, rgba(139, 92, 246, 0.3) 0%, transparent 70%), radial-gradient(ellipse at 60% 20%, rgba(236, 72, 153, 0.25) 0%, transparent 50%), radial-gradient(ellipse at 40% 80%, rgba(34, 197, 94, 0.2) 0%, transparent 65%), #000000",
+
+ // Deep Ocean Glow
+ "radial-gradient(70% 55% at 50% 50%, #2a5d77 0%, #184058 18%, #0f2a43 34%, #0a1b30 50%, #071226 66%, #040d1c 80%, #020814 92%, #01040d 97%, #000309 100%), radial-gradient(160% 130% at 10% 10%, rgba(0,0,0,0) 38%, #000309 76%, #000208 100%), radial-gradient(160% 130% at 90% 90%, rgba(0,0,0,0) 38%, #000309 76%, #000208 100%)",
+
+ // Crimson Core Glow
+ "linear-gradient(0deg, rgba(0,0,0,0.6), rgba(0,0,0,0.6)), radial-gradient(68% 58% at 50% 50%, #c81e3a 0%, #a51d35 16%, #7d1a2f 32%, #591828 46%, #3c1722 60%, #2a151d 72%, #1f1317 84%, #141013 94%, #0a0a0a 100%), radial-gradient(90% 75% at 50% 50%, rgba(228,42,66,0.06) 0%, rgba(228,42,66,0) 55%), radial-gradient(150% 120% at 8% 8%, rgba(0,0,0,0) 42%, #0b0a0a 82%, #070707 100%), radial-gradient(150% 120% at 92% 92%, rgba(0,0,0,0) 42%, #0b0a0a 82%, #070707 100%), radial-gradient(60% 50% at 50% 60%, rgba(240,60,80,0.06), rgba(0,0,0,0) 60%), #050505",
+
+ // Northern Aurora
+ "radial-gradient(ellipse 70% 55% at 50% 50%, rgba(255, 20, 147, 0.15), transparent 50%), radial-gradient(ellipse 160% 130% at 10% 10%, rgba(0, 255, 255, 0.12), transparent 60%), radial-gradient(ellipse 160% 130% at 90% 90%, rgba(138, 43, 226, 0.18), transparent 65%), radial-gradient(ellipse 110% 50% at 80% 30%, rgba(255, 215, 0, 0.08), transparent 40%), #000000",
+
+ // Royal Purple Background
+ "radial-gradient(circle at 50% 50%, rgba(147, 51, 234, 0.2) 0%, rgba(147, 51, 234, 0.12) 25%, rgba(147, 51, 234, 0.05) 35%, transparent 50%), #000000",
];
diff --git a/apps/web/src/data/colors/solid.ts b/apps/web/src/data/colors/solid.ts
index 9a12a587..8a946df4 100644
--- a/apps/web/src/data/colors/solid.ts
+++ b/apps/web/src/data/colors/solid.ts
@@ -1,244 +1,244 @@
export const colors = [
- "#ffffff",
- "#000000",
- "#ffe2e2",
- "#ffc9c9",
- "#ffa2a2",
- "#ff6467",
- "#fb2c36",
- "#e7000b",
- "#c10007",
- "#9f0712",
- "#82181a",
- "#460809",
- "#fff7ed",
- "#ffedd4",
- "#ffd6a7",
- "#ffb86a",
- "#ff8904",
- "#ff6900",
- "#f54900",
- "#ca3500",
- "#9f2d00",
- "#7e2a0c",
- "#441306",
- "#fffbeb",
- "#fef3c6",
- "#fee685",
- "#ffd230",
- "#ffb900",
- "#fe9a00",
- "#e17100",
- "#bb4d00",
- "#973c00",
- "#7b3306",
- "#461901",
- "#fefce8",
- "#fef9c2",
- "#fff085",
- "#ffdf20",
- "#fdc700",
- "#f0b100",
- "#d08700",
- "#a65f00",
- "#894b00",
- "#733e0a",
- "#432004",
- "#f7fee7",
- "#ecfcca",
- "#d8f999",
- "#bbf451",
- "#9ae600",
- "#7ccf00",
- "#5ea500",
- "#497d00",
- "#3c6300",
- "#35530e",
- "#192e03",
- "#f0fdf4",
- "#dcfce7",
- "#b9f8cf",
- "#7bf1a8",
- "#05df72",
- "#00c950",
- "#00a63e",
- "#008236",
- "#016630",
- "#0d542b",
- "#032e15",
- "#ecfdf5",
- "#d0fae5",
- "#a4f4cf",
- "#5ee9b5",
- "#00d492",
- "#00bc7d",
- "#009966",
- "#007a55",
- "#006045",
- "#004f3b",
- "#002c22",
- "#f0fdfa",
- "#cbfbf1",
- "#96f7e4",
- "#46ecd5",
- "#00d5be",
- "#00bba7",
- "#009689",
- "#00786f",
- "#005f5a",
- "#0b4f4a",
- "#022f2e",
- "#ecfeff",
- "#cefafe",
- "#a2f4fd",
- "#53eafd",
- "#00d3f2",
- "#00b8db",
- "#0092b8",
- "#007595",
- "#005f78",
- "#104e64",
- "#053345",
- "#f0f9ff",
- "#dff2fe",
- "#b8e6fe",
- "#74d4ff",
- "#00bcff",
- "#00a6f4",
- "#0084d1",
- "#0069a8",
- "#00598a",
- "#024a70",
- "#052f4a",
- "#eff6ff",
- "#dbeafe",
- "#bedbff",
- "#8ec5ff",
- "#51a2ff",
- "#2b7fff",
- "#155dfc",
- "#1447e6",
- "#193cb8",
- "#1c398e",
- "#162456",
- "#eef2ff",
- "#e0e7ff",
- "#c6d2ff",
- "#a3b3ff",
- "#7c86ff",
- "#615fff",
- "#4f39f6",
- "#432dd7",
- "#372aac",
- "#312c85",
- "#1e1a4d",
- "#f5f3ff",
- "#ede9fe",
- "#ddd6ff",
- "#c4b4ff",
- "#a684ff",
- "#8e51ff",
- "#7f22fe",
- "#7008e7",
- "#5d0ec0",
- "#4d179a",
- "#2f0d68",
- "#faf5ff",
- "#f3e8ff",
- "#e9d4ff",
- "#dab2ff",
- "#c27aff",
- "#ad46ff",
- "#9810fa",
- "#8200db",
- "#6e11b0",
- "#59168b",
- "#3c0366",
- "#fdf4ff",
- "#fae8ff",
- "#f6cfff",
- "#f4a8ff",
- "#ed6aff",
- "#e12afb",
- "#c800de",
- "#a800b7",
- "#8a0194",
- "#721378",
- "#4b004f",
- "#fdf2f8",
- "#fce7f3",
- "#fccee8",
- "#fda5d5",
- "#fb64b6",
- "#f6339a",
- "#e60076",
- "#c6005c",
- "#a3004c",
- "#861043",
- "#510424",
- "#fff1f2",
- "#ffe4e6",
- "#ffccd3",
- "#ffa1ad",
- "#ff637e",
- "#ff2056",
- "#ec003f",
- "#c70036",
- "#a50036",
- "#8b0836",
- "#4d0218",
- "#f8fafc",
- "#f1f5f9",
- "#e2e8f0",
- "#cad5e2",
- "#90a1b9",
- "#62748e",
- "#45556c",
- "#314158",
- "#1d293d",
- "#0f172b",
- "#020618",
- "#f9fafb",
- "#f3f4f6",
- "#e5e7eb",
- "#d1d5dc",
- "#99a1af",
- "#6a7282",
- "#4a5565",
- "#364153",
- "#1e2939",
- "#101828",
- "#030712",
- "#fafafa",
- "#f4f4f5",
- "#e4e4e7",
- "#d4d4d8",
- "#9f9fa9",
- "#71717b",
- "#52525c",
- "#3f3f46",
- "#27272a",
- "#18181b",
- "#09090b",
- "#f5f5f5",
- "#e5e5e5",
- "#d4d4d4",
- "#a1a1a1",
- "#737373",
- "#525252",
- "#404040",
- "#262626",
- "#171717",
- "#0a0a0a",
- "#fafaf9",
- "#f5f5f4",
- "#e7e5e4",
- "#d6d3d1",
- "#a6a09b",
- "#79716b",
- "#57534d",
- "#44403b",
- "#292524",
- "#1c1917",
- "#0c0a09",
+ "#ffffff",
+ "#000000",
+ "#ffe2e2",
+ "#ffc9c9",
+ "#ffa2a2",
+ "#ff6467",
+ "#fb2c36",
+ "#e7000b",
+ "#c10007",
+ "#9f0712",
+ "#82181a",
+ "#460809",
+ "#fff7ed",
+ "#ffedd4",
+ "#ffd6a7",
+ "#ffb86a",
+ "#ff8904",
+ "#ff6900",
+ "#f54900",
+ "#ca3500",
+ "#9f2d00",
+ "#7e2a0c",
+ "#441306",
+ "#fffbeb",
+ "#fef3c6",
+ "#fee685",
+ "#ffd230",
+ "#ffb900",
+ "#fe9a00",
+ "#e17100",
+ "#bb4d00",
+ "#973c00",
+ "#7b3306",
+ "#461901",
+ "#fefce8",
+ "#fef9c2",
+ "#fff085",
+ "#ffdf20",
+ "#fdc700",
+ "#f0b100",
+ "#d08700",
+ "#a65f00",
+ "#894b00",
+ "#733e0a",
+ "#432004",
+ "#f7fee7",
+ "#ecfcca",
+ "#d8f999",
+ "#bbf451",
+ "#9ae600",
+ "#7ccf00",
+ "#5ea500",
+ "#497d00",
+ "#3c6300",
+ "#35530e",
+ "#192e03",
+ "#f0fdf4",
+ "#dcfce7",
+ "#b9f8cf",
+ "#7bf1a8",
+ "#05df72",
+ "#00c950",
+ "#00a63e",
+ "#008236",
+ "#016630",
+ "#0d542b",
+ "#032e15",
+ "#ecfdf5",
+ "#d0fae5",
+ "#a4f4cf",
+ "#5ee9b5",
+ "#00d492",
+ "#00bc7d",
+ "#009966",
+ "#007a55",
+ "#006045",
+ "#004f3b",
+ "#002c22",
+ "#f0fdfa",
+ "#cbfbf1",
+ "#96f7e4",
+ "#46ecd5",
+ "#00d5be",
+ "#00bba7",
+ "#009689",
+ "#00786f",
+ "#005f5a",
+ "#0b4f4a",
+ "#022f2e",
+ "#ecfeff",
+ "#cefafe",
+ "#a2f4fd",
+ "#53eafd",
+ "#00d3f2",
+ "#00b8db",
+ "#0092b8",
+ "#007595",
+ "#005f78",
+ "#104e64",
+ "#053345",
+ "#f0f9ff",
+ "#dff2fe",
+ "#b8e6fe",
+ "#74d4ff",
+ "#00bcff",
+ "#00a6f4",
+ "#0084d1",
+ "#0069a8",
+ "#00598a",
+ "#024a70",
+ "#052f4a",
+ "#eff6ff",
+ "#dbeafe",
+ "#bedbff",
+ "#8ec5ff",
+ "#51a2ff",
+ "#2b7fff",
+ "#155dfc",
+ "#1447e6",
+ "#193cb8",
+ "#1c398e",
+ "#162456",
+ "#eef2ff",
+ "#e0e7ff",
+ "#c6d2ff",
+ "#a3b3ff",
+ "#7c86ff",
+ "#615fff",
+ "#4f39f6",
+ "#432dd7",
+ "#372aac",
+ "#312c85",
+ "#1e1a4d",
+ "#f5f3ff",
+ "#ede9fe",
+ "#ddd6ff",
+ "#c4b4ff",
+ "#a684ff",
+ "#8e51ff",
+ "#7f22fe",
+ "#7008e7",
+ "#5d0ec0",
+ "#4d179a",
+ "#2f0d68",
+ "#faf5ff",
+ "#f3e8ff",
+ "#e9d4ff",
+ "#dab2ff",
+ "#c27aff",
+ "#ad46ff",
+ "#9810fa",
+ "#8200db",
+ "#6e11b0",
+ "#59168b",
+ "#3c0366",
+ "#fdf4ff",
+ "#fae8ff",
+ "#f6cfff",
+ "#f4a8ff",
+ "#ed6aff",
+ "#e12afb",
+ "#c800de",
+ "#a800b7",
+ "#8a0194",
+ "#721378",
+ "#4b004f",
+ "#fdf2f8",
+ "#fce7f3",
+ "#fccee8",
+ "#fda5d5",
+ "#fb64b6",
+ "#f6339a",
+ "#e60076",
+ "#c6005c",
+ "#a3004c",
+ "#861043",
+ "#510424",
+ "#fff1f2",
+ "#ffe4e6",
+ "#ffccd3",
+ "#ffa1ad",
+ "#ff637e",
+ "#ff2056",
+ "#ec003f",
+ "#c70036",
+ "#a50036",
+ "#8b0836",
+ "#4d0218",
+ "#f8fafc",
+ "#f1f5f9",
+ "#e2e8f0",
+ "#cad5e2",
+ "#90a1b9",
+ "#62748e",
+ "#45556c",
+ "#314158",
+ "#1d293d",
+ "#0f172b",
+ "#020618",
+ "#f9fafb",
+ "#f3f4f6",
+ "#e5e7eb",
+ "#d1d5dc",
+ "#99a1af",
+ "#6a7282",
+ "#4a5565",
+ "#364153",
+ "#1e2939",
+ "#101828",
+ "#030712",
+ "#fafafa",
+ "#f4f4f5",
+ "#e4e4e7",
+ "#d4d4d8",
+ "#9f9fa9",
+ "#71717b",
+ "#52525c",
+ "#3f3f46",
+ "#27272a",
+ "#18181b",
+ "#09090b",
+ "#f5f5f5",
+ "#e5e5e5",
+ "#d4d4d4",
+ "#a1a1a1",
+ "#737373",
+ "#525252",
+ "#404040",
+ "#262626",
+ "#171717",
+ "#0a0a0a",
+ "#fafaf9",
+ "#f5f5f4",
+ "#e7e5e4",
+ "#d6d3d1",
+ "#a6a09b",
+ "#79716b",
+ "#57534d",
+ "#44403b",
+ "#292524",
+ "#1c1917",
+ "#0c0a09",
];
diff --git a/apps/web/src/data/colors/syntax-ui.tsx b/apps/web/src/data/colors/syntax-ui.tsx
index 50c7e7cc..e650b7d2 100644
--- a/apps/web/src/data/colors/syntax-ui.tsx
+++ b/apps/web/src/data/colors/syntax-ui.tsx
@@ -1,32 +1,32 @@
// These are the gradients from Syntax UI (https://syntaxui.com/effects/gradients)
export const syntaxUIGradients = [
- // Cyan to Blue gradients
- "linear-gradient(to right, #22d3ee, #0ea5e9, #0284c7)",
- "linear-gradient(to right, #bfdbfe, #a5f3fc)",
- "linear-gradient(to right, #22d3ee, #0ea5e9, #0284c7)",
+ // Cyan to Blue gradients
+ "linear-gradient(to right, #22d3ee, #0ea5e9, #0284c7)",
+ "linear-gradient(to right, #bfdbfe, #a5f3fc)",
+ "linear-gradient(to right, #22d3ee, #0ea5e9, #0284c7)",
- // Purple gradients
- "linear-gradient(to right, #e9d5ff, #d8b4fe, #c084fc)",
- "linear-gradient(to right, #c4b5fd, #a78bfa, #8b5cf6)",
+ // Purple gradients
+ "linear-gradient(to right, #e9d5ff, #d8b4fe, #c084fc)",
+ "linear-gradient(to right, #c4b5fd, #a78bfa, #8b5cf6)",
- // Blue gradients
- "linear-gradient(to right, #93c5fd, #60a5fa, #3b82f6)",
- "linear-gradient(to right, #93c5fd, #60a5fa, #3b82f6)",
+ // Blue gradients
+ "linear-gradient(to right, #93c5fd, #60a5fa, #3b82f6)",
+ "linear-gradient(to right, #93c5fd, #60a5fa, #3b82f6)",
- // Green gradients
- "linear-gradient(to right, #6ee7b7, #34d399, #10b981)",
- "linear-gradient(to right, #d1fae5, #a7f3d0, #6ee7b7)",
+ // Green gradients
+ "linear-gradient(to right, #6ee7b7, #34d399, #10b981)",
+ "linear-gradient(to right, #d1fae5, #a7f3d0, #6ee7b7)",
- // Red gradient
- "linear-gradient(to right, #fca5a5, #f87171, #ef4444)",
+ // Red gradient
+ "linear-gradient(to right, #fca5a5, #f87171, #ef4444)",
- // Yellow/Orange gradient
- "linear-gradient(to right, #fde68a, #fbbf24, #f59e0b)",
+ // Yellow/Orange gradient
+ "linear-gradient(to right, #fde68a, #fbbf24, #f59e0b)",
- // Pink gradient
- "linear-gradient(to right, #fbcfe8, #f9a8d4, #f472b6)",
+ // Pink gradient
+ "linear-gradient(to right, #fbcfe8, #f9a8d4, #f472b6)",
- // Neon radial gradient
- "radial-gradient(circle at bottom left, #ff00ff, #00ffff)",
+ // Neon radial gradient
+ "radial-gradient(circle at bottom left, #ff00ff, #00ffff)",
];
diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts
deleted file mode 100644
index 71614300..00000000
--- a/apps/web/src/env.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { vercel } from "@t3-oss/env-core/presets-zod";
-import { createEnv } from "@t3-oss/env-nextjs";
-import { z } from "zod";
-import { keys as auth } from "@opencut/auth/keys";
-import { keys as db } from "@opencut/db/keys";
-
-export const env = createEnv({
- extends: [vercel(), auth(), db()],
- server: {
- ANALYZE: z.string().optional(),
- // Added by Vercel
- NEXT_RUNTIME: z.enum(["nodejs", "edge"]).optional(),
- NODE_ENV: z
- .enum(["development", "production", "test"])
- .default("development"),
- UPSTASH_REDIS_REST_URL: z.string().url(),
- UPSTASH_REDIS_REST_TOKEN: z.string(),
- FREESOUND_CLIENT_ID: z.string(),
- FREESOUND_API_KEY: z.string(),
- // R2 / Cloudflare
- CLOUDFLARE_ACCOUNT_ID: z.string(),
- R2_ACCESS_KEY_ID: z.string(),
- R2_SECRET_ACCESS_KEY: z.string(),
- R2_BUCKET_NAME: z.string(),
- // Modal transcription
- MODAL_TRANSCRIPTION_URL: z.string(),
- },
- client: {},
- runtimeEnv: {
- ANALYZE: process.env.ANALYZE,
- NEXT_RUNTIME: process.env.NEXT_RUNTIME,
- NODE_ENV: process.env.NODE_ENV,
- UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL,
- UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN,
- FREESOUND_CLIENT_ID: process.env.FREESOUND_CLIENT_ID,
- FREESOUND_API_KEY: process.env.FREESOUND_API_KEY,
- // R2 / Cloudflare
- CLOUDFLARE_ACCOUNT_ID: process.env.CLOUDFLARE_ACCOUNT_ID,
- R2_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID,
- R2_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY,
- R2_BUCKET_NAME: process.env.R2_BUCKET_NAME,
- // Modal transcription
- MODAL_TRANSCRIPTION_URL: process.env.MODAL_TRANSCRIPTION_URL,
- },
-});
diff --git a/apps/web/src/hooks/actions/use-action-handler.ts b/apps/web/src/hooks/actions/use-action-handler.ts
new file mode 100644
index 00000000..3f6dbd21
--- /dev/null
+++ b/apps/web/src/hooks/actions/use-action-handler.ts
@@ -0,0 +1,52 @@
+import { useCallback, useEffect, useRef } from "react";
+import type {
+ TAction,
+ TActionFunc,
+ TActionHandlerOptions,
+ TArgOfAction,
+ TInvocationTrigger,
+} from "@/lib/actions";
+import { bindAction, unbindAction } from "@/lib/actions";
+
+export function useActionHandler<A extends TAction>(
+ action: A,
+ handler: TActionFunc<A>,
+ isActive: TActionHandlerOptions,
+) {
+ const handlerRef = useRef<TActionFunc<A>>(handler);
+ const isBoundRef = useRef(false);
+
+ useEffect(() => {
+ handlerRef.current = handler;
+ }, [handler]);
+
+ const stableHandler = useCallback(
+ (...parameters: [TArgOfAction<A>, TInvocationTrigger?]) => {
+ (
+ handlerRef.current as (
+ ...handlerParameters: [TArgOfAction<A>, TInvocationTrigger?]
+ ) => void
+ )(...parameters);
+ },
+ [],
+ ) as TActionFunc<A>;
+
+ useEffect(() => {
+ const shouldBind =
+ isActive === undefined ||
+ (typeof isActive === "boolean" ? isActive : isActive.current);
+
+ if (shouldBind && !isBoundRef.current) {
+ bindAction(action, stableHandler);
+ isBoundRef.current = true;
+ } else if (!shouldBind && isBoundRef.current) {
+ unbindAction(action, stableHandler);
+ isBoundRef.current = false;
+ }
+
+ return () => {
+ unbindAction(action, stableHandler);
+ isBoundRef.current = false;
+ };
+ }, [action, stableHandler, isActive]);
+}
diff --git a/apps/web/src/hooks/actions/use-editor-actions.ts b/apps/web/src/hooks/actions/use-editor-actions.ts
new file mode 100644
index 00000000..d1ff6c92
--- /dev/null
+++ b/apps/web/src/hooks/actions/use-editor-actions.ts
@@ -0,0 +1,312 @@
+"use client";
+
+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 { getElementsAtTime } from "@/lib/timeline";
+
+export function useEditorActions() {
+ const editor = useEditor();
+ const activeProject = editor.project.getActive();
+ const { selectedElements, setElementSelection } = useElementSelection();
+ const { clipboard, setClipboard, toggleSnapping } = useTimelineStore();
+
+ useActionHandler(
+ "toggle-play",
+ () => {
+ editor.playback.toggle();
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "stop-playback",
+ () => {
+ if (editor.playback.getIsPlaying()) {
+ editor.playback.toggle();
+ }
+ editor.playback.seek({ time: 0 });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "seek-forward",
+ (args) => {
+ const seconds = args?.seconds ?? 1;
+ editor.playback.seek({
+ time: Math.min(
+ editor.timeline.getTotalDuration(),
+ editor.playback.getCurrentTime() + seconds,
+ ),
+ });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "seek-backward",
+ (args) => {
+ const seconds = args?.seconds ?? 1;
+ editor.playback.seek({
+ time: Math.max(0, editor.playback.getCurrentTime() - seconds),
+ });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "frame-step-forward",
+ () => {
+ const fps = activeProject.settings.fps;
+ editor.playback.seek({
+ time: Math.min(
+ editor.timeline.getTotalDuration(),
+ editor.playback.getCurrentTime() + 1 / fps,
+ ),
+ });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "frame-step-backward",
+ () => {
+ const fps = activeProject.settings.fps;
+ editor.playback.seek({
+ time: Math.max(0, editor.playback.getCurrentTime() - 1 / fps),
+ });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "jump-forward",
+ (args) => {
+ const seconds = args?.seconds ?? 5;
+ editor.playback.seek({
+ time: Math.min(
+ editor.timeline.getTotalDuration(),
+ editor.playback.getCurrentTime() + seconds,
+ ),
+ });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "jump-backward",
+ (args) => {
+ const seconds = args?.seconds ?? 5;
+ editor.playback.seek({
+ time: Math.max(0, editor.playback.getCurrentTime() - seconds),
+ });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "goto-start",
+ () => {
+ editor.playback.seek({ time: 0 });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "goto-end",
+ () => {
+ editor.playback.seek({ time: editor.timeline.getTotalDuration() });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "split",
+ () => {
+ const currentTime = editor.playback.getCurrentTime();
+ const elementsToSplit =
+ selectedElements.length > 0
+ ? selectedElements
+ : getElementsAtTime({
+ tracks: editor.timeline.getTracks(),
+ time: currentTime,
+ });
+
+ if (elementsToSplit.length === 0) return;
+
+ editor.timeline.splitElements({
+ elements: elementsToSplit,
+ splitTime: currentTime,
+ });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "split-left",
+ () => {
+ const currentTime = editor.playback.getCurrentTime();
+ const elementsToSplit =
+ selectedElements.length > 0
+ ? selectedElements
+ : getElementsAtTime({
+ tracks: editor.timeline.getTracks(),
+ time: currentTime,
+ });
+
+ if (elementsToSplit.length === 0) return;
+
+ editor.timeline.splitElements({
+ elements: elementsToSplit,
+ splitTime: currentTime,
+ retainSide: "right",
+ });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "split-right",
+ () => {
+ const currentTime = editor.playback.getCurrentTime();
+ const elementsToSplit =
+ selectedElements.length > 0
+ ? selectedElements
+ : getElementsAtTime({
+ tracks: editor.timeline.getTracks(),
+ time: currentTime,
+ });
+
+ if (elementsToSplit.length === 0) return;
+
+ editor.timeline.splitElements({
+ elements: elementsToSplit,
+ splitTime: currentTime,
+ retainSide: "left",
+ });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "delete-selected",
+ () => {
+ if (selectedElements.length === 0) {
+ return;
+ }
+ editor.timeline.deleteElements({
+ elements: selectedElements,
+ });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "select-all",
+ () => {
+ const allElements = editor.timeline.getTracks().flatMap((track) =>
+ track.elements.map((element) => ({
+ trackId: track.id,
+ elementId: element.id,
+ })),
+ );
+ setElementSelection({ elements: allElements });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "duplicate-selected",
+ () => {
+ editor.timeline.duplicateElements({
+ elements: selectedElements,
+ });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "toggle-elements-muted-selected",
+ () => {
+ editor.timeline.toggleElementsMuted({ elements: selectedElements });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "toggle-elements-visibility-selected",
+ () => {
+ editor.timeline.toggleElementsVisibility({ elements: selectedElements });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "toggle-bookmark",
+ () => {
+ editor.scenes.toggleBookmark({ time: editor.playback.getCurrentTime() });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "copy-selected",
+ () => {
+ if (selectedElements.length === 0) return;
+
+ const results = editor.timeline.getElementsWithTracks({
+ elements: selectedElements,
+ });
+ const items = results.map(({ track, element }) => {
+ const { ...elementWithoutId } = element;
+ return {
+ trackId: track.id,
+ trackType: track.type,
+ element: elementWithoutId,
+ };
+ });
+
+ setClipboard({ items });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "paste-copied",
+ () => {
+ if (!clipboard?.items.length) return;
+
+ editor.timeline.pasteAtTime({
+ time: editor.playback.getCurrentTime(),
+ clipboardItems: clipboard.items,
+ });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "toggle-snapping",
+ () => {
+ toggleSnapping();
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "undo",
+ () => {
+ editor.command.undo();
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "redo",
+ () => {
+ editor.command.redo();
+ },
+ undefined,
+ );
+}
diff --git a/apps/web/src/hooks/auth/useLogin.ts b/apps/web/src/hooks/auth/useLogin.ts
deleted file mode 100644
index b2c5ef98..00000000
--- a/apps/web/src/hooks/auth/useLogin.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { useCallback, useState } from "react";
-import { useRouter } from "next/navigation";
-import { signIn } from "@opencut/auth/client";
-
-export function useLogin() {
- const router = useRouter();
- const [email, setEmail] = useState("");
- const [password, setPassword] = useState("");
- const [error, setError] = useState<string | null>(null);
- const [isEmailLoading, setIsEmailLoading] = useState(false);
- const [isGoogleLoading, setIsGoogleLoading] = useState(false);
-
- const handleLogin = useCallback(async () => {
- setError(null);
- setIsEmailLoading(true);
-
- const { error } = await signIn.email({
- email,
- password,
- });
-
- if (error) {
- setError(error.message || "An unexpected error occurred.");
- setIsEmailLoading(false);
- return;
- }
-
- router.push("/projects");
- }, [router, email, password]);
-
- const handleGoogleLogin = async () => {
- setError(null);
- setIsGoogleLoading(true);
-
- try {
- await signIn.social({
- provider: "google",
- callbackURL: "/projects",
- });
- } catch (error) {
- setError("Failed to sign in with Google. Please try again.");
- setIsGoogleLoading(false);
- }
- };
-
- const isAnyLoading = isEmailLoading || isGoogleLoading;
-
- return {
- email,
- setEmail,
- password,
- setPassword,
- error,
- isEmailLoading,
- isGoogleLoading,
- isAnyLoading,
- handleLogin,
- handleGoogleLogin,
- };
-}
diff --git a/apps/web/src/hooks/auth/useSignUp.ts b/apps/web/src/hooks/auth/useSignUp.ts
deleted file mode 100644
index b6039f85..00000000
--- a/apps/web/src/hooks/auth/useSignUp.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { useState, useCallback } from "react";
-import { useRouter } from "next/navigation";
-import { signUp, signIn } from "@opencut/auth/client";
-
-export function useSignUp() {
- const router = useRouter();
- const [name, setName] = useState("");
- const [email, setEmail] = useState("");
- const [password, setPassword] = useState("");
- const [error, setError] = useState<string | null>(null);
- const [isEmailLoading, setIsEmailLoading] = useState(false);
- const [isGoogleLoading, setIsGoogleLoading] = useState(false);
-
- const handleSignUp = useCallback(async () => {
- setError(null);
- setIsEmailLoading(true);
-
- const { error } = await signUp.email({
- name,
- email,
- password,
- });
-
- if (error) {
- setError(error.message || "An unexpected error occurred.");
- setIsEmailLoading(false);
- return;
- }
-
- router.push("/login");
- }, [name, email, password, router]);
-
- const handleGoogleSignUp = useCallback(async () => {
- setError(null);
- setIsGoogleLoading(true);
-
- try {
- await signIn.social({
- provider: "google",
- });
-
- router.push("/editor");
- } catch (error) {
- setError("Failed to sign up with Google. Please try again.");
- setIsGoogleLoading(false);
- }
- }, [router]);
-
- const isAnyLoading = isEmailLoading || isGoogleLoading;
-
- return {
- name,
- setName,
- email,
- setEmail,
- password,
- setPassword,
- error,
- isEmailLoading,
- isGoogleLoading,
- isAnyLoading,
- handleSignUp,
- handleGoogleSignUp,
- };
-}
diff --git a/apps/web/src/hooks/storage/use-local-storage.ts b/apps/web/src/hooks/storage/use-local-storage.ts
new file mode 100644
index 00000000..d0188ab3
--- /dev/null
+++ b/apps/web/src/hooks/storage/use-local-storage.ts
@@ -0,0 +1,58 @@
+import { useState, useEffect, useCallback, useRef } from "react";
+
+export function useLocalStorage<T>({
+ key,
+ defaultValue,
+}: {
+ key: string;
+ defaultValue: T;
+}): [
+ T,
+ ({ value }: { value: T | ((previousValue: T) => T) }) => void,
+ boolean,
+] {
+ const [value, setValue] = useState<T>(defaultValue);
+ const [isReady, setIsReady] = useState(false);
+ const valueRef = useRef(defaultValue);
+
+ // avoid hydration mismatch by reading after mount
+ useEffect(() => {
+ try {
+ const storedValue = localStorage.getItem(key);
+ if (storedValue !== null) {
+ const parsedValue = JSON.parse(storedValue) as T;
+ valueRef.current = parsedValue;
+ setValue(parsedValue);
+ }
+ } catch {
+ // localstorage might be unavailable
+ }
+ setIsReady(true);
+ }, [key]);
+
+ // sync to localstorage after hydration
+ useEffect(() => {
+ if (!isReady) return;
+
+ try {
+ localStorage.setItem(key, JSON.stringify(value));
+ } catch {
+ // localstorage might be full or disabled
+ }
+ }, [key, value, isReady]);
+
+ const setValueWithCallback = useCallback(
+ ({ value: nextValue }: { value: T | ((previousValue: T) => T) }) => {
+ const resolvedValue =
+ typeof nextValue === "function"
+ ? (nextValue as (previousValue: T) => T)(valueRef.current)
+ : nextValue;
+
+ valueRef.current = resolvedValue;
+ setValue(resolvedValue);
+ },
+ [],
+ );
+
+ return [value, setValueWithCallback, isReady];
+}
diff --git a/apps/web/src/hooks/timeline/element/use-element-interaction.ts b/apps/web/src/hooks/timeline/element/use-element-interaction.ts
new file mode 100644
index 00000000..c92d6722
--- /dev/null
+++ b/apps/web/src/hooks/timeline/element/use-element-interaction.ts
@@ -0,0 +1,619 @@
+import {
+ useState,
+ useCallback,
+ useEffect,
+ useRef,
+ type MouseEvent as ReactMouseEvent,
+ type RefObject,
+} from "react";
+import { useEditor } from "@/hooks/use-editor";
+import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import { snapTimeToFrame } from "@/lib/time";
+import { computeDropTarget } from "@/lib/timeline/drop-utils";
+import { generateUUID } from "@/utils/id";
+import { useTimelineSnapping } from "@/hooks/timeline/use-timeline-snapping";
+import type {
+ DropTarget,
+ ElementDragState,
+ TimelineElement,
+ TimelineTrack,
+} from "@/types/timeline";
+import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
+
+const DRAG_THRESHOLD_PX = 5;
+
+interface UseElementInteractionProps {
+ zoomLevel: number;
+ timelineRef: RefObject<HTMLDivElement | null>;
+ tracksContainerRef: RefObject<HTMLDivElement | null>;
+ tracksScrollRef: RefObject<HTMLDivElement | null>;
+ headerRef?: RefObject<HTMLElement | null>;
+ snappingEnabled: boolean;
+ onSnapPointChange?: (snapPoint: SnapPoint | null) => void;
+}
+
+const initialDragState: ElementDragState = {
+ isDragging: false,
+ elementId: null,
+ trackId: null,
+ startMouseX: 0,
+ startMouseY: 0,
+ startElementTime: 0,
+ clickOffsetTime: 0,
+ currentTime: 0,
+ currentMouseY: 0,
+};
+
+interface PendingDragState {
+ elementId: string;
+ trackId: string;
+ startMouseX: number;
+ startMouseY: number;
+ startElementTime: number;
+ clickOffsetTime: number;
+}
+
+function getMouseTimeFromClientX({
+ clientX,
+ containerRect,
+ zoomLevel,
+ scrollLeft,
+}: {
+ clientX: number;
+ containerRect: DOMRect;
+ zoomLevel: number;
+ scrollLeft: number;
+}): number {
+ const mouseX = clientX - containerRect.left + scrollLeft;
+ return Math.max(
+ 0,
+ mouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel),
+ );
+}
+
+function getClickOffsetTime({
+ clientX,
+ elementRect,
+ zoomLevel,
+}: {
+ clientX: number;
+ elementRect: DOMRect;
+ zoomLevel: number;
+}): number {
+ const clickOffsetX = clientX - elementRect.left;
+ return clickOffsetX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel);
+}
+
+function getVerticalDragDirection({
+ startMouseY,
+ currentMouseY,
+}: {
+ startMouseY: number;
+ currentMouseY: number;
+}): "up" | "down" | null {
+ if (currentMouseY < startMouseY) return "up";
+ if (currentMouseY > startMouseY) return "down";
+ return null;
+}
+
+function getDragDropTarget({
+ clientX,
+ clientY,
+ elementId,
+ trackId,
+ tracks,
+ tracksContainerRef,
+ tracksScrollRef,
+ headerRef,
+ zoomLevel,
+ snappedTime,
+ verticalDragDirection,
+}: {
+ clientX: number;
+ clientY: number;
+ elementId: string;
+ trackId: string;
+ tracks: TimelineTrack[];
+ tracksContainerRef: RefObject<HTMLDivElement | null>;
+ tracksScrollRef: RefObject<HTMLDivElement | null>;
+ headerRef?: RefObject<HTMLElement | null>;
+ zoomLevel: number;
+ snappedTime: number;
+ verticalDragDirection?: "up" | "down" | null;
+}): DropTarget | null {
+ const containerRect = tracksContainerRef.current?.getBoundingClientRect();
+ const scrollContainer = tracksScrollRef.current;
+ if (!containerRect || !scrollContainer) return null;
+
+ const sourceTrack = tracks.find(({ id }) => id === trackId);
+ const movingElement = sourceTrack?.elements.find(
+ ({ id }) => id === elementId,
+ );
+ if (!movingElement) return null;
+
+ const elementDuration = movingElement.duration;
+ const scrollLeft = scrollContainer.scrollLeft;
+ const scrollTop = scrollContainer.scrollTop;
+ const scrollContainerRect = scrollContainer.getBoundingClientRect();
+ const headerHeight = headerRef?.current?.getBoundingClientRect().height ?? 0;
+ const mouseX = clientX - scrollContainerRect.left + scrollLeft;
+ const mouseY = clientY - scrollContainerRect.top + scrollTop - headerHeight;
+
+ return computeDropTarget({
+ elementType: movingElement.type,
+ mouseX,
+ mouseY,
+ tracks,
+ playheadTime: snappedTime,
+ isExternalDrop: false,
+ elementDuration,
+ pixelsPerSecond: TIMELINE_CONSTANTS.PIXELS_PER_SECOND,
+ zoomLevel,
+ startTimeOverride: snappedTime,
+ excludeElementId: movingElement.id,
+ verticalDragDirection,
+ });
+}
+
+interface StartDragParams
+ extends Omit<
+ ElementDragState,
+ "isDragging" | "currentTime" | "currentMouseY"
+ > {
+ initialCurrentTime: number;
+ initialCurrentMouseY: number;
+}
+
+export function useElementInteraction({
+ zoomLevel,
+ timelineRef,
+ tracksContainerRef,
+ tracksScrollRef,
+ headerRef,
+ snappingEnabled,
+ onSnapPointChange,
+}: UseElementInteractionProps) {
+ const editor = useEditor();
+ const tracks = editor.timeline.getTracks();
+ const { snapElementEdge } = useTimelineSnapping();
+ const {
+ isElementSelected,
+ selectElement,
+ handleElementClick: handleSelectionClick,
+ } = useElementSelection();
+
+ const [dragState, setDragState] =
+ useState<ElementDragState>(initialDragState);
+ const [dragDropTarget, setDragDropTarget] = useState<DropTarget | null>(null);
+ const [isPendingDrag, setIsPendingDrag] = useState(false);
+ const pendingDragRef = useRef<PendingDragState | null>(null);
+ const lastMouseXRef = useRef(0);
+ const mouseDownLocationRef = useRef<{ x: number; y: number } | null>(null);
+
+ const startDrag = useCallback(
+ ({
+ elementId,
+ trackId,
+ startMouseX,
+ startMouseY,
+ startElementTime,
+ clickOffsetTime,
+ initialCurrentTime,
+ initialCurrentMouseY,
+ }: StartDragParams) => {
+ setDragState({
+ isDragging: true,
+ elementId,
+ trackId,
+ startMouseX,
+ startMouseY,
+ startElementTime,
+ clickOffsetTime,
+ currentTime: initialCurrentTime,
+ currentMouseY: initialCurrentMouseY,
+ });
+ },
+ [],
+ );
+
+ const endDrag = useCallback(() => {
+ setDragState(initialDragState);
+ setDragDropTarget(null);
+ }, []);
+
+ const getDragSnapResult = useCallback(
+ ({
+ frameSnappedTime,
+ movingElement,
+ }: {
+ frameSnappedTime: number;
+ movingElement: TimelineElement | null | undefined;
+ }) => {
+ if (!snappingEnabled || !movingElement) {
+ return { snappedTime: frameSnappedTime, snapPoint: null };
+ }
+
+ const elementDuration = movingElement.duration;
+ const playheadTime = editor.playback.getCurrentTime();
+
+ const startSnap = snapElementEdge({
+ targetTime: frameSnappedTime,
+ elementDuration,
+ tracks,
+ playheadTime,
+ zoomLevel,
+ excludeElementId: movingElement.id,
+ snapToStart: true,
+ });
+
+ const endSnap = snapElementEdge({
+ targetTime: frameSnappedTime,
+ elementDuration,
+ tracks,
+ playheadTime,
+ zoomLevel,
+ excludeElementId: movingElement.id,
+ snapToStart: false,
+ });
+
+ const snapResult =
+ startSnap.snapDistance <= endSnap.snapDistance ? startSnap : endSnap;
+ if (!snapResult.snapPoint) {
+ return { snappedTime: frameSnappedTime, snapPoint: null };
+ }
+
+ return {
+ snappedTime: snapResult.snappedTime,
+ snapPoint: snapResult.snapPoint,
+ };
+ },
+ [snappingEnabled, editor.playback, snapElementEdge, tracks, zoomLevel],
+ );
+
+ useEffect(() => {
+ if (!dragState.isDragging && !isPendingDrag) return;
+
+ const handleMouseMove = ({ clientX, clientY }: MouseEvent) => {
+ let startedDragThisEvent = false;
+ const timeline = timelineRef.current;
+ const scrollContainer = tracksScrollRef.current;
+ if (!timeline || !scrollContainer) return;
+ lastMouseXRef.current = clientX;
+
+ if (isPendingDrag && pendingDragRef.current) {
+ const deltaX = Math.abs(clientX - pendingDragRef.current.startMouseX);
+ const deltaY = Math.abs(clientY - pendingDragRef.current.startMouseY);
+ if (deltaX > DRAG_THRESHOLD_PX || deltaY > DRAG_THRESHOLD_PX) {
+ const activeProject = editor.project.getActive();
+ if (!activeProject) return;
+ const scrollLeft = scrollContainer.scrollLeft;
+ const mouseTime = getMouseTimeFromClientX({
+ clientX,
+ containerRect: scrollContainer.getBoundingClientRect(),
+ zoomLevel,
+ scrollLeft,
+ });
+ const adjustedTime = Math.max(
+ 0,
+ mouseTime - pendingDragRef.current.clickOffsetTime,
+ );
+ const snappedTime = snapTimeToFrame({
+ time: adjustedTime,
+ fps: activeProject.settings.fps,
+ });
+ startDrag({
+ ...pendingDragRef.current,
+ initialCurrentTime: snappedTime,
+ initialCurrentMouseY: clientY,
+ });
+ startedDragThisEvent = true;
+ pendingDragRef.current = null;
+ setIsPendingDrag(false);
+ } else {
+ return;
+ }
+ }
+
+ if (startedDragThisEvent) {
+ return;
+ }
+
+ if (dragState.elementId && dragState.trackId) {
+ const alreadySelected = isElementSelected({
+ trackId: dragState.trackId,
+ elementId: dragState.elementId,
+ });
+ if (!alreadySelected) {
+ selectElement({
+ trackId: dragState.trackId,
+ elementId: dragState.elementId,
+ });
+ }
+ }
+
+ const activeProject = editor.project.getActive();
+ if (!activeProject) return;
+
+ const scrollLeft = scrollContainer.scrollLeft;
+ const mouseTime = getMouseTimeFromClientX({
+ clientX,
+ containerRect: scrollContainer.getBoundingClientRect(),
+ zoomLevel,
+ scrollLeft,
+ });
+ const adjustedTime = Math.max(0, mouseTime - dragState.clickOffsetTime);
+ const fps = activeProject.settings.fps;
+ const frameSnappedTime = snapTimeToFrame({ time: adjustedTime, fps });
+
+ const sourceTrack = tracks.find(({ id }) => id === dragState.trackId);
+ const movingElement = sourceTrack?.elements.find(
+ ({ id }) => id === dragState.elementId,
+ );
+ const { snappedTime, snapPoint } = getDragSnapResult({
+ frameSnappedTime,
+ movingElement,
+ });
+ setDragState((previousDragState) => ({
+ ...previousDragState,
+ currentTime: snappedTime,
+ currentMouseY: clientY,
+ }));
+ onSnapPointChange?.(snapPoint);
+
+ if (dragState.elementId && dragState.trackId) {
+ const verticalDragDirection = getVerticalDragDirection({
+ startMouseY: dragState.startMouseY,
+ currentMouseY: clientY,
+ });
+ const dropTarget = getDragDropTarget({
+ clientX,
+ clientY,
+ elementId: dragState.elementId,
+ trackId: dragState.trackId,
+ tracks,
+ tracksContainerRef,
+ tracksScrollRef,
+ headerRef,
+ zoomLevel,
+ snappedTime,
+ verticalDragDirection,
+ });
+ setDragDropTarget(dropTarget?.isNewTrack ? dropTarget : null);
+ }
+ };
+
+ document.addEventListener("mousemove", handleMouseMove);
+ return () => document.removeEventListener("mousemove", handleMouseMove);
+ }, [
+ dragState.isDragging,
+ dragState.clickOffsetTime,
+ dragState.elementId,
+ dragState.startMouseY,
+ dragState.trackId,
+ zoomLevel,
+ isElementSelected,
+ selectElement,
+ editor.project,
+ timelineRef,
+ tracksScrollRef,
+ tracksContainerRef,
+ headerRef,
+ tracks,
+ isPendingDrag,
+ startDrag,
+ getDragSnapResult,
+ onSnapPointChange,
+ ]);
+
+ useEffect(() => {
+ if (!dragState.isDragging) return;
+
+ const handleMouseUp = ({ clientX, clientY }: MouseEvent) => {
+ if (!dragState.elementId || !dragState.trackId) return;
+
+ if (mouseDownLocationRef.current) {
+ const deltaX = Math.abs(clientX - mouseDownLocationRef.current.x);
+ const deltaY = Math.abs(clientY - mouseDownLocationRef.current.y);
+ if (deltaX <= DRAG_THRESHOLD_PX && deltaY <= DRAG_THRESHOLD_PX) {
+ mouseDownLocationRef.current = null;
+ endDrag();
+ onSnapPointChange?.(null);
+ return;
+ }
+ }
+
+ const dropTarget = getDragDropTarget({
+ clientX,
+ clientY,
+ elementId: dragState.elementId,
+ trackId: dragState.trackId,
+ tracks,
+ tracksContainerRef,
+ tracksScrollRef,
+ headerRef,
+ zoomLevel,
+ snappedTime: dragState.currentTime,
+ verticalDragDirection: getVerticalDragDirection({
+ startMouseY: dragState.startMouseY,
+ currentMouseY: clientY,
+ }),
+ });
+ if (!dropTarget) {
+ endDrag();
+ onSnapPointChange?.(null);
+ return;
+ }
+ const snappedTime = dragState.currentTime;
+
+ const sourceTrack = tracks.find(({ id }) => id === dragState.trackId);
+ if (!sourceTrack) {
+ endDrag();
+ onSnapPointChange?.(null);
+ return;
+ }
+
+ if (dropTarget.isNewTrack) {
+ const newTrackId = generateUUID();
+
+ editor.timeline.moveElement({
+ sourceTrackId: dragState.trackId,
+ targetTrackId: newTrackId,
+ elementId: dragState.elementId,
+ newStartTime: snappedTime,
+ createTrack: { type: sourceTrack.type, index: dropTarget.trackIndex },
+ });
+ } else {
+ const targetTrack = tracks[dropTarget.trackIndex];
+ if (targetTrack) {
+ editor.timeline.moveElement({
+ sourceTrackId: dragState.trackId,
+ targetTrackId: targetTrack.id,
+ elementId: dragState.elementId,
+ newStartTime: snappedTime,
+ });
+ }
+ }
+
+ endDrag();
+ onSnapPointChange?.(null);
+ };
+
+ document.addEventListener("mouseup", handleMouseUp);
+ return () => document.removeEventListener("mouseup", handleMouseUp);
+ }, [
+ dragState.isDragging,
+ dragState.elementId,
+ dragState.startMouseY,
+ dragState.trackId,
+ dragState.currentTime,
+ zoomLevel,
+ tracks,
+ endDrag,
+ onSnapPointChange,
+ editor.timeline,
+ tracksContainerRef,
+ tracksScrollRef,
+ headerRef,
+ ]);
+
+ useEffect(() => {
+ if (!isPendingDrag) return;
+
+ const handleMouseUp = () => {
+ pendingDragRef.current = null;
+ setIsPendingDrag(false);
+ onSnapPointChange?.(null);
+ };
+
+ document.addEventListener("mouseup", handleMouseUp);
+ return () => document.removeEventListener("mouseup", handleMouseUp);
+ }, [isPendingDrag, onSnapPointChange]);
+
+ const handleElementMouseDown = useCallback(
+ ({
+ event,
+ element,
+ track,
+ }: {
+ event: ReactMouseEvent;
+ element: TimelineElement;
+ track: TimelineTrack;
+ }) => {
+ const isRightClick = event.button === 2;
+
+ // right-click: don't stop propagation so ContextMenu can open
+ if (isRightClick) {
+ const alreadySelected = isElementSelected({
+ trackId: track.id,
+ elementId: element.id,
+ });
+ if (!alreadySelected) {
+ handleSelectionClick({
+ trackId: track.id,
+ elementId: element.id,
+ isMultiKey: false,
+ });
+ }
+ return;
+ }
+
+ // left-click: stop propagation for drag operations
+ event.stopPropagation();
+ mouseDownLocationRef.current = { x: event.clientX, y: event.clientY };
+
+ const isMultiSelect = event.metaKey || event.ctrlKey || event.shiftKey;
+
+ // multi-select: toggle selection
+ if (isMultiSelect) {
+ handleSelectionClick({
+ trackId: track.id,
+ elementId: element.id,
+ isMultiKey: true,
+ });
+ }
+
+ // start drag
+ const clickOffsetTime = getClickOffsetTime({
+ clientX: event.clientX,
+ elementRect: event.currentTarget.getBoundingClientRect(),
+ zoomLevel,
+ });
+ pendingDragRef.current = {
+ elementId: element.id,
+ trackId: track.id,
+ startMouseX: event.clientX,
+ startMouseY: event.clientY,
+ startElementTime: element.startTime,
+ clickOffsetTime,
+ };
+ setIsPendingDrag(true);
+ },
+ [zoomLevel, isElementSelected, handleSelectionClick],
+ );
+
+ const handleElementClick = useCallback(
+ ({
+ event,
+ element,
+ track,
+ }: {
+ event: ReactMouseEvent;
+ element: TimelineElement;
+ track: TimelineTrack;
+ }) => {
+ event.stopPropagation();
+
+ // was it a drag or a click?
+ if (mouseDownLocationRef.current) {
+ const deltaX = Math.abs(event.clientX - mouseDownLocationRef.current.x);
+ const deltaY = Math.abs(event.clientY - mouseDownLocationRef.current.y);
+ if (deltaX > DRAG_THRESHOLD_PX || deltaY > DRAG_THRESHOLD_PX) {
+ mouseDownLocationRef.current = null;
+ return;
+ }
+ }
+
+ // modifier keys already handled in mousedown
+ if (event.metaKey || event.ctrlKey || event.shiftKey) return;
+
+ // single click: select if not selected
+ const alreadySelected = isElementSelected({
+ trackId: track.id,
+ elementId: element.id,
+ });
+ if (!alreadySelected) {
+ selectElement({ trackId: track.id, elementId: element.id });
+ }
+ },
+ [isElementSelected, selectElement],
+ );
+
+ return {
+ dragState,
+ dragDropTarget,
+ handleElementMouseDown,
+ handleElementClick,
+ lastMouseXRef,
+ };
+}
diff --git a/apps/web/src/hooks/timeline/element/use-element-resize.ts b/apps/web/src/hooks/timeline/element/use-element-resize.ts
new file mode 100644
index 00000000..803d9788
--- /dev/null
+++ b/apps/web/src/hooks/timeline/element/use-element-resize.ts
@@ -0,0 +1,331 @@
+import { useState, useEffect, useRef, useCallback } from "react";
+import type { TimelineElement, TimelineTrack } from "@/types/timeline";
+import { snapTimeToFrame } from "@/lib/time";
+import { EditorCore } from "@/core";
+import {
+ useTimelineSnapping,
+ type SnapPoint,
+} from "@/hooks/timeline/use-timeline-snapping";
+import { useTimelineStore } from "@/stores/timeline-store";
+
+export interface ResizeState {
+ elementId: string;
+ side: "left" | "right";
+ startX: number;
+ initialTrimStart: number;
+ initialTrimEnd: number;
+ initialStartTime: number;
+ initialDuration: number;
+}
+
+interface UseTimelineElementResizeProps {
+ element: TimelineElement;
+ track: TimelineTrack;
+ zoomLevel: number;
+ onSnapPointChange?: (snapPoint: SnapPoint | null) => void;
+ onResizeStateChange?: (params: { isResizing: boolean }) => void;
+}
+
+export function useTimelineElementResize({
+ element,
+ track,
+ zoomLevel,
+ onSnapPointChange,
+ onResizeStateChange,
+}: UseTimelineElementResizeProps) {
+ const editor = EditorCore.getInstance();
+ const activeProject = editor.project.getActive();
+ const snappingEnabled = useTimelineStore((state) => state.snappingEnabled);
+ const { findSnapPoints, snapToNearestPoint } = useTimelineSnapping();
+
+ const [resizing, setResizing] = useState<ResizeState | null>(null);
+ const [currentTrimStart, setCurrentTrimStart] = useState(element.trimStart);
+ const [currentTrimEnd, setCurrentTrimEnd] = useState(element.trimEnd);
+ const [currentStartTime, setCurrentStartTime] = useState(element.startTime);
+ const [currentDuration, setCurrentDuration] = useState(element.duration);
+ const currentTrimStartRef = useRef(element.trimStart);
+ const currentTrimEndRef = useRef(element.trimEnd);
+ const currentStartTimeRef = useRef(element.startTime);
+ const currentDurationRef = useRef(element.duration);
+
+ const handleResizeStart = ({
+ e,
+ elementId,
+ side,
+ }: {
+ e: React.MouseEvent;
+ elementId: string;
+ side: "left" | "right";
+ }) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ setResizing({
+ elementId,
+ side,
+ startX: e.clientX,
+ initialTrimStart: element.trimStart,
+ initialTrimEnd: element.trimEnd,
+ initialStartTime: element.startTime,
+ initialDuration: element.duration,
+ });
+
+ setCurrentTrimStart(element.trimStart);
+ setCurrentTrimEnd(element.trimEnd);
+ setCurrentStartTime(element.startTime);
+ setCurrentDuration(element.duration);
+ currentTrimStartRef.current = element.trimStart;
+ currentTrimEndRef.current = element.trimEnd;
+ currentStartTimeRef.current = element.startTime;
+ currentDurationRef.current = element.duration;
+ onResizeStateChange?.({ isResizing: true });
+ };
+
+ const canExtendElementDuration = useCallback(() => {
+ if (element.type === "text" || element.type === "image") {
+ return true;
+ }
+
+ return false;
+ }, [element.type]);
+
+ const updateTrimFromMouseMove = useCallback(({ clientX }: { clientX: number }) => {
+ if (!resizing) return;
+
+ const deltaX = clientX - resizing.startX;
+ let deltaTime = deltaX / (50 * zoomLevel);
+ let resizeSnapPoint: SnapPoint | null = null;
+
+ const projectFps = activeProject.settings.fps;
+ const minDurationSeconds = 1 / projectFps;
+ const canSnap = snappingEnabled;
+ if (canSnap) {
+ const tracks = editor.timeline.getTracks();
+ const playheadTime = editor.playback.getCurrentTime();
+ const snapPoints = findSnapPoints({
+ tracks,
+ playheadTime,
+ excludeElementId: element.id,
+ });
+ if (resizing.side === "left") {
+ const targetStartTime = resizing.initialStartTime + deltaTime;
+ const snapResult = snapToNearestPoint({
+ targetTime: targetStartTime,
+ snapPoints,
+ zoomLevel,
+ });
+ resizeSnapPoint = snapResult.snapPoint;
+ if (snapResult.snapPoint) {
+ deltaTime = snapResult.snappedTime - resizing.initialStartTime;
+ }
+ } else {
+ const baseEndTime =
+ resizing.initialStartTime + resizing.initialDuration;
+ const targetEndTime = baseEndTime + deltaTime;
+ const snapResult = snapToNearestPoint({
+ targetTime: targetEndTime,
+ snapPoints,
+ zoomLevel,
+ });
+ resizeSnapPoint = snapResult.snapPoint;
+ if (snapResult.snapPoint) {
+ deltaTime = snapResult.snappedTime - baseEndTime;
+ }
+ }
+ }
+ onSnapPointChange?.(resizeSnapPoint);
+
+ if (resizing.side === "left") {
+ const sourceDuration =
+ resizing.initialTrimStart +
+ resizing.initialDuration +
+ resizing.initialTrimEnd;
+ const maxAllowed =
+ sourceDuration - resizing.initialTrimEnd - minDurationSeconds;
+ const calculated = resizing.initialTrimStart + deltaTime;
+
+ if (calculated >= 0 && calculated <= maxAllowed) {
+ const newTrimStart = snapTimeToFrame({
+ time: Math.min(maxAllowed, calculated),
+ fps: projectFps,
+ });
+ const trimDelta = newTrimStart - resizing.initialTrimStart;
+ const newStartTime = snapTimeToFrame({
+ time: resizing.initialStartTime + trimDelta,
+ fps: projectFps,
+ });
+ const newDuration = snapTimeToFrame({
+ time: resizing.initialDuration - trimDelta,
+ fps: projectFps,
+ });
+
+ setCurrentTrimStart(newTrimStart);
+ setCurrentStartTime(newStartTime);
+ setCurrentDuration(newDuration);
+ currentTrimStartRef.current = newTrimStart;
+ currentStartTimeRef.current = newStartTime;
+ currentDurationRef.current = newDuration;
+ } else if (calculated < 0) {
+ if (canExtendElementDuration()) {
+ const extensionAmount = Math.abs(calculated);
+ const maxExtension = resizing.initialStartTime;
+ const actualExtension = Math.min(extensionAmount, maxExtension);
+ const newStartTime = snapTimeToFrame({
+ time: resizing.initialStartTime - actualExtension,
+ fps: projectFps,
+ });
+ const newDuration = snapTimeToFrame({
+ time: resizing.initialDuration + actualExtension,
+ fps: projectFps,
+ });
+
+ setCurrentTrimStart(0);
+ setCurrentStartTime(newStartTime);
+ setCurrentDuration(newDuration);
+ currentTrimStartRef.current = 0;
+ currentStartTimeRef.current = newStartTime;
+ currentDurationRef.current = newDuration;
+ } else {
+ const trimDelta = 0 - resizing.initialTrimStart;
+ const newStartTime = snapTimeToFrame({
+ time: resizing.initialStartTime + trimDelta,
+ fps: projectFps,
+ });
+ const newDuration = snapTimeToFrame({
+ time: resizing.initialDuration - trimDelta,
+ fps: projectFps,
+ });
+
+ setCurrentTrimStart(0);
+ setCurrentStartTime(newStartTime);
+ setCurrentDuration(newDuration);
+ currentTrimStartRef.current = 0;
+ currentStartTimeRef.current = newStartTime;
+ currentDurationRef.current = newDuration;
+ }
+ }
+ } else {
+ const sourceDuration =
+ resizing.initialTrimStart +
+ resizing.initialDuration +
+ resizing.initialTrimEnd;
+ const newTrimEnd = resizing.initialTrimEnd - deltaTime;
+
+ if (newTrimEnd < 0) {
+ if (canExtendElementDuration()) {
+ const extensionNeeded = Math.abs(newTrimEnd);
+ const baseDuration =
+ resizing.initialDuration + resizing.initialTrimEnd;
+ const newDuration = snapTimeToFrame({
+ time: baseDuration + extensionNeeded,
+ fps: projectFps,
+ });
+
+ setCurrentDuration(newDuration);
+ setCurrentTrimEnd(0);
+ currentDurationRef.current = newDuration;
+ currentTrimEndRef.current = 0;
+ } else {
+ const extensionToLimit = resizing.initialTrimEnd;
+ const newDuration = snapTimeToFrame({
+ time: resizing.initialDuration + extensionToLimit,
+ fps: projectFps,
+ });
+
+ setCurrentDuration(newDuration);
+ setCurrentTrimEnd(0);
+ currentDurationRef.current = newDuration;
+ currentTrimEndRef.current = 0;
+ }
+ } else {
+ const maxTrimEnd =
+ sourceDuration - resizing.initialTrimStart - minDurationSeconds;
+ const clampedTrimEnd = Math.min(maxTrimEnd, Math.max(0, newTrimEnd));
+ const finalTrimEnd = snapTimeToFrame({
+ time: clampedTrimEnd,
+ fps: projectFps,
+ });
+ const trimDelta = finalTrimEnd - resizing.initialTrimEnd;
+ const newDuration = snapTimeToFrame({
+ time: resizing.initialDuration - trimDelta,
+ fps: projectFps,
+ });
+
+ setCurrentTrimEnd(finalTrimEnd);
+ setCurrentDuration(newDuration);
+ currentTrimEndRef.current = finalTrimEnd;
+ currentDurationRef.current = newDuration;
+ }
+ }
+ }, [resizing, zoomLevel, activeProject.settings.fps, snappingEnabled, editor, findSnapPoints, snapToNearestPoint, element.id, onSnapPointChange, canExtendElementDuration]);
+
+ const handleResizeEnd = useCallback(() => {
+ if (!resizing) return;
+
+ const finalTrimStart = currentTrimStartRef.current;
+ const finalTrimEnd = currentTrimEndRef.current;
+ const finalStartTime = currentStartTimeRef.current;
+ const finalDuration = currentDurationRef.current;
+ const trimStartChanged = finalTrimStart !== resizing.initialTrimStart;
+ const trimEndChanged = finalTrimEnd !== resizing.initialTrimEnd;
+ const startTimeChanged = finalStartTime !== resizing.initialStartTime;
+ const durationChanged = finalDuration !== resizing.initialDuration;
+
+ if (trimStartChanged || trimEndChanged) {
+ editor.timeline.updateElementTrim({
+ elementId: element.id,
+ trimStart: finalTrimStart,
+ trimEnd: finalTrimEnd,
+ });
+ }
+
+ if (startTimeChanged) {
+ editor.timeline.updateElementStartTime({
+ elements: [{ trackId: track.id, elementId: element.id }],
+ startTime: finalStartTime,
+ });
+ }
+
+ if (durationChanged) {
+ editor.timeline.updateElementDuration({
+ trackId: track.id,
+ elementId: element.id,
+ duration: finalDuration,
+ });
+ }
+
+ setResizing(null);
+ onResizeStateChange?.({ isResizing: false });
+ onSnapPointChange?.(null);
+ }, [resizing, editor.timeline, element.id, track.id, onResizeStateChange, onSnapPointChange]);
+
+ useEffect(() => {
+ if (!resizing) return;
+
+ const handleDocumentMouseMove = ({ clientX }: MouseEvent) => {
+ updateTrimFromMouseMove({ clientX });
+ };
+
+ const handleDocumentMouseUp = () => {
+ handleResizeEnd();
+ };
+
+ document.addEventListener("mousemove", handleDocumentMouseMove);
+ document.addEventListener("mouseup", handleDocumentMouseUp);
+
+ return () => {
+ document.removeEventListener("mousemove", handleDocumentMouseMove);
+ document.removeEventListener("mouseup", handleDocumentMouseUp);
+ };
+ }, [resizing, handleResizeEnd, updateTrimFromMouseMove]);
+
+ return {
+ resizing,
+ isResizing: resizing !== null,
+ handleResizeStart,
+ currentTrimStart,
+ currentTrimEnd,
+ currentStartTime,
+ currentDuration,
+ };
+}
diff --git a/apps/web/src/hooks/timeline/element/use-element-selection.ts b/apps/web/src/hooks/timeline/element/use-element-selection.ts
new file mode 100644
index 00000000..38694c8b
--- /dev/null
+++ b/apps/web/src/hooks/timeline/element/use-element-selection.ts
@@ -0,0 +1,114 @@
+import { useCallback, useSyncExternalStore } from "react";
+import { useEditor } from "@/hooks/use-editor";
+
+type ElementRef = { trackId: string; elementId: string };
+
+export function useElementSelection() {
+ const editor = useEditor();
+ const selectedElements = useSyncExternalStore(
+ (listener) => editor.selection.subscribe(listener),
+ () => editor.selection.getSelectedElements(),
+ );
+
+ const isElementSelected = useCallback(
+ ({ trackId, elementId }: ElementRef) =>
+ selectedElements.some(
+ (element) =>
+ element.trackId === trackId && element.elementId === elementId,
+ ),
+ [selectedElements],
+ );
+
+ const selectElement = useCallback(
+ ({ trackId, elementId }: ElementRef) => {
+ editor.selection.setSelectedElements({ elements: [{ trackId, elementId }] });
+ },
+ [editor],
+ );
+
+ const addElementToSelection = useCallback(
+ ({ trackId, elementId }: ElementRef) => {
+ const alreadySelected = selectedElements.some(
+ (element) =>
+ element.trackId === trackId && element.elementId === elementId,
+ );
+ if (alreadySelected) return;
+
+ editor.selection.setSelectedElements({
+ elements: [...selectedElements, { trackId, elementId }],
+ });
+ },
+ [selectedElements, editor],
+ );
+
+ const removeElementFromSelection = useCallback(
+ ({ trackId, elementId }: ElementRef) => {
+ editor.selection.setSelectedElements({
+ elements: selectedElements.filter(
+ (element) =>
+ !(element.trackId === trackId && element.elementId === elementId),
+ ),
+ });
+ },
+ [selectedElements, editor],
+ );
+
+ const toggleElementSelection = useCallback(
+ ({ trackId, elementId }: ElementRef) => {
+ const alreadySelected = selectedElements.some(
+ (element) =>
+ element.trackId === trackId && element.elementId === elementId,
+ );
+
+ if (alreadySelected) {
+ removeElementFromSelection({ trackId, elementId });
+ } else {
+ addElementToSelection({ trackId, elementId });
+ }
+ },
+ [selectedElements, addElementToSelection, removeElementFromSelection],
+ );
+
+ const clearElementSelection = useCallback(() => {
+ editor.selection.clearSelection();
+ }, [editor]);
+
+ const setElementSelection = useCallback(
+ ({ elements }: { elements: ElementRef[] }) => {
+ editor.selection.setSelectedElements({ elements });
+ },
+ [editor],
+ );
+
+ /**
+ * Handles click interaction on an element.
+ * - Regular click: select only this element
+ * - Multi-key click (Ctrl/Cmd): toggle this element in selection
+ */
+ const handleElementClick = useCallback(
+ ({
+ trackId,
+ elementId,
+ isMultiKey,
+ }: ElementRef & { isMultiKey: boolean }) => {
+ if (isMultiKey) {
+ toggleElementSelection({ trackId, elementId });
+ } else {
+ selectElement({ trackId, elementId });
+ }
+ },
+ [toggleElementSelection, selectElement],
+ );
+
+ return {
+ selectedElements,
+ isElementSelected,
+ selectElement,
+ setElementSelection,
+ addElementToSelection,
+ removeElementFromSelection,
+ toggleElementSelection,
+ clearElementSelection,
+ handleElementClick,
+ };
+}
diff --git a/apps/web/src/hooks/timeline/use-edge-auto-scroll.ts b/apps/web/src/hooks/timeline/use-edge-auto-scroll.ts
new file mode 100644
index 00000000..0ddab654
--- /dev/null
+++ b/apps/web/src/hooks/timeline/use-edge-auto-scroll.ts
@@ -0,0 +1,100 @@
+import { useEffect, useRef } from "react";
+
+interface UseEdgeAutoScrollParams {
+ isActive: boolean;
+ getMouseClientX: () => number;
+ rulerScrollRef: React.RefObject<HTMLDivElement>;
+ tracksScrollRef: React.RefObject<HTMLDivElement>;
+ contentWidth: number;
+ edgeThreshold?: number;
+ maxScrollSpeed?: number;
+}
+
+export function useEdgeAutoScroll({
+ isActive,
+ getMouseClientX,
+ rulerScrollRef,
+ tracksScrollRef,
+ contentWidth,
+ edgeThreshold = 100,
+ maxScrollSpeed = 15,
+}: UseEdgeAutoScrollParams): void {
+ const rafRef = useRef<number | null>(null);
+
+ useEffect(() => {
+ if (!isActive) {
+ if (rafRef.current) {
+ cancelAnimationFrame(rafRef.current);
+ rafRef.current = null;
+ }
+ return;
+ }
+
+ const step = () => {
+ const rulerViewport = rulerScrollRef.current;
+ const tracksViewport = tracksScrollRef.current;
+ if (!rulerViewport || !tracksViewport) {
+ rafRef.current = requestAnimationFrame(step);
+ return;
+ }
+
+ const viewportRect = rulerViewport.getBoundingClientRect();
+ const mouseX = getMouseClientX();
+ const mouseXRelative = mouseX - viewportRect.left;
+
+ const viewportWidth = rulerViewport.clientWidth;
+ const intrinsicContentWidth = rulerViewport.scrollWidth;
+ const effectiveContentWidth = Math.max(
+ contentWidth,
+ intrinsicContentWidth,
+ );
+ const scrollMax = Math.max(0, effectiveContentWidth - viewportWidth);
+
+ let scrollSpeed = 0;
+
+ if (mouseXRelative < edgeThreshold && rulerViewport.scrollLeft > 0) {
+ const edgeDistance = Math.max(0, mouseXRelative);
+ const intensity = 1 - edgeDistance / edgeThreshold;
+ scrollSpeed = -maxScrollSpeed * intensity;
+ } else if (
+ mouseXRelative > viewportWidth - edgeThreshold &&
+ rulerViewport.scrollLeft < scrollMax
+ ) {
+ const edgeDistance = Math.max(
+ 0,
+ viewportWidth - edgeThreshold - mouseXRelative,
+ );
+ const intensity = 1 - edgeDistance / edgeThreshold;
+ scrollSpeed = maxScrollSpeed * intensity;
+ }
+
+ if (scrollSpeed !== 0) {
+ const newScrollLeft = Math.max(
+ 0,
+ Math.min(scrollMax, rulerViewport.scrollLeft + scrollSpeed),
+ );
+ rulerViewport.scrollLeft = newScrollLeft;
+ tracksViewport.scrollLeft = newScrollLeft;
+ }
+
+ rafRef.current = requestAnimationFrame(step);
+ };
+
+ rafRef.current = requestAnimationFrame(step);
+
+ return () => {
+ if (rafRef.current) {
+ cancelAnimationFrame(rafRef.current);
+ rafRef.current = null;
+ }
+ };
+ }, [
+ isActive,
+ getMouseClientX,
+ rulerScrollRef,
+ tracksScrollRef,
+ contentWidth,
+ edgeThreshold,
+ maxScrollSpeed,
+ ]);
+}
diff --git a/apps/web/src/hooks/timeline/use-scroll-sync.ts b/apps/web/src/hooks/timeline/use-scroll-sync.ts
new file mode 100644
index 00000000..cdd915b0
--- /dev/null
+++ b/apps/web/src/hooks/timeline/use-scroll-sync.ts
@@ -0,0 +1,173 @@
+import { useEffect, useRef } from "react";
+
+interface UseScrollSyncProps {
+ rulerScrollRef?: React.RefObject<HTMLDivElement>;
+ tracksScrollRef: React.RefObject<HTMLDivElement>;
+ trackLabelsScrollRef?: React.RefObject<HTMLDivElement>;
+ bookmarksScrollRef?: React.RefObject<HTMLDivElement>;
+}
+
+export function useScrollSync({
+ tracksScrollRef,
+ rulerScrollRef,
+ trackLabelsScrollRef,
+ bookmarksScrollRef,
+}: UseScrollSyncProps) {
+ const isUpdatingRef = useRef(false);
+ const lastRulerSync = useRef(0);
+ const lastTracksSync = useRef(0);
+ const lastVerticalSync = useRef(0);
+ const lastBookmarksSync = useRef(0);
+
+ useEffect(() => {
+ const rulerViewport = rulerScrollRef?.current ?? null;
+ const tracksViewport = tracksScrollRef.current;
+ const trackLabelsViewport = trackLabelsScrollRef?.current;
+ const bookmarksViewport = bookmarksScrollRef?.current;
+ let handleBookmarksScroll: (() => void) | null = null;
+
+ if (!tracksViewport) return;
+
+ const syncScrollLeft = ({
+ target,
+ scrollLeft,
+ }: {
+ target: HTMLDivElement | null | undefined;
+ scrollLeft: number;
+ }) => {
+ if (target) {
+ target.scrollLeft = scrollLeft;
+ }
+ };
+
+ const handleRulerScroll = () => {
+ const now = Date.now();
+ if (isUpdatingRef.current || now - lastRulerSync.current < 16) return;
+ lastRulerSync.current = now;
+ isUpdatingRef.current = true;
+ tracksViewport.scrollLeft = rulerViewport?.scrollLeft ?? 0;
+ syncScrollLeft({
+ target: bookmarksViewport,
+ scrollLeft: rulerViewport?.scrollLeft ?? 0,
+ });
+ isUpdatingRef.current = false;
+ };
+
+ const handleTracksScroll = () => {
+ const now = Date.now();
+ if (isUpdatingRef.current || now - lastTracksSync.current < 16) return;
+ lastTracksSync.current = now;
+ isUpdatingRef.current = true;
+ syncScrollLeft({
+ target: rulerViewport,
+ scrollLeft: tracksViewport.scrollLeft,
+ });
+ syncScrollLeft({
+ target: bookmarksViewport,
+ scrollLeft: tracksViewport.scrollLeft,
+ });
+ isUpdatingRef.current = false;
+ };
+
+ if (rulerViewport && rulerViewport !== tracksViewport) {
+ rulerViewport.addEventListener("scroll", handleRulerScroll);
+ }
+ if (tracksViewport !== rulerViewport) {
+ tracksViewport.addEventListener("scroll", handleTracksScroll);
+ }
+
+ if (bookmarksViewport) {
+ handleBookmarksScroll = () => {
+ const now = Date.now();
+ if (isUpdatingRef.current || now - lastBookmarksSync.current < 16)
+ return;
+ lastBookmarksSync.current = now;
+ isUpdatingRef.current = true;
+ const nextScrollLeft = bookmarksViewport.scrollLeft;
+ tracksViewport.scrollLeft = nextScrollLeft;
+ syncScrollLeft({
+ target: rulerViewport,
+ scrollLeft: nextScrollLeft,
+ });
+ isUpdatingRef.current = false;
+ };
+
+ if (bookmarksViewport !== tracksViewport) {
+ bookmarksViewport.addEventListener("scroll", handleBookmarksScroll);
+ }
+ }
+
+ if (trackLabelsViewport) {
+ const handleTrackLabelsScroll = () => {
+ const now = Date.now();
+ if (isUpdatingRef.current || now - lastVerticalSync.current < 16)
+ return;
+ lastVerticalSync.current = now;
+ isUpdatingRef.current = true;
+ tracksViewport.scrollTop = trackLabelsViewport.scrollTop;
+ isUpdatingRef.current = false;
+ };
+
+ const handleTracksVerticalScroll = () => {
+ const now = Date.now();
+ if (isUpdatingRef.current || now - lastVerticalSync.current < 16)
+ return;
+ lastVerticalSync.current = now;
+ isUpdatingRef.current = true;
+ trackLabelsViewport.scrollTop = tracksViewport.scrollTop;
+ isUpdatingRef.current = false;
+ };
+
+ trackLabelsViewport.addEventListener("scroll", handleTrackLabelsScroll);
+ tracksViewport.addEventListener("scroll", handleTracksVerticalScroll);
+
+ return () => {
+ if (rulerViewport && rulerViewport !== tracksViewport) {
+ rulerViewport.removeEventListener("scroll", handleRulerScroll);
+ }
+ if (tracksViewport !== rulerViewport) {
+ tracksViewport.removeEventListener("scroll", handleTracksScroll);
+ }
+ if (
+ bookmarksViewport &&
+ bookmarksViewport !== tracksViewport &&
+ handleBookmarksScroll
+ ) {
+ bookmarksViewport.removeEventListener(
+ "scroll",
+ handleBookmarksScroll,
+ );
+ }
+ trackLabelsViewport.removeEventListener(
+ "scroll",
+ handleTrackLabelsScroll,
+ );
+ tracksViewport.removeEventListener(
+ "scroll",
+ handleTracksVerticalScroll,
+ );
+ };
+ }
+
+ return () => {
+ if (rulerViewport && rulerViewport !== tracksViewport) {
+ rulerViewport.removeEventListener("scroll", handleRulerScroll);
+ }
+ if (tracksViewport !== rulerViewport) {
+ tracksViewport.removeEventListener("scroll", handleTracksScroll);
+ }
+ if (
+ bookmarksViewport &&
+ bookmarksViewport !== tracksViewport &&
+ handleBookmarksScroll
+ ) {
+ bookmarksViewport.removeEventListener("scroll", handleBookmarksScroll);
+ }
+ };
+ }, [
+ rulerScrollRef,
+ tracksScrollRef,
+ trackLabelsScrollRef,
+ bookmarksScrollRef,
+ ]);
+}
diff --git a/apps/web/src/hooks/timeline/use-selection-box.ts b/apps/web/src/hooks/timeline/use-selection-box.ts
new file mode 100644
index 00000000..702abad9
--- /dev/null
+++ b/apps/web/src/hooks/timeline/use-selection-box.ts
@@ -0,0 +1,243 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import { getCumulativeHeightBefore, getTrackHeight } from "@/lib/timeline";
+import { useEditor } from "../use-editor";
+
+interface UseSelectionBoxProps {
+ containerRef: React.RefObject<HTMLElement>;
+ onSelectionComplete: (
+ elements: { trackId: string; elementId: string }[],
+ ) => void;
+ isEnabled?: boolean;
+ tracksScrollRef: React.RefObject<HTMLDivElement>;
+ zoomLevel: number;
+}
+
+interface SelectionBoxState {
+ startPos: { x: number; y: number };
+ currentPos: { x: number; y: number };
+ isActive: boolean;
+}
+
+interface SelectionRectangle {
+ left: number;
+ top: number;
+ right: number;
+ bottom: number;
+}
+
+function getNormalizedRectangle({
+ startPos,
+ endPos,
+}: {
+ startPos: { x: number; y: number };
+ endPos: { x: number; y: number };
+}): SelectionRectangle {
+ return {
+ left: Math.min(startPos.x, endPos.x),
+ top: Math.min(startPos.y, endPos.y),
+ right: Math.max(startPos.x, endPos.x),
+ bottom: Math.max(startPos.y, endPos.y),
+ };
+}
+
+function getSelectionRectangleInContent({
+ container,
+ scrollContainer,
+ startPos,
+ endPos,
+}: {
+ container: HTMLElement;
+ scrollContainer: HTMLDivElement | null;
+ startPos: { x: number; y: number };
+ endPos: { x: number; y: number };
+}): SelectionRectangle {
+ const containerRect = container.getBoundingClientRect();
+ const scrollLeft = scrollContainer?.scrollLeft ?? 0;
+ const scrollTop = scrollContainer?.scrollTop ?? 0;
+
+ const adjustedStart = {
+ x: startPos.x - containerRect.left + scrollLeft,
+ y: startPos.y - containerRect.top + scrollTop,
+ };
+ const adjustedEnd = {
+ x: endPos.x - containerRect.left + scrollLeft,
+ y: endPos.y - containerRect.top + scrollTop,
+ };
+
+ return getNormalizedRectangle({
+ startPos: adjustedStart,
+ endPos: adjustedEnd,
+ });
+}
+
+function isRectangleIntersecting({
+ elementRectangle,
+ selectionRectangle,
+}: {
+ elementRectangle: SelectionRectangle;
+ selectionRectangle: SelectionRectangle;
+}): boolean {
+ return !(
+ elementRectangle.right < selectionRectangle.left ||
+ elementRectangle.left > selectionRectangle.right ||
+ elementRectangle.bottom < selectionRectangle.top ||
+ elementRectangle.top > selectionRectangle.bottom
+ );
+}
+
+export function useSelectionBox({
+ containerRef,
+ onSelectionComplete,
+ isEnabled = true,
+ tracksScrollRef,
+ zoomLevel,
+}: UseSelectionBoxProps) {
+ const editor = useEditor();
+ const tracks = editor.timeline.getTracks();
+ const [selectionBox, setSelectionBox] = useState<SelectionBoxState | null>(
+ null,
+ );
+ const justFinishedSelectingRef = useRef(false);
+
+ const handleMouseDown = useCallback(
+ ({ clientX, clientY }: React.MouseEvent) => {
+ if (!isEnabled) return;
+
+ setSelectionBox({
+ startPos: { x: clientX, y: clientY },
+ currentPos: { x: clientX, y: clientY },
+ isActive: false,
+ });
+ },
+ [isEnabled],
+ );
+
+ const selectElementsInBox = useCallback(
+ ({
+ startPos,
+ endPos,
+ }: {
+ startPos: { x: number; y: number };
+ endPos: { x: number; y: number };
+ }) => {
+ if (!containerRef.current) return;
+
+ const container = containerRef.current;
+ const selectionRectangle = getSelectionRectangleInContent({
+ container,
+ scrollContainer: tracksScrollRef.current,
+ startPos,
+ endPos,
+ });
+ const pixelsPerSecond = TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
+ const selectedElements: { trackId: string; elementId: string }[] = [];
+
+ for (const [trackIndex, track] of tracks.entries()) {
+ const trackTop = getCumulativeHeightBefore({
+ tracks,
+ trackIndex,
+ });
+ const trackHeight = getTrackHeight({ type: track.type });
+ const elementTop = trackTop;
+ const elementBottom = trackTop + trackHeight;
+
+ for (const element of track.elements) {
+ const elementLeft = element.startTime * pixelsPerSecond;
+ const elementRight = elementLeft + element.duration * pixelsPerSecond;
+
+ const elementRectangle = {
+ left: elementLeft,
+ top: elementTop,
+ right: elementRight,
+ bottom: elementBottom,
+ };
+
+ const intersects = isRectangleIntersecting({
+ elementRectangle,
+ selectionRectangle,
+ });
+
+ if (intersects) {
+ selectedElements.push({
+ trackId: track.id,
+ elementId: element.id,
+ });
+ }
+ }
+ }
+ onSelectionComplete(selectedElements);
+ },
+ [containerRef, onSelectionComplete, tracks, tracksScrollRef, zoomLevel],
+ );
+
+ 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 shouldActivate = deltaX > 5 || deltaY > 5;
+
+ const newSelectionBox = {
+ ...selectionBox,
+ currentPos: { x: clientX, y: clientY },
+ isActive: shouldActivate || selectionBox.isActive,
+ };
+
+ setSelectionBox(newSelectionBox);
+
+ if (newSelectionBox.isActive) {
+ selectElementsInBox({
+ startPos: newSelectionBox.startPos,
+ endPos: newSelectionBox.currentPos,
+ });
+ }
+ };
+
+ 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, selectElementsInBox]);
+
+ useEffect(() => {
+ if (!selectionBox) return;
+
+ const previousBodyUserSelect = document.body.style.userSelect;
+ const container = containerRef.current;
+ const previousContainerUserSelect = container?.style.userSelect ?? "";
+
+ document.body.style.userSelect = "none";
+ if (container) container.style.userSelect = "none";
+
+ return () => {
+ document.body.style.userSelect = previousBodyUserSelect;
+ if (container) container.style.userSelect = previousContainerUserSelect;
+ };
+ }, [selectionBox, containerRef]);
+
+ const shouldIgnoreClick = useCallback(() => {
+ return justFinishedSelectingRef.current;
+ }, []);
+
+ return {
+ selectionBox,
+ handleMouseDown,
+ isSelecting: selectionBox?.isActive || false,
+ shouldIgnoreClick,
+ };
+}
diff --git a/apps/web/src/hooks/timeline/use-snap-indicator-position.ts b/apps/web/src/hooks/timeline/use-snap-indicator-position.ts
new file mode 100644
index 00000000..c6ecbf7c
--- /dev/null
+++ b/apps/web/src/hooks/timeline/use-snap-indicator-position.ts
@@ -0,0 +1,62 @@
+import { useEffect, useState } from "react";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import type { TimelineTrack } from "@/types/timeline";
+
+interface UseSnapIndicatorPositionParams {
+ snapPoint: { time: number } | null;
+ zoomLevel: number;
+ tracks: TimelineTrack[];
+ timelineRef: React.RefObject<HTMLDivElement>;
+ trackLabelsRef?: React.RefObject<HTMLDivElement>;
+ tracksScrollRef: React.RefObject<HTMLDivElement>;
+}
+
+interface SnapIndicatorPosition {
+ leftPosition: number;
+ topPosition: number;
+ height: number;
+}
+
+export function useSnapIndicatorPosition({
+ snapPoint,
+ zoomLevel,
+ tracks,
+ timelineRef,
+ trackLabelsRef,
+ tracksScrollRef,
+}: UseSnapIndicatorPositionParams): SnapIndicatorPosition {
+ const [scrollLeft, setScrollLeft] = useState(0);
+
+ useEffect(() => {
+ const tracksViewport = tracksScrollRef.current;
+
+ if (!tracksViewport) return;
+
+ const handleScroll = () => {
+ setScrollLeft(tracksViewport.scrollLeft);
+ };
+
+ setScrollLeft(tracksViewport.scrollLeft);
+
+ tracksViewport.addEventListener("scroll", handleScroll);
+ return () => tracksViewport.removeEventListener("scroll", handleScroll);
+ }, [tracksScrollRef]);
+
+ const timelineContainerHeight = timelineRef.current?.offsetHeight || 400;
+ const totalHeight = timelineContainerHeight - 8; // 8px padding from edges
+
+ const trackLabelsWidth =
+ tracks.length > 0 && trackLabelsRef?.current
+ ? trackLabelsRef.current.offsetWidth
+ : 0;
+
+ const timelinePosition =
+ (snapPoint?.time || 0) * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
+ const leftPosition = trackLabelsWidth + timelinePosition - scrollLeft;
+
+ return {
+ leftPosition,
+ topPosition: 0,
+ height: totalHeight,
+ };
+}
diff --git a/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts b/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts
new file mode 100644
index 00000000..01af9362
--- /dev/null
+++ b/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts
@@ -0,0 +1,460 @@
+import { useState, useCallback, type RefObject } from "react";
+import { useEditor } from "@/hooks/use-editor";
+import { processMediaAssets } from "@/lib/media/processing";
+import { toast } from "sonner";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import { snapTimeToFrame } from "@/lib/time";
+import {
+ buildTextElement,
+ buildStickerElement,
+ buildUploadAudioElement,
+ buildVideoElement,
+ buildImageElement,
+} from "@/lib/timeline/element-utils";
+import { computeDropTarget } from "@/lib/timeline/drop-utils";
+import { getDragData, hasDragData } from "@/lib/drag-data";
+import type { TrackType, DropTarget, ElementType } from "@/types/timeline";
+import type { MediaDragData, StickerDragData } from "@/types/drag";
+
+interface UseTimelineDragDropProps {
+ containerRef: RefObject<HTMLDivElement | null>;
+ headerRef?: RefObject<HTMLElement | null>;
+ zoomLevel: number;
+}
+
+export function useTimelineDragDrop({
+ containerRef,
+ headerRef,
+ zoomLevel,
+}: UseTimelineDragDropProps) {
+ const editor = useEditor();
+ const [isDragOver, setIsDragOver] = useState(false);
+ const [dropTarget, setDropTarget] = useState<DropTarget | null>(null);
+ const [dragElementType, setElementType] = useState<ElementType | null>(null);
+
+ const tracks = editor.timeline.getTracks();
+ const currentTime = editor.playback.getCurrentTime();
+ const mediaAssets = editor.media.getAssets();
+ const activeProject = editor.project.getActive();
+
+ const getSnappedTime = useCallback(
+ ({ time }: { time: number }) => {
+ const projectFps = activeProject.settings.fps;
+ return snapTimeToFrame({ time, fps: projectFps });
+ },
+ [activeProject.settings.fps],
+ );
+
+ const getElementType = useCallback(
+ ({ dataTransfer }: { dataTransfer: DataTransfer }): ElementType | null => {
+ const dragData = getDragData({ dataTransfer });
+ if (!dragData) return null;
+
+ if (dragData.type === "text") return "text";
+ if (dragData.type === "sticker") return "sticker";
+ if (dragData.type === "media") {
+ return dragData.mediaType;
+ }
+ return null;
+ },
+ [],
+ );
+
+ const getElementDuration = useCallback(
+ ({
+ elementType,
+ mediaId,
+ }: {
+ elementType: ElementType;
+ mediaId?: string;
+ }): number => {
+ if (elementType === "text" || elementType === "sticker") {
+ return TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
+ }
+ if (mediaId) {
+ const media = mediaAssets.find((m) => m.id === mediaId);
+ return media?.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
+ }
+ return TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
+ },
+ [mediaAssets],
+ );
+
+ const handleDragEnter = useCallback((e: React.DragEvent) => {
+ e.preventDefault();
+ const hasAsset = hasDragData({ dataTransfer: e.dataTransfer });
+ const hasFiles = e.dataTransfer.types.includes("Files");
+ if (!hasAsset && !hasFiles) return;
+ setIsDragOver(true);
+ }, []);
+
+ const handleDragOver = useCallback(
+ (e: React.DragEvent) => {
+ e.preventDefault();
+
+ const rect = containerRef.current?.getBoundingClientRect();
+ if (!rect) return;
+
+ const headerHeight =
+ headerRef?.current?.getBoundingClientRect().height ?? 0;
+ const hasFiles = e.dataTransfer.types.includes("Files");
+ const isExternal =
+ hasFiles && !hasDragData({ dataTransfer: e.dataTransfer });
+
+ const elementType = getElementType({ dataTransfer: e.dataTransfer });
+
+ if (!elementType && hasFiles && isExternal) {
+ setDropTarget(null);
+ setElementType(null);
+ return;
+ }
+
+ if (!elementType) return;
+
+ setElementType(elementType);
+
+ const dragData = getDragData({ dataTransfer: e.dataTransfer });
+ const duration = getElementDuration({
+ elementType,
+ mediaId: dragData?.type === "media" ? dragData.id : undefined,
+ });
+
+ const mouseX = e.clientX - rect.left;
+ const mouseY = Math.max(0, e.clientY - rect.top - headerHeight);
+
+ const target = computeDropTarget({
+ elementType,
+ mouseX,
+ mouseY,
+ tracks,
+ playheadTime: currentTime,
+ isExternalDrop: isExternal,
+ elementDuration: duration,
+ pixelsPerSecond: TIMELINE_CONSTANTS.PIXELS_PER_SECOND,
+ zoomLevel,
+ });
+
+ target.xPosition = getSnappedTime({ time: target.xPosition });
+
+ setDropTarget(target);
+ e.dataTransfer.dropEffect = "copy";
+ },
+ [
+ containerRef,
+ headerRef,
+ tracks,
+ currentTime,
+ zoomLevel,
+ getElementType,
+ getElementDuration,
+ getSnappedTime,
+ ],
+ );
+
+ const handleDragLeave = useCallback(
+ (e: React.DragEvent) => {
+ e.preventDefault();
+ const rect = containerRef.current?.getBoundingClientRect();
+ if (rect) {
+ const { clientX, clientY } = e;
+ if (
+ clientX < rect.left ||
+ clientX > rect.right ||
+ clientY < rect.top ||
+ clientY > rect.bottom
+ ) {
+ setIsDragOver(false);
+ setDropTarget(null);
+ setElementType(null);
+ }
+ }
+ },
+ [containerRef],
+ );
+
+ const executeTextDrop = useCallback(
+ ({
+ target,
+ dragData,
+ }: {
+ target: DropTarget;
+ dragData: { name?: string; content?: string };
+ }) => {
+ let trackId: string;
+
+ if (target.isNewTrack) {
+ trackId = editor.timeline.addTrack({
+ type: "text",
+ index: target.trackIndex,
+ });
+ } else {
+ const track = tracks[target.trackIndex];
+ if (!track) return;
+ trackId = track.id;
+ }
+
+ const element = buildTextElement({
+ raw: {
+ name: dragData.name ?? "",
+ content: dragData.content ?? "",
+ },
+ startTime: target.xPosition,
+ });
+
+ editor.timeline.insertElement({
+ placement: { mode: "explicit", trackId },
+ element,
+ });
+ },
+ [editor.timeline, tracks],
+ );
+
+ const executeStickerDrop = useCallback(
+ ({
+ target,
+ dragData,
+ }: {
+ target: DropTarget;
+ dragData: StickerDragData;
+ }) => {
+ let trackId: string;
+
+ if (target.isNewTrack) {
+ trackId = editor.timeline.addTrack({
+ type: "sticker",
+ index: target.trackIndex,
+ });
+ } else {
+ const track = tracks[target.trackIndex];
+ if (!track) return;
+ trackId = track.id;
+ }
+
+ const element = buildStickerElement({
+ iconName: dragData.iconName,
+ startTime: target.xPosition,
+ });
+
+ editor.timeline.insertElement({
+ placement: { mode: "explicit", trackId },
+ element,
+ });
+ },
+ [editor.timeline, tracks],
+ );
+
+ const executeMediaDrop = useCallback(
+ ({ target, dragData }: { target: DropTarget; dragData: MediaDragData }) => {
+ const mediaAsset = mediaAssets.find((m) => m.id === dragData.id);
+ if (!mediaAsset) return;
+
+ const trackType: TrackType =
+ dragData.mediaType === "audio" ? "audio" : "video";
+ let trackId: string;
+
+ if (target.isNewTrack) {
+ trackId = editor.timeline.addTrack({
+ type: trackType,
+ index: target.trackIndex,
+ });
+ } else {
+ const track = tracks[target.trackIndex];
+ if (!track) return;
+ trackId = track.id;
+ }
+
+ const duration =
+ mediaAsset.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
+
+ if (dragData.mediaType === "audio") {
+ editor.timeline.insertElement({
+ placement: { mode: "explicit", trackId },
+ element: buildUploadAudioElement({
+ mediaId: mediaAsset.id,
+ name: mediaAsset.name,
+ duration,
+ startTime: target.xPosition,
+ }),
+ });
+ } else if (dragData.mediaType === "video") {
+ editor.timeline.insertElement({
+ placement: { mode: "explicit", trackId },
+ element: buildVideoElement({
+ mediaId: mediaAsset.id,
+ name: mediaAsset.name,
+ duration,
+ startTime: target.xPosition,
+ }),
+ });
+ } else {
+ editor.timeline.insertElement({
+ placement: { mode: "explicit", trackId },
+ element: buildImageElement({
+ mediaId: mediaAsset.id,
+ name: mediaAsset.name,
+ duration,
+ startTime: target.xPosition,
+ }),
+ });
+ }
+ },
+ [editor.timeline, mediaAssets, tracks],
+ );
+
+ const executeFileDrop = useCallback(
+ async ({
+ files,
+ mouseX,
+ mouseY,
+ }: {
+ files: File[];
+ mouseX: number;
+ mouseY: number;
+ }) => {
+ if (!activeProject) return;
+
+ const processedAssets = await processMediaAssets({ files });
+
+ for (const asset of processedAssets) {
+ await editor.media.addMediaAsset({
+ projectId: activeProject.metadata.id,
+ asset,
+ });
+
+ const added = editor.media
+ .getAssets()
+ .find((m) => m.name === asset.name && m.url === asset.url);
+
+ if (added) {
+ const duration =
+ added.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
+ const currentTracks = editor.timeline.getTracks();
+ const dropTarget = computeDropTarget({
+ elementType: added.type,
+ mouseX,
+ mouseY,
+ tracks: currentTracks,
+ playheadTime: currentTime,
+ isExternalDrop: true,
+ elementDuration: duration,
+ pixelsPerSecond: TIMELINE_CONSTANTS.PIXELS_PER_SECOND,
+ zoomLevel,
+ });
+
+ const trackType: TrackType =
+ added.type === "audio" ? "audio" : "video";
+ const trackId = dropTarget.isNewTrack
+ ? editor.timeline.addTrack({
+ type: trackType,
+ index: dropTarget.trackIndex,
+ })
+ : currentTracks[dropTarget.trackIndex]?.id;
+
+ if (!trackId) return;
+
+ if (added.type === "audio") {
+ editor.timeline.insertElement({
+ placement: { mode: "explicit", trackId },
+ element: buildUploadAudioElement({
+ mediaId: added.id,
+ name: added.name,
+ duration,
+ startTime: dropTarget.xPosition,
+ buffer: new AudioBuffer({ length: 1, sampleRate: 44100 }),
+ }),
+ });
+ } else if (added.type === "video") {
+ editor.timeline.insertElement({
+ placement: { mode: "explicit", trackId },
+ element: buildVideoElement({
+ mediaId: added.id,
+ name: added.name,
+ duration,
+ startTime: dropTarget.xPosition,
+ }),
+ });
+ } else {
+ editor.timeline.insertElement({
+ placement: { mode: "explicit", trackId },
+ element: buildImageElement({
+ mediaId: added.id,
+ name: added.name,
+ duration,
+ startTime: dropTarget.xPosition,
+ }),
+ });
+ }
+ }
+ }
+ },
+ [activeProject, editor.media, editor.timeline, currentTime, zoomLevel],
+ );
+
+ const handleDrop = useCallback(
+ async (e: React.DragEvent) => {
+ e.preventDefault();
+
+ const hasAsset = hasDragData({ dataTransfer: e.dataTransfer });
+ const hasFiles = e.dataTransfer.files?.length > 0;
+
+ if (!hasAsset && !hasFiles) return;
+
+ const currentTarget = dropTarget;
+ setIsDragOver(false);
+ setDropTarget(null);
+ setElementType(null);
+
+ try {
+ if (hasAsset) {
+ if (!currentTarget) return;
+ const dragData = getDragData({ dataTransfer: e.dataTransfer });
+ if (!dragData) return;
+
+ if (dragData.type === "text") {
+ executeTextDrop({ target: currentTarget, dragData });
+ } else if (dragData.type === "sticker") {
+ executeStickerDrop({ target: currentTarget, dragData });
+ } else {
+ executeMediaDrop({ target: currentTarget, dragData });
+ }
+ } else if (hasFiles) {
+ const rect = containerRef.current?.getBoundingClientRect();
+ if (!rect) return;
+ const mouseX = e.clientX - rect.left;
+ const headerHeight =
+ headerRef?.current?.getBoundingClientRect().height ?? 0;
+ const mouseY = Math.max(0, e.clientY - rect.top - headerHeight);
+ await executeFileDrop({
+ files: Array.from(e.dataTransfer.files),
+ mouseX,
+ mouseY,
+ });
+ }
+ } catch (err) {
+ console.error("Failed to process drop:", err);
+ toast.error("Failed to process drop");
+ }
+ },
+ [
+ dropTarget,
+ executeTextDrop,
+ executeStickerDrop,
+ executeMediaDrop,
+ executeFileDrop,
+ containerRef,
+ headerRef,
+ ],
+ );
+
+ return {
+ isDragOver,
+ dropTarget,
+ dragElementType,
+ dragProps: {
+ onDragEnter: handleDragEnter,
+ onDragOver: handleDragOver,
+ onDragLeave: handleDragLeave,
+ onDrop: handleDrop,
+ },
+ };
+}
diff --git a/apps/web/src/hooks/timeline/use-timeline-playhead.ts b/apps/web/src/hooks/timeline/use-timeline-playhead.ts
new file mode 100644
index 00000000..55e04a68
--- /dev/null
+++ b/apps/web/src/hooks/timeline/use-timeline-playhead.ts
@@ -0,0 +1,218 @@
+import { getSnappedSeekTime } from "@/lib/time";
+import { useState, useEffect, useCallback, useRef } from "react";
+import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
+import { useEditor } from "../use-editor";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+
+interface UseTimelinePlayheadProps {
+ zoomLevel: number;
+ rulerRef: React.RefObject<HTMLDivElement>;
+ rulerScrollRef: React.RefObject<HTMLDivElement>;
+ tracksScrollRef: React.RefObject<HTMLDivElement>;
+ playheadRef?: React.RefObject<HTMLDivElement>;
+}
+
+export function useTimelinePlayhead({
+ zoomLevel,
+ rulerRef,
+ rulerScrollRef,
+ tracksScrollRef,
+ playheadRef,
+}: UseTimelinePlayheadProps) {
+ const editor = useEditor();
+ const activeProject = editor.project.getActive();
+ const currentTime = editor.playback.getCurrentTime();
+ const duration = editor.timeline.getTotalDuration();
+ const isPlaying = editor.playback.getIsPlaying();
+
+ const seek = useCallback(
+ ({ time }: { time: number }) => editor.playback.seek({ time }),
+ [editor.playback],
+ );
+
+ const [isScrubbing, setIsScrubbing] = useState(false);
+ const [scrubTime, setScrubTime] = useState<number | null>(null);
+
+ const [isDraggingRuler, setIsDraggingRuler] = useState(false);
+ const [hasDraggedRuler, setHasDraggedRuler] = useState(false);
+ const lastMouseXRef = useRef<number>(0);
+
+ const playheadPosition =
+ isScrubbing && scrubTime !== null ? scrubTime : currentTime;
+
+ const handleScrub = useCallback(
+ ({ event }: { event: MouseEvent | React.MouseEvent }) => {
+ const ruler = rulerRef.current;
+ if (!ruler) return;
+ const rulerRect = ruler.getBoundingClientRect();
+ const relativeMouseX = event.clientX - rulerRect.left;
+
+ const timelineContentWidth =
+ duration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
+
+ const clampedMouseX = Math.max(
+ 0,
+ Math.min(timelineContentWidth, relativeMouseX),
+ );
+
+ const rawTime = Math.max(
+ 0,
+ Math.min(
+ duration,
+ clampedMouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel),
+ ),
+ );
+ const framesPerSecond = activeProject.settings.fps;
+ const time = getSnappedSeekTime({
+ rawTime,
+ duration,
+ fps: framesPerSecond,
+ });
+
+ setScrubTime(time);
+ seek({ time });
+
+ lastMouseXRef.current = event.clientX;
+ },
+ [duration, zoomLevel, seek, rulerRef, activeProject.settings.fps],
+ );
+
+ const handlePlayheadMouseDown = useCallback(
+ ({ event }: { event: React.MouseEvent }) => {
+ event.preventDefault();
+ event.stopPropagation();
+ setIsScrubbing(true);
+ handleScrub({ event });
+ },
+ [handleScrub],
+ );
+
+ const handleRulerMouseDown = useCallback(
+ ({ event }: { event: React.MouseEvent }) => {
+ if (event.button !== 0) return;
+
+ if (playheadRef?.current?.contains(event.target as Node)) return;
+
+ event.preventDefault();
+ setIsDraggingRuler(true);
+ setHasDraggedRuler(false);
+
+ setIsScrubbing(true);
+ handleScrub({ event });
+ },
+ [handleScrub, playheadRef],
+ );
+
+ const handlePlayheadMouseDownEvent = useCallback(
+ (event: React.MouseEvent) => handlePlayheadMouseDown({ event }),
+ [handlePlayheadMouseDown],
+ );
+
+ const handleRulerMouseDownEvent = useCallback(
+ (event: React.MouseEvent) => handleRulerMouseDown({ event }),
+ [handleRulerMouseDown],
+ );
+
+ useEdgeAutoScroll({
+ isActive: isScrubbing,
+ getMouseClientX: () => lastMouseXRef.current,
+ rulerScrollRef,
+ tracksScrollRef,
+ contentWidth: duration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel,
+ });
+
+ useEffect(() => {
+ if (!isScrubbing) return;
+
+ const handleMouseMove = ({ event }: { event: MouseEvent }) => {
+ handleScrub({ event });
+ if (isDraggingRuler) {
+ setHasDraggedRuler(true);
+ }
+ };
+
+ const handleMouseUp = ({ event }: { event: MouseEvent }) => {
+ setIsScrubbing(false);
+ if (scrubTime !== null) {
+ seek({ time: scrubTime });
+ editor.project.setTimelineViewState({
+ viewState: {
+ zoomLevel,
+ scrollLeft: tracksScrollRef.current?.scrollLeft ?? 0,
+ playheadTime: scrubTime,
+ },
+ });
+ }
+ setScrubTime(null);
+
+ if (isDraggingRuler) {
+ setIsDraggingRuler(false);
+ if (!hasDraggedRuler) {
+ handleScrub({ event });
+ }
+ setHasDraggedRuler(false);
+ }
+ };
+
+ const onMouseMove = (event: MouseEvent) => handleMouseMove({ event });
+ const onMouseUp = (event: MouseEvent) => handleMouseUp({ event });
+
+ window.addEventListener("mousemove", onMouseMove);
+ window.addEventListener("mouseup", onMouseUp);
+
+ return () => {
+ window.removeEventListener("mousemove", onMouseMove);
+ window.removeEventListener("mouseup", onMouseUp);
+ };
+ }, [
+ isScrubbing,
+ scrubTime,
+ seek,
+ handleScrub,
+ isDraggingRuler,
+ hasDraggedRuler,
+ editor,
+ tracksScrollRef,
+ zoomLevel,
+ ]);
+
+ useEffect(() => {
+ if (!isPlaying || isScrubbing) return;
+
+ const rulerViewport = rulerScrollRef.current;
+ const tracksViewport = tracksScrollRef.current;
+ if (!rulerViewport || !tracksViewport) return;
+
+ const playheadPixels =
+ playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
+ const viewportWidth = rulerViewport.clientWidth;
+ const scrollMinimum = 0;
+ const scrollMaximum = rulerViewport.scrollWidth - viewportWidth;
+
+ const needsScroll =
+ playheadPixels < rulerViewport.scrollLeft ||
+ playheadPixels > rulerViewport.scrollLeft + viewportWidth;
+
+ if (needsScroll) {
+ const desiredScroll = Math.max(
+ scrollMinimum,
+ Math.min(scrollMaximum, playheadPixels - viewportWidth / 2),
+ );
+ rulerViewport.scrollLeft = tracksViewport.scrollLeft = desiredScroll;
+ }
+ }, [
+ playheadPosition,
+ zoomLevel,
+ rulerScrollRef,
+ tracksScrollRef,
+ isScrubbing,
+ isPlaying,
+ ]);
+
+ return {
+ playheadPosition,
+ handlePlayheadMouseDown: handlePlayheadMouseDownEvent,
+ handleRulerMouseDown: handleRulerMouseDownEvent,
+ isDraggingRuler,
+ };
+}
diff --git a/apps/web/src/hooks/timeline/use-timeline-seek.ts b/apps/web/src/hooks/timeline/use-timeline-seek.ts
new file mode 100644
index 00000000..f5dc6ddb
--- /dev/null
+++ b/apps/web/src/hooks/timeline/use-timeline-seek.ts
@@ -0,0 +1,197 @@
+import { useCallback, useRef } from "react";
+import type { MutableRefObject, RefObject } from "react";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import { getSnappedSeekTime } from "@/lib/time";
+import { useEditor } from "../use-editor";
+
+interface UseTimelineSeekProps {
+ playheadRef: RefObject<HTMLDivElement>;
+ trackLabelsRef: RefObject<HTMLDivElement>;
+ rulerScrollRef: RefObject<HTMLDivElement>;
+ tracksScrollRef: RefObject<HTMLDivElement>;
+ zoomLevel: number;
+ duration: number;
+ isSelecting: boolean;
+ clearSelectedElements: () => void;
+ seek: (time: number) => void;
+}
+
+function resetMouseTracking({
+ mouseTrackingRef,
+}: {
+ mouseTrackingRef: MutableRefObject<{
+ isMouseDown: boolean;
+ downX: number;
+ downY: number;
+ downTime: number;
+ }>;
+}) {
+ mouseTrackingRef.current = {
+ isMouseDown: false,
+ downX: 0,
+ downY: 0,
+ downTime: 0,
+ };
+}
+
+function setMouseTracking({
+ mouseTrackingRef,
+ event,
+}: {
+ mouseTrackingRef: MutableRefObject<{
+ isMouseDown: boolean;
+ downX: number;
+ downY: number;
+ downTime: number;
+ }>;
+ event: React.MouseEvent;
+}) {
+ mouseTrackingRef.current = {
+ isMouseDown: true,
+ downX: event.clientX,
+ downY: event.clientY,
+ downTime: event.timeStamp,
+ };
+}
+
+export function useTimelineSeek({
+ playheadRef,
+ trackLabelsRef,
+ rulerScrollRef,
+ tracksScrollRef,
+ zoomLevel,
+ duration,
+ isSelecting,
+ clearSelectedElements,
+ seek,
+}: UseTimelineSeekProps) {
+ const editor = useEditor();
+ const activeProject = editor.project.getActive();
+
+ const mouseTrackingRef = useRef({
+ isMouseDown: false,
+ downX: 0,
+ downY: 0,
+ downTime: 0,
+ });
+
+ const handleTracksMouseDown = useCallback((event: React.MouseEvent) => {
+ if (event.button !== 0) return;
+ setMouseTracking({ mouseTrackingRef, event });
+ }, []);
+
+ const handleRulerMouseDown = useCallback((event: React.MouseEvent) => {
+ if (event.button !== 0) return;
+ setMouseTracking({ mouseTrackingRef, event });
+ }, []);
+
+ const shouldProcessTimelineClick = useCallback(
+ ({ event }: { event: React.MouseEvent }) => {
+ const target = event.target as HTMLElement;
+ const { isMouseDown, downX, downY, downTime } = mouseTrackingRef.current;
+ const deltaX = Math.abs(event.clientX - downX);
+ const deltaY = Math.abs(event.clientY - downY);
+ const deltaTime = event.timeStamp - downTime;
+ const isPlayhead = !!playheadRef.current?.contains(target);
+ const isTrackLabels = !!trackLabelsRef.current?.contains(target);
+ const shouldBlockForDrag = deltaX > 5 || deltaY > 5 || deltaTime > 500;
+
+ if (!isMouseDown) return false;
+ if (shouldBlockForDrag) return false;
+ if (isSelecting) return false;
+ if (isPlayhead) return false;
+ if (isTrackLabels) {
+ clearSelectedElements();
+ return false;
+ }
+
+ return true;
+ },
+ [isSelecting, clearSelectedElements, playheadRef, trackLabelsRef],
+ );
+
+ const handleTimelineSeek = useCallback(
+ ({
+ event,
+ source,
+ }: {
+ event: React.MouseEvent;
+ source: "ruler" | "tracks";
+ }) => {
+ const scrollContainer =
+ source === "ruler" ? rulerScrollRef.current : tracksScrollRef.current;
+
+ if (!scrollContainer) return;
+
+ const rect = scrollContainer.getBoundingClientRect();
+ const mouseX = event.clientX - rect.left;
+ const scrollLeft = scrollContainer.scrollLeft;
+
+ const rawTime = Math.max(
+ 0,
+ Math.min(
+ duration,
+ (mouseX + scrollLeft) /
+ (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel),
+ ),
+ );
+
+ const projectFps = activeProject?.settings.fps || 30;
+ const time = getSnappedSeekTime({
+ rawTime,
+ duration,
+ fps: projectFps,
+ });
+ seek(time);
+ editor.project.setTimelineViewState({
+ viewState: {
+ zoomLevel,
+ scrollLeft: scrollContainer.scrollLeft,
+ playheadTime: time,
+ },
+ });
+ },
+ [
+ duration,
+ zoomLevel,
+ rulerScrollRef,
+ tracksScrollRef,
+ seek,
+ editor,
+ activeProject?.settings.fps,
+ ],
+ );
+
+ const handleTracksClick = useCallback(
+ (event: React.MouseEvent) => {
+ const shouldProcess = shouldProcessTimelineClick({ event });
+ resetMouseTracking({ mouseTrackingRef });
+
+ if (shouldProcess) {
+ clearSelectedElements();
+ handleTimelineSeek({ event, source: "tracks" });
+ }
+ },
+ [shouldProcessTimelineClick, handleTimelineSeek, clearSelectedElements],
+ );
+
+ const handleRulerClick = useCallback(
+ (event: React.MouseEvent) => {
+ const shouldProcess = shouldProcessTimelineClick({ event });
+ resetMouseTracking({ mouseTrackingRef });
+
+ if (shouldProcess) {
+ clearSelectedElements();
+ handleTimelineSeek({ event, source: "ruler" });
+ }
+ },
+ [shouldProcessTimelineClick, handleTimelineSeek, clearSelectedElements],
+ );
+
+ return {
+ handleTracksMouseDown,
+ handleTracksClick,
+ handleRulerMouseDown,
+ handleRulerClick,
+ };
+}
diff --git a/apps/web/src/hooks/timeline/use-timeline-snapping.ts b/apps/web/src/hooks/timeline/use-timeline-snapping.ts
new file mode 100644
index 00000000..4cd6f775
--- /dev/null
+++ b/apps/web/src/hooks/timeline/use-timeline-snapping.ts
@@ -0,0 +1,159 @@
+import { useCallback } from "react";
+import type { TimelineTrack } from "@/types/timeline";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+
+export interface SnapPoint {
+ time: number;
+ type: "element-start" | "element-end" | "playhead";
+ elementId?: string;
+ trackId?: string;
+}
+
+export interface SnapResult {
+ snappedTime: number;
+ snapPoint: SnapPoint | null;
+ snapDistance: number;
+}
+
+export interface UseTimelineSnappingOptions {
+ snapThreshold?: number;
+ enableElementSnapping?: boolean;
+ enablePlayheadSnapping?: boolean;
+}
+
+export function useTimelineSnapping({
+ snapThreshold = 10,
+ enableElementSnapping = true,
+ enablePlayheadSnapping = true,
+}: UseTimelineSnappingOptions = {}) {
+ const findSnapPoints = useCallback(
+ ({
+ tracks,
+ playheadTime,
+ excludeElementId,
+ }: {
+ tracks: Array<TimelineTrack>;
+ playheadTime: number;
+ excludeElementId?: string;
+ }): SnapPoint[] => {
+ const snapPoints: SnapPoint[] = [];
+
+ if (enableElementSnapping) {
+ for (const track of tracks) {
+ for (const element of track.elements) {
+ if (element.id === excludeElementId) continue;
+
+ const elementStart = element.startTime;
+ const elementEnd = element.startTime + element.duration;
+
+ snapPoints.push(
+ {
+ time: elementStart,
+ type: "element-start",
+ elementId: element.id,
+ trackId: track.id,
+ },
+ {
+ time: elementEnd,
+ type: "element-end",
+ elementId: element.id,
+ trackId: track.id,
+ },
+ );
+ }
+ }
+ }
+
+ if (enablePlayheadSnapping) {
+ snapPoints.push({
+ time: playheadTime,
+ type: "playhead",
+ });
+ }
+
+ return snapPoints;
+ },
+ [enableElementSnapping, enablePlayheadSnapping],
+ );
+
+ const snapToNearestPoint = useCallback(
+ ({
+ targetTime,
+ snapPoints,
+ zoomLevel,
+ }: {
+ targetTime: number;
+ snapPoints: Array<SnapPoint>;
+ zoomLevel: number;
+ }): SnapResult => {
+ const pixelsPerSecond = TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
+ const thresholdInSeconds = snapThreshold / pixelsPerSecond;
+
+ let closestSnapPoint: SnapPoint | null = null;
+ let closestDistance = Infinity;
+
+ for (const snapPoint of snapPoints) {
+ const distance = Math.abs(targetTime - snapPoint.time);
+ if (distance < thresholdInSeconds && distance < closestDistance) {
+ closestDistance = distance;
+ closestSnapPoint = snapPoint;
+ }
+ }
+
+ return {
+ snappedTime: closestSnapPoint ? closestSnapPoint.time : targetTime,
+ snapPoint: closestSnapPoint,
+ snapDistance: closestDistance,
+ };
+ },
+ [snapThreshold],
+ );
+
+ const snapElementEdge = useCallback(
+ ({
+ targetTime,
+ elementDuration,
+ tracks,
+ playheadTime,
+ zoomLevel,
+ excludeElementId,
+ snapToStart = true,
+ }: {
+ targetTime: number;
+ elementDuration: number;
+ tracks: Array<TimelineTrack>;
+ playheadTime: number;
+ zoomLevel: number;
+ excludeElementId?: string;
+ snapToStart?: boolean;
+ }): SnapResult => {
+ const snapPoints = findSnapPoints({
+ tracks,
+ playheadTime,
+ excludeElementId,
+ });
+
+ const effectiveTargetTime = snapToStart
+ ? targetTime
+ : targetTime + elementDuration;
+ const snapResult = snapToNearestPoint({
+ targetTime: effectiveTargetTime,
+ snapPoints,
+ zoomLevel,
+ });
+
+ if (!snapToStart && snapResult.snapPoint) {
+ snapResult.snappedTime = snapResult.snappedTime - elementDuration;
+ }
+
+ return snapResult;
+ },
+ [findSnapPoints, snapToNearestPoint],
+ );
+
+ return {
+ snapElementEdge,
+ findSnapPoints,
+ snapToNearestPoint,
+ };
+}
diff --git a/apps/web/src/hooks/timeline/use-timeline-zoom.ts b/apps/web/src/hooks/timeline/use-timeline-zoom.ts
new file mode 100644
index 00000000..96a4be43
--- /dev/null
+++ b/apps/web/src/hooks/timeline/use-timeline-zoom.ts
@@ -0,0 +1,224 @@
+import {
+ type WheelEvent as ReactWheelEvent,
+ type RefObject,
+ useCallback,
+ useEffect,
+ useLayoutEffect,
+ useRef,
+ useState,
+} from "react";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import { useEditor } from "@/hooks/use-editor";
+
+interface UseTimelineZoomProps {
+ containerRef: RefObject<HTMLDivElement>;
+ minZoom?: number;
+ initialZoom?: number;
+ initialScrollLeft?: number;
+ initialPlayheadTime?: number;
+ tracksScrollRef: RefObject<HTMLDivElement>;
+ rulerScrollRef: RefObject<HTMLDivElement>;
+}
+
+interface UseTimelineZoomReturn {
+ zoomLevel: number;
+ setZoomLevel: (zoomLevel: number | ((prev: number) => number)) => void;
+ handleWheel: (event: ReactWheelEvent) => void;
+ saveScrollPosition: () => void;
+}
+
+export function useTimelineZoom({
+ containerRef,
+ minZoom = TIMELINE_CONSTANTS.ZOOM_MIN,
+ initialZoom,
+ initialScrollLeft,
+ initialPlayheadTime,
+ tracksScrollRef,
+ rulerScrollRef,
+}: UseTimelineZoomProps): UseTimelineZoomReturn {
+ const editor = useEditor();
+ const hasInitializedRef = useRef(false);
+ const hasRestoredPlayheadRef = useRef(false);
+ const scrollSaveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
+ null,
+ );
+
+ const [zoomLevel, setZoomLevel] = useState(() => {
+ if (initialZoom !== undefined) {
+ hasInitializedRef.current = true;
+ return Math.max(
+ minZoom,
+ Math.min(TIMELINE_CONSTANTS.ZOOM_MAX, initialZoom),
+ );
+ }
+ return minZoom;
+ });
+ const previousZoomRef = useRef(zoomLevel);
+ const hasRestoredScrollRef = useRef(false);
+
+ const handleWheel = useCallback(
+ (event: ReactWheelEvent) => {
+ const isZoomGesture = event.ctrlKey || event.metaKey;
+ const isHorizontalScrollGesture =
+ event.shiftKey || Math.abs(event.deltaX) > Math.abs(event.deltaY);
+
+ if (isHorizontalScrollGesture) {
+ return;
+ }
+
+ // pinch-zoom (ctrl/meta + wheel)
+ if (isZoomGesture) {
+ const zoomMultiplier = event.deltaY > 0 ? 1 / 1.1 : 1.1;
+ setZoomLevel((prev) => {
+ const nextZoom = Math.max(
+ minZoom,
+ Math.min(TIMELINE_CONSTANTS.ZOOM_MAX, prev * zoomMultiplier),
+ );
+ return nextZoom;
+ });
+ // for horizontal scrolling (when shift is held or horizontal wheel movement),
+ // let the event bubble up to allow ScrollArea to handle it
+ return;
+ }
+ },
+ [minZoom],
+ );
+
+ useEffect(() => {
+ if (initialZoom !== undefined && !hasInitializedRef.current) {
+ hasInitializedRef.current = true;
+ setZoomLevel(
+ Math.max(minZoom, Math.min(TIMELINE_CONSTANTS.ZOOM_MAX, initialZoom)),
+ );
+ return;
+ }
+ setZoomLevel((prev) => {
+ if (prev < minZoom) {
+ return minZoom;
+ }
+ return prev;
+ });
+ }, [minZoom, initialZoom]);
+
+ const wrappedSetZoomLevel = useCallback(
+ (zoomLevelOrUpdater: number | ((prev: number) => number)) => {
+ setZoomLevel((prev) => {
+ const nextZoom =
+ typeof zoomLevelOrUpdater === "function"
+ ? zoomLevelOrUpdater(prev)
+ : zoomLevelOrUpdater;
+ const clampedZoom = Math.max(
+ minZoom,
+ Math.min(TIMELINE_CONSTANTS.ZOOM_MAX, nextZoom),
+ );
+ return clampedZoom;
+ });
+ },
+ [minZoom],
+ );
+
+ useLayoutEffect(() => {
+ const previousZoom = previousZoomRef.current;
+ if (previousZoom === zoomLevel) return;
+
+ const scrollElement = tracksScrollRef.current;
+ if (scrollElement) {
+ editor.project.setTimelineViewState({
+ viewState: {
+ zoomLevel,
+ scrollLeft: scrollElement.scrollLeft,
+ playheadTime: editor.playback.getCurrentTime(),
+ },
+ });
+ }
+
+ previousZoomRef.current = zoomLevel;
+ }, [zoomLevel, editor, tracksScrollRef]);
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies: tracksScrollRef is a stable ref
+ const saveScrollPosition = useCallback(() => {
+ if (scrollSaveTimeoutRef.current) {
+ clearTimeout(scrollSaveTimeoutRef.current);
+ }
+ scrollSaveTimeoutRef.current = setTimeout(() => {
+ const scrollElement = tracksScrollRef.current;
+ if (scrollElement) {
+ editor.project.setTimelineViewState({
+ viewState: {
+ zoomLevel,
+ scrollLeft: scrollElement.scrollLeft,
+ playheadTime: editor.playback.getCurrentTime(),
+ },
+ });
+ }
+ }, 300);
+ }, [zoomLevel, editor]);
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies: refs are stable
+ useEffect(() => {
+ if (initialScrollLeft === undefined) return;
+ if (hasRestoredScrollRef.current) return;
+ const scrollElement = tracksScrollRef.current;
+ if (!scrollElement) return;
+
+ const restoreScroll = () => {
+ scrollElement.scrollLeft = initialScrollLeft;
+ if (rulerScrollRef.current) {
+ rulerScrollRef.current.scrollLeft = initialScrollLeft;
+ }
+ hasRestoredScrollRef.current = true;
+ };
+
+ if (scrollElement.scrollWidth > 0) {
+ restoreScroll();
+ } else {
+ const observer = new ResizeObserver(() => {
+ if (scrollElement.scrollWidth > 0) {
+ restoreScroll();
+ hasRestoredScrollRef.current = true;
+ observer.disconnect();
+ }
+ });
+ observer.observe(scrollElement);
+ return () => observer.disconnect();
+ }
+ }, [initialScrollLeft]);
+
+ useEffect(() => {
+ if (initialPlayheadTime !== undefined && !hasRestoredPlayheadRef.current) {
+ hasRestoredPlayheadRef.current = true;
+ editor.playback.seek({ time: initialPlayheadTime });
+ }
+ }, [initialPlayheadTime, editor]);
+
+ // prevent browser zoom in the timeline
+ useEffect(() => {
+ const preventZoom = (event: WheelEvent) => {
+ const isZoomKeyPressed = event.ctrlKey || event.metaKey;
+ const isInContainer = containerRef.current?.contains(
+ event.target as Node,
+ );
+ // only check isInContainer, not isInTimeline state - the state check
+ // causes race conditions where the closure captures stale state
+ if (isZoomKeyPressed && isInContainer) {
+ event.preventDefault();
+ }
+ };
+
+ document.addEventListener("wheel", preventZoom, {
+ passive: false,
+ capture: true,
+ });
+
+ return () => {
+ document.removeEventListener("wheel", preventZoom, { capture: true });
+ };
+ }, [containerRef]);
+
+ return {
+ zoomLevel,
+ setZoomLevel: wrappedSetZoomLevel,
+ handleWheel,
+ saveScrollPosition,
+ };
+}
diff --git a/apps/web/src/hooks/use-aspect-ratio.ts b/apps/web/src/hooks/use-aspect-ratio.ts
deleted file mode 100644
index b721b74e..00000000
--- a/apps/web/src/hooks/use-aspect-ratio.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { useEditorStore } from "@/stores/editor-store";
-import { useMediaStore, getMediaAspectRatio } from "@/stores/media-store";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { DEFAULT_CANVAS_SIZE, useProjectStore } from "@/stores/project-store";
-
-export function useAspectRatio() {
- const { canvasPresets } = useEditorStore();
- const { activeProject } = useProjectStore();
- const { mediaFiles } = useMediaStore();
- const { tracks } = useTimelineStore();
-
- const canvasSize = activeProject?.canvasSize || DEFAULT_CANVAS_SIZE;
- const canvasMode = activeProject?.canvasMode || "preset";
-
- // Find the current preset based on canvas size
- const currentPreset = canvasPresets.find(
- (preset) =>
- preset.width === canvasSize.width && preset.height === canvasSize.height
- );
-
- // Get the original aspect ratio from the first video/image in timeline
- const getOriginalAspectRatio = (): number => {
- // Find first video or image in timeline
- for (const track of tracks) {
- for (const element of track.elements) {
- if (element.type === "media") {
- const mediaFile = mediaFiles.find(
- (file) => file.id === element.mediaId
- );
- if (
- mediaFile &&
- (mediaFile.type === "video" || mediaFile.type === "image")
- ) {
- return getMediaAspectRatio(mediaFile);
- }
- }
- }
- }
- return 16 / 9; // Default aspect ratio
- };
-
- // Get current aspect ratio
- const getCurrentAspectRatio = (): number => {
- return canvasSize.width / canvasSize.height;
- };
-
- // Format aspect ratio as a readable string
- const formatAspectRatio = (aspectRatio: number): string => {
- // Check if it matches a common aspect ratio
- const ratios = [
- { ratio: 16 / 9, label: "16:9" },
- { ratio: 9 / 16, label: "9:16" },
- { ratio: 1, label: "1:1" },
- { ratio: 4 / 3, label: "4:3" },
- { ratio: 3 / 4, label: "3:4" },
- { ratio: 21 / 9, label: "21:9" },
- ];
-
- for (const { ratio, label } of ratios) {
- if (Math.abs(aspectRatio - ratio) < 0.01) {
- return label;
- }
- }
-
- // If not a common ratio, format as decimal
- return aspectRatio.toFixed(2);
- };
-
- // Check if current mode is "Original"
- const isOriginal = canvasMode === "original";
-
- // Get display name for current aspect ratio
- const getDisplayName = (): string => {
- // If explicitly set to original mode, always show "Original"
- if (canvasMode === "original") {
- return "Original";
- }
-
- if (currentPreset) {
- return currentPreset.name;
- }
-
- return formatAspectRatio(getCurrentAspectRatio());
- };
-
- return {
- currentPreset,
- canvasMode,
- isOriginal,
- getCurrentAspectRatio,
- getOriginalAspectRatio,
- formatAspectRatio,
- getDisplayName,
- canvasSize,
- canvasPresets,
- };
-}
diff --git a/apps/web/src/hooks/use-drag-drop.ts b/apps/web/src/hooks/use-drag-drop.ts
deleted file mode 100644
index 0b1d4008..00000000
--- a/apps/web/src/hooks/use-drag-drop.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { useState, useRef } from "react";
-
-interface UseDragDropOptions {
- onDrop?: (files: FileList) => void;
-}
-
-// Helper function to check if drag contains files from external sources (not internal app drags)
-const containsFiles = (dataTransfer: DataTransfer): boolean => {
- // Check if this is an internal app drag (media item)
- if (dataTransfer.types.includes("application/x-media-item")) {
- return false;
- }
-
- // Only show overlay for external file drags
- return dataTransfer.types.includes("Files");
-};
-
-export function useDragDrop(options: UseDragDropOptions = {}) {
- const [isDragOver, setIsDragOver] = useState(false);
- const dragCounterRef = useRef(0);
-
- const handleDragEnter = (e: React.DragEvent) => {
- e.preventDefault();
-
- // Only handle external file drags, not internal app element drags
- if (!containsFiles(e.dataTransfer)) {
- return;
- }
-
- dragCounterRef.current += 1;
- if (!isDragOver) {
- setIsDragOver(true);
- }
- };
-
- const handleDragOver = (e: React.DragEvent) => {
- e.preventDefault();
-
- // Only handle file drags
- if (!containsFiles(e.dataTransfer)) {
- return;
- }
- };
-
- const handleDragLeave = (e: React.DragEvent) => {
- e.preventDefault();
-
- // Only handle file drags
- if (!containsFiles(e.dataTransfer)) {
- return;
- }
-
- dragCounterRef.current -= 1;
- if (dragCounterRef.current === 0) {
- setIsDragOver(false);
- }
- };
-
- const handleDrop = (e: React.DragEvent) => {
- e.preventDefault();
- setIsDragOver(false);
- dragCounterRef.current = 0;
-
- // Only handle file drops
- if (
- options.onDrop &&
- e.dataTransfer.files &&
- containsFiles(e.dataTransfer)
- ) {
- options.onDrop(e.dataTransfer.files);
- }
- };
-
- const dragProps = {
- onDragEnter: handleDragEnter,
- onDragOver: handleDragOver,
- onDragLeave: handleDragLeave,
- onDrop: handleDrop,
- };
-
- return {
- isDragOver,
- dragProps,
- };
-}
diff --git a/apps/web/src/hooks/use-edge-auto-scroll.ts b/apps/web/src/hooks/use-edge-auto-scroll.ts
deleted file mode 100644
index d3cf8127..00000000
--- a/apps/web/src/hooks/use-edge-auto-scroll.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import { useEffect, useRef } from "react";
-
-interface UseEdgeAutoScrollParams {
- isActive: boolean;
- getMouseClientX: () => number;
- rulerScrollRef: React.RefObject<HTMLDivElement>;
- tracksScrollRef: React.RefObject<HTMLDivElement>;
- contentWidth: number;
- edgeThreshold?: number;
- maxScrollSpeed?: number;
-}
-
-// Provides smooth edge auto-scrolling for horizontal timeline interactions.
-export function useEdgeAutoScroll({
- isActive,
- getMouseClientX,
- rulerScrollRef,
- tracksScrollRef,
- contentWidth,
- edgeThreshold = 100,
- maxScrollSpeed = 15,
-}: UseEdgeAutoScrollParams): void {
- const rafRef = useRef<number | null>(null);
-
- useEffect(() => {
- if (!isActive) {
- if (rafRef.current) {
- cancelAnimationFrame(rafRef.current);
- rafRef.current = null;
- }
- return;
- }
-
- const step = () => {
- const rulerViewport = rulerScrollRef.current;
- const tracksViewport = tracksScrollRef.current;
- if (!rulerViewport || !tracksViewport) {
- rafRef.current = requestAnimationFrame(step);
- return;
- }
-
- const viewportRect = rulerViewport.getBoundingClientRect();
- const mouseX = getMouseClientX();
- const mouseXRelative = mouseX - viewportRect.left;
-
- const viewportWidth = rulerViewport.clientWidth;
- const intrinsicContentWidth = rulerViewport.scrollWidth;
- const effectiveContentWidth = Math.max(
- contentWidth,
- intrinsicContentWidth
- );
- const scrollMax = Math.max(0, effectiveContentWidth - viewportWidth);
-
- let scrollSpeed = 0;
-
- if (mouseXRelative < edgeThreshold && rulerViewport.scrollLeft > 0) {
- const edgeDistance = Math.max(0, mouseXRelative);
- const intensity = 1 - edgeDistance / edgeThreshold;
- scrollSpeed = -maxScrollSpeed * intensity;
- } else if (
- mouseXRelative > viewportWidth - edgeThreshold &&
- rulerViewport.scrollLeft < scrollMax
- ) {
- const edgeDistance = Math.max(
- 0,
- viewportWidth - edgeThreshold - mouseXRelative
- );
- const intensity = 1 - edgeDistance / edgeThreshold;
- scrollSpeed = maxScrollSpeed * intensity;
- }
-
- if (scrollSpeed !== 0) {
- const newScrollLeft = Math.max(
- 0,
- Math.min(scrollMax, rulerViewport.scrollLeft + scrollSpeed)
- );
- rulerViewport.scrollLeft = newScrollLeft;
- tracksViewport.scrollLeft = newScrollLeft;
- }
-
- rafRef.current = requestAnimationFrame(step);
- };
-
- rafRef.current = requestAnimationFrame(step);
-
- return () => {
- if (rafRef.current) {
- cancelAnimationFrame(rafRef.current);
- rafRef.current = null;
- }
- };
- }, [
- isActive,
- getMouseClientX,
- rulerScrollRef,
- tracksScrollRef,
- contentWidth,
- edgeThreshold,
- maxScrollSpeed,
- ]);
-}
diff --git a/apps/web/src/hooks/use-editor-actions.ts b/apps/web/src/hooks/use-editor-actions.ts
deleted file mode 100644
index 9287d2e0..00000000
--- a/apps/web/src/hooks/use-editor-actions.ts
+++ /dev/null
@@ -1,241 +0,0 @@
-"use client";
-
-import { useEffect } from "react";
-import { useActionHandler } from "@/constants/actions";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { usePlaybackStore } from "@/stores/playback-store";
-import { DEFAULT_FPS, useProjectStore } from "@/stores/project-store";
-import { toast } from "sonner";
-
-export function useEditorActions() {
- const {
- tracks,
- selectedElements,
- clearSelectedElements,
- setSelectedElements,
- deleteSelected,
- splitSelected,
- addElementToTrack,
- snappingEnabled,
- toggleSnapping,
- undo,
- redo,
- } = useTimelineStore();
-
- const { currentTime, duration, isPlaying, toggle, seek } = usePlaybackStore();
- const { activeProject } = useProjectStore();
-
- // Playback actions
- useActionHandler(
- "toggle-play",
- () => {
- toggle();
- },
- undefined
- );
-
- useActionHandler(
- "stop-playback",
- () => {
- if (isPlaying) {
- toggle();
- }
- seek(0);
- },
- undefined
- );
-
- useActionHandler(
- "seek-forward",
- (args) => {
- const seconds = args?.seconds ?? 1;
- seek(Math.min(duration, currentTime + seconds));
- },
- undefined
- );
-
- useActionHandler(
- "seek-backward",
- (args) => {
- const seconds = args?.seconds ?? 1;
- seek(Math.max(0, currentTime - seconds));
- },
- undefined
- );
-
- useActionHandler(
- "frame-step-forward",
- () => {
- const projectFps = activeProject?.fps || DEFAULT_FPS;
- seek(Math.min(duration, currentTime + 1 / projectFps));
- },
- undefined
- );
-
- useActionHandler(
- "frame-step-backward",
- () => {
- const projectFps = activeProject?.fps || DEFAULT_FPS;
- seek(Math.max(0, currentTime - 1 / projectFps));
- },
- undefined
- );
-
- useActionHandler(
- "jump-forward",
- (args) => {
- const seconds = args?.seconds ?? 5;
- seek(Math.min(duration, currentTime + seconds));
- },
- undefined
- );
-
- useActionHandler(
- "jump-backward",
- (args) => {
- const seconds = args?.seconds ?? 5;
- seek(Math.max(0, currentTime - seconds));
- },
- undefined
- );
-
- useActionHandler(
- "goto-start",
- () => {
- seek(0);
- },
- undefined
- );
-
- useActionHandler(
- "goto-end",
- () => {
- seek(duration);
- },
- undefined
- );
-
- // Timeline editing actions
- useActionHandler(
- "split-element",
- () => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one element to split");
- return;
- }
-
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t: any) => t.id === trackId);
- const element = track?.elements.find((el: any) => el.id === elementId);
-
- if (element) {
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
-
- if (currentTime > effectiveStart && currentTime < effectiveEnd) {
- splitSelected(currentTime, trackId, elementId);
- } else {
- toast.error("Playhead must be within selected element");
- }
- }
- },
- undefined
- );
-
- useActionHandler(
- "delete-selected",
- () => {
- if (selectedElements.length === 0) {
- return;
- }
- deleteSelected();
- },
- undefined
- );
-
- useActionHandler(
- "select-all",
- () => {
- const allElements = tracks.flatMap((track: any) =>
- track.elements.map((element: any) => ({
- trackId: track.id,
- elementId: element.id,
- }))
- );
- setSelectedElements(allElements);
- },
- undefined
- );
-
- useActionHandler(
- "duplicate-selected",
- () => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one element to duplicate");
- return;
- }
-
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t: any) => t.id === trackId);
- const element = track?.elements.find((el: any) => el.id === elementId);
-
- if (element) {
- const newStartTime =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd) +
- 0.1;
- const { id, ...elementWithoutId } = element;
-
- addElementToTrack(trackId, {
- ...elementWithoutId,
- startTime: newStartTime,
- });
- }
- },
- undefined
- );
-
- useActionHandler(
- "copy-selected",
- () => {
- if (selectedElements.length === 0) return;
- useTimelineStore.getState().copySelected();
- },
- undefined
- );
-
- useActionHandler(
- "paste-selected",
- () => {
- useTimelineStore.getState().pasteAtTime(currentTime);
- },
- undefined
- );
-
- useActionHandler(
- "toggle-snapping",
- () => {
- toggleSnapping();
- },
- undefined
- );
-
- // History actions
- useActionHandler(
- "undo",
- () => {
- undo();
- },
- undefined
- );
-
- useActionHandler(
- "redo",
- () => {
- redo();
- },
- undefined
- );
-}
diff --git a/apps/web/src/hooks/use-editor.ts b/apps/web/src/hooks/use-editor.ts
new file mode 100644
index 00000000..ff3e2db1
--- /dev/null
+++ b/apps/web/src/hooks/use-editor.ts
@@ -0,0 +1,39 @@
+import { useCallback, useMemo, useRef, useSyncExternalStore } from "react";
+import { EditorCore } from "@/core";
+
+export function useEditor(): EditorCore {
+ const editor = useMemo(() => EditorCore.getInstance(), []);
+ const versionRef = useRef(0);
+
+ const subscribe = useCallback(
+ (onStoreChange: () => void) => {
+ const handleStoreChange = () => {
+ versionRef.current += 1;
+ onStoreChange();
+ };
+
+ const unsubscribers = [
+ editor.playback.subscribe(handleStoreChange),
+ editor.timeline.subscribe(handleStoreChange),
+ editor.scenes.subscribe(handleStoreChange),
+ editor.project.subscribe(handleStoreChange),
+ editor.media.subscribe(handleStoreChange),
+ editor.renderer.subscribe(handleStoreChange),
+ editor.selection.subscribe(handleStoreChange),
+ ];
+
+ return () => {
+ for (const unsubscribe of unsubscribers) {
+ unsubscribe();
+ }
+ };
+ },
+ [editor],
+ );
+
+ const getSnapshot = useCallback(() => versionRef.current, []);
+
+ useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
+
+ return editor;
+}
diff --git a/apps/web/src/hooks/use-file-upload.ts b/apps/web/src/hooks/use-file-upload.ts
new file mode 100644
index 00000000..19badd98
--- /dev/null
+++ b/apps/web/src/hooks/use-file-upload.ts
@@ -0,0 +1,103 @@
+import { useState, useRef } from "react";
+import { hasDragData } from "@/lib/drag-data";
+
+interface UseFileUploadOptions {
+ accept?: string;
+ multiple?: boolean;
+ onFilesSelected?: (files: FileList) => void;
+}
+
+function containsFiles(dataTransfer: DataTransfer): boolean {
+ return !hasDragData({ dataTransfer }) && dataTransfer.types.includes("Files");
+}
+
+export function useFileUpload({
+ accept,
+ multiple,
+ onFilesSelected,
+}: UseFileUploadOptions = {}) {
+ const [isDragOver, setIsDragOver] = useState(false);
+ const dragCounterRef = useRef(0);
+ const inputRef = useRef<HTMLInputElement>(null);
+
+ function openFilePicker() {
+ if (!inputRef.current) return;
+
+ inputRef.current.accept = accept || "*";
+ inputRef.current.multiple = multiple || false;
+ inputRef.current.click();
+ }
+
+ function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
+ const files = event.target.files;
+ if (files && files.length > 0 && onFilesSelected) {
+ onFilesSelected(files);
+ }
+
+ if (event.target) {
+ event.target.value = "";
+ }
+ }
+
+ function handleDragEnter(e: React.DragEvent) {
+ e.preventDefault();
+
+ if (!containsFiles(e.dataTransfer)) return;
+
+ dragCounterRef.current += 1;
+ setIsDragOver(true);
+ }
+
+ function handleDragOver(e: React.DragEvent) {
+ e.preventDefault();
+
+ if (!containsFiles(e.dataTransfer)) return;
+ }
+
+ function handleDragLeave(e: React.DragEvent) {
+ e.preventDefault();
+
+ if (!containsFiles(e.dataTransfer)) return;
+
+ dragCounterRef.current -= 1;
+ if (dragCounterRef.current === 0) {
+ setIsDragOver(false);
+ }
+ }
+
+ function handleDrop(e: React.DragEvent) {
+ e.preventDefault();
+ setIsDragOver(false);
+ dragCounterRef.current = 0;
+
+ if (onFilesSelected && containsFiles(e.dataTransfer)) {
+ const files = e.dataTransfer.files;
+ const shouldUseMultiple = multiple ?? false;
+
+ if (shouldUseMultiple) {
+ onFilesSelected(files);
+ } else if (files.length > 0) {
+ const dataTransfer = new DataTransfer();
+ dataTransfer.items.add(files[0]);
+ onFilesSelected(dataTransfer.files);
+ }
+ }
+ }
+
+ return {
+ isDragOver,
+ openFilePicker,
+ fileInputProps: {
+ ref: inputRef,
+ type: "file",
+ style: { display: "none" },
+ onChange: handleFileChange,
+ },
+ dragProps: {
+ onDragEnter: handleDragEnter,
+ onDragOver: handleDragOver,
+ onDragLeave: handleDragLeave,
+ onDrop: handleDrop,
+ },
+ };
+}
diff --git a/apps/web/src/hooks/use-frame-cache.ts b/apps/web/src/hooks/use-frame-cache.ts
deleted file mode 100644
index cce187f1..00000000
--- a/apps/web/src/hooks/use-frame-cache.ts
+++ /dev/null
@@ -1,348 +0,0 @@
-import { useRef, useCallback } from "react";
-import {
- TimelineTrack,
- TimelineElement,
- MediaElement,
- TextElement,
-} from "@/types/timeline";
-import { MediaFile } from "@/types/media";
-import { TProject } from "@/types/project";
-
-interface CachedFrame {
- imageData: ImageData;
- timelineHash: string;
- timestamp: number;
-}
-
-interface FrameCacheOptions {
- maxCacheSize?: number; // Maximum number of cached frames
- cacheResolution?: number; // Frames per second to cache at
-}
-
-// Shared singleton cache across hook instances (HMR-safe)
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const __frameCacheGlobal: any = globalThis as any;
-const __sharedFrameCache: Map<number, CachedFrame> =
- __frameCacheGlobal.__sharedFrameCache ?? new Map<number, CachedFrame>();
-__frameCacheGlobal.__sharedFrameCache = __sharedFrameCache;
-
-export function useFrameCache(options: FrameCacheOptions = {}) {
- const { maxCacheSize = 300, cacheResolution = 30 } = options; // 10 seconds at 30fps
-
- const frameCacheRef = useRef(__sharedFrameCache);
-
- // Generate a hash of the timeline state that affects rendering
- const getTimelineHash = useCallback(
- (
- time: number,
- tracks: TimelineTrack[],
- mediaFiles: MediaFile[],
- activeProject: TProject | null,
- sceneId?: string
- ): string => {
- // Get elements that are active at this time
- const activeElements: Array<{
- id: string;
- type: string;
- startTime: number;
- duration: number;
- trimStart: number;
- trimEnd: number;
- mediaId?: string;
- // Text-specific properties
- content?: string;
- fontSize?: number;
- fontFamily?: string;
- color?: string;
- backgroundColor?: string;
- x?: number;
- y?: number;
- rotation?: number;
- opacity?: number;
- }> = [];
-
- for (const track of tracks) {
- if (track.muted) continue;
-
- for (const element of track.elements) {
- // Check if element has hidden property (some elements might not have it)
- const isHidden = "hidden" in element ? element.hidden : false;
- if (isHidden) continue;
-
- const elementStart = element.startTime;
- const elementEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
-
- if (time >= elementStart && time < elementEnd) {
- if (element.type === "media") {
- const mediaElement = element as MediaElement;
- activeElements.push({
- id: element.id,
- type: element.type,
- startTime: element.startTime,
- duration: element.duration,
- trimStart: element.trimStart,
- trimEnd: element.trimEnd,
- mediaId: mediaElement.mediaId,
- });
- } else if (element.type === "text") {
- const textElement = element as TextElement;
- activeElements.push({
- id: element.id,
- type: element.type,
- startTime: element.startTime,
- duration: element.duration,
- trimStart: element.trimStart,
- trimEnd: element.trimEnd,
- content: textElement.content,
- fontSize: textElement.fontSize,
- fontFamily: textElement.fontFamily,
- color: textElement.color,
- backgroundColor: textElement.backgroundColor,
- x: textElement.x,
- y: textElement.y,
- rotation: textElement.rotation,
- opacity: textElement.opacity,
- });
- }
- }
- }
- }
-
- // Include project settings that affect rendering
- const projectState = {
- backgroundColor: activeProject?.backgroundColor,
- backgroundType: activeProject?.backgroundType,
- blurIntensity: activeProject?.blurIntensity,
- canvasSize: activeProject?.canvasSize,
- };
-
- const hash = {
- activeElements,
- projectState,
- sceneId,
- time: Math.floor(time * cacheResolution) / cacheResolution,
- };
- return JSON.stringify(hash);
- },
- [cacheResolution]
- );
-
- // Check if a frame is cached and valid
- const isFrameCached = useCallback(
- (
- time: number,
- tracks: TimelineTrack[],
- mediaFiles: MediaFile[],
- activeProject: TProject | null,
- sceneId?: string
- ): boolean => {
- const frameKey = Math.floor(time * cacheResolution);
- const cached = frameCacheRef.current.get(frameKey);
-
- if (!cached) return false;
-
- const currentHash = getTimelineHash(
- time,
- tracks,
- mediaFiles,
- activeProject,
- sceneId
- );
- return cached.timelineHash === currentHash;
- },
- [getTimelineHash, cacheResolution]
- );
-
- // Get cached frame if available and valid
- const getCachedFrame = useCallback(
- (
- time: number,
- tracks: TimelineTrack[],
- mediaFiles: MediaFile[],
- activeProject: TProject | null,
- sceneId?: string
- ): ImageData | null => {
- const frameKey = Math.floor(time * cacheResolution);
- const cached = frameCacheRef.current.get(frameKey);
-
- if (!cached) {
- return null;
- }
-
- const currentHash = getTimelineHash(
- time,
- tracks,
- mediaFiles,
- activeProject,
- sceneId
- );
- console.log(cached.timelineHash === currentHash);
- if (cached.timelineHash !== currentHash) {
- // Cache is stale, remove it
- console.log(
- "Cache miss - hash mismatch:",
- JSON.stringify({
- cachedHash: cached.timelineHash.slice(0, 100),
- currentHash: currentHash.slice(0, 100),
- })
- );
- frameCacheRef.current.delete(frameKey);
- return null;
- }
-
- return cached.imageData;
- },
- [getTimelineHash, cacheResolution]
- );
-
- // Cache a rendered frame
- const cacheFrame = useCallback(
- (
- time: number,
- imageData: ImageData,
- tracks: TimelineTrack[],
- mediaFiles: MediaFile[],
- activeProject: TProject | null,
- sceneId?: string
- ): void => {
- const frameKey = Math.floor(time * cacheResolution);
- const timelineHash = getTimelineHash(
- time,
- tracks,
- mediaFiles,
- activeProject,
- sceneId
- );
-
- // Enforce cache size limit (LRU eviction)
- if (frameCacheRef.current.size >= maxCacheSize) {
- // Remove oldest entries
- const entries = Array.from(frameCacheRef.current.entries());
- entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
-
- // Remove oldest 20% of entries
- const toRemove = Math.floor(entries.length * 0.2);
- for (let i = 0; i < toRemove; i++) {
- frameCacheRef.current.delete(entries[i][0]);
- }
- }
-
- frameCacheRef.current.set(frameKey, {
- imageData,
- timelineHash,
- timestamp: Date.now(),
- });
- },
- [getTimelineHash, cacheResolution, maxCacheSize]
- );
-
- // Clear cache when timeline changes significantly
- const invalidateCache = useCallback(() => {
- frameCacheRef.current.clear();
- }, []);
-
- // Get render status for timeline indicator
- const getRenderStatus = useCallback(
- (
- time: number,
- tracks: TimelineTrack[],
- mediaFiles: MediaFile[],
- activeProject: TProject | null,
- sceneId?: string
- ): "cached" | "not-cached" => {
- return isFrameCached(time, tracks, mediaFiles, activeProject, sceneId)
- ? "cached"
- : "not-cached";
- },
- [isFrameCached]
- );
-
- // Pre-render frames around current time
- const preRenderNearbyFrames = useCallback(
- async (
- currentTime: number,
- tracks: TimelineTrack[],
- mediaFiles: MediaFile[],
- activeProject: TProject | null,
- renderFunction: (time: number) => Promise<ImageData>,
- sceneId?: string,
- range: number = 3 // seconds
- ) => {
- const framesToPreRender: number[] = [];
-
- // Calculate frames to pre-render (around current time)
- for (
- let offset = -range;
- offset <= range;
- offset += 1 / cacheResolution
- ) {
- const time = currentTime + offset;
- if (time < 0) continue;
-
- if (!isFrameCached(time, tracks, mediaFiles, activeProject, sceneId)) {
- framesToPreRender.push(time);
- }
- }
-
- // Expand to full 1-second buckets to avoid fragmented tiny cache regions
- const secondsToPreRender = new Set<number>();
- for (const t of framesToPreRender) {
- secondsToPreRender.add(Math.floor(t));
- }
-
- const expandedTimes: number[] = [];
- for (const s of secondsToPreRender) {
- for (let k = 0; k < cacheResolution; k++) {
- const t = s + k / cacheResolution;
- if (t < 0) continue;
- if (!isFrameCached(t, tracks, mediaFiles, activeProject, sceneId)) {
- expandedTimes.push(t);
- }
- }
- }
-
- // Sort forward-first near currentTime to improve perceived responsiveness
- expandedTimes.sort((a, b) => {
- const da = a >= currentTime ? a - currentTime : currentTime - a + 1e6;
- const db = b >= currentTime ? b - currentTime : currentTime - b + 1e6;
- return da - db;
- });
-
- // Cap total scheduled renders to avoid jank (e.g., up to 90 frames)
- const CAP = Math.max(30, Math.min(90, cacheResolution * 3));
- const toSchedule = expandedTimes.slice(0, CAP);
-
- // Pre-render during idle time
- for (const time of toSchedule) {
- requestIdleCallback(async () => {
- try {
- const imageData = await renderFunction(time);
- cacheFrame(
- time,
- imageData,
- tracks,
- mediaFiles,
- activeProject,
- sceneId
- );
- } catch (error) {
- console.warn(`Pre-render failed for time ${time}:`, error);
- }
- });
- }
- },
- [isFrameCached, cacheFrame, cacheResolution]
- );
-
- return {
- isFrameCached,
- getCachedFrame,
- cacheFrame,
- invalidateCache,
- getRenderStatus,
- preRenderNearbyFrames,
- cacheSize: frameCacheRef.current.size,
- };
-}
diff --git a/apps/web/src/hooks/use-highlight-scroll.ts b/apps/web/src/hooks/use-highlight-scroll.ts
deleted file mode 100644
index 241bffb2..00000000
--- a/apps/web/src/hooks/use-highlight-scroll.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useEffect, useState, useRef } from "react";
-
-export function useHighlightScroll(
- highlightId: string | null,
- onClearHighlight: () => void,
- highlightDuration = 1000
-) {
- const [highlightedId, setHighlightedId] = useState<string | null>(null);
- const elementRefs = useRef<Map<string, HTMLElement>>(new Map());
-
- const registerElement = (id: string, element: HTMLElement | null) => {
- if (element) {
- elementRefs.current.set(id, element);
- } else {
- elementRefs.current.delete(id);
- }
- };
-
- useEffect(() => {
- if (!highlightId) return;
-
- setHighlightedId(highlightId);
-
- const target = elementRefs.current.get(highlightId);
- target?.scrollIntoView({ block: "center" });
-
- const timeout = setTimeout(() => {
- setHighlightedId(null);
- onClearHighlight();
- }, highlightDuration);
-
- return () => clearTimeout(timeout);
- }, [highlightId, onClearHighlight, highlightDuration]);
-
- return { highlightedId, registerElement };
-}
diff --git a/apps/web/src/hooks/use-infinite-scroll.ts b/apps/web/src/hooks/use-infinite-scroll.ts
index 0f8db198..4f823834 100644
--- a/apps/web/src/hooks/use-infinite-scroll.ts
+++ b/apps/web/src/hooks/use-infinite-scroll.ts
@@ -1,35 +1,35 @@
import { useRef, useCallback } from "react";
interface UseInfiniteScrollOptions {
- onLoadMore: () => void;
- hasMore: boolean;
- isLoading: boolean;
- threshold?: number;
- enabled?: boolean;
+ onLoadMore: () => void;
+ hasMore: boolean;
+ isLoading: boolean;
+ threshold?: number;
+ enabled?: boolean;
}
export function useInfiniteScroll({
- onLoadMore,
- hasMore,
- isLoading,
- threshold = 200,
- enabled = true,
+ onLoadMore,
+ hasMore,
+ isLoading,
+ threshold = 200,
+ enabled = true,
}: UseInfiniteScrollOptions) {
- const scrollAreaRef = useRef<HTMLDivElement>(null);
+ const scrollAreaRef = useRef<HTMLDivElement>(null);
- const handleScroll = useCallback(
- (event: React.UIEvent<HTMLDivElement>) => {
- if (!enabled) return;
+ const handleScroll = useCallback(
+ (event: React.UIEvent<HTMLDivElement>) => {
+ if (!enabled) return;
- const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
- const nearBottom = scrollTop + clientHeight >= scrollHeight - threshold;
+ const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
+ const nearBottom = scrollTop + clientHeight >= scrollHeight - threshold;
- if (nearBottom && hasMore && !isLoading) {
- onLoadMore();
- }
- },
- [onLoadMore, hasMore, isLoading, threshold, enabled]
- );
+ if (nearBottom && hasMore && !isLoading) {
+ onLoadMore();
+ }
+ },
+ [onLoadMore, hasMore, isLoading, threshold, enabled],
+ );
- return { scrollAreaRef, handleScroll };
+ return { scrollAreaRef, handleScroll };
}
diff --git a/apps/web/src/hooks/use-keybinding-conflicts.ts b/apps/web/src/hooks/use-keybinding-conflicts.ts
deleted file mode 100644
index cc5dd340..00000000
--- a/apps/web/src/hooks/use-keybinding-conflicts.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-"use client";
-
-import { useMemo } from "react";
-import { useKeybindingsStore } from "@/stores/keybindings-store";
-import { ActionWithOptionalArgs } from "@/constants/actions";
-
-export interface KeybindingConflictInfo {
- key: string;
- actions: ActionWithOptionalArgs[];
- isConflict: boolean;
-}
-
-export const useKeybindingConflicts = () => {
- const { keybindings } = useKeybindingsStore();
-
- const conflicts = useMemo(() => {
- const keyToActions: Record<string, ActionWithOptionalArgs[]> = {};
- const conflictList: KeybindingConflictInfo[] = [];
-
- // Group actions by key
- Object.entries(keybindings).forEach(([key, action]) => {
- if (!keyToActions[key]) {
- keyToActions[key] = [];
- }
- keyToActions[key].push(action);
- });
-
- // Find conflicts
- Object.entries(keyToActions).forEach(([key, actions]) => {
- const uniqueActions = [...new Set(actions)];
- conflictList.push({
- key,
- actions: uniqueActions,
- isConflict: uniqueActions.length > 1,
- });
- });
-
- return conflictList.filter((item) => item.isConflict);
- }, [keybindings]);
-
- const hasConflicts = conflicts.length > 0;
-
- const getConflictsForKey = (key: string): KeybindingConflictInfo | null => {
- return conflicts.find((conflict) => conflict.key === key) || null;
- };
-
- const getConflictsForAction = (
- action: ActionWithOptionalArgs
- ): KeybindingConflictInfo[] => {
- return conflicts.filter((conflict) => conflict.actions.includes(action));
- };
-
- return {
- conflicts,
- hasConflicts,
- getConflictsForKey,
- getConflictsForAction,
- };
-};
diff --git a/apps/web/src/hooks/use-keybindings.ts b/apps/web/src/hooks/use-keybindings.ts
index 7f6505b0..e59d1eaf 100644
--- a/apps/web/src/hooks/use-keybindings.ts
+++ b/apps/web/src/hooks/use-keybindings.ts
@@ -1,75 +1,75 @@
import { useEffect } from "react";
-import { invokeAction } from "../constants/actions";
+import { invokeAction } from "@/lib/actions";
import { useKeybindingsStore } from "@/stores/keybindings-store";
/**
- * A composable that hooks to the caller component's
+ * a composable that hooks to the caller component's
* lifecycle and hooks to the keyboard events to fire
* the appropriate actions based on keybindings
*/
export function useKeybindingsListener() {
- const { keybindings, getKeybindingString, keybindingsEnabled, isRecording } =
- useKeybindingsStore();
+ const { keybindings, getKeybindingString, keybindingsEnabled, isRecording } =
+ useKeybindingsStore();
- useEffect(() => {
- const handleKeyDown = (ev: KeyboardEvent) => {
- // Do not check keybinds if the mode is disabled
- if (!keybindingsEnabled) return;
- // ignore key events if user is changing keybindings
- if (isRecording) return;
+ useEffect(() => {
+ const eventOptions: AddEventListenerOptions = { capture: true };
+ const handleKeyDown = (ev: KeyboardEvent) => {
+ // do not check keybinds if the mode is disabled
+ if (!keybindingsEnabled) return;
+ // ignore key events if user is changing keybindings
+ if (isRecording) return;
- const binding = getKeybindingString(ev);
- if (!binding) return;
+ const binding = getKeybindingString(ev);
+ if (!binding) return;
- const boundAction = keybindings[binding];
- if (!boundAction) return;
+ const boundAction = keybindings[binding];
+ if (!boundAction) return;
- const activeElement = document.activeElement;
- const isTextInput =
- activeElement &&
- (activeElement.tagName === "INPUT" ||
- activeElement.tagName === "TEXTAREA" ||
- (activeElement as HTMLElement).isContentEditable);
+ const activeElement = document.activeElement;
+ const isTextInput =
+ activeElement &&
+ (activeElement.tagName === "INPUT" ||
+ activeElement.tagName === "TEXTAREA" ||
+ (activeElement as HTMLElement).isContentEditable);
- if (isTextInput) return;
+ if (isTextInput) return;
- ev.preventDefault();
+ ev.preventDefault();
- // Handle actions with default arguments
- let actionArgs: any;
+ switch (boundAction) {
+ case "seek-forward":
+ invokeAction("seek-forward", { seconds: 1 }, "keypress");
+ break;
+ case "seek-backward":
+ invokeAction("seek-backward", { seconds: 1 }, "keypress");
+ break;
+ case "jump-forward":
+ invokeAction("jump-forward", { seconds: 5 }, "keypress");
+ break;
+ case "jump-backward":
+ invokeAction("jump-backward", { seconds: 5 }, "keypress");
+ break;
+ default:
+ invokeAction(boundAction, undefined, "keypress");
+ }
+ };
- if (boundAction === "seek-forward") {
- actionArgs = { seconds: 1 };
- } else if (boundAction === "seek-backward") {
- actionArgs = { seconds: 1 };
- } else if (boundAction === "jump-forward") {
- actionArgs = { seconds: 5 };
- } else if (boundAction === "jump-backward") {
- actionArgs = { seconds: 5 };
- }
+ document.addEventListener("keydown", handleKeyDown, eventOptions);
- invokeAction(boundAction, actionArgs, "keypress");
- };
-
- document.addEventListener("keydown", handleKeyDown);
-
- return () => {
- document.removeEventListener("keydown", handleKeyDown);
- };
- }, [keybindings, getKeybindingString, keybindingsEnabled, isRecording]);
+ return () => {
+ document.removeEventListener("keydown", handleKeyDown, eventOptions);
+ };
+ }, [keybindings, getKeybindingString, keybindingsEnabled, isRecording]);
}
/**
- * This composable allows for the UI component to be disabled if the component in question is mounted
+ * this composable allows for the UI component to be disabled if the component in question is mounted
*/
export function useKeybindingDisabler() {
- const { disableKeybindings, enableKeybindings } = useKeybindingsStore();
+ const { disableKeybindings, enableKeybindings } = useKeybindingsStore();
- return {
- disableKeybindings,
- enableKeybindings,
- };
+ return {
+ disableKeybindings,
+ enableKeybindings,
+ };
}
-
-// Export the bindings for backward compatibility
-export const bindings = {};
diff --git a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts
index 29e3c09b..80f7f07a 100644
--- a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts
+++ b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts
@@ -2,138 +2,81 @@
import { useMemo } from "react";
import { useKeybindingsStore } from "@/stores/keybindings-store";
-import { Action } from "@/constants/actions";
-import { getPlatformAlternateKey, getPlatformSpecialKey } from "@/lib/utils";
+import { ACTIONS, type TAction } from "@/lib/actions";
+import {
+ getPlatformAlternateKey,
+ getPlatformSpecialKey,
+} from "@/utils/platform";
export interface KeyboardShortcut {
- id: string;
- keys: string[];
- description: string;
- category: string;
- action: Action;
- icon?: React.ReactNode;
+ id: string;
+ keys: string[];
+ description: string;
+ category: string;
+ action: TAction;
+ icon?: React.ReactNode;
}
-// Map actions to their descriptions and categories
-const actionDescriptions: Record<
- Action,
- { description: string; category: string }
-> = {
- "toggle-play": { description: "Play/Pause", category: "Playback" },
- "stop-playback": { description: "Stop playback", category: "Playback" },
- "seek-forward": {
- description: "Seek forward 1 second",
- category: "Playback",
- },
- "seek-backward": {
- description: "Seek backward 1 second",
- category: "Playback",
- },
- "frame-step-forward": {
- description: "Frame step forward",
- category: "Navigation",
- },
- "frame-step-backward": {
- description: "Frame step backward",
- category: "Navigation",
- },
- "jump-forward": {
- description: "Jump forward 5 seconds",
- category: "Navigation",
- },
- "jump-backward": {
- description: "Jump backward 5 seconds",
- category: "Navigation",
- },
- "goto-start": { description: "Go to timeline start", category: "Navigation" },
- "goto-end": { description: "Go to timeline end", category: "Navigation" },
- "split-element": {
- description: "Split element at playhead",
- category: "Editing",
- },
- "delete-selected": {
- description: "Delete selected elements",
- category: "Editing",
- },
- "select-all": { description: "Select all elements", category: "Selection" },
- "duplicate-selected": {
- description: "Duplicate selected element",
- category: "Selection",
- },
- "toggle-snapping": { description: "Toggle snapping", category: "Editing" },
- undo: { description: "Undo", category: "History" },
- redo: { description: "Redo", category: "History" },
- "copy-selected": {
- description: "Copy selected elements",
- category: "Editing",
- },
- "paste-selected": {
- description: "Paste elements at playhead",
- category: "Editing",
- },
-};
+function formatKey({ key }: { key: string }): string {
+ return key
+ .replace("ctrl", getPlatformSpecialKey())
+ .replace("alt", getPlatformAlternateKey())
+ .replace("shift", "Shift")
+ .replace("left", "←")
+ .replace("right", "→")
+ .replace("up", "↑")
+ .replace("down", "↓")
+ .replace("space", "Space")
+ .replace("home", "Home")
+ .replace("enter", "Enter")
+ .replace("end", "End")
+ .replace("delete", "Delete")
+ .replace("backspace", "Backspace")
+ .replace("-", "+");
+}
-// Convert key binding format to display format
-const formatKey = (key: string): string => {
- return key
- .replace("ctrl", getPlatformSpecialKey())
- .replace("alt", getPlatformAlternateKey())
- .replace("shift", "Shift")
- .replace("left", "←")
- .replace("right", "→")
- .replace("up", "↑")
- .replace("down", "↓")
- .replace("space", "Space")
- .replace("home", "Home")
- .replace("enter", "Enter")
- .replace("end", "End")
- .replace("delete", "Delete")
- .replace("backspace", "Backspace")
- .replace("-", "+");
-};
+export function useKeyboardShortcutsHelp() {
+ const { keybindings } = useKeybindingsStore();
-export const useKeyboardShortcutsHelp = () => {
- const { keybindings } = useKeybindingsStore();
+ const shortcuts = useMemo(() => {
+ const result: KeyboardShortcut[] = [];
+ const actionToKeys: Record<string, string[]> = {};
- const shortcuts = useMemo(() => {
- const result: KeyboardShortcut[] = [];
+ for (const [key, action] of Object.entries(keybindings)) {
+ if (action) {
+ if (!actionToKeys[action]) {
+ actionToKeys[action] = [];
+ }
+ actionToKeys[action].push(formatKey({ key }));
+ }
+ }
- // Group keybindings by action
- const actionToKeys: Record<Action, string[]> = {} as any;
+ for (const [actionId, keys] of Object.entries(actionToKeys)) {
+ if (!isAction(actionId)) continue;
- Object.entries(keybindings).forEach(([key, action]) => {
- if (action) {
- if (!actionToKeys[action]) {
- actionToKeys[action] = [];
- }
- actionToKeys[action].push(formatKey(key));
- }
- });
+ const actionDef = ACTIONS[actionId];
+ result.push({
+ id: actionId,
+ keys,
+ description: actionDef.description,
+ category: actionDef.category,
+ action: actionId,
+ });
+ }
- // Convert to shortcuts format
- Object.entries(actionToKeys).forEach(([action, keys]) => {
- const actionInfo = actionDescriptions[action as Action];
- if (actionInfo) {
- result.push({
- id: action,
- keys,
- description: actionInfo.description,
- category: actionInfo.category,
- action: action as Action,
- });
- }
- });
+ return result.sort((a, b) => {
+ if (a.category !== b.category) {
+ return a.category.localeCompare(b.category);
+ }
+ return a.description.localeCompare(b.description);
+ });
+ }, [keybindings]);
- // Sort shortcuts by category first, then by description to ensure consistent ordering
- return result.sort((a, b) => {
- if (a.category !== b.category) {
- return a.category.localeCompare(b.category);
- }
- return a.description.localeCompare(b.description);
- });
- }, [keybindings]);
+ return {
+ shortcuts,
+ };
+}
- return {
- shortcuts,
- };
-};
+function isAction(id: string): id is TAction {
+ return id in ACTIONS;
+}
diff --git a/apps/web/src/hooks/use-mobile.ts b/apps/web/src/hooks/use-mobile.ts
new file mode 100644
index 00000000..d6a67b93
--- /dev/null
+++ b/apps/web/src/hooks/use-mobile.ts
@@ -0,0 +1,19 @@
+import { useEffect, useState } from "react";
+
+const MOBILE_BREAKPOINT = 768;
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined);
+
+ useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ };
+ mql.addEventListener("change", onChange);
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ return () => mql.removeEventListener("change", onChange);
+ }, []);
+
+ return !!isMobile;
+}
diff --git a/apps/web/src/hooks/use-mobile.tsx b/apps/web/src/hooks/use-mobile.tsx
deleted file mode 100644
index 48fab93c..00000000
--- a/apps/web/src/hooks/use-mobile.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as React from "react";
-
-const MOBILE_BREAKPOINT = 768;
-
-export function useIsMobile() {
- const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
- undefined
- );
-
- React.useEffect(() => {
- const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
- const onChange = () => {
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
- };
- mql.addEventListener("change", onChange);
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
- return () => mql.removeEventListener("change", onChange);
- }, []);
-
- return !!isMobile;
-}
diff --git a/apps/web/src/hooks/use-playback-controls.ts b/apps/web/src/hooks/use-playback-controls.ts
deleted file mode 100644
index c27911d2..00000000
--- a/apps/web/src/hooks/use-playback-controls.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { useEffect, useCallback } from "react";
-import { usePlaybackStore } from "@/stores/playback-store";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { toast } from "sonner";
-
-export const usePlaybackControls = () => {
- const { isPlaying, currentTime, play, pause, seek } = usePlaybackStore();
-
- const {
- selectedElements,
- tracks,
- splitSelected,
- splitAndKeepLeft,
- splitAndKeepRight,
- separateAudio,
- } = useTimelineStore();
-
- const handleSplitSelectedElement = useCallback(() => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one element to split");
- return;
- }
-
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((e) => e.id === elementId);
-
- if (!element) return;
-
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
-
- if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
- toast.error("Playhead must be within selected element");
- return;
- }
-
- splitSelected(currentTime, trackId, elementId);
- }, [selectedElements, tracks, currentTime, splitSelected]);
-
- const handleSplitAndKeepLeftCallback = useCallback(() => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one element");
- return;
- }
-
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((e) => e.id === elementId);
-
- if (!element) return;
-
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
-
- if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
- toast.error("Playhead must be within selected element");
- return;
- }
-
- splitAndKeepLeft(trackId, elementId, currentTime);
- }, [selectedElements, tracks, currentTime, splitAndKeepLeft]);
-
- const handleSplitAndKeepRightCallback = useCallback(() => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one element");
- return;
- }
-
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((e) => e.id === elementId);
-
- if (!element) return;
-
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
-
- if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
- toast.error("Playhead must be within selected element");
- return;
- }
-
- splitAndKeepRight(trackId, elementId, currentTime);
- }, [selectedElements, tracks, currentTime, splitAndKeepRight]);
-
- const handleSeparateAudioCallback = useCallback(() => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one media element to separate audio");
- return;
- }
-
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t) => t.id === trackId);
-
- if (!track || track.type !== "media") {
- toast.error("Select a media element to separate audio");
- return;
- }
-
- separateAudio(trackId, elementId);
- }, [selectedElements, tracks, separateAudio]);
-};
diff --git a/apps/web/src/hooks/use-raf-loop.ts b/apps/web/src/hooks/use-raf-loop.ts
new file mode 100644
index 00000000..54b1e662
--- /dev/null
+++ b/apps/web/src/hooks/use-raf-loop.ts
@@ -0,0 +1,20 @@
+import { useEffect, useRef } from "react";
+
+export function useRafLoop(callback: ({ time }: { time: number }) => void) {
+ const requestRef = useRef<number>(0);
+ const previousTimeRef = useRef<number | null>(null);
+
+ useEffect(() => {
+ const loop = ({ time }: { time: number }) => {
+ if (previousTimeRef.current !== null) {
+ const deltaTime = time - previousTimeRef.current;
+ callback({ time: deltaTime });
+ }
+ previousTimeRef.current = time;
+ requestRef.current = requestAnimationFrame((time) => loop({ time }));
+ };
+
+ requestRef.current = requestAnimationFrame((time) => loop({ time }));
+ return () => cancelAnimationFrame(requestRef.current);
+ }, [callback]);
+}
diff --git a/apps/web/src/hooks/use-reveal-item.ts b/apps/web/src/hooks/use-reveal-item.ts
new file mode 100644
index 00000000..5004f60c
--- /dev/null
+++ b/apps/web/src/hooks/use-reveal-item.ts
@@ -0,0 +1,36 @@
+import { useEffect, useState, useRef } from "react";
+
+export function useRevealItem(
+ highlightId: string | null,
+ onClearHighlight: () => void,
+ highlightDuration = 1000,
+) {
+ const [highlightedId, setHighlightedId] = useState<string | null>(null);
+ const elementRefs = useRef<Map<string, HTMLElement>>(new Map());
+
+ const registerElement = (id: string, element: HTMLElement | null) => {
+ if (element) {
+ elementRefs.current.set(id, element);
+ } else {
+ elementRefs.current.delete(id);
+ }
+ };
+
+ useEffect(() => {
+ if (!highlightId) return;
+
+ setHighlightedId(highlightId);
+
+ const target = elementRefs.current.get(highlightId);
+ target?.scrollIntoView({ block: "center" });
+
+ const timeout = setTimeout(() => {
+ setHighlightedId(null);
+ onClearHighlight();
+ }, highlightDuration);
+
+ return () => clearTimeout(timeout);
+ }, [highlightId, onClearHighlight, highlightDuration]);
+
+ return { highlightedId, registerElement };
+}
diff --git a/apps/web/src/hooks/use-selection-box.ts b/apps/web/src/hooks/use-selection-box.ts
deleted file mode 100644
index 87026500..00000000
--- a/apps/web/src/hooks/use-selection-box.ts
+++ /dev/null
@@ -1,215 +0,0 @@
-import { useState, useEffect, useCallback } from "react";
-
-interface UseSelectionBoxProps {
- containerRef: React.RefObject<HTMLElement>;
- playheadRef?: React.RefObject<HTMLElement>;
- onSelectionComplete: (
- elements: { trackId: string; elementId: string }[]
- ) => void;
- isEnabled?: boolean;
-}
-
-interface SelectionBoxState {
- startPos: { x: number; y: number };
- currentPos: { x: number; y: number };
- isActive: boolean;
-}
-
-export function useSelectionBox({
- containerRef,
- playheadRef,
- onSelectionComplete,
- isEnabled = true,
-}: UseSelectionBoxProps) {
- const [selectionBox, setSelectionBox] = useState<SelectionBoxState | null>(
- null
- );
- const [justFinishedSelecting, setJustFinishedSelecting] = useState(false);
-
- // Mouse down handler to start selection
- const handleMouseDown = useCallback(
- (e: React.MouseEvent) => {
- if (!isEnabled) return;
-
- // Only start selection on empty space clicks
- if ((e.target as HTMLElement).closest(".timeline-element")) {
- return;
- }
- if (playheadRef?.current?.contains(e.target as Node)) {
- return;
- }
- if ((e.target as HTMLElement).closest("[data-track-labels]")) {
- return;
- }
- // Don't start selection when clicking in the ruler area - this interferes with playhead dragging
- if ((e.target as HTMLElement).closest("[data-ruler-area]")) {
- return;
- }
-
- setSelectionBox({
- startPos: { x: e.clientX, y: e.clientY },
- currentPos: { x: e.clientX, y: e.clientY },
- isActive: false, // Will become active when mouse moves
- });
- },
- [isEnabled, playheadRef]
- );
-
- // Function to select elements within the selection box
- const selectElementsInBox = useCallback(
- (startPos: { x: number; y: number }, endPos: { x: number; y: number }) => {
- if (!containerRef.current) return;
-
- const container = containerRef.current;
- const containerRect = container.getBoundingClientRect();
-
- // Calculate selection rectangle in container coordinates
- const startX = startPos.x - containerRect.left;
- const startY = startPos.y - containerRect.top;
- const endX = endPos.x - containerRect.left;
- const endY = endPos.y - containerRect.top;
-
- const selectionRect = {
- left: Math.min(startX, endX),
- top: Math.min(startY, endY),
- right: Math.max(startX, endX),
- bottom: Math.max(startY, endY),
- };
-
- // Find all timeline elements within the selection rectangle
- const timelineElements = container.querySelectorAll(".timeline-element");
-
- const selectedElements: { trackId: string; elementId: string }[] = [];
-
- timelineElements.forEach((element) => {
- const elementRect = element.getBoundingClientRect();
- // Use absolute coordinates for more accurate intersection detection
- const elementAbsolute = {
- left: elementRect.left,
- top: elementRect.top,
- right: elementRect.right,
- bottom: elementRect.bottom,
- };
-
- const selectionAbsolute = {
- left: startPos.x,
- top: startPos.y,
- right: endPos.x,
- bottom: endPos.y,
- };
-
- // Normalize selection rectangle (handle dragging in any direction)
- const normalizedSelection = {
- left: Math.min(selectionAbsolute.left, selectionAbsolute.right),
- top: Math.min(selectionAbsolute.top, selectionAbsolute.bottom),
- right: Math.max(selectionAbsolute.left, selectionAbsolute.right),
- bottom: Math.max(selectionAbsolute.top, selectionAbsolute.bottom),
- };
-
- const elementId = element.getAttribute("data-element-id");
- const trackId = element.getAttribute("data-track-id");
-
- // Check if element intersects with selection rectangle (any overlap)
- // Using absolute coordinates for more precise detection
- const intersects = !(
- elementAbsolute.right < normalizedSelection.left ||
- elementAbsolute.left > normalizedSelection.right ||
- elementAbsolute.bottom < normalizedSelection.top ||
- elementAbsolute.top > normalizedSelection.bottom
- );
-
- if (intersects && elementId && trackId) {
- selectedElements.push({ trackId, elementId });
- }
- });
-
- // Always call the callback - with elements or empty array to clear selection
- console.log(
- JSON.stringify({ selectElementsInBox: selectedElements.length })
- );
- onSelectionComplete(selectedElements);
- },
- [containerRef, onSelectionComplete]
- );
-
- // Effect to track selection box movement
- useEffect(() => {
- if (!selectionBox) return;
-
- const handleMouseMove = (e: MouseEvent) => {
- const deltaX = Math.abs(e.clientX - selectionBox.startPos.x);
- const deltaY = Math.abs(e.clientY - selectionBox.startPos.y);
-
- // Start selection if mouse moved more than 5px
- const shouldActivate = deltaX > 5 || deltaY > 5;
-
- const newSelectionBox = {
- ...selectionBox,
- currentPos: { x: e.clientX, y: e.clientY },
- isActive: shouldActivate || selectionBox.isActive,
- };
-
- setSelectionBox(newSelectionBox);
-
- // Real-time visual feedback: update selection as we drag
- if (newSelectionBox.isActive) {
- selectElementsInBox(
- newSelectionBox.startPos,
- newSelectionBox.currentPos
- );
- }
- };
-
- const handleMouseUp = () => {
- console.log(
- JSON.stringify({ mouseUp: { wasActive: selectionBox?.isActive } })
- );
-
- // If we had an active selection, mark that we just finished selecting
- if (selectionBox?.isActive) {
- console.log(JSON.stringify({ settingJustFinishedSelecting: true }));
- setJustFinishedSelecting(true);
- // Clear the flag after a short delay to allow click events to check it
- setTimeout(() => {
- console.log(JSON.stringify({ clearingJustFinishedSelecting: true }));
- setJustFinishedSelecting(false);
- }, 50);
- }
-
- // Don't call selectElementsInBox again - real-time selection already handled it
- // Just clean up the selection box visual
- setSelectionBox(null);
- };
-
- window.addEventListener("mousemove", handleMouseMove);
- window.addEventListener("mouseup", handleMouseUp);
-
- return () => {
- window.removeEventListener("mousemove", handleMouseMove);
- window.removeEventListener("mouseup", handleMouseUp);
- };
- }, [selectionBox, selectElementsInBox]);
-
- useEffect(() => {
- if (!selectionBox?.isActive) return;
-
- const previousBodyUserSelect = document.body.style.userSelect;
- const container = containerRef.current;
- const previousContainerUserSelect = container?.style.userSelect ?? "";
-
- document.body.style.userSelect = "none";
- if (container) container.style.userSelect = "none";
-
- return () => {
- document.body.style.userSelect = previousBodyUserSelect;
- if (container) container.style.userSelect = previousContainerUserSelect;
- };
- }, [selectionBox?.isActive, containerRef]);
-
- return {
- selectionBox,
- handleMouseDown,
- isSelecting: selectionBox?.isActive || false,
- justFinishedSelecting,
- };
-}
diff --git a/apps/web/src/hooks/use-sound-search.ts b/apps/web/src/hooks/use-sound-search.ts
index db9a7e8e..263a01e2 100644
--- a/apps/web/src/hooks/use-sound-search.ts
+++ b/apps/web/src/hooks/use-sound-search.ts
@@ -1,156 +1,154 @@
import { useEffect } from "react";
import { useSoundsStore } from "@/stores/sounds-store";
-/**
- * Custom hook for searching sound effects with race condition protection.
- * Uses global Zustand store to persist search state across tab switches.
- * - Debounced search (300ms)
- * - Race condition protection with cleanup
- * - Proper error handling
- */
+export function useSoundSearch({
+ query,
+ commercialOnly,
+}: {
+ query: string;
+ commercialOnly: boolean;
+}) {
+ const {
+ searchResults,
+ isSearching,
+ searchError,
+ lastSearchQuery,
+ currentPage,
+ hasNextPage,
+ isLoadingMore,
+ totalCount,
+ setSearchResults,
+ setSearching,
+ setSearchError,
+ setLastSearchQuery,
+ setCurrentPage,
+ setHasNextPage,
+ setTotalCount,
+ setLoadingMore,
+ appendSearchResults,
+ appendTopSounds,
+ resetPagination,
+ } = useSoundsStore();
-export function useSoundSearch(query: string, commercialOnly: boolean) {
- const {
- searchResults,
- isSearching,
- searchError,
- lastSearchQuery,
- currentPage,
- hasNextPage,
- isLoadingMore,
- totalCount,
- setSearchResults,
- setSearching,
- setSearchError,
- setLastSearchQuery,
- setCurrentPage,
- setHasNextPage,
- setTotalCount,
- setLoadingMore,
- appendSearchResults,
- appendTopSounds,
- resetPagination,
- } = useSoundsStore();
+ const loadMore = async () => {
+ if (isLoadingMore || !hasNextPage) return;
- // Load more function for infinite scroll
- const loadMore = async () => {
- if (isLoadingMore || !hasNextPage) return;
+ try {
+ setLoadingMore({ loading: true });
+ const nextPage = currentPage + 1;
- try {
- setLoadingMore(true);
- const nextPage = currentPage + 1;
+ const searchParams = new URLSearchParams({
+ page: nextPage.toString(),
+ type: "effects",
+ });
- const searchParams = new URLSearchParams({
- page: nextPage.toString(),
- type: "effects",
- });
+ if (query.trim()) {
+ searchParams.set("q", query);
+ }
- if (query.trim()) {
- searchParams.set("q", query);
- }
+ searchParams.set("commercial_only", commercialOnly.toString());
+ const response = await fetch(
+ `/api/sounds/search?${searchParams.toString()}`,
+ );
- searchParams.set("commercial_only", commercialOnly.toString());
- const response = await fetch(
- `/api/sounds/search?${searchParams.toString()}`
- );
+ if (response.ok) {
+ const data = await response.json();
- if (response.ok) {
- const data = await response.json();
+ if (query.trim()) {
+ appendSearchResults(data.results);
+ } else {
+ appendTopSounds(data.results);
+ }
- // Append to appropriate array based on whether we have a query
- if (query.trim()) {
- appendSearchResults(data.results);
- } else {
- appendTopSounds(data.results);
- }
+ setCurrentPage({ page: nextPage });
+ setHasNextPage({ hasNext: !!data.next });
+ setTotalCount(data.count);
+ } else {
+ setSearchError({ error: `Load more failed: ${response.status}` });
+ }
+ } catch (err) {
+ setSearchError({
+ error: err instanceof Error ? err.message : "Load more failed",
+ });
+ } finally {
+ setLoadingMore({ loading: false });
+ }
+ };
- setCurrentPage(nextPage);
- setHasNextPage(!!data.next);
- setTotalCount(data.count);
- } else {
- setSearchError(`Load more failed: ${response.status}`);
- }
- } catch (err) {
- setSearchError(err instanceof Error ? err.message : "Load more failed");
- } finally {
- setLoadingMore(false);
- }
- };
+ useEffect(() => {
+ if (!query.trim()) {
+ setSearchResults({ results: [] });
+ setSearchError({ error: null });
+ setLastSearchQuery({ query: "" });
+ return;
+ }
- useEffect(() => {
- if (!query.trim()) {
- setSearchResults([]);
- setSearchError(null);
- setLastSearchQuery("");
- // Don't reset pagination here - top sounds pagination is managed by prefetcher
- return;
- }
+ if (query === lastSearchQuery && searchResults.length > 0) {
+ return;
+ }
- // If we already searched for this query and have results, don't search again
- if (query === lastSearchQuery && searchResults.length > 0) {
- return;
- }
+ let ignore = false;
- let ignore = false;
+ const timeoutId = setTimeout(async () => {
+ try {
+ setSearching({ searching: true });
+ setSearchError({ error: null });
+ resetPagination();
- const timeoutId = setTimeout(async () => {
- try {
- setSearching(true);
- setSearchError(null);
- resetPagination();
+ const response = await fetch(
+ `/api/sounds/search?q=${encodeURIComponent(query)}&type=effects&page=1`,
+ );
- const response = await fetch(
- `/api/sounds/search?q=${encodeURIComponent(query)}&type=effects&page=1`
- );
+ if (!ignore) {
+ if (response.ok) {
+ const data = await response.json();
+ setSearchResults({ results: data.results });
+ setLastSearchQuery({ query: query });
+ setHasNextPage({ hasNext: !!data.next });
+ setTotalCount({ count: data.count });
+ setCurrentPage({ page: 1 });
+ } else {
+ setSearchError({ error: `Search failed: ${response.status}` });
+ }
+ }
+ } catch (err) {
+ if (!ignore) {
+ setSearchError({
+ error: err instanceof Error ? err.message : "Search failed",
+ });
+ }
+ } finally {
+ if (!ignore) {
+ setSearching({ searching: false });
+ }
+ }
+ }, 300);
- if (!ignore) {
- if (response.ok) {
- const data = await response.json();
- setSearchResults(data.results);
- setLastSearchQuery(query);
- setHasNextPage(!!data.next);
- setTotalCount(data.count);
- setCurrentPage(1);
- } else {
- setSearchError(`Search failed: ${response.status}`);
- }
- }
- } catch (err) {
- if (!ignore) {
- setSearchError(err instanceof Error ? err.message : "Search failed");
- }
- } finally {
- if (!ignore) {
- setSearching(false);
- }
- }
- }, 300);
+ return () => {
+ clearTimeout(timeoutId);
+ ignore = true;
+ };
+ }, [
+ query,
+ lastSearchQuery,
+ searchResults.length,
+ setSearchResults,
+ setSearching,
+ setSearchError,
+ setLastSearchQuery,
+ setCurrentPage,
+ setHasNextPage,
+ setTotalCount,
+ resetPagination,
+ ]);
- return () => {
- clearTimeout(timeoutId);
- ignore = true;
- };
- }, [
- query,
- lastSearchQuery,
- searchResults.length,
- setSearchResults,
- setSearching,
- setSearchError,
- setLastSearchQuery,
- setCurrentPage,
- setHasNextPage,
- setTotalCount,
- resetPagination,
- ]);
-
- return {
- results: searchResults,
- isLoading: isSearching,
- error: searchError,
- loadMore,
- hasNextPage,
- isLoadingMore,
- totalCount,
- };
+ return {
+ results: searchResults,
+ isLoading: isSearching,
+ error: searchError,
+ loadMore,
+ hasNextPage,
+ isLoadingMore,
+ totalCount,
+ };
}
diff --git a/apps/web/src/hooks/use-timeline-element-resize.ts b/apps/web/src/hooks/use-timeline-element-resize.ts
deleted file mode 100644
index 4812459a..00000000
--- a/apps/web/src/hooks/use-timeline-element-resize.ts
+++ /dev/null
@@ -1,257 +0,0 @@
-import { useState, useEffect } from "react";
-import { ResizeState, TimelineElement, TimelineTrack } from "@/types/timeline";
-import { useMediaStore } from "@/stores/media-store";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { DEFAULT_FPS, useProjectStore } from "@/stores/project-store";
-import { snapTimeToFrame } from "@/constants/timeline-constants";
-
-interface UseTimelineElementResizeProps {
- element: TimelineElement;
- track: TimelineTrack;
- zoomLevel: number;
-}
-
-export function useTimelineElementResize({
- element,
- track,
- zoomLevel,
-}: UseTimelineElementResizeProps) {
- const [resizing, setResizing] = useState<ResizeState | null>(null);
- const { mediaFiles } = useMediaStore();
- const {
- updateElementStartTime,
- updateElementTrim,
- updateElementDuration,
- pushHistory,
- } = useTimelineStore();
-
- // Set up document-level mouse listeners during resize (like proper drag behavior)
- useEffect(() => {
- if (!resizing) return;
-
- const handleDocumentMouseMove = (e: MouseEvent) => {
- updateTrimFromMouseMove({ clientX: e.clientX });
- };
-
- const handleDocumentMouseUp = () => {
- handleResizeEnd();
- };
-
- // Add document-level listeners for proper drag behavior
- document.addEventListener("mousemove", handleDocumentMouseMove);
- document.addEventListener("mouseup", handleDocumentMouseUp);
-
- return () => {
- document.removeEventListener("mousemove", handleDocumentMouseMove);
- document.removeEventListener("mouseup", handleDocumentMouseUp);
- };
- }, [resizing]); // Re-run when resizing state changes
-
- const handleResizeStart = (
- e: React.MouseEvent,
- elementId: string,
- side: "left" | "right"
- ) => {
- e.stopPropagation();
- e.preventDefault();
-
- // Push history once at the start of the resize operation
- pushHistory();
-
- setResizing({
- elementId,
- side,
- startX: e.clientX,
- initialTrimStart: element.trimStart,
- initialTrimEnd: element.trimEnd,
- });
- };
-
- const canExtendElementDuration = () => {
- // Text elements can always be extended
- if (element.type === "text") {
- return true;
- }
-
- // Media elements - check the media type
- if (element.type === "media") {
- const mediaFile = mediaFiles.find((file) => file.id === element.mediaId);
- if (!mediaFile) return false;
-
- // Images can be extended (static content)
- if (mediaFile.type === "image") {
- return true;
- }
-
- // Videos and audio cannot be extended beyond their natural duration
- // (no additional content exists)
- return false;
- }
-
- return false;
- };
-
- const updateTrimFromMouseMove = (e: { clientX: number }) => {
- if (!resizing) return;
-
- const deltaX = e.clientX - resizing.startX;
- // Reasonable sensitivity for resize operations - similar to timeline scale
- const deltaTime = deltaX / (50 * zoomLevel);
-
- // Get project FPS for frame snapping
- const projectStore = useProjectStore.getState();
- const projectFps = projectStore.activeProject?.fps || DEFAULT_FPS;
-
- if (resizing.side === "left") {
- // Left resize - different behavior for media vs text/image elements
- const maxAllowed = element.duration - resizing.initialTrimEnd - 0.1;
- const calculated = resizing.initialTrimStart + deltaTime;
-
- if (calculated >= 0) {
- // Normal trimming within available content
- const newTrimStart = snapTimeToFrame(
- Math.min(maxAllowed, calculated),
- projectFps
- );
- const trimDelta = newTrimStart - resizing.initialTrimStart;
- const newStartTime = snapTimeToFrame(
- element.startTime + trimDelta,
- projectFps
- );
-
- updateElementTrim(
- track.id,
- element.id,
- newTrimStart,
- resizing.initialTrimEnd,
- false
- );
- updateElementStartTime(track.id, element.id, newStartTime, false);
- } else {
- // Trying to extend beyond trimStart = 0
- if (canExtendElementDuration()) {
- // Text/Image: extend element to the left by moving startTime and increasing duration
- const extensionAmount = Math.abs(calculated);
- const maxExtension = element.startTime;
- const actualExtension = Math.min(extensionAmount, maxExtension);
- const newStartTime = snapTimeToFrame(
- element.startTime - actualExtension,
- projectFps
- );
- const newDuration = snapTimeToFrame(
- element.duration + actualExtension,
- projectFps
- );
-
- // Keep trimStart at 0 and extend the element
- updateElementTrim(
- track.id,
- element.id,
- 0,
- resizing.initialTrimEnd,
- false
- );
- updateElementDuration(track.id, element.id, newDuration, false);
- updateElementStartTime(track.id, element.id, newStartTime, false);
- } else {
- // Video/Audio: can't extend beyond original content - limit to trimStart = 0
- const newTrimStart = 0;
- const trimDelta = newTrimStart - resizing.initialTrimStart;
- const newStartTime = snapTimeToFrame(
- element.startTime + trimDelta,
- projectFps
- );
-
- updateElementTrim(
- track.id,
- element.id,
- newTrimStart,
- resizing.initialTrimEnd,
- false
- );
- updateElementStartTime(track.id, element.id, newStartTime, false);
- }
- }
- } else {
- // Right resize - can extend duration for supported element types
- const calculated = resizing.initialTrimEnd - deltaTime;
-
- if (calculated < 0) {
- // We're trying to extend beyond original duration
- if (canExtendElementDuration()) {
- // Extend the duration instead of reducing trimEnd further
- const extensionNeeded = Math.abs(calculated);
- const newDuration = snapTimeToFrame(
- element.duration + extensionNeeded,
- projectFps
- );
- const newTrimEnd = 0; // Reset trimEnd to 0 since we're extending
-
- // Update duration first, then trim
- updateElementDuration(track.id, element.id, newDuration, false);
- updateElementTrim(
- track.id,
- element.id,
- resizing.initialTrimStart,
- newTrimEnd,
- false
- );
- } else {
- // Can't extend - just set trimEnd to 0 (maximum possible extension)
- updateElementTrim(
- track.id,
- element.id,
- resizing.initialTrimStart,
- 0,
- false
- );
- }
- } else {
- // Normal trimming within original duration
- // Calculate the desired end time based on mouse movement
- const currentEndTime =
- element.startTime +
- element.duration -
- element.trimStart -
- element.trimEnd;
- const desiredEndTime = currentEndTime + deltaTime;
-
- // Snap the desired end time to frame
- const snappedEndTime = snapTimeToFrame(desiredEndTime, projectFps);
-
- // Calculate what trimEnd should be to achieve this snapped end time
- const newTrimEnd = Math.max(
- 0,
- element.duration -
- element.trimStart -
- (snappedEndTime - element.startTime)
- );
-
- // Ensure we don't trim more than available content (leave at least 0.1s visible)
- const maxTrimEnd = element.duration - element.trimStart - 0.1;
- const finalTrimEnd = Math.min(maxTrimEnd, newTrimEnd);
-
- updateElementTrim(
- track.id,
- element.id,
- element.trimStart,
- finalTrimEnd,
- false
- );
- }
- }
- };
-
- const handleResizeEnd = () => {
- setResizing(null);
- };
-
- return {
- resizing,
- isResizing: resizing !== null,
- handleResizeStart,
- // Return empty handlers since we use document listeners now
- handleResizeMove: () => {}, // Not used anymore
- handleResizeEnd: () => {}, // Not used anymore
- };
-}
diff --git a/apps/web/src/hooks/use-timeline-playhead.ts b/apps/web/src/hooks/use-timeline-playhead.ts
deleted file mode 100644
index 57ce085a..00000000
--- a/apps/web/src/hooks/use-timeline-playhead.ts
+++ /dev/null
@@ -1,218 +0,0 @@
-import { snapTimeToFrame } from "@/constants/timeline-constants";
-import { DEFAULT_FPS, useProjectStore } from "@/stores/project-store";
-import { usePlaybackStore } from "@/stores/playback-store";
-import { useState, useEffect, useCallback, useRef } from "react";
-import { useEdgeAutoScroll } from "@/hooks/use-edge-auto-scroll";
-
-interface UseTimelinePlayheadProps {
- currentTime: number;
- duration: number;
- zoomLevel: number;
- seek: (time: number) => void;
- rulerRef: React.RefObject<HTMLDivElement>;
- rulerScrollRef: React.RefObject<HTMLDivElement>;
- tracksScrollRef: React.RefObject<HTMLDivElement>;
- playheadRef?: React.RefObject<HTMLDivElement>;
-}
-
-export function useTimelinePlayhead({
- currentTime,
- duration,
- zoomLevel,
- seek,
- rulerRef,
- rulerScrollRef,
- tracksScrollRef,
- playheadRef,
-}: UseTimelinePlayheadProps) {
- // Playhead scrubbing state
- const [isScrubbing, setIsScrubbing] = useState(false);
- const [scrubTime, setScrubTime] = useState<number | null>(null);
-
- // Ruler drag detection state
- const [isDraggingRuler, setIsDraggingRuler] = useState(false);
- const [hasDraggedRuler, setHasDraggedRuler] = useState(false);
- const lastMouseXRef = useRef<number>(0);
-
- const playheadPosition =
- isScrubbing && scrubTime !== null ? scrubTime : currentTime;
-
- // --- Playhead Scrubbing Handlers ---
- const handlePlayheadMouseDown = useCallback(
- (e: React.MouseEvent) => {
- e.preventDefault();
- e.stopPropagation(); // Prevent ruler drag from triggering
- setIsScrubbing(true);
- handleScrub(e);
- },
- [duration, zoomLevel]
- );
-
- // Ruler mouse down handler
- const handleRulerMouseDown = useCallback(
- (e: React.MouseEvent) => {
- // Only handle left mouse button
- if (e.button !== 0) return;
-
- // Don't interfere if clicking on the playhead itself
- if (playheadRef?.current?.contains(e.target as Node)) return;
-
- e.preventDefault();
- setIsDraggingRuler(true);
- setHasDraggedRuler(false);
-
- // Start scrubbing immediately
- setIsScrubbing(true);
- handleScrub(e);
- },
- [duration, zoomLevel]
- );
-
- const handleScrub = useCallback(
- (e: MouseEvent | React.MouseEvent) => {
- const ruler = rulerRef.current;
- if (!ruler) return;
- const rect = ruler.getBoundingClientRect();
- const rawX = e.clientX - rect.left;
-
- // Get the timeline content width based on duration and zoom
- const timelineContentWidth = duration * 50 * zoomLevel; // TIMELINE_CONSTANTS.PIXELS_PER_SECOND = 50
-
- // Constrain x to be within the timeline content bounds
- const x = Math.max(0, Math.min(timelineContentWidth, rawX));
-
- const rawTime = Math.max(0, Math.min(duration, x / (50 * zoomLevel)));
- // Use frame snapping for playhead scrubbing
- const projectStore = useProjectStore.getState();
- const projectFps = projectStore.activeProject?.fps || DEFAULT_FPS;
- const time = snapTimeToFrame(rawTime, projectFps);
-
- // Debug logging
- if (rawX < 0 || x !== rawX) {
- console.log(
- "PLAYHEAD DEBUG:",
- JSON.stringify({
- mouseX: e.clientX,
- rulerLeft: rect.left,
- rawX,
- constrainedX: x,
- timelineContentWidth,
- rawTime,
- finalTime: time,
- duration,
- zoomLevel,
- playheadPx: time * 50 * zoomLevel,
- })
- );
- }
-
- setScrubTime(time);
- seek(time); // update video preview in real time
-
- // Store mouse position for auto-scrolling
- lastMouseXRef.current = e.clientX;
- },
- [duration, zoomLevel, seek, rulerRef]
- );
-
- useEdgeAutoScroll({
- isActive: isScrubbing,
- getMouseClientX: () => lastMouseXRef.current,
- rulerScrollRef,
- tracksScrollRef,
- contentWidth: duration * 50 * zoomLevel,
- });
-
- // Mouse move/up event handlers
- useEffect(() => {
- if (!isScrubbing) return;
-
- const onMouseMove = (e: MouseEvent) => {
- handleScrub(e);
- // Mark that we've dragged if ruler drag is active
- if (isDraggingRuler) {
- setHasDraggedRuler(true);
- }
- };
-
- const onMouseUp = (e: MouseEvent) => {
- setIsScrubbing(false);
- if (scrubTime !== null) seek(scrubTime); // finalize seek
- setScrubTime(null);
-
- // Handle ruler click vs drag
- if (isDraggingRuler) {
- setIsDraggingRuler(false);
- // If we didn't drag, treat it as a click-to-seek
- if (!hasDraggedRuler) {
- handleScrub(e);
- }
- setHasDraggedRuler(false);
- }
- };
-
- window.addEventListener("mousemove", onMouseMove);
- window.addEventListener("mouseup", onMouseUp);
-
- // Edge auto-scroll is handled by useEdgeAutoScroll
-
- return () => {
- window.removeEventListener("mousemove", onMouseMove);
- window.removeEventListener("mouseup", onMouseUp);
- // nothing to cleanup for edge auto scroll
- };
- }, [
- isScrubbing,
- scrubTime,
- seek,
- handleScrub,
- isDraggingRuler,
- hasDraggedRuler,
- // edge auto scroll hook is independent
- ]);
-
- // --- Playhead auto-scroll effect (only during playback) ---
- useEffect(() => {
- const { isPlaying } = usePlaybackStore.getState();
-
- // Only auto-scroll during playback, not during manual interactions
- if (!isPlaying || isScrubbing) return;
-
- const rulerViewport = rulerScrollRef.current;
- const tracksViewport = tracksScrollRef.current;
- if (!rulerViewport || !tracksViewport) return;
-
- const playheadPx = playheadPosition * 50 * zoomLevel; // TIMELINE_CONSTANTS.PIXELS_PER_SECOND = 50
- const viewportWidth = rulerViewport.clientWidth;
- const scrollMin = 0;
- const scrollMax = rulerViewport.scrollWidth - viewportWidth;
-
- // Only auto-scroll if playhead is completely out of view (no buffer)
- const needsScroll =
- playheadPx < rulerViewport.scrollLeft ||
- playheadPx > rulerViewport.scrollLeft + viewportWidth;
-
- if (needsScroll) {
- // Center the playhead in the viewport
- const desiredScroll = Math.max(
- scrollMin,
- Math.min(scrollMax, playheadPx - viewportWidth / 2)
- );
- rulerViewport.scrollLeft = tracksViewport.scrollLeft = desiredScroll;
- }
- }, [
- playheadPosition,
- duration,
- zoomLevel,
- rulerScrollRef,
- tracksScrollRef,
- isScrubbing,
- ]);
-
- return {
- playheadPosition,
- handlePlayheadMouseDown,
- handleRulerMouseDown,
- isDraggingRuler,
- };
-}
diff --git a/apps/web/src/hooks/use-timeline-snapping.ts b/apps/web/src/hooks/use-timeline-snapping.ts
deleted file mode 100644
index f7f96392..00000000
--- a/apps/web/src/hooks/use-timeline-snapping.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-import { useCallback } from "react";
-import { TimelineTrack } from "@/types/timeline";
-import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
-
-export interface SnapPoint {
- time: number;
- type: "element-start" | "element-end" | "playhead";
- elementId?: string;
- trackId?: string;
-}
-
-export interface SnapResult {
- snappedTime: number;
- snapPoint: SnapPoint | null;
- snapDistance: number;
-}
-
-export interface UseTimelineSnappingOptions {
- snapThreshold?: number; // Distance in pixels to trigger snapping
- enableElementSnapping?: boolean;
- enablePlayheadSnapping?: boolean;
-}
-
-export function useTimelineSnapping({
- snapThreshold = 10,
- enableElementSnapping = true,
- enablePlayheadSnapping = true,
-}: UseTimelineSnappingOptions = {}) {
- const findSnapPoints = useCallback(
- (
- tracks: TimelineTrack[],
- currentTime: number,
- playheadTime: number,
- zoomLevel: number,
- excludeElementId?: string
- ): SnapPoint[] => {
- const snapPoints: SnapPoint[] = [];
-
- // Add element snap points
- if (enableElementSnapping) {
- tracks.forEach((track) => {
- track.elements.forEach((element) => {
- // Skip the element being dragged
- if (element.id === excludeElementId) return;
-
- const elementStart = element.startTime;
- const elementEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
-
- snapPoints.push(
- {
- time: elementStart,
- type: "element-start",
- elementId: element.id,
- trackId: track.id,
- },
- {
- time: elementEnd,
- type: "element-end",
- elementId: element.id,
- trackId: track.id,
- }
- );
- });
- });
- }
-
- // Add playhead snap point
- if (enablePlayheadSnapping) {
- snapPoints.push({
- time: playheadTime,
- type: "playhead",
- });
- }
-
- return snapPoints;
- },
- [enableElementSnapping, enablePlayheadSnapping]
- );
-
- const snapToNearestPoint = useCallback(
- (
- targetTime: number,
- snapPoints: SnapPoint[],
- zoomLevel: number
- ): SnapResult => {
- const pixelsPerSecond = TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
- const thresholdInSeconds = snapThreshold / pixelsPerSecond;
-
- let closestSnapPoint: SnapPoint | null = null;
- let closestDistance = Infinity;
-
- snapPoints.forEach((snapPoint) => {
- const distance = Math.abs(targetTime - snapPoint.time);
- if (distance < thresholdInSeconds && distance < closestDistance) {
- closestDistance = distance;
- closestSnapPoint = snapPoint;
- }
- });
-
- return {
- snappedTime: closestSnapPoint
- ? (closestSnapPoint as SnapPoint).time
- : targetTime,
- snapPoint: closestSnapPoint,
- snapDistance: closestDistance,
- };
- },
- [snapThreshold]
- );
-
- const snapElementPosition = useCallback(
- (
- targetTime: number,
- tracks: TimelineTrack[],
- playheadTime: number,
- zoomLevel: number,
- excludeElementId?: string
- ): SnapResult => {
- const snapPoints = findSnapPoints(
- tracks,
- targetTime,
- playheadTime,
- zoomLevel,
- excludeElementId
- );
-
- return snapToNearestPoint(targetTime, snapPoints, zoomLevel);
- },
- [findSnapPoints, snapToNearestPoint]
- );
-
- const snapElementEdge = useCallback(
- (
- targetTime: number,
- elementDuration: number,
- tracks: TimelineTrack[],
- playheadTime: number,
- zoomLevel: number,
- excludeElementId?: string,
- snapToStart = true // true for start edge, false for end edge
- ): SnapResult => {
- const snapPoints = findSnapPoints(
- tracks,
- targetTime,
- playheadTime,
- zoomLevel,
- excludeElementId
- );
-
- // For end edge snapping, we need to account for element duration
- const effectiveTargetTime = snapToStart
- ? targetTime
- : targetTime + elementDuration;
- const snapResult = snapToNearestPoint(
- effectiveTargetTime,
- snapPoints,
- zoomLevel
- );
-
- // Adjust the snapped time back for end edge
- if (!snapToStart && snapResult.snapPoint) {
- snapResult.snappedTime = snapResult.snappedTime - elementDuration;
- }
-
- return snapResult;
- },
- [findSnapPoints, snapToNearestPoint]
- );
-
- return {
- snapElementPosition,
- snapElementEdge,
- findSnapPoints,
- snapToNearestPoint,
- };
-}
diff --git a/apps/web/src/hooks/use-timeline-zoom.ts b/apps/web/src/hooks/use-timeline-zoom.ts
deleted file mode 100644
index e764c550..00000000
--- a/apps/web/src/hooks/use-timeline-zoom.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { useState, useCallback, useEffect, RefObject } from "react";
-
-interface UseTimelineZoomProps {
- containerRef: RefObject<HTMLDivElement>;
- isInTimeline?: boolean;
-}
-
-interface UseTimelineZoomReturn {
- zoomLevel: number;
- setZoomLevel: (zoomLevel: number | ((prev: number) => number)) => void;
- handleWheel: (e: React.WheelEvent) => void;
-}
-
-export function useTimelineZoom({
- containerRef,
- isInTimeline = false,
-}: UseTimelineZoomProps): UseTimelineZoomReturn {
- const [zoomLevel, setZoomLevel] = useState(1);
-
- const handleWheel = useCallback((e: React.WheelEvent) => {
- // Only zoom if user is using pinch gesture (ctrlKey or metaKey is true)
- if (e.ctrlKey || e.metaKey) {
- e.preventDefault();
- const delta = e.deltaY > 0 ? -0.15 : 0.15;
- setZoomLevel((prev) => Math.max(0.1, Math.min(10, prev + delta)));
- }
- // For horizontal scrolling (when shift is held or horizontal wheel movement),
- // let the event bubble up to allow ScrollArea to handle it
- else if (e.shiftKey || Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
- // Don't prevent default - let ScrollArea handle horizontal scrolling
- return;
- }
- // Otherwise, allow normal scrolling
- }, []);
-
- // Prevent browser zooming in/out when in timeline
- useEffect(() => {
- const preventZoom = (e: WheelEvent) => {
- if (
- isInTimeline &&
- (e.ctrlKey || e.metaKey) &&
- containerRef.current?.contains(e.target as Node)
- ) {
- e.preventDefault();
- }
- };
-
- document.addEventListener("wheel", preventZoom, { passive: false });
-
- return () => {
- document.removeEventListener("wheel", preventZoom);
- };
- }, [isInTimeline, containerRef]);
-
- return {
- zoomLevel,
- setZoomLevel,
- handleWheel,
- };
-}
diff --git a/apps/web/src/hooks/use-toast.ts b/apps/web/src/hooks/use-toast.ts
deleted file mode 100644
index dcc02dcd..00000000
--- a/apps/web/src/hooks/use-toast.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-"use client";
-
-// Inspired by react-hot-toast library
-import * as React from "react";
-
-import type { ToastActionElement, ToastProps } from "../components/ui/toast";
-
-const TOAST_LIMIT = 1;
-const TOAST_REMOVE_DELAY = 1_000_000;
-
-type ToasterToast = ToastProps & {
- id: string;
- title?: React.ReactNode;
- description?: React.ReactNode;
- action?: ToastActionElement;
-};
-
-const actionTypes = {
- ADD_TOAST: "ADD_TOAST",
- UPDATE_TOAST: "UPDATE_TOAST",
- DISMISS_TOAST: "DISMISS_TOAST",
- REMOVE_TOAST: "REMOVE_TOAST",
-} as const;
-
-let count = 0;
-
-function genId() {
- count = (count + 1) % Number.MAX_SAFE_INTEGER;
- return count.toString();
-}
-
-type ActionType = typeof actionTypes;
-
-type Action =
- | {
- type: ActionType["ADD_TOAST"];
- toast: ToasterToast;
- }
- | {
- type: ActionType["UPDATE_TOAST"];
- toast: Partial<ToasterToast>;
- }
- | {
- type: ActionType["DISMISS_TOAST"];
- toastId?: ToasterToast["id"];
- }
- | {
- type: ActionType["REMOVE_TOAST"];
- toastId?: ToasterToast["id"];
- };
-
-interface State {
- toasts: ToasterToast[];
-}
-
-const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
-
-const addToRemoveQueue = (toastId: string) => {
- if (toastTimeouts.has(toastId)) {
- return;
- }
-
- const timeout = setTimeout(() => {
- toastTimeouts.delete(toastId);
- dispatch({
- type: "REMOVE_TOAST",
- toastId,
- });
- }, TOAST_REMOVE_DELAY);
-
- toastTimeouts.set(toastId, timeout);
-};
-
-export const reducer = (state: State, action: Action): State => {
- switch (action.type) {
- case "ADD_TOAST":
- return {
- ...state,
- toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
- };
-
- case "UPDATE_TOAST":
- return {
- ...state,
- toasts: state.toasts.map((t) =>
- t.id === action.toast.id ? { ...t, ...action.toast } : t
- ),
- };
-
- case "DISMISS_TOAST": {
- const { toastId } = action;
-
- // ! Side effects ! - This could be extracted into a dismissToast() action,
- // but I'll keep it here for simplicity
- if (toastId) {
- addToRemoveQueue(toastId);
- } else {
- state.toasts.forEach((toast) => {
- addToRemoveQueue(toast.id);
- });
- }
-
- return {
- ...state,
- toasts: state.toasts.map((t) =>
- t.id === toastId || toastId === undefined
- ? {
- ...t,
- open: false,
- }
- : t
- ),
- };
- }
- case "REMOVE_TOAST":
- if (action.toastId === undefined) {
- return {
- ...state,
- toasts: [],
- };
- }
- return {
- ...state,
- toasts: state.toasts.filter((t) => t.id !== action.toastId),
- };
- }
-};
-
-const listeners: Array<(state: State) => void> = [];
-
-let memoryState: State = { toasts: [] };
-
-function dispatch(action: Action) {
- memoryState = reducer(memoryState, action);
- listeners.forEach((listener) => {
- listener(memoryState);
- });
-}
-
-type Toast = Omit<ToasterToast, "id">;
-
-function toast({ ...props }: Toast) {
- const id = genId();
-
- const update = (props: ToasterToast) =>
- dispatch({
- type: "UPDATE_TOAST",
- toast: { ...props, id },
- });
- const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
-
- dispatch({
- type: "ADD_TOAST",
- toast: {
- ...props,
- id,
- open: true,
- onOpenChange: (open) => {
- if (!open) dismiss();
- },
- },
- });
-
- return {
- id,
- dismiss,
- update,
- };
-}
-
-function useToast() {
- const [state, setState] = React.useState<State>(memoryState);
-
- React.useEffect(() => {
- listeners.push(setState);
- return () => {
- const index = listeners.indexOf(setState);
- if (index > -1) {
- listeners.splice(index, 1);
- }
- };
- }, [state]);
-
- return {
- ...state,
- toast,
- dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
- };
-}
-
-export { useToast, toast };
diff --git a/apps/web/src/lib/actions/definitions.ts b/apps/web/src/lib/actions/definitions.ts
new file mode 100644
index 00000000..83144a25
--- /dev/null
+++ b/apps/web/src/lib/actions/definitions.ts
@@ -0,0 +1,162 @@
+import type { ShortcutKey } from "@/types/keybinding";
+
+export type TActionCategory =
+ | "playback"
+ | "navigation"
+ | "editing"
+ | "selection"
+ | "history"
+ | "timeline"
+ | "controls";
+
+export interface TActionDefinition {
+ description: string;
+ category: TActionCategory;
+ defaultShortcuts?: ShortcutKey[];
+ args?: Record<string, unknown>;
+}
+
+export const ACTIONS = {
+ "toggle-play": {
+ description: "Play/Pause",
+ category: "playback",
+ defaultShortcuts: ["space", "k"],
+ },
+ "stop-playback": {
+ description: "Stop playback",
+ category: "playback",
+ },
+ "seek-forward": {
+ description: "Seek forward 1 second",
+ category: "playback",
+ defaultShortcuts: ["l"],
+ args: { seconds: "number" },
+ },
+ "seek-backward": {
+ description: "Seek backward 1 second",
+ category: "playback",
+ defaultShortcuts: ["j"],
+ args: { seconds: "number" },
+ },
+ "frame-step-forward": {
+ description: "Frame step forward",
+ category: "navigation",
+ defaultShortcuts: ["right"],
+ },
+ "frame-step-backward": {
+ description: "Frame step backward",
+ category: "navigation",
+ defaultShortcuts: ["left"],
+ },
+ "jump-forward": {
+ description: "Jump forward 5 seconds",
+ category: "navigation",
+ defaultShortcuts: ["shift+right"],
+ args: { seconds: "number" },
+ },
+ "jump-backward": {
+ description: "Jump backward 5 seconds",
+ category: "navigation",
+ defaultShortcuts: ["shift+left"],
+ args: { seconds: "number" },
+ },
+ "goto-start": {
+ description: "Go to timeline start",
+ category: "navigation",
+ defaultShortcuts: ["home", "enter"],
+ },
+ "goto-end": {
+ description: "Go to timeline end",
+ category: "navigation",
+ defaultShortcuts: ["end"],
+ },
+ split: {
+ description: "Split elements at playhead",
+ category: "editing",
+ defaultShortcuts: ["s"],
+ },
+ "split-left": {
+ description: "Split and remove left",
+ category: "editing",
+ defaultShortcuts: ["q"],
+ },
+ "split-right": {
+ description: "Split and remove right",
+ category: "editing",
+ defaultShortcuts: ["w"],
+ },
+ "delete-selected": {
+ description: "Delete selected elements",
+ category: "editing",
+ defaultShortcuts: ["backspace", "delete"],
+ },
+ "copy-selected": {
+ description: "Copy selected elements",
+ category: "editing",
+ defaultShortcuts: ["ctrl+c"],
+ },
+ "paste-copied": {
+ description: "Paste elements at playhead",
+ category: "editing",
+ defaultShortcuts: ["ctrl+v"],
+ },
+ "toggle-snapping": {
+ description: "Toggle snapping",
+ category: "editing",
+ defaultShortcuts: ["n"],
+ },
+ "select-all": {
+ description: "Select all elements",
+ category: "selection",
+ defaultShortcuts: ["ctrl+a"],
+ },
+ "duplicate-selected": {
+ description: "Duplicate selected element",
+ category: "selection",
+ defaultShortcuts: ["ctrl+d"],
+ },
+ "toggle-elements-muted-selected": {
+ description: "Mute/unmute selected elements",
+ category: "selection",
+ },
+ "toggle-elements-visibility-selected": {
+ description: "Show/hide selected elements",
+ category: "selection",
+ },
+ "toggle-bookmark": {
+ description: "Toggle bookmark at playhead",
+ category: "timeline",
+ },
+ undo: {
+ description: "Undo",
+ category: "history",
+ defaultShortcuts: ["ctrl+z"],
+ },
+ redo: {
+ description: "Redo",
+ category: "history",
+ defaultShortcuts: ["ctrl+shift+z", "ctrl+y"],
+ },
+} as const satisfies Record<string, TActionDefinition>;
+
+export type TAction = keyof typeof ACTIONS;
+
+export function getActionDefinition(action: TAction): TActionDefinition {
+ return ACTIONS[action];
+}
+
+export function getDefaultShortcuts(): Record<ShortcutKey, TAction> {
+ const shortcuts: Record<string, TAction> = {};
+
+ for (const [action, def] of Object.entries(ACTIONS) as Array<
+ [TAction, TActionDefinition]
+ >) {
+ if (def.defaultShortcuts) {
+ for (const shortcut of def.defaultShortcuts) {
+ shortcuts[shortcut] = action;
+ }
+ }
+ }
+
+ return shortcuts as Record<ShortcutKey, TAction>;
+}
diff --git a/apps/web/src/lib/actions/index.ts b/apps/web/src/lib/actions/index.ts
new file mode 100644
index 00000000..801a846a
--- /dev/null
+++ b/apps/web/src/lib/actions/index.ts
@@ -0,0 +1,3 @@
+export * from "./definitions";
+export * from "./types";
+export * from "./registry";
diff --git a/apps/web/src/lib/actions/registry.ts b/apps/web/src/lib/actions/registry.ts
new file mode 100644
index 00000000..b9c84529
--- /dev/null
+++ b/apps/web/src/lib/actions/registry.ts
@@ -0,0 +1,61 @@
+import type {
+ TAction,
+ TActionFunc,
+ TActionWithArgs,
+ TActionWithOptionalArgs,
+ TActionArgsMap,
+ TArgOfAction,
+ TInvocationTrigger,
+} from "./types";
+
+type ActionHandler = (arg: unknown, trigger?: TInvocationTrigger) => void;
+const boundActions: Partial<Record<TAction, ActionHandler[]>> = {};
+
+export function bindAction<A extends TAction>(
+ action: A,
+ handler: TActionFunc<A>,
+) {
+ const handlers = boundActions[action];
+ const typedHandler = handler as ActionHandler;
+ if (handlers) {
+ handlers.push(typedHandler);
+ } else {
+ boundActions[action] = [typedHandler];
+ }
+}
+
+export function unbindAction<A extends TAction>(
+ action: A,
+ handler: TActionFunc<A>,
+) {
+ const handlers = boundActions[action];
+ if (!handlers) return;
+
+ const typedHandler = handler as ActionHandler;
+ boundActions[action] = handlers.filter((h) => h !== typedHandler);
+
+ if (boundActions[action]?.length === 0) {
+ delete boundActions[action];
+ }
+}
+
+type InvokeActionFunc = {
+ (
+ action: TActionWithOptionalArgs,
+ args?: undefined,
+ trigger?: TInvocationTrigger,
+ ): void;
+ <A extends TActionWithArgs>(
+ action: A,
+ args: TActionArgsMap[A],
+ trigger?: TInvocationTrigger,
+ ): void;
+};
+
+export const invokeAction: InvokeActionFunc = <A extends TAction>(
+ action: A,
+ args?: TArgOfAction<A>,
+ trigger?: TInvocationTrigger,
+) => {
+ boundActions[action]?.forEach((handler) => handler(args, trigger));
+};
diff --git a/apps/web/src/lib/actions/types.ts b/apps/web/src/lib/actions/types.ts
new file mode 100644
index 00000000..508bf10e
--- /dev/null
+++ b/apps/web/src/lib/actions/types.ts
@@ -0,0 +1,42 @@
+import type { MutableRefObject } from "react";
+import type { TAction } from "./definitions";
+
+export type { TAction };
+
+export type TActionArgsMap = {
+ "seek-forward": { seconds: number } | undefined;
+ "seek-backward": { seconds: number } | undefined;
+ "jump-forward": { seconds: number } | undefined;
+ "jump-backward": { seconds: number } | undefined;
+};
+
+type TKeysWithValueUndefined<T> = {
+ [K in keyof T]: undefined extends T[K] ? K : never;
+}[keyof T];
+
+export type TActionWithArgs = keyof TActionArgsMap;
+
+export type TActionWithOptionalArgs =
+ | TActionWithNoArgs
+ | TKeysWithValueUndefined<TActionArgsMap>;
+
+export type TActionWithNoArgs = Exclude<TAction, TActionWithArgs>;
+
+export type TArgOfAction<A extends TAction> = A extends TActionWithArgs
+ ? TActionArgsMap[A]
+ : undefined;
+
+export type TActionFunc<A extends TAction> = A extends TActionWithArgs
+ ? (arg: TArgOfAction<A>, trigger?: TInvocationTrigger) => void
+ : (_?: undefined, trigger?: TInvocationTrigger) => void;
+
+export type TInvocationTrigger = "keypress" | "mouseclick";
+
+export type TBoundActionList = {
+ [A in TAction]?: Array<TActionFunc<A>>;
+};
+
+export type TActionHandlerOptions =
+ | MutableRefObject<boolean>
+ | boolean
+ | undefined;
diff --git a/apps/web/src/lib/auth/client.ts b/apps/web/src/lib/auth/client.ts
new file mode 100644
index 00000000..63b441f7
--- /dev/null
+++ b/apps/web/src/lib/auth/client.ts
@@ -0,0 +1,6 @@
+import { createAuthClient } from "better-auth/react";
+import { webEnv } from "@opencut/env/web";
+
+export const { signIn, signUp, useSession } = createAuthClient({
+ baseURL: webEnv.NEXT_PUBLIC_SITE_URL,
+});
diff --git a/apps/web/src/lib/auth/server.ts b/apps/web/src/lib/auth/server.ts
new file mode 100644
index 00000000..978d7383
--- /dev/null
+++ b/apps/web/src/lib/auth/server.ts
@@ -0,0 +1,43 @@
+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 "@opencut/env/web";
+
+const redis = new Redis({
+ url: webEnv.UPSTASH_REDIS_REST_URL,
+ token: webEnv.UPSTASH_REDIS_REST_TOKEN,
+});
+
+export const auth = betterAuth({
+ database: drizzleAdapter(db, {
+ provider: "pg",
+ usePlural: true,
+ }),
+ secret: webEnv.BETTER_AUTH_SECRET,
+ user: {
+ deleteUser: {
+ enabled: true,
+ },
+ },
+ emailAndPassword: {
+ enabled: true,
+ },
+ rateLimit: {
+ storage: "secondary-storage",
+ customStorage: {
+ get: async (key) => {
+ const value = await redis.get(key);
+ return value as RateLimit | undefined;
+ },
+ set: async (key, value) => {
+ await redis.set(key, value);
+ },
+ },
+ },
+ baseURL: webEnv.NEXT_PUBLIC_SITE_URL,
+ appName: "OpenCut",
+ trustedOrigins: [webEnv.NEXT_PUBLIC_SITE_URL],
+});
+
+export type Auth = typeof auth;
diff --git a/apps/web/src/lib/blog-query.ts b/apps/web/src/lib/blog-query.ts
deleted file mode 100644
index 5cba0cf3..00000000
--- a/apps/web/src/lib/blog-query.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import type {
- MarbleAuthorList,
- MarbleCategoryList,
- MarblePost,
- MarblePostList,
- MarbleTagList,
-} from "@/types/post";
-import { unified } from "unified";
-import rehypeParse from "rehype-parse";
-import rehypeStringify from "rehype-stringify";
-import rehypeSlug from "rehype-slug";
-import rehypeAutolinkHeadings from "rehype-autolink-headings";
-import rehypeSanitize from "rehype-sanitize";
-
-const url =
- process.env.NEXT_PUBLIC_MARBLE_API_URL ?? "https://api.marblecms.com";
-const key = process.env.MARBLE_WORKSPACE_KEY ?? "cmd4iw9mm0006l804kwqv0k46";
-
-async function fetchFromMarble<T>(endpoint: string): Promise<T> {
- try {
- const response = await fetch(`${url}/${key}/${endpoint}`);
- if (!response.ok) {
- throw new Error(
- `Failed to fetch ${endpoint}: ${response.status} ${response.statusText}`
- );
- }
- return (await response.json()) as T;
- } catch (error) {
- console.error(`Error fetching ${endpoint}:`, error);
- throw error;
- }
-}
-
-export async function getPosts() {
- return fetchFromMarble<MarblePostList>("posts");
-}
-
-export async function getTags() {
- return fetchFromMarble<MarbleTagList>("tags");
-}
-
-export async function getSinglePost(slug: string) {
- return fetchFromMarble<MarblePost>(`posts/${slug}`);
-}
-
-export async function getCategories() {
- return fetchFromMarble<MarbleCategoryList>("categories");
-}
-
-export async function getAuthors() {
- return fetchFromMarble<MarbleAuthorList>("authors");
-}
-
-export async function processHtmlContent(html: string): Promise<string> {
- const processor = unified()
- .use(rehypeSanitize)
- .use(rehypeParse, { fragment: true })
- .use(rehypeSlug)
- .use(rehypeAutolinkHeadings, { behavior: "append" })
- .use(rehypeStringify);
-
- const file = await processor.process(html);
- return String(file);
-}
diff --git a/apps/web/src/lib/blog/query.ts b/apps/web/src/lib/blog/query.ts
new file mode 100644
index 00000000..e6d68bb6
--- /dev/null
+++ b/apps/web/src/lib/blog/query.ts
@@ -0,0 +1,72 @@
+import type {
+ MarbleAuthorList,
+ MarbleCategoryList,
+ MarblePost,
+ MarblePostList,
+ MarbleTagList,
+} from "@/types/blog";
+import { unified } from "unified";
+import rehypeParse from "rehype-parse";
+import rehypeStringify from "rehype-stringify";
+import rehypeSlug from "rehype-slug";
+import rehypeAutolinkHeadings from "rehype-autolink-headings";
+import rehypeSanitize from "rehype-sanitize";
+
+const url =
+ process.env.NEXT_PUBLIC_MARBLE_API_URL ?? "https://api.marblecms.com";
+const key = process.env.MARBLE_WORKSPACE_KEY ?? "cmd4iw9mm0006l804kwqv0k46";
+
+async function fetchFromMarble<T>({
+ endpoint,
+}: {
+ endpoint: string;
+}): Promise<T> {
+ try {
+ const response = await fetch(`${url}/${key}/${endpoint}`);
+ if (!response.ok) {
+ throw new Error(
+ `Failed to fetch ${endpoint}: ${response.status} ${response.statusText}`,
+ );
+ }
+ return (await response.json()) as T;
+ } catch (error) {
+ console.error(`Error fetching ${endpoint}:`, error);
+ throw error;
+ }
+}
+
+export async function getPosts() {
+ return fetchFromMarble<MarblePostList>({ endpoint: "posts" });
+}
+
+export async function getTags() {
+ return fetchFromMarble<MarbleTagList>({ endpoint: "tags" });
+}
+
+export async function getSinglePost({ slug }: { slug: string }) {
+ return fetchFromMarble<MarblePost>({ endpoint: `posts/${slug}` });
+}
+
+export async function getCategories() {
+ return fetchFromMarble<MarbleCategoryList>({ endpoint: "categories" });
+}
+
+export async function getAuthors() {
+ return fetchFromMarble<MarbleAuthorList>({ endpoint: "authors" });
+}
+
+export async function processHtmlContent({
+ html,
+}: {
+ html: string;
+}): Promise<string> {
+ const processor = unified()
+ .use(rehypeSanitize)
+ .use(rehypeParse, { fragment: true })
+ .use(rehypeSlug)
+ .use(rehypeAutolinkHeadings, { behavior: "append" })
+ .use(rehypeStringify);
+
+ const file = await processor.process({ value: html, type: "html" });
+ return String(file);
+}
diff --git a/apps/web/src/lib/canvas-gradients.ts b/apps/web/src/lib/canvas-gradients.ts
deleted file mode 100644
index 7d1cdc05..00000000
--- a/apps/web/src/lib/canvas-gradients.ts
+++ /dev/null
@@ -1,219 +0,0 @@
-function splitBackgroundLayers(input: string): string[] {
- const layers: string[] = [];
- let depth = 0;
- let start = 0;
- for (let i = 0; i < input.length; i += 1) {
- const ch = input[i] as string;
- if (ch === "(") depth += 1;
- else if (ch === ")") depth -= 1;
- else if (ch === "," && depth === 0) {
- layers.push(input.slice(start, i).trim());
- start = i + 1;
- }
- }
- layers.push(input.slice(start).trim());
- return layers;
-}
-
-function parseColorStop(stop: string): { color: string; position?: number } {
- const s = stop.trim();
-
- // Handle functional colors like rgba(), rgb(), hsla(), hsl()
- const colorFunctions = ["rgba(", "rgb(", "hsla(", "hsl("];
- let color = "";
- let remaining = "";
-
- for (const fn of colorFunctions) {
- if (s.startsWith(fn)) {
- let depth = 0;
- for (let i = 0; i < s.length; i += 1) {
- const ch = s[i] as string;
- if (ch === "(") depth += 1;
- else if (ch === ")") {
- depth -= 1;
- if (depth === 0) {
- color = s.slice(0, i + 1);
- remaining = s.slice(i + 1).trim();
- break;
- }
- }
- }
- break;
- }
- }
-
- if (!color) {
- const parts = s.split(/\s+/);
- color = parts[0] as string;
- remaining = parts.slice(1).join(" ");
- }
-
- // Convert transparent to transparent white for better blending
- if (color === "transparent") {
- color = "rgba(255, 255, 255, 0)";
- }
-
- // Parse position if present
- let position: number | undefined;
- if (remaining) {
- const posMatch = remaining.match(/(\d+(?:\.\d+)?)%/);
- if (posMatch) {
- position = parseFloat(posMatch[1] as string) / 100;
- }
- }
-
- return { color, position };
-}
-
-function parseLinearGradient(layer: string, width: number, height: number) {
- const inside = layer.slice(layer.indexOf("(") + 1, layer.lastIndexOf(")"));
- const parts = splitBackgroundLayers(inside);
- const dir = (parts.shift() || "").trim();
-
- let x0 = 0,
- y0 = 0,
- x1 = width,
- y1 = 0; // default: to right
-
- if (dir.endsWith("deg")) {
- const deg = parseFloat(dir);
- const rad = (deg * Math.PI) / 180;
- const cx = width / 2;
- const cy = height / 2;
- const r = Math.hypot(width, height) / 2;
- x0 = cx - Math.cos(rad) * r;
- y0 = cy - Math.sin(rad) * r;
- x1 = cx + Math.cos(rad) * r;
- y1 = cy + Math.sin(rad) * r;
- } else if (dir.startsWith("to ")) {
- const d = dir.slice(3).trim();
- if (d === "right") {
- x0 = 0;
- y0 = 0;
- x1 = width;
- y1 = 0;
- } else if (d === "left") {
- x0 = width;
- y0 = 0;
- x1 = 0;
- y1 = 0;
- } else if (d === "bottom") {
- x0 = 0;
- y0 = 0;
- x1 = 0;
- y1 = height;
- } else if (d === "top") {
- x0 = 0;
- y0 = height;
- x1 = 0;
- y1 = 0;
- }
- } else {
- // No direction specified, treat as first color
- parts.unshift(dir);
- }
-
- return { x0, y0, x1, y1, colors: parts };
-}
-
-function parseRadialGradient(layer: string, width: number, height: number) {
- const inside = layer.slice(layer.indexOf("(") + 1, layer.lastIndexOf(")"));
- const parts = splitBackgroundLayers(inside);
- const first = (parts.shift() || "").trim();
-
- let cx = width / 2;
- let cy = height / 2;
-
- if (first.startsWith("circle at")) {
- const pos = first.replace("circle at", "").trim();
- const coords = pos.split(/\s+/);
-
- for (let i = 0; i < coords.length; i += 1) {
- const coord = coords[i] as string;
- if (coord.endsWith("%")) {
- const val = parseFloat(coord) / 100;
- if (i === 0) cx = val * width;
- else if (i === 1) cy = val * height;
- } else if (coord === "left") cx = 0;
- else if (coord === "right") cx = width;
- else if (coord === "top") cy = 0;
- else if (coord === "bottom") cy = height;
- else if (coord === "center") {
- if (i === 0) cx = width / 2;
- else if (i === 1) cy = height / 2;
- }
- }
- } else {
- parts.unshift(first);
- }
-
- // Use farthest-corner for radius
- const r = Math.max(
- Math.hypot(cx, cy),
- Math.hypot(width - cx, cy),
- Math.hypot(cx, height - cy),
- Math.hypot(width - cx, height - cy)
- );
-
- return { cx, cy, r, colors: parts };
-}
-
-export function drawCssBackground(
- ctx: CanvasRenderingContext2D,
- width: number,
- height: number,
- css: string
-): void {
- const layers = splitBackgroundLayers(css).filter(Boolean);
-
- // Draw layers from last to first (CSS background order)
- for (let i = layers.length - 1; i >= 0; i -= 1) {
- const layer = layers[i] as string;
-
- if (layer.startsWith("linear-gradient(")) {
- const { x0, y0, x1, y1, colors } = parseLinearGradient(
- layer,
- width,
- height
- );
- const grad = ctx.createLinearGradient(x0, y0, x1, y1);
- const colorStops = colors.map((c) => parseColorStop(c as string));
-
- // Handle positions properly
- for (let j = 0; j < colorStops.length; j += 1) {
- const stop = colorStops[j] as { color: string; position?: number };
- const pos = stop.position ?? j / Math.max(1, colorStops.length - 1);
- grad.addColorStop(Math.max(0, Math.min(1, pos)), stop.color);
- }
-
- ctx.fillStyle = grad;
- ctx.fillRect(0, 0, width, height);
- } else if (layer.startsWith("radial-gradient(")) {
- const { cx, cy, r, colors } = parseRadialGradient(layer, width, height);
- const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
- const colorStops = colors.map((c) => parseColorStop(c as string));
-
- // Handle positions properly
- for (let j = 0; j < colorStops.length; j += 1) {
- const stop = colorStops[j] as { color: string; position?: number };
- const pos = stop.position ?? j / Math.max(1, colorStops.length - 1);
- grad.addColorStop(Math.max(0, Math.min(1, pos)), stop.color);
- }
-
- ctx.fillStyle = grad;
- ctx.fillRect(0, 0, width, height);
- } else if (
- layer.startsWith("#") ||
- layer.startsWith("rgb") ||
- layer.startsWith("hsl") ||
- layer === "transparent" ||
- layer === "white" ||
- layer === "black"
- ) {
- if (layer !== "transparent") {
- ctx.fillStyle = layer;
- ctx.fillRect(0, 0, width, height);
- }
- }
- }
-}
diff --git a/apps/web/src/lib/commands/base-command.ts b/apps/web/src/lib/commands/base-command.ts
new file mode 100644
index 00000000..66062c52
--- /dev/null
+++ b/apps/web/src/lib/commands/base-command.ts
@@ -0,0 +1,11 @@
+export abstract class Command {
+ abstract execute(): void;
+
+ undo(): void {
+ throw new Error("Undo not implemented for this command");
+ }
+
+ redo(): void {
+ this.execute();
+ }
+}
diff --git a/apps/web/src/lib/commands/index.ts b/apps/web/src/lib/commands/index.ts
new file mode 100644
index 00000000..6700087b
--- /dev/null
+++ b/apps/web/src/lib/commands/index.ts
@@ -0,0 +1,6 @@
+export { Command } from "./base-command";
+
+export * from "./timeline";
+export * from "./media";
+export * from "./scene";
+export * from "./project";
diff --git a/apps/web/src/lib/commands/media/add-media-asset.ts b/apps/web/src/lib/commands/media/add-media-asset.ts
new file mode 100644
index 00000000..d947cb97
--- /dev/null
+++ b/apps/web/src/lib/commands/media/add-media-asset.ts
@@ -0,0 +1,61 @@
+import { Command } from "@/lib/commands/base-command";
+import { EditorCore } from "@/core";
+import type { MediaAsset } from "@/types/assets";
+import { generateUUID } from "@/utils/id";
+import { storageService } from "@/services/storage/storage-service";
+
+export class AddMediaAssetCommand extends Command {
+ private assetId: string;
+ private savedAssets: MediaAsset[] | null = null;
+ private createdAsset: MediaAsset | null = null;
+
+ constructor(
+ private projectId: string,
+ private asset: Omit<MediaAsset, "id">,
+ ) {
+ super();
+ this.assetId = generateUUID();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedAssets = [...editor.media.getAssets()];
+
+ this.createdAsset = {
+ ...this.asset,
+ id: this.assetId,
+ };
+
+ editor.media.setAssets({
+ assets: [...this.savedAssets, this.createdAsset],
+ });
+
+ storageService
+ .saveMediaAsset({
+ projectId: this.projectId,
+ mediaAsset: this.createdAsset,
+ })
+ .catch((error) => {
+ console.error("Failed to save media item:", error);
+ });
+ }
+
+ undo(): void {
+ if (this.savedAssets) {
+ const editor = EditorCore.getInstance();
+ editor.media.setAssets({ assets: this.savedAssets });
+
+ if (this.createdAsset) {
+ storageService
+ .deleteMediaAsset({ projectId: this.projectId, id: this.assetId })
+ .catch((error) => {
+ console.error("Failed to delete media item on undo:", error);
+ });
+ }
+ }
+ }
+
+ getAssetId(): string {
+ return this.assetId;
+ }
+}
diff --git a/apps/web/src/lib/commands/media/index.ts b/apps/web/src/lib/commands/media/index.ts
new file mode 100644
index 00000000..466222f2
--- /dev/null
+++ b/apps/web/src/lib/commands/media/index.ts
@@ -0,0 +1,2 @@
+export { AddMediaAssetCommand } from "./add-media-asset";
+export { RemoveMediaAssetCommand } from "./remove-media-asset";
diff --git a/apps/web/src/lib/commands/media/remove-media-asset.ts b/apps/web/src/lib/commands/media/remove-media-asset.ts
new file mode 100644
index 00000000..aa1a5775
--- /dev/null
+++ b/apps/web/src/lib/commands/media/remove-media-asset.ts
@@ -0,0 +1,85 @@
+import { Command } from "@/lib/commands/base-command";
+import { EditorCore } from "@/core";
+import type { MediaAsset } from "@/types/assets";
+import { storageService } from "@/services/storage/storage-service";
+import { videoCache } from "@/services/media/video-cache";
+import { hasMediaId } from "@/lib/timeline/element-utils";
+import type { TimelineTrack } from "@/types/timeline";
+
+export class RemoveMediaAssetCommand extends Command {
+ private savedAssets: MediaAsset[] | null = null;
+ private savedTracks: TimelineTrack[] | null = null;
+ private removedAsset: MediaAsset | null = null;
+
+ constructor(
+ private projectId: string,
+ private assetId: string,
+ ) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ const assets = editor.media.getAssets();
+
+ this.savedAssets = [...assets];
+ this.savedTracks = editor.timeline.getTracks();
+
+ this.removedAsset =
+ assets.find((media) => media.id === this.assetId) ?? null;
+
+ if (!this.removedAsset) {
+ console.error("Media asset not found:", this.assetId);
+ return;
+ }
+
+ videoCache.clearVideo({ mediaId: this.assetId });
+
+ editor.media.setAssets({
+ assets: assets.filter((media) => media.id !== this.assetId),
+ });
+
+ const elementsToRemove: Array<{ trackId: string; elementId: string }> = [];
+
+ for (const track of this.savedTracks) {
+ for (const element of track.elements) {
+ if (hasMediaId(element) && element.mediaId === this.assetId) {
+ elementsToRemove.push({ trackId: track.id, elementId: element.id });
+ }
+ }
+ }
+
+ if (elementsToRemove.length > 0) {
+ editor.timeline.deleteElements({ elements: elementsToRemove });
+ }
+
+ storageService
+ .deleteMediaAsset({ projectId: this.projectId, id: this.assetId })
+ .catch((error) => {
+ console.error("Failed to delete media item:", error);
+ });
+ }
+
+ undo(): void {
+ const editor = EditorCore.getInstance();
+
+ if (this.savedAssets) {
+ editor.media.setAssets({ assets: this.savedAssets });
+ }
+
+ if (this.savedTracks) {
+ editor.timeline.updateTracks(this.savedTracks);
+ }
+
+ if (this.removedAsset) {
+ storageService
+ .saveMediaAsset({
+ projectId: this.projectId,
+ mediaAsset: this.removedAsset,
+ })
+ .catch((error) => {
+ console.error("Failed to restore media item on undo:", error);
+ });
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/project/index.ts b/apps/web/src/lib/commands/project/index.ts
new file mode 100644
index 00000000..a9f26edc
--- /dev/null
+++ b/apps/web/src/lib/commands/project/index.ts
@@ -0,0 +1 @@
+export * from "./update-project-settings";
diff --git a/apps/web/src/lib/commands/project/update-project-settings.ts b/apps/web/src/lib/commands/project/update-project-settings.ts
new file mode 100644
index 00000000..b7ab75ee
--- /dev/null
+++ b/apps/web/src/lib/commands/project/update-project-settings.ts
@@ -0,0 +1,46 @@
+import { Command } from "@/lib/commands/base-command";
+import { EditorCore } from "@/core";
+import type { TProject, TProjectSettings } from "@/types/project";
+
+export class UpdateProjectSettingsCommand extends Command {
+ private savedSettings: TProjectSettings | null = null;
+ private savedUpdatedAt: Date | null = null;
+
+ constructor(private updates: Partial<TProjectSettings>) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ const activeProject = editor.project.getActive();
+ if (!activeProject) return;
+
+ this.savedSettings = activeProject.settings;
+ this.savedUpdatedAt = activeProject.metadata.updatedAt;
+
+ const updatedProject: TProject = {
+ ...activeProject,
+ settings: { ...activeProject.settings, ...this.updates },
+ metadata: { ...activeProject.metadata, updatedAt: new Date() },
+ };
+
+ editor.project.setActiveProject({ project: updatedProject });
+ editor.save.markDirty();
+ }
+
+ undo(): void {
+ if (!this.savedSettings || !this.savedUpdatedAt) return;
+ const editor = EditorCore.getInstance();
+ const activeProject = editor.project.getActive();
+ if (!activeProject) return;
+
+ const updatedProject: TProject = {
+ ...activeProject,
+ settings: this.savedSettings,
+ metadata: { ...activeProject.metadata, updatedAt: this.savedUpdatedAt },
+ };
+
+ editor.project.setActiveProject({ project: updatedProject });
+ editor.save.markDirty();
+ }
+}
diff --git a/apps/web/src/lib/commands/scene/create-scene.ts b/apps/web/src/lib/commands/scene/create-scene.ts
new file mode 100644
index 00000000..5fea5d53
--- /dev/null
+++ b/apps/web/src/lib/commands/scene/create-scene.ts
@@ -0,0 +1,40 @@
+import { Command } from "@/lib/commands/base-command";
+import { EditorCore } from "@/core";
+import type { TScene } from "@/types/timeline";
+import { buildDefaultScene } from "@/lib/scenes";
+
+export class CreateSceneCommand extends Command {
+ private savedScenes: TScene[] | null = null;
+ private createdScene: TScene | null = null;
+
+ constructor(
+ private name: string,
+ private isMain: boolean = false,
+ ) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedScenes = [...editor.scenes.getScenes()];
+
+ this.createdScene = buildDefaultScene({
+ name: this.name,
+ isMain: this.isMain,
+ });
+
+ const updatedScenes = [...this.savedScenes, this.createdScene];
+ editor.scenes.setScenes({ scenes: updatedScenes });
+ }
+
+ undo(): void {
+ if (this.savedScenes) {
+ const editor = EditorCore.getInstance();
+ editor.scenes.setScenes({ scenes: this.savedScenes });
+ }
+ }
+
+ getSceneId(): string {
+ return this.createdScene?.id ?? "";
+ }
+}
diff --git a/apps/web/src/lib/commands/scene/delete-scene.ts b/apps/web/src/lib/commands/scene/delete-scene.ts
new file mode 100644
index 00000000..83066e24
--- /dev/null
+++ b/apps/web/src/lib/commands/scene/delete-scene.ts
@@ -0,0 +1,59 @@
+import { Command } from "@/lib/commands/base-command";
+import { EditorCore } from "@/core";
+import type { TScene } from "@/types/timeline";
+import { canDeleteScene, getFallbackSceneAfterDelete } from "@/lib/scenes";
+
+export class DeleteSceneCommand extends Command {
+ private savedScenes: TScene[] | null = null;
+ private savedActiveSceneId: string | null = null;
+ private deletedScene: TScene | null = null;
+
+ constructor(private sceneId: string) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ const scenes = editor.scenes.getScenes();
+ const activeScene = editor.scenes.getActiveScene();
+
+ this.savedScenes = [...scenes];
+ this.savedActiveSceneId = activeScene?.id ?? null;
+
+ this.deletedScene = scenes.find((s) => s.id === this.sceneId) ?? null;
+
+ if (!this.deletedScene) {
+ console.error("Scene not found:", this.sceneId);
+ return;
+ }
+
+ const { canDelete, reason } = canDeleteScene({ scene: this.deletedScene });
+ if (!canDelete) {
+ console.error("Cannot delete scene:", reason);
+ return;
+ }
+
+ const updatedScenes = scenes.filter((s) => s.id !== this.sceneId);
+
+ const newActiveScene = getFallbackSceneAfterDelete({
+ scenes: updatedScenes,
+ deletedSceneId: this.sceneId,
+ currentSceneId: activeScene?.id ?? null,
+ });
+
+ editor.scenes.setScenes({
+ scenes: updatedScenes,
+ activeSceneId: newActiveScene?.id,
+ });
+ }
+
+ undo(): void {
+ if (this.savedScenes && this.deletedScene) {
+ const editor = EditorCore.getInstance();
+ editor.scenes.setScenes({
+ scenes: this.savedScenes,
+ activeSceneId: this.savedActiveSceneId ?? undefined,
+ });
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/scene/index.ts b/apps/web/src/lib/commands/scene/index.ts
new file mode 100644
index 00000000..5bf9406a
--- /dev/null
+++ b/apps/web/src/lib/commands/scene/index.ts
@@ -0,0 +1,5 @@
+export { CreateSceneCommand } from "./create-scene";
+export { DeleteSceneCommand } from "./delete-scene";
+export { RenameSceneCommand } from "./rename-scene";
+export { ToggleBookmarkCommand } from "./toggle-bookmark";
+export { RemoveBookmarkCommand } from "./remove-bookmark";
diff --git a/apps/web/src/lib/commands/scene/remove-bookmark.ts b/apps/web/src/lib/commands/scene/remove-bookmark.ts
new file mode 100644
index 00000000..a3029366
--- /dev/null
+++ b/apps/web/src/lib/commands/scene/remove-bookmark.ts
@@ -0,0 +1,59 @@
+import { Command } from "@/lib/commands/base-command";
+import { EditorCore } from "@/core";
+import type { TScene } from "@/types/timeline";
+import { updateSceneInArray } from "@/lib/scenes";
+import {
+ getFrameTime,
+ removeBookmarkFromArray,
+} from "@/lib/timeline/bookmarks";
+
+export class RemoveBookmarkCommand extends Command {
+ private savedScenes: TScene[] | null = null;
+ private frameTime: number = 0;
+
+ constructor(private time: number) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ const activeScene = editor.scenes.getActiveScene();
+ const activeProject = editor.project.getActive();
+
+ if (!activeScene || !activeProject) {
+ return;
+ }
+
+ const scenes = editor.scenes.getScenes();
+ this.savedScenes = [...scenes];
+
+ this.frameTime = getFrameTime({
+ time: this.time,
+ fps: activeProject.settings.fps,
+ });
+
+ const updatedBookmarks = removeBookmarkFromArray({
+ bookmarks: activeScene.bookmarks,
+ frameTime: this.frameTime,
+ });
+
+ if (updatedBookmarks.length === activeScene.bookmarks.length) {
+ return;
+ }
+
+ const updatedScenes = updateSceneInArray({
+ scenes,
+ sceneId: activeScene.id,
+ updates: { bookmarks: updatedBookmarks },
+ });
+
+ editor.scenes.setScenes({ scenes: updatedScenes });
+ }
+
+ undo(): void {
+ if (this.savedScenes) {
+ const editor = EditorCore.getInstance();
+ editor.scenes.setScenes({ scenes: this.savedScenes });
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/scene/rename-scene.ts b/apps/web/src/lib/commands/scene/rename-scene.ts
new file mode 100644
index 00000000..dc62369d
--- /dev/null
+++ b/apps/web/src/lib/commands/scene/rename-scene.ts
@@ -0,0 +1,46 @@
+import { Command } from "@/lib/commands/base-command";
+import { EditorCore } from "@/core";
+import type { TScene } from "@/types/timeline";
+import { updateSceneInArray } from "@/lib/scenes";
+
+export class RenameSceneCommand extends Command {
+ private savedScenes: TScene[] | null = null;
+ private previousName: string | null = null;
+
+ constructor(
+ private sceneId: string,
+ private newName: string,
+ ) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ const scenes = editor.scenes.getScenes();
+
+ this.savedScenes = [...scenes];
+
+ const scene = scenes.find((s) => s.id === this.sceneId);
+ if (!scene) {
+ console.error("Scene not found:", this.sceneId);
+ return;
+ }
+
+ this.previousName = scene.name;
+
+ const updatedScenes = updateSceneInArray({
+ scenes,
+ sceneId: this.sceneId,
+ updates: { name: this.newName, updatedAt: new Date() },
+ });
+
+ editor.scenes.setScenes({ scenes: updatedScenes });
+ }
+
+ undo(): void {
+ if (this.savedScenes && this.previousName !== null) {
+ const editor = EditorCore.getInstance();
+ editor.scenes.setScenes({ scenes: this.savedScenes });
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/scene/toggle-bookmark.ts b/apps/web/src/lib/commands/scene/toggle-bookmark.ts
new file mode 100644
index 00000000..ac76d8fc
--- /dev/null
+++ b/apps/web/src/lib/commands/scene/toggle-bookmark.ts
@@ -0,0 +1,52 @@
+import { Command } from "@/lib/commands/base-command";
+import { EditorCore } from "@/core";
+import type { TScene } from "@/types/timeline";
+import { updateSceneInArray } from "@/lib/scenes";
+import { getFrameTime, toggleBookmarkInArray } from "@/lib/timeline/bookmarks";
+
+export class ToggleBookmarkCommand extends Command {
+ private savedScenes: TScene[] | null = null;
+ private frameTime: number = 0;
+
+ constructor(private time: number) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ const activeScene = editor.scenes.getActiveScene();
+ const activeProject = editor.project.getActive();
+
+ if (!activeScene || !activeProject) {
+ return;
+ }
+
+ const scenes = editor.scenes.getScenes();
+ this.savedScenes = [...scenes];
+
+ this.frameTime = getFrameTime({
+ time: this.time,
+ fps: activeProject.settings.fps,
+ });
+
+ const updatedBookmarks = toggleBookmarkInArray({
+ bookmarks: activeScene.bookmarks,
+ frameTime: this.frameTime,
+ });
+
+ const updatedScenes = updateSceneInArray({
+ scenes,
+ sceneId: activeScene.id,
+ updates: { bookmarks: updatedBookmarks },
+ });
+
+ editor.scenes.setScenes({ scenes: updatedScenes });
+ }
+
+ undo(): void {
+ if (this.savedScenes) {
+ const editor = EditorCore.getInstance();
+ editor.scenes.setScenes({ scenes: this.savedScenes });
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/timeline/clipboard/index.ts b/apps/web/src/lib/commands/timeline/clipboard/index.ts
new file mode 100644
index 00000000..11a8bed4
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/clipboard/index.ts
@@ -0,0 +1 @@
+export { PasteCommand } from "./paste";
diff --git a/apps/web/src/lib/commands/timeline/clipboard/paste.ts b/apps/web/src/lib/commands/timeline/clipboard/paste.ts
new file mode 100644
index 00000000..9c979790
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/clipboard/paste.ts
@@ -0,0 +1,263 @@
+import { Command } from "@/lib/commands/base-command";
+import { EditorCore } from "@/core";
+import type {
+ TimelineTrack,
+ TimelineElement,
+ ClipboardItem,
+} from "@/types/timeline";
+import { generateUUID } from "@/utils/id";
+import { wouldElementOverlap } from "@/lib/timeline/element-utils";
+import {
+ buildEmptyTrack,
+ getHighestInsertIndexForTrack,
+} from "@/lib/timeline/track-utils";
+
+export class PasteCommand extends Command {
+ private savedState: TimelineTrack[] | null = null;
+ private pastedElements: { trackId: string; elementId: string }[] = [];
+ private previousSelection: { trackId: string; elementId: string }[] = [];
+
+ constructor(
+ private time: number,
+ private clipboardItems: ClipboardItem[],
+ ) {
+ super();
+ }
+
+ execute(): void {
+ if (this.clipboardItems.length === 0) return;
+
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+ this.previousSelection = editor.selection.getSelectedElements();
+ this.pastedElements = [];
+
+ const minStart = Math.min(
+ ...this.clipboardItems.map((item) => item.element.startTime),
+ );
+
+ const 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.findIndex(
+ (track) => track.id === trackId,
+ );
+ const resolvedTargetIndex = resolveTargetTrackIndex({
+ tracks: updatedTracks,
+ sourceTrackIndex,
+ trackType,
+ elements: elementsToAdd,
+ });
+
+ if (resolvedTargetIndex >= 0) {
+ const targetTrack = updatedTracks[resolvedTargetIndex];
+ updatedTracks[resolvedTargetIndex] = {
+ ...targetTrack,
+ elements: [...targetTrack.elements, ...elementsToAdd],
+ } as TimelineTrack;
+ for (const element of elementsToAdd) {
+ this.pastedElements.push({
+ trackId: targetTrack.id,
+ elementId: element.id,
+ });
+ }
+ continue;
+ }
+
+ const insertIndex = resolveInsertIndexForNewTrack({
+ tracks: updatedTracks,
+ sourceTrackIndex,
+ trackType,
+ });
+ const newTrack = buildTrackWithElements({
+ trackType,
+ elements: elementsToAdd,
+ });
+ updatedTracks.splice(insertIndex, 0, newTrack);
+ for (const element of elementsToAdd) {
+ this.pastedElements.push({
+ trackId: newTrack.id,
+ elementId: element.id,
+ });
+ }
+ }
+
+ editor.timeline.updateTracks(updatedTracks);
+
+ if (this.pastedElements.length > 0) {
+ editor.selection.setSelectedElements({ elements: this.pastedElements });
+ }
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ editor.selection.setSelectedElements({ elements: this.previousSelection });
+ }
+ }
+
+ getPastedElements(): { trackId: string; elementId: string }[] {
+ return this.pastedElements;
+ }
+}
+
+function groupClipboardItemsByTrackId({
+ clipboardItems,
+}: {
+ clipboardItems: ClipboardItem[];
+}): Map<string, ClipboardItem[]> {
+ const groupedItems = new Map<string, ClipboardItem[]>();
+
+ 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: ClipboardItem[];
+ 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,
+ } as TimelineElement);
+ }
+
+ return elementsToAdd;
+}
+
+function resolveTargetTrackIndex({
+ tracks,
+ sourceTrackIndex,
+ trackType,
+ elements,
+}: {
+ tracks: TimelineTrack[];
+ sourceTrackIndex: number;
+ trackType: ClipboardItem["trackType"];
+ elements: TimelineElement[];
+}): number {
+ if (sourceTrackIndex >= 0) {
+ const aboveIndex = sourceTrackIndex - 1;
+ if (aboveIndex < 0) {
+ return -1;
+ }
+
+ const aboveTrack = tracks[aboveIndex];
+ if (aboveTrack.type !== trackType) {
+ return -1;
+ }
+
+ const canPlaceOnAbove = canPlaceElementsOnTrack({
+ track: aboveTrack,
+ elements,
+ });
+ return canPlaceOnAbove ? aboveIndex : -1;
+ }
+
+ const highestCompatibleIndex = tracks.findIndex(
+ (track) => track.type === trackType,
+ );
+ if (highestCompatibleIndex < 0) {
+ return -1;
+ }
+
+ const highestCompatibleTrack = tracks[highestCompatibleIndex];
+ const canPlaceOnHighest = canPlaceElementsOnTrack({
+ track: highestCompatibleTrack,
+ elements,
+ });
+
+ return canPlaceOnHighest ? highestCompatibleIndex : -1;
+}
+
+function resolveInsertIndexForNewTrack({
+ tracks,
+ sourceTrackIndex,
+ trackType,
+}: {
+ tracks: TimelineTrack[];
+ sourceTrackIndex: number;
+ trackType: ClipboardItem["trackType"];
+}): number {
+ if (sourceTrackIndex >= 0) {
+ return sourceTrackIndex;
+ }
+
+ const highestCompatibleIndex = tracks.findIndex(
+ (track) => track.type === trackType,
+ );
+ if (highestCompatibleIndex >= 0) {
+ return highestCompatibleIndex;
+ }
+
+ return getHighestInsertIndexForTrack({ tracks, trackType });
+}
+
+function canPlaceElementsOnTrack({
+ track,
+ elements,
+}: {
+ track: TimelineTrack;
+ elements: TimelineElement[];
+}): boolean {
+ for (const element of elements) {
+ const endTime = element.startTime + element.duration;
+ const hasOverlap = wouldElementOverlap({
+ elements: track.elements,
+ startTime: element.startTime,
+ endTime,
+ });
+ if (hasOverlap) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function buildTrackWithElements({
+ trackType,
+ elements,
+}: {
+ trackType: ClipboardItem["trackType"];
+ elements: TimelineElement[];
+}): TimelineTrack {
+ const newTrackId = generateUUID();
+ const newTrackBase = buildEmptyTrack({ id: newTrackId, type: trackType });
+ return {
+ ...newTrackBase,
+ elements,
+ } as TimelineTrack;
+}
diff --git a/apps/web/src/lib/commands/timeline/element/delete-elements.ts b/apps/web/src/lib/commands/timeline/element/delete-elements.ts
new file mode 100644
index 00000000..91eb6397
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/element/delete-elements.ts
@@ -0,0 +1,48 @@
+import { Command } from "@/lib/commands/base-command";
+import type { TimelineTrack } from "@/types/timeline";
+import { EditorCore } from "@/core";
+import { isMainTrack } from "@/lib/timeline";
+
+export class DeleteElementsCommand extends Command {
+ private savedState: TimelineTrack[] | null = null;
+
+ constructor(private elements: { trackId: string; elementId: string }[]) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+
+ const updatedTracks = this.savedState
+ .map((track) => {
+ const hasElementsToDelete = this.elements.some(
+ (el) => el.trackId === track.id,
+ );
+
+ if (!hasElementsToDelete) {
+ return track;
+ }
+
+ return {
+ ...track,
+ elements: track.elements.filter(
+ (element) =>
+ !this.elements.some(
+ (el) => el.trackId === track.id && el.elementId === element.id,
+ ),
+ ),
+ } as typeof track;
+ })
+ .filter((track) => track.elements.length > 0 || isMainTrack(track));
+
+ editor.timeline.updateTracks(updatedTracks);
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts b/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts
new file mode 100644
index 00000000..4bec6444
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts
@@ -0,0 +1,114 @@
+import { Command } from "@/lib/commands/base-command";
+import type { TimelineElement, TimelineTrack } from "@/types/timeline";
+import { generateUUID } from "@/utils/id";
+import { EditorCore } from "@/core";
+import {
+ buildEmptyTrack,
+ getHighestInsertIndexForTrack,
+} from "@/lib/timeline/track-utils";
+
+interface DuplicateElementsParams {
+ elements: { trackId: string; elementId: string }[];
+}
+
+export class DuplicateElementsCommand extends Command {
+ private duplicatedElements: { trackId: string; elementId: string }[] = [];
+ private savedState: TimelineTrack[] | null = null;
+ private previousSelection: { trackId: string; elementId: string }[] = [];
+ private elements: DuplicateElementsParams["elements"];
+
+ constructor({ elements }: DuplicateElementsParams) {
+ super();
+ this.elements = elements;
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+ this.previousSelection = editor.selection.getSelectedElements();
+ this.duplicatedElements = [];
+
+ const updatedTracks = [...this.savedState];
+
+ for (const track of this.savedState) {
+ const elementsToDuplicate = this.elements.filter(
+ (el) => el.trackId === track.id,
+ );
+
+ if (elementsToDuplicate.length === 0) {
+ continue;
+ }
+
+ const elementIdsToDuplicate = new Set(
+ elementsToDuplicate.map((element) => element.elementId),
+ );
+ const newTrackElements: TimelineElement[] = [];
+
+ const newTrackId = generateUUID();
+ const newTrackBase = buildEmptyTrack({
+ id: newTrackId,
+ type: track.type,
+ });
+
+ for (const element of track.elements) {
+ if (!elementIdsToDuplicate.has(element.id)) {
+ continue;
+ }
+
+ const newId = generateUUID();
+ this.duplicatedElements.push({
+ trackId: newTrackId,
+ elementId: newId,
+ });
+ newTrackElements.push(
+ buildDuplicateElement({
+ element,
+ id: newId,
+ startTime: element.startTime,
+ }),
+ );
+ }
+
+ const newTrack = {
+ ...newTrackBase,
+ elements: newTrackElements,
+ } as TimelineTrack;
+
+ const insertIndex = getHighestInsertIndexForTrack({
+ tracks: updatedTracks,
+ trackType: track.type,
+ });
+ updatedTracks.splice(insertIndex, 0, newTrack);
+ }
+
+ editor.timeline.updateTracks(updatedTracks);
+
+ if (this.duplicatedElements.length > 0) {
+ editor.selection.setSelectedElements({ elements: this.duplicatedElements });
+ }
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ editor.selection.setSelectedElements({ elements: this.previousSelection });
+ }
+ }
+
+ 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 };
+}
diff --git a/apps/web/src/lib/commands/timeline/element/index.ts b/apps/web/src/lib/commands/timeline/element/index.ts
new file mode 100644
index 00000000..8b719afa
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/element/index.ts
@@ -0,0 +1,11 @@
+export { InsertElementCommand } from "./insert-element";
+export { DeleteElementsCommand } from "./delete-elements";
+export { DuplicateElementsCommand } from "./duplicate-elements";
+export { UpdateElementTrimCommand } from "./update-element-trim";
+export { UpdateElementDurationCommand } from "./update-element-duration";
+export { UpdateElementStartTimeCommand } from "./update-element-start-time";
+export { SplitElementsCommand } from "./split-elements";
+export { UpdateTextElementCommand } from "./update-text-element";
+export { ToggleElementsVisibilityCommand } from "./toggle-elements-visibility";
+export { ToggleElementsMutedCommand } from "./toggle-elements-muted";
+export { MoveElementCommand } from "./move-elements";
diff --git a/apps/web/src/lib/commands/timeline/element/insert-element.ts b/apps/web/src/lib/commands/timeline/element/insert-element.ts
new file mode 100644
index 00000000..f95935a5
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/element/insert-element.ts
@@ -0,0 +1,311 @@
+import { Command } from "@/lib/commands/base-command";
+import { EditorCore } from "@/core";
+import type {
+ CreateTimelineElement,
+ TimelineTrack,
+ TimelineElement,
+ TrackType,
+ ElementType,
+} from "@/types/timeline";
+import { generateUUID } from "@/utils/id";
+import {
+ requiresMediaId,
+ wouldElementOverlap,
+} from "@/lib/timeline/element-utils";
+import {
+ buildEmptyTrack,
+ canElementGoOnTrack,
+ getDefaultInsertIndexForTrack,
+ validateElementTrackCompatibility,
+} from "@/lib/timeline/track-utils";
+import type { MediaAsset } from "@/types/assets";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+
+type InsertElementPlacement =
+ | { mode: "explicit"; trackId: string }
+ | { mode: "auto"; trackType?: TrackType; insertIndex?: number };
+
+export interface InsertElementParams {
+ element: CreateTimelineElement;
+ placement: InsertElementPlacement;
+}
+
+export class InsertElementCommand extends Command {
+ private elementId: string;
+ private savedState: TimelineTrack[] | null = null;
+ private targetTrackId: string | null = null;
+
+ constructor({ element, placement }: InsertElementParams) {
+ super();
+ this.elementId = generateUUID();
+ this.element = element;
+ this.placement = placement;
+ }
+
+ private element: CreateTimelineElement;
+ private placement: InsertElementPlacement;
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+
+ if (!this.savedState) {
+ console.error("Tracks not available");
+ return;
+ }
+
+ if (!this.validateElementBasics({ element: this.element })) {
+ return;
+ }
+
+ const totalElementsInTimeline = this.savedState.reduce(
+ (total, t) => total + t.elements.length,
+ 0,
+ );
+ const isFirstElement = totalElementsInTimeline === 0;
+
+ const newElement = this.buildElement({ element: this.element });
+ const updateResult = this.resolveTracksWithElement({
+ tracks: this.savedState,
+ element: newElement,
+ });
+
+ if (!updateResult) {
+ return;
+ }
+
+ const { updatedTracks, targetTrackId } = updateResult;
+ this.targetTrackId = targetTrackId;
+
+ const isVisualMedia =
+ newElement.type === "video" || newElement.type === "image";
+
+ if (isFirstElement && isVisualMedia) {
+ const mediaAssets = editor.media.getAssets();
+ const activeProject = editor.project.getActive();
+ const asset = mediaAssets.find(
+ (item: MediaAsset) => item.id === newElement.mediaId,
+ );
+
+ if (asset?.width && asset?.height) {
+ const nextCanvasSize = { width: asset.width, height: asset.height };
+ const shouldSetOriginalCanvasSize =
+ !activeProject?.settings.originalCanvasSize;
+ editor.project.updateSettings({
+ settings: {
+ canvasSize: nextCanvasSize,
+ ...(shouldSetOriginalCanvasSize
+ ? { originalCanvasSize: nextCanvasSize }
+ : {}),
+ },
+ pushHistory: false,
+ });
+ }
+
+ if (asset?.type === "video" && asset?.fps) {
+ editor.project.updateSettings({
+ settings: { fps: asset.fps },
+ pushHistory: false,
+ });
+ }
+ }
+
+ editor.timeline.updateTracks(updatedTracks);
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+
+ getElementId(): string {
+ return this.elementId;
+ }
+
+ getTrackId(): string | null {
+ return this.targetTrackId;
+ }
+
+ private buildElement({
+ element,
+ }: {
+ element: CreateTimelineElement;
+ }): TimelineElement {
+ return {
+ ...element,
+ id: this.elementId,
+ startTime: element.startTime,
+ trimStart: element.trimStart ?? 0,
+ trimEnd: element.trimEnd ?? 0,
+ duration: element.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION,
+ } as TimelineElement;
+ }
+
+ private validateElementBasics({
+ element,
+ }: {
+ element: CreateTimelineElement;
+ }): boolean {
+ if (requiresMediaId({ element }) && !("mediaId" in element)) {
+ console.error("Element requires mediaId");
+ return false;
+ }
+
+ if (
+ element.type === "audio" &&
+ element.sourceType === "library" &&
+ !element.sourceUrl
+ ) {
+ console.error("Library audio element must have sourceUrl");
+ return false;
+ }
+
+ if (element.type === "sticker" && !element.iconName) {
+ console.error("Sticker element must have iconName");
+ return false;
+ }
+
+ if (element.type === "text" && !element.content) {
+ console.error("Text element must have content");
+ return false;
+ }
+
+ return true;
+ }
+
+ private resolveTracksWithElement({
+ tracks,
+ element,
+ }: {
+ tracks: TimelineTrack[];
+ element: TimelineElement;
+ }): { updatedTracks: TimelineTrack[]; targetTrackId: string } | null {
+ const placement = this.placement;
+
+ if (placement.mode === "explicit") {
+ const targetTrack = tracks.find(
+ (track) => track.id === placement.trackId,
+ );
+
+ if (!targetTrack) {
+ console.error("Track not found:", placement.trackId);
+ return null;
+ }
+
+ const validation = validateElementTrackCompatibility({
+ element,
+ track: targetTrack,
+ });
+
+ if (!validation.isValid) {
+ console.error(validation.errorMessage);
+ return null;
+ }
+
+ const updatedTracks = tracks.map((track) =>
+ track.id === targetTrack.id
+ ? { ...track, elements: [...track.elements, element] }
+ : track,
+ ) as TimelineTrack[];
+
+ return { updatedTracks, targetTrackId: targetTrack.id };
+ }
+
+ const trackType =
+ placement.trackType ?? this.getTrackTypeForElement({ element });
+
+ if (
+ placement.trackType &&
+ !canElementGoOnTrack({
+ elementType: element.type,
+ trackType,
+ })
+ ) {
+ console.error(
+ `${element.type} elements cannot be placed on ${trackType} tracks`,
+ );
+ return null;
+ }
+
+ const elementEndTime = element.startTime + element.duration;
+ const existingTrack = tracks.find((track) => {
+ if (
+ !canElementGoOnTrack({
+ elementType: element.type,
+ trackType: track.type,
+ })
+ ) {
+ return false;
+ }
+
+ return !wouldElementOverlap({
+ elements: track.elements,
+ startTime: element.startTime,
+ endTime: elementEndTime,
+ });
+ });
+
+ if (existingTrack) {
+ const updatedTracks = tracks.map((track) =>
+ track.id === existingTrack.id
+ ? { ...track, elements: [...track.elements, element] }
+ : track,
+ ) as TimelineTrack[];
+
+ return { updatedTracks, targetTrackId: existingTrack.id };
+ }
+
+ const newTrackId = generateUUID();
+ const newTrack = buildEmptyTrack({
+ id: newTrackId,
+ type: trackType,
+ });
+ const newTrackWithElement = {
+ ...newTrack,
+ elements: [...newTrack.elements, element],
+ } as TimelineTrack;
+
+ const updatedTracks = [...tracks];
+ const insertIndex =
+ placement.insertIndex ??
+ this.getAutoInsertIndex({ tracks: updatedTracks, trackType });
+ updatedTracks.splice(insertIndex, 0, newTrackWithElement);
+
+ return { updatedTracks, targetTrackId: newTrackId };
+ }
+
+ private getAutoInsertIndex({
+ tracks,
+ trackType,
+ }: {
+ tracks: TimelineTrack[];
+ trackType: TrackType;
+ }): number {
+ if (trackType === "text") {
+ const firstVideoTrackIndex = tracks.findIndex(
+ (track) => track.type === "video",
+ );
+ if (firstVideoTrackIndex >= 0) {
+ return firstVideoTrackIndex;
+ }
+ }
+
+ return getDefaultInsertIndexForTrack({
+ tracks,
+ trackType,
+ });
+ }
+
+ private getTrackTypeForElement({
+ element,
+ }: {
+ element: { type: ElementType };
+ }): TrackType {
+ if (element.type === "video" || element.type === "image") {
+ return "video";
+ }
+ return element.type;
+ }
+}
diff --git a/apps/web/src/lib/commands/timeline/element/move-elements.ts b/apps/web/src/lib/commands/timeline/element/move-elements.ts
new file mode 100644
index 00000000..2d4f9236
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/element/move-elements.ts
@@ -0,0 +1,127 @@
+import { Command } from "@/lib/commands/base-command";
+import { EditorCore } from "@/core";
+import type {
+ TimelineTrack,
+ TimelineElement,
+ TrackType,
+} from "@/types/timeline";
+import {
+ buildEmptyTrack,
+ isMainTrack,
+ validateElementTrackCompatibility,
+} from "@/lib/timeline/track-utils";
+
+export class MoveElementCommand extends Command {
+ private savedState: TimelineTrack[] | null = null;
+
+ constructor(
+ private sourceTrackId: string,
+ private targetTrackId: string,
+ private elementId: string,
+ private newStartTime: number,
+ private createTrack?: { type: TrackType; index: number },
+ ) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+
+ const sourceTrack = this.savedState.find(
+ (t) => t.id === this.sourceTrackId,
+ );
+ const element = sourceTrack?.elements.find(
+ (el) => el.id === this.elementId,
+ );
+
+ if (!sourceTrack || !element) {
+ console.error("Source track or element not found");
+ return;
+ }
+
+ let targetTrack = this.savedState.find((t) => t.id === this.targetTrackId);
+ let tracksToUpdate = this.savedState;
+ if (!targetTrack && this.createTrack) {
+ const newTrack = buildEmptyTrack({
+ id: this.targetTrackId,
+ type: this.createTrack.type,
+ });
+ tracksToUpdate = [...this.savedState];
+ tracksToUpdate.splice(this.createTrack.index, 0, newTrack);
+ targetTrack = newTrack;
+ }
+ if (!targetTrack) {
+ console.error("Target track not found");
+ return;
+ }
+
+ const validation = validateElementTrackCompatibility({
+ element,
+ track: targetTrack,
+ });
+
+ if (!validation.isValid) {
+ console.error(validation.errorMessage);
+ return;
+ }
+
+ const movedElement: TimelineElement = {
+ ...element,
+ startTime: this.newStartTime,
+ };
+
+ const isSameTrack = this.sourceTrackId === this.targetTrackId;
+
+ let updatedTracks = tracksToUpdate.map((track) => {
+ if (isSameTrack && track.id === this.sourceTrackId) {
+ return {
+ ...track,
+ elements: track.elements.map((el) =>
+ el.id === this.elementId ? movedElement : el,
+ ),
+ };
+ }
+
+ if (track.id === this.sourceTrackId) {
+ return {
+ ...track,
+ elements: track.elements.filter((el) => el.id !== this.elementId),
+ };
+ }
+
+ if (track.id === this.targetTrackId) {
+ return {
+ ...track,
+ elements: [...track.elements, movedElement],
+ };
+ }
+
+ return track;
+ }) as TimelineTrack[];
+
+ if (!isSameTrack) {
+ const sourceTrackAfterMove = updatedTracks.find(
+ (track) => track.id === this.sourceTrackId,
+ );
+ if (
+ sourceTrackAfterMove &&
+ sourceTrackAfterMove.elements.length === 0 &&
+ !isMainTrack(sourceTrackAfterMove)
+ ) {
+ updatedTracks = updatedTracks.filter(
+ (track) => track.id !== this.sourceTrackId,
+ );
+ }
+ }
+
+ editor.timeline.updateTracks(updatedTracks);
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/timeline/element/split-elements.ts b/apps/web/src/lib/commands/timeline/element/split-elements.ts
new file mode 100644
index 00000000..13d4d730
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/element/split-elements.ts
@@ -0,0 +1,133 @@
+import { Command } from "@/lib/commands/base-command";
+import type { TimelineTrack } from "@/types/timeline";
+import { generateUUID } from "@/utils/id";
+import { EditorCore } from "@/core";
+
+export class SplitElementsCommand extends Command {
+ private savedState: TimelineTrack[] | null = null;
+ private rightSideElements: { trackId: string; elementId: string }[] = [];
+ private previousSelection: { trackId: string; elementId: string }[] = [];
+
+ constructor(
+ private elements: { trackId: string; elementId: string }[],
+ private splitTime: number,
+ private retainSide: "both" | "left" | "right" = "both",
+ ) {
+ super();
+ }
+
+ getRightSideElements(): { trackId: string; elementId: string }[] {
+ return this.rightSideElements;
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+ this.previousSelection = editor.selection.getSelectedElements();
+ this.rightSideElements = [];
+
+ const updatedTracks = this.savedState.map((track) => {
+ const elementsToSplit = this.elements.filter(
+ (el) => el.trackId === track.id,
+ );
+
+ if (elementsToSplit.length === 0) {
+ return track;
+ }
+
+ return {
+ ...track,
+ elements: track.elements.flatMap((element) => {
+ const shouldSplit = elementsToSplit.some(
+ (el) => el.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;
+
+ if (this.retainSide === "left") {
+ return [
+ {
+ ...element,
+ duration: leftVisibleDuration,
+ trimEnd: element.trimEnd + rightVisibleDuration,
+ name: `${element.name} (left)`,
+ },
+ ];
+ }
+
+ if (this.retainSide === "right") {
+ const newId = generateUUID();
+ this.rightSideElements.push({
+ trackId: track.id,
+ elementId: newId,
+ });
+ return [
+ {
+ ...element,
+ id: newId,
+ startTime: this.splitTime,
+ duration: rightVisibleDuration,
+ trimStart: element.trimStart + leftVisibleDuration,
+ name: `${element.name} (right)`,
+ },
+ ];
+ }
+
+ // "both" - split into two pieces
+ const secondElementId = generateUUID();
+ this.rightSideElements.push({
+ trackId: track.id,
+ elementId: secondElementId,
+ });
+
+ return [
+ {
+ ...element,
+ duration: leftVisibleDuration,
+ trimEnd: element.trimEnd + rightVisibleDuration,
+ name: `${element.name} (left)`,
+ },
+ {
+ ...element,
+ id: secondElementId,
+ startTime: this.splitTime,
+ duration: rightVisibleDuration,
+ trimStart: element.trimStart + leftVisibleDuration,
+ name: `${element.name} (right)`,
+ },
+ ];
+ }),
+ } as typeof track;
+ });
+
+ editor.timeline.updateTracks(updatedTracks);
+
+ if (this.rightSideElements.length > 0) {
+ editor.selection.setSelectedElements({ elements: this.rightSideElements });
+ }
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ editor.selection.setSelectedElements({ elements: this.previousSelection });
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/timeline/element/toggle-elements-muted.ts b/apps/web/src/lib/commands/timeline/element/toggle-elements-muted.ts
new file mode 100644
index 00000000..1fa7421e
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/element/toggle-elements-muted.ts
@@ -0,0 +1,57 @@
+import { Command } from "@/lib/commands/base-command";
+import type { TimelineTrack } from "@/types/timeline";
+import { canElementHaveAudio } from "@/lib/timeline/element-utils";
+import { EditorCore } from "@/core";
+
+export class ToggleElementsMutedCommand extends Command {
+ private savedState: TimelineTrack[] | null = null;
+
+ constructor(private elements: { trackId: string; elementId: string }[]) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+
+ const mutableElements = this.elements.filter(({ trackId, elementId }) => {
+ const track = this.savedState?.find((t) => t.id === trackId);
+ const element = track?.elements.find((e) => e.id === elementId);
+ return element && canElementHaveAudio(element);
+ });
+
+ if (mutableElements.length === 0) {
+ return;
+ }
+
+ const shouldMute = mutableElements.some(({ trackId, elementId }) => {
+ const track = this.savedState?.find((t) => t.id === trackId);
+ const element = track?.elements.find((e) => e.id === elementId);
+ return element && canElementHaveAudio(element) && !element.muted;
+ });
+
+ const updatedTracks = this.savedState.map((track) => {
+ const newElements = track.elements.map((element) => {
+ const shouldUpdate = mutableElements.some(
+ ({ trackId, elementId }) =>
+ track.id === trackId && element.id === elementId,
+ );
+ return shouldUpdate &&
+ canElementHaveAudio(element) &&
+ element.muted !== shouldMute
+ ? { ...element, muted: shouldMute }
+ : element;
+ });
+ return { ...track, elements: newElements } as typeof track;
+ });
+
+ editor.timeline.updateTracks(updatedTracks);
+ }
+
+ 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-elements-visibility.ts b/apps/web/src/lib/commands/timeline/element/toggle-elements-visibility.ts
new file mode 100644
index 00000000..daeb759a
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/element/toggle-elements-visibility.ts
@@ -0,0 +1,47 @@
+import { Command } from "@/lib/commands/base-command";
+import type { TimelineTrack } from "@/types/timeline";
+import { canElementBeHidden } from "@/lib/timeline/element-utils";
+import { EditorCore } from "@/core";
+
+export class ToggleElementsVisibilityCommand extends Command {
+ private savedState: TimelineTrack[] | null = null;
+
+ constructor(private elements: { trackId: string; elementId: string }[]) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+
+ const shouldHide = this.elements.some(({ trackId, elementId }) => {
+ const track = this.savedState?.find((t) => t.id === trackId);
+ const element = track?.elements.find((e) => e.id === elementId);
+ return element && canElementBeHidden(element) && !element.hidden;
+ });
+
+ const updatedTracks = this.savedState.map((track) => {
+ const newElements = track.elements.map((element) => {
+ const shouldUpdate = this.elements.some(
+ ({ trackId, elementId }) =>
+ track.id === trackId && element.id === elementId,
+ );
+ return shouldUpdate &&
+ canElementBeHidden(element) &&
+ element.hidden !== shouldHide
+ ? { ...element, hidden: shouldHide }
+ : element;
+ });
+ return { ...track, elements: newElements } as typeof track;
+ });
+
+ editor.timeline.updateTracks(updatedTracks);
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/timeline/element/update-element-duration.ts b/apps/web/src/lib/commands/timeline/element/update-element-duration.ts
new file mode 100644
index 00000000..6b3a860b
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/element/update-element-duration.ts
@@ -0,0 +1,37 @@
+import { Command } from "@/lib/commands/base-command";
+import type { TimelineTrack } from "@/types/timeline";
+import { EditorCore } from "@/core";
+
+export class UpdateElementDurationCommand extends Command {
+ private savedState: TimelineTrack[] | null = null;
+
+ constructor(
+ private trackId: string,
+ private elementId: string,
+ private duration: number,
+ ) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+
+ const updatedTracks = this.savedState.map((t) => {
+ if (t.id !== this.trackId) return t;
+ const newElements = t.elements.map((el) =>
+ el.id === this.elementId ? { ...el, duration: this.duration } : el,
+ );
+ return { ...t, elements: newElements } as typeof t;
+ });
+
+ editor.timeline.updateTracks(updatedTracks);
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/timeline/element/update-element-start-time.ts b/apps/web/src/lib/commands/timeline/element/update-element-start-time.ts
new file mode 100644
index 00000000..5312000f
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/element/update-element-start-time.ts
@@ -0,0 +1,48 @@
+import { Command } from "@/lib/commands/base-command";
+import type { TimelineTrack } from "@/types/timeline";
+import { EditorCore } from "@/core";
+
+export class UpdateElementStartTimeCommand extends Command {
+ private savedState: TimelineTrack[] | null = null;
+
+ constructor(
+ private elements: { trackId: string; elementId: string }[],
+ private startTime: number,
+ ) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+
+ const updatedTracks = this.savedState.map((track) => {
+ const hasElementsToUpdate = this.elements.some(
+ (el) => el.trackId === track.id,
+ );
+
+ if (!hasElementsToUpdate) {
+ return track;
+ }
+
+ const newElements = track.elements.map((element) => {
+ const shouldUpdate = this.elements.some(
+ (el) => el.elementId === element.id && el.trackId === track.id,
+ );
+ return shouldUpdate
+ ? { ...element, startTime: Math.max(0, this.startTime) }
+ : element;
+ });
+ return { ...track, elements: newElements } as typeof track;
+ });
+
+ editor.timeline.updateTracks(updatedTracks);
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/timeline/element/update-element-trim.ts b/apps/web/src/lib/commands/timeline/element/update-element-trim.ts
new file mode 100644
index 00000000..99c63287
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/element/update-element-trim.ts
@@ -0,0 +1,48 @@
+import { Command } from "@/lib/commands/base-command";
+import type { TimelineTrack } from "@/types/timeline";
+import { EditorCore } from "@/core";
+
+export class UpdateElementTrimCommand extends Command {
+ private savedState: TimelineTrack[] | null = null;
+
+ constructor(
+ private elementId: string,
+ private trimStart: number,
+ private trimEnd: number,
+ private startTime?: number,
+ private duration?: number,
+ ) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+
+ const updatedTracks = this.savedState.map((track) => {
+ const newElements = track.elements.map((element) => {
+ if (element.id !== this.elementId) {
+ return element;
+ }
+
+ return {
+ ...element,
+ trimStart: this.trimStart,
+ trimEnd: this.trimEnd,
+ startTime: this.startTime ?? element.startTime,
+ duration: this.duration ?? element.duration,
+ };
+ });
+ return { ...track, elements: newElements } as typeof track;
+ });
+
+ editor.timeline.updateTracks(updatedTracks);
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/timeline/element/update-text-element.ts b/apps/web/src/lib/commands/timeline/element/update-text-element.ts
new file mode 100644
index 00000000..e3cc4a32
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/element/update-text-element.ts
@@ -0,0 +1,52 @@
+import { Command } from "@/lib/commands/base-command";
+import type { TextElement, TimelineTrack } from "@/types/timeline";
+import { EditorCore } from "@/core";
+
+export class UpdateTextElementCommand extends Command {
+ private savedState: TimelineTrack[] | null = null;
+
+ constructor(
+ private trackId: string,
+ private elementId: string,
+ private updates: Partial<
+ Pick<
+ TextElement,
+ | "content"
+ | "fontSize"
+ | "fontFamily"
+ | "color"
+ | "backgroundColor"
+ | "textAlign"
+ | "fontWeight"
+ | "fontStyle"
+ | "textDecoration"
+ >
+ >,
+ ) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+
+ const updatedTracks = this.savedState.map((t) => {
+ if (t.id !== this.trackId) return t;
+ const newElements = t.elements.map((el) =>
+ el.id === this.elementId && el.type === "text"
+ ? { ...el, ...this.updates }
+ : el,
+ );
+ return { ...t, elements: newElements } as typeof t;
+ });
+
+ editor.timeline.updateTracks(updatedTracks);
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/timeline/index.ts b/apps/web/src/lib/commands/timeline/index.ts
new file mode 100644
index 00000000..30bc81ef
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/index.ts
@@ -0,0 +1,3 @@
+export * from "./track";
+export * from "./element";
+export * from "./clipboard";
diff --git a/apps/web/src/lib/commands/timeline/track/add-track.ts b/apps/web/src/lib/commands/timeline/track/add-track.ts
new file mode 100644
index 00000000..95338888
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/track/add-track.ts
@@ -0,0 +1,53 @@
+import { Command } from "@/lib/commands/base-command";
+import type { TrackType, TimelineTrack } from "@/types/timeline";
+import { generateUUID } from "@/utils/id";
+import { EditorCore } from "@/core";
+import {
+ buildEmptyTrack,
+ getDefaultInsertIndexForTrack,
+} from "@/lib/timeline/track-utils";
+
+export class AddTrackCommand extends Command {
+ private trackId: string;
+ private savedState: TimelineTrack[] | null = null;
+
+ constructor(
+ private type: TrackType,
+ private index?: number,
+ ) {
+ super();
+ this.trackId = generateUUID();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+
+ const newTrack: TimelineTrack = buildEmptyTrack({
+ id: this.trackId,
+ type: this.type,
+ });
+
+ const updatedTracks = [...(this.savedState || [])];
+ const insertIndex =
+ this.index ??
+ getDefaultInsertIndexForTrack({
+ tracks: updatedTracks,
+ trackType: this.type,
+ });
+ updatedTracks.splice(insertIndex, 0, newTrack);
+
+ editor.timeline.updateTracks(updatedTracks);
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+
+ getTrackId(): string {
+ return this.trackId;
+ }
+}
diff --git a/apps/web/src/lib/commands/timeline/track/index.ts b/apps/web/src/lib/commands/timeline/track/index.ts
new file mode 100644
index 00000000..9d4bd9c3
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/track/index.ts
@@ -0,0 +1,4 @@
+export { AddTrackCommand } from "./add-track";
+export { RemoveTrackCommand } from "./remove-track";
+export { ToggleTrackMuteCommand } from "./toggle-track-mute";
+export { ToggleTrackVisibilityCommand } from "./toggle-track-visibility";
diff --git a/apps/web/src/lib/commands/timeline/track/remove-track.ts b/apps/web/src/lib/commands/timeline/track/remove-track.ts
new file mode 100644
index 00000000..a32aacd7
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/track/remove-track.ts
@@ -0,0 +1,35 @@
+import { Command } from "@/lib/commands/base-command";
+import { EditorCore } from "@/core";
+import type { TimelineTrack } from "@/types/timeline";
+import { getMainTrack } from "@/lib/timeline";
+
+export class RemoveTrackCommand extends Command {
+ private savedState: TimelineTrack[] | null = null;
+
+ constructor(private trackId: string) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+ const targetTrack = this.savedState.find(
+ (track) => track.id === this.trackId,
+ );
+ const mainTrack = getMainTrack({ tracks: this.savedState });
+ if (mainTrack?.id === targetTrack?.id) {
+ return;
+ }
+ const updatedTracks = this.savedState.filter(
+ (track) => track.id !== this.trackId,
+ );
+ editor.timeline.updateTracks(updatedTracks);
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts b/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts
new file mode 100644
index 00000000..3fe526c7
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts
@@ -0,0 +1,39 @@
+import { Command } from "@/lib/commands/base-command";
+import type { TimelineTrack } from "@/types/timeline";
+import { EditorCore } from "@/core";
+import { canTracktHaveAudio } from "@/lib/timeline";
+
+export class ToggleTrackMuteCommand extends Command {
+ private savedState: TimelineTrack[] | null = null;
+
+ constructor(private trackId: string) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+
+ const targetTrack = this.savedState.find(
+ (track) => track.id === this.trackId,
+ );
+ if (!targetTrack) {
+ return;
+ }
+
+ const updatedTracks = this.savedState.map((track) =>
+ track.id === this.trackId && canTracktHaveAudio(track)
+ ? { ...track, muted: !track.muted }
+ : track,
+ );
+
+ editor.timeline.updateTracks(updatedTracks);
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+}
diff --git a/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts b/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts
new file mode 100644
index 00000000..e81714ac
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts
@@ -0,0 +1,40 @@
+import { Command } from "@/lib/commands/base-command";
+import type { TimelineTrack } from "@/types/timeline";
+import { EditorCore } from "@/core";
+import { canTrackBeHidden } from "@/lib/timeline";
+
+export class ToggleTrackVisibilityCommand extends Command {
+ private savedState: TimelineTrack[] | null = null;
+
+ constructor(private trackId: string) {
+ super();
+ }
+
+ execute(): void {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+
+ const targetTrack = this.savedState.find(
+ (track) => track.id === this.trackId,
+ );
+ if (!targetTrack) {
+ return;
+ }
+
+ const updatedTracks = this.savedState.map((track) => {
+ if (track.id === this.trackId && canTrackBeHidden(track)) {
+ return { ...track, hidden: !track.hidden };
+ }
+ return track;
+ });
+
+ editor.timeline.updateTracks(updatedTracks);
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+}
diff --git a/apps/web/src/lib/db/index.ts b/apps/web/src/lib/db/index.ts
new file mode 100644
index 00000000..290e7c2a
--- /dev/null
+++ b/apps/web/src/lib/db/index.ts
@@ -0,0 +1,19 @@
+import { drizzle } from "drizzle-orm/postgres-js";
+import postgres from "postgres";
+import * as schema from "./schema";
+import { webEnv } from "@opencut/env/web";
+
+let _db: ReturnType<typeof drizzle> | null = null;
+
+function getDb() {
+ if (!_db) {
+ const client = postgres(webEnv.DATABASE_URL);
+ _db = drizzle(client, { schema });
+ }
+
+ return _db;
+}
+
+export const db = getDb();
+
+export * from "./schema";
diff --git a/apps/web/src/lib/db/schema.ts b/apps/web/src/lib/db/schema.ts
new file mode 100644
index 00000000..dcd7db67
--- /dev/null
+++ b/apps/web/src/lib/db/schema.ts
@@ -0,0 +1,59 @@
+import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
+
+export const users = pgTable("users", {
+ id: text("id").primaryKey(),
+ name: text("name").notNull(),
+ email: text("email").notNull().unique(),
+ emailVerified: boolean("email_verified").default(false).notNull(),
+ image: text("image"),
+ createdAt: timestamp("created_at")
+ .$defaultFn(() => /* @__PURE__ */ new Date())
+ .notNull(),
+ updatedAt: timestamp("updated_at")
+ .$defaultFn(() => /* @__PURE__ */ new Date())
+ .notNull(),
+}).enableRLS();
+
+export const sessions = pgTable("sessions", {
+ id: text("id").primaryKey(),
+ expiresAt: timestamp("expires_at").notNull(),
+ token: text("token").notNull().unique(),
+ createdAt: timestamp("created_at").notNull(),
+ updatedAt: timestamp("updated_at").notNull(),
+ ipAddress: text("ip_address"),
+ userAgent: text("user_agent"),
+ userId: text("user_id")
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+}).enableRLS();
+
+export const accounts = pgTable("accounts", {
+ id: text("id").primaryKey(),
+ accountId: text("account_id").notNull(),
+ providerId: text("provider_id").notNull(),
+ userId: text("user_id")
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+ accessToken: text("access_token"),
+ refreshToken: text("refresh_token"),
+ idToken: text("id_token"),
+ accessTokenExpiresAt: timestamp("access_token_expires_at"),
+ refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
+ scope: text("scope"),
+ password: text("password"),
+ createdAt: timestamp("created_at").notNull(),
+ updatedAt: timestamp("updated_at").notNull(),
+}).enableRLS();
+
+export const verifications = pgTable("verifications", {
+ id: text("id").primaryKey(),
+ identifier: text("identifier").notNull(),
+ value: text("value").notNull(),
+ expiresAt: timestamp("expires_at").notNull(),
+ createdAt: timestamp("created_at").$defaultFn(
+ () => /* @__PURE__ */ new Date(),
+ ),
+ updatedAt: timestamp("updated_at").$defaultFn(
+ () => /* @__PURE__ */ new Date(),
+ ),
+}).enableRLS();
diff --git a/apps/web/src/lib/drag-data.ts b/apps/web/src/lib/drag-data.ts
new file mode 100644
index 00000000..067b0a35
--- /dev/null
+++ b/apps/web/src/lib/drag-data.ts
@@ -0,0 +1,48 @@
+import type { TimelineDragData } from "@/types/drag";
+
+const MIME_TYPE = "application/x-timeline-drag";
+let lastDragData: TimelineDragData | null = null;
+
+export function setDragData({
+ dataTransfer,
+ dragData,
+}: {
+ dataTransfer: DataTransfer;
+ dragData: TimelineDragData;
+}): void {
+ dataTransfer.setData(MIME_TYPE, JSON.stringify(dragData));
+ dataTransfer.setData("text/plain", JSON.stringify(dragData));
+ lastDragData = dragData;
+}
+
+export function getDragData({
+ dataTransfer,
+}: {
+ dataTransfer: DataTransfer;
+}): TimelineDragData | null {
+ const data = dataTransfer.getData(MIME_TYPE);
+ if (data) return JSON.parse(data) as TimelineDragData;
+
+ const textData = dataTransfer.getData("text/plain");
+ if (textData) {
+ try {
+ return JSON.parse(textData) as TimelineDragData;
+ } catch {
+ return lastDragData;
+ }
+ }
+
+ return lastDragData;
+}
+
+export function hasDragData({
+ dataTransfer,
+}: {
+ dataTransfer: DataTransfer;
+}): boolean {
+ return dataTransfer.types.includes(MIME_TYPE) || lastDragData !== null;
+}
+
+export function clearDragData(): void {
+ lastDragData = null;
+}
diff --git a/apps/web/src/lib/editor-utils.ts b/apps/web/src/lib/editor-utils.ts
deleted file mode 100644
index f041aa5e..00000000
--- a/apps/web/src/lib/editor-utils.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { CanvasSize } from "@/types/editor";
-
-const DEFAULT_CANVAS_PRESETS = [
- { name: "16:9", width: 1920, height: 1080 },
- { name: "9:16", width: 1080, height: 1920 },
- { name: "1:1", width: 1080, height: 1080 },
- { name: "4:3", width: 1440, height: 1080 },
-];
-
-/**
- * Helper function to find the best matching canvas preset for an aspect ratio
- * @param aspectRatio The target aspect ratio to match
- * @returns The best matching canvas size
- */
-export function findBestCanvasPreset(aspectRatio: number): CanvasSize {
- // Calculate aspect ratio for each preset and find the closest match
- let bestMatch = DEFAULT_CANVAS_PRESETS[0]; // Default to 16:9 HD
- let smallestDifference = Math.abs(
- aspectRatio - bestMatch.width / bestMatch.height
- );
-
- for (const preset of DEFAULT_CANVAS_PRESETS) {
- const presetAspectRatio = preset.width / preset.height;
- const difference = Math.abs(aspectRatio - presetAspectRatio);
-
- if (difference < smallestDifference) {
- smallestDifference = difference;
- bestMatch = preset;
- }
- }
-
- // If the difference is still significant (> 0.1), create a custom size
- // based on the media aspect ratio with a reasonable resolution
- const bestAspectRatio = bestMatch.width / bestMatch.height;
- if (Math.abs(aspectRatio - bestAspectRatio) > 0.1) {
- // Create custom dimensions based on the aspect ratio
- if (aspectRatio > 1) {
- // Landscape - use 1920 width
- return { width: 1920, height: Math.round(1920 / aspectRatio) };
- }
- // Portrait or square - use 1080 height
- return { width: Math.round(1080 * aspectRatio), height: 1080 };
- }
-
- return { width: bestMatch.width, height: bestMatch.height };
-}
diff --git a/apps/web/src/lib/export.ts b/apps/web/src/lib/export.ts
index 52c58736..67621e16 100644
--- a/apps/web/src/lib/export.ts
+++ b/apps/web/src/lib/export.ts
@@ -1,301 +1,18 @@
-import {
- Output,
- Mp4OutputFormat,
- WebMOutputFormat,
- BufferTarget,
- CanvasSource,
- AudioBufferSource,
- QUALITY_LOW,
- QUALITY_MEDIUM,
- QUALITY_HIGH,
- QUALITY_VERY_HIGH,
-} from "mediabunny";
-import { renderTimelineFrame } from "./timeline-renderer";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { useMediaStore } from "@/stores/media-store";
-import { useProjectStore } from "@/stores/project-store";
-import { DEFAULT_FPS, DEFAULT_CANVAS_SIZE } from "@/stores/project-store";
-import { ExportOptions, ExportResult } from "@/types/export";
-import { TimelineTrack } from "@/types/timeline";
-import { MediaFile } from "@/types/media";
+import { EXPORT_MIME_TYPES } from "@/constants/export-constants";
+import type { ExportFormat } from "@/types/export";
-export const DEFAULT_EXPORT_OPTIONS: ExportOptions = {
- format: "mp4",
- quality: "high",
- includeAudio: true,
-};
-
-const qualityMap = {
- low: QUALITY_LOW,
- medium: QUALITY_MEDIUM,
- high: QUALITY_HIGH,
- very_high: QUALITY_VERY_HIGH,
-};
-
-interface AudioElement {
- buffer: AudioBuffer;
- startTime: number;
- duration: number;
- trimStart: number;
- trimEnd: number;
- muted: boolean;
-}
-
-async function createTimelineAudioBuffer(
- tracks: TimelineTrack[],
- mediaFiles: MediaFile[],
- duration: number,
- sampleRate: number = 44100
-): Promise<AudioBuffer | null> {
- // Get Web Audio context
- const audioContext = new (window.AudioContext ||
- (window as any).webkitAudioContext)();
-
- // Collect all audio elements from timeline
- const audioElements: AudioElement[] = [];
- const mediaMap = new Map<string, MediaFile>(mediaFiles.map((m) => [m.id, m]));
-
- for (const track of tracks) {
- if (track.muted) continue;
-
- for (const element of track.elements) {
- if (element.type !== "media") continue;
-
- const mediaElement = element;
- const mediaItem = mediaMap.get(mediaElement.mediaId);
- if (!mediaItem || mediaItem.type !== "audio") continue;
-
- const visibleDuration =
- mediaElement.duration - mediaElement.trimStart - mediaElement.trimEnd;
- if (visibleDuration <= 0) continue;
-
- try {
- // Decode audio file
- const arrayBuffer = await mediaItem.file.arrayBuffer();
- const audioBuffer = await audioContext.decodeAudioData(
- arrayBuffer.slice(0)
- );
-
- audioElements.push({
- buffer: audioBuffer,
- startTime: mediaElement.startTime,
- duration: mediaElement.duration,
- trimStart: mediaElement.trimStart,
- trimEnd: mediaElement.trimEnd,
- muted: mediaElement.muted || track.muted || false,
- });
- } catch (error) {
- console.warn(`Failed to decode audio file ${mediaItem.name}:`, error);
- }
- }
- }
-
- if (audioElements.length === 0) {
- return null; // No audio to mix
- }
-
- // Create output buffer
- const outputChannels = 2; // Stereo
- const outputLength = Math.ceil(duration * sampleRate);
- const outputBuffer = audioContext.createBuffer(
- outputChannels,
- outputLength,
- sampleRate
- );
-
- // Mix all audio elements
- for (const element of audioElements) {
- if (element.muted) continue;
-
- const {
- buffer,
- startTime,
- trimStart,
- trimEnd,
- duration: elementDuration,
- } = element;
-
- // Calculate timing
- const sourceStartSample = Math.floor(trimStart * buffer.sampleRate);
- const sourceDuration = elementDuration - trimStart - trimEnd;
- const sourceLengthSamples = Math.floor(sourceDuration * buffer.sampleRate);
- const outputStartSample = Math.floor(startTime * sampleRate);
-
- // Resample if needed (simple approach)
- const resampleRatio = sampleRate / buffer.sampleRate;
- const resampledLength = Math.floor(sourceLengthSamples * resampleRatio);
-
- // Mix each channel
- for (let channel = 0; channel < outputChannels; channel++) {
- const outputData = outputBuffer.getChannelData(channel);
- const sourceChannel = Math.min(channel, buffer.numberOfChannels - 1);
- const sourceData = buffer.getChannelData(sourceChannel);
-
- for (let i = 0; i < resampledLength; i++) {
- const outputIndex = outputStartSample + i;
- if (outputIndex >= outputLength) break;
-
- // Simple resampling (could be improved with proper interpolation)
- const sourceIndex = sourceStartSample + Math.floor(i / resampleRatio);
- if (sourceIndex >= sourceData.length) break;
-
- outputData[outputIndex] += sourceData[sourceIndex];
- }
- }
- }
-
- return outputBuffer;
-}
-
-export async function exportProject(
- options: ExportOptions
-): Promise<ExportResult> {
- const { format, quality, fps, includeAudio, onProgress, onCancel } = options;
-
- try {
- const timelineStore = useTimelineStore.getState();
- const mediaStore = useMediaStore.getState();
- const projectStore = useProjectStore.getState();
-
- const { tracks, getTotalDuration } = timelineStore;
- const { mediaFiles } = mediaStore;
- const { activeProject } = projectStore;
-
- if (!activeProject) {
- return { success: false, error: "No active project" };
- }
-
- const duration = getTotalDuration();
- if (duration === 0) {
- return { success: false, error: "Project is empty" };
- }
-
- const exportFps = fps || activeProject.fps || DEFAULT_FPS;
- const canvasSize = activeProject.canvasSize || DEFAULT_CANVAS_SIZE;
-
- const outputFormat =
- format === "webm" ? new WebMOutputFormat() : new Mp4OutputFormat();
-
- // BufferTarget for smaller files, StreamTarget for larger ones
- // TODO: Implement StreamTarget
- const output = new Output({
- format: outputFormat,
- target: new BufferTarget(),
- });
-
- // Canvas for rendering
- const canvas = document.createElement("canvas");
- canvas.width = canvasSize.width;
- canvas.height = canvasSize.height;
- const ctx = canvas.getContext("2d");
-
- if (!ctx) {
- return { success: false, error: "Failed to create canvas context" };
- }
-
- const videoSource = new CanvasSource(canvas, {
- codec: format === "webm" ? "vp9" : "avc", // VP9 for WebM, H.264 for MP4
- bitrate: qualityMap[quality],
- });
-
- output.addVideoTrack(videoSource, { frameRate: exportFps });
-
- // Add audio track if requested (but don't add data yet)
- let audioSource: AudioBufferSource | null = null;
- let audioBuffer: AudioBuffer | null = null;
-
- if (includeAudio) {
- onProgress?.(0.05); // 5% for audio processing
-
- audioBuffer = await createTimelineAudioBuffer(
- tracks,
- mediaFiles,
- duration
- );
-
- if (audioBuffer) {
- audioSource = new AudioBufferSource({
- codec: format === "webm" ? "opus" : "aac", // Opus for WebM, AAC for MP4
- bitrate: qualityMap[quality], // Use same quality for audio
- });
-
- output.addAudioTrack(audioSource);
- }
- }
-
- // Start the output (after all tracks are added)
- await output.start();
-
- // Now add audio data after starting
- if (audioSource && audioBuffer) {
- await audioSource.add(audioBuffer);
- audioSource.close();
- }
-
- const totalFrames = Math.ceil(duration * exportFps);
- let cancelled = false;
-
- // Render each frame
- for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
- // Check for cancellation
- if (onCancel?.()) {
- cancelled = true;
- break;
- }
-
- const time = frameIndex / exportFps;
-
- await renderTimelineFrame({
- ctx,
- time,
- canvasWidth: canvas.width,
- canvasHeight: canvas.height,
- tracks,
- mediaFiles,
- backgroundType: activeProject.backgroundType,
- blurIntensity: activeProject.blurIntensity,
- backgroundColor:
- activeProject.backgroundType === "blur"
- ? undefined
- : activeProject.backgroundColor || "#000000",
- projectCanvasSize: canvasSize,
- });
-
- const frameDuration = 1 / exportFps;
- await videoSource.add(time, frameDuration);
-
- // Adjust progress to account for audio processing (5% at start)
- const videoProgress = includeAudio
- ? 0.05 + (frameIndex / totalFrames) * 0.95
- : frameIndex / totalFrames;
- onProgress?.(videoProgress);
- }
-
- if (cancelled) {
- await output.cancel();
- return { success: false, cancelled: true };
- }
- videoSource.close();
- await output.finalize();
- onProgress?.(1);
-
- return {
- success: true,
- buffer: output.target.buffer || undefined,
- };
- } catch (error) {
- console.error("Export failed:", error);
- return {
- success: false,
- error: error instanceof Error ? error.message : "Unknown export error",
- };
- }
-}
-
-export function getExportMimeType(format: "mp4" | "webm"): string {
- return format === "webm" ? "video/webm" : "video/mp4";
+export function getExportMimeType({
+ format,
+}: {
+ format: ExportFormat;
+}): string {
+ return EXPORT_MIME_TYPES[format];
}
-export function getExportFileExtension(format: "mp4" | "webm"): string {
- return `.${format}`;
+export function getExportFileExtension({
+ format,
+}: {
+ format: ExportFormat;
+}): string {
+ return `.${format}`;
}
diff --git a/apps/web/src/lib/font-config.ts b/apps/web/src/lib/font-config.ts
deleted file mode 100644
index b47b3c3e..00000000
--- a/apps/web/src/lib/font-config.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import {
- Inter,
- Roboto,
- Open_Sans,
- Playfair_Display,
- Comic_Neue,
-} from "next/font/google";
-
-// Configure all fonts
-const inter = Inter({ subsets: ["latin"] });
-const roboto = Roboto({ subsets: ["latin"], weight: ["400", "700"] });
-const openSans = Open_Sans({ subsets: ["latin"] });
-const playfairDisplay = Playfair_Display({ subsets: ["latin"] });
-const comicNeue = Comic_Neue({ subsets: ["latin"], weight: ["400", "700"] });
-
-// Export font class mapping for use in components
-export const FONT_CLASS_MAP = {
- Inter: inter.className,
- Roboto: roboto.className,
- "Open Sans": openSans.className,
- "Playfair Display": playfairDisplay.className,
- "Comic Neue": comicNeue.className,
- Arial: "",
- Helvetica: "",
- "Times New Roman": "",
- Georgia: "",
-} as const;
-
-// Export individual fonts for use in layout
-export const fonts = {
- inter,
- roboto,
- openSans,
- playfairDisplay,
- comicNeue,
-};
-
-// Default font for the body
-export const defaultFont = inter;
diff --git a/apps/web/src/lib/gradients/canvas.ts b/apps/web/src/lib/gradients/canvas.ts
new file mode 100644
index 00000000..e2287333
--- /dev/null
+++ b/apps/web/src/lib/gradients/canvas.ts
@@ -0,0 +1,923 @@
+import type {
+ GradientAst,
+ Color,
+ ColorStop,
+ GradientOrientation,
+} from "./parser";
+import { parseGradient } from "./parser";
+
+type BackgroundLayer =
+ | { type: "color"; value: string }
+ | { type: "gradient"; value: GradientAst };
+
+type LinearPoints = {
+ x0: number;
+ y0: number;
+ x1: number;
+ y1: number;
+ length: number;
+};
+
+type RadialDimensions = {
+ cx: number;
+ cy: number;
+ rx: number;
+ ry: number;
+};
+
+type PositionKeyword = "left" | "center" | "right" | "top" | "bottom";
+
+type Distance =
+ | { type: "%"; value: string }
+ | { type: "position-keyword"; value: string }
+ | { type: "calc"; value: string }
+ | { type: "px"; value: string }
+ | { type: "em"; value: string };
+
+type Position = { type: "position"; value: { x?: Distance; y?: Distance } };
+
+type Shape = {
+ type: "shape";
+ value: "circle" | "ellipse";
+ style?: Distance | { type: "extent-keyword"; value: string } | Position;
+ at?: Position;
+};
+
+type DefaultRadial = { type: "default-radial"; at: Position };
+
+type ExtentKeyword = { type: "extent-keyword"; value: string; at?: Position };
+
+type RadialOrientation = Shape | ExtentKeyword | DefaultRadial;
+
+const gradientLayerPattern =
+ /^(?:-(webkit|o|ms|moz)-)?(linear-gradient|repeating-linear-gradient|radial-gradient|repeating-radial-gradient)/i;
+
+export function drawCssBackground({
+ ctx,
+ width,
+ height,
+ css,
+}: {
+ ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
+ width: number;
+ height: number;
+ css: string;
+}): void {
+ const layers = parseBackgroundLayers({ css });
+ const layersInPaintOrder = layers.slice().reverse();
+
+ for (const layer of layersInPaintOrder) {
+ if (layer.type === "color") {
+ ctx.fillStyle = layer.value;
+ ctx.fillRect(0, 0, width, height);
+ continue;
+ }
+
+ drawGradientLayer({ ctx, width, height, gradient: layer.value });
+ }
+}
+
+const parseBackgroundLayers = ({
+ css,
+}: {
+ css: string;
+}): Array<BackgroundLayer> => {
+ const segments = splitCssLayers({ css });
+ const layers: Array<BackgroundLayer> = [];
+
+ for (const segment of segments) {
+ if (!segment) {
+ continue;
+ }
+
+ if (gradientLayerPattern.test(segment)) {
+ try {
+ const parsed = parseGradient({ code: segment.trim() });
+ for (const gradient of parsed) {
+ layers.push({ type: "gradient", value: gradient });
+ }
+ continue;
+ } catch {
+ layers.push({ type: "color", value: segment });
+ continue;
+ }
+ }
+
+ layers.push({ type: "color", value: segment });
+ }
+
+ return layers;
+};
+
+const splitCssLayers = ({ css }: { css: string }): Array<string> => {
+ const layers: Array<string> = [];
+ let current = "";
+ let depth = 0;
+
+ for (const char of css) {
+ if (char === "(") {
+ depth += 1;
+ }
+ if (char === ")") {
+ depth = Math.max(0, depth - 1);
+ }
+
+ if (char === "," && depth === 0) {
+ layers.push(current.trim());
+ current = "";
+ continue;
+ }
+
+ current += char;
+ }
+
+ if (current.trim()) {
+ layers.push(current.trim());
+ }
+
+ return layers;
+};
+
+const drawGradientLayer = ({
+ ctx,
+ width,
+ height,
+ gradient,
+}: {
+ ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
+ width: number;
+ height: number;
+ gradient: GradientAst;
+}): void => {
+ if (gradient.type.includes("linear")) {
+ drawLinearGradient({ ctx, width, height, gradient });
+ return;
+ }
+
+ drawRadialGradient({ ctx, width, height, gradient });
+};
+
+const drawLinearGradient = ({
+ ctx,
+ width,
+ height,
+ gradient,
+}: {
+ ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
+ width: number;
+ height: number;
+ gradient: GradientAst;
+}): void => {
+ const { x0, y0, x1, y1, length } = resolveLinearPoints({
+ width,
+ height,
+ orientation: gradient.orientation,
+ });
+ const canvasGradient = ctx.createLinearGradient(x0, y0, x1, y1);
+ const colorStops = normalizeColorStops({
+ colorStops: gradient.colorStops,
+ gradientLength: length,
+ });
+
+ for (const stop of colorStops) {
+ canvasGradient.addColorStop(stop.offset, stop.color);
+ }
+
+ ctx.fillStyle = canvasGradient;
+ ctx.fillRect(0, 0, width, height);
+};
+
+const drawRadialGradient = ({
+ ctx,
+ width,
+ height,
+ gradient,
+}: {
+ ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
+ width: number;
+ height: number;
+ gradient: GradientAst;
+}): void => {
+ const { cx, cy, rx, ry } = resolveRadialDimensions({
+ width,
+ height,
+ orientation: gradient.orientation,
+ });
+ const gradientLength = Math.max(rx, ry);
+ const colorStops = normalizeColorStops({
+ colorStops: gradient.colorStops,
+ gradientLength,
+ });
+
+ if (rx === ry || ry === 0) {
+ const canvasGradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, rx);
+ for (const stop of colorStops) {
+ canvasGradient.addColorStop(stop.offset, stop.color);
+ }
+ ctx.fillStyle = canvasGradient;
+ ctx.fillRect(0, 0, width, height);
+ return;
+ }
+
+ const scaleY = ry / rx;
+ ctx.save();
+ ctx.translate(cx, cy);
+ ctx.scale(1, scaleY);
+ const canvasGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, rx);
+ for (const stop of colorStops) {
+ canvasGradient.addColorStop(stop.offset, stop.color);
+ }
+ ctx.fillStyle = canvasGradient;
+ ctx.fillRect(-cx, -cy / scaleY, width, height / scaleY);
+ ctx.restore();
+};
+
+const resolveLinearPoints = ({
+ width,
+ height,
+ orientation,
+}: {
+ width: number;
+ height: number;
+ orientation: GradientOrientation | undefined;
+}): LinearPoints => {
+ const angle = resolveLinearAngle({ orientation });
+ const radians = (angle * Math.PI) / 180;
+ const dx = Math.sin(radians);
+ const dy = -Math.cos(radians);
+ const centerX = width / 2;
+ const centerY = height / 2;
+ const halfLength = (Math.abs(width * dx) + Math.abs(height * dy)) / 2;
+ const x0 = centerX - dx * halfLength;
+ const y0 = centerY - dy * halfLength;
+ const x1 = centerX + dx * halfLength;
+ const y1 = centerY + dy * halfLength;
+ const length = Math.hypot(x1 - x0, y1 - y0);
+
+ return { x0, y0, x1, y1, length };
+};
+
+const resolveLinearAngle = ({
+ orientation,
+}: {
+ orientation: GradientOrientation | undefined;
+}): number => {
+ if (!orientation) {
+ return 180;
+ }
+
+ if (!Array.isArray(orientation)) {
+ if (orientation.type === "angular") {
+ return Number.parseFloat(orientation.value);
+ }
+
+ if (orientation.type === "directional") {
+ return angleFromDirectional({ value: orientation.value });
+ }
+ }
+
+ return 180;
+};
+
+const angleFromDirectional = ({ value }: { value: string }): number => {
+ const normalized = value.toLowerCase().replace("to", "").trim();
+ const parts = normalized.split(/\s+/).filter(Boolean);
+ let dx = 0;
+ let dy = 0;
+
+ for (const part of parts) {
+ if (part === "left") {
+ dx = -1;
+ }
+ if (part === "right") {
+ dx = 1;
+ }
+ if (part === "top") {
+ dy = -1;
+ }
+ if (part === "bottom") {
+ dy = 1;
+ }
+ }
+
+ if (dx === 0 && dy === 0) {
+ return 180;
+ }
+
+ const angle = (Math.atan2(dx, -dy) * 180) / Math.PI;
+ return (angle + 360) % 360;
+};
+
+const resolveRadialDimensions = ({
+ width,
+ height,
+ orientation,
+}: {
+ width: number;
+ height: number;
+ orientation: GradientOrientation | undefined;
+}): RadialDimensions => {
+ const centerFallback = { cx: width / 2, cy: height / 2 };
+ const radial = Array.isArray(orientation)
+ ? (orientation[0] as RadialOrientation | undefined)
+ : undefined;
+
+ if (!radial) {
+ const { rx, ry } = resolveRadialExtents({
+ width,
+ height,
+ cx: centerFallback.cx,
+ cy: centerFallback.cy,
+ shape: "ellipse",
+ extent: "farthest-corner",
+ });
+ return { ...centerFallback, rx, ry };
+ }
+
+ if (radial.type === "shape") {
+ const { cx, cy } = resolveRadialCenter({
+ width,
+ height,
+ position: radial.at,
+ });
+ const shape = radial.value;
+
+ if (radial.style && radial.style.type === "position") {
+ const xDist = radial.style.value.x;
+ const yDist = radial.style.value.y;
+ const rx = xDist
+ ? resolveEllipseDimension({ distance: xDist, axisSize: width })
+ : width / 2;
+ const ry = yDist
+ ? resolveEllipseDimension({ distance: yDist, axisSize: height })
+ : height / 2;
+ return { cx, cy, rx, ry };
+ }
+
+ if (radial.style && radial.style.type !== "extent-keyword") {
+ const resolvedRadius = resolveDistanceInPixels({
+ distance: radial.style,
+ axisSize: Math.max(width, height),
+ });
+ if (shape === "circle") {
+ return {
+ cx,
+ cy,
+ rx: resolvedRadius ?? 0,
+ ry: resolvedRadius ?? 0,
+ };
+ }
+
+ const radius = resolvedRadius ?? 0;
+ return { cx, cy, rx: radius, ry: radius };
+ }
+
+ const extent =
+ radial.style && radial.style.type === "extent-keyword"
+ ? radial.style.value
+ : "farthest-corner";
+ const { rx, ry } = resolveRadialExtents({
+ width,
+ height,
+ cx,
+ cy,
+ shape,
+ extent,
+ });
+ return { cx, cy, rx, ry };
+ }
+
+ if (radial.type === "extent-keyword") {
+ const { cx, cy } = resolveRadialCenter({
+ width,
+ height,
+ position: radial.at,
+ });
+ const { rx, ry } = resolveRadialExtents({
+ width,
+ height,
+ cx,
+ cy,
+ shape: "ellipse",
+ extent: radial.value,
+ });
+ return { cx, cy, rx, ry };
+ }
+
+ const { cx, cy } = resolveRadialCenter({
+ width,
+ height,
+ position: radial.at,
+ });
+ const { rx, ry } = resolveRadialExtents({
+ width,
+ height,
+ cx,
+ cy,
+ shape: "ellipse",
+ extent: "farthest-corner",
+ });
+ return { cx, cy, rx, ry };
+};
+
+const resolveRadialCenter = ({
+ width,
+ height,
+ position,
+}: {
+ width: number;
+ height: number;
+ position?: Position;
+}): { cx: number; cy: number } => {
+ if (!position) {
+ return { cx: width / 2, cy: height / 2 };
+ }
+
+ const normalized = normalizePositionKeywords({ position });
+ const cx = resolvePositionValue({
+ distance: normalized.value.x,
+ axisSize: width,
+ axis: "x",
+ });
+ const cy = resolvePositionValue({
+ distance: normalized.value.y,
+ axisSize: height,
+ axis: "y",
+ });
+
+ return { cx, cy };
+};
+
+const normalizePositionKeywords = ({
+ position,
+}: {
+ position: Position;
+}): Position => {
+ const xValue = position.value.x;
+ const yValue = position.value.y;
+
+ if (
+ xValue?.type === "position-keyword" &&
+ yValue?.type === "position-keyword"
+ ) {
+ const xKeyword = xValue.value.toLowerCase() as PositionKeyword;
+ const yKeyword = yValue.value.toLowerCase() as PositionKeyword;
+ const xIsVertical = xKeyword === "top" || xKeyword === "bottom";
+ const yIsHorizontal = yKeyword === "left" || yKeyword === "right";
+
+ if (xIsVertical && yIsHorizontal) {
+ return {
+ type: "position",
+ value: {
+ x: { type: "position-keyword", value: yKeyword },
+ y: { type: "position-keyword", value: xKeyword },
+ },
+ };
+ }
+ }
+
+ return position;
+};
+
+const resolveRadialExtents = ({
+ width,
+ height,
+ cx,
+ cy,
+ shape,
+ extent,
+}: {
+ width: number;
+ height: number;
+ cx: number;
+ cy: number;
+ shape: "circle" | "ellipse";
+ extent: string;
+}): { rx: number; ry: number } => {
+ const left = cx;
+ const right = width - cx;
+ const top = cy;
+ const bottom = height - cy;
+
+ if (shape === "circle") {
+ const distances = [
+ Math.hypot(left, top),
+ Math.hypot(right, top),
+ Math.hypot(left, bottom),
+ Math.hypot(right, bottom),
+ ];
+ if (extent === "closest-side") {
+ return { rx: Math.min(left, right, top, bottom), ry: 0 };
+ }
+ if (extent === "farthest-side") {
+ return { rx: Math.max(left, right, top, bottom), ry: 0 };
+ }
+ if (extent === "closest-corner") {
+ return { rx: Math.min(...distances), ry: 0 };
+ }
+ return { rx: Math.max(...distances), ry: 0 };
+ }
+
+ if (extent === "closest-side") {
+ return { rx: Math.min(left, right), ry: Math.min(top, bottom) };
+ }
+ if (extent === "farthest-side") {
+ return { rx: Math.max(left, right), ry: Math.max(top, bottom) };
+ }
+
+ const corners = [
+ { dx: left, dy: top },
+ { dx: right, dy: top },
+ { dx: left, dy: bottom },
+ { dx: right, dy: bottom },
+ ];
+ const sorted = corners
+ .slice()
+ .sort((a, b) => Math.hypot(a.dx, a.dy) - Math.hypot(b.dx, b.dy));
+ const chosen =
+ extent === "closest-corner" ? sorted[0] : sorted[sorted.length - 1];
+ return { rx: Math.abs(chosen.dx), ry: Math.abs(chosen.dy) };
+};
+
+const resolvePositionValue = ({
+ distance,
+ axisSize,
+ axis,
+}: {
+ distance?: Distance;
+ axisSize: number;
+ axis: "x" | "y";
+}): number => {
+ if (!distance) {
+ return axisSize / 2;
+ }
+
+ if (distance.type === "%") {
+ return (Number.parseFloat(distance.value) / 100) * axisSize;
+ }
+
+ if (distance.type === "position-keyword") {
+ return keywordToPosition({
+ value: distance.value,
+ axisSize,
+ axis,
+ });
+ }
+
+ if (distance.type === "px") {
+ return Number.parseFloat(distance.value);
+ }
+
+ if (distance.type === "em") {
+ return Number.parseFloat(distance.value) * 16;
+ }
+
+ return axisSize / 2;
+};
+
+const resolveDistanceInPixels = ({
+ distance,
+ axisSize,
+}: {
+ distance: Distance;
+ axisSize: number;
+}): number | null => {
+ if (distance.type === "%") {
+ return (Number.parseFloat(distance.value) / 100) * axisSize;
+ }
+
+ if (distance.type === "px") {
+ return Number.parseFloat(distance.value);
+ }
+
+ if (distance.type === "em") {
+ return Number.parseFloat(distance.value) * 16;
+ }
+
+ return null;
+};
+
+const resolveEllipseDimension = ({
+ distance,
+ axisSize,
+}: {
+ distance: Distance;
+ axisSize: number;
+}): number => {
+ if (distance.type === "%") {
+ return (Number.parseFloat(distance.value) / 100) * axisSize;
+ }
+
+ if (distance.type === "px") {
+ return Number.parseFloat(distance.value);
+ }
+
+ if (distance.type === "em") {
+ return Number.parseFloat(distance.value) * 16;
+ }
+
+ return axisSize / 2;
+};
+
+const keywordToPosition = ({
+ value,
+ axisSize,
+ axis,
+}: {
+ value: string;
+ axisSize: number;
+ axis: "x" | "y";
+}): number => {
+ const keyword = value.toLowerCase() as PositionKeyword;
+
+ if (keyword === "center") {
+ return axisSize / 2;
+ }
+
+ if (axis === "x") {
+ if (keyword === "left") {
+ return 0;
+ }
+ if (keyword === "right") {
+ return axisSize;
+ }
+ }
+
+ if (axis === "y") {
+ if (keyword === "top") {
+ return 0;
+ }
+ if (keyword === "bottom") {
+ return axisSize;
+ }
+ }
+
+ return axisSize / 2;
+};
+
+const isTransparent = ({ color }: { color: string }): boolean => {
+ const lower = color.toLowerCase().trim();
+ if (lower === "transparent") {
+ return true;
+ }
+ const rgbaMatch = lower.match(
+ /^rgba\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)$/,
+ );
+ if (rgbaMatch && Number.parseFloat(rgbaMatch[1]) === 0) {
+ return true;
+ }
+ return false;
+};
+
+const parseColorToRgb = ({
+ color,
+}: {
+ color: string;
+}): { r: number; g: number; b: number; a: number } | null => {
+ const lower = color.toLowerCase().trim();
+
+ const hexMatch = lower.match(/^#([0-9a-f]{3,8})$/);
+ if (hexMatch) {
+ const hex = hexMatch[1];
+ if (hex.length === 3) {
+ return {
+ r: Number.parseInt(hex[0] + hex[0], 16),
+ g: Number.parseInt(hex[1] + hex[1], 16),
+ b: Number.parseInt(hex[2] + hex[2], 16),
+ a: 1,
+ };
+ }
+ if (hex.length === 6) {
+ return {
+ r: Number.parseInt(hex.slice(0, 2), 16),
+ g: Number.parseInt(hex.slice(2, 4), 16),
+ b: Number.parseInt(hex.slice(4, 6), 16),
+ a: 1,
+ };
+ }
+ if (hex.length === 8) {
+ return {
+ r: Number.parseInt(hex.slice(0, 2), 16),
+ g: Number.parseInt(hex.slice(2, 4), 16),
+ b: Number.parseInt(hex.slice(4, 6), 16),
+ a: Number.parseInt(hex.slice(6, 8), 16) / 255,
+ };
+ }
+ }
+
+ const rgbMatch = lower.match(
+ /^rgb\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/,
+ );
+ if (rgbMatch) {
+ return {
+ r: Number.parseFloat(rgbMatch[1]),
+ g: Number.parseFloat(rgbMatch[2]),
+ b: Number.parseFloat(rgbMatch[3]),
+ a: 1,
+ };
+ }
+
+ const rgbaMatch = lower.match(
+ /^rgba\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/,
+ );
+ if (rgbaMatch) {
+ return {
+ r: Number.parseFloat(rgbaMatch[1]),
+ g: Number.parseFloat(rgbaMatch[2]),
+ b: Number.parseFloat(rgbaMatch[3]),
+ a: Number.parseFloat(rgbaMatch[4]),
+ };
+ }
+
+ return null;
+};
+
+const fixTransparentStops = ({
+ stops,
+}: {
+ stops: Array<{ color: string; offset: number | null }>;
+}): Array<{ color: string; offset: number | null }> => {
+ if (stops.length === 0) {
+ return stops;
+ }
+
+ const result = stops.map((stop) => ({ ...stop }));
+
+ for (let i = 0; i < result.length; i++) {
+ if (!isTransparent({ color: result[i].color })) {
+ continue;
+ }
+
+ let donorColor: { r: number; g: number; b: number } | null = null;
+
+ for (let j = i - 1; j >= 0; j--) {
+ if (!isTransparent({ color: result[j].color })) {
+ const parsed = parseColorToRgb({ color: result[j].color });
+ if (parsed) {
+ donorColor = parsed;
+ break;
+ }
+ }
+ }
+
+ if (!donorColor) {
+ for (let j = i + 1; j < result.length; j++) {
+ if (!isTransparent({ color: result[j].color })) {
+ const parsed = parseColorToRgb({ color: result[j].color });
+ if (parsed) {
+ donorColor = parsed;
+ break;
+ }
+ }
+ }
+ }
+
+ if (donorColor) {
+ result[i].color =
+ `rgba(${donorColor.r},${donorColor.g},${donorColor.b},0)`;
+ }
+ }
+
+ return result;
+};
+
+const normalizeColorStops = ({
+ colorStops,
+ gradientLength,
+}: {
+ colorStops: Array<ColorStop>;
+ gradientLength: number;
+}): Array<{ color: string; offset: number }> => {
+ const mappedStops = colorStops.map((stop) => ({
+ color: colorToString({ color: stop }),
+ offset: resolveStopOffset({ stop, gradientLength }),
+ }));
+
+ const fixedStops = fixTransparentStops({ stops: mappedStops });
+ const resolvedStops = fixedStops.map((stop) => ({ ...stop }));
+ const knownIndices = resolvedStops
+ .map((stop, index) => (stop.offset === null ? null : index))
+ .filter((index): index is number => index !== null);
+
+ if (knownIndices.length === 0) {
+ const step = resolvedStops.length > 1 ? 1 / (resolvedStops.length - 1) : 1;
+ for (let index = 0; index < resolvedStops.length; index += 1) {
+ resolvedStops[index].offset = step * index;
+ }
+ return clampStops({ stops: resolvedStops });
+ }
+
+ const firstKnown = knownIndices[0];
+ if (resolvedStops[firstKnown].offset === null) {
+ resolvedStops[firstKnown].offset = 0;
+ }
+
+ for (let index = 0; index < firstKnown; index += 1) {
+ const nextOffset = resolvedStops[firstKnown].offset ?? 0;
+ resolvedStops[index].offset = (nextOffset * index) / firstKnown;
+ }
+
+ for (let i = 0; i < knownIndices.length - 1; i += 1) {
+ const startIndex = knownIndices[i];
+ const endIndex = knownIndices[i + 1];
+ const startOffset = resolvedStops[startIndex].offset ?? 0;
+ const endOffset = resolvedStops[endIndex].offset ?? startOffset;
+ const gap = endIndex - startIndex;
+
+ if (gap <= 1) {
+ continue;
+ }
+
+ const step = (endOffset - startOffset) / gap;
+ for (let index = 1; index < gap; index += 1) {
+ resolvedStops[startIndex + index].offset = startOffset + step * index;
+ }
+ }
+
+ const lastKnown = knownIndices[knownIndices.length - 1];
+ const lastOffset = resolvedStops[lastKnown].offset ?? 1;
+ if (lastKnown < resolvedStops.length - 1) {
+ const gap = resolvedStops.length - 1 - lastKnown;
+ const step = (1 - lastOffset) / gap;
+ for (let index = 1; index <= gap; index += 1) {
+ resolvedStops[lastKnown + index].offset = lastOffset + step * index;
+ }
+ }
+
+ return clampStops({ stops: resolvedStops });
+};
+
+const clampStops = ({
+ stops,
+}: {
+ stops: Array<{ color: string; offset: number | null }>;
+}): Array<{ color: string; offset: number }> => {
+ return stops.map((stop) => ({
+ color: stop.color,
+ offset: clamp01({ value: stop.offset ?? 0 }),
+ }));
+};
+
+const resolveStopOffset = ({
+ stop,
+ gradientLength,
+}: {
+ stop: ColorStop;
+ gradientLength: number;
+}): number | null => {
+ if (!stop.length) {
+ return null;
+ }
+
+ if (stop.length.type === "%") {
+ return Number.parseFloat(stop.length.value) / 100;
+ }
+
+ if (stop.length.type === "px") {
+ return Number.parseFloat(stop.length.value) / gradientLength;
+ }
+
+ if (stop.length.type === "em") {
+ return (Number.parseFloat(stop.length.value) * 16) / gradientLength;
+ }
+
+ return null;
+};
+
+const clamp01 = ({ value }: { value: number }): number => {
+ if (value < 0) {
+ return 0;
+ }
+ if (value > 1) {
+ return 1;
+ }
+ return value;
+};
+
+const colorToString = ({ color }: { color: Color }): string => {
+ if (color.type === "hex") {
+ return `#${color.value}`;
+ }
+
+ if (color.type === "literal") {
+ return color.value;
+ }
+
+ if (color.type === "rgb") {
+ return `rgb(${color.value.join(",")})`;
+ }
+
+ if (color.type === "rgba") {
+ return `rgba(${color.value.join(",")})`;
+ }
+
+ if (color.type === "hsl") {
+ return `hsl(${color.value.join(",")})`;
+ }
+
+ if (color.type === "hsla") {
+ return `hsla(${color.value.join(",")})`;
+ }
+
+ return `var(${color.value})`;
+};
diff --git a/apps/web/src/lib/gradients/index.ts b/apps/web/src/lib/gradients/index.ts
new file mode 100644
index 00000000..490a8fbf
--- /dev/null
+++ b/apps/web/src/lib/gradients/index.ts
@@ -0,0 +1,2 @@
+export * from "./parser";
+export * from "./canvas";
diff --git a/apps/web/src/lib/gradients/parser.ts b/apps/web/src/lib/gradients/parser.ts
new file mode 100644
index 00000000..55440e3c
--- /dev/null
+++ b/apps/web/src/lib/gradients/parser.ts
@@ -0,0 +1,652 @@
+/*
+ * Original source: https://github.com/rafaelcaricio/gradient-parser/blob/master/lib/parser.js
+ */
+
+type GradientType =
+ | "linear-gradient"
+ | "repeating-linear-gradient"
+ | "radial-gradient"
+ | "repeating-radial-gradient";
+
+type DirectionalOrientation = { type: "directional"; value: string };
+type AngularOrientation = { type: "angular"; value: string };
+type LinearOrientation = DirectionalOrientation | AngularOrientation;
+
+type Distance =
+ | { type: "%"; value: string }
+ | { type: "position-keyword"; value: string }
+ | { type: "calc"; value: string }
+ | { type: "px"; value: string }
+ | { type: "em"; value: string };
+
+type PositionValue = { x?: Distance; y?: Distance };
+type Position = { type: "position"; value: PositionValue };
+
+type ExtentKeyword = { type: "extent-keyword"; value: string };
+
+type ShapeValue = "circle" | "ellipse";
+type Shape = {
+ type: "shape";
+ value: ShapeValue;
+ style?: Distance | ExtentKeyword | Position;
+ at?: Position;
+};
+
+type DefaultRadial = { type: "default-radial"; at: Position };
+
+type RadialOrientation =
+ | Shape
+ | (ExtentKeyword & { at?: Position })
+ | DefaultRadial;
+
+export type GradientOrientation = LinearOrientation | Array<RadialOrientation>;
+
+export type Color =
+ | { type: "hex"; value: string }
+ | { type: "literal"; value: string }
+ | { type: "rgb"; value: Array<string> }
+ | { type: "rgba"; value: Array<string> }
+ | { type: "hsl"; value: [string, string, string] }
+ | { type: "hsla"; value: [string, string, string, string] }
+ | { type: "var"; value: string };
+
+export type ColorStop = Color & { length?: Distance };
+
+export type GradientAst = {
+ type: GradientType;
+ orientation: GradientOrientation | undefined;
+ colorStops: Array<ColorStop>;
+};
+
+type Tokens = {
+ linearGradient: RegExp;
+ repeatingLinearGradient: RegExp;
+ radialGradient: RegExp;
+ repeatingRadialGradient: RegExp;
+ sideOrCorner: RegExp;
+ extentKeywords: RegExp;
+ positionKeywords: RegExp;
+ pixelValue: RegExp;
+ percentageValue: RegExp;
+ emValue: RegExp;
+ angleValue: RegExp;
+ radianValue: RegExp;
+ startCall: RegExp;
+ endCall: RegExp;
+ comma: RegExp;
+ hexColor: RegExp;
+ literalColor: RegExp;
+ rgbColor: RegExp;
+ rgbaColor: RegExp;
+ varColor: RegExp;
+ calcValue: RegExp;
+ variableName: RegExp;
+ number: RegExp;
+ hslColor: RegExp;
+ hslaColor: RegExp;
+};
+
+const tokens: Tokens = {
+ linearGradient: /^(-(webkit|o|ms|moz)-)?(linear-gradient)/i,
+ repeatingLinearGradient:
+ /^(-(webkit|o|ms|moz)-)?(repeating-linear-gradient)/i,
+ radialGradient: /^(-(webkit|o|ms|moz)-)?(radial-gradient)/i,
+ repeatingRadialGradient:
+ /^(-(webkit|o|ms|moz)-)?(repeating-radial-gradient)/i,
+ sideOrCorner:
+ /^to (left (top|bottom)|right (top|bottom)|top (left|right)|bottom (left|right)|left|right|top|bottom)/i,
+ extentKeywords:
+ /^(closest-side|closest-corner|farthest-side|farthest-corner|contain|cover)/,
+ positionKeywords: /^(left|center|right|top|bottom)/i,
+ pixelValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))px/,
+ percentageValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))%/,
+ emValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))em/,
+ angleValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))deg/,
+ radianValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))rad/,
+ startCall: /^\(/,
+ endCall: /^\)/,
+ comma: /^,/,
+ hexColor: /^#([0-9a-fA-F]+)/,
+ literalColor: /^([a-zA-Z]+)/,
+ rgbColor: /^rgb/i,
+ rgbaColor: /^rgba/i,
+ varColor: /^var/i,
+ calcValue: /^calc/i,
+ variableName: /^(--[a-zA-Z0-9-,\s#]+)/,
+ number: /^(([0-9]*\.[0-9]+)|([0-9]+\.?))/,
+ hslColor: /^hsl/i,
+ hslaColor: /^hsla/i,
+};
+
+let input = "";
+
+const error = ({ message }: { message: string }): never => {
+ const err = new Error(`${input}: ${message}`);
+ (err as Error & { source?: string }).source = input;
+ throw err;
+};
+
+const getAst = (): Array<GradientAst> => {
+ const ast = matchListDefinitions();
+
+ if (input.length > 0) {
+ error({ message: "Invalid input not EOF" });
+ }
+
+ return ast;
+};
+
+const matchListDefinitions = (): Array<GradientAst> =>
+ matchListing({ matcher: matchDefinition });
+
+const matchDefinition = (): GradientAst | undefined =>
+ matchGradient({
+ gradientType: "linear-gradient",
+ pattern: tokens.linearGradient,
+ orientationMatcher: matchLinearOrientation,
+ }) ||
+ matchGradient({
+ gradientType: "repeating-linear-gradient",
+ pattern: tokens.repeatingLinearGradient,
+ orientationMatcher: matchLinearOrientation,
+ }) ||
+ matchGradient({
+ gradientType: "radial-gradient",
+ pattern: tokens.radialGradient,
+ orientationMatcher: matchListRadialOrientations,
+ }) ||
+ matchGradient({
+ gradientType: "repeating-radial-gradient",
+ pattern: tokens.repeatingRadialGradient,
+ orientationMatcher: matchListRadialOrientations,
+ });
+
+const matchGradient = ({
+ gradientType,
+ pattern,
+ orientationMatcher,
+}: {
+ gradientType: GradientType;
+ pattern: RegExp;
+ orientationMatcher: () => GradientOrientation | undefined;
+}): GradientAst | undefined =>
+ matchCall({
+ pattern,
+ callback: () => {
+ const orientation = orientationMatcher();
+ if (orientation && !scan({ regexp: tokens.comma })) {
+ error({ message: "Missing comma before color stops" });
+ }
+
+ return {
+ type: gradientType,
+ orientation,
+ colorStops: matchListing({ matcher: matchColorStop }),
+ };
+ },
+ });
+
+const matchCall = <T>({
+ pattern,
+ callback,
+}: {
+ pattern: RegExp;
+ callback: (captures: RegExpExecArray) => T;
+}): T | undefined => {
+ const captures = scan({ regexp: pattern });
+
+ if (!captures) {
+ return undefined;
+ }
+
+ if (!scan({ regexp: tokens.startCall })) {
+ error({ message: "Missing (" });
+ }
+
+ const result = callback(captures);
+
+ if (!scan({ regexp: tokens.endCall })) {
+ error({ message: "Missing )" });
+ }
+
+ return result;
+};
+
+const matchLinearOrientation = (): LinearOrientation | undefined => {
+ const sideOrCorner = matchSideOrCorner();
+ if (sideOrCorner) {
+ return sideOrCorner;
+ }
+
+ const legacyDirection = match({
+ type: "position-keyword",
+ pattern: tokens.positionKeywords,
+ captureIndex: 1,
+ });
+ if (legacyDirection) {
+ return {
+ type: "directional",
+ value: legacyDirection.value,
+ };
+ }
+
+ return matchAngle();
+};
+
+const matchSideOrCorner = (): DirectionalOrientation | undefined =>
+ match({ type: "directional", pattern: tokens.sideOrCorner, captureIndex: 1 });
+
+const matchAngle = (): AngularOrientation | undefined =>
+ match({ type: "angular", pattern: tokens.angleValue, captureIndex: 1 }) ||
+ match({ type: "angular", pattern: tokens.radianValue, captureIndex: 1 });
+
+const matchListRadialOrientations = ():
+ | Array<RadialOrientation>
+ | undefined => {
+ const radialOrientation = matchRadialOrientation();
+ if (!radialOrientation) {
+ return undefined;
+ }
+
+ const radialOrientations: Array<RadialOrientation> = [radialOrientation];
+ const lookaheadCache = input;
+
+ if (!scan({ regexp: tokens.comma })) {
+ return radialOrientations;
+ }
+
+ const nextRadial = matchRadialOrientation();
+ if (!nextRadial) {
+ input = lookaheadCache;
+ return radialOrientations;
+ }
+
+ radialOrientations.push(nextRadial);
+ return radialOrientations;
+};
+
+const matchRadialOrientation = (): RadialOrientation | undefined => {
+ const radialType = matchCircle() || matchEllipse();
+ if (radialType) {
+ radialType.at = matchAtPosition();
+ return radialType;
+ }
+
+ const extent = matchExtentKeyword();
+ if (extent) {
+ const positionAt = matchAtPosition();
+ if (positionAt) {
+ return { ...extent, at: positionAt };
+ }
+ return extent;
+ }
+
+ const implicitEllipse = matchImplicitEllipse();
+ if (implicitEllipse) {
+ return implicitEllipse;
+ }
+
+ const atPosition = matchAtPosition();
+ if (atPosition) {
+ return { type: "default-radial", at: atPosition };
+ }
+
+ const defaultPosition = matchPositioning();
+ if (defaultPosition) {
+ return { type: "default-radial", at: defaultPosition };
+ }
+
+ return undefined;
+};
+
+const matchImplicitEllipse = (): Shape | undefined => {
+ const lookaheadCache = input;
+
+ const width = matchDistance();
+ if (!width) {
+ return undefined;
+ }
+
+ const height = matchDistance();
+ if (!height) {
+ input = lookaheadCache;
+ return undefined;
+ }
+
+ const atPos = matchAtPosition();
+ if (!atPos) {
+ input = lookaheadCache;
+ return undefined;
+ }
+
+ return {
+ type: "shape",
+ value: "ellipse",
+ style: { type: "position", value: { x: width, y: height } },
+ at: atPos,
+ };
+};
+
+const matchCircle = (): Shape | undefined => {
+ const circle = match({
+ type: "shape",
+ pattern: /^(circle)/i,
+ captureIndex: 0,
+ }) as Shape | undefined;
+
+ if (!circle) {
+ return undefined;
+ }
+
+ circle.style = matchLength() || matchExtentKeyword();
+ circle.value = "circle";
+ return circle;
+};
+
+const matchEllipse = (): Shape | undefined => {
+ const ellipse = match({
+ type: "shape",
+ pattern: /^(ellipse)/i,
+ captureIndex: 0,
+ }) as Shape | undefined;
+
+ if (!ellipse) {
+ return undefined;
+ }
+
+ ellipse.style = matchPositioning() || matchDistance() || matchExtentKeyword();
+ ellipse.value = "ellipse";
+ return ellipse;
+};
+
+const matchExtentKeyword = (): ExtentKeyword | undefined =>
+ match({
+ type: "extent-keyword",
+ pattern: tokens.extentKeywords,
+ captureIndex: 1,
+ });
+
+const matchAtPosition = (): Position | undefined => {
+ if (!match({ type: "position", pattern: /^at/, captureIndex: 0 })) {
+ return undefined;
+ }
+
+ const positioning = matchPositioning();
+ if (!positioning) {
+ error({ message: "Missing positioning value" });
+ }
+
+ return positioning;
+};
+
+const matchPositioning = (): Position | undefined => {
+ const location = matchCoordinates();
+
+ if (!location.x && !location.y) {
+ return undefined;
+ }
+
+ return {
+ type: "position",
+ value: location,
+ };
+};
+
+const matchCoordinates = (): PositionValue => ({
+ x: matchDistance(),
+ y: matchDistance(),
+});
+
+const matchListing = <T>({
+ matcher,
+}: {
+ matcher: () => T | undefined;
+}): Array<T> => {
+ const captures = matcher();
+ const result: Array<T> = [];
+
+ if (!captures) {
+ return result;
+ }
+
+ result.push(captures);
+ while (scan({ regexp: tokens.comma })) {
+ const nextCapture = matcher() ?? error({ message: "One extra comma" });
+ result.push(nextCapture);
+ }
+
+ return result;
+};
+
+const matchColorStop = (): ColorStop => {
+ const color = matchColor() ?? error({ message: "Expected color definition" });
+ const length = matchDistance();
+ return { ...color, length };
+};
+
+const matchColor = (): Color | undefined =>
+ matchHexColor() ||
+ matchHSLAColor() ||
+ matchHSLColor() ||
+ matchRGBAColor() ||
+ matchRGBColor() ||
+ matchVarColor() ||
+ matchLiteralColor();
+
+const matchLiteralColor = (): Color | undefined =>
+ match({ type: "literal", pattern: tokens.literalColor, captureIndex: 0 });
+
+const matchHexColor = (): Color | undefined =>
+ match({ type: "hex", pattern: tokens.hexColor, captureIndex: 1 });
+
+const matchRGBColor = (): Color | undefined =>
+ matchCall({
+ pattern: tokens.rgbColor,
+ callback: () => ({
+ type: "rgb",
+ value: matchListing({ matcher: matchNumber }),
+ }),
+ });
+
+const matchRGBAColor = (): Color | undefined =>
+ matchCall({
+ pattern: tokens.rgbaColor,
+ callback: () => ({
+ type: "rgba",
+ value: matchListing({ matcher: matchNumber }),
+ }),
+ });
+
+const matchVarColor = (): Color | undefined =>
+ matchCall({
+ pattern: tokens.varColor,
+ callback: () => ({
+ type: "var",
+ value: matchVariableName(),
+ }),
+ });
+
+const matchHSLColor = (): Color | undefined =>
+ matchCall({
+ pattern: tokens.hslColor,
+ callback: () => {
+ const lookahead = scan({ regexp: tokens.percentageValue });
+ if (lookahead) {
+ error({
+ message:
+ "HSL hue value must be a number in degrees (0-360) or normalized (-360 to 360), not a percentage",
+ });
+ }
+
+ const hue = matchNumber();
+ scan({ regexp: tokens.comma });
+ let captures = scan({ regexp: tokens.percentageValue });
+ const sat = captures ? captures[1] : null;
+ scan({ regexp: tokens.comma });
+ captures = scan({ regexp: tokens.percentageValue });
+ const light = captures ? captures[1] : null;
+ const ensuredSat =
+ sat ??
+ error({
+ message:
+ "Expected percentage value for saturation and lightness in HSL",
+ });
+ const ensuredLight =
+ light ??
+ error({
+ message:
+ "Expected percentage value for saturation and lightness in HSL",
+ });
+ return {
+ type: "hsl",
+ value: [hue, ensuredSat, ensuredLight],
+ };
+ },
+ });
+
+const matchHSLAColor = (): Color | undefined =>
+ matchCall({
+ pattern: tokens.hslaColor,
+ callback: () => {
+ const hue = matchNumber();
+ scan({ regexp: tokens.comma });
+ let captures = scan({ regexp: tokens.percentageValue });
+ const sat = captures ? captures[1] : null;
+ scan({ regexp: tokens.comma });
+ captures = scan({ regexp: tokens.percentageValue });
+ const light = captures ? captures[1] : null;
+ scan({ regexp: tokens.comma });
+ const alpha = matchNumber();
+ const ensuredSat =
+ sat ??
+ error({
+ message:
+ "Expected percentage value for saturation and lightness in HSLA",
+ });
+ const ensuredLight =
+ light ??
+ error({
+ message:
+ "Expected percentage value for saturation and lightness in HSLA",
+ });
+ return {
+ type: "hsla",
+ value: [hue, ensuredSat, ensuredLight, alpha],
+ };
+ },
+ });
+
+const matchVariableName = (): string => {
+ const captures =
+ scan({ regexp: tokens.variableName }) ??
+ error({ message: "Expected CSS variable name" });
+ return captures[1];
+};
+
+const matchNumber = (): string => {
+ const captures =
+ scan({ regexp: tokens.number }) ?? error({ message: "Expected number" });
+ return captures[1];
+};
+
+const matchDistance = (): Distance | undefined =>
+ match({ type: "%", pattern: tokens.percentageValue, captureIndex: 1 }) ||
+ matchPositionKeyword() ||
+ matchCalc() ||
+ matchLength();
+
+const matchPositionKeyword = (): Distance | undefined =>
+ match({
+ type: "position-keyword",
+ pattern: tokens.positionKeywords,
+ captureIndex: 1,
+ });
+
+const matchCalc = (): Distance | undefined =>
+ matchCall({
+ pattern: tokens.calcValue,
+ callback: () => {
+ let openParenCount = 1;
+ let index = 0;
+
+ while (openParenCount > 0 && index < input.length) {
+ const char = input.charAt(index);
+ if (char === "(") {
+ openParenCount++;
+ } else if (char === ")") {
+ openParenCount--;
+ }
+ index++;
+ }
+
+ if (openParenCount > 0) {
+ error({ message: "Missing closing parenthesis in calc() expression" });
+ }
+
+ const calcContent = input.slice(0, index - 1);
+ consume({ size: index - 1 });
+
+ return {
+ type: "calc",
+ value: calcContent,
+ };
+ },
+ });
+
+const matchLength = (): Distance | undefined =>
+ match({ type: "px", pattern: tokens.pixelValue, captureIndex: 1 }) ||
+ match({ type: "em", pattern: tokens.emValue, captureIndex: 1 });
+
+const match = <TType extends string>({
+ type,
+ pattern,
+ captureIndex,
+}: {
+ type: TType;
+ pattern: RegExp;
+ captureIndex: number;
+}): { type: TType; value: string } | undefined => {
+ const captures = scan({ regexp: pattern });
+ if (!captures) {
+ return undefined;
+ }
+
+ return {
+ type,
+ value: captures[captureIndex],
+ };
+};
+
+const scan = ({ regexp }: { regexp: RegExp }): RegExpExecArray | null => {
+ const blankCaptures = /^[\n\r\t\s]+/.exec(input);
+ if (blankCaptures) {
+ consume({ size: blankCaptures[0].length });
+ }
+
+ const captures = regexp.exec(input);
+ if (captures) {
+ consume({ size: captures[0].length });
+ }
+
+ return captures;
+};
+
+const consume = ({ size }: { size: number }): void => {
+ input = input.slice(size);
+};
+
+export const parseGradient = ({
+ code,
+}: {
+ code: string;
+}): Array<GradientAst> => {
+ input = code.toString().trim();
+ if (input.endsWith(";")) {
+ input = input.slice(0, -1);
+ }
+ return getAst();
+};
+
+export const GradientParser = {
+ parse: parseGradient,
+};
diff --git a/apps/web/src/lib/iconify-api.ts b/apps/web/src/lib/iconify-api.ts
index 9d1bcb97..e4afe111 100644
--- a/apps/web/src/lib/iconify-api.ts
+++ b/apps/web/src/lib/iconify-api.ts
@@ -1,242 +1,241 @@
-
export const ICONIFY_HOSTS = [
- "https://api.iconify.design",
- "https://api.simplesvg.com",
- "https://api.unisvg.com",
+ "https://api.iconify.design",
+ "https://api.simplesvg.com",
+ "https://api.unisvg.com",
];
let currentHost = ICONIFY_HOSTS[0];
async function fetchWithFallback(path: string): Promise<Response> {
- for (const host of ICONIFY_HOSTS) {
- try {
- const response = await fetch(`${host}${path}`, {
- signal: AbortSignal.timeout(2000),
- });
- if (response.ok) {
- currentHost = host;
- return response;
- }
- } catch (error) {
- console.warn(`Failed to fetch from ${host}:`, error);
- }
- }
- throw new Error("All API hosts failed");
+ for (const host of ICONIFY_HOSTS) {
+ try {
+ const response = await fetch(`${host}${path}`, {
+ signal: AbortSignal.timeout(2000),
+ });
+ if (response.ok) {
+ currentHost = host;
+ return response;
+ }
+ } catch (error) {
+ console.warn(`Failed to fetch from ${host}:`, error);
+ }
+ }
+ throw new Error("All API hosts failed");
}
export interface IconSet {
- prefix: string;
- name: string;
- total: number;
- author?: {
- name: string;
- url?: string;
- };
- license?: {
- title: string;
- spdx?: string;
- url?: string;
- };
- samples?: string[];
- category?: string;
- palette?: boolean;
+ prefix: string;
+ name: string;
+ total: number;
+ author?: {
+ name: string;
+ url?: string;
+ };
+ license?: {
+ title: string;
+ spdx?: string;
+ url?: string;
+ };
+ samples?: string[];
+ category?: string;
+ palette?: boolean;
}
export interface IconSearchResult {
- icons: string[];
- total: number;
- limit: number;
- start: number;
- collections: Record<string, IconSet>;
+ icons: string[];
+ total: number;
+ limit: number;
+ start: number;
+ collections: Record<string, IconSet>;
}
export interface CollectionInfo {
- prefix: string;
- total: number;
- title?: string;
- uncategorized?: string[];
- categories?: Record<string, string[]>;
- hidden?: string[];
- aliases?: Record<string, string>;
+ prefix: string;
+ total: number;
+ title?: string;
+ uncategorized?: string[];
+ categories?: Record<string, string[]>;
+ hidden?: string[];
+ aliases?: Record<string, string>;
}
export async function getCollections(
- category?: string
+ category?: string,
): Promise<Record<string, IconSet>> {
- try {
- const response = await fetchWithFallback("/collections?pretty=1");
- const data = (await response.json()) as Record<string, IconSet>;
+ try {
+ const response = await fetchWithFallback("/collections?pretty=1");
+ const data = (await response.json()) as Record<string, IconSet>;
- if (category) {
- const filtered = Object.fromEntries(
- Object.entries(data).filter(
- ([_key, info]) => info.category === category
- )
- ) as Record<string, IconSet>;
- return filtered;
- }
+ if (category) {
+ const filtered = Object.fromEntries(
+ Object.entries(data).filter(
+ ([_key, info]) => info.category === category,
+ ),
+ ) as Record<string, IconSet>;
+ return filtered;
+ }
- return data;
- } catch (error) {
- console.error("Failed to fetch collections:", error);
- return {};
- }
+ return data;
+ } catch (error) {
+ console.error("Failed to fetch collections:", error);
+ return {};
+ }
}
export async function getCollection(
- prefix: string
+ prefix: string,
): Promise<CollectionInfo | null> {
- try {
- const response = await fetchWithFallback(
- `/collection?prefix=${prefix}&pretty=1`
- );
- return await response.json();
- } catch (error) {
- console.error(`Failed to fetch collection ${prefix}:`, error);
- return null;
- }
+ try {
+ const response = await fetchWithFallback(
+ `/collection?prefix=${prefix}&pretty=1`,
+ );
+ return await response.json();
+ } catch (error) {
+ console.error(`Failed to fetch collection ${prefix}:`, error);
+ return null;
+ }
}
export async function searchIcons(
- query: string,
- limit: number = 64,
- prefixes?: string[],
- category?: string
+ query: string,
+ limit: number = 64,
+ prefixes?: string[],
+ category?: string,
): Promise<IconSearchResult> {
- const params = new URLSearchParams({
- query,
- limit: limit.toString(),
- pretty: "1",
- });
+ const params = new URLSearchParams({
+ query,
+ limit: limit.toString(),
+ pretty: "1",
+ });
- if (prefixes?.length) {
- params.append("prefixes", prefixes.join(","));
- }
+ if (prefixes?.length) {
+ params.append("prefixes", prefixes.join(","));
+ }
- if (category) {
- params.append("category", category);
- }
+ if (category) {
+ params.append("category", category);
+ }
- try {
- const response = await fetchWithFallback(`/search?${params}`);
- return await response.json();
- } catch (error) {
- console.error("Failed to search icons:", error);
- return {
- icons: [],
- total: 0,
- limit,
- start: 0,
- collections: {},
- };
- }
+ try {
+ const response = await fetchWithFallback(`/search?${params}`);
+ return await response.json();
+ } catch (error) {
+ console.error("Failed to search icons:", error);
+ return {
+ icons: [],
+ total: 0,
+ limit,
+ start: 0,
+ collections: {},
+ };
+ }
}
export function buildIconSvgUrl(
- host: string,
- iconName: string,
- params?: {
- color?: string;
- width?: number;
- height?: number;
- flip?: "horizontal" | "vertical" | "horizontal,vertical";
- rotate?: number | string;
- }
+ host: string,
+ iconName: string,
+ params?: {
+ color?: string;
+ width?: number;
+ height?: number;
+ flip?: "horizontal" | "vertical" | "horizontal,vertical";
+ rotate?: number | string;
+ },
): string {
- const [prefix, name] = iconName.includes(":")
- ? iconName.split(":")
- : ["", iconName];
+ const [prefix, name] = iconName.includes(":")
+ ? iconName.split(":")
+ : ["", iconName];
- if (!prefix || !name) {
- throw new Error('Invalid icon name format. Expected "prefix:name"');
- }
+ if (!prefix || !name) {
+ throw new Error('Invalid icon name format. Expected "prefix:name"');
+ }
- const urlParams = new URLSearchParams();
+ const urlParams = new URLSearchParams();
- if (params?.color) {
- urlParams.append("color", params.color.replace("#", "%23"));
- }
+ if (params?.color) {
+ urlParams.append("color", params.color.replace("#", "%23"));
+ }
- if (params?.width) {
- urlParams.append("width", params.width.toString());
- }
+ if (params?.width) {
+ urlParams.append("width", params.width.toString());
+ }
- if (params?.height) {
- urlParams.append("height", params.height.toString());
- }
+ if (params?.height) {
+ urlParams.append("height", params.height.toString());
+ }
- if (params?.flip) {
- urlParams.append("flip", params.flip);
- }
+ if (params?.flip) {
+ urlParams.append("flip", params.flip);
+ }
- if (params?.rotate) {
- urlParams.append("rotate", params.rotate.toString());
- }
+ if (params?.rotate) {
+ urlParams.append("rotate", params.rotate.toString());
+ }
- const queryString = urlParams.toString();
- return `${host}/${prefix}/${name}.svg${queryString ? "?" + queryString : ""}`;
+ const queryString = urlParams.toString();
+ return `${host}/${prefix}/${name}.svg${queryString ? `?${queryString}` : ""}`;
}
export function getIconSvgUrl(
- iconName: string,
- params?: Parameters<typeof buildIconSvgUrl>[2]
+ iconName: string,
+ params?: Parameters<typeof buildIconSvgUrl>[2],
): string {
- return buildIconSvgUrl(currentHost, iconName, params);
+ return buildIconSvgUrl(currentHost, iconName, params);
}
export async function downloadSvgAsText(
- iconName: string,
- params?: Parameters<typeof getIconSvgUrl>[1]
+ iconName: string,
+ params?: Parameters<typeof getIconSvgUrl>[1],
): Promise<string> {
- const url = getIconSvgUrl(iconName, params);
- const response = await fetch(url);
- if (!response.ok) {
- throw new Error(`Failed to download SVG: ${response.statusText}`);
- }
- return await response.text();
+ const url = getIconSvgUrl(iconName, params);
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Failed to download SVG: ${response.statusText}`);
+ }
+ return await response.text();
}
export function svgToFile(svgText: string, fileName: string): File {
- const blob = new Blob([svgText], { type: "image/svg+xml" });
- return new File([blob], fileName, { type: "image/svg+xml" });
+ const blob = new Blob([svgText], { type: "image/svg+xml" });
+ return new File([blob], fileName, { type: "image/svg+xml" });
}
export const POPULAR_COLLECTIONS = {
- general: [
- { prefix: "mdi", name: "Material Design Icons" },
- { prefix: "ic", name: "Google Material Icons" },
- { prefix: "ph", name: "Phosphor" },
- { prefix: "heroicons", name: "Heroicons" },
- { prefix: "lucide", name: "Lucide" },
- { prefix: "tabler", name: "Tabler Icons" },
- { prefix: "fe", name: "Feather Icons" },
- { prefix: "bi", name: "Bootstrap Icons" },
- ],
- brands: [
- { prefix: "simple-icons", name: "Simple Icons" },
- { prefix: "logos", name: "SVG Logos" },
- { prefix: "skill-icons", name: "Skill Icons" },
- { prefix: "devicon", name: "Devicon" },
- { prefix: "fa-brands", name: "Font Awesome Brands" },
- ],
- emoji: [
- { prefix: "noto", name: "Noto Emoji" },
- { prefix: "twemoji", name: "Twemoji" },
- { prefix: "fluent-emoji", name: "Fluent Emoji" },
- { prefix: "fluent-emoji-flat", name: "Fluent Emoji Flat" },
- { prefix: "emojione", name: "EmojiOne" },
- { prefix: "openmoji", name: "OpenMoji" },
- ],
+ general: [
+ { prefix: "mdi", name: "Material Design Icons" },
+ { prefix: "ic", name: "Google Material Icons" },
+ { prefix: "ph", name: "Phosphor" },
+ { prefix: "heroicons", name: "Heroicons" },
+ { prefix: "lucide", name: "Lucide" },
+ { prefix: "tabler", name: "Tabler Icons" },
+ { prefix: "fe", name: "Feather Icons" },
+ { prefix: "bi", name: "Bootstrap Icons" },
+ ],
+ brands: [
+ { prefix: "simple-icons", name: "Simple Icons" },
+ { prefix: "logos", name: "SVG Logos" },
+ { prefix: "skill-icons", name: "Skill Icons" },
+ { prefix: "devicon", name: "Devicon" },
+ { prefix: "fa-brands", name: "Font Awesome Brands" },
+ ],
+ emoji: [
+ { prefix: "noto", name: "Noto Emoji" },
+ { prefix: "twemoji", name: "Twemoji" },
+ { prefix: "fluent-emoji", name: "Fluent Emoji" },
+ { prefix: "fluent-emoji-flat", name: "Fluent Emoji Flat" },
+ { prefix: "emojione", name: "EmojiOne" },
+ { prefix: "openmoji", name: "OpenMoji" },
+ ],
};
export function getCategoriesFromCollections(
- collections: Record<string, IconSet>
+ collections: Record<string, IconSet>,
): string[] {
- const categories = new Set<string>();
- Object.values(collections).forEach((collection) => {
- if (collection.category) {
- categories.add(collection.category);
- }
- });
- return Array.from(categories).sort();
+ const categories = new Set<string>();
+ Object.values(collections).forEach((collection) => {
+ if (collection.category) {
+ categories.add(collection.category);
+ }
+ });
+ return Array.from(categories).sort();
}
diff --git a/apps/web/src/lib/media-processing.ts b/apps/web/src/lib/media-processing.ts
deleted file mode 100644
index 7a31466f..00000000
--- a/apps/web/src/lib/media-processing.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { toast } from "sonner";
-import {
- getFileType,
- getMediaDuration,
- getImageDimensions,
-} from "@/stores/media-store";
-import { MediaFile } from "@/types/media";
-import { generateThumbnail, getVideoInfo } from "./mediabunny-utils";
-
-export interface ProcessedMediaItem extends Omit<MediaFile, "id"> {}
-
-export async function processMediaFiles(
- files: FileList | File[],
- onProgress?: (progress: number) => void
-): Promise<ProcessedMediaItem[]> {
- const fileArray = Array.from(files);
- const processedItems: ProcessedMediaItem[] = [];
-
- const total = fileArray.length;
- let completed = 0;
-
- for (const file of fileArray) {
- const fileType = getFileType(file);
-
- if (!fileType) {
- toast.error(`Unsupported file type: ${file.name}`);
- continue;
- }
-
- const url = URL.createObjectURL(file);
- let thumbnailUrl: string | undefined;
- let duration: number | undefined;
- let width: number | undefined;
- let height: number | undefined;
- let fps: number | undefined;
-
- try {
- if (fileType === "image") {
- const dimensions = await getImageDimensions(file);
- width = dimensions.width;
- height = dimensions.height;
- } else if (fileType === "video") {
- try {
- const videoInfo = await getVideoInfo({ videoFile: file });
- duration = videoInfo.duration;
- width = videoInfo.width;
- height = videoInfo.height;
- fps = videoInfo.fps;
-
- thumbnailUrl = await generateThumbnail({
- videoFile: file,
- timeInSeconds: 1,
- });
- } catch (error) {
- console.warn("Video processing failed", error);
- }
- } else if (fileType === "audio") {
- // For audio, we don't set width/height/fps (they'll be undefined)
- duration = await getMediaDuration(file);
- }
-
- processedItems.push({
- name: file.name,
- type: fileType,
- file,
- url,
- thumbnailUrl,
- duration,
- width,
- height,
- fps,
- });
-
- await new Promise((resolve) => setTimeout(resolve, 0));
-
- completed += 1;
- if (onProgress) {
- const percent = Math.round((completed / total) * 100);
- onProgress(percent);
- }
- } catch (error) {
- console.error("Error processing file:", file.name, error);
- toast.error(`Failed to process ${file.name}`);
- URL.revokeObjectURL(url); // Clean up on error
- }
- }
-
- return processedItems;
-}
diff --git a/apps/web/src/lib/media/audio.ts b/apps/web/src/lib/media/audio.ts
new file mode 100644
index 00000000..b45e348e
--- /dev/null
+++ b/apps/web/src/lib/media/audio.ts
@@ -0,0 +1,459 @@
+import type {
+ AudioElement,
+ LibraryAudioElement,
+ TimelineElement,
+ TimelineTrack,
+} from "@/types/timeline";
+import type { MediaAsset } from "@/types/assets";
+import { canElementHaveAudio } from "@/lib/timeline/element-utils";
+import { canTracktHaveAudio } from "@/lib/timeline";
+import { mediaSupportsAudio } from "@/lib/media/media-utils";
+
+export type CollectedAudioElement = Omit<
+ AudioElement,
+ "type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl"
+> & { buffer: AudioBuffer };
+
+export function createAudioContext(): AudioContext {
+ const AudioContextConstructor =
+ window.AudioContext ||
+ (window as typeof window & { webkitAudioContext?: typeof AudioContext })
+ .webkitAudioContext;
+
+ return new AudioContextConstructor();
+}
+
+export interface DecodedAudio {
+ samples: Float32Array;
+ sampleRate: number;
+}
+
+export async function decodeAudioToFloat32({
+ audioBlob,
+}: {
+ audioBlob: Blob;
+}): Promise<DecodedAudio> {
+ const audioContext = createAudioContext();
+ const arrayBuffer = await audioBlob.arrayBuffer();
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
+
+ // mix down to mono
+ const numChannels = audioBuffer.numberOfChannels;
+ const length = audioBuffer.length;
+ const samples = new Float32Array(length);
+
+ for (let i = 0; i < length; i++) {
+ let sum = 0;
+ for (let channel = 0; channel < numChannels; channel++) {
+ sum += audioBuffer.getChannelData(channel)[i];
+ }
+ samples[i] = sum / numChannels;
+ }
+
+ return { samples, sampleRate: audioBuffer.sampleRate };
+}
+
+export async function collectAudioElements({
+ tracks,
+ mediaAssets,
+ audioContext,
+}: {
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
+ audioContext: AudioContext;
+}): Promise<CollectedAudioElement[]> {
+ const mediaMap = new Map<string, MediaAsset>(
+ mediaAssets.map((media) => [media.id, media]),
+ );
+ const pendingElements: Array<Promise<CollectedAudioElement | null>> = [];
+
+ for (const track of tracks) {
+ if (canTracktHaveAudio(track) && track.muted) continue;
+
+ for (const element of track.elements) {
+ if (element.type !== "audio") continue;
+ if (element.duration <= 0) continue;
+
+ const isTrackMuted = canTracktHaveAudio(track) && track.muted;
+ pendingElements.push(
+ resolveAudioBufferForElement({
+ element,
+ mediaMap,
+ audioContext,
+ }).then((audioBuffer) => {
+ if (!audioBuffer) return null;
+ return {
+ buffer: audioBuffer,
+ startTime: element.startTime,
+ duration: element.duration,
+ trimStart: element.trimStart,
+ trimEnd: element.trimEnd,
+ muted: element.muted || isTrackMuted,
+ };
+ }),
+ );
+ }
+ }
+
+ const resolvedElements = await Promise.all(pendingElements);
+ const audioElements: CollectedAudioElement[] = [];
+ for (const element of resolvedElements) {
+ if (element) audioElements.push(element);
+ }
+ return audioElements;
+}
+
+async function resolveAudioBufferForElement({
+ element,
+ mediaMap,
+ audioContext,
+}: {
+ element: AudioElement;
+ mediaMap: Map<string, MediaAsset>;
+ audioContext: AudioContext;
+}): Promise<AudioBuffer | null> {
+ try {
+ if (element.sourceType === "upload") {
+ const asset = mediaMap.get(element.mediaId);
+ if (!asset || asset.type !== "audio") return null;
+
+ const arrayBuffer = await asset.file.arrayBuffer();
+ return await audioContext.decodeAudioData(arrayBuffer.slice(0));
+ }
+
+ if (element.buffer) return element.buffer;
+
+ const response = await fetch(element.sourceUrl);
+ if (!response.ok) {
+ throw new Error(`Library audio fetch failed: ${response.status}`);
+ }
+
+ const arrayBuffer = await response.arrayBuffer();
+ return await audioContext.decodeAudioData(arrayBuffer.slice(0));
+ } catch (error) {
+ console.warn("Failed to decode audio:", error);
+ return null;
+ }
+}
+
+interface AudioMixSource {
+ file: File;
+ startTime: number;
+ duration: number;
+ trimStart: number;
+ trimEnd: number;
+}
+
+export interface AudioClipSource {
+ id: string;
+ sourceKey: string;
+ file: File;
+ startTime: number;
+ duration: number;
+ trimStart: number;
+ trimEnd: number;
+ muted: boolean;
+}
+
+async function fetchLibraryAudioSource({
+ element,
+}: {
+ element: LibraryAudioElement;
+}): Promise<AudioMixSource | null> {
+ try {
+ const response = await fetch(element.sourceUrl);
+ if (!response.ok) {
+ throw new Error(`Library audio fetch failed: ${response.status}`);
+ }
+
+ const blob = await response.blob();
+ const file = new File([blob], `${element.name}.mp3`, {
+ type: "audio/mpeg",
+ });
+
+ return {
+ file,
+ startTime: element.startTime,
+ duration: element.duration,
+ trimStart: element.trimStart,
+ trimEnd: element.trimEnd,
+ };
+ } catch (error) {
+ console.warn("Failed to fetch library audio:", error);
+ return null;
+ }
+}
+
+async function fetchLibraryAudioClip({
+ element,
+ muted,
+}: {
+ element: LibraryAudioElement;
+ muted: boolean;
+}): Promise<AudioClipSource | null> {
+ try {
+ const response = await fetch(element.sourceUrl);
+ if (!response.ok) {
+ throw new Error(`Library audio fetch failed: ${response.status}`);
+ }
+
+ const blob = await response.blob();
+ const file = new File([blob], `${element.name}.mp3`, {
+ type: "audio/mpeg",
+ });
+
+ return {
+ id: element.id,
+ sourceKey: element.id,
+ file,
+ startTime: element.startTime,
+ duration: element.duration,
+ trimStart: element.trimStart,
+ trimEnd: element.trimEnd,
+ muted,
+ };
+ } catch (error) {
+ console.warn("Failed to fetch library audio:", error);
+ return null;
+ }
+}
+
+function collectMediaAudioSource({
+ element,
+ mediaAsset,
+}: {
+ element: TimelineElement;
+ mediaAsset: MediaAsset;
+}): AudioMixSource {
+ return {
+ file: mediaAsset.file,
+ startTime: element.startTime,
+ duration: element.duration,
+ trimStart: element.trimStart,
+ trimEnd: element.trimEnd,
+ };
+}
+
+function collectMediaAudioClip({
+ element,
+ mediaAsset,
+ muted,
+}: {
+ element: TimelineElement;
+ mediaAsset: MediaAsset;
+ muted: boolean;
+}): AudioClipSource {
+ return {
+ id: element.id,
+ sourceKey: mediaAsset.id,
+ file: mediaAsset.file,
+ startTime: element.startTime,
+ duration: element.duration,
+ trimStart: element.trimStart,
+ trimEnd: element.trimEnd,
+ muted,
+ };
+}
+
+export async function collectAudioMixSources({
+ tracks,
+ mediaAssets,
+}: {
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
+}): Promise<AudioMixSource[]> {
+ const audioMixSources: AudioMixSource[] = [];
+ const mediaMap = new Map<string, MediaAsset>(
+ mediaAssets.map((asset) => [asset.id, asset]),
+ );
+ const pendingLibrarySources: Array<Promise<AudioMixSource | null>> = [];
+
+ for (const track of tracks) {
+ if (canTracktHaveAudio(track) && track.muted) continue;
+
+ for (const element of track.elements) {
+ if (!canElementHaveAudio(element)) continue;
+
+ if (element.type === "audio") {
+ if (element.sourceType === "upload") {
+ const mediaAsset = mediaMap.get(element.mediaId);
+ if (!mediaAsset) continue;
+
+ audioMixSources.push(
+ collectMediaAudioSource({ element, mediaAsset }),
+ );
+ } else {
+ pendingLibrarySources.push(fetchLibraryAudioSource({ element }));
+ }
+ continue;
+ }
+
+ if (element.type === "video") {
+ const mediaAsset = mediaMap.get(element.mediaId);
+ if (!mediaAsset) continue;
+
+ if (mediaSupportsAudio({ media: mediaAsset })) {
+ audioMixSources.push(
+ collectMediaAudioSource({ element, mediaAsset }),
+ );
+ }
+ }
+ }
+ }
+
+ const resolvedLibrarySources = await Promise.all(pendingLibrarySources);
+ for (const source of resolvedLibrarySources) {
+ if (source) audioMixSources.push(source);
+ }
+
+ return audioMixSources;
+}
+
+export async function collectAudioClips({
+ tracks,
+ mediaAssets,
+}: {
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
+}): Promise<AudioClipSource[]> {
+ const clips: AudioClipSource[] = [];
+ const mediaMap = new Map<string, MediaAsset>(
+ mediaAssets.map((asset) => [asset.id, asset]),
+ );
+ const pendingLibraryClips: Array<Promise<AudioClipSource | null>> = [];
+
+ for (const track of tracks) {
+ const isTrackMuted = canTracktHaveAudio(track) && track.muted;
+
+ for (const element of track.elements) {
+ if (!canElementHaveAudio(element)) continue;
+
+ const isElementMuted =
+ "muted" in element ? (element.muted ?? false) : false;
+ const muted = isTrackMuted || isElementMuted;
+
+ if (element.type === "audio") {
+ if (element.sourceType === "upload") {
+ const mediaAsset = mediaMap.get(element.mediaId);
+ if (!mediaAsset) continue;
+
+ clips.push(
+ collectMediaAudioClip({
+ element,
+ mediaAsset,
+ muted,
+ }),
+ );
+ } else {
+ pendingLibraryClips.push(fetchLibraryAudioClip({ element, muted }));
+ }
+ continue;
+ }
+
+ if (element.type === "video") {
+ const mediaAsset = mediaMap.get(element.mediaId);
+ if (!mediaAsset) continue;
+
+ if (mediaSupportsAudio({ media: mediaAsset })) {
+ clips.push(
+ collectMediaAudioClip({
+ element,
+ mediaAsset,
+ muted,
+ }),
+ );
+ }
+ }
+ }
+ }
+
+ const resolvedLibraryClips = await Promise.all(pendingLibraryClips);
+ for (const clip of resolvedLibraryClips) {
+ if (clip) clips.push(clip);
+ }
+
+ return clips;
+}
+
+export async function createTimelineAudioBuffer({
+ tracks,
+ mediaAssets,
+ duration,
+ sampleRate = 44100,
+ audioContext,
+}: {
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
+ duration: number;
+ sampleRate?: number;
+ audioContext?: AudioContext;
+}): Promise<AudioBuffer | null> {
+ const context = audioContext ?? createAudioContext();
+
+ const audioElements = await collectAudioElements({
+ tracks,
+ mediaAssets,
+ audioContext: context,
+ });
+
+ if (audioElements.length === 0) return null;
+
+ const outputChannels = 2;
+ const outputLength = Math.ceil(duration * sampleRate);
+ const outputBuffer = context.createBuffer(
+ outputChannels,
+ outputLength,
+ sampleRate,
+ );
+
+ for (const element of audioElements) {
+ if (element.muted) continue;
+
+ mixAudioChannels({
+ element,
+ outputBuffer,
+ outputLength,
+ sampleRate,
+ });
+ }
+
+ return outputBuffer;
+}
+
+function mixAudioChannels({
+ element,
+ outputBuffer,
+ outputLength,
+ sampleRate,
+}: {
+ element: CollectedAudioElement;
+ outputBuffer: AudioBuffer;
+ outputLength: number;
+ sampleRate: number;
+}): void {
+ const { buffer, startTime, trimStart, duration: elementDuration } = element;
+
+ const sourceStartSample = Math.floor(trimStart * buffer.sampleRate);
+ const sourceLengthSamples = Math.floor(elementDuration * buffer.sampleRate);
+ const outputStartSample = Math.floor(startTime * sampleRate);
+
+ const resampleRatio = sampleRate / buffer.sampleRate;
+ const resampledLength = Math.floor(sourceLengthSamples * resampleRatio);
+
+ const outputChannels = 2;
+ for (let channel = 0; channel < outputChannels; channel++) {
+ const outputData = outputBuffer.getChannelData(channel);
+ const sourceChannel = Math.min(channel, buffer.numberOfChannels - 1);
+ const sourceData = buffer.getChannelData(sourceChannel);
+
+ for (let i = 0; i < resampledLength; i++) {
+ const outputIndex = outputStartSample + i;
+ if (outputIndex >= outputLength) break;
+
+ const sourceIndex = sourceStartSample + Math.floor(i / resampleRatio);
+ if (sourceIndex >= sourceData.length) break;
+
+ outputData[outputIndex] += sourceData[sourceIndex];
+ }
+ }
+}
diff --git a/apps/web/src/lib/media/media-utils.ts b/apps/web/src/lib/media/media-utils.ts
new file mode 100644
index 00000000..4b487bf2
--- /dev/null
+++ b/apps/web/src/lib/media/media-utils.ts
@@ -0,0 +1,32 @@
+import type { MediaAsset, MediaType } from "@/types/assets";
+
+export const SUPPORTS_AUDIO: readonly MediaType[] = ["audio", "video"];
+
+export function mediaSupportsAudio({
+ media,
+}: {
+ media: MediaAsset | null | undefined;
+}): boolean {
+ if (!media) return false;
+ return SUPPORTS_AUDIO.includes(media.type);
+}
+
+export const getMediaTypeFromFile = ({
+ file,
+}: {
+ file: File;
+}): MediaType | null => {
+ const { type } = file;
+
+ if (type.startsWith("image/")) {
+ return "image";
+ }
+ if (type.startsWith("video/")) {
+ return "video";
+ }
+ if (type.startsWith("audio/")) {
+ return "audio";
+ }
+
+ return null;
+};
diff --git a/apps/web/src/lib/media/mediabunny.ts b/apps/web/src/lib/media/mediabunny.ts
new file mode 100644
index 00000000..11fe9837
--- /dev/null
+++ b/apps/web/src/lib/media/mediabunny.ts
@@ -0,0 +1,226 @@
+import { Input, ALL_FORMATS, BlobSource, AudioBufferSink } from "mediabunny";
+import { collectAudioMixSources } from "@/lib/media/audio";
+import type { TimelineTrack } from "@/types/timeline";
+import type { MediaAsset } from "@/types/assets";
+
+export async function getVideoInfo({
+ videoFile,
+}: {
+ videoFile: File;
+}): Promise<{
+ duration: number;
+ width: number;
+ height: number;
+ fps: number;
+}> {
+ const input = new Input({
+ source: new BlobSource(videoFile),
+ formats: ALL_FORMATS,
+ });
+
+ const duration = await input.computeDuration();
+ const videoTrack = await input.getPrimaryVideoTrack();
+
+ if (!videoTrack) {
+ throw new Error("No video track found in the file");
+ }
+
+ const packetStats = await videoTrack.computePacketStats(100);
+ const fps = packetStats.averagePacketRate;
+
+ return {
+ duration,
+ width: videoTrack.displayWidth,
+ height: videoTrack.displayHeight,
+ fps,
+ };
+}
+
+const SAMPLE_RATE = 44100;
+const NUM_CHANNELS = 2;
+
+export const extractTimelineAudio = async ({
+ tracks,
+ mediaAssets,
+ totalDuration,
+ onProgress,
+}: {
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
+ totalDuration: number;
+ onProgress?: (progress: number) => void;
+}): Promise<Blob> => {
+ if (totalDuration === 0) {
+ return createWavBlob({ samples: new Float32Array(SAMPLE_RATE * 0.1) });
+ }
+
+ const audioMixSources = await collectAudioMixSources({
+ tracks,
+ mediaAssets,
+ });
+
+ if (audioMixSources.length === 0) {
+ const silentDuration = Math.max(1, totalDuration);
+ const silentSamples = new Float32Array(
+ Math.ceil(silentDuration * SAMPLE_RATE) * NUM_CHANNELS,
+ );
+ return createWavBlob({ samples: silentSamples });
+ }
+
+ const totalSamples = Math.ceil(totalDuration * SAMPLE_RATE);
+ const mixBuffers = [
+ new Float32Array(totalSamples),
+ new Float32Array(totalSamples),
+ ];
+
+ for (let i = 0; i < audioMixSources.length; i++) {
+ const source = audioMixSources[i];
+
+ if (onProgress) {
+ onProgress((i / audioMixSources.length) * 90);
+ }
+
+ try {
+ await decodeAndMixAudioSource({
+ source,
+ mixBuffers,
+ totalSamples,
+ });
+ } catch (error) {
+ console.warn(
+ `Failed to process audio source ${source.file.name}:`,
+ error,
+ );
+ }
+ }
+
+ // clamp to prevent clipping
+ for (const channel of mixBuffers) {
+ for (let i = 0; i < channel.length; i++) {
+ channel[i] = Math.max(-1, Math.min(1, channel[i]));
+ }
+ }
+
+ // interleave channels for wav output
+ const interleavedSamples = new Float32Array(totalSamples * NUM_CHANNELS);
+ for (let i = 0; i < totalSamples; i++) {
+ interleavedSamples[i * 2] = mixBuffers[0][i];
+ interleavedSamples[i * 2 + 1] = mixBuffers[1][i];
+ }
+
+ if (onProgress) {
+ onProgress(100);
+ }
+
+ return createWavBlob({ samples: interleavedSamples });
+};
+
+async function decodeAndMixAudioSource({
+ source,
+ mixBuffers,
+ totalSamples,
+}: {
+ source: {
+ file: File;
+ startTime: number;
+ duration: number;
+ trimStart: number;
+ };
+ mixBuffers: Float32Array[];
+ totalSamples: number;
+}): Promise<void> {
+ const input = new Input({
+ source: new BlobSource(source.file),
+ formats: ALL_FORMATS,
+ });
+
+ const audioTrack = await input.getPrimaryAudioTrack();
+ if (!audioTrack) return;
+
+ const sink = new AudioBufferSink(audioTrack);
+ const trimEnd = source.trimStart + source.duration;
+
+ for await (const { buffer, timestamp } of sink.buffers(
+ source.trimStart,
+ trimEnd,
+ )) {
+ const relativeTime = timestamp - source.trimStart;
+ const outputStartSample = Math.floor(
+ (source.startTime + relativeTime) * SAMPLE_RATE,
+ );
+
+ // resample if needed
+ const resampleRatio = SAMPLE_RATE / buffer.sampleRate;
+
+ for (let ch = 0; ch < NUM_CHANNELS; ch++) {
+ const sourceChannel = Math.min(ch, buffer.numberOfChannels - 1);
+ const channelData = buffer.getChannelData(sourceChannel);
+ const outputChannel = mixBuffers[ch];
+
+ const resampledLength = Math.floor(channelData.length * resampleRatio);
+ for (let i = 0; i < resampledLength; i++) {
+ const outputIdx = outputStartSample + i;
+ if (outputIdx < 0 || outputIdx >= totalSamples) continue;
+
+ const sourceIdx = Math.floor(i / resampleRatio);
+ if (sourceIdx < channelData.length) {
+ outputChannel[outputIdx] += channelData[sourceIdx];
+ }
+ }
+ }
+ }
+}
+
+function createWavBlob({ samples }: { samples: Float32Array }): Blob {
+ const numChannels = NUM_CHANNELS;
+ const bitsPerSample = 16;
+ const bytesPerSample = bitsPerSample / 8;
+ const numSamples = samples.length / numChannels;
+ const dataSize = numSamples * numChannels * bytesPerSample;
+ const buffer = new ArrayBuffer(44 + dataSize);
+ const view = new DataView(buffer);
+
+ // riff header
+ writeString({ view, offset: 0, str: "RIFF" });
+ view.setUint32(4, 36 + dataSize, true);
+ writeString({ view, offset: 8, str: "WAVE" });
+
+ // fmt chunk
+ writeString({ view, offset: 12, str: "fmt " });
+ view.setUint32(16, 16, true);
+ view.setUint16(20, 1, true);
+ view.setUint16(22, numChannels, true);
+ view.setUint32(24, SAMPLE_RATE, true);
+ view.setUint32(28, SAMPLE_RATE * numChannels * bytesPerSample, true);
+ view.setUint16(32, numChannels * bytesPerSample, true);
+ view.setUint16(34, bitsPerSample, true);
+
+ // data chunk
+ writeString({ view, offset: 36, str: "data" });
+ view.setUint32(40, dataSize, true);
+
+ // convert float32 to int16 and write
+ let offset = 44;
+ for (let i = 0; i < samples.length; i++) {
+ const sample = Math.max(-1, Math.min(1, samples[i]));
+ const int16 = sample < 0 ? sample * 0x8000 : sample * 0x7fff;
+ view.setInt16(offset, int16, true);
+ offset += 2;
+ }
+
+ return new Blob([buffer], { type: "audio/wav" });
+}
+
+function writeString({
+ view,
+ offset,
+ str,
+}: {
+ view: DataView;
+ offset: number;
+ str: string;
+}): void {
+ for (let i = 0; i < str.length; i++) {
+ view.setUint8(offset + i, str.charCodeAt(i));
+ }
+}
diff --git a/apps/web/src/lib/media/processing.ts b/apps/web/src/lib/media/processing.ts
new file mode 100644
index 00000000..8f9c045c
--- /dev/null
+++ b/apps/web/src/lib/media/processing.ts
@@ -0,0 +1,282 @@
+import { toast } from "sonner";
+import type { MediaAsset } from "@/types/assets";
+import { getMediaTypeFromFile } from "@/lib/media/media-utils";
+import { getVideoInfo } from "./mediabunny";
+import { Input, ALL_FORMATS, BlobSource, VideoSampleSink } from "mediabunny";
+
+export interface ProcessedMediaAsset extends Omit<MediaAsset, "id"> {}
+
+const THUMBNAIL_MAX_WIDTH = 1280;
+const THUMBNAIL_MAX_HEIGHT = 720;
+
+const getThumbnailSize = ({
+ width,
+ height,
+}: {
+ width: number;
+ height: number;
+}): { width: number; height: number } => {
+ const aspectRatio = width / height;
+ let targetWidth = width;
+ let targetHeight = height;
+
+ if (targetWidth > THUMBNAIL_MAX_WIDTH) {
+ targetWidth = THUMBNAIL_MAX_WIDTH;
+ targetHeight = Math.round(targetWidth / aspectRatio);
+ }
+ if (targetHeight > THUMBNAIL_MAX_HEIGHT) {
+ targetHeight = THUMBNAIL_MAX_HEIGHT;
+ targetWidth = Math.round(targetHeight * aspectRatio);
+ }
+
+ return { width: targetWidth, height: targetHeight };
+};
+
+const renderToThumbnailDataUrl = ({
+ width,
+ height,
+ draw,
+}: {
+ width: number;
+ height: number;
+ draw: ({
+ context,
+ width,
+ height,
+ }: {
+ context: CanvasRenderingContext2D;
+ width: number;
+ height: number;
+ }) => void;
+}): string => {
+ const size = getThumbnailSize({ width, height });
+ const canvas = document.createElement("canvas");
+ canvas.width = size.width;
+ canvas.height = size.height;
+ const context = canvas.getContext("2d");
+
+ if (!context) {
+ throw new Error("Could not get canvas context");
+ }
+
+ draw({ context, width: size.width, height: size.height });
+ return canvas.toDataURL("image/jpeg", 0.8);
+};
+
+export async function generateThumbnail({
+ videoFile,
+ timeInSeconds,
+}: {
+ videoFile: File;
+ timeInSeconds: number;
+}): Promise<string> {
+ const input = new Input({
+ source: new BlobSource(videoFile),
+ formats: ALL_FORMATS,
+ });
+
+ const videoTrack = await input.getPrimaryVideoTrack();
+ if (!videoTrack) {
+ throw new Error("No video track found in the file");
+ }
+
+ const canDecode = await videoTrack.canDecode();
+ if (!canDecode) {
+ throw new Error("Video codec not supported for decoding");
+ }
+
+ const sink = new VideoSampleSink(videoTrack);
+
+ const frame = await sink.getSample(timeInSeconds);
+
+ if (!frame) {
+ throw new Error("Could not get frame at specified time");
+ }
+
+ try {
+ return renderToThumbnailDataUrl({
+ width: videoTrack.displayWidth,
+ height: videoTrack.displayHeight,
+ draw: ({ context, width, height }) => {
+ frame.draw(context, 0, 0, width, height);
+ },
+ });
+ } finally {
+ frame.close();
+ }
+}
+
+export async function generateImageThumbnail({
+ imageFile,
+}: {
+ imageFile: File;
+}): Promise<string> {
+ return new Promise((resolve, reject) => {
+ const image = new window.Image();
+ const objectUrl = URL.createObjectURL(imageFile);
+
+ image.addEventListener("load", () => {
+ try {
+ const dataUrl = renderToThumbnailDataUrl({
+ width: image.naturalWidth,
+ height: image.naturalHeight,
+ draw: ({ context, width, height }) => {
+ context.drawImage(image, 0, 0, width, height);
+ },
+ });
+ resolve(dataUrl);
+ } catch (error) {
+ reject(
+ error instanceof Error ? error : new Error("Could not render image"),
+ );
+ } finally {
+ URL.revokeObjectURL(objectUrl);
+ image.remove();
+ }
+ });
+
+ image.addEventListener("error", () => {
+ URL.revokeObjectURL(objectUrl);
+ image.remove();
+ reject(new Error("Could not load image"));
+ });
+
+ image.src = objectUrl;
+ });
+}
+
+export async function processMediaAssets({
+ files,
+ onProgress,
+}: {
+ files: FileList | File[];
+ onProgress?: ({ progress }: { progress: number }) => void;
+}): Promise<ProcessedMediaAsset[]> {
+ const fileArray = Array.from(files);
+ const processedAssets: ProcessedMediaAsset[] = [];
+
+ const total = fileArray.length;
+ let completed = 0;
+
+ for (const file of fileArray) {
+ const fileType = getMediaTypeFromFile({ file });
+
+ if (!fileType) {
+ toast.error(`Unsupported file type: ${file.name}`);
+ continue;
+ }
+
+ const url = URL.createObjectURL(file);
+ let thumbnailUrl: string | undefined;
+ let duration: number | undefined;
+ let width: number | undefined;
+ let height: number | undefined;
+ let fps: number | undefined;
+
+ try {
+ if (fileType === "image") {
+ const dimensions = await getImageDimensions({ file });
+ width = dimensions.width;
+ height = dimensions.height;
+ thumbnailUrl = await generateImageThumbnail({ imageFile: file });
+ } else if (fileType === "video") {
+ try {
+ const videoInfo = await getVideoInfo({ videoFile: file });
+ duration = videoInfo.duration;
+ width = videoInfo.width;
+ height = videoInfo.height;
+ fps = Number.isFinite(videoInfo.fps)
+ ? Math.round(videoInfo.fps * 1000) / 1000
+ : undefined;
+
+ thumbnailUrl = await generateThumbnail({
+ videoFile: file,
+ timeInSeconds: 1,
+ });
+ } catch (error) {
+ console.warn("Video processing failed", error);
+ }
+ } else if (fileType === "audio") {
+ // For audio, we don't set width/height/fps (they'll be undefined)
+ duration = await getMediaDuration({ file });
+ }
+
+ processedAssets.push({
+ name: file.name,
+ type: fileType,
+ file,
+ url,
+ thumbnailUrl,
+ duration,
+ width,
+ height,
+ fps,
+ });
+
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ completed += 1;
+ if (onProgress) {
+ const percent = Math.round((completed / total) * 100);
+ onProgress({ progress: percent });
+ }
+ } catch (error) {
+ console.error("Error processing file:", file.name, error);
+ toast.error(`Failed to process ${file.name}`);
+ URL.revokeObjectURL(url); // Clean up on error
+ }
+ }
+
+ return processedAssets;
+}
+
+const getImageDimensions = ({
+ file,
+}: {
+ file: File;
+}): Promise<{ width: number; height: number }> => {
+ return new Promise((resolve, reject) => {
+ const img = new window.Image();
+ const objectUrl = URL.createObjectURL(file);
+
+ img.addEventListener("load", () => {
+ const width = img.naturalWidth;
+ const height = img.naturalHeight;
+ resolve({ width, height });
+ URL.revokeObjectURL(objectUrl);
+ img.remove();
+ });
+
+ img.addEventListener("error", () => {
+ reject(new Error("Could not load image"));
+ URL.revokeObjectURL(objectUrl);
+ img.remove();
+ });
+
+ img.src = objectUrl;
+ });
+};
+
+const getMediaDuration = ({ file }: { file: File }): Promise<number> => {
+ return new Promise((resolve, reject) => {
+ const element = document.createElement(
+ file.type.startsWith("video/") ? "video" : "audio",
+ ) as HTMLVideoElement;
+ const objectUrl = URL.createObjectURL(file);
+
+ element.addEventListener("loadedmetadata", () => {
+ resolve(element.duration);
+ URL.revokeObjectURL(objectUrl);
+ element.remove();
+ });
+
+ element.addEventListener("error", () => {
+ reject(new Error("Could not load media"));
+ URL.revokeObjectURL(objectUrl);
+ element.remove();
+ });
+
+ element.src = objectUrl;
+ element.load();
+ });
+};
diff --git a/apps/web/src/lib/mediabunny-utils.ts b/apps/web/src/lib/mediabunny-utils.ts
deleted file mode 100644
index fcb3da0f..00000000
--- a/apps/web/src/lib/mediabunny-utils.ts
+++ /dev/null
@@ -1,311 +0,0 @@
-import { FFmpeg } from "@ffmpeg/ffmpeg";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { useMediaStore } from "@/stores/media-store";
-import { Input, ALL_FORMATS, BlobSource, VideoSampleSink } from "mediabunny";
-
-let ffmpeg: FFmpeg | null = null;
-
-export const initFFmpeg = async (): Promise<FFmpeg> => {
- if (ffmpeg) return ffmpeg;
-
- ffmpeg = new FFmpeg();
- await ffmpeg.load(); // Use default config
-
- return ffmpeg;
-};
-
-export async function generateThumbnail({
- videoFile,
- timeInSeconds,
-}: {
- videoFile: File;
- timeInSeconds: number;
-}): Promise<string> {
- const input = new Input({
- source: new BlobSource(videoFile),
- formats: ALL_FORMATS,
- });
-
- const videoTrack = await input.getPrimaryVideoTrack();
- if (!videoTrack) {
- throw new Error("No video track found in the file");
- }
-
- // Check if we can decode this video
- const canDecode = await videoTrack.canDecode();
- if (!canDecode) {
- throw new Error("Video codec not supported for decoding");
- }
-
- const sink = new VideoSampleSink(videoTrack);
-
- const frame = await sink.getSample(timeInSeconds);
-
- if (!frame) {
- throw new Error("Could not get frame at specified time");
- }
-
- const canvas = document.createElement("canvas");
- canvas.width = 320;
- canvas.height = 240;
- const ctx = canvas.getContext("2d");
-
- if (!ctx) {
- throw new Error("Could not get canvas context");
- }
-
- frame.draw(ctx, 0, 0, 320, 240);
-
- return new Promise((resolve, reject) => {
- canvas.toBlob(
- (blob) => {
- if (blob) {
- resolve(URL.createObjectURL(blob));
- } else {
- reject(new Error("Failed to create thumbnail blob"));
- }
- },
- "image/jpeg",
- 0.8
- );
- });
-}
-
-export async function getVideoInfo({
- videoFile,
-}: {
- videoFile: File;
-}): Promise<{
- duration: number;
- width: number;
- height: number;
- fps: number;
-}> {
- const input = new Input({
- source: new BlobSource(videoFile),
- formats: ALL_FORMATS,
- });
-
- const duration = await input.computeDuration();
- const videoTrack = await input.getPrimaryVideoTrack();
-
- if (!videoTrack) {
- throw new Error("No video track found in the file");
- }
-
- // Get frame rate from packet statistics
- const packetStats = await videoTrack.computePacketStats(100);
- const fps = packetStats.averagePacketRate;
-
- return {
- duration,
- width: videoTrack.displayWidth,
- height: videoTrack.displayHeight,
- fps,
- };
-}
-
-// Audio mixing for timeline - keeping FFmpeg for now due to complexity
-// TODO: Replace with Mediabunny audio processing when implementing canvas preview
-export const extractTimelineAudio = async (
- onProgress?: (progress: number) => void
-): Promise<Blob> => {
- // Create fresh FFmpeg instance for this operation
- const ffmpeg = new FFmpeg();
-
- try {
- await ffmpeg.load();
- } catch (error) {
- console.error("Failed to load fresh FFmpeg instance:", error);
- throw new Error("Unable to initialize audio processing. Please try again.");
- }
-
- const timeline = useTimelineStore.getState();
- const mediaStore = useMediaStore.getState();
-
- const tracks = timeline.tracks;
- const totalDuration = timeline.getTotalDuration();
-
- if (totalDuration === 0) {
- const emptyAudioData = new ArrayBuffer(44);
- return new Blob([emptyAudioData], { type: "audio/wav" });
- }
-
- if (onProgress) {
- ffmpeg.on("progress", ({ progress }) => {
- onProgress(progress * 100);
- });
- }
-
- const audioElements: Array<{
- file: File;
- startTime: number;
- duration: number;
- trimStart: number;
- trimEnd: number;
- trackMuted: boolean;
- }> = [];
-
- for (const track of tracks) {
- if (track.muted) continue;
-
- for (const element of track.elements) {
- if (element.type === "media") {
- const mediaFile = mediaStore.mediaFiles.find(
- (m) => m.id === element.mediaId
- );
- if (!mediaFile) continue;
-
- if (mediaFile.type === "video" || mediaFile.type === "audio") {
- audioElements.push({
- file: mediaFile.file,
- startTime: element.startTime,
- duration: element.duration,
- trimStart: element.trimStart,
- trimEnd: element.trimEnd,
- trackMuted: track.muted || false,
- });
- }
- }
- }
- }
-
- if (audioElements.length === 0) {
- // Return silent audio if no audio elements
- const silentDuration = Math.max(1, totalDuration); // At least 1 second
- try {
- const silentAudio = await generateSilentAudio(silentDuration);
- return silentAudio;
- } catch (error) {
- console.error("Failed to generate silent audio:", error);
- throw new Error("Unable to generate audio for empty timeline.");
- }
- }
-
- // Create a complex filter to mix all audio sources
- const inputFiles: string[] = [];
- const filterInputs: string[] = [];
-
- try {
- for (let i = 0; i < audioElements.length; i++) {
- const element = audioElements[i];
- const inputName = `input_${i}.${element.file.name.split(".").pop()}`;
- inputFiles.push(inputName);
-
- try {
- await ffmpeg.writeFile(
- inputName,
- new Uint8Array(await element.file.arrayBuffer())
- );
- } catch (error) {
- console.error(`Failed to write file ${element.file.name}:`, error);
- throw new Error(
- `Unable to process file: ${element.file.name}. The file may be corrupted or in an unsupported format.`
- );
- }
-
- const actualStart = element.trimStart;
- const actualDuration =
- element.duration - element.trimStart - element.trimEnd;
-
- const filterName = `audio_${i}`;
- filterInputs.push(
- `[${i}:a]atrim=start=${actualStart}:duration=${actualDuration},asetpts=PTS-STARTPTS,adelay=${element.startTime * 1000}|${element.startTime * 1000}[${filterName}]`
- );
- }
-
- const mixFilter =
- audioElements.length === 1
- ? `[audio_0]aresample=44100,aformat=sample_fmts=s16:channel_layouts=stereo[out]`
- : `${filterInputs.map((_, i) => `[audio_${i}]`).join("")}amix=inputs=${audioElements.length}:duration=longest:dropout_transition=2,aresample=44100,aformat=sample_fmts=s16:channel_layouts=stereo[out]`;
-
- const complexFilter = [...filterInputs, mixFilter].join(";");
- const outputName = "timeline_audio.wav";
-
- const ffmpegArgs = [
- ...inputFiles.flatMap((name) => ["-i", name]),
- "-filter_complex",
- complexFilter,
- "-map",
- "[out]",
- "-t",
- totalDuration.toString(),
- "-c:a",
- "pcm_s16le",
- "-ar",
- "44100",
- outputName,
- ];
-
- try {
- await ffmpeg.exec(ffmpegArgs);
- } catch (error) {
- console.error("FFmpeg execution failed:", error);
- throw new Error(
- "Audio processing failed. Some audio files may be corrupted or incompatible."
- );
- }
-
- const data = await ffmpeg.readFile(outputName);
- const blob = new Blob([data], { type: "audio/wav" });
-
- return blob;
- } catch (error) {
- for (const inputFile of inputFiles) {
- try {
- await ffmpeg.deleteFile(inputFile);
- } catch (cleanupError) {
- console.warn(`Failed to cleanup file ${inputFile}:`, cleanupError);
- }
- }
- try {
- await ffmpeg.deleteFile("timeline_audio.wav");
- } catch (cleanupError) {
- console.warn("Failed to cleanup output file:", cleanupError);
- }
-
- throw error;
- } finally {
- for (const inputFile of inputFiles) {
- try {
- await ffmpeg.deleteFile(inputFile);
- } catch (cleanupError) {}
- }
- try {
- await ffmpeg.deleteFile("timeline_audio.wav");
- } catch (cleanupError) {}
- }
-};
-
-const generateSilentAudio = async (durationSeconds: number): Promise<Blob> => {
- const ffmpeg = await initFFmpeg();
- const outputName = "silent.wav";
-
- try {
- await ffmpeg.exec([
- "-f",
- "lavfi",
- "-i",
- `anullsrc=channel_layout=stereo:sample_rate=44100`,
- "-t",
- durationSeconds.toString(),
- "-c:a",
- "pcm_s16le",
- outputName,
- ]);
-
- const data = await ffmpeg.readFile(outputName);
- const blob = new Blob([data], { type: "audio/wav" });
-
- return blob;
- } catch (error) {
- console.error("Failed to generate silent audio:", error);
- throw error;
- } finally {
- try {
- await ffmpeg.deleteFile(outputName);
- } catch (cleanupError) {
- // Silent cleanup
- }
- }
-};
diff --git a/apps/web/src/lib/rate-limit.ts b/apps/web/src/lib/rate-limit.ts
index 92f70805..d18889af 100644
--- a/apps/web/src/lib/rate-limit.ts
+++ b/apps/web/src/lib/rate-limit.ts
@@ -1,16 +1,21 @@
-// lib/rate-limit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
-import { env } from "@/env";
+import { webEnv } from "@opencut/env/web";
const redis = new Redis({
- url: env.UPSTASH_REDIS_REST_URL,
- token: env.UPSTASH_REDIS_REST_TOKEN,
+ url: webEnv.UPSTASH_REDIS_REST_URL,
+ token: webEnv.UPSTASH_REDIS_REST_TOKEN,
});
export const baseRateLimit = new Ratelimit({
- redis,
- limiter: Ratelimit.slidingWindow(100, "1 m"), // 100 requests per minute
- analytics: true,
- prefix: "rate-limit",
+ redis,
+ limiter: Ratelimit.slidingWindow(100, "1 m"), // 100 requests per minute
+ analytics: true,
+ prefix: "rate-limit",
});
+
+export async function checkRateLimit({ request }: { request: Request }) {
+ const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
+ const { success } = await baseRateLimit.limit(ip);
+ return { success, limited: !success };
+}
diff --git a/apps/web/src/lib/scenes.ts b/apps/web/src/lib/scenes.ts
new file mode 100644
index 00000000..8c043e83
--- /dev/null
+++ b/apps/web/src/lib/scenes.ts
@@ -0,0 +1,103 @@
+import type { TScene } from "@/types/timeline";
+import { generateUUID } from "@/utils/id";
+import { calculateTotalDuration } from "@/lib/timeline";
+import { ensureMainTrack } from "@/lib/timeline/track-utils";
+
+export function getMainScene({ scenes }: { scenes: TScene[] }): TScene | null {
+ return scenes.find((scene) => scene.isMain) || null;
+}
+
+export function ensureMainScene({ scenes }: { scenes: TScene[] }): TScene[] {
+ const hasMain = scenes.some((scene) => scene.isMain);
+ if (!hasMain) {
+ const mainScene = buildDefaultScene({ name: "Main scene", isMain: true });
+ return [mainScene, ...scenes];
+ }
+ return scenes;
+}
+
+export function buildDefaultScene({
+ name,
+ isMain,
+}: {
+ name: string;
+ isMain: boolean;
+}): TScene {
+ const tracks = ensureMainTrack({ tracks: [] });
+ return {
+ id: generateUUID(),
+ name,
+ isMain,
+ tracks,
+ bookmarks: [],
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+}
+
+export function canDeleteScene({ scene }: { scene: TScene }): {
+ canDelete: boolean;
+ reason?: string;
+} {
+ if (scene.isMain) {
+ return { canDelete: false, reason: "Cannot delete main scene" };
+ }
+ return { canDelete: true };
+}
+
+export function getFallbackSceneAfterDelete({
+ scenes,
+ deletedSceneId,
+ currentSceneId,
+}: {
+ scenes: TScene[];
+ deletedSceneId: string;
+ currentSceneId: string | null;
+}): TScene | null {
+ if (currentSceneId !== deletedSceneId) {
+ return scenes.find((s) => s.id === currentSceneId) || null;
+ }
+ return getMainScene({ scenes });
+}
+
+export function findCurrentScene({
+ scenes,
+ currentSceneId,
+}: {
+ scenes: TScene[];
+ currentSceneId: string;
+}): TScene | null {
+ return (
+ scenes.find((s) => s.id === currentSceneId) ||
+ getMainScene({ scenes }) ||
+ scenes[0] ||
+ null
+ );
+}
+
+export function getProjectDurationFromScenes({
+ scenes,
+}: {
+ scenes: TScene[];
+}): number {
+ const mainScene = getMainScene({ scenes }) ?? scenes[0] ?? null;
+ if (!mainScene) {
+ return 0;
+ }
+
+ return calculateTotalDuration({ tracks: mainScene.tracks });
+}
+
+export function updateSceneInArray({
+ scenes,
+ sceneId,
+ updates,
+}: {
+ scenes: TScene[];
+ sceneId: string;
+ updates: Partial<TScene>;
+}): TScene[] {
+ return scenes.map((scene) =>
+ scene.id === sceneId ? { ...scene, ...updates } : scene,
+ );
+}
diff --git a/apps/web/src/lib/schemas/waitlist.ts b/apps/web/src/lib/schemas/waitlist.ts
deleted file mode 100644
index c45540d5..00000000
--- a/apps/web/src/lib/schemas/waitlist.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { z } from "zod";
-
-export const exportWaitlistSchema = z.object({
- email: z.string().email().max(320),
-});
-
-export const exportWaitlistResponseSchema = z.object({
- success: z.boolean(),
- alreadySubscribed: z.boolean().optional(),
-});
-
-export type ExportWaitlistInput = z.infer<typeof exportWaitlistSchema>;
-export type ExportWaitlistResponse = z.infer<
- typeof exportWaitlistResponseSchema
->;
diff --git a/apps/web/src/lib/storage/indexeddb-adapter.ts b/apps/web/src/lib/storage/indexeddb-adapter.ts
deleted file mode 100644
index 597305c7..00000000
--- a/apps/web/src/lib/storage/indexeddb-adapter.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { StorageAdapter } from "./types";
-
-export class IndexedDBAdapter<T> implements StorageAdapter<T> {
- private dbName: string;
- private storeName: string;
- private version: number;
-
- constructor(dbName: string, storeName: string, version = 1) {
- this.dbName = dbName;
- this.storeName = storeName;
- this.version = version;
- }
-
- private async getDB(): Promise<IDBDatabase> {
- return new Promise((resolve, reject) => {
- const request = indexedDB.open(this.dbName, this.version);
-
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve(request.result);
-
- request.onupgradeneeded = (event) => {
- const db = (event.target as IDBOpenDBRequest).result;
- if (!db.objectStoreNames.contains(this.storeName)) {
- db.createObjectStore(this.storeName, { keyPath: "id" });
- }
- };
- });
- }
-
- async get(key: string): Promise<T | null> {
- const db = await this.getDB();
- const transaction = db.transaction([this.storeName], "readonly");
- const store = transaction.objectStore(this.storeName);
-
- return new Promise((resolve, reject) => {
- const request = store.get(key);
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve(request.result || null);
- });
- }
-
- async set(key: string, value: T): Promise<void> {
- const db = await this.getDB();
- const transaction = db.transaction([this.storeName], "readwrite");
- const store = transaction.objectStore(this.storeName);
-
- return new Promise((resolve, reject) => {
- const request = store.put({ id: key, ...value });
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve();
- });
- }
-
- async remove(key: string): Promise<void> {
- const db = await this.getDB();
- const transaction = db.transaction([this.storeName], "readwrite");
- const store = transaction.objectStore(this.storeName);
-
- return new Promise((resolve, reject) => {
- const request = store.delete(key);
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve();
- });
- }
-
- async list(): Promise<string[]> {
- const db = await this.getDB();
- const transaction = db.transaction([this.storeName], "readonly");
- const store = transaction.objectStore(this.storeName);
-
- return new Promise((resolve, reject) => {
- const request = store.getAllKeys();
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve(request.result as string[]);
- });
- }
-
- async clear(): Promise<void> {
- const db = await this.getDB();
- const transaction = db.transaction([this.storeName], "readwrite");
- const store = transaction.objectStore(this.storeName);
-
- return new Promise((resolve, reject) => {
- const request = store.clear();
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve();
- });
- }
-}
diff --git a/apps/web/src/lib/storage/opfs-adapter.ts b/apps/web/src/lib/storage/opfs-adapter.ts
deleted file mode 100644
index 87b5c886..00000000
--- a/apps/web/src/lib/storage/opfs-adapter.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { StorageAdapter } from "./types";
-
-export class OPFSAdapter implements StorageAdapter<File> {
- private directoryName: string;
-
- constructor(directoryName = "media") {
- this.directoryName = directoryName;
- }
-
- private async getDirectory(): Promise<FileSystemDirectoryHandle> {
- const opfsRoot = await navigator.storage.getDirectory();
- return await opfsRoot.getDirectoryHandle(this.directoryName, {
- create: true,
- });
- }
-
- async get(key: string): Promise<File | null> {
- try {
- const directory = await this.getDirectory();
- const fileHandle = await directory.getFileHandle(key);
- return await fileHandle.getFile();
- } catch (error) {
- if ((error as Error).name === "NotFoundError") {
- return null;
- }
- throw error;
- }
- }
-
- async set(key: string, file: File): Promise<void> {
- const directory = await this.getDirectory();
- const fileHandle = await directory.getFileHandle(key, { create: true });
- const writable = await fileHandle.createWritable();
-
- await writable.write(file);
- await writable.close();
- }
-
- async remove(key: string): Promise<void> {
- try {
- const directory = await this.getDirectory();
- await directory.removeEntry(key);
- } catch (error) {
- if ((error as Error).name !== "NotFoundError") {
- throw error;
- }
- }
- }
-
- async list(): Promise<string[]> {
- const directory = await this.getDirectory();
- const keys: string[] = [];
-
- for await (const name of directory.keys()) {
- keys.push(name);
- }
-
- return keys;
- }
-
- async clear(): Promise<void> {
- const directory = await this.getDirectory();
-
- for await (const name of directory.keys()) {
- await directory.removeEntry(name);
- }
- }
-
- // Helper method to check OPFS support
- static isSupported(): boolean {
- return "storage" in navigator && "getDirectory" in navigator.storage;
- }
-}
diff --git a/apps/web/src/lib/storage/storage-service.ts b/apps/web/src/lib/storage/storage-service.ts
deleted file mode 100644
index 2688f6e8..00000000
--- a/apps/web/src/lib/storage/storage-service.ts
+++ /dev/null
@@ -1,480 +0,0 @@
-import { TProject } from "@/types/project";
-import { MediaFile } from "@/types/media";
-import { IndexedDBAdapter } from "./indexeddb-adapter";
-import { OPFSAdapter } from "./opfs-adapter";
-import {
- MediaFileData,
- StorageConfig,
- SerializedProject,
- SerializedScene,
- TimelineData,
-} from "./types";
-import { TimelineTrack } from "@/types/timeline";
-import { SavedSoundsData, SavedSound, SoundEffect } from "@/types/sounds";
-
-class StorageService {
- private projectsAdapter: IndexedDBAdapter<SerializedProject>;
- private savedSoundsAdapter: IndexedDBAdapter<SavedSoundsData>;
- private config: StorageConfig;
-
- constructor() {
- this.config = {
- projectsDb: "video-editor-projects",
- mediaDb: "video-editor-media",
- timelineDb: "video-editor-timelines",
- savedSoundsDb: "video-editor-saved-sounds",
- version: 1,
- };
-
- this.projectsAdapter = new IndexedDBAdapter<SerializedProject>(
- this.config.projectsDb,
- "projects",
- this.config.version
- );
-
- this.savedSoundsAdapter = new IndexedDBAdapter<SavedSoundsData>(
- this.config.savedSoundsDb,
- "saved-sounds",
- this.config.version
- );
- }
-
- // Helper to get project-specific media adapters
- private getProjectMediaAdapters({ projectId }: { projectId: string }) {
- const mediaMetadataAdapter = new IndexedDBAdapter<MediaFileData>(
- `${this.config.mediaDb}-${projectId}`,
- "media-metadata",
- this.config.version
- );
-
- const mediaFilesAdapter = new OPFSAdapter(`media-files-${projectId}`);
-
- return { mediaMetadataAdapter, mediaFilesAdapter };
- }
-
- // Helper to get project-specific timeline adapter
- private getProjectTimelineAdapter({
- projectId,
- sceneId,
- }: {
- projectId: string;
- sceneId?: string;
- }) {
- const dbName = sceneId
- ? `${this.config.timelineDb}-${projectId}-${sceneId}`
- : `${this.config.timelineDb}-${projectId}`;
-
- return new IndexedDBAdapter<TimelineData>(
- dbName,
- "timeline",
- this.config.version
- );
- }
-
- // Project operations
- async saveProject({ project }: { project: TProject }): Promise<void> {
- // Convert TProject to serializable format
- const serializedScenes: SerializedScene[] = project.scenes.map((scene) => ({
- id: scene.id,
- name: scene.name,
- isMain: scene.isMain,
- createdAt: scene.createdAt.toISOString(),
- updatedAt: scene.updatedAt.toISOString(),
- }));
-
- const serializedProject: SerializedProject = {
- id: project.id,
- name: project.name,
- thumbnail: project.thumbnail,
- createdAt: project.createdAt.toISOString(),
- updatedAt: project.updatedAt.toISOString(),
- scenes: serializedScenes,
- currentSceneId: project.currentSceneId,
- backgroundColor: project.backgroundColor,
- backgroundType: project.backgroundType,
- blurIntensity: project.blurIntensity,
- bookmarks: project.bookmarks,
- fps: project.fps,
- canvasSize: project.canvasSize,
- canvasMode: project.canvasMode,
- };
-
- await this.projectsAdapter.set(project.id, serializedProject);
- }
-
- async loadProject({ id }: { id: string }): Promise<TProject | null> {
- const serializedProject = await this.projectsAdapter.get(id);
-
- if (!serializedProject) return null;
-
- // Now convert serialized scenes back to Scene objects
- const scenes =
- serializedProject.scenes?.map((scene) => ({
- id: scene.id,
- name: scene.name,
- isMain: scene.isMain,
- createdAt: new Date(scene.createdAt),
- updatedAt: new Date(scene.updatedAt),
- })) || [];
-
- // Convert back to TProject format
- const project = {
- id: serializedProject.id,
- name: serializedProject.name,
- thumbnail: serializedProject.thumbnail,
- createdAt: new Date(serializedProject.createdAt),
- updatedAt: new Date(serializedProject.updatedAt),
- scenes,
- currentSceneId: serializedProject.currentSceneId || "",
- backgroundColor: serializedProject.backgroundColor,
- backgroundType: serializedProject.backgroundType,
- blurIntensity: serializedProject.blurIntensity,
- bookmarks: serializedProject.bookmarks,
- fps: serializedProject.fps,
- canvasSize: serializedProject.canvasSize,
- canvasMode: serializedProject.canvasMode,
- };
- return project;
- }
-
- async loadAllProjects(): Promise<TProject[]> {
- const projectIds = await this.projectsAdapter.list();
- const projects: TProject[] = [];
-
- for (const id of projectIds) {
- const project = await this.loadProject({ id });
- if (project) {
- projects.push(project);
- }
- }
-
- // Sort by last updated (most recent first)
- return projects.sort(
- (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
- );
- }
-
- async deleteProject({ id }: { id: string }): Promise<void> {
- await this.projectsAdapter.remove(id);
- }
-
- // Media operations
- async saveMediaFile({
- projectId,
- mediaItem,
- }: {
- projectId: string;
- mediaItem: MediaFile;
- }): Promise<void> {
- const { mediaMetadataAdapter, mediaFilesAdapter } =
- this.getProjectMediaAdapters({ projectId });
-
- // Save file to project-specific OPFS
- await mediaFilesAdapter.set(mediaItem.id, mediaItem.file);
-
- // Save metadata to project-specific IndexedDB
- const metadata: MediaFileData = {
- id: mediaItem.id,
- name: mediaItem.name,
- type: mediaItem.type,
- size: mediaItem.file.size,
- lastModified: mediaItem.file.lastModified,
- width: mediaItem.width,
- height: mediaItem.height,
- duration: mediaItem.duration,
- ephemeral: mediaItem.ephemeral,
- };
-
- await mediaMetadataAdapter.set(mediaItem.id, metadata);
- }
-
- async loadMediaFile({
- projectId,
- id,
- }: {
- projectId: string;
- id: string;
- }): Promise<MediaFile | null> {
- const { mediaMetadataAdapter, mediaFilesAdapter } =
- this.getProjectMediaAdapters({ projectId });
-
- const [file, metadata] = await Promise.all([
- mediaFilesAdapter.get(id),
- mediaMetadataAdapter.get(id),
- ]);
-
- if (!file || !metadata) return null;
-
- let url: string;
- if (metadata.type === "image" && (!file.type || file.type === "")) {
- try {
- const text = await file.text();
- if (text.trim().startsWith("<svg")) {
- const svgBlob = new Blob([text], { type: "image/svg+xml" });
- url = URL.createObjectURL(svgBlob);
- } else {
- url = URL.createObjectURL(file);
- }
- } catch {
- url = URL.createObjectURL(file);
- }
- } else {
- url = URL.createObjectURL(file);
- }
-
- return {
- id: metadata.id,
- name: metadata.name,
- type: metadata.type,
- file,
- url,
- width: metadata.width,
- height: metadata.height,
- duration: metadata.duration,
- ephemeral: metadata.ephemeral,
- };
- }
-
- async loadAllMediaFiles({
- projectId,
- }: {
- projectId: string;
- }): Promise<MediaFile[]> {
- const { mediaMetadataAdapter } = this.getProjectMediaAdapters({
- projectId,
- });
-
- const mediaIds = await mediaMetadataAdapter.list();
- const mediaItems: MediaFile[] = [];
-
- for (const id of mediaIds) {
- const item = await this.loadMediaFile({ projectId, id });
- if (item) {
- mediaItems.push(item);
- }
- }
-
- return mediaItems;
- }
-
- async deleteMediaFile({
- projectId,
- id,
- }: {
- projectId: string;
- id: string;
- }): Promise<void> {
- const { mediaMetadataAdapter, mediaFilesAdapter } =
- this.getProjectMediaAdapters({ projectId });
-
- await Promise.all([
- mediaFilesAdapter.remove(id),
- mediaMetadataAdapter.remove(id),
- ]);
- }
-
- async deleteProjectMedia({
- projectId,
- }: {
- projectId: string;
- }): Promise<void> {
- const { mediaMetadataAdapter, mediaFilesAdapter } =
- this.getProjectMediaAdapters({ projectId });
-
- await Promise.all([
- mediaMetadataAdapter.clear(),
- mediaFilesAdapter.clear(),
- ]);
- }
-
- // Timeline operations - supports both legacy and scene-based storage
- async saveTimeline({
- projectId,
- tracks,
- sceneId,
- }: {
- projectId: string;
- tracks: TimelineTrack[];
- sceneId?: string;
- }): Promise<void> {
- const timelineAdapter = this.getProjectTimelineAdapter({
- projectId,
- sceneId,
- });
- const timelineData: TimelineData = {
- tracks,
- lastModified: new Date().toISOString(),
- };
- await timelineAdapter.set("timeline", timelineData);
- }
-
- async loadTimeline({
- projectId,
- sceneId,
- }: {
- projectId: string;
- sceneId?: string;
- }): Promise<TimelineTrack[] | null> {
- const timelineAdapter = this.getProjectTimelineAdapter({
- projectId,
- sceneId,
- });
- const timelineData = await timelineAdapter.get("timeline");
- return timelineData ? timelineData.tracks : null;
- }
-
- async deleteProjectTimeline({
- projectId,
- }: {
- projectId: string;
- }): Promise<void> {
- const timelineAdapter = this.getProjectTimelineAdapter({ projectId });
- await timelineAdapter.remove("timeline");
- }
-
- // Utility methods
- async clearAllData(): Promise<void> {
- // Clear all projects
- await this.projectsAdapter.clear();
-
- // Note: Project-specific media and timelines will be cleaned up when projects are deleted
- }
-
- async getStorageInfo(): Promise<{
- projects: number;
- isOPFSSupported: boolean;
- isIndexedDBSupported: boolean;
- }> {
- const projectIds = await this.projectsAdapter.list();
-
- return {
- projects: projectIds.length,
- isOPFSSupported: this.isOPFSSupported(),
- isIndexedDBSupported: this.isIndexedDBSupported(),
- };
- }
-
- async getProjectStorageInfo({ projectId }: { projectId: string }): Promise<{
- mediaItems: number;
- hasTimeline: boolean;
- }> {
- const { mediaMetadataAdapter } = this.getProjectMediaAdapters({
- projectId,
- });
- const timelineAdapter = this.getProjectTimelineAdapter({ projectId });
-
- const [mediaIds, timelineData] = await Promise.all([
- mediaMetadataAdapter.list(),
- timelineAdapter.get("timeline"),
- ]);
-
- return {
- mediaItems: mediaIds.length,
- hasTimeline: !!timelineData,
- };
- }
-
- async loadSavedSounds(): Promise<SavedSoundsData> {
- try {
- const savedSoundsData = await this.savedSoundsAdapter.get("user-sounds");
- return (
- savedSoundsData || {
- sounds: [],
- lastModified: new Date().toISOString(),
- }
- );
- } catch (error) {
- console.error("Failed to load saved sounds:", error);
- return { sounds: [], lastModified: new Date().toISOString() };
- }
- }
-
- async saveSoundEffect({
- soundEffect,
- }: {
- soundEffect: SoundEffect;
- }): Promise<void> {
- try {
- const currentData = await this.loadSavedSounds();
-
- // Check if sound is already saved
- if (currentData.sounds.some((sound) => sound.id === soundEffect.id)) {
- return; // Already saved
- }
-
- const savedSound: SavedSound = {
- id: soundEffect.id,
- name: soundEffect.name,
- username: soundEffect.username,
- previewUrl: soundEffect.previewUrl,
- downloadUrl: soundEffect.downloadUrl,
- duration: soundEffect.duration,
- tags: soundEffect.tags,
- license: soundEffect.license,
- savedAt: new Date().toISOString(),
- };
-
- const updatedData: SavedSoundsData = {
- sounds: [...currentData.sounds, savedSound],
- lastModified: new Date().toISOString(),
- };
-
- await this.savedSoundsAdapter.set("user-sounds", updatedData);
- } catch (error) {
- console.error("Failed to save sound effect:", error);
- throw error;
- }
- }
-
- async removeSavedSound({ soundId }: { soundId: number }): Promise<void> {
- try {
- const currentData = await this.loadSavedSounds();
-
- const updatedData: SavedSoundsData = {
- sounds: currentData.sounds.filter((sound) => sound.id !== soundId),
- lastModified: new Date().toISOString(),
- };
-
- await this.savedSoundsAdapter.set("user-sounds", updatedData);
- } catch (error) {
- console.error("Failed to remove saved sound:", error);
- throw error;
- }
- }
-
- async isSoundSaved({ soundId }: { soundId: number }): Promise<boolean> {
- try {
- const currentData = await this.loadSavedSounds();
- return currentData.sounds.some((sound) => sound.id === soundId);
- } catch (error) {
- console.error("Failed to check if sound is saved:", error);
- return false;
- }
- }
-
- async clearSavedSounds(): Promise<void> {
- try {
- await this.savedSoundsAdapter.remove("user-sounds");
- } catch (error) {
- console.error("Failed to clear saved sounds:", error);
- throw error;
- }
- }
-
- // Check browser support
- isOPFSSupported(): boolean {
- return OPFSAdapter.isSupported();
- }
-
- isIndexedDBSupported(): boolean {
- return "indexedDB" in window;
- }
-
- isFullySupported(): boolean {
- return this.isIndexedDBSupported() && this.isOPFSSupported();
- }
-}
-
-// Export singleton instance
-export const storageService = new StorageService();
-export { StorageService };
diff --git a/apps/web/src/lib/storage/types.ts b/apps/web/src/lib/storage/types.ts
deleted file mode 100644
index e66239cd..00000000
--- a/apps/web/src/lib/storage/types.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { TProject, Scene } from "@/types/project";
-import { TimelineTrack } from "@/types/timeline";
-
-export interface StorageAdapter<T> {
- get(key: string): Promise<T | null>;
- set(key: string, value: T): Promise<void>;
- remove(key: string): Promise<void>;
- list(): Promise<string[]>;
- clear(): Promise<void>;
-}
-
-export interface MediaFileData {
- id: string;
- name: string;
- type: "image" | "video" | "audio";
- size: number;
- lastModified: number;
- width?: number;
- height?: number;
- duration?: number;
- ephemeral?: boolean;
- sourceStickerIconName?: string;
- // File will be stored separately in OPFS
-}
-
-// Legacy timeline data, kept for backward compatibility
-export interface TimelineData {
- tracks: TimelineTrack[];
- lastModified: string;
-}
-
-export interface SceneTimelineData {
- sceneId: string;
- tracks: TimelineTrack[];
- lastModified: string;
-}
-
-export type SerializedScene = Omit<Scene, "createdAt" | "updatedAt"> & {
- createdAt: string;
- updatedAt: string;
-};
-
-export interface StorageConfig {
- projectsDb: string;
- mediaDb: string;
- timelineDb: string;
- savedSoundsDb: string;
- version: number;
-}
-
-// Helper type for serialization - converts Date objects to strings
-export type SerializedProject = Omit<
- TProject,
- "createdAt" | "updatedAt" | "scenes"
-> & {
- createdAt: string;
- updatedAt: string;
- scenes: SerializedScene[];
- bookmarks?: number[];
-};
-
-// Extend FileSystemDirectoryHandle with missing async iterator methods
-declare global {
- interface FileSystemDirectoryHandle {
- keys(): AsyncIterableIterator<string>;
- values(): AsyncIterableIterator<FileSystemHandle>;
- entries(): AsyncIterableIterator<[string, FileSystemHandle]>;
- }
-}
diff --git a/apps/web/src/lib/time.ts b/apps/web/src/lib/time.ts
index 20e2ff6d..b6761b0d 100644
--- a/apps/web/src/lib/time.ts
+++ b/apps/web/src/lib/time.ts
@@ -1,137 +1,220 @@
-// Time-related utility functions
+import type { TTimeCode } from "@/types/time";
-import { DEFAULT_FPS } from "@/stores/project-store";
+export function roundToFrame({
+ time,
+ fps,
+}: {
+ time: number;
+ fps: number;
+}): number {
+ return Math.round(time * fps) / fps;
+}
-export type TimeCode = "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF";
+export function formatTimeCode({
+ timeInSeconds,
+ format = "HH:MM:SS:CS",
+ fps,
+}: {
+ timeInSeconds: number;
+ format?: TTimeCode;
+ fps?: number;
+}): string {
+ const hours = Math.floor(timeInSeconds / 3600);
+ const minutes = Math.floor((timeInSeconds % 3600) / 60);
+ const seconds = Math.floor(timeInSeconds % 60);
+ const centiseconds = Math.floor((timeInSeconds % 1) * 100);
+ const frames = fps ? Math.floor((timeInSeconds % 1) * fps) : 0;
-// Helper function to format time in various formats (MM:SS, HH:MM:SS, HH:MM:SS:CS, HH:MM:SS:FF)
-export const formatTimeCode = (
- timeInSeconds: number,
- format: TimeCode = "HH:MM:SS:CS",
- fps = DEFAULT_FPS
-): string => {
- const hours = Math.floor(timeInSeconds / 3600);
- const minutes = Math.floor((timeInSeconds % 3600) / 60);
- const seconds = Math.floor(timeInSeconds % 60);
- const centiseconds = Math.floor((timeInSeconds % 1) * 100);
- const frames = Math.floor((timeInSeconds % 1) * fps);
+ switch (format) {
+ case "MM:SS":
+ return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
+ case "HH:MM:SS":
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
+ case "HH:MM:SS:CS":
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${centiseconds.toString().padStart(2, "0")}`;
+ case "HH:MM:SS:FF":
+ if (!fps) throw new Error("FPS is required for HH:MM:SS:FF format");
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${frames.toString().padStart(2, "0")}`;
+ }
+}
- switch (format) {
- case "MM:SS":
- return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
- case "HH:MM:SS":
- return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
- case "HH:MM:SS:CS":
- return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${centiseconds.toString().padStart(2, "0")}`;
- case "HH:MM:SS:FF":
- return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${frames.toString().padStart(2, "0")}`;
- }
-};
+export function parseTimeCode({
+ timeCode,
+ format = "HH:MM:SS:CS",
+ fps,
+}: {
+ timeCode: string;
+ format?: TTimeCode;
+ fps: number;
+}): number | null {
+ if (!timeCode || typeof timeCode !== "string") return null;
-export const parseTimeCode = (
- timeCode: string,
- format: TimeCode = "HH:MM:SS:CS",
- fps = DEFAULT_FPS
-): number | null => {
- if (!timeCode || typeof timeCode !== "string") return null;
+ const cleanTimeCode = timeCode.trim();
- // Remove any extra whitespace
- const cleanTimeCode = timeCode.trim();
+ try {
+ switch (format) {
+ case "MM:SS": {
+ const parts = cleanTimeCode.split(":");
+ if (parts.length !== 2) return null;
+ const [minutes, seconds] = parts.map((part) => parseInt(part, 10));
+ if (Number.isNaN(minutes) || Number.isNaN(seconds)) return null;
+ if (minutes < 0 || seconds < 0 || seconds >= 60) return null;
+ return minutes * 60 + seconds;
+ }
- try {
- switch (format) {
- case "MM:SS": {
- const parts = cleanTimeCode.split(":");
- if (parts.length !== 2) return null;
- const [minutes, seconds] = parts.map((part) => parseInt(part, 10));
- if (isNaN(minutes) || isNaN(seconds)) return null;
- if (minutes < 0 || seconds < 0 || seconds >= 60) return null;
- return minutes * 60 + seconds;
- }
+ case "HH:MM:SS": {
+ const parts = cleanTimeCode.split(":");
+ if (parts.length !== 3) return null;
+ const [hours, minutes, seconds] = parts.map((part) =>
+ parseInt(part, 10),
+ );
+ if (
+ Number.isNaN(hours) ||
+ Number.isNaN(minutes) ||
+ Number.isNaN(seconds)
+ )
+ return null;
+ if (
+ hours < 0 ||
+ minutes < 0 ||
+ seconds < 0 ||
+ minutes >= 60 ||
+ seconds >= 60
+ )
+ return null;
+ return hours * 3600 + minutes * 60 + seconds;
+ }
- case "HH:MM:SS": {
- const parts = cleanTimeCode.split(":");
- if (parts.length !== 3) return null;
- const [hours, minutes, seconds] = parts.map((part) =>
- parseInt(part, 10)
- );
- if (isNaN(hours) || isNaN(minutes) || isNaN(seconds)) return null;
- if (
- hours < 0 ||
- minutes < 0 ||
- seconds < 0 ||
- minutes >= 60 ||
- seconds >= 60
- )
- return null;
- return hours * 3600 + minutes * 60 + seconds;
- }
+ case "HH:MM:SS:CS": {
+ const parts = cleanTimeCode.split(":");
+ if (parts.length !== 4) return null;
+ const [hours, minutes, seconds, centiseconds] = parts.map((part) =>
+ parseInt(part, 10),
+ );
+ if (
+ Number.isNaN(hours) ||
+ Number.isNaN(minutes) ||
+ Number.isNaN(seconds) ||
+ Number.isNaN(centiseconds)
+ )
+ return null;
+ if (
+ hours < 0 ||
+ minutes < 0 ||
+ seconds < 0 ||
+ centiseconds < 0 ||
+ minutes >= 60 ||
+ seconds >= 60 ||
+ centiseconds >= 100
+ )
+ return null;
+ return hours * 3600 + minutes * 60 + seconds + centiseconds / 100;
+ }
- case "HH:MM:SS:CS": {
- const parts = cleanTimeCode.split(":");
- if (parts.length !== 4) return null;
- const [hours, minutes, seconds, centiseconds] = parts.map((part) =>
- parseInt(part, 10)
- );
- if (
- isNaN(hours) ||
- isNaN(minutes) ||
- isNaN(seconds) ||
- isNaN(centiseconds)
- )
- return null;
- if (
- hours < 0 ||
- minutes < 0 ||
- seconds < 0 ||
- centiseconds < 0 ||
- minutes >= 60 ||
- seconds >= 60 ||
- centiseconds >= 100
- )
- return null;
- return hours * 3600 + minutes * 60 + seconds + centiseconds / 100;
- }
+ case "HH:MM:SS:FF": {
+ const parts = cleanTimeCode.split(":");
+ if (parts.length !== 4) return null;
+ const [hours, minutes, seconds, frames] = parts.map((part) =>
+ parseInt(part, 10),
+ );
+ if (
+ Number.isNaN(hours) ||
+ Number.isNaN(minutes) ||
+ Number.isNaN(seconds) ||
+ Number.isNaN(frames)
+ )
+ return null;
+ if (
+ hours < 0 ||
+ minutes < 0 ||
+ seconds < 0 ||
+ frames < 0 ||
+ minutes >= 60 ||
+ seconds >= 60 ||
+ frames >= fps
+ )
+ return null;
+ return hours * 3600 + minutes * 60 + seconds + frames / fps;
+ }
+ }
+ } catch {
+ return null;
+ }
+}
- case "HH:MM:SS:FF": {
- const parts = cleanTimeCode.split(":");
- if (parts.length !== 4) return null;
- const [hours, minutes, seconds, frames] = parts.map((part) =>
- parseInt(part, 10)
- );
- if (isNaN(hours) || isNaN(minutes) || isNaN(seconds) || isNaN(frames))
- return null;
- if (
- hours < 0 ||
- minutes < 0 ||
- seconds < 0 ||
- frames < 0 ||
- minutes >= 60 ||
- seconds >= 60 ||
- frames >= fps
- )
- return null;
- return hours * 3600 + minutes * 60 + seconds + frames / fps;
- }
- }
- } catch {
- return null;
- }
+export function guessTimeCodeFormat({
+ timeCode,
+}: {
+ timeCode: string;
+}): TTimeCode | null {
+ if (!timeCode || typeof timeCode !== "string") return null;
- return null;
-};
+ const numbers = timeCode.split(":");
-export const guessTimeCodeFormat = (timeCode: string): TimeCode | null => {
- if (!timeCode || typeof timeCode !== "string") return null;
+ if (!numbers.every((n) => !Number.isNaN(Number(n)))) return null;
- const numbers = timeCode.split(":");
+ if (numbers.length === 2) return "MM:SS";
+ if (numbers.length === 3) return "HH:MM:SS";
+ // todo: how to tell frames apart from cs?
+ if (numbers.length === 4) return "HH:MM:SS:FF";
- if (!numbers.every((n) => !isNaN(Number(n)))) return null;
+ return null;
+}
- if (numbers.length === 2) return "MM:SS";
- if (numbers.length === 3) return "HH:MM:SS";
- // todo: how to tell frames apart from cs?
- if (numbers.length === 4) return "HH:MM:SS:FF";
+export function timeToFrame({
+ time,
+ fps,
+}: {
+ time: number;
+ fps: number;
+}): number {
+ return Math.round(time * fps);
+}
- return null;
-};
+export function frameToTime({
+ frame,
+ fps,
+}: {
+ frame: number;
+ fps: number;
+}): number {
+ return frame / fps;
+}
+
+export function snapTimeToFrame({
+ time,
+ fps,
+}: {
+ time: number;
+ fps: number;
+}): number {
+ if (fps <= 0) return time;
+ const frame = timeToFrame({ time, fps });
+ return frameToTime({ frame, fps });
+}
+
+export function getSnappedSeekTime({
+ rawTime,
+ duration,
+ fps,
+}: {
+ rawTime: number;
+ duration: number;
+ fps: number;
+}): number {
+ const snappedTime = snapTimeToFrame({ time: rawTime, fps });
+ return Math.max(0, Math.min(duration, snappedTime));
+}
+
+export function getLastFrameTime({
+ duration,
+ fps,
+}: {
+ duration: number;
+ fps: number;
+}): number {
+ if (duration <= 0) return 0;
+ if (fps <= 0) return duration;
+ const frameOffset = 1 / fps;
+ return Math.max(0, duration - frameOffset);
+}
diff --git a/apps/web/src/lib/timeline-renderer.ts b/apps/web/src/lib/timeline-renderer.ts
deleted file mode 100644
index b2650b7c..00000000
--- a/apps/web/src/lib/timeline-renderer.ts
+++ /dev/null
@@ -1,279 +0,0 @@
-import type { TimelineTrack } from "@/types/timeline";
-import type { MediaFile } from "@/types/media";
-import type { BlurIntensity } from "@/types/project";
-import { videoCache } from "./video-cache";
-import { drawCssBackground } from "./canvas-gradients";
-
-export interface RenderContext {
- ctx: CanvasRenderingContext2D;
- time: number;
- canvasWidth: number;
- canvasHeight: number;
- tracks: TimelineTrack[];
- mediaFiles: MediaFile[];
- backgroundColor?: string;
- backgroundType?: "color" | "blur";
- blurIntensity?: BlurIntensity;
- projectCanvasSize?: { width: number; height: number };
-}
-
-const imageElementCache = new Map<string, HTMLImageElement>();
-
-async function getImageElement(
- mediaItem: MediaFile
-): Promise<HTMLImageElement> {
- const cacheKey = mediaItem.id;
- const cached = imageElementCache.get(cacheKey);
- if (cached) return cached;
- const img = new Image();
- await new Promise<void>((resolve, reject) => {
- img.onload = () => resolve();
- img.onerror = () => reject(new Error("Image load failed"));
- img.src = mediaItem.url || URL.createObjectURL(mediaItem.file);
- });
- imageElementCache.set(cacheKey, img);
- return img;
-}
-
-export async function renderTimelineFrame({
- ctx,
- time,
- canvasWidth,
- canvasHeight,
- tracks,
- mediaFiles,
- backgroundColor,
- backgroundType,
- blurIntensity,
- projectCanvasSize,
-}: RenderContext): Promise<void> {
- // Background
- ctx.clearRect(0, 0, canvasWidth, canvasHeight);
- if (
- backgroundColor &&
- backgroundColor !== "transparent" &&
- !backgroundColor.includes("gradient")
- ) {
- ctx.fillStyle = backgroundColor;
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
- }
-
- // If backgroundColor is a CSS gradient string, draw it
- if (backgroundColor && backgroundColor.includes("gradient")) {
- drawCssBackground(ctx, canvasWidth, canvasHeight, backgroundColor);
- }
-
- const scaleX = projectCanvasSize ? canvasWidth / projectCanvasSize.width : 1;
- const scaleY = projectCanvasSize
- ? canvasHeight / projectCanvasSize.height
- : 1;
- const idToMedia = new Map(mediaFiles.map((m) => [m.id, m] as const));
- const active: Array<{
- track: TimelineTrack;
- element: TimelineTrack["elements"][number];
- mediaItem: MediaFile | null;
- }> = [];
-
- for (let t = tracks.length - 1; t >= 0; t -= 1) {
- const track = tracks[t];
- for (const element of track.elements) {
- if (element.hidden) continue;
- const elementStart = element.startTime;
- const elementEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
- if (time >= elementStart && time < elementEnd) {
- let mediaItem: MediaFile | null = null;
- if (element.type === "media") {
- mediaItem =
- element.mediaId === "test"
- ? null
- : idToMedia.get(element.mediaId) || null;
- }
- active.push({ track, element, mediaItem });
- }
- }
- }
-
- // If background is set to blur, draw the active media as a blurred cover layer first
- if (backgroundType === "blur") {
- const blurPx = Math.max(0, blurIntensity ?? 8);
- // Find a suitable media element (video/image) among active elements
- const bgCandidate = active.find(({ element, mediaItem }) => {
- return (
- element.type === "media" &&
- mediaItem !== null &&
- (mediaItem.type === "video" || mediaItem.type === "image")
- );
- });
- if (bgCandidate && bgCandidate.mediaItem) {
- const { element, mediaItem } = bgCandidate;
- try {
- if (mediaItem.type === "video") {
- const localTime = time - element.startTime + element.trimStart;
- const frame = await videoCache.getFrameAt(
- mediaItem.id,
- mediaItem.file,
- Math.max(0, localTime)
- );
- if (frame) {
- const mediaW = Math.max(1, mediaItem.width || canvasWidth);
- const mediaH = Math.max(1, mediaItem.height || canvasHeight);
- const coverScale = Math.max(
- canvasWidth / mediaW,
- canvasHeight / mediaH
- );
- const drawW = mediaW * coverScale;
- const drawH = mediaH * coverScale;
- const drawX = (canvasWidth - drawW) / 2;
- const drawY = (canvasHeight - drawH) / 2;
- ctx.save();
- ctx.filter = `blur(${blurPx}px)`;
- ctx.drawImage(frame.canvas, drawX, drawY, drawW, drawH);
- ctx.restore();
- }
- } else if (mediaItem.type === "image") {
- const img = await getImageElement(mediaItem);
- const mediaW = Math.max(
- 1,
- mediaItem.width || img.naturalWidth || canvasWidth
- );
- const mediaH = Math.max(
- 1,
- mediaItem.height || img.naturalHeight || canvasHeight
- );
- const coverScale = Math.max(
- canvasWidth / mediaW,
- canvasHeight / mediaH
- );
- const drawW = mediaW * coverScale;
- const drawH = mediaH * coverScale;
- const drawX = (canvasWidth - drawW) / 2;
- const drawY = (canvasHeight - drawH) / 2;
- ctx.save();
- ctx.filter = `blur(${blurPx}px)`;
- ctx.drawImage(img, drawX, drawY, drawW, drawH);
- ctx.restore();
- }
- } catch {
- // Ignore background blur failures; foreground will still render
- }
- }
- }
-
- for (const { element, mediaItem } of active) {
- if (element.type === "media" && mediaItem) {
- if (mediaItem.type === "video") {
- try {
- const localTime = time - element.startTime + element.trimStart;
-
- const frame = await videoCache.getFrameAt(
- mediaItem.id,
- mediaItem.file,
- localTime
- );
- if (!frame) continue;
-
- const mediaW = Math.max(1, mediaItem.width || canvasWidth);
- const mediaH = Math.max(1, mediaItem.height || canvasHeight);
- const containScale = Math.min(
- canvasWidth / mediaW,
- canvasHeight / mediaH
- );
- const drawW = mediaW * containScale;
- const drawH = mediaH * containScale;
- const drawX = (canvasWidth - drawW) / 2;
- const drawY = (canvasHeight - drawH) / 2;
-
- ctx.drawImage(frame.canvas, drawX, drawY, drawW, drawH);
- } catch (error) {
- console.warn(
- `Failed to render video frame for ${mediaItem.name}:`,
- error
- );
- }
- }
- if (mediaItem.type === "image") {
- const img = new Image();
- await new Promise<void>((resolve, reject) => {
- img.onload = () => resolve();
- img.onerror = () => reject(new Error("Image load failed"));
- img.src = mediaItem.url || URL.createObjectURL(mediaItem.file);
- });
- const mediaW = Math.max(
- 1,
- mediaItem.width || img.naturalWidth || canvasWidth
- );
- const mediaH = Math.max(
- 1,
- mediaItem.height || img.naturalHeight || canvasHeight
- );
- const containScale = Math.min(
- canvasWidth / mediaW,
- canvasHeight / mediaH
- );
- const drawW = mediaW * containScale;
- const drawH = mediaH * containScale;
- const drawX = (canvasWidth - drawW) / 2;
- const drawY = (canvasHeight - drawH) / 2;
- ctx.drawImage(img, drawX, drawY, drawW, drawH);
- }
- }
- if (element.type === "text") {
- const text = element;
- const posX = canvasWidth / 2 + text.x * scaleX;
- const posY = canvasHeight / 2 + text.y * scaleY;
- ctx.save();
- ctx.translate(posX, posY);
- ctx.rotate((text.rotation * Math.PI) / 180);
- ctx.globalAlpha = Math.max(0, Math.min(1, text.opacity));
- const px = text.fontSize * scaleX;
- const weight = text.fontWeight === "bold" ? "bold " : "";
- const style = text.fontStyle === "italic" ? "italic " : "";
- ctx.font = `${style}${weight}${px}px ${text.fontFamily}`;
- ctx.fillStyle = text.color;
- ctx.textAlign = text.textAlign as CanvasTextAlign;
- ctx.textBaseline = "middle";
- const metrics = ctx.measureText(text.content);
- const hasBoxMetrics =
- "actualBoundingBoxAscent" in metrics &&
- "actualBoundingBoxDescent" in metrics;
- const ascent = hasBoxMetrics
- ? (
- metrics as TextMetrics & {
- actualBoundingBoxAscent: number;
- actualBoundingBoxDescent: number;
- }
- ).actualBoundingBoxAscent
- : px * 0.8;
- const descent = hasBoxMetrics
- ? (
- metrics as TextMetrics & {
- actualBoundingBoxAscent: number;
- actualBoundingBoxDescent: number;
- }
- ).actualBoundingBoxDescent
- : px * 0.2;
- const textW = metrics.width;
- const textH = ascent + descent;
- const padX = 8 * scaleX;
- const padY = 4 * scaleX;
- if (text.backgroundColor) {
- ctx.save();
- ctx.fillStyle = text.backgroundColor;
- let bgLeft = -textW / 2;
- if (ctx.textAlign === "left") bgLeft = 0;
- if (ctx.textAlign === "right") bgLeft = -textW;
- ctx.fillRect(
- bgLeft - padX,
- -textH / 2 - padY,
- textW + padX * 2,
- textH + padY * 2
- );
- ctx.restore();
- }
- ctx.fillText(text.content, 0, 0);
- ctx.restore();
- }
- }
-}
diff --git a/apps/web/src/lib/timeline.ts b/apps/web/src/lib/timeline.ts
deleted file mode 100644
index 85fcabe6..00000000
--- a/apps/web/src/lib/timeline.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { TimelineElement } from "@/types/timeline";
-
-// Helper function to check for element overlaps and prevent invalid timeline states
-export const checkElementOverlaps = (elements: TimelineElement[]): boolean => {
- // Sort elements by start time
- const sortedElements = [...elements].sort(
- (a, b) => a.startTime - b.startTime
- );
-
- for (let i = 0; i < sortedElements.length - 1; i++) {
- const current = sortedElements[i];
- const next = sortedElements[i + 1];
-
- const currentEnd =
- current.startTime +
- (current.duration - current.trimStart - current.trimEnd);
-
- // Check if current element overlaps with next element
- if (currentEnd > next.startTime) return true; // Overlap detected
- }
-
- return false; // No overlaps
-};
-
-// Helper function to resolve overlaps by adjusting element positions
-export const resolveElementOverlaps = (
- elements: TimelineElement[]
-): TimelineElement[] => {
- // Sort elements by start time
- const sortedElements = [...elements].sort(
- (a, b) => a.startTime - b.startTime
- );
- const resolvedElements: TimelineElement[] = [];
-
- for (let i = 0; i < sortedElements.length; i++) {
- const current = { ...sortedElements[i] };
-
- if (resolvedElements.length > 0) {
- const previous = resolvedElements[resolvedElements.length - 1];
- const previousEnd =
- previous.startTime +
- (previous.duration - previous.trimStart - previous.trimEnd);
-
- // If current element would overlap with previous, push it after previous ends
- if (current.startTime < previousEnd) {
- current.startTime = previousEnd;
- }
- }
-
- resolvedElements.push(current);
- }
-
- return resolvedElements;
-};
diff --git a/apps/web/src/lib/timeline/bookmarks.ts b/apps/web/src/lib/timeline/bookmarks.ts
new file mode 100644
index 00000000..0b06c2f6
--- /dev/null
+++ b/apps/web/src/lib/timeline/bookmarks.ts
@@ -0,0 +1,61 @@
+import { roundToFrame } from "@/lib/time";
+
+export function findBookmarkIndex({
+ bookmarks,
+ frameTime,
+}: {
+ bookmarks: number[];
+ frameTime: number;
+}): number {
+ return bookmarks.findIndex(
+ (bookmark) => Math.abs(bookmark - frameTime) < 0.001,
+ );
+}
+
+export function isBookmarkAtTime({
+ bookmarks,
+ frameTime,
+}: {
+ bookmarks: number[];
+ frameTime: number;
+}): boolean {
+ return bookmarks.some((bookmark) => Math.abs(bookmark - frameTime) < 0.001);
+}
+
+export function toggleBookmarkInArray({
+ bookmarks,
+ frameTime,
+}: {
+ bookmarks: number[];
+ frameTime: number;
+}): number[] {
+ const bookmarkIndex = findBookmarkIndex({ bookmarks, frameTime });
+
+ if (bookmarkIndex !== -1) {
+ return bookmarks.filter((_, i) => i !== bookmarkIndex);
+ }
+
+ return [...bookmarks, frameTime].sort((a, b) => a - b);
+}
+
+export function removeBookmarkFromArray({
+ bookmarks,
+ frameTime,
+}: {
+ bookmarks: number[];
+ frameTime: number;
+}): number[] {
+ return bookmarks.filter(
+ (bookmark) => Math.abs(bookmark - frameTime) >= 0.001,
+ );
+}
+
+export function getFrameTime({
+ time,
+ fps,
+}: {
+ time: number;
+ fps: number;
+}): number {
+ return roundToFrame({ time, fps });
+}
diff --git a/apps/web/src/lib/timeline/drop-utils.ts b/apps/web/src/lib/timeline/drop-utils.ts
new file mode 100644
index 00000000..8c22194a
--- /dev/null
+++ b/apps/web/src/lib/timeline/drop-utils.ts
@@ -0,0 +1,234 @@
+import type { TimelineTrack, ElementType } from "@/types/timeline";
+import { TRACK_HEIGHTS, TRACK_GAP } from "@/constants/timeline-constants";
+import { wouldElementOverlap } from "./element-utils";
+import type { ComputeDropTargetParams, DropTarget } from "@/types/timeline";
+import { isMainTrack } from "./track-utils";
+
+function getTrackAtY({
+ mouseY,
+ tracks,
+ verticalDragDirection,
+}: {
+ mouseY: number;
+ tracks: TimelineTrack[];
+ verticalDragDirection?: "up" | "down" | null;
+}): { trackIndex: number; relativeY: number } | null {
+ let cumulativeHeight = 0;
+
+ for (let i = 0; i < tracks.length; i++) {
+ const trackHeight = TRACK_HEIGHTS[tracks[i].type];
+ const trackTop = cumulativeHeight;
+ const trackBottom = trackTop + trackHeight;
+
+ if (mouseY >= trackTop && mouseY < trackBottom) {
+ return {
+ trackIndex: i,
+ relativeY: mouseY - trackTop,
+ };
+ }
+
+ if (i < tracks.length - 1 && verticalDragDirection) {
+ const gapTop = trackBottom;
+ const gapBottom = gapTop + TRACK_GAP;
+ if (mouseY >= gapTop && mouseY < gapBottom) {
+ const isDraggingUp = verticalDragDirection === "up";
+ return {
+ trackIndex: isDraggingUp ? i : i + 1,
+ relativeY: isDraggingUp ? trackHeight - 1 : 0,
+ };
+ }
+ }
+
+ cumulativeHeight += trackHeight + TRACK_GAP;
+ }
+
+ return null;
+}
+
+function isCompatible({
+ elementType,
+ trackType,
+}: {
+ elementType: ElementType;
+ trackType: TimelineTrack["type"];
+}): boolean {
+ if (elementType === "text") return trackType === "text";
+ if (elementType === "audio") return trackType === "audio";
+ if (elementType === "sticker") return trackType === "sticker";
+ if (elementType === "video" || elementType === "image") {
+ return trackType === "video";
+ }
+ return false;
+}
+
+function getMainTrackIndex({ tracks }: { tracks: TimelineTrack[] }): number {
+ return tracks.findIndex((track) => isMainTrack(track));
+}
+
+function findInsertIndex({
+ elementType,
+ tracks,
+ preferredIndex,
+ insertAbove,
+}: {
+ elementType: ElementType;
+ tracks: TimelineTrack[];
+ preferredIndex: number;
+ insertAbove: boolean;
+}): { index: number; position: "above" | "below" } {
+ const mainTrackIndex = getMainTrackIndex({ tracks });
+
+ if (elementType === "audio") {
+ if (preferredIndex <= mainTrackIndex) {
+ return { index: mainTrackIndex + 1, position: "below" };
+ }
+ return {
+ index: insertAbove ? preferredIndex : preferredIndex + 1,
+ position: insertAbove ? "above" : "below",
+ };
+ }
+
+ const overlayInsertIndex = insertAbove ? preferredIndex : preferredIndex + 1;
+
+ if (mainTrackIndex >= 0 && overlayInsertIndex > mainTrackIndex) {
+ return { index: mainTrackIndex, position: "above" };
+ }
+
+ return {
+ index: overlayInsertIndex,
+ position: insertAbove ? "above" : "below",
+ };
+}
+
+export function computeDropTarget({
+ elementType,
+ mouseX,
+ mouseY,
+ tracks,
+ playheadTime,
+ isExternalDrop,
+ elementDuration,
+ pixelsPerSecond,
+ zoomLevel,
+ verticalDragDirection,
+ startTimeOverride,
+ excludeElementId,
+}: ComputeDropTargetParams): DropTarget {
+ const xPosition =
+ typeof startTimeOverride === "number"
+ ? startTimeOverride
+ : isExternalDrop
+ ? playheadTime
+ : Math.max(0, mouseX / (pixelsPerSecond * zoomLevel));
+
+ const mainTrackIndex = getMainTrackIndex({ tracks });
+
+ if (tracks.length === 0) {
+ if (elementType === "audio") {
+ return {
+ trackIndex: 0,
+ isNewTrack: true,
+ insertPosition: "below",
+ xPosition,
+ };
+ }
+ return { trackIndex: 0, isNewTrack: true, insertPosition: null, xPosition };
+ }
+
+ const trackAtMouse = getTrackAtY({ mouseY, tracks, verticalDragDirection });
+
+ if (!trackAtMouse) {
+ const isAboveAllTracks = mouseY < 0;
+
+ if (elementType === "audio") {
+ return {
+ trackIndex: tracks.length,
+ isNewTrack: true,
+ insertPosition: "below",
+ xPosition,
+ };
+ }
+
+ if (isAboveAllTracks) {
+ return {
+ trackIndex: 0,
+ isNewTrack: true,
+ insertPosition: "above",
+ xPosition,
+ };
+ }
+
+ return {
+ trackIndex: Math.max(0, mainTrackIndex),
+ isNewTrack: true,
+ insertPosition: "above",
+ xPosition,
+ };
+ }
+
+ const { trackIndex, relativeY } = trackAtMouse;
+ const track = tracks[trackIndex];
+ const trackHeight = TRACK_HEIGHTS[track.type];
+ const isInUpperHalf = relativeY < trackHeight / 2;
+
+ const isTrackCompatible = isCompatible({
+ elementType,
+ trackType: track.type,
+ });
+
+ const endTime = xPosition + elementDuration;
+ const hasOverlap = wouldElementOverlap({
+ elements: track.elements,
+ startTime: xPosition,
+ endTime,
+ excludeElementId,
+ });
+
+ if (isTrackCompatible && !hasOverlap) {
+ return {
+ trackIndex,
+ isNewTrack: false,
+ insertPosition: null,
+ xPosition,
+ };
+ }
+
+ let insertAbove = isInUpperHalf;
+ if (!isTrackCompatible && verticalDragDirection) {
+ insertAbove = verticalDragDirection === "up";
+ }
+
+ const { index, position } = findInsertIndex({
+ elementType,
+ tracks,
+ preferredIndex: trackIndex,
+ insertAbove,
+ });
+
+ return {
+ trackIndex: index,
+ isNewTrack: true,
+ insertPosition: position,
+ xPosition,
+ };
+}
+
+export function getDropLineY({
+ dropTarget,
+ tracks,
+}: {
+ dropTarget: DropTarget;
+ tracks: TimelineTrack[];
+}): number {
+ const safeTrackIndex = Math.min(
+ Math.max(dropTarget.trackIndex, 0),
+ tracks.length,
+ );
+ let y = 0;
+
+ for (let i = 0; i < safeTrackIndex; i++) {
+ y += TRACK_HEIGHTS[tracks[i].type] + TRACK_GAP;
+ }
+
+ return y;
+}
diff --git a/apps/web/src/lib/timeline/element-utils.ts b/apps/web/src/lib/timeline/element-utils.ts
new file mode 100644
index 00000000..a167e548
--- /dev/null
+++ b/apps/web/src/lib/timeline/element-utils.ts
@@ -0,0 +1,304 @@
+import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import type {
+ CreateTimelineElement,
+ CreateVideoElement,
+ CreateImageElement,
+ CreateStickerElement,
+ CreateUploadAudioElement,
+ CreateLibraryAudioElement,
+ TextElement,
+ TimelineElement,
+ TimelineTrack,
+ AudioElement,
+ VideoElement,
+ ImageElement,
+ StickerElement,
+ UploadAudioElement,
+} from "@/types/timeline";
+
+export function canElementHaveAudio(
+ element: TimelineElement,
+): element is AudioElement | VideoElement {
+ return element.type === "audio" || element.type === "video";
+}
+
+export function canElementBeHidden(
+ element: TimelineElement,
+): element is VideoElement | ImageElement | TextElement | StickerElement {
+ return element.type !== "audio";
+}
+
+export function hasMediaId(
+ element: TimelineElement,
+): element is UploadAudioElement | VideoElement | ImageElement {
+ return "mediaId" in element;
+}
+
+export function requiresMediaId({
+ element,
+}: {
+ element: CreateTimelineElement;
+}): boolean {
+ return (
+ element.type === "video" ||
+ element.type === "image" ||
+ (element.type === "audio" && element.sourceType === "upload")
+ );
+}
+
+export function checkElementOverlaps({
+ elements,
+}: {
+ elements: TimelineElement[];
+}): boolean {
+ const sortedElements = [...elements].sort(
+ (a, b) => a.startTime - b.startTime,
+ );
+
+ for (let i = 0; i < sortedElements.length - 1; i++) {
+ const current = sortedElements[i];
+ const next = sortedElements[i + 1];
+
+ const currentEnd = current.startTime + current.duration;
+
+ if (currentEnd > next.startTime) return true;
+ }
+
+ return false;
+}
+
+export function resolveElementOverlaps({
+ elements,
+}: {
+ elements: TimelineElement[];
+}): TimelineElement[] {
+ const sortedElements = [...elements].sort(
+ (a, b) => a.startTime - b.startTime,
+ );
+ const resolvedElements: TimelineElement[] = [];
+
+ for (let i = 0; i < sortedElements.length; i++) {
+ const current = { ...sortedElements[i] };
+
+ if (resolvedElements.length > 0) {
+ const previous = resolvedElements[resolvedElements.length - 1];
+ const previousEnd = previous.startTime + previous.duration;
+
+ if (current.startTime < previousEnd) {
+ current.startTime = previousEnd;
+ }
+ }
+
+ resolvedElements.push(current);
+ }
+
+ return resolvedElements;
+}
+
+export function wouldElementOverlap({
+ elements,
+ startTime,
+ endTime,
+ excludeElementId,
+}: {
+ elements: TimelineElement[];
+ startTime: number;
+ endTime: number;
+ excludeElementId?: string;
+}): boolean {
+ return elements.some((el) => {
+ if (excludeElementId && el.id === excludeElementId) return false;
+ const elEnd = el.startTime + el.duration;
+ return startTime < elEnd && endTime > el.startTime;
+ });
+}
+
+export function buildTextElement({
+ raw,
+ startTime,
+}: {
+ raw: Partial<Omit<TextElement, "type" | "id">>;
+ startTime: number;
+}): CreateTimelineElement {
+ const t = raw as Partial<TextElement>;
+
+ return {
+ type: "text",
+ name: t.name ?? DEFAULT_TEXT_ELEMENT.name,
+ content: t.content ?? DEFAULT_TEXT_ELEMENT.content,
+ duration: t.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION,
+ startTime,
+ trimStart: 0,
+ trimEnd: 0,
+ fontSize:
+ typeof t.fontSize === "number"
+ ? t.fontSize
+ : DEFAULT_TEXT_ELEMENT.fontSize,
+ fontFamily: t.fontFamily ?? DEFAULT_TEXT_ELEMENT.fontFamily,
+ color: t.color ?? DEFAULT_TEXT_ELEMENT.color,
+ backgroundColor: t.backgroundColor ?? DEFAULT_TEXT_ELEMENT.backgroundColor,
+ textAlign: t.textAlign ?? DEFAULT_TEXT_ELEMENT.textAlign,
+ fontWeight: t.fontWeight ?? DEFAULT_TEXT_ELEMENT.fontWeight,
+ fontStyle: t.fontStyle ?? DEFAULT_TEXT_ELEMENT.fontStyle,
+ textDecoration: t.textDecoration ?? DEFAULT_TEXT_ELEMENT.textDecoration,
+ transform: t.transform ?? DEFAULT_TEXT_ELEMENT.transform,
+ opacity: t.opacity ?? DEFAULT_TEXT_ELEMENT.opacity,
+ };
+}
+
+export function buildStickerElement({
+ iconName,
+ startTime,
+}: {
+ iconName: string;
+ startTime: number;
+}): CreateStickerElement {
+ return {
+ type: "sticker",
+ name: iconName.split(":")[1] || iconName,
+ iconName,
+ duration: TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION,
+ startTime,
+ trimStart: 0,
+ trimEnd: 0,
+ transform: { scale: 1, position: { x: 0, y: 0 }, rotate: 0 },
+ opacity: 1,
+ };
+}
+
+export function buildVideoElement({
+ mediaId,
+ name,
+ duration,
+ startTime,
+}: {
+ mediaId: string;
+ name: string;
+ duration: number;
+ startTime: number;
+}): CreateVideoElement {
+ return {
+ type: "video",
+ mediaId,
+ name,
+ duration,
+ startTime,
+ trimStart: 0,
+ trimEnd: 0,
+ muted: false,
+ hidden: false,
+ transform: { scale: 1, position: { x: 0, y: 0 }, rotate: 0 },
+ opacity: 1,
+ };
+}
+
+export function buildImageElement({
+ mediaId,
+ name,
+ duration,
+ startTime,
+}: {
+ mediaId: string;
+ name: string;
+ duration: number;
+ startTime: number;
+}): CreateImageElement {
+ return {
+ type: "image",
+ mediaId,
+ name,
+ duration,
+ startTime,
+ trimStart: 0,
+ trimEnd: 0,
+ hidden: false,
+ transform: { scale: 1, position: { x: 0, y: 0 }, rotate: 0 },
+ opacity: 1,
+ };
+}
+
+export function buildUploadAudioElement({
+ mediaId,
+ name,
+ duration,
+ startTime,
+ buffer,
+}: {
+ mediaId: string;
+ name: string;
+ duration: number;
+ startTime: number;
+ buffer?: AudioBuffer;
+}): CreateUploadAudioElement {
+ const element: CreateUploadAudioElement = {
+ type: "audio",
+ sourceType: "upload",
+ mediaId,
+ name,
+ duration,
+ startTime,
+ trimStart: 0,
+ trimEnd: 0,
+ volume: 1,
+ muted: false,
+ };
+ if (buffer) {
+ element.buffer = buffer;
+ }
+ return element;
+}
+
+export function buildLibraryAudioElement({
+ sourceUrl,
+ name,
+ duration,
+ startTime,
+ buffer,
+}: {
+ sourceUrl: string;
+ name: string;
+ duration: number;
+ startTime: number;
+ buffer?: AudioBuffer;
+}): CreateLibraryAudioElement {
+ const element: CreateLibraryAudioElement = {
+ type: "audio",
+ sourceType: "library",
+ sourceUrl,
+ name,
+ duration,
+ startTime,
+ trimStart: 0,
+ trimEnd: 0,
+ volume: 1,
+ muted: false,
+ };
+ if (buffer) {
+ element.buffer = buffer;
+ }
+ return element;
+}
+
+export function getElementsAtTime({
+ tracks,
+ time,
+}: {
+ tracks: TimelineTrack[];
+ time: number;
+}): { trackId: string; elementId: string }[] {
+ const result: { trackId: string; elementId: string }[] = [];
+
+ for (const track of tracks) {
+ for (const element of track.elements) {
+ const elementStart = element.startTime;
+ const elementEnd = element.startTime + element.duration;
+
+ if (time > elementStart && time < elementEnd) {
+ result.push({ trackId: track.id, elementId: element.id });
+ }
+ }
+ }
+
+ return result;
+}
diff --git a/apps/web/src/lib/timeline/index.ts b/apps/web/src/lib/timeline/index.ts
new file mode 100644
index 00000000..f3ace94f
--- /dev/null
+++ b/apps/web/src/lib/timeline/index.ts
@@ -0,0 +1,23 @@
+import type { TimelineTrack } from "@/types/timeline";
+
+export * from "./track-utils";
+export * from "./element-utils";
+export * from "./zoom-utils";
+export * from "./ruler-utils";
+
+export function calculateTotalDuration({
+ tracks,
+}: {
+ tracks: TimelineTrack[];
+}): number {
+ if (tracks.length === 0) return 0;
+
+ const trackEndTimes = tracks.map((track) =>
+ track.elements.reduce((maxEnd, element) => {
+ const elementEnd = element.startTime + element.duration;
+ return Math.max(maxEnd, elementEnd);
+ }, 0),
+ );
+
+ return Math.max(...trackEndTimes, 0);
+}
diff --git a/apps/web/src/lib/timeline/ruler-utils.ts b/apps/web/src/lib/timeline/ruler-utils.ts
new file mode 100644
index 00000000..d0a67857
--- /dev/null
+++ b/apps/web/src/lib/timeline/ruler-utils.ts
@@ -0,0 +1,228 @@
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+
+/**
+ * Frame intervals for labels - starts at 2 so there's always at least
+ * one tick between labels even at max zoom.
+ * Pattern: 2, 3, 5, 10, 15 (matches CapCut)
+ */
+const LABEL_FRAME_INTERVALS = [2, 3, 5, 10, 15] as const;
+
+/**
+ * Frame intervals for ticks - can go down to 1 for max granularity.
+ */
+const TICK_FRAME_INTERVALS = [1, 2, 3, 5, 10, 15] as const;
+
+/**
+ * Second intervals for when we're zoomed out past frame-level detail.
+ */
+const SECOND_MULTIPLIERS = [1, 2, 3, 5, 10, 15, 30, 60] as const;
+
+/**
+ * Minimum pixel spacing between labels to keep them readable
+ */
+const MIN_LABEL_SPACING_PX = 120;
+
+/**
+ * Minimum pixel spacing between ticks. Much denser than labels.
+ */
+const MIN_TICK_SPACING_PX = 18;
+
+export interface RulerConfig {
+ /** Time interval in seconds between each label */
+ labelIntervalSeconds: number;
+ /** Time interval in seconds between each tick */
+ tickIntervalSeconds: number;
+}
+
+/**
+ * Determines the optimal label and tick intervals based on zoom level and FPS.
+ *
+ * Labels and ticks scale independently:
+ * - Labels need wide spacing (~50px) to stay readable
+ * - Ticks can be denser (~8px) to show finer subdivisions
+ *
+ * Example at different zoom levels:
+ * - Very zoomed in: labels every 2f, ticks every 1f
+ * - Zoomed in: labels every 10f, ticks every 1f
+ * - Zoomed out: labels every 15f, ticks every 3f
+ * - Very zoomed out: labels every 1s, ticks every 5f
+ */
+export function getRulerConfig({
+ zoomLevel,
+ fps,
+}: {
+ zoomLevel: number;
+ fps: number;
+}): RulerConfig {
+ const pixelsPerSecond = TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
+ const pixelsPerFrame = pixelsPerSecond / fps;
+
+ const labelIntervalSeconds = findOptimalInterval({
+ pixelsPerFrame,
+ pixelsPerSecond,
+ fps,
+ minSpacingPx: MIN_LABEL_SPACING_PX,
+ frameIntervals: LABEL_FRAME_INTERVALS,
+ });
+
+ const rawTickIntervalSeconds = findOptimalInterval({
+ pixelsPerFrame,
+ pixelsPerSecond,
+ fps,
+ minSpacingPx: MIN_TICK_SPACING_PX,
+ frameIntervals: TICK_FRAME_INTERVALS,
+ });
+
+ // Ensure tick interval divides evenly into label interval so labels always land on ticks
+ const tickIntervalSeconds = ensureTickDividesLabel({
+ tickIntervalSeconds: rawTickIntervalSeconds,
+ labelIntervalSeconds,
+ pixelsPerFrame,
+ fps,
+ });
+
+ return { labelIntervalSeconds, tickIntervalSeconds };
+}
+
+/**
+ * Adjusts tick interval to ensure it divides evenly into the label interval.
+ * This guarantees labels always land on tick positions.
+ */
+function ensureTickDividesLabel({
+ tickIntervalSeconds,
+ labelIntervalSeconds,
+ pixelsPerFrame,
+ fps,
+}: {
+ tickIntervalSeconds: number;
+ labelIntervalSeconds: number;
+ pixelsPerFrame: number;
+ fps: number;
+}): number {
+ const labelFrames = Math.round(labelIntervalSeconds * fps);
+ const tickFrames = Math.round(tickIntervalSeconds * fps);
+
+ // If tick already divides label evenly, we're good
+ if (labelFrames % tickFrames === 0) {
+ return tickIntervalSeconds;
+ }
+
+ // Find the smallest tick interval that divides the label interval and has adequate spacing
+ for (const candidateFrames of TICK_FRAME_INTERVALS) {
+ if (labelFrames % candidateFrames === 0) {
+ const candidateSpacing = pixelsPerFrame * candidateFrames;
+ // Accept if spacing meets minimum threshold
+ if (candidateSpacing >= MIN_TICK_SPACING_PX) {
+ return candidateFrames / fps;
+ }
+ }
+ }
+
+ // Fallback: use the label interval itself (no intermediate ticks)
+ return labelIntervalSeconds;
+}
+
+function findOptimalInterval({
+ pixelsPerFrame,
+ pixelsPerSecond,
+ fps,
+ minSpacingPx,
+ frameIntervals,
+}: {
+ pixelsPerFrame: number;
+ pixelsPerSecond: number;
+ fps: number;
+ minSpacingPx: number;
+ frameIntervals: readonly number[];
+}): number {
+ // Try frame-level intervals first
+ for (const frameInterval of frameIntervals) {
+ const pixelSpacing = pixelsPerFrame * frameInterval;
+ if (pixelSpacing >= minSpacingPx) {
+ return frameInterval / fps;
+ }
+ }
+
+ // Then try second-level intervals
+ for (const secondMultiplier of SECOND_MULTIPLIERS) {
+ const pixelSpacing = pixelsPerSecond * secondMultiplier;
+ if (pixelSpacing >= minSpacingPx) {
+ return secondMultiplier;
+ }
+ }
+
+ return 60;
+}
+
+/**
+ * Checks if a time should have a label based on the label interval.
+ */
+export function shouldShowLabel({
+ time,
+ labelIntervalSeconds,
+}: {
+ time: number;
+ labelIntervalSeconds: number;
+}): boolean {
+ const epsilon = 0.0001;
+ const remainder = time % labelIntervalSeconds;
+ return remainder < epsilon || remainder > labelIntervalSeconds - epsilon;
+}
+
+/**
+ * Formats a ruler tick label.
+ *
+ * - On second boundaries: "MM:SS" (e.g., "00:00", "01:30")
+ * - Between seconds: "Xf" (e.g., "5f", "15f")
+ */
+export function formatRulerLabel({
+ timeInSeconds,
+ fps,
+}: {
+ timeInSeconds: number;
+ fps: number;
+}): string {
+ if (isSecondBoundary({ timeInSeconds })) {
+ return formatTimestamp({ timeInSeconds });
+ }
+
+ const frameWithinSecond = getFrameWithinSecond({ timeInSeconds, fps });
+ return `${frameWithinSecond}f`;
+}
+
+/**
+ * Checks if a time falls exactly on a second boundary.
+ */
+function isSecondBoundary({
+ timeInSeconds,
+}: {
+ timeInSeconds: number;
+}): boolean {
+ const epsilon = 0.0001;
+ const remainder = timeInSeconds % 1;
+ return remainder < epsilon || remainder > 1 - epsilon;
+}
+
+/**
+ * Gets the frame number within the current second.
+ */
+function getFrameWithinSecond({
+ timeInSeconds,
+ fps,
+}: {
+ timeInSeconds: number;
+ fps: number;
+}): number {
+ const fractionalPart = timeInSeconds % 1;
+ return Math.round(fractionalPart * fps);
+}
+
+/**
+ * Formats a timestamp as MM:SS.
+ */
+function formatTimestamp({ timeInSeconds }: { timeInSeconds: number }): string {
+ const totalSeconds = Math.round(timeInSeconds);
+ const minutes = Math.floor(totalSeconds / 60);
+ const seconds = totalSeconds % 60;
+ return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
+}
diff --git a/apps/web/src/lib/timeline/track-utils.ts b/apps/web/src/lib/timeline/track-utils.ts
new file mode 100644
index 00000000..b6261569
--- /dev/null
+++ b/apps/web/src/lib/timeline/track-utils.ts
@@ -0,0 +1,244 @@
+import type {
+ TrackType,
+ TimelineTrack,
+ ElementType,
+ VideoTrack,
+ AudioTrack,
+ StickerTrack,
+ TextTrack,
+} from "@/types/timeline";
+import {
+ TRACK_COLORS,
+ TRACK_HEIGHTS,
+ TRACK_GAP,
+} from "@/constants/timeline-constants";
+import { generateUUID } from "@/utils/id";
+
+export function canTracktHaveAudio(
+ track: TimelineTrack,
+): track is VideoTrack | AudioTrack {
+ return track.type === "audio" || track.type === "video";
+}
+
+export function canTrackBeHidden(
+ track: TimelineTrack,
+): track is VideoTrack | TextTrack | StickerTrack {
+ return track.type !== "audio";
+}
+
+export function getTrackColor({ type }: { type: TrackType }) {
+ return TRACK_COLORS[type];
+}
+
+export function getTrackClasses({ type }: { type: TrackType }) {
+ const colors = TRACK_COLORS[type];
+ return `${colors.background}`.trim();
+}
+
+export function getTrackHeight({ type }: { type: TrackType }): number {
+ return TRACK_HEIGHTS[type];
+}
+
+export function getCumulativeHeightBefore({
+ tracks,
+ trackIndex,
+}: {
+ tracks: Array<{ type: TrackType }>;
+ trackIndex: number;
+}): number {
+ return tracks
+ .slice(0, trackIndex)
+ .reduce(
+ (sum, track) => sum + getTrackHeight({ type: track.type }) + TRACK_GAP,
+ 0,
+ );
+}
+
+export function getTotalTracksHeight({
+ tracks,
+}: {
+ tracks: Array<{ type: TrackType }>;
+}): number {
+ const tracksHeight = tracks.reduce(
+ (sum, track) => sum + getTrackHeight({ type: track.type }),
+ 0,
+ );
+ const gapsHeight = Math.max(0, tracks.length - 1) * TRACK_GAP;
+ return tracksHeight + gapsHeight;
+}
+
+export function buildEmptyTrack({
+ id,
+ type,
+ name,
+}: {
+ id: string;
+ type: TrackType;
+ name?: string;
+}): TimelineTrack {
+ const trackName =
+ name ??
+ (type === "video"
+ ? "Video track"
+ : type === "text"
+ ? "Text track"
+ : type === "audio"
+ ? "Audio track"
+ : type === "sticker"
+ ? "Sticker track"
+ : "Track");
+
+ switch (type) {
+ case "video":
+ return {
+ id,
+ name: trackName,
+ type: "video",
+ elements: [],
+ hidden: false,
+ muted: false,
+ isMain: false,
+ };
+ case "text":
+ return {
+ id,
+ name: trackName,
+ type: "text",
+ elements: [],
+ hidden: false,
+ };
+ case "sticker":
+ return {
+ id,
+ name: trackName,
+ type: "sticker",
+ elements: [],
+ hidden: false,
+ };
+ case "audio":
+ return {
+ id,
+ name: trackName,
+ type: "audio",
+ elements: [],
+ muted: false,
+ };
+ default:
+ throw new Error(`Unsupported track type: ${type}`);
+ }
+}
+
+export function getDefaultInsertIndexForTrack({
+ tracks,
+ trackType,
+}: {
+ tracks: TimelineTrack[];
+ trackType: TrackType;
+}): number {
+ if (trackType === "audio") {
+ return tracks.length;
+ }
+
+ const mainTrackIndex = tracks.findIndex((track) => isMainTrack(track));
+ if (mainTrackIndex >= 0) {
+ return mainTrackIndex;
+ }
+
+ const firstAudioTrackIndex = tracks.findIndex(
+ (track) => track.type === "audio",
+ );
+ if (firstAudioTrackIndex >= 0) {
+ return firstAudioTrackIndex;
+ }
+
+ return tracks.length;
+}
+
+export function getHighestInsertIndexForTrack({
+ tracks,
+ trackType,
+}: {
+ tracks: TimelineTrack[];
+ trackType: TrackType;
+}): number {
+ const mainTrackIndex = tracks.findIndex((track) => isMainTrack(track));
+
+ if (trackType === "audio") {
+ return mainTrackIndex >= 0 ? mainTrackIndex + 1 : tracks.length;
+ }
+
+ return 0;
+}
+
+export function isMainTrack(track: TimelineTrack): track is VideoTrack {
+ return track.type === "video" && track.isMain === true;
+}
+
+export function getMainTrack({
+ tracks,
+}: {
+ tracks: TimelineTrack[];
+}): TimelineTrack | null {
+ return tracks.find((track) => isMainTrack(track)) ?? null;
+}
+
+export function ensureMainTrack({
+ tracks,
+}: {
+ tracks: TimelineTrack[];
+}): TimelineTrack[] {
+ const hasMainTrack = tracks.some((track) => isMainTrack(track));
+
+ if (!hasMainTrack) {
+ const mainTrack: TimelineTrack = {
+ id: generateUUID(),
+ name: "Main Track",
+ type: "video",
+ elements: [],
+ muted: false,
+ isMain: true,
+ hidden: false,
+ };
+ return [mainTrack, ...tracks];
+ }
+
+ return tracks;
+}
+
+export function canElementGoOnTrack({
+ elementType,
+ trackType,
+}: {
+ elementType: ElementType;
+ trackType: TrackType;
+}): boolean {
+ if (elementType === "text") return trackType === "text";
+ if (elementType === "audio") return trackType === "audio";
+ if (elementType === "sticker") return trackType === "sticker";
+ if (elementType === "video" || elementType === "image") {
+ return trackType === "video";
+ }
+ return false;
+}
+
+export function validateElementTrackCompatibility({
+ element,
+ track,
+}: {
+ element: { type: ElementType };
+ track: { type: TrackType };
+}): { isValid: boolean; errorMessage?: string } {
+ const isValid = canElementGoOnTrack({
+ elementType: element.type,
+ trackType: track.type,
+ });
+
+ if (!isValid) {
+ return {
+ isValid: false,
+ errorMessage: `${element.type} elements cannot be placed on ${track.type} tracks`,
+ };
+ }
+
+ return { isValid: true };
+}
diff --git a/apps/web/src/lib/timeline/zoom-utils.ts b/apps/web/src/lib/timeline/zoom-utils.ts
new file mode 100644
index 00000000..49f7fcb6
--- /dev/null
+++ b/apps/web/src/lib/timeline/zoom-utils.ts
@@ -0,0 +1,53 @@
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+
+const PADDING_MAX_RATIO = 0.75;
+const PADDING_MIN_RATIO = 0.15;
+const PADDING_MIN_AT_ZOOM_PERCENT = 0.2;
+
+export function getTimelineZoomMin({
+ duration,
+ containerWidth,
+}: {
+ duration: number;
+ containerWidth: number | null | undefined;
+}): number {
+ const safeDuration = Math.max(duration, 1);
+ const safeContainerWidth = containerWidth ?? 1000;
+ const contentRatioAtMinZoom = 1 - PADDING_MAX_RATIO;
+ const availableWidth = safeContainerWidth * contentRatioAtMinZoom;
+ const zoomToFit =
+ availableWidth / (safeDuration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND);
+
+ return Math.min(TIMELINE_CONSTANTS.ZOOM_MAX, zoomToFit);
+}
+
+export function getTimelinePaddingPx({
+ containerWidth,
+ zoomLevel,
+ minZoom,
+}: {
+ containerWidth: number;
+ zoomLevel: number;
+ minZoom: number;
+}): number {
+ const zoomPercent = getZoomPercent({ zoomLevel, minZoom });
+ const paddingTransitionPercent = Math.min(
+ zoomPercent / PADDING_MIN_AT_ZOOM_PERCENT,
+ 1,
+ );
+ const paddingRatio =
+ PADDING_MAX_RATIO -
+ (PADDING_MAX_RATIO - PADDING_MIN_RATIO) * paddingTransitionPercent;
+
+ return containerWidth * paddingRatio;
+}
+
+export function getZoomPercent({
+ zoomLevel,
+ minZoom,
+}: {
+ zoomLevel: number;
+ minZoom: number;
+}): number {
+ return (zoomLevel - minZoom) / (TIMELINE_CONSTANTS.ZOOM_MAX - minZoom);
+}
diff --git a/apps/web/src/lib/transcription-utils.ts b/apps/web/src/lib/transcription-utils.ts
deleted file mode 100644
index 6388a3b6..00000000
--- a/apps/web/src/lib/transcription-utils.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { env } from "@/env";
-
-export function isTranscriptionConfigured() {
- const missingVars = [];
-
- if (!env.CLOUDFLARE_ACCOUNT_ID) missingVars.push("CLOUDFLARE_ACCOUNT_ID");
- if (!env.R2_ACCESS_KEY_ID) missingVars.push("R2_ACCESS_KEY_ID");
- if (!env.R2_SECRET_ACCESS_KEY) missingVars.push("R2_SECRET_ACCESS_KEY");
- if (!env.R2_BUCKET_NAME) missingVars.push("R2_BUCKET_NAME");
- if (!env.MODAL_TRANSCRIPTION_URL) missingVars.push("MODAL_TRANSCRIPTION_URL");
-
- return { configured: missingVars.length === 0, missingVars };
-}
diff --git a/apps/web/src/lib/transcription/caption.ts b/apps/web/src/lib/transcription/caption.ts
new file mode 100644
index 00000000..c773b7b0
--- /dev/null
+++ b/apps/web/src/lib/transcription/caption.ts
@@ -0,0 +1,49 @@
+import type { TranscriptionSegment, CaptionChunk } from "@/types/transcription";
+import {
+ DEFAULT_WORDS_PER_CAPTION,
+ MIN_CAPTION_DURATION_SECONDS,
+} from "@/constants/transcription-constants";
+
+export function buildCaptionChunks({
+ segments,
+ wordsPerChunk = DEFAULT_WORDS_PER_CAPTION,
+ minDuration = MIN_CAPTION_DURATION_SECONDS,
+}: {
+ segments: TranscriptionSegment[];
+ wordsPerChunk?: number;
+ minDuration?: number;
+}): CaptionChunk[] {
+ const captions: CaptionChunk[] = [];
+ let globalEndTime = 0;
+
+ for (const segment of segments) {
+ const words = segment.text.trim().split(/\s+/);
+ if (words.length === 0 || (words.length === 1 && words[0] === "")) continue;
+
+ const segmentDuration = segment.end - segment.start;
+ const wordsPerSecond = words.length / segmentDuration;
+
+ const chunks: string[] = [];
+ for (let i = 0; i < words.length; i += wordsPerChunk) {
+ chunks.push(words.slice(i, i + wordsPerChunk).join(" "));
+ }
+
+ let chunkStartTime = segment.start;
+ for (const chunk of chunks) {
+ const chunkWords = chunk.split(/\s+/).length;
+ const chunkDuration = Math.max(minDuration, chunkWords / wordsPerSecond);
+ const adjustedStartTime = Math.max(chunkStartTime, globalEndTime);
+
+ captions.push({
+ text: chunk,
+ startTime: adjustedStartTime,
+ duration: chunkDuration,
+ });
+
+ globalEndTime = adjustedStartTime + chunkDuration;
+ chunkStartTime += chunkDuration;
+ }
+ }
+
+ return captions;
+}
diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts
deleted file mode 100644
index 42159b2e..00000000
--- a/apps/web/src/lib/utils.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-// Generic utilities
-
-import { type ClassValue, clsx } from "clsx";
-import { twMerge } from "tailwind-merge";
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
-}
-
-export function uppercase(str: string) {
- return str.toUpperCase();
-}
-
-/**
- * Generates a UUID v4 string
- * Uses crypto.randomUUID() if available, otherwise falls back to a custom implementation
- */
-export function generateUUID(): string {
- // Use the native crypto.randomUUID if available
- if (
- typeof crypto !== "undefined" &&
- typeof crypto.randomUUID === "function"
- ) {
- return crypto.randomUUID();
- }
-
- // Secure fallback using crypto.getRandomValues
- const bytes = new Uint8Array(16);
- crypto.getRandomValues(bytes);
-
- // Set version 4 (UUIDv4)
- bytes[6] = (bytes[6] & 0x0f) | 0x40;
- // Set variant 10xxxxxx
- bytes[8] = (bytes[8] & 0x3f) | 0x80;
-
- const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
-
- return (
- hex.slice(0, 4).join("") +
- "-" +
- hex.slice(4, 6).join("") +
- "-" +
- hex.slice(6, 8).join("") +
- "-" +
- hex.slice(8, 10).join("") +
- "-" +
- hex.slice(10, 16).join("")
- );
-}
-
-export function isDOMElement(el: any): el is HTMLElement {
- return !!el && (el instanceof Element || el instanceof HTMLElement);
-}
-
-export function isTypableElement(el: HTMLElement): boolean {
- // If content editable, then it is editable
- if (el.isContentEditable) return true;
-
- // If element is an input and the input is enabled, then it is typable
- if (el.tagName === "INPUT") {
- return !(el as HTMLInputElement).disabled;
- }
- // If element is a textarea and the input is enabled, then it is typable
- if (el.tagName === "TEXTAREA") {
- return !(el as HTMLTextAreaElement).disabled;
- }
-
- return false;
-}
-export function isAppleDevice() {
- return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
-}
-
-export function getPlatformSpecialKey() {
- return isAppleDevice() ? "⌘" : "Ctrl";
-}
-
-export function getPlatformAlternateKey() {
- return isAppleDevice() ? "⌥" : "Alt";
-}
diff --git a/apps/web/src/lib/video-cache.ts b/apps/web/src/lib/video-cache.ts
deleted file mode 100644
index aeb62d1c..00000000
--- a/apps/web/src/lib/video-cache.ts
+++ /dev/null
@@ -1,186 +0,0 @@
-import {
- Input,
- ALL_FORMATS,
- BlobSource,
- CanvasSink,
- WrappedCanvas,
-} from "mediabunny";
-
-interface VideoSinkData {
- sink: CanvasSink;
- iterator: AsyncGenerator<WrappedCanvas, void, unknown> | null;
- currentFrame: WrappedCanvas | null;
- lastTime: number;
-}
-export class VideoCache {
- private sinks = new Map<string, VideoSinkData>();
- private initPromises = new Map<string, Promise<void>>();
-
- async getFrameAt(
- mediaId: string,
- file: File,
- time: number
- ): Promise<WrappedCanvas | null> {
- await this.ensureSink(mediaId, file);
-
- const sinkData = this.sinks.get(mediaId);
- if (!sinkData) return null;
-
- if (
- sinkData.currentFrame &&
- this.isFrameValid(sinkData.currentFrame, time)
- ) {
- return sinkData.currentFrame;
- }
-
- if (
- sinkData.iterator &&
- sinkData.currentFrame &&
- time >= sinkData.lastTime &&
- time < sinkData.lastTime + 2.0
- ) {
- const frame = await this.iterateToTime(sinkData, time);
- if (frame) return frame;
- }
-
- return await this.seekToTime(sinkData, time);
- }
-
- private isFrameValid(frame: WrappedCanvas, time: number): boolean {
- return time >= frame.timestamp && time < frame.timestamp + frame.duration;
- }
- private async iterateToTime(
- sinkData: VideoSinkData,
- targetTime: number
- ): Promise<WrappedCanvas | null> {
- if (!sinkData.iterator) return null;
-
- try {
- while (true) {
- const { value: frame, done } = await sinkData.iterator.next();
-
- if (done || !frame) break;
-
- sinkData.currentFrame = frame;
- sinkData.lastTime = frame.timestamp;
-
- if (this.isFrameValid(frame, targetTime)) {
- return frame;
- }
-
- if (frame.timestamp > targetTime + 1.0) break;
- }
- } catch (error) {
- console.warn("Iterator failed, will restart:", error);
- sinkData.iterator = null;
- }
-
- return null;
- }
- private async seekToTime(
- sinkData: VideoSinkData,
- time: number
- ): Promise<WrappedCanvas | null> {
- try {
- if (sinkData.iterator) {
- await sinkData.iterator.return();
- sinkData.iterator = null;
- }
-
- sinkData.iterator = sinkData.sink.canvases(time);
- sinkData.lastTime = time;
-
- const { value: frame } = await sinkData.iterator.next();
-
- if (frame) {
- sinkData.currentFrame = frame;
- return frame;
- }
- } catch (error) {
- console.warn("Failed to seek video:", error);
- }
-
- return null;
- }
- private async ensureSink(mediaId: string, file: File): Promise<void> {
- if (this.sinks.has(mediaId)) return;
-
- if (this.initPromises.has(mediaId)) {
- await this.initPromises.get(mediaId);
- return;
- }
-
- const initPromise = this.initializeSink(mediaId, file);
- this.initPromises.set(mediaId, initPromise);
-
- try {
- await initPromise;
- } finally {
- this.initPromises.delete(mediaId);
- }
- }
- private async initializeSink(mediaId: string, file: File): Promise<void> {
- try {
- const input = new Input({
- source: new BlobSource(file),
- formats: ALL_FORMATS,
- });
-
- const videoTrack = await input.getPrimaryVideoTrack();
- if (!videoTrack) {
- throw new Error("No video track found");
- }
-
- const canDecode = await videoTrack.canDecode();
- if (!canDecode) {
- throw new Error("Video codec not supported for decoding");
- }
-
- const sink = new CanvasSink(videoTrack, {
- poolSize: 3,
- fit: "contain",
- });
-
- this.sinks.set(mediaId, {
- sink,
- iterator: null,
- currentFrame: null,
- lastTime: -1,
- });
- } catch (error) {
- console.error(`Failed to initialize video sink for ${mediaId}:`, error);
- throw error;
- }
- }
-
- clearVideo(mediaId: string): void {
- const sinkData = this.sinks.get(mediaId);
- if (sinkData) {
- if (sinkData.iterator) {
- sinkData.iterator.return();
- }
-
- this.sinks.delete(mediaId);
- }
-
- this.initPromises.delete(mediaId);
- }
-
- clearAll(): void {
- for (const [mediaId] of this.sinks) {
- this.clearVideo(mediaId);
- }
- }
-
- getStats() {
- return {
- totalSinks: this.sinks.size,
- activeSinks: Array.from(this.sinks.values()).filter((s) => s.iterator)
- .length,
- cachedFrames: Array.from(this.sinks.values()).filter(
- (s) => s.currentFrame
- ).length,
- };
- }
-}
-export const videoCache = new VideoCache();
diff --git a/apps/web/src/lib/zk-encryption.ts b/apps/web/src/lib/zk-encryption.ts
deleted file mode 100644
index 58734807..00000000
--- a/apps/web/src/lib/zk-encryption.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * True zero-knowledge encryption utilities
- * Keys are generated randomly in the browser and never derived from server secrets
- */
-
-export interface ZeroKnowledgeEncryptionResult {
- encryptedData: ArrayBuffer;
- key: ArrayBuffer;
- iv: ArrayBuffer;
-}
-
-/**
- * Encrypt data with a randomly generated key (true zero-knowledge)
- */
-export async function encryptWithRandomKey(
- data: ArrayBuffer
-): Promise<ZeroKnowledgeEncryptionResult> {
- // Generate a truly random 256-bit key
- const key = crypto.getRandomValues(new Uint8Array(32));
-
- // Generate random IV
- const iv = crypto.getRandomValues(new Uint8Array(12));
-
- // Import the key for encryption
- const cryptoKey = await crypto.subtle.importKey(
- "raw",
- key,
- { name: "AES-GCM" },
- false,
- ["encrypt"]
- );
-
- // Encrypt the data
- const encryptedResult = await crypto.subtle.encrypt(
- { name: "AES-GCM", iv },
- cryptoKey,
- data
- );
-
- // For AES-GCM, we need to append the authentication tag
- // The encrypted result contains both ciphertext and tag
- return {
- encryptedData: encryptedResult,
- key: key.buffer,
- iv: iv.buffer,
- };
-}
-
-/**
- * Convert ArrayBuffer to base64 string for transmission
- */
-export function arrayBufferToBase64(buffer: ArrayBuffer): string {
- const bytes = new Uint8Array(buffer);
- let binary = "";
- for (let i = 0; i < bytes.byteLength; i++) {
- binary += String.fromCharCode(bytes[i]);
- }
- return btoa(binary);
-}
-
-/**
- * Convert base64 string back to ArrayBuffer
- */
-export function base64ToArrayBuffer(base64: string): ArrayBuffer {
- const binary = atob(base64);
- const bytes = new Uint8Array(binary.length);
- for (let i = 0; i < binary.length; i++) {
- bytes[i] = binary.charCodeAt(i);
- }
- return bytes.buffer;
-}
diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts
index b5fc2b4b..c7f28025 100644
--- a/apps/web/src/middleware.ts
+++ b/apps/web/src/middleware.ts
@@ -1,24 +1,18 @@
import { NextResponse } from "next/server";
-import type { NextRequest } from "next/server";
-export async function middleware(request: NextRequest) {
- // Handle fuckcapcut.com domain redirect
- if (request.headers.get("host") === "fuckcapcut.com") {
- return NextResponse.redirect("https://opencut.app/why-not-capcut", 301);
- }
-
- return NextResponse.next();
+export async function middleware() {
+ return NextResponse.next();
}
export const config = {
- matcher: [
- /*
- * Match all request paths except for the ones starting with:
- * - api (API routes)
- * - _next/static (static files)
- * - _next/image (image optimization files)
- * - favicon.ico (favicon file)
- */
- "/((?!api|_next/static|_next/image|favicon.ico).*)",
- ],
+ matcher: [
+ /*
+ * Match all request paths except for the ones starting with:
+ * - api (API routes)
+ * - _next/static (static files)
+ * - _next/image (image optimization files)
+ * - favicon.ico (favicon file)
+ */
+ "/((?!api|_next/static|_next/image|favicon.ico).*)",
+ ],
};
diff --git a/apps/web/src/services/media/video-cache.ts b/apps/web/src/services/media/video-cache.ts
new file mode 100644
index 00000000..f3ddfcb2
--- /dev/null
+++ b/apps/web/src/services/media/video-cache.ts
@@ -0,0 +1,314 @@
+import {
+ Input,
+ ALL_FORMATS,
+ BlobSource,
+ CanvasSink,
+ type WrappedCanvas,
+} from "mediabunny";
+
+interface VideoSinkData {
+ sink: CanvasSink;
+ iterator: AsyncGenerator<WrappedCanvas, void, unknown> | null;
+ currentFrame: WrappedCanvas | null;
+ nextFrame: WrappedCanvas | null;
+ lastTime: number;
+ prefetching: boolean;
+ prefetchPromise: Promise<void> | null;
+}
+export class VideoCache {
+ private sinks = new Map<string, VideoSinkData>();
+ private initPromises = new Map<string, Promise<void>>();
+
+ async getFrameAt({
+ mediaId,
+ file,
+ time,
+ }: {
+ mediaId: string;
+ file: File;
+ time: number;
+ }): Promise<WrappedCanvas | null> {
+ await this.ensureSink({ mediaId, file });
+
+ const sinkData = this.sinks.get(mediaId);
+ if (!sinkData) return null;
+
+ if (sinkData.nextFrame && sinkData.nextFrame.timestamp <= time) {
+ sinkData.currentFrame = sinkData.nextFrame;
+ sinkData.nextFrame = null;
+ this.startPrefetch({ sinkData });
+ }
+
+ if (
+ sinkData.currentFrame &&
+ this.isFrameValid({ frame: sinkData.currentFrame, time })
+ ) {
+ if (!sinkData.nextFrame && !sinkData.prefetching) {
+ this.startPrefetch({ sinkData });
+ }
+ return sinkData.currentFrame;
+ }
+
+ if (
+ sinkData.iterator &&
+ sinkData.currentFrame &&
+ time >= sinkData.lastTime &&
+ time < sinkData.lastTime + 2.0
+ ) {
+ const frame = await this.iterateToTime({ sinkData, targetTime: time });
+ if (frame) {
+ if (!sinkData.nextFrame && !sinkData.prefetching) {
+ this.startPrefetch({ sinkData });
+ }
+ return frame;
+ }
+ }
+
+ const frame = await this.seekToTime({ sinkData, time });
+ if (frame && !sinkData.nextFrame && !sinkData.prefetching) {
+ this.startPrefetch({ sinkData });
+ }
+ return frame;
+ }
+
+ private isFrameValid({
+ frame,
+ time,
+ }: {
+ frame: WrappedCanvas;
+ time: number;
+ }): boolean {
+ return time >= frame.timestamp && time < frame.timestamp + frame.duration;
+ }
+ private async iterateToTime({
+ sinkData,
+ targetTime,
+ }: {
+ sinkData: VideoSinkData;
+ targetTime: number;
+ }): Promise<WrappedCanvas | null> {
+ if (!sinkData.iterator) return null;
+
+ try {
+ while (true) {
+ // Wait for any pending prefetch to finish before touching iterator
+ if (sinkData.prefetching && sinkData.prefetchPromise) {
+ await sinkData.prefetchPromise;
+ }
+
+ // Check if the nextFrame (which might have just arrived) is what we need
+ if (
+ sinkData.nextFrame &&
+ sinkData.nextFrame.timestamp <= targetTime + 0.05 // Tolerance
+ ) {
+ sinkData.currentFrame = sinkData.nextFrame;
+ sinkData.nextFrame = null;
+ } else {
+ const { value: frame, done } = await sinkData.iterator.next();
+
+ if (done || !frame) break;
+
+ sinkData.currentFrame = frame;
+ }
+
+ const frame = sinkData.currentFrame;
+ if (!frame) break;
+
+ sinkData.lastTime = frame.timestamp;
+
+ if (this.isFrameValid({ frame, time: targetTime })) {
+ return frame;
+ }
+
+ if (frame.timestamp > targetTime + 1.0) break;
+ }
+ } catch (error) {
+ console.warn("Iterator failed, will restart:", error);
+ sinkData.iterator = null;
+ }
+
+ return null;
+ }
+ private async seekToTime({
+ sinkData,
+ time,
+ }: {
+ sinkData: VideoSinkData;
+ time: number;
+ }): Promise<WrappedCanvas | null> {
+ try {
+ if (sinkData.prefetching && sinkData.prefetchPromise) {
+ await sinkData.prefetchPromise;
+ }
+
+ if (sinkData.iterator) {
+ await sinkData.iterator.return();
+ sinkData.iterator = null;
+ }
+
+ sinkData.nextFrame = null;
+ sinkData.iterator = sinkData.sink.canvases(time);
+ sinkData.lastTime = time;
+
+ // Fetch current frame
+ const { value: frame } = await sinkData.iterator.next();
+
+ if (frame) {
+ sinkData.currentFrame = frame;
+
+ // Aggressively fetch next frame immediately to fill buffer
+ // This matches the mediaplayer example which fetches 2 frames on start
+ try {
+ const { value: next } = await sinkData.iterator.next();
+ if (next) {
+ sinkData.nextFrame = next;
+ }
+ } catch (e) {
+ console.warn("Failed to pre-fetch next frame on seek:", e);
+ }
+
+ return frame;
+ }
+ } catch (error) {
+ console.warn("Failed to seek video:", error);
+ }
+
+ return null;
+ }
+
+ private startPrefetch({ sinkData }: { sinkData: VideoSinkData }): void {
+ if (sinkData.prefetching || !sinkData.iterator || sinkData.nextFrame) {
+ return;
+ }
+
+ sinkData.prefetching = true;
+ sinkData.prefetchPromise = this.prefetchNextFrame({ sinkData });
+ }
+
+ private async prefetchNextFrame({
+ sinkData,
+ }: {
+ sinkData: VideoSinkData;
+ }): Promise<void> {
+ if (!sinkData.iterator) {
+ sinkData.prefetching = false;
+ sinkData.prefetchPromise = null;
+ return;
+ }
+
+ try {
+ const { value: frame, done } = await sinkData.iterator.next();
+
+ if (done || !frame) {
+ sinkData.prefetching = false;
+ sinkData.prefetchPromise = null;
+ return;
+ }
+
+ sinkData.nextFrame = frame;
+ sinkData.prefetching = false;
+ sinkData.prefetchPromise = null;
+ } catch (error) {
+ console.warn("Prefetch failed:", error);
+ sinkData.prefetching = false;
+ sinkData.prefetchPromise = null;
+ sinkData.iterator = null;
+ }
+ }
+ private async ensureSink({
+ mediaId,
+ file,
+ }: {
+ mediaId: string;
+ file: File;
+ }): Promise<void> {
+ if (this.sinks.has(mediaId)) return;
+
+ if (this.initPromises.has(mediaId)) {
+ await this.initPromises.get(mediaId);
+ return;
+ }
+
+ const initPromise = this.initializeSink({ mediaId, file });
+ this.initPromises.set(mediaId, initPromise);
+
+ try {
+ await initPromise;
+ } finally {
+ this.initPromises.delete(mediaId);
+ }
+ }
+ private async initializeSink({
+ mediaId,
+ file,
+ }: {
+ mediaId: string;
+ file: File;
+ }): Promise<void> {
+ try {
+ const input = new Input({
+ source: new BlobSource(file),
+ formats: ALL_FORMATS,
+ });
+
+ const videoTrack = await input.getPrimaryVideoTrack();
+ if (!videoTrack) {
+ throw new Error("No video track found");
+ }
+
+ const canDecode = await videoTrack.canDecode();
+ if (!canDecode) {
+ throw new Error("Video codec not supported for decoding");
+ }
+
+ const sink = new CanvasSink(videoTrack, {
+ poolSize: 3,
+ fit: "contain",
+ });
+
+ this.sinks.set(mediaId, {
+ sink,
+ iterator: null,
+ currentFrame: null,
+ nextFrame: null,
+ lastTime: -1,
+ prefetching: false,
+ prefetchPromise: null,
+ });
+ } catch (error) {
+ console.error(`Failed to initialize video sink for ${mediaId}:`, error);
+ throw error;
+ }
+ }
+
+ clearVideo({ mediaId }: { mediaId: string }): void {
+ const sinkData = this.sinks.get(mediaId);
+ if (sinkData) {
+ if (sinkData.iterator) {
+ void sinkData.iterator.return();
+ }
+
+ this.sinks.delete(mediaId);
+ }
+
+ this.initPromises.delete(mediaId);
+ }
+
+ clearAll(): void {
+ for (const [mediaId] of this.sinks) {
+ this.clearVideo({ mediaId });
+ }
+ }
+
+ getStats() {
+ return {
+ totalSinks: this.sinks.size,
+ activeSinks: Array.from(this.sinks.values()).filter((s) => s.iterator)
+ .length,
+ cachedFrames: Array.from(this.sinks.values()).filter(
+ (s) => s.currentFrame,
+ ).length,
+ };
+ }
+}
+export const videoCache = new VideoCache();
diff --git a/apps/web/src/services/renderer/canvas-renderer.ts b/apps/web/src/services/renderer/canvas-renderer.ts
new file mode 100644
index 00000000..840d8b52
--- /dev/null
+++ b/apps/web/src/services/renderer/canvas-renderer.ts
@@ -0,0 +1,87 @@
+import type { BaseNode } from "./nodes/base-node";
+
+export type CanvasRendererParams = {
+ width: number;
+ height: number;
+ fps: number;
+};
+
+export class CanvasRenderer {
+ canvas: OffscreenCanvas | HTMLCanvasElement;
+ context: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;
+ width: number;
+ height: number;
+ fps: number;
+
+ constructor({ width, height, fps }: CanvasRendererParams) {
+ this.width = width;
+ this.height = height;
+ this.fps = fps;
+
+ try {
+ this.canvas = new OffscreenCanvas(width, height);
+ } catch {
+ this.canvas = document.createElement("canvas");
+ this.canvas.width = width;
+ this.canvas.height = height;
+ }
+
+ const context = this.canvas.getContext("2d");
+ if (!context) {
+ throw new Error("Failed to get canvas context");
+ }
+
+ this.context = context as
+ | OffscreenCanvasRenderingContext2D
+ | CanvasRenderingContext2D;
+ }
+
+ setSize({ width, height }: { width: number; height: number }) {
+ this.width = width;
+ this.height = height;
+
+ if (this.canvas instanceof OffscreenCanvas) {
+ this.canvas = new OffscreenCanvas(width, height);
+ } else {
+ this.canvas.width = width;
+ this.canvas.height = height;
+ }
+
+ const context = this.canvas.getContext("2d");
+ if (!context) {
+ throw new Error("Failed to get canvas context");
+ }
+ this.context = context as
+ | OffscreenCanvasRenderingContext2D
+ | CanvasRenderingContext2D;
+ }
+
+ private clear() {
+ this.context.fillStyle = "black";
+ this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
+ }
+
+ async render({ node, time }: { node: BaseNode; time: number }) {
+ this.clear();
+ await node.render({ renderer: this, time });
+ }
+
+ async renderToCanvas({
+ node,
+ time,
+ targetCanvas,
+ }: {
+ node: BaseNode;
+ time: number;
+ targetCanvas: HTMLCanvasElement;
+ }) {
+ await this.render({ node, time });
+
+ const ctx = targetCanvas.getContext("2d");
+ if (!ctx) {
+ throw new Error("Failed to get target canvas context");
+ }
+
+ ctx.drawImage(this.canvas, 0, 0, targetCanvas.width, targetCanvas.height);
+ }
+}
diff --git a/apps/web/src/services/renderer/nodes/base-node.ts b/apps/web/src/services/renderer/nodes/base-node.ts
new file mode 100644
index 00000000..55bd055a
--- /dev/null
+++ b/apps/web/src/services/renderer/nodes/base-node.ts
@@ -0,0 +1,35 @@
+import type { CanvasRenderer } from "../canvas-renderer";
+
+export type BaseNodeParams = object | undefined;
+
+export class BaseNode<Params extends BaseNodeParams = BaseNodeParams> {
+ params: Params;
+
+ constructor(params?: Params) {
+ this.params = params ?? ({} as Params);
+ }
+
+ children: BaseNode[] = [];
+
+ add(child: BaseNode) {
+ this.children.push(child);
+ return this;
+ }
+
+ remove(child: BaseNode) {
+ this.children = this.children.filter((c) => c !== child);
+ return this;
+ }
+
+ async render({
+ renderer,
+ time,
+ }: {
+ renderer: CanvasRenderer;
+ time: number;
+ }): Promise<void> {
+ for (const child of this.children) {
+ await child.render({ renderer, time });
+ }
+ }
+}
diff --git a/apps/web/src/services/renderer/nodes/blur-background-node.ts b/apps/web/src/services/renderer/nodes/blur-background-node.ts
new file mode 100644
index 00000000..504161d9
--- /dev/null
+++ b/apps/web/src/services/renderer/nodes/blur-background-node.ts
@@ -0,0 +1,75 @@
+import type { CanvasRenderer } from "../canvas-renderer";
+import { BaseNode } from "./base-node";
+
+export type BlurBackgroundNodeParams = {
+ blurIntensity: number;
+ contentNodes: BaseNode[];
+};
+
+export class BlurBackgroundNode extends BaseNode<BlurBackgroundNodeParams> {
+ private blurIntensity: number;
+ private contentNodes: BaseNode[];
+
+ constructor(params: BlurBackgroundNodeParams) {
+ super(params);
+ this.blurIntensity = params.blurIntensity;
+ this.contentNodes = params.contentNodes;
+ }
+
+ async render({
+ renderer,
+ time,
+ }: {
+ renderer: CanvasRenderer;
+ time: number;
+ }): Promise<void> {
+ let offscreen: OffscreenCanvas | HTMLCanvasElement;
+ let offscreenCtx:
+ | OffscreenCanvasRenderingContext2D
+ | CanvasRenderingContext2D;
+
+ try {
+ offscreen = new OffscreenCanvas(renderer.width, renderer.height);
+ const ctx = offscreen.getContext("2d");
+ if (!ctx) {
+ throw new Error("failed to get offscreen canvas context");
+ }
+ offscreenCtx = ctx;
+ } catch {
+ offscreen = document.createElement("canvas");
+ offscreen.width = renderer.width;
+ offscreen.height = renderer.height;
+ const ctx = offscreen.getContext("2d");
+ if (!ctx) {
+ throw new Error("failed to get canvas context");
+ }
+ offscreenCtx = ctx;
+ }
+
+ const originalContext = renderer.context;
+ renderer.context = offscreenCtx;
+
+ for (const node of this.contentNodes) {
+ await node.render({ renderer, time });
+ }
+
+ renderer.context = originalContext;
+
+ const zoomScale = 1.4;
+ const scaledWidth = renderer.width * zoomScale;
+ const scaledHeight = renderer.height * zoomScale;
+ const offsetX = (renderer.width - scaledWidth) / 2;
+ const offsetY = (renderer.height - scaledHeight) / 2;
+
+ renderer.context.save();
+ renderer.context.filter = `blur(${this.blurIntensity}px)`;
+ renderer.context.drawImage(
+ offscreen as CanvasImageSource,
+ offsetX,
+ offsetY,
+ scaledWidth,
+ scaledHeight,
+ );
+ renderer.context.restore();
+ }
+}
diff --git a/apps/web/src/services/renderer/nodes/color-node.ts b/apps/web/src/services/renderer/nodes/color-node.ts
new file mode 100644
index 00000000..d3afbf9b
--- /dev/null
+++ b/apps/web/src/services/renderer/nodes/color-node.ts
@@ -0,0 +1,31 @@
+import { drawCssBackground } from "@/lib/gradients";
+import type { CanvasRenderer } from "../canvas-renderer";
+import { BaseNode } from "./base-node";
+
+export type ColorNodeParams = {
+ color: string;
+};
+
+export class ColorNode extends BaseNode<ColorNodeParams> {
+ private color: string;
+
+ constructor(params: ColorNodeParams) {
+ super(params);
+ this.color = params.color;
+ }
+
+ async render({ renderer }: { renderer: CanvasRenderer }) {
+ if (/gradient\(/i.test(this.color)) {
+ drawCssBackground({
+ ctx: renderer.context,
+ width: renderer.width,
+ height: renderer.height,
+ css: this.color,
+ });
+ return;
+ }
+
+ renderer.context.fillStyle = this.color;
+ renderer.context.fillRect(0, 0, renderer.width, renderer.height);
+ }
+}
diff --git a/apps/web/src/services/renderer/nodes/image-node.ts b/apps/web/src/services/renderer/nodes/image-node.ts
new file mode 100644
index 00000000..2a684d5f
--- /dev/null
+++ b/apps/web/src/services/renderer/nodes/image-node.ts
@@ -0,0 +1,93 @@
+import type { CanvasRenderer } from "../canvas-renderer";
+import { BaseNode } from "./base-node";
+import type { BaseMediaNodeParams } from "./video-node";
+
+const IMAGE_EPSILON = 1 / 1000;
+
+export type ImageNodeParams = BaseMediaNodeParams;
+
+export class ImageNode extends BaseNode<ImageNodeParams> {
+ private image?: HTMLImageElement;
+ private readyPromise: Promise<void>;
+
+ constructor(params: ImageNodeParams) {
+ super(params);
+ this.readyPromise = this.load();
+ }
+
+ private async load() {
+ const image = new Image();
+ this.image = image;
+ const url = URL.createObjectURL(this.params.file);
+
+ await new Promise<void>((resolve, reject) => {
+ image.onload = () => resolve();
+ image.onerror = () => reject(new Error("Image load failed"));
+ image.src = url;
+ });
+
+ URL.revokeObjectURL(url);
+ }
+
+ private getImageTime(time: number) {
+ return time - this.params.timeOffset + this.params.trimStart;
+ }
+
+ private isInRange(time: number) {
+ const imageTime = this.getImageTime(time);
+ return (
+ imageTime >= this.params.trimStart - IMAGE_EPSILON &&
+ imageTime < this.params.trimStart + this.params.duration
+ );
+ }
+
+ async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) {
+ await super.render({ renderer, time });
+
+ if (!this.isInRange(time)) {
+ return;
+ }
+
+ await this.readyPromise;
+
+ if (!this.image) {
+ return;
+ }
+
+ renderer.context.save();
+
+ if (this.params.opacity !== undefined) {
+ renderer.context.globalAlpha = this.params.opacity;
+ }
+
+ if (
+ this.params.x !== undefined &&
+ this.params.y !== undefined &&
+ this.params.width !== undefined &&
+ this.params.height !== undefined
+ ) {
+ renderer.context.drawImage(
+ this.image,
+ this.params.x,
+ this.params.y,
+ this.params.width,
+ this.params.height,
+ );
+ } else {
+ const mediaW = this.image.naturalWidth || renderer.width;
+ const mediaH = this.image.naturalHeight || renderer.height;
+ const containScale = Math.min(
+ renderer.width / mediaW,
+ renderer.height / mediaH,
+ );
+ const drawW = mediaW * containScale;
+ const drawH = mediaH * containScale;
+ const drawX = (renderer.width - drawW) / 2;
+ const drawY = (renderer.height - drawH) / 2;
+
+ renderer.context.drawImage(this.image, drawX, drawY, drawW, drawH);
+ }
+
+ renderer.context.restore();
+ }
+}
diff --git a/apps/web/src/services/renderer/nodes/root-node.ts b/apps/web/src/services/renderer/nodes/root-node.ts
new file mode 100644
index 00000000..336018e2
--- /dev/null
+++ b/apps/web/src/services/renderer/nodes/root-node.ts
@@ -0,0 +1,11 @@
+import { BaseNode } from "./base-node";
+
+export type RootNodeParams = {
+ duration: number;
+};
+
+export class RootNode extends BaseNode<RootNodeParams> {
+ get duration() {
+ return this.params.duration ?? 0;
+ }
+}
diff --git a/apps/web/src/services/renderer/nodes/sticker-node.ts b/apps/web/src/services/renderer/nodes/sticker-node.ts
new file mode 100644
index 00000000..5af13a6f
--- /dev/null
+++ b/apps/web/src/services/renderer/nodes/sticker-node.ts
@@ -0,0 +1,87 @@
+import type { CanvasRenderer } from "../canvas-renderer";
+import { BaseNode, type BaseNodeParams } from "./base-node";
+import type { Transform } from "@/types/timeline";
+
+const STICKER_EPSILON = 1 / 1000;
+
+export type StickerNodeParams = BaseNodeParams & {
+ iconName: string;
+ duration: number;
+ timeOffset: number;
+ trimStart: number;
+ trimEnd: number;
+ transform: Transform;
+ opacity: number;
+ color?: string;
+};
+
+export class StickerNode extends BaseNode<StickerNodeParams> {
+ private image?: HTMLImageElement;
+ private readyPromise: Promise<void>;
+
+ constructor(params: StickerNodeParams) {
+ super(params);
+ this.readyPromise = this.load();
+ }
+
+ private async load() {
+ const image = new Image();
+ this.image = image;
+ const color = this.params.color
+ ? `&color=${encodeURIComponent(this.params.color)}`
+ : "";
+ const url = `https://api.iconify.design/${this.params.iconName}.svg?width=200&height=200${color}`;
+
+ await new Promise<void>((resolve, reject) => {
+ image.onload = () => resolve();
+ image.onerror = () =>
+ reject(new Error(`Failed to load sticker: ${this.params.iconName}`));
+ image.src = url;
+ });
+ }
+
+ private getStickerTime(time: number) {
+ return time - this.params.timeOffset + this.params.trimStart;
+ }
+
+ private isInRange(time: number) {
+ const stickerTime = this.getStickerTime(time);
+ return (
+ stickerTime >= this.params.trimStart - STICKER_EPSILON &&
+ stickerTime < this.params.trimStart + this.params.duration
+ );
+ }
+
+ async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) {
+ await super.render({ renderer, time });
+
+ if (!this.isInRange(time)) {
+ return;
+ }
+
+ await this.readyPromise;
+
+ if (!this.image) {
+ return;
+ }
+
+ const { transform, opacity } = this.params;
+ const size = 200 * transform.scale;
+ const x = renderer.width / 2 + transform.position.x - size / 2;
+ const y = renderer.height / 2 + transform.position.y - size / 2;
+
+ renderer.context.save();
+ renderer.context.globalAlpha = opacity;
+
+ if (transform.rotate !== 0) {
+ const centerX = x + size / 2;
+ const centerY = y + size / 2;
+ renderer.context.translate(centerX, centerY);
+ renderer.context.rotate((transform.rotate * Math.PI) / 180);
+ renderer.context.translate(-centerX, -centerY);
+ }
+
+ renderer.context.drawImage(this.image, x, y, size, size);
+ renderer.context.restore();
+ }
+}
diff --git a/apps/web/src/services/renderer/nodes/text-node.ts b/apps/web/src/services/renderer/nodes/text-node.ts
new file mode 100644
index 00000000..2cbf3d2a
--- /dev/null
+++ b/apps/web/src/services/renderer/nodes/text-node.ts
@@ -0,0 +1,74 @@
+import type { CanvasRenderer } from "../canvas-renderer";
+import { BaseNode } from "./base-node";
+import type { TextElement } from "@/types/timeline";
+
+export type TextNodeParams = TextElement & {
+ canvasCenter: { x: number; y: number };
+ textBaseline?: CanvasTextBaseline;
+};
+
+export class TextNode extends BaseNode<TextNodeParams> {
+ isInRange({ time }: { time: number }) {
+ return (
+ time >= this.params.startTime &&
+ time < this.params.startTime + this.params.duration
+ );
+ }
+
+ async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) {
+ if (!this.isInRange({ time })) {
+ return;
+ }
+
+ renderer.context.save();
+
+ const x = this.params.transform.position.x + this.params.canvasCenter.x;
+ const y = this.params.transform.position.y + this.params.canvasCenter.y;
+
+ renderer.context.translate(x, y);
+ if (this.params.transform.rotate) {
+ renderer.context.rotate((this.params.transform.rotate * Math.PI) / 180);
+ }
+
+ const fontWeight = this.params.fontWeight === "bold" ? "bold" : "normal";
+ const fontStyle = this.params.fontStyle === "italic" ? "italic" : "normal";
+ renderer.context.font = `${fontStyle} ${fontWeight} ${this.params.fontSize}px ${this.params.fontFamily}`;
+ renderer.context.textAlign = this.params.textAlign;
+ renderer.context.textBaseline = this.params.textBaseline || "middle";
+ renderer.context.fillStyle = this.params.color;
+
+ const prevAlpha = renderer.context.globalAlpha;
+ renderer.context.globalAlpha = this.params.opacity;
+
+ if (this.params.backgroundColor) {
+ const metrics = renderer.context.measureText(this.params.content);
+ const ascent =
+ metrics.actualBoundingBoxAscent ?? this.params.fontSize * 0.8;
+ const descent =
+ metrics.actualBoundingBoxDescent ?? this.params.fontSize * 0.2;
+ const textW = metrics.width;
+ const textH = ascent + descent;
+ const padX = 8;
+ const padY = 4;
+
+ renderer.context.fillStyle = this.params.backgroundColor;
+ let bgLeft = -textW / 2;
+ if (renderer.context.textAlign === "left") bgLeft = 0;
+ if (renderer.context.textAlign === "right") bgLeft = -textW;
+
+ renderer.context.fillRect(
+ bgLeft - padX,
+ -textH / 2 - padY,
+ textW + padX * 2,
+ textH + padY * 2,
+ );
+
+ renderer.context.fillStyle = this.params.color;
+ }
+
+ renderer.context.fillText(this.params.content, 0, 0);
+
+ renderer.context.globalAlpha = prevAlpha;
+ renderer.context.restore();
+ }
+}
diff --git a/apps/web/src/services/renderer/nodes/video-node.ts b/apps/web/src/services/renderer/nodes/video-node.ts
new file mode 100644
index 00000000..01394b3d
--- /dev/null
+++ b/apps/web/src/services/renderer/nodes/video-node.ts
@@ -0,0 +1,84 @@
+import type { CanvasRenderer } from "../canvas-renderer";
+import { BaseNode } from "./base-node";
+import { videoCache } from "@/services/media/video-cache";
+
+const VIDEO_EPSILON = 1 / 1000;
+
+export interface BaseMediaNodeParams {
+ file: File;
+ duration: number;
+ timeOffset: number;
+ trimStart: number;
+ trimEnd: number;
+ x?: number;
+ y?: number;
+ width?: number;
+ height?: number;
+ opacity?: number;
+}
+
+export interface VideoNodeParams extends BaseMediaNodeParams {
+ mediaId: string;
+}
+
+export class VideoNode extends BaseNode<VideoNodeParams> {
+ private getVideoTime(time: number) {
+ return time - this.params.timeOffset + this.params.trimStart;
+ }
+
+ private isInRange(time: number) {
+ const videoTime = this.getVideoTime(time);
+ return (
+ videoTime >= this.params.trimStart - VIDEO_EPSILON &&
+ videoTime < this.params.trimStart + this.params.duration
+ );
+ }
+
+ async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) {
+ await super.render({ renderer, time });
+
+ if (!this.isInRange(time)) {
+ return;
+ }
+
+ const videoTime = this.getVideoTime(time);
+ const frame = await videoCache.getFrameAt({
+ mediaId: this.params.mediaId,
+ file: this.params.file,
+ time: videoTime,
+ });
+
+ if (frame) {
+ renderer.context.save();
+
+ if (this.params.opacity !== undefined) {
+ renderer.context.globalAlpha = this.params.opacity;
+ }
+
+ if (
+ this.params.x !== undefined &&
+ this.params.y !== undefined &&
+ this.params.width !== undefined &&
+ this.params.height !== undefined
+ ) {
+ renderer.context.drawImage(
+ frame.canvas,
+ this.params.x,
+ this.params.y,
+ this.params.width,
+ this.params.height,
+ );
+ } else {
+ renderer.context.drawImage(
+ frame.canvas,
+ 0,
+ 0,
+ renderer.width,
+ renderer.height,
+ );
+ }
+
+ renderer.context.restore();
+ }
+ }
+}
diff --git a/apps/web/src/services/renderer/scene-builder.ts b/apps/web/src/services/renderer/scene-builder.ts
new file mode 100644
index 00000000..b36edda1
--- /dev/null
+++ b/apps/web/src/services/renderer/scene-builder.ts
@@ -0,0 +1,130 @@
+import type { TimelineTrack } from "@/types/timeline";
+import type { MediaAsset } from "@/types/assets";
+import { RootNode } from "./nodes/root-node";
+import { VideoNode } from "./nodes/video-node";
+import { ImageNode } from "./nodes/image-node";
+import { TextNode } from "./nodes/text-node";
+import { StickerNode } from "./nodes/sticker-node";
+import { ColorNode } from "./nodes/color-node";
+import { BlurBackgroundNode } from "./nodes/blur-background-node";
+import type { TBackground, TCanvasSize } from "@/types/project";
+import { DEFAULT_BLUR_INTENSITY } from "@/constants/project-constants";
+import { isMainTrack } from "@/lib/timeline";
+
+export type BuildSceneParams = {
+ canvasSize: TCanvasSize;
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
+ duration: number;
+ background: TBackground;
+};
+
+export function buildScene(params: BuildSceneParams) {
+ const { tracks, mediaAssets, duration, canvasSize, background } = params;
+
+ const rootNode = new RootNode({ duration });
+ const mediaMap = new Map(mediaAssets.map((m) => [m.id, m]));
+
+ const visibleTracks = tracks.filter(
+ (track) => !("hidden" in track && track.hidden),
+ );
+
+ const mainTrack = visibleTracks.find((track) => isMainTrack(track)) ?? null;
+ const orderedTracksTopToBottom = [
+ ...visibleTracks.filter((track) => !isMainTrack(track)),
+ ...(mainTrack ? [mainTrack] : []),
+ ];
+
+ const orderedTracksBottomToTop = orderedTracksTopToBottom.slice().reverse();
+
+ const contentNodes = [];
+
+ for (const track of orderedTracksBottomToTop) {
+ const elements = track.elements
+ .filter((element) => !("hidden" in element && element.hidden))
+ .slice()
+ .sort((a, b) => {
+ if (a.startTime !== b.startTime) return a.startTime - b.startTime;
+ return a.id.localeCompare(b.id);
+ });
+
+ for (const element of elements) {
+ if (element.type === "video" || element.type === "image") {
+ const mediaAsset = mediaMap.get(element.mediaId);
+ if (!mediaAsset?.file) {
+ continue;
+ }
+
+ if (mediaAsset.type === "video") {
+ contentNodes.push(
+ new VideoNode({
+ mediaId: mediaAsset.id,
+ file: mediaAsset.file,
+ duration: element.duration,
+ timeOffset: element.startTime,
+ trimStart: element.trimStart,
+ trimEnd: element.trimEnd,
+ }),
+ );
+ }
+ if (mediaAsset.type === "image") {
+ contentNodes.push(
+ new ImageNode({
+ file: mediaAsset.file,
+ duration: element.duration,
+ timeOffset: element.startTime,
+ trimStart: element.trimStart,
+ trimEnd: element.trimEnd,
+ }),
+ );
+ }
+ }
+
+ if (element.type === "text") {
+ contentNodes.push(
+ new TextNode({
+ ...element,
+ canvasCenter: { x: canvasSize.width / 2, y: canvasSize.height / 2 },
+ textBaseline: "middle",
+ }),
+ );
+ }
+
+ if (element.type === "sticker") {
+ contentNodes.push(
+ new StickerNode({
+ iconName: element.iconName,
+ duration: element.duration,
+ timeOffset: element.startTime,
+ trimStart: element.trimStart,
+ trimEnd: element.trimEnd,
+ transform: element.transform,
+ opacity: element.opacity,
+ color: element.color,
+ }),
+ );
+ }
+ }
+ }
+
+ if (background.type === "blur") {
+ rootNode.add(
+ new BlurBackgroundNode({
+ blurIntensity: background.blurIntensity ?? DEFAULT_BLUR_INTENSITY,
+ contentNodes,
+ }),
+ );
+ for (const node of contentNodes) {
+ rootNode.add(node);
+ }
+ } else {
+ if (background.type === "color" && background.color !== "transparent") {
+ rootNode.add(new ColorNode({ color: background.color }));
+ }
+ for (const node of contentNodes) {
+ rootNode.add(node);
+ }
+ }
+
+ return rootNode;
+}
diff --git a/apps/web/src/services/renderer/scene-exporter.ts b/apps/web/src/services/renderer/scene-exporter.ts
new file mode 100644
index 00000000..39e94e55
--- /dev/null
+++ b/apps/web/src/services/renderer/scene-exporter.ts
@@ -0,0 +1,152 @@
+import EventEmitter from "eventemitter3";
+
+import {
+ Output,
+ Mp4OutputFormat,
+ WebMOutputFormat,
+ BufferTarget,
+ CanvasSource,
+ AudioBufferSource,
+ QUALITY_LOW,
+ QUALITY_MEDIUM,
+ QUALITY_HIGH,
+ QUALITY_VERY_HIGH,
+} from "mediabunny";
+import type { RootNode } from "./nodes/root-node";
+import { CanvasRenderer } from "./canvas-renderer";
+
+export type ExportFormat = "mp4" | "webm";
+export type ExportQuality = "low" | "medium" | "high" | "very_high";
+
+type ExportParams = {
+ width: number;
+ height: number;
+ fps: number;
+ format: ExportFormat;
+ quality: ExportQuality;
+ shouldIncludeAudio?: boolean;
+ audioBuffer?: AudioBuffer;
+};
+
+const qualityMap = {
+ low: QUALITY_LOW,
+ medium: QUALITY_MEDIUM,
+ high: QUALITY_HIGH,
+ very_high: QUALITY_VERY_HIGH,
+};
+
+export type SceneExporterEvents = {
+ progress: [progress: number];
+ complete: [buffer: ArrayBuffer];
+ error: [error: Error];
+ cancelled: [];
+};
+
+export class SceneExporter extends EventEmitter<SceneExporterEvents> {
+ private renderer: CanvasRenderer;
+ private format: ExportFormat;
+ private quality: ExportQuality;
+ private shouldIncludeAudio: boolean;
+ private audioBuffer?: AudioBuffer;
+
+ private isCancelled = false;
+
+ constructor({
+ width,
+ height,
+ fps,
+ format,
+ quality,
+ shouldIncludeAudio,
+ audioBuffer,
+ }: ExportParams) {
+ super();
+ this.renderer = new CanvasRenderer({
+ width,
+ height,
+ fps,
+ });
+
+ this.format = format;
+ this.quality = quality;
+ this.shouldIncludeAudio = shouldIncludeAudio ?? false;
+ this.audioBuffer = audioBuffer;
+ }
+
+ cancel(): void {
+ this.isCancelled = true;
+ }
+
+ async export({
+ rootNode,
+ }: {
+ rootNode: RootNode;
+ }): Promise<ArrayBuffer | null> {
+ const { fps } = this.renderer;
+ const frameCount = Math.ceil(rootNode.duration * fps);
+
+ const outputFormat =
+ this.format === "webm" ? new WebMOutputFormat() : new Mp4OutputFormat();
+
+ const output = new Output({
+ format: outputFormat,
+ target: new BufferTarget(),
+ });
+
+ const videoSource = new CanvasSource(this.renderer.canvas, {
+ codec: this.format === "webm" ? "vp9" : "avc",
+ bitrate: qualityMap[this.quality],
+ });
+
+ output.addVideoTrack(videoSource, { frameRate: fps });
+
+ let audioSource: AudioBufferSource | null = null;
+ if (this.shouldIncludeAudio && this.audioBuffer) {
+ audioSource = new AudioBufferSource({
+ codec: this.format === "webm" ? "opus" : "aac",
+ bitrate: qualityMap[this.quality],
+ });
+ output.addAudioTrack(audioSource);
+ }
+
+ await output.start();
+
+ if (audioSource && this.audioBuffer) {
+ await audioSource.add(this.audioBuffer);
+ audioSource.close();
+ }
+
+ for (let i = 0; i < frameCount; i++) {
+ if (this.isCancelled) {
+ await output.cancel();
+ this.emit("cancelled");
+ return null;
+ }
+
+ const time = i / fps;
+ await this.renderer.render({ node: rootNode, time });
+ await videoSource.add(time, 1 / fps);
+
+ this.emit("progress", i / frameCount);
+ }
+
+ if (this.isCancelled) {
+ await output.cancel();
+ this.emit("cancelled");
+ return null;
+ }
+
+ videoSource.close();
+ await output.finalize();
+ this.emit("progress", 1);
+
+ const buffer = output.target.buffer;
+ if (!buffer) {
+ this.emit("error", new Error("Failed to export video"));
+ return null;
+ }
+
+ this.emit("complete", buffer);
+ return buffer;
+ }
+}
diff --git a/apps/web/src/services/storage/indexeddb-adapter.ts b/apps/web/src/services/storage/indexeddb-adapter.ts
new file mode 100644
index 00000000..cc4d03a0
--- /dev/null
+++ b/apps/web/src/services/storage/indexeddb-adapter.ts
@@ -0,0 +1,101 @@
+import type { StorageAdapter } from "./types";
+
+export class IndexedDBAdapter<T> implements StorageAdapter<T> {
+ private dbName: string;
+ private storeName: string;
+ private version: number;
+
+ constructor(dbName: string, storeName: string, version = 1) {
+ this.dbName = dbName;
+ this.storeName = storeName;
+ this.version = version;
+ }
+
+ private async getDB(): Promise<IDBDatabase> {
+ return new Promise((resolve, reject) => {
+ const request = indexedDB.open(this.dbName, this.version);
+
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => resolve(request.result);
+
+ request.onupgradeneeded = (event) => {
+ const db = (event.target as IDBOpenDBRequest).result;
+ if (!db.objectStoreNames.contains(this.storeName)) {
+ db.createObjectStore(this.storeName, { keyPath: "id" });
+ }
+ };
+ });
+ }
+
+ async get(key: string): Promise<T | null> {
+ const db = await this.getDB();
+ const transaction = db.transaction([this.storeName], "readonly");
+ const store = transaction.objectStore(this.storeName);
+
+ return new Promise((resolve, reject) => {
+ const request = store.get(key);
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => resolve(request.result || null);
+ });
+ }
+
+ async set(key: string, value: T): Promise<void> {
+ const db = await this.getDB();
+ const transaction = db.transaction([this.storeName], "readwrite");
+ const store = transaction.objectStore(this.storeName);
+
+ return new Promise((resolve, reject) => {
+ const request = store.put({ id: key, ...value });
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => resolve();
+ });
+ }
+
+ async remove(key: string): Promise<void> {
+ const db = await this.getDB();
+ const transaction = db.transaction([this.storeName], "readwrite");
+ const store = transaction.objectStore(this.storeName);
+
+ return new Promise((resolve, reject) => {
+ const request = store.delete(key);
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => resolve();
+ });
+ }
+
+ async list(): Promise<string[]> {
+ const db = await this.getDB();
+ const transaction = db.transaction([this.storeName], "readonly");
+ const store = transaction.objectStore(this.storeName);
+
+ return new Promise((resolve, reject) => {
+ const request = store.getAllKeys();
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => resolve(request.result as string[]);
+ });
+ }
+
+ async getAll(): Promise<T[]> {
+ const db = await this.getDB();
+ const transaction = db.transaction([this.storeName], "readonly");
+ const store = transaction.objectStore(this.storeName);
+
+ return new Promise((resolve, reject) => {
+ const request = store.getAll();
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => resolve(request.result || []);
+ });
+ }
+
+ async clear(): Promise<void> {
+ const db = await this.getDB();
+ const transaction = db.transaction([this.storeName], "readwrite");
+ const store = transaction.objectStore(this.storeName);
+
+ return new Promise((resolve, reject) => {
+ const request = store.clear();
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => resolve();
+ });
+ }
+}
diff --git a/apps/web/src/services/storage/migrations/base.ts b/apps/web/src/services/storage/migrations/base.ts
new file mode 100644
index 00000000..c7558264
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/base.ts
@@ -0,0 +1,5 @@
+export abstract class StorageMigration {
+ abstract from: number;
+ abstract to: number;
+ abstract run(): Promise<void>;
+}
diff --git a/apps/web/src/services/storage/migrations/index.ts b/apps/web/src/services/storage/migrations/index.ts
new file mode 100644
index 00000000..2343362f
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/index.ts
@@ -0,0 +1,14 @@
+export { StorageMigration } from "./base";
+export { StorageVersionManager } from "./version-manager";
+export { runStorageMigrations } from "./runner";
+import { V0toV1Migration } from "./v0-to-v1";
+import { V1toV2Migration } from "./v1-to-v2";
+import { V2toV3Migration } from "./v2-to-v3";
+
+export const CURRENT_STORAGE_VERSION = 3;
+
+export const migrations = [
+ new V0toV1Migration(),
+ new V1toV2Migration(),
+ new V2toV3Migration(),
+];
diff --git a/apps/web/src/services/storage/migrations/runner.ts b/apps/web/src/services/storage/migrations/runner.ts
new file mode 100644
index 00000000..d9277cb0
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/runner.ts
@@ -0,0 +1,134 @@
+import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
+import type { StorageMigration } from "./base";
+import { StorageVersionManager } from "./version-manager";
+
+export interface StorageMigrationResult {
+ fromVersion: number;
+ toVersion: number;
+ migrated: boolean;
+}
+
+export type StorageMigrationCallbacks = {
+ onMigrationStart?: ({
+ fromVersion,
+ toVersion,
+ }: {
+ fromVersion: number;
+ toVersion: number;
+ }) => void;
+ onMigrationComplete?: ({
+ fromVersion,
+ toVersion,
+ }: {
+ fromVersion: number;
+ toVersion: number;
+ }) => void;
+};
+
+type ProjectRecord = Record<string, unknown>;
+
+export async function runStorageMigrations({
+ migrations,
+ versionManager = new StorageVersionManager(),
+ callbacks,
+}: {
+ migrations: StorageMigration[];
+ versionManager?: StorageVersionManager;
+ callbacks?: StorageMigrationCallbacks;
+}): Promise<StorageMigrationResult> {
+ const versionRecord = await versionManager.getVersionRecord();
+ const inferredVersion = versionRecord
+ ? null
+ : await inferStorageVersionFromProjects();
+ const fromVersion =
+ versionRecord?.inProgress?.from ??
+ versionRecord?.version ??
+ inferredVersion ??
+ 0;
+
+ if (!versionRecord) {
+ await versionManager.setVersion({ version: fromVersion });
+ }
+
+ const orderedMigrations = [...migrations].sort((a, b) => a.from - b.from);
+ let currentVersion = fromVersion;
+
+ for (const migration of orderedMigrations) {
+ if (migration.from !== currentVersion) {
+ continue;
+ }
+
+ await versionManager.setInProgress({
+ from: migration.from,
+ to: migration.to,
+ });
+ callbacks?.onMigrationStart?.({
+ fromVersion: migration.from,
+ toVersion: migration.to,
+ });
+ await migration.run();
+ currentVersion = migration.to;
+ await versionManager.setVersion({ version: currentVersion });
+ await versionManager.clearInProgress();
+ callbacks?.onMigrationComplete?.({
+ fromVersion: migration.from,
+ toVersion: migration.to,
+ });
+ }
+
+ return {
+ fromVersion,
+ toVersion: currentVersion,
+ migrated: currentVersion !== fromVersion,
+ };
+}
+
+async function inferStorageVersionFromProjects(): Promise<number> {
+ const projectsAdapter = new IndexedDBAdapter<unknown>(
+ "video-editor-projects",
+ "projects",
+ 1,
+ );
+ const projects = await projectsAdapter.getAll();
+
+ if (projects.length === 0) {
+ return 0;
+ }
+
+ let lowestVersion = Number.POSITIVE_INFINITY;
+
+ for (const project of projects) {
+ const projectVersion = checkProjectVersion({ project });
+ if (projectVersion < lowestVersion) {
+ lowestVersion = projectVersion;
+ }
+ }
+
+ if (lowestVersion === Number.POSITIVE_INFINITY) {
+ return 0;
+ }
+
+ return lowestVersion;
+}
+
+function checkProjectVersion({ project }: { project: unknown }): number {
+ if (!isRecord(project)) {
+ return 0;
+ }
+
+ const versionValue = project.version;
+ if (typeof versionValue === "number") {
+ return versionValue;
+ }
+
+ const scenesValue = project.scenes;
+ if (Array.isArray(scenesValue) && scenesValue.length > 0) {
+ return 1;
+ }
+
+ return 0;
+}
+
+function isRecord(value: unknown): value is ProjectRecord {
+ return typeof value === "object" && value !== null;
+}
diff --git a/apps/web/src/services/storage/migrations/v0-to-v1.ts b/apps/web/src/services/storage/migrations/v0-to-v1.ts
new file mode 100644
index 00000000..7ec388f1
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/v0-to-v1.ts
@@ -0,0 +1,93 @@
+import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
+import type { SerializedScene } from "@/services/storage/types";
+import { buildDefaultScene } from "@/lib/scenes";
+import type { TScene } from "@/types/timeline";
+import { StorageMigration } from "./base";
+
+type ProjectRecord = Record<string, unknown>;
+
+export class V0toV1Migration extends StorageMigration {
+ from = 0;
+ to = 1;
+
+ async run(): Promise<void> {
+ const projectsAdapter = new IndexedDBAdapter<unknown>(
+ "video-editor-projects",
+ "projects",
+ 1,
+ );
+ const projects = await projectsAdapter.getAll();
+
+ for (const project of projects) {
+ if (!isRecord(project)) {
+ continue;
+ }
+
+ const scenesValue = project.scenes;
+ if (Array.isArray(scenesValue) && scenesValue.length > 0) {
+ continue;
+ }
+
+ const mainScene = buildDefaultScene({ isMain: true, name: "Main scene" });
+ const serializedScene = serializeScene({ scene: mainScene });
+ const updatedProject: ProjectRecord = {
+ ...project,
+ scenes: [serializedScene],
+ currentSceneId: mainScene.id,
+ version: 1,
+ };
+
+ const updatedAt = new Date().toISOString();
+ if (isRecord(project.metadata)) {
+ updatedProject.metadata = {
+ ...project.metadata,
+ updatedAt,
+ };
+ } else {
+ updatedProject.updatedAt = updatedAt;
+ }
+
+ const projectId = getProjectId({ project: updatedProject });
+ if (!projectId) {
+ continue;
+ }
+
+ await projectsAdapter.set(projectId, updatedProject);
+ }
+ }
+}
+
+function serializeScene({ scene }: { scene: TScene }): SerializedScene {
+ return {
+ id: scene.id,
+ name: scene.name,
+ isMain: scene.isMain,
+ tracks: scene.tracks,
+ bookmarks: scene.bookmarks,
+ createdAt: scene.createdAt.toISOString(),
+ updatedAt: scene.updatedAt.toISOString(),
+ };
+}
+
+function getProjectId({ project }: { project: ProjectRecord }): string | null {
+ const idValue = project.id;
+ if (typeof idValue === "string" && idValue.length > 0) {
+ return idValue;
+ }
+
+ const metadataValue = project.metadata;
+ if (!isRecord(metadataValue)) {
+ return null;
+ }
+
+ const metadataId = metadataValue.id;
+ if (typeof metadataId === "string" && metadataId.length > 0) {
+ return metadataId;
+ }
+
+ return null;
+}
+
+function isRecord(value: unknown): value is ProjectRecord {
+ return typeof value === "object" && value !== null;
+}
diff --git a/apps/web/src/services/storage/migrations/v1-to-v2.ts b/apps/web/src/services/storage/migrations/v1-to-v2.ts
new file mode 100644
index 00000000..05d302ea
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/v1-to-v2.ts
@@ -0,0 +1,339 @@
+import {
+ DEFAULT_BLUR_INTENSITY,
+ DEFAULT_CANVAS_SIZE,
+ DEFAULT_COLOR,
+ DEFAULT_FPS,
+} from "@/constants/project-constants";
+import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
+import { StorageMigration } from "./base";
+
+type ProjectRecord = Record<string, unknown>;
+
+export class V1toV2Migration extends StorageMigration {
+ from = 1;
+ to = 2;
+
+ async run(): Promise<void> {
+ const projectsAdapter = new IndexedDBAdapter<unknown>(
+ "video-editor-projects",
+ "projects",
+ 1,
+ );
+ const projects = await projectsAdapter.getAll();
+
+ for (const project of projects) {
+ if (!isRecord(project)) {
+ continue;
+ }
+
+ const projectId = getProjectId({ project });
+ if (!projectId) {
+ continue;
+ }
+
+ if (isV2Project({ project })) {
+ continue;
+ }
+
+ const migratedProject = migrateProject({ project, projectId });
+ await projectsAdapter.set(projectId, migratedProject);
+ }
+ }
+}
+
+function migrateProject({
+ project,
+ projectId,
+}: {
+ project: ProjectRecord;
+ projectId: string;
+}): ProjectRecord {
+ const createdAt = normalizeDateString({ value: project.createdAt });
+ const updatedAt = normalizeDateString({ value: project.updatedAt });
+ const metadataValue = project.metadata;
+
+ const metadata = isRecord(metadataValue)
+ ? {
+ id: getStringValue({ value: metadataValue.id, fallback: projectId }),
+ name: getStringValue({ value: metadataValue.name, fallback: "" }),
+ thumbnail: getStringValue({ value: metadataValue.thumbnail }),
+ createdAt: normalizeDateString({ value: metadataValue.createdAt }),
+ updatedAt: normalizeDateString({ value: metadataValue.updatedAt }),
+ }
+ : {
+ id: projectId,
+ name: getStringValue({ value: project.name, fallback: "" }),
+ thumbnail: getStringValue({ value: project.thumbnail }),
+ createdAt,
+ updatedAt,
+ };
+
+ const scenesValue = project.scenes;
+ const scenes = Array.isArray(scenesValue) ? scenesValue : [];
+ const legacyBookmarks = Array.isArray(project.bookmarks)
+ ? project.bookmarks
+ : null;
+ const normalizedScenes = applyLegacyBookmarks({
+ scenes,
+ legacyBookmarks,
+ });
+
+ const settingsValue = project.settings;
+ const settings = isRecord(settingsValue)
+ ? {
+ fps: getNumberValue({
+ value: settingsValue.fps,
+ fallback: DEFAULT_FPS,
+ }),
+ canvasSize: getCanvasSizeValue({
+ value: settingsValue.canvasSize,
+ fallback: DEFAULT_CANVAS_SIZE,
+ }),
+ background: getBackgroundValue({
+ value: settingsValue.background,
+ }),
+ }
+ : {
+ fps: getNumberValue({ value: project.fps, fallback: DEFAULT_FPS }),
+ canvasSize: getCanvasSizeValue({
+ value: project.canvasSize,
+ fallback: DEFAULT_CANVAS_SIZE,
+ }),
+ background: getBackgroundValue({
+ value: project.background,
+ backgroundType: project.backgroundType,
+ backgroundColor: project.backgroundColor,
+ blurIntensity: project.blurIntensity,
+ }),
+ };
+
+ const currentSceneId = getCurrentSceneId({
+ value: project.currentSceneId,
+ scenes: normalizedScenes,
+ });
+
+ return {
+ ...project,
+ metadata,
+ scenes: normalizedScenes,
+ currentSceneId,
+ settings,
+ version: 2,
+ };
+}
+
+function getProjectId({ project }: { project: ProjectRecord }): string | null {
+ const idValue = project.id;
+ if (typeof idValue === "string" && idValue.length > 0) {
+ return idValue;
+ }
+
+ const metadataValue = project.metadata;
+ if (!isRecord(metadataValue)) {
+ return null;
+ }
+
+ const metadataId = metadataValue.id;
+ if (typeof metadataId === "string" && metadataId.length > 0) {
+ return metadataId;
+ }
+
+ return null;
+}
+
+function getCurrentSceneId({
+ value,
+ scenes,
+}: {
+ value: unknown;
+ scenes: unknown[];
+}): string {
+ if (typeof value === "string" && value.length > 0) {
+ return value;
+ }
+
+ const mainSceneId = findMainSceneId({ scenes });
+ if (mainSceneId) {
+ return mainSceneId;
+ }
+
+ return "";
+}
+
+function findMainSceneId({ scenes }: { scenes: unknown[] }): string | null {
+ for (const scene of scenes) {
+ if (!isRecord(scene)) {
+ continue;
+ }
+
+ if (scene.isMain === true && typeof scene.id === "string") {
+ return scene.id;
+ }
+ }
+
+ for (const scene of scenes) {
+ if (!isRecord(scene)) {
+ continue;
+ }
+
+ if (typeof scene.id === "string") {
+ return scene.id;
+ }
+ }
+
+ return null;
+}
+
+function applyLegacyBookmarks({
+ scenes,
+ legacyBookmarks,
+}: {
+ scenes: unknown[];
+ legacyBookmarks: unknown[] | null;
+}): unknown[] {
+ if (!legacyBookmarks || legacyBookmarks.length === 0) {
+ return scenes;
+ }
+
+ const mainSceneId = findMainSceneId({ scenes });
+
+ return scenes.map((scene) => {
+ if (!isRecord(scene)) {
+ return scene;
+ }
+
+ if (mainSceneId && scene.id !== mainSceneId) {
+ return scene;
+ }
+
+ if (Array.isArray(scene.bookmarks) && scene.bookmarks.length > 0) {
+ return scene;
+ }
+
+ return {
+ ...scene,
+ bookmarks: legacyBookmarks,
+ };
+ });
+}
+
+function getBackgroundValue({
+ value,
+ backgroundType,
+ backgroundColor,
+ blurIntensity,
+}: {
+ value: unknown;
+ backgroundType?: unknown;
+ backgroundColor?: unknown;
+ blurIntensity?: unknown;
+}): {
+ type: "color" | "blur";
+ color?: string;
+ blurIntensity?: number;
+} {
+ if (isRecord(value)) {
+ const typeValue = value.type;
+ if (typeValue === "blur") {
+ return {
+ type: "blur",
+ blurIntensity: getNumberValue({
+ value: value.blurIntensity,
+ fallback: DEFAULT_BLUR_INTENSITY,
+ }),
+ };
+ }
+
+ return {
+ type: "color",
+ color: getStringValue({ value: value.color, fallback: DEFAULT_COLOR }),
+ };
+ }
+
+ if (backgroundType === "blur") {
+ return {
+ type: "blur",
+ blurIntensity: getNumberValue({
+ value: blurIntensity,
+ fallback: DEFAULT_BLUR_INTENSITY,
+ }),
+ };
+ }
+
+ return {
+ type: "color",
+ color: getStringValue({ value: backgroundColor, fallback: DEFAULT_COLOR }),
+ };
+}
+
+function getCanvasSizeValue({
+ value,
+ fallback,
+}: {
+ value: unknown;
+ fallback: { width: number; height: number };
+}): { width: number; height: number } {
+ if (isRecord(value)) {
+ const width = getNumberValue({
+ value: value.width,
+ fallback: fallback.width,
+ });
+ const height = getNumberValue({
+ value: value.height,
+ fallback: fallback.height,
+ });
+
+ return { width, height };
+ }
+
+ return fallback;
+}
+
+function getNumberValue({
+ value,
+ fallback,
+}: {
+ value: unknown;
+ fallback: number;
+}): number {
+ return typeof value === "number" ? value : fallback;
+}
+
+function getStringValue({
+ value,
+ fallback,
+}: {
+ value: unknown;
+ fallback?: string;
+}): string | undefined {
+ if (typeof value === "string") {
+ return value;
+ }
+
+ return fallback;
+}
+
+function normalizeDateString({ value }: { value: unknown }): string {
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (typeof value === "string") {
+ return value;
+ }
+
+ return new Date().toISOString();
+}
+
+function isV2Project({ project }: { project: ProjectRecord }): boolean {
+ const versionValue = project.version;
+ if (typeof versionValue === "number" && versionValue >= 2) {
+ return true;
+ }
+
+ return isRecord(project.metadata) && isRecord(project.settings);
+}
+
+function isRecord(value: unknown): value is ProjectRecord {
+ return typeof value === "object" && value !== null;
+}
diff --git a/apps/web/src/services/storage/migrations/v2-to-v3.ts b/apps/web/src/services/storage/migrations/v2-to-v3.ts
new file mode 100644
index 00000000..eb6db26d
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/v2-to-v3.ts
@@ -0,0 +1,92 @@
+import { getProjectDurationFromScenes } from "@/lib/scenes";
+import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
+import type { TScene } from "@/types/timeline";
+import { StorageMigration } from "./base";
+
+type ProjectRecord = Record<string, unknown>;
+
+export class V2toV3Migration extends StorageMigration {
+ from = 2;
+ to = 3;
+
+ async run(): Promise<void> {
+ const projectsAdapter = new IndexedDBAdapter<unknown>(
+ "video-editor-projects",
+ "projects",
+ 1,
+ );
+ const projects = await projectsAdapter.getAll();
+
+ for (const project of projects) {
+ if (!isRecord(project)) {
+ continue;
+ }
+
+ const projectId = getProjectId({ project });
+ if (!projectId) {
+ continue;
+ }
+
+ if (isV3Project({ project })) {
+ continue;
+ }
+
+ const scenes = getScenes({ project });
+ const duration = getProjectDurationFromScenes({ scenes });
+
+ const metadataValue = project.metadata;
+ const metadata = isRecord(metadataValue)
+ ? { ...metadataValue, duration }
+ : { duration };
+
+ const migratedProject = {
+ ...project,
+ metadata,
+ version: 3,
+ };
+
+ await projectsAdapter.set(projectId, migratedProject);
+ }
+ }
+}
+
+function getProjectId({ project }: { project: ProjectRecord }): string | null {
+ const idValue = project.id;
+ if (typeof idValue === "string" && idValue.length > 0) {
+ return idValue;
+ }
+
+ const metadataValue = project.metadata;
+ if (!isRecord(metadataValue)) {
+ return null;
+ }
+
+ const metadataId = metadataValue.id;
+ if (typeof metadataId === "string" && metadataId.length > 0) {
+ return metadataId;
+ }
+
+ return null;
+}
+
+function getScenes({ project }: { project: ProjectRecord }): TScene[] {
+ const scenesValue = project.scenes;
+ if (!Array.isArray(scenesValue)) {
+ return [];
+ }
+
+ return scenesValue.filter(isRecord) as unknown as TScene[];
+}
+
+function isV3Project({ project }: { project: ProjectRecord }): boolean {
+ const versionValue = project.version;
+ if (typeof versionValue === "number" && versionValue >= 3) {
+ return true;
+ }
+
+ return isRecord(project.metadata) && typeof project.metadata.duration === "number";
+}
+
+function isRecord(value: unknown): value is ProjectRecord {
+ return typeof value === "object" && value !== null;
+}
diff --git a/apps/web/src/services/storage/migrations/version-manager.ts b/apps/web/src/services/storage/migrations/version-manager.ts
new file mode 100644
index 00000000..879498ce
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/version-manager.ts
@@ -0,0 +1,73 @@
+import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
+
+type StorageVersionRecord = {
+ version: number;
+ inProgress?: {
+ from: number;
+ to: number;
+ };
+};
+
+const DEFAULT_DB_NAME = "video-editor-meta";
+const DEFAULT_STORE_NAME = "storage-version";
+const DEFAULT_DB_VERSION = 1;
+const STORAGE_VERSION_KEY = "storage-version";
+
+export class StorageVersionManager {
+ private adapter: IndexedDBAdapter<StorageVersionRecord>;
+
+ constructor({
+ dbName = DEFAULT_DB_NAME,
+ storeName = DEFAULT_STORE_NAME,
+ version = DEFAULT_DB_VERSION,
+ }: {
+ dbName?: string;
+ storeName?: string;
+ version?: number;
+ } = {}) {
+ this.adapter = new IndexedDBAdapter<StorageVersionRecord>(
+ dbName,
+ storeName,
+ version,
+ );
+ }
+
+ async getVersion(): Promise<number> {
+ const record = await this.adapter.get(STORAGE_VERSION_KEY);
+ return record?.version ?? 0;
+ }
+
+ async getVersionRecord(): Promise<StorageVersionRecord | null> {
+ return this.adapter.get(STORAGE_VERSION_KEY);
+ }
+
+ async setVersion({ version }: { version: number }): Promise<void> {
+ const record = await this.getVersionRecord();
+ const inProgress = record?.inProgress;
+ await this.adapter.set(STORAGE_VERSION_KEY, {
+ version,
+ ...(inProgress ? { inProgress } : {}),
+ });
+ }
+
+ async setInProgress({
+ from,
+ to,
+ }: {
+ from: number;
+ to: number;
+ }): Promise<void> {
+ const record = await this.getVersionRecord();
+ const version = record?.version ?? 0;
+ await this.adapter.set(STORAGE_VERSION_KEY, {
+ version,
+ inProgress: { from, to },
+ });
+ }
+
+ async clearInProgress(): Promise<void> {
+ const record = await this.getVersionRecord();
+ const version = record?.version ?? 0;
+ await this.adapter.set(STORAGE_VERSION_KEY, { version });
+ }
+}
diff --git a/apps/web/src/services/storage/opfs-adapter.ts b/apps/web/src/services/storage/opfs-adapter.ts
new file mode 100644
index 00000000..879223b5
--- /dev/null
+++ b/apps/web/src/services/storage/opfs-adapter.ts
@@ -0,0 +1,73 @@
+import type { StorageAdapter } from "./types";
+
+export class OPFSAdapter implements StorageAdapter<File> {
+ private directoryName: string;
+
+ constructor(directoryName = "media") {
+ this.directoryName = directoryName;
+ }
+
+ private async getDirectory(): Promise<FileSystemDirectoryHandle> {
+ const opfsRoot = await navigator.storage.getDirectory();
+ return await opfsRoot.getDirectoryHandle(this.directoryName, {
+ create: true,
+ });
+ }
+
+ async get(key: string): Promise<File | null> {
+ try {
+ const directory = await this.getDirectory();
+ const fileHandle = await directory.getFileHandle(key);
+ return await fileHandle.getFile();
+ } catch (error) {
+ if ((error as Error).name === "NotFoundError") {
+ return null;
+ }
+ throw error;
+ }
+ }
+
+ async set(key: string, file: File): Promise<void> {
+ const directory = await this.getDirectory();
+ const fileHandle = await directory.getFileHandle(key, { create: true });
+ const writable = await fileHandle.createWritable();
+
+ await writable.write(file);
+ await writable.close();
+ }
+
+ async remove(key: string): Promise<void> {
+ try {
+ const directory = await this.getDirectory();
+ await directory.removeEntry(key);
+ } catch (error) {
+ if ((error as Error).name !== "NotFoundError") {
+ throw error;
+ }
+ }
+ }
+
+ async list(): Promise<string[]> {
+ const directory = await this.getDirectory();
+ const keys: string[] = [];
+
+ for await (const name of directory.keys()) {
+ keys.push(name);
+ }
+
+ return keys;
+ }
+
+ async clear(): Promise<void> {
+ const directory = await this.getDirectory();
+
+ for await (const name of directory.keys()) {
+ await directory.removeEntry(name);
+ }
+ }
+
+ // Helper method to check OPFS support
+ static isSupported(): boolean {
+ return "storage" in navigator && "getDirectory" in navigator.storage;
+ }
+}
diff --git a/apps/web/src/services/storage/storage-service.ts b/apps/web/src/services/storage/storage-service.ts
new file mode 100644
index 00000000..cb0b2ced
--- /dev/null
+++ b/apps/web/src/services/storage/storage-service.ts
@@ -0,0 +1,475 @@
+import type { TProject, TProjectMetadata } from "@/types/project";
+import { getProjectDurationFromScenes } from "@/lib/scenes";
+import type { MediaAsset } from "@/types/assets";
+import { IndexedDBAdapter } from "./indexeddb-adapter";
+import { OPFSAdapter } from "./opfs-adapter";
+import type {
+ MediaAssetData,
+ StorageConfig,
+ SerializedProject,
+ SerializedScene,
+} from "./types";
+import type { SavedSoundsData, SavedSound, SoundEffect } from "@/types/sounds";
+import {
+ migrations,
+ runStorageMigrations,
+} from "@/services/storage/migrations";
+import type { TimelineTrack, TScene } from "@/types/timeline";
+
+class StorageService {
+ private projectsAdapter: IndexedDBAdapter<SerializedProject>;
+ private savedSoundsAdapter: IndexedDBAdapter<SavedSoundsData>;
+ private config: StorageConfig;
+ private migrationsPromise: Promise<void> | null = null;
+
+ constructor() {
+ this.config = {
+ projectsDb: "video-editor-projects",
+ mediaDb: "video-editor-media",
+ savedSoundsDb: "video-editor-saved-sounds",
+ version: 1,
+ };
+
+ this.projectsAdapter = new IndexedDBAdapter<SerializedProject>(
+ this.config.projectsDb,
+ "projects",
+ this.config.version,
+ );
+
+ this.savedSoundsAdapter = new IndexedDBAdapter<SavedSoundsData>(
+ this.config.savedSoundsDb,
+ "saved-sounds",
+ this.config.version,
+ );
+ }
+
+ private async ensureMigrations(): Promise<void> {
+ if (this.migrationsPromise) {
+ await this.migrationsPromise;
+ return;
+ }
+
+ this.migrationsPromise = runStorageMigrations({ migrations }).then(
+ () => undefined,
+ );
+ await this.migrationsPromise;
+ }
+
+ private getProjectMediaAdapters({ projectId }: { projectId: string }) {
+ const mediaMetadataAdapter = new IndexedDBAdapter<MediaAssetData>(
+ `${this.config.mediaDb}-${projectId}`,
+ "media-metadata",
+ this.config.version,
+ );
+
+ const mediaAssetsAdapter = new OPFSAdapter(`media-files-${projectId}`);
+
+ return { mediaMetadataAdapter, mediaAssetsAdapter };
+ }
+
+ private stripAudioBuffers({
+ tracks,
+ }: {
+ tracks: TimelineTrack[];
+ }): TimelineTrack[] {
+ return tracks.map((track) => {
+ if (track.type !== "audio") return track;
+ return {
+ ...track,
+ elements: track.elements.map((element) => {
+ const { buffer: _buffer, ...rest } = element;
+ return rest;
+ }),
+ };
+ });
+ }
+
+ async saveProject({ project }: { project: TProject }): Promise<void> {
+ const duration =
+ project.metadata.duration ??
+ getProjectDurationFromScenes({ scenes: project.scenes });
+ const serializedScenes: SerializedScene[] = project.scenes.map((scene) => ({
+ id: scene.id,
+ name: scene.name,
+ isMain: scene.isMain,
+ tracks: this.stripAudioBuffers({ tracks: scene.tracks }),
+ bookmarks: scene.bookmarks,
+ createdAt: scene.createdAt.toISOString(),
+ updatedAt: scene.updatedAt.toISOString(),
+ }));
+
+ const serializedProject: SerializedProject = {
+ metadata: {
+ id: project.metadata.id,
+ name: project.metadata.name,
+ thumbnail: project.metadata.thumbnail,
+ duration,
+ createdAt: project.metadata.createdAt.toISOString(),
+ updatedAt: project.metadata.updatedAt.toISOString(),
+ },
+ scenes: serializedScenes,
+ currentSceneId: project.currentSceneId,
+ settings: project.settings,
+ version: project.version,
+ timelineViewState: project.timelineViewState,
+ };
+
+ await this.projectsAdapter.set(project.metadata.id, serializedProject);
+ }
+
+ async loadProject({
+ id,
+ }: {
+ id: string;
+ }): Promise<{ project: TProject } | null> {
+ await this.ensureMigrations();
+ const serializedProject = await this.projectsAdapter.get(id);
+
+ if (!serializedProject) return null;
+
+ const scenes =
+ serializedProject.scenes?.map((scene) => ({
+ id: scene.id,
+ name: scene.name,
+ isMain: scene.isMain,
+ tracks: (scene.tracks ?? []).map((track) =>
+ track.type === "video"
+ ? { ...track, isMain: track.isMain ?? false } // legacy: isMain was optional
+ : track,
+ ),
+ bookmarks: scene.bookmarks ?? [],
+ createdAt: new Date(scene.createdAt),
+ updatedAt: new Date(scene.updatedAt),
+ })) ?? [];
+
+ const project: TProject = {
+ metadata: {
+ id: serializedProject.metadata.id,
+ name: serializedProject.metadata.name,
+ thumbnail: serializedProject.metadata.thumbnail,
+ duration:
+ serializedProject.metadata.duration ??
+ getProjectDurationFromScenes({ scenes }),
+ createdAt: new Date(serializedProject.metadata.createdAt),
+ updatedAt: new Date(serializedProject.metadata.updatedAt),
+ },
+ scenes,
+ currentSceneId: serializedProject.currentSceneId || "",
+ settings: serializedProject.settings,
+ version: serializedProject.version,
+ timelineViewState: serializedProject.timelineViewState,
+ };
+
+ return { project };
+ }
+
+ async loadAllProjects(): Promise<TProject[]> {
+ const projectIds = await this.projectsAdapter.list();
+ const projects: TProject[] = [];
+
+ for (const id of projectIds) {
+ const result = await this.loadProject({ id });
+ if (result?.project) {
+ projects.push(result.project);
+ }
+ }
+
+ return projects.sort(
+ (a, b) => b.metadata.updatedAt.getTime() - a.metadata.updatedAt.getTime(),
+ );
+ }
+
+ async loadAllProjectsMetadata(): Promise<TProjectMetadata[]> {
+ await this.ensureMigrations();
+ const serializedProjects = await this.projectsAdapter.getAll();
+
+ const metadata = serializedProjects.map((serializedProject) => ({
+ id: serializedProject.metadata.id,
+ name: serializedProject.metadata.name,
+ thumbnail: serializedProject.metadata.thumbnail,
+ duration:
+ serializedProject.metadata.duration ??
+ getProjectDurationFromScenes({
+ scenes: (serializedProject.scenes ?? []) as unknown as TScene[],
+ }),
+ createdAt: new Date(serializedProject.metadata.createdAt),
+ updatedAt: new Date(serializedProject.metadata.updatedAt),
+ }));
+
+ return metadata.sort(
+ (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),
+ );
+ }
+
+ async deleteProject({ id }: { id: string }): Promise<void> {
+ await this.projectsAdapter.remove(id);
+ }
+
+ async saveMediaAsset({
+ projectId,
+ mediaAsset,
+ }: {
+ projectId: string;
+ mediaAsset: MediaAsset;
+ }): Promise<void> {
+ const { mediaMetadataAdapter, mediaAssetsAdapter } =
+ this.getProjectMediaAdapters({ projectId });
+
+ await mediaAssetsAdapter.set(mediaAsset.id, mediaAsset.file);
+
+ const metadata: MediaAssetData = {
+ id: mediaAsset.id,
+ name: mediaAsset.name,
+ type: mediaAsset.type,
+ size: mediaAsset.file.size,
+ lastModified: mediaAsset.file.lastModified,
+ width: mediaAsset.width,
+ height: mediaAsset.height,
+ duration: mediaAsset.duration,
+ thumbnailUrl: mediaAsset.thumbnailUrl,
+ ephemeral: mediaAsset.ephemeral,
+ };
+
+ await mediaMetadataAdapter.set(mediaAsset.id, metadata);
+ }
+
+ async loadMediaAsset({
+ projectId,
+ id,
+ }: {
+ projectId: string;
+ id: string;
+ }): Promise<MediaAsset | null> {
+ const { mediaMetadataAdapter, mediaAssetsAdapter } =
+ this.getProjectMediaAdapters({ projectId });
+
+ const [file, metadata] = await Promise.all([
+ mediaAssetsAdapter.get(id),
+ mediaMetadataAdapter.get(id),
+ ]);
+
+ if (!file || !metadata) return null;
+
+ let url: string;
+ if (metadata.type === "image" && (!file.type || file.type === "")) {
+ try {
+ const text = await file.text();
+ if (text.trim().startsWith("<svg")) {
+ const svgBlob = new Blob([text], { type: "image/svg+xml" });
+ url = URL.createObjectURL(svgBlob);
+ } else {
+ url = URL.createObjectURL(file);
+ }
+ } catch {
+ url = URL.createObjectURL(file);
+ }
+ } else {
+ url = URL.createObjectURL(file);
+ }
+
+ return {
+ id: metadata.id,
+ name: metadata.name,
+ type: metadata.type,
+ file,
+ url,
+ width: metadata.width,
+ height: metadata.height,
+ duration: metadata.duration,
+ thumbnailUrl: metadata.thumbnailUrl,
+ ephemeral: metadata.ephemeral,
+ };
+ }
+
+ async loadAllMediaAssets({
+ projectId,
+ }: {
+ projectId: string;
+ }): Promise<MediaAsset[]> {
+ const { mediaMetadataAdapter } = this.getProjectMediaAdapters({
+ projectId,
+ });
+
+ const mediaIds = await mediaMetadataAdapter.list();
+ const mediaItems: MediaAsset[] = [];
+
+ for (const id of mediaIds) {
+ const item = await this.loadMediaAsset({ projectId, id });
+ if (item) {
+ mediaItems.push(item);
+ }
+ }
+
+ return mediaItems;
+ }
+
+ async deleteMediaAsset({
+ projectId,
+ id,
+ }: {
+ projectId: string;
+ id: string;
+ }): Promise<void> {
+ const { mediaMetadataAdapter, mediaAssetsAdapter } =
+ this.getProjectMediaAdapters({ projectId });
+
+ await Promise.all([
+ mediaAssetsAdapter.remove(id),
+ mediaMetadataAdapter.remove(id),
+ ]);
+ }
+
+ async deleteProjectMedia({
+ projectId,
+ }: {
+ projectId: string;
+ }): Promise<void> {
+ const { mediaMetadataAdapter, mediaAssetsAdapter } =
+ this.getProjectMediaAdapters({ projectId });
+
+ await Promise.all([
+ mediaMetadataAdapter.clear(),
+ mediaAssetsAdapter.clear(),
+ ]);
+ }
+
+ // Utility methods
+ async clearAllData(): Promise<void> {
+ // Clear all projects
+ await this.projectsAdapter.clear();
+
+ // Note: Project-specific media and timelines will be cleaned up when projects are deleted
+ }
+
+ async getStorageInfo(): Promise<{
+ projects: number;
+ isOPFSSupported: boolean;
+ isIndexedDBSupported: boolean;
+ }> {
+ const projectIds = await this.projectsAdapter.list();
+
+ return {
+ projects: projectIds.length,
+ isOPFSSupported: this.isOPFSSupported(),
+ isIndexedDBSupported: this.isIndexedDBSupported(),
+ };
+ }
+
+ async getProjectStorageInfo({ projectId }: { projectId: string }): Promise<{
+ mediaItems: number;
+ }> {
+ const { mediaMetadataAdapter } = this.getProjectMediaAdapters({
+ projectId,
+ });
+
+ const mediaIds = await mediaMetadataAdapter.list();
+
+ return {
+ mediaItems: mediaIds.length,
+ };
+ }
+
+ async loadSavedSounds(): Promise<SavedSoundsData> {
+ try {
+ const savedSoundsData = await this.savedSoundsAdapter.get("user-sounds");
+ return (
+ savedSoundsData || {
+ sounds: [],
+ lastModified: new Date().toISOString(),
+ }
+ );
+ } catch (error) {
+ console.error("Failed to load saved sounds:", error);
+ return { sounds: [], lastModified: new Date().toISOString() };
+ }
+ }
+
+ async saveSoundEffect({
+ soundEffect,
+ }: {
+ soundEffect: SoundEffect;
+ }): Promise<void> {
+ try {
+ const currentData = await this.loadSavedSounds();
+
+ // Check if sound is already saved
+ if (currentData.sounds.some((sound) => sound.id === soundEffect.id)) {
+ return; // Already saved
+ }
+
+ const savedSound: SavedSound = {
+ id: soundEffect.id,
+ name: soundEffect.name,
+ username: soundEffect.username,
+ previewUrl: soundEffect.previewUrl,
+ downloadUrl: soundEffect.downloadUrl,
+ duration: soundEffect.duration,
+ tags: soundEffect.tags,
+ license: soundEffect.license,
+ savedAt: new Date().toISOString(),
+ };
+
+ const updatedData: SavedSoundsData = {
+ sounds: [...currentData.sounds, savedSound],
+ lastModified: new Date().toISOString(),
+ };
+
+ await this.savedSoundsAdapter.set("user-sounds", updatedData);
+ } catch (error) {
+ console.error("Failed to save sound effect:", error);
+ throw error;
+ }
+ }
+
+ async removeSavedSound({ soundId }: { soundId: number }): Promise<void> {
+ try {
+ const currentData = await this.loadSavedSounds();
+
+ const updatedData: SavedSoundsData = {
+ sounds: currentData.sounds.filter((sound) => sound.id !== soundId),
+ lastModified: new Date().toISOString(),
+ };
+
+ await this.savedSoundsAdapter.set("user-sounds", updatedData);
+ } catch (error) {
+ console.error("Failed to remove saved sound:", error);
+ throw error;
+ }
+ }
+
+ async isSoundSaved({ soundId }: { soundId: number }): Promise<boolean> {
+ try {
+ const currentData = await this.loadSavedSounds();
+ return currentData.sounds.some((sound) => sound.id === soundId);
+ } catch (error) {
+ console.error("Failed to check if sound is saved:", error);
+ return false;
+ }
+ }
+
+ async clearSavedSounds(): Promise<void> {
+ try {
+ await this.savedSoundsAdapter.remove("user-sounds");
+ } catch (error) {
+ console.error("Failed to clear saved sounds:", error);
+ throw error;
+ }
+ }
+
+ // Check browser support
+ isOPFSSupported(): boolean {
+ return OPFSAdapter.isSupported();
+ }
+
+ isIndexedDBSupported(): boolean {
+ return "indexedDB" in window;
+ }
+
+ isFullySupported(): boolean {
+ return this.isIndexedDBSupported() && this.isOPFSSupported();
+ }
+}
+
+// Export singleton instance
+export const storageService = new StorageService();
+export { StorageService };
diff --git a/apps/web/src/services/storage/types.ts b/apps/web/src/services/storage/types.ts
new file mode 100644
index 00000000..e7e045f5
--- /dev/null
+++ b/apps/web/src/services/storage/types.ts
@@ -0,0 +1,66 @@
+import type { MediaType } from "@/types/assets";
+import type {
+ TProject,
+ TProjectMetadata,
+ TTimelineViewState,
+} from "@/types/project";
+import type { TScene } from "@/types/timeline";
+
+export interface StorageAdapter<T> {
+ get(key: string): Promise<T | null>;
+ set(key: string, value: T): Promise<void>;
+ remove(key: string): Promise<void>;
+ list(): Promise<string[]>;
+ clear(): Promise<void>;
+}
+
+export interface MediaAssetData {
+ id: string;
+ name: string;
+ type: MediaType;
+ size: number;
+ lastModified: number;
+ width?: number;
+ height?: number;
+ duration?: number;
+ fps?: number;
+ ephemeral?: boolean;
+ thumbnailUrl?: string;
+ sourceStickerIconName?: string;
+}
+
+export type SerializedScene = Omit<TScene, "createdAt" | "updatedAt"> & {
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type SerializedProjectMetadata = Omit<
+ TProjectMetadata,
+ "createdAt" | "updatedAt"
+> & {
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type SerializedProject = Omit<TProject, "metadata" | "scenes"> & {
+ metadata: SerializedProjectMetadata;
+ scenes: SerializedScene[];
+ timelineViewState?: TTimelineViewState;
+};
+
+export interface StorageConfig {
+ projectsDb: string;
+ mediaDb: string;
+ savedSoundsDb: string;
+ version: number;
+}
+
+// TypeScript type augmentation to add async iterator methods to FileSystemDirectoryHandle
+// These methods are part of the File System Access API spec but may not be in all type definitions
+declare global {
+ interface FileSystemDirectoryHandle {
+ keys(): AsyncIterableIterator<string>;
+ values(): AsyncIterableIterator<FileSystemHandle>;
+ entries(): AsyncIterableIterator<[string, FileSystemHandle]>;
+ }
+}
diff --git a/apps/web/src/services/transcription/index.ts b/apps/web/src/services/transcription/index.ts
new file mode 100644
index 00000000..37c650da
--- /dev/null
+++ b/apps/web/src/services/transcription/index.ts
@@ -0,0 +1,186 @@
+import type {
+ TranscriptionResult,
+ TranscriptionProgress,
+ TranscriptionModelId,
+} from "@/types/transcription";
+import {
+ DEFAULT_TRANSCRIPTION_MODEL,
+ TRANSCRIPTION_MODELS,
+} from "@/constants/transcription-constants";
+import type { WorkerMessage, WorkerResponse } from "./worker";
+import type { LanguageCode } from "@/types/language";
+
+type ProgressCallback = (progress: TranscriptionProgress) => void;
+
+class TranscriptionService {
+ private worker: Worker | null = null;
+ private currentModelId: TranscriptionModelId | null = null;
+ private isInitialized = false;
+ private isInitializing = false;
+
+ async transcribe({
+ audioData,
+ language,
+ modelId = DEFAULT_TRANSCRIPTION_MODEL,
+ onProgress,
+ }: {
+ audioData: Float32Array;
+ language?: LanguageCode;
+ modelId?: TranscriptionModelId;
+ onProgress?: ProgressCallback;
+ }): Promise<TranscriptionResult> {
+ await this.ensureWorker({ modelId, onProgress });
+
+ return new Promise((resolve, reject) => {
+ if (!this.worker) {
+ reject(new Error("Worker not initialized"));
+ return;
+ }
+
+ const handleMessage = (event: MessageEvent<WorkerResponse>) => {
+ const response = event.data;
+
+ switch (response.type) {
+ case "transcribe-progress":
+ onProgress?.({
+ status: "transcribing",
+ progress: response.progress,
+ message: "Transcribing audio...",
+ });
+ break;
+
+ case "transcribe-complete":
+ this.worker?.removeEventListener("message", handleMessage);
+ resolve({
+ text: response.text,
+ segments: response.segments,
+ language,
+ });
+ break;
+
+ case "transcribe-error":
+ this.worker?.removeEventListener("message", handleMessage);
+ reject(new Error(response.error));
+ break;
+
+ case "cancelled":
+ this.worker?.removeEventListener("message", handleMessage);
+ reject(new Error("Transcription cancelled"));
+ break;
+ }
+ };
+
+ this.worker.addEventListener("message", handleMessage);
+
+ this.worker.postMessage({
+ type: "transcribe",
+ audio: audioData,
+ language,
+ } satisfies WorkerMessage);
+ });
+ }
+
+ cancel() {
+ this.worker?.postMessage({ type: "cancel" } satisfies WorkerMessage);
+ }
+
+ private async ensureWorker({
+ modelId,
+ onProgress,
+ }: {
+ modelId: TranscriptionModelId;
+ onProgress?: ProgressCallback;
+ }): Promise<void> {
+ const needsNewModel = this.currentModelId !== modelId;
+
+ if (this.worker && this.isInitialized && !needsNewModel) {
+ return;
+ }
+
+ if (this.isInitializing && !needsNewModel) {
+ await this.waitForInit();
+ return;
+ }
+
+ this.terminate();
+ this.isInitializing = true;
+ this.isInitialized = false;
+
+ const model = TRANSCRIPTION_MODELS.find((m) => m.id === modelId);
+ if (!model) {
+ throw new Error(`Unknown model: ${modelId}`);
+ }
+
+ this.worker = new Worker(new URL("./worker.ts", import.meta.url), {
+ type: "module",
+ });
+
+ return new Promise((resolve, reject) => {
+ if (!this.worker) {
+ reject(new Error("Failed to create worker"));
+ return;
+ }
+
+ const handleMessage = (event: MessageEvent<WorkerResponse>) => {
+ const response = event.data;
+
+ switch (response.type) {
+ case "init-progress":
+ onProgress?.({
+ status: "loading-model",
+ progress: response.progress,
+ message: `Loading ${model.name} model...`,
+ });
+ break;
+
+ case "init-complete":
+ this.worker?.removeEventListener("message", handleMessage);
+ this.isInitialized = true;
+ this.isInitializing = false;
+ this.currentModelId = modelId;
+ resolve();
+ break;
+
+ case "init-error":
+ this.worker?.removeEventListener("message", handleMessage);
+ this.isInitializing = false;
+ this.terminate();
+ reject(new Error(response.error));
+ break;
+ }
+ };
+
+ this.worker.addEventListener("message", handleMessage);
+
+ this.worker.postMessage({
+ type: "init",
+ modelId: model.huggingFaceId,
+ } satisfies WorkerMessage);
+ });
+ }
+
+ private waitForInit(): Promise<void> {
+ return new Promise((resolve) => {
+ const checkInit = () => {
+ if (this.isInitialized) {
+ resolve();
+ } else if (!this.isInitializing) {
+ resolve();
+ } else {
+ setTimeout(checkInit, 100);
+ }
+ };
+ checkInit();
+ });
+ }
+
+ terminate() {
+ this.worker?.terminate();
+ this.worker = null;
+ this.isInitialized = false;
+ this.isInitializing = false;
+ this.currentModelId = null;
+ }
+}
+
+export const transcriptionService = new TranscriptionService();
diff --git a/apps/web/src/services/transcription/worker.ts b/apps/web/src/services/transcription/worker.ts
new file mode 100644
index 00000000..2aa9673c
--- /dev/null
+++ b/apps/web/src/services/transcription/worker.ts
@@ -0,0 +1,176 @@
+import {
+ pipeline,
+ type AutomaticSpeechRecognitionPipeline,
+ type AutomaticSpeechRecognitionOutput,
+} from "@huggingface/transformers";
+import type { TranscriptionSegment } from "@/types/transcription";
+import {
+ DEFAULT_CHUNK_LENGTH_SECONDS,
+ DEFAULT_STRIDE_SECONDS,
+} from "@/constants/transcription-constants";
+
+export type WorkerMessage =
+ | { type: "init"; modelId: string }
+ | { type: "transcribe"; audio: Float32Array; language: string }
+ | { type: "cancel" };
+
+export type WorkerResponse =
+ | { type: "init-progress"; progress: number }
+ | { type: "init-complete" }
+ | { type: "init-error"; error: string }
+ | { type: "transcribe-progress"; progress: number }
+ | {
+ type: "transcribe-complete";
+ text: string;
+ segments: TranscriptionSegment[];
+ }
+ | { type: "transcribe-error"; error: string }
+ | { type: "cancelled" };
+
+let transcriber: AutomaticSpeechRecognitionPipeline | null = null;
+let cancelled = false;
+let lastReportedProgress = -1;
+const fileBytes = new Map<string, { loaded: number; total: number }>();
+
+self.onmessage = async (event: MessageEvent<WorkerMessage>) => {
+ const message = event.data;
+
+ switch (message.type) {
+ case "init":
+ await handleInit({ modelId: message.modelId });
+ break;
+ case "transcribe":
+ await handleTranscribe({
+ audio: message.audio,
+ language: message.language,
+ });
+ break;
+ case "cancel":
+ cancelled = true;
+ self.postMessage({ type: "cancelled" } satisfies WorkerResponse);
+ break;
+ }
+};
+
+async function handleInit({ modelId }: { modelId: string }) {
+ lastReportedProgress = -1;
+ fileBytes.clear();
+
+ try {
+ transcriber = (await pipeline("automatic-speech-recognition", modelId, {
+ dtype: "q4",
+ device: "auto",
+ progress_callback: (progressInfo: {
+ status?: string;
+ file?: string;
+ loaded?: number;
+ total?: number;
+ }) => {
+ const file = progressInfo.file;
+ if (!file) return;
+
+ const loaded = progressInfo.loaded ?? 0;
+ const total = progressInfo.total ?? 0;
+
+ if (progressInfo.status === "progress" && total > 0) {
+ fileBytes.set(file, { loaded, total });
+ } else if (progressInfo.status === "done") {
+ const existing = fileBytes.get(file);
+ if (existing) {
+ fileBytes.set(file, {
+ loaded: existing.total,
+ total: existing.total,
+ });
+ }
+ }
+
+ // sum all bytes
+ let totalLoaded = 0;
+ let totalSize = 0;
+ for (const { loaded, total } of fileBytes.values()) {
+ totalLoaded += loaded;
+ totalSize += total;
+ }
+
+ if (totalSize === 0) return;
+
+ const overallProgress = (totalLoaded / totalSize) * 100;
+ const roundedProgress = Math.floor(overallProgress);
+
+ if (roundedProgress !== lastReportedProgress) {
+ lastReportedProgress = roundedProgress;
+ self.postMessage({
+ type: "init-progress",
+ progress: roundedProgress,
+ } satisfies WorkerResponse);
+ }
+ },
+ })) as unknown as AutomaticSpeechRecognitionPipeline;
+
+ self.postMessage({ type: "init-complete" } satisfies WorkerResponse);
+ } catch (error) {
+ self.postMessage({
+ type: "init-error",
+ error: error instanceof Error ? error.message : "Failed to load model",
+ } satisfies WorkerResponse);
+ }
+}
+
+async function handleTranscribe({
+ audio,
+ language,
+}: {
+ audio: Float32Array;
+ language: string;
+}) {
+ if (!transcriber) {
+ self.postMessage({
+ type: "transcribe-error",
+ error: "Model not initialized",
+ } satisfies WorkerResponse);
+ return;
+ }
+
+ cancelled = false;
+
+ try {
+ const rawResult = await transcriber(audio, {
+ chunk_length_s: DEFAULT_CHUNK_LENGTH_SECONDS,
+ stride_length_s: DEFAULT_STRIDE_SECONDS,
+ language: language === "auto" ? undefined : language,
+ return_timestamps: true,
+ });
+
+ if (cancelled) return;
+
+ const result: AutomaticSpeechRecognitionOutput = Array.isArray(rawResult)
+ ? rawResult[0]
+ : rawResult;
+
+ const segments: TranscriptionSegment[] = [];
+
+ if (result.chunks) {
+ for (const chunk of result.chunks) {
+ if (chunk.timestamp && chunk.timestamp.length >= 2) {
+ segments.push({
+ text: chunk.text,
+ start: chunk.timestamp[0] ?? 0,
+ end: chunk.timestamp[1] ?? chunk.timestamp[0] ?? 0,
+ });
+ }
+ }
+ }
+
+ self.postMessage({
+ type: "transcribe-complete",
+ text: result.text,
+ segments,
+ } satisfies WorkerResponse);
+ } catch (error) {
+ if (cancelled) return;
+ self.postMessage({
+ type: "transcribe-error",
+ error: error instanceof Error ? error.message : "Transcription failed",
+ } satisfies WorkerResponse);
+ }
+}
diff --git a/apps/web/src/stores/assets-panel-store.tsx b/apps/web/src/stores/assets-panel-store.tsx
new file mode 100644
index 00000000..21ceb6a3
--- /dev/null
+++ b/apps/web/src/stores/assets-panel-store.tsx
@@ -0,0 +1,107 @@
+import type { ElementType } from "react";
+import { create } from "zustand";
+import {
+ ArrowRightDoubleIcon,
+ ClosedCaptionIcon,
+ Folder03Icon,
+ Happy01Icon,
+ HeadphonesIcon,
+ MagicWand05Icon,
+ TextIcon,
+ Settings01Icon,
+ SlidersHorizontalIcon,
+ ColorsIcon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react";
+
+export const TAB_KEYS = [
+ "media",
+ "sounds",
+ "text",
+ "stickers",
+ "effects",
+ "transitions",
+ "captions",
+ "filters",
+ "adjustment",
+ "settings",
+] as const;
+
+export type Tab = (typeof TAB_KEYS)[number];
+
+const createHugeiconsIcon =
+ ({ icon }: { icon: IconSvgElement }) =>
+ ({ className }: { className?: string }) => (
+ <HugeiconsIcon icon={icon} className={className} />
+ );
+
+export const tabs = {
+ media: {
+ icon: createHugeiconsIcon({ icon: Folder03Icon }),
+ label: "Media",
+ },
+ sounds: {
+ icon: createHugeiconsIcon({ icon: HeadphonesIcon }),
+ label: "Sounds",
+ },
+ text: {
+ icon: createHugeiconsIcon({ icon: TextIcon }),
+ label: "Text",
+ },
+ stickers: {
+ icon: createHugeiconsIcon({ icon: Happy01Icon }),
+ label: "Stickers",
+ },
+ effects: {
+ icon: createHugeiconsIcon({ icon: MagicWand05Icon }),
+ label: "Effects",
+ },
+ transitions: {
+ icon: createHugeiconsIcon({ icon: ArrowRightDoubleIcon }),
+ label: "Transitions",
+ },
+ captions: {
+ icon: createHugeiconsIcon({ icon: ClosedCaptionIcon }),
+ label: "Captions",
+ },
+ filters: {
+ icon: createHugeiconsIcon({ icon: ColorsIcon }),
+ label: "Filters",
+ },
+ adjustment: {
+ icon: createHugeiconsIcon({ icon: SlidersHorizontalIcon }),
+ label: "Adjustment",
+ },
+ settings: {
+ icon: createHugeiconsIcon({ icon: Settings01Icon }),
+ label: "Settings",
+ },
+} satisfies Record<
+ Tab,
+ { icon: ElementType<{ className?: string }>; label: string }
+>;
+
+type MediaViewMode = "grid" | "list";
+
+interface AssetsPanelStore {
+ activeTab: Tab;
+ setActiveTab: (tab: Tab) => void;
+ highlightMediaId: string | null;
+ requestRevealMedia: (mediaId: string) => void;
+ clearHighlight: () => void;
+
+ /* Media */
+ mediaViewMode: MediaViewMode;
+ setMediaViewMode: (mode: MediaViewMode) => void;
+}
+
+export const useAssetsPanelStore = create<AssetsPanelStore>((set) => ({
+ activeTab: "media",
+ setActiveTab: (tab) => set({ activeTab: tab }),
+ highlightMediaId: null,
+ requestRevealMedia: (mediaId) =>
+ set({ activeTab: "media", highlightMediaId: mediaId }),
+ clearHighlight: () => set({ highlightMediaId: null }),
+ mediaViewMode: "grid",
+ setMediaViewMode: (mode) => set({ mediaViewMode: mode }),
+}));
diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/stores/editor-store.ts
index 9923aa87..5c4856d3 100644
--- a/apps/web/src/stores/editor-store.ts
+++ b/apps/web/src/stores/editor-store.ts
@@ -1,91 +1,70 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
-import { CanvasPreset } from "@/types/editor";
-
-export type PlatformLayout = "tiktok";
-
-export const PLATFORM_LAYOUTS: Record<PlatformLayout, string> = {
- tiktok: "TikTok",
-};
+import type { TPlatformLayout } from "@/types/editor";
+import { DEFAULT_CANVAS_PRESETS } from "@/constants/project-constants";
+import type { TCanvasSize } from "@/types/project";
interface LayoutGuideSettings {
- platform: PlatformLayout | null;
+ platform: TPlatformLayout | null;
}
interface EditorState {
- // Loading states
- isInitializing: boolean;
- isPanelsReady: boolean;
-
- // Editor UI settings
- canvasPresets: CanvasPreset[];
- layoutGuide: LayoutGuideSettings;
-
- // Actions
- setInitializing: (loading: boolean) => void;
- setPanelsReady: (ready: boolean) => void;
- initializeApp: () => Promise<void>;
- setLayoutGuide: (settings: Partial<LayoutGuideSettings>) => void;
- toggleLayoutGuide: (platform: PlatformLayout) => void;
+ isInitializing: boolean;
+ isPanelsReady: boolean;
+ canvasPresets: TCanvasSize[];
+ layoutGuide: LayoutGuideSettings;
+ setInitializing: (loading: boolean) => void;
+ setPanelsReady: (ready: boolean) => void;
+ initializeApp: () => Promise<void>;
+ setLayoutGuide: (settings: Partial<LayoutGuideSettings>) => void;
+ toggleLayoutGuide: (platform: TPlatformLayout) => void;
}
-const DEFAULT_CANVAS_PRESETS: CanvasPreset[] = [
- { name: "16:9", width: 1920, height: 1080 },
- { name: "9:16", width: 1080, height: 1920 },
- { name: "1:1", width: 1080, height: 1080 },
- { name: "4:3", width: 1440, height: 1080 },
-];
-
export const useEditorStore = create<EditorState>()(
- persist(
- (set) => ({
- // Initial states
- isInitializing: true,
- isPanelsReady: false,
- canvasPresets: DEFAULT_CANVAS_PRESETS,
- layoutGuide: {
- platform: null,
- },
-
- // Actions
- setInitializing: (loading) => {
- set({ isInitializing: loading });
- },
+ persist(
+ (set) => ({
+ isInitializing: true,
+ isPanelsReady: false,
+ canvasPresets: DEFAULT_CANVAS_PRESETS,
+ layoutGuide: {
+ platform: null,
+ },
+ setInitializing: (loading) => {
+ set({ isInitializing: loading });
+ },
- setPanelsReady: (ready) => {
- set({ isPanelsReady: ready });
- },
+ setPanelsReady: (ready) => {
+ set({ isPanelsReady: ready });
+ },
- initializeApp: async () => {
- console.log("Initializing video editor...");
- set({ isInitializing: true, isPanelsReady: false });
+ initializeApp: async () => {
+ set({ isInitializing: true, isPanelsReady: false });
- set({ isPanelsReady: true, isInitializing: false });
- console.log("Video editor ready");
- },
+ set({ isPanelsReady: true, isInitializing: false });
+ },
- setLayoutGuide: (settings) => {
- set((state) => ({
- layoutGuide: {
- ...state.layoutGuide,
- ...settings,
- },
- }));
- },
+ setLayoutGuide: (settings) => {
+ set((state) => ({
+ layoutGuide: {
+ ...state.layoutGuide,
+ ...settings,
+ },
+ }));
+ },
- toggleLayoutGuide: (platform) => {
- set((state) => ({
- layoutGuide: {
- platform: state.layoutGuide.platform === platform ? null : platform,
- },
- }));
- },
- }),
- {
- name: "editor-settings",
- partialize: (state) => ({
- layoutGuide: state.layoutGuide,
- }),
- }
- )
+ toggleLayoutGuide: (platform) => {
+ set((state) => ({
+ layoutGuide: {
+ platform: state.layoutGuide.platform === platform ? null : platform,
+ },
+ }));
+ },
+ }),
+ {
+ name: "editor-settings",
+ partialize: (state) => ({
+ layoutGuide: state.layoutGuide,
+ }),
+ },
+ ),
);
diff --git a/apps/web/src/stores/keybindings-store.ts b/apps/web/src/stores/keybindings-store.ts
index febcaa30..5ff48ffc 100644
--- a/apps/web/src/stores/keybindings-store.ts
+++ b/apps/web/src/stores/keybindings-store.ts
@@ -2,264 +2,255 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
-import { ActionWithOptionalArgs } from "@/constants/actions";
-import { isAppleDevice, isDOMElement, isTypableElement } from "@/lib/utils";
-import { KeybindingConfig, ShortcutKey } from "@/types/keybinding";
+import type { TActionWithOptionalArgs } from "@/lib/actions";
+import { getDefaultShortcuts } from "@/lib/actions";
+import { isTypableDOMElement } from "@/utils/browser";
+import { isAppleDevice } from "@/utils/platform";
+import type { KeybindingConfig, ShortcutKey } from "@/types/keybinding";
+import {
+ runMigrations,
+ CURRENT_VERSION,
+} from "./keybindings/migrations";
-// Default keybindings configuration
-export const defaultKeybindings: KeybindingConfig = {
- space: "toggle-play",
- j: "seek-backward",
- k: "toggle-play",
- l: "seek-forward",
- left: "frame-step-backward",
- right: "frame-step-forward",
- "shift+left": "jump-backward",
- "shift+right": "jump-forward",
- home: "goto-start",
- enter: "goto-start",
- end: "goto-end",
- s: "split-element",
- n: "toggle-snapping",
- "ctrl+a": "select-all",
- "ctrl+d": "duplicate-selected",
- "ctrl+c": "copy-selected",
- "ctrl+v": "paste-selected",
- "ctrl+z": "undo",
- "ctrl+shift+z": "redo",
- "ctrl+y": "redo",
- delete: "delete-selected",
- backspace: "delete-selected",
-};
+export const defaultKeybindings: KeybindingConfig = getDefaultShortcuts();
export interface KeybindingConflict {
- key: ShortcutKey;
- existingAction: ActionWithOptionalArgs;
- newAction: ActionWithOptionalArgs;
+ key: ShortcutKey;
+ existingAction: TActionWithOptionalArgs;
+ newAction: TActionWithOptionalArgs;
}
interface KeybindingsState {
- keybindings: KeybindingConfig;
- isCustomized: boolean;
- keybindingsEnabled: boolean;
- isRecording: boolean;
+ keybindings: KeybindingConfig;
+ isCustomized: boolean;
+ keybindingsEnabled: boolean;
+ isRecording: boolean;
- // Actions
- updateKeybinding: (key: ShortcutKey, action: ActionWithOptionalArgs) => void;
- removeKeybinding: (key: ShortcutKey) => void;
- resetToDefaults: () => void;
- importKeybindings: (config: KeybindingConfig) => void;
- exportKeybindings: () => KeybindingConfig;
- enableKeybindings: () => void;
- disableKeybindings: () => void;
- setIsRecording: (isRecording: boolean) => void;
-
- // Validation
- validateKeybinding: (
- key: ShortcutKey,
- action: ActionWithOptionalArgs
- ) => KeybindingConflict | null;
- getKeybindingsForAction: (action: ActionWithOptionalArgs) => ShortcutKey[];
+ updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => void;
+ removeKeybinding: (key: ShortcutKey) => void;
+ resetToDefaults: () => void;
+ importKeybindings: (config: KeybindingConfig) => void;
+ exportKeybindings: () => KeybindingConfig;
+ enableKeybindings: () => void;
+ disableKeybindings: () => void;
+ setIsRecording: (isRecording: boolean) => void;
+ validateKeybinding: (
+ key: ShortcutKey,
+ action: TActionWithOptionalArgs,
+ ) => KeybindingConflict | null;
+ getKeybindingsForAction: (action: TActionWithOptionalArgs) => ShortcutKey[];
+ getKeybindingString: (ev: KeyboardEvent) => ShortcutKey | null;
+}
- // Utility
- getKeybindingString: (ev: KeyboardEvent) => ShortcutKey | null;
+function isDOMElement(el: EventTarget | null): el is HTMLElement {
+ return !!el && (el instanceof Element || el instanceof HTMLElement);
}
export const useKeybindingsStore = create<KeybindingsState>()(
- persist(
- (set, get) => ({
- keybindings: { ...defaultKeybindings },
- isCustomized: false,
- keybindingsEnabled: true,
- isRecording: false,
+ persist(
+ (set, get) => ({
+ keybindings: { ...defaultKeybindings },
+ isCustomized: false,
+ keybindingsEnabled: true,
+ isRecording: false,
- updateKeybinding: (key: ShortcutKey, action: ActionWithOptionalArgs) => {
- set((state) => {
- const newKeybindings = { ...state.keybindings };
- newKeybindings[key] = action;
+ updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => {
+ set((state) => {
+ const newKeybindings = { ...state.keybindings };
+ newKeybindings[key] = action;
- return {
- keybindings: newKeybindings,
- isCustomized: true,
- };
- });
- },
+ return {
+ keybindings: newKeybindings,
+ isCustomized: true,
+ };
+ });
+ },
- removeKeybinding: (key: ShortcutKey) => {
- set((state) => {
- const newKeybindings = { ...state.keybindings };
- delete newKeybindings[key];
+ removeKeybinding: (key: ShortcutKey) => {
+ set((state) => {
+ const newKeybindings = { ...state.keybindings };
+ delete newKeybindings[key];
- return {
- keybindings: newKeybindings,
- isCustomized: true,
- };
- });
- },
+ return {
+ keybindings: newKeybindings,
+ isCustomized: true,
+ };
+ });
+ },
- resetToDefaults: () => {
- set({
- keybindings: { ...defaultKeybindings },
- isCustomized: false,
- });
- },
+ resetToDefaults: () => {
+ set({
+ keybindings: { ...defaultKeybindings },
+ isCustomized: false,
+ });
+ },
- enableKeybindings: () => {
- set({ keybindingsEnabled: true });
- },
+ enableKeybindings: () => {
+ set({ keybindingsEnabled: true });
+ },
- disableKeybindings: () => {
- set({ keybindingsEnabled: false });
- },
+ disableKeybindings: () => {
+ set({ keybindingsEnabled: false });
+ },
- importKeybindings: (config: KeybindingConfig) => {
- // Validate all keys and actions
- for (const [key, action] of Object.entries(config)) {
- // Validate the key format
- if (typeof key !== "string" || key.length === 0) {
- throw new Error(`Invalid key format: ${key}`);
- }
- }
- set({
- keybindings: { ...config },
- isCustomized: true,
- });
- },
+ importKeybindings: (config: KeybindingConfig) => {
+ // Validate all keys and actions
+ for (const [key] of Object.entries(config)) {
+ // Validate the key format
+ if (typeof key !== "string" || key.length === 0) {
+ throw new Error(`Invalid key format: ${key}`);
+ }
+ }
+ set({
+ keybindings: { ...config },
+ isCustomized: true,
+ });
+ },
- exportKeybindings: () => {
- return get().keybindings;
- },
+ exportKeybindings: () => {
+ return get().keybindings;
+ },
- validateKeybinding: (
- key: ShortcutKey,
- action: ActionWithOptionalArgs
- ) => {
- const { keybindings } = get();
- const existingAction = keybindings[key];
+ validateKeybinding: (
+ key: ShortcutKey,
+ action: TActionWithOptionalArgs,
+ ) => {
+ const { keybindings } = get();
+ const existingAction = keybindings[key];
- if (existingAction && existingAction !== action) {
- return {
- key,
- existingAction,
- newAction: action,
- };
- }
+ if (existingAction && existingAction !== action) {
+ return {
+ key,
+ existingAction,
+ newAction: action,
+ };
+ }
- return null;
- },
- setIsRecording: (isRecording: boolean) => {
- set({ isRecording });
- },
+ return null;
+ },
+ setIsRecording: (isRecording: boolean) => {
+ set({ isRecording });
+ },
- getKeybindingsForAction: (action: ActionWithOptionalArgs) => {
- const { keybindings } = get();
- return Object.keys(keybindings).filter(
- (key) => keybindings[key as ShortcutKey] === action
- ) as ShortcutKey[];
- },
+ getKeybindingsForAction: (action: TActionWithOptionalArgs) => {
+ const { keybindings } = get();
+ return Object.keys(keybindings).filter(
+ (key) => keybindings[key as ShortcutKey] === action,
+ ) as ShortcutKey[];
+ },
- getKeybindingString: (ev: KeyboardEvent) => {
- return generateKeybindingString(ev) as ShortcutKey | null;
- },
- }),
- {
- name: "opencut-keybindings",
- version: 2,
- }
- )
+ getKeybindingString: (ev: KeyboardEvent) => {
+ return generateKeybindingString(ev) as ShortcutKey | null;
+ },
+ }),
+ {
+ name: "opencut-keybindings",
+ version: CURRENT_VERSION,
+ partialize: (state) => ({
+ keybindings: state.keybindings,
+ isCustomized: state.isCustomized,
+ }),
+ migrate: (persisted, version) =>
+ runMigrations({ state: persisted, fromVersion: version }),
+ },
+ ),
);
// Utility functions
function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
- const target = ev.target;
+ const target = ev.target;
- // We may or may not have a modifier key
- const modifierKey = getActiveModifier(ev);
+ // We may or may not have a modifier key
+ const modifierKey = getActiveModifier(ev);
- // We will always have a non-modifier key
- const key = getPressedKey(ev);
- if (!key) return null;
+ // We will always have a non-modifier key
+ const key = getPressedKey(ev);
+ if (!key) return null;
- // All key combos backed by modifiers are valid shortcuts (whether currently typing or not)
- if (modifierKey) {
- // If the modifier is shift and the target is an input, we ignore
- if (
- modifierKey === "shift" &&
- isDOMElement(target) &&
- isTypableElement(target)
- ) {
- return null;
- }
+ // All key combos backed by modifiers are valid shortcuts (whether currently typing or not)
+ if (modifierKey) {
+ // If the modifier is shift and the target is an input, we ignore
+ if (
+ modifierKey === "shift" &&
+ isDOMElement(target) &&
+ isTypableDOMElement({ element: target as HTMLElement })
+ ) {
+ return null;
+ }
- return `${modifierKey}+${key}` as ShortcutKey;
- }
+ return `${modifierKey}+${key}` as ShortcutKey;
+ }
- // no modifier key here then we do not do anything while on input
- if (isDOMElement(target) && isTypableElement(target)) return null;
+ // no modifier key here then we do not do anything while on input
+ if (
+ isDOMElement(target) &&
+ isTypableDOMElement({ element: target as HTMLElement })
+ )
+ return null;
- // single key while not input
- return `${key}` as ShortcutKey;
+ // single key while not input
+ return `${key}` as ShortcutKey;
}
function getPressedKey(ev: KeyboardEvent): string | null {
- // Sometimes the property code is not available on the KeyboardEvent object
- const key = (ev.key ?? "").toLowerCase();
- const code = ev.code ?? "";
+ // Sometimes the property code is not available on the KeyboardEvent object
+ const key = (ev.key ?? "").toLowerCase();
+ const code = ev.code ?? "";
- if (code === "Space" || key === " " || key === "spacebar" || key === "space")
- return "space";
+ if (code === "Space" || key === " " || key === "spacebar" || key === "space")
+ return "space";
- // Check arrow keys
- if (key.startsWith("arrow")) {
- return key.slice(5);
- }
+ // Check arrow keys
+ if (key.startsWith("arrow")) {
+ return key.slice(5);
+ }
- // Check for special keys
- if (key === "tab") return "tab";
- if (key === "home") return "home";
- if (key === "end") return "end";
- if (key === "delete") return "delete";
- if (key === "backspace") return "backspace";
+ // Check for special keys
+ if (key === "tab") return "tab";
+ if (key === "home") return "home";
+ if (key === "end") return "end";
+ if (key === "delete") return "delete";
+ if (key === "backspace") return "backspace";
- // Check letter keys
- if (code.startsWith("Key")) {
- const letter = code.slice(3).toLowerCase();
- if (letter.length === 1 && letter >= "a" && letter <= "z") {
- return letter;
- }
- }
+ // Check letter keys
+ if (code.startsWith("Key")) {
+ const letter = code.slice(3).toLowerCase();
+ if (letter.length === 1 && letter >= "a" && letter <= "z") {
+ return letter;
+ }
+ }
- // Check number keys using physical position for AZERTY support
- if (code.startsWith("Digit")) {
- const digit = code.slice(5);
- if (digit.length === 1 && digit >= "0" && digit <= "9") {
- return digit;
- }
- }
+ // Check number keys using physical position for AZERTY support
+ if (code.startsWith("Digit")) {
+ const digit = code.slice(5);
+ if (digit.length === 1 && digit >= "0" && digit <= "9") {
+ return digit;
+ }
+ }
- // Fallback for other layouts
- const isDigit = key.length === 1 && key >= "0" && key <= "9";
- if (isDigit) return key;
+ // Fallback for other layouts
+ const isDigit = key.length === 1 && key >= "0" && key <= "9";
+ if (isDigit) return key;
- // Check if slash, period or enter
- if (key === "/" || key === "." || key === "enter") return key;
+ // Check if slash, period or enter
+ if (key === "/" || key === "." || key === "enter") return key;
- // If no other cases match, this is not a valid key
- return null;
+ // If no other cases match, this is not a valid key
+ return null;
}
function getActiveModifier(ev: KeyboardEvent): string | null {
- const modifierKeys = {
- ctrl: isAppleDevice() ? ev.metaKey : ev.ctrlKey,
- alt: ev.altKey,
- shift: ev.shiftKey,
- };
+ const modifierKeys = {
+ ctrl: isAppleDevice() ? ev.metaKey : ev.ctrlKey,
+ alt: ev.altKey,
+ shift: ev.shiftKey,
+ };
- // active modifier: ctrl | alt | ctrl+alt | ctrl+shift | ctrl+alt+shift | alt+shift
- // modiferKeys object's keys are sorted to match the above order
- const activeModifier = Object.keys(modifierKeys)
- .filter((key) => modifierKeys[key as keyof typeof modifierKeys])
- .join("+");
+ // active modifier: ctrl | alt | ctrl+alt | ctrl+shift | ctrl+alt+shift | alt+shift
+ // modiferKeys object's keys are sorted to match the above order
+ const activeModifier = Object.keys(modifierKeys)
+ .filter((key) => modifierKeys[key as keyof typeof modifierKeys])
+ .join("+");
- return activeModifier === "" ? null : activeModifier;
+ return activeModifier === "" ? null : activeModifier;
}
diff --git a/apps/web/src/stores/keybindings/migrations/index.ts b/apps/web/src/stores/keybindings/migrations/index.ts
new file mode 100644
index 00000000..1689802e
--- /dev/null
+++ b/apps/web/src/stores/keybindings/migrations/index.ts
@@ -0,0 +1,28 @@
+import { v2ToV3 } from './v2-to-v3';
+
+type MigrationFn = ({ state }: { state: unknown }) => unknown;
+
+/**
+ * key = version we're migrating from
+ * value = migration function
+ */
+const migrations: Record<number, MigrationFn> = {
+ 2: v2ToV3,
+};
+
+export const CURRENT_VERSION = 3;
+
+export function runMigrations({
+ state,
+ fromVersion,
+}: {
+ state: unknown;
+ fromVersion: number;
+}): unknown {
+ let current = state;
+ for (let version = fromVersion; version < CURRENT_VERSION; version++) {
+ const migrate = migrations[version];
+ if (migrate) current = migrate({ state: current });
+ }
+ return current;
+}
diff --git a/apps/web/src/stores/keybindings/migrations/v2-to-v3.ts b/apps/web/src/stores/keybindings/migrations/v2-to-v3.ts
new file mode 100644
index 00000000..1517b52b
--- /dev/null
+++ b/apps/web/src/stores/keybindings/migrations/v2-to-v3.ts
@@ -0,0 +1,26 @@
+import type { KeybindingConfig, ShortcutKey } from "@/types/keybinding";
+import type { TActionWithOptionalArgs } from "@/lib/actions";
+
+interface V2State {
+ keybindings: KeybindingConfig;
+ isCustomized: boolean;
+}
+
+export function v2ToV3({ state }: { state: unknown }): unknown {
+ const v2 = state as V2State;
+
+ const renames: Record<string, string> = {
+ "split-selected": "split",
+ "split-selected-left": "split-left",
+ "split-selected-right": "split-right",
+ };
+
+ const migrated = { ...v2.keybindings };
+ for (const [key, action] of Object.entries(migrated)) {
+ if (action && renames[action]) {
+ migrated[key as ShortcutKey] = renames[action] as TActionWithOptionalArgs;
+ }
+ }
+
+ return { ...v2, keybindings: migrated };
+}
diff --git a/apps/web/src/stores/media-store.ts b/apps/web/src/stores/media-store.ts
deleted file mode 100644
index fcbd6575..00000000
--- a/apps/web/src/stores/media-store.ts
+++ /dev/null
@@ -1,296 +0,0 @@
-import { create } from "zustand";
-import { storageService } from "@/lib/storage/storage-service";
-import { useTimelineStore } from "./timeline-store";
-import { generateUUID } from "@/lib/utils";
-import { MediaType, MediaFile } from "@/types/media";
-import { videoCache } from "@/lib/video-cache";
-
-interface MediaStore {
- mediaFiles: MediaFile[];
- isLoading: boolean;
-
- // Actions
- addMediaFile: (
- projectId: string,
- file: Omit<MediaFile, "id">
- ) => Promise<void>;
- removeMediaFile: (projectId: string, id: string) => Promise<void>;
- loadProjectMedia: (projectId: string) => Promise<void>;
- clearProjectMedia: (projectId: string) => Promise<void>;
- clearAllMedia: () => void;
-}
-
-// Helper function to determine file type
-export const getFileType = (file: File): MediaType | null => {
- const { type } = file;
-
- if (type.startsWith("image/")) {
- return "image";
- }
- if (type.startsWith("video/")) {
- return "video";
- }
- if (type.startsWith("audio/")) {
- return "audio";
- }
-
- return null;
-};
-
-// Helper function to get image dimensions
-export const getImageDimensions = (
- file: File
-): Promise<{ width: number; height: number }> => {
- return new Promise((resolve, reject) => {
- const img = new window.Image();
-
- img.addEventListener("load", () => {
- const width = img.naturalWidth;
- const height = img.naturalHeight;
- resolve({ width, height });
- img.remove();
- });
-
- img.addEventListener("error", () => {
- reject(new Error("Could not load image"));
- img.remove();
- });
-
- img.src = URL.createObjectURL(file);
- });
-};
-
-// Helper function to generate video thumbnail and get dimensions
-export const generateVideoThumbnail = (
- file: File
-): Promise<{ thumbnailUrl: string; width: number; height: number }> => {
- return new Promise((resolve, reject) => {
- const video = document.createElement("video") as HTMLVideoElement;
- const canvas = document.createElement("canvas") as HTMLCanvasElement;
- const ctx = canvas.getContext("2d");
-
- if (!ctx) {
- reject(new Error("Could not get canvas context"));
- return;
- }
-
- video.addEventListener("loadedmetadata", () => {
- canvas.width = video.videoWidth;
- canvas.height = video.videoHeight;
-
- // Seek to 1 second or 10% of duration, whichever is smaller
- video.currentTime = Math.min(1, video.duration * 0.1);
- });
-
- video.addEventListener("seeked", () => {
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
- const thumbnailUrl = canvas.toDataURL("image/jpeg", 0.8);
- const width = video.videoWidth;
- const height = video.videoHeight;
-
- resolve({ thumbnailUrl, width, height });
-
- // Cleanup
- video.remove();
- canvas.remove();
- });
-
- video.addEventListener("error", () => {
- reject(new Error("Could not load video"));
- video.remove();
- canvas.remove();
- });
-
- video.src = URL.createObjectURL(file);
- video.load();
- });
-};
-
-// Helper function to get media duration
-export const getMediaDuration = (file: File): Promise<number> => {
- return new Promise((resolve, reject) => {
- const element = document.createElement(
- file.type.startsWith("video/") ? "video" : "audio"
- ) as HTMLVideoElement;
-
- element.addEventListener("loadedmetadata", () => {
- resolve(element.duration);
- element.remove();
- });
-
- element.addEventListener("error", () => {
- reject(new Error("Could not load media"));
- element.remove();
- });
-
- element.src = URL.createObjectURL(file);
- element.load();
- });
-};
-
-export const getMediaAspectRatio = (item: MediaFile): number => {
- if (item.width && item.height) {
- return item.width / item.height;
- }
- return 16 / 9; // Default aspect ratio
-};
-
-export const useMediaStore = create<MediaStore>((set, get) => ({
- mediaFiles: [],
- isLoading: false,
-
- addMediaFile: async (projectId, file) => {
- const newItem: MediaFile = {
- ...file,
- id: generateUUID(),
- };
-
- // Add to local state immediately for UI responsiveness
- set((state) => ({
- mediaFiles: [...state.mediaFiles, newItem],
- }));
-
- // Save to persistent storage in background
- try {
- await storageService.saveMediaFile({ projectId, mediaItem: newItem });
- } catch (error) {
- console.error("Failed to save media item:", error);
- // Remove from local state if save failed
- set((state) => ({
- mediaFiles: state.mediaFiles.filter((media) => media.id !== newItem.id),
- }));
- }
- },
-
- removeMediaFile: async (projectId: string, id: string) => {
- const state = get();
- const item = state.mediaFiles.find((media) => media.id === id);
-
- videoCache.clearVideo(id);
-
- // Cleanup object URLs to prevent memory leaks
- if (item?.url) {
- URL.revokeObjectURL(item.url);
- if (item.thumbnailUrl) {
- URL.revokeObjectURL(item.thumbnailUrl);
- }
- }
-
- // 1) Remove from local state immediately
- set((state) => ({
- mediaFiles: state.mediaFiles.filter((media) => media.id !== id),
- }));
-
- // 2) Cascade into the timeline: remove any elements using this media ID
- const timeline = useTimelineStore.getState();
- const { tracks, deleteSelected, setSelectedElements } = timeline;
-
- // Find all elements that reference this media
- const elementsToRemove: Array<{ trackId: string; elementId: string }> = [];
- for (const track of tracks) {
- for (const el of track.elements) {
- if (el.type === "media" && el.mediaId === id) {
- elementsToRemove.push({ trackId: track.id, elementId: el.id });
- }
- }
- }
-
- // If there are elements to remove, use unified delete function
- if (elementsToRemove.length > 0) {
- setSelectedElements(elementsToRemove);
- deleteSelected();
- }
-
- // 3) Remove from persistent storage
- try {
- await storageService.deleteMediaFile({ projectId, id });
- } catch (error) {
- console.error("Failed to delete media item:", error);
- }
- },
-
- loadProjectMedia: async (projectId) => {
- set({ isLoading: true });
-
- try {
- const mediaItems = await storageService.loadAllMediaFiles({ projectId });
-
- // Regenerate thumbnails for video items
- const updatedMediaItems = await Promise.all(
- mediaItems.map(async (item) => {
- if (item.type === "video" && item.file) {
- try {
- const { thumbnailUrl, width, height } =
- await generateVideoThumbnail(item.file);
- return {
- ...item,
- thumbnailUrl,
- width: width || item.width,
- height: height || item.height,
- };
- } catch (error) {
- console.error(
- `Failed to regenerate thumbnail for video ${item.id}:`,
- error
- );
- return item;
- }
- }
- return item;
- })
- );
-
- set({ mediaFiles: updatedMediaItems });
- } catch (error) {
- console.error("Failed to load media items:", error);
- } finally {
- set({ isLoading: false });
- }
- },
-
- clearProjectMedia: async (projectId) => {
- const state = get();
-
- // Cleanup all object URLs
- state.mediaFiles.forEach((item) => {
- if (item.url) {
- URL.revokeObjectURL(item.url);
- }
- if (item.thumbnailUrl) {
- URL.revokeObjectURL(item.thumbnailUrl);
- }
- });
-
- // Clear local state
- set({ mediaFiles: [] });
-
- // Clear persistent storage
- try {
- const mediaIds = state.mediaFiles.map((item) => item.id);
- await Promise.all(
- mediaIds.map((id) => storageService.deleteMediaFile({ projectId, id }))
- );
- } catch (error) {
- console.error("Failed to clear media items from storage:", error);
- }
- },
-
- clearAllMedia: () => {
- const state = get();
-
- videoCache.clearAll();
-
- // Cleanup all object URLs
- state.mediaFiles.forEach((item) => {
- if (item.url) {
- URL.revokeObjectURL(item.url);
- }
- if (item.thumbnailUrl) {
- URL.revokeObjectURL(item.thumbnailUrl);
- }
- });
-
- // Clear local state
- set({ mediaFiles: [] });
- },
-}));
diff --git a/apps/web/src/stores/panel-store.ts b/apps/web/src/stores/panel-store.ts
index ae09c13b..5d2621a4 100644
--- a/apps/web/src/stores/panel-store.ts
+++ b/apps/web/src/stores/panel-store.ts
@@ -1,225 +1,93 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
+import { PANEL_CONFIG } from "@/constants/editor-constants";
-export type PanelPreset =
- | "default"
- | "media"
- | "inspector"
- | "vertical-preview";
-
-interface PanelSizes {
- toolsPanel: number;
- previewPanel: number;
- propertiesPanel: number;
- mainContent: number;
- timeline: number;
+export interface PanelSizes {
+ tools: number;
+ preview: number;
+ properties: number;
+ mainContent: number;
+ timeline: number;
}
-export const PRESET_CONFIGS: Record<PanelPreset, PanelSizes> = {
- default: {
- toolsPanel: 25,
- previewPanel: 50,
- propertiesPanel: 25,
- mainContent: 70,
- timeline: 30,
- },
- media: {
- toolsPanel: 30,
- previewPanel: 45,
- propertiesPanel: 25,
- mainContent: 100,
- timeline: 25,
- },
- inspector: {
- toolsPanel: 30,
- previewPanel: 70,
- propertiesPanel: 30,
- mainContent: 75,
- timeline: 25,
- },
- "vertical-preview": {
- toolsPanel: 30,
- previewPanel: 40,
- propertiesPanel: 30,
- mainContent: 75,
- timeline: 25,
- },
-};
-
-interface PanelState extends PanelSizes {
- activePreset: PanelPreset;
- presetCustomSizes: Record<PanelPreset, Partial<PanelSizes>>;
- resetCounter: number;
-
- mediaViewMode: "grid" | "list";
-
- setToolsPanel: (size: number) => void;
- setPreviewPanel: (size: number) => void;
- setPropertiesPanel: (size: number) => void;
- setMainContent: (size: number) => void;
- setTimeline: (size: number) => void;
- setMediaViewMode: (mode: "grid" | "list") => void;
+export type PanelId = keyof PanelSizes;
- setActivePreset: (preset: PanelPreset) => void;
- resetPreset: (preset: PanelPreset) => void;
- getCurrentPresetSizes: () => PanelSizes;
+interface PanelState {
+ panels: PanelSizes;
+ setPanel: (panel: PanelId, size: number) => void;
+ setPanels: (sizes: Partial<PanelSizes>) => void;
+ resetPanels: () => void;
}
export const usePanelStore = create<PanelState>()(
- persist(
- (set, get) => ({
- ...PRESET_CONFIGS.default,
- activePreset: "default" as PanelPreset,
- presetCustomSizes: {
- default: {},
- media: {},
- inspector: {},
- "vertical-preview": {},
- },
- resetCounter: 0,
-
- mediaViewMode: "grid" as const,
-
- setToolsPanel: (size) => {
- const { activePreset, presetCustomSizes } = get();
- set({
- toolsPanel: size,
- presetCustomSizes: {
- ...presetCustomSizes,
- [activePreset]: {
- ...presetCustomSizes[activePreset],
- toolsPanel: size,
- },
- },
- });
- },
- setPreviewPanel: (size) => {
- const { activePreset, presetCustomSizes } = get();
- set({
- previewPanel: size,
- presetCustomSizes: {
- ...presetCustomSizes,
- [activePreset]: {
- ...presetCustomSizes[activePreset],
- previewPanel: size,
- },
- },
- });
- },
- setPropertiesPanel: (size) => {
- const { activePreset, presetCustomSizes } = get();
- set({
- propertiesPanel: size,
- presetCustomSizes: {
- ...presetCustomSizes,
- [activePreset]: {
- ...presetCustomSizes[activePreset],
- propertiesPanel: size,
- },
- },
- });
- },
- setMainContent: (size) => {
- const { activePreset, presetCustomSizes } = get();
- set({
- mainContent: size,
- presetCustomSizes: {
- ...presetCustomSizes,
- [activePreset]: {
- ...presetCustomSizes[activePreset],
- mainContent: size,
- },
- },
- });
- },
- setTimeline: (size) => {
- const { activePreset, presetCustomSizes } = get();
- set({
- timeline: size,
- presetCustomSizes: {
- ...presetCustomSizes,
- [activePreset]: {
- ...presetCustomSizes[activePreset],
- timeline: size,
- },
- },
- });
- },
- setMediaViewMode: (mode) => set({ mediaViewMode: mode }),
-
- setActivePreset: (preset) => {
- const {
- activePreset: currentPreset,
- presetCustomSizes,
- toolsPanel,
- previewPanel,
- propertiesPanel,
- mainContent,
- timeline,
- } = get();
-
- const updatedPresetCustomSizes = {
- ...presetCustomSizes,
- [currentPreset]: {
- toolsPanel,
- previewPanel,
- propertiesPanel,
- mainContent,
- timeline,
- },
- };
-
- const defaultSizes = PRESET_CONFIGS[preset];
- const customSizes = updatedPresetCustomSizes[preset] || {};
- const finalSizes = { ...defaultSizes, ...customSizes } as PanelSizes;
-
- set({
- activePreset: preset,
- presetCustomSizes: updatedPresetCustomSizes,
- ...finalSizes,
- });
- },
-
- resetPreset: (preset) => {
- const { presetCustomSizes, activePreset, resetCounter } = get();
- const defaultSizes = PRESET_CONFIGS[preset];
-
- const newPresetCustomSizes = {
- ...presetCustomSizes,
- [preset]: {},
- };
-
- const updates: Partial<PanelState> = {
- presetCustomSizes: newPresetCustomSizes,
- resetCounter: resetCounter + 1,
- };
+ persist(
+ (set) => ({
+ ...PANEL_CONFIG,
+ setPanel: (panel, size) =>
+ set((state) => ({
+ panels: {
+ ...state.panels,
+ [panel]: size,
+ },
+ })),
+ setPanels: (sizes) =>
+ set((state) => ({
+ panels: {
+ ...state.panels,
+ ...sizes,
+ },
+ })),
+ resetPanels: () => set({ ...PANEL_CONFIG }),
+ }),
+ {
+ name: "panel-sizes",
+ version: 2,
+ migrate: (persistedState) => {
+ const state = persistedState as
+ | {
+ panels?: Partial<PanelSizes> | null;
+ toolsPanel?: number;
+ previewPanel?: number;
+ propertiesPanel?: number;
+ mainContent?: number;
+ timeline?: number;
+ tools?: number;
+ preview?: number;
+ properties?: number;
+ }
+ | undefined
+ | null;
- if (preset === activePreset) {
- Object.assign(updates, defaultSizes);
- }
+ if (!state) return { panels: { ...PANEL_CONFIG.panels } };
- set(updates);
- },
+ if (state.panels && typeof state.panels === "object") {
+ return {
+ panels: {
+ ...PANEL_CONFIG.panels,
+ ...state.panels,
+ },
+ };
+ }
- getCurrentPresetSizes: () => {
- const {
- toolsPanel,
- previewPanel,
- propertiesPanel,
- mainContent,
- timeline,
- } = get();
- return {
- toolsPanel,
- previewPanel,
- propertiesPanel,
- mainContent,
- timeline,
- };
- },
- }),
- {
- name: "panel-sizes",
- }
- )
+ return {
+ panels: {
+ tools: state.tools ?? state.toolsPanel ?? PANEL_CONFIG.panels.tools,
+ preview:
+ state.preview ??
+ state.previewPanel ??
+ PANEL_CONFIG.panels.preview,
+ properties:
+ state.properties ??
+ state.propertiesPanel ??
+ PANEL_CONFIG.panels.properties,
+ mainContent: state.mainContent ?? PANEL_CONFIG.panels.mainContent,
+ timeline: state.timeline ?? PANEL_CONFIG.panels.timeline,
+ },
+ };
+ },
+ partialize: (state) => ({
+ panels: state.panels,
+ }),
+ },
+ ),
);
diff --git a/apps/web/src/stores/playback-store.ts b/apps/web/src/stores/playback-store.ts
deleted file mode 100644
index c2b9cf4a..00000000
--- a/apps/web/src/stores/playback-store.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-import { create } from "zustand";
-import type { PlaybackState, PlaybackControls } from "@/types/playback";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { DEFAULT_FPS, useProjectStore } from "./project-store";
-
-interface PlaybackStore extends PlaybackState, PlaybackControls {
- setDuration: (duration: number) => void;
- setCurrentTime: (time: number) => void;
-}
-
-let playbackTimer: number | null = null;
-
-const startTimer = (store: () => PlaybackStore) => {
- if (playbackTimer) cancelAnimationFrame(playbackTimer);
-
- // Use requestAnimationFrame for smoother updates
- const updateTime = () => {
- const state = store();
- if (state.isPlaying && state.currentTime < state.duration) {
- const now = performance.now();
- const delta = (now - lastUpdate) / 1000; // Convert to seconds
- lastUpdate = now;
-
- const newTime = state.currentTime + delta * state.speed;
-
- // Get actual content duration from timeline store
- const actualContentDuration = useTimelineStore
- .getState()
- .getTotalDuration();
-
- // Stop at actual content end, not timeline duration (which has 10s minimum)
- // It was either this or reducing default min timeline to 1 second
- const effectiveDuration =
- actualContentDuration > 0 ? actualContentDuration : state.duration;
-
- if (newTime >= effectiveDuration) {
- // When content completes, pause just before the end so we can see the last frame
- const projectFps = useProjectStore.getState().activeProject?.fps;
- if (!projectFps)
- console.error("Project FPS is not set, assuming " + DEFAULT_FPS + "fps");
-
- const frameOffset = 1 / (projectFps ?? DEFAULT_FPS); // Stop 1 frame before end based on project FPS
- const stopTime = Math.max(0, effectiveDuration - frameOffset);
-
- state.pause();
- state.setCurrentTime(stopTime);
- // Notify video elements to sync with end position
- window.dispatchEvent(
- new CustomEvent("playback-seek", {
- detail: { time: stopTime },
- })
- );
- } else {
- state.setCurrentTime(newTime);
- // Notify video elements to sync
- window.dispatchEvent(
- new CustomEvent("playback-update", { detail: { time: newTime } })
- );
- }
- }
- playbackTimer = requestAnimationFrame(updateTime);
- };
-
- let lastUpdate = performance.now();
- playbackTimer = requestAnimationFrame(updateTime);
-};
-
-const stopTimer = () => {
- if (playbackTimer) {
- cancelAnimationFrame(playbackTimer);
- playbackTimer = null;
- }
-};
-
-export const usePlaybackStore = create<PlaybackStore>((set, get) => ({
- isPlaying: false,
- currentTime: 0,
- duration: 0,
- volume: 1,
- muted: false,
- previousVolume: 1,
- speed: 1.0,
-
- play: () => {
- const state = get();
-
- const actualContentDuration = useTimelineStore
- .getState()
- .getTotalDuration();
- const effectiveDuration =
- actualContentDuration > 0 ? actualContentDuration : state.duration;
-
- if (effectiveDuration > 0) {
- const fps = useProjectStore.getState().activeProject?.fps ?? DEFAULT_FPS;
- const frameOffset = 1 / fps;
- const endThreshold = Math.max(0, effectiveDuration - frameOffset);
-
- if (state.currentTime >= endThreshold) {
- get().seek(0);
- }
- }
-
- set({ isPlaying: true });
- startTimer(get);
- },
-
- pause: () => {
- set({ isPlaying: false });
- stopTimer();
- },
-
- toggle: () => {
- const { isPlaying } = get();
- if (isPlaying) {
- get().pause();
- } else {
- get().play();
- }
- },
-
- seek: (time: number) => {
- const { duration } = get();
- const clampedTime = Math.max(0, Math.min(duration, time));
- set({ currentTime: clampedTime });
-
- const event = new CustomEvent("playback-seek", {
- detail: { time: clampedTime },
- });
- window.dispatchEvent(event);
- },
-
- setVolume: (volume: number) =>
- set((state) => ({
- volume: Math.max(0, Math.min(1, volume)),
- muted: volume === 0,
- previousVolume: volume > 0 ? volume : state.previousVolume,
- })),
-
- setSpeed: (speed: number) => {
- const newSpeed = Math.max(0.1, Math.min(2.0, speed));
- set({ speed: newSpeed });
-
- const event = new CustomEvent("playback-speed", {
- detail: { speed: newSpeed },
- });
- window.dispatchEvent(event);
- },
-
- setDuration: (duration: number) => set({ duration }),
- setCurrentTime: (time: number) => set({ currentTime: time }),
-
- mute: () => {
- const { volume, previousVolume } = get();
- set({
- muted: true,
- previousVolume: volume > 0 ? volume : previousVolume,
- volume: 0,
- });
- },
-
- unmute: () => {
- const { previousVolume } = get();
- set({ muted: false, volume: previousVolume ?? 1 });
- },
-
- toggleMute: () => {
- const { muted } = get();
- if (muted) {
- get().unmute();
- } else {
- get().mute();
- }
- },
-}));
diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts
deleted file mode 100644
index c58f7309..00000000
--- a/apps/web/src/stores/project-store.ts
+++ /dev/null
@@ -1,571 +0,0 @@
-import { TProject, BlurIntensity, Scene } from "@/types/project";
-import { create } from "zustand";
-import { storageService } from "@/lib/storage/storage-service";
-import { toast } from "sonner";
-import { useMediaStore } from "./media-store";
-import { useTimelineStore } from "./timeline-store";
-import { useSceneStore } from "./scene-store";
-import { generateUUID } from "@/lib/utils";
-import { CanvasSize, CanvasMode } from "@/types/editor";
-
-export const DEFAULT_CANVAS_SIZE: CanvasSize = { width: 1920, height: 1080 };
-export const DEFAULT_FPS = 30;
-
-export function createMainScene(): Scene {
- return {
- id: generateUUID(),
- name: "Main Scene",
- isMain: true,
- createdAt: new Date(),
- updatedAt: new Date(),
- };
-}
-
-const createDefaultProject = (name: string): TProject => {
- const mainScene = createMainScene();
-
- return {
- id: generateUUID(),
- name,
- thumbnail: "",
- createdAt: new Date(),
- updatedAt: new Date(),
- scenes: [mainScene],
- currentSceneId: mainScene.id,
- backgroundColor: "#000000",
- backgroundType: "color",
- blurIntensity: 8,
- bookmarks: [],
- fps: DEFAULT_FPS,
- canvasSize: DEFAULT_CANVAS_SIZE,
- canvasMode: "preset",
- };
-};
-
-interface ProjectStore {
- activeProject: TProject | null;
- savedProjects: TProject[];
- isLoading: boolean;
- isInitialized: boolean;
- invalidProjectIds?: Set<string>;
-
- // Actions
- createNewProject: (name: string) => Promise<string>;
- loadProject: (id: string) => Promise<void>;
- saveCurrentProject: () => Promise<void>;
- loadAllProjects: () => Promise<void>;
- deleteProject: (id: string) => Promise<void>;
- closeProject: () => void;
- renameProject: (projectId: string, name: string) => Promise<void>;
- duplicateProject: (projectId: string) => Promise<string>;
- updateProjectBackground: (backgroundColor: string) => Promise<void>;
- updateBackgroundType: (
- type: "color" | "blur",
- options?: { backgroundColor?: string; blurIntensity?: BlurIntensity }
- ) => Promise<void>;
- updateProjectFps: (fps: number) => Promise<void>;
- updateCanvasSize: (size: CanvasSize, mode: CanvasMode) => Promise<void>;
-
- // Bookmark methods
- toggleBookmark: (time: number) => Promise<void>;
- isBookmarked: (time: number) => boolean;
- removeBookmark: (time: number) => Promise<void>;
-
- getFilteredAndSortedProjects: (
- searchQuery: string,
- sortOption: string
- ) => TProject[];
-
- // Global invalid project ID tracking
- isInvalidProjectId: (id: string) => boolean;
- markProjectIdAsInvalid: (id: string) => void;
- clearInvalidProjectIds: () => void;
-}
-
-export const useProjectStore = create<ProjectStore>((set, get) => ({
- activeProject: null,
- savedProjects: [],
- isLoading: true,
- isInitialized: false,
- invalidProjectIds: new Set<string>(),
-
- // Implementation of bookmark methods
- toggleBookmark: async (time: number) => {
- const { activeProject } = get();
- if (!activeProject) return;
-
- // Round time to the nearest frame
- const fps = activeProject.fps || DEFAULT_FPS;
- const frameTime = Math.round(time * fps) / fps;
-
- const bookmarks = activeProject.bookmarks || [];
- let updatedBookmarks: number[];
-
- // Check if already bookmarked
- const bookmarkIndex = bookmarks.findIndex(
- (bookmark) => Math.abs(bookmark - frameTime) < 0.001
- );
-
- if (bookmarkIndex !== -1) {
- // Remove bookmark
- updatedBookmarks = bookmarks.filter((_, i) => i !== bookmarkIndex);
- } else {
- // Add bookmark
- updatedBookmarks = [...bookmarks, frameTime].sort((a, b) => a - b);
- }
-
- const updatedProject = {
- ...activeProject,
- bookmarks: updatedBookmarks,
- updatedAt: new Date(),
- };
-
- try {
- await storageService.saveProject({ project: updatedProject });
- set({ activeProject: updatedProject });
- await get().loadAllProjects(); // Refresh the list
- } catch (error) {
- console.error("Failed to update project bookmarks:", error);
- toast.error("Failed to update bookmarks", {
- description: "Please try again",
- });
- }
- },
-
- isBookmarked: (time: number) => {
- const { activeProject } = get();
- if (!activeProject || !activeProject.bookmarks) return false;
-
- // Round time to the nearest frame
- const fps = activeProject.fps || DEFAULT_FPS;
- const frameTime = Math.round(time * fps) / fps;
-
- return activeProject.bookmarks.some(
- (bookmark) => Math.abs(bookmark - frameTime) < 0.001
- );
- },
-
- removeBookmark: async (time: number) => {
- const { activeProject } = get();
- if (!activeProject || !activeProject.bookmarks) return;
-
- // Round time to the nearest frame
- const fps = activeProject.fps || DEFAULT_FPS;
- const frameTime = Math.round(time * fps) / fps;
-
- const updatedBookmarks = activeProject.bookmarks.filter(
- (bookmark) => Math.abs(bookmark - frameTime) >= 0.001
- );
-
- if (updatedBookmarks.length === activeProject.bookmarks.length) {
- // No bookmark found to remove
- return;
- }
-
- const updatedProject = {
- ...activeProject,
- bookmarks: updatedBookmarks,
- updatedAt: new Date(),
- };
-
- try {
- await storageService.saveProject({ project: updatedProject });
- set({ activeProject: updatedProject });
- await get().loadAllProjects(); // Refresh the list
- } catch (error) {
- console.error("Failed to update project bookmarks:", error);
- toast.error("Failed to remove bookmark", {
- description: "Please try again",
- });
- }
- },
-
- createNewProject: async (name: string) => {
- const newProject = createDefaultProject(name);
-
- set({ activeProject: newProject });
-
- const mediaStore = useMediaStore.getState();
- const timelineStore = useTimelineStore.getState();
- const sceneStore = useSceneStore.getState();
-
- mediaStore.clearAllMedia();
- timelineStore.clearTimeline();
-
- sceneStore.initializeScenes({
- scenes: newProject.scenes,
- currentSceneId: newProject.currentSceneId,
- });
-
- try {
- await storageService.saveProject({ project: newProject });
- // Reload all projects to update the list
- await get().loadAllProjects();
- return newProject.id;
- } catch (error) {
- toast.error("Failed to save new project");
- throw error;
- }
- },
-
- loadProject: async (id: string) => {
- if (!get().isInitialized) {
- set({ isLoading: true });
- }
-
- // Prevent flicker when switching projects - clear all stores
- const mediaStore = useMediaStore.getState();
- const timelineStore = useTimelineStore.getState();
- const sceneStore = useSceneStore.getState();
-
- mediaStore.clearAllMedia();
- timelineStore.clearTimeline();
- sceneStore.clearScenes();
-
- try {
- const project = await storageService.loadProject({ id });
- if (project) {
- set({ activeProject: project });
-
- let currentScene = null;
- if (project.scenes && project.scenes.length > 0) {
- sceneStore.initializeScenes({
- scenes: project.scenes,
- currentSceneId: project.currentSceneId,
- });
- // Get current scene directly from project data (don't rely on store state)
- currentScene =
- project.scenes.find((s) => s.id === project.currentSceneId) ||
- project.scenes.find((s) => s.isMain) ||
- project.scenes[0];
- }
-
- await Promise.all([
- mediaStore.loadProjectMedia(id),
- timelineStore.loadProjectTimeline({
- projectId: id,
- sceneId: currentScene?.id,
- }),
- ]);
- } else {
- throw new Error(`Project with id ${id} not found`);
- }
- } catch (error) {
- console.error("Failed to load project:", error);
- throw error; // Re-throw so the editor page can handle it
- } finally {
- set({ isLoading: false });
- }
- },
-
- saveCurrentProject: async () => {
- const { activeProject } = get();
- if (!activeProject) return;
-
- try {
- const timelineStore = useTimelineStore.getState();
- const sceneStore = useSceneStore.getState();
- const currentScene = sceneStore.currentScene;
-
- await Promise.all([
- storageService.saveProject({ project: activeProject }),
- timelineStore.saveProjectTimeline({
- projectId: activeProject.id,
- sceneId: currentScene?.id,
- }),
- ]);
- await get().loadAllProjects(); // Refresh the list
- } catch (error) {
- console.error("Failed to save project:", error);
- }
- },
-
- loadAllProjects: async () => {
- if (!get().isInitialized) {
- set({ isLoading: true });
- }
-
- try {
- const projects = await storageService.loadAllProjects();
- set({ savedProjects: projects });
- } catch (error) {
- console.error("Failed to load projects:", error);
- } finally {
- set({ isLoading: false, isInitialized: true });
- }
- },
-
- deleteProject: async (id: string) => {
- try {
- await Promise.all([
- storageService.deleteProjectMedia({ projectId: id }),
- storageService.deleteProjectTimeline({ projectId: id }),
- storageService.deleteProject({ id }),
- ]);
- await get().loadAllProjects(); // Refresh the list
-
- // If deleted active project, close it and clear data
- const { activeProject } = get();
- if (activeProject?.id === id) {
- set({ activeProject: null });
- const mediaStore = useMediaStore.getState();
- const timelineStore = useTimelineStore.getState();
- const sceneStore = useSceneStore.getState();
-
- mediaStore.clearAllMedia();
- timelineStore.clearTimeline();
- sceneStore.clearScenes();
- }
- } catch (error) {
- console.error("Failed to delete project:", error);
- }
- },
-
- closeProject: () => {
- set({ activeProject: null });
-
- const mediaStore = useMediaStore.getState();
- const timelineStore = useTimelineStore.getState();
- const sceneStore = useSceneStore.getState();
-
- mediaStore.clearAllMedia();
- timelineStore.clearTimeline();
- sceneStore.clearScenes();
- },
-
- renameProject: async (id: string, name: string) => {
- const { savedProjects } = get();
-
- // Find the project to rename
- const projectToRename = savedProjects.find((p) => p.id === id);
- if (!projectToRename) {
- toast.error("Project not found", {
- description: "Please try again",
- });
- return;
- }
-
- const updatedProject = {
- ...projectToRename,
- name,
- updatedAt: new Date(),
- };
-
- try {
- await storageService.saveProject({ project: updatedProject });
-
- await get().loadAllProjects();
-
- // Update activeProject if same project
- const { activeProject } = get();
- if (activeProject?.id === id) {
- set({ activeProject: updatedProject });
- }
- } catch (error) {
- console.error("Failed to rename project:", error);
- toast.error("Failed to rename project", {
- description:
- error instanceof Error ? error.message : "Please try again",
- });
- }
- },
-
- duplicateProject: async (projectId: string) => {
- try {
- const project = await storageService.loadProject({ id: projectId });
- if (!project) {
- toast.error("Project not found", {
- description: "Please try again",
- });
- throw new Error("Project not found");
- }
-
- const { savedProjects } = get();
-
- // Extract the base name (remove any existing numbering)
- const numberMatch = project.name.match(/^\((\d+)\)\s+(.+)$/);
- const baseName = numberMatch ? numberMatch[2] : project.name;
- const existingNumbers: number[] = [];
-
- // Check for pattern "(number) baseName" in existing projects
- savedProjects.forEach((p) => {
- const match = p.name.match(/^\((\d+)\)\s+(.+)$/);
- if (match && match[2] === baseName) {
- existingNumbers.push(parseInt(match[1], 10));
- }
- });
-
- const nextNumber =
- existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1;
-
- const newProject: TProject = {
- ...project,
- id: generateUUID(),
- name: `(${nextNumber}) ${baseName}`,
- createdAt: new Date(),
- updatedAt: new Date(),
- };
-
- await storageService.saveProject({ project: newProject });
- await get().loadAllProjects();
- return newProject.id;
- } catch (error) {
- console.error("Failed to duplicate project:", error);
- toast.error("Failed to duplicate project", {
- description:
- error instanceof Error ? error.message : "Please try again",
- });
- throw error;
- }
- },
-
- updateProjectBackground: async (backgroundColor: string) => {
- const { activeProject } = get();
- if (!activeProject) return;
-
- const updatedProject = {
- ...activeProject,
- backgroundColor,
- updatedAt: new Date(),
- };
-
- try {
- await storageService.saveProject({ project: updatedProject });
- set({ activeProject: updatedProject });
- await get().loadAllProjects();
- } catch (error) {
- console.error("Failed to update project background:", error);
- toast.error("Failed to update background", {
- description: "Please try again",
- });
- }
- },
-
- updateBackgroundType: async (
- type: "color" | "blur",
- options?: { backgroundColor?: string; blurIntensity?: BlurIntensity }
- ) => {
- const { activeProject } = get();
- if (!activeProject) return;
-
- const updatedProject = {
- ...activeProject,
- backgroundType: type,
- ...(options?.backgroundColor && {
- backgroundColor: options.backgroundColor,
- }),
- ...(options?.blurIntensity !== undefined && {
- blurIntensity: options.blurIntensity,
- }),
- updatedAt: new Date(),
- };
-
- try {
- await storageService.saveProject({ project: updatedProject });
- set({ activeProject: updatedProject });
- await get().loadAllProjects();
- } catch (error) {
- console.error("Failed to update background type:", error);
- toast.error("Failed to update background", {
- description: "Please try again",
- });
- }
- },
-
- updateProjectFps: async (fps: number) => {
- const { activeProject } = get();
- if (!activeProject) return;
-
- const updatedProject = {
- ...activeProject,
- fps,
- updatedAt: new Date(),
- };
-
- try {
- await storageService.saveProject({ project: updatedProject });
- set({ activeProject: updatedProject });
- await get().loadAllProjects();
- } catch (error) {
- console.error("Failed to update project FPS:", error);
- toast.error("Failed to update project FPS", {
- description: "Please try again",
- });
- }
- },
-
- updateCanvasSize: async (size: CanvasSize, mode: CanvasMode) => {
- const { activeProject } = get();
- if (!activeProject) return;
-
- const updatedProject = {
- ...activeProject,
- canvasSize: size,
- canvasMode: mode,
- updatedAt: new Date(),
- };
-
- try {
- await storageService.saveProject({ project: updatedProject });
- set({ activeProject: updatedProject });
- await get().loadAllProjects();
- } catch (error) {
- console.error("Failed to update canvas size:", error);
- toast.error("Failed to update canvas size", {
- description: "Please try again",
- });
- }
- },
-
- getFilteredAndSortedProjects: (searchQuery: string, sortOption: string) => {
- const { savedProjects } = get();
-
- const filteredProjects = savedProjects.filter((project) =>
- project.name.toLowerCase().includes(searchQuery.toLowerCase())
- );
-
- const sortedProjects = [...filteredProjects].sort((a, b) => {
- const [key, order] = sortOption.split("-");
-
- if (key !== "createdAt" && key !== "name") {
- console.warn(`Invalid sort key: ${key}`);
- return 0;
- }
-
- const aValue = a[key];
- const bValue = b[key];
-
- if (aValue === undefined || bValue === undefined) return 0;
-
- if (order === "asc") {
- if (aValue < bValue) return -1;
- if (aValue > bValue) return 1;
- return 0;
- }
- if (aValue > bValue) return -1;
- if (aValue < bValue) return 1;
- return 0;
- });
-
- return sortedProjects;
- },
-
- // Global invalid project ID tracking
- isInvalidProjectId: (id: string) => {
- const invalidIds = get().invalidProjectIds || new Set();
- return invalidIds.has(id);
- },
-
- markProjectIdAsInvalid: (id: string) => {
- set((state) => ({
- invalidProjectIds: new Set([
- ...(state.invalidProjectIds || new Set()),
- id,
- ]),
- }));
- },
-
- clearInvalidProjectIds: () => {
- set({ invalidProjectIds: new Set() });
- },
-}));
diff --git a/apps/web/src/stores/scene-store.ts b/apps/web/src/stores/scene-store.ts
deleted file mode 100644
index a9f65a49..00000000
--- a/apps/web/src/stores/scene-store.ts
+++ /dev/null
@@ -1,318 +0,0 @@
-import { create } from "zustand";
-import { Scene } from "@/types/project";
-import { useProjectStore } from "./project-store";
-import { useTimelineStore } from "./timeline-store";
-import { storageService } from "@/lib/storage/storage-service";
-import { generateUUID } from "@/lib/utils";
-
-export function getMainScene({ scenes }: { scenes: Scene[] }): Scene | null {
- return scenes.find((scene) => scene.isMain) || null;
-}
-
-function ensureMainScene(scenes: Scene[]): Scene[] {
- const hasMain = scenes.some((scene) => scene.isMain);
- if (!hasMain) {
- const mainScene: Scene = {
- id: generateUUID(),
- name: "Main scene",
- isMain: true,
- createdAt: new Date(),
- updatedAt: new Date(),
- };
- return [mainScene, ...scenes];
- }
- return scenes;
-}
-
-interface SceneStore {
- // Current scene state
- currentScene: Scene | null;
- scenes: Scene[];
-
- // Scene management
- createScene: ({
- name,
- isMain,
- }: {
- name: string;
- isMain: boolean;
- }) => Promise<string>;
- deleteScene: ({ sceneId }: { sceneId: string }) => Promise<void>;
- renameScene: ({
- sceneId,
- name,
- }: {
- sceneId: string;
- name: string;
- }) => Promise<void>;
- switchToScene: ({ sceneId }: { sceneId: string }) => Promise<void>;
-
- // Scene utilities
- getMainScene: () => Scene | null;
- getCurrentScene: () => Scene | null;
-
- // Project integration
- loadProjectScenes: ({ projectId }: { projectId: string }) => Promise<void>;
- initializeScenes: ({
- scenes,
- currentSceneId,
- }: {
- scenes: Scene[];
- currentSceneId?: string;
- }) => void;
- clearScenes: () => void;
-}
-
-export const useSceneStore = create<SceneStore>((set, get) => ({
- currentScene: null,
- scenes: [],
-
- createScene: async ({ name, isMain = false }) => {
- const { scenes } = get();
-
- const newScene = {
- id: generateUUID(),
- name,
- isMain,
- isBackground: false,
- createdAt: new Date(),
- updatedAt: new Date(),
- };
- const updatedScenes = [...scenes, newScene];
-
- const projectStore = useProjectStore.getState();
- const { activeProject } = projectStore;
-
- if (!activeProject) {
- throw new Error("No active project");
- }
-
- const updatedProject = {
- ...activeProject,
- scenes: updatedScenes,
- updatedAt: new Date(),
- };
-
- try {
- await storageService.saveProject({ project: updatedProject });
- useProjectStore.setState({ activeProject: updatedProject });
- set({ scenes: updatedScenes });
- return newScene.id;
- } catch (error) {
- console.error("Failed to create scene:", error);
- throw error;
- }
- },
-
- deleteScene: async ({ sceneId }: { sceneId: string }) => {
- const { scenes, currentScene } = get();
- const sceneToDelete = scenes.find((s) => s.id === sceneId);
-
- if (!sceneToDelete) {
- throw new Error("Scene not found");
- }
-
- if (sceneToDelete.isMain) {
- throw new Error("Cannot delete main scene");
- }
-
- const updatedScenes = scenes.filter((s) => s.id !== sceneId);
-
- // Determine new current scene if we're deleting the current one
- let newCurrentScene = currentScene;
- if (currentScene?.id === sceneId) {
- newCurrentScene = getMainScene({ scenes: updatedScenes });
- }
-
- // Update project
- const projectStore = useProjectStore.getState();
- const { activeProject } = projectStore;
-
- if (!activeProject) {
- throw new Error("No active project");
- }
-
- const updatedProject = {
- ...activeProject,
- scenes: updatedScenes,
- updatedAt: new Date(),
- };
-
- try {
- await storageService.saveProject({ project: updatedProject });
- useProjectStore.setState({ activeProject: updatedProject });
- set({
- scenes: updatedScenes,
- currentScene: newCurrentScene,
- });
-
- // If we switched scenes, load the new scene's timeline
- if (newCurrentScene && newCurrentScene.id !== currentScene?.id) {
- const timelineStore = useTimelineStore.getState();
- await timelineStore.loadProjectTimeline({
- projectId: activeProject.id,
- sceneId: newCurrentScene.id,
- });
- }
- } catch (error) {
- console.error("Failed to delete scene:", error);
- throw error;
- }
- },
-
- renameScene: async ({ sceneId, name }: { sceneId: string; name: string }) => {
- const { scenes } = get();
- const updatedScenes = scenes.map((scene) =>
- scene.id === sceneId ? { ...scene, name, updatedAt: new Date() } : scene
- );
-
- // Update project
- const projectStore = useProjectStore.getState();
- const { activeProject } = projectStore;
-
- if (!activeProject) {
- throw new Error("No active project");
- }
-
- const updatedProject = {
- ...activeProject,
- scenes: updatedScenes,
- updatedAt: new Date(),
- };
-
- try {
- await storageService.saveProject({ project: updatedProject });
- useProjectStore.setState({ activeProject: updatedProject });
- set({
- scenes: updatedScenes,
- currentScene: updatedScenes.find((s) => s.id === sceneId) || null,
- });
- } catch (error) {
- console.error("Failed to rename scene:", error);
- throw error;
- }
- },
-
- switchToScene: async ({ sceneId }: { sceneId: string }) => {
- const { scenes } = get();
- const targetScene = scenes.find((s) => s.id === sceneId);
-
- if (!targetScene) {
- throw new Error("Scene not found");
- }
-
- const timelineStore = useTimelineStore.getState();
- const projectStore = useProjectStore.getState();
- const { activeProject } = projectStore;
- const { currentScene } = get();
-
- if (activeProject && currentScene) {
- await timelineStore.saveProjectTimeline({
- projectId: activeProject.id,
- sceneId: currentScene.id,
- });
- }
-
- if (activeProject) {
- await timelineStore.loadProjectTimeline({
- projectId: activeProject.id,
- sceneId,
- });
-
- const updatedProject = {
- ...activeProject,
- currentSceneId: sceneId,
- updatedAt: new Date(),
- };
-
- await storageService.saveProject({ project: updatedProject });
- useProjectStore.setState({ activeProject: updatedProject });
- }
-
- set({ currentScene: targetScene });
- },
-
- getMainScene: () => {
- const { scenes } = get();
- return scenes.find((scene) => scene.isMain) || null;
- },
-
- getCurrentScene: () => {
- return get().currentScene;
- },
-
- loadProjectScenes: async ({ projectId }: { projectId: string }) => {
- try {
- const project = await storageService.loadProject({ id: projectId });
- if (project?.scenes) {
- const ensuredScenes = project.scenes.map((scene) => ({
- ...scene,
- isMain: scene.isMain || false,
- }));
- const currentScene =
- ensuredScenes.find((s) => s.id === project.currentSceneId) ||
- ensuredScenes[0];
-
- set({
- scenes: ensuredScenes,
- currentScene,
- });
- }
- } catch (error) {
- console.error("Failed to load project scenes:", error);
- set({ scenes: [], currentScene: null });
- }
- },
-
- initializeScenes: ({
- scenes,
- currentSceneId,
- }: {
- scenes: Scene[];
- currentSceneId?: string;
- }) => {
- const ensuredScenes = ensureMainScene(scenes);
- const currentScene = currentSceneId
- ? ensuredScenes.find((s) => s.id === currentSceneId)
- : null;
-
- const fallbackScene = getMainScene({ scenes: ensuredScenes });
-
- set({
- scenes: ensuredScenes,
- currentScene: currentScene || fallbackScene,
- });
-
- if (ensuredScenes.length > scenes.length) {
- const projectStore = useProjectStore.getState();
- const { activeProject } = projectStore;
-
- if (activeProject) {
- const updatedProject = {
- ...activeProject,
- scenes: ensuredScenes,
- updatedAt: new Date(),
- };
-
- storageService
- .saveProject({ project: updatedProject })
- .then(() => {
- useProjectStore.setState({ activeProject: updatedProject });
- })
- .catch((error) => {
- console.error(
- "Failed to save project with background scene:",
- error
- );
- });
- }
- }
- },
-
- clearScenes: () => {
- set({
- scenes: [],
- currentScene: null,
- });
- },
-}));
diff --git a/apps/web/src/stores/sounds-store.ts b/apps/web/src/stores/sounds-store.ts
index 579eee7a..bea1a4bc 100644
--- a/apps/web/src/stores/sounds-store.ts
+++ b/apps/web/src/stores/sounds-store.ts
@@ -1,282 +1,261 @@
import { create } from "zustand";
import type { SoundEffect, SavedSound } from "@/types/sounds";
-import { storageService } from "@/lib/storage/storage-service";
+import { storageService } from "@/services/storage/storage-service";
import { toast } from "sonner";
-import { useMediaStore } from "./media-store";
-import { useTimelineStore } from "./timeline-store";
-import { useProjectStore } from "./project-store";
-import { usePlaybackStore } from "./playback-store";
+import { EditorCore } from "@/core";
+import { buildLibraryAudioElement } from "@/lib/timeline/element-utils";
interface SoundsStore {
- topSoundEffects: SoundEffect[];
- isLoading: boolean;
- error: string | null;
- hasLoaded: boolean;
+ topSoundEffects: SoundEffect[];
+ isLoading: boolean;
+ error: string | null;
+ hasLoaded: boolean;
+ showCommercialOnly: boolean;
+ toggleCommercialFilter: () => void;
+ searchQuery: string;
+ searchResults: SoundEffect[];
+ isSearching: boolean;
+ searchError: string | null;
+ lastSearchQuery: string;
+ scrollPosition: number;
+ currentPage: number;
+ hasNextPage: boolean;
+ totalCount: number;
+ isLoadingMore: boolean;
+ savedSounds: SavedSound[];
+ isSavedSoundsLoaded: boolean;
+ isLoadingSavedSounds: boolean;
+ savedSoundsError: string | null;
- // Filter state
- showCommercialOnly: boolean;
- toggleCommercialFilter: () => void;
-
- // Search state
- searchQuery: string;
- searchResults: SoundEffect[];
- isSearching: boolean;
- searchError: string | null;
- lastSearchQuery: string;
- scrollPosition: number;
-
- // Pagination state
- currentPage: number;
- hasNextPage: boolean;
- totalCount: number;
- isLoadingMore: boolean;
-
- // Saved sounds state
- savedSounds: SavedSound[];
- isSavedSoundsLoaded: boolean;
- isLoadingSavedSounds: boolean;
- savedSoundsError: string | null;
-
- // Timeline integration
- addSoundToTimeline: (sound: SoundEffect) => Promise<boolean>;
-
- setTopSoundEffects: (sounds: SoundEffect[]) => void;
- setLoading: (loading: boolean) => void;
- setError: (error: string | null) => void;
- setHasLoaded: (loaded: boolean) => void;
-
- // Search actions
- setSearchQuery: (query: string) => void;
- setSearchResults: (results: SoundEffect[]) => void;
- setSearching: (searching: boolean) => void;
- setSearchError: (error: string | null) => void;
- setLastSearchQuery: (query: string) => void;
- setScrollPosition: (position: number) => void;
-
- // Pagination actions
- setCurrentPage: (page: number) => void;
- setHasNextPage: (hasNext: boolean) => void;
- setTotalCount: (count: number) => void;
- setLoadingMore: (loading: boolean) => void;
- appendSearchResults: (results: SoundEffect[]) => void;
- appendTopSounds: (results: SoundEffect[]) => void;
- resetPagination: () => void;
-
- // Saved sounds actions
- loadSavedSounds: () => Promise<void>;
- saveSoundEffect: (soundEffect: SoundEffect) => Promise<void>;
- removeSavedSound: (soundId: number) => Promise<void>;
- isSoundSaved: (soundId: number) => boolean;
- toggleSavedSound: (soundEffect: SoundEffect) => Promise<void>;
- clearSavedSounds: () => Promise<void>;
+ addSoundToTimeline: ({ sound }: { sound: SoundEffect }) => Promise<boolean>;
+ setTopSoundEffects: ({ sounds }: { sounds: SoundEffect[] }) => void;
+ setLoading: ({ loading }: { loading: boolean }) => void;
+ setError: ({ error }: { error: string | null }) => void;
+ setHasLoaded: ({ loaded }: { loaded: boolean }) => void;
+ setSearchQuery: ({ query }: { query: string }) => void;
+ setSearchResults: ({ results }: { results: SoundEffect[] }) => void;
+ setSearching: ({ searching }: { searching: boolean }) => void;
+ setSearchError: ({ error }: { error: string | null }) => void;
+ setLastSearchQuery: ({ query }: { query: string }) => void;
+ setScrollPosition: ({ position }: { position: number }) => void;
+ setCurrentPage: ({ page }: { page: number }) => void;
+ setHasNextPage: ({ hasNext }: { hasNext: boolean }) => void;
+ setTotalCount: ({ count }: { count: number }) => void;
+ setLoadingMore: ({ loading }: { loading: boolean }) => void;
+ appendSearchResults: ({ results }: { results: SoundEffect[] }) => void;
+ appendTopSounds: ({ results }: { results: SoundEffect[] }) => void;
+ resetPagination: () => void;
+ loadSavedSounds: () => Promise<void>;
+ saveSoundEffect: ({
+ soundEffect,
+ }: {
+ soundEffect: SoundEffect;
+ }) => Promise<void>;
+ removeSavedSound: ({ soundId }: { soundId: number }) => Promise<void>;
+ isSoundSaved: ({ soundId }: { soundId: number }) => boolean;
+ toggleSavedSound: ({
+ soundEffect,
+ }: {
+ soundEffect: SoundEffect;
+ }) => Promise<void>;
+ clearSavedSounds: () => Promise<void>;
}
export const useSoundsStore = create<SoundsStore>((set, get) => ({
- topSoundEffects: [],
- isLoading: false,
- error: null,
- hasLoaded: false,
- showCommercialOnly: true,
-
- toggleCommercialFilter: () => {
- set((state) => ({ showCommercialOnly: !state.showCommercialOnly }));
- },
+ topSoundEffects: [],
+ isLoading: false,
+ error: null,
+ hasLoaded: false,
+ showCommercialOnly: true,
- // Search state
- searchQuery: "",
- searchResults: [],
- isSearching: false,
- searchError: null,
- lastSearchQuery: "",
- scrollPosition: 0,
+ toggleCommercialFilter: () => {
+ set((state) => ({ showCommercialOnly: !state.showCommercialOnly }));
+ },
- // Pagination state
- currentPage: 1,
- hasNextPage: false,
- totalCount: 0,
- isLoadingMore: false,
+ searchQuery: "",
+ searchResults: [],
+ isSearching: false,
+ searchError: null,
+ lastSearchQuery: "",
+ scrollPosition: 0,
+ currentPage: 1,
+ hasNextPage: false,
+ totalCount: 0,
+ isLoadingMore: false,
+ savedSounds: [],
+ isSavedSoundsLoaded: false,
+ isLoadingSavedSounds: false,
+ savedSoundsError: null,
- // Saved sounds state
- savedSounds: [],
- isSavedSoundsLoaded: false,
- isLoadingSavedSounds: false,
- savedSoundsError: null,
+ setTopSoundEffects: ({ sounds }) => set({ topSoundEffects: sounds }),
+ setLoading: ({ loading }) => set({ isLoading: loading }),
+ setError: ({ error }) => set({ error }),
+ setHasLoaded: ({ loaded }) => set({ hasLoaded: loaded }),
+ setSearchQuery: ({ query }) => set({ searchQuery: query }),
+ setSearchResults: ({ results }) =>
+ set({ searchResults: results, currentPage: 1 }),
+ setSearching: ({ searching }) => set({ isSearching: searching }),
+ setSearchError: ({ error }) => set({ searchError: error }),
+ setLastSearchQuery: ({ query }) => set({ lastSearchQuery: query }),
+ setScrollPosition: ({ position }) => set({ scrollPosition: position }),
+ setCurrentPage: ({ page }) => set({ currentPage: page }),
+ setHasNextPage: ({ hasNext }) => set({ hasNextPage: hasNext }),
+ setTotalCount: ({ count }) => set({ totalCount: count }),
+ setLoadingMore: ({ loading }) => set({ isLoadingMore: loading }),
- setTopSoundEffects: (sounds) => set({ topSoundEffects: sounds }),
- setLoading: (loading) => set({ isLoading: loading }),
- setError: (error) => set({ error }),
- setHasLoaded: (loaded) => set({ hasLoaded: loaded }),
+ appendSearchResults: ({ results }) =>
+ set((state) => ({
+ searchResults: [...state.searchResults, ...results],
+ })),
- // Search actions
- setSearchQuery: (query) => set({ searchQuery: query }),
- setSearchResults: (results) =>
- set({ searchResults: results, currentPage: 1 }),
- setSearching: (searching) => set({ isSearching: searching }),
- setSearchError: (error) => set({ searchError: error }),
- setLastSearchQuery: (query) => set({ lastSearchQuery: query }),
- setScrollPosition: (position) => set({ scrollPosition: position }),
+ appendTopSounds: ({ results }) =>
+ set((state) => ({
+ topSoundEffects: [...state.topSoundEffects, ...results],
+ })),
- // Pagination actions
- setCurrentPage: (page) => set({ currentPage: page }),
- setHasNextPage: (hasNext) => set({ hasNextPage: hasNext }),
- setTotalCount: (count) => set({ totalCount: count }),
- setLoadingMore: (loading) => set({ isLoadingMore: loading }),
- appendSearchResults: (results) =>
- set((state) => ({
- searchResults: [...state.searchResults, ...results],
- })),
- appendTopSounds: (results) =>
- set((state) => ({
- topSoundEffects: [...state.topSoundEffects, ...results],
- })),
- resetPagination: () =>
- set({
- currentPage: 1,
- hasNextPage: false,
- totalCount: 0,
- isLoadingMore: false,
- }),
+ resetPagination: () =>
+ set({
+ currentPage: 1,
+ hasNextPage: false,
+ totalCount: 0,
+ isLoadingMore: false,
+ }),
- // Saved sounds actions
- loadSavedSounds: async () => {
- if (get().isSavedSoundsLoaded) return;
+ loadSavedSounds: async () => {
+ if (get().isSavedSoundsLoaded) return;
- try {
- set({ isLoadingSavedSounds: true, savedSoundsError: null });
- const savedSoundsData = await storageService.loadSavedSounds();
- set({
- savedSounds: savedSoundsData.sounds,
- isSavedSoundsLoaded: true,
- isLoadingSavedSounds: false,
- });
- } catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : "Failed to load saved sounds";
- set({
- savedSoundsError: errorMessage,
- isLoadingSavedSounds: false,
- });
- console.error("Failed to load saved sounds:", error);
- }
- },
+ try {
+ set({ isLoadingSavedSounds: true, savedSoundsError: null });
+ const savedSoundsData = await storageService.loadSavedSounds();
+ set({
+ savedSounds: savedSoundsData.sounds,
+ isSavedSoundsLoaded: true,
+ isLoadingSavedSounds: false,
+ });
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Failed to load saved sounds";
+ set({
+ savedSoundsError: errorMessage,
+ isLoadingSavedSounds: false,
+ });
+ console.error("Failed to load saved sounds:", error);
+ }
+ },
- saveSoundEffect: async (soundEffect: SoundEffect) => {
- try {
- await storageService.saveSoundEffect({ soundEffect });
+ saveSoundEffect: async ({ soundEffect }) => {
+ try {
+ await storageService.saveSoundEffect({ soundEffect });
- // Refresh saved sounds
- const savedSoundsData = await storageService.loadSavedSounds();
- set({ savedSounds: savedSoundsData.sounds });
- } catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : "Failed to save sound";
- set({ savedSoundsError: errorMessage });
- toast.error("Failed to save sound");
- console.error("Failed to save sound:", error);
- }
- },
+ const savedSoundsData = await storageService.loadSavedSounds();
+ set({ savedSounds: savedSoundsData.sounds });
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Failed to save sound";
+ set({ savedSoundsError: errorMessage });
+ toast.error("Failed to save sound");
+ console.error("Failed to save sound:", error);
+ }
+ },
- removeSavedSound: async (soundId: number) => {
- try {
- await storageService.removeSavedSound({ soundId });
+ removeSavedSound: async ({ soundId }) => {
+ try {
+ await storageService.removeSavedSound({ soundId });
- // Update local state immediately
- set((state) => ({
- savedSounds: state.savedSounds.filter((sound) => sound.id !== soundId),
- }));
- } catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : "Failed to remove sound";
- set({ savedSoundsError: errorMessage });
- toast.error("Failed to remove sound");
- console.error("Failed to remove sound:", error);
- }
- },
+ set((state) => ({
+ savedSounds: state.savedSounds.filter((sound) => sound.id !== soundId),
+ }));
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Failed to remove sound";
+ set({ savedSoundsError: errorMessage });
+ toast.error("Failed to remove sound");
+ console.error("Failed to remove sound:", error);
+ }
+ },
- isSoundSaved: (soundId: number) => {
- const { savedSounds } = get();
- return savedSounds.some((sound) => sound.id === soundId);
- },
+ isSoundSaved: ({ soundId }) => {
+ const { savedSounds } = get();
+ return savedSounds.some((sound) => sound.id === soundId);
+ },
- toggleSavedSound: async (soundEffect: SoundEffect) => {
- const { isSoundSaved, saveSoundEffect, removeSavedSound } = get();
+ toggleSavedSound: async ({ soundEffect }) => {
+ const { isSoundSaved, saveSoundEffect, removeSavedSound } = get();
- if (isSoundSaved(soundEffect.id)) {
- await removeSavedSound(soundEffect.id);
- } else {
- await saveSoundEffect(soundEffect);
- }
- },
+ if (isSoundSaved({ soundId: soundEffect.id })) {
+ await removeSavedSound({ soundId: soundEffect.id });
+ } else {
+ await saveSoundEffect({ soundEffect });
+ }
+ },
- clearSavedSounds: async () => {
- try {
- await storageService.clearSavedSounds();
- set({
- savedSounds: [],
- savedSoundsError: null,
- });
- } catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : "Failed to clear saved sounds";
- set({ savedSoundsError: errorMessage });
- toast.error("Failed to clear saved sounds");
- console.error("Failed to clear saved sounds:", error);
- }
- },
+ clearSavedSounds: async () => {
+ try {
+ await storageService.clearSavedSounds();
+ set({
+ savedSounds: [],
+ savedSoundsError: null,
+ });
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Failed to clear saved sounds";
+ set({ savedSoundsError: errorMessage });
+ toast.error("Failed to clear saved sounds");
+ console.error("Failed to clear saved sounds:", error);
+ }
+ },
- addSoundToTimeline: async (sound) => {
- const activeProject = useProjectStore.getState().activeProject;
- if (!activeProject) {
- toast.error("No active project");
- return false;
- }
+ addSoundToTimeline: async ({ sound }) => {
+ const audioUrl = sound.previewUrl;
+ if (!audioUrl) {
+ toast.error("Sound file not available");
+ return false;
+ }
- const audioUrl = sound.previewUrl;
- if (!audioUrl) {
- toast.error("Sound file not available");
- return false;
- }
+ try {
+ const editor = EditorCore.getInstance();
+ const currentTime = editor.playback.getCurrentTime();
+ const tracks = editor.timeline.getTracks();
- try {
- const response = await fetch(audioUrl);
- if (!response.ok)
- throw new Error(`Failed to download audio: ${response.statusText}`);
+ const response = await fetch(audioUrl);
+ if (!response.ok)
+ throw new Error(`Failed to download audio: ${response.statusText}`);
- const blob = await response.blob();
- const file = new File([blob], `${sound.name}.mp3`, {
- type: "audio/mpeg",
- });
+ const arrayBuffer = await response.arrayBuffer();
+ const audioContext = new AudioContext();
+ const buffer = await audioContext.decodeAudioData(arrayBuffer);
- await useMediaStore.getState().addMediaFile(activeProject.id, {
- name: sound.name,
- type: "audio",
- file,
- duration: sound.duration,
- url: URL.createObjectURL(file),
- });
+ const audioTrack = tracks.find((t) => t.type === "audio");
+ let trackId: string;
- const mediaItem = useMediaStore
- .getState()
- .mediaFiles.find((item) => item.file === file);
- if (!mediaItem) throw new Error("Failed to create media item");
+ if (audioTrack) {
+ trackId = audioTrack.id;
+ } else {
+ trackId = editor.timeline.addTrack({ type: "audio" });
+ }
- const success = useTimelineStore
- .getState()
- .addElementAtTime(mediaItem, usePlaybackStore.getState().currentTime);
+ const element = buildLibraryAudioElement({
+ sourceUrl: audioUrl,
+ name: sound.name,
+ duration: sound.duration,
+ startTime: currentTime,
+ buffer,
+ });
- if (success) {
- return true;
- }
- throw new Error("Failed to add to timeline - check for overlaps");
- } catch (error) {
- console.error("Failed to add sound to timeline:", error);
- toast.error(
- error instanceof Error
- ? error.message
- : "Failed to add sound to timeline",
- { id: `sound-${sound.id}` }
- );
- return false;
- }
- },
+ editor.timeline.insertElement({
+ placement: { mode: "explicit", trackId },
+ element,
+ });
+ return true;
+ } catch (error) {
+ console.error("Failed to add sound to timeline:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Failed to add sound to timeline",
+ { id: `sound-${sound.id}` },
+ );
+ return false;
+ }
+ },
}));
diff --git a/apps/web/src/stores/stickers-store.ts b/apps/web/src/stores/stickers-store.ts
index c7c9ef98..edc78220 100644
--- a/apps/web/src/stores/stickers-store.ts
+++ b/apps/web/src/stores/stickers-store.ts
@@ -1,231 +1,176 @@
import { create } from "zustand";
import {
- getCollections,
- getCollection,
- searchIcons,
- downloadSvgAsText,
- svgToFile,
- type IconSet,
- type CollectionInfo,
- type IconSearchResult,
+ getCollections,
+ getCollection,
+ searchIcons,
+ type IconSet,
+ type CollectionInfo,
+ type IconSearchResult,
} from "@/lib/iconify-api";
-import { useProjectStore } from "@/stores/project-store";
-import { useMediaStore } from "@/stores/media-store";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { usePlaybackStore } from "@/stores/playback-store";
-import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
-import type { MediaFile } from "@/types/media";
+import { EditorCore } from "@/core";
+import { buildStickerElement } from "@/lib/timeline/element-utils";
+import { STICKER_CATEGORY_CONFIG } from "@/constants/stickers-constants";
+import type { StickerCategory } from "@/types/stickers";
-export type StickerCategory = "all" | "general" | "brands" | "emoji";
+type ViewMode = "search" | "browse" | "collection";
interface StickersStore {
- searchQuery: string;
- selectedCategory: StickerCategory;
- selectedCollection: string | null;
- viewMode: "search" | "browse" | "collection";
+ searchQuery: string;
+ selectedCategory: StickerCategory;
+ selectedCollection: string | null;
+ viewMode: ViewMode;
+ collections: Record<string, IconSet>;
+ currentCollection: CollectionInfo | null;
+ searchResults: IconSearchResult | null;
+ recentStickers: string[];
+ isLoadingCollections: boolean;
+ isLoadingCollection: boolean;
+ isSearching: boolean;
+ addingSticker: string | null;
- collections: Record<string, IconSet>;
- currentCollection: CollectionInfo | null;
- searchResults: IconSearchResult | null;
- recentStickers: string[];
- isLoadingCollections: boolean;
- isLoadingCollection: boolean;
- isSearching: boolean;
- isDownloading: boolean;
- addingSticker: string | null;
-
- setSearchQuery: (query: string) => void;
- setSelectedCategory: (category: StickerCategory) => void;
- setSelectedCollection: (collection: string | null) => void;
- setViewMode: (mode: "search" | "browse" | "collection") => void;
-
- loadCollections: () => Promise<void>;
- loadCollection: (prefix: string) => Promise<void>;
- searchStickers: (query: string) => Promise<void>;
- downloadSticker: (iconName: string) => Promise<File | null>;
- addStickerToTimeline: (iconName: string) => Promise<void>;
-
- addToRecentStickers: (iconName: string) => void;
- clearRecentStickers: () => void;
+ setSearchQuery: ({ query }: { query: string }) => void;
+ setSelectedCategory: ({ category }: { category: StickerCategory }) => void;
+ setSelectedCollection: ({
+ collection,
+ }: {
+ collection: string | null;
+ }) => void;
+ setViewMode: ({ mode }: { mode: ViewMode }) => void;
+ loadCollections: () => Promise<void>;
+ loadCollection: ({ prefix }: { prefix: string }) => Promise<void>;
+ searchStickers: ({ query }: { query: string }) => Promise<void>;
+ addStickerToTimeline: ({ iconName }: { iconName: string }) => void;
+ addToRecentStickers: ({ iconName }: { iconName: string }) => void;
+ clearRecentStickers: () => void;
}
const MAX_RECENT_STICKERS = 50;
export const useStickersStore = create<StickersStore>((set, get) => ({
- searchQuery: "",
- selectedCategory: "all",
- selectedCollection: null,
- viewMode: "browse",
-
- collections: {},
- currentCollection: null,
- searchResults: null,
- recentStickers: [],
-
- isLoadingCollections: false,
- isLoadingCollection: false,
- isSearching: false,
- isDownloading: false,
- addingSticker: null,
-
- setSearchQuery: (query) => set({ searchQuery: query }),
-
- setSelectedCategory: (category) =>
- set({
- selectedCategory: category,
- viewMode: "browse",
- selectedCollection: null,
- currentCollection: null,
- }),
-
- setSelectedCollection: (collection) => {
- set({
- selectedCollection: collection,
- viewMode: collection ? "collection" : "browse",
- currentCollection: null,
- });
-
- if (collection) {
- get().loadCollection(collection);
- }
- },
-
- setViewMode: (mode) => set({ viewMode: mode }),
+ searchQuery: "",
+ selectedCategory: "all",
+ selectedCollection: null,
+ viewMode: "browse",
- loadCollections: async () => {
- set({ isLoadingCollections: true });
- try {
- const collections = await getCollections();
- set({ collections });
- } catch (error) {
- console.error("Failed to load collections:", error);
- } finally {
- set({ isLoadingCollections: false });
- }
- },
+ collections: {},
+ currentCollection: null,
+ searchResults: null,
+ recentStickers: [],
- loadCollection: async (prefix: string) => {
- set({ isLoadingCollection: true });
- try {
- const collection = await getCollection(prefix);
- set({ currentCollection: collection });
- } catch (error) {
- console.error(`Failed to load collection ${prefix}:`, error);
- set({ currentCollection: null });
- } finally {
- set({ isLoadingCollection: false });
- }
- },
+ isLoadingCollections: false,
+ isLoadingCollection: false,
+ isSearching: false,
+ addingSticker: null,
- searchStickers: async (query: string) => {
- if (!query.trim()) {
- set({ searchResults: null, viewMode: "browse" });
- return;
- }
+ setSearchQuery: ({ query }) => set({ searchQuery: query }),
- const { selectedCategory } = get();
+ setSelectedCategory: ({ category }) =>
+ set({
+ selectedCategory: category,
+ viewMode: "browse",
+ selectedCollection: null,
+ currentCollection: null,
+ }),
- set({ isSearching: true, viewMode: "search" });
- try {
- let category: string | undefined;
+ setSelectedCollection: ({ collection }) => {
+ set({
+ selectedCollection: collection,
+ viewMode: collection ? "collection" : "browse",
+ currentCollection: null,
+ });
- if (selectedCategory !== "all") {
- if (selectedCategory === "general") {
- category = "General";
- } else if (selectedCategory === "brands") {
- category = "Brands / Social";
- } else if (selectedCategory === "emoji") {
- category = "Emoji";
- }
- }
+ if (collection) {
+ get().loadCollection({ prefix: collection });
+ }
+ },
- const results = await searchIcons(query, 100, undefined, category);
- set({ searchResults: results });
- } catch (error) {
- console.error("Search failed:", error);
- set({ searchResults: null });
- } finally {
- set({ isSearching: false });
- }
- },
+ setViewMode: ({ mode }) => set({ viewMode: mode }),
- downloadSticker: async (iconName: string) => {
- set({ isDownloading: true });
- try {
- const svgText = await downloadSvgAsText(iconName, {
- width: 200,
- height: 200,
- });
+ loadCollections: async () => {
+ set({ isLoadingCollections: true });
+ try {
+ const collections = await getCollections();
+ set({ collections });
+ } catch (error) {
+ console.error("Failed to load collections:", error);
+ } finally {
+ set({ isLoadingCollections: false });
+ }
+ },
- const fileName = `${iconName.replace(":", "-")}.svg`;
- const file = svgToFile(svgText, fileName);
+ loadCollection: async ({ prefix }: { prefix: string }) => {
+ set({ isLoadingCollection: true });
+ try {
+ const collection = await getCollection(prefix);
+ set({ currentCollection: collection });
+ } catch (error) {
+ console.error(`Failed to load collection ${prefix}:`, error);
+ set({ currentCollection: null });
+ } finally {
+ set({ isLoadingCollection: false });
+ }
+ },
- get().addToRecentStickers(iconName);
+ searchStickers: async ({ query }: { query: string }) => {
+ if (!query.trim()) {
+ set({ searchResults: null, viewMode: "browse" });
+ return;
+ }
- return file;
- } catch (error) {
- console.error(`Failed to download sticker ${iconName}:`, error);
- return null;
- } finally {
- set({ isDownloading: false });
- }
- },
+ const { selectedCategory } = get();
- addStickerToTimeline: async (iconName: string) => {
- set({ addingSticker: iconName });
- try {
- const { activeProject } = useProjectStore.getState();
- if (!activeProject) {
- throw new Error("No active project");
- }
+ set({ isSearching: true, viewMode: "search" });
+ try {
+ const category = STICKER_CATEGORY_CONFIG[selectedCategory];
+ const results = await searchIcons(query, 100, undefined, category);
+ set({ searchResults: results });
+ } catch (error) {
+ console.error("Search failed:", error);
+ set({ searchResults: null });
+ } finally {
+ set({ isSearching: false });
+ }
+ },
- const file = await get().downloadSticker(iconName);
- if (!file) {
- throw new Error("Failed to download sticker");
- }
+ addStickerToTimeline: ({ iconName }: { iconName: string }) => {
+ set({ addingSticker: iconName });
+ try {
+ const editor = EditorCore.getInstance();
+ const currentTime = editor.playback.getCurrentTime();
+ const tracks = editor.timeline.getTracks();
- const mediaItem: Omit<MediaFile, "id"> = {
- name: iconName.replace(":", "-"),
- type: "image",
- file,
- url: URL.createObjectURL(file),
- width: 200,
- height: 200,
- duration: TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION,
- ephemeral: false,
- };
+ const stickerTrack = tracks.find((t) => t.type === "sticker");
+ let trackId: string;
- const { addMediaFile } = useMediaStore.getState();
- await addMediaFile(activeProject.id, mediaItem);
+ if (stickerTrack) {
+ trackId = stickerTrack.id;
+ } else {
+ trackId = editor.timeline.addTrack({ type: "sticker" });
+ }
- const added = useMediaStore
- .getState()
- .mediaFiles.find(
- (m) => m.url === mediaItem.url && m.name === mediaItem.name
- );
- if (!added) {
- throw new Error("Sticker not in media store");
- }
+ const element = buildStickerElement({ iconName, startTime: currentTime });
+ editor.timeline.insertElement({
+ placement: { mode: "explicit", trackId },
+ element,
+ });
- const { currentTime } = usePlaybackStore.getState();
- const { addElementAtTime } = useTimelineStore.getState();
- addElementAtTime(added, currentTime);
- } finally {
- set({ addingSticker: null });
- }
- },
+ get().addToRecentStickers({ iconName });
+ } finally {
+ set({ addingSticker: null });
+ }
+ },
- addToRecentStickers: (iconName: string) => {
- set((state) => {
- const recent = [
- iconName,
- ...state.recentStickers.filter((s) => s !== iconName),
- ];
- return {
- recentStickers: recent.slice(0, MAX_RECENT_STICKERS),
- };
- });
- },
+ addToRecentStickers: ({ iconName }: { iconName: string }) => {
+ set((state) => {
+ const recent = [
+ iconName,
+ ...state.recentStickers.filter((s) => s !== iconName),
+ ];
+ return {
+ recentStickers: recent.slice(0, MAX_RECENT_STICKERS),
+ };
+ });
+ },
- clearRecentStickers: () => set({ recentStickers: [] }),
+ clearRecentStickers: () => set({ recentStickers: [] }),
}));
diff --git a/apps/web/src/stores/text-properties-store.ts b/apps/web/src/stores/text-properties-store.ts
index b3ce3689..c7476b46 100644
--- a/apps/web/src/stores/text-properties-store.ts
+++ b/apps/web/src/stores/text-properties-store.ts
@@ -1,33 +1,33 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
-export type TextPropertiesTab = "transform" | "style";
+export type TextPropertiesTab = "text" | "transform";
export interface TextPropertiesTabMeta {
- value: TextPropertiesTab;
- label: string;
+ value: TextPropertiesTab;
+ label: string;
}
export const TEXT_PROPERTIES_TABS: ReadonlyArray<TextPropertiesTabMeta> = [
- { value: "transform", label: "Transform" },
- { value: "style", label: "Style" },
+ { value: "text", label: "Text" },
+ { value: "transform", label: "Transform" },
] as const;
export function isTextPropertiesTab(value: string): value is TextPropertiesTab {
- return TEXT_PROPERTIES_TABS.some((t) => t.value === value);
+ return TEXT_PROPERTIES_TABS.some((t) => t.value === value);
}
interface TextPropertiesState {
- activeTab: TextPropertiesTab;
- setActiveTab: (tab: TextPropertiesTab) => void;
+ activeTab: TextPropertiesTab;
+ setActiveTab: (tab: TextPropertiesTab) => void;
}
export const useTextPropertiesStore = create<TextPropertiesState>()(
- persist(
- (set) => ({
- activeTab: "transform",
- setActiveTab: (tab) => set({ activeTab: tab }),
- }),
- { name: "text-properties" }
- )
+ persist(
+ (set) => ({
+ activeTab: "text",
+ setActiveTab: (tab) => set({ activeTab: tab }),
+ }),
+ { name: "text-properties" },
+ ),
);
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts
index aeeb65c5..7b555cd9 100644
--- a/apps/web/src/stores/timeline-store.ts
+++ b/apps/web/src/stores/timeline-store.ts
@@ -1,1889 +1,44 @@
-import { create } from "zustand";
-import {
- TrackType,
- TimelineElement,
- CreateTimelineElement,
- TimelineTrack,
- TextElement,
- DragData,
- MediaElement,
- sortTracksByOrder,
- ensureMainTrack,
- validateElementTrackCompatibility,
-} from "@/types/timeline";
-import { useMediaStore, getMediaAspectRatio } from "./media-store";
-import { MediaFile, MediaType } from "@/types/media";
-import { findBestCanvasPreset } from "@/lib/editor-utils";
-import { storageService } from "@/lib/storage/storage-service";
-import { useProjectStore } from "./project-store";
-import { useSceneStore } from "./scene-store";
-import { generateUUID } from "@/lib/utils";
-import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
-import { checkElementOverlaps, resolveElementOverlaps } from "@/lib/timeline";
-import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
-import { usePlaybackStore } from "./playback-store";
-
-// Helper function to manage element naming with suffixes
-const getElementNameWithSuffix = (
- originalName: string,
- suffix: string
-): string => {
- // Remove existing suffixes to prevent accumulation
- const baseName = originalName
- .replace(/ \(left\)$/i, "")
- .replace(/ \(right\)$/i, "")
- .replace(/ \(audio\)$/i, "")
- .replace(/ \(split \d+\)$/i, "");
+/**
+ * UI state for the timeline
+ * For core logic, use EditorCore instead.
+ */
- return `${baseName} (${suffix})`;
-};
+import { create } from "zustand";
+import type { ClipboardItem } from "@/types/timeline";
interface TimelineStore {
- // Private track storage
- _tracks: TimelineTrack[];
- history: TimelineTrack[][];
- redoStack: TimelineTrack[][];
-
- // Clipboard buffer
- clipboard: {
- items: Array<{ trackType: TrackType; element: CreateTimelineElement }>;
- } | null;
-
- // Always returns properly ordered tracks with main track ensured
- tracks: TimelineTrack[];
-
- // Manual method if you need to force recomputation
- getSortedTracks: () => TimelineTrack[];
-
- // Snapping settings
- snappingEnabled: boolean;
-
- // Snapping actions
- toggleSnapping: () => void;
-
- // Ripple editing mode
- rippleEditingEnabled: boolean;
- toggleRippleEditing: () => void;
-
- // Multi-selection
- selectedElements: { trackId: string; elementId: string }[];
- selectElement: (trackId: string, elementId: string, multi?: boolean) => void;
- deselectElement: (trackId: string, elementId: string) => void;
- clearSelectedElements: () => void;
- setSelectedElements: (
- elements: { trackId: string; elementId: string }[]
- ) => void;
-
- // Drag state
- dragState: {
- isDragging: boolean;
- elementId: string | null;
- trackId: string | null;
- startMouseX: number;
- startElementTime: number;
- clickOffsetTime: number;
- currentTime: number;
- };
- setDragState: (dragState: Partial<TimelineStore["dragState"]>) => void;
- startDrag: (
- elementId: string,
- trackId: string,
- startMouseX: number,
- startElementTime: number,
- clickOffsetTime: number
- ) => void;
- updateDragTime: (currentTime: number) => void;
- endDrag: () => void;
-
- // Actions
- addTrack: (type: TrackType) => string;
- insertTrackAt: (type: TrackType, index: number) => string;
- removeTrack: (trackId: string) => void;
- removeTrackWithRipple: (trackId: string) => void;
- addElementToTrack: (trackId: string, element: CreateTimelineElement) => void;
-
- moveElementToTrack: (
- fromTrackId: string,
- toTrackId: string,
- elementId: string
- ) => void;
- updateElementTrim: (
- trackId: string,
- elementId: string,
- trimStart: number,
- trimEnd: number,
- pushHistory?: boolean
- ) => void;
- updateElementDuration: (
- trackId: string,
- elementId: string,
- duration: number,
- pushHistory?: boolean
- ) => void;
- updateElementStartTime: (
- trackId: string,
- elementId: string,
- startTime: number,
- pushHistory?: boolean
- ) => void;
- toggleTrackMute: (trackId: string) => void;
- splitAndKeepLeft: (
- trackId: string,
- elementId: string,
- splitTime: number
- ) => void;
- splitAndKeepRight: (
- trackId: string,
- elementId: string,
- splitTime: number
- ) => void;
- separateAudio: (trackId: string, elementId: string) => string | null;
-
- // Replace media for an element
- replaceElementMedia: (
- trackId: string,
- elementId: string,
- newFile: File
- ) => Promise<{ success: boolean; error?: string }>;
-
- // Ripple editing functions
- updateElementStartTimeWithRipple: (
- trackId: string,
- elementId: string,
- newStartTime: number
- ) => void;
- removeElementFromTrackWithRipple: (
- trackId: string,
- elementId: string,
- pushHistory?: boolean
- ) => void;
-
- // Computed values
- getTotalDuration: () => number;
- getProjectThumbnail: (projectId: string) => Promise<string | null>;
-
- // History actions
- undo: () => void;
- redo: () => void;
- pushHistory: () => void;
-
- // Persistence actions
- loadProjectTimeline: ({
- projectId,
- sceneId,
- }: {
- projectId: string;
- sceneId?: string;
- }) => Promise<void>;
- saveProjectTimeline: ({
- projectId,
- sceneId,
- }: {
- projectId: string;
- sceneId?: string;
- }) => Promise<void>;
- clearTimeline: () => void;
-
- // Clipboard actions
- copySelected: () => void;
- pasteAtTime: (time: number) => void;
-
- // Unified selection-aware actions
- deleteSelected: (trackId?: string, elementId?: string) => void;
- splitSelected: (
- splitTime: number,
- trackId?: string,
- elementId?: string
- ) => void;
- toggleSelectedHidden: (trackId?: string, elementId?: string) => void;
- toggleSelectedMuted: (trackId?: string, elementId?: string) => void;
- duplicateElement: (trackId: string, elementId: string) => void;
- revealElementInMedia: (elementId: string) => void;
- replaceElementWithFile: (
- trackId: string,
- elementId: string,
- file: File
- ) => Promise<void>;
- getContextMenuState: (
- trackId: string,
- elementId: string
- ) => {
- isMultipleSelected: boolean;
- isCurrentElementSelected: boolean;
- hasAudioElements: boolean;
- canSplitSelected: boolean;
- currentTime: number;
- };
- updateTextElement: (
- trackId: string,
- elementId: string,
- updates: Partial<
- Pick<
- TextElement,
- | "content"
- | "fontSize"
- | "fontFamily"
- | "color"
- | "backgroundColor"
- | "textAlign"
- | "fontWeight"
- | "fontStyle"
- | "textDecoration"
- | "x"
- | "y"
- | "rotation"
- | "opacity"
- >
- >
- ) => void;
- checkElementOverlap: (
- trackId: string,
- startTime: number,
- duration: number,
- excludeElementId?: string
- ) => boolean;
- findOrCreateTrack: (trackType: TrackType) => string;
- addElementAtTime: (
- item: MediaFile | TextElement,
- currentTime?: number
- ) => boolean;
- addElementToNewTrack: (item: MediaFile | TextElement | DragData) => boolean;
+ snappingEnabled: boolean;
+ toggleSnapping: () => void;
+ rippleEditingEnabled: boolean;
+ toggleRippleEditing: () => void;
+ clipboard: {
+ items: ClipboardItem[];
+ } | null;
+ setClipboard: (
+ clipboard: {
+ items: ClipboardItem[];
+ } | null,
+ ) => void;
}
-export const useTimelineStore = create<TimelineStore>((set, get) => {
- // Helper to update tracks and maintain ordering
- const updateTracks = (newTracks: TimelineTrack[]) => {
- const tracksWithMain = ensureMainTrack(newTracks);
- const sortedTracks = sortTracksByOrder(tracksWithMain);
- set({
- _tracks: tracksWithMain,
- tracks: sortedTracks,
- });
- };
-
- // Helper to auto-save timeline changes
- const autoSaveTimeline = async () => {
- const activeProject = useProjectStore.getState().activeProject;
- const currentScene = useSceneStore.getState().currentScene;
-
- if (activeProject && currentScene) {
- try {
- await storageService.saveTimeline({
- projectId: activeProject.id,
- tracks: get()._tracks,
- sceneId: currentScene.id,
- });
- } catch (error) {
- console.error("Failed to auto-save timeline:", error);
- }
- } else {
- console.warn(
- "Auto-save skipped - missing activeProject or currentScene:",
- {
- hasProject: !!activeProject,
- hasScene: !!currentScene,
- sceneName: currentScene?.name,
- }
- );
- }
- };
-
- // Helper to update tracks and auto-save
- const updateTracksAndSave = (newTracks: TimelineTrack[]) => {
- updateTracks(newTracks);
- // Auto-save in background
- setTimeout(autoSaveTimeline, 100);
- };
-
- // Initialize with proper track ordering
- const initialTracks = ensureMainTrack([]);
- const sortedInitialTracks = sortTracksByOrder(initialTracks);
-
- return {
- _tracks: initialTracks,
- tracks: sortedInitialTracks,
- history: [],
- redoStack: [],
- selectedElements: [],
- rippleEditingEnabled: false,
- clipboard: null,
-
- // Snapping settings defaults
- snappingEnabled: true,
-
- getSortedTracks: () => {
- const { _tracks } = get();
- const tracksWithMain = ensureMainTrack(_tracks);
- return sortTracksByOrder(tracksWithMain);
- },
-
- pushHistory: () => {
- const { _tracks, history } = get();
- set({
- history: [...history, JSON.parse(JSON.stringify(_tracks))],
- redoStack: [],
- });
- },
-
- undo: () => {
- const { history, redoStack, _tracks } = get();
- if (history.length === 0) return;
- const prev = history[history.length - 1];
- updateTracksAndSave(prev);
- set({
- history: history.slice(0, -1),
- redoStack: [...redoStack, JSON.parse(JSON.stringify(_tracks))],
- });
- },
-
- selectElement: (trackId, elementId, multi = false) => {
- set((state) => {
- const exists = state.selectedElements.some(
- (c) => c.trackId === trackId && c.elementId === elementId
- );
- if (multi) {
- return exists
- ? {
- selectedElements: state.selectedElements.filter(
- (c) => !(c.trackId === trackId && c.elementId === elementId)
- ),
- }
- : {
- selectedElements: [
- ...state.selectedElements,
- { trackId, elementId },
- ],
- };
- }
- return { selectedElements: [{ trackId, elementId }] };
- });
- },
-
- deselectElement: (trackId, elementId) => {
- set((state) => ({
- selectedElements: state.selectedElements.filter(
- (c) => !(c.trackId === trackId && c.elementId === elementId)
- ),
- }));
- },
-
- clearSelectedElements: () => {
- set({ selectedElements: [] });
- },
-
- setSelectedElements: (elements) => set({ selectedElements: elements }),
-
- addTrack: (type) => {
- get().pushHistory();
-
- const trackName =
- type === "media"
- ? "Media Track"
- : type === "text"
- ? "Text Track"
- : type === "audio"
- ? "Audio Track"
- : "Track";
-
- const newTrack: TimelineTrack = {
- id: generateUUID(),
- name: trackName,
- type,
- elements: [],
- muted: false,
- };
-
- updateTracksAndSave([...get()._tracks, newTrack]);
- return newTrack.id;
- },
-
- insertTrackAt: (type, index) => {
- get().pushHistory();
-
- const trackName =
- type === "media"
- ? "Media Track"
- : type === "text"
- ? "Text Track"
- : type === "audio"
- ? "Audio Track"
- : "Track";
-
- const newTrack: TimelineTrack = {
- id: generateUUID(),
- name: trackName,
- type,
- elements: [],
- muted: false,
- };
-
- const newTracks = [...get()._tracks];
- newTracks.splice(index, 0, newTrack);
- updateTracksAndSave(newTracks);
- return newTrack.id;
- },
-
- removeTrack: (trackId) => {
- const { rippleEditingEnabled } = get();
-
- if (rippleEditingEnabled) {
- get().removeTrackWithRipple(trackId);
- } else {
- get().pushHistory();
- updateTracksAndSave(
- get()._tracks.filter((track) => track.id !== trackId)
- );
- }
- },
-
- removeTrackWithRipple: (trackId) => {
- const { _tracks } = get();
- const trackToRemove = _tracks.find((t) => t.id === trackId);
-
- if (!trackToRemove) return;
-
- get().pushHistory();
-
- const occupiedRanges = trackToRemove.elements.map((element) => ({
- startTime: element.startTime,
- endTime:
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd),
- }));
-
- occupiedRanges.sort((a, b) => a.startTime - b.startTime);
-
- const mergedRanges: Array<{
- startTime: number;
- endTime: number;
- duration: number;
- }> = [];
-
- for (const range of occupiedRanges) {
- if (mergedRanges.length === 0) {
- mergedRanges.push({
- startTime: range.startTime,
- endTime: range.endTime,
- duration: range.endTime - range.startTime,
- });
- } else {
- const lastRange = mergedRanges[mergedRanges.length - 1];
- if (range.startTime <= lastRange.endTime) {
- lastRange.endTime = Math.max(lastRange.endTime, range.endTime);
- lastRange.duration = lastRange.endTime - lastRange.startTime;
- } else {
- mergedRanges.push({
- startTime: range.startTime,
- endTime: range.endTime,
- duration: range.endTime - range.startTime,
- });
- }
- }
- }
-
- const updatedTracks = _tracks
- .filter((track) => track.id !== trackId)
- .map((track) => {
- const updatedElements = track.elements.map((element) => {
- let newStartTime = element.startTime;
-
- for (let i = mergedRanges.length - 1; i >= 0; i--) {
- const gap = mergedRanges[i];
- if (newStartTime >= gap.endTime) {
- newStartTime -= gap.duration;
- }
- }
-
- return {
- ...element,
- startTime: Math.max(0, newStartTime),
- };
- });
-
- const hasOverlaps = checkElementOverlaps(updatedElements);
- if (hasOverlaps) {
- const resolvedElements = resolveElementOverlaps(updatedElements);
- return { ...track, elements: resolvedElements };
- }
-
- return { ...track, elements: updatedElements };
- });
-
- updateTracksAndSave(updatedTracks);
- },
-
- addElementToTrack: (trackId, elementData) => {
- get().pushHistory();
-
- const track = get()._tracks.find((t) => t.id === trackId);
- if (!track) {
- console.error("Track not found:", trackId);
- return;
- }
-
- const validation = validateElementTrackCompatibility(elementData, track);
- if (!validation.isValid) {
- console.error(validation.errorMessage);
- return;
- }
-
- if (elementData.type === "media" && !elementData.mediaId) {
- console.error("Media element must have mediaId");
- return;
- }
-
- if (elementData.type === "text" && !elementData.content) {
- console.error("Text element must have content");
- return;
- }
-
- const currentState = get();
- const totalElementsInTimeline = currentState._tracks.reduce(
- (total, track) => total + track.elements.length,
- 0
- );
- const isFirstElement = totalElementsInTimeline === 0;
-
- const newElement: TimelineElement = {
- ...elementData,
- id: generateUUID(),
- startTime: elementData.startTime,
- trimStart: elementData.trimStart ?? 0,
- trimEnd: elementData.trimEnd ?? 0,
- ...(elementData.type === "media"
- ? { muted: elementData.muted ?? false }
- : {}),
- } as TimelineElement;
-
- if (isFirstElement && newElement.type === "media") {
- const mediaStore = useMediaStore.getState();
- const mediaItem = mediaStore.mediaFiles.find(
- (item) => item.id === newElement.mediaId
- );
-
- if (
- mediaItem &&
- (mediaItem.type === "image" || mediaItem.type === "video")
- ) {
- const projectStore = useProjectStore.getState();
- projectStore.updateCanvasSize(
- findBestCanvasPreset(getMediaAspectRatio(mediaItem)),
- "original"
- );
- }
-
- if (mediaItem && mediaItem.type === "video" && mediaItem.fps) {
- const projectStore = useProjectStore.getState();
- if (projectStore.activeProject) {
- projectStore.updateProjectFps(mediaItem.fps);
- }
- }
- }
-
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? { ...track, elements: [...track.elements, newElement] }
- : track
- )
- );
-
- get().selectElement(trackId, newElement.id);
- },
-
- removeElementFromTrackWithRipple: (
- trackId,
- elementId,
- pushHistory = true
- ) => {
- const { _tracks, rippleEditingEnabled } = get();
-
- if (!rippleEditingEnabled) {
- // Inline non-ripple removal logic
- if (pushHistory) get().pushHistory();
- updateTracksAndSave(
- _tracks
- .map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.filter(
- (element) => element.id !== elementId
- ),
- }
- : track
- )
- .filter((track) => track.elements.length > 0)
- );
- return;
- }
-
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((e) => e.id === elementId);
-
- if (!element || !track) return;
-
- if (pushHistory) get().pushHistory();
-
- const elementStartTime = element.startTime;
- const elementDuration =
- element.duration - element.trimStart - element.trimEnd;
- const elementEndTime = elementStartTime + elementDuration;
-
- const updatedTracks = _tracks
- .map((currentTrack) => {
- const shouldApplyRipple = currentTrack.id === trackId;
-
- const updatedElements = currentTrack.elements
- .filter((currentElement) => {
- if (
- currentElement.id === elementId &&
- currentTrack.id === trackId
- ) {
- return false;
- }
- return true;
- })
- .map((currentElement) => {
- if (!shouldApplyRipple) {
- return currentElement;
- }
-
- if (currentElement.startTime >= elementEndTime) {
- return {
- ...currentElement,
- startTime: Math.max(
- 0,
- currentElement.startTime - elementDuration
- ),
- };
- }
- return currentElement;
- });
-
- const hasOverlaps = checkElementOverlaps(updatedElements);
- if (hasOverlaps) {
- const resolvedElements = resolveElementOverlaps(updatedElements);
- return { ...currentTrack, elements: resolvedElements };
- }
-
- return { ...currentTrack, elements: updatedElements };
- })
- .filter((track) => track.elements.length > 0 || track.isMain);
-
- updateTracksAndSave(updatedTracks);
- },
-
- moveElementToTrack: (fromTrackId, toTrackId, elementId) => {
- get().pushHistory();
-
- const fromTrack = get()._tracks.find((track) => track.id === fromTrackId);
- const toTrack = get()._tracks.find((track) => track.id === toTrackId);
- const elementToMove = fromTrack?.elements.find(
- (element) => element.id === elementId
- );
-
- if (!elementToMove || !toTrack) return;
-
- const validation = validateElementTrackCompatibility(
- elementToMove,
- toTrack
- );
- if (!validation.isValid) {
- console.error(validation.errorMessage);
- return;
- }
-
- const newTracks = get()
- ._tracks.map((track) => {
- if (track.id === fromTrackId) {
- return {
- ...track,
- elements: track.elements.filter(
- (element) => element.id !== elementId
- ),
- };
- }
- if (track.id === toTrackId) {
- return {
- ...track,
- elements: [...track.elements, elementToMove],
- };
- }
- return track;
- })
- .filter((track) => track.elements.length > 0);
-
- updateTracksAndSave(newTracks);
- },
-
- updateElementTrim: (
- trackId,
- elementId,
- trimStart,
- trimEnd,
- pushHistory = true
- ) => {
- if (pushHistory) get().pushHistory();
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((element) =>
- element.id === elementId
- ? { ...element, trimStart, trimEnd }
- : element
- ),
- }
- : track
- )
- );
- },
-
- updateElementDuration: (
- trackId,
- elementId,
- duration,
- pushHistory = true
- ) => {
- if (pushHistory) get().pushHistory();
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((element) =>
- element.id === elementId ? { ...element, duration } : element
- ),
- }
- : track
- )
- );
- },
-
- updateElementStartTime: (
- trackId,
- elementId,
- startTime,
- pushHistory = true
- ) => {
- if (pushHistory) get().pushHistory();
- const clampedStartTime = Math.max(0, startTime);
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((element) =>
- element.id === elementId
- ? { ...element, startTime: clampedStartTime }
- : element
- ),
- }
- : track
- )
- );
- },
-
- updateElementStartTimeWithRipple: (trackId, elementId, newStartTime) => {
- const { _tracks, rippleEditingEnabled } = get();
-
- if (!rippleEditingEnabled) {
- get().updateElementStartTime(trackId, elementId, newStartTime);
- return;
- }
-
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((e) => e.id === elementId);
-
- if (!element || !track) return;
-
- get().pushHistory();
-
- const oldStartTime = element.startTime;
- const oldEndTime =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
- const newEndTime =
- newStartTime + (element.duration - element.trimStart - element.trimEnd);
- const timeDelta = newStartTime - oldStartTime;
-
- const updatedTracks = _tracks.map((currentTrack) => {
- const shouldApplyRipple = currentTrack.id === trackId;
-
- const updatedElements = currentTrack.elements.map((currentElement) => {
- if (currentElement.id === elementId && currentTrack.id === trackId) {
- return { ...currentElement, startTime: Math.max(0, newStartTime) };
- }
-
- if (!shouldApplyRipple) {
- return currentElement;
- }
-
- const currentElementStart = currentElement.startTime;
- const currentElementEnd =
- currentElement.startTime +
- (currentElement.duration -
- currentElement.trimStart -
- currentElement.trimEnd);
-
- if (timeDelta > 0) {
- if (currentElementStart >= oldEndTime) {
- return {
- ...currentElement,
- startTime: currentElementStart + timeDelta,
- };
- }
- } else if (timeDelta < 0) {
- if (
- currentElementStart >= newEndTime &&
- currentElementStart >= oldStartTime
- ) {
- return {
- ...currentElement,
- startTime: Math.max(0, currentElementStart + timeDelta),
- };
- }
- }
-
- return currentElement;
- });
-
- const hasOverlaps = checkElementOverlaps(updatedElements);
- if (hasOverlaps) {
- const resolvedElements = resolveElementOverlaps(updatedElements);
- return { ...currentTrack, elements: resolvedElements };
- }
-
- return { ...currentTrack, elements: updatedElements };
- });
-
- updateTracksAndSave(updatedTracks);
- },
-
- toggleTrackMute: (trackId) => {
- get().pushHistory();
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId ? { ...track, muted: !track.muted } : track
- )
- );
- },
-
- updateTextElement: (trackId, elementId, updates) => {
- get().pushHistory();
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((element) =>
- element.id === elementId && element.type === "text"
- ? { ...element, ...updates }
- : element
- ),
- }
- : track
- )
- );
- },
-
- // Split element and keep only the left portion
- splitAndKeepLeft: (trackId, elementId, splitTime) => {
- const { _tracks } = get();
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((c) => c.id === elementId);
-
- if (!element) return;
-
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
-
- if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return;
-
- get().pushHistory();
-
- const relativeTime = splitTime - element.startTime;
- const durationToRemove =
- element.duration - element.trimStart - element.trimEnd - relativeTime;
-
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((c) =>
- c.id === elementId
- ? {
- ...c,
- trimEnd: c.trimEnd + durationToRemove,
- name: getElementNameWithSuffix(c.name, "left"),
- }
- : c
- ),
- }
- : track
- )
- );
- },
-
- // Split element and keep only the right portion
- splitAndKeepRight: (trackId, elementId, splitTime) => {
- const { _tracks } = get();
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((c) => c.id === elementId);
-
- if (!element) return;
-
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
-
- if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return;
+export const useTimelineStore = create<TimelineStore>((set) => ({
+ snappingEnabled: true,
- get().pushHistory();
+ toggleSnapping: () => {
+ set((state) => ({ snappingEnabled: !state.snappingEnabled }));
+ },
- const relativeTime = splitTime - element.startTime;
+ rippleEditingEnabled: false,
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((c) =>
- c.id === elementId
- ? {
- ...c,
- startTime: splitTime,
- trimStart: c.trimStart + relativeTime,
- name: getElementNameWithSuffix(c.name, "right"),
- }
- : c
- ),
- }
- : track
- )
- );
- },
+ toggleRippleEditing: () => {
+ set((state) => ({
+ rippleEditingEnabled: !state.rippleEditingEnabled,
+ }));
+ },
- // Extract audio from video element to an audio track
- separateAudio: (trackId, elementId) => {
- const { _tracks } = get();
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((c) => c.id === elementId);
+ clipboard: null,
- if (!element || track?.type !== "media") return null;
-
- get().pushHistory();
-
- const existingAudioTrack = _tracks.find((t) => t.type === "audio");
- const audioElementId = generateUUID();
-
- if (existingAudioTrack) {
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === existingAudioTrack.id
- ? {
- ...track,
- elements: [
- ...track.elements,
- {
- ...element,
- id: audioElementId,
- name: getElementNameWithSuffix(element.name, "audio"),
- },
- ],
- }
- : track
- )
- );
- } else {
- const newAudioTrack: TimelineTrack = {
- id: generateUUID(),
- name: "Audio Track",
- type: "audio",
- elements: [
- {
- ...element,
- id: audioElementId,
- name: getElementNameWithSuffix(element.name, "audio"),
- },
- ],
- muted: false,
- };
-
- updateTracksAndSave([...get()._tracks, newAudioTrack]);
- }
-
- return audioElementId;
- },
-
- // Replace media for an element
- replaceElementMedia: async (
- trackId: string,
- elementId: string,
- newFile: File
- ): Promise<{ success: boolean; error?: string }> => {
- const { _tracks } = get();
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((c) => c.id === elementId);
-
- if (!element) {
- return { success: false, error: "Timeline element not found" };
- }
-
- if (element.type !== "media") {
- return {
- success: false,
- error: "Replace is only available for media clips",
- };
- }
-
- try {
- const mediaStore = useMediaStore.getState();
- const projectStore = useProjectStore.getState();
-
- if (!projectStore.activeProject) {
- return { success: false, error: "No active project found" };
- }
-
- const {
- getFileType,
- getImageDimensions,
- generateVideoThumbnail,
- getMediaDuration,
- } = await import("./media-store");
-
- const fileType = getFileType(newFile);
- if (!fileType) {
- return {
- success: false,
- error:
- "Unsupported file type. Please select a video, audio, or image file.",
- };
- }
-
- const mediaData: Omit<MediaFile, "id"> = {
- name: newFile.name,
- type: fileType as MediaType,
- file: newFile,
- url: URL.createObjectURL(newFile),
- };
-
- try {
- if (fileType === "image") {
- const { width, height } = await getImageDimensions(newFile);
- mediaData.width = width;
- mediaData.height = height;
- } else if (fileType === "video") {
- const [duration, { thumbnailUrl, width, height }] =
- await Promise.all([
- getMediaDuration(newFile),
- generateVideoThumbnail(newFile),
- ]);
- mediaData.duration = duration;
- mediaData.thumbnailUrl = thumbnailUrl;
- mediaData.width = width;
- mediaData.height = height;
- } else if (fileType === "audio") {
- mediaData.duration = await getMediaDuration(newFile);
- }
- } catch (error) {
- return {
- success: false,
- error: `Failed to process ${fileType} file: ${error instanceof Error ? error.message : "Unknown error"}`,
- };
- }
-
- try {
- await mediaStore.addMediaFile(
- projectStore.activeProject.id,
- mediaData
- );
- } catch (error) {
- return {
- success: false,
- error: `Failed to add media to project: ${error instanceof Error ? error.message : "Unknown error"}`,
- };
- }
-
- const newMediaItem = mediaStore.mediaFiles.find(
- (item) => item.file === newFile
- );
-
- if (!newMediaItem) {
- return {
- success: false,
- error: "Failed to create media item in project. Please try again.",
- };
- }
-
- get().pushHistory();
-
- updateTracksAndSave(
- _tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((c) =>
- c.id === elementId
- ? {
- ...c,
- mediaId: newMediaItem.id,
- name: newMediaItem.name,
- duration: newMediaItem.duration || c.duration,
- }
- : c
- ),
- }
- : track
- )
- );
-
- return { success: true };
- } catch (error) {
- console.error("Failed to replace element media:", error);
- return {
- success: false,
- error: `Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`,
- };
- }
- },
-
- getTotalDuration: () => {
- const { _tracks } = get();
- if (_tracks.length === 0) return 0;
-
- const trackEndTimes = _tracks.map((track) =>
- track.elements.reduce((maxEnd, element) => {
- const elementEnd =
- element.startTime +
- element.duration -
- element.trimStart -
- element.trimEnd;
- return Math.max(maxEnd, elementEnd);
- }, 0)
- );
-
- return Math.max(...trackEndTimes, 0);
- },
-
- getProjectThumbnail: async (projectId) => {
- try {
- const project = await storageService.loadProject({ id: projectId });
- if (!project) return null;
-
- // For scene-based projects, use main scene timeline
- // For legacy projects, use legacy timeline format
- let sceneId: string | undefined;
- if (project.scenes && project.scenes.length > 0) {
- const mainScene = project.scenes.find((s) => s.isMain);
- sceneId = mainScene?.id;
- }
-
- const tracks = await storageService.loadTimeline({
- projectId,
- sceneId,
- });
- const mediaItems = await storageService.loadAllMediaFiles({
- projectId,
- });
-
- if (!tracks || !mediaItems.length) return null;
-
- const firstMediaElement = tracks
- .flatMap((track) => track.elements)
- .filter((element) => element.type === "media")
- .sort((a, b) => a.startTime - b.startTime)[0];
-
- if (!firstMediaElement) return null;
-
- const mediaFile = mediaItems.find(
- (item) => item.id === firstMediaElement.mediaId
- );
- if (!mediaFile) return null;
-
- if (mediaFile.type === "video" && mediaFile.file) {
- const { generateVideoThumbnail } = await import(
- "@/stores/media-store"
- );
- const { thumbnailUrl } = await generateVideoThumbnail(mediaFile.file);
- return thumbnailUrl;
- }
- if (mediaFile.type === "image" && mediaFile.url) {
- return mediaFile.url;
- }
-
- return null;
- } catch (error) {
- console.error("Failed to get project thumbnail:", error);
- return null;
- }
- },
-
- redo: () => {
- const { redoStack } = get();
- if (redoStack.length === 0) return;
- const next = redoStack[redoStack.length - 1];
- updateTracksAndSave(next);
- set({ redoStack: redoStack.slice(0, -1) });
- },
-
- dragState: {
- isDragging: false,
- elementId: null,
- trackId: null,
- startMouseX: 0,
- startElementTime: 0,
- clickOffsetTime: 0,
- currentTime: 0,
- },
-
- setDragState: (dragState) =>
- set((state) => ({
- dragState: { ...state.dragState, ...dragState },
- })),
-
- startDrag: (
- elementId,
- trackId,
- startMouseX,
- startElementTime,
- clickOffsetTime
- ) => {
- set({
- dragState: {
- isDragging: true,
- elementId,
- trackId,
- startMouseX,
- startElementTime,
- clickOffsetTime,
- currentTime: startElementTime,
- },
- });
- },
-
- updateDragTime: (currentTime) => {
- set((state) => ({
- dragState: {
- ...state.dragState,
- currentTime,
- },
- }));
- },
-
- endDrag: () => {
- set({
- dragState: {
- isDragging: false,
- elementId: null,
- trackId: null,
- startMouseX: 0,
- startElementTime: 0,
- clickOffsetTime: 0,
- currentTime: 0,
- },
- });
- },
-
- loadProjectTimeline: async ({
- projectId,
- sceneId,
- }: {
- projectId: string;
- sceneId?: string;
- }) => {
- try {
- const tracks = await storageService.loadTimeline({
- projectId,
- sceneId,
- });
-
- if (tracks) {
- updateTracks(tracks);
- } else {
- const defaultTracks = ensureMainTrack([]);
- updateTracks(defaultTracks);
- }
- set({ history: [], redoStack: [] });
- } catch (error) {
- console.error("Failed to load timeline:", error);
- const defaultTracks = ensureMainTrack([]);
- updateTracks(defaultTracks);
- set({ history: [], redoStack: [] });
- }
- },
-
- saveProjectTimeline: async ({
- projectId,
- sceneId,
- }: {
- projectId: string;
- sceneId?: string;
- }) => {
- const { _tracks } = get();
- try {
- await storageService.saveTimeline({
- projectId,
- tracks: _tracks,
- sceneId,
- });
- } catch (error) {
- console.error("Failed to save timeline:", error);
- }
- },
-
- clearTimeline: () => {
- const defaultTracks = ensureMainTrack([]);
- updateTracks(defaultTracks);
- set({
- history: [],
- redoStack: [],
- selectedElements: [],
- clipboard: null,
- });
- },
-
- // Snapping actions
- toggleSnapping: () => {
- set((state) => ({ snappingEnabled: !state.snappingEnabled }));
- },
-
- // Ripple editing functions
- toggleRippleEditing: () => {
- set((state) => ({
- rippleEditingEnabled: !state.rippleEditingEnabled,
- }));
- },
-
- checkElementOverlap: (trackId, startTime, duration, excludeElementId) => {
- const track = get()._tracks.find((t) => t.id === trackId);
- if (!track) return false;
-
- const overlap = track.elements.some((element) => {
- const elementEnd =
- element.startTime +
- element.duration -
- element.trimStart -
- element.trimEnd;
-
- if (element.id === excludeElementId) {
- return false;
- }
-
- return (
- (startTime >= element.startTime && startTime < elementEnd) ||
- (startTime + duration > element.startTime &&
- startTime + duration <= elementEnd) ||
- (startTime < element.startTime && startTime + duration > elementEnd)
- );
- });
- return overlap;
- },
-
- findOrCreateTrack: (trackType) => {
- if (trackType === "text") {
- return get().insertTrackAt(trackType, 0);
- }
-
- const existingTrack = get()._tracks.find((t) => t.type === trackType);
- if (existingTrack) {
- return existingTrack.id;
- }
-
- return get().addTrack(trackType);
- },
-
- addElementAtTime: (item: MediaFile | TextElement, currentTime = 0) => {
- if (item.type === "text") {
- const targetTrackId = get().insertTrackAt("text", 0);
- get().addElementToTrack(
- targetTrackId,
- buildTextElement(item, currentTime)
- );
- return true;
- }
-
- const media = item as MediaFile;
- const trackType = media.type === "audio" ? "audio" : "media";
- const targetTrackId = get().insertTrackAt(trackType, 0);
- get().addElementToTrack(targetTrackId, {
- type: "media",
- mediaId: media.id,
- name: media.name,
- duration: media.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION,
- startTime: currentTime,
- trimStart: 0,
- trimEnd: 0,
- muted: false,
- });
- return true;
- },
-
- addElementToNewTrack: (item) => {
- if (item.type === "text") {
- const targetTrackId = get().insertTrackAt("text", 0);
- get().addElementToTrack(
- targetTrackId,
- buildTextElement(item as TextElement | DragData, 0)
- );
- return true;
- }
-
- const media = item as MediaFile;
- const trackType = media.type === "audio" ? "audio" : "media";
- const targetTrackId = get().insertTrackAt(trackType, 0);
- get().addElementToTrack(targetTrackId, {
- type: "media",
- mediaId: media.id,
- name: media.name,
- duration: media.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION,
- startTime: 0,
- trimStart: 0,
- trimEnd: 0,
- muted: false,
- });
- return true;
- },
-
- copySelected: () => {
- const { selectedElements, _tracks } = get();
- if (selectedElements.length === 0) return;
-
- const items: Array<{
- trackType: TrackType;
- element: CreateTimelineElement;
- }> = [];
-
- for (const { trackId, elementId } of selectedElements) {
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((e) => e.id === elementId);
- if (!track || !element) continue;
-
- // Prepare a creation-friendly copy without id
- const { id: _id, ...rest } = element as TimelineElement;
- items.push({
- trackType: track.type,
- element: rest as CreateTimelineElement,
- });
- }
-
- set({ clipboard: { items } });
- },
-
- pasteAtTime: (time) => {
- const { clipboard } = get();
- if (!clipboard || clipboard.items.length === 0) return;
-
- // Determine reference start time offset based on earliest element in clipboard
- const minStart = Math.min(
- ...clipboard.items.map((x) => x.element.startTime)
- );
-
- get().pushHistory();
-
- for (const item of clipboard.items) {
- const targetTrackId = get().findOrCreateTrack(item.trackType);
- const relativeOffset = item.element.startTime - minStart;
- const startTime = Math.max(0, time + relativeOffset);
-
- // Ensure no overlap on target track
- const duration =
- item.element.duration - item.element.trimStart - item.element.trimEnd;
- const hasOverlap = get().checkElementOverlap(
- targetTrackId,
- startTime,
- duration
- );
- if (hasOverlap) {
- // If overlap, nudge forward slightly until free (simple resolve)
- let candidate = startTime;
- let safety = 0;
- while (
- get().checkElementOverlap(targetTrackId, candidate, duration) &&
- safety < 1000
- ) {
- candidate += 0.01;
- safety += 1;
- }
- get().addElementToTrack(targetTrackId, {
- ...item.element,
- startTime: candidate,
- });
- } else {
- get().addElementToTrack(targetTrackId, {
- ...item.element,
- startTime,
- });
- }
- }
- },
-
- deleteSelected: (trackId?: string, elementId?: string) => {
- const { selectedElements, rippleEditingEnabled } = get();
-
- const elementsToDelete =
- trackId && elementId
- ? [{ trackId, elementId }]
- : selectedElements.length > 0
- ? selectedElements
- : [];
-
- if (elementsToDelete.length === 0) return;
-
- get().pushHistory();
-
- if (rippleEditingEnabled) {
- for (const { trackId: tId, elementId: eId } of elementsToDelete) {
- get().removeElementFromTrackWithRipple(tId, eId, false);
- }
- } else {
- updateTracksAndSave(
- get()
- ._tracks.map((track) => ({
- ...track,
- elements: track.elements.filter(
- (element) =>
- !elementsToDelete.some(
- ({ trackId: tId, elementId: eId }) =>
- track.id === tId && element.id === eId
- )
- ),
- }))
- .filter((track) => track.elements.length > 0)
- );
- }
-
- get().clearSelectedElements();
- },
-
- splitSelected: (splitTime, trackId?: string, elementId?: string) => {
- const { selectedElements, _tracks } = get();
-
- const elementsToProcess =
- trackId && elementId
- ? [{ trackId, elementId }]
- : selectedElements.length > 0
- ? selectedElements
- : [];
-
- if (elementsToProcess.length === 0) return;
-
- const elementsToSplit: Array<{
- trackId: string;
- elementId: string;
- element: TimelineElement;
- }> = [];
-
- for (const { trackId: tId, elementId: eId } of elementsToProcess) {
- const track = _tracks.find((t) => t.id === tId);
- const element = track?.elements.find((e) => e.id === eId);
- if (!track || !element) continue;
-
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
-
- if (splitTime > effectiveStart && splitTime < effectiveEnd) {
- elementsToSplit.push({ trackId: tId, elementId: eId, element });
- }
- }
-
- if (elementsToSplit.length === 0) {
- const { toast } = require("sonner");
- const isMultiple = elementsToProcess.length > 1;
- toast.error(
- isMultiple
- ? "Playhead must be within all selected elements to split"
- : "Playhead must be within element to split"
- );
- return;
- }
-
- get().pushHistory();
-
- updateTracksAndSave(
- get()._tracks.map((track) => {
- const elementsToSplitInTrack = elementsToSplit.filter(
- ({ trackId: tId }) => tId === track.id
- );
-
- if (elementsToSplitInTrack.length === 0) return track;
-
- return {
- ...track,
- elements: track.elements.flatMap((c) => {
- const elementToSplit = elementsToSplitInTrack.find(
- ({ elementId: eId }) => eId === c.id
- );
-
- if (!elementToSplit) return [c];
-
- const relativeTime = splitTime - elementToSplit.element.startTime;
- const firstDuration = relativeTime;
- const secondDuration =
- elementToSplit.element.duration -
- elementToSplit.element.trimStart -
- elementToSplit.element.trimEnd -
- relativeTime;
-
- const secondElementId = generateUUID();
-
- return [
- {
- ...c,
- trimEnd: c.trimEnd + secondDuration,
- name: getElementNameWithSuffix(c.name, "left"),
- },
- {
- ...c,
- id: secondElementId,
- startTime: splitTime,
- trimStart: c.trimStart + firstDuration,
- name: getElementNameWithSuffix(c.name, "right"),
- },
- ];
- }),
- };
- })
- );
- },
-
- toggleSelectedHidden: (trackId?: string, elementId?: string) => {
- const { selectedElements, _tracks } = get();
-
- const elementsToProcess =
- trackId && elementId
- ? [{ trackId, elementId }]
- : selectedElements.length > 0
- ? selectedElements
- : [];
-
- if (elementsToProcess.length === 0) return;
-
- get().pushHistory();
-
- const shouldHide = elementsToProcess.some(
- ({ trackId: tId, elementId: eId }) => {
- const track = _tracks.find((t) => t.id === tId);
- const element = track?.elements.find((e) => e.id === eId);
- return element && !element.hidden;
- }
- );
-
- updateTracksAndSave(
- _tracks.map((track) => ({
- ...track,
- elements: track.elements.map((element) => {
- const shouldUpdate = elementsToProcess.some(
- ({ trackId: tId, elementId: eId }) =>
- track.id === tId && element.id === eId
- );
- return shouldUpdate && element.hidden !== shouldHide
- ? { ...element, hidden: shouldHide }
- : element;
- }),
- }))
- );
- },
-
- toggleSelectedMuted: (trackId?: string, elementId?: string) => {
- const { selectedElements, _tracks } = get();
-
- const elementsToProcess =
- trackId && elementId
- ? [{ trackId, elementId }]
- : selectedElements.length > 0
- ? selectedElements
- : [];
-
- if (elementsToProcess.length === 0) return;
-
- get().pushHistory();
-
- const audioElements = elementsToProcess.filter(
- ({ trackId: tId, elementId: eId }) => {
- const track = _tracks.find((t) => t.id === tId);
- const element = track?.elements.find((e) => e.id === eId);
- return element?.type === "media";
- }
- );
-
- if (audioElements.length === 0) return;
-
- const shouldMute = audioElements.some(
- ({ trackId: tId, elementId: eId }) => {
- const track = _tracks.find((t) => t.id === tId);
- const element = track?.elements.find((e) => e.id === eId);
- return element?.type === "media" && !element.muted;
- }
- );
-
- updateTracksAndSave(
- _tracks.map((track) => ({
- ...track,
- elements: track.elements.map((element) => {
- const shouldUpdate = audioElements.some(
- ({ trackId: tId, elementId: eId }) =>
- track.id === tId && element.id === eId
- );
- return shouldUpdate &&
- element.type === "media" &&
- element.muted !== shouldMute
- ? { ...element, muted: shouldMute }
- : element;
- }),
- }))
- );
- },
-
- duplicateElement: (trackId, elementId) => {
- const { _tracks } = get();
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((e) => e.id === elementId);
- if (!track || !element) return;
-
- const { id, ...elementWithoutId } = element;
- const effectiveDuration =
- element.duration - element.trimStart - element.trimEnd;
-
- get().addElementToTrack(trackId, {
- ...elementWithoutId,
- name: `${element.name} (copy)`,
- startTime: element.startTime + effectiveDuration + 0.1,
- } as CreateTimelineElement);
- },
-
- revealElementInMedia: (elementId) => {
- const {
- useMediaPanelStore,
- } = require("../components/editor/media-panel/store");
- const { requestRevealMedia } = useMediaPanelStore.getState();
-
- const { _tracks } = get();
- const element = _tracks
- .flatMap((track) => track.elements)
- .find((el) => el.id === elementId);
-
- if (element?.type === "media") {
- requestRevealMedia(element.mediaId);
- }
- },
-
- replaceElementWithFile: async (trackId, elementId, file) => {
- try {
- const result = await get().replaceElementMedia(
- trackId,
- elementId,
- file
- );
- if (result.success) {
- const { toast } = await import("sonner");
- toast.success("Clip replaced successfully");
- } else {
- const { toast } = await import("sonner");
- toast.error(result.error || "Failed to replace clip");
- }
- } catch (error) {
- console.error("Unexpected error replacing clip:", error);
- const { toast } = await import("sonner");
- toast.error(
- `Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`
- );
- }
- },
-
- getContextMenuState: (trackId, elementId) => {
- const { selectedElements, _tracks } = get();
- const { currentTime } = usePlaybackStore.getState();
- const { mediaFiles } = useMediaStore.getState();
-
- const isMultipleSelected = selectedElements.length > 1;
- const isCurrentElementSelected = selectedElements.some(
- (sel) => sel.trackId === trackId && sel.elementId === elementId
- );
-
- const hasAudioElements = selectedElements.some(
- ({ trackId: tId, elementId: eId }) => {
- const selectedTrack = _tracks.find((t) => t.id === tId);
- const selectedElement = selectedTrack?.elements.find(
- (e) => e.id === eId
- );
- if (selectedElement?.type !== "media") return false;
- const mediaElement = selectedElement as MediaElement;
- const mediaItem = mediaFiles.find(
- (file: MediaFile) => file.id === mediaElement.mediaId
- );
- return mediaItem?.type === "audio" || mediaItem?.type === "video";
- }
- );
-
- const canSplitSelected = selectedElements.every(
- ({ trackId: tId, elementId: eId }) => {
- const selectedTrack = _tracks.find((t) => t.id === tId);
- const selectedElement = selectedTrack?.elements.find(
- (e) => e.id === eId
- );
- if (!selectedElement) return false;
- const effectiveStart = selectedElement.startTime;
- const effectiveEnd =
- selectedElement.startTime +
- (selectedElement.duration -
- selectedElement.trimStart -
- selectedElement.trimEnd);
- return currentTime > effectiveStart && currentTime < effectiveEnd;
- }
- );
-
- return {
- isMultipleSelected,
- isCurrentElementSelected,
- hasAudioElements,
- canSplitSelected,
- currentTime,
- };
- },
- };
-});
-
-function buildTextElement(
- raw: TextElement | DragData,
- startTime: number
-): CreateTimelineElement {
- const t = raw as Partial<TextElement>;
-
- return {
- type: "text",
- name: t.name ?? DEFAULT_TEXT_ELEMENT.name,
- content: t.content ?? DEFAULT_TEXT_ELEMENT.content,
- duration: t.duration ?? TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
- startTime,
- trimStart: 0,
- trimEnd: 0,
- fontSize:
- typeof t.fontSize === "number"
- ? t.fontSize
- : DEFAULT_TEXT_ELEMENT.fontSize,
- fontFamily: t.fontFamily ?? DEFAULT_TEXT_ELEMENT.fontFamily,
- color: t.color ?? DEFAULT_TEXT_ELEMENT.color,
- backgroundColor: t.backgroundColor ?? DEFAULT_TEXT_ELEMENT.backgroundColor,
- textAlign: t.textAlign ?? DEFAULT_TEXT_ELEMENT.textAlign,
- fontWeight: t.fontWeight ?? DEFAULT_TEXT_ELEMENT.fontWeight,
- fontStyle: t.fontStyle ?? DEFAULT_TEXT_ELEMENT.fontStyle,
- textDecoration: t.textDecoration ?? DEFAULT_TEXT_ELEMENT.textDecoration,
- x: typeof t.x === "number" ? t.x : DEFAULT_TEXT_ELEMENT.x,
- y: typeof t.y === "number" ? t.y : DEFAULT_TEXT_ELEMENT.y,
- rotation:
- typeof t.rotation === "number"
- ? t.rotation
- : DEFAULT_TEXT_ELEMENT.rotation,
- opacity:
- typeof t.opacity === "number" ? t.opacity : DEFAULT_TEXT_ELEMENT.opacity,
- };
-}
+ setClipboard: (clipboard) => {
+ set({ clipboard });
+ },
+}));
diff --git a/apps/web/src/types/assets.ts b/apps/web/src/types/assets.ts
new file mode 100644
index 00000000..c8dddfc7
--- /dev/null
+++ b/apps/web/src/types/assets.ts
@@ -0,0 +1,9 @@
+import type { MediaAssetData } from "@/services/storage/types";
+
+export type MediaType = "image" | "video" | "audio";
+
+export interface MediaAsset
+ extends Omit<MediaAssetData, "size" | "lastModified"> {
+ file: File;
+ url?: string;
+}
diff --git a/apps/web/src/types/blog.ts b/apps/web/src/types/blog.ts
new file mode 100644
index 00000000..16795b02
--- /dev/null
+++ b/apps/web/src/types/blog.ts
@@ -0,0 +1,92 @@
+export type Post = {
+ id: string;
+ slug: string;
+ title: string;
+ content: string;
+ description: string;
+ coverImage: string;
+ publishedAt: Date;
+ updatedAt: Date;
+ authors: {
+ id: string;
+ name: string;
+ image: string;
+ }[];
+ category: {
+ id: string;
+ slug: string;
+ name: string;
+ };
+ tags: {
+ id: string;
+ slug: string;
+ name: string;
+ }[];
+ attribution: {
+ author: string;
+ url: string;
+ } | null;
+};
+
+export type Pagination = {
+ limit: number;
+ currpage: number;
+ nextPage: number | null;
+ prevPage: number | null;
+ totalItems: number;
+ totalPages: number;
+};
+
+export type MarblePostList = {
+ posts: Post[];
+ pagination: Pagination;
+};
+
+export type MarblePost = {
+ post: Post;
+};
+
+export type Tag = {
+ id: string;
+ name: string;
+ slug: string;
+};
+
+export type MarbleTag = {
+ tag: Tag;
+};
+
+export type MarbleTagList = {
+ tags: Tag[];
+ pagination: Pagination;
+};
+
+export type Category = {
+ id: string;
+ name: string;
+ slug: string;
+};
+
+export type MarbleCategory = {
+ category: Category;
+};
+
+export type MarbleCategoryList = {
+ categories: Category[];
+ pagination: Pagination;
+};
+
+export type Author = {
+ id: string;
+ name: string;
+ image: string;
+};
+
+export type MarbleAuthor = {
+ author: Author;
+};
+
+export type MarbleAuthorList = {
+ authors: Author[];
+ pagination: Pagination;
+};
diff --git a/apps/web/src/types/drag.ts b/apps/web/src/types/drag.ts
new file mode 100644
index 00000000..e6e5daba
--- /dev/null
+++ b/apps/web/src/types/drag.ts
@@ -0,0 +1,21 @@
+interface BaseDragData {
+ id: string;
+ name: string;
+}
+
+export interface MediaDragData extends BaseDragData {
+ type: "media";
+ mediaType: "image" | "video" | "audio";
+}
+
+export interface TextDragData extends BaseDragData {
+ type: "text";
+ content: string;
+}
+
+export interface StickerDragData extends BaseDragData {
+ type: "sticker";
+ iconName: string;
+}
+
+export type TimelineDragData = MediaDragData | TextDragData | StickerDragData;
diff --git a/apps/web/src/types/editor.ts b/apps/web/src/types/editor.ts
index 6cbc6a6a..0413b208 100644
--- a/apps/web/src/types/editor.ts
+++ b/apps/web/src/types/editor.ts
@@ -1,28 +1 @@
-export type BackgroundType = "blur" | "mirror" | "color";
-
-export type CanvasMode = "preset" | "original" | "custom";
-
-export interface CanvasSize {
- width: number;
- height: number;
-}
-
-export interface CanvasPreset {
- name: string;
- width: number;
- height: number;
-}
-
-export interface TextElementDragState {
- isDragging: boolean;
- elementId: string | null;
- trackId: string | null;
- startX: number;
- startY: number;
- initialElementX: number;
- initialElementY: number;
- currentX: number;
- currentY: number;
- elementWidth: number;
- elementHeight: number;
-}
+export type TPlatformLayout = "tiktok";
diff --git a/apps/web/src/types/export.ts b/apps/web/src/types/export.ts
index 093268ce..351f5fc4 100644
--- a/apps/web/src/types/export.ts
+++ b/apps/web/src/types/export.ts
@@ -1,18 +1,27 @@
-export type ExportFormat = "mp4" | "webm";
-export type ExportQuality = "low" | "medium" | "high" | "very_high";
+export const EXPORT_QUALITY_VALUES = [
+ "low",
+ "medium",
+ "high",
+ "very_high",
+] as const;
+
+export const EXPORT_FORMAT_VALUES = ["mp4", "webm"] as const;
+
+export type ExportFormat = (typeof EXPORT_FORMAT_VALUES)[number];
+export type ExportQuality = (typeof EXPORT_QUALITY_VALUES)[number];
export interface ExportOptions {
- format: ExportFormat;
- quality: ExportQuality;
- fps?: number;
- includeAudio?: boolean;
- onProgress?: (progress: number) => void;
- onCancel?: () => boolean;
+ format: ExportFormat;
+ quality: ExportQuality;
+ fps?: number;
+ includeAudio?: boolean;
+ onProgress?: ({ progress }: { progress: number }) => void;
+ onCancel?: () => boolean;
}
export interface ExportResult {
- success: boolean;
- buffer?: ArrayBuffer;
- error?: string;
- cancelled?: boolean;
+ success: boolean;
+ buffer?: ArrayBuffer;
+ error?: string;
+ cancelled?: boolean;
}
diff --git a/apps/web/src/types/keybinding.ts b/apps/web/src/types/keybinding.ts
index 831f3abd..f9b84e31 100644
--- a/apps/web/src/types/keybinding.ts
+++ b/apps/web/src/types/keybinding.ts
@@ -1,73 +1,71 @@
-import { ActionWithOptionalArgs } from "@/constants/actions";
+import type { TActionWithOptionalArgs } from "@/lib/actions";
/**
* Alt is also regarded as macOS OPTION (⌥) key
* Ctrl is also regarded as macOS COMMAND (⌘) key (NOTE: this differs from HTML Keyboard spec where COMMAND is Meta key!)
*/
export type ModifierKeys =
- | "ctrl"
- | "alt"
- | "shift"
- | "ctrl+shift"
- | "alt+shift"
- | "ctrl+alt"
- | "ctrl+alt+shift";
+ | "ctrl"
+ | "alt"
+ | "shift"
+ | "ctrl+shift"
+ | "alt+shift"
+ | "ctrl+alt"
+ | "ctrl+alt+shift";
-/* eslint-disable prettier/prettier */
-// prettier-ignore
export type Key =
- | "a"
- | "b"
- | "c"
- | "d"
- | "e"
- | "f"
- | "g"
- | "h"
- | "i"
- | "j"
- | "k"
- | "l"
- | "m"
- | "n"
- | "o"
- | "p"
- | "q"
- | "r"
- | "s"
- | "t"
- | "u"
- | "v"
- | "w"
- | "x"
- | "y"
- | "z"
- | "0"
- | "1"
- | "2"
- | "3"
- | "4"
- | "5"
- | "6"
- | "7"
- | "8"
- | "9"
- | "up"
- | "down"
- | "left"
- | "right"
- | "/"
- | "?"
- | "."
- | "enter"
- | "tab"
- | "space"
- | "escape"
- | "esc"
- | "backspace"
- | "delete"
- | "home"
- | "end";
+ | "a"
+ | "b"
+ | "c"
+ | "d"
+ | "e"
+ | "f"
+ | "g"
+ | "h"
+ | "i"
+ | "j"
+ | "k"
+ | "l"
+ | "m"
+ | "n"
+ | "o"
+ | "p"
+ | "q"
+ | "r"
+ | "s"
+ | "t"
+ | "u"
+ | "v"
+ | "w"
+ | "x"
+ | "y"
+ | "z"
+ | "0"
+ | "1"
+ | "2"
+ | "3"
+ | "4"
+ | "5"
+ | "6"
+ | "7"
+ | "8"
+ | "9"
+ | "up"
+ | "down"
+ | "left"
+ | "right"
+ | "/"
+ | "?"
+ | "."
+ | "enter"
+ | "tab"
+ | "space"
+ | "escape"
+ | "esc"
+ | "backspace"
+ | "delete"
+ | "home"
+ | "end";
/* eslint-enable */
export type ModifierBasedShortcutKey = `${ModifierKeys}+${Key}`;
@@ -77,5 +75,5 @@ export type SingleCharacterShortcutKey = `${Key}`;
export type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey;
export type KeybindingConfig = {
- [key in ShortcutKey]?: ActionWithOptionalArgs;
+ [key in ShortcutKey]?: TActionWithOptionalArgs;
};
diff --git a/apps/web/src/types/language.ts b/apps/web/src/types/language.ts
new file mode 100644
index 00000000..a3f527c8
--- /dev/null
+++ b/apps/web/src/types/language.ts
@@ -0,0 +1,4 @@
+import type { LANGUAGES } from "@/constants/language-constants";
+
+export type Language = (typeof LANGUAGES)[number];
+export type LanguageCode = Language["code"];
diff --git a/apps/web/src/types/media.ts b/apps/web/src/types/media.ts
deleted file mode 100644
index 6e94fcff..00000000
--- a/apps/web/src/types/media.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export type MediaType = "image" | "video" | "audio";
-
-// What's stored in media library
-export interface MediaFile {
- id: string;
- name: string;
- type: MediaType;
- file: File;
- url?: string; // Object URL for preview
- thumbnailUrl?: string; // For video thumbnails
- duration?: number; // For video/audio duration
- width?: number; // For video/image width
- height?: number; // For video/image height
- fps?: number; // For video frame rate
- // Ephemeral items are used by timeline directly and should not appear in the media library or be persisted
- ephemeral?: boolean;
-}
diff --git a/apps/web/src/types/playback.ts b/apps/web/src/types/playback.ts
deleted file mode 100644
index 8758ea03..00000000
--- a/apps/web/src/types/playback.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-export interface PlaybackState {
- isPlaying: boolean;
- currentTime: number;
- duration: number;
- volume: number;
- speed: number;
- muted: boolean;
- previousVolume?: number;
-}
-
-export interface PlaybackControls {
- play: () => void;
- pause: () => void;
- seek: (time: number) => void;
- setVolume: (volume: number) => void;
- setSpeed: (speed: number) => void;
- toggle: () => void;
- mute: () => void;
- unmute: () => void;
- toggleMute: () => void;
-}
diff --git a/apps/web/src/types/post.ts b/apps/web/src/types/post.ts
deleted file mode 100644
index fbee239e..00000000
--- a/apps/web/src/types/post.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-export type Post = {
- id: string;
- slug: string;
- title: string;
- content: string;
- description: string;
- coverImage: string;
- publishedAt: Date;
- updatedAt: Date;
- authors: {
- id: string;
- name: string;
- image: string;
- }[];
- category: {
- id: string;
- slug: string;
- name: string;
- };
- tags: {
- id: string;
- slug: string;
- name: string;
- }[];
- attribution: {
- author: string;
- url: string;
- } | null;
-};
-
-export type Pagination = {
- limit: number;
- currpage: number;
- nextPage: number | null;
- prevPage: number | null;
- totalItems: number;
- totalPages: number;
-};
-
-export type MarblePostList = {
- posts: Post[];
- pagination: Pagination;
-};
-
-export type MarblePost = {
- post: Post;
-};
-
-export type Tag = {
- id: string;
- name: string;
- slug: string;
-};
-
-export type MarbleTag = {
- tag: Tag;
-};
-
-export type MarbleTagList = {
- tags: Tag[];
- pagination: Pagination;
-};
-
-export type Category = {
- id: string;
- name: string;
- slug: string;
-};
-
-export type MarbleCategory = {
- category: Category;
-};
-
-export type MarbleCategoryList = {
- categories: Category[];
- pagination: Pagination;
-};
-
-export type Author = {
- id: string;
- name: string;
- image: string;
-};
-
-export type MarbleAuthor = {
- author: Author;
-};
-
-export type MarbleAuthorList = {
- authors: Author[];
- pagination: Pagination;
-};
diff --git a/apps/web/src/types/project.ts b/apps/web/src/types/project.ts
index f182653d..c8074c26 100644
--- a/apps/web/src/types/project.ts
+++ b/apps/web/src/types/project.ts
@@ -1,35 +1,51 @@
-import { CanvasSize } from "./editor";
+import type { TScene } from "./timeline";
-export type BlurIntensity = 4 | 8 | 18;
+export type TBackground =
+ | {
+ type: "color";
+ color: string;
+ }
+ | {
+ type: "blur";
+ blurIntensity: number;
+ };
-export interface Scene {
- id: string;
- name: string;
- isMain: boolean;
- createdAt: Date;
- updatedAt: Date;
+export interface TCanvasSize {
+ width: number;
+ height: number;
+}
+
+export interface TProjectMetadata {
+ id: string;
+ name: string;
+ thumbnail?: string;
+ duration: number;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+export interface TProjectSettings {
+ fps: number;
+ canvasSize: TCanvasSize;
+ originalCanvasSize?: TCanvasSize | null;
+ background: TBackground;
+}
+
+export interface TTimelineViewState {
+ zoomLevel: number;
+ scrollLeft: number;
+ playheadTime: number;
}
export interface TProject {
- id: string;
- name: string;
- thumbnail: string;
- createdAt: Date;
- updatedAt: Date;
- scenes: Scene[];
- currentSceneId: string;
- mediaItems?: string[];
- backgroundColor?: string;
- backgroundType?: "color" | "blur";
- blurIntensity?: BlurIntensity;
- fps?: number;
- bookmarks?: number[];
- canvasSize: CanvasSize;
- /**
- * Tracks how the canvas size was set:
- * - "preset": User selected a standard preset (e.g. 16:9, 9:16)
- * - "original": Using the first media item's dimensions
- * - "custom": User set a custom aspect ratio or dimensions
- */
- canvasMode: "preset" | "original" | "custom";
+ metadata: TProjectMetadata;
+ scenes: TScene[];
+ currentSceneId: string;
+ settings: TProjectSettings;
+ version: number;
+ timelineViewState?: TTimelineViewState;
}
+
+export type TProjectSortKey = "createdAt" | "updatedAt" | "name" | "duration";
+export type TSortOrder = "asc" | "desc";
+export type TProjectSortOption = `${TProjectSortKey}-${TSortOrder}`;
diff --git a/apps/web/src/types/sounds.ts b/apps/web/src/types/sounds.ts
index faa485eb..f7526846 100644
--- a/apps/web/src/types/sounds.ts
+++ b/apps/web/src/types/sounds.ts
@@ -1,39 +1,39 @@
export interface SoundEffect {
- id: number;
- name: string;
- description: string;
- url: string;
- previewUrl?: string;
- downloadUrl?: string;
- duration: number;
- filesize: number;
- type: string;
- channels: number;
- bitrate: number;
- bitdepth: number;
- samplerate: number;
- username: string;
- tags: string[];
- license: string;
- created: string;
- downloads: number;
- rating: number;
- ratingCount: number;
+ id: number;
+ name: string;
+ description: string;
+ url: string;
+ previewUrl?: string;
+ downloadUrl?: string;
+ duration: number;
+ filesize: number;
+ type: string;
+ channels: number;
+ bitrate: number;
+ bitdepth: number;
+ samplerate: number;
+ username: string;
+ tags: string[];
+ license: string;
+ created: string;
+ downloads: number;
+ rating: number;
+ ratingCount: number;
}
export interface SavedSound {
- id: number; // freesound id
- name: string;
- username: string;
- previewUrl?: string;
- downloadUrl?: string;
- duration: number;
- tags: string[];
- license: string;
- savedAt: string; // iso date string
+ id: number; // freesound id
+ name: string;
+ username: string;
+ previewUrl?: string;
+ downloadUrl?: string;
+ duration: number;
+ tags: string[];
+ license: string;
+ savedAt: string; // iso date string
}
export interface SavedSoundsData {
- sounds: SavedSound[];
- lastModified: string;
+ sounds: SavedSound[];
+ lastModified: string;
}
diff --git a/apps/web/src/types/stickers.ts b/apps/web/src/types/stickers.ts
new file mode 100644
index 00000000..c6adfd7a
--- /dev/null
+++ b/apps/web/src/types/stickers.ts
@@ -0,0 +1,3 @@
+import type { STICKER_CATEGORIES } from "@/constants/stickers-constants";
+
+export type StickerCategory = (typeof STICKER_CATEGORIES)[number];
diff --git a/apps/web/src/types/time.ts b/apps/web/src/types/time.ts
new file mode 100644
index 00000000..4009a1e1
--- /dev/null
+++ b/apps/web/src/types/time.ts
@@ -0,0 +1 @@
+export type TTimeCode = "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF";
diff --git a/apps/web/src/types/timeline.ts b/apps/web/src/types/timeline.ts
index 61ba6af0..69d40518 100644
--- a/apps/web/src/types/timeline.ts
+++ b/apps/web/src/types/timeline.ts
@@ -1,166 +1,190 @@
-import { MediaType } from "@/types/media";
-import { generateUUID } from "@/lib/utils";
+export interface TScene {
+ id: string;
+ name: string;
+ isMain: boolean;
+ tracks: TimelineTrack[];
+ bookmarks: number[];
+ createdAt: Date;
+ updatedAt: Date;
+}
-export type TrackType = "media" | "text" | "audio";
+export type TrackType = "video" | "text" | "audio" | "sticker";
-// Base element properties
-interface BaseTimelineElement {
- id: string;
- name: string;
- duration: number;
- startTime: number;
- trimStart: number;
- trimEnd: number;
- hidden?: boolean;
+interface BaseTrack {
+ id: string;
+ name: string;
}
-// Media element that references MediaStore
-export interface MediaElement extends BaseTimelineElement {
- type: "media";
- mediaId: string;
- muted?: boolean;
+export interface VideoTrack extends BaseTrack {
+ type: "video";
+ elements: (VideoElement | ImageElement)[];
+ isMain: boolean;
+ muted: boolean;
+ hidden: boolean;
}
-// Text element with embedded text data
-export interface TextElement extends BaseTimelineElement {
- type: "text";
- content: string;
- fontSize: number;
- fontFamily: string;
- color: string;
- backgroundColor: string;
- textAlign: "left" | "center" | "right";
- fontWeight: "normal" | "bold";
- fontStyle: "normal" | "italic";
- textDecoration: "none" | "underline" | "line-through";
- x: number; // Position relative to canvas center
- y: number; // Position relative to canvas center
- rotation: number; // in degrees
- opacity: number; // 0-1
+export interface TextTrack extends BaseTrack {
+ type: "text";
+ elements: TextElement[];
+ hidden: boolean;
}
-// Typed timeline elements
-export type TimelineElement = MediaElement | TextElement;
+export interface AudioTrack extends BaseTrack {
+ type: "audio";
+ elements: AudioElement[];
+ muted: boolean;
+}
-// Creation types (without id, for addElementToTrack)
-export type CreateMediaElement = Omit<MediaElement, "id">;
-export type CreateTextElement = Omit<TextElement, "id">;
-export type CreateTimelineElement = CreateMediaElement | CreateTextElement;
+export interface StickerTrack extends BaseTrack {
+ type: "sticker";
+ elements: StickerElement[];
+ hidden: boolean;
+}
+
+export type TimelineTrack = VideoTrack | TextTrack | AudioTrack | StickerTrack;
-export interface TimelineElementProps {
- element: TimelineElement;
- track: TimelineTrack;
- zoomLevel: number;
- isSelected: boolean;
- onElementMouseDown: (e: React.MouseEvent, element: TimelineElement) => void;
- onElementClick: (e: React.MouseEvent, element: TimelineElement) => void;
+export interface Transform {
+ scale: number;
+ position: {
+ x: number;
+ y: number;
+ };
+ rotate: number;
}
-export interface ResizeState {
- elementId: string;
- side: "left" | "right";
- startX: number;
- initialTrimStart: number;
- initialTrimEnd: number;
+interface BaseAudioElement extends BaseTimelineElement {
+ type: "audio";
+ volume: number;
+ muted?: boolean;
+ buffer?: AudioBuffer;
}
-// Drag data types for type-safe drag and drop
-export interface MediaItemDragData {
- id: string;
- type: MediaType;
- name: string;
+export interface UploadAudioElement extends BaseAudioElement {
+ sourceType: "upload";
+ mediaId: string;
}
-export interface TextItemDragData {
- id: string;
- type: "text";
- name: string;
- content: string;
+export interface LibraryAudioElement extends BaseAudioElement {
+ sourceType: "library";
+ sourceUrl: string;
}
-export type DragData = MediaItemDragData | TextItemDragData;
+export type AudioElement = UploadAudioElement | LibraryAudioElement;
-export interface TimelineTrack {
- id: string;
- name: string;
- type: TrackType;
- elements: TimelineElement[];
- muted?: boolean;
- isMain?: boolean;
+interface BaseTimelineElement {
+ id: string;
+ name: string;
+ duration: number;
+ startTime: number;
+ trimStart: number;
+ trimEnd: number;
}
-export function sortTracksByOrder(tracks: TimelineTrack[]): TimelineTrack[] {
- return [...tracks].sort((a, b) => {
- // Text tracks always go to the top
- if (a.type === "text" && b.type !== "text") return -1;
- if (b.type === "text" && a.type !== "text") return 1;
-
- // Audio tracks always go to bottom
- if (a.type === "audio" && b.type !== "audio") return 1;
- if (b.type === "audio" && a.type !== "audio") return -1;
+export interface VideoElement extends BaseTimelineElement {
+ type: "video";
+ mediaId: string;
+ muted?: boolean;
+ hidden?: boolean;
+ transform: Transform;
+ opacity: number;
+}
- // Main track goes above audio but below text tracks
- if (a.isMain && !b.isMain && b.type !== "audio" && b.type !== "text")
- return 1;
- if (b.isMain && !a.isMain && a.type !== "audio" && a.type !== "text")
- return -1;
+export interface ImageElement extends BaseTimelineElement {
+ type: "image";
+ mediaId: string;
+ hidden?: boolean;
+ transform: Transform;
+ opacity: number;
+}
- // Within same category, maintain creation order
- return 0;
- });
+export interface TextElement extends BaseTimelineElement {
+ type: "text";
+ content: string;
+ fontSize: number;
+ fontFamily: string;
+ color: string;
+ backgroundColor: string;
+ textAlign: "left" | "center" | "right";
+ fontWeight: "normal" | "bold";
+ fontStyle: "normal" | "italic";
+ textDecoration: "none" | "underline" | "line-through";
+ hidden?: boolean;
+ transform: Transform;
+ opacity: number;
}
-export function getMainTrack(tracks: TimelineTrack[]): TimelineTrack | null {
- return tracks.find((track) => track.isMain) || null;
+export interface StickerElement extends BaseTimelineElement {
+ type: "sticker";
+ iconName: string;
+ hidden?: boolean;
+ transform: Transform;
+ opacity: number;
+ color?: string;
}
-export function ensureMainTrack(tracks: TimelineTrack[]): TimelineTrack[] {
- const hasMainTrack = tracks.some((track) => track.isMain);
+export type TimelineElement =
+ | AudioElement
+ | VideoElement
+ | ImageElement
+ | TextElement
+ | StickerElement;
- if (!hasMainTrack) {
- // Create main track if it doesn't exist
- const mainTrack: TimelineTrack = {
- id: generateUUID(),
- name: "Main Track",
- type: "media",
- elements: [],
- muted: false,
- isMain: true,
- };
- return [mainTrack, ...tracks];
- }
+export type ElementType = TimelineElement["type"];
- return tracks;
-}
+export type CreateUploadAudioElement = Omit<UploadAudioElement, "id">;
+export type CreateLibraryAudioElement = Omit<LibraryAudioElement, "id">;
+export type CreateAudioElement =
+ | CreateUploadAudioElement
+ | CreateLibraryAudioElement;
+export type CreateVideoElement = Omit<VideoElement, "id">;
+export type CreateImageElement = Omit<ImageElement, "id">;
+export type CreateTextElement = Omit<TextElement, "id">;
+export type CreateStickerElement = Omit<StickerElement, "id">;
+export type CreateTimelineElement =
+ | CreateAudioElement
+ | CreateVideoElement
+ | CreateImageElement
+ | CreateTextElement
+ | CreateStickerElement;
-// Timeline validation utilities
-export function canElementGoOnTrack(
- elementType: "text" | "media",
- trackType: TrackType
-): boolean {
- if (elementType === "text") {
- return trackType === "text";
- }
- if (elementType === "media") {
- return trackType === "media" || trackType === "audio";
- }
- return false;
-}
+// ---- Drag State ----
-export function validateElementTrackCompatibility(
- element: { type: "text" | "media" },
- track: { type: TrackType }
-): { isValid: boolean; errorMessage?: string } {
- const isValid = canElementGoOnTrack(element.type, track.type);
+export interface ElementDragState {
+ isDragging: boolean;
+ elementId: string | null;
+ trackId: string | null;
+ startMouseX: number;
+ startMouseY: number;
+ startElementTime: number;
+ clickOffsetTime: number;
+ currentTime: number;
+ currentMouseY: number;
+}
- if (!isValid) {
- const errorMessage =
- element.type === "text"
- ? "Text elements can only be placed on text tracks"
- : "Media elements can only be placed on media or audio tracks";
+export interface DropTarget {
+ trackIndex: number;
+ isNewTrack: boolean;
+ insertPosition: "above" | "below" | null;
+ xPosition: number;
+}
- return { isValid: false, errorMessage };
- }
+export interface ComputeDropTargetParams {
+ elementType: ElementType;
+ mouseX: number;
+ mouseY: number;
+ tracks: TimelineTrack[];
+ playheadTime: number;
+ isExternalDrop: boolean;
+ elementDuration: number;
+ pixelsPerSecond: number;
+ zoomLevel: number;
+ verticalDragDirection?: "up" | "down" | null;
+ startTimeOverride?: number;
+ excludeElementId?: string;
+}
- return { isValid: true };
+export interface ClipboardItem {
+ trackId: string;
+ trackType: TrackType;
+ element: CreateTimelineElement;
}
diff --git a/apps/web/src/types/transcription.ts b/apps/web/src/types/transcription.ts
new file mode 100644
index 00000000..f189c6c9
--- /dev/null
+++ b/apps/web/src/types/transcription.ts
@@ -0,0 +1,47 @@
+import type { LanguageCode } from "./language";
+
+export type TranscriptionLanguage = LanguageCode | "auto";
+
+export interface TranscriptionSegment {
+ text: string;
+ start: number;
+ end: number;
+}
+
+export interface TranscriptionResult {
+ text: string;
+ segments: TranscriptionSegment[];
+ language: string;
+}
+
+export type TranscriptionStatus =
+ | "idle"
+ | "loading-model"
+ | "transcribing"
+ | "complete"
+ | "error";
+
+export interface TranscriptionProgress {
+ status: TranscriptionStatus;
+ progress: number;
+ message?: string;
+}
+
+export type TranscriptionModelId =
+ | "whisper-tiny"
+ | "whisper-small"
+ | "whisper-medium"
+ | "whisper-large-v3-turbo";
+
+export interface TranscriptionModel {
+ id: TranscriptionModelId;
+ name: string;
+ huggingFaceId: string;
+ description: string;
+}
+
+export interface CaptionChunk {
+ text: string;
+ startTime: number;
+ duration: number;
+}
diff --git a/apps/web/src/utils/browser.ts b/apps/web/src/utils/browser.ts
new file mode 100644
index 00000000..2b9aa297
--- /dev/null
+++ b/apps/web/src/utils/browser.ts
@@ -0,0 +1,17 @@
+export function isTypableDOMElement({
+ element,
+}: {
+ element: HTMLElement;
+}): boolean {
+ if (element.isContentEditable) return true;
+
+ if (element.tagName === "INPUT") {
+ return !(element as HTMLInputElement).disabled;
+ }
+
+ if (element.tagName === "TEXTAREA") {
+ return !(element as HTMLTextAreaElement).disabled;
+ }
+
+ return false;
+}
diff --git a/apps/web/src/utils/date.ts b/apps/web/src/utils/date.ts
new file mode 100644
index 00000000..6a7e9ca5
--- /dev/null
+++ b/apps/web/src/utils/date.ts
@@ -0,0 +1,7 @@
+export function formatDate({ date }: { date: Date }): string {
+ return date.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ });
+}
diff --git a/apps/web/src/utils/geometry.ts b/apps/web/src/utils/geometry.ts
new file mode 100644
index 00000000..23fc4f46
--- /dev/null
+++ b/apps/web/src/utils/geometry.ts
@@ -0,0 +1,13 @@
+export function dimensionToAspectRatio({
+ width,
+ height,
+}: {
+ width: number;
+ height: number;
+}): string {
+ const gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b));
+ const divisor = gcd(width, height);
+ const aspectWidth = width / divisor;
+ const aspectHeight = height / divisor;
+ return `${aspectWidth}:${aspectHeight}`;
+}
diff --git a/apps/web/src/utils/id.ts b/apps/web/src/utils/id.ts
new file mode 100644
index 00000000..e452af43
--- /dev/null
+++ b/apps/web/src/utils/id.ts
@@ -0,0 +1,28 @@
+export function generateUUID(): string {
+ if (
+ typeof crypto !== "undefined" &&
+ typeof crypto.randomUUID === "function"
+ ) {
+ return crypto.randomUUID();
+ }
+
+ const bytes = new Uint8Array(16);
+ crypto.getRandomValues(bytes);
+
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
+
+ const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
+
+ return (
+ hex.slice(0, 4).join("") +
+ "-" +
+ hex.slice(4, 6).join("") +
+ "-" +
+ hex.slice(6, 8).join("") +
+ "-" +
+ hex.slice(8, 10).join("") +
+ "-" +
+ hex.slice(10, 16).join("")
+ );
+}
diff --git a/apps/web/src/utils/math.ts b/apps/web/src/utils/math.ts
new file mode 100644
index 00000000..012ac747
--- /dev/null
+++ b/apps/web/src/utils/math.ts
@@ -0,0 +1,11 @@
+export function clamp({
+ value,
+ min,
+ max,
+}: {
+ value: number;
+ min: number;
+ max: number;
+}): number {
+ return Math.max(min, Math.min(max, value));
+}
diff --git a/apps/web/src/utils/platform.ts b/apps/web/src/utils/platform.ts
new file mode 100644
index 00000000..3b1f748b
--- /dev/null
+++ b/apps/web/src/utils/platform.ts
@@ -0,0 +1,11 @@
+export function getPlatformSpecialKey(): string {
+ return isAppleDevice() ? "⌘" : "Ctrl";
+}
+
+export function getPlatformAlternateKey(): string {
+ return isAppleDevice() ? "⌥" : "Alt";
+}
+
+export function isAppleDevice(): boolean {
+ return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
+}
diff --git a/apps/web/src/utils/string.ts b/apps/web/src/utils/string.ts
new file mode 100644
index 00000000..893456ed
--- /dev/null
+++ b/apps/web/src/utils/string.ts
@@ -0,0 +1,7 @@
+export function capitalizeFirstLetter({ string }: { string: string }) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+export function uppercase({ string }: { string: string }) {
+ return string.toUpperCase();
+}
diff --git a/apps/web/src/utils/ui.ts b/apps/web/src/utils/ui.ts
new file mode 100644
index 00000000..a92aab43
--- /dev/null
+++ b/apps/web/src/utils/ui.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]): string {
+ return twMerge(clsx(inputs));
+}
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index 0a966d20..ee05eade 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -11,7 +11,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
- "jsx": "preserve",
+ "jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@@ -29,7 +29,10 @@
"apps/web/.next/types/**/*.ts",
"next-env.d.ts",
".next/types/**/*.ts",
- "src/types/**/*.d.ts"
+ "src/types/**/*.d.ts",
+ "../../use-frame-cache.ts",
+ "src/stores/timeline-store.ts",
+ ".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}
diff --git a/apps/web/tsconfig.tsbuildinfo b/apps/web/tsconfig.tsbuildinfo
index 37dfbac9..a8f86303 100644
--- a/apps/web/tsconfig.tsbuildinfo
+++ b/apps/web/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"fileNames":["../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2023.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2023.array.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2023.collection.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2023.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.collection.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.object.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.promise.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.regexp.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.string.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.array.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.collection.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.promise.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.iterator.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.float16.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.bun/drizzle-kit@0.31.4/node_modules/drizzle-kit/index-baurj6ib.d.mts","../../node_modules/.bun/drizzle-kit@0.31.4/node_modules/drizzle-kit/index.d.mts","../../node_modules/@types/node/compatibility/disposable.d.ts","../../node_modules/@types/node/compatibility/indexable.d.ts","../../node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/@types/node/compatibility/index.d.ts","../../node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/undici-types/header.d.ts","../../node_modules/undici-types/readable.d.ts","../../node_modules/undici-types/file.d.ts","../../node_modules/undici-types/fetch.d.ts","../../node_modules/undici-types/formdata.d.ts","../../node_modules/undici-types/connector.d.ts","../../node_modules/undici-types/client.d.ts","../../node_modules/undici-types/errors.d.ts","../../node_modules/undici-types/dispatcher.d.ts","../../node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/undici-types/global-origin.d.ts","../../node_modules/undici-types/pool-stats.d.ts","../../node_modules/undici-types/pool.d.ts","../../node_modules/undici-types/handlers.d.ts","../../node_modules/undici-types/balanced-pool.d.ts","../../node_modules/undici-types/agent.d.ts","../../node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/undici-types/mock-agent.d.ts","../../node_modules/undici-types/mock-client.d.ts","../../node_modules/undici-types/mock-pool.d.ts","../../node_modules/undici-types/mock-errors.d.ts","../../node_modules/undici-types/proxy-agent.d.ts","../../node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/undici-types/retry-handler.d.ts","../../node_modules/undici-types/retry-agent.d.ts","../../node_modules/undici-types/api.d.ts","../../node_modules/undici-types/interceptors.d.ts","../../node_modules/undici-types/util.d.ts","../../node_modules/undici-types/cookies.d.ts","../../node_modules/undici-types/patch.d.ts","../../node_modules/undici-types/websocket.d.ts","../../node_modules/undici-types/eventsource.d.ts","../../node_modules/undici-types/filereader.d.ts","../../node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/undici-types/content-type.d.ts","../../node_modules/undici-types/cache.d.ts","../../node_modules/undici-types/index.d.ts","../../node_modules/@types/node/globals.d.ts","../../node_modules/@types/node/assert.d.ts","../../node_modules/@types/node/assert/strict.d.ts","../../node_modules/@types/node/async_hooks.d.ts","../../node_modules/@types/node/buffer.d.ts","../../node_modules/@types/node/child_process.d.ts","../../node_modules/@types/node/cluster.d.ts","../../node_modules/@types/node/console.d.ts","../../node_modules/@types/node/constants.d.ts","../../node_modules/@types/node/crypto.d.ts","../../node_modules/@types/node/dgram.d.ts","../../node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/@types/node/dns.d.ts","../../node_modules/@types/node/dns/promises.d.ts","../../node_modules/@types/node/domain.d.ts","../../node_modules/@types/node/dom-events.d.ts","../../node_modules/@types/node/events.d.ts","../../node_modules/@types/node/fs.d.ts","../../node_modules/@types/node/fs/promises.d.ts","../../node_modules/@types/node/http.d.ts","../../node_modules/@types/node/http2.d.ts","../../node_modules/@types/node/https.d.ts","../../node_modules/@types/node/inspector.d.ts","../../node_modules/@types/node/module.d.ts","../../node_modules/@types/node/net.d.ts","../../node_modules/@types/node/os.d.ts","../../node_modules/@types/node/path.d.ts","../../node_modules/@types/node/perf_hooks.d.ts","../../node_modules/@types/node/process.d.ts","../../node_modules/@types/node/punycode.d.ts","../../node_modules/@types/node/querystring.d.ts","../../node_modules/@types/node/readline.d.ts","../../node_modules/@types/node/readline/promises.d.ts","../../node_modules/@types/node/repl.d.ts","../../node_modules/@types/node/sea.d.ts","../../node_modules/@types/node/sqlite.d.ts","../../node_modules/@types/node/stream.d.ts","../../node_modules/@types/node/stream/promises.d.ts","../../node_modules/@types/node/stream/consumers.d.ts","../../node_modules/@types/node/stream/web.d.ts","../../node_modules/@types/node/string_decoder.d.ts","../../node_modules/@types/node/test.d.ts","../../node_modules/@types/node/timers.d.ts","../../node_modules/@types/node/timers/promises.d.ts","../../node_modules/@types/node/tls.d.ts","../../node_modules/@types/node/trace_events.d.ts","../../node_modules/@types/node/tty.d.ts","../../node_modules/@types/node/url.d.ts","../../node_modules/@types/node/util.d.ts","../../node_modules/@types/node/v8.d.ts","../../node_modules/@types/node/vm.d.ts","../../node_modules/@types/node/wasi.d.ts","../../node_modules/@types/node/worker_threads.d.ts","../../node_modules/@types/node/zlib.d.ts","../../node_modules/@types/node/index.d.ts","../../node_modules/.bun/dotenv@16.6.1/node_modules/dotenv/lib/main.d.ts","./drizzle.config.ts","../../node_modules/.bun/@types+react@18.3.23/node_modules/@types/react/global.d.ts","../../node_modules/.bun/csstype@3.1.3/node_modules/csstype/index.d.ts","../../node_modules/.bun/@types+prop-types@15.7.15/node_modules/@types/prop-types/index.d.ts","../../node_modules/.bun/@types+react@18.3.23/node_modules/@types/react/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/styled-jsx/types/css.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/styled-jsx/types/macro.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/styled-jsx/types/style.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/styled-jsx/types/global.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/styled-jsx/types/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/amp.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/amp.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/get-page-files.d.ts","../../node_modules/.bun/@types+react@18.3.23/node_modules/@types/react/canary.d.ts","../../node_modules/.bun/@types+react@18.3.23/node_modules/@types/react/experimental.d.ts","../../node_modules/.bun/@types+react-dom@18.3.7+fccada1f982c16e9/node_modules/@types/react-dom/index.d.ts","../../node_modules/.bun/@types+react-dom@18.3.7+fccada1f982c16e9/node_modules/@types/react-dom/canary.d.ts","../../node_modules/.bun/@types+react-dom@18.3.7+fccada1f982c16e9/node_modules/@types/react-dom/experimental.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/fallback.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/webpack/webpack.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/config.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/load-custom-routes.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/image-config.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/body-streams.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/cache-control.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/setup-exception-listeners.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/worker.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/constants.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/app-router-headers.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/rendering-mode.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/router-utils/build-prefetch-segment-data-route.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/require-hook.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/experimental/ppr.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/plugins/app-build-manifest-plugin.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/page-types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/segment-config/app/app-segment-config.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/segment-config/pages/pages-segment-config.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/analysis/get-page-static-info.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-polyfill-crypto.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-baseline.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-extensions/error-inspect.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-extensions/random.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-extensions/date.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-extensions/web-crypto.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-extensions/node-crypto.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/page-extensions-type.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/instrumentation/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/coalesced-function.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/router-utils/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/constants.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/trace/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/trace/trace.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/trace/shared.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/trace/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/load-jsconfig.d.ts","../../node_modules/.bun/@next+env@15.4.3/node_modules/@next/env/dist/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/plugins/telemetry-plugin/use-cache-tracker-utils.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/plugins/telemetry-plugin/telemetry-plugin.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/telemetry/storage.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/build-context.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/bloom-filter.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack-config.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-kind.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-definitions/route-definition.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/swc/generated-native.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/swc/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/dev/parse-version-info.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/next-devtools/shared/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/dev/dev-indicator-server-state.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/dev/hot-reloader-types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/cache-handlers/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/response-cache/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/resume-data-cache/cache-store.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/resume-data-cache/resume-data-cache.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/render-result.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/i18n-provider.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/next-url.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/cookies.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/request.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/after/builtin-request-context.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/response.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/segment-config/middleware/middleware-config.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/base-http/node.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-definitions/locale-route-definition.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-definitions/pages-route-definition.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/mitt.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/with-router.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/router.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/route-loader.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/page-loader.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router/router.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-definitions/app-page-route-definition.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/loaders/next-app-loader/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/app-dir-module.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/cache-signal.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/work-unit-async-storage-instance.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/response-cache/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request/fallback-params.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/lazy-result.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/implicit-tags.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/work-unit-async-storage.external.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/deep-readonly.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router/utils/parse-relative-url.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/app-render.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/amp-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-page/module.compiled.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/error-boundary.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/layout-router.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/render-from-template-context.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/action-async-storage-instance.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/action-async-storage.external.d.ts","../../node_modules/.bun/@types+react@18.3.23/node_modules/@types/react/jsx-runtime.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/client-page.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/client-segment.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request/search-params.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/hooks-server-context.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/http-access-fallback/error-boundary.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/extra-types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/resolvers.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/icons.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/resolve-metadata.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/metadata.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/metadata/metadata-boundary.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/rsc/preloads.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/rsc/postpone.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/rsc/taint.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/collect-segment-data.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/next-devtools/userspace/app/segment-explorer-node.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/entry-base.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/templates/app-page.d.ts","../../node_modules/.bun/@types+react@18.3.23/node_modules/@types/react/jsx-dev-runtime.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/entrypoints.d.ts","../../node_modules/.bun/@types+react-dom@18.3.7+fccada1f982c16e9/node_modules/@types/react-dom/client.d.ts","../../node_modules/.bun/@types+react-dom@18.3.7+fccada1f982c16e9/node_modules/@types/react-dom/server.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/entrypoints.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-page/module.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/adapter.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/use-cache/cache-life.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/flight-data-helpers.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/pages/module.compiled.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/templates/pages.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/pages/module.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/next-devtools/userspace/pages/pages-dev-overlay-setup.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/render.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-definitions/pages-api-route-definition.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-matches/pages-api-route-match.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-matchers/route-matcher.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/normalizers/normalizer.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/normalizers/locale-route-normalizer.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/normalizers/request/pathname-normalizer.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/normalizers/request/suffix.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/normalizers/request/rsc.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/normalizers/request/prefetch-rsc.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/normalizers/request/next-data.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/normalizers/request/segment-prefix-rsc.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/base-server.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/async-callback-set.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","../../node_modules/.bun/sharp@0.34.3/node_modules/sharp/lib/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/image-optimizer.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/next-server.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/lru-cache.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/dev-bundler-service.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/static-paths/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/dev/static-paths-worker.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/dev/next-dev-server.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/next.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/render-server.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/router-server.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/router-utils/router-server-context.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/route-module.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/load-components.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-definitions/app-route-route-definition.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/async-storage/work-store.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/http.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-route/shared-modules.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/redirect-status-code.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/redirect-error.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/templates/app-route.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-route/module.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-route/module.compiled.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/segment-config/app/app-segments.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/utils.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/turborepo-access-trace/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/turborepo-access-trace/result.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/turborepo-access-trace/helpers.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/turborepo-access-trace/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/export/routes/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/export/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/export/worker.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/worker.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/build/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/incremental-cache/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/after/after.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/after/after-context.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/work-async-storage-instance.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/work-async-storage.external.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request/params.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-matches/route-match.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request-meta.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/cli/next-test.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/config-shared.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/base-http/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/api-utils/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/utils.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/pages/_app.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/app.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/use-cache/cache-tag.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/cache.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/runtime-config.external.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/config.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/pages/_document.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/document.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/dynamic.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dynamic.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/pages/_error.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/error.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/head.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/head.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request/cookies.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request/headers.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request/draft-mode.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/headers.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/get-img-props.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/image-component.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/image-external.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/image.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/link.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/link.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/redirect.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/not-found.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/forbidden.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/unauthorized.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/unstable-rethrow.server.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/unstable-rethrow.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/navigation.react-server.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/navigation.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/navigation.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/router.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/client/script.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/script.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/image-response.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/@vercel/og/emoji/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/@vercel/og/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/after/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request/root-params.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request/connection.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/server.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/types/global.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/types/compiled.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/image-types/global.d.ts","./next-env.d.ts","../../node_modules/.bun/botid@1.4.2+39dd938b2b570c60/node_modules/botid/dist/next/config/index.d.ts","./next.config.ts","../../node_modules/.bun/source-map-js@1.2.1/node_modules/source-map-js/source-map.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/previous-map.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/input.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/css-syntax-error.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/declaration.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/root.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/warning.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/lazy-result.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/no-work-result.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/processor.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/result.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/document.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/rule.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/node.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/comment.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/container.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/at-rule.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/list.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/postcss.d.ts","../../node_modules/.bun/postcss@8.5.6/node_modules/postcss/lib/postcss.d.mts","../../node_modules/.bun/tailwindcss@3.4.17/node_modules/tailwindcss/types/generated/corepluginlist.d.ts","../../node_modules/.bun/tailwindcss@3.4.17/node_modules/tailwindcss/types/generated/colors.d.ts","../../node_modules/.bun/tailwindcss@3.4.17/node_modules/tailwindcss/types/config.d.ts","../../node_modules/.bun/tailwindcss@3.4.17/node_modules/tailwindcss/types/index.d.ts","./tailwind.config.ts","../../node_modules/.bun/@t3-oss+env-core@0.13.8+50555f8ded582d61/node_modules/@t3-oss/env-core/dist/presets-cmsjz_ts.d.ts","../../node_modules/.bun/@t3-oss+env-core@0.13.8+50555f8ded582d61/node_modules/@t3-oss/env-core/dist/presets-zod.d.ts","../../node_modules/.bun/@t3-oss+env-core@0.13.8+50555f8ded582d61/node_modules/@t3-oss/env-core/dist/index.d.ts","../../node_modules/.bun/@t3-oss+env-nextjs@0.13.8+50555f8ded582d61/node_modules/@t3-oss/env-nextjs/dist/index.d.ts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/typealiases.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/index.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/zoderror.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseutil.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/enumutil.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorutil.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/partialutil.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/standard-schema.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/index.d.cts","../../node_modules/.bun/@t3-oss+env-core@0.13.8+b738c2472c9862c9/node_modules/@t3-oss/env-core/dist/index.d.ts","../../node_modules/.bun/@t3-oss+env-nextjs@0.13.8+b738c2472c9862c9/node_modules/@t3-oss/env-nextjs/dist/index.d.ts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/standard-schema.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/util.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/versions.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/schemas.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/checks.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/errors.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/core.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/parse.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/regexes.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/ar.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/az.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/be.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/ca.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/cs.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/de.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/en.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/eo.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/es.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/fa.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/fi.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/fr.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/fr-ca.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/he.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/hu.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/id.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/it.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/ja.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/kh.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/ko.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/mk.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/ms.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/nl.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/no.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/ota.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/ps.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/pl.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/pt.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/ru.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/sl.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/sv.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/ta.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/th.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/tr.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/ua.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/ur.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/vi.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/zh-cn.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/zh-tw.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/locales/index.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/registries.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/doc.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/function.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/api.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/json-schema.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/to-json-schema.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/core/index.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/classic/errors.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/classic/parse.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/classic/schemas.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/classic/checks.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/classic/compat.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/classic/iso.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/classic/coerce.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/v4/classic/external.d.cts","../../node_modules/.bun/zod@4.0.5/node_modules/zod/index.d.cts","../../packages/auth/src/keys.ts","../../packages/db/src/keys.ts","./src/env.ts","./src/middleware.ts","./src/constants/site.ts","./src/app/metadata.ts","./src/app/robots.ts","./src/types/post.ts","../../node_modules/.bun/@types+unist@3.0.3/node_modules/@types/unist/index.d.ts","../../node_modules/.bun/vfile-message@4.0.2/node_modules/vfile-message/lib/index.d.ts","../../node_modules/.bun/vfile-message@4.0.2/node_modules/vfile-message/index.d.ts","../../node_modules/.bun/vfile@6.0.3/node_modules/vfile/lib/index.d.ts","../../node_modules/.bun/vfile@6.0.3/node_modules/vfile/index.d.ts","../../node_modules/.bun/unified@11.0.5/node_modules/unified/lib/callable-instance.d.ts","../../node_modules/.bun/trough@2.2.0/node_modules/trough/lib/index.d.ts","../../node_modules/.bun/trough@2.2.0/node_modules/trough/index.d.ts","../../node_modules/.bun/unified@11.0.5/node_modules/unified/lib/index.d.ts","../../node_modules/.bun/unified@11.0.5/node_modules/unified/index.d.ts","../../node_modules/.bun/@types+hast@3.0.4/node_modules/@types/hast/index.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/common/html.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/common/token.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/common/error-codes.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/tokenizer/preprocessor.d.ts","../../node_modules/.bun/entities@6.0.1/node_modules/entities/dist/esm/generated/decode-data-html.d.ts","../../node_modules/.bun/entities@6.0.1/node_modules/entities/dist/esm/generated/decode-data-xml.d.ts","../../node_modules/.bun/entities@6.0.1/node_modules/entities/dist/esm/decode-codepoint.d.ts","../../node_modules/.bun/entities@6.0.1/node_modules/entities/dist/esm/decode.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/tokenizer/index.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/tree-adapters/interface.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/parser/open-element-stack.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/parser/formatting-element-list.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/parser/index.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/tree-adapters/default.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/serializer/index.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/common/foreign-content.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/index.d.ts","../../node_modules/.bun/property-information@7.1.0/node_modules/property-information/lib/util/info.d.ts","../../node_modules/.bun/property-information@7.1.0/node_modules/property-information/lib/find.d.ts","../../node_modules/.bun/property-information@7.1.0/node_modules/property-information/lib/hast-to-react.d.ts","../../node_modules/.bun/property-information@7.1.0/node_modules/property-information/lib/normalize.d.ts","../../node_modules/.bun/property-information@7.1.0/node_modules/property-information/index.d.ts","../../node_modules/.bun/hast-util-from-parse5@8.0.3/node_modules/hast-util-from-parse5/lib/index.d.ts","../../node_modules/.bun/hast-util-from-parse5@8.0.3/node_modules/hast-util-from-parse5/index.d.ts","../../node_modules/.bun/hast-util-from-html@2.0.3/node_modules/hast-util-from-html/lib/errors.d.ts","../../node_modules/.bun/hast-util-from-html@2.0.3/node_modules/hast-util-from-html/lib/types.d.ts","../../node_modules/.bun/hast-util-from-html@2.0.3/node_modules/hast-util-from-html/lib/index.d.ts","../../node_modules/.bun/hast-util-from-html@2.0.3/node_modules/hast-util-from-html/index.d.ts","../../node_modules/.bun/rehype-parse@9.0.1/node_modules/rehype-parse/lib/index.d.ts","../../node_modules/.bun/rehype-parse@9.0.1/node_modules/rehype-parse/index.d.ts","../../node_modules/.bun/stringify-entities@4.0.4/node_modules/stringify-entities/lib/util/format-smart.d.ts","../../node_modules/.bun/stringify-entities@4.0.4/node_modules/stringify-entities/lib/core.d.ts","../../node_modules/.bun/stringify-entities@4.0.4/node_modules/stringify-entities/lib/index.d.ts","../../node_modules/.bun/stringify-entities@4.0.4/node_modules/stringify-entities/index.d.ts","../../node_modules/.bun/hast-util-to-html@9.0.5/node_modules/hast-util-to-html/lib/index.d.ts","../../node_modules/.bun/hast-util-to-html@9.0.5/node_modules/hast-util-to-html/index.d.ts","../../node_modules/.bun/rehype-stringify@10.0.1/node_modules/rehype-stringify/index.d.ts","../../node_modules/.bun/rehype-slug@6.0.0/node_modules/rehype-slug/lib/index.d.ts","../../node_modules/.bun/rehype-slug@6.0.0/node_modules/rehype-slug/index.d.ts","../../node_modules/.bun/hast-util-is-element@3.0.0/node_modules/hast-util-is-element/lib/index.d.ts","../../node_modules/.bun/hast-util-is-element@3.0.0/node_modules/hast-util-is-element/index.d.ts","../../node_modules/.bun/rehype-autolink-headings@7.1.0/node_modules/rehype-autolink-headings/lib/index.d.ts","../../node_modules/.bun/rehype-autolink-headings@7.1.0/node_modules/rehype-autolink-headings/index.d.ts","../../node_modules/.bun/hast-util-sanitize@5.0.2/node_modules/hast-util-sanitize/lib/index.d.ts","../../node_modules/.bun/hast-util-sanitize@5.0.2/node_modules/hast-util-sanitize/lib/schema.d.ts","../../node_modules/.bun/hast-util-sanitize@5.0.2/node_modules/hast-util-sanitize/index.d.ts","../../node_modules/.bun/rehype-sanitize@6.0.0/node_modules/rehype-sanitize/lib/index.d.ts","../../node_modules/.bun/rehype-sanitize@6.0.0/node_modules/rehype-sanitize/index.d.ts","./src/lib/blog-query.ts","./src/app/sitemap.ts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/standard-schema.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/util.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/versions.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/schemas.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/checks.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/errors.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/core.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/parse.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/regexes.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/ar.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/az.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/be.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/ca.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/cs.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/de.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/en.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/eo.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/es.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/fa.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/fi.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/fr.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/fr-ca.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/he.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/hu.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/id.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/it.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/ja.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/kh.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/ko.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/mk.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/ms.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/nl.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/no.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/ota.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/ps.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/pl.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/pt.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/ru.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/sl.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/sv.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/ta.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/th.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/tr.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/ua.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/ur.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/vi.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/zh-cn.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/zh-tw.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/locales/index.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/registries.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/doc.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/function.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/api.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/json-schema.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/to-json-schema.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/core/index.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/classic/errors.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/classic/parse.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/classic/schemas.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/classic/checks.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/classic/compat.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/classic/iso.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/classic/coerce.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/classic/external.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/classic/index.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/index.d.cts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/operation-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/identifier-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/check-constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/column-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/default-value-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/generated-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/schemable-identifier-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/table-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/insert-result.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/delete-result.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/update-result.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/util/type-error.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/merge-result.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/util/type-utils.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/references-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/column-definition-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/add-column-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/drop-column-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/rename-column-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/raw-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/alter-column-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/foreign-key-constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/primary-key-constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/unique-constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/add-constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/drop-constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/modify-column-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/drop-index-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/add-index-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/rename-constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/alter-table-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/where-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/create-index-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/create-schema-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/create-table-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/value-list-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/create-type-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/from-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/group-by-item-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/group-by-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/having-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/on-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/join-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/limit-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/offset-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/collate-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/order-by-item-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/order-by-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/alias-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/select-all-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/reference-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/simple-reference-expression-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/selection-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/common-table-expression-name-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/common-table-expression-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/with-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/select-modifier-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/operation-node-source.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/expression/expression.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/util/explainable.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/explain-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/set-operation-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/value-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/fetch-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/top-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/select-query-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/create-view-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/drop-schema-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/drop-table-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/drop-type-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/drop-view-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/output-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/returning-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/when-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/merge-query-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/column-update-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/on-conflict-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/on-duplicate-key-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/or-action-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/insert-query-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/update-query-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/using-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/delete-query-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/query-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/refresh-materialized-view-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/util/query-id.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-compiler/compiled-query.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-compiler/query-compiler.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/driver/database-connection.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/driver/driver.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/database-introspector.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/dialect-adapter.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/dialect.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/driver/connection-provider.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/plugin/kysely-plugin.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-executor/query-executor.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/util/compilable.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/default-value-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/column-definition-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/data-type-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/data-type-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/foreign-key-constraint-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/alter-column-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/alter-table-executor.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/alter-table-add-foreign-key-constraint-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/alter-table-drop-constraint-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/select-query-builder-expression.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/binary-operation-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/operator-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/value-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/util/column-type.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/binary-operation-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/join-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dynamic/dynamic-table-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/table-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/join-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dynamic/dynamic-reference-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/select-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/collate-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/order-by-item-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/order-by-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/group-by-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/where-interface.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/no-result-error.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/having-interface.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/set-operation-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/util/streamable.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/and-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/or-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/parens-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/expression/expression-wrapper.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/order-by-interface.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/select-query-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/coalesce-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/partition-by-item-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/partition-by-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/over-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/aggregate-function-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/partition-by-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/over-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/aggregate-function-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/function-module.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/case-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/case-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/json-path-leg-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/json-path-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/json-operator-chain-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/json-reference-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/json-path-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/tuple-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/select-from-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/expression/expression-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/expression-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/reference-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/alter-table-add-index-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/unique-constraint-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/primary-key-constraint-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/check-constraint-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/alter-table-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/create-index-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/create-schema-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/create-table-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/drop-index-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/drop-schema-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/drop-table-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-executor/query-executor-provider.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/raw-builder/raw-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/create-view-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/drop-view-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/create-type-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/drop-type-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/refresh-materialized-view-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/schema/schema.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dynamic/dynamic.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/primitive-value-list-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/values-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/insert-values-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/update-set-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/returning-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/returning-interface.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/on-conflict-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/output-interface.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/insert-query-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/update-query-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/delete-query-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/cte-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/with-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/delete-from-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/update-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-builder/merge-query-builder.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/merge-into-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-creator.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/util/log.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/savepoint-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/util/provide-controlled-connection.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/kysely.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/raw-builder/sql.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-executor/query-executor-base.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-executor/default-query-executor.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-executor/noop-query-executor.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/list-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/default-insert-value-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/unary-operation-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/function-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/tuple-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/matched-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/cast-node.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/operation-node-visitor.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/query-compiler/default-query-compiler.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/driver/default-connection-provider.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/driver/single-connection-provider.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/driver/dummy-driver.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/dialect-adapter-base.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/sqlite/sqlite-dialect-config.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/sqlite/sqlite-dialect.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/sqlite/sqlite-driver.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/postgres/postgres-query-compiler.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/postgres/postgres-introspector.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/postgres/postgres-adapter.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/mysql/mysql-dialect-config.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/mysql/mysql-dialect.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/mysql/mysql-driver.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/mysql/mysql-query-compiler.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/mysql/mysql-introspector.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/mysql/mysql-adapter.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/postgres/postgres-dialect-config.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/postgres/postgres-driver.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/postgres/postgres-dialect.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/sqlite/sqlite-query-compiler.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/sqlite/sqlite-introspector.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/sqlite/sqlite-adapter.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/mssql/mssql-adapter.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/mssql/mssql-dialect-config.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/mssql/mssql-dialect.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/mssql/mssql-driver.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/mssql/mssql-introspector.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/dialect/mssql/mssql-query-compiler.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/migration/migrator.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/migration/file-migration-provider.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/plugin/camel-case/camel-case-plugin.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/plugin/deduplicate-joins/deduplicate-joins-plugin.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/plugin/with-schema/with-schema-plugin.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/plugin/parse-json-results/parse-json-results-plugin.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/plugin/handle-empty-in-lists/handle-empty-in-lists.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/plugin/handle-empty-in-lists/handle-empty-in-lists-plugin.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/operation-node/operation-node-transformer.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/util/infer-result.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/util/log-once.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/parser/unary-operation-parser.d.ts","../../node_modules/.bun/kysely@0.28.3/node_modules/kysely/dist/esm/index.d.ts","../../node_modules/.bun/better-call@1.0.12/node_modules/better-call/dist/router-bep4ze3q.d.ts","../../node_modules/.bun/better-call@1.0.12/node_modules/better-call/dist/index.d.ts","../../node_modules/.bun/better-auth@1.3.2/node_modules/better-auth/dist/shared/better-auth.zsfsbnqt.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/types.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwe/general/decrypt.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwe/general/encrypt.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jws/compact/verify.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jws/flattened/verify.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jws/general/verify.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwt/verify.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwt/decrypt.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwt/produce.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jws/compact/sign.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jws/flattened/sign.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jws/general/sign.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwt/sign.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwt/encrypt.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwk/thumbprint.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwk/embedded.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwks/local.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwks/remote.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/jwt/unsecured.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/key/export.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/key/import.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/util/decode_protected_header.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/util/decode_jwt.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/util/errors.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/key/generate_key_pair.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/key/generate_secret.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/util/base64url.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/util/runtime.d.ts","../../node_modules/.bun/jose@5.10.0/node_modules/jose/dist/types/index.d.ts","../../node_modules/.bun/better-auth@1.3.2/node_modules/better-auth/dist/shared/better-auth.dpa2nz5l.d.ts","../../node_modules/.bun/better-auth@1.3.2/node_modules/better-auth/dist/shared/better-auth.dj2hyomw.d.ts","../../node_modules/.bun/@better-fetch+fetch@1.1.18/node_modules/@better-fetch/fetch/dist/index.d.ts","../../node_modules/.bun/nanostores@0.11.4/node_modules/nanostores/atom/index.d.ts","../../node_modules/.bun/nanostores@0.11.4/node_modules/nanostores/map/index.d.ts","../../node_modules/.bun/nanostores@0.11.4/node_modules/nanostores/map-creator/index.d.ts","../../node_modules/.bun/nanostores@0.11.4/node_modules/nanostores/clean-stores/index.d.ts","../../node_modules/.bun/nanostores@0.11.4/node_modules/nanostores/task/index.d.ts","../../node_modules/.bun/nanostores@0.11.4/node_modules/nanostores/computed/index.d.ts","../../node_modules/.bun/nanostores@0.11.4/node_modules/nanostores/deep-map/path.d.ts","../../node_modules/.bun/nanostores@0.11.4/node_modules/nanostores/deep-map/index.d.ts","../../node_modules/.bun/nanostores@0.11.4/node_modules/nanostores/keep-mount/index.d.ts","../../node_modules/.bun/nanostores@0.11.4/node_modules/nanostores/lifecycle/index.d.ts","../../node_modules/.bun/nanostores@0.11.4/node_modules/nanostores/listen-keys/index.d.ts","../../node_modules/.bun/nanostores@0.11.4/node_modules/nanostores/index.d.ts","../../node_modules/.bun/better-auth@1.3.2/node_modules/better-auth/dist/shared/better-auth.b_g094aj.d.ts","../../node_modules/.bun/better-auth@1.3.2/node_modules/better-auth/dist/shared/better-auth.dehjp1rk.d.ts","../../node_modules/.bun/better-auth@1.3.2/node_modules/better-auth/dist/shared/better-auth.bwe61noi.d.ts","../../node_modules/.bun/better-auth@1.3.2/node_modules/better-auth/dist/index.d.ts","../../node_modules/.bun/better-auth@1.3.2/node_modules/better-auth/dist/adapters/drizzle-adapter/index.d.ts","../../node_modules/.bun/postgres@3.4.7/node_modules/postgres/types/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/entity.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/logger.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/casing.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/table.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/operations.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/subquery.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/query-builders/select.types.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sql/sql.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/utils.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sql/expressions/conditions.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sql/expressions/select.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sql/expressions/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sql/functions/aggregate.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/query-builders/query-builder.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sql/functions/vector.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sql/functions/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sql/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/checks.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/sequence.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/int.common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/bigintt.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/boolean.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/bytes.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/custom.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/date-duration.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/decimal.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/double-precision.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/duration.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/integer.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/json.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/date.common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/localdate.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/localtime.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/real.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/relative-duration.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/smallint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/text.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/timestamp.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/timestamptz.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/uuid.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/all.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/indexes.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/roles.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/policies.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/primary-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/unique-constraint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/table.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/foreign-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/bigint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/columns/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/view-base.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/cache/core/types.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/relations.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/query-builders/count.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/query-promise.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/runnable-query.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/query-builders/query.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/query-builders/raw.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/subquery.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/db.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/query-builders/delete.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/query-builders/update.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/query-builders/insert.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/query-builders/refresh-materialized-view.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/query-builders/select.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/query-builders/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/dialect.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/query-builders/query-builder.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/view-common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/view.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/query-builders/select.types.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/alias.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/schema.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/utils.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/gel-core/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/checks.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/binary.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/boolean.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/char.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/custom.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/date.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/datetime.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/decimal.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/double.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/enum.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/float.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/int.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/json.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/mediumint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/real.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/serial.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/smallint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/text.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/time.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/date.common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/timestamp.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/tinyint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/varbinary.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/varchar.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/year.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/all.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/indexes.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/primary-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/unique-constraint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/table.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/foreign-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/bigint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/columns/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/migrator.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/query-builders/delete.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/subquery.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/view-base.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/query-builders/select.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/query-builders/query-builder.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/query-builders/update.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/query-builders/insert.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/dialect.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/query-builders/count.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/query-builders/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/query-builders/query.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/db.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/view-common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/view.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/query-builders/select.types.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/alias.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/schema.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/utils.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/mysql-core/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/checks.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/bigserial.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/boolean.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/char.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/cidr.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/custom.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/date.common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/date.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/double-precision.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/inet.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/sequence.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/int.common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/integer.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/timestamp.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/interval.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/json.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/jsonb.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/line.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/macaddr.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/macaddr8.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/numeric.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/point.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/postgis_extension/geometry.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/real.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/serial.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/smallint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/smallserial.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/text.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/time.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/uuid.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/varchar.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/vector_extension/bit.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/vector_extension/halfvec.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/vector_extension/sparsevec.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/vector_extension/vector.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/all.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/indexes.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/roles.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/policies.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/primary-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/unique-constraint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/table.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/foreign-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/bigint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/enum.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/columns/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/view-base.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/query-builders/delete.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/query-builders/update.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/query-builders/insert.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/query-builders/refresh-materialized-view.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/subquery.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/query-builders/select.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/query-builders/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/dialect.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/query-builders/query-builder.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/view-common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/view.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/query-builders/select.types.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/alias.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/schema.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/utils.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/utils/array.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/utils/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/binary.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/boolean.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/char.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/custom.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/date.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/datetime.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/decimal.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/double.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/enum.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/float.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/int.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/json.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/mediumint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/real.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/serial.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/smallint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/text.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/time.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/date.common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/timestamp.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/tinyint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/varbinary.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/varchar.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/vector.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/year.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/all.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/indexes.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/primary-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/unique-constraint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/table.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/bigint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/columns/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/query-builders/delete.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/query-builders/update.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/query-builders/insert.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/dialect.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/cache/core/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore/driver.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/query-builders/count.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/subquery.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/query-builders/select.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/query-builders/query-builder.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/query-builders/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/db.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/query-builders/select.types.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/alias.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/schema.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/utils.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/singlestore-core/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/checks.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/columns/custom.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/indexes.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/primary-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/unique-constraint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/view-base.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/query-builders/count.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/query-builders/query.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/subquery.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/db.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/query-builders/raw.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/query-builders/delete.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/query-builders/update.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/query-builders/insert.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/query-builders/select.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/query-builders/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/dialect.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/query-builders/query-builder.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/view.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/utils.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/columns/integer.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/columns/numeric.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/columns/real.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/columns/text.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/columns/all.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/table.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/foreign-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/columns/common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/columns/blob.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/columns/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/query-builders/select.types.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/alias.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/sqlite-core/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/column-builder.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/column.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/alias.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/errors.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/view-common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/cache/core/cache.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/query-builders/count.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/query-builders/query.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/query-builders/raw.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/pg-core/db.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/postgres-js/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/postgres-js/driver.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+8d8af77f431c301f/node_modules/drizzle-orm/postgres-js/index.d.ts","../../packages/db/src/schema.ts","../../packages/db/src/index.ts","../../packages/auth/src/server.ts","../../node_modules/.bun/better-auth@1.3.2/node_modules/better-auth/dist/client/react/index.d.ts","../../packages/auth/src/client.ts","../../packages/auth/src/index.ts","../../node_modules/.bun/better-auth@1.3.2+6dbf9a050bc9aadb/node_modules/better-auth/dist/shared/better-auth.zsfsbnqt.d.ts","../../node_modules/.bun/better-auth@1.3.2+6dbf9a050bc9aadb/node_modules/better-auth/dist/shared/better-auth.dpa2nz5l.d.ts","../../node_modules/.bun/better-auth@1.3.2+6dbf9a050bc9aadb/node_modules/better-auth/dist/shared/better-auth.dj2hyomw.d.ts","../../node_modules/.bun/better-auth@1.3.2+6dbf9a050bc9aadb/node_modules/better-auth/dist/integrations/next-js.d.ts","./src/app/api/auth/[...all]/route.ts","./src/app/api/health/route.ts","../../node_modules/.bun/botid@1.4.2+39dd938b2b570c60/node_modules/botid/dist/server/index.d.ts","../../node_modules/nanoid/index.d.ts","../../node_modules/.bun/@upstash+redis@1.35.1/node_modules/@upstash/redis/zmscore-bsheakn7.d.mts","../../node_modules/.bun/@upstash+redis@1.35.1/node_modules/@upstash/redis/nodejs.d.mts","../../node_modules/.bun/@upstash+core-analytics@0.0.10/node_modules/@upstash/core-analytics/dist/index.d.ts","../../node_modules/.bun/@upstash+ratelimit@2.0.5+eb344dddf3a10dc0/node_modules/@upstash/ratelimit/dist/index.d.ts","./src/lib/rate-limit.ts","./src/app/api/waitlist/route.ts","./src/app/api/waitlist/token/route.ts","../../node_modules/.bun/feed@5.1.0/node_modules/feed/lib/feed.d.ts","./src/app/rss.xml/route.ts","../../node_modules/.bun/lucide-react@0.468.0+f4eacebf2041cd4f/node_modules/lucide-react/dist/lucide-react.d.ts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/vanilla.d.mts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/react.d.mts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/index.d.mts","./src/components/editor/media-panel/store.ts","./src/constants/actions.ts","./src/constants/font-constants.ts","./src/types/project.ts","./src/lib/storage/types.ts","./src/lib/storage/indexeddb-adapter.ts","./src/lib/storage/opfs-adapter.ts","./src/lib/storage/storage-service.ts","./src/types/editor.ts","./src/stores/editor-store.ts","../../node_modules/.bun/sonner@1.7.4+6dbf9a050bc9aadb/node_modules/sonner/dist/index.d.ts","../../node_modules/.bun/clsx@2.1.1/node_modules/clsx/clsx.d.mts","../../node_modules/.bun/tailwind-merge@2.6.0/node_modules/tailwind-merge/dist/types.d.ts","./src/lib/utils.ts","./src/stores/project-store.ts","./src/lib/timeline.ts","./src/stores/timeline-store.ts","./src/stores/media-store.ts","./src/types/timeline.ts","./src/constants/timeline-constants.ts","./src/data/colors.ts","./src/hooks/use-aspect-ratio.ts","./src/hooks/use-drag-drop.ts","./src/types/playback.ts","./src/stores/playback-store.ts","./src/hooks/use-editor-actions.ts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/middleware/redux.d.mts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/middleware/devtools.d.mts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/middleware/subscribewithselector.d.mts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/middleware/combine.d.mts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/middleware/persist.d.mts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/middleware.d.mts","./src/types/keybinding.ts","./src/stores/keybindings-store.ts","./src/hooks/use-keybinding-conflicts.ts","./src/hooks/use-keybindings.ts","./src/hooks/use-keyboard-shortcuts-help.ts","./src/hooks/use-playback-controls.ts","./src/hooks/use-selection-box.ts","./src/hooks/use-timeline-element-resize.ts","./src/hooks/use-timeline-playhead.ts","./src/hooks/use-timeline-snapping.ts","./src/hooks/use-timeline-zoom.ts","../../node_modules/.bun/@radix-ui+react-accessible-icon@1.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-accessible-icon/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-context@1.1.2+0e2fb8dbc083adda/node_modules/@radix-ui/react-context/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-primitive@2.1.3+b41f8805ee63d2ff/node_modules/@radix-ui/react-primitive/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-collapsible@1.1.11+b41f8805ee63d2ff/node_modules/@radix-ui/react-collapsible/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-accordion@1.2.11+b41f8805ee63d2ff/node_modules/@radix-ui/react-accordion/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-dismissable-layer@1.1.10+b41f8805ee63d2ff/node_modules/@radix-ui/react-dismissable-layer/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-focus-scope@1.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-focus-scope/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-portal@1.1.9+b41f8805ee63d2ff/node_modules/@radix-ui/react-portal/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-dialog@1.1.14+b41f8805ee63d2ff/node_modules/@radix-ui/react-dialog/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-alert-dialog@1.1.14+b41f8805ee63d2ff/node_modules/@radix-ui/react-alert-dialog/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-aspect-ratio@1.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-aspect-ratio/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-avatar@1.1.10+b41f8805ee63d2ff/node_modules/@radix-ui/react-avatar/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-checkbox@1.3.2+b41f8805ee63d2ff/node_modules/@radix-ui/react-checkbox/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-arrow@1.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-arrow/dist/index.d.mts","../../node_modules/.bun/@radix-ui+rect@1.1.1/node_modules/@radix-ui/rect/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-popper@1.2.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-popper/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-roving-focus@1.1.10+b41f8805ee63d2ff/node_modules/@radix-ui/react-roving-focus/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-menu@2.1.15+b41f8805ee63d2ff/node_modules/@radix-ui/react-menu/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-context-menu@2.2.15+b41f8805ee63d2ff/node_modules/@radix-ui/react-context-menu/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-direction@1.1.1+0e2fb8dbc083adda/node_modules/@radix-ui/react-direction/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-dropdown-menu@2.1.15+b41f8805ee63d2ff/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-label@2.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-label/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-form@0.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-form/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-hover-card@1.1.14+b41f8805ee63d2ff/node_modules/@radix-ui/react-hover-card/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-menubar@1.1.15+b41f8805ee63d2ff/node_modules/@radix-ui/react-menubar/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-visually-hidden@1.2.3+b41f8805ee63d2ff/node_modules/@radix-ui/react-visually-hidden/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-navigation-menu@1.2.13+b41f8805ee63d2ff/node_modules/@radix-ui/react-navigation-menu/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-one-time-password-field@0.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-one-time-password-field/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-password-toggle-field@0.1.2+b41f8805ee63d2ff/node_modules/@radix-ui/react-password-toggle-field/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-popover@1.1.14+b41f8805ee63d2ff/node_modules/@radix-ui/react-popover/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-progress@1.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-progress/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-radio-group@1.3.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-radio-group/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-scroll-area@1.2.9+b41f8805ee63d2ff/node_modules/@radix-ui/react-scroll-area/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-select@2.2.5+b41f8805ee63d2ff/node_modules/@radix-ui/react-select/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-separator@1.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-separator/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-slider@1.3.5+b41f8805ee63d2ff/node_modules/@radix-ui/react-slider/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-slot@1.2.3+0e2fb8dbc083adda/node_modules/@radix-ui/react-slot/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-switch@1.2.5+b41f8805ee63d2ff/node_modules/@radix-ui/react-switch/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-tabs@1.1.12+b41f8805ee63d2ff/node_modules/@radix-ui/react-tabs/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-toast@1.2.14+b41f8805ee63d2ff/node_modules/@radix-ui/react-toast/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-toggle@1.1.9+b41f8805ee63d2ff/node_modules/@radix-ui/react-toggle/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-toggle-group@1.1.10+b41f8805ee63d2ff/node_modules/@radix-ui/react-toggle-group/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-toolbar@1.1.10+b41f8805ee63d2ff/node_modules/@radix-ui/react-toolbar/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-tooltip@1.2.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-tooltip/dist/index.d.mts","../../node_modules/.bun/radix-ui@1.4.2+b41f8805ee63d2ff/node_modules/radix-ui/dist/index.d.mts","../../node_modules/.bun/class-variance-authority@0.7.1/node_modules/class-variance-authority/dist/types.d.ts","../../node_modules/.bun/class-variance-authority@0.7.1/node_modules/class-variance-authority/dist/index.d.ts","./src/components/ui/toast.tsx","./src/hooks/use-toast.ts","./src/hooks/auth/uselogin.ts","./src/hooks/auth/usesignup.ts","./src/lib/fetch-github-stars.ts","../../node_modules/.bun/@ffmpeg+ffmpeg@0.12.15/node_modules/@ffmpeg/ffmpeg/dist/esm/types.d.ts","../../node_modules/.bun/@ffmpeg+ffmpeg@0.12.15/node_modules/@ffmpeg/ffmpeg/dist/esm/classes.d.ts","../../node_modules/.bun/@ffmpeg+ffmpeg@0.12.15/node_modules/@ffmpeg/ffmpeg/dist/esm/index.d.ts","../../node_modules/.bun/@ffmpeg+util@0.12.2/node_modules/@ffmpeg/util/dist/cjs/types.d.ts","../../node_modules/.bun/@ffmpeg+util@0.12.2/node_modules/@ffmpeg/util/dist/cjs/index.d.ts","./src/lib/ffmpeg-utils.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/@next/font/dist/types.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/@next/font/dist/google/index.d.ts","../../node_modules/.bun/next@15.4.3+6dbf9a050bc9aadb/node_modules/next/font/google/index.d.ts","./src/lib/font-config.ts","./src/lib/media-processing.ts","./src/lib/time.ts","./src/lib/waitlist.ts","./src/stores/panel-store.ts","../../node_modules/.bun/next-themes@0.4.6+6dbf9a050bc9aadb/node_modules/next-themes/dist/index.d.ts","../../node_modules/.bun/@vercel+analytics@1.5.0+39dd938b2b570c60/node_modules/@vercel/analytics/dist/react/index.d.mts","./src/components/ui/sonner.tsx","./src/components/ui/tooltip.tsx","./src/components/storage-provider.tsx","../../node_modules/.bun/botid@1.4.2+39dd938b2b570c60/node_modules/botid/dist/client/index.d.ts","./src/app/layout.tsx","../../node_modules/.bun/motion-utils@12.23.6/node_modules/motion-utils/dist/index.d.ts","../../node_modules/.bun/motion-dom@12.23.6/node_modules/motion-dom/dist/index.d.ts","../../node_modules/.bun/framer-motion@12.23.6+6dbf9a050bc9aadb/node_modules/framer-motion/dist/types.d-bq-qm38r.d.ts","../../node_modules/.bun/framer-motion@12.23.6+6dbf9a050bc9aadb/node_modules/framer-motion/dist/types/index.d.ts","../../node_modules/.bun/motion@12.23.6+6dbf9a050bc9aadb/node_modules/motion/dist/react.d.ts","./src/components/ui/button.tsx","./src/components/ui/sponsor-button.tsx","./src/components/icons.tsx","./src/components/landing/handlebars.tsx","./src/components/landing/hero.tsx","./src/components/header-base.tsx","./src/components/header.tsx","../../node_modules/.bun/react-icons@5.5.0+f4eacebf2041cd4f/node_modules/react-icons/lib/iconsmanifest.d.ts","../../node_modules/.bun/react-icons@5.5.0+f4eacebf2041cd4f/node_modules/react-icons/lib/iconbase.d.ts","../../node_modules/.bun/react-icons@5.5.0+f4eacebf2041cd4f/node_modules/react-icons/lib/iconcontext.d.ts","../../node_modules/.bun/react-icons@5.5.0+f4eacebf2041cd4f/node_modules/react-icons/lib/index.d.ts","../../node_modules/.bun/react-icons@5.5.0+f4eacebf2041cd4f/node_modules/react-icons/ri/index.d.ts","../../node_modules/.bun/react-icons@5.5.0+f4eacebf2041cd4f/node_modules/react-icons/fa6/index.d.ts","./src/components/footer.tsx","./src/app/page.tsx","./src/components/ui/card.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/separator.tsx","./src/components/ui/alert.tsx","./src/app/(auth)/login/page.tsx","./src/app/(auth)/signup/page.tsx","./src/components/ui/avatar.tsx","./src/app/blog/page.tsx","./src/components/ui/prose.tsx","./src/app/blog/[slug]/page.tsx","./src/components/ui/badge.tsx","./src/app/contributors/page.tsx","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/panel.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/types.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/panelgroup.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/panelresizehandleregistry.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/panelresizehandle.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/constants.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/assert.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/csp.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/cursor.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/dom/getpanelelement.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/dom/getpanelelementsforgroup.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/dom/getpanelgroupelement.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/dom/getresizehandleelement.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/dom/getresizehandleelementindex.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/dom/getresizehandleelementsforgroup.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/dom/getresizehandlepanelids.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/rects/types.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/rects/getintersectingrectangle.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/rects/intersects.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/index.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/react-resizable-panels.cjs.d.mts","./src/components/ui/resizable.tsx","./src/components/editor/media-panel/tabbar.tsx","./src/components/editor/media-panel/drag-overlay.tsx","./src/components/ui/context-menu.tsx","./src/components/ui/select.tsx","./src/components/ui/aspect-ratio.tsx","./src/components/ui/draggable-item.tsx","./src/components/editor/media-panel/views/media.tsx","./src/components/editor/media-panel/views/text.tsx","./src/components/editor/media-panel/views/audio.tsx","./src/components/editor/media-panel/index.tsx","./src/components/ui/scroll-area.tsx","./src/components/editor/properties-panel/audio-properties.tsx","./src/components/editor/properties-panel/media-properties.tsx","./src/components/ui/textarea.tsx","./src/components/ui/font-picker.tsx","./src/components/ui/slider.tsx","./src/components/editor/properties-panel/property-item.tsx","./src/components/editor/properties-panel/text-properties.tsx","./src/components/editor/properties-panel/index.tsx","../../node_modules/.bun/wavesurfer.js@7.10.0/node_modules/wavesurfer.js/dist/event-emitter.d.ts","../../node_modules/.bun/wavesurfer.js@7.10.0/node_modules/wavesurfer.js/dist/base-plugin.d.ts","../../node_modules/.bun/wavesurfer.js@7.10.0/node_modules/wavesurfer.js/dist/dom.d.ts","../../node_modules/.bun/wavesurfer.js@7.10.0/node_modules/wavesurfer.js/dist/player.d.ts","../../node_modules/.bun/wavesurfer.js@7.10.0/node_modules/wavesurfer.js/dist/wavesurfer.d.ts","./src/components/editor/audio-waveform.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/editor/timeline/timeline-element.tsx","./src/components/editor/timeline/timeline-track.tsx","./src/components/editor/timeline/timeline-playhead.tsx","./src/components/editor/selection-box.tsx","./src/components/editor/snap-indicator.tsx","./src/components/editor/timeline/index.tsx","./src/components/ui/video-player.tsx","./src/components/ui/audio-player.tsx","./src/components/ui/popover.tsx","./src/components/background-settings.tsx","./src/components/editor/preview-panel.tsx","./src/components/ui/dialog.tsx","./src/components/keyboard-shortcuts-help.tsx","./src/components/editor-header.tsx","./src/components/editor-provider.tsx","../../node_modules/.bun/@types+mdast@4.0.4/node_modules/@types/mdast/index.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/state.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/footer.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/blockquote.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/break.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/code.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/delete.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/emphasis.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/footnote-reference.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/heading.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/html.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/image-reference.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/image.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/inline-code.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/link-reference.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/link.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/list-item.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/list.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/paragraph.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/root.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/strong.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/table.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/table-cell.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/table-row.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/text.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/thematic-break.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/index.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/index.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/index.d.ts","../../node_modules/.bun/remark-rehype@11.1.2/node_modules/remark-rehype/lib/index.d.ts","../../node_modules/.bun/remark-rehype@11.1.2/node_modules/remark-rehype/index.d.ts","../../node_modules/.bun/react-markdown@10.1.0+0e2fb8dbc083adda/node_modules/react-markdown/lib/index.d.ts","../../node_modules/.bun/react-markdown@10.1.0+0e2fb8dbc083adda/node_modules/react-markdown/index.d.ts","./src/components/onboarding.tsx","./src/app/editor/[project_id]/page.tsx","./src/app/privacy/page.tsx","./src/components/delete-project-dialog.tsx","./src/components/rename-project-dialog.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/skeleton.tsx","./src/app/projects/page.tsx","./src/app/roadmap/page.tsx","./src/app/terms/page.tsx","./src/app/why-not-capcut/page.tsx","./src/components/editor/speed-control.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/breadcrumb.tsx","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/locale/types.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/fp/types.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/types.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/add.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addbusinessdays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/adddays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addhours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addisoweekyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addmilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addminutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addmonths.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addquarters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addweeks.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/areintervalsoverlapping.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/clamp.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/closestindexto.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/closestto.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/compareasc.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/comparedesc.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/constructfrom.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/constructnow.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/daystoweeks.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinbusinessdays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceincalendardays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceincalendarisoweekyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceincalendarisoweeks.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceincalendarmonths.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceincalendarquarters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceincalendarweeks.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceincalendaryears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceindays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinhours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinisoweekyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinmilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinminutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinmonths.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinquarters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinweeks.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachdayofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachhourofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachminuteofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachmonthofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachquarterofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachweekofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachweekendofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachweekendofmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachweekendofyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachyearofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofdecade.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofhour.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofisoweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofisoweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofminute.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofquarter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofsecond.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endoftoday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endoftomorrow.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofyesterday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/_lib/format/formatters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/_lib/format/longformatters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/format.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatdistance.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatdistancestrict.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatdistancetonow.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatdistancetonowstrict.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatduration.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatiso.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatiso9075.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatisoduration.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatrfc3339.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatrfc7231.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatrelative.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/fromunixtime.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getdate.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getdayofyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getdaysinmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getdaysinyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getdecade.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/_lib/defaultoptions.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getdefaultoptions.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/gethours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getisoday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getisoweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getisoweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getisoweeksinyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getmilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getminutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getoverlappingdaysinintervals.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getquarter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/gettime.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getunixtime.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getweekofmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getweeksinmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/hourstomilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/hourstominutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/hourstoseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/interval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/intervaltoduration.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/intlformat.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/intlformatdistance.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isafter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isbefore.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isdate.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isequal.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isexists.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isfirstdayofmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isfriday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isfuture.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/islastdayofmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isleapyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/ismatch.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/ismonday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/ispast.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issameday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issamehour.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issameisoweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issameisoweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issameminute.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issamemonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issamequarter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issamesecond.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issameweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issameyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issaturday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issunday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthishour.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthisisoweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthisminute.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthismonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthisquarter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthissecond.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthisweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthisyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthursday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/istoday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/istomorrow.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/istuesday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isvalid.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/iswednesday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isweekend.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/iswithininterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isyesterday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lastdayofdecade.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lastdayofisoweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lastdayofisoweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lastdayofmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lastdayofquarter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lastdayofweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lastdayofyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/_lib/format/lightformatters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lightformat.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/max.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/milliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/millisecondstohours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/millisecondstominutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/millisecondstoseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/min.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/minutestohours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/minutestomilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/minutestoseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/monthstoquarters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/monthstoyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nextday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nextfriday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nextmonday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nextsaturday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nextsunday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nextthursday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nexttuesday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nextwednesday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/parse/_lib/types.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/parse/_lib/setter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/parse/_lib/parser.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/parse/_lib/parsers.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/parse.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/parseiso.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/parsejson.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previousday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previousfriday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previousmonday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previoussaturday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previoussunday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previousthursday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previoustuesday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previouswednesday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/quarterstomonths.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/quarterstoyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/roundtonearesthours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/roundtonearestminutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/secondstohours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/secondstomilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/secondstominutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/set.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setdate.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setdayofyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setdefaultoptions.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/sethours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setisoday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setisoweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setisoweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setmilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setminutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setquarter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofdecade.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofhour.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofisoweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofisoweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofminute.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofquarter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofsecond.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startoftoday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startoftomorrow.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofyesterday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/sub.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subbusinessdays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subdays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subhours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subisoweekyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/submilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subminutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/submonths.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subquarters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subweeks.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/todate.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/transpose.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/weekstodays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/yearstodays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/yearstomonths.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/yearstoquarters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/index.d.mts","../../node_modules/.bun/react-day-picker@8.10.1+97355278ff284068/node_modules/react-day-picker/dist/index.d.ts","./src/components/ui/calendar.tsx","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/alignment.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/noderects.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/axis.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/slidestoscroll.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/limit.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/scrollcontain.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/dragtracker.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/utils.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/animations.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/counter.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/eventhandler.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/eventstore.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/percentofview.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/resizehandler.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/vector1d.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/scrollbody.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/scrollbounds.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/scrolllooper.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/scrollprogress.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/slideregistry.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/scrolltarget.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/scrollto.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/slidefocus.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/translate.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/slidelooper.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/slideshandler.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/slidesinview.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/engine.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/optionshandler.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/plugins.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/emblacarousel.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/draghandler.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/options.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/index.d.ts","../../node_modules/.bun/embla-carousel-react@8.6.0+f4eacebf2041cd4f/node_modules/embla-carousel-react/esm/components/useemblacarousel.d.ts","../../node_modules/.bun/embla-carousel-react@8.6.0+f4eacebf2041cd4f/node_modules/embla-carousel-react/esm/index.d.ts","./src/components/ui/carousel.tsx","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/container/surface.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/container/layer.d.ts","../../node_modules/.bun/@types+d3-time@3.0.4/node_modules/@types/d3-time/index.d.ts","../../node_modules/.bun/@types+d3-scale@4.0.9/node_modules/@types/d3-scale/index.d.ts","../../node_modules/.bun/victory-vendor@36.9.2/node_modules/victory-vendor/d3-scale.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/xaxis.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/yaxis.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/util/types.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/defaultlegendcontent.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/util/payload/getuniqpayload.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/legend.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/defaulttooltipcontent.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/tooltip.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/responsivecontainer.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/cell.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/text.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/label.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/labellist.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/customized.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/sector.d.ts","../../node_modules/.bun/@types+d3-path@3.1.1/node_modules/@types/d3-path/index.d.ts","../../node_modules/.bun/@types+d3-shape@3.1.7/node_modules/@types/d3-shape/index.d.ts","../../node_modules/.bun/victory-vendor@36.9.2/node_modules/victory-vendor/d3-shape.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/curve.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/rectangle.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/polygon.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/dot.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/cross.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/symbols.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/polar/polargrid.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/polar/polarradiusaxis.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/polar/polarangleaxis.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/polar/pie.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/polar/radar.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/polar/radialbar.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/brush.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/util/ifoverflowmatches.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/referenceline.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/referencedot.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/referencearea.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/cartesianaxis.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/cartesiangrid.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/line.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/area.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/util/barutils.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/bar.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/zaxis.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/errorbar.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/scatter.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/util/getlegendprops.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/util/chartutils.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/accessibilitymanager.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/types.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/generatecategoricalchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/linechart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/barchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/piechart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/treemap.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/sankey.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/radarchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/scatterchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/areachart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/radialbarchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/composedchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/sunburstchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/trapezoid.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/numberaxis/funnel.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/funnelchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/util/global.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/index.d.ts","./src/components/ui/chart.tsx","./src/components/ui/collapsible.tsx","../../node_modules/@radix-ui/react-dialog/dist/index.d.mts","../../node_modules/.bun/cmdk@1.1.1+b41f8805ee63d2ff/node_modules/cmdk/dist/index.d.ts","./src/components/ui/command.tsx","../../node_modules/.bun/vaul@1.1.2+b41f8805ee63d2ff/node_modules/vaul/dist/index.d.mts","./src/components/ui/drawer.tsx","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/constants.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/utils/createsubject.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/events.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/path/common.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/path/eager.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/path/index.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/fieldarray.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/resolvers.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/form.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/utils.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/fields.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/errors.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/validator.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/controller.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/index.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/controller.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/form.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/logic/appenderrors.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/logic/createformcontrol.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/logic/index.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/usecontroller.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/usefieldarray.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/useform.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/useformcontext.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/useformstate.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/usewatch.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/utils/get.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/utils/set.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/utils/index.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/index.d.ts","./src/components/ui/form.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/image-timeline-treatment.tsx","../../node_modules/.bun/input-otp@1.4.2+6dbf9a050bc9aadb/node_modules/input-otp/dist/index.d.ts","./src/components/ui/input-otp.tsx","./src/components/ui/menubar.tsx","./src/components/ui/navigation-menu.tsx","./src/components/ui/pagination.tsx","../../node_modules/.bun/libphonenumber-js@1.12.10/node_modules/libphonenumber-js/types.d.ts","../../node_modules/.bun/libphonenumber-js@1.12.10/node_modules/libphonenumber-js/core/index.d.ts","../../node_modules/.bun/libphonenumber-js@1.12.10/node_modules/libphonenumber-js/min/index.d.ts","../../node_modules/.bun/react-phone-number-input@3.4.12+6dbf9a050bc9aadb/node_modules/react-phone-number-input/index.d.ts","../../node_modules/.bun/react-phone-number-input@3.4.12+6dbf9a050bc9aadb/node_modules/react-phone-number-input/flags/index.d.ts","./src/components/ui/phone-input.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/sheet.tsx","./src/hooks/use-mobile.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle.tsx","./src/components/ui/toggle-group.tsx","./.next/types/cache-life.d.ts","./.next/types/app/layout.ts","./.next/types/app/page.ts","./.next/types/app/editor/[project_id]/page.ts","./.next/types/app/projects/page.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/index.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/globals.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/s3.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/fetch.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/bun.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/globals.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/s3.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/fetch.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/bun.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/extensions.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/devserver.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/ffi.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/html-rewriter.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/jsc.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/sqlite.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/test.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/wasm.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/overrides.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/deprecated.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/redis.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/shell.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/experimental.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/bun.ns.d.ts","../../node_modules/.bun/bun-types@1.2.18+fccada1f982c16e9/node_modules/bun-types/index.d.ts","../../node_modules/.bun/@types+bun@1.2.18+fccada1f982c16e9/node_modules/@types/bun/index.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/extensions.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/devserver.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/ffi.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/html-rewriter.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/jsc.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/sqlite.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/test.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/wasm.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/overrides.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/deprecated.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/redis.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/shell.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/experimental.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/bun.ns.d.ts","../../node_modules/.bun/bun-types@1.2.19+fccada1f982c16e9/node_modules/bun-types/index.d.ts","../../node_modules/.bun/@types+bun@1.2.19+fccada1f982c16e9/node_modules/@types/bun/index.d.ts","../../node_modules/.bun/pg-types@2.2.0/node_modules/pg-types/index.d.ts","../../node_modules/.bun/pg-protocol@1.10.3/node_modules/pg-protocol/dist/messages.d.ts","../../node_modules/.bun/pg-protocol@1.10.3/node_modules/pg-protocol/dist/serializer.d.ts","../../node_modules/.bun/pg-protocol@1.10.3/node_modules/pg-protocol/dist/parser.d.ts","../../node_modules/.bun/pg-protocol@1.10.3/node_modules/pg-protocol/dist/index.d.ts","../../node_modules/.bun/@types+pg@8.15.4/node_modules/@types/pg/lib/type-overrides.d.ts","../../node_modules/.bun/@types+pg@8.15.4/node_modules/@types/pg/index.d.ts","../../node_modules/@types/d3-array/index.d.ts","../../node_modules/@types/d3-color/index.d.ts","../../node_modules/@types/d3-ease/index.d.ts","../../node_modules/@types/d3-interpolate/index.d.ts","../../node_modules/@types/d3-path/index.d.ts","../../node_modules/@types/d3-scale/index.d.ts","../../node_modules/@types/d3-shape/index.d.ts","../../node_modules/@types/d3-time/index.d.ts","../../node_modules/@types/d3-timer/index.d.ts","../../node_modules/@types/prop-types/index.d.ts","../../node_modules/@types/use-sync-external-store/index.d.ts"],"fileIdsList":[[88,130,331,1616,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,331,1485,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,331,1505,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,331,1622,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,435,436,437,438,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[82,88,130,181,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,485,486,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,485,488,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,459,468,1366,1462,1491,1493,1506,1507,1508,1509,1510,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,459,468,1366,1463,1491,1493,1506,1507,1508,1509,1510,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1348,1352,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,481,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,135,453,481,532,602,1344,1355,1356,1361,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,135,453,481,602,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,457,468,485,667,1497,1509,1515,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,457,459,485,667,1497,1506,1513,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,459,485,1366,1491,1493,1497,1506,1513,1517,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,468,1384,1407,1478,1540,1550,1559,1572,1577,1580,1581,1615,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,471,602,605,1474,1479,1480,1481,1482,1483,1484,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,485,604,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,485,604,1495,1497,1504,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,459,485,1493,1497,1504,1506,1517,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,457,459,468,1366,1373,1384,1386,1491,1506,1507,1544,1566,1618,1619,1620,1621,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,459,485,1383,1493,1497,1504,1517,1614,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,604,667,1364,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,485,604,667,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1497,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,457,1366,1383,1384,1390,1491,1493,1575,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1383,1491,1578,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,459,1366,1384,1386,1476,1491,1496,1507,1579,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1379,1395,1405,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1564,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1366,1491,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1370,1541,1547,1548,1549,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1366,1369,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1370,1383,1491,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1507,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1380,1384,1386,1387,1392,1475,1491,1507,1542,1543,1544,1546,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1386,1388,1389,1546,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1379,1383,1384,1386,1387,1388,1391,1394,1474,1476,1491,1566,1573,1574,1576,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1388,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1384,1386,1387,1389,1391,1508,1544,1551,1552,1553,1558,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1383,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1372,1386,1388,1507,1554,1555,1556,1557,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1388,1389,1411,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1394,1491,1508,1556,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1380,1384,1386,1387,1388,1389,1394,1408,1411,1412,1475,1482,1491,1543,1551,1556,1568,1569,1570,1571,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1380,1386,1387,1388,1389,1394,1409,1491,1543,1565,1566,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1388,1389,1410,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1380,1384,1386,1387,1388,1389,1394,1411,1567,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,457,459,1464,1490,1502,1503,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1383,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,457,459,1366,1491,1496,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1380,1383,1403,1406,1491,1578,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1490,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,457,459,1366,1490,1491,1492,1493,1494,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1491,1578,1614,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1491,1507,1578,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1377,1380,1384,1387,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1383,1457,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1383,1457,1491,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1383,1459,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1457,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1394,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1383,1457,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1383,1457,1459,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1383,1491,1887,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1383,1491,1924,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1383,1937,1995,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1383,1421,1578,1999,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1383,1457,1459,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1383,1457,1551,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,197,198,199,1366,1383,1394,1482,1491,1545,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1383,2001,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1372,1544,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1383,1457,1508,2032,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1378,1383,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1383,2036,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1383,1491,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1383,1491,1507,1551,1575,2000,2044,2045,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1383,1539,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1383,1447,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1366,1383,1457,1459,1482,1491,1507,1509,1621,2049,2050,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1380,1479,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1460,1461,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1383,1457,1459,2056,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,516,518,532,600,601,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,468,1347,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1379,1386,1387,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1371,1380,1384,1386,1394,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1371,1403,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1380,1386,1394,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1386,1387,1388,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1384,1389,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1388,1389,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1460,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,607,617,648,655,657,661,666,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1467,1469,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1473,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1380,1387,1470,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,602,1358,1360,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1374,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1373,1374,1375,1376,1387,1388,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1373,1388,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1381,1382,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1344,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1369,1378,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1369,1371,1383,1401,1402,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1369,1377,1383,1386,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1369,1401,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1369,1393,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1369,1373,1377,1380,1383,1386,1387,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1369,1377,1379,1380,1383,1384,1385,1387,1388,1389,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1371,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1383,1387,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,513,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1465,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1465,1466,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1468,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1414,1415,1416,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1414,1421,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1415,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1414,1415,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,319,1414,1415,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1414,1415,1430,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1414,1415,1418,1419,1420,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,319,1414,1415,1434,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1414,1415,1418,1420,1428,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1414,1415,1418,1419,1420,1428,1429,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,319,1414,1415,1429,1430,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1414,1415,1418,1438,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1415,1429,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1414,1415,1418,1419,1420,1428,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1414,1415,1426,1427,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1414,1415,1429,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1414,1415,1418,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1414,1415,1429,1453,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1414,1415,1429,1447,1454,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,515,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,517,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,533,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2086,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100,2102],[88,130,1928,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1946,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,608,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,142,162,170,180,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2080,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2096,2097,2098,2099,2100,2104,2105,2108,2109,2110],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100,2110],[88,130,186,197,198,199,346,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,197,198,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,198,346,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,190,196,430,477,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,190,195,430,477,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,183,184,185,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1358,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1358,1359,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1357,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,724,734,985,987,1021,1349,1350,1351,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,724,734,985,987,1349,1350,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,734,1021,1349,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,724,734,985,987,988,1021,1022,1023,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,724,734,985,987,988,1021,1022,1023,1024,1036,1037,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,724,734,985,987,988,1021,1022,1023,1024,1036,1037,1038,1039,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,987,988,1023,1024,1036,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1023,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,724,734,985,987,988,1022,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,734,988,1021,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,986,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,319,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,485,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,125,130,135,143,145,170,174,178,2064,2065,2066,2067,2068,2069,2070,2072,2073,2074,2076,2077,2078,2079,2080,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2096,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,125,130,2064,2065,2066,2067,2068,2069,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,135,153,162,165,170,174,178,2064,2065,2066,2067,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,180,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2075,2076,2077,2078,2079,2080,2081,2082,2083,2084,2085,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,135,143,144,151,170,179,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2096,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,143,2064,2065,2066,2067,2068,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,125,130,135,143,145,170,174,178,2064,2065,2066,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2080,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2096,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,125,130,2064,2065,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2092,2093,2094,2095,2097,2098,2099,2100],[88,125,130,135,153,162,165,170,174,178,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,180,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2091,2092,2093,2094,2095,2096,2097,2098,2099,2100,2101],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2093,2094,2095,2097,2098,2099,2100],[88,130,135,143,144,151,165,170,179,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2080,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2099,2100],[88,130,143,2064,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2097,2098,2099,2100],[88,130,1381,1458,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1381,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1421,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1632,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1630,1632,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1630,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1632,1696,1697,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1699,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1700,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1717,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714,1715,1716,1718,1719,1720,1721,1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809,1810,1811,1812,1813,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1793,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1632,1697,1817,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1630,1814,1815,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1816,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1814,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1630,1631,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,173,180,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,170,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2080,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2096,2097,2098,2099,2100],[81,88,130,170,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2080,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2096,2097,2098,2099,2100],[88,130,1043,1046,1050,1096,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1095,1334,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1335,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1120,1175,1242,1294,1328,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1046,1050,1051,1329,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1089,1094,1116,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1059,1089,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1074,1075,1076,1077,1078,1079,1080,1081,1082,1092,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1062,1091,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1091,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1084,1089,1090,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1089,1091,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1091,1329,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1089,1091,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1074,1075,1076,1077,1078,1079,1080,1081,1082,1091,1092,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1061,1091,1329,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1073,1091,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1073,1089,1091,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1050,1051,1056,1089,1093,1094,1096,1098,1101,1102,1103,1105,1111,1112,1116,1335,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1089,1093,1096,1111,1115,1116,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1089,1093,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1060,1061,1084,1085,1086,1087,1088,1089,1090,1093,1103,1104,1105,1111,1112,1114,1115,1117,1118,1119,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1089,1093,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1085,1089,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1089,1105,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1049,1050,1089,1099,1100,1105,1112,1116,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1106,1107,1108,1109,1110,1113,1116,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1046,1048,1049,1050,1056,1084,1089,1091,1099,1100,1105,1107,1112,1113,1116,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1050,1056,1093,1103,1110,1112,1116,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1089,1096,1099,1100,1105,1112,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1097,1099,1100,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1099,1100,1105,1112,1115,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1049,1050,1051,1056,1089,1093,1094,1095,1099,1100,1103,1105,1112,1116,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1046,1047,1048,1049,1050,1051,1056,1089,1093,1094,1105,1110,1115,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1046,1048,1049,1050,1051,1089,1091,1094,1099,1100,1105,1112,1116,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1061,1089,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1059,1095,1096,1097,1104,1112,1116,1335,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1048,1049,1050,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1046,1060,1083,1084,1086,1087,1088,1090,1091,1329,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1048,1050,1060,1084,1086,1087,1088,1089,1090,1093,1094,1115,1120,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1049,1050,1051,1056,1091,1094,1113,1114,1329,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1044,1046,1047,1048,1051,1059,1096,1099,1329,1330,1331,1332,1333,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1150,1158,1171,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1150,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1141,1142,1143,1144,1145,1153,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1152,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1152,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1150,1151,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1150,1152,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1150,1152,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1141,1142,1143,1144,1145,1152,1153,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1132,1152,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1140,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1050,1051,1096,1150,1157,1158,1163,1164,1165,1166,1168,1171,1335,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1096,1150,1152,1155,1156,1161,1162,1168,1171,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1150,1154,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1121,1147,1148,1149,1150,1151,1154,1157,1163,1165,1167,1168,1169,1170,1172,1173,1174,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1150,1154,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1150,1158,1168,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1050,1051,1099,1150,1152,1163,1168,1171,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1156,1159,1160,1161,1162,1171,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1046,1050,1056,1095,1099,1100,1150,1152,1160,1161,1163,1168,1171,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1157,1159,1163,1171,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1096,1099,1150,1163,1168,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1049,1050,1051,1056,1095,1099,1147,1150,1154,1157,1158,1163,1168,1171,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1046,1047,1048,1049,1050,1051,1056,1150,1154,1158,1159,1168,1170,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1050,1051,1095,1099,1150,1152,1163,1168,1171,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1150,1170,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1095,1096,1163,1167,1171,1335,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1048,1049,1050,1056,1160,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1046,1121,1146,1147,1148,1149,1151,1152,1329,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1048,1121,1147,1148,1149,1150,1151,1158,1159,1170,1175,1334,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1049,1050,1056,1154,1158,1160,1169,1329,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1046,1050,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1217,1223,1236,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1059,1217,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1177,1178,1179,1180,1181,1183,1184,1185,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1220,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1187,1219,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1219,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1219,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1212,1217,1218,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1217,1219,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1219,1329,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1182,1219,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1217,1219,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1177,1178,1179,1180,1181,1183,1184,1185,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1219,1220,1221,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1186,1219,1329,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1189,1219,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1217,1219,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1182,1189,1217,1219,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1182,1217,1219,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1050,1051,1096,1217,1222,1223,1224,1228,1229,1231,1232,1235,1236,1335,1336,1337,1338,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1096,1155,1217,1222,1224,1231,1235,1236,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1217,1222,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1176,1186,1212,1213,1214,1215,1216,1217,1218,1222,1224,1229,1231,1232,1234,1235,1237,1238,1239,1241,1339,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1217,1222,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1213,1217,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1217,1224,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1049,1050,1056,1095,1099,1100,1217,1224,1232,1236,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1225,1226,1227,1228,1230,1233,1236,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1046,1048,1049,1050,1056,1095,1099,1100,1212,1217,1219,1224,1226,1232,1233,1236,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1050,1222,1229,1230,1232,1236,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1096,1099,1100,1217,1224,1232,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1099,1100,1224,1232,1235,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1049,1050,1051,1056,1095,1099,1100,1217,1222,1223,1224,1229,1232,1236,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1046,1047,1048,1049,1050,1051,1056,1217,1222,1223,1224,1230,1235,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1046,1048,1049,1050,1051,1056,1095,1099,1100,1217,1219,1223,1224,1232,1236,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1186,1217,1221,1235,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1059,1095,1096,1097,1232,1236,1335,1339,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1048,1049,1050,1056,1233,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1046,1176,1211,1212,1214,1215,1216,1218,1219,1329,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1048,1050,1176,1212,1214,1215,1216,1217,1218,1222,1223,1235,1242,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1240,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1049,1050,1051,1056,1219,1223,1233,1234,1329,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1042,1043,1051,1339,1340,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1340,1341,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1042,1043,1044,1050,1051,1095,1096,1224,1232,1236,1242,1280,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1059,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1046,1047,1048,1050,1051,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1046,1050,1051,1054,1330,1334,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1329,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1334,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1272,1290,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1262,1263,1264,1265,1266,1267,1274,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1273,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1273,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1272,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1272,1273,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1272,1273,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1059,1273,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1262,1263,1264,1265,1266,1267,1273,1274,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1253,1273,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1261,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1050,1096,1272,1279,1282,1283,1284,1287,1289,1290,1335,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1096,1155,1272,1273,1276,1277,1278,1289,1290,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1269,1270,1271,1272,1275,1279,1284,1287,1288,1289,1291,1292,1293,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1272,1275,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1272,1275,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1272,1289,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1050,1051,1099,1272,1273,1279,1289,1290,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1276,1277,1278,1285,1286,1290,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1046,1050,1099,1100,1272,1273,1277,1279,1289,1290,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1279,1284,1285,1290,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1049,1050,1051,1056,1095,1099,1272,1275,1279,1284,1289,1290,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1046,1047,1048,1049,1050,1051,1056,1272,1275,1285,1289,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1050,1051,1099,1272,1273,1279,1289,1290,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1272,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1095,1096,1279,1288,1290,1335,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1048,1049,1050,1056,1286,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1046,1268,1269,1270,1271,1273,1329,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1048,1050,1269,1270,1271,1272,1294,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1044,1051,1096,1279,1281,1288,1335,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1044,1050,1051,1095,1096,1279,1280,1289,1290,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1050,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1052,1053,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1055,1057,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1050,1056,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1050,1054,1058,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1045,1046,1048,1049,1051,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1300,1321,1326,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1321,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1296,1316,1317,1318,1319,1324,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1323,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1321,1322,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1321,1323,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1296,1316,1317,1318,1319,1323,1324,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1315,1321,1323,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1323,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1051,1321,1323,1329,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1050,1051,1096,1300,1301,1302,1303,1306,1311,1312,1321,1326,1335,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1096,1155,1306,1311,1321,1325,1326,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1321,1325,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1295,1297,1298,1299,1303,1304,1306,1311,1312,1314,1315,1321,1322,1325,1327,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1321,1325,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1306,1314,1321,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1049,1050,1051,1099,1100,1306,1312,1321,1323,1326,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1307,1308,1309,1310,1313,1326,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1049,1050,1051,1056,1099,1100,1297,1306,1308,1312,1313,1321,1323,1326,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1303,1310,1312,1326,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1051,1096,1099,1100,1306,1312,1321,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1097,1099,1100,1312,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1049,1050,1051,1056,1095,1099,1100,1300,1303,1306,1312,1321,1325,1326,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1046,1047,1048,1049,1050,1051,1056,1300,1306,1310,1314,1321,1325,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1048,1049,1050,1051,1099,1100,1300,1306,1312,1321,1323,1326,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1050,1095,1096,1097,1099,1304,1305,1312,1326,1335,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1048,1049,1050,1056,1313,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1046,1295,1297,1298,1299,1320,1322,1323,1329,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1321,1323,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1048,1050,1295,1297,1298,1299,1300,1314,1321,1322,1328,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1049,1050,1056,1300,1313,1323,1329,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1043,1047,1050,1051,1330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1044,1046,1050,1330,1335,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1922,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1923,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1896,1916,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1890,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1891,1895,1896,1897,1898,1899,1901,1903,1904,1909,1910,1919,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1891,1896,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1899,1916,1918,1921,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1890,1891,1892,1893,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1920,1921,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1919,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1889,1891,1892,1894,1902,1911,1914,1915,1920,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1896,1921,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1917,1919,1921,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1890,1891,1896,1899,1919,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1903,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1893,1901,1903,1904,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1893,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1893,1903,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1897,1898,1899,1903,1904,1909,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1899,1900,1904,1908,1910,1919,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1891,1903,1912,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1892,1893,1894,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1899,1919,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1899,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1890,1891,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1891,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1895,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1899,1904,1916,1917,1918,1919,1921,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,623,624,625,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,319,1486,1487,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,319,1486,1487,1488,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,642,644,645,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,612,618,642,644,1610,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,610,642,643,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,608,612,618,641,1610,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,612,618,635,640,642,1610,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,658,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,618,642,1610,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,662,663,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,664,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,653,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,618,640,642,652,1610,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,989,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,989,999,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,827,931,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,931,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,823,825,826,827,931,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,931,948,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,746,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,823,825,826,827,828,931,968,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,822,824,825,968,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,826,931,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,751,752,766,780,781,810,944,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,827,931,948,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,824,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,823,825,826,827,828,931,955,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,822,823,824,825,955,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,768,944,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,823,825,826,827,828,931,961,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,822,823,824,825,961,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,944,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,823,825,826,827,828,931,949,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,823,824,825,949,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,814,937,944,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,822,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,824,825,829,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,823,824,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,824,825,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,824,829,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,787,793,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,784,793,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,849,852,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,746,748,794,831,836,844,845,846,847,850,866,868,877,879,884,885,886,888,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,746,748,784,794,847,863,864,865,888,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,784,793,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,736,737,738,739,740,741,742,743,744,745,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,834,835,837,838,843,844,845,846,847,848,850,851,853,854,855,856,858,859,860,862,863,864,865,866,867,868,870,871,872,873,876,877,878,879,880,881,882,883,884,887,888,889,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,915,916,917,918,919,920,925,927,928,931,932,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,794,821,822,824,825,826,828,830,831,832,877,879,901,908,909,927,928,929,930,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,973,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,750,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,759,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,736,754,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,767,782,783,872,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,738,754,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,736,742,751,752,753,755,760,761,762,763,764,765,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,809,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,736,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,737,738,739,740,749,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,738,742,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,789,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,737,756,757,758,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,736,742,754,767,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,742,748,750,759,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,741,771,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,738,741,754,801,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,767,773,778,779,782,783,791,796,800,807,808,817,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,738,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,741,742,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,742,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,741,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,795,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,798,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,736,738,742,749,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,774,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,738,742,791,796,800,807,808,812,813,814,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,777,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,798,844,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,844,880,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,786,881,882,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,742,778,784,791,800,807,808,809,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,736,738,767,811,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,811,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,736,737,738,739,740,741,742,749,750,751,752,753,754,755,756,757,758,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,788,789,790,791,792,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,820,821,835,843,844,863,864,865,870,871,872,873,878,880,881,882,883,910,911,936,937,938,939,940,941,942,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,736,737,738,739,740,741,742,749,750,751,752,753,754,755,756,757,758,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,788,789,790,791,792,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,820,835,843,844,863,864,865,870,871,872,873,878,880,881,882,883,910,911,936,937,938,939,940,941,942,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,781,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,782,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,782,783,870,871,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,787,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,870,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,736,738,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,767,778,782,783,788,794,795,796,800,801,807,808,810,815,816,818,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,738,742,785,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,738,742,748,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,788,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,767,773,774,775,776,778,779,780,782,783,788,791,792,796,797,799,800,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,742,784,785,787,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,738,786,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,767,773,778,779,783,791,796,800,807,808,811,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,771,910,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,790,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,793,794,843,844,845,846,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,794,835,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,794,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,744,748,850,920,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,784,794,842,887,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,774,887,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,738,845,846,887,911,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,778,848,850,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,747,748,850,925,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,782,794,852,855,888,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,852,870,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,738,748,784,786,787,794,842,844,846,852,856,883,888,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,743,744,745,747,853,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,754,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,850,868,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,788,794,846,852,868,887,888,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,794,797,887,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,742,748,784,794,849,888,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,845,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,844,888,889,938,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,745,748,850,919,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,811,845,846,887,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,794,798,842,888,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,790,794,918,919,920,921,927,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,823,824,830,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,823,824,830,979,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,771,843,844,910,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,821,823,824,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,784,794,847,856,867,873,875,888,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,746,794,845,847,866,878,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,790,793,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,744,746,748,793,794,795,818,819,821,822,830,831,832,845,847,850,851,853,856,858,859,862,867,888,889,914,915,917,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,746,748,794,842,846,866,869,876,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,847,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,743,746,748,793,794,795,815,819,821,822,830,831,832,846,853,859,862,888,912,913,914,915,916,917,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,778,793,847,888,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,784,794,881,883,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,747,748,793,794,810,819,821,822,831,832,845,847,850,851,853,859,888,889,912,913,914,915,917,919,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,819,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,793,794,812,846,847,858,888,889,913,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,794,856,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,782,793,854,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,887,888,914,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,793,794,856,867,872,874,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,846,853,914,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,794,801,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,746,748,794,795,799,800,801,819,821,822,830,831,832,842,845,846,847,850,851,853,856,857,858,859,860,861,862,866,867,888,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,745,746,748,793,794,795,816,819,821,822,830,831,832,845,847,850,851,853,856,858,859,862,867,888,889,913,914,915,917,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,847,888,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,821,823,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,735,736,737,738,739,740,741,742,749,750,751,752,753,754,755,756,757,758,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,788,789,790,791,792,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,820,821,822,823,835,843,844,863,864,865,870,871,872,873,878,880,881,882,883,910,911,936,937,938,939,940,941,942,943,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,754,763,766,768,769,770,772,802,803,804,805,806,810,819,820,821,822,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,743,791,830,831,850,853,868,886,918,921,922,923,924,926,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,821,822,823,824,827,829,830,933,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,822,827,830,933,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,821,822,823,824,827,829,830,831,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,831,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,821,822,823,824,827,829,830,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,754,794,821,822,824,830,901,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,902,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,755,793,833,836,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,749,766,793,821,822,831,832,837,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,766,768,793,794,821,822,831,832,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,766,793,794,821,822,831,832,834,836,837,838,839,840,841,890,891,892,893,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,766,793,821,822,831,832,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,737,793,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,749,750,793,794,833,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,768,793,794,821,822,831,832,847,887,889,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,769,793,821,822,831,832,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,770,793,794,821,822,831,832,834,836,837,891,892,893,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,772,793,821,822,831,832,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,793,802,821,822,831,832,868,902,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,763,793,821,822,831,832,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,793,803,821,822,831,832,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,793,804,821,822,831,832,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,793,805,821,822,831,832,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,793,806,821,822,831,832,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,749,756,793,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,757,793,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,793,820,821,822,831,832,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,830,831,894,895,896,897,898,899,900,903,904,905,906,907,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,758,793,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,794,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,743,744,745,747,748,822,832,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,748,822,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,743,744,745,746,747,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2041,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,618,642,1582,1583,1584,1608,1609,1610,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,618,642,1583,1610,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,618,642,1582,1583,1610,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,612,618,642,1582,1584,1610,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1486,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1489,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1026,1027,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1025,1026,1029,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1025,1031,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1025,1026,1027,1028,1029,1030,1032,1033,1034,1035,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1026,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1025,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,192,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,433,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,440,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,203,217,218,219,221,427,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,203,242,244,246,247,250,427,429,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,203,207,209,210,211,212,213,416,427,429,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,427,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,218,313,397,406,423,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,203,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,200,423,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,254,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,253,427,429,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,295,313,342,483,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,306,323,406,422,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,358,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,410,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,409,410,411,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,409,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,194,200,203,207,210,214,215,216,218,222,230,231,351,386,407,427,430,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,203,220,238,242,243,248,249,427,483,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,220,483,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,231,238,293,427,483,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,483,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,203,220,221,483,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,245,483,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,214,408,415,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,156,319,423,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,319,423,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,319,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,314,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,310,356,423,466,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,403,460,461,462,463,465,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,402,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,402,403,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,211,352,353,354,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,352,355,356,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,464,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,352,356,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,204,454,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,173,186,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,220,283,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,220,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,281,285,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,282,432,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1471,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,180,186,190,195,196,430,475,476,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,207,262,352,362,376,397,412,413,427,428,483,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,230,414,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,430,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,202,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,295,309,322,332,334,422,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,156,295,309,331,332,333,422,482,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,325,326,327,328,329,330,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,327,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,331,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,282,319,432,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,319,431,432,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,319,432,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,376,419,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,419,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,428,432,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,318,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,129,130,317,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,232,263,302,303,305,306,307,308,349,352,422,425,428,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,232,303,352,356,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,306,422,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,306,315,316,318,320,321,322,323,324,335,336,337,338,339,340,341,422,423,483,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,300,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,156,232,233,262,277,307,349,350,351,356,376,397,418,427,428,429,430,483,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,422,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,129,130,218,303,304,307,351,418,420,421,428,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,306,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,129,130,262,267,296,297,298,299,300,301,302,305,422,423,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,267,268,296,428,429,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,218,303,351,352,376,418,422,428,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,427,429,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,162,425,428,429,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,156,173,200,207,220,232,233,235,263,264,269,274,277,302,307,352,362,364,367,369,372,373,374,375,397,417,418,423,425,427,428,429,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,162,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,203,204,205,215,417,425,426,430,432,483,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,162,173,250,252,254,255,256,257,483,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,156,173,200,242,252,273,274,275,276,302,352,367,376,382,385,387,397,418,423,425,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,214,215,230,351,386,418,427,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,173,204,207,302,380,425,427,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,294,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,383,384,394,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,425,427,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,303,304,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,302,307,417,432,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,156,236,242,276,367,376,382,385,389,425,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,214,230,242,390,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,203,235,392,417,427,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,173,427,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,220,234,235,236,247,258,391,393,417,427,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,194,232,307,396,430,432,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,156,173,207,214,222,230,233,263,269,273,274,275,276,277,302,352,364,376,377,379,381,397,417,418,423,424,425,432,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,162,214,382,388,394,425,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,225,226,227,228,229,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,264,368,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,370,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,368,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,370,371,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,207,262,428,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,156,202,204,232,263,277,307,360,361,397,425,429,430,432,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,156,173,206,211,302,361,424,428,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,296,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,297,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,298,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,423,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,251,260,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,207,251,263,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,259,260,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,261,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,251,252,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,251,278,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,251,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,264,366,424,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,365,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,252,423,424,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,363,424,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,252,423,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,349,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,263,292,295,302,303,309,312,343,345,348,352,396,425,428,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,286,289,290,291,310,311,356,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,197,198,199,319,344,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,197,198,199,319,344,347,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,405,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,218,268,306,307,318,323,352,396,398,399,400,401,403,404,407,417,422,427,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,356,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,360,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,263,279,357,359,362,396,425,430,432,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,286,287,288,289,290,291,310,311,356,431,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,156,173,194,233,251,252,277,302,307,394,395,397,417,418,427,428,430,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,268,270,273,418,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,264,427,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,267,306,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,266,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,268,269,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,265,267,427,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,206,268,270,271,272,427,428,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,352,353,355,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,237,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,204,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,423,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,194,277,307,430,432,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,204,454,455,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,285,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,156,173,186,202,249,280,282,284,432,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,220,423,428,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,156,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,378,423,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,143,145,156,186,202,238,244,285,430,431,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,195,196,430,477,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,187,188,189,190,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,135,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,239,240,241,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,239,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,147,156,180,186,190,195,196,197,199,200,202,233,331,389,429,432,477,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,442,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,444,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,446,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1472,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,448,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,450,451,452,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,456,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,191,193,434,439,441,443,445,447,449,453,457,459,468,469,471,481,482,483,484,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,458,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,467,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,282,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,470,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,129,130,268,270,271,273,322,423,472,473,474,477,478,479,480,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,180,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,620,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,619,620,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,619,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,619,620,621,627,628,631,632,633,634,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,620,628,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,619,620,621,627,628,629,630,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,619,628,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,628,632,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,620,621,622,626,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,621,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,619,620,628,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,180,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100,2105,2106,2107],[88,130,162,180,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100,2105],[88,130,505,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,503,505,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,494,502,503,504,506,508,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,492,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,495,500,505,508,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,491,508,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,495,496,499,500,501,508,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,495,496,497,499,500,508,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,492,493,494,495,496,500,501,502,504,505,506,508,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,508,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,490,492,493,494,495,496,497,499,500,501,502,503,504,505,506,507,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,490,508,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,495,497,498,500,501,508,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,499,508,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,500,501,505,508,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,493,503,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,162,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,637,638,639,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,636,640,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,640,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1413,1416,1417,1420,1421,1422,1423,1424,1425,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1886,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,2017,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2017,2018,2019,2022,2023,2024,2025,2026,2027,2028,2031,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2017,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2020,2021,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,2015,2017,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2012,2013,2015,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2008,2011,2013,2015,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2012,2015,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,2003,2004,2005,2008,2009,2010,2012,2013,2014,2015,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2005,2008,2009,2010,2011,2012,2013,2014,2015,2016,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2012,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2006,2012,2013,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2006,2007,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2011,2013,2014,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2011,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2003,2008,2013,2014,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2029,2030,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1501,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1498,1499,1500,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1613,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,617,618,642,648,655,1610,1612,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2044,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,2042,2043,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1519,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1536,1537,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1520,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1522,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1520,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1519,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1535,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1538,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1931,1932,1933,1949,1952,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1931,1932,1933,1942,1950,1970,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1930,1933,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1933,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1931,1932,1933,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1931,1932,1933,1968,1971,1974,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1931,1932,1933,1942,1949,1952,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1931,1932,1933,1942,1950,1962,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1931,1932,1933,1942,1952,1962,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1931,1932,1933,1942,1962,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1931,1932,1933,1937,1943,1949,1954,1972,1973,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1933,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1933,1977,1978,1979,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1933,1976,1977,1978,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1933,1950,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1933,1976,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1933,1942,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1933,1934,1935,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1933,1935,1937,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1926,1927,1931,1932,1933,1934,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1963,1964,1965,1966,1967,1968,1969,1971,1972,1973,1974,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1933,1991,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1933,1945,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1933,1952,1956,1957,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1933,1943,1945,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1933,1948,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1933,1971,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1933,1948,1975,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1936,1976,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,186,1930,1931,1932,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,660,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,618,642,659,1610,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,617,618,642,646,647,648,655,1610,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,612,618,642,646,1610,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,664,665,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,618,642,664,1610,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,656,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,617,618,642,648,654,655,1610,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1610,1611,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,612,617,618,642,648,655,1582,1610,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,162,180,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,651,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,649,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,649,650,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,510,511,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,509,512,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,614,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,91,92,93,94,95,96,97,98,99,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,119,120,121,122,123,124,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,612,616,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,608,612,613,615,617,648,655,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,609,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,610,611,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,608,610,612,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1929,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1947,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1560,1564,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1560,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1561,1562,1563,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,531,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,522,523,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,519,520,522,524,525,530,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,520,522,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,530,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,522,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,519,520,522,525,526,527,528,529,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,519,520,521,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,724,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,724,727,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,717,724,725,726,727,728,729,730,731,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,732,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,724,725,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,724,726,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,670,672,673,674,675,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,670,672,674,675,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,670,672,674,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,670,672,673,675,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,670,672,675,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,670,671,672,673,674,675,676,677,717,718,719,720,721,722,723,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,672,675,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,669,670,671,673,674,675,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,672,718,722,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,672,673,674,675,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,733,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,674,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,598,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,590,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,590,593,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,583,590,591,592,593,594,595,596,597,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,590,591,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,590,592,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,536,538,539,540,541,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,536,538,540,541,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,536,538,540,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,536,538,539,541,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,536,538,541,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,536,537,538,539,540,541,542,543,583,584,585,586,587,588,589,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,538,541,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,535,536,537,539,540,541,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,538,584,588,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,538,539,540,541,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,540,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1367,1368,1396,1397,1398,1400,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1396,1397,1398,1399,1400,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1367,1396,1397,1398,1400,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100,2112],[88,127,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,129,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,135,165,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,131,136,142,143,150,162,173,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,131,132,142,150,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[83,84,85,88,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,133,174,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,134,135,143,151,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,135,162,170,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,136,138,142,150,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,129,130,137,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,138,139,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,140,142,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,129,130,142,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,142,143,144,162,173,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,142,143,144,157,162,165,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2080,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2096,2097,2098,2099,2100],[88,125,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,125,130,138,142,145,150,162,173,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,142,143,145,146,150,162,170,173,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,147,162,170,173,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[86,87,88,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,142,148,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,149,173,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,138,142,150,162,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,151,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,152,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,129,130,153,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,127,128,129,130,131,132,133,134,135,136,137,138,139,140,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2080,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2096,2097,2098,2099,2100],[88,130,155,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,142,157,158,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,157,159,174,176,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,142,162,163,165,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,164,165,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,162,163,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,165,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2096,2097,2098,2099,2100],[88,130,166,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,127,130,162,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,142,168,169,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,168,169,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,135,150,162,170,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2080,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2096,2097,2098,2099,2100],[88,130,171,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,150,172,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,145,156,173,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,135,174,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,162,175,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,149,176,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,177,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,142,144,153,162,165,173,176,178,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,162,179,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,97,101,130,173,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,97,130,162,173,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,92,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,94,97,130,170,173,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2080,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2096,2097,2098,2099,2100],[88,130,150,170,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2080,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2096,2097,2098,2099,2100],[88,92,130,180,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,94,97,130,150,173,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,89,90,93,96,130,142,162,173,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,97,104,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,89,95,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,97,118,119,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,93,97,130,165,173,180,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2096,2097,2098,2099,2100],[88,118,130,180,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,91,92,130,180,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,97,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,97,112,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,97,104,105,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,95,97,105,106,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,96,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,89,92,97,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,97,101,105,106,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,101,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,95,97,100,130,173,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,89,94,97,104,130,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,92,97,118,130,178,180,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,600,1346,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,600,1345,1347,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,534,599,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,600,1040,1041,1344,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,601,1042,1334,1342,1343,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100],[88,130,1242,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2076,2077,2078,2079,2081,2082,2083,2084,2088,2089,2090,2092,2093,2094,2095,2097,2098,2099,2100]],"fileInfos":[{"version":"69684132aeb9b5642cbcd9e22dff7818ff0ee1aa831728af0ecf97d3364d5546","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","impliedFormat":1},{"version":"8fd575e12870e9944c7e1d62e1f5a73fcf23dd8d3a321f2a2c74c20d022283fe","impliedFormat":1},{"version":"8bf8b5e44e3c9c36f98e1007e8b7018c0f38d8adc07aecef42f5200114547c70","impliedFormat":1},{"version":"092c2bfe125ce69dbb1223c85d68d4d2397d7d8411867b5cc03cec902c233763","affectsGlobalScope":true,"impliedFormat":1},{"version":"07f073f19d67f74d732b1adea08e1dc66b1b58d77cb5b43931dee3d798a2fd53","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"936e80ad36a2ee83fc3caf008e7c4c5afe45b3cf3d5c24408f039c1d47bdc1df","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"fef8cfad2e2dc5f5b3d97a6f4f2e92848eb1b88e897bb7318cef0e2820bceaab","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"b5ce7a470bc3628408429040c4e3a53a27755022a32fd05e2cb694e7015386c7","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","affectsGlobalScope":true,"impliedFormat":1},{"version":"87dc0f382502f5bbce5129bdc0aea21e19a3abbc19259e0b43ae038a9fc4e326","affectsGlobalScope":true,"impliedFormat":1},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true,"impliedFormat":1},{"version":"56e4ed5aab5f5920980066a9409bfaf53e6d21d3f8d020c17e4de584d29600ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ece9f17b3866cc077099c73f4983bddbcb1dc7ddb943227f1ec070f529dedd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a6282c8827e4b9a95f4bf4f5c205673ada31b982f50572d27103df8ceb8013c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c9319a09485199c1f7b0498f2988d6d2249793ef67edda49d1e584746be9032","affectsGlobalScope":true,"impliedFormat":1},{"version":"e3a2a0cee0f03ffdde24d89660eba2685bfbdeae955a6c67e8c4c9fd28928eeb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811c71eee4aa0ac5f7adf713323a5c41b0cf6c4e17367a34fbce379e12bbf0a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"60037901da1a425516449b9a20073aa03386cce92f7a1fd902d7602be3a7c2e9","affectsGlobalScope":true,"impliedFormat":1},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true,"impliedFormat":1},{"version":"22adec94ef7047a6c9d1af3cb96be87a335908bf9ef386ae9fd50eeb37f44c47","affectsGlobalScope":true,"impliedFormat":1},{"version":"4245fee526a7d1754529d19227ecbf3be066ff79ebb6a380d78e41648f2f224d","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"92d5369f7f9480aac66bbcd295c0b2d82077295c8bf0f0fe08fa3c3321225e49","impliedFormat":99},{"version":"878390f2f3d349610300c7603fb9dff16cfb92fcc6f5fc1f9b262e5bbd6479e5","impliedFormat":99},{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"a79e62f1e20467e11a904399b8b18b18c0c6eea6b50c1168bf215356d5bebfaf","affectsGlobalScope":true,"impliedFormat":1},{"version":"d802f0e6b5188646d307f070d83512e8eb94651858de8a82d1e47f60fb6da4e2","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"a12d953aa755b14ac1d28ecdc1e184f3285b01d6d1e58abc11bf1826bc9d80e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"a38efe83ff77c34e0f418a806a01ca3910c02ee7d64212a59d59bca6c2c38fa1","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"3fe4022ba1e738034e38ad9afacbf0f1f16b458ed516326f5bf9e4a31e9be1dc","impliedFormat":1},{"version":"a957197054b074bcdf5555d26286e8461680c7c878040d0f4e2d5509a7524944","affectsGlobalScope":true,"impliedFormat":1},{"version":"4314c7a11517e221f7296b46547dbc4df047115b182f544d072bdccffa57fc72","impliedFormat":1},{"version":"e9b97d69510658d2f4199b7d384326b7c4053b9e6645f5c19e1c2a54ede427fc","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"f478f6f5902dc144c0d6d7bdc919c5177cac4d17a8ca8653c2daf6d7dc94317f","affectsGlobalScope":true,"impliedFormat":1},{"version":"19d5f8d3930e9f99aa2c36258bf95abbe5adf7e889e6181872d1cdba7c9a7dd5","impliedFormat":1},{"version":"9855e02d837744303391e5623a531734443a5f8e6e8755e018c41d63ad797db2","impliedFormat":1},{"version":"0a6b3ad6e19dd0fe347a54cbfd8c8bd5091951a2f97b2f17e0af011bfde05482","impliedFormat":1},{"version":"0a37a4672f163d7fe46a414923d0ef1b0526dcd2d2d3d01c65afe6da03bf2495","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"71450bbc2d82821d24ca05699a533e72758964e9852062c53b30f31c36978ab8","affectsGlobalScope":true,"impliedFormat":1},{"version":"88bc59b32d0d5b4e5d9632ac38edea23454057e643684c3c0b94511296f2998c","affectsGlobalScope":true,"impliedFormat":1},{"version":"25d130083f833251b5b4c2794890831b1b8ce2ead24089f724181576cf9d7279","impliedFormat":1},{"version":"ffe66ee5c9c47017aca2136e95d51235c10e6790753215608bff1e712ff54ec6","impliedFormat":1},{"version":"206a70e72af3e24688397b81304358526ce70d020e4c2606c4acfd1fa1e81fb2","impliedFormat":1},{"version":"017caf5d2a8ef581cf94f678af6ce7415e06956317946315560f1487b9a56167","impliedFormat":1},{"version":"528b62e4272e3ddfb50e8eed9e359dedea0a4d171c3eb8f337f4892aac37b24b","impliedFormat":1},{"version":"d71535813e39c23baa113bc4a29a0e187b87d1105ccc8c5a6ebaca38d9a9bff2","impliedFormat":1},{"version":"aa9e37a18f4a50ea4bb5f118d03d144cc779b778e0e3fe60ee80c3add19e613b","affectsGlobalScope":true,"impliedFormat":1},{"version":"f72bc8fe16da67e4e3268599295797b202b95e54bd215a03f97e925dd1502a36","impliedFormat":1},{"version":"b1b6ee0d012aeebe11d776a155d8979730440082797695fc8e2a5c326285678f","impliedFormat":1},{"version":"45875bcae57270aeb3ebc73a5e3fb4c7b9d91d6b045f107c1d8513c28ece71c0","impliedFormat":1},{"version":"915e18c559321c0afaa8d34674d3eb77e1ded12c3e85bf2a9891ec48b07a1ca5","affectsGlobalScope":true,"impliedFormat":1},{"version":"636302a00dfd1f9fe6e8e91e4e9350c6518dcc8d51a474e4fc3a9ba07135100b","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"8145e07aad6da5f23f2fcd8c8e4c5c13fb26ee986a79d03b0829b8fce152d8b2","impliedFormat":1},{"version":"e1120271ebbc9952fdc7b2dd3e145560e52e06956345e6fdf91d70ca4886464f","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"e1ce1d622f1e561f6cdf246372ead3bbc07ce0342024d0e9c7caf3136f712698","impliedFormat":1},{"version":"199c8269497136f3a0f4da1d1d90ab033f899f070e0dd801946f2a241c8abba2","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"125d792ec6c0c0f657d758055c494301cc5fdb327d9d9d5960b3f129aff76093","impliedFormat":1},{"version":"27e4532aaaa1665d0dd19023321e4dc12a35a741d6b8e1ca3517fcc2544e0efe","affectsGlobalScope":true,"impliedFormat":1},{"version":"2754d8221d77c7b382096651925eb476f1066b3348da4b73fe71ced7801edada","impliedFormat":1},{"version":"6266d94fb9165d42716e45f3a537ca9f59c07b1dfa8394a659acf139134807db","affectsGlobalScope":true,"impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"98ffdf93dfdd206516971d28e3e473f417a5cfd41172e46b4ce45008f640588e","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"7d6ff413e198d25639f9f01f16673e7df4e4bd2875a42455afd4ecc02ef156da","affectsGlobalScope":true,"impliedFormat":1},{"version":"a7692a54334fd08960cd0c610ff509c2caa93998e0dcefa54021489bcc67c22d","affectsGlobalScope":true,"impliedFormat":1},{"version":"0f7e00beefa952297cde4638b2124d2d9a1eed401960db18edcadaa8500c78eb","impliedFormat":1},{"version":"3a051941721a7f905544732b0eb819c8d88333a96576b13af08b82c4f17581e4","impliedFormat":1},{"version":"ac5ed35e649cdd8143131964336ab9076937fa91802ec760b3ea63b59175c10a","impliedFormat":1},{"version":"1e25f8d0a8573cafd5b5a16af22d26ab872068a693b9dbccd3f72846ab373655","affectsGlobalScope":true,"impliedFormat":1},{"version":"3797dd6f4ea3dc15f356f8cdd3128bfa18122213b38a80d6c1f05d8e13cbdad8","impliedFormat":1},{"version":"ad90122e1cb599b3bc06a11710eb5489101be678f2920f2322b0ac3e195af78d","impliedFormat":1},{"version":"0c5f112b6d3377b9e8214d8920e1a69d8098b881d941f2ab3ca45234d13d68de","impliedFormat":1},{"version":"7328fb56302eb8df2722d4065c7e1618ebb20d919dac48318cbedbb7fbd36604","signature":"be4efb33a02376028077c85dc024a66f2dc9654f3e8a4bf126cb057c079730a1"},{"version":"eb5b19b86227ace1d29ea4cf81387279d04bb34051e944bc53df69f58914b788","affectsGlobalScope":true,"impliedFormat":1},{"version":"8a8eb4ebffd85e589a1cc7c178e291626c359543403d58c9cd22b81fab5b1fb9","impliedFormat":1},{"version":"87d9d29dbc745f182683f63187bf3d53fd8673e5fca38ad5eaab69798ed29fbc","impliedFormat":1},{"version":"472f5aab7edc498a0a761096e8e254c5bc3323d07a1e7f5f8b8ec0d6395b60a0","affectsGlobalScope":true,"impliedFormat":1},{"version":"acd8fd5090ac73902278889c38336ff3f48af6ba03aa665eb34a75e7ba1dccc4","impliedFormat":1},{"version":"d6258883868fb2680d2ca96bc8b1352cab69874581493e6d52680c5ffecdb6cc","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"f258e3960f324a956fc76a3d3d9e964fff2244ff5859dcc6ce5951e5413ca826","impliedFormat":1},{"version":"643f7232d07bf75e15bd8f658f664d6183a0efaca5eb84b48201c7671a266979","impliedFormat":1},{"version":"0f6666b58e9276ac3a38fdc80993d19208442d6027ab885580d93aec76b4ef00","impliedFormat":1},{"version":"05fd364b8ef02fb1e174fbac8b825bdb1e5a36a016997c8e421f5fab0a6da0a0","impliedFormat":1},{"version":"631eff75b0e35d1b1b31081d55209abc43e16b49426546ab5a9b40bdd40b1f60","impliedFormat":1},{"version":"3609e455ffcba8176c8ce0aa57f8258fe10cf03987e27f1fab68f702b4426521","impliedFormat":1},{"version":"d1bd4e51810d159899aad1660ccb859da54e27e08b8c9862b40cd36c1d9ff00f","impliedFormat":1},{"version":"17ed71200119e86ccef2d96b73b02ce8854b76ad6bd21b5021d4269bec527b5f","impliedFormat":1},{"version":"1cfa8647d7d71cb03847d616bd79320abfc01ddea082a49569fda71ac5ece66b","impliedFormat":1},{"version":"bb7a61dd55dc4b9422d13da3a6bb9cc5e89be888ef23bbcf6558aa9726b89a1c","impliedFormat":1},{"version":"413df52d4ea14472c2fa5bee62f7a40abd1eb49be0b9722ee01ee4e52e63beb2","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"77497ec7d02338725444582c8ae7eb2085243a9f8c4113ca40b9b4fd941f2319","impliedFormat":1},{"version":"24f8562308dd8ba6013120557fa7b44950b619610b2c6cb8784c79f11e3c4f90","impliedFormat":1},{"version":"ba1ae645ccbff0137326f99084f5cf87c9fa988c59906177d59deabeee9e428d","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"57d6ac03382e30e9213641ff4f18cf9402bb246b77c13c8e848c0b1ca2b7ef92","impliedFormat":1},{"version":"ce75b1aebb33d510ff28af960a9221410a3eaf7f18fc5f21f9404075fba77256","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"496bbf339f3838c41f164238543e9fe5f1f10659cb30b68903851618464b98ba","impliedFormat":1},{"version":"44e0a682d3a20df46bbf8e7e37f2f10b1604d4ab08b3beda1c365e6d9c3ec74d","impliedFormat":1},{"version":"97395dc4fd32e20b8888849266065caf0b45d12575242c308e8604a4288ec3e5","impliedFormat":1},{"version":"9e2739b32f741859263fdba0244c194ca8e96da49b430377930b8f721d77c000","impliedFormat":1},{"version":"fb1d8e814a3eeb5101ca13515e0548e112bd1ff3fb358ece535b93e94adf5a3a","impliedFormat":1},{"version":"ffa495b17a5ef1d0399586b590bd281056cee6ce3583e34f39926f8dcc6ecdb5","impliedFormat":1},{"version":"98b18458acb46072947aabeeeab1e410f047e0cacc972943059ca5500b0a5e95","impliedFormat":1},{"version":"361e2b13c6765d7f85bb7600b48fde782b90c7c41105b7dab1f6e7871071ba20","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"b6db56e4903e9c32e533b78ac85522de734b3d3a8541bf24d256058d464bf04b","impliedFormat":1},{"version":"24daa0366f837d22c94a5c0bad5bf1fd0f6b29e1fae92dc47c3072c3fdb2fbd5","impliedFormat":1},{"version":"570bb5a00836ffad3e4127f6adf581bfc4535737d8ff763a4d6f4cc877e60d98","impliedFormat":1},{"version":"889c00f3d32091841268f0b994beba4dceaa5df7573be12c2c829d7c5fbc232c","impliedFormat":1},{"version":"65f43099ded6073336e697512d9b80f2d4fec3182b7b2316abf712e84104db00","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"12b8dfed70961bea1861e5d39e433580e71323abb5d33da6605182ec569db584","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"7e560f533aaf88cf9d3b427dcf6c112dd3f2ee26d610e2587583b6c354c753db","impliedFormat":1},{"version":"71e0082342008e4dfb43202df85ea0986ef8e003c921a1e49999d0234a3019da","impliedFormat":1},{"version":"27ab780875bcbb65e09da7496f2ca36288b0c541abaa75c311450a077d54ec15","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"380647d8f3b7f852cca6d154a376dbf8ac620a2f12b936594504a8a852e71d2f","impliedFormat":1},{"version":"208c9af9429dd3c76f5927b971263174aaa4bc7621ddec63f163640cbd3c473c","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"a23185bc5ef590c287c28a91baf280367b50ae4ea40327366ad01f6f4a8edbc5","impliedFormat":1},{"version":"bb37588926aba35c9283fe8d46ebf4e79ffe976343105f5c6d45f282793352b2","impliedFormat":1},{"version":"002eae065e6960458bda3cf695e578b0d1e2785523476f8a9170b103c709cd4f","impliedFormat":1},{"version":"c51641ab4bfa31b7a50a0ca37edff67f56fab3149881024345b13f2b48b7d2de","impliedFormat":1},{"version":"05c97cddbaf99978f83d96de2d8af86aded9332592f08ce4a284d72d0952c391","impliedFormat":1},{"version":"72179f9dd22a86deaad4cc3490eb0fe69ee084d503b686985965654013f1391b","impliedFormat":1},{"version":"2e6114a7dd6feeef85b2c80120fdbfb59a5529c0dcc5bfa8447b6996c97a69f5","impliedFormat":1},{"version":"7b6ff760c8a240b40dab6e4419b989f06a5b782f4710d2967e67c695ef3e93c4","impliedFormat":1},{"version":"c8f004e6036aa1c764ad4ec543cf89a5c1893a9535c80ef3f2b653e370de45e6","impliedFormat":1},{"version":"dd80b1e600d00f5c6a6ba23f455b84a7db121219e68f89f10552c54ba46e4dc9","impliedFormat":1},{"version":"b064c36f35de7387d71c599bfcf28875849a1dbc733e82bd26cae3d1cd060521","impliedFormat":1},{"version":"6a148329edecbda07c21098639ef4254ef7869fb25a69f58e5d6a8b7b69d4236","impliedFormat":1},{"version":"8de9fe97fa9e00ec00666fa77ab6e91b35d25af8ca75dabcb01e14ad3299b150","impliedFormat":1},{"version":"f63ab283a1c8f5c79fabe7ca4ef85f9633339c4f0e822fce6a767f9d59282af2","impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"a54c996c8870ef1728a2c1fa9b8eaec0bf4a8001cd2583c02dd5869289465b10","impliedFormat":1},{"version":"3e7efde639c6a6c3edb9847b3f61e308bf7a69685b92f665048c45132f51c218","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"0c28b634994a944d8cb9ea841b80f861827ea4fbe16fb2152b039aba5d1af801","impliedFormat":1},{"version":"33117f749afa2a897890989c3f75cbf86119bf81a8899f227cdc86c9166cd896","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"50256e9c31318487f3752b7ac12ff365c8949953e04568009c8705db802776fb","impliedFormat":1},{"version":"7d73b24e7bf31dfb8a931ca6c4245f6bb0814dfae17e4b60c9e194a631fe5f7b","impliedFormat":1},{"version":"8d1fd7b451f69cd173e6e20272e0d64ba4a8a1fe0eb3ef5f82134a5b0cb7c9df","impliedFormat":1},{"version":"d6e73f8010935b7b4c7487b6fb13ea197cc610f0965b759bec03a561ccf8423a","impliedFormat":1},{"version":"174f3864e398f3f33f9a446a4f403d55a892aa55328cf6686135dfaf9e171657","impliedFormat":1},{"version":"824c76aec8d8c7e65769688cbee102238c0ef421ed6686f41b2a7d8e7e78a931","impliedFormat":1},{"version":"75b868be3463d5a8cfc0d9396f0a3d973b8c297401d00bfb008a42ab16643f13","impliedFormat":1},{"version":"05c8cd040dc6b8aa18f310b12eaf0407dc4d122ec035dc5b0c9b97e795abfeec","impliedFormat":1},{"version":"d682336018141807fb602709e2d95a192828fcb8d5ba06dda3833a8ea98f69e3","impliedFormat":1},{"version":"6124e973eab8c52cabf3c07575204efc1784aca6b0a30c79eb85fe240a857efa","impliedFormat":1},{"version":"0d891735a21edc75df51f3eb995e18149e119d1ce22fd40db2b260c5960b914e","impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","impliedFormat":1},{"version":"51b1709e7ad186919a0e30237a8607100143a86d28771b3d3f046359aca1e65c","impliedFormat":1},{"version":"b01bd582a6e41457bc56e6f0f9de4cb17f33f5f3843a7cf8210ac9c18472fb0f","impliedFormat":1},{"version":"0a437ae178f999b46b6153d79095b60c42c996bc0458c04955f1c996dc68b971","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"4a7baeb6325920044f66c0f8e5e6f1f52e06e6d87588d837bdf44feb6f35c664","impliedFormat":1},{"version":"6dcf60530c25194a9ee0962230e874ff29d34c59605d8e069a49928759a17e0a","impliedFormat":1},{"version":"7274fbffbd7c9589d8d0ffba68157237afd5cecff1e99881ea3399127e60572f","impliedFormat":1},{"version":"1a42d2ec31a1fe62fdc51591768695ed4a2dc64c01be113e7ff22890bebb5e3f","impliedFormat":1},{"version":"1a82deef4c1d39f6882f28d275cad4c01f907b9b39be9cbc472fcf2cf051e05b","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"0c7c947ff881c4274c0800deaa0086971e0bfe51f89a33bd3048eaa3792d4876","affectsGlobalScope":true,"impliedFormat":1},{"version":"db01d18853469bcb5601b9fc9826931cc84cc1a1944b33cad76fd6f1e3d8c544","affectsGlobalScope":true,"impliedFormat":1},{"version":"a8f8e6ab2fa07b45251f403548b78eaf2022f3c2254df3dc186cb2671fe4996d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"15b36126e0089bfef173ab61329e8286ce74af5e809d8a72edcafd0cc049057f","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"a57b1802794433adec9ff3fed12aa79d671faed86c49b09e02e1ac41b4f1d33a","impliedFormat":1},{"version":"ad10d4f0517599cdeca7755b930f148804e3e0e5b5a3847adce0f1f71bbccd74","impliedFormat":1},{"version":"1042064ece5bb47d6aba91648fbe0635c17c600ebdf567588b4ca715602f0a9d","impliedFormat":1},{"version":"4360ad4de54de2d5c642c4375d5eab0e7fe94ebe8adca907e6c186bbef75a54d","impliedFormat":1},{"version":"4a889f2c763edb4d55cb624257272ac10d04a1cad2ed2948b10ed4a7fda2a428","impliedFormat":1},{"version":"7bb79aa2fead87d9d56294ef71e056487e848d7b550c9a367523ee5416c44cfa","impliedFormat":1},{"version":"43542b120b07d198a86a21f6df97e6fe4a6327e960342777eefaa407baee2a62","impliedFormat":1},{"version":"090fa057d7b2c429119fde252e3b7276a7d75a3ec172a9a23aa922dfac5345e8","impliedFormat":1},{"version":"27ff4196654e6373c9af16b6165120e2dd2169f9ad6abb5c935af5abd8c7938c","impliedFormat":1},{"version":"24428762d0c97b44c4784d28eee9556547167c4592d20d542a79243f7ca6a73f","impliedFormat":1},{"version":"d6406c629bb3efc31aedb2de809bef471e475c86c7e67f3ef9b676b5d7e0d6b2","impliedFormat":1},{"version":"8c030e515014c10a2b98f9f48408e3ba18023dfd3f56e3312c6c2f3ae1f55a16","impliedFormat":1},{"version":"dafc31e9e8751f437122eb8582b93d477e002839864410ff782504a12f2a550c","impliedFormat":1},{"version":"4e31a4e6319cee44ce4cec0f8892c60289cfbdbec11dda19c85559bb8ab53bc2","impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","impliedFormat":1},{"version":"f56bdc6884648806d34bc66d31cdb787c4718d04105ce2cd88535db214631f82","impliedFormat":1},{"version":"20e06cdda4a8fdd7c1b548259f89f01b04e56a513e834463d7bac5632c7cf906","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"13283350547389802aa35d9f2188effaeac805499169a06ef5cd77ce2a0bd63f","impliedFormat":1},{"version":"ce791f6ea807560f08065d1af6014581eeb54a05abd73294777a281b6dfd73c2","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"49f95e989b4632c6c2a578cc0078ee19a5831832d79cc59abecf5160ea71abad","impliedFormat":1},{"version":"21b4672313ae95583ade84f97ae6bbeaf242ecae783f5653e2e99ac4e21cbbe1","impliedFormat":1},{"version":"ce0df82a9ae6f914ba08409d4d883983cc08e6d59eb2df02d8e4d68309e7848b","impliedFormat":1},{"version":"796273b2edc72e78a04e86d7c58ae94d370ab93a0ddf40b1aa85a37a1c29ecd7","impliedFormat":1},{"version":"5df15a69187d737d6d8d066e189ae4f97e41f4d53712a46b2710ff9f8563ec9f","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"e17cd049a1448de4944800399daa4a64c5db8657cc9be7ef46be66e2a2cd0e7c","impliedFormat":1},{"version":"d05fb434f4ba073aed74b6c62eff1723c835de2a963dbb091e000a2decb5a691","impliedFormat":1},{"version":"bff8c8bffbf5f302a30ccb1c0557dae477892d50a80eecfe393bd89bac7fb41d","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"4d4927cbee21750904af7acf940c5e3c491b4d5ebc676530211e389dd375607a","impliedFormat":1},{"version":"72105519d0390262cf0abe84cf41c926ade0ff475d35eb21307b2f94de985778","impliedFormat":1},{"version":"8a97e578a9bc40eb4f1b0ca78f476f2e9154ecbbfd5567ee72943bab37fc156a","impliedFormat":1},{"version":"a58abf1f5c8feb335475097abeddd32fd71c4dc2065a3d28cf15cacabad9654a","impliedFormat":1},{"version":"ccf6dd45b708fb74ba9ed0f2478d4eb9195c9dfef0ff83a6092fa3cf2ff53b4f","impliedFormat":1},{"version":"2d7db1d73456e8c5075387d4240c29a2a900847f9c1bff106a2e490da8fbd457","impliedFormat":1},{"version":"2b15c805f48e4e970f8ec0b1915f22d13ca6212375e8987663e2ef5f0205e832","impliedFormat":1},{"version":"f22d05663d873ee7a600faf78abb67f3f719d32266803440cf11d5db7ac0cab2","impliedFormat":1},{"version":"d93c544ad20197b3976b0716c6d5cd5994e71165985d31dcab6e1f77feb4b8f2","impliedFormat":1},{"version":"35069c2c417bd7443ae7c7cafd1de02f665bf015479fec998985ffbbf500628c","impliedFormat":1},{"version":"a8b1c79a833ee148251e88a2553d02ce1641d71d2921cce28e79678f3d8b96aa","impliedFormat":1},{"version":"126d4f950d2bba0bd45b3a86c76554d4126c16339e257e6d2fabf8b6bf1ce00c","impliedFormat":1},{"version":"9e21f8e2c0cfea713a4a372f284b60089c0841eb90bf3610539d89dbcd12d65a","impliedFormat":1},{"version":"045b752f44bf9bbdcaffd882424ab0e15cb8d11fa94e1448942e338c8ef19fba","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"7fa117f0f4f132ba132794982a35c840287997ee186753f78abe48508812c238","impliedFormat":1},{"version":"6ce54b2cfe4cf91138e2f5f114fe222a8819968336385cbcafd26ca89ebd4f50","impliedFormat":1},{"version":"b612fc66f534bd447bb1d5d52a29217a80780e1d57633875c9d8a333503f378a","impliedFormat":1},{"version":"0e8aef93d79b000deb6ec336b5645c87de167168e184e84521886f9ecc69a4b5","impliedFormat":1},{"version":"56ccb49443bfb72e5952f7012f0de1a8679f9f75fc93a5c1ac0bafb28725fc5f","impliedFormat":1},{"version":"d90b9f1520366d713a73bd30c5a9eb0040d0fb6076aff370796bc776fd705943","impliedFormat":1},{"version":"05321b823dd3781d0b6aac8700bfdc0c9181d56479fe52ba6a40c9196fd661a8","impliedFormat":1},{"version":"736a8712572e21ee73337055ce15edb08142fc0f59cd5410af4466d04beff0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"bef86adb77316505c6b471da1d9b8c9e428867c2566270e8894d4d773a1c4dc2","impliedFormat":1},{"version":"a7d72cf676f5117df919b8a73da2cfa20cf9939fdb263fd496fb77f95c35335d","impliedFormat":1},{"version":"a3e7d932dc9c09daa99141a8e4800fc6c58c625af0d4bbb017773dc36da75426","impliedFormat":1},{"version":"43e96a3d5d1411ab40ba2f61d6a3192e58177bcf3b133a80ad2a16591611726d","impliedFormat":1},{"version":"219e5e67ea4630410167444a715ecc172d9462b7910cd066eca18f6ed27d907b","impliedFormat":1},{"version":"ca921bf56756cb6fe957f6af693a35251b134fb932dc13f3dfff0bb7106f80b4","impliedFormat":1},{"version":"fee92c97f1aa59eb7098a0cc34ff4df7e6b11bae71526aca84359a2575f313d8","impliedFormat":1},{"version":"acfbb7b38e876b43cb07d0c8bd1a2e84dd641d9d2b67d772e8977337398bfff5","impliedFormat":1},{"version":"2ab6d334bcbf2aff3acfc4fd8c73ecd82b981d3c3aa47b3f3b89281772286904","impliedFormat":1},{"version":"d07cbc787a997d83f7bde3877fec5fb5b12ce8c1b7047eb792996ed9726b4dde","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"268c6788d4791a66cc5c153c41d2313d6f3c0d3e35edce3ce05e21c31f972ae0","impliedFormat":1},{"version":"b83cb14474fa60c5f3ec660146b97d122f0735627f80d82dd03e8caa39b4388c","impliedFormat":1},{"version":"f374cb24e93e7798c4d9e83ff872fa52d2cdb36306392b840a6ddf46cb925cb6","impliedFormat":1},{"version":"6ad71551fba5dbf440780090c82f5e0a7b64f602e0f0f678317659f12131f253","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"20865ac316b8893c1a0cc383ccfc1801443fbcc2a7255be166cf90d03fac88c9","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"461d0ad8ae5f2ff981778af912ba71b37a8426a33301daa00f21c6ccb27f8156","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"fcafff163ca5e66d3b87126e756e1b6dfa8c526aa9cd2a2b0a9da837d81bbd72","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"45490817629431853543adcb91c0673c25af52a456479588b6486daba34f68bb","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"8b4327413e5af38cd8cb97c59f48c3c866015d5d642f28518e3a891c469f240e","impliedFormat":1},{"version":"cd767eea328a0ed87d2e028147a022f209fadf420199254253a6cffe8e234df8","impliedFormat":1},{"version":"4b20fcf10a5413680e39f5666464859fc56b1003e7dfe2405ced82371ebd49b6","impliedFormat":1},{"version":"f0f3f57e29b40e9cb0c4b155a96de2f61e51700d2c335dd547ef3c85e668c6a8","impliedFormat":1},{"version":"f7d628893c9fa52ba3ab01bcb5e79191636c4331ee5667ecc6373cbccff8ae12","impliedFormat":1},{"version":"a169ba2d40cc94a500759aa86eded1f63395252bb7508a8b67dc681ff413ac8d","impliedFormat":1},{"version":"6a76daf108400ca1333e325772f24f40ebdde2120ef68f8c87d7a1adf0257541","impliedFormat":1},{"version":"7fa321c806b965bac02883573db0b1466e5edd14c479d156079eb08f1086f1d1","impliedFormat":1},{"version":"d663134457d8d669ae0df34eabd57028bddc04fc444c4bc04bc5215afc91e1f4","impliedFormat":1},{"version":"a52674bc98da7979607e0f44d4c015c59c1b1d264c83fc50ec79ff2cfea06723","impliedFormat":1},{"version":"89b3d1b267c4380fbb8e5cadccbb284843b90066f16a2f6e8a5b3a030bb7dcfb","impliedFormat":1},{"version":"8514c62ce38e58457d967e9e73f128eedc1378115f712b9eef7127f7c88f82ae","impliedFormat":1},{"version":"01698747a0d3c3ebf261865f9f912658aff9b726f7ebda11e19222725cfb0965","impliedFormat":1},{"version":"9b06ce81ad598c9c6b011cb66182fa66575ad6bd1f8f655830a6a0223a197ab7","impliedFormat":1},{"version":"d9d32f94056181c31f553b32ce41d0ef75004912e27450738d57efcd2409c324","impliedFormat":1},{"version":"752513f35f6cff294ffe02d6027c41373adf7bfa35e593dbfd53d95c203635ee","impliedFormat":1},{"version":"6c800b281b9e89e69165fd11536195488de3ff53004e55905e6c0059a2d8591e","impliedFormat":1},{"version":"7d4254b4c6c67a29d5e7f65e67d72540480ac2cfb041ca484847f5ae70480b62","impliedFormat":1},{"version":"1ee834bfd4a06aafdc46f5542d089565a26e031ebf854ef5b08cb75ec42d68fb","impliedFormat":1},{"version":"8c901126d73f09ecdea4785e9a187d1ac4e793e07da308009db04a7283ec2f37","impliedFormat":1},{"version":"db97922b767bd2675fdfa71e08b49c38b7d2c847a1cc4a7274cb77be23b026f1","impliedFormat":1},{"version":"e2f64b40fe8d3a77d5462dc4a75ead61c76bf464208b506c1465dac4e195f710","impliedFormat":1},{"version":"94fe3281392e1015b22f39535878610b4fa6f1388dc8d78746be3bc4e4bb8950","impliedFormat":1},{"version":"2652448ac55a2010a1f71dd141f828b682298d39728f9871e1cdf8696ef443fd","impliedFormat":1},{"version":"e3a9871a4a736910b0b77bc063d5f9c272578b3743269ebe93b275b0c52a9815","impliedFormat":1},{"version":"120599fd965257b1f4d0ff794bc696162832d9d8467224f4665f713a3119078b","impliedFormat":1},{"version":"5433f33b0a20300cca35d2f229a7fc20b0e8477c44be2affeb21cb464af60c76","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"bd4131091b773973ca5d2326c60b789ab1f5e02d8843b3587effe6e1ea7c9d86","impliedFormat":1},{"version":"c7f6485931085bf010fbaf46880a9b9ec1a285ad9dc8c695a9e936f5a48f34b4","impliedFormat":1},{"version":"73a39452c4b498728757c4a7f756a3b9bed1f8a02c278cb803665cc7897e6930","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"0427df5c06fafc5fe126d14b9becd24160a288deff40e838bfbd92a35f8d0d00","impliedFormat":1},{"version":"191a32cecf67da01119a7bce3132228fa9388e2bbfc5c1662542e71f9f20134a","impliedFormat":1},{"version":"49c346823ba6d4b12278c12c977fb3a31c06b9ca719015978cb145eb86da1c61","impliedFormat":1},{"version":"bfac6e50eaa7e73bb66b7e052c38fdc8ccfc8dbde2777648642af33cf349f7f1","impliedFormat":1},{"version":"92f7c1a4da7fbfd67a2228d1687d5c2e1faa0ba865a94d3550a3941d7527a45d","impliedFormat":1},{"version":"f53b120213a9289d9a26f5af90c4c686dd71d91487a0aa5451a38366c70dc64b","impliedFormat":1},{"version":"83fe880c090afe485a5c02262c0b7cdd76a299a50c48d9bde02be8e908fb4ae6","impliedFormat":1},{"version":"0a372c2d12a259da78e21b25974d2878502f14d89c6d16b97bd9c5017ab1bc12","impliedFormat":1},{"version":"57d67b72e06059adc5e9454de26bbfe567d412b962a501d263c75c2db430f40e","impliedFormat":1},{"version":"6511e4503cf74c469c60aafd6589e4d14d5eb0a25f9bf043dcbecdf65f261972","impliedFormat":1},{"version":"885e0c913a60577fa4827e5412055011a7532124fd9e054febb6808b0d7fec3d","impliedFormat":1},{"version":"6e2261cd9836b2c25eecb13940d92c024ebed7f8efe23c4b084145cd3a13b8a6","impliedFormat":1},{"version":"a67b87d0281c97dfc1197ef28dfe397fc2c865ccd41f7e32b53f647184cc7307","impliedFormat":1},{"version":"771ffb773f1ddd562492a6b9aaca648192ac3f056f0e1d997678ff97dbb6bf9b","impliedFormat":1},{"version":"232f70c0cf2b432f3a6e56a8dc3417103eb162292a9fd376d51a3a9ea5fbbf6f","impliedFormat":1},{"version":"d7ed1f4bd5589cb08f3af26839a0dc2472e4d1a3c380e167f0186b1f5e7c27d3","impliedFormat":1},{"version":"cfb5f0ab72180f4e0b9ed1534847a63d5394b9a8ee685ae149d25fd53f1aec66","impliedFormat":1},{"version":"8a0e762ceb20c7e72504feef83d709468a70af4abccb304f32d6b9bac1129b2c","impliedFormat":1},{"version":"26f83053ec70baea288b5281deb2cf11f6f9ea79bc654db1a6602b0b7ec085ff","impliedFormat":1},{"version":"9252d498a77517aab5d8d4b5eb9d71e4b225bbc7123df9713e08181de63180f6","impliedFormat":1},{"version":"c3b0db2267ff477aa00683219dd8738cd24a930da4df23fecb5910f27e7e49b3","impliedFormat":1},{"version":"35e6379c3f7cb27b111ad4c1aa69538fd8e788ab737b8ff7596a1b40e96f4f90","impliedFormat":1},{"version":"1fffe726740f9787f15b532e1dc870af3cd964dbe29e191e76121aa3dd8693f2","impliedFormat":1},{"version":"a3128a84a9568762a2996df79717d92154d18dd894681fc0ab3a098fa7f8ee3b","affectsGlobalScope":true,"impliedFormat":1},{"version":"4c12b845a35c0f753c1cf29d7d042d4da0206b1ba040a9bfff193a086bcdc248","impliedFormat":1},{"version":"2896c2e673a5d3bd9b4246811f79486a073cbb03950c3d252fba10003c57411a","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","impliedFormat":1},{"version":"51bf55bb6eb80f11b3aa59fb0a9571565a7ea304a19381f6da5630f4b2e206c4","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"98a787be42bd92f8c2a37d7df5f13e5992da0d967fab794adbb7ee18370f9849","impliedFormat":1},{"version":"5c96bad5f78466785cdad664c056e9e2802d5482ca5f862ed19ba34ffbb7b3a4","impliedFormat":1},{"version":"b7fff2d004c5879cae335db8f954eb1d61242d9f2d28515e67902032723caeab","impliedFormat":1},{"version":"5f3dc10ae646f375776b4e028d2bed039a93eebbba105694d8b910feebbe8b9c","impliedFormat":1},{"version":"bb0cd7862b72f5eba39909c9889d566e198fcaddf7207c16737d0c2246112678","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"320f4091e33548b554d2214ce5fc31c96631b513dffa806e2e3a60766c8c49d9","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"d90d5f524de38889d1e1dbc2aeef00060d779f8688c02766ddb9ca195e4a713d","impliedFormat":1},{"version":"a3f41ed1b4f2fc3049394b945a68ae4fdefd49fa1739c32f149d32c0545d67f5","impliedFormat":1},{"version":"bad68fd0401eb90fe7da408565c8aee9c7a7021c2577aec92fa1382e8876071a","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"fec01479923e169fb52bd4f668dbeef1d7a7ea6e6d491e15617b46f2cacfa37d","impliedFormat":1},{"version":"8a8fb3097ba52f0ae6530ec6ab34e43e316506eb1d9aa29420a4b1e92a81442d","impliedFormat":1},{"version":"44e09c831fefb6fe59b8e65ad8f68a7ecc0e708d152cfcbe7ba6d6080c31c61e","impliedFormat":1},{"version":"1c0a98de1323051010ce5b958ad47bc1c007f7921973123c999300e2b7b0ecc0","impliedFormat":1},{"version":"4655709c9cb3fd6db2b866cab7c418c40ed9533ce8ea4b66b5f17ec2feea46a9","impliedFormat":1},{"version":"87affad8e2243635d3a191fa72ef896842748d812e973b7510a55c6200b3c2a4","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"2c3a42dbc1d6ef817733691513b6421c8d1aa607afe3601904e3d31f1f72324a","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"330896c1a2b9693edd617be24fbf9e5895d6e18c7955d6c08f028f272b37314d","impliedFormat":1},{"version":"1d9c0a9a6df4e8f29dc84c25c5aa0bb1da5456ebede7a03e03df08bb8b27bae6","impliedFormat":1},{"version":"84380af21da938a567c65ef95aefb5354f676368ee1a1cbb4cae81604a4c7d17","impliedFormat":1},{"version":"1af3e1f2a5d1332e136f8b0b95c0e6c0a02aaabd5092b36b64f3042a03debf28","impliedFormat":1},{"version":"30d8da250766efa99490fc02801047c2c6d72dd0da1bba6581c7e80d1d8842a4","impliedFormat":1},{"version":"03566202f5553bd2d9de22dfab0c61aa163cabb64f0223c08431fb3fc8f70280","impliedFormat":1},{"version":"9a01f12466488eccd8d9eafc8fecb9926c175a4bf4a8f73a07c3bcf8b3363282","impliedFormat":1},{"version":"b80f624162276f24a4ec78b8e86fbee80ca255938e12f8b58e7a8f1a6937120b","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"5bf5c7a44e779790d1eb54c234b668b15e34affa95e78eada73e5757f61ed76a","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"5c634644d45a1b6bc7b05e71e05e52ec04f3d73d9ac85d5927f647a5f965181a","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"a68d4b3182e8d776cdede7ac9630c209a7bfbb59191f99a52479151816ef9f9e","impliedFormat":99},{"version":"39644b343e4e3d748344af8182111e3bbc594930fff0170256567e13bbdbebb0","impliedFormat":99},{"version":"ed7fd5160b47b0de3b1571c5c5578e8e7e3314e33ae0b8ea85a895774ee64749","impliedFormat":99},{"version":"63a7595a5015e65262557f883463f934904959da563b4f788306f699411e9bac","impliedFormat":1},{"version":"9e40365afca304124bc53eb03412643abf074a1580e4dc279a7a16000d11f985","impliedFormat":1},{"version":"4ba137d6553965703b6b55fd2000b4e07ba365f8caeb0359162ad7247f9707a6","impliedFormat":1},{"version":"ceec3c81b2d81f5e3b855d9367c1d4c664ab5046dff8fd56552df015b7ccbe8f","affectsGlobalScope":true,"impliedFormat":1},{"version":"8fac4a15690b27612d8474fb2fc7cc00388df52d169791b78d1a3645d60b4c8b","affectsGlobalScope":true,"impliedFormat":1},{"version":"064ac1c2ac4b2867c2ceaa74bbdce0cb6a4c16e7c31a6497097159c18f74aa7c","impliedFormat":1},{"version":"3dc14e1ab45e497e5d5e4295271d54ff689aeae00b4277979fdd10fa563540ae","impliedFormat":1},{"version":"1d63055b690a582006435ddd3aa9c03aac16a696fac77ce2ed808f3e5a06efab","impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","impliedFormat":1},"f2b3bca04d1bfe583daae1e1f798c92ec24bb6693bd88d0a09ba6802dee362a8",{"version":"93cddca2ec6a5a8a1b942af0459cbd78d1b1b48e576cdabecc82811843a2cc28","impliedFormat":1},{"version":"29c2e5ddccfb04d5383aed2cc1e538e2984f0c198004846d284be45f4e9a7ba7","signature":"9c0eeb8de8dfdfb63ebcef09564c84ae1f6b6187c8e8e274fc5b0ee0a2c8432b"},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"8885cf05f3e2abf117590bbb951dcf6359e3e5ac462af1c901cfd24c6a6472e2","impliedFormat":1},{"version":"333caa2bfff7f06017f114de738050dd99a765c7eb16571c6d25a38c0d5365dc","impliedFormat":1},{"version":"e61df3640a38d535fd4bc9f4a53aef17c296b58dc4b6394fd576b808dd2fe5e6","impliedFormat":1},{"version":"459920181700cec8cbdf2a5faca127f3f17fd8dd9d9e577ed3f5f3af5d12a2e4","impliedFormat":1},{"version":"4719c209b9c00b579553859407a7e5dcfaa1c472994bd62aa5dd3cc0757eb077","impliedFormat":1},{"version":"7ec359bbc29b69d4063fe7dad0baaf35f1856f914db16b3f4f6e3e1bca4099fa","impliedFormat":1},{"version":"70790a7f0040993ca66ab8a07a059a0f8256e7bb57d968ae945f696cbff4ac7a","impliedFormat":1},{"version":"d1b9a81e99a0050ca7f2d98d7eedc6cda768f0eb9fa90b602e7107433e64c04c","impliedFormat":1},{"version":"a022503e75d6953d0e82c2c564508a5c7f8556fad5d7f971372d2d40479e4034","impliedFormat":1},{"version":"b215c4f0096f108020f666ffcc1f072c81e9f2f95464e894a5d5f34c5ea2a8b1","impliedFormat":1},{"version":"644491cde678bd462bb922c1d0cfab8f17d626b195ccb7f008612dc31f445d2d","impliedFormat":1},{"version":"dfe54dab1fa4961a6bcfba68c4ca955f8b5bbeb5f2ab3c915aa7adaa2eabc03a","impliedFormat":1},{"version":"1251d53755b03cde02466064260bb88fd83c30006a46395b7d9167340bc59b73","impliedFormat":1},{"version":"47865c5e695a382a916b1eedda1b6523145426e48a2eae4647e96b3b5e52024f","impliedFormat":1},{"version":"4cdf27e29feae6c7826cdd5c91751cc35559125e8304f9e7aed8faef97dcf572","impliedFormat":1},{"version":"331b8f71bfae1df25d564f5ea9ee65a0d847c4a94baa45925b6f38c55c7039bf","impliedFormat":1},{"version":"2a771d907aebf9391ac1f50e4ad37952943515eeea0dcc7e78aa08f508294668","impliedFormat":1},{"version":"0146fd6262c3fd3da51cb0254bb6b9a4e42931eb2f56329edd4c199cb9aaf804","impliedFormat":1},{"version":"183f480885db5caa5a8acb833c2be04f98056bdcc5fb29e969ff86e07efe57ab","impliedFormat":99},{"version":"b558c9a18ea4e6e4157124465c3ef1063e64640da139e67be5edb22f534f2f08","impliedFormat":1},{"version":"01374379f82be05d25c08d2f30779fa4a4c41895a18b93b33f14aeef51768692","impliedFormat":1},{"version":"b0dee183d4e65cf938242efaf3d833c6b645afb35039d058496965014f158141","impliedFormat":1},{"version":"c0bbbf84d3fbd85dd60d040c81e8964cc00e38124a52e9c5dcdedf45fea3f213","impliedFormat":1},{"version":"5f3b5c6dd5f53d5066790eef8a99d2eda4486fba31d533a414fe72634425281c","signature":"d09eedef89a79193ae63e9d0769313e534a12c570154280abbc56d79e367c833"},{"version":"e5b87874d208aecc6f6d9b937641833e6ac20ea8168d3a6e97652e15f40a816a","impliedFormat":99},{"version":"be004064f4c3328ef6aa254d01b7a97d507698a6e421a973d32220481727a3e0","impliedFormat":99},{"version":"73e5555643a4bf2820d108777bf94d196297ccb2ddef743f99bd379c31cb61fc","impliedFormat":99},{"version":"68fc7da7956f13295692186c7de9dff30aadc5860ce8fffbb1be39c5fd4624fa","impliedFormat":99},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"293eadad9dead44c6fd1db6de552663c33f215c55a1bfa2802a1bceed88ff0ec","impliedFormat":1},{"version":"833e92c058d033cde3f29a6c7603f517001d1ddd8020bc94d2067a3bc69b2a8e","impliedFormat":1},{"version":"08b2fae7b0f553ad9f79faec864b179fc58bc172e295a70943e8585dd85f600c","impliedFormat":1},{"version":"f12edf1672a94c578eca32216839604f1e1c16b40a1896198deabf99c882b340","impliedFormat":1},{"version":"e3498cf5e428e6c6b9e97bd88736f26d6cf147dedbfa5a8ad3ed8e05e059af8a","impliedFormat":1},{"version":"dba3f34531fd9b1b6e072928b6f885aa4d28dd6789cbd0e93563d43f4b62da53","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"e4b03ddcf8563b1c0aee782a185286ed85a255ce8a30df8453aade2188bbc904","impliedFormat":1},{"version":"2329d90062487e1eaca87b5e06abcbbeeecf80a82f65f949fd332cfcf824b87b","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"4fdb529707247a1a917a4626bfb6a293d52cd8ee57ccf03830ec91d39d606d6d","impliedFormat":1},{"version":"a9ebb67d6bbead6044b43714b50dcb77b8f7541ffe803046fdec1714c1eba206","impliedFormat":1},{"version":"5780b706cece027f0d4444fbb4e1af62dc51e19da7c3d3719f67b22b033859b9","impliedFormat":1},{"version":"73e5555643a4bf2820d108777bf94d196297ccb2ddef743f99bd379c31cb61fc","impliedFormat":99},{"version":"68fc7da7956f13295692186c7de9dff30aadc5860ce8fffbb1be39c5fd4624fa","impliedFormat":99},{"version":"309ebd217636d68cf8784cbc3272c16fb94fb8e969e18b6fe88c35200340aef1","impliedFormat":1},{"version":"91cf9887208be8641244827c18e620166edf7e1c53114930b54eaeaab588a5be","impliedFormat":1},{"version":"ef9b6279acc69002a779d0172916ef22e8be5de2d2469ff2f4bb019a21e89de2","impliedFormat":1},{"version":"9c20e0cce5b7f0a0a46c3a7717d3a9485456769ebfdfd6afa7b5babbcea6f86e","impliedFormat":1},{"version":"88863d76039cc550f8b7688a213dd051ae80d94a883eb99389d6bc4ce21c8688","impliedFormat":1},{"version":"b2732d3b39b116431fb2048679ba11805110bc94c7763d50bfbe98f2a1923882","impliedFormat":1},{"version":"243649afb10d950e7e83ee4d53bd2fbd615bb579a74cf6c1ce10e64402cdf9bb","impliedFormat":1},{"version":"35575179030368798cbcd50da928a275234445c9a0df32d4a2c694b2b3d20439","impliedFormat":1},{"version":"0cf1e5928aae1cca4dcd1a78e19a5833018b84829a116f8fbfab29dc63446a49","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"26384fb401f582cae1234213c3dc75fdc80e3d728a0a1c55b405be8a0c6dddbe","impliedFormat":1},{"version":"26384fb401f582cae1234213c3dc75fdc80e3d728a0a1c55b405be8a0c6dddbe","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"26384fb401f582cae1234213c3dc75fdc80e3d728a0a1c55b405be8a0c6dddbe","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"03268b4d02371bdf514f513797ed3c9eb0840b0724ff6778bda0ef74c35273be","impliedFormat":1},{"version":"3511847babb822e10715a18348d1cbb0dae73c4e4c0a1bcf7cbc12771b310d45","impliedFormat":1},{"version":"add0ce7b77ba5b308492fa68f77f24d1ed1d9148534bdf05ac17c30763fc1a79","impliedFormat":1},{"version":"53f00dc83ccceb8fad22eb3aade64e4bcdb082115f230c8ba3d40f79c835c30e","impliedFormat":1},{"version":"7d94df9d46f72799f956caf7409ae56bd7e754363b0ab6ea7bd865759b679011","impliedFormat":1},{"version":"9078205849121a5d37a642949d687565498da922508eacb0e5a0c3de427f0ae5","impliedFormat":1},{"version":"e8f8f095f137e96dc64b56e59556c02f3c31db4b354801d6ae3b90dceae60240","impliedFormat":1},{"version":"451abef2a26cebb6f54236e68de3c33691e3b47b548fd4c8fa05fd84ab2238ff","impliedFormat":1},{"version":"6042774c61ece4ba77b3bf375f15942eb054675b7957882a00c22c0e4fe5865c","impliedFormat":1},{"version":"41f185713d78f7af0253a339927dc04b485f46210d6bc0691cf908e3e8ded2a1","impliedFormat":1},{"version":"7390fbf07c0f9a8148db2824ce1a98dc59bb0156071bd7cb109dac908c120fab","impliedFormat":1},{"version":"ffc3e1064146c1cafda1b0686ae9679ba1fb706b2f415e057be01614bf918dba","impliedFormat":1},{"version":"e27451b24234dfed45f6cf22112a04955183a99c42a2691fb4936d63cfe42761","impliedFormat":1},{"version":"58d65a2803c3b6629b0e18c8bf1bc883a686fcf0333230dd0151ab6e85b74307","impliedFormat":1},{"version":"e818471014c77c103330aee11f00a7a00b37b35500b53ea6f337aefacd6174c9","impliedFormat":1},{"version":"dca963a986285211cfa75b9bb57914538de29585d34217d03b538e6473ac4c44","impliedFormat":1},{"version":"29f823cbe0166e10e7176a94afe609a24b9e5af3858628c541ff8ce1727023cd","impliedFormat":1},"367a12c2e2db88c68626f79ab918cc5fcd814890844b3b6035e6919a9db5210c","290b8dc1951c8c779876524fd99c3273ea47346f65124ac8000efee2838663e1",{"version":"4909bb692b565ddfe777895b8aa5c7824718d39c3f3640ca9b0033e889863d13","signature":"e51ec281349aff51e89cf0b4c3bd44e425f8485d0465a494fcfa79d4c3d5de37"},{"version":"739d0b60c5223835b7ce48ea2fff64ab7e43f0bbcaa55eac0795281422e554b3","signature":"21b832c580e166a405f1ce5d8777db886486ced53a6f6edf0512270f5974618d"},{"version":"7242cd4b39c34b38962ec12a31461e2b6077008567bd574db727d40c98388432","signature":"d2a49c8880f8a4de258564b7bf4ee2eb16c5961386f5532b788437479be39244"},{"version":"f34db05e2ff5083eedbac8cebca5cd53d3fd5c3a60acec41028fd3d633b69860","signature":"87b79fd569bcdf71c08547d45eceadfd769d52e070d3d55e6252790c232d47a6"},{"version":"cef53d4239fe17f3e7c827c4281d343e8efcbc718cefea253e67cb420d3eac9f","signature":"5f5303388c23daf221df682ba0e6ed13924ad3a925fae5ec71f4cdfb6adf1b79"},{"version":"236d66d49bc5f9dda1de6c1a30e7c1d81497e4afcfc61e90a2a491321a9aedfc","signature":"39104c0ab7ff0e8994a3da419b2ea6d46a7a9f67529e2d643bafad2d68f3f6ac"},{"version":"89121c1bf2990f5219bfd802a3e7fc557de447c62058d6af68d6b6348d64499a","impliedFormat":1},{"version":"5c5d901a999dfe64746ef4244618ae0628ac8afdb07975e3d5ed66e33c767ed0","impliedFormat":99},{"version":"85d08536e6cd9787f82261674e7d566421a84d286679db1503432a6ccf9e9625","impliedFormat":99},{"version":"5702b3c2f5d248290ed99419d77ca1cc3e6c29db5847172377659c50e6303768","impliedFormat":99},{"version":"9764b2eb5b4fc0b8951468fb3dbd6cd922d7752343ef5fbf1a7cd3dfcd54a75e","impliedFormat":99},{"version":"1fc2d3fe8f31c52c802c4dee6c0157c5a1d1f6be44ece83c49174e316cf931ad","impliedFormat":99},{"version":"dc4aae103a0c812121d9db1f7a5ea98231801ed405bf577d1c9c46a893177e36","impliedFormat":99},{"version":"106d3f40907ba68d2ad8ce143a68358bad476e1cc4a5c710c11c7dbaac878308","impliedFormat":99},{"version":"42ad582d92b058b88570d5be95393cf0a6c09a29ba9aa44609465b41d39d2534","impliedFormat":99},{"version":"36e051a1e0d2f2a808dbb164d846be09b5d98e8b782b37922a3b75f57ee66698","impliedFormat":99},{"version":"79b4369233a12c6fa4a07301ecb7085802c98f3a77cf9ab97eee27e1656f82e6","impliedFormat":1},{"version":"19990350fca066265b2c190c9b6cde1229f35002ea2d4df8c9e397e9942f6c89","impliedFormat":99},{"version":"8fb8fdda477cd7382477ffda92c2bb7d9f7ef583b1aa531eb6b2dc2f0a206c10","impliedFormat":99},{"version":"66995b0c991b5c5d42eff1d950733f85482c7419f7296ab8952e03718169e379","impliedFormat":99},{"version":"9863f888da357e35e013ca3465b794a490a198226bd8232c2f81fb44e16ff323","impliedFormat":99},{"version":"84bc2d80326a83ee4a6e7cba2fd480b86502660770c0e24da96535af597c9f1e","impliedFormat":99},{"version":"ea27768379b866ee3f5da2419650acdb01125479f7af73580a4bceb25b79e372","impliedFormat":99},{"version":"598931eeb4362542cae5845f95c5f0e45ac668925a40ce201e244d7fe808e965","impliedFormat":99},{"version":"da9ef88cde9f715756da642ad80c4cd87a987f465d325462d6bc2a0b11d202c8","impliedFormat":99},{"version":"b4c6184d78303b0816e779a48bef779b15aea4a66028eb819aac0abee8407dea","impliedFormat":99},{"version":"db085d2171d48938a99e851dafe0e486dce9859e5dfa73c21de5ed3d4d6fb0c5","impliedFormat":99},{"version":"62a3ad1ddd1f5974b3bf105680b3e09420f2230711d6520a521fab2be1a32838","impliedFormat":99},{"version":"a77be6fc44c876bc10c897107f84eaba10790913ebdcad40fcda7e47469b2160","impliedFormat":99},{"version":"06cf55b6da5cef54eaaf51cdc3d4e5ebf16adfdd9ebd20cec7fe719be9ced017","impliedFormat":99},{"version":"91f5dbcdb25d145a56cffe957ec665256827892d779ef108eb2f3864faff523b","impliedFormat":99},{"version":"052ba354bab8fb943e0bc05a0769f7b81d7c3b3c6cd0f5cfa53c7b2da2a525c5","impliedFormat":99},{"version":"927955a3de5857e0a1c575ced5a4245e74e6821d720ed213141347dd1870197f","impliedFormat":99},{"version":"fec804d54cd97dd77e956232fc37dc13f53e160d4bbeeb5489e86eeaa91f7ebd","impliedFormat":99},{"version":"2ccdfd33a753c18e8e5fe8a1eadefff968531d920bc9cdc7e4c97b0c6d3dcaf8","impliedFormat":99},{"version":"d64a434d7fb5040dbe7d5f4911145deda53e281b3f1887b9a610defd51b3c1a2","impliedFormat":99},{"version":"927f406568919fd7cd238ef7fe5e9c5e9ec826f1fff89830e480aff8cfd197da","impliedFormat":99},{"version":"a77d742410fe78bb054d325b690fda75459531db005b62ba0e9371c00163353c","impliedFormat":99},{"version":"f8de61dd3e3c4dc193bb341891d67d3979cb5523a57fcacaf46bf1e6284e6c35","impliedFormat":99},{"version":"43866c6355cd21476b3a5cf0dc422e8a56f7acf5010acf7d9c7ea296b3beed44","impliedFormat":99},{"version":"70a8c15739dae81bdd251aecfa667521beac1ca3d62d5510ef1d168367976efd","impliedFormat":99},{"version":"117fc7342e10087d11eea826713624c5ae6b2d886e4a4a592b1cb6a30e3a1eca","impliedFormat":99},{"version":"c9bb4f55c9b9f1942bd206cce9722107b1653236a2c3805a3ffc1b78e60129a7","impliedFormat":99},{"version":"fc610233291d35d0d2c3a0214f4a46d20ad989c74431b9b5d535141065890e41","impliedFormat":99},{"version":"09b7befce01ae33053c59bcdf1c66502af074023fae5a2729b8a0411e1b94788","impliedFormat":99},{"version":"add39e62eed912ab8defd0c6473b6a4d158e22b6343f960a96825924ac87621c","impliedFormat":99},{"version":"64cd32a60b0316be7adc86fad333ec9befbab241486ecf9b0e867b80acc7e9f1","impliedFormat":99},{"version":"a65735a086ae8b401c1c41b51b41546532670c919fd2cedc1606fd186fcee2d7","impliedFormat":99},{"version":"fe021dbde66bd0d6195d4116dcb4c257966ebc8cfba0f34441839415e9e913e1","impliedFormat":99},{"version":"d52a4b1cabee2c94ed18c741c480a45dd9fed32477dd94a9cc8630a8bc263426","impliedFormat":99},{"version":"d059a52684789e6ef30f8052244cb7c52fb786e4066ac415c50642174cc76d14","impliedFormat":99},{"version":"addca1bb7478ebc3f1c67b710755acc945329875207a3c9befd6b5cbcab12574","impliedFormat":99},{"version":"50b565f4771b6b150cbf3ae31eb815c31f15e2e0f45518958a5f4348a1a01660","impliedFormat":99},{"version":"74958763e089797b627dae00e31679752826f7e91c0818e971be919dacb21c1b","impliedFormat":99},{"version":"2cab237ff67a4f114999005809e65fd586f7fdb56e253e1208cabea827df7e42","impliedFormat":99},{"version":"4e3ab6678655e507463a9bfa1aa39a4a5497fac4c75e5f7f7a16c0b7d001c34a","impliedFormat":99},{"version":"49766a3e83c44708ea1357fa410c309837f4420826a5e265e3fb81e0087c7025","impliedFormat":99},{"version":"9f9fba2db9bb11058b153cdede4ec4e3ceed37d51a18a2edfaf7dfbef0a2b241","impliedFormat":99},{"version":"7a8db4befe8f75591173ae1038856ef6ce3ecfee9e22f1b616779018a8779471","impliedFormat":99},{"version":"006ffd4a92ea7050298f50e44dcc03155b943454bb874c0a5a3ad7c8ae92a50b","impliedFormat":99},{"version":"e981c553d5f18821019546df17a5ade9e26316417279a4e478f28d72cfe6c6f8","impliedFormat":99},{"version":"4b29df4bba7b409b5fa9a8db9f4b672eed5ef80664ff09de5d11724aebc5a30b","impliedFormat":99},{"version":"f5282507ffe12947a832a8d9be748df429b3ef61454df95750fb72ff24108a68","impliedFormat":99},{"version":"22872735dfe76e24d30e89624385361d3d077c8d940257aea9b48d57839c906f","impliedFormat":99},{"version":"bd93deb5a61f121242eb17014a20254b517e411b7144769b2389cc56bb254c40","impliedFormat":99},{"version":"d9de4ffb64c6b08aee469c79e96095f803302e55da3848a4d9197cd63904577d","signature":"841b05b9d63f4a65434027c12ccdd2e277ee6ea9ed4f23382e55afeb98e60439"},{"version":"bd90a34ef05a9ae919456c00208cbecec85ed4b4fd03ad88d55c659627c3b07b","signature":"a6861a72bed1cae361b69956a9d680a181b9774d468210f4868ba9a4bd07610d"},{"version":"309ebd217636d68cf8784cbc3272c16fb94fb8e969e18b6fe88c35200340aef1","impliedFormat":1},{"version":"91cf9887208be8641244827c18e620166edf7e1c53114930b54eaeaab588a5be","impliedFormat":1},{"version":"ef9b6279acc69002a779d0172916ef22e8be5de2d2469ff2f4bb019a21e89de2","impliedFormat":1},{"version":"71623b889c23a332292c85f9bf41469c3f2efa47f81f12c73e14edbcffa270d3","affectsGlobalScope":true,"impliedFormat":1},{"version":"88863d76039cc550f8b7688a213dd051ae80d94a883eb99389d6bc4ce21c8688","impliedFormat":1},{"version":"e9ce511dae7201b833936d13618dff01815a9db2e6c2cc28646e21520c452d6c","impliedFormat":1},{"version":"243649afb10d950e7e83ee4d53bd2fbd615bb579a74cf6c1ce10e64402cdf9bb","impliedFormat":1},{"version":"35575179030368798cbcd50da928a275234445c9a0df32d4a2c694b2b3d20439","impliedFormat":1},{"version":"c939cb12cb000b4ec9c3eca3fe7dee1fe373ccb801237631d9252bad10206d61","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"26384fb401f582cae1234213c3dc75fdc80e3d728a0a1c55b405be8a0c6dddbe","impliedFormat":1},{"version":"26384fb401f582cae1234213c3dc75fdc80e3d728a0a1c55b405be8a0c6dddbe","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"26384fb401f582cae1234213c3dc75fdc80e3d728a0a1c55b405be8a0c6dddbe","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"b42d3651103a532f7492e719a828647af97306b2356ae757ebb7f17f4a8c41e5","impliedFormat":1},{"version":"03268b4d02371bdf514f513797ed3c9eb0840b0724ff6778bda0ef74c35273be","impliedFormat":1},{"version":"3511847babb822e10715a18348d1cbb0dae73c4e4c0a1bcf7cbc12771b310d45","impliedFormat":1},{"version":"80e653fbbec818eecfe95d182dc65a1d107b343d970159a71922ac4491caa0af","impliedFormat":1},{"version":"53f00dc83ccceb8fad22eb3aade64e4bcdb082115f230c8ba3d40f79c835c30e","impliedFormat":1},{"version":"35475931e8b55c4d33bfe3abc79f5673924a0bd4224c7c6108a4e08f3521643c","impliedFormat":1},{"version":"9078205849121a5d37a642949d687565498da922508eacb0e5a0c3de427f0ae5","impliedFormat":1},{"version":"e8f8f095f137e96dc64b56e59556c02f3c31db4b354801d6ae3b90dceae60240","impliedFormat":1},{"version":"451abef2a26cebb6f54236e68de3c33691e3b47b548fd4c8fa05fd84ab2238ff","impliedFormat":1},{"version":"6042774c61ece4ba77b3bf375f15942eb054675b7957882a00c22c0e4fe5865c","impliedFormat":1},{"version":"41f185713d78f7af0253a339927dc04b485f46210d6bc0691cf908e3e8ded2a1","impliedFormat":1},{"version":"23ee410c645f68bd99717527de1586e3eb826f166d654b74250ad92b27311fde","impliedFormat":1},{"version":"ffc3e1064146c1cafda1b0686ae9679ba1fb706b2f415e057be01614bf918dba","impliedFormat":1},{"version":"995869b1ddf66bbcfdb417f7446f610198dcce3280a0ae5c8b332ed985c01855","impliedFormat":1},{"version":"58d65a2803c3b6629b0e18c8bf1bc883a686fcf0333230dd0151ab6e85b74307","impliedFormat":1},{"version":"e818471014c77c103330aee11f00a7a00b37b35500b53ea6f337aefacd6174c9","impliedFormat":1},{"version":"dca963a986285211cfa75b9bb57914538de29585d34217d03b538e6473ac4c44","impliedFormat":1},{"version":"d8bc0c5487582c6d887c32c92d8b4ffb23310146fcb1d82adf4b15c77f57c4ac","impliedFormat":1},{"version":"8cb31102790372bebfd78dd56d6752913b0f3e2cefbeb08375acd9f5ba737155","impliedFormat":1},{"version":"df09e59ace0cf7fd8e3c767b0b8f3d5b2212bd40d4e9dbf49a388526ead5e545","impliedFormat":99},{"version":"c5acf9061cb86da7716d98e12d6e96e2e356641eb0a21b33165653fb2cd6680f","impliedFormat":99},{"version":"ebd02963d7c47cf26f254068e7ad81858433e51e0e5c4ffd7b3b2f6fd0bce17a","impliedFormat":99},{"version":"3a648a8b64b69923c0930df4fa3b390dfa9d61ac0d17cfca55a29d6703db1b42","impliedFormat":99},{"version":"55bb540169182762bc332474d3547675dc00627e00a491b80b01dbc6c9e018fa","impliedFormat":99},{"version":"0f11987bd734a55e04f7ee8376a8f5be9374d887b67a670d076c6a5cc7211226","impliedFormat":99},{"version":"45a02ead1994cac3ac844522b01d603c5c36289259488b794e616f1655ecb7db","impliedFormat":99},{"version":"4dc4c3eca0a15be5bafa5ac220d839188097dfcfb44951221459b9b11e733352","impliedFormat":99},{"version":"aa0af7166f48f67765f96dc70c1d7f9f55ae264b96cadf5b6077b2bc0aa2b5dd","impliedFormat":99},{"version":"2fc9c7c6695b151ffd3ed667d6d793c2f656461978e840eff1d1350fc0bb1ebb","impliedFormat":99},{"version":"4d590f0e0b4abaf693f94d08b5c414928f2571aea5ac6efb97e4646e195dac48","impliedFormat":99},{"version":"bf1655c135bd654637f98f934f9a9eb4d6450194ca2f4968b79263608da59fdd","impliedFormat":99},{"version":"1ebe079cc9ed9ec4cd11d02c70f209caf16e9dd8e1e801a36648ce711bb3c404","impliedFormat":99},{"version":"e96acf03dd1fb9e4088e345cb052aed0f6c4779bbcdf1a4e84a2419cfad9f040","impliedFormat":99},{"version":"db367fd2faba92ed81ca1cb947d94d7bf104dc55caf18c44d2a2b6ac1b1dfafd","impliedFormat":99},{"version":"c18b9de619509cb2e83fb6db359d017de6cb5e9fe2838aed5361623ea44ef56a","impliedFormat":99},{"version":"e0ad85268102b4d552b53de0f93f8d27dc52cebe2ee6ca3f3f4cb88131c6a3a3","impliedFormat":99},{"version":"f6f03c94d64776248cad31d4503b9a5ee102bb1ce99b830a5a74c908927d2459","impliedFormat":99},{"version":"9ba212cc8d5f5e0bbbcdc8b31c1969dcace0d4bb0dc1dbbe14a288617d68a6db","impliedFormat":99},{"version":"d4b914632888f47bee35d94706dce53e9c35481d38a560180779469f4ee9159e","impliedFormat":99},{"version":"c19d8eb43817185ce1210471e1b59269112f6c25fc63fb455fba7b6c74a25bfe","impliedFormat":99},{"version":"647bead3b77e0fc7f2e2bed7a305d8beed67748dc4bc20f0ca174b7b7ecb099e","impliedFormat":99},{"version":"3bf193f73208a3e1c1317565d15b047303a33e3a39c54edb6e78a4d69827d97c","impliedFormat":99},{"version":"52d332b914c6b216f01562bcba195317680c4dfa3e0b6c645f473ecd6a29fc57","impliedFormat":99},{"version":"1d07950c5ceb2865d3d384a76f0c14bdca38c01c87bc1f3ee4df411a0c65a346","impliedFormat":99},{"version":"05301dc91249ca23b960eaf3e5efcd7aa99d493807cc18ddd955a4d0fe113f5c","impliedFormat":99},{"version":"fa473ebc4a55939b20e229501fd9d3aac5f578e4779f0f8f6a6306c848e1632a","impliedFormat":99},{"version":"e7a6ee2d07d956992ee90bf2d4055ca3a15342ba05cc5b7e2e7fd15f69cbfe61","impliedFormat":99},{"version":"487b0dbdebde79164f7b2ea782788737a4252b9040781db6c3a9722e2bb9ecc8","impliedFormat":99},{"version":"b71bbca9b845474bcd410aa47ef73dc14f55384e614e1558d588809f3413374e","impliedFormat":99},{"version":"f69309172758f286bd1d5dd70953ef4ac546fd733a31ad26eec05a456677737e","impliedFormat":99},{"version":"2b75d65afd6f248c992ed04d466a2e47825549c4738bdffb409e5763f5fc7826","impliedFormat":99},{"version":"b67227c32b487f6d4f76b6cfecfef75034390d2b14aed5ee33d1f01b2ac584df","impliedFormat":99},{"version":"663eb800efde225856c1e789ba85b6ec6603e12028473670221333c2c7f3bbb8","impliedFormat":99},{"version":"3936a5aaeb9d200a9b00225d230881437d29002a9b6e9719b4f782a44e215150","impliedFormat":99},{"version":"3fc35b978a159e75f36c8b9f5ae51c95de011eac0a994befd85a03972e06906f","impliedFormat":99},{"version":"0d75677f2e01e829154f73b93af966b3437b2d9565d10fc4eb03175bdb988cb7","impliedFormat":99},{"version":"4c516c6471d8203af3120cee24f3c2c0fb379958d428c5e5bb6ab8228052f683","impliedFormat":99},{"version":"d6513ddef6323a64583ee62ed1a8c9f2dd0ddb755772702181d0855c521e41ac","impliedFormat":99},{"version":"70efc2aa2b0bad5614d70c4697e7c4efb954e868d92c4d750b009c75758ecc07","impliedFormat":99},{"version":"2f8b2550af2d98da27a168baac999bb025cc3e916711b34b03bde2cce68e9be9","impliedFormat":99},{"version":"4cbf4d996793d757ff712ae7bd96b1227a09fb95fac447090d9cce63e0eb9460","impliedFormat":99},{"version":"8cbe9368fca284e894250d336b795a83c64397b574c249d25efe40ba657db8b8","impliedFormat":99},{"version":"f6face0c6f608d87be446227996f9da6b89b1d226ac2cdbcf0454714c69e5287","impliedFormat":99},{"version":"cbaa48aef231497ab562060d3742707984c43a9d0e2ee28da7abb2efe4a0b392","impliedFormat":99},{"version":"e1951d09be373ebc5370c0eff4af4a86e841251df119e6727e97e7ca714fc6ff","impliedFormat":99},{"version":"de2c2da9e6d8390e0f60cbe4b94dc4e1ea6f613e38418408da8de133958662c4","impliedFormat":99},{"version":"285c03dafff17a2767cd0a23f93912dc5e0f3ff7ac3c9da4a80cdfee9979452c","impliedFormat":99},{"version":"9c70dde5822201db2c3f208eb8d95f463caa103d211b49399569dfcd0f394a92","impliedFormat":99},{"version":"fcbc330594ee211b8e7eb56f4ec59175ab239288ecc7749634e665dee33ca181","impliedFormat":99},{"version":"5743905ac2de3204bcd9768fdeaec993fed8291bde54094ddabfa7f28573936d","impliedFormat":99},{"version":"643700414df81efee3059191cc2759c29623ff95f462190a0e4a6afe2c1640eb","impliedFormat":99},{"version":"707669372976b9a569b6ac40c5aafd61b6f9d03c12f60c06cfad234c73d18369","impliedFormat":99},{"version":"20640c93feb6d5f926e147456f6d19bcf3648d52d17ed1d62bd11cdee59761ca","impliedFormat":99},{"version":"ea88eb7247f90f0de73f3617a700625fc1b8c037ff03f4665534b978f3c3fd01","impliedFormat":99},{"version":"d6cb4d8b3499d80fb3d17e1911c6290928ef5a4d1a7751bca143bbef441012d9","impliedFormat":99},{"version":"b2ec10940611f3311aa42fce3bb65d3476b4eb48a00e9a93d1f85b6989c79500","impliedFormat":99},{"version":"b345d1cb103363741f885729eb562931b5bffb63d06acd6cf634212ea945cb9e","impliedFormat":99},{"version":"fd1a6d390ef510226ddf46350854d278a53738921cbb9e4de78bf7b6105df48d","impliedFormat":99},{"version":"ebddf120f55aa3a40cc08b374dd9077d1e497730c41ac124e66de3341f1dd83e","impliedFormat":99},{"version":"53c89482e50d4edcb80e217cf20d9126c6a595bc204ee834131d372895160018","impliedFormat":99},{"version":"7322a3401773f0c9fa87c7ef2ee13e0c660a5a926507ae8aca263bb3f4b2334e","impliedFormat":99},{"version":"deab327003debcefe7668fa28d2373b5a3c40b258f7948496b57ced275bb3eb3","impliedFormat":99},{"version":"fca8f9bf4b3544e8f293725684ae0a982e234504ce08b5dd4a477e06c3c792c5","impliedFormat":99},{"version":"5d17ad04870e5304037f31da3cc752da331e2b70ce333fb3c14a8884709a95b3","impliedFormat":99},{"version":"c65d7fae88667583386f30789ef1a77041df5a210f73338c34125a1bd4d98f7e","impliedFormat":99},{"version":"c7497efbdffb6c2db351d59da966c8a316207ad90e34bd3e46df7c01c157e11a","impliedFormat":99},{"version":"88779dc6d2d69b984969c2ac9450b512f8b4c54beae5bd51025b3e7b3909145c","impliedFormat":99},{"version":"a3a613da8d5a5b13af698d39b09fff499efdb0e8f536ab242e84c13370e3fce2","impliedFormat":99},{"version":"e161d627db35259f52c3eea227dab5483e0de833299fd7bc61823071927cda60","impliedFormat":99},{"version":"0ab06534ed1471f55971306ebd9151f2843d39e926f182773edc44afae2b3035","impliedFormat":99},{"version":"17e3178d17edec81153b214b3b8b1167c8951130100919a709d8157a117a12b6","impliedFormat":99},{"version":"c940f913dc8325a06b5abdaaa3a10651aeb6af99ccf2dd91cae6c3729fef8f81","impliedFormat":99},{"version":"3fd14efbc5a75b0a0ca5d581549b796f6e19b50d40a0ad4f67205fcb19274ee6","impliedFormat":99},{"version":"00dd58e1e52bdfd6c0b9d4dd3756014bbb02d1c3fb377d92a70a19893e1f33cd","impliedFormat":99},{"version":"8c147b2524e908e635a0fd569febe08152ec0b53152b5841e3d678474728f33b","impliedFormat":99},{"version":"a513595cad81255731831101bd714d77c3c7fadb3d5ebf1829d77fe025124b77","impliedFormat":99},{"version":"4ee05c416af71157410043a44a0803671e03c8bfca346d6f832ea047334b1cb6","impliedFormat":99},{"version":"1e74e54ccc165f3ddbe5460e2c6cc6c8aa2d3145a094d1b67c237303f61bb022","impliedFormat":99},{"version":"2e7bc808bf8376a838bc8a63edd68215cc3fb89ef6dfbd5bb679cd4d2827b43b","impliedFormat":99},{"version":"a6e51e0a926dc2b2b2d08512fea404d66095cc305765aaaa636918a34eaed159","impliedFormat":99},{"version":"7cf96480652b73719ce014b24ad8ac9c97620c64ee6acf8005be75d5b0988929","impliedFormat":99},{"version":"2f7c95858885b15628d20c06d1b41d2b91b6b4cd3dfc8e1389a1446420e6a74b","impliedFormat":99},{"version":"72ae884c8c22be1964b1911e84ce375bc5bdeccc25509b6333216a65c6c4a5e2","impliedFormat":99},{"version":"b02e828785ad66c35216229f1de36d28fecccaaf5b287dee5475932fb8b50219","impliedFormat":99},{"version":"053dd60a1bd76248ab2a7613fe365295525670e7d27264bece2b19053ddefec5","impliedFormat":99},{"version":"5d6ef65ccf14b0d51af503adffccdbaa846848cf0fe82310816cf82eb364d107","impliedFormat":99},{"version":"6c5bccbebab44e389a90c9302393910cd796e024e55ae1aae14bffd791f99464","impliedFormat":99},{"version":"71a747ae19d152aa688d767408ca753168ddd756fac5b9dba79461949433e00f","impliedFormat":99},{"version":"f7f93c42c4e7b5972e78f7b62fb00271c545d4f5247c23a9a263dbbcd968d906","impliedFormat":99},{"version":"2efba86762e23c705bc4ca720ebd84f94dc7b6565e268cf96ea504acdc2a52ef","impliedFormat":99},{"version":"4be799bfee1766047c11b3b5d371ca9e3993526d50c3e276e7cdb3943dd680a6","impliedFormat":99},{"version":"6d6c78dd576e10af137436f02d785194ead22da4a785f37bfc9fa793fb3b73ce","impliedFormat":99},{"version":"3e57fd3a8f13addca1c32a9a792e63d21baa4fcf706d23930f01ea312afacb04","impliedFormat":99},{"version":"38e61720edb6523a2ff0c62d2b06160d9b1c5916f8b04d3bf31e93f370fd5a29","impliedFormat":99},{"version":"f4cda2ff97e70f9f017b9b80bb5cd3e4570f3a527628562de2bf178af995d126","impliedFormat":99},{"version":"ebe9d82154a3bf6a6af680c3dcc6921b911624ea8f60699235c9c65fca087c3f","impliedFormat":99},{"version":"456bf57ef493ec750b79ffe7849813631db7b60827f36786cb672049a131d376","impliedFormat":99},{"version":"5f94250b6f8f598b1c42e624702098872b3afdf2ae6e391a02be7c0549aa64e7","impliedFormat":99},{"version":"1b2dfd1acca60e1782f8682e82860db220ae34c13a78e6795ad28c16a1146158","impliedFormat":99},{"version":"a40a75b4d4010077a911591554902897e1dd013f8a85225b6037a62f7056d437","impliedFormat":99},{"version":"ee8e06eaf1522a5e00fbfaa6473fea44dd74afd6f4e95f9da1a89af671aa2918","impliedFormat":99},{"version":"cb42b5a11ea87d65efb0aa44e08a3ca428542612c1b423066eb5f511afdf2533","impliedFormat":99},{"version":"bd883a743f4ce1d3206b3079446c2f6d2f806520bf9b8971ccd7d7fd983ce868","impliedFormat":99},{"version":"9e22adacca7d1de31f486abe4cbce49203c103d4530700a5c6f632f1c51f03eb","impliedFormat":99},{"version":"710d8a9f9860482a9467a7470bb47352a7a0efc7380c07228d3c9f51ef442bc4","impliedFormat":99},{"version":"995564ce50215678ed1a073b9eb63b5243c3b67e4edf44df299ccc0a8374cbe2","impliedFormat":99},{"version":"72d3929f8a6326462f3965821c38b8da7283081048ad4fbbe5a6b894b2467460","impliedFormat":99},{"version":"5515019e3a6ebbd431a945b6a43f31d139ae4b93e0a5ae91a915e02caef1832c","impliedFormat":99},{"version":"eb0ca7737f9fbc78b265201c1ac5fb93a26a0a0c457501f23097607318da6251","impliedFormat":99},{"version":"9f054267c51ac465965d91c20fd5057fd36cea9bd4656d514f4bebcade9c911a","impliedFormat":99},{"version":"e0586a07833fd675c3a32ffde2e1f586720759e8016cdcd535163e845fadb6fa","impliedFormat":99},{"version":"75c4008fe916b067ee4ddef78222d33024327da376289e9cbb100f356e117a03","impliedFormat":99},{"version":"85ad7a1017cff3848472528d792291038ebaf44b049a3afcaf0db612fa1b23a0","impliedFormat":99},{"version":"086c76363400b2153572922a22facb6a3cbb6dc6c3266cd75b7a4c55b564f8ae","impliedFormat":99},{"version":"ba883ef1d897a12d7e8a1c7347a20d733a5cd508eedc3fc0a3090fbbac936bc5","impliedFormat":99},{"version":"d8220fa464578acebc7fc4af92f2c57f8395025875a7eadb2ac69e0ddb9ac43d","impliedFormat":99},{"version":"9096832f382f5b5cb27ba00faa8c231d562623db74fc4025b0aba6bd233b8818","impliedFormat":99},{"version":"22b54bbe3779cb65ac35e420f96ec152a90be7a785b80ef9fa499d73b1ec58f1","impliedFormat":99},{"version":"178ae1eaa5cd24618fec31c62ee6b66f5f57d76b075d9d8b34cc0db5543c0fec","impliedFormat":99},{"version":"4dacb781ef89e1e92bed4d756f3b5941b19862083c124c0a50cf9aa225d78482","impliedFormat":99},{"version":"9aba87f9132dd2043482a72d3df5b2eff6aca78e0e8d7939253a7fcfc004b344","impliedFormat":99},{"version":"5fee9904e02e1475a281704b9afe8fc962e40084df5dffff4b4395dc7d552da2","impliedFormat":99},{"version":"dc9226ce99210a4a6ed075475c46292018f6a77eb038b65f860f05b883dbe0a7","impliedFormat":99},{"version":"f29d44cfd07de9939378795273c4232c8430a950ffdfac7010438b03577477e6","impliedFormat":99},{"version":"228e796062abd583bd87436562070d78425a0166aeac16b63459983b02acedb3","impliedFormat":99},{"version":"f5c623592de0fe3277e4195f52950c8d1f81e920d9be54682f609573b5503ba6","impliedFormat":99},{"version":"8002100726ad65ae695ef88b091b9c8cb73e024eaf23b31d228a5a8ce19af31f","impliedFormat":99},{"version":"22ad4f64a29216936a641bc51587ad5c4d2e843643091ebea4f9d0a472b8692c","impliedFormat":99},{"version":"0661abac34d843381137240cdd238d481637f5023ad952046b24a627c256194c","impliedFormat":99},{"version":"0cf60f5f3c66ac7b22d1e4a685c0b513328688886cb879394089f42f993e43a5","impliedFormat":99},{"version":"de8a83b2cb7e7f44e73155dd613e24141d97acdefc668333ea2b64d3a4ea7ae2","impliedFormat":99},{"version":"0b5a8af5558892fcd5c250a2dd2140f285dcc51672dd309fde24cef92836e6fa","impliedFormat":99},{"version":"c6ccfcc54bd078a3d99c51a06bcf779b15149a22471a70c54eefab43e3353ba1","impliedFormat":99},{"version":"8887205714f61e6586adf32374134738e460b4d8cfe03d513a38999913862daf","impliedFormat":99},{"version":"e1e593588e6cf59347c7a20017b214ac4b00562f6a2ec8e5c609e0ae965075f6","impliedFormat":99},{"version":"276367f57e2b9e574e1ca1a48eb22072a60d906295c96bd7aeafad5fc3d08b77","impliedFormat":99},{"version":"31d4161e79a2eeecae8e3f859da4d3d9afb1e6f3dfe1dc66380450a54c97528f","impliedFormat":99},{"version":"83b25a220cfdfa0e7590f1296945a56cf5f071461affa11651c8d0b059572aa7","impliedFormat":99},{"version":"1494274584ccf5a2af0572f0c3107739ed59b15aa96990db50fd8116eb4b3ccd","impliedFormat":99},{"version":"f4cf2ee04922bedeaacbc3f52e261c0b7c2fc8f81a5ed2299b4f50816d5e268b","impliedFormat":99},{"version":"bca68928478692b05d4ec10e88e725f29915437a5374e660c6cfbaf044c1930d","impliedFormat":99},{"version":"10f1dd47fe245c2fe7c27a4c9c066d8402b1d68991ab34c252ada59cbdfcc78b","impliedFormat":99},{"version":"790bef520dfac9dd348fe22c53568f048c6cb3ce21a8e3f046d01e8c0a66a943","impliedFormat":99},{"version":"f201350305673baab74b8917bf96149b3322d9806c683d510267d9a139b44900","impliedFormat":99},{"version":"d1893af3d12efecdb31c4062a82a92ce789e4d34aeb2a218c301c2c486d4fc78","impliedFormat":99},{"version":"25822bc7f060daf4c5f2e5fa075b2caf7f8bdedcbbab000269a97ff45f974745","impliedFormat":99},{"version":"da9e88283164077cae7301cdbb258966dde1d8a67e6af6b05c7a18349dde6321","impliedFormat":99},{"version":"e3f384585923f83d37a4ef1b75d1642632349c27e8f629acf23ea835877ddef3","impliedFormat":99},{"version":"44f0f5e119fb798c76d39c0383689991b25353639007a62d59224f2b8d88e004","impliedFormat":99},{"version":"3bb5c33e46d256998d12908375054dad7d82c6ccb866fd9e0fef3dac96acc402","impliedFormat":99},{"version":"c01a88ada696e9f65a4dd8248bd9a568a3f1ce0c2eaa5e7f8696a2c3b3573654","impliedFormat":99},{"version":"8bdede5bed57c1bb12a501cbd8ef0e0779c449c435b2b67b4074de4a6efabdfe","impliedFormat":99},{"version":"77bdf606434a7182de2ae5fe635523a95eccaf0c144f91df95e102a7c46c97a2","impliedFormat":99},{"version":"8d95114eac22e8ef4f8665a186d6608b55206f8d34a426c980dc9d2cd18b1e0d","impliedFormat":99},{"version":"b382cb44e04f416c8d67b5b6f1d2b118d01add9d9a98e7864fbf192c830f1efa","impliedFormat":99},{"version":"6ee2350f8ff32fa2bd3d379814f2d8a52063226b59c3d7379d83bd77d8683a87","impliedFormat":99},{"version":"ab84dfaa666066aaefee2739103b45c01c44c187e646b9020917f81c19793d4b","impliedFormat":99},{"version":"b1b4aa28430990a9f1bea96d31efe0583470cdd85244b74aa58074459a7a3518","impliedFormat":99},{"version":"ddba6ad2106348564085490c92de42a6d398377f9c806c30aafd67a8889ca4b7","impliedFormat":99},{"version":"465e84b9e824d62c531c6003c66f1bc73ba508bf60aa5c9797e2e3a4ec7a108b","impliedFormat":99},{"version":"156d4e8169fa27ddebf8c26b1158180fce5fca563216c8c16bdc2c5db663296e","impliedFormat":99},{"version":"3228a0ec21ce9bc0453a93d7d4c0c9b22bc06649457385e2113911293793717b","impliedFormat":99},{"version":"ceff24a8c06a2b16792aae8426b706018c4234e8504acf1cbba8ee6b79390161","impliedFormat":99},{"version":"1cce3949d58c46bc0764c89482a0be2b58d0b2a94a15e3147c88e73359658a40","impliedFormat":99},{"version":"7322c128662ae51bafb78bfa85a03e3da779b52e72d164c1bf22cdc65236270c","impliedFormat":99},{"version":"9a40c1020a86217fb3131a564315af933ce48aa1ef9264545bb1a2b410adb15c","impliedFormat":99},{"version":"0a8f0977ee6ed9db6042459c08fe444e7ef4a4b1b6d349d72655d90543aafff6","impliedFormat":99},{"version":"922d235d0784fdc0437ae8c038372fabb0b874486b65a47774fa34bda34dff3b","impliedFormat":99},{"version":"dc5aff116a7790b183c5f09e94f83a7c7e608c6085e6ad75b1629a83f5fc6c36","impliedFormat":99},{"version":"4d9e83ce19109b83aec7c181865a6c17a629130bcd7859dd9a09bc22725e347d","impliedFormat":99},{"version":"484b9305a7ff05e1028722f4a992db637cb6e31197490763deae399b36849d3e","impliedFormat":99},{"version":"d171cc95b1171193ecd8c047145fbb1644021394a18efcee1f3adb422ac36200","impliedFormat":99},{"version":"a09f4987f2ebde2a6b46bc5ca4b021b50ef09a01466b6545b0a2e7defcbeeb59","impliedFormat":99},{"version":"c9f95e2f5326df254b2c867de54f7264763065fa4d29f5f9d10960d97352afcf","impliedFormat":99},{"version":"0b4ba5551e44d84fd641b8f06eb3df38aa343d2c23a1358ad1b61f001764bf5f","impliedFormat":99},{"version":"ad0d9cecb6cf3ca943759fb015f684b455700272602349bc9754efdd5c73b2ae","impliedFormat":99},{"version":"4b75bbb5000a38175a6e728aaab07b10dda25c887c10f22c036261cba87471d2","impliedFormat":99},{"version":"cd4143e44f649e0c2674f3e3c1f6623f6f48342945214de732111944f8fa7e50","impliedFormat":99},{"version":"daf0673602c9217ac44106c295b579681811096ec2fa57a3fcd4d6470eaac8b8","impliedFormat":99},{"version":"c30a39369f4c75dc0d040f08e544f4b658ea695ce416be68ecf26c205e41ae5d","impliedFormat":99},{"version":"6da1127d73b53b3295d75624872a91cbac0eab602cb68ef8473d1414038e0408","impliedFormat":99},{"version":"8026ee081397a1ebdbdf20ddde81471c23d4c5e10038d110223505a8f32b77fd","impliedFormat":99},{"version":"4b1049d3aabfab678c821cdfa9c753c6adf33251ddda47d47059e00ce13f916a","impliedFormat":99},{"version":"941f6d0f05176fa7112d76b4f6f47326242500e112f3bb52868d17ac58e907fd","impliedFormat":99},{"version":"938edca549e0a6e4682f3324fc7c8a67f8944ab0c2dbdc8a54afd933c69e135f","impliedFormat":99},{"version":"3b2ac31bb38b7b625e5c5a69834dfe310248fb42edd297ca682de50d44555b1b","impliedFormat":99},{"version":"735331968e5f9c95e860641150eee5cd76e3f4d32d91d308fd31ba96bcecc49f","impliedFormat":99},{"version":"02353129e38fd07cc487b5f822ac710ec117e43e479e9f9f8039418ed3291ff5","impliedFormat":99},{"version":"54bd44d1d220488406919d2ddbdb92cef690c8ebfe41d2cdc61a8aaf26d6396c","impliedFormat":99},{"version":"59166f97779bdf70c8f36b8aeba6676d9b9ff64a256c9976e906eedfb6b87ae1","impliedFormat":99},{"version":"88f2b0ad065d1ff42736c1efeb0e14061b3091d9376c272672be3f27d167a152","impliedFormat":99},{"version":"5b6aef51a17a2533ddcb1460c8381462c10ee6e59ebdef99cd98176a738d7ba4","impliedFormat":99},{"version":"39841a65b5d4421d8f9e40b0f968a20ddd6ec345ccb24fae316ec02718916dd4","impliedFormat":99},{"version":"be922b6a92064b78554dfbf46decbddf5a0b023f49a656a7865e17ab0bf710c8","impliedFormat":99},{"version":"b8f0d69d3bcdf8894d0e10e4a4eb3d2cb3fc27fd3ea5802a9b2c1ba025690fc9","impliedFormat":99},{"version":"6431a32a41a7a92213bccdd8f1cac95bc4038896c6b36eb816946f6ebd1de07d","affectsGlobalScope":true,"impliedFormat":99},{"version":"8a6161ab51e94182d29dc5d4663db8d67aca7d4d43edce0f134b6d4dfaa42f2d","impliedFormat":99},{"version":"4b2fee8608e19bffaf53670f0af416bb2d3b84d2f9e319883f35804f195c6269","impliedFormat":99},{"version":"73fcba8699b817135e8217d4cb242403b8e97f2286afc4886778373fd7f5d687","impliedFormat":99},{"version":"4033b35f38b85606d366e29401cd63bb44b11c631fbe530e7cb6dea285dbce1e","impliedFormat":99},{"version":"6fca4a007c11a2cb5cfe738643b21c59127d45d8ac3356c1fcce8d2ea5c9b2ed","impliedFormat":99},{"version":"53c5c0ad9ed0605c92add7c41b57b99dce5cdabbf7ca05748d5555883d6dd486","impliedFormat":99},{"version":"5a13364736cf0eee277e0ea30431627ad754b51c96b95da0e5cae0155ba48d6d","impliedFormat":99},{"version":"aaf2c6a7eb583c145f1bd2491cced2654160785a4ba146dd57bb3ad8d1ad756c","impliedFormat":99},{"version":"b7e920c3467c6146140f4b95c402aef269731c2ba92299efe2eec22dcc71f30b","impliedFormat":99},{"version":"adb4426a3053d8d0f06b034134b939a2ebad9a29a07c595b9c70c736e4a52911","impliedFormat":99},{"version":"945740c51603a9a460909d8a5a6e32463a5c0cc2aa09ee7b928f2d72b6090734","impliedFormat":99},{"version":"b21436fd1ac202941df49d04311e510a742003849e46278a074829d016ff7e5c","impliedFormat":99},{"version":"8f8d4762a569fb8826e41be03a2fdf21f8c9f3f0d6ff42b7e7e68ef563855756","impliedFormat":99},{"version":"e7c940ea5bcfe1616f567f6a505b4b6fe5caef9e34d26988ef0a1fb40a3abbe1","impliedFormat":99},{"version":"2ef6dc247554af42f4a3e3c8e21742cae4599fa05f59a9c2504e982f508adbbc","impliedFormat":99},{"version":"e37e763321474ae8dfc20fce7462479a7b93fa151e0416ddbca263422e18d26b","impliedFormat":99},{"version":"92e145f2246906544d0fa367ef29239783441fa3e434e16f074d89804149ad29","impliedFormat":99},{"version":"4232ec8f460c0485c081f91381162bbdff18fe2de916770a4e946ce12388b4d1","impliedFormat":99},{"version":"49d3dacad2aa3680975ed967177cd45a49e0aa39811686269014941fd28356c8","impliedFormat":99},{"version":"775485ad2851461363171bd9b3f7807d3f2b612f0a20ab80e59f048632255a29","impliedFormat":99},{"version":"2c94d2217244dd31275ca5e404560c5c2105b5f06f8985d0f039f39caa1e9e30","impliedFormat":99},{"version":"9c88b05bdfe9898787a8776baaacc92b0499b0083905032bd9f3615a3135c26f","impliedFormat":99},{"version":"1e95f09a13a9555c87a921646cb1a2b2647476f73c4135af2e2c0e33c44b6c08","impliedFormat":99},{"version":"507029db6003a8e49680a599deb3898856d23b218c69900d2bba4083c1a34a97","impliedFormat":99},{"version":"7eda1f0806110518d3f03d78f93925af494ac263872eea3a85a5bfebd2b48bcb","impliedFormat":99},{"version":"28f91b1c0b330f4102efd145b38c6e07509220c0a214dded8aef3d3d469df6aa","impliedFormat":99},{"version":"afab761b301923855eb2a1849d23fe9d1dfee534fd986f6c227ed520d02a2d59","impliedFormat":99},{"version":"6da7497c314303f19ba36082297c9347ac524e7e9789714f688893fc786f4f9e","impliedFormat":99},{"version":"ae6a3e4c8c1119fe1bb44f8aed2f0f4b135fd42f7da862e144557ec897b5739a","impliedFormat":99},{"version":"35a7f9a074b2a6d3376eaa2046db7af262b632076d6888956a62785307691a46","impliedFormat":99},{"version":"b5548c7600a9b944d52aed0074767d92ac85cbef42521e8baacd71055338383c","impliedFormat":99},{"version":"f037ed5250876c6be9ed862687f133a35242b367681db9147f03dd7de2fef358","impliedFormat":99},{"version":"4712d78270086b6e4307b499ac7e45149c576bfc7e1ab4aa0b9b93d6cca923ec","impliedFormat":99},{"version":"e06d432a94dc47f95de8488b0b4bdde54b888b1b0632eb946d7b112fa5c14eac","impliedFormat":99},{"version":"1ef7446acfc034c230c2a783d271d1032321f029396453511eed15243b41cb59","impliedFormat":99},{"version":"86cf1a2280404a0607abb5849f3136dad6df1cd16da64fe907699ee36f937206","impliedFormat":99},{"version":"75fd7bc87b6b5ce7460b1bd5f7ccdd949c149211612893574c530ceaebed5cbb","impliedFormat":99},{"version":"e61ccfac1b24d6feede2dd2afba891e6b288830ae71102459496f22560fcc004","impliedFormat":99},{"version":"49a26201f50fa9a816e0931156323d9a4029891ddc5ee40792c57b1afb8cdff4","impliedFormat":99},{"version":"56cadc658182ee85d96ac84a5d31139eae2545aaf62cd1effaf0db5aa6b70e05","impliedFormat":99},{"version":"1586ef3a163f46a7db0481bd8fbb88a261e30d547f4a2f4a835e849d41025ba6","impliedFormat":99},{"version":"c5937640e2d65a7738ccbc1c8f5b9e78d630ebd5fb8593eef5e30b4ea99b8d2f","impliedFormat":99},{"version":"8e7628593ebe34ec1022035f7683a2ef92bb9cb531c07fbdc0fea64928f4ea7b","impliedFormat":99},{"version":"f4a377ca062dc8a02a638f2eb10b6c94e198aaf91728e346f748301565c99658","impliedFormat":99},{"version":"10c0fe874f64e1a821a0e6f6ecba3d2082db08011e96f86168c26fefc6588236","impliedFormat":99},{"version":"746ffa1873008cd4f50d2ebad2c4e67a42e00eb36cb007630a8c664bbf193227","impliedFormat":99},{"version":"3ab3564a240e86c68ed9057a868c721998ca17123dc7cdd29d8018199be73342","impliedFormat":99},{"version":"1d246c73f66479fb9676aa7bdb713ce9a712e0785b7957f5bf450a8dcb8106be","impliedFormat":99},{"version":"86373a2c826bc505376b8baadaf1961628b065aa0820c89abf1cb7abfbd07afb","impliedFormat":99},{"version":"a051b97de62cd18a86ea252ac37ee07640d3cf6d66aeeb126aa4c41f3c4ce3fe","impliedFormat":99},{"version":"6d00a86fe567e3fc0a389c30e49f23e14aec923345eff22f5c95507305a5fac6","impliedFormat":99},{"version":"e9214291673a507e06de72638d08cb77a5a83946ff371fe3118231fd14b66148","impliedFormat":99},{"version":"6afd93aec340602a842a3fd846432339eed3581ee1328e65dc9ddf04967681d0","impliedFormat":99},{"version":"40405e75f72dd4fe99bb2fdbb7d2b2e0d12eca86da8df669bafec4466616f57c","impliedFormat":99},{"version":"f9ecc480939f38ffab76c67e071fe69cfff07b49100170b59100010ba5a65f0a","impliedFormat":99},{"version":"d4e4495cc8b784ea848a29cfe457722c6dbedd8d766627ea1f4eac3b113ddf31","impliedFormat":99},{"version":"d8e02aa2838623274cd72e3e379f931ce28af86202d05cc4310f4a9d03408c61","impliedFormat":99},{"version":"7bb53546e9bd6e3f22804497a41d4b885674e7b15b7d64c7d3f83722dfd2b456","impliedFormat":1},{"version":"4083e6d84bfe72b0835b600185c7b7ce321da3d6053f866859185eefc161e7a0","impliedFormat":1},{"version":"b883e245dc30c73b655ffe175712cac82981fc999d6284685f0ed7c1dac8aa6f","impliedFormat":1},{"version":"626e3504b81883fa94578c2a97eff345fadc5eae17a57c39f585655eef5b8272","impliedFormat":1},{"version":"e9a15eeba29ceb0ee109dd5e0282d2877d8165d87251f2ea9741a82685a25c61","impliedFormat":1},{"version":"c6cb06cc021d9149301f3c51762a387f9d7571feed74273b157d934c56857fac","impliedFormat":1},{"version":"cd7c133395a1c72e7c9e546f62292f839819f50a8aa46050f8588b63ef56df88","impliedFormat":1},{"version":"196f5f74208ce4accea017450ed2abc9ce4ab13c29a9ea543db4c2d715a19183","impliedFormat":1},{"version":"4687c961ab2e3107379f139d22932253afb7dd52e75a18890e70d4a376cdf5d9","impliedFormat":1},{"version":"ae8cfe2e3bdef3705fc294d07869a0ab8a52d9b623d1cc0482b6fc2be262b015","impliedFormat":1},{"version":"94c8e9c00244bbf1c868ca526b12b4db1fab144e3f5e18af3591b5b471854157","impliedFormat":1},{"version":"827d576995f67a6205c0f048ae32f6a1cf7bda9a7a76917ab286ef11d7987fd7","impliedFormat":1},{"version":"cb5dc83310a61d2bb351ddcdcaa6ec1cf60cc965d26ce6f156a28b4062e96ab2","impliedFormat":1},{"version":"0091cb2456a823e123fe76faa8b94dea81db421770d9a9c9ade1b111abe0fcd1","impliedFormat":1},{"version":"034d811fd7fb2262ad35b21df0ecab14fdd513e25dbf563572068e3f083957d9","impliedFormat":1},{"version":"298bcc906dd21d62b56731f9233795cd11d88e062329f5df7cdb4e499207cdd4","impliedFormat":1},{"version":"f7e64be58c24f2f0b7116bed8f8c17e6543ddcdc1f46861d5c54217b4a47d731","impliedFormat":1},{"version":"966394e0405e675ca1282edbfa5140df86cb6dc025e0f957985f059fe4b9d5d6","impliedFormat":1},{"version":"b0587deb3f251b7ad289240c54b7c41161bb6488807d1f713e0a14c540cbcaee","impliedFormat":1},{"version":"4254aab77d0092cab52b34c2e0ab235f24f82a5e557f11d5409ae02213386e29","impliedFormat":1},{"version":"19db45929fad543b26b12504ee4e3ff7d9a8bddc1fc3ed39723c2259e3a4590f","impliedFormat":1},{"version":"b21934bebe4cd01c02953ab8d17be4d33d69057afdb5469be3956e84a09a8d99","impliedFormat":1},{"version":"b2b734c414d440c92a17fd409fa8dac89f425031a6fc7843bac765c6c174d1ca","impliedFormat":1},{"version":"239f39e8ad95065f5188a7acd8dbefbbbf94d9e00c460ffdc331e24bc1f63a54","impliedFormat":1},{"version":"d44f78893cb79e00e16a028e3023a65c1f2968352378e8e323f8c8f88b8da495","impliedFormat":1},{"version":"32afc9daae92391cb4efeb0d2dac779dc0fb17c69be0eb171fd5ed7f7908eeb4","impliedFormat":1},{"version":"b835c6e093ad9cda87d376c248735f7e4081f64d304b7c54a688f1276875cbf0","impliedFormat":1},{"version":"a9eabe1d0b20e967a18758a77884fbd61b897d72a57ddd9bf7ea6ef1a3f4514b","impliedFormat":1},{"version":"64c5059e7d7a80fe99d7dad639f3ba765f8d5b42c5b265275d7cd68f8426be75","impliedFormat":1},{"version":"05dc1970dc02c54db14d23ff7a30af00efbd7735313aa8af45c4fd4f5c3d3a33","impliedFormat":1},{"version":"a0caf07fe750954ad4cf079c5cf036be2191a758c2700424085ffde6af60d185","impliedFormat":1},{"version":"1ea59d0d71022de8ea1c98a3f88d452ad5701c7f85e74ddaa0b3b9a34ed0e81c","impliedFormat":1},{"version":"eab89b3aa37e9e48b2679f4abe685d56ac371daa8fbe68526c6b0c914eb28474","impliedFormat":1},{"version":"4d1a9c5960c005d2fe9cf5f5a6bc70850149ea0454666f133595a6acd6b593e3","impliedFormat":99},{"version":"8439d349f53c599dae7d7b9df3382688b1cf167e6da1fc6cfa3aef4c54a3b0b6","impliedFormat":99},{"version":"b3881d7a0becfe1d507a36f40f2d8cbaa1a682cdb5570e24761ac0396142b8be","impliedFormat":99},{"version":"361afaeb04e8fedb5f4c85a61086200bdc302b62a58852091d7bc1e3dff34986","impliedFormat":99},{"version":"59f471a26313fc2bccfadf56191a55b704a1dfbafaa7c3f2c37e25de8a1e9c14","impliedFormat":99},{"version":"42fc44363e281c50d570d28f0336d364d904244435c515f6973fed990ca7925f","impliedFormat":99},{"version":"0bb96d1b7886f8348ee457c22db99c258f563e6e4371410c8c0137c54f8b6332","impliedFormat":99},{"version":"107dec9919e26cd898658841caac2186b3b10ca2e81ba0ecc9407ac989b0b860","impliedFormat":99},{"version":"a6f32c6ebdf43913196c351ed0152695f0d76dbe8226002e2d6654835e0cb685","impliedFormat":99},{"version":"66c41552364289ef6eb841fdbc2eeb7d40b2c79cf2d92009cc1537e4d5d7454b","impliedFormat":99},{"version":"f72856f3920a6baf267ca04fe086e1e00034953931fcac9ed59f1e651c444eec","impliedFormat":99},{"version":"ee10a6b8d4948616a923e953b40dd564d87f4c6c960353a4ab40f9ac5953508a","impliedFormat":99},{"version":"616f4301604d5263a177d9d378a417940ee51f4661dc970c446265139b3dc2d7","impliedFormat":99},{"version":"cc8621f4a86f09a9d63af2008516e3284fa8dee2da7ac3e010a7a344267e9fb9","impliedFormat":99},{"version":"da37f3e19d6e2b5bb10cc3c6bcb5d2e84c4d5cb9bd9a12ba05ee43c9200a9b23","impliedFormat":99},{"version":"99e7111361e06b6f5262371d764116a5faa69f021d74c8c3ae1584d5a486a75c","impliedFormat":99},{"version":"7d3d9f991564d3cec0a7d5d75c1aa89cbaeeb8184106d0a92c0e54ec01420103","impliedFormat":99},{"version":"b9481313926f4e6c25941bb0fb426c1ad764e3fba09bad9519f4c8532ff26210","impliedFormat":99},{"version":"b9a223bbab79b9a07ba163672633095a6b92c602bd3b28e39ba5a66519cc478f","impliedFormat":99},{"version":"7a48c5cd1ba5cb5497f020e8af6df1d6632072548b1b6079815bb2d46a76793b","impliedFormat":99},{"version":"12d19496f25ecd6afef2094be494b3b0ae12c02bd631901f6da760c7540a5ec1","impliedFormat":1},{"version":"c6fe327c538417b8dd5b9bb32abcd7911534b10da3a4514f3445cdb28cf3abf2","impliedFormat":99},{"version":"0065cdb7ac9f5b19921632de63f888ec2cc11ad57f7fc868f44bf0faad2fce3e","impliedFormat":99},{"version":"8c1adc3171d0287f3a26f4891a7d1834c89999573a9b444aa5ff519dcc43a2b7","impliedFormat":99},{"version":"27aee784c447854a4719f11058579e49f08faa70d06d8e30abe00f5e25538de6","impliedFormat":99},{"version":"fbc610f9dde70f0bbea39eefec2e31ca1d99f715e9c71fb118bd2306a832bcb5","impliedFormat":99},{"version":"a829052855dca3affb8e2ef0afa0f013b03fa9b55762348b1fba76d9c2741c99","impliedFormat":99},{"version":"1d61288b34b2dd2029b85bc70fabbb1da90c2a370396d5df5f620e62eb47ddbe","impliedFormat":99},{"version":"5a2cf4cd852a58131b320da62269b2143850920ce27e8fdec41fed5c2c54ec95","impliedFormat":99},{"version":"43552100e757fad5a9bb5dabc0ea24ba3b6f2632eb1a4be8915da39d65e83e1c","impliedFormat":99},{"version":"6a99940a8a76a1aa20ae6f2afd8e909e47e0b17df939e7cf5a585171480655ff","impliedFormat":99},{"version":"043195af0b52aadd10713870dd60369df0377ed153104b26e6bac1213b19f63e","impliedFormat":99},{"version":"ad17a36132569045ab97c8e5badf8febb556011a8ed7b2776ff823967d6d5aca","impliedFormat":99},{"version":"698d2b22251dbbfc0735e2d6ed350addead9ad031fac48b8bb316e0103d865db","impliedFormat":99},{"version":"7298d28b75c52e89c0b3e5681eac19e14480132cd30baaba5e5ca10211a740ef","impliedFormat":99},{"version":"ff10facf373a13d2864ff4de38c4892d74be27d9c6468dac49c08adabbf9b0eb","impliedFormat":99},{"version":"97b1cf4599cc3bc2e84b997aa1af60d91ca489d96bea0e20aaff0e52a5504b29","impliedFormat":99},{"version":"853dfbcd0999d3edc6be547d83dc0e0d75bf44530365b9583e75519d35984c35","impliedFormat":99},{"version":"9c80bed388d4ed47080423402db9cb1b35a31449045a83a0487f4dfde3d9d747","impliedFormat":99},{"version":"f29bc6a122a4a26c4e23289daae3aa845a18af10da90989cb8b51987e962b7be","impliedFormat":99},{"version":"3a1f39e098971c10633a064bd7a5dbdec464fcf3864300772763c16aa24457f9","impliedFormat":99},{"version":"20e614d6e045d687c3f7d707561b7655ad6177e859afc0c55649b7e346704c77","impliedFormat":99},{"version":"aa0ae1910ba709bc9db460bdc89a6a24d262be1fbea99451bedac8cbbc5fb0cd","impliedFormat":99},{"version":"161d113c2a8b8484de2916480c7ba505c81633d201200d12678f7f91b7a086f0","impliedFormat":99},{"version":"b998a57d4f43e32ac50a1a11f4505e1d7f71c3b87f155c140debe40df10386c8","impliedFormat":99},{"version":"5710e8ed9797ae0042e815eb8f87df2956cb1bf912939c9b98eeb58494a63c13","impliedFormat":99},{"version":"a6bb421dccfec767dbd3e99180b24c07c4a216c0fd549f54a3313f6ce3f9d2c7","impliedFormat":99},{"version":"3b6f1be46f573b1c1f3e6cd949890bfb96b40ff90b6f313e425a379c1c4d5d77","impliedFormat":99},{"version":"28a2c54d0a78d32c29f7279ca04dc6c7860c008579e4e3033938c0ed0201eb9a","impliedFormat":99},{"version":"c2714a402843287624210a47ebea2b1c8dd3ad1438f448633f6831e31eaf37b8","impliedFormat":99},{"version":"b89945ec6707415d739f3e76f2820982d4927dc6b681910b3c433b5ad261b817","impliedFormat":99},{"version":"a72d5822fb2a2c1ef985b30aed889f4c00342c90e12318762fccc550c6a599cf","impliedFormat":99},{"version":"c8616ab60eda93ca87fbb20aada1d6a6cdbcd2cb181a70a2d7728a3cb0613391","impliedFormat":99},{"version":"eeddfd3e0b09890822068de5248d38144f8328e74b5292847eb4e558d8aba8cb","impliedFormat":99},{"version":"d4dc0b6592543314c8549c71e35ad2ec4a57904662d905ff9585836bde1c855a","impliedFormat":99},{"version":"56e1687a174cd10912a35a4676af434bb213aafa5d4371040986c578afe644ab","impliedFormat":99},{"version":"470c280cc484340b97d0942e0c3aa312399eba3849ceb95312d0d7413bac7458","impliedFormat":99},{"version":"ae183f4a6300aad2be92cdbd4dd12d8bcd36eddf8dd1846f998c237235fe0c33","impliedFormat":99},{"version":"4b0eeffddaf51b967e95926a825a6ba1205b81b3a8fecddbe21eaf0e86bdee91","impliedFormat":99},{"version":"bf3ec0d42e33e487c359a989b30e1c9e90fa06de484dc4751e93fb34a9b5cf90","impliedFormat":99},{"version":"7b9656a61d83df1a46c38c2984dbf96dd057bf48f477ddf3f8990311ab98ec23","impliedFormat":99},{"version":"366b85ddb698f3a035e0caa68dc9fef39a85c4368c0810eaf937c3a3c63ac31e","impliedFormat":99},{"version":"d440ee730bc60a5c605903842e398863e7ecdb7a91fc32a9152f14061bf6cc17","impliedFormat":99},{"version":"a12c86c4a691608d19a75320946c80bbce38bb62c091dda32572aee7158edd38","impliedFormat":99},{"version":"3109cb3f8ab0308d2944c26742b6a8a02b4a4ffc23f479a81f0e945d6a6721dd","impliedFormat":99},{"version":"a2289c12a987f2a06f4cf049afde4fdc9455a4af37913445148865938c6eb613","impliedFormat":99},{"version":"55933c1450edcfaf166429425dbbad0a27c0ae8672d5ab5d427e46946a6f2f63","impliedFormat":99},{"version":"6c684fda6998db4112e82367c9e82e27996dc8086a10d58ac9b51d89770d5f9d","impliedFormat":99},{"version":"5c4b4dd983471fcaed17ad3241c98a1f880729f1ca579ddbcdae7e0bf04035df","impliedFormat":99},{"version":"9e430429c7e9e70071a836ac91a1bf6e6651f91d47d9f4baf0a92eefc6130818","impliedFormat":99},{"version":"b3db7f6d7ef72669dc83fa1ff7b90a2ec31d1d8f82778f2a00ef6d101f5247e5","impliedFormat":99},{"version":"354f61bd2a5acaf20462bc4d61048aa25f8fc0dd04dfe3d2f30bdbabbab54e7d","impliedFormat":99},{"version":"d51756340928e549f076c832d7bc2b4180385597b0b4daaa50e422bed53e1a72","impliedFormat":99},{"version":"32c6e3ef96f2bcbc1db7d7f891459241657633aa663cab6812fb28ade7c90608","impliedFormat":99},{"version":"ac2ea00eb8f73665842e57e729e14c6d3feabe9859dc5e87a1ed451b20b889e4","impliedFormat":99},{"version":"730cb342a128f5a8a036ffbd6dbc1135b623ce2100cefe1e1817bb8845bc7100","impliedFormat":99},{"version":"78e387f16df573a98dd51b3c86d023ddbd5bf68e510711a9fee8340e7ccc3703","impliedFormat":99},{"version":"e2381c64702025b4d57b005e94ed0b994b5592488d76f1e5f67f59d1860ebb70","impliedFormat":99},{"version":"d7dfcb039ff9cff38ccd48d2cc1ba95ca45c316670eddbcf81784e21b7128692","impliedFormat":99},{"version":"acaf0a60eb243938f7742df08bf5d52482fbea033fd27141ee3a6d878bbb0d3d","impliedFormat":99},{"version":"fb89aeecfc8eb28f5677c2c89bced74d13442b7f4ebd01ce2ce92127d1b36d69","impliedFormat":99},{"version":"9e91cb0a5bd7aefa2b94a2872828d6d2321df0ca44412e74d99e8b94e579b7d8","impliedFormat":99},{"version":"3e4f06b464ef1654b91be02777d1773ccc5d43b53c1c8b0a9794ec224cfe8928","impliedFormat":99},{"version":"192c1a207b44af476190ae66920636de5d56c33b57206bbc2421adc23f673e2e","impliedFormat":99},{"version":"e5aa35b3740170492e06e60989d35a222cfda2148507c650ea55753f726c9213","impliedFormat":99},{"version":"057aa42f6983120c35373aed62b219ffcbd7b476b2df08709139a9eb8dfeed26","impliedFormat":99},{"version":"95a0c46b4675d4d02de6a7c167738f1176b53b26ebec9ccfe8e5d9acb0dc7aee","impliedFormat":99},{"version":"94ad4d9745811c482ae3bad61e5b206e0904f77e0dacf783199193a3df9f6ce6","impliedFormat":99},{"version":"407dc18ecd25802296fade17be81d0d4f499ae75fe88ed132f94e7efdad269e2","impliedFormat":99},{"version":"77dabe31d44c48782c529d5c9acddc41f799bf9b424b259596131efc77355478","impliedFormat":99},{"version":"f6dfe21d867aa5e13bc53d536b69b66427f571707a01e7c3604dc51ded097313","impliedFormat":99},{"version":"4ecd02d0e4ccf7befb9c28802c6c208060e33291d56fd1868900ca295c399077","impliedFormat":99},{"version":"37ada75be4b3f6b888f538091020d81b2a0ad721dc42734f70f639fa4703a5c8","impliedFormat":99},{"version":"aa73ff0024d5434a3e87ea2824f6faece7aad7b9f6c22bd399268241ca051dc7","impliedFormat":99},{"version":"4c9fb50b0697756bab3e4095f28839cf5b55430a4744d2ebbaf850ec8dca54d8","impliedFormat":99},{"version":"782868b723c055c5612c4a243f72a78a8b3c0c3b707ae04954e36e8ab966df4c","impliedFormat":99},{"version":"3de9d9ad4876972e7599fc0b3bddb0fddb1923be75787480a599045a30f14292","impliedFormat":99},{"version":"0f4b3c05937bbdb9cf954722ddc97cd72624e3b810f6f2cf4be334adb1796ec1","impliedFormat":99},{"version":"9fc243c4c87d8560348501080341e923be2e70bf7b5e09a1b26c585d97ae8535","impliedFormat":99},{"version":"4f97089fe15655ae448c9d005bb9a87cc4e599b155edc9e115738c87aa788464","impliedFormat":99},{"version":"f948d562d0a8085f1bd17b50798d5032529a75c147f40adfeb4fd3e453368643","impliedFormat":99},{"version":"22929f9874783b059156ee3cfa864d6f718e1abf9c139f298a037ae0274186f6","impliedFormat":99},{"version":"c72a7c316459b2e872ca4a9aca36cc05d1354798cee10077c57ff34a34440ac2","impliedFormat":99},{"version":"3e5bbf8893b975875f5325ebf790ab1ab38a4173f295ffea2ed1f108d9b1512c","impliedFormat":99},{"version":"9e4a38448c1d26d4503cf408cc96f81b7440a3f0a95d2741df2459fe29807f67","impliedFormat":99},{"version":"84124d21216da35986f92d4d7d1192ca54620baeca32b267d6d7f08b5db00df9","impliedFormat":99},{"version":"efba354914a2dc1056a55510188b6ced85ead18c5d10cc8a767b534e2db4b11b","impliedFormat":99},{"version":"25f5bf39f0785a2976d0af5ac02f5c18ca759cde62bc48dd1d0d99871d9ad86f","impliedFormat":99},{"version":"e711fa7718a2060058ff98ac6bff494c1615b9d42c4f03aa9c8270bc34927164","impliedFormat":99},{"version":"e324b2143fa6e32fac37ed9021b88815e181b045a9f17dbb555b72d55e47cdc1","impliedFormat":99},{"version":"3e90ea83e3803a3da248229e3027a01428c3b3de0f3029f86c121dc76c5cdcc2","impliedFormat":99},{"version":"9368c3e26559a30ad3431d461f3e1b9060ab1d59413f9576e37e19aaf2458041","impliedFormat":99},{"version":"915e5bb8e0e5e65f1dc5f5f36b53872ffcdcaef53903e1c5db7338ea0d57587a","impliedFormat":99},{"version":"92cf986f065f18496f7fcb4f135bff8692588c5973e6c270d523191ef13525ad","impliedFormat":99},{"version":"652f2bd447e7135918bc14c74b964e5fe48f0ba10ff05e96ed325c45ac2e65fb","impliedFormat":99},{"version":"cc2156d0ec0f00ff121ce1a91e23bd2f35b5ab310129ad9f920ddaf1a18c2a4d","impliedFormat":99},{"version":"7b371e5d6e44e49b5c4ff88312ae00e11ab798cfcdd629dee13edc97f32133d8","impliedFormat":99},{"version":"e9166dab89930e97bb2ce6fc18bcc328de1287b1d6e42c2349a0f136fc1f73e6","impliedFormat":99},{"version":"6dc0813d9091dfaed7d19df0c5a079ee72e0248ce5e412562c5633913900be25","impliedFormat":99},{"version":"e704c601079399b3f2ec4acdfc4c761f5fe42f533feaaab7d2c1c1528248ca3e","impliedFormat":99},{"version":"49104d28daa32b15716179e61d76b343635c40763d75fe11369f681a8346b976","impliedFormat":99},{"version":"04cd3418706b1851d2c1d394644775626529c23e615a829b8abfe26ec0ee3aef","impliedFormat":99},{"version":"21e459e9485fc48f21708d946c102e4aaa4a87b4c9ad178e1c5667e3ff6bbc59","impliedFormat":99},{"version":"97e685ac984fc93dcdae6c24f733a7a466274c103fdcf5d3b028eaa9245f59d6","impliedFormat":99},{"version":"68526ea8f3bbf75a95f63a3629bebe3eb8a8d2f81af790ce40bc6aad352a0c12","impliedFormat":99},{"version":"bcab57f5fe8791f2576249dfcc21a688ecf2a5929348cfe94bf3eb152cff8205","impliedFormat":99},{"version":"b5428f35f4ebf7ea46652b0158181d9c709e40a0182e93034b291a9dc53718d8","impliedFormat":99},{"version":"0afcd28553038bca2db622646c1e7fcf3fb6a1c4d3b919ef205a6014edeeae0f","impliedFormat":99},{"version":"ee016606dd83ceedc6340f36c9873fbc319a864948bc88837e71bd3b99fdb4f6","impliedFormat":99},{"version":"0e09ffe659fdd2e452e1cbe4159a51059ae4b2de7c9a02227553f69b82303234","impliedFormat":99},{"version":"4126cb6e6864f09ca50c23a6986f74e8744e6216f08c0e1fe91ab16260dab248","impliedFormat":99},{"version":"4927dba9193c224e56aa3e71474d17623d78a236d58711d8f517322bd752b320","impliedFormat":99},{"version":"3d3f189177511d1452e7095471e3e7854b8c44d94443485dc21f6599c2161921","impliedFormat":99},{"version":"bb0519ff5ef245bbf829d51ad1f90002de702b536691f25334136864be259ec5","impliedFormat":99},{"version":"a64e28f2333ea0324632cf81fd73dc0f7090525547a76308cb1dfe5dab96596a","impliedFormat":99},{"version":"883f9faa0229f5d114f8c89dadd186d0bdf60bdafe94d67d886e0e3b81a3372e","impliedFormat":99},{"version":"d204b9ae964f73721d593e97c54fc55f7fd67de826ce9e9f14b1e762190f23d1","impliedFormat":99},{"version":"91830d20b424859e5582a141efe9a799dc520b5cce17d61b579fb053c9a6cd85","impliedFormat":99},{"version":"68115cdc58303bad32e2b6d59e821ccaada2c3fb63f964df7bd4b2ebd6735e80","impliedFormat":99},{"version":"ee27e47098f1d0955c8a70a50ab89eb0d033d28c5f2d76e071d8f52a804afe07","impliedFormat":99},{"version":"7957b11f126c6af955dc2e08a1288013260f9ec2776ff8cc69045270643bf43e","impliedFormat":99},{"version":"d010efe139c8bb78497dc7185dddbbcefc84d3059b5d8549c26221257818a961","impliedFormat":99},{"version":"85059ed9b6605d92c753daf3a534855ba944be69ff1a12ab4eca28cefbabd07a","impliedFormat":99},{"version":"687208233ae7a969baa2d0c565c9f24eb4cb1e64d6cfb30f71afec9e929e58c2","impliedFormat":99},{"version":"ea68a96f4e2ba9ca97d557b7080fbdb7f6e6cf781bb6d2e084e54da2ac2bb36c","impliedFormat":99},{"version":"879de92d0104d490be2f9571face192664ec9b45e87afd3f024dbbf18afb4399","impliedFormat":99},{"version":"424df1d45a2602f93010cb92967dfe76c3fcadad77d59deb9ca9f7ab76995d40","impliedFormat":99},{"version":"21f96085ed19d415725c5a7d665de964f8283cacef43957de10bdd0333721cc4","impliedFormat":99},{"version":"e8d4da9e0859c6d41c4f1c3f4d0e70446554ba6a6ab91e470f01af6a2dcac9bf","impliedFormat":99},{"version":"2e2421a3eec7afefa5a1344a6852d6fee6304678e2d4ee5380b7805f0ac8b58a","impliedFormat":99},{"version":"a10fd5d76a2aaba572bec4143a35ff58912e81f107aa9e6d97f0cd11e4f12483","impliedFormat":99},{"version":"1215f54401c4af167783d0f88f5bfb2dcb6f0dacf48495607920229a84005538","impliedFormat":99},{"version":"476f8eb2ea60d8ad6b2e9a056fdda655b13fd891b73556b85ef0e2af4f764180","impliedFormat":99},{"version":"2fe93aef0ee58eaa1b22a9b93c8d8279fe94490160703e1aabeff026591f8300","impliedFormat":99},{"version":"bbb02e695c037f84947e56da3485bb0d0da9493ed005fa59e4b3c5bc6d448529","impliedFormat":99},{"version":"ba666b3ab51c8bc916c0cebc11a23f4afec6c504c767fd5f0228358f7d285322","impliedFormat":99},{"version":"c10972922d1887fe48ed1722e04ab963e85e1ac12263a167edef9b804a2af097","impliedFormat":99},{"version":"6efeacbd1759ea57a4c7264eb766c531ae0ab2c00385294be58bc5031ef43ad1","impliedFormat":99},{"version":"1c261f5504d0175be4f1b6b99f101f4c3a129a5a29fc768e65c52d6861ca5784","impliedFormat":99},{"version":"f0e69b5877b378d47cbac219992b851e2bbc0f7e3a3d3579d67496dabd341ec4","impliedFormat":99},{"version":"b5ea27f19a54feca5621f5ba36a51026128ea98e7777e5d47f08b79637527cf5","impliedFormat":99},{"version":"b54890769fa3c34ab3eb7e315b474f52d5237c86c35f17d59eb21541e7078f11","impliedFormat":99},{"version":"c133db4b6c17a96db7fa36607c59151dec1e5364d9444cbe15e8c0ea4943861e","impliedFormat":99},{"version":"3a0514f77606d399838431166a0da6dbd9f3c7914eae5bbfbd603e3b6a552959","impliedFormat":99},{"version":"fa568f8d605595e1cffbfca3e8c8c492cf88ae2c6ed151f6c64acf0f9e8c25d8","impliedFormat":99},{"version":"c76fb65cb2eb09a0ee91f02ff5b43a607b94a12c34d16d005b2c0afc62870766","impliedFormat":99},{"version":"cf7af60a0d4308a150df0ab01985aabb1128638df2c22dd81a2f5b74495a3e45","impliedFormat":99},{"version":"913bbf31f6b3a7388b0c92c39aec4e2b5dba6711bf3b04d065bd80c85b6da007","impliedFormat":99},{"version":"42d8c168ca861f0a5b3c4c1a91ff299f07e07c2dd31532cd586fd1ee7b5e3ae6","impliedFormat":99},{"version":"a29faa7cb35193109ec1777562ca52c72e7382ffe9916b26859b5874ad61ff29","impliedFormat":99},{"version":"15bdf2eeef95500ba9f1602896e288cb425e50462b77a07fa4ca23f1068abb21","impliedFormat":99},{"version":"452db58fd828ab87401f6cecc9a44e75fa40716cc4be80a6f66cf0a43c5a60cc","impliedFormat":99},{"version":"54592d0215a3fd239a6aa773b1e1a448dc598b7be6ce9554629cd006ee63a9d6","impliedFormat":99},{"version":"9ee28966bb038151e21e240234f81c6ba5be6fde90b07a9e57d4d84ae8bc030c","impliedFormat":99},{"version":"2fe1c1b2b8a41c22a4e44b0ac7316323d1627d8c72f3f898fa979e8b60d83753","impliedFormat":99},{"version":"956e43b28b5244b27fdb431a1737a90f68c042e162673769330947a8d727d399","impliedFormat":99},{"version":"92a2034da56c329a965c55fd7cffb31ccb293627c7295a114a2ccd19ab558d28","impliedFormat":99},{"version":"c1b7957cd42a98ab392ef9027565404e5826d290a2b3239a81fbac51970b2e63","impliedFormat":99},{"version":"4861ee34a633706bcbba4ea64216f52c82c0b972f3e790b14cf02202994d87c5","impliedFormat":99},{"version":"7af4e33f8b95528de005282d6cca852c48d293655dd7118ad3ce3d4e2790146f","impliedFormat":99},{"version":"df345b8d5bf736526fb45ae28992d043b2716838a128d73a47b18efffe90ffa7","impliedFormat":99},{"version":"d22c5b9861c5fc08ccd129b5fc3dcdc7536e053c0c1d463f3ab39820f751c923","impliedFormat":99},{"version":"dcc38f415a89780b34d827b45493d6dbadb05447d194feb4498172e508c416ac","impliedFormat":99},{"version":"7e917e3b599572a2dd9cfa58ff1f68fda9e659537c077a2c08380b2f2b14f523","impliedFormat":99},{"version":"20b108e922abd1c1966c3f7eb79e530d9ac2140e5f51bfa90f299ad5a3180cf9","impliedFormat":99},{"version":"2bc82315d4e4ed88dc470778e2351a11bc32d57e5141807e4cdb612727848740","impliedFormat":99},{"version":"e2dd1e90801b6cd63705f8e641e41efd1e65abd5fce082ef66d472ba1e7b531b","impliedFormat":99},{"version":"a3cb22545f99760ba147eec92816f8a96222fbb95d62e00706a4c0637176df28","impliedFormat":99},{"version":"287671a0fe52f3e017a58a7395fd8e00f1d7cd9af974a8c4b2baf35cfda63cfa","impliedFormat":99},{"version":"e2cdad7543a43a2fb6ed9b5928821558a03665d3632c95e3212094358ae5896b","impliedFormat":99},{"version":"326a980e72f7b9426be0805774c04838e95195b467bea2072189cefe708e9be7","impliedFormat":99},{"version":"e3588e9db86c6eaa572c313a23bf10f7f2f8370e62972996ac79b99da065acaa","impliedFormat":99},{"version":"1f4700278d1383d6b53ef1f5aecd88e84d1b7e77578761838ffac8e305655c29","impliedFormat":99},{"version":"6362a4854c52419f71f14d3fee88b3b434d1e89dcd58a970e9a82602c0fd707a","impliedFormat":99},{"version":"fb1cc1e09d57dfeb315875453a228948b904cbe1450aaf8fda396ff58364a740","impliedFormat":99},{"version":"50652ed03ea16011bb20e5fa5251301bb7e88c80a6bf0c2ea7ed469be353923b","impliedFormat":99},{"version":"d388e0c1c9a42d59ce88412d3f6ce111f63ce2ff558e0a3f84510092431dfee0","impliedFormat":99},{"version":"35ea0a1e995aef5ae19b1553548a793c76eb31bdf7fef30bc74656660c3a09c3","impliedFormat":99},{"version":"56f4ae4e34cbff1e4158ccada4feea68a357bae86adb3bedaa65260d0af579df","impliedFormat":99},{"version":"6eebdacf8e85b2cf70ad7a2f43ead1f8acccfd214ab57ff1d989e9e35661015d","impliedFormat":99},{"version":"a4f90a12cbfac13b45d256697ce70a6b4227790ca2bf3898ffd2359c19eab4eb","impliedFormat":99},{"version":"4a6c2ac831cff2d8fa846dfb010ee5f7afce3f1b9bd294298ee54fdc555f1161","impliedFormat":99},{"version":"a8d491b4eb728dab387933a518d9e1f32d5c9d5a5225ff134d847b0c8cc9c8ce","impliedFormat":99},{"version":"668f628ae1f164dcf6ea8f334ea6a629d5d1a8e7a2754245720a8326ff7f1dc0","impliedFormat":99},{"version":"5105c00e1ae2c0a17c4061e552fa9ec8c74ec41f69359b8719cb88523781018e","impliedFormat":99},{"version":"d2c033af6f2ea426de4657177f0e548ee5bed6756c618a8b3b296c424e542388","impliedFormat":99},{"version":"2d4530d6228c27906cb4351f0b6af52ff761a7fab728622c5f67e946f55f7f00","impliedFormat":99},{"version":"ec359d001e98bf56b0e06b4882bd1421fd088d4d181dff3138f52175c0582a51","impliedFormat":99},{"version":"45be28de10e6f91aacb29fbd2955ba65a0fd3d1b5fddefece9c381043e91e68d","impliedFormat":99},{"version":"77dabe31d44c48782c529d5c9acddc41f799bf9b424b259596131efc77355478","impliedFormat":99},{"version":"6801ebe0b7ab3b24832bc352e939302f481496b5d90b3bc128c00823990d7c7d","impliedFormat":99},{"version":"0abb1feddc76a0283c7e8e8910c28b366612a71f8bfdd5ca42271d7ad96e50b2","impliedFormat":99},{"version":"ac56b2f316b70d6a727fdbbcfa8d124bcd1798c293487acb2b27a43b5c886bb0","impliedFormat":99},{"version":"d849376baf73ec0b17ffd29de702a2fdbbe0c0390ec91bebf12b6732bf430d29","impliedFormat":99},{"version":"40dcd290c10cc7b04a55f7ee5c76f77250f48022cea1624eba2c0589753993b4","impliedFormat":99},{"version":"0f9c9f7d13a5cf1c63eb56318b6ae4dfa2accef1122b2e88b5ed1c22a4f24e3b","impliedFormat":99},{"version":"9c4178832d47d29c9af3b1377c6b019f7813828887b80bb96777393f700eb260","impliedFormat":99},{"version":"dddb8672a0a6d0e51958d539beb906669a0f1d3be87425aaa0ae3141a9ad6402","impliedFormat":99},{"version":"6b514d5159d0d189675a1d5a707ba068a6da6bc097afb2828aae0c98d8b32f08","impliedFormat":99},{"version":"39d7dbcfec85393fedc8c7cf62ee93f7e97c67605279492b085723b54ccaca8e","impliedFormat":99},{"version":"81882f1fa8d1e43debb7fa1c71f50aa14b81de8c94a7a75db803bb714a9d4e27","impliedFormat":99},{"version":"c727a1218e119f1549b56dd0057e721d67cfa456c060174bac8a5594d95cdb2d","impliedFormat":99},{"version":"bca335fd821572e3f8f1522f6c3999b0bc1fe3782b4d443c317df57c925543ed","impliedFormat":99},{"version":"73332a05f142e33969f9a9b4fb9c12b08b57f09ada25eb3bb94194ca035dc83d","impliedFormat":99},{"version":"c366621e6a8febe9bbca8c26275a1272d99a45440156ca11c860df7aa9d97e6d","impliedFormat":99},{"version":"d9397a54c21d12091a2c9f1d6e40d23baa327ae0b5989462a7a4c6e88e360781","impliedFormat":99},{"version":"dc0e2f7f4d1f850eb20e226de8e751d29d35254b36aa34412509e74d79348b75","impliedFormat":99},{"version":"af3102f6aec26d237c750decefdc7a37d167226bb1f90af80e1e900ceb197659","impliedFormat":99},{"version":"dea1773a15722931fbfe48c14a2a1e1ad4b06a9d9f315b6323ee112c0522c814","impliedFormat":99},{"version":"b26e3175cf5cee8367964e73647d215d1bf38be594ac5362a096c611c0e2eea8","impliedFormat":99},{"version":"4280093ace6386de2a0d941b04cff77dda252f59a0c08282bd3d41ccc79f1a50","impliedFormat":99},{"version":"fe17427083904947a4125a325d5e2afa3a3d34adaedf6630170886a74803f4a2","impliedFormat":99},{"version":"0246f9f332b3c3171dcdd10edafab6eccb918c04b2509a74e251f82e8d423fb7","impliedFormat":99},{"version":"f6ef33c2ff6bbdf1654609a6ca52e74600d16d933fda1893f969fc922160d4d7","impliedFormat":99},{"version":"1abd22816a0d992fd33b3465bf17a5c8066bf13a8c6ca4fc0cd28884b495762d","impliedFormat":99},{"version":"82032a08169ea01cf01aa5fd3f7a02f1f417697df5e42fc27d811d23450bc28d","impliedFormat":99},{"version":"9c8cbd1871126e98602502444cffb28997e6aa9fbc62d85a844d9fd142e9ae1b","impliedFormat":99},{"version":"b0e20abc4a73df8f97b3f1223cc330e9ba3b2062db1908aa2a97754a792139ac","impliedFormat":99},{"version":"bc1f2428d738ab789339030078adf313100471c37d8d69f6cf512a5715333afc","impliedFormat":99},{"version":"dc500c6a23c9432849c82478bdab762fa7bdf9245298c2279a7063dd05ae9f9a","impliedFormat":99},{"version":"cd1b6a2503fc554dcab602e053565c4696e4262b641b897664d840a61f519229","impliedFormat":99},{"version":"af1580cd202df0e33fc592fe1d75d720c15930a4127a87633542b33811316724","impliedFormat":99},{"version":"538608f9242fbf4260d694f19c95b454f855152ab3b882ac72114f19b08984d2","impliedFormat":99},{"version":"cd0e1083bd8ae52661544329c311836abdda5d5dda89fc5d7ab038956c0394e8","impliedFormat":99},{"version":"9ea6fea875302b2bb3976f7431680affc45a4319499d057ce12be04e4f487bf9","impliedFormat":99},{"version":"66e0c3f9875da7be383d0c78c8b8940b6ebae3c6a0fbfd7e77698b5e8ade3b05","impliedFormat":99},{"version":"da38d326fe6a72491cad23ea22c4c94dfc244363b6a3ec8a03b5ad5f4ee6337b","impliedFormat":99},{"version":"da587bf084b08ea4e36a134ec5fb19ae71a0f32ec3ec2a22158029cb2b671e28","impliedFormat":99},{"version":"517a31c520e08c51cfe6d372bc0f5a6bf7bd6287b670bcaa180a1e05c6d4c4da","impliedFormat":99},{"version":"0263d94b7d33716a01d3e3a348b56c2c59e6d897d89b4210bdbf27311127223c","impliedFormat":99},{"version":"d0120e583750834bf1951c8b9936781a98426fe8d3ad3d951f96e12f43090469","impliedFormat":99},{"version":"a2e6a99c0fb4257e9301d592da0834a2cb321b9b1e0a81498424036109295f8b","impliedFormat":99},{"version":"c6b5ae9f99f1fccadc23d56307a28c8490c48e687678f2cafa006b3b9b8e73e4","impliedFormat":99},{"version":"eae178ee8d7292bcd23be2b773dda60b055bc008a0ddce2acc1bfe30cc36cf04","impliedFormat":99},{"version":"e0b5f197fb47b39a4689ad356b8488e335bbf399b283492c0ffae0cfda88837b","impliedFormat":99},{"version":"adb7aa4b8d8b423d0d7e78b6a8affb88c3a32a98e21cd54fcafd570ad8588d0c","impliedFormat":99},{"version":"643e22362c15304f344868ec0e7c0b4a1bc2b56c8b81d5b9f0ee0a6f3c690fff","impliedFormat":99},{"version":"f89e713e33bfcc7cc1d505a1e76f260b7aae72f8ba83f800ab47b5db2fed8653","impliedFormat":99},{"version":"4e095c719ab15aa641872ab286d8be229562c4b3dc4eec79888bc4e8e0426ed8","impliedFormat":99},{"version":"6022afc443d2fe0af44f2f5912a0bdd7d17e32fd1d49e6c5694cbc2c0fa11a8f","impliedFormat":99},{"version":"6dd3f823ac463041d89c84d7bbf74931a38d874a9716040492ac7a16c7d2f023","impliedFormat":99},{"version":"a5bf6d947ce6a4f1935e692c376058493dbfbd9f69d9b60bbaf43fd5d22c324b","impliedFormat":99},{"version":"4927ef881b202105603e8416d63f317a1f1ea47d321e32826b9b20a44caa55e2","impliedFormat":99},{"version":"914d11655546eba92ac24d73e6efdb350738bcf4a9a161a2b96e904bad4de809","impliedFormat":99},{"version":"f9fdd2efc37eefc321338d39b5bd341b2aa82292b72610cb900f205f6803ff66","impliedFormat":99},{"version":"687208233ae7a969baa2d0c565c9f24eb4cb1e64d6cfb30f71afec9e929e58c2","impliedFormat":99},{"version":"62e7bd567baa5bac0771297f45c78365918fb7ba7adba64013b32faa645e5d6d","impliedFormat":99},{"version":"3fb3501967b0f0224023736d0ad41419482b88a69122e5cb46a50ae5635adb6a","impliedFormat":99},{"version":"06d66a6723085295f3f0ecd254a674478c4dba80e7b01c23a9693a586682252f","impliedFormat":99},{"version":"cc411cd97607f993efb008c8b8a67207e50fdd927b7e33657e8e332c2326c9f3","impliedFormat":99},{"version":"b144c6cdf6525af185cd417dc85fd680a386f0840d7135932a8b6839fdee4da6","impliedFormat":99},{"version":"e8dfa804c81c6b3e3dc411ea7cea81adf192fe20b7c6db21bf5574255f1c9c0e","impliedFormat":99},{"version":"572ee8f367fe4068ccb83f44028ddb124c93e3b2dcc20d65e27544d77a0b84d3","impliedFormat":99},{"version":"7d604c1d876ef8b7fec441cf799296fd0d8f66844cf2232d82cf36eb2ddff8fe","impliedFormat":99},{"version":"7b86b536d3e8ca578f8fbc7e48500f89510925aeda67ed82d5b5a3213baf5685","impliedFormat":99},{"version":"861596a3b58ade9e9733374bd6b45e5833b8b80fd2eb9fe504368fc8f73ae257","impliedFormat":99},{"version":"a3da7cf20826f3344ad9a8a56da040186a1531cace94e2788a2db795f277df94","impliedFormat":99},{"version":"900a9da363740d29e4df6298e09fad18ae01771d4639b4024aa73841c6a725da","impliedFormat":99},{"version":"442f6a9e83bb7d79ff61877dc5f221eea37f1d8609d8848dfbc6228ebc7a8e90","impliedFormat":99},{"version":"4e979a85e80e332414f45089ff02f396683c0b5919598032a491eb7b981fedfd","impliedFormat":99},{"version":"6d3496cac1c65b8a645ecbb3e45ec678dd4d39ce360eecbcb6806a33e3d9a7ae","impliedFormat":99},{"version":"9909129eb7301f470e49bbf19f62a6e7dcdfe9c39fdc3f5030fd1578565c1d28","impliedFormat":99},{"version":"7ee8d0a327359e4b13421db97c77a3264e76474d4ee7d1b1ca303a736060dbc6","impliedFormat":99},{"version":"7e4fc245cc369ba9c1a39df427563e008b8bfe5bf73c6c3f5d3a928d926a8708","impliedFormat":99},{"version":"3aa7c4c9a6a658802099fb7f72495b9ba80d8203b2a35c4669ddfcbbe4ff402b","impliedFormat":99},{"version":"d39330cb139d83d5fa5071995bb615ea48aa093018646d4985acd3c04b4e443d","impliedFormat":99},{"version":"663800dc36a836040573a5bb161d044da01e1eaf827ccc71a40721c532125a80","impliedFormat":99},{"version":"f28691d933673efd0f69ac7eae66dea47f44d8aa29ec3f9e8b3210f3337d34df","impliedFormat":99},{"version":"ae89fb16575dc616df3ff907c6338d94cfa731881ecef82155b21ab4134b3826","impliedFormat":99},{"version":"687208233ae7a969baa2d0c565c9f24eb4cb1e64d6cfb30f71afec9e929e58c2","impliedFormat":99},{"version":"f716500cce26a598e550ac0908723b9c452e0929738c55a3c7fe3c348416c3d0","impliedFormat":99},{"version":"6b7c511d20403a5a1e3f5099056bc55973479960ceff56c066ff0dd14174c53c","impliedFormat":99},{"version":"48b83bd0962dac0e99040e91a49f794d341c7271e1744d84e1077e43ecda9e04","impliedFormat":99},{"version":"b8fd98862aa6e7ea8fe0663309f15b15f54add29d610e70d62cbccff39ea5065","impliedFormat":99},{"version":"ffa53626a9de934a9447b4152579a54a61b2ea103dbbf02b0f65519bfef98cdd","impliedFormat":99},{"version":"d171a70a6e5ff6700fa3e2f0569a15b12401ad9bc5f4d650f8b844f7f20ef977","impliedFormat":99},{"version":"b6e9b15869788861fff21ec7f371bda9a2e1a1b15040cc005db4d2e792ece5ca","impliedFormat":99},{"version":"22c844fbe7c52ee4e27da1e33993c3bbb60f378fa27bb8348f32841baecb9086","impliedFormat":99},{"version":"dee6934166088b55fe84eae24de63d2e7aae9bfe918dfe635b252f682ceca95a","impliedFormat":99},{"version":"c39b9c4f5cc37a8ed51bef12075f5d023135e11a9b215739fd0dd87ee8da804a","impliedFormat":99},{"version":"db027bc9edef650cff3cbe542959f0d4ef8532073308c04a5217af25fc4f5860","impliedFormat":99},{"version":"a4e026fe4d88d36f577fbd38a390bd846a698206b6d0ca669a70c226e444af1b","impliedFormat":99},{"version":"b5a0d4f7a2d54acbe0d05f4d9f5c9efaaeddc06c3ee0ca0c66aba037e1dca34b","impliedFormat":99},{"version":"fa910f88f55844718a277ee9519206abce66629de2692676c3e2ad1c9278bdfd","impliedFormat":99},{"version":"a886a5af337cce28fe3e956fd0ed921345933163f5b14f739266ba9400b92484","impliedFormat":99},{"version":"9ae87bd743e93b6384efbfa306bde1fa70b6ff27533983e1e1fe08a4ef7037b8","impliedFormat":99},{"version":"5f7c0a4aad7a3406db65d674a5de9e36e0d08773f638b0f49d70e441de7127c0","impliedFormat":99},{"version":"29062edaa0d16f06627831f95681877b49c576c0a439ccd1a2f2a8173774d6b2","impliedFormat":99},{"version":"49fcfda71ea42a9475b530479a547f93d4e88c2deb0c713845243f5c08af8d76","impliedFormat":99},{"version":"6d640d840f53fb5f1613829a7627096717b9b0d98356fb86bb771b6168299e2e","impliedFormat":99},{"version":"07603bb68d27ff41499e4ed871cde4f6b4bb519c389dcf25d7f0256dfaa56554","impliedFormat":99},{"version":"6bd4aa523d61e94da44cee0ee0f3b6c8d5f1a91ef0bd9e8a8cf14530b0a1a6df","impliedFormat":99},{"version":"e3ee1b2216275817b78d5ae0a448410089bc1bd2ed05951eb1958b66affbdec0","impliedFormat":99},{"version":"17da8f27c18a2a07c1a48feb81887cb69dacc0af77c3257217016dacf9202151","impliedFormat":99},{"version":"8395cc6350a8233a4da1c471bdac6b63d5ed0a0605da9f1e0c50818212145b5b","impliedFormat":99},{"version":"b58dda762d6bd8608d50e1a9cc4b4a1663a9d4aa50a9476d592a6ecdc6194af4","impliedFormat":99},{"version":"bc14cb4f3868dab2a0293f54a8fe10aa23c0428f37aece586270e35631dd6b67","impliedFormat":99},{"version":"946e34a494ec3237c2e2a3cb4320f5d678936845c0acf680b6358acf5ecc7a34","impliedFormat":99},{"version":"b85aa9cc05b0c2d32bec9a10c8329138d9297e3ab76d4dd321d6e08b767b33ed","impliedFormat":99},{"version":"b478cef88033c3b939a6b8a9076af57fc7030e7fd957557f82f2f57eddfc2b51","impliedFormat":99},{"version":"2fac70f99da22181acfda399eed248b47395a8eeb33c9c82d75ca966aee58912","impliedFormat":99},"395ce4e40890d4e3f8962c1ea63805e7873e2b8b5fd6da88598b7bc131d8a9db","aafd57b7adcfe2752a4bda91963caf71cea0e5ecd4553826424573c70ac27d65","e8537bebb5d7b02544de702a182190f88a3ae36fd615850f93c0b42a09426201",{"version":"6b00143abdd09f7e789809c4dd737aa8032da306a52b63b180abef8c0278c92a","impliedFormat":99},"e5dcd7390ee46d48d088acc275c3398b5e232ca5096f116718f7946e5da01bcf","c3a83167fc568331b4ad75852bfcae4f90e50c2e486d19693b46e0d2063ce407",{"version":"d8e02aa2838623274cd72e3e379f931ce28af86202d05cc4310f4a9d03408c61","impliedFormat":99},{"version":"4d1a9c5960c005d2fe9cf5f5a6bc70850149ea0454666f133595a6acd6b593e3","impliedFormat":99},{"version":"8439d349f53c599dae7d7b9df3382688b1cf167e6da1fc6cfa3aef4c54a3b0b6","impliedFormat":99},{"version":"16f883251db6af07b8314899c735b2aaf723bd0596828ac2b521b9a91ea7686a","impliedFormat":99},{"version":"a47000de38fcb07c48b9dfd1a22b85be24c422ec1448a353b625eb71b87e8543","signature":"010c7c70c42e3ae1797dd61ea025f3dc9d0470f423be2e57583c6f2b41dbd54a"},{"version":"7ecbe0e0b134c9dfb6a5cf6464eafa62218768df02491a84cf8bec3eb2aedf55","signature":"a3df50373eaab5122c1384f66eac08aedf5505f13be691f57565e2dc852a3ea0"},{"version":"02f2ff84b21ee762b48bb33d33731c6b5fb1a16c21797c8c0b4e668af4ea3d07","impliedFormat":1},{"version":"a45ee7555d019a67fbe092898d1aef0b1d02a9f6679ab84461ff515b4460d706","impliedFormat":99},{"version":"bcf5edfb2b188348d16a7dd26c83a79856a7e81a7800051109d99aa211ef6947","impliedFormat":99},{"version":"6d870cbc76c9654c649c58dd664a875bab499e3e746b34ff2a6b6d363a3e0686","impliedFormat":99},{"version":"c1da3f540f39091e02624208125e7d825fe6839d153399c21315725acf0adbe1","impliedFormat":1},{"version":"91648eebacde37f34eace96a9d3f269875ae85331e70a4ffb89e76f0382338ae","impliedFormat":1},{"version":"6b564cbbc03305790e97ec6cdb2da7d0231880e5d1a44974b098857e391ebbeb","signature":"33062a0c02d85c654071b656a3b178315a6b9640f71281be8a283acbf3914be3"},{"version":"f09971f9fed038b945b9d327c4f828c34014873bd0119c70342d1c5b38b7bc3c","signature":"05407b20be3c021be8d811f8e863915b2467d4ba691a9f4d17c30c30d7d079a5"},{"version":"6d81c7ae02ce31a75fa7952145a8b0df497832fd1771718985010b909252c8dd","signature":"23887314798a44611ab91e96f4f31037459b91780ce9806e9d336b21e64dffbc"},{"version":"e1be4a2a139d2a99071ff9c81d7c3401a04612b032c16b4212a66e254c784217","impliedFormat":99},{"version":"1b1d7e2eb25adf80e3e15f68121ce2100d33ab8cf1b07a362239d3b038db8b1a","signature":"193f3698143959625b77bab03ef64fae1876dda0ed7438f9da236b49a3b9b826"},{"version":"40eb4eb274d7def754b3352c23328af96a69261fe5e99d20df41f1d5de8d0f5e","impliedFormat":1},{"version":"4d7d964609a07368d076ce943b07106c5ebee8138c307d3273ba1cf3a0c3c751","impliedFormat":99},{"version":"0e48c1354203ba2ca366b62a0f22fec9e10c251d9d6420c6d435da1d079e6126","impliedFormat":99},{"version":"0662a451f0584bb3026340c3661c3a89774182976cd373eca502a1d3b5c7b580","impliedFormat":99},{"version":"30de07ba48a9bd1c4f63375c48a33b8fd188b365c08517f515eb4d2e1b7861fd","signature":"f5636abac9c3d7d7c8eba0a0692c305faf9e605dc7cd2e735c1392374cf90ef0"},{"version":"4d06375aa6c40c68ac15e1fafda889fa5b121feb5d4ed646dda97d04aecade0d","signature":"ccfb7c908454912967cef19aaf9626a963858df89d6d8a56b299d749ca2dcbf3"},{"version":"95e963837647fb1d0623e5c830d1ecbce34845adc7083d444c0431b0dc175d2d","signature":"d0d0b3d9e3ac3534f4e3033589f51ec1ca8505d382dca6b15779c105c7bf9ad2"},{"version":"55ed31f8e15f2428b48001209cc07764197d5f628d721d89c971ac4007c16fca","signature":"2c97adf60d0f9ecae3a2a82409ed1e0660385b86d79e714e587e0c079c48183c"},{"version":"58f868665ea08a5cb9e8d7216f0d4559b2ce6c4408da6247563286c968f712a0","signature":"143a1d7899f75c792cae42a083be5af66b106984143469de4b89c4e675f7b078","affectsGlobalScope":true},{"version":"9bd535b0e6913321066c70cb9f06bce3fa4c0245db5db11ff711f42d11f29de5","signature":"5e8b289c58d0ef781c5fb54c1aa4b9c3f4fb99eb3c5660715eddafd0f43d47cf"},{"version":"50056a12f1e3ca57666e2bb2540daa2c47cf477d456f920c8ca03986d9d49040","signature":"a575347f08bceafc67ec61295040ebd6d8a8f697126fdb88ccd620ec1824bc08"},{"version":"11fc7d6659cdee67c183ba7f62f311de94f4fec188949e08137f696763bf0ed6","signature":"05acacbe7d71108c4df7033b828ae1f4c2a268a910783ed46ca33f88d6c4d341"},{"version":"409782511002849bfc73e5a2096e1efa3024f220747806fc246c109b33e6c3be","signature":"99f354346eca19d79c063f1b80854e527509fa55557567923b1d3025ace35518"},{"version":"8f77b3b50ce80617b3b9cff5006fa646207e557320040549e3d5ffac21c3b98e","signature":"b96e5dae99a39290a14aaf36662e2650a6de601814c814f87179e709e5ab4519"},{"version":"e6b8f3cd057e49a50b57a52acc38cff7c224def2249464d489295e0e1d200af6","impliedFormat":1},{"version":"c57b441e0c0a9cbdfa7d850dae1f8a387d6f81cbffbc3cd0465d530084c2417d","impliedFormat":99},{"version":"26c57c9f839e6d2048d6c25e81f805ba0ca32a28fd4d824399fd5456c9b0575b","impliedFormat":1},{"version":"c0516f0939c27dd46f3017e882cfd93dbb9610160ee814c0be0bb87101a0142b","signature":"98204207446a53ecdae733dd448ee017b69571cb0c692c465bb133131829b17d"},{"version":"242db9526d1b493b54d3536d21dea19dbd1b8fd567745a006c6cea422e25a46d","signature":"f4f134f61ae8caf9b22ac22b92fb882d25a8c5f0c2c375364eec6c2aa3fff070"},{"version":"077de269ff65c82e00d342179cbf870cb87fb65bb8573bf80a12d03a9f2c9738","signature":"1072feaf2e85b97d0b6b95fd7fc259fa90294bb0a7069df31d6a67c6ec5363b5"},{"version":"1913c2669d6340305255ce8955acd5459bed6373c11cbc2313913fbcce023b1b","signature":"4b686dd1ea719cbcb2e6391f8774ddd4dd3caff990f098b39895d0a84d741163"},{"version":"145c1fbdb342584544a7f593db746ff786545cd72b61c4dad039d293969b0de9","signature":"221d742fa2ba723b0d1ab360281f631ce49c3ea01f25195692de202ad778bf0c"},{"version":"f124d58f4b2d522ee67481419a13c88485cb60b554e053bdc867849f7fdd023e","signature":"392e2ddb1185a3f47d60d39a600369113fcde4effdc60e0ec4d3bd75dbe8dc3f"},{"version":"ea3455b6bd44c3bdfe0f53006dce50191d723c5636106ea1cd49d62d5900a25a","signature":"6f7ee1c734d1193633694dca1efd7679b9454861b6a332f4331a9e02fec14260"},{"version":"b829c76711471c8a451826a57a51ee361a1214f72fcc74f51bb4df934e213796","signature":"355893155b6e80bb1d801e19ecf8b00c1c837681b0876f0571afb9a12325e43f"},{"version":"58572d98acce945af2e29640c2d9e7bce81e2647c9a54ba5bebca6ac761c9349","signature":"ae3cb34f763ca6c87eaf9d2ef7cca0c655e8719830f0758dd109a005c4574273"},{"version":"159a7cf0c4bfc1db15478a444e01f1a71770f163e22ceaad7725430b4a89d320","signature":"021f5232c6738ab07d1f3eea74394f0f3c234ce0ad9cacd21383ba2a58f3733b"},{"version":"10ab128000fa593aee1859f90ba6e31b5c69736db6d0f7b1989333bc1c97be15","signature":"d86b1eb5fa068b9820f6427ee57e2fa2967441fdcfd25bbe9dc86845f6d46d9d"},{"version":"02a4e4525185017f7d83f4671bb1655ae80e5268028c884188434c82af92cc7a","signature":"7abd853e12728070e39dc3bd24038eee0ba70bf479a5771f22b409229ce6b3b7"},{"version":"44ef3e27c2c4fef5a739dfc9152250fc8df127164ebecd060784b6453005c1c1","signature":"128b66558240654e7d7c9e15bc920647eaa13c9bc7802637ba68e1236bf7afd8"},{"version":"68219da40672405b0632a0a544d1319b5bfe3fa0401f1283d4c9854b0cc114ce","impliedFormat":99},{"version":"ee40ce45ec7c5888f0c1042abc595649d08f51e509af2c78c77403f1db75482a","impliedFormat":99},{"version":"7841bca23a8296afd82fd036fc8d3b1fed3c1e0c82ee614254693ccd47e916fc","impliedFormat":99},{"version":"b09c433ed46538d0dc7e40f49a9bf532712221219761a0f389e60349c59b3932","impliedFormat":99},{"version":"06360da67958e51b36f6f2545214dca3f1bf61c9aef6e451294fcc9aca230690","impliedFormat":99},{"version":"50bc55fc955fa799a4a1a11c3484077f06cc87033c68f5da6466b520876cb12f","impliedFormat":99},{"version":"e8f33af6bdca5a3b90e928516f39c146529f1dc03bf201b96892de3e6bf019ba","signature":"2e2552723dd9f7d873fcaa75356a9d8da98080cc604965911a327afe2247a526"},{"version":"ad6f944b4f5efe49d373f87acff2dd633c9aa7b663d1c386f36bde3a0ac0ccde","signature":"2fa04aa89cae9e10bd7f680b16bbaab666eeeaae586172c6a27adcd4feef295f"},{"version":"a415389d5df5b304b38bc568d50b07d4e77d2ab0ad3767e3378c704ebfbe5ae6","signature":"df955871fdf5f4c124a0cbbf1c7d1c5e1391e6928ce2af9c88412dc540c8bf7f"},{"version":"0ba6f829d5a1cf7a46674ae05fc7130855386400fbddb77f76e62d815926d309","signature":"963c2a3b2e53abed1302ff62aec14bce2a3f9784060bd3180de5c70d5145d141"},{"version":"25360f7a64361bc5a1dde6b291c26dd95822c60e384a309bb3984f09da8b5f98","signature":"46939687b5f5bd1430dd08201b4ffa481cb222161194a12163a66496ae6c86e4"},{"version":"509890cefcd19cc64b79206a3a51b1f3c310348c85f1a61cef729d9fd5d4ce77","signature":"db3f8acd21ab5bc2f4f89bcc187d16e6ecae27c289a72782ffef26f960b62bbb"},{"version":"964e2b5cd04a33bcf0170236afc5d91b46168e189da652c514e4f2b101eb9d4b","signature":"4f7592a75cfd01c6c26c5f58f40f4205a917a46b7836cc922cf022a0a1c7e335"},{"version":"6cf21ac3be69759bbdce20e3bdbf06e2b0f4a3735c2250b6cc51afa56167289f","signature":"99b15666c46eca52d2529df2a25868a5a9db4a2811b15e816d77552659c2225c"},{"version":"870fddc39056507c0a148cbe63646482bd919a442c6767e122182071254de9dc","signature":"759f60150aa79543562b0a73ff30754049f50f23c1e1384e357bf69cee6939fd"},{"version":"0e4a4c43584b8bc8f243c6e164be95c4b1ca51a366b1190cc364e931026382c4","signature":"d3fe80f9537bc18bbdcc6f860f81c1c7fd9c2573bb76ce75fdd1aee61ebad374"},{"version":"e589332f05d669ebe74bc4074bbed569546506992112c9b132c745130426cf69","signature":"6cac45b0e556cbce5690f6063a6f9be26da1a84161a82dccdb58053d50c7f23b"},{"version":"024829c0b317972acf4f871bf701525f81896ad74015f1a52d46ae6036205cb9","impliedFormat":99},{"version":"a9373d52584b48809ffd61d74f5b3dfd127da846e3c4ee3c415560386df3994b","impliedFormat":99},{"version":"caf4af98bf464ad3e10c46cf7d340556f89197aab0f87f032c7b84eb8ddb24d9","impliedFormat":99},{"version":"0943a6e4e026d0de8a4969ee975a7283e0627bf41aa4635d8502f6f24365ac9b","impliedFormat":99},{"version":"1461efc4aefd3e999244f238f59c9b9753a7e3dfede923ebe2b4a11d6e13a0d0","impliedFormat":99},{"version":"7ec047b73f621c526468517fea779fec2007dd05baa880989def59126c98ef79","impliedFormat":99},{"version":"8dd450de6d756cee0761f277c6dc58b0b5a66b8c274b980949318b8cad26d712","impliedFormat":99},{"version":"904d6ad970b6bd825449480488a73d9b98432357ab38cf8d31ffd651ae376ff5","impliedFormat":99},{"version":"dfcf16e716338e9fe8cf790ac7756f61c85b83b699861df970661e97bf482692","impliedFormat":99},{"version":"31c30cc54e8c3da37c8e2e40e5658471f65915df22d348990d1601901e8c9ff3","impliedFormat":99},{"version":"36d8011f1437aecf0e6e88677d933e4fb3403557f086f4ac00c5a4cb6d028ac2","impliedFormat":99},{"version":"8085954ba165e611c6230596078063627f3656fed3fb68ad1e36a414c4d7599a","impliedFormat":99},{"version":"2c57db2bf2dbd9e8ef4853be7257d62a1cb72845f7b976bb4ee827d362675f96","impliedFormat":99},{"version":"6b5f886fe41e2e767168e491fe6048398ed6439d44e006d9f51cc31265f08978","impliedFormat":99},{"version":"56a87e37f91f5625eb7d5f8394904f3f1e2a90fb08f347161dc94f1ae586bdd0","impliedFormat":99},{"version":"6b863463764ae572b9ada405bf77aac37b5e5089a3ab420d0862e4471051393b","impliedFormat":99},{"version":"68b6a7501a56babd7bcd840e0d638ee7ec582f1e70b3c36ebf32e5e5836913c8","impliedFormat":99},{"version":"89783bd45ab35df55203b522f8271500189c3526976af533a599a86caaf31362","impliedFormat":99},{"version":"6da2e0928bdab05861abc4e4abebea0c7cf0b67e25374ba35a94df2269563dd8","impliedFormat":99},{"version":"e7b00bec016013bcde74268d837a8b57173951add2b23c8fd12ffe57f204d88f","impliedFormat":99},{"version":"26e6c521a290630ea31f0205a46a87cab35faac96e2b30606f37bae7bcda4f9d","impliedFormat":99},{"version":"71acd198e19fa38447a3cbc5c33f2f5a719d933fccf314aaff0e8b0593271324","impliedFormat":99},{"version":"044047026c70439867589d8596ffe417b56158a1f054034f590166dd793b676b","impliedFormat":99},{"version":"89ad9a4e8044299f356f38879a1c2176bc60c997519b442c92cc5a70b731a360","impliedFormat":99},{"version":"fd4f58cd6b5fc8ce8af0d04bfef5142f15c4bafaac9a9899c6daa056f10bb517","impliedFormat":99},{"version":"2a00cea77767cb26393ee6f972fd32941249a0d65b246bfcb20a780a2b919a21","impliedFormat":99},{"version":"440cb5b34e06fabe3dcb13a3f77b98d771bf696857c8e97ce170b4f345f8a26b","impliedFormat":99},{"version":"5bc7f0946c94e23765bd1b8f62dc3ab65d7716285ca7cf45609f57777ddb436f","impliedFormat":99},{"version":"7d5a5e603a68faea3d978630a84cacad7668f11e14164c4dd10224fa1e210f56","impliedFormat":99},{"version":"2535fc1a5fe64892783ff8f61321b181c24f824e688a4a05ae738da33466605b","impliedFormat":99},{"version":"cbfd5ef0c8fdb4983202252b5f5758a579f4500edc3b9ad413da60cffb5c3564","impliedFormat":99},{"version":"9f7a3c434912fd3feb87af4aabdf0d1b614152ecb5e7b2aa1fff3429879cdd51","impliedFormat":99},{"version":"99d1a601593495371e798da1850b52877bf63d0678f15722d5f048e404f002e4","impliedFormat":99},{"version":"1179ef8174e0e4a09d35576199df04803b1db17c0fb35b9326442884bc0b0cce","impliedFormat":99},{"version":"9c580c6eae94f8c9a38373566e59d5c3282dc194aa266b23a50686fe10560159","impliedFormat":99},{"version":"cc3738ba01d9af5ba1206a313896837ff8779791afcd9869e582783550f17f38","impliedFormat":99},{"version":"a80ec72f5e178862476deaeed532c305bdfcd3627014ae7ac2901356d794fc93","impliedFormat":99},{"version":"4a5aa16151dbec524bb043a5cbce2c3fec75957d175475c115a953aca53999a9","impliedFormat":99},{"version":"7a14bf21ae8a29d64c42173c08f026928daf418bed1b97b37ac4bb2aa197b89b","impliedFormat":99},{"version":"c5013d60cbff572255ccc87c314c39e198c8cc6c5aa7855db7a21b79e06a510f","impliedFormat":99},{"version":"69ec8d900cfec3d40e50490fedbbea5c1b49d32c38adbc236e73a3b8978c0b11","impliedFormat":99},{"version":"7fd629484ba6772b686885b443914655089246f75a13dd685845d0abae337671","impliedFormat":99},{"version":"13dcccb62e8537329ac0448f088ab16fe5b0bbed71e56906d28d202072759804","impliedFormat":99},{"version":"233267a4a036c64aee95f66a0d31e3e0ef048cccc57dd66f9cf87582b38691e4","impliedFormat":99},{"version":"ccb9fbe369885d02cf6c2b2948fb5060451565d37b04356bbe753807f98e0682","impliedFormat":99},{"version":"2fbe402f0ee5aa8ab55367f88030f79d46211c0a0f342becaa9f648bf8534e9d","impliedFormat":1},{"version":"b94258ef37e67474ac5522e9c519489a55dcb3d4a8f645e335fc68ea2215fe88","impliedFormat":1},{"version":"be963a5bd56aeddfc57616466c1f8af426378d908d9e475f334b30471a4ba884","signature":"da7dfbcc3ba76a065601622916971b178c2242e4f9c3484ece58aeecaac92104"},{"version":"d397a673d640a178fb574fe8ac035fb57f8f428a0a69160f284c9b7bb6f4f29e","signature":"1b427e8590c534e0c3830d04f5c4afecc99c593854076e7176f86ad500fd36c8"},{"version":"0f160143e59439d507e29bbf239aa377d69e69779a3fd54b43e8b7dc44d179f1","signature":"29b6366c63d9d8fa31e31204b6f86084ad65d8c09e02b33371af51e923a1be60"},{"version":"f381a21f18ee7425b9658040042af9eabccee29291bfaf7dbb9010490ba39a05","signature":"cf16c98b79d1eccebf51f529c413c59ff5e63cbd6033913b5233dd5d145c728c"},{"version":"6aad7de0a0c87312d1564195b269cadf1c6d5000608305203d847f0128834797","signature":"e576932de1f726a32ad237ac3510636117f9d2778374e9b6e83c8899af36811a"},{"version":"b6f91d557f8532d654af6a59116bf7f7c22f3039ba71510243f85f99fd9c5371","impliedFormat":1},{"version":"e443acd5ebb476c19e1da0858570a9cbbd0077404b2dff4c078a4ffa448e090e","impliedFormat":1},{"version":"cad19572420f7ead17272082d9582ebaed7f2856e19875542f83daf8d25d3b5d","impliedFormat":1},{"version":"63f0ec970b9a648cf7f8307e9e63a5f24a62f48c1a51c22b5271a9f18a36e903","impliedFormat":99},{"version":"f0ff8ea5237bebecfed33eacbfeb6bf599a0c3cfbdbe06f5c383ca97856f05fd","impliedFormat":99},{"version":"9bf290d5ed3e90694fe0926327b604409a56819ae723878b9043809220e972df","signature":"9831330dee74d8956e6810ac43e4b67b26800d57c5caddd9c66f189d822a44db"},{"version":"fe93c474ab38ac02e30e3af073412b4f92b740152cf3a751fdaee8cbea982341","impliedFormat":1},{"version":"6dc25f6d56cfb757dd1fdf38eb6a2059b812a13fbd81ade8e0cbbd93295c5987","impliedFormat":1},{"version":"1e00b8bf9e3766c958218cd6144ffe08418286f89ff44ba5a2cc830c03dd22c7","impliedFormat":1},{"version":"98f6680f965241c89395ec9a6ecdf37da3bafa5cc8b7e459e27ffe0ee9700808","signature":"c6c870f3fec59f8c6a9ec8a7aef26c2ed1e1117b6dbd3036bdd0ca7eb21ee2c5"},{"version":"f6117aec06cf14616d3eeaca2a5d50bd51a8b1b7a3d9e73515ec65d0cd90d53b","signature":"ca6aa01423ec7733fb240701edae05fdcbe48cf2625c000e25aeae9546480fb6"},{"version":"132ed858100f582439be9b86768a49b36f00fd20aa4c9bb82888865b4e6f0aaa","signature":"0973b7d3c5292f37d39bc7463cafe4af537869e24ccfbe83ee135f78975da5c1"},{"version":"350d0bfdedcf66569293d164f4575cdb656c9c6060e7360bfdb5358b0e41bc82","signature":"5f57f63b2d6f259b6a0864dbce12ce42bcc7f4ea864828e433c6bc87d9ec356e"},{"version":"303c94943278564da916b28af0d25a2d05de2b98a81e7d5b32506d524b2439bc","signature":"cad33fd2be553879ffd526b822bb7d4a964f8b4fd913e8a6b54ee87e0e845907"},{"version":"6c05d0fcee91437571513c404e62396ee798ff37a2d8bef2104accdc79deb9c0","impliedFormat":1},{"version":"429aacaf3955390f06048784af81b187ef273825d1f290b9e35684b51346b11b","affectsGlobalScope":true,"impliedFormat":99},{"version":"9eefb21954cb036d002e68ec2f521b2c09ddc7889f6bf69b2e7cb406b0003a34","signature":"a1949f6531a858c6305c94f2a3910d5b6cf43e597b23deadaa7fb26737bc3b34"},{"version":"dbb685a20c75a2082aa1a4e65d1c78194a76e9fe750337c17a6cd959c3a739ef","signature":"c07a10db00013b762a62aea6c416c4f742163ec900708ed46cebfa4bebff25b0"},{"version":"321fd6a8249295720f911dcb179ac4a637f77581f909af7b8052f316e076cccb","signature":"14ccce935efffb51549ae9d32af0c472ff7350f70b62992a4cbc50ef03254f74"},{"version":"afb5c44616d15e04c44ac6ee0abdb32983c1134a5dd3827f1bc701d73fb6199b","affectsGlobalScope":true,"impliedFormat":1},{"version":"670cfaff6a8e574e577506a4ad591d7163dfc980be3723193351bd8613efc375","signature":"0d33653a6cdafbd7aa1c7f7260548e7600f0ee2baab2556587d31182ef8dcf80"},{"version":"37c7961117708394f64361ade31a41f96cef7f2a6606300821c72438dd4abda3","impliedFormat":1},{"version":"19feb8551285d9041090b2aac242de4089aa0abe3777a89ff9a804225e561832","affectsGlobalScope":true,"impliedFormat":1},{"version":"a4553f8b822eb81601e9ff242bde19eebc440201e687161598c267892ee84bdf","affectsGlobalScope":true,"impliedFormat":1},{"version":"724dea86a503f9bdf755f5c628cea9de4a63aafdf0f6ca25db5e0172ddc0d883","impliedFormat":1},{"version":"39875f62f78dfcd829f2d38f7b06e8a8d5e39bbb9487abe63b81b41fe3b58c04","impliedFormat":1},{"version":"01141b610aa76df368ea5a2d6ef07abd6df8c68c8f8a059970f1e348d7ac4c50","signature":"6181facfcc5ffd7d073c53067e9c466e672f3d3057d4c12865843f5eb9213e78"},{"version":"5bbdfeaca969e3c1dc0a0cf3982a3b26ad032bb2c9808418034c332521fa4aaa","signature":"402b4f97c618e041e8cbada612b7540ebddc75da8bfa1cb058ee02f34f3efffb"},{"version":"d4b1a8fd1bc758e4e3f0d19ec26524aae80f8048524a798c88e23ebc688af087","signature":"16a63b574277c84e41df88c14d57604ba7a3649092c3b73a549f4d338f6978d4"},{"version":"c013dae1f00eb17ecff4563fa6f9107b9524cfa67bcba0c6c98f765d0d8c8862","signature":"1817abd08ae5b9438b81564ca71539f3f3246c6a85e8a0617ac24665e6a36c75"},{"version":"9a7bc3d48f58a822862b0b16b5a1f2eac43fe9e4070be3115ef9ade60a754f52","signature":"dbcb8b0b6279a350f5700a8899a784d31cb51c13c86e06f020ac0a260f4aac7f"},{"version":"f47f7b8aa563656c5e464862e0622997062763052259352e1cde6e17320c6c66","signature":"00ca5c6fc28e39cebe2766c45f01446003017319a470fb23205247c98ef81c0f"},{"version":"36d744194260455270cad1437e8bb38cd8f35dd9fbd3aba365ce1c057b775f9c","signature":"b38a9e9add7938286ce894c1ec3e07c4950e6da7bd69e4294a21931b6e4cf9ba"},{"version":"d04f947114fa00a20ee3c3182bb2863c30869df93293cc673f200defadbd69d9","impliedFormat":1},{"version":"4c629a21fb1b4f2428660f662d5fef6282e359d369f9e5ec5fd6ac197c1906ee","impliedFormat":1},{"version":"785926dee839d0b3f5e479615d5653d77f6a9ef8aa4eea5bbdce2703c860b254","impliedFormat":1},{"version":"66d5c68894bb2975727cd550b53cd6f9d99f7cb77cb0cbecdd4af1c9332b01dd","impliedFormat":1},{"version":"cf62a7edc0f8e389ef165baf6d664cc20eb272de3e5ce056064031ffb0c452f0","impliedFormat":1},{"version":"c991b4ab8278b08f73510d383ae74a9df03a876ba4aa66efa9d87d0bfdbf486b","impliedFormat":1},{"version":"26bb422e69d1eafe073ea833266a49ad098e4b85d933d7aefacda852593ef2e3","signature":"ce646e3f553e635398f12c5b8dc8b218d6c55577c74bc266463414789ae07f09"},{"version":"ccdc3f0feae8f828b00cd502de1621de0bbe1dc8ea24605e7b9ffb085bdc9f7c","signature":"7a4dd9acc5705beff7d52365cf4c86a779ece8cee4388be28507384392ecb611"},{"version":"6fb0dd8d98c4374aff3c1812edd139a826c6cd7a6d0ac164dfe08677a374dc2f","signature":"8fc67ddd42c820c7ce9d330fd429fbf2da5362b5424f80c2a5f3244f5f50f6c5"},{"version":"4b74c810d1862fedeb177f7eea72b8c539a2f2718e914f1878fed4de4b75f1b0","signature":"506094e1dc2747f7e734e971e941585e40a8632184acf476b34964de270daa88"},{"version":"ea5fdc2c6e98adce3deb78d0c942ba22a29c8681b79f662035161dd8223665dd","signature":"74f7141a41f4fb646176486a9bda7ad6ce22202a2c46afcb42a691fdc4261c15"},{"version":"f6edfa96e639cb9f9cb6e937a45b12324ba0710c51d3b384396aa4d7b0d066e4","signature":"9e2b7a970acb37795476eb2e0e6ef9397c03a82abbba1e0bce24e23879279d0e"},{"version":"5565ddee7570101bdbfefba57c198911ec26ae14d4f644cf7d9a8e56e67f2841","signature":"5b2dcfc0066c04996c1976d24fb091cb11ccb7b28231965bd84fe649e2d72aad"},{"version":"554ccd913de67174a356eeef8b24bcca0f01a719d0c723dc228e5ec8fbbcb3b5","signature":"6688ab89eeb44744e19abbce57a209ff048b992ce8a5b45801380982eddb21e7"},{"version":"dbe452b12320ff810e634f8bda5a8fae141013335c67427b750239d7d97f7a10","signature":"6688ab89eeb44744e19abbce57a209ff048b992ce8a5b45801380982eddb21e7"},{"version":"3c8bc8615f3a97799855f21ebfa2e1ce01df9fb407775baa03a5224a043f5150","signature":"33e8cebeb0e9014cddffb1bd6967251edca2c0e6f4b4607ffd054f0f757eb3c6"},{"version":"92321e702c9b734303e91789a69638149c1a15369c39d62f9e7850f07dc0ad2e","signature":"24bf1c0c6ac8bba9b5205c5b930ff728990fb54d5d39601894c363d35a59cb7a"},{"version":"d9fc0c3d44c571f359144012e46313b40dadfde96cd077db4f038b756b301ebb","signature":"1e1156d9e5d5faabd983c66ef5c51164608035131f04d2629c59c8be4cd30959"},{"version":"b1ce2af9c99f6289e1658d499c0b54b39fc3baee0cb16d361637f81c0ccba8eb","signature":"e64f570502c4e9754d2c96a5c8590d43377445406631420385e9dbb67ba99985"},{"version":"4d3c0619a4b0b7ddfb2a75e79827ada039711b1dcc40f51f3630f3350ff146a5","signature":"a3ba39021620b6bf8cf656cedc23d193342a050b12bb650e1981b9418e502434"},{"version":"8f674ebede2aaaa082b5951af37e852a4f295921e606eb60b34d23e294172d6b","signature":"d23747eee8e0a39dd0f939b282d446749416ed25ebee387f3d2c59677357aac4"},{"version":"a81a0eea036dd60a2c2edc52466bb2853bef379c3b9de327fe9fff6e3c38e6c5","impliedFormat":1},{"version":"348c13a1c9160681e41bc5cd3cc519dd8170d38a36a30480b41849f60f5bf8a0","impliedFormat":1},{"version":"c772a37a02356897d6f9872e30fcc2108f43ad943cc112bd1acc5415a876e9f8","impliedFormat":1},{"version":"279248c34ecd223fc46224f86384ebf49c775eb69329ad644d3d99f1205f3e7d","impliedFormat":1},{"version":"74dedffc2d09627f5a4de02bbd7eedf634938c13c2cc4e92f0b4135573432783","impliedFormat":1},{"version":"1f2bbbe38d5e536607b385f04c3d2cbf1e678c5ded7e8c5871ad8ae91ef33c3d","impliedFormat":1},{"version":"3aa3513d5e13d028202e788d763f021d2d113bd673087b42a2606ab50345492d","impliedFormat":1},{"version":"f012173d64d0579875aa60405de21ad379af7971b93bf46bee23acc5fa2b76a4","impliedFormat":1},{"version":"dcf5dc3ce399d472929c170de58422b549130dd540531623c830aaaaf3dd5f93","impliedFormat":1},{"version":"ec35f1490510239b89c745c948007c5dd00a8dca0861a836dcf0db5360679a2d","impliedFormat":1},{"version":"32868e4ec9b6bd4b1d96d24611343404b3a0a37064a7ac514b1d66b48325a911","impliedFormat":1},{"version":"4bbea07f21ff84bf3ceeb218b5a8c367c6e0f08014d3fd09e457d2ffb2826b9c","impliedFormat":1},{"version":"873a07dbeb0f8a3018791d245c0cf10c3289c8f7162cdbbb4a5b9cf723136185","impliedFormat":1},{"version":"43839af7f24edbd4b4e42e861eb7c0d85d80ec497095bb5002c93b451e9fcf88","impliedFormat":1},{"version":"54a7ee56aadecbe8126744f7787f54f79d1e110adab8fe7026ad83a9681f136a","impliedFormat":1},{"version":"6333c727ee2b79cdab55e9e10971e59cbfee26c73dfb350972cfd97712fc2162","impliedFormat":1},{"version":"8743b4356e522c26dc37f20cde4bcdb5ebd0a71a3afe156e81c099db7f34621d","impliedFormat":1},{"version":"af3d97c3a0da9491841efc4e25585247aa76772b840dd279dbff714c69d3a1ec","impliedFormat":1},{"version":"d9ac50fe802967929467413a79631698b8d8f4f2dc692b207e509b6bb3a92524","impliedFormat":1},{"version":"34d017b29ca5107bf2832b992e4cee51ed497f074724a4b4a7b6386b7f8297c9","impliedFormat":1},{"version":"b75d56703daaffcb31a7cdebf190856e07739a9481f01c2919f95bde99be9424","impliedFormat":99},{"version":"edf2eb7cf1828640acbb5822fb470e5c5b49f41c77fa685d4f747496a28aa5bc","signature":"0666cc5e36fccf2f3c059de42a7467ae63349fc74b8af32f953af905341be132"},{"version":"e7b8a0313747812082710ad62e2ae6ab32fdef5b0832bfcdf6c7d56d5b78b186","signature":"a3f5921a8d7ab7d0040fa7352cedac4115d525441cb3f1efb0098408d96b8b4a"},{"version":"3ae0690c80128f1379343e202f76676cb411d80c361d625575139a6df81a1dd5","signature":"ffd37f81659885d2277b93d3f241b6d6c8ac36056d13184757d8ded2efc3267d"},{"version":"0448d3d7b4cb521c7dcec1e745cde4c3f8069c8e1ca38e825107a1c748305551","signature":"b0408b6f3574be8e9229064971b26fcec022f44e32bd7c742ba61d04ca6b75f7"},{"version":"bb62fa0df58dee6ef3672f8f6edac543da3c494d2733d393b0952db71ebbcfe8","signature":"4c76f4c8dc5b3a21af8051ee259a261da454c072cbd070e835ccb005bada9ab4"},{"version":"5518b814ca2443dc0c71a00fe79159f477d3dd1557e2c9cbb304cf4df53eabb7","signature":"d870380b279ca88c89db97f332a61f051fbc5000b151348df9ace8e2d82bae5a"},{"version":"87579ddbe12c3795f838433abdb67bf346243066a2b8da9bce58e62d8de9a33a","signature":"83e145c5770e0338947541c14c83405bdc282b7e705e5e0152f650bb0686668f"},{"version":"fca8d6826fa1f9fb0e7bc426fa136633ace99ab594445683c9e122689e6ad683","signature":"12e96671705f236910b34abf2af510b984762c83228c16330ec2e3aa0c58b724"},{"version":"76fdbb8eba1c60d5bca5cee45f31fdb886e973eebec0898570021c0f4b8a1df5","signature":"27f1d53729910f23475a4db05f37c9af2bee6c5a6126944b75be19f2d3efb34d"},{"version":"ce4486eb95d1a94ee70fe1d3781f0573a182b0ae35295e2cc622984f41928974","signature":"fdd8dca508717ff7130bc2990227f7824a40c728541e94962629414cb21aa076"},{"version":"574b407bf7f276ebc9d49e4e6ee536f465bd41a4499d26b889ddb0c0e90140ed","signature":"7680a0d34dda1d9c5031f6d7c53bdf29230e33a2de88c44eef9f896726b4ffeb"},{"version":"c106a6b10589840707ec02c83d52953bc69a097cbb6085eb5458133a6f116192","signature":"a34a541572d1afa7e2e3dd26a6a473e2a03ed7354f5ed445685a483e9a1a9129"},{"version":"821d0dad2c0727940ac7910833b281e9fb475af6413edfc5bb380f2357499499","signature":"6ddd6a4cd4f6f3e90eb65285492bbd2890f8bf95642eddf21ec76deef915c15f"},{"version":"e8b1ead2590d5399ecaa713cf8c906b55523473fe55038f0f2cdb0428ccb731e","signature":"9177c457793eae5967cba29eb002119f25cf4a21e0b174ce66ef9ce17893cbfd"},{"version":"00942de8f43e448e95ecc76e5206d9da7d483e2ef8b3e07d4b0945a94e07a3d7","signature":"a013754e9c9372195578014767d9daa25f3a37a2cac34b96228225fbf6ba5c86"},{"version":"cadd4dd348a5eacbadc0c59b254814aeac8076d9bd1735c4c2a8a51d33117180","signature":"fd29d3400f8bdfbc70548cf71a4f27ca1c097845e4ba88c3322671a05163a2f0"},{"version":"c5b45951bd44b61c9b72ae72c13f79060eebc452dfc1263220515d869b0c712a","signature":"4380ba8dda48544e6b271c3b9bbc98802ab7962dad4e47e0e361d87b586df8fe"},{"version":"f1fb76094c19a1acf116dbe4dcf410676054822a013cdf29ae734521b8201f9f","signature":"a139c8ba8e2f80b631d6044d892c124dab4da3979d25882a3a76cfbe17254abf"},{"version":"b48d9821fadd54f21dff3e5f8ef915541ea79cb2f411f0150c44d602e7e20dbc","signature":"e0765ae3f00305586713ce62a4bf664422d24e87628b08ef81f15a3cba671ce6"},{"version":"027f4a05669892ab1326f72ac5eb84ab3dce780c91c39821e48d2f2a44272bad","signature":"214c15e6e71bd17e65a02339cee5bdd545ddbcea1a068bc58030f73eb306a5b7"},{"version":"9edcaa14e241f336ce11fbb13dfad933a5b2bf82dac5adb263dccfc40b890476","impliedFormat":99},{"version":"e3954ab905c6b7b87abd112e8f4c37865e63a7c3e0d089e2a533bd6614e254f2","impliedFormat":99},{"version":"0fb59682975069330ff1ae73ff3536dcd8b3f882d1f499eac023fd5ab22b4662","impliedFormat":99},{"version":"e7a4acd437c3e337f6580360cfe0827f00bfc94641ece1b7af4d5f75dfe4dbdf","impliedFormat":99},{"version":"e64b8b239623500a8d3d43f7b78146b9a9868c559208e603cc6e6199f854173c","impliedFormat":99},{"version":"69dc0677101b9b9895a16659640f1741180e4e7a29aabad2f5d92dd3333ef213","signature":"a611390c50fa771375d7d9b65cab4113f5f4a4babb52040100e64bc8354d78ff"},{"version":"15993f8ffda363848ead89a03c86fb5e505fcefbfdcf10bd85467bdc8275491e","signature":"cee698b913e7e007925346dc6179dfadcf2d1078255b0882205e52fd670a0f93"},{"version":"84e841502878baf2162f90dc32649a16c72cc49ed470e51bf702cc5ce4b25d1c","signature":"3fb60d8ac6b217d6e6111bd6e6f0b23d74ddfd396bfad99773ddacea5468333d"},{"version":"fe4c46a2a2bc2ff461f23f2df5e36ee697471be49a0b74ba430e1fc8f5357214","signature":"246ca7b44ee23d9ebbd17b8e84aed66ab0476034ad3aeb909b5342c5cfff0d89"},{"version":"09b05977baa4555e5dd1621e358dedaa3dc334e2887d5333eb3fdf22521b5967","signature":"0718a106aca3f632f6d289ddc5a682da9a6fbac041e85bf1d430697d538857f8"},{"version":"93f46314ca5cd6c66550e43fd7b9ff7b9dddb99dee9c3cb32525418703064044","signature":"49f09adfb62b142a1d0cf9e29a3b3faef79cb671237355dc971b3a5c3bd931ad"},{"version":"cb68caac612f2a6b3aca39f9973d4ef97adc37849657b353979244cc6eff4e28","signature":"9cd3b83650a88ffbf9b6ca3f3885fff403ad75fc861d4c1ba8e3228ede56c58f"},{"version":"8f76a6a07dc35658e197f7556297b345a2d1837b98956f134239f6024a226873","signature":"dd32c2b8aef08002349f8917a663aaf6e36b81a4134e44358fc84a3222b311fa"},{"version":"cf1ea2ea5cac46a67a386c5a689bb830e735035a523f2d75dd633ef80bdec268","signature":"fb6b3be1962c8cf5c462c90c10991082a107e399d48c84db2736af910afdab53"},{"version":"84c74f587aacb22d3203b9cd92bf9af65a055711d78d40c3f5bd0f015e00ce6b","signature":"ea564ae8f69e719fe85ac76e98eae5f1aa6561d8cf71a566911dfd48e81aee30"},{"version":"30be8b57a5a098447aee12bb0dd20b3573353a7b9956b9983132f0cb08163f03","signature":"9b0fc84934fe47863b2b614f2855a7aa0e88a38dc2cdb49f2c83983c615a5cb3"},{"version":"d0a9d76951c0cc9e75cfb7aaf58fe3cc8c7002b016153d0c76d12fec87b6a19d","signature":"48d4cb41063937c9066c62dff5cd67c2d935404e3ac11642ef269749609143c7"},{"version":"881ae59bd7b942797a451babc4ab3ae17de7748009d2512affdb8a493c735684","signature":"6b1029c752307fbd6189cd28a9f561181599fa5f4a36e41f49c8f2f7b6005491"},{"version":"74c72e8c78ccc896ae083443d98371fd20229c1fa9d2d2a32a107660f50ca28c","signature":"42aa6299b5d2b05ad1e9ac07aea7a803bb3cd7c90261093ae641f2cb7c3440ea"},{"version":"8fef71e8b89525935250b629a25b7f075adc27664034c6412c1602eb34161119","signature":"f42caa1c990576c228235815d660f6caf80cf9bd3d18e7c45c7354fe25acedb4"},{"version":"d44fd557a07bebc9cd09af0863255ea1e90f9e7f126aa7429bc60c29e732b26e","signature":"32b49b022bbe45e607741f81fc4ef06cd3a6b44d0a79db4f89d2db55e41ba4a5"},{"version":"0657584e3daa390cece6f40ffc7ff55ded733031c1c855607bf47ac9cd8875aa","signature":"b212e3487976b16c9a414adb7c8edefe9f325de2f109a94f9c59158a745dabec"},{"version":"d4a22007b481fe2a2e6bfd3a42c00cd62d41edb36d30fc4697df2692e9891fc8","impliedFormat":1},{"version":"a510938c29a2e04183c801a340f0bbb5a0ae091651bd659214a8587d710ddfbb","impliedFormat":99},{"version":"07bcf85b52f652572fc2a7ec58e6de5dd4fcaf9bbc6f4706b124378cedcbb95c","impliedFormat":99},{"version":"4368a800522ca3dd131d3bbc05f2c46a8b7d612eefca41d5c2e5ac0428a45582","impliedFormat":99},{"version":"720e56f06175c21512bcaeed59a4d4173cd635ea7b4df3739901791b83f835b9","impliedFormat":99},{"version":"349949a8894257122f278f418f4ee2d39752c67b1f06162bb59747d8d06bbc51","impliedFormat":99},{"version":"364832fbef8fb60e1fee868343c0b64647ab8a4e6b0421ca6dafb10dff9979ba","impliedFormat":99},{"version":"dfe4d1087854351e45109f87e322a4fb9d3d28d8bd92aa0460f3578320f024e9","impliedFormat":99},{"version":"886051ae2ccc4c5545bedb4f9af372d69c7c3844ae68833ed1fba8cae8d90ef8","impliedFormat":99},{"version":"3f4e5997cb760b0ef04a7110b4dd18407718e7502e4bf6cd8dd8aa97af8456ff","impliedFormat":99},{"version":"381b5f28b29f104bbdd130704f0a0df347f2fc6cb7bab89cfdc2ec637e613f78","impliedFormat":99},{"version":"a52baccd4bf285e633816caffe74e7928870ce064ebc2a702e54d5e908228777","impliedFormat":99},{"version":"c6120582914acd667ce268849283702a625fee9893e9cad5cd27baada5f89f50","impliedFormat":99},{"version":"da1c22fbbf43de3065d227f8acbc10b132dfa2f3c725db415adbe392f6d1359f","impliedFormat":99},{"version":"858880acbe7e15f7e4f06ac82fd8f394dfe2362687271d5860900d584856c205","impliedFormat":99},{"version":"8dfb1bf0a03e4db2371bafe9ac3c5fb2a4481c77e904d2a210f3fed7d2ad243a","impliedFormat":99},{"version":"bc840f0c5e7274e66f61212bb517fb4348d3e25ed57a27e7783fed58301591e0","impliedFormat":99},{"version":"26438d4d1fc8c9923aea60424369c6e9e13f7ce2672e31137aa3d89b7e1ba9af","impliedFormat":99},{"version":"1ace7207aa2566178c72693b145a566f1209677a2d5e9fb948c8be56a1a61ca9","impliedFormat":99},{"version":"a776df294180c0fdb62ba1c56a959b0bb1d2967d25b372abefdb13d6eba14caf","impliedFormat":99},{"version":"6c88ea4c3b86430dd03de268fd178803d22dc6aa85f954f41b1a27c6bb6227f2","impliedFormat":99},{"version":"11e17a3addf249ae2d884b35543d2b40fabf55ddcbc04f8ee3dcdae8a0ce61eb","impliedFormat":99},{"version":"4fd8aac8f684ee9b1a61807c65ee48f217bf12c77eb169a84a3ba8ddf7335a86","impliedFormat":99},{"version":"1d0736a4bfcb9f32de29d6b15ac2fa0049fd447980cf1159d219543aa5266426","impliedFormat":99},{"version":"11083c0a8f45d2ec174df1cb565c7ba9770878d6820bf01d76d4fedb86052a77","impliedFormat":99},{"version":"d8e37104ef452b01cefe43990821adc3c6987423a73a1252aa55fb1d9ebc7e6d","impliedFormat":99},{"version":"f5622423ee5642dcf2b92d71b37967b458e8df3cf90b468675ff9fddaa532a0f","impliedFormat":99},{"version":"21a942886d6b3e372db0504c5ee277285cbe4f517a27fc4763cf8c48bd0f4310","impliedFormat":99},{"version":"41a4b2454b2d3a13b4fc4ec57d6a0a639127369f87da8f28037943019705d619","impliedFormat":99},{"version":"e9b82ac7186490d18dffaafda695f5d975dfee549096c0bf883387a8b6c3ab5a","impliedFormat":99},{"version":"eed9b5f5a6998abe0b408db4b8847a46eb401c9924ddc5b24b1cede3ebf4ee8c","impliedFormat":99},{"version":"af85fde8986fdad68e96e871ae2d5278adaf2922d9879043b9313b18fae920b1","impliedFormat":99},{"version":"8a1f5d2f7cf4bf851cc9baae82056c3316d3c6d29561df28aff525556095554b","impliedFormat":99},{"version":"9ee1f62857343437430903826226eab4a6731828888c1ac173bfed1dc1e6177f","signature":"7d562126a8607db4d8cb3624830db1372608c9b6b59384dd1eb8417b2c4ab2be"},{"version":"d2360349e11623e033a729c16ccbb93984dfcb59d4cb12fb3ce76d9aadd99de7","signature":"7e5b1852f58978b1095f9a48e6d76d113c0c7b3a6c247302ef18e8b489880e94"},{"version":"7a0a4244d8cb27627752026c0e4be10e8404f2fde1d192b2696a29b8df2427eb","signature":"f02eed248f1ae959203e8f56933540efc81ceca55365cab8bbcebf4fe0413cb8"},{"version":"025912f4e8c29f644ed2169a8f59f0a34ab7e7b5a298de2e6cc495a166618edb","signature":"be4c979512c978ad22544a765039ba9338e90539e656e0adbac357d9d5116890"},{"version":"98431ba8667a72310bf87e8d20a8d9fc6b444c03501234ad270a37c64e9c823a","signature":"58ddaee7d7383718665829fbc6930adc17395d9f46e47bbeb997a36869e48b49"},{"version":"d005d73b140b90106093b7aa32aee7c7385bfc83bbf6bbce16362368cd17910b","signature":"62078e13c0629f619120e6439193760e66dab737560a7bbf73492b17e2b623e6"},{"version":"4d309fce0b8c8cd4fc2fe174785790de4fcb2a21b07b7a22058a2afabe7c4b24","signature":"bf8805d1c9460e86c8913319136ff824429f96aaaec7bc38e43c5671532e8b31"},{"version":"b56fc20d655a0e0eecb61ed1557969d1c6e6c5a8b96bd40df65053cc8cac4c65","signature":"3de8abf8a377118fd5540f4a72d9af24c60ad375b5a3d239c2b866ccce029dee"},{"version":"b5307ecce976f66f0b0dcd0c9a841edd29ba143729cbd86be28e88226529955b","signature":"3de316d0e3fdbfc3f86a39b80815b805efd46df49130a368fd5490cccf0ba983"},{"version":"d7beda47d1fd55e81e09dfc58e0ae3e8ff150936e855ab89c2a4ea0c3b6bce98","signature":"a72817d64cac5d1d92cfa266b8bb5ddb7ec6f12d0be571ed5eeb393dd9795371"},{"version":"b4dae15558cf17805565a38bd72a0f754c528400fb83780b37af87369d72422c","signature":"e2e9e6eba9363c5b2aeb1c03df70e773d0d730143547004f07caf7e3c1fd37d8"},{"version":"168ddd74877e5268b68ffa92c0b4851bcbeb3ffcff03f09f86b75ba98f75b63b","signature":"31a40bb81e0bcdda2d3e78a12df728ed96a8f5b43650c8f10b6650f435345144"},{"version":"fae089d170ac936b0905232b26d816e781ac170d1d360ec7db7e9550a8b2d03c","signature":"270bd604cb5498de3612dbff8cc7e93c32ce404c0e6809a598e3abf7825ccc75"},{"version":"ae34ecb995774d2df9c05e84114804b45b5b153979005a71a41398806d899556","signature":"84debec1e152c45a3dee9d170564b70fa89a162bf380c8350f2a6c1e2a1ad222"},{"version":"10b6e68769cba431d3ca78484351ad6efcc8f0d4955807acc2b5936ec85079a6","signature":"321b88cbbf0caf0cf3c50b7537663af45ea3b597f7c854c842f8ae917d9dfcd1"},{"version":"f4e8f4151c3490cf7b68c685aabe901cbab19f962aaa2f118a97550e22689a76","impliedFormat":1},{"version":"799003c0ab928582fca04977f47b8d85b43a8de610f4eef0ad2d069fbb9f9399","impliedFormat":1},{"version":"d998eea476c695d8e4ff9d007d5b46d49ca2ffa052f74dc20ca516425abd57b1","impliedFormat":1},{"version":"a0bd46d587005aad4819980f6cf2dbcd80ebf584ed1a946202326a27158ba70e","impliedFormat":1},{"version":"07fcbb61a71bd69a92a5bbde69e60654666cf966b5675c2010c3bf9f436f056a","impliedFormat":1},{"version":"88b2eb23d36692162f2bf1e50577ebcde26de017260473e03ed9a0e61e2726a4","impliedFormat":1},{"version":"23ffbd8c0e20a697d2ea5a0cf7513fb6e42c955a7648f021da12541728f62182","impliedFormat":1},{"version":"43fba5fc019a4ce721a6f53ddb97fdc34c55049cfb793bc544d5c864ee5560b9","impliedFormat":1},{"version":"f4e12292c9a7663a13d152195019711c427c552eb0fa02705e0f61370cd5547a","impliedFormat":1},{"version":"c127ebf14d1b59d1604865008fb072865c5ca52277621f566092fe1f42ce0954","impliedFormat":1},{"version":"def638da26d84825a312113a20649d3086861de7c06a18ea13121278702976fd","impliedFormat":1},{"version":"fbaf86f8ba11298dea2727ce0da84b4ab6ae6c265e1919d44aff7d9b2bbc578a","impliedFormat":1},{"version":"c1010caaeaca8e420c6e040c2e822dbe18702459c93a7d2d5de38597d477b8cd","impliedFormat":1},{"version":"e1f0d8392efd9d71f2644eb97d3f33d90827e30ea8051d93b6f92bb11dff520a","impliedFormat":1},{"version":"085211167559ca307d4053bb8d2298d5ad83cbc3d2ae9bb4c8435a4cabf59369","impliedFormat":1},{"version":"55fc49198d8a85a73cdb79e596d9381cfdc9de93c32c77d42e661c1c1e7268ef","impliedFormat":1},{"version":"6a53fb3df8dd32ed1a65502ca30aeae19cfe80990e78ba68162d6cb2a7fed129","impliedFormat":1},{"version":"b5dcc18d7902597a5584a43c1146ca4fe0295ceb5125f724c1348f6a851dd6ed","impliedFormat":1},{"version":"0c6b0f3fbe6eb6a3805170b3766a341118c92ed7b6d1f193b9f35aa82f594846","impliedFormat":1},{"version":"60eaadb36cf157c5cae9c40e84fa367d04f52a150db3920dbe35139780739143","impliedFormat":1},{"version":"4680a32b1098c49dc87881329af1e68af9af94e051e1b9e19fed555a786f6ce6","impliedFormat":1},{"version":"89fcd129ec37f321cddcdb6b258ffe562de4281e90ec3ccbe7c1199ba39359ca","impliedFormat":1},{"version":"4313011f692861c2c1f5205d7f9a473e763adab6444f9853b96937b187fb19f7","impliedFormat":1},{"version":"caa57157e7bdb8d5f1efe56826fb84a6c8f22a1927bba7fa21fd54e2a44ccba2","impliedFormat":1},{"version":"6b74700abfe4a9b88be957fd8e373cfd998efb1a5f6ad122da49a92997e183ad","impliedFormat":1},{"version":"9ef1342f193bd8bae86c64e450c3ac468ef08652110355e1f3cdd45362eb95c4","impliedFormat":1},{"version":"6853c91662c36a2bf4c8371a87177c819007c76a23c293ef3f686ce9157ae4c8","impliedFormat":1},{"version":"9be1c5dabce43380d13fc621100676b03d420b5687b08d1288f479bee68ab7a8","impliedFormat":1},{"version":"8996d218010896712678e6a0337d8ef8b81c1066ab76f637dd8253f0d6ff838d","impliedFormat":1},{"version":"a15603bf387fc45defe28a68f405a6c29105e135c4e8538eeb6d0a1ef5b69a81","impliedFormat":1},{"version":"84e2532e4d42949a2775cdd8bb7b2b97370dd6ddb683d0c199b21bf6978b152d","impliedFormat":1},{"version":"22bf5f19f620db3b8392cfece44bdd587cdbed80ba39c88a53697d427135bf37","impliedFormat":1},{"version":"23ebbd8d484d07e1c1d8783169c20570ed8409966b28f6be6cf8e970d76ef491","impliedFormat":1},{"version":"18b6fa2c778cad6489f2febf76433453f5e2432ec3535f2d45ae7d803b93cc17","impliedFormat":1},{"version":"609d0d7419999cf44529e6ba687e2944b2fc7ad2570d278fd4e6b1683c075149","impliedFormat":1},{"version":"249cf421b8878a3fe948d9c02f6b0bae65491b3bb974c2ffc612341406fa78ff","impliedFormat":1},{"version":"b4aa22522d653428c8148ddbf1dcc1fb3a3471e15eb1964429a67c390d8c7f38","impliedFormat":1},{"version":"30b2cee905b1848b61c7d28082ebfa2675dd5545c0d25d1c093ce21a905cdccc","impliedFormat":1},{"version":"0a2a2eed4137368735205de97c245f2a685af1a7f1bf8d636b918a0ee4ff4326","impliedFormat":1},{"version":"69f342ce86706aa2835a62898e93ea7a1f21b1d89c70845da69371441bb6cd56","impliedFormat":1},{"version":"b5ab4282affcfd860dd1cc3201653f591509a586d110f8e5b1b010508ba79b2c","impliedFormat":1},{"version":"d396233f6cd3edf0d33c2fbfc84ded029c3ea4a05af3c94d09d31a367cced111","impliedFormat":1},{"version":"bc41a726c817624a5136ae893d7aac7c4dc93c771e8d243a670324bccf39b02b","impliedFormat":1},{"version":"710728600e4b3197f834c4dd1956443be787d2e647a72f190bf6519f235aaadd","impliedFormat":1},{"version":"a45097e01ef30ba26640fed365376ab3ccd5faf97d03f20daff3355a7e60286a","impliedFormat":1},{"version":"763cbb7c22199f43fd5c2b1566af5ba96bf7366f125dd31a038a2291cbc89254","impliedFormat":1},{"version":"031933bf279b7563e11100b5e1746397caf3a278596796a87bc0db23cf68dc9e","impliedFormat":1},{"version":"a4a54c1f58fc6e25a82e2c0f651bf680058bd7f72cfb2d43b85ee0ab5fe2e87e","impliedFormat":1},{"version":"9613d789b6f1037f2523a8f70e1b736f1da4566b470593da062be5c9e13dac57","impliedFormat":1},{"version":"0d2a320763a0c9c71493f8f1069971018c8720a6e7e5a8f10c26b6de79aa2f7d","impliedFormat":1},{"version":"817e0df27a237a268dc16e5acffc19f9a74467093af7a0ba164ee927007a4d25","impliedFormat":1},{"version":"43102521b5ca50ff1865188c3c60790feaed94dc9262b25d4adec4dbc76f9035","impliedFormat":1},{"version":"f99947f8d873b960b0115e506ef9c43f4e40c2071b1d20375564538af4a6023b","impliedFormat":1},{"version":"c1e5ad5ca89d18d2a36d25e8ec105623648cf35615825e202c7d8295a49d61ab","impliedFormat":1},{"version":"2b6c9cb81da4e0a2e32a58230e8c0dec49fc5b345efb7f7a3648b98956be4b13","impliedFormat":1},{"version":"99e34af3ede50062dcc826a1c3ce2d45562060dfd0f29f8066381a6ef548bf2a","impliedFormat":1},{"version":"49f5c2a23ea5fc4b2cdb4426f09d1c8b83f8409fa2af13ef38845cc9b9d4bc3d","impliedFormat":1},{"version":"e935227675144b64ecde3489e4a5e242eeb25fdd6b7464b8c21ad1f7a0faa88b","impliedFormat":1},{"version":"b42e6bbe88dc79c2d6dc5605fb9c15184e70f64bdd7b8d4069b802b90ce86df6","impliedFormat":1},{"version":"b9cd712399fdc00fdae07e96c9b39c3cb311e2a8a5425f1bd583f13cab35e44b","impliedFormat":1},{"version":"5a978550ae131b7fef441d67372fd972abab98ea9fdb9fa266e8bdc89edcb8d6","impliedFormat":1},{"version":"4f287919cfc1d26420db9f0457cd5c8780b1ef0a9f949570936abe48d3a43d91","impliedFormat":1},{"version":"496b23b2fd07e614bc01d90dd4388996cb18cd5f3a612d98201e9f683e58ad2e","impliedFormat":1},{"version":"dcfbe42824f37c5fb6dc7b9427ef2500791ec0d30825ecb614f15b8d5bf5a667","impliedFormat":1},{"version":"390124ad2361b46bf01851d25e331cd7eed355d04451d8b2a4aa985c9de4f8ce","impliedFormat":1},{"version":"14d94f17772c3a58eda01b6603490983d845ee2012cd643f7497b4e22566aacb","impliedFormat":1},{"version":"03ef2386c683707ce741a1c30cb126e8c51a908aa0acc01c3471fafb9baaacd5","impliedFormat":1},{"version":"66a372e03c41d2d5e920df5282dadcec2acae4c629cb51cab850825d2a144cea","impliedFormat":1},{"version":"5b48ba9a30a93176a93c87f9e0abf26a9df457eeb808928009439ca578b56f27","impliedFormat":1},{"version":"4707625392316d3c16edbd0716f4ac310e8ff5d346d58f4d01a2b7e0533a23df","impliedFormat":1},{"version":"154d58a4b2d9c552dc864ea39c223d66efd0ed2dd8b55bd13db5225d14322915","impliedFormat":1},{"version":"6a830433fa072931b4ea3eb9aa5fa7d283f470080586a27bfe69837a0f12de9a","impliedFormat":1},{"version":"d25e930e181f4f69b2b128514538f2abb54ef1d48a046ad776ac6f1cda885a72","impliedFormat":1},{"version":"0259b4c21bc93b52ca82c755f97fc90481072bcc44a8010131b2ea7326cf03fe","impliedFormat":1},{"version":"bea43a13a1104a640da0cb049db85c6993f484a6cc03660496b97824719ecc91","impliedFormat":1},{"version":"0224239d61fe66d4900544d912b2e11c2cca24b4707d53fdb94b874a01e29f48","impliedFormat":1},{"version":"2bce8fd2d16a9432110bbe0ba1e663fd02f7d8b8968cd10178ea7bc306c4a5df","impliedFormat":1},{"version":"9c4ad63738346873d685e5c086acbf41199e7022eff5b72bb668931e9ca42404","impliedFormat":1},{"version":"cfb6329bf8ce324e83fe4bbdee537d866a0d5328246f149a0958b75d033de409","impliedFormat":1},{"version":"efc3816f19ea87a7050c84271ea3d3aad9631a517c168013c4f4b6724c287ce0","impliedFormat":1},{"version":"f99f6737336140047e8dd4ade3859f08331aa4b17bc2bd5f156a25c54e0febbc","impliedFormat":1},{"version":"12a2b25c7c9c05c8994adf193e65749926acfcc076381f7166c2f709a97bdf0a","impliedFormat":1},{"version":"0f93a3fdd517c1e45218cd0027c1d6b82237e379dc6b66d693aab1fe74c82e81","impliedFormat":1},{"version":"03c753da0bee80ad0d0f1819b9b42dfe9bf9f436664caf15325aa426246fd891","impliedFormat":1},{"version":"18f5bf1dae429c451f20171427c9e3223fade4346af4dfd817725cbeb247a09d","impliedFormat":1},{"version":"a4eece5fab202e840dd84f7239e511017a8162edb8fc8b54ff2851c5c844125c","impliedFormat":1},{"version":"c4a94af483a63bf947d89f97553a55df5107c605ec8a26f0b9b8bdcc14bd6d89","impliedFormat":1},{"version":"19de2915ccebc0a1482c2337b34cb178d446def2493bf775c4018a4ea355adb8","impliedFormat":1},{"version":"9be8fc03c8b5392cd17d40fd61063d73f08d0ee3457ecf075dcb3768ae1427bd","impliedFormat":1},{"version":"3b568b63f0e8b3873629a4d7a918dce4266ad41461004ab979f8dcdfd13532bb","impliedFormat":1},{"version":"a5e5223c775fe30d606b8aaa521953c925d5ad176a531c2b69437d2461aaabbd","impliedFormat":1},{"version":"8cbf41d2d1ce8ac2066783ae00613c33feef07493796f638e30beaf892e4354a","impliedFormat":1},{"version":"e22ad737718160df198cd428f18da707177d0467934cecdeed4be6e067b0c619","impliedFormat":1},{"version":"15bf5ed8cb7c1a1e1db53fa9b45bc1a1c73c0497735343a8d0c59fdb596a3744","impliedFormat":1},{"version":"791fce84bce8b6948e4f23422d9cbbd7d08c74b3f91cca12dcae83d96079798b","impliedFormat":1},{"version":"8a2619c8e24305f6b9700b35af178394b995dcb28690a57a71cca87ee7e709ae","impliedFormat":1},{"version":"f95fd2fc3cc164921a891f5d6c935fa0d014a576223dd098fc64677e696b0025","impliedFormat":1},{"version":"8c9cecaaa9caba9a8caa47f46dcf24b524b27899b286d8edcc75a81b370d2ba3","impliedFormat":1},{"version":"2b7a82692ecc877c5379df9653902e23f2d0d0bc9f210ec3cf9e47be54413c5c","impliedFormat":1},{"version":"e2ad09c011cf9d7ee128875406bef787eeb504659495f42656a0098c15fe646c","impliedFormat":1},{"version":"eb518567ea6b0b2623f9a6d37c364e1b1ac9d8b508d79e558f64ac05c17e2685","impliedFormat":1},{"version":"630a48fb8f6b07161588e0aee3f9d301c59c97e1532c884118f89368baf4073b","impliedFormat":1},{"version":"14736c608aa46120f8d6d0bc5e0721b46b927bc7eba20e479600571935f27062","impliedFormat":1},{"version":"7574803692d2230db13205a7749b9c3587dccaccdf9e76f003f9e08078bb6d09","impliedFormat":1},{"version":"f3cc1588e666651c51353b1728460bee8acbc6e0f36be8c025eaaf292dca525d","impliedFormat":1},{"version":"0d4ea8a20527dcf3ad6cf1bd188b8ad4e449df174fad09b9e540ed81080af834","impliedFormat":1},{"version":"aa82876d59912d25becff5a79ed7341af04c71bfeb2221cc0417bc34531125e2","impliedFormat":1},{"version":"6f4b0389f439adc84cba35d45428668eabcfbdd351ba17e459d414ca51ab8eb8","impliedFormat":1},{"version":"d5dd33d15fbb07668c264b38065ac542a07a7650af4917727bbc09b58570e862","impliedFormat":1},{"version":"7d90202d0212e9cdc91a20bfddf04a539c89f09fe1d64db3343546fa2eb37e71","impliedFormat":1},{"version":"1a5d073c95a3a4480b17d2fa7fd41862a9df0cb2afaee86834b13649e96bdb45","impliedFormat":1},{"version":"2092495a5b3116c760527a690c4529748f2d8b126cdd5f56b2ce2230b48aba3f","impliedFormat":1},{"version":"620b29d6adbd4061bc0a8fedf145fcc8e8fc9648fb6e0a39726e33babb4e07bc","impliedFormat":1},{"version":"931eda51b5977f7f3fa7a0d9afde01cfd8b0cc1df0bb66dcf8c2cf6e7090384e","impliedFormat":1},{"version":"b084a412374bdd124048c52c4e8a82d64f3adec6c0a9ad5ecbb7317636039b0f","impliedFormat":1},{"version":"11199daa694c3ced3cc2a382a3fa7bd64e95eb40f9bbc3979fc8fb43f5ba38cc","impliedFormat":1},{"version":"2c86f279d7db3c024de0f21cd9c8c2c972972f842357016bfbbd86955723b223","impliedFormat":1},{"version":"dfb53b9d748df3e140b0fddb75f74d21d7623e800bb1f233817a1a2118d4bb24","impliedFormat":1},{"version":"8cfc293b33082003cacbf7856b8b5e2d6dd3bde46abbd575b0c935dc83af4844","impliedFormat":1},{"version":"7730c538d6d35efe95d2c0d246b1371565b13037e893178033360b4c9d2ac863","impliedFormat":1},{"version":"b256694544b0d45495942720852d9597116979d52f2b53c559fda31f635c60df","impliedFormat":1},{"version":"794e8831c68cc471671430ee0998397ea7a62c3b706b30304efdc3eaff77545a","impliedFormat":1},{"version":"9cfc1b227477e31988e3fb18d26b6988618f4a5da9b7da6bc3df7fc12fb2602e","impliedFormat":1},{"version":"264a292b6024567dd901fdabbf3239a8742bea426432cdbda4cf390b224188e1","impliedFormat":1},{"version":"f1556a28bb8e33862dcfa9da7e6f1dca0b149faf433fe6a50153ae76f3362db1","impliedFormat":1},{"version":"1d321aea1c6a77b2a44e02e5c2aeff290e3f1675ead1a86652b6d77f5fea2b32","impliedFormat":1},{"version":"4910efc2ce1f96d6e71a9e7c9437812ffae5764b33ab3831c614663f62294124","impliedFormat":1},{"version":"e3ceab51a36e8b34ab787af1a7cf02b9312b6651bac67c750579b3f05af646c1","impliedFormat":1},{"version":"baf9f145bcee1b765bed6e79fd45e1ff0ca297a81315944de81eb5d6fff2d13d","impliedFormat":1},{"version":"2afd62362b83db93cd20de22489fe4d46c6f51822069802620589a51ccad4b99","impliedFormat":1},{"version":"9f0cd9bd4ab608123b88328c78814738cbdee620f29258b89ef8cd923f07ff9c","impliedFormat":1},{"version":"801186c9e765583c825f28dab63a7ad12db5609e36dc6d9acbdc97d23888a463","impliedFormat":1},{"version":"96c515141c6135ccd6fb655fb9e3500074a9216ba956fb685dc8edc33f689594","impliedFormat":1},{"version":"416af6d65fc76c9ced6795f255cb1096c9d7947bede75b82289732b74d902784","impliedFormat":1},{"version":"a280c68b128ebba35fb044965d67895201c2f83b6b28281bb8b023ade68bf665","impliedFormat":1},{"version":"6fa118f15723b099a41d3beea98ed059bcd1b3eda708acf98c5eff0c7e88832f","impliedFormat":1},{"version":"dcbf582243e20ea50d283f28f4f64e9990b4ed4a608757e996160c63cff6aa99","impliedFormat":1},{"version":"efa432d8fd562529c4e9f859fd936676dd8fef5d3b4bedb06f754e4740056ea9","impliedFormat":1},{"version":"a59b66720b2ccf2e0150fafb49e8da8dabdf4e1be36244a4ccd92f5bd18e1e9e","impliedFormat":1},{"version":"c657fb1ec3b727d6a14a24c71ea20c41cb7d26a503e8e41b726bb919eb964534","impliedFormat":1},{"version":"50d6d3174868f6e974355bf8e8db8c8b3fcf059315282a0c359ecf799d95514a","impliedFormat":1},{"version":"86bf79091014a1424fc55122caa47f08622b721a4d614b97dd620e3037711541","impliedFormat":1},{"version":"7a63313dff3a57f824a926e49a7262f7bd14e0e833cf45fa5af6da25286769c2","impliedFormat":1},{"version":"36dcaeffe1a1aed1cb84d4feba32895bf442795170edccc874fa32232b2354e5","impliedFormat":1},{"version":"686c6962d04d90edafc174aa5940acb9c9db8949c8d425131c01d796cf9a3aef","impliedFormat":1},{"version":"2b1dbc3d5762d6865744b6e7be94b8b9004097698c37e93e06983e42dd8fe93b","impliedFormat":1},{"version":"eb5e8f74826bdf3a6a0644d37a0f48133f8ad0b5298cc2c574102868542ba4eb","impliedFormat":1},{"version":"c6a82a9673ba517cf04dd0803513257d0adf101aed2e3b162a54d840c9a1a3b2","impliedFormat":1},{"version":"fc9f0f415abaa323efcecc4a4e0b6763bfe576e32043546d44f1de6541b6399b","impliedFormat":1},{"version":"2c4d772ac7ac56a44deef82903364eb7c78dd7bc997701123df0ce4639fe39bb","impliedFormat":1},{"version":"9369ef11eed17c1c223fdea9c0fa39e83f3722914ef390b1448db3d71620c93a","impliedFormat":1},{"version":"aa84130dbc9049bba6095f87932138698f53259b642635f6c9e92dd0ddc7512c","impliedFormat":1},{"version":"084ceadd21efabd4b58667dca00d4f644306099151d2ee18cd28a395855b8009","impliedFormat":1},{"version":"b9503e29f06c99b352b7cae052da19e3599fa42899509d32b23a27c9bb5bebf6","impliedFormat":1},{"version":"75188920fe6ccc14070fe9a65c036049f1141d968c627b623d4a897ec3587e15","impliedFormat":1},{"version":"e2e1df7f45013d2b34f8d08e6ae5a9339724b0ea251b5445fcca3e170e640105","impliedFormat":1},{"version":"af06feb5d18a6ea11c088b683bdb571800d1f76b98d848eecdf41e5ec8f317fd","impliedFormat":1},{"version":"0596af52b95e0c8adc2c07f49f109d746b164739c5866fa8bb394dd6329a3725","impliedFormat":1},{"version":"c3365d08fe7a1ccc3b8e8638edc30123007f3241b4604e2585b9f14422ab97d8","impliedFormat":1},{"version":"a7a3d96b04bb0ec8cb7d2669767c4756f97dd70d08548f9e6522dde4de8e8a03","impliedFormat":1},{"version":"745e960e885a4ba04c872225cbb44bd67a7490d169ceaefab7c0dfc444768676","impliedFormat":1},{"version":"0b1ce1768cde3535493a9daf99e3bbb8c7dcc3a7f9d8cd358cb846af71ce5cdf","impliedFormat":1},{"version":"48b9603f6e8a7c94b727277592a089f94261baa64e6c9d18165da0481663a69e","impliedFormat":1},{"version":"3c20a3bb0c50c819419f44aa55acc58476dad4754a16884cef06012d02b0722f","impliedFormat":1},{"version":"4dc64902cb86e677a928293593658fbf53388f9a30d2b934140c70a7267b07ec","impliedFormat":1},{"version":"cb4fd56539a61d163ea9befe6b0292c32aa68a104c1f68f61416f1bc769bcfba","impliedFormat":1},{"version":"0d852bdc2b72b22393a8eebe374ee3efe3e0d44e630037b5e1b6087985388e62","impliedFormat":1},{"version":"b6c9a2deefb6a57ff68d2a38d33c34407b9939487fc9ee9f32ba3ecf2987a88a","impliedFormat":1},{"version":"f6b371377bab3018dac2bca63e27502ecbd5d06f708ad7e312658d3b5315d948","impliedFormat":1},{"version":"faa72893e85cb8ebb1dafde6b427e5204e60bb5f3ee6576bb64c01db1f255bc8","impliedFormat":1},{"version":"95b7ed47b31a6eaddcdd853ee0871f2bb61e39ce36a01d03dfafb83766f6c10c","impliedFormat":1},{"version":"19287d6b76288c2814f1633bdd68d2b76748757ffd355e73e41151644e4773d6","impliedFormat":1},{"version":"fc4e6ec7dade5f9d422b153c5d8f6ad074bd9cc4e280415b7dc58fb5c52b5df1","impliedFormat":1},{"version":"3aea973106e1184db82d8880f0ca134388b6cbc420f7309d1c8947b842886349","impliedFormat":1},{"version":"765e278c464923da94dda7c2b281ece92f58981642421ae097862effe2bd30fa","impliedFormat":1},{"version":"de260bed7f7d25593f59e859bd7c7f8c6e6bb87e8686a0fcafa3774cb5ca02d8","impliedFormat":1},{"version":"d95c4eaad4df9e564859f0c74a177fa0b2e5f8a155939b52580566ab6b311c3f","impliedFormat":1},{"version":"7192a6d17bfa06e83ba14287907b7c671bef9b7111c146f59c6ea753cfc736b9","impliedFormat":1},{"version":"5156d3d392db5d77e1e2f3ea723c0a8bd3ca8acffe3b754b10c84b12f55a6e10","impliedFormat":1},{"version":"a6494e7833ee04386a9f0c686726f7cb05f52f6e069d9293475ccb1e791ee0da","impliedFormat":1},{"version":"d9af0c89a310256851238f509a22aa1071a464d35dc22ea8c2a0bae42dd81bc5","impliedFormat":1},{"version":"291642a66e55e6ca38b029bc6921c7301f5c7b7acf21ae588a5f352e6c1f6d58","impliedFormat":1},{"version":"43cd7c37298b051d1ce0307d94105bcd792c6c7e017282c9d13f1097c27408e8","impliedFormat":1},{"version":"e00d8cce6e2e627654e49c543b582568ad0bf27c1d4ad1018d26aff78d7599df","impliedFormat":1},{"version":"ed13354f0d96fb6d5878655b1fead51722b54875e91d5e53ef16de5b71a0e278","impliedFormat":1},{"version":"fcb934d0fcdee06a8571bd90aa3a63aa288c784b3ebcecfe7ae90d3104d321f4","impliedFormat":1},{"version":"af682dfabe85688289b420d939020a10eb61f0120e393d53c127f1968b3e9f66","impliedFormat":1},{"version":"0dca04006bf13f72240c6a6a502df9c0b49c41c3cab2be75e81e9b592dcd4ea8","impliedFormat":1},{"version":"7dc0b5e3d7be8e1f451f0545448c2eaa02683f230797d24434b36f9820d5a641","impliedFormat":1},{"version":"247af61cdc3f4ec7876b9e993a2ecdd069e10934ff790c9cee5811842bff49eb","impliedFormat":1},{"version":"4be8c2c63d5cd1381081d90021ddfaef106881df4129eddeeaba906f2d0f75d0","impliedFormat":1},{"version":"012f621d6eb28172afb1b2dc23898d8bc74cf35a6d76b63e5581aa8e50fa71b3","impliedFormat":1},{"version":"3a561fa91097e4580c5349ce72e69d247c31c11d29f39e1d0bd3716042ff2c0b","impliedFormat":1},{"version":"bc9981a79dda3badea61d716d368a280c370267e900f43321f828495f4fef23c","impliedFormat":1},{"version":"2ed3b93d55aea416d7be8d49fe25016430caab0fe64c87d641e4c2c551130d17","impliedFormat":1},{"version":"3d66dfc31dd26092c3663d9623b6fc5cec90878606941a19e2b884c4eacd1a24","impliedFormat":1},{"version":"6916c678060af14a8ce8d78a1929d84184e9507fba7ab75142c1bcb646e1c789","impliedFormat":1},{"version":"3eea74afae095028597b3954bde69390f568afc66d457f64fff56e416ea47811","impliedFormat":1},{"version":"549fb2d19deb7d7cae64922918ddddf190109508cc6c7c47033478f7359556d2","impliedFormat":1},{"version":"e7023afc677a74f03f8ccb567532fe9eedd1f5241ee74be7b75ac2336514f6f6","impliedFormat":1},{"version":"ff55505622eac7d104b9ab9570f4cc67166ba47dd8f3badfb85605d55dd6bdc9","impliedFormat":1},{"version":"102fac015b1eebfa13305cb90fd91a4f0bbcabb10f2343556b3483bbb0a04b62","impliedFormat":1},{"version":"18a1f4493f2dbad5fd4f7d9bfba683c98cf5ed5a4fa704fa0d9884e3876e2446","impliedFormat":1},{"version":"f57e6707d035ab89a03797d34faef37deefd3dd90aa17d90de2f33dce46a2c56","impliedFormat":1},{"version":"cc8b559b2cf9380ca72922c64576a43f000275c72042b2af2415ce0fb88d7077","impliedFormat":1},{"version":"1a337ca294c428ba8f2eb01e887b28d080ee4a4307ae87e02e468b1d26af4a74","impliedFormat":1},{"version":"310fe80ff40a158c2de408efbe9de11e249c53d2de5e33ca32798e6f3fbc8822","impliedFormat":1},{"version":"d6ce96c7bb34945c1d444101f44e0f8ba0bba8ab7587a6cc009a9934b538c335","impliedFormat":1},{"version":"1b10a2715917601939a9288d49beccd45b591723256495b229569cd67bbe48a8","impliedFormat":1},{"version":"7498dfdeed2e003ec49cdf726ff6c293002d1d7fdadbc398ce8aafe6d0688de7","impliedFormat":1},{"version":"8492306a4864a1dc6fc7e0cc0de0ae9279cbd37f3aae3e9dc1065afcdc83dddc","impliedFormat":1},{"version":"9c86abbc4fd0248f56abc12aaecd76854517389af405d5ec2eb187fdb00a606f","impliedFormat":1},{"version":"9ffd906f14f8b059d6b95d6640920f530507e596e548f7a595da58ab66e3ce76","impliedFormat":1},{"version":"1884bccc10ce40adca470c2c371c1c938b36824f169c56f7f43d860416ca0a4c","impliedFormat":1},{"version":"986b55b4f920c99d77c1845f2542df6f746cb5adc9ab93eb1545a7e6ef37590d","impliedFormat":1},{"version":"cd00906068b81fbd8a22d021580ac505e272844408174520fafed0ae00627a5d","impliedFormat":1},{"version":"69fab68a769c17a52a24b868aeb644f3ee14abaa5064115f575ddd59231105ce","impliedFormat":1},{"version":"e181eb86b2caf80fe18c72efce6b913bc226e4a69a5456eaf4f859f1c29c6fd6","impliedFormat":1},{"version":"93f7871380478bc6acf02ad9f3dc7da0c21997caebbe782eb93a11b7bd06a46d","impliedFormat":1},{"version":"d00279ab020713264f570d5181c89ca362b7de8abddf96733de86bce0eca082c","impliedFormat":1},{"version":"f7db473f1d5d2a124f14886ac9dbfeccfbb94a98bbe1610a47c30c2933afa279","impliedFormat":1},{"version":"f44cf6c6d608ef925831e550b19841b5d71bd87195bd346604ff05644fb0d29c","impliedFormat":1},{"version":"154f23902d7a3fcdace4c20b654da7355fee4b7f807d1f77d6c9a24a8756013a","impliedFormat":1},{"version":"562f4f3c75a497d3ad7709381f850bb8c7646a9c6e94fdf8e91928e23d155411","impliedFormat":1},{"version":"4583380b676ee59b70a9696b42acfa986cd5f32430f37672e04f31f40b05df74","impliedFormat":1},{"version":"ad0a13f35a0d88803979f8ea9050ad7441e09d21a509abf2f303e18c1267af17","impliedFormat":1},{"version":"ba9781c718ab3d09cbde1216029072698d2da6135f0d2f856ba387d6caceb13e","impliedFormat":1},{"version":"d7c597c14698ba5fc8010076afa426f029b2d8edabb5073270c070cc645ba638","impliedFormat":1},{"version":"bd2afc69cf1d85cd950a99813bc7eff007d8afa496e7c2142a845cd1181d0474","impliedFormat":1},{"version":"558b462b23ea186d094dbff158d652acd58c0988c9fd53af81a8903412aa5901","impliedFormat":1},{"version":"0e984ae642a15973d652fd7b0d2712a284787d0d7a1db99aa49af0121e47f1df","impliedFormat":1},{"version":"0ad53ee208a23eef2a5cb3d85f2a9dc1019fd5e69179c4b0c02dc56c40d611c4","impliedFormat":1},{"version":"7a6898b26947bd356f33f4efef3eb23e61174d85dca19f41a8780d6bb4bfb405","impliedFormat":1},{"version":"9fe30349d26f34e85209fb06340bac34177f7eae3d6bb69dc12cd179d2c13ddf","impliedFormat":1},{"version":"d568c51d2c4360fd407445e39f4d86891dba04083402602bf5f24fd3969cacbb","impliedFormat":1},{"version":"b2483a924349ec835f4d778dd6787447a2f8bfbb651164851bff29d5b3d990a6","impliedFormat":1},{"version":"aae66889332cff4b2f7586c5c8758abc394d8d1c48f9b04b0c257e58f629d285","impliedFormat":1},{"version":"0f86c85130c64d6dbe6a9090bb3df71c4b0987bce4a08afe1ac4ece597655b9c","impliedFormat":1},{"version":"0ce28ad2671baed24517e1c1f4f2a986029137635bce788ee8fb542f002ac5b8","impliedFormat":1},{"version":"cd12e4fe77d24db98d66049360a4269299bcfb9dc3a1b47078ab1b4afac394cb","impliedFormat":1},{"version":"1589e5ac394b2b2e64264da3e1798d0e103b4f408f5bae1527d9e706f98269c7","impliedFormat":1},{"version":"ff8181aa0fde5ec2d737aecc5ebaa9e881379041f13e5ce1745620e17f78dcf9","impliedFormat":1},{"version":"0b2e54504b568c08df1e7db11c105786742866ba51e20486ab9b2286637d268f","impliedFormat":1},{"version":"bc1ffc3a2dca8ee715571739be3ec74d079e60505e1d0d2446e4978f6c75ba5c","impliedFormat":1},{"version":"770a40373470dff27b3f7022937ea2668a0854d7977c9d22073e1c62af537727","impliedFormat":1},{"version":"a0f8ce72cb02247a112ce4a2fa0f122478a8e99c90a5e6b676b41a68b1891ad2","impliedFormat":1},{"version":"6e957ea18b2bf951cf3995d115ad9bfa439e8d891aeb1afc901d793202c0b90d","impliedFormat":1},{"version":"a1c65bd78725f9172b5846c3c58ddf4bcbb43a30ab19e951f0102552fbfd3d5d","impliedFormat":1},{"version":"04718c7325e7df4bac9a6d026a0a2bd5a8b54501f274aaf93a03b5d1d0635bd1","impliedFormat":1},{"version":"405205f932d4e0ce688a380fa3150b1c7ff60e7fc89909e11a33eab7af240edb","impliedFormat":1},{"version":"566fc1a6616a522f8b45082032a33e6d37ff7df3f7d4d63c3cce9017d0345178","impliedFormat":1},{"version":"3b699b08db04559803b85aa0809748e61427b3d831f77834b8206e9f2ed20c93","impliedFormat":1},{"version":"b27242dd3af2a5548d0c7231db7da63d6373636d6c4e72d9b616adaa2acef7e1","impliedFormat":1},{"version":"e0ee7ba0571b83c53a3d6ec761cf391e7128d8f8f590f8832c28661b73c21b68","impliedFormat":1},{"version":"072bfd97fc61c894ef260723f43a416d49ebd8b703696f647c8322671c598873","impliedFormat":1},{"version":"e70875232f5d5528f1650dd6f5c94a5bed344ecf04bdbb998f7f78a3c1317d02","impliedFormat":1},{"version":"8e495129cb6cd8008de6f4ff8ce34fe1302a9e0dcff8d13714bd5593be3f7898","impliedFormat":99},{"version":"8079d851ffa1dbe193fe36643b3d67f4fdf8d360df4c900065788eff44bc15a7","impliedFormat":1},{"version":"c40b58c43a5df3bc9410d7c1359f740aa97448464407c55a26237ec5712645fc","signature":"18cee1b4db6c6caa50d98f11c8022dbd1c2534e5284554920aa5b421f16ba552"},{"version":"e516240bc1e5e9faef055432b900bc0d3c9ca7edce177fdabbc6c53d728cced8","impliedFormat":99},{"version":"5402765feacf44e052068ccb4535a346716fa1318713e3dae1af46e1e85f29a9","impliedFormat":99},{"version":"e16ec5d4796e7a765810efee80373675cedc4aa4814cf7272025a88addf5f0be","impliedFormat":99},{"version":"1f57157fcd45f9300c6efcfc53e2071fbe43396b0a7ed2701fbd1efb5599f07f","impliedFormat":99},{"version":"9f1886f3efddfac35babcada2d454acd4e23164345d11c979966c594af63468b","impliedFormat":99},{"version":"a3541c308f223863526df064933e408eba640c0208c7345769d7dc330ad90407","impliedFormat":99},{"version":"59af208befeb7b3c9ab0cb6c511e4fec54ede11922f2ffb7b497351deaf8aa2e","impliedFormat":99},{"version":"928b16f344f6cddaba565da8238f4cf2ddf12fe03eb426ab46a7560e9b3078fa","impliedFormat":99},{"version":"120bdf62bccef4ea96562a3d30dd60c9d55481662f5cf31c19725f56c0056b34","impliedFormat":99},{"version":"39e0da933908de42ba76ea1a92e4657305ae195804cfaa8760664e80baac2d6a","impliedFormat":99},{"version":"55ce6ca8df9d774d60cef58dd5d716807d5cc8410b8b065c06d3edac13f2e726","impliedFormat":99},{"version":"788a0faf3f28d43ce3793b4147b7539418a887b4a15a00ffb037214ed8f0b7f6","impliedFormat":99},{"version":"a3e66e7b8ccdab967cd4ada0f178151f1c42746eabb589a06958482fd4ed354e","impliedFormat":99},{"version":"bf45a2964a872c9966d06b971d0823daecbd707f97e927f2368ba54bb1b13a90","impliedFormat":99},{"version":"39973a12c57e06face646fb79462aabe8002e5523eec4e86e399228eb34b32c9","impliedFormat":99},{"version":"f01091e9b5028acfb38208113ae051fad8a0b4b8ec1f7137a2a5cf903c47eefc","impliedFormat":99},{"version":"b3e87824c9e7e3a3be7f76246e45c8d603ce83d116733047200b3aa95875445b","impliedFormat":99},{"version":"7e1f7f9ae14e362d41167dc861be6a8d76eca30dde3a9893c42946dc5a5fc686","impliedFormat":99},{"version":"9308ef3b9433063ac753a55c3f36d6d89fa38a8e6c51e05d9d8329c7f1174f24","impliedFormat":99},{"version":"cd3bb1aa24726a0abd67558fde5759fe968c3c6aa3ec7bad272e718851502894","impliedFormat":99},{"version":"1ae0f22c3b8420b5c2fec118f07b7ebd5ae9716339ab3477f63c603fe7a151c8","impliedFormat":99},{"version":"919ff537fff349930acc8ad8b875fd985a17582fb1beb43e2f558c541fd6ecd9","impliedFormat":99},{"version":"4e67811e45bae6c44bd6f13a160e4188d72fd643665f40c2ac3e8a27552d3fd9","impliedFormat":99},{"version":"3d1450fd1576c1073f6f4db9ebae5104e52e2c4599afb68d7d6c3d283bdbaf4f","impliedFormat":99},{"version":"c072af873c33ff11af126c56a846dfada32461b393983a72b6da7bff373e0002","impliedFormat":99},{"version":"de66e997ea5376d4aeb16d77b86f01c7b7d6d72fbb738241966459d42a4089e0","impliedFormat":99},{"version":"d77ea3b91e4bc44d710b7c9487c2c6158e8e5a3439d25fc578befeb27b03efd7","impliedFormat":99},{"version":"a3d5c695c3d1ebc9b0bd55804afaf2ac7c97328667cbeedf2c0861b933c45d3e","impliedFormat":99},{"version":"270724545d446036f42ddea422ee4d06963db1563ccc5e18b01c76f6e67968ae","impliedFormat":99},{"version":"85441c4f6883f7cfd1c5a211c26e702d33695acbabec8044e7fa6831ed501b45","impliedFormat":99},{"version":"0f268017a6b1891fdeea69c2a11d576646d7fd9cdfc8aac74d003cd7e87e9c5a","impliedFormat":99},{"version":"9ece188c336c80358742a5a0279f2f550175f5a07264349d8e0ce64db9701c0b","impliedFormat":99},{"version":"cf41b0fc7d57643d1a8d21af07b0247db2f2d7e2391c2e55929e9c00fbe6ab9a","impliedFormat":99},{"version":"11e7ddddd9eddaac56a6f23d8699ae7a94c2a55ae8c986fdabc719d3c3e875a1","impliedFormat":99},{"version":"dd129c2d348be7dbf9f15d34661defdfc11ee00628ca6f7161bead46095c6bc3","impliedFormat":99},{"version":"c38d8e7cfc64bbfc14a63346388249c1cfa2cc02166c5f37e5a57da4790ce27f","impliedFormat":99},{"version":"04fe74b54ee95b6cd9fac1094a9651fb4624963aa4b77051f9b4ce01a1de2603","signature":"77a8091fc8ef1f8b7608330b519ca909ac3e9ce1329af32c1153ef8851762ed2"},{"version":"7e3373dde2bba74076250204bd2af3aa44225717435e46396ef076b1954d2729","impliedFormat":1},{"version":"1c3dfad66ff0ba98b41c98c6f41af096fc56e959150bc3f44b2141fb278082fd","impliedFormat":1},{"version":"56208c500dcb5f42be7e18e8cb578f257a1a89b94b3280c506818fed06391805","impliedFormat":1},{"version":"0c94c2e497e1b9bcfda66aea239d5d36cd980d12a6d9d59e66f4be1fa3da5d5a","impliedFormat":1},{"version":"eb9271b3c585ea9dc7b19b906a921bf93f30f22330408ffec6df6a22057f3296","impliedFormat":1},{"version":"0205ee059bd2c4e12dcadc8e2cbd0132e27aeba84082a632681bd6c6c61db710","impliedFormat":1},{"version":"a694d38afadc2f7c20a8b1d150c68ac44d1d6c0229195c4d52947a89980126bc","impliedFormat":1},{"version":"9f1e00eab512de990ba27afa8634ca07362192063315be1f8166bc3dcc7f0e0f","impliedFormat":1},{"version":"9674788d4c5fcbd55c938e6719177ac932c304c94e0906551cc57a7942d2b53b","impliedFormat":1},{"version":"86dac6ce3fcd0a069b67a1ac9abdbce28588ea547fd2b42d73c1a2b7841cf182","impliedFormat":1},{"version":"4d34fbeadba0009ed3a1a5e77c99a1feedec65d88c4d9640910ff905e4e679f7","impliedFormat":1},{"version":"9d90361f495ed7057462bcaa9ae8d8dbad441147c27716d53b3dfeaea5bb7fc8","impliedFormat":1},{"version":"8fcc5571404796a8fe56e5c4d05049acdeac9c7a72205ac15b35cb463916d614","impliedFormat":1},{"version":"a3b3a1712610260c7ab96e270aad82bd7b28a53e5776f25a9a538831057ff44c","impliedFormat":1},{"version":"33a2af54111b3888415e1d81a7a803d37fada1ed2f419c427413742de3948ff5","impliedFormat":1},{"version":"d5a4fca3b69f2f740e447efb9565eecdbbe4e13f170b74dd4a829c5c9a5b8ebf","impliedFormat":1},{"version":"56f1e1a0c56efce87b94501a354729d0a0898508197cb50ab3e18322eb822199","impliedFormat":1},{"version":"8960e8c1730aa7efb87fcf1c02886865229fdbf3a8120dd08bb2305d2241bd7e","impliedFormat":1},{"version":"27bf82d1d38ea76a590cbe56873846103958cae2b6f4023dc59dd8282b66a38a","impliedFormat":1},{"version":"0daaab2afb95d5e1b75f87f59ee26f85a5f8d3005a799ac48b38976b9b521e69","impliedFormat":1},{"version":"2c378d9368abcd2eba8c29b294d40909845f68557bc0b38117e4f04fc56e5f9c","impliedFormat":1},{"version":"bb220eaac1677e2ad82ac4e7fd3e609a0c7b6f2d6d9c673a35068c97f9fcd5cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c60b14c297cc569c648ddaea70bc1540903b7f4da416edd46687e88a543515a1","impliedFormat":1},{"version":"94a802503ca276212549e04e4c6b11c4c14f4fa78722f90f7f0682e8847af434","impliedFormat":1},{"version":"9c0217750253e3bf9c7e3821e51cff04551c00e63258d5e190cf8bd3181d5d4a","impliedFormat":1},{"version":"5c2e7f800b757863f3ddf1a98d7521b8da892a95c1b2eafb48d652a782891677","impliedFormat":1},{"version":"21317aac25f94069dbcaa54492c014574c7e4d680b3b99423510b51c4e36035f","impliedFormat":1},{"version":"c61d8275c35a76cb12c271b5fa8707bb46b1e5778a370fd6037c244c4df6a725","impliedFormat":1},{"version":"c7793cb5cd2bef461059ca340fbcd19d7ddac7ab3dcc6cd1c90432fca260a6ae","impliedFormat":1},{"version":"fd3bf6d545e796ebd31acc33c3b20255a5bc61d963787fc8473035ea1c09d870","impliedFormat":1},{"version":"c7af51101b509721c540c86bb5fc952094404d22e8a18ced30c38a79619916fa","impliedFormat":1},{"version":"59c8f7d68f79c6e3015f8aee218282d47d3f15b85e5defc2d9d1961b6ffed7a0","impliedFormat":1},{"version":"93a2049cbc80c66aa33582ec2648e1df2df59d2b353d6b4a97c9afcbb111ccab","impliedFormat":1},{"version":"d04d359e40db3ae8a8c23d0f096ad3f9f73a9ef980f7cb252a1fdc1e7b3a2fb9","impliedFormat":1},{"version":"84aa4f0c33c729557185805aae6e0df3bd084e311da67a10972bbcf400321ff0","impliedFormat":1},{"version":"cf6cbe50e3f87b2f4fd1f39c0dc746b452d7ce41b48aadfdb724f44da5b6f6ed","impliedFormat":1},{"version":"3cf494506a50b60bf506175dead23f43716a088c031d3aa00f7220b3fbcd56c9","impliedFormat":1},{"version":"f2d47126f1544c40f2b16fc82a66f97a97beac2085053cf89b49730a0e34d231","impliedFormat":1},{"version":"724ac138ba41e752ae562072920ddee03ba69fe4de5dafb812e0a35ef7fb2c7e","impliedFormat":1},{"version":"e4eb3f8a4e2728c3f2c3cb8e6b60cadeb9a189605ee53184d02d265e2820865c","impliedFormat":1},{"version":"f16cb1b503f1a64b371d80a0018949135fbe06fb4c5f78d4f637b17921a49ee8","impliedFormat":1},{"version":"f4808c828723e236a4b35a1415f8f550ff5dec621f81deea79bf3a051a84ffd0","impliedFormat":1},{"version":"3b810aa3410a680b1850ab478d479c2f03ed4318d1e5bf7972b49c4d82bacd8d","impliedFormat":1},{"version":"0ce7166bff5669fcb826bc6b54b246b1cf559837ea9cc87c3414cc70858e6097","impliedFormat":1},{"version":"6ea095c807bc7cc36bc1774bc2a0ef7174bf1c6f7a4f6b499170b802ce214bfe","impliedFormat":1},{"version":"3549400d56ee2625bb5cc51074d3237702f1f9ffa984d61d9a2db2a116786c22","impliedFormat":1},{"version":"5327f9a620d003b202eff5db6be0b44e22079793c9a926e0a7a251b1dbbdd33f","impliedFormat":1},{"version":"b60f6734309d20efb9b0e0c7e6e68282ee451592b9c079dd1a988bb7a5eeb5e7","impliedFormat":1},{"version":"f4187a4e2973251fd9655598aa7e6e8bba879939a73188ee3290bb090cc46b15","impliedFormat":1},{"version":"44c1a26f578277f8ccef3215a4bd642a0a4fbbaf187cf9ae3053591c891fdc9c","impliedFormat":1},{"version":"a5989cd5e1e4ca9b327d2f93f43e7c981f25ee12a81c2ebde85ec7eb30f34213","impliedFormat":1},{"version":"f65b8fa1532dfe0ef2c261d63e72c46fe5f089b28edcd35b3526328d42b412b8","impliedFormat":1},{"version":"1060083aacfc46e7b7b766557bff5dafb99de3128e7bab772240877e5bfe849d","impliedFormat":1},{"version":"d61a3fa4243c8795139e7352694102315f7a6d815ad0aeb29074cfea1eb67e93","impliedFormat":1},{"version":"1f66b80bad5fa29d9597276821375ddf482c84cfb12e8adb718dc893ffce79e0","impliedFormat":1},{"version":"1ed8606c7b3612e15ff2b6541e5a926985cbb4d028813e969c1976b7f4133d73","impliedFormat":1},{"version":"c086ab778e9ba4b8dbb2829f42ef78e2b28204fc1a483e42f54e45d7a96e5737","impliedFormat":1},{"version":"dd0b9b00a39436c1d9f7358be8b1f32571b327c05b5ed0e88cc91f9d6b6bc3c9","impliedFormat":1},{"version":"a951a7b2224a4e48963762f155f5ad44ca1145f23655dde623ae312d8faeb2f2","impliedFormat":1},{"version":"cd960c347c006ace9a821d0a3cffb1d3fbc2518a4630fb3d77fe95f7fd0758b8","impliedFormat":1},{"version":"fe1f3b21a6cc1a6bc37276453bd2ac85910a8bdc16842dc49b711588e89b1b77","impliedFormat":1},{"version":"1a6a21ff41d509ab631dbe1ea14397c518b8551f040e78819f9718ef80f13975","impliedFormat":1},{"version":"0a55c554e9e858e243f714ce25caebb089e5cc7468d5fd022c1e8fa3d8e8173d","impliedFormat":1},{"version":"3a5e0fe9dcd4b1a9af657c487519a3c39b92a67b1b21073ff20e37f7d7852e32","impliedFormat":1},{"version":"977aeb024f773799d20985c6817a4c0db8fed3f601982a52d4093e0c60aba85f","impliedFormat":1},{"version":"d59cf5116848e162c7d3d954694f215b276ad10047c2854ed2ee6d14a481411f","impliedFormat":1},{"version":"50098be78e7cbfc324dfc04983571c80539e55e11a0428f83a090c13c41824a2","impliedFormat":1},{"version":"08e767d9d3a7e704a9ea5f057b0f020fd5880bc63fbb4aa6ffee73be36690014","impliedFormat":1},{"version":"dd6051c7b02af0d521857069c49897adb8595d1f0e94487d53ebc157294ef864","impliedFormat":1},{"version":"79c6a11f75a62151848da39f6098549af0dd13b22206244961048326f451b2a8","impliedFormat":1},{"version":"dcca73931cbcaddec4d6c01d39e9ed65d2bf95a06e1e17e8d69ea580df318847","signature":"9c61db3519329b720cd3f73efc6c31616faf8f8ed6a6ce21ddc49164889129c7"},{"version":"a189d741df65c764bd851c4c42c7d6ebfb8ad1a93fe13f90403d6fc0efd7564c","signature":"36b328f629e1ddb764e4620846380d6758f02514308a74eb128378fe6207da55"},{"version":"dfcf16e716338e9fe8cf790ac7756f61c85b83b699861df970661e97bf482692","impliedFormat":99},{"version":"bb703864a1bc9ca5ac3589ffd83785f6dc86f7f6c485c97d7ffd53438777cb9e","impliedFormat":1},{"version":"afd1edacaa854443388a01c10bb5e54727d42bf548eb7504660e063adbf78a84","signature":"45ccdc2d955ebe212fdfb23d17c76984d86c91ee90f269447ed2d2f24540c020"},{"version":"e7441be68f390975c6155c805cea8f54cc1b7f3656b6b9440ecbbbd7753499e6","impliedFormat":99},{"version":"3dd468a9cc452e3857900a9ac10856d2eda56d59d804323a1a948ea496158a64","signature":"c4678287db674a348a9d703a599dab7f1dd4768be8b2a7ce8ac02026abf87f7a"},{"version":"91b4ce96f6ad631a0a6920eb0ab928159ff01a439ae0e266ecdc9ea83126a195","impliedFormat":1},{"version":"88efe27bebddb62da9655a9f093e0c27719647e96747f16650489dc9671075d6","impliedFormat":1},{"version":"e348f128032c4807ad9359a1fff29fcbc5f551c81be807bfa86db5a45649b7ba","impliedFormat":1},{"version":"8ee6b07974528da39b7835556e12dd3198c0a13e4a9de321217cd2044f3de22e","impliedFormat":1},{"version":"5e1d8a07714f909beaaaf4d0ffe507345a99f2db967493dd8ebbfbb4f18e83ca","impliedFormat":1},{"version":"5f12132800d430adbe59b49c2c0354d85a71ada7d756e34250a655baa8ad4ae5","impliedFormat":1},{"version":"1996d1cd7d585a8359a35878f67abdd73cc35b1f675c9c6b147b202fdd8dfc3f","impliedFormat":1},{"version":"5a50dbfc042633fdb558e53b30b0a005e0b78e142a1fe1147a8d6618ca69ec99","impliedFormat":1},{"version":"a8b0f3d8b205c32395727230bff7849c947150fdd6288855cbe75bc662c4e236","impliedFormat":1},{"version":"6fb55bb881f4a7167649e6925df076f64a1db2f50632df4674e4621a9445c954","impliedFormat":1},{"version":"4374cefdde5c6e9bad52b0436e887b8325b8f407c12035194ad02c28f1553a3a","impliedFormat":1},{"version":"9b70cad270593f676aecfe4d1611dc766464f0b8138527b0ebbf1ff773578d69","impliedFormat":1},{"version":"b4f85bfb7e831703ac81737361842f1ae4d924b42c5d1af2bff93cca521de4d1","impliedFormat":1},{"version":"5fea76008a2d537ca09d569ffae4e08b991b4a5ff90e9f4783bc983584454ede","impliedFormat":1},{"version":"21575cdeaca6a2c2a0beb8c2ecbc981d9deb95f879f82dc7d6e325fe8737b5ba","impliedFormat":1},{"version":"40ec58f0fadd0b3981b3d383e1c12fa0680115ae9f018387fc2cfc0bbcf23204","impliedFormat":1},{"version":"849b9e7283b7309a4556c9b90bb8e2dfc27751f157798065bbc513dcddb09a8c","impliedFormat":1},{"version":"10e109212c7be8a9f66e988e5d6c2a8900c9d14bf6beadf5fa70d32ada3425cf","impliedFormat":1},{"version":"2b821aeb31e690092f8eae671dd961a9d0fd598ff4883ce0a600c90e9e8fa716","impliedFormat":1},{"version":"26602933b613e4df3868a6c82e14fffa2393a08531cb333ed27b151923462981","impliedFormat":1},{"version":"f57a588d8f6b3ce5c8b494f2dc759a8885eaee18e80a4952df47de45403fedbe","impliedFormat":1},{"version":"34735727b3fe7a0ed0651a0f88d06449163d1989a2b2de7f047473adc7c1c383","impliedFormat":1},{"version":"a5b13abc88ab3186e713c445e59e2f6eee20c6167943517bc2f56985d89b8c55","impliedFormat":1},{"version":"3844b45a774bafe226260cf0772376dce72121ebb801d03902c70a7f11da832b","impliedFormat":1},{"version":"7ae65fe95b18205e241e6695cb2c61c0828d660aca7d08f68781b439a800e6b8","impliedFormat":1},{"version":"e025419f23ccceafd7f5ab3141a86a6bb9fc3b33c44fe62b288d7b19baffc95b","impliedFormat":1},{"version":"369b7270eeeb37982203b2cb18c7302947b89bf5818c1d3d2e95a0418f02b74e","impliedFormat":1},{"version":"94f95d223e2783b0aef4d15d7f6990a6a550fe17d099c501395f690337f7105e","impliedFormat":1},{"version":"039bd8d1e0d151570b66e75ee152877fb0e2f42eca43718632ac195e6884be34","impliedFormat":1},{"version":"89fb1e22c3c98cbb86dc3e5949012bdae217f2b5d768a2cc74e1c4b413c25ad2","impliedFormat":1},{"version":"0ffd8bf120b14b8621d5e44374ee2ef43eb61eb799532f9f69679c839a72bcea","signature":"335e6e2b14f23ae70e5608a6b757c9c5f8ab9f1264a3c45d8194e9b05ef948bd"},{"version":"ce2ed38348f98d43d204dc925560fc17f874c4a6f0fecd48165f832a9964bb07","signature":"994534597e772bc135a36815cd936a8a63f27a29879b249eac14caf6e341364d"},{"version":"146b1e4efbe82a5818759b7c814c46b2ee0304cfb640bc84d3a51f987a2964a0","signature":"79df09b0ca5f4171bed6c7a0003aff6d9a5d2ca31c55d13dea4e7551c9e8d045"},{"version":"b843496b17a2bbd79c83809c73fd9c59fab53d3e361e04e52e2d489524eea764","impliedFormat":1},{"version":"a61194a2845f307263916752060412f080fb81259f7551fef4fbcbaad7b1e6d2","signature":"beea2d803c513304f23cdcd064aa4ec04e9b170d1633f6de0b9e5efb57d84285"},{"version":"9bf936220a96f6b4b6421d8a6c873a4000fcd4bbe3087009eb66e9b14ba67402","signature":"c864e85d82bcda21c3823324b9f852cb1ba3bf754af15388a10376ff8e0373f5"},{"version":"89b53c7301d85f91f25fa79fe6b2f27c3d8a18d2008a6e4978e65c0de0afadab","signature":"bc566a7efbfa5e0f5ae124a0d2c2f66291391387483da8da6278e9c4e642615e"},{"version":"6647dabde65059a98e8ff76f79eb686e34197557c2e64ee71b4ac5ac10ae5a67","signature":"118b46eb19d27e24b754ecda23cdae72b28a02b38734c3a1ca88d43ef535631c"},{"version":"4d9bffaca7e0f0880868bab5fd351f9e4d57fcc6567654c4c330516fea7932aa","impliedFormat":99},{"version":"867b845defb7e20f8928fdb8c5e2e7c473f701bb54e8f6624cdc3ebfec2e1c50","impliedFormat":99},{"version":"fcebf6064ee19f019cd677748310bbea900dc57fd8646427cf3418af70902d86","impliedFormat":99},{"version":"a93a11986f470d2c99c689ee101a04003d3b08d66e884782c147865017c15484","impliedFormat":99},{"version":"73b48bdb2946a0fa88829e88d32a69daee9b7c22db26524f89e9abe31f9b7cca","impliedFormat":99},{"version":"c59591435db37f16f2b6ff851d5abed0dea4acacd893da80ae270c7353845328","signature":"6e792f758932bcb89403f0efca4da19a9a8b02bf358f6f1846bdb49d0f1dbbd6"},{"version":"5b6350bc2042ac593a5866b5a6b5d1e5c13c87c3a5b00a35957354bac071df4d","signature":"ee8f7490d800ed0d3034d1323f59c9b3f84bc6eff75eab4659b22e0c24ed91d1"},{"version":"f7990d2b35828a207c0e0599ce4034ed4f38043f5c5f0d1fcbf8c81165e171a5","signature":"af84054c9bbd37500c026a1453c398ad1b002598c1e3adea687daa440a08933e"},{"version":"bfaf89cd935c7d0a4307b2a38e422e96369ebf264c4604fac9d7b1bb3064ddd3","signature":"c60274b5dcedff867dd1807e99c8f7ccaccb60da522c7906cf316baef83dc008"},{"version":"049af00eaaa8e81f03190edfb1efdce7143a83c70b10254b82dbc18efb55e64f","signature":"32f7c168ee545e9b432d2a861f4fb0bc645e21e88c774cedf54e72d1e3ccc049"},{"version":"3ba6aaaea131f31f3760a6f08be19f49a5249651aaaca602816266ea1a92ce39","signature":"c8492e39783ead38203aa0fac387e51a30b75f59effd500c06aadbf0d1d5a9b8"},{"version":"3b1af9d0a088cc300eec0d026a133bd09e98a22499251c7c2a7774bc7e7d2dee","signature":"f77dac591bbb93fd5024c15a73ccb10025f76c5b33a001760c579e13341ae834"},{"version":"22adff6a682588e50ae2fc66805aa5a34b3a9c0cc6c906558df4c517b04b514c","signature":"523cbf15f5b12fdc02dbcf3f59602623f8b49c4cc351678ce8b0106575cdddbf"},{"version":"db057b2f5fbed438f9ba8d622f846102b68856507be1fdc7aa9c012bcc94a0a5","signature":"7b19c29db78da0b295d52145f5a8c8926bdbe05316cddd45bfa891b869f8089e"},{"version":"1cfede71071e11bbc648d0313653f724d071922e661ae4ffc07261892b2df433","signature":"69c1bca7d225c0d1c0e98c3bb671e2caa0ad5bbb569d61dee619d523f6b33806"},{"version":"862634e7bf895b8f4704a19a20feeee34d1e482a015aa544007699511095dda4","signature":"f1af6c273588e3cab7ac3adb929621e150468d24e7d5a9fb38478fd15ea86150"},{"version":"274e2d57a93ac9a4adcd221af61148d5a60a35875fec8f15e44ec7e98a71ebab","signature":"56e8d9b65fa494ad967a207e0c661c7b7db20a2938d13b7f03094d2a0d912a3c"},"c3085aa7c267e4a12c37d1330a7a4a29c38f8626db50456302ec5a79c215f656",{"version":"60408f4159f05be198a1beb9aa2aa250df5551c50a7f68e15f0211430a5f656f","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"85e529e13fdeb532931202713e4eea179f01fab950fa8f73092332d4242f33ae","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"3c1d9ca0ceaaa29de976db76c30328fdcda50798d92f6b036508d0c608f96550","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"897b55c91ef49548e1edd80c5865f1b22a15e05c00ec1705489c78b8eec25c80","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"e8a763a4ebce303ca18717d2166d67950b0362ff3319def0c28ecb794b9f04d3","affectsGlobalScope":true,"impliedFormat":1},{"version":"ee31fab138939ef1903831933d524de9944511759778eedaaed56d6eb7f8697d","impliedFormat":1},{"version":"e45cc72cc9e7ad726ec83141fa4cd221d432062de34586ff107a0442ae28bf19","impliedFormat":1},{"version":"d9455aaf258770f187384ebe910495634a7fb904ee7a1c11755427a1e611eea8","impliedFormat":1},{"version":"309f3a357cc08760a602bd9b1177d4474426e6e2897a7295c898920198d968fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"ee31fab138939ef1903831933d524de9944511759778eedaaed56d6eb7f8697d","impliedFormat":1},{"version":"e45cc72cc9e7ad726ec83141fa4cd221d432062de34586ff107a0442ae28bf19","impliedFormat":1},{"version":"e24ee8b47c634d66662923650caa3d1510803debcd210085ca883f85cbecce4e","impliedFormat":1},{"version":"6d7ec38e96b4ad4d977e7376d9a92253927c1342ed6c102bd4dc4950deed1dd1","impliedFormat":1},{"version":"34810cb47e6bee7cd4bad2f174793f5926ba5889c5d180e29b02c1871a820476","affectsGlobalScope":true,"impliedFormat":1},{"version":"7115f1157a00937d712e042a011eb85e9d80b13eff78bac5f210ee852f96879d","impliedFormat":1},{"version":"0ac74c7586880e26b6a599c710b59284a284e084a2bbc82cd40fb3fbfdea71ae","affectsGlobalScope":true,"impliedFormat":1},{"version":"2ce12357dadbb8efc4e4ec4dab709c8071bf992722fc9adfea2fe0bd5b50923f","impliedFormat":1},{"version":"56c685ea062afe11fd775c31dc33acc449f780b17ba392ac154799323ebc9647","impliedFormat":1},{"version":"a21a5564fa4fc44ea813ce3055030b0060df12acca6a183d77b2090429be618a","impliedFormat":1},{"version":"b05b9ef20d18697e468c3ae9cecfff3f47e8976f9522d067047e3f236db06a41","affectsGlobalScope":true,"impliedFormat":1},{"version":"eec5e9a5629f6740aac21e49783a373a3767770ad559cd41285ebbb0db39a4a2","affectsGlobalScope":true,"impliedFormat":1},{"version":"1745f0b1ab53f414b4f8ebb2c6a902fda28d40f454edac8e92b4d7c974a2051c","affectsGlobalScope":true,"impliedFormat":1},{"version":"f7f5d83fe8eff2f1910e8b0437527041e5cc45aa15cda443f27dbadc3d5805e7","impliedFormat":1},{"version":"1a7a729938558fe198d979d3f53dece9c9112124b7b081a7fa0adcc98bf15fd8","impliedFormat":1},{"version":"c61c37176b7a6c043df76f437e402ea9abc9f19e9652a0d37629dfc8b7e83497","impliedFormat":1},{"version":"067f76ab5254b1bdfc94154730b7a30c12e3aad8b9d04ec62c0d6b7a1f40ea0e","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f95bf00d9d01de72cddbeabe5bdcb019248c209df4976a814756504afdb9291","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be812b06e518320ba82e2aff3ac2ca37370a9df917db708f081b9043fa3315","impliedFormat":1},{"version":"6d7ec38e96b4ad4d977e7376d9a92253927c1342ed6c102bd4dc4950deed1dd1","impliedFormat":1},{"version":"fc9e630f9302d0414ccd6c8ed2706659cff5ae454a56560c6122fa4a3fac5bbd","affectsGlobalScope":true,"impliedFormat":1},{"version":"7115f1157a00937d712e042a011eb85e9d80b13eff78bac5f210ee852f96879d","impliedFormat":1},{"version":"0ac74c7586880e26b6a599c710b59284a284e084a2bbc82cd40fb3fbfdea71ae","affectsGlobalScope":true,"impliedFormat":1},{"version":"2ce12357dadbb8efc4e4ec4dab709c8071bf992722fc9adfea2fe0bd5b50923f","impliedFormat":1},{"version":"56c685ea062afe11fd775c31dc33acc449f780b17ba392ac154799323ebc9647","impliedFormat":1},{"version":"085b6073465a6358e693823dcc03128f359e28780e8a497bf5fcf1b577b5c960","impliedFormat":1},{"version":"b05b9ef20d18697e468c3ae9cecfff3f47e8976f9522d067047e3f236db06a41","affectsGlobalScope":true,"impliedFormat":1},{"version":"b12b645134e106bea2c135a8a64c933329e195d3b8ca2fc6eeeec73dd8836924","affectsGlobalScope":true,"impliedFormat":1},{"version":"fbe8d85bf780a22fb2838e9807c53b3ea2610f46ae925a771eba421c44c5a4c6","affectsGlobalScope":true,"impliedFormat":1},{"version":"f7f5d83fe8eff2f1910e8b0437527041e5cc45aa15cda443f27dbadc3d5805e7","impliedFormat":1},{"version":"f5e8617ad68d001f3e26035ee699dad75df20ada6adc88524cb631d9f9b871fa","impliedFormat":1},{"version":"c61c37176b7a6c043df76f437e402ea9abc9f19e9652a0d37629dfc8b7e83497","impliedFormat":1},{"version":"067f76ab5254b1bdfc94154730b7a30c12e3aad8b9d04ec62c0d6b7a1f40ea0e","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f95bf00d9d01de72cddbeabe5bdcb019248c209df4976a814756504afdb9291","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be812b06e518320ba82e2aff3ac2ca37370a9df917db708f081b9043fa3315","impliedFormat":1},{"version":"f60e3e3060207ac982da13363181fd7ee4beecc19a7c569f0d6bb034331066c2","impliedFormat":1},{"version":"17230b34bb564a3a2e36f9d3985372ccab4ad1722df2c43f7c5c2b553f68e5db","impliedFormat":1},{"version":"6e5c9272f6b3783be7bdddaf207cccdb8e033be3d14c5beacc03ae9d27d50929","impliedFormat":1},{"version":"9b4f7ff9681448c72abe38ea8eefd7ffe0c3aefe495137f02012a08801373f71","impliedFormat":1},{"version":"0dfe35191a04e8f9dc7caeb9f52f2ee07402736563d12cbccd15fb5f31ac877f","impliedFormat":1},{"version":"798367363a3274220cbed839b883fe2f52ba7197b25e8cb2ac59c1e1fd8af6b7","impliedFormat":1},{"version":"fe62b82c98a4d5bca3f8de616b606d20211b18c14e881bb6856807d9ab58131b","impliedFormat":1},{"version":"e0c868a08451c879984ccf4d4e3c1240b3be15af8988d230214977a3a3dad4ce","impliedFormat":1},{"version":"6fc1a4f64372593767a9b7b774e9b3b92bf04e8785c3f9ea98973aa9f4bbe490","impliedFormat":1},{"version":"ff09b6fbdcf74d8af4e131b8866925c5e18d225540b9b19ce9485ca93e574d84","impliedFormat":1},{"version":"d5895252efa27a50f134a9b580aa61f7def5ab73d0a8071f9b5bf9a317c01c2d","impliedFormat":1},{"version":"2c378d9368abcd2eba8c29b294d40909845f68557bc0b38117e4f04fc56e5f9c","impliedFormat":1},{"version":"0c94c2e497e1b9bcfda66aea239d5d36cd980d12a6d9d59e66f4be1fa3da5d5a","impliedFormat":1},{"version":"bb220eaac1677e2ad82ac4e7fd3e609a0c7b6f2d6d9c673a35068c97f9fcd5cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"56208c500dcb5f42be7e18e8cb578f257a1a89b94b3280c506818fed06391805","impliedFormat":1},{"version":"1f366bde16e0513fa7b64f87f86689c4d36efd85afce7eb24753e9c99b91c319","impliedFormat":1},{"version":"87d9d29dbc745f182683f63187bf3d53fd8673e5fca38ad5eaab69798ed29fbc","impliedFormat":1},{"version":"7fa8d75d229eeaee235a801758d9c694e94405013fe77d5d1dd8e3201fc414f1","impliedFormat":1}],"root":[182,487,489,514,[602,607],667,668,1353,1354,[1361,1363],1365,[1370,1379],[1383,1395],[1402,1412],[1460,1464],1470,[1474,1478],[1481,1483],1485,[1491,1497],[1504,1518],[1540,1559],[1565,1581],[1615,1629],1888,1925,1996,1997,2000,2002,[2033,2035],[2037,2040],[2046,2062]],"options":{"allowJs":true,"esModuleInterop":true,"jsx":1,"module":99,"skipLibCheck":true,"strict":true,"target":4},"referencedMap":[[2061,1],[2059,2],[2060,3],[2062,4],[2058,5],[182,6],[487,7],[489,8],[1511,9],[1512,10],[1353,11],[1354,12],[1362,13],[1363,14],[1516,15],[1514,16],[1518,17],[1616,18],[1485,19],[605,20],[1505,21],[1617,22],[1622,23],[1623,24],[606,20],[1365,25],[668,26],[1624,22],[1625,27],[1576,28],[1618,29],[1580,30],[1581,31],[1565,32],[1542,33],[1550,34],[1370,35],[1541,36],[1549,37],[1547,38],[1548,39],[1577,40],[1552,41],[1559,42],[1553,41],[1557,43],[1558,44],[1570,45],[1571,46],[1626,47],[1572,48],[1567,49],[1569,50],[1568,51],[1504,52],[1496,53],[1497,54],[1493,55],[1579,56],[1494,57],[1495,58],[1615,59],[1619,60],[1483,61],[1627,62],[1628,63],[1510,64],[1545,65],[1574,66],[1513,67],[1517,64],[1629,62],[1491,68],[1888,69],[1506,53],[1925,70],[1996,71],[1620,62],[1997,65],[2000,72],[1543,73],[1578,74],[1546,75],[2002,76],[1566,73],[1555,77],[2033,78],[2034,67],[2035,79],[2037,80],[1507,81],[1508,68],[2038,62],[2039,73],[2040,81],[2046,82],[1575,67],[2047,67],[1515,53],[2048,62],[1540,83],[1551,67],[1544,73],[1509,84],[2049,73],[2051,85],[1621,43],[1556,67],[1481,86],[1492,57],[2052,67],[2053,53],[2054,67],[1554,53],[1460,73],[2055,87],[2057,88],[2056,68],[1482,67],[1573,66],[1371,45],[1372,55],[604,55],[1389,41],[1390,55],[602,89],[1462,90],[1463,90],[1391,91],[1392,45],[1395,92],[1404,93],[1405,93],[1406,93],[2050,45],[1407,94],[1408,45],[1409,95],[1410,96],[1411,97],[1412,45],[1461,98],[667,99],[1464,55],[1470,100],[1474,101],[1475,102],[1361,103],[1375,104],[1376,104],[1377,105],[1374,106],[1476,55],[1385,41],[1383,107],[1477,108],[603,12],[1379,109],[1403,110],[1387,111],[1478,112],[1394,113],[1384,114],[1386,115],[1378,55],[1402,116],[1393,55],[607,55],[1373,55],[1388,117],[514,118],[1024,55],[1466,119],[1467,120],[1465,55],[1469,121],[1468,55],[244,55],[1413,45],[1417,122],[1422,123],[1426,124],[1423,124],[1424,125],[1425,126],[1416,125],[1431,127],[1414,45],[1421,128],[1432,45],[1418,124],[1433,127],[1419,124],[1435,129],[1436,130],[1434,124],[1430,131],[1437,132],[1439,133],[1440,134],[1441,45],[1442,135],[1428,136],[1420,124],[1415,45],[1443,125],[1444,137],[1429,125],[1445,125],[1446,135],[1447,124],[1448,125],[1449,45],[1450,125],[1451,137],[1452,138],[1454,139],[1453,124],[1455,140],[1456,130],[1438,124],[1427,55],[517,55],[515,55],[516,141],[533,55],[518,142],[534,143],[2087,144],[2103,145],[1946,55],[1929,146],[1947,147],[1928,55],[618,148],[1582,148],[2110,149],[2109,150],[185,55],[198,151],[346,45],[199,152],[197,45],[347,153],[195,154],[196,155],[183,55],[186,156],[344,45],[319,45],[608,55],[1359,157],[1360,158],[1358,159],[1357,55],[1480,55],[1352,160],[1351,161],[1350,162],[1349,55],[1041,163],[1346,164],[1040,165],[1037,166],[1039,167],[1038,55],[1023,168],[1022,169],[988,55],[987,170],[986,55],[1484,171],[488,172],[1355,55],[2071,173],[2085,55],[2081,174],[2073,175],[2084,176],[2072,177],[2070,178],[2074,179],[2068,180],[2075,55],[2086,181],[2076,182],[2080,183],[2082,184],[2069,185],[2083,186],[2077,187],[2078,188],[2079,189],[2067,190],[2101,55],[2097,191],[2089,192],[2100,193],[2088,194],[2066,195],[2090,196],[2064,197],[2091,55],[2102,198],[2092,199],[2096,200],[2098,201],[2065,202],[2099,203],[2093,204],[2094,205],[2095,206],[1459,207],[1458,208],[1381,55],[1999,209],[184,55],[1717,210],[1696,211],[1793,55],[1697,212],[1633,210],[1634,55],[1635,55],[1636,55],[1637,55],[1638,55],[1639,55],[1640,55],[1641,55],[1642,55],[1643,55],[1644,55],[1645,210],[1646,210],[1647,55],[1648,55],[1649,55],[1650,55],[1651,55],[1652,55],[1653,55],[1654,55],[1655,55],[1657,55],[1656,55],[1658,55],[1659,55],[1660,210],[1661,55],[1662,55],[1663,210],[1664,55],[1665,55],[1666,210],[1667,55],[1668,210],[1669,210],[1670,210],[1671,55],[1672,210],[1673,210],[1674,210],[1675,210],[1676,210],[1678,210],[1679,55],[1680,55],[1677,210],[1681,210],[1682,55],[1683,55],[1684,55],[1685,55],[1686,55],[1687,55],[1688,55],[1689,55],[1690,55],[1691,55],[1692,55],[1693,210],[1694,55],[1695,55],[1698,213],[1699,210],[1700,210],[1701,214],[1702,215],[1703,210],[1704,210],[1705,210],[1706,210],[1709,210],[1707,55],[1708,55],[1631,55],[1710,55],[1711,55],[1712,55],[1713,55],[1714,55],[1715,55],[1716,55],[1718,216],[1719,55],[1720,55],[1721,55],[1723,55],[1722,55],[1724,55],[1725,55],[1726,55],[1727,210],[1728,55],[1729,55],[1730,55],[1731,55],[1732,210],[1733,210],[1735,210],[1734,210],[1736,55],[1737,55],[1738,55],[1739,55],[1886,217],[1740,210],[1741,210],[1742,55],[1743,55],[1744,55],[1745,55],[1746,55],[1747,55],[1748,55],[1749,55],[1750,55],[1751,55],[1752,55],[1753,55],[1754,210],[1755,55],[1756,55],[1757,55],[1758,55],[1759,55],[1760,55],[1761,55],[1762,55],[1763,55],[1764,55],[1765,210],[1766,55],[1767,55],[1768,55],[1769,55],[1770,55],[1771,55],[1772,55],[1773,55],[1774,55],[1775,210],[1776,55],[1777,55],[1778,55],[1779,55],[1780,55],[1781,55],[1782,55],[1783,55],[1784,210],[1785,55],[1786,55],[1787,55],[1788,55],[1789,55],[1790,55],[1791,210],[1792,55],[1794,218],[1630,210],[1795,55],[1796,210],[1797,55],[1798,55],[1799,55],[1800,55],[1801,55],[1802,55],[1803,55],[1804,55],[1805,55],[1806,210],[1807,55],[1808,55],[1809,55],[1810,55],[1811,55],[1812,55],[1813,55],[1818,219],[1816,220],[1817,221],[1815,222],[1814,210],[1819,55],[1820,55],[1821,210],[1822,55],[1823,55],[1824,55],[1825,55],[1826,55],[1827,55],[1828,55],[1829,55],[1830,55],[1831,210],[1832,210],[1833,55],[1834,55],[1835,55],[1836,210],[1837,55],[1838,210],[1839,55],[1840,216],[1841,55],[1842,55],[1843,55],[1844,55],[1845,55],[1846,55],[1847,55],[1848,55],[1849,55],[1850,210],[1851,210],[1852,55],[1853,55],[1854,55],[1855,55],[1856,55],[1857,55],[1858,55],[1859,55],[1860,55],[1861,55],[1862,55],[1863,55],[1864,210],[1865,210],[1866,55],[1867,55],[1868,210],[1869,55],[1870,55],[1871,55],[1872,55],[1873,55],[1874,55],[1875,55],[1876,55],[1877,55],[1878,55],[1879,55],[1880,55],[1881,210],[1632,223],[1882,55],[1883,55],[1884,55],[1885,55],[181,224],[81,225],[82,226],[1331,227],[1335,228],[1280,229],[1095,55],[1045,230],[1329,231],[1330,232],[1043,55],[1332,233],[1117,234],[1060,235],[1083,236],[1092,237],[1063,237],[1064,238],[1065,238],[1091,239],[1066,240],[1067,238],[1073,241],[1068,242],[1069,238],[1070,238],[1093,243],[1062,244],[1071,237],[1072,242],[1074,245],[1075,245],[1076,242],[1077,238],[1078,237],[1079,238],[1080,246],[1081,246],[1082,238],[1104,247],[1112,248],[1090,249],[1120,250],[1084,251],[1086,252],[1087,249],[1098,253],[1106,254],[1111,255],[1108,256],[1113,257],[1101,258],[1102,259],[1109,260],[1110,261],[1116,262],[1107,263],[1085,233],[1118,264],[1061,233],[1105,265],[1103,266],[1089,267],[1088,249],[1119,268],[1094,269],[1114,55],[1115,270],[1334,271],[1044,233],[1155,55],[1172,272],[1121,273],[1146,274],[1153,275],[1122,275],[1123,275],[1124,276],[1152,277],[1125,278],[1140,275],[1126,279],[1127,279],[1128,276],[1129,275],[1130,276],[1131,275],[1154,280],[1132,275],[1133,275],[1134,281],[1135,275],[1136,275],[1137,281],[1138,276],[1139,275],[1141,282],[1142,281],[1143,275],[1144,276],[1145,275],[1167,283],[1163,284],[1151,285],[1175,286],[1147,287],[1148,285],[1164,288],[1156,289],[1165,290],[1162,291],[1160,292],[1166,293],[1159,294],[1171,295],[1161,296],[1173,297],[1168,298],[1157,299],[1150,300],[1149,285],[1174,301],[1158,269],[1169,55],[1170,302],[1047,303],[1237,304],[1176,305],[1211,306],[1220,307],[1177,308],[1178,308],[1179,309],[1180,308],[1219,310],[1181,311],[1182,312],[1183,313],[1184,308],[1221,314],[1222,315],[1185,308],[1187,316],[1188,307],[1190,317],[1191,318],[1192,318],[1193,309],[1194,308],[1195,308],[1196,314],[1197,309],[1198,309],[1199,318],[1200,308],[1201,307],[1202,308],[1203,309],[1204,319],[1189,320],[1205,308],[1206,309],[1207,308],[1208,308],[1209,308],[1210,308],[1339,321],[1232,322],[1218,323],[1242,324],[1212,325],[1214,326],[1215,323],[1336,327],[1225,328],[1231,329],[1227,330],[1233,331],[1337,332],[1338,259],[1228,333],[1230,334],[1236,335],[1226,336],[1213,233],[1238,337],[1186,233],[1224,338],[1229,339],[1217,340],[1216,323],[1239,341],[1240,55],[1241,342],[1223,269],[1234,55],[1235,343],[1341,344],[1342,345],[1340,346],[1056,347],[1049,348],[1099,233],[1096,349],[1100,350],[1097,351],[1291,352],[1268,353],[1274,354],[1243,354],[1244,354],[1245,355],[1273,356],[1246,357],[1261,354],[1247,358],[1248,358],[1249,355],[1250,354],[1251,359],[1252,354],[1275,360],[1253,354],[1254,354],[1255,361],[1256,354],[1257,354],[1258,361],[1259,355],[1260,354],[1262,362],[1263,361],[1264,354],[1265,355],[1266,354],[1267,354],[1288,363],[1279,364],[1294,365],[1269,366],[1270,367],[1283,368],[1276,369],[1287,370],[1278,371],[1286,372],[1285,373],[1290,374],[1277,375],[1292,376],[1289,377],[1284,378],[1272,379],[1271,367],[1293,380],[1282,381],[1281,382],[1052,383],[1054,384],[1053,383],[1055,383],[1058,385],[1057,386],[1059,387],[1050,388],[1327,389],[1295,390],[1320,391],[1324,392],[1323,393],[1296,394],[1325,395],[1316,396],[1317,392],[1318,397],[1319,398],[1304,399],[1312,400],[1322,401],[1328,402],[1297,403],[1298,401],[1301,404],[1307,405],[1311,406],[1309,407],[1313,408],[1302,409],[1305,410],[1310,411],[1326,412],[1308,413],[1306,414],[1303,415],[1321,416],[1299,417],[1315,418],[1300,269],[1314,419],[1048,269],[1046,420],[1051,421],[1333,55],[1923,422],[1924,423],[1889,55],[1897,424],[1891,425],[1898,55],[1920,426],[1895,427],[1919,428],[1916,429],[1899,430],[1900,55],[1893,55],[1890,55],[1921,431],[1917,432],[1901,55],[1918,433],[1902,434],[1904,435],[1905,436],[1894,437],[1906,438],[1907,437],[1909,438],[1910,439],[1911,440],[1913,441],[1908,442],[1914,443],[1915,444],[1892,445],[1912,446],[1896,447],[1903,55],[1922,448],[625,55],[626,449],[623,55],[624,55],[1364,55],[1488,450],[1489,451],[646,452],[643,55],[645,453],[644,454],[642,455],[641,456],[659,457],[658,458],[664,459],[662,458],[663,460],[654,461],[653,462],[2036,45],[1021,463],[990,464],[1000,464],[991,464],[1001,464],[992,464],[993,464],[1008,464],[1007,464],[1009,464],[1010,464],[1002,464],[994,464],[1003,464],[995,464],[1004,464],[996,464],[998,464],[1006,465],[999,464],[1005,465],[1011,465],[997,464],[1012,464],[1017,464],[1018,464],[1013,464],[989,55],[1019,55],[1015,464],[1014,464],[1016,464],[1020,464],[826,55],[948,466],[827,467],[828,468],[967,469],[968,470],[969,471],[970,472],[971,473],[972,474],[960,475],[955,476],[956,477],[957,478],[959,473],[958,479],[954,475],[961,476],[963,480],[962,481],[953,473],[952,482],[966,475],[949,476],[950,483],[951,484],[965,473],[964,485],[829,476],[824,486],[945,487],[825,488],[947,489],[946,490],[852,491],[849,492],[909,493],[887,494],[866,495],[794,496],[985,497],[931,498],[974,499],[973,467],[751,500],[760,501],[764,502],[873,503],[784,504],[755,505],[766,506],[863,504],[843,504],[878,507],[942,504],[737,508],[781,508],[750,509],[738,508],[811,504],[789,510],[790,511],[759,512],[768,513],[769,508],[770,514],[772,515],[802,516],[835,504],[937,504],[739,504],[818,517],[752,518],[761,508],[763,519],[803,508],[804,520],[805,521],[806,521],[796,522],[799,523],[756,524],[773,504],[939,504],[740,504],[774,504],[775,525],[776,504],[736,504],[815,526],[778,527],[882,528],[880,504],[881,529],[883,530],[779,504],[936,504],[941,504],[810,531],[762,500],[780,504],[812,532],[813,533],[777,504],[793,504],[981,534],[943,535],[735,55],[844,504],[814,504],[864,504],[782,536],[783,537],[807,504],[872,538],[865,504],[870,539],[871,540],[757,541],[910,504],[819,542],[754,504],[786,543],[749,544],[820,521],[753,518],[765,508],[808,545],[741,508],[785,504],[792,504],[801,546],[788,547],[797,504],[787,548],[742,521],[800,504],[940,504],[938,504],[758,541],[816,549],[817,504],[771,504],[798,504],[911,550],[809,504],[767,504],[791,551],[847,552],[869,553],[854,55],[836,554],[833,555],[923,556],[888,557],[857,558],[912,559],[851,560],[926,561],[856,562],[874,563],[889,564],[914,565],[929,566],[886,567],[853,568],[861,569],[850,570],[885,571],[984,572],[924,573],[913,574],[845,575],[922,576],[975,577],[976,577],[980,578],[979,579],[830,580],[978,577],[977,577],[876,581],[879,582],[921,583],[920,584],[744,55],[877,585],[860,586],[918,587],[743,55],[848,588],[884,589],[925,590],[747,55],[859,591],[916,592],[867,593],[855,594],[917,595],[875,596],[915,597],[842,598],[868,599],[919,600],[745,55],[858,601],[822,602],[944,603],[823,604],[927,605],[934,606],[935,607],[933,608],[901,609],[831,610],[902,611],[932,612],[838,613],[840,614],[890,615],[894,616],[841,617],[839,617],[893,618],[834,619],[895,620],[896,621],[897,622],[905,623],[903,624],[898,625],[899,626],[900,627],[906,628],[904,629],[837,630],[892,631],[907,632],[908,633],[891,634],[846,635],[832,486],[795,636],[982,637],[983,55],[928,638],[930,490],[821,55],[862,55],[746,55],[748,639],[2042,640],[2043,640],[2041,55],[1366,45],[1610,641],[1584,642],[1585,643],[1586,643],[1587,643],[1588,643],[1589,643],[1590,643],[1591,643],[1592,643],[1593,643],[1594,643],[1608,644],[1595,643],[1596,643],[1597,643],[1598,643],[1599,643],[1600,643],[1601,643],[1602,643],[1604,643],[1605,643],[1603,643],[1606,643],[1607,643],[1609,643],[1583,645],[1487,646],[1486,55],[1490,647],[1025,55],[1028,648],[1030,649],[1032,650],[1031,55],[1036,651],[1033,648],[1034,652],[1035,652],[1027,652],[1026,653],[1029,55],[1479,45],[193,654],[434,655],[439,5],[441,656],[220,657],[248,658],[417,659],[243,660],[231,55],[212,55],[218,55],[407,661],[272,662],[219,55],[386,663],[253,664],[254,665],[343,666],[404,667],[359,668],[411,669],[412,670],[410,671],[409,55],[408,672],[250,673],[221,674],[293,55],[294,675],[216,55],[232,676],[222,677],[277,676],[274,676],[205,676],[246,678],[245,55],[416,679],[426,55],[211,55],[320,680],[321,681],[314,45],[462,55],[323,55],[324,682],[315,683],[336,45],[467,684],[466,685],[461,55],[403,686],[402,55],[460,687],[316,45],[355,688],[353,689],[463,55],[465,690],[464,55],[354,691],[455,692],[458,693],[284,694],[283,695],[282,696],[470,45],[281,697],[266,55],[473,55],[1472,698],[1471,55],[476,55],[475,45],[477,699],[201,55],[413,700],[414,701],[415,702],[234,55],[210,703],[200,55],[203,704],[335,705],[334,706],[325,55],[326,55],[333,55],[328,55],[331,707],[327,55],[329,708],[332,709],[330,708],[217,55],[208,55],[209,676],[256,55],[341,682],[361,682],[433,710],[442,711],[446,712],[420,713],[419,55],[269,55],[478,714],[429,715],[317,716],[318,717],[309,718],[299,55],[340,719],[300,720],[342,721],[338,722],[337,55],[339,55],[352,723],[421,724],[422,725],[301,726],[306,727],[297,728],[399,729],[428,730],[276,731],[376,732],[206,733],[427,734],[202,660],[257,55],[258,735],[388,736],[255,55],[387,737],[194,55],[381,738],[233,55],[295,739],[377,55],[207,55],[259,55],[385,740],[215,55],[264,741],[305,742],[418,743],[304,55],[384,55],[390,744],[391,745],[213,55],[393,746],[395,747],[394,748],[236,55],[383,733],[397,749],[382,750],[389,751],[224,55],[227,55],[225,55],[229,55],[226,55],[228,55],[230,752],[223,55],[369,753],[368,55],[374,754],[370,755],[373,756],[372,756],[375,754],[371,755],[263,757],[362,758],[425,759],[480,55],[450,760],[452,761],[303,55],[451,762],[423,724],[479,763],[322,724],[214,55],[302,764],[260,765],[261,766],[262,767],[292,768],[398,768],[278,768],[363,769],[279,769],[252,770],[251,55],[367,771],[366,772],[365,773],[364,774],[424,775],[313,776],[349,777],[312,778],[345,779],[348,780],[406,781],[405,782],[401,783],[358,784],[360,785],[357,786],[396,787],[351,55],[438,55],[350,788],[400,55],[265,789],[298,700],[296,790],[267,791],[270,792],[474,55],[268,793],[271,793],[436,55],[435,55],[437,55],[472,55],[273,794],[311,45],[192,55],[356,795],[249,55],[238,796],[307,55],[444,45],[454,797],[291,45],[448,682],[290,798],[431,799],[289,797],[204,55],[456,800],[287,45],[288,45],[280,55],[237,55],[286,801],[285,802],[235,803],[308,804],[275,804],[392,55],[379,805],[378,55],[440,55],[310,45],[432,806],[187,45],[190,807],[191,808],[188,45],[189,55],[247,809],[242,810],[241,55],[240,811],[239,55],[430,812],[443,813],[445,814],[447,815],[1473,816],[449,817],[453,818],[486,819],[457,819],[485,820],[459,821],[468,822],[469,823],[471,824],[481,825],[484,703],[483,55],[482,826],[621,827],[634,828],[619,55],[620,829],[635,830],[630,831],[631,832],[629,833],[633,834],[627,835],[622,836],[632,837],[628,828],[2108,838],[2105,826],[2107,839],[2106,55],[2104,55],[506,840],[504,841],[505,842],[493,843],[494,841],[501,844],[492,845],[497,846],[507,55],[498,847],[503,848],[509,849],[508,850],[491,851],[499,852],[500,853],[495,854],[502,840],[496,855],[1042,856],[640,857],[637,858],[638,55],[639,55],[636,859],[1457,860],[1887,861],[2003,55],[2018,862],[2019,862],[2032,863],[2020,864],[2021,864],[2022,865],[2016,866],[2014,867],[2005,55],[2009,868],[2013,869],[2011,870],[2017,871],[2006,872],[2007,873],[2008,874],[2010,875],[2012,876],[2015,877],[2023,864],[2024,864],[2025,864],[2026,862],[2027,864],[2028,864],[2004,864],[2029,55],[2031,878],[2030,864],[1503,879],[1499,45],[1500,45],[1498,55],[1501,880],[1502,879],[1614,881],[1613,882],[2045,883],[2044,884],[1524,55],[1538,885],[1519,45],[1521,886],[1523,887],[1522,888],[1520,55],[1525,55],[1526,55],[1527,55],[1528,55],[1529,55],[1530,55],[1531,55],[1532,55],[1533,55],[1534,889],[1536,890],[1537,890],[1535,55],[1539,891],[1969,892],[1971,893],[1961,894],[1966,895],[1967,896],[1973,897],[1968,898],[1965,899],[1964,900],[1963,901],[1974,902],[1931,895],[1932,895],[1972,895],[1977,903],[1987,904],[1981,904],[1989,904],[1993,904],[1979,905],[1980,904],[1982,904],[1985,904],[1988,904],[1984,906],[1986,904],[1990,45],[1983,895],[1978,907],[1940,45],[1944,45],[1934,895],[1937,45],[1942,895],[1943,908],[1936,909],[1939,45],[1941,45],[1938,910],[1927,45],[1926,45],[1995,911],[1992,912],[1958,913],[1957,895],[1955,45],[1956,895],[1959,914],[1960,915],[1953,45],[1949,916],[1952,895],[1951,895],[1950,895],[1945,895],[1954,916],[1991,895],[1970,917],[1976,918],[1975,919],[1994,55],[1962,55],[1935,55],[1933,920],[661,921],[660,922],[648,923],[647,924],[666,925],[665,926],[657,927],[656,458],[655,928],[1612,929],[1611,930],[380,931],[1380,45],[490,55],[652,932],[650,933],[651,934],[649,55],[1382,55],[512,935],[511,55],[510,55],[513,936],[615,937],[614,55],[79,55],[80,55],[13,55],[14,55],[16,55],[15,55],[2,55],[17,55],[18,55],[19,55],[20,55],[21,55],[22,55],[23,55],[24,55],[3,55],[25,55],[26,55],[4,55],[27,55],[31,55],[28,55],[29,55],[30,55],[32,55],[33,55],[34,55],[5,55],[35,55],[36,55],[37,55],[38,55],[6,55],[42,55],[39,55],[40,55],[41,55],[43,55],[7,55],[44,55],[49,55],[50,55],[45,55],[46,55],[47,55],[48,55],[8,55],[54,55],[51,55],[52,55],[53,55],[55,55],[9,55],[56,55],[57,55],[58,55],[60,55],[59,55],[61,55],[62,55],[10,55],[63,55],[64,55],[65,55],[11,55],[66,55],[67,55],[68,55],[69,55],[70,55],[1,55],[71,55],[72,55],[12,55],[76,55],[74,55],[78,55],[73,55],[77,55],[75,55],[2063,938],[617,939],[613,55],[616,940],[2001,209],[610,941],[609,148],[612,942],[611,943],[1930,944],[1948,945],[1561,946],[1562,55],[1560,55],[1563,947],[1564,948],[532,949],[524,950],[531,951],[526,55],[527,55],[525,952],[528,953],[519,55],[520,55],[521,949],[523,954],[529,55],[530,955],[522,956],[728,957],[731,958],[729,958],[725,957],[732,959],[733,960],[730,958],[726,961],[727,962],[721,963],[673,964],[675,965],[719,55],[674,966],[720,967],[724,968],[722,55],[676,964],[677,55],[718,969],[672,970],[669,55],[723,971],[670,972],[671,55],[734,973],[678,974],[679,974],[680,974],[681,974],[682,974],[683,974],[684,974],[685,974],[686,974],[687,974],[688,974],[690,974],[689,974],[691,974],[692,974],[693,974],[717,975],[694,974],[695,974],[696,974],[697,974],[698,974],[699,974],[700,974],[701,974],[702,974],[704,974],[703,974],[705,974],[706,974],[707,974],[708,974],[709,974],[710,974],[711,974],[712,974],[713,974],[714,974],[715,974],[716,974],[599,976],[594,977],[597,978],[595,978],[591,977],[598,979],[596,978],[592,980],[593,981],[587,982],[539,983],[541,984],[585,55],[540,985],[586,986],[590,987],[588,55],[542,983],[543,55],[584,988],[538,989],[535,55],[589,990],[536,991],[537,55],[544,992],[545,992],[546,992],[547,992],[548,992],[549,992],[550,992],[551,992],[552,992],[553,992],[554,992],[556,992],[555,992],[557,992],[558,992],[559,992],[583,993],[560,992],[561,992],[562,992],[563,992],[564,992],[565,992],[566,992],[567,992],[568,992],[570,992],[569,992],[571,992],[572,992],[573,992],[574,992],[575,992],[576,992],[577,992],[578,992],[579,992],[580,992],[581,992],[582,992],[1369,994],[1401,995],[1399,996],[1397,996],[1400,996],[1396,996],[1398,996],[1368,996],[1367,55],[1998,128],[2111,55],[2112,55],[2113,55],[2114,997],[2115,55],[2116,146],[2117,147],[2118,55],[2119,55],[127,998],[128,998],[129,999],[88,1000],[130,1001],[131,1002],[132,1003],[83,55],[86,1004],[84,55],[85,55],[133,1005],[134,1006],[135,1007],[136,1008],[137,1009],[138,1010],[139,1010],[141,55],[140,1011],[142,1012],[143,1013],[144,1014],[126,1015],[87,55],[145,1016],[146,1017],[147,1018],[180,1019],[148,1020],[149,1021],[150,1022],[151,1023],[152,1024],[153,1025],[154,1026],[155,1027],[156,804],[157,1028],[158,1028],[159,1029],[160,55],[161,55],[162,1030],[164,1031],[163,1032],[165,1033],[166,1034],[167,1035],[168,1036],[169,1037],[170,1038],[171,1039],[172,1040],[173,1041],[174,1042],[175,1043],[176,1044],[177,1045],[178,1046],[179,1047],[2120,55],[2121,55],[1356,55],[104,1048],[114,1049],[103,1048],[124,1050],[95,1051],[94,1052],[123,826],[117,1053],[122,1054],[97,1055],[111,1056],[96,1057],[120,1058],[92,1059],[91,826],[121,1060],[93,1061],[98,1062],[99,55],[102,1062],[89,55],[125,938],[115,1063],[106,1064],[107,1065],[109,1066],[105,1067],[108,1068],[118,826],[100,1069],[101,1070],[110,1071],[90,856],[113,1063],[112,1062],[116,55],[119,1072],[1347,1073],[1348,1074],[600,1075],[1345,1076],[1344,1077],[601,1075],[1343,1078]],"affectedFilesPendingEmit":[2061,2059,2060,2062,182,489,1511,1512,1353,1354,1362,1363,1516,1514,1518,1616,1485,605,1505,1617,1622,1623,606,1365,668,1624,1625,1576,1618,1580,1581,1565,1542,1550,1370,1541,1549,1547,1548,1577,1552,1559,1553,1557,1558,1570,1571,1626,1572,1567,1569,1568,1504,1496,1497,1493,1579,1494,1495,1615,1619,1483,1627,1628,1510,1545,1574,1513,1517,1629,1491,1888,1506,1925,1996,1620,1997,2000,1543,1578,1546,2002,1566,1555,2033,2034,2035,2037,1507,1508,2038,2039,2040,2046,1575,2047,1515,2048,1540,1551,1544,1509,2049,2051,1621,1556,1481,1492,2052,2053,2054,1554,1460,2055,2057,2056,1482,1573,1371,1372,604,1389,1390,602,1462,1463,1391,1392,1395,1404,1405,1406,2050,1407,1408,1409,1410,1411,1412,1461,667,1464,1470,1474,1475,1361,1375,1376,1377,1374,1476,1385,1383,1477,603,1379,1403,1387,1478,1394,1384,1386,1378,1402,1393,607,1373,1388,514],"version":"5.8.3"} \ No newline at end of file
+{"fileNames":["../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2023.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2023.array.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2023.collection.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2023.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.collection.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.object.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.promise.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.regexp.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.es2024.string.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.array.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.collection.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.intl.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.promise.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.iterator.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.esnext.float16.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.bun/@types+react@18.3.23/node_modules/@types/react/global.d.ts","../../node_modules/.bun/csstype@3.1.3/node_modules/csstype/index.d.ts","../../node_modules/.bun/@types+prop-types@15.7.15/node_modules/@types/prop-types/index.d.ts","../../node_modules/.bun/@types+react@18.3.23/node_modules/@types/react/index.d.ts","../../node_modules/.bun/@types+react@18.3.23/node_modules/@types/react/jsx-runtime.d.ts","../../node_modules/.bun/drizzle-kit@0.31.4/node_modules/drizzle-kit/index-baurj6ib.d.mts","../../node_modules/.bun/drizzle-kit@0.31.4/node_modules/drizzle-kit/index.d.mts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/utility.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/header.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/readable.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/fetch.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/formdata.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/connector.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/client-stats.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/client.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/errors.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/dispatcher.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/global-origin.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/pool-stats.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/pool.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/handlers.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/h2c-client.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/agent.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/mock-call-history.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/mock-agent.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/mock-client.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/mock-pool.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/mock-errors.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/retry-handler.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/retry-agent.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/api.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/cache-interceptor.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/interceptors.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/util.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/cookies.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/patch.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/websocket.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/eventsource.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/content-type.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/cache.d.ts","../../node_modules/.bun/undici-types@7.10.0/node_modules/undici-types/index.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/globals.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/assert.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/buffer.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/child_process.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/cluster.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/console.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/constants.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/crypto.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/dgram.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/dns.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/domain.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/dom-events.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/events.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/fs.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/http.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/http2.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/https.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/inspector.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/module.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/net.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/os.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/path.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/process.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/punycode.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/querystring.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/readline.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/repl.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/sea.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/sqlite.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/stream.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/stream/web.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/test.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/timers.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/tls.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/trace_events.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/tty.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/url.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/util.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/v8.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/vm.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/wasi.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/zlib.d.ts","../../node_modules/.bun/@types+node@24.2.1/node_modules/@types/node/index.d.ts","../../node_modules/.bun/dotenv@16.6.1/node_modules/dotenv/lib/main.d.ts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/json-schema.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/standard-schema.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/registries.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/to-json-schema.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/util.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/versions.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/schemas.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/checks.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/errors.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/core.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/parse.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/regexes.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/ar.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/az.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/be.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/bg.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/ca.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/cs.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/da.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/de.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/en.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/eo.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/es.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/fa.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/fi.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/fr.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/fr-ca.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/he.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/hu.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/hy.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/id.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/is.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/it.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/ja.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/ka.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/kh.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/km.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/ko.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/lt.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/mk.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/ms.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/nl.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/no.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/ota.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/ps.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/pl.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/pt.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/ru.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/sl.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/sv.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/ta.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/th.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/tr.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/ua.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/uk.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/ur.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/uz.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/vi.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/zh-cn.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/zh-tw.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/yo.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/locales/index.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/doc.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/api.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/json-schema-processors.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/json-schema-generator.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/core/index.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/classic/errors.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/classic/parse.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/classic/schemas.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/classic/checks.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/classic/compat.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/classic/from-json-schema.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/classic/iso.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/classic/coerce.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/classic/external.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/index.d.cts","../../packages/env/src/web.ts","./drizzle.config.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/styled-jsx/types/css.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/styled-jsx/types/macro.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/styled-jsx/types/style.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/styled-jsx/types/global.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/styled-jsx/types/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/get-page-files.d.ts","../../node_modules/.bun/@types+react@18.3.23/node_modules/@types/react/canary.d.ts","../../node_modules/.bun/@types+react@18.3.23/node_modules/@types/react/experimental.d.ts","../../node_modules/.bun/@types+react-dom@18.3.7+fccada1f982c16e9/node_modules/@types/react-dom/index.d.ts","../../node_modules/.bun/@types+react-dom@18.3.7+fccada1f982c16e9/node_modules/@types/react-dom/canary.d.ts","../../node_modules/.bun/@types+react-dom@18.3.7+fccada1f982c16e9/node_modules/@types/react-dom/experimental.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/fallback.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/webpack/webpack.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/entry-constants.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/constants.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/config.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/load-custom-routes.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/image-config.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/body-streams.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/cache-control.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/setup-exception-listeners.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/worker.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/constants.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/bundler.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/experimental/ppr.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/page-types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/segment-config/app/app-segment-config.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/segment-config/pages/pages-segment-config.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/analysis/get-page-static-info.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/require-hook.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-polyfill-crypto.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-baseline.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-extensions/error-inspect.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-extensions/console-file.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-extensions/console-exit.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-extensions/console-dim.external.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-extensions/unhandled-rejection.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-extensions/random.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-extensions/date.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-extensions/web-crypto.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-extensions/node-crypto.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment-extensions/fast-set-immediate.external.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/node-environment.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/page-extensions-type.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-kind.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-definitions/route-definition.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-definitions/app-page-route-definition.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/cache-handlers/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/response-cache/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/resume-data-cache/cache-store.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/resume-data-cache/resume-data-cache.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/app-router-headers.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/render-result.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/instrumentation/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/coalesced-function.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/router-utils/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/trace/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/trace/trace.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/trace/shared.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/trace/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/load-jsconfig.d.ts","../../node_modules/.bun/@next+env@16.1.3/node_modules/@next/env/dist/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/plugins/telemetry-plugin/use-cache-tracker-utils.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/plugins/telemetry-plugin/telemetry-plugin.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/telemetry/storage.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/build-context.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/bloom-filter.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack-config.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/swc/generated-native.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/swc/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/dev/parse-version-info.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/next-devtools/shared/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/dev/dev-indicator-server-state.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/next-devtools/dev-overlay/cache-indicator.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/parse-stack.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/next-devtools/server/shared.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/next-devtools/shared/stack-frame.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/next-devtools/dev-overlay/utils/get-error-by-type.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/next-devtools/dev-overlay/container/runtime-error/render-error.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/next-devtools/dev-overlay/shared.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/dev/debug-channel.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/dev/hot-reloader-types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/i18n-provider.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/next-url.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/cookies.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/request.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/after/builtin-request-context.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/response.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/segment-config/middleware/middleware-config.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-definitions/locale-route-definition.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-definitions/pages-route-definition.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/deep-readonly.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/next-devtools/userspace/pages/pages-dev-overlay-setup.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/render.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/mitt.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/with-router.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/router.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/route-loader.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/page-loader.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router/router.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/readonly-url-search-params.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/app-router-types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/flight-data-helpers.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/router-reducer/ppr-navigations.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/segment-cache/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/segment-cache/navigation.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/segment-cache/cache-key.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/pages/module.compiled.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/templates/pages.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/pages/module.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/pages/builtin/_error.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/load-default-error-components.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/base-http/node.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/response-cache/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-definitions/pages-api-route-definition.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-matches/pages-api-route-match.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-matchers/route-matcher.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/normalizers/normalizer.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/normalizers/locale-route-normalizer.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/normalizers/request/pathname-normalizer.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/normalizers/request/suffix.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/normalizers/request/rsc.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/normalizers/request/next-data.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/normalizers/request/segment-prefix-rsc.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/static-paths/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/base-server.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/async-callback-set.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","../../node_modules/.bun/sharp@0.34.5/node_modules/sharp/lib/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/image-optimizer.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/next-server.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/lru-cache.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/dev-bundler-service.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/use-cache/cache-life.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/dev/static-paths-worker.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/dev/next-dev-server.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/next.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/render-server.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/router-server.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/router-utils/router-server-context.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/route-module.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/load-components.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/adapter.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/webpack/loaders/next-app-loader/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/app-dir-module.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/cache-signal.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request/fallback-params.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/work-unit-async-storage-instance.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/lazy-result.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/implicit-tags.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/staged-rendering.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/work-unit-async-storage.external.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/router/utils/parse-relative-url.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/app-render.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/error-boundary.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/layout-router.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/render-from-template-context.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/action-async-storage-instance.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/action-async-storage.external.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/client-page.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/client-segment.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request/search-params.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/hooks-server-context.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/http-access-fallback/error-boundary.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/extra-types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/resolvers.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/types/icons.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/resolve-metadata.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/metadata/metadata.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/lib/framework/boundary-components.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/rsc/preloads.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/rsc/postpone.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/rsc/taint.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/segment-cache/segment-value-encoding.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/collect-segment-data.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/next-devtools/userspace/app/segment-explorer-node.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/entry-base.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/templates/app-page.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/rendering-mode.d.ts","../../node_modules/.bun/@types+react@18.3.23/node_modules/@types/react/jsx-dev-runtime.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/entrypoints.d.ts","../../node_modules/.bun/@types+react-dom@18.3.7+fccada1f982c16e9/node_modules/@types/react-dom/client.d.ts","../../node_modules/.bun/@types+react-dom@18.3.7+fccada1f982c16e9/node_modules/@types/react-dom/server.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/entrypoints.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-page/module.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-page/module.compiled.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-definitions/app-route-route-definition.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/async-storage/work-store.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/http.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-route/shared-modules.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/redirect-status-code.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/redirect-error.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/templates/app-route.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-route/module.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-modules/app-route/module.compiled.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/segment-config/app/app-segments.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/utils.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/router-utils/build-prefetch-segment-data-route.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/turborepo-access-trace/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/turborepo-access-trace/result.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/turborepo-access-trace/helpers.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/turborepo-access-trace/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/export/routes/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/export/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/export/worker.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/worker.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/lib/incremental-cache/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/after/after.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/after/after-context.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/work-async-storage-instance.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/create-error-handler.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/action-revalidation-kind.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/app-render/work-async-storage.external.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request/params.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/route-matches/route-match.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request-meta.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/cli/next-test.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/config-shared.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/base-http/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/api-utils/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/build/adapter/build-complete.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/utils.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/pages/_app.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/app.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/use-cache/cache-tag.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/cache.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/pages/_document.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/document.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/dynamic.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dynamic.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/pages/_error.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/error.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/head.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/head.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request/cookies.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request/headers.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request/draft-mode.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/headers.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/get-img-props.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/image-component.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/shared/lib/image-external.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/image.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/link.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/link.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/unrecognized-action-error.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/redirect.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/not-found.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/forbidden.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/unauthorized.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/unstable-rethrow.server.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/unstable-rethrow.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/navigation.react-server.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/components/navigation.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/navigation.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/router.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/client/script.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/script.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/web/spec-extension/image-response.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/@vercel/og/emoji/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/@vercel/og/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/after/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/server/request/connection.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/server.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/types/global.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/types/compiled.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/image-types/global.d.ts","./next-env.d.ts","../../node_modules/.bun/botid@1.4.2+9bc11be94d70553a/node_modules/botid/dist/next/config/index.d.ts","./next.config.ts","./src/middleware.ts","../../node_modules/.bun/@types+react@19.2.7/node_modules/@types/react/global.d.ts","../../node_modules/.bun/csstype@3.2.3/node_modules/csstype/index.d.ts","../../node_modules/.bun/@types+react@19.2.7/node_modules/@types/react/index.d.ts","../../node_modules/.bun/@types+react@19.2.7/node_modules/@types/react/jsx-runtime.d.ts","../../packages/ui/src/icons/index.tsx","./src/constants/site-constants.ts","./src/app/metadata.ts","./src/app/robots.ts","./src/types/blog.ts","../../node_modules/.bun/@types+unist@3.0.3/node_modules/@types/unist/index.d.ts","../../node_modules/.bun/vfile-message@4.0.2/node_modules/vfile-message/lib/index.d.ts","../../node_modules/.bun/vfile-message@4.0.2/node_modules/vfile-message/index.d.ts","../../node_modules/.bun/vfile@6.0.3/node_modules/vfile/lib/index.d.ts","../../node_modules/.bun/vfile@6.0.3/node_modules/vfile/index.d.ts","../../node_modules/.bun/unified@11.0.5/node_modules/unified/lib/callable-instance.d.ts","../../node_modules/.bun/trough@2.2.0/node_modules/trough/lib/index.d.ts","../../node_modules/.bun/trough@2.2.0/node_modules/trough/index.d.ts","../../node_modules/.bun/unified@11.0.5/node_modules/unified/lib/index.d.ts","../../node_modules/.bun/unified@11.0.5/node_modules/unified/index.d.ts","../../node_modules/.bun/@types+hast@3.0.4/node_modules/@types/hast/index.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/common/html.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/common/token.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/common/error-codes.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/tokenizer/preprocessor.d.ts","../../node_modules/.bun/entities@6.0.1/node_modules/entities/dist/esm/generated/decode-data-html.d.ts","../../node_modules/.bun/entities@6.0.1/node_modules/entities/dist/esm/generated/decode-data-xml.d.ts","../../node_modules/.bun/entities@6.0.1/node_modules/entities/dist/esm/decode-codepoint.d.ts","../../node_modules/.bun/entities@6.0.1/node_modules/entities/dist/esm/decode.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/tokenizer/index.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/tree-adapters/interface.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/parser/open-element-stack.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/parser/formatting-element-list.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/parser/index.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/tree-adapters/default.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/serializer/index.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/common/foreign-content.d.ts","../../node_modules/.bun/parse5@7.3.0/node_modules/parse5/dist/index.d.ts","../../node_modules/.bun/property-information@7.1.0/node_modules/property-information/lib/util/info.d.ts","../../node_modules/.bun/property-information@7.1.0/node_modules/property-information/lib/find.d.ts","../../node_modules/.bun/property-information@7.1.0/node_modules/property-information/lib/hast-to-react.d.ts","../../node_modules/.bun/property-information@7.1.0/node_modules/property-information/lib/normalize.d.ts","../../node_modules/.bun/property-information@7.1.0/node_modules/property-information/index.d.ts","../../node_modules/.bun/hast-util-from-parse5@8.0.3/node_modules/hast-util-from-parse5/lib/index.d.ts","../../node_modules/.bun/hast-util-from-parse5@8.0.3/node_modules/hast-util-from-parse5/index.d.ts","../../node_modules/.bun/hast-util-from-html@2.0.3/node_modules/hast-util-from-html/lib/errors.d.ts","../../node_modules/.bun/hast-util-from-html@2.0.3/node_modules/hast-util-from-html/lib/types.d.ts","../../node_modules/.bun/hast-util-from-html@2.0.3/node_modules/hast-util-from-html/lib/index.d.ts","../../node_modules/.bun/hast-util-from-html@2.0.3/node_modules/hast-util-from-html/index.d.ts","../../node_modules/.bun/rehype-parse@9.0.1/node_modules/rehype-parse/lib/index.d.ts","../../node_modules/.bun/rehype-parse@9.0.1/node_modules/rehype-parse/index.d.ts","../../node_modules/.bun/stringify-entities@4.0.4/node_modules/stringify-entities/lib/util/format-smart.d.ts","../../node_modules/.bun/stringify-entities@4.0.4/node_modules/stringify-entities/lib/core.d.ts","../../node_modules/.bun/stringify-entities@4.0.4/node_modules/stringify-entities/lib/index.d.ts","../../node_modules/.bun/stringify-entities@4.0.4/node_modules/stringify-entities/index.d.ts","../../node_modules/.bun/hast-util-to-html@9.0.5/node_modules/hast-util-to-html/lib/index.d.ts","../../node_modules/.bun/hast-util-to-html@9.0.5/node_modules/hast-util-to-html/index.d.ts","../../node_modules/.bun/rehype-stringify@10.0.1/node_modules/rehype-stringify/index.d.ts","../../node_modules/.bun/rehype-slug@6.0.0/node_modules/rehype-slug/lib/index.d.ts","../../node_modules/.bun/rehype-slug@6.0.0/node_modules/rehype-slug/index.d.ts","../../node_modules/.bun/hast-util-is-element@3.0.0/node_modules/hast-util-is-element/lib/index.d.ts","../../node_modules/.bun/hast-util-is-element@3.0.0/node_modules/hast-util-is-element/index.d.ts","../../node_modules/.bun/rehype-autolink-headings@7.1.0/node_modules/rehype-autolink-headings/lib/index.d.ts","../../node_modules/.bun/rehype-autolink-headings@7.1.0/node_modules/rehype-autolink-headings/index.d.ts","../../node_modules/.bun/hast-util-sanitize@5.0.2/node_modules/hast-util-sanitize/lib/index.d.ts","../../node_modules/.bun/hast-util-sanitize@5.0.2/node_modules/hast-util-sanitize/lib/schema.d.ts","../../node_modules/.bun/hast-util-sanitize@5.0.2/node_modules/hast-util-sanitize/index.d.ts","../../node_modules/.bun/rehype-sanitize@6.0.0/node_modules/rehype-sanitize/lib/index.d.ts","../../node_modules/.bun/rehype-sanitize@6.0.0/node_modules/rehype-sanitize/index.d.ts","./src/lib/blog/query.ts","./src/app/sitemap.ts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/types/helper.d.mts","../../node_modules/.bun/better-call@1.1.8/node_modules/better-call/dist/helper.d.mts","../../node_modules/.bun/better-call@1.1.8/node_modules/better-call/dist/standard-schema.d.mts","../../node_modules/.bun/better-call@1.1.8/node_modules/better-call/dist/error.d.mts","../../node_modules/.bun/better-call@1.1.8/node_modules/better-call/dist/cookies.d.mts","../../node_modules/.bun/better-call@1.1.8/node_modules/better-call/dist/openapi.d.mts","../../node_modules/.bun/better-call@1.1.8/node_modules/better-call/dist/endpoint.d.mts","../../node_modules/.bun/better-call@1.1.8/node_modules/better-call/dist/middleware.d.mts","../../node_modules/.bun/better-call@1.1.8/node_modules/better-call/dist/context.d.mts","../../node_modules/.bun/better-call@1.1.8/node_modules/better-call/dist/router.d.mts","../../node_modules/.bun/better-call@1.1.8/node_modules/better-call/dist/to-response.d.mts","../../node_modules/.bun/better-call@1.1.8/node_modules/better-call/dist/index.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/types/cookie.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/schema/account.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/schema/rate-limit.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/schema/session.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/schema/user.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/schema/verification.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/get-tables.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/schema/shared.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/index.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/adapter/get-default-field-name.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/adapter/get-default-model-name.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/adapter/get-field-attributes.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/adapter/get-field-name.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/adapter/get-id-field.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/adapter/get-model-name.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/adapter/types.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/adapter/factory.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/adapter/utils.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/adapter/index.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/env/logger.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/oauth2/oauth-provider.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/oauth2/client-credentials-token.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/oauth2/create-authorization-url.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/oauth2/refresh-access-token.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/oauth2/utils.d.mts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/types.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jwe/general/decrypt.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jwe/general/encrypt.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jws/compact/verify.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jws/flattened/verify.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jws/general/verify.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jwt/verify.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jwt/decrypt.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jws/compact/sign.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jws/flattened/sign.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jws/general/sign.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jwt/sign.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jwt/encrypt.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jwk/thumbprint.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jwk/embedded.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jwks/local.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jwks/remote.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/jwt/unsecured.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/key/export.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/key/import.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/util/decode_protected_header.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/util/decode_jwt.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/util/errors.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/key/generate_key_pair.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/key/generate_secret.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/util/base64url.d.ts","../../node_modules/.bun/jose@6.1.3/node_modules/jose/dist/types/index.d.ts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/oauth2/validate-authorization-code.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/oauth2/verify.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/oauth2/index.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/apple.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/atlassian.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/cognito.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/discord.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/facebook.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/figma.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/github.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/microsoft-entra-id.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/google.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/huggingface.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/slack.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/spotify.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/twitch.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/twitter.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/dropbox.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/kick.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/linear.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/linkedin.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/gitlab.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/tiktok.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/reddit.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/roblox.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/salesforce.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/vk.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/zoom.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/notion.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/kakao.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/naver.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/line.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/paybin.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/paypal.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/polar.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/vercel.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/social-providers/index.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/types/context.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/api/index.d.mts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/operation-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/identifier-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/check-constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/column-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/default-value-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/generated-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/schemable-identifier-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/table-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/insert-result.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/delete-result.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/update-result.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/util/type-error.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/merge-result.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/util/type-utils.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/references-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/column-definition-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/add-column-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/drop-column-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/rename-column-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/raw-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/alter-column-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/foreign-key-constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/primary-key-constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/unique-constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/add-constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/drop-constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/modify-column-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/drop-index-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/add-index-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/rename-constraint-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/alter-table-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/where-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/create-index-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/create-schema-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/create-table-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/value-list-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/create-type-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/from-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/group-by-item-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/group-by-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/having-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/on-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/join-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/limit-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/offset-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/collate-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/order-by-item-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/order-by-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/alias-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/select-all-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/reference-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/simple-reference-expression-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/selection-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/common-table-expression-name-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/common-table-expression-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/with-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/select-modifier-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/operation-node-source.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/expression/expression.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/util/explainable.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/explain-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/set-operation-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/value-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/fetch-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/top-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/select-query-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/create-view-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/drop-schema-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/drop-table-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/drop-type-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/drop-view-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/output-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/returning-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/when-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/merge-query-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/column-update-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/on-conflict-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/on-duplicate-key-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/or-action-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/insert-query-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/update-query-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/using-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/delete-query-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/query-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/refresh-materialized-view-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/util/query-id.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-compiler/compiled-query.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-compiler/query-compiler.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/driver/database-connection.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/driver/driver.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/database-introspector.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/dialect-adapter.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/dialect.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/driver/connection-provider.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/plugin/kysely-plugin.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-executor/query-executor.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/util/compilable.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/default-value-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/column-definition-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/data-type-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/data-type-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/foreign-key-constraint-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/alter-column-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/alter-table-executor.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/alter-table-add-foreign-key-constraint-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/alter-table-drop-constraint-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/select-query-builder-expression.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/binary-operation-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/operator-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/value-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/util/column-type.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/binary-operation-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/join-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dynamic/dynamic-table-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/table-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/join-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dynamic/dynamic-reference-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/select-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/collate-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/order-by-item-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/order-by-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/group-by-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/where-interface.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/no-result-error.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/having-interface.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/set-operation-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/util/streamable.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/and-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/or-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/parens-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/expression/expression-wrapper.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/order-by-interface.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/select-query-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/coalesce-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/partition-by-item-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/partition-by-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/over-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/aggregate-function-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/partition-by-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/over-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/aggregate-function-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/function-module.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/case-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/case-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/json-path-leg-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/json-path-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/json-operator-chain-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/json-reference-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/json-path-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/tuple-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/select-from-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/expression/expression-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/expression-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/reference-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/alter-table-add-index-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/unique-constraint-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/primary-key-constraint-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/check-constraint-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/alter-table-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/create-index-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/create-schema-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/create-table-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/drop-index-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/drop-schema-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/drop-table-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-executor/query-executor-provider.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/raw-builder/raw-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/create-view-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/drop-view-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/create-type-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/drop-type-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/refresh-materialized-view-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/schema/schema.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dynamic/dynamic.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/primitive-value-list-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/values-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/insert-values-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/update-set-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/returning-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/returning-interface.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/on-conflict-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/output-interface.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/insert-query-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/update-query-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/delete-query-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/cte-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/with-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/delete-from-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/update-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-builder/merge-query-builder.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/merge-into-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-creator.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/util/log.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/savepoint-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/util/provide-controlled-connection.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/kysely.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/raw-builder/sql.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-executor/query-executor-base.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-executor/default-query-executor.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-executor/noop-query-executor.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/list-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/default-insert-value-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/unary-operation-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/function-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/tuple-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/matched-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/cast-node.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/operation-node-visitor.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/query-compiler/default-query-compiler.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/driver/default-connection-provider.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/driver/single-connection-provider.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/driver/dummy-driver.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/dialect-adapter-base.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/sqlite/sqlite-dialect-config.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/sqlite/sqlite-dialect.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/sqlite/sqlite-driver.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/postgres/postgres-query-compiler.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/postgres/postgres-introspector.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/postgres/postgres-adapter.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/mysql/mysql-dialect-config.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/mysql/mysql-dialect.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/mysql/mysql-driver.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/mysql/mysql-query-compiler.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/mysql/mysql-introspector.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/mysql/mysql-adapter.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/postgres/postgres-dialect-config.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/postgres/postgres-driver.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/postgres/postgres-dialect.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/sqlite/sqlite-query-compiler.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/sqlite/sqlite-introspector.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/sqlite/sqlite-adapter.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/mssql/mssql-adapter.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/mssql/mssql-dialect-config.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/mssql/mssql-dialect.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/mssql/mssql-driver.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/mssql/mssql-introspector.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/dialect/mssql/mssql-query-compiler.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/migration/migrator.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/migration/file-migration-provider.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/plugin/camel-case/camel-case-plugin.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/plugin/deduplicate-joins/deduplicate-joins-plugin.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/plugin/with-schema/with-schema-plugin.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/plugin/parse-json-results/parse-json-results-plugin.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/plugin/handle-empty-in-lists/handle-empty-in-lists.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/plugin/handle-empty-in-lists/handle-empty-in-lists-plugin.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/operation-node/operation-node-transformer.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/util/infer-result.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/util/log-once.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/parser/unary-operation-parser.d.ts","../../node_modules/.bun/kysely@0.28.9/node_modules/kysely/dist/esm/index.d.ts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/types/init-options.d.mts","../../node_modules/.bun/@better-fetch+fetch@1.1.21/node_modules/@better-fetch/fetch/dist/index.d.ts","../../node_modules/.bun/nanostores@1.1.0/node_modules/nanostores/atom/index.d.ts","../../node_modules/.bun/nanostores@1.1.0/node_modules/nanostores/map/index.d.ts","../../node_modules/.bun/nanostores@1.1.0/node_modules/nanostores/map-creator/index.d.ts","../../node_modules/.bun/nanostores@1.1.0/node_modules/nanostores/clean-stores/index.d.ts","../../node_modules/.bun/nanostores@1.1.0/node_modules/nanostores/task/index.d.ts","../../node_modules/.bun/nanostores@1.1.0/node_modules/nanostores/computed/index.d.ts","../../node_modules/.bun/nanostores@1.1.0/node_modules/nanostores/deep-map/path.d.ts","../../node_modules/.bun/nanostores@1.1.0/node_modules/nanostores/deep-map/index.d.ts","../../node_modules/.bun/nanostores@1.1.0/node_modules/nanostores/effect/index.d.ts","../../node_modules/.bun/nanostores@1.1.0/node_modules/nanostores/keep-mount/index.d.ts","../../node_modules/.bun/nanostores@1.1.0/node_modules/nanostores/lifecycle/index.d.ts","../../node_modules/.bun/nanostores@1.1.0/node_modules/nanostores/listen-keys/index.d.ts","../../node_modules/.bun/nanostores@1.1.0/node_modules/nanostores/index.d.ts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/types/plugin-client.d.mts","../../node_modules/.bun/@standard-schema+spec@1.1.0/node_modules/@standard-schema/spec/dist/index.d.ts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/types/index.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/type.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/db/plugin.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/types/plugin.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/index.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/types/helper.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/db/field.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/db/adapter-base.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/db/adapter-kysely.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/db/field-converter.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/adapters/kysely-adapter/types.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/db/get-migration.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/db/get-schema.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/env/color-depth.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/env/env-impl.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/env/index.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/db/internal-adapter.d.mts","../../node_modules/.bun/better-call@1.1.8+3b1ddfa528571d14/node_modules/better-call/dist/helper.d.mts","../../node_modules/.bun/better-call@1.1.8+3b1ddfa528571d14/node_modules/better-call/dist/standard-schema.d.mts","../../node_modules/.bun/better-call@1.1.8+3b1ddfa528571d14/node_modules/better-call/dist/error.d.mts","../../node_modules/.bun/better-call@1.1.8+3b1ddfa528571d14/node_modules/better-call/dist/cookies.d.mts","../../node_modules/.bun/better-call@1.1.8+3b1ddfa528571d14/node_modules/better-call/dist/openapi.d.mts","../../node_modules/.bun/better-call@1.1.8+3b1ddfa528571d14/node_modules/better-call/dist/endpoint.d.mts","../../node_modules/.bun/better-call@1.1.8+3b1ddfa528571d14/node_modules/better-call/dist/middleware.d.mts","../../node_modules/.bun/better-call@1.1.8+3b1ddfa528571d14/node_modules/better-call/dist/context.d.mts","../../node_modules/.bun/better-call@1.1.8+3b1ddfa528571d14/node_modules/better-call/dist/router.d.mts","../../node_modules/.bun/better-call@1.1.8+3b1ddfa528571d14/node_modules/better-call/dist/to-response.d.mts","../../node_modules/.bun/better-call@1.1.8+3b1ddfa528571d14/node_modules/better-call/dist/index.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/types/api.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/types/plugins.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/types/adapter.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/types/index.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/utils/get-request-ip.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/middlewares/oauth.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/middlewares/origin-check.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/middlewares/index.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/routes/account.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/routes/callback.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/routes/email-verification.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/routes/error.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/routes/ok.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/routes/password.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/routes/session.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/routes/sign-in.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/routes/sign-out.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/routes/sign-up.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/routes/update-user.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/routes/index.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/api/index.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/error/codes.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/error/index.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/types/auth.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/types/models.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/db/schema.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/db/to-zod.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/db/with-hooks.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/db/index.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/client/path-to-object.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/client/types.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/auth/full.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/oauth2/state.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/utils/hide-metadata.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/utils/deprecate.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/utils/error-codes.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/utils/id.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/utils/json.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/utils/string.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/utils/index.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/utils/index.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/async_hooks/index.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/context/endpoint-context.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/context/global.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/context/request-state.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/context/transaction.d.mts","../../node_modules/.bun/@better-auth+core@1.4.15+914e986bb716334c/node_modules/@better-auth/core/dist/context/index.d.mts","../../node_modules/.bun/@better-auth+telemetry@1.4.15+a0cddfd1733416ff/node_modules/@better-auth/telemetry/dist/index.d.mts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/classic/index.d.cts","../../node_modules/.bun/zod@4.3.5/node_modules/zod/v4/index.d.cts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/index.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/adapters/drizzle-adapter/drizzle-adapter.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/adapters/drizzle-adapter/index.d.mts","../../node_modules/.bun/@upstash+redis@1.35.4/node_modules/@upstash/redis/zmscore-dwj9vh1g.d.mts","../../node_modules/.bun/@upstash+redis@1.35.4/node_modules/@upstash/redis/nodejs.d.mts","../../node_modules/.bun/postgres@3.4.7/node_modules/postgres/types/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/entity.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/logger.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/casing.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/table.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/operations.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/subquery.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/query-builders/select.types.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sql/sql.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/utils.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sql/expressions/conditions.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sql/expressions/select.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sql/expressions/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sql/functions/aggregate.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/query-builders/query-builder.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sql/functions/vector.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sql/functions/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sql/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/checks.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/sequence.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/int.common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/bigintt.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/boolean.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/bytes.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/custom.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/date-duration.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/decimal.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/double-precision.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/duration.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/integer.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/json.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/date.common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/localdate.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/localtime.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/real.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/relative-duration.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/smallint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/text.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/timestamp.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/timestamptz.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/uuid.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/all.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/indexes.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/roles.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/policies.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/primary-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/unique-constraint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/table.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/foreign-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/bigint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/columns/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/view-base.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/cache/core/types.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/relations.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/query-builders/count.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/query-promise.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/runnable-query.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/query-builders/query.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/query-builders/raw.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/subquery.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/db.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/query-builders/delete.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/query-builders/update.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/query-builders/insert.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/query-builders/refresh-materialized-view.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/query-builders/select.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/query-builders/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/dialect.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/query-builders/query-builder.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/view-common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/view.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/query-builders/select.types.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/alias.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/schema.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/utils.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/gel-core/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/checks.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/binary.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/boolean.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/char.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/custom.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/date.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/datetime.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/decimal.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/double.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/enum.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/float.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/int.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/json.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/mediumint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/real.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/serial.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/smallint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/text.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/time.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/date.common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/timestamp.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/tinyint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/varbinary.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/varchar.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/year.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/all.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/indexes.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/primary-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/unique-constraint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/table.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/foreign-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/bigint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/columns/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/migrator.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/query-builders/delete.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/subquery.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/view-base.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/query-builders/select.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/query-builders/query-builder.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/query-builders/update.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/query-builders/insert.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/dialect.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/query-builders/count.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/query-builders/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/query-builders/query.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/db.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/view-common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/view.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/query-builders/select.types.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/alias.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/schema.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/utils.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/mysql-core/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/checks.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/bigserial.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/boolean.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/char.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/cidr.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/custom.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/date.common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/date.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/double-precision.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/inet.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/sequence.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/int.common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/integer.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/timestamp.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/interval.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/json.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/jsonb.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/line.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/macaddr.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/macaddr8.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/numeric.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/point.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/postgis_extension/geometry.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/real.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/serial.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/smallint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/smallserial.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/text.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/time.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/uuid.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/varchar.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/vector_extension/bit.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/vector_extension/halfvec.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/vector_extension/sparsevec.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/vector_extension/vector.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/all.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/indexes.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/roles.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/policies.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/primary-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/unique-constraint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/table.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/foreign-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/bigint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/enum.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/columns/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/view-base.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/query-builders/delete.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/query-builders/update.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/query-builders/insert.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/query-builders/refresh-materialized-view.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/subquery.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/query-builders/select.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/query-builders/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/dialect.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/query-builders/query-builder.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/view-common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/view.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/query-builders/select.types.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/alias.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/schema.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/utils.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/utils/array.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/utils/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/binary.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/boolean.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/char.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/custom.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/date.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/datetime.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/decimal.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/double.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/enum.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/float.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/int.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/json.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/mediumint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/real.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/serial.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/smallint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/text.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/time.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/date.common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/timestamp.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/tinyint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/varbinary.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/varchar.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/vector.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/year.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/all.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/indexes.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/primary-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/unique-constraint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/table.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/bigint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/columns/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/query-builders/delete.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/query-builders/update.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/query-builders/insert.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/dialect.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/cache/core/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore/driver.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/query-builders/count.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/subquery.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/query-builders/select.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/query-builders/query-builder.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/query-builders/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/db.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/query-builders/select.types.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/alias.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/schema.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/utils.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/singlestore-core/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/checks.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/columns/custom.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/indexes.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/primary-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/unique-constraint.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/view-base.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/query-builders/count.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/query-builders/query.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/subquery.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/db.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/query-builders/raw.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/query-builders/delete.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/query-builders/update.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/query-builders/insert.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/query-builders/select.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/query-builders/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/dialect.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/query-builders/query-builder.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/view.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/utils.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/columns/integer.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/columns/numeric.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/columns/real.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/columns/text.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/columns/all.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/table.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/foreign-keys.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/columns/common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/columns/blob.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/columns/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/query-builders/select.types.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/alias.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/sqlite-core/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/column-builder.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/column.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/alias.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/errors.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/view-common.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/index.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/cache/core/cache.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/query-builders/count.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/query-builders/query.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/query-builders/raw.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/pg-core/db.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/postgres-js/session.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/postgres-js/driver.d.ts","../../node_modules/.bun/drizzle-orm@0.44.3+7fe4c603ac54098d/node_modules/drizzle-orm/postgres-js/index.d.ts","./src/lib/db/schema.ts","./src/lib/db/index.ts","./src/lib/auth/server.ts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/integrations/next-js.d.mts","./src/app/api/auth/[...all]/route.ts","./src/app/api/health/route.ts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/typealiases.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/index.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/zoderror.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseutil.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/enumutil.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorutil.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/partialutil.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/standard-schema.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.d.cts","../../node_modules/.bun/zod@3.25.76/node_modules/zod/index.d.cts","../../node_modules/.bun/@upstash+redis@1.35.1/node_modules/@upstash/redis/zmscore-bsheakn7.d.mts","../../node_modules/.bun/@upstash+redis@1.35.1/node_modules/@upstash/redis/nodejs.d.mts","../../node_modules/.bun/@upstash+core-analytics@0.0.10/node_modules/@upstash/core-analytics/dist/index.d.ts","../../node_modules/.bun/@upstash+ratelimit@2.0.6+56bca893043c254b/node_modules/@upstash/ratelimit/dist/index.d.ts","./src/lib/rate-limit.ts","./src/app/api/sounds/search/route.ts","../../node_modules/.bun/feed@5.1.0/node_modules/feed/lib/feed.d.ts","./src/app/rss.xml/route.ts","../../node_modules/.bun/lucide-react@0.468.0+f4eacebf2041cd4f/node_modules/lucide-react/dist/lucide-react.d.ts","../../node_modules/.bun/clsx@2.1.1/node_modules/clsx/clsx.d.mts","../../node_modules/.bun/tailwind-merge@2.6.0/node_modules/tailwind-merge/dist/types.d.ts","./src/utils/ui.ts","../../node_modules/.bun/motion-utils@12.23.6/node_modules/motion-utils/dist/index.d.ts","../../node_modules/.bun/motion-dom@12.23.6/node_modules/motion-dom/dist/index.d.ts","../../node_modules/.bun/framer-motion@12.23.6+6dbf9a050bc9aadb/node_modules/framer-motion/dist/types.d-bq-qm38r.d.ts","../../node_modules/.bun/framer-motion@12.23.6+6dbf9a050bc9aadb/node_modules/framer-motion/dist/types/index.d.ts","../../node_modules/.bun/motion@12.23.6+6dbf9a050bc9aadb/node_modules/motion/dist/react.d.ts","../../node_modules/.bun/react-country-flag@3.1.0+f4eacebf2041cd4f/node_modules/react-country-flag/dist/index.d.ts","./src/components/language-select.tsx","./src/constants/captions-constants.ts","./src/types/editor.ts","./src/constants/editor-constants.ts","./src/types/export.ts","./src/constants/export-constants.ts","./src/constants/font-constants.ts","./src/types/timeline.ts","./src/types/project.ts","./src/constants/project-constants.ts","./src/constants/stickers-constants.ts","./src/constants/timeline-constants.tsx","./src/constants/text-constants.ts","./src/types/transcription.ts","./src/constants/transcription-constants.ts","./src/core/managers/playback-manager.ts","./src/utils/id.ts","./src/lib/timeline/track-utils.ts","./src/lib/timeline/element-utils.ts","./src/lib/timeline/zoom-utils.ts","./src/lib/timeline/index.ts","./src/lib/commands/base-command.ts","./src/lib/commands/timeline/track/add-track.ts","./src/lib/commands/timeline/track/remove-track.ts","./src/lib/commands/timeline/track/toggle-track-mute.ts","./src/lib/commands/timeline/track/toggle-track-visibility.ts","./src/lib/commands/timeline/track/index.ts","./src/services/storage/types.ts","./src/types/assets.ts","./src/lib/commands/timeline/element/insert-element.ts","./src/lib/commands/timeline/element/delete-elements.ts","./src/lib/commands/timeline/element/duplicate-elements.ts","./src/lib/commands/timeline/element/update-element-trim.ts","./src/lib/commands/timeline/element/update-element-duration.ts","./src/lib/commands/timeline/element/update-element-start-time.ts","./src/lib/commands/timeline/element/split-elements.ts","./src/lib/commands/timeline/element/update-text-element.ts","./src/lib/commands/timeline/element/toggle-elements-visibility.ts","./src/lib/commands/timeline/element/toggle-elements-muted.ts","./src/lib/commands/timeline/element/move-elements.ts","./src/lib/commands/timeline/element/index.ts","./src/lib/commands/timeline/clipboard/paste.ts","./src/lib/commands/timeline/clipboard/index.ts","./src/lib/commands/timeline/index.ts","./src/core/managers/timeline-manager.ts","./src/services/storage/indexeddb-adapter.ts","./src/services/storage/opfs-adapter.ts","./src/types/sounds.ts","./src/services/storage/migrations/base.ts","./src/services/storage/migrations/version-manager.ts","./src/services/storage/migrations/runner.ts","./src/lib/scenes.ts","./src/services/storage/migrations/v0-to-v1.ts","./src/services/storage/migrations/v1-to-v2.ts","./src/services/storage/migrations/index.ts","./src/services/storage/storage-service.ts","./src/types/time.ts","./src/lib/time.ts","./src/lib/timeline/bookmarks.ts","./src/lib/commands/scene/create-scene.ts","./src/lib/commands/scene/delete-scene.ts","./src/lib/commands/scene/rename-scene.ts","./src/lib/commands/scene/toggle-bookmark.ts","./src/lib/commands/scene/remove-bookmark.ts","./src/lib/commands/scene/index.ts","./src/core/managers/scenes-manager.ts","../../node_modules/.bun/sonner@1.7.4+6dbf9a050bc9aadb/node_modules/sonner/dist/index.d.ts","./src/lib/commands/project/update-project-settings.ts","./src/lib/commands/project/index.ts","./src/lib/media/media-utils.ts","../../node_modules/.bun/@types+dom-webcodecs@0.1.13/node_modules/@types/dom-webcodecs/webcodecs.generated.d.ts","../../node_modules/.bun/@types+dom-webcodecs@0.1.13/node_modules/@types/dom-webcodecs/index.d.ts","../../node_modules/.bun/@types+dom-mediacapture-transform@0.1.11/node_modules/@types/dom-mediacapture-transform/index.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/misc.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/metadata.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/packet.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/codec-data.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/subtitles.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/codec.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/sample.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/encode.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/media-source.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/output-format.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/target.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/output.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/source.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/input-format.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/media-sink.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/input-track.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/input.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/conversion.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/custom-coder.d.ts","../../node_modules/.bun/mediabunny@1.29.1/node_modules/mediabunny/dist/modules/src/index.d.ts","./src/lib/media/audio.ts","./src/lib/media/mediabunny.ts","./src/lib/media/processing.ts","./src/core/managers/project-manager.ts","./src/services/media/video-cache.ts","./src/core/managers/media-manager.ts","./src/services/renderer/canvas-renderer.ts","./src/services/renderer/nodes/base-node.ts","./src/services/renderer/nodes/root-node.ts","../../node_modules/.bun/eventemitter3@5.0.1/node_modules/eventemitter3/index.d.ts","./src/services/renderer/scene-exporter.ts","./src/services/renderer/nodes/video-node.ts","./src/services/renderer/nodes/image-node.ts","./src/services/renderer/nodes/text-node.ts","./src/services/renderer/nodes/sticker-node.ts","./src/lib/gradients/parser.ts","./src/lib/gradients/canvas.ts","./src/lib/gradients/index.ts","./src/services/renderer/nodes/color-node.ts","./src/services/renderer/nodes/blur-background-node.ts","./src/services/renderer/scene-builder.ts","./src/core/managers/renderer-manager.ts","./src/lib/commands/media/add-media-asset.ts","./src/lib/commands/media/remove-media-asset.ts","./src/lib/commands/media/index.ts","./src/lib/commands/index.ts","./src/core/managers/commands.ts","./src/core/managers/save-manager.ts","./src/core/index.ts","./src/data/colors/pattern-craft.ts","./src/data/colors/solid.ts","./src/hooks/use-editor.ts","./src/types/drag.ts","./src/lib/drag-data.ts","./src/hooks/use-file-upload.ts","./src/hooks/use-infinite-scroll.ts","./src/types/keybinding.ts","./src/lib/actions/definitions.ts","./src/lib/actions/types.ts","./src/lib/actions/registry.ts","./src/lib/actions/index.ts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/vanilla.d.mts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/react.d.mts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/index.d.mts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/middleware/redux.d.mts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/middleware/devtools.d.mts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/middleware/subscribewithselector.d.mts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/middleware/combine.d.mts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/middleware/persist.d.mts","../../node_modules/.bun/zustand@5.0.6+0e2fb8dbc083adda/node_modules/zustand/esm/middleware.d.mts","./src/utils/browser.ts","./src/utils/platform.ts","./src/stores/keybindings-store.ts","./src/hooks/use-keybindings.ts","./src/hooks/use-keyboard-shortcuts-help.ts","./src/hooks/use-raf-loop.ts","./src/hooks/use-reveal-item.ts","./src/stores/sounds-store.ts","./src/hooks/use-sound-search.ts","./src/hooks/actions/use-action-handler.ts","./src/stores/timeline-store.ts","./src/hooks/timeline/element/use-element-selection.ts","./src/hooks/actions/use-editor-actions.ts","./src/hooks/timeline/use-edge-auto-scroll.ts","./src/hooks/timeline/use-scroll-sync.ts","./src/hooks/timeline/use-selection-box.ts","./src/hooks/timeline/use-snap-indicator-position.ts","./src/lib/timeline/drop-utils.ts","./src/hooks/timeline/use-timeline-drag-drop.ts","./src/hooks/timeline/use-timeline-playhead.ts","./src/hooks/timeline/use-timeline-seek.ts","./src/hooks/timeline/use-timeline-snapping.ts","./src/hooks/timeline/use-timeline-zoom.ts","./src/hooks/timeline/element/use-element-interaction.ts","./src/hooks/timeline/element/use-element-resize.ts","./src/lib/export.ts","./src/lib/iconify-api.ts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/client/react/react-store.d.mts","../../node_modules/.bun/better-auth@1.4.15+dd42c8d3451e57ba/node_modules/better-auth/dist/client/react/index.d.mts","./src/lib/auth/client.ts","./src/lib/transcription/caption.ts","../../node_modules/.bun/onnxruntime-common@1.21.0/node_modules/onnxruntime-common/dist/esm/onnx-model.d.ts","../../node_modules/.bun/onnxruntime-common@1.21.0/node_modules/onnxruntime-common/dist/esm/tensor-factory.d.ts","../../node_modules/.bun/onnxruntime-common@1.21.0/node_modules/onnxruntime-common/dist/esm/tensor-conversion.d.ts","../../node_modules/.bun/onnxruntime-common@1.21.0/node_modules/onnxruntime-common/dist/esm/tensor-utils.d.ts","../../node_modules/.bun/onnxruntime-common@1.21.0/node_modules/onnxruntime-common/dist/esm/type-helper.d.ts","../../node_modules/.bun/onnxruntime-common@1.21.0/node_modules/onnxruntime-common/dist/esm/tensor.d.ts","../../node_modules/.bun/onnxruntime-common@1.21.0/node_modules/onnxruntime-common/dist/esm/onnx-value.d.ts","../../node_modules/.bun/onnxruntime-common@1.21.0/node_modules/onnxruntime-common/dist/esm/inference-session.d.ts","../../node_modules/.bun/onnxruntime-common@1.21.0/node_modules/onnxruntime-common/dist/esm/backend-impl.d.ts","../../node_modules/.bun/onnxruntime-common@1.21.0/node_modules/onnxruntime-common/dist/esm/backend.d.ts","../../node_modules/.bun/onnxruntime-common@1.21.0/node_modules/onnxruntime-common/dist/esm/env.d.ts","../../node_modules/.bun/onnxruntime-common@1.21.0/node_modules/onnxruntime-common/dist/esm/trace.d.ts","../../node_modules/.bun/onnxruntime-common@1.21.0/node_modules/onnxruntime-common/dist/esm/index.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/env.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/utils/core.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/utils/devices.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/utils/dtypes.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/utils/hub.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/backends/onnx.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/utils/maths.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/utils/tensor.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/configs.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/utils/data-structures.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/tokenizers.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/generation/streamers.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/generation/configuration_utils.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/generation/stopping_criteria.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/generation/logits_process.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/generation/parameters.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/whisper/generation_whisper.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/utils/image.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/base/image_processors_utils.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/base/feature_extraction_utils.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/base/processing_utils.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/utils/audio.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/auto/processing_auto.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/pipelines.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/utils/video.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/audio_spectrogram_transformer/feature_extraction_audio_spectrogram_transformer.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/encodec/feature_extraction_encodec.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/clap/feature_extraction_clap.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/dac/feature_extraction_dac.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/gemma3n/feature_extraction_gemma3n.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/moonshine/feature_extraction_moonshine.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/parakeet/feature_extraction_parakeet.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/pyannote/feature_extraction_pyannote.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/seamless_m4t/feature_extraction_seamless_m4t.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/snac/feature_extraction_snac.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/speecht5/feature_extraction_speecht5.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/wav2vec2/feature_extraction_wav2vec2.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/wespeaker/feature_extraction_wespeaker.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/whisper/feature_extraction_whisper.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/feature_extractors.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/auto/feature_extraction_auto.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/beit/image_processing_beit.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/bit/image_processing_bit.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/chinese_clip/image_processing_chinese_clip.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/clip/image_processing_clip.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/convnext/image_processing_convnext.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/deit/image_processing_deit.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/detr/image_processing_detr.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/dinov3_vit/image_processing_dinov3_vit.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/donut/image_processing_donut.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/dpt/image_processing_dpt.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/efficientnet/image_processing_efficientnet.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/glpn/image_processing_glpn.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/grounding_dino/image_processing_grounding_dino.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/idefics3/image_processing_idefics3.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/janus/image_processing_janus.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/jina_clip/image_processing_jina_clip.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/llava_onevision/image_processing_llava_onevision.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/maskformer/image_processing_maskformer.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/mask2former/image_processing_mask2former.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/mobilenet_v1/image_processing_mobilenet_v1.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/mobilenet_v2/image_processing_mobilenet_v2.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/mobilenet_v3/image_processing_mobilenet_v3.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/mobilenet_v4/image_processing_mobilenet_v4.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/mobilevit/image_processing_mobilevit.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/nougat/image_processing_nougat.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/owlvit/image_processing_owlvit.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/owlv2/image_processing_owlv2.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/phi3_v/image_processing_phi3_v.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/pixtral/image_processing_pixtral.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/pvt/image_processing_pvt.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/qwen2_vl/image_processing_qwen2_vl.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/rt_detr/image_processing_rt_detr.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/sam/image_processing_sam.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/sam2/image_processing_sam2.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/sam3/image_processing_sam3.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/segformer/image_processing_segformer.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/siglip/image_processing_siglip.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/smolvlm/image_processing_smolvlm.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/swin2sr/image_processing_swin2sr.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/vit/image_processing_vit.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/vitmatte/image_processing_vitmatte.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/vitpose/image_processing_vitpose.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/yolos/image_processing_yolos.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/image_processors.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/auto/image_processing_auto.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/florence2/processing_florence2.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/gemma3n/processing_gemma3n.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/grounding_dino/processing_grounding_dino.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/idefics3/processing_idefics3.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/janus/processing_janus.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/jina_clip/processing_jina_clip.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/llava/processing_llava.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/mgp_str/processing_mgp_str.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/moonshine/processing_moonshine.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/owlvit/processing_owlvit.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/paligemma/processing_paligemma.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/phi3_v/processing_phi3_v.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/pixtral/processing_pixtral.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/pyannote/processing_pyannote.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/qwen2_vl/processing_qwen2_vl.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/sam/processing_sam.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/sam2/processing_sam2.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/smolvlm/processing_smolvlm.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/speecht5/processing_speecht5.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/ultravox/processing_ultravox.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/voxtral/processing_voxtral.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/wav2vec2/processing_wav2vec2.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/wav2vec2_with_lm/processing_wav2vec2_with_lm.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/whisper/processing_whisper.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/models/processors.d.ts","../../node_modules/.bun/@huggingface+transformers@3.8.1/node_modules/@huggingface/transformers/types/transformers.d.ts","./src/services/transcription/worker.ts","./src/services/transcription/index.ts","./src/stores/assets-panel-store.ts","./src/stores/editor-store.ts","./src/stores/panel-store.ts","./src/types/stickers.ts","./src/stores/stickers-store.ts","./src/stores/text-properties-store.ts","./src/utils/date.ts","./src/utils/geometry.ts","./src/utils/math.ts","./src/utils/string-utils.ts","../../node_modules/.bun/@radix-ui+react-accessible-icon@1.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-accessible-icon/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-context@1.1.2+0e2fb8dbc083adda/node_modules/@radix-ui/react-context/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-primitive@2.1.3+b41f8805ee63d2ff/node_modules/@radix-ui/react-primitive/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-collapsible@1.1.11+b41f8805ee63d2ff/node_modules/@radix-ui/react-collapsible/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-accordion@1.2.11+b41f8805ee63d2ff/node_modules/@radix-ui/react-accordion/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-dismissable-layer@1.1.10+b41f8805ee63d2ff/node_modules/@radix-ui/react-dismissable-layer/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-focus-scope@1.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-focus-scope/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-portal@1.1.9+b41f8805ee63d2ff/node_modules/@radix-ui/react-portal/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-dialog@1.1.14+b41f8805ee63d2ff/node_modules/@radix-ui/react-dialog/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-alert-dialog@1.1.14+b41f8805ee63d2ff/node_modules/@radix-ui/react-alert-dialog/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-aspect-ratio@1.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-aspect-ratio/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-avatar@1.1.10+b41f8805ee63d2ff/node_modules/@radix-ui/react-avatar/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-checkbox@1.3.2+b41f8805ee63d2ff/node_modules/@radix-ui/react-checkbox/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-arrow@1.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-arrow/dist/index.d.mts","../../node_modules/.bun/@radix-ui+rect@1.1.1/node_modules/@radix-ui/rect/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-popper@1.2.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-popper/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-roving-focus@1.1.10+b41f8805ee63d2ff/node_modules/@radix-ui/react-roving-focus/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-menu@2.1.15+b41f8805ee63d2ff/node_modules/@radix-ui/react-menu/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-context-menu@2.2.15+b41f8805ee63d2ff/node_modules/@radix-ui/react-context-menu/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-direction@1.1.1+0e2fb8dbc083adda/node_modules/@radix-ui/react-direction/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-dropdown-menu@2.1.15+b41f8805ee63d2ff/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-label@2.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-label/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-form@0.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-form/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-hover-card@1.1.14+b41f8805ee63d2ff/node_modules/@radix-ui/react-hover-card/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-menubar@1.1.15+b41f8805ee63d2ff/node_modules/@radix-ui/react-menubar/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-visually-hidden@1.2.3+b41f8805ee63d2ff/node_modules/@radix-ui/react-visually-hidden/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-navigation-menu@1.2.13+b41f8805ee63d2ff/node_modules/@radix-ui/react-navigation-menu/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-one-time-password-field@0.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-one-time-password-field/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-password-toggle-field@0.1.2+b41f8805ee63d2ff/node_modules/@radix-ui/react-password-toggle-field/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-popover@1.1.14+b41f8805ee63d2ff/node_modules/@radix-ui/react-popover/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-progress@1.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-progress/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-radio-group@1.3.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-radio-group/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-scroll-area@1.2.9+b41f8805ee63d2ff/node_modules/@radix-ui/react-scroll-area/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-select@2.2.5+b41f8805ee63d2ff/node_modules/@radix-ui/react-select/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-separator@1.1.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-separator/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-slider@1.3.5+b41f8805ee63d2ff/node_modules/@radix-ui/react-slider/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-slot@1.2.3+0e2fb8dbc083adda/node_modules/@radix-ui/react-slot/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-switch@1.2.5+b41f8805ee63d2ff/node_modules/@radix-ui/react-switch/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-tabs@1.1.12+b41f8805ee63d2ff/node_modules/@radix-ui/react-tabs/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-toast@1.2.14+b41f8805ee63d2ff/node_modules/@radix-ui/react-toast/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-toggle@1.1.9+b41f8805ee63d2ff/node_modules/@radix-ui/react-toggle/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-toggle-group@1.1.10+b41f8805ee63d2ff/node_modules/@radix-ui/react-toggle-group/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-toolbar@1.1.10+b41f8805ee63d2ff/node_modules/@radix-ui/react-toolbar/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-tooltip@1.2.7+b41f8805ee63d2ff/node_modules/@radix-ui/react-tooltip/dist/index.d.mts","../../node_modules/.bun/radix-ui@1.4.2+b41f8805ee63d2ff/node_modules/radix-ui/dist/index.d.mts","../../node_modules/.bun/class-variance-authority@0.7.1/node_modules/class-variance-authority/dist/types.d.ts","../../node_modules/.bun/class-variance-authority@0.7.1/node_modules/class-variance-authority/dist/index.d.ts","./src/components/ui/button.tsx","../../node_modules/.bun/next-themes@0.4.6+6dbf9a050bc9aadb/node_modules/next-themes/dist/index.d.ts","./src/components/theme-toggle.tsx","./src/components/header.tsx","../../node_modules/.bun/react-icons@5.5.0+f4eacebf2041cd4f/node_modules/react-icons/lib/iconsmanifest.d.ts","../../node_modules/.bun/react-icons@5.5.0+f4eacebf2041cd4f/node_modules/react-icons/lib/iconbase.d.ts","../../node_modules/.bun/react-icons@5.5.0+f4eacebf2041cd4f/node_modules/react-icons/lib/iconcontext.d.ts","../../node_modules/.bun/react-icons@5.5.0+f4eacebf2041cd4f/node_modules/react-icons/lib/index.d.ts","../../node_modules/.bun/react-icons@5.5.0+f4eacebf2041cd4f/node_modules/react-icons/ri/index.d.ts","../../node_modules/.bun/react-icons@5.5.0+f4eacebf2041cd4f/node_modules/react-icons/fa6/index.d.ts","./src/components/footer.tsx","./src/app/base-page.tsx","./src/components/ui/sonner.tsx","./src/components/ui/tooltip.tsx","../../node_modules/.bun/botid@1.4.2+9bc11be94d70553a/node_modules/botid/dist/client/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/@next/font/dist/types.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/dist/compiled/@next/font/dist/google/index.d.ts","../../node_modules/.bun/next@16.1.3+6dbf9a050bc9aadb/node_modules/next/font/google/index.d.ts","./src/app/layout.tsx","./src/components/landing/handlebars.tsx","./src/components/landing/hero.tsx","./src/app/page.tsx","./src/components/ui/avatar.tsx","./src/components/ui/separator.tsx","./src/app/blog/page.tsx","./src/components/ui/prose.tsx","./src/app/blog/[slug]/page.tsx","./src/components/ui/card.tsx","./src/components/github-contribute-section.tsx","./src/app/contributors/page.tsx","./src/app/editor/[project_id]/layout.tsx","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/panel.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/types.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/panelgroup.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/panelresizehandleregistry.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/panelresizehandle.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/constants.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/assert.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/csp.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/cursor.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/dom/getpanelelement.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/dom/getpanelelementsforgroup.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/dom/getpanelgroupelement.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/dom/getresizehandleelement.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/dom/getresizehandleelementindex.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/dom/getresizehandleelementsforgroup.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/dom/getresizehandlepanelids.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/rects/types.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/rects/getintersectingrectangle.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/utils/rects/intersects.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/declarations/src/index.d.ts","../../node_modules/.bun/react-resizable-panels@2.1.9+6dbf9a050bc9aadb/node_modules/react-resizable-panels/dist/react-resizable-panels.cjs.d.mts","./src/components/ui/resizable.tsx","./src/components/editor/assets-panel/tabbar.tsx","./src/components/editor/assets-panel/drag-overlay.tsx","./src/components/ui/context-menu.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/aspect-ratio.tsx","./src/components/editor/draggable-item.tsx","./src/components/editor/assets-panel/views/media.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/tabs.tsx","./src/components/editor/panel-base-view.tsx","./src/components/editor/assets-panel/views/text.tsx","./src/components/ui/input.tsx","./src/components/ui/dialog.tsx","./src/components/editor/assets-panel/views/sounds.tsx","./src/components/ui/input-with-back.tsx","./src/components/editor/assets-panel/views/stickers.tsx","./src/components/ui/select.tsx","./src/components/editor/properties-panel/property-item.tsx","./src/data/colors/syntax-ui.tsx","./src/components/editor/assets-panel/views/settings.tsx","./src/components/editor/assets-panel/views/captions.tsx","./src/components/editor/assets-panel/index.tsx","./src/components/editor/properties-panel/audio-properties.tsx","./src/components/editor/properties-panel/video-properties.tsx","./src/components/ui/textarea.tsx","./src/components/ui/font-picker.tsx","./src/components/ui/slider.tsx","./src/components/ui/color-picker.tsx","./src/components/editor/properties-panel/text-properties.tsx","./src/components/editor/properties-panel/index.tsx","../../node_modules/.bun/wavesurfer.js@7.10.0/node_modules/wavesurfer.js/dist/event-emitter.d.ts","../../node_modules/.bun/wavesurfer.js@7.10.0/node_modules/wavesurfer.js/dist/base-plugin.d.ts","../../node_modules/.bun/wavesurfer.js@7.10.0/node_modules/wavesurfer.js/dist/dom.d.ts","../../node_modules/.bun/wavesurfer.js@7.10.0/node_modules/wavesurfer.js/dist/player.d.ts","../../node_modules/.bun/wavesurfer.js@7.10.0/node_modules/wavesurfer.js/dist/wavesurfer.d.ts","./src/components/editor/timeline/audio-waveform.tsx","./src/components/editor/timeline/timeline-element.tsx","./src/components/editor/timeline/timeline-track.tsx","./src/components/editor/timeline/timeline-playhead.tsx","./src/components/editor/selection-box.tsx","./src/components/editor/timeline/snap-indicator.tsx","./src/components/ui/split-button.tsx","./src/components/editor/editable-timecode.tsx","./src/components/ui/sheet.tsx","./src/components/editor/scenes-view.tsx","./src/components/editor/timeline/timeline-toolbar.tsx","./src/components/editor/timeline/timeline-tick.tsx","./src/components/editor/timeline/timeline-ruler.tsx","./src/components/editor/timeline/bookmarks.tsx","./src/components/editor/timeline/drag-line.tsx","./src/components/editor/timeline/index.tsx","../../node_modules/.bun/use-deep-compare-effect@1.8.1+f4eacebf2041cd4f/node_modules/use-deep-compare-effect/dist/index.d.ts","./src/components/editor/preview-panel.tsx","./src/components/keyboard-shortcuts-help.tsx","./src/components/rename-project-dialog.tsx","./src/components/editor/delete-project-dialog.tsx","./src/components/ui/popover.tsx","./src/components/ui/label.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/progress.tsx","./src/components/ui/checkbox.tsx","./src/components/editor/export-button.tsx","./src/components/editor/editor-header.tsx","./src/components/providers/editor-provider.tsx","../../node_modules/.bun/@types+mdast@4.0.4/node_modules/@types/mdast/index.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/state.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/footer.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/blockquote.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/break.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/code.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/delete.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/emphasis.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/footnote-reference.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/heading.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/html.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/image-reference.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/image.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/inline-code.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/link-reference.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/link.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/list-item.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/list.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/paragraph.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/root.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/strong.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/table.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/table-cell.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/table-row.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/text.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/thematic-break.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/handlers/index.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/lib/index.d.ts","../../node_modules/.bun/mdast-util-to-hast@13.2.0/node_modules/mdast-util-to-hast/index.d.ts","../../node_modules/.bun/remark-rehype@11.1.2/node_modules/remark-rehype/lib/index.d.ts","../../node_modules/.bun/remark-rehype@11.1.2/node_modules/remark-rehype/index.d.ts","../../node_modules/.bun/react-markdown@10.1.0+0e2fb8dbc083adda/node_modules/react-markdown/lib/index.d.ts","../../node_modules/.bun/react-markdown@10.1.0+0e2fb8dbc083adda/node_modules/react-markdown/index.d.ts","./src/components/editor/onboarding.tsx","./src/components/editor/migration-dialog.tsx","./src/app/editor/[project_id]/page.tsx","./src/components/ui/accordion.tsx","./src/app/privacy/page.tsx","./src/components/ui/skeleton.tsx","./src/app/projects/page.tsx","./src/components/ui/badge.tsx","./src/components/ui/react-markdown-wrapper.tsx","./src/app/roadmap/page.tsx","./src/app/sponsors/page.tsx","./src/app/terms/page.tsx","./src/components/storage-provider.tsx","./src/components/editor/layout-guide-overlay.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/breadcrumb.tsx","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/locale/types.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/fp/types.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/types.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/add.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addbusinessdays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/adddays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addhours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addisoweekyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addmilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addminutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addmonths.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addquarters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addweeks.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/addyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/areintervalsoverlapping.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/clamp.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/closestindexto.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/closestto.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/compareasc.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/comparedesc.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/constructfrom.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/constructnow.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/daystoweeks.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinbusinessdays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceincalendardays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceincalendarisoweekyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceincalendarisoweeks.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceincalendarmonths.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceincalendarquarters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceincalendarweeks.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceincalendaryears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceindays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinhours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinisoweekyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinmilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinminutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinmonths.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinquarters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinweeks.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/differenceinyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachdayofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachhourofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachminuteofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachmonthofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachquarterofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachweekofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachweekendofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachweekendofmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachweekendofyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/eachyearofinterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofdecade.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofhour.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofisoweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofisoweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofminute.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofquarter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofsecond.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endoftoday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endoftomorrow.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/endofyesterday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/_lib/format/formatters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/_lib/format/longformatters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/format.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatdistance.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatdistancestrict.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatdistancetonow.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatdistancetonowstrict.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatduration.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatiso.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatiso9075.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatisoduration.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatrfc3339.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatrfc7231.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/formatrelative.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/fromunixtime.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getdate.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getdayofyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getdaysinmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getdaysinyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getdecade.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/_lib/defaultoptions.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getdefaultoptions.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/gethours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getisoday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getisoweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getisoweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getisoweeksinyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getmilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getminutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getoverlappingdaysinintervals.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getquarter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/gettime.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getunixtime.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getweekofmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getweeksinmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/getyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/hourstomilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/hourstominutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/hourstoseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/interval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/intervaltoduration.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/intlformat.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/intlformatdistance.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isafter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isbefore.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isdate.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isequal.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isexists.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isfirstdayofmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isfriday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isfuture.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/islastdayofmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isleapyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/ismatch.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/ismonday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/ispast.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issameday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issamehour.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issameisoweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issameisoweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issameminute.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issamemonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issamequarter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issamesecond.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issameweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issameyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issaturday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/issunday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthishour.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthisisoweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthisminute.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthismonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthisquarter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthissecond.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthisweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthisyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isthursday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/istoday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/istomorrow.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/istuesday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isvalid.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/iswednesday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isweekend.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/iswithininterval.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/isyesterday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lastdayofdecade.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lastdayofisoweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lastdayofisoweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lastdayofmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lastdayofquarter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lastdayofweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lastdayofyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/_lib/format/lightformatters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/lightformat.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/max.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/milliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/millisecondstohours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/millisecondstominutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/millisecondstoseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/min.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/minutestohours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/minutestomilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/minutestoseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/monthstoquarters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/monthstoyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nextday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nextfriday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nextmonday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nextsaturday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nextsunday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nextthursday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nexttuesday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/nextwednesday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/parse/_lib/types.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/parse/_lib/setter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/parse/_lib/parser.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/parse/_lib/parsers.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/parse.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/parseiso.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/parsejson.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previousday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previousfriday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previousmonday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previoussaturday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previoussunday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previousthursday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previoustuesday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/previouswednesday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/quarterstomonths.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/quarterstoyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/roundtonearesthours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/roundtonearestminutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/secondstohours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/secondstomilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/secondstominutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/set.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setdate.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setdayofyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setdefaultoptions.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/sethours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setisoday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setisoweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setisoweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setmilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setminutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setquarter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/setyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofdecade.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofhour.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofisoweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofisoweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofminute.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofmonth.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofquarter.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofsecond.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startoftoday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startoftomorrow.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofweek.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofweekyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofyear.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/startofyesterday.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/sub.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subbusinessdays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subdays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subhours.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subisoweekyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/submilliseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subminutes.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/submonths.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subquarters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subseconds.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subweeks.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/subyears.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/todate.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/transpose.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/weekstodays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/yearstodays.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/yearstomonths.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/yearstoquarters.d.ts","../../node_modules/.bun/date-fns@3.6.0/node_modules/date-fns/index.d.mts","../../node_modules/.bun/react-day-picker@8.10.1+97355278ff284068/node_modules/react-day-picker/dist/index.d.ts","./src/components/ui/calendar.tsx","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/alignment.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/noderects.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/axis.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/slidestoscroll.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/limit.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/scrollcontain.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/dragtracker.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/utils.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/animations.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/counter.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/eventhandler.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/eventstore.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/percentofview.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/resizehandler.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/vector1d.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/scrollbody.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/scrollbounds.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/scrolllooper.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/scrollprogress.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/slideregistry.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/scrolltarget.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/scrollto.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/slidefocus.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/translate.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/slidelooper.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/slideshandler.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/slidesinview.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/engine.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/optionshandler.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/plugins.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/emblacarousel.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/draghandler.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/components/options.d.ts","../../node_modules/.bun/embla-carousel@8.6.0/node_modules/embla-carousel/esm/index.d.ts","../../node_modules/.bun/embla-carousel-react@8.6.0+f4eacebf2041cd4f/node_modules/embla-carousel-react/esm/components/useemblacarousel.d.ts","../../node_modules/.bun/embla-carousel-react@8.6.0+f4eacebf2041cd4f/node_modules/embla-carousel-react/esm/index.d.ts","./src/components/ui/carousel.tsx","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/container/surface.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/container/layer.d.ts","../../node_modules/.bun/@types+d3-time@3.0.4/node_modules/@types/d3-time/index.d.ts","../../node_modules/.bun/@types+d3-scale@4.0.9/node_modules/@types/d3-scale/index.d.ts","../../node_modules/.bun/victory-vendor@36.9.2/node_modules/victory-vendor/d3-scale.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/xaxis.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/yaxis.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/util/types.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/defaultlegendcontent.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/util/payload/getuniqpayload.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/legend.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/defaulttooltipcontent.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/tooltip.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/responsivecontainer.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/cell.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/text.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/label.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/labellist.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/component/customized.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/sector.d.ts","../../node_modules/.bun/@types+d3-path@3.1.1/node_modules/@types/d3-path/index.d.ts","../../node_modules/.bun/@types+d3-shape@3.1.7/node_modules/@types/d3-shape/index.d.ts","../../node_modules/.bun/victory-vendor@36.9.2/node_modules/victory-vendor/d3-shape.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/curve.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/rectangle.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/polygon.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/dot.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/cross.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/symbols.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/polar/polargrid.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/polar/polarradiusaxis.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/polar/polarangleaxis.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/polar/pie.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/polar/radar.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/polar/radialbar.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/brush.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/util/ifoverflowmatches.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/referenceline.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/referencedot.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/referencearea.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/cartesianaxis.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/cartesiangrid.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/line.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/area.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/util/barutils.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/bar.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/zaxis.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/errorbar.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/cartesian/scatter.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/util/getlegendprops.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/util/chartutils.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/accessibilitymanager.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/types.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/generatecategoricalchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/linechart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/barchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/piechart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/treemap.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/sankey.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/radarchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/scatterchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/areachart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/radialbarchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/composedchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/sunburstchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/shape/trapezoid.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/numberaxis/funnel.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/chart/funnelchart.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/util/global.d.ts","../../node_modules/.bun/recharts@2.15.4+6dbf9a050bc9aadb/node_modules/recharts/types/index.d.ts","./src/components/ui/chart.tsx","./src/components/ui/collapsible.tsx","../../node_modules/.bun/@radix-ui+react-dismissable-layer@1.1.11+b41f8805ee63d2ff/node_modules/@radix-ui/react-dismissable-layer/dist/index.d.mts","../../node_modules/.bun/@radix-ui+react-dialog@1.1.15+b41f8805ee63d2ff/node_modules/@radix-ui/react-dialog/dist/index.d.mts","../../node_modules/.bun/cmdk@1.1.1+b41f8805ee63d2ff/node_modules/cmdk/dist/index.d.ts","./src/components/ui/command.tsx","../../node_modules/.bun/vaul@1.1.2+b41f8805ee63d2ff/node_modules/vaul/dist/index.d.mts","./src/components/ui/drawer.tsx","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/constants.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/utils/createsubject.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/events.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/path/common.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/path/eager.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/path/index.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/fieldarray.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/resolvers.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/form.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/utils.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/fields.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/errors.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/validator.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/controller.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/types/index.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/controller.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/form.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/logic/appenderrors.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/logic/createformcontrol.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/logic/index.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/usecontroller.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/usefieldarray.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/useform.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/useformcontext.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/useformstate.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/usewatch.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/utils/get.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/utils/set.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/utils/index.d.ts","../../node_modules/.bun/react-hook-form@7.60.0+f4eacebf2041cd4f/node_modules/react-hook-form/dist/index.d.ts","./src/components/ui/form.tsx","./src/components/ui/hover-card.tsx","../../node_modules/.bun/input-otp@1.4.2+6dbf9a050bc9aadb/node_modules/input-otp/dist/index.d.ts","./src/components/ui/input-otp.tsx","./src/components/ui/menubar.tsx","./src/components/ui/navigation-menu.tsx","./src/components/ui/pagination.tsx","../../node_modules/.bun/libphonenumber-js@1.12.10/node_modules/libphonenumber-js/types.d.ts","../../node_modules/.bun/libphonenumber-js@1.12.10/node_modules/libphonenumber-js/core/index.d.ts","../../node_modules/.bun/libphonenumber-js@1.12.10/node_modules/libphonenumber-js/min/index.d.ts","../../node_modules/.bun/react-phone-number-input@3.4.12+6dbf9a050bc9aadb/node_modules/react-phone-number-input/index.d.ts","../../node_modules/.bun/react-phone-number-input@3.4.12+6dbf9a050bc9aadb/node_modules/react-phone-number-input/flags/index.d.ts","./src/components/ui/phone-input.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/toast.tsx","./src/components/ui/toggle.tsx","./src/components/ui/toggle-group.tsx","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/header.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/readable.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/file.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/fetch.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/formdata.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/connector.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/client.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/errors.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/dispatcher.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/global-origin.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/pool-stats.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/pool.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/handlers.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/agent.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/mock-agent.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/mock-client.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/mock-pool.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/mock-errors.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/retry-handler.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/retry-agent.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/api.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/interceptors.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/util.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/cookies.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/patch.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/websocket.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/eventsource.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/filereader.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/content-type.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/cache.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/index.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/globals.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/s3.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/fetch.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/bun.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/globals.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/s3.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/fetch.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/bun.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/extensions.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/devserver.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/ffi.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/html-rewriter.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/jsc.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/sqlite.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/vendor/expect-type/utils.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/vendor/expect-type/overloads.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/vendor/expect-type/branding.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/vendor/expect-type/messages.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/vendor/expect-type/index.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/test.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/wasm.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/overrides.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/deprecated.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/redis.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/shell.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/experimental.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/sql.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/security.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/bun.ns.d.ts","../../node_modules/.bun/bun-types@1.2.21+fccada1f982c16e9/node_modules/bun-types/index.d.ts","../../node_modules/.bun/@types+bun@1.2.21+fccada1f982c16e9/node_modules/@types/bun/index.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/extensions.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/devserver.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/ffi.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/html-rewriter.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/jsc.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/sqlite.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/vendor/expect-type/utils.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/vendor/expect-type/overloads.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/vendor/expect-type/branding.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/vendor/expect-type/messages.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/vendor/expect-type/index.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/test.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/wasm.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/overrides.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/deprecated.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/redis.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/shell.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/serve.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/sql.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/security.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/bundle.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/bun.ns.d.ts","../../node_modules/.bun/bun-types@1.3.6/node_modules/bun-types/index.d.ts","../../node_modules/.bun/@types+bun@1.3.6/node_modules/@types/bun/index.d.ts","../../node_modules/.bun/pg-types@2.2.0/node_modules/pg-types/index.d.ts","../../node_modules/.bun/pg-protocol@1.10.3/node_modules/pg-protocol/dist/messages.d.ts","../../node_modules/.bun/pg-protocol@1.10.3/node_modules/pg-protocol/dist/serializer.d.ts","../../node_modules/.bun/pg-protocol@1.10.3/node_modules/pg-protocol/dist/parser.d.ts","../../node_modules/.bun/pg-protocol@1.10.3/node_modules/pg-protocol/dist/index.d.ts","../../node_modules/.bun/@types+pg@8.15.4/node_modules/@types/pg/lib/type-overrides.d.ts","../../node_modules/.bun/@types+pg@8.15.4/node_modules/@types/pg/index.d.ts"],"fileIdsList":[[85,87,90,135,186,264,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,584,585,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,584,587,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1421,1422,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,580,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,264,580,1438,1443,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1450,1819,1826,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,556,568,584,598,658,1827,1839,1841,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,558,584,598,658,1827,1838,1839,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,558,584,594,595,1447,1816,1827,1838,1843,1844,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,568,1761,1868,1890,1898,1919,1921,1931,1932,1966,1967,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,264,571,596,1817,1828,1829,1830,1833,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,584,595,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,584,595,1819,1826,1836,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,584,595,1827,1839,1969,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,556,558,568,1447,1465,1523,1581,1765,1816,1829,1843,1872,1880,1923,1924,1929,1967,1971,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,584,1450,1827,1844,1973,1974,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,595,658,1445,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,584,595,658,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,556,558,584,595,1447,1827,1843,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1447,1816,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1759,1839,1869,1875,1879,1882,1884,1888,1889,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1450,1759,1829,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1457,1458,1469,1470,1550,1551,1581,1630,1758,1816,1878,1886,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1450,1464,1468,1485,1523,1552,1581,1584,1606,1759,1816,1829,1870,1871,1872,1874,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,556,1447,1450,1465,1466,1579,1580,1581,1760,1766,1878,1885,1886,1887,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1450,1504,1585,1607,1608,1816,1839,1872,1876,1877,1880,1881,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,556,1447,1450,1467,1523,1585,1626,1762,1763,1816,1829,1874,1876,1878,1883,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1469,1475,1581,1874,1878,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1816,1881,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,274,275,276,1447,1450,1581,1582,1583,1816,1829,1873,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1450,1513,1514,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,558,568,595,1447,1523,1581,1816,1818,1825,1872,1922,1923,1924,1930,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,594,1447,1450,1461,1462,1581,1625,1816,1886,1925,1926,1927,1928,1929,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,556,1760,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1447,1581,1881,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,595,1447,1816,1881,1965,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1450,1839,1876,1877,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1556,1558,1570,1581,1605,1920,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1447,1581,1611,1876,1891,1892,1897,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1450,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1450,1463,1464,1466,1581,1764,1767,1768,1816,1829,1878,1880,1886,1893,1894,1895,1896,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1450,1508,1523,1581,1816,1881,1912,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1903,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1447,1468,1581,1876,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1617,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1464,1468,1477,1581,1610,1611,1614,1615,1618,1619,1620,1621,1622,1623,1871,1876,1906,1907,1908,1909,1914,1916,1917,1918,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1616,1621,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1447,1464,1468,1477,1485,1526,1581,1590,1611,1621,1624,1759,1871,1904,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1468,1581,1619,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1466,1468,1581,1876,1915,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1468,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1447,1450,1468,1514,1581,1590,1610,1816,1829,1895,1910,1911,1913,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1450,1464,1468,1581,1611,1613,1621,1905,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,556,558,595,1768,1824,1825,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,558,594,595,1447,1816,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,556,558,594,595,1447,1450,1455,1816,1818,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1523,1602,1604,1816,1881,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,556,558,1447,1816,1835,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1450,1455,1456,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,568,1447,1581,1603,1612,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1816,1880,1881,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1512,1523,1581,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1447,1450,1816,1817,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1450,1813,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1450,1813,1816,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1450,1815,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1813,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1450,1813,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1450,1813,1815,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1450,1816,2240,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1450,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1450,1816,2277,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1450,2290,2348,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,274,275,276,1450,1880,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1450,1881,2352,2353,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1450,1813,1815,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1450,1813,1876,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1450,2355,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1463,1885,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1450,1813,1926,2386,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1450,2389,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1455,1816,1880,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1450,1816,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1447,1450,1816,1876,1880,1925,2354,2397,2398,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1450,1965,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1450,1867,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1450,1803,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1450,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1523,1817,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1450,1816,1839,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1450,1813,1815,2403,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1457,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1459,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1461,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1465,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,594,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1468,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1447,1464,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1470,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1472,1501,1522,1553,1555,1571,1576,1577,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1575,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1473,1475,1485,1512,1554,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1461,1464,1465,1466,1473,1508,1511,1512,1523,1525,1552,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1461,1550,1558,1560,1570,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1474,1508,1512,1515,1521,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1477,1486,1500,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1590,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1581,1609,1610,1611,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1464,1468,1473,1514,1581,1611,1617,1621,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1464,1514,1578,1610,1621,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1610,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1468,1477,1581,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1464,1468,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1464,1468,1475,1514,1523,1552,1581,1582,1583,1617,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1468,1514,1581,1613,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1468,1514,1581,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1468,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1583,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1590,1602,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1590,1601,1602,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1607,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1586,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1587,1588,1589,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1588,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1587,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,264,1628,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,264,1113,1115,1117,1420,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,598,608,639,646,648,652,657,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1478,1500,1521,1525,1574,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1473,1478,1485,1512,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1572,1573,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1475,1478,1485,1512,1554,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1524,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1465,1478,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1478,1508,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1516,1517,1518,1519,1520,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1478,1508,1515,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1498,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1473,1474,1475,1478,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1477,1478,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1473,1474,1478,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1468,1473,1474,1475,1478,1485,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1474,1478,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1473,1478,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1475,1478,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1478,1578,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1483,1497,1499,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1479,1480,1481,1482,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,264,1118,1418,1419,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1318,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1582,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1461,1462,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1565,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1565,1566,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1475,1477,1485,1526,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1485,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1485,1549,1550,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1485,1523,1526,1549,1551,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,264,1117,1442,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1473,1474,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1513,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1514,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1468,1474,1475,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1468,1469,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1474,1475,1476,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1468,1473,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1470,1471,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1549,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1557,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1556,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1556,1557,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1556,1557,1567,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1556,1557,1561,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1556,1557,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1549,1556,1557,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1465,1466,1477,1485,1558,1561,1562,1563,1564,1568,1569,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1549,1556,1558,1559,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1484,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1505,1506,1507,1509,1510,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1502,1505,1506,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1484,1502,1505,1508,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1466,1502,1505,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1502,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1465,1484,1485,1502,1503,1504,1511,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1465,1485,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1470,1471,1757,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1470,1471,1756,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1447,1593,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1459,1465,1466,1593,1599,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1586,1590,1593,1599,1600,1601,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1460,1593,1599,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1475,1504,1512,1523,1578,1593,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1467,1475,1578,1593,1626,1762,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1593,1599,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1464,1593,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1590,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1467,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,1448,1449,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,671,672,675,676,680,690,691,692,730,765,1018,1035,1036,1039,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,134,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,671,765,1035,1104,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1104,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1105,1106,1107,1108,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,134,135,690,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,681,682,683,684,685,686,687,690,1018,1035,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1036,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,680,1018,1035,1036,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,681,682,683,684,685,686,687,688,689,1018,1035,1036,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,660,690,1018,1035,1036,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1018,1035,1036,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,673,674,675,676,677,678,679,1036,1037,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,263,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,660,1034,1035,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,691,1048,1049,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1084,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,660,672,765,1018,1033,1035,1038,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,692,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,692,730,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,692,693,694,695,696,728,729,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,660,1035,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,692,727,730,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,727,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,263,692,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,671,672,673,675,676,677,680,690,691,692,730,1018,1036,1038,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,671,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,660,672,765,1018,1033,1034,1038,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,166,660,671,673,674,675,676,677,680,690,691,764,765,766,1017,1036,1038,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,660,1018,1019,1032,1038,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,660,671,680,765,766,1017,1018,1037,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,691,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1097,1098,1099,1100,1101,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,680,1039,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1643,1646,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1648,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1648,1651,1661,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1648,1654,1663,1664,1756,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1645,1646,1647,1648,1651,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1643,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1652,1655,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1651,1660,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1651,1655,1656,1657,1658,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1654,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1648,1651,1652,1656,1657,1659,1660,1661,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1651,1664,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1648,1664,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1648,1663,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1665,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1663,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1671,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1651,1661,1663,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1663,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1654,1665,1730,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1654,1661,1665,1666,1685,1730,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1654,1661,1665,1730,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720,1721,1722,1723,1724,1725,1726,1727,1728,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1651,1654,1661,1663,1665,1730,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1703,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1663,1756,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1651,1654,1665,1730,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1654,1665,1685,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1694,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1711,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1651,1663,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1665,1677,1756,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1665,1730,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1718,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1746,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1719,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1699,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1734,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1673,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1664,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1661,1663,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1656,1659,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1648,1651,1654,1656,1661,1662,1665,1666,1667,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1648,1651,1653,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1644,1646,1647,1648,1650,1651,1652,1654,1655,1657,1658,1661,1662,1663,1664,1665,1666,1667,1668,1669,1684,1685,1729,1730,1755,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1651,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1643,1645,1646,1647,1652,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,420,1651,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1649,1650,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1661,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1770,1771,1772,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1770,1777,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1771,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1770,1771,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1770,1771,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1770,1771,1786,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1770,1771,1774,1775,1776,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1770,1771,1775,1776,2351,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1770,1771,1790,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1770,1771,1774,1776,1784,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1770,1771,1774,1775,1776,1784,1785,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1770,1771,1785,1786,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1770,1771,1774,1794,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1771,1785,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1770,1771,1774,1775,1776,1784,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1770,1771,1782,1783,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1770,1771,1785,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1770,1771,1774,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1770,1771,1785,1809,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1770,1771,1785,1803,1810,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2471,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492,2495],[90,135,2281,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2299,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1528,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,599,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,132,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,140,170,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,136,141,147,148,155,167,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,136,137,147,155,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,138,179,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,139,140,148,156,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,140,167,175,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,141,143,147,155,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,134,135,142,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,143,144,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,145,147,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,134,135,147,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,147,148,149,167,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,147,148,149,162,167,170,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,130,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,130,135,143,147,150,155,167,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,147,148,150,151,155,167,175,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,152,167,175,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[88,89,90,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,147,153,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,154,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,143,147,155,167,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,156,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,157,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,134,135,158,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,132,133,134,135,136,137,138,139,140,141,142,143,144,145,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,160,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,161,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,147,162,163,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,162,164,179,181,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,147,167,168,170,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,169,170,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,167,168,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,170,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,171,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,132,135,167,172,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,147,173,174,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,173,174,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,140,155,167,175,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,176,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,155,177,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,161,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,140,179,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,167,180,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,154,181,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,182,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,147,149,158,167,170,178,180,181,183,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,167,184,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,147,167,175,185,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492,2497,2498,2501,2502,2503],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492,2503],[84,90,135,274,275,276,490,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,274,275,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,275,490,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,269,273,531,577,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,269,272,531,577,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[81,82,83,90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,590,591,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,592,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1440,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1117,1441,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1439,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1116,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,690,1039,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1114,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,253,263,730,766,1039,1040,1041,1050,1062,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1086,1087,1091,1113,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1068,1069,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1039,1062,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,263,730,1062,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,263,1062,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,263,1039,1062,1066,1087,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1062,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,263,1039,1040,1062,1066,1087,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1039,1066,1086,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1019,1039,1040,1062,1093,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1019,1032,1039,1040,1085,1093,1627,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1032,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1039,1040,1041,1066,1086,1087,1091,1092,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,680,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,680,1034,1039,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,680,1039,1045,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,680,1041,1042,1043,1044,1046,1047,1051,1088,1089,1090,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,690,1039,1050,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,680,1039,1066,1087,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,263,680,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,680,690,1039,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,253,263,680,727,730,764,1039,1040,1050,1062,1063,1064,1065,1066,1083,1085,1086,1087,1093,1094,1095,1096,1102,1103,1109,1110,1112,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1039,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,690,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1040,1062,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1039,1040,1063,1064,1083,1085,1087,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,764,1039,1040,1063,1064,1065,1086,1087,1093,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,680,1039,1040,1041,1086,1091,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,680,1039,1040,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1095,1096,1102,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1052,1053,1054,1055,1057,1058,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1052,1053,1054,1055,1056,1058,1059,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1053,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1052,1057,1059,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1057,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1057,1058,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,661,662,663,664,666,667,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,661,662,663,664,665,667,668,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,662,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,661,662,663,664,665,666,667,668,669,670,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,661,666,668,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,666,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,666,667,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,584,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,140,148,150,175,179,183,2441,2442,2443,2444,2445,2446,2447,2448,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2441,2442,2443,2444,2445,2446,2447,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,140,158,167,170,175,179,183,2441,2442,2443,2444,2445,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,185,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2453,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2470,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,140,148,149,156,170,175,178,184,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,148,2442,2443,2444,2445,2446,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2460,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2456,2457,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2456,2457,2458,2459,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2456,2458,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2456,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,140,148,150,175,179,183,2441,2442,2443,2444,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2441,2442,2443,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,140,158,167,170,175,179,183,2441,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,185,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2476,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492,2493,2494],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[13,90,131,135,138,140,148,149,156,170,175,178,184,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2489,2490,2491,2492],[90,135,148,2442,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2483,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2479,2480,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2479,2480,2481,2482,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2479,2481,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2479,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2486,2487,2488,2489,2490,2491,2492],[90,135,1448,1814,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1448,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1777,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1985,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1983,1985,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1983,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1985,2049,2050,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2052,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2053,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2070,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2033,2034,2035,2036,2037,2038,2039,2040,2041,2042,2043,2044,2045,2046,2047,2048,2051,2052,2053,2054,2055,2056,2057,2058,2059,2060,2061,2062,2063,2064,2065,2066,2067,2068,2069,2071,2072,2073,2074,2075,2076,2077,2078,2079,2080,2081,2082,2083,2084,2085,2086,2087,2088,2089,2090,2091,2092,2093,2094,2095,2096,2097,2098,2099,2100,2101,2102,2103,2104,2105,2106,2107,2108,2109,2110,2111,2112,2113,2114,2115,2116,2117,2118,2119,2120,2121,2122,2123,2124,2125,2126,2127,2128,2129,2130,2131,2132,2133,2134,2135,2136,2137,2138,2139,2140,2141,2142,2143,2144,2145,2147,2148,2149,2150,2151,2152,2153,2154,2155,2156,2157,2158,2159,2160,2161,2162,2163,2164,2165,2166,2171,2172,2173,2174,2175,2176,2177,2178,2179,2180,2181,2182,2183,2184,2185,2186,2187,2188,2189,2190,2191,2192,2193,2194,2195,2196,2197,2198,2199,2200,2201,2202,2203,2204,2205,2206,2207,2208,2209,2210,2211,2212,2213,2214,2215,2216,2217,2218,2219,2220,2221,2222,2223,2224,2225,2226,2227,2228,2229,2230,2231,2232,2233,2234,2235,2236,2237,2238,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2146,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1985,2050,2170,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1983,2167,2168,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2169,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2167,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1983,1984,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,178,185,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,175,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[86,90,135,175,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1122,1126,1172,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1171,1410,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1411,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1196,1251,1318,1370,1404,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1122,1126,1127,1405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1165,1170,1192,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1135,1165,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1150,1151,1152,1153,1154,1155,1156,1157,1158,1168,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1138,1167,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1167,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1160,1165,1166,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1165,1167,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1167,1405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1165,1167,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1150,1151,1152,1153,1154,1155,1156,1157,1158,1167,1168,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1137,1167,1405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1149,1167,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1149,1165,1167,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1126,1127,1132,1165,1169,1170,1172,1174,1177,1178,1179,1181,1187,1188,1192,1411,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1165,1169,1172,1187,1191,1192,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1165,1169,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1136,1137,1160,1161,1162,1163,1164,1165,1166,1169,1179,1180,1181,1187,1188,1190,1191,1193,1194,1195,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1165,1169,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1161,1165,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1165,1181,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1125,1126,1165,1175,1176,1181,1188,1192,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1182,1183,1184,1185,1186,1189,1192,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1122,1124,1125,1126,1132,1160,1165,1167,1175,1176,1181,1183,1188,1189,1192,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1126,1132,1169,1179,1186,1188,1192,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1165,1172,1175,1176,1181,1188,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1173,1175,1176,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1175,1176,1181,1188,1191,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1125,1126,1127,1132,1165,1169,1170,1171,1175,1176,1179,1181,1188,1192,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1122,1123,1124,1125,1126,1127,1132,1165,1169,1170,1181,1186,1191,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1122,1124,1125,1126,1127,1165,1167,1170,1175,1176,1181,1188,1192,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1137,1165,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1135,1171,1172,1173,1180,1188,1192,1411,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1124,1125,1126,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1122,1136,1159,1160,1162,1163,1164,1166,1167,1405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1124,1126,1136,1160,1162,1163,1164,1165,1166,1169,1170,1191,1196,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1125,1126,1127,1132,1167,1170,1189,1190,1405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1120,1122,1123,1124,1127,1135,1172,1175,1405,1406,1407,1408,1409,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1226,1234,1247,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1226,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1217,1218,1219,1220,1221,1229,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1228,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1228,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1226,1227,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1226,1228,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1226,1228,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1217,1218,1219,1220,1221,1228,1229,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1208,1228,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1216,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1126,1127,1172,1226,1233,1234,1239,1240,1241,1242,1244,1247,1411,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1172,1226,1228,1231,1232,1237,1238,1244,1247,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1226,1230,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1197,1223,1224,1225,1226,1227,1230,1233,1239,1241,1243,1244,1245,1246,1248,1249,1250,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1226,1230,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1226,1234,1244,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1126,1127,1175,1226,1228,1239,1244,1247,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1232,1235,1236,1237,1238,1247,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1122,1126,1132,1171,1175,1176,1226,1228,1236,1237,1239,1244,1247,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1233,1235,1239,1247,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1172,1175,1226,1239,1244,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1125,1126,1127,1132,1171,1175,1223,1226,1230,1233,1234,1239,1244,1247,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1122,1123,1124,1125,1126,1127,1132,1226,1230,1234,1235,1244,1246,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1126,1127,1171,1175,1226,1228,1239,1244,1247,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1226,1246,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1171,1172,1239,1243,1247,1411,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1124,1125,1126,1132,1236,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1122,1197,1222,1223,1224,1225,1227,1228,1405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1124,1197,1223,1224,1225,1226,1227,1234,1235,1246,1251,1410,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1125,1126,1132,1230,1234,1236,1245,1405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1122,1126,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1293,1299,1312,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1135,1293,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1253,1254,1255,1256,1257,1259,1260,1261,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1296,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1263,1295,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1295,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1295,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1288,1293,1294,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1293,1295,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1295,1405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1258,1295,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1293,1295,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1253,1254,1255,1256,1257,1259,1260,1261,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1295,1296,1297,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1262,1295,1405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1265,1295,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1293,1295,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1258,1265,1293,1295,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1258,1293,1295,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1126,1127,1172,1293,1298,1299,1300,1304,1305,1307,1308,1311,1312,1411,1412,1413,1414,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1172,1231,1293,1298,1300,1307,1311,1312,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1293,1298,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1252,1262,1288,1289,1290,1291,1292,1293,1294,1298,1300,1305,1307,1308,1310,1311,1313,1314,1315,1317,1415,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1293,1298,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1289,1293,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1293,1300,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1125,1126,1132,1171,1175,1176,1293,1300,1308,1312,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1301,1302,1303,1304,1306,1309,1312,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1122,1124,1125,1126,1132,1171,1175,1176,1288,1293,1295,1300,1302,1308,1309,1312,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1126,1298,1305,1306,1308,1312,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1172,1175,1176,1293,1300,1308,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1175,1176,1300,1308,1311,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1125,1126,1127,1132,1171,1175,1176,1293,1298,1299,1300,1305,1308,1312,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1122,1123,1124,1125,1126,1127,1132,1293,1298,1299,1300,1306,1311,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1122,1124,1125,1126,1127,1132,1171,1175,1176,1293,1295,1299,1300,1308,1312,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1262,1293,1297,1311,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1135,1171,1172,1173,1308,1312,1411,1415,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1124,1125,1126,1132,1309,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1122,1252,1287,1288,1290,1291,1292,1294,1295,1405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1124,1126,1252,1288,1290,1291,1292,1293,1294,1298,1299,1311,1318,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1316,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1125,1126,1127,1132,1295,1299,1309,1310,1405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1118,1119,1127,1415,1416,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1416,1417,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1118,1119,1120,1126,1127,1171,1172,1300,1308,1312,1318,1356,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1122,1123,1124,1126,1127,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1122,1126,1127,1130,1406,1410,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1410,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1348,1366,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1338,1339,1340,1341,1342,1343,1350,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1349,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1349,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1348,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1348,1349,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1348,1349,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1135,1349,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1338,1339,1340,1341,1342,1343,1349,1350,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1329,1349,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1337,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1126,1172,1348,1355,1358,1359,1360,1363,1365,1366,1411,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1172,1231,1348,1349,1352,1353,1354,1365,1366,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1345,1346,1347,1348,1351,1355,1360,1363,1364,1365,1367,1368,1369,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1348,1351,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1348,1351,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1348,1365,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1126,1127,1175,1348,1349,1355,1365,1366,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1352,1353,1354,1361,1362,1366,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1122,1126,1175,1176,1348,1349,1353,1355,1365,1366,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1355,1360,1361,1366,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1125,1126,1127,1132,1171,1175,1348,1351,1355,1360,1365,1366,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1122,1123,1124,1125,1126,1127,1132,1348,1351,1361,1365,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1126,1127,1175,1348,1349,1355,1365,1366,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1348,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1171,1172,1355,1364,1366,1411,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1124,1125,1126,1132,1362,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1122,1344,1345,1346,1347,1349,1405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1124,1126,1345,1346,1347,1348,1370,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1120,1127,1172,1355,1357,1364,1411,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1120,1126,1127,1171,1172,1355,1356,1365,1366,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1126,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1128,1129,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1131,1133,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1126,1132,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1126,1130,1134,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1121,1122,1124,1125,1127,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1376,1397,1402,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1397,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1372,1392,1393,1394,1395,1400,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1399,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1397,1398,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1397,1399,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1372,1392,1393,1394,1395,1399,1400,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1391,1397,1399,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1399,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1127,1397,1399,1405,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1126,1127,1172,1376,1377,1378,1379,1382,1387,1388,1397,1402,1411,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1172,1231,1382,1387,1397,1401,1402,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1397,1401,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1371,1373,1374,1375,1379,1380,1382,1387,1388,1390,1391,1397,1398,1401,1403,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1397,1401,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1382,1390,1397,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1125,1126,1127,1175,1176,1382,1388,1397,1399,1402,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1383,1384,1385,1386,1389,1402,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1125,1126,1127,1132,1175,1176,1373,1382,1384,1388,1389,1397,1399,1402,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1379,1386,1388,1402,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1127,1172,1175,1176,1382,1388,1397,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1173,1175,1176,1388,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1125,1126,1127,1132,1171,1175,1176,1376,1379,1382,1388,1397,1401,1402,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1122,1123,1124,1125,1126,1127,1132,1376,1382,1386,1390,1397,1401,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1124,1125,1126,1127,1175,1176,1376,1382,1388,1397,1399,1402,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1126,1171,1172,1173,1175,1380,1381,1388,1402,1411,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1124,1125,1126,1132,1389,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1122,1371,1373,1374,1375,1396,1398,1399,1405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1397,1399,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1124,1126,1371,1373,1374,1375,1376,1390,1397,1398,1404,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1125,1126,1132,1376,1389,1399,1405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1119,1123,1126,1127,1406,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1120,1122,1126,1406,1411,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2275,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2276,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2249,2269,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2243,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2244,2248,2249,2250,2251,2252,2254,2256,2257,2262,2263,2272,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2244,2249,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2252,2269,2271,2274,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2243,2244,2245,2246,2249,2250,2251,2252,2253,2254,2255,2256,2257,2258,2259,2260,2261,2262,2263,2264,2265,2266,2267,2268,2273,2274,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2272,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2242,2244,2245,2247,2255,2264,2267,2268,2273,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2249,2274,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2270,2272,2274,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2243,2244,2249,2252,2272,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2256,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2246,2254,2256,2257,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2246,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2246,2256,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2250,2251,2252,2256,2257,2262,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2252,2253,2257,2261,2263,2272,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2244,2256,2265,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2245,2246,2247,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2252,2272,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2252,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2243,2244,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2244,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2248,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2252,2257,2269,2270,2271,2272,2274,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,614,615,616,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1451,1452,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,1451,1452,1453,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,633,635,636,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,603,609,633,635,1961,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,601,633,634,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,599,603,609,632,1961,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,603,609,626,631,633,1961,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,649,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,609,633,1961,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,653,654,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,655,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,644,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,609,631,633,643,1961,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,697,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,859,963,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,963,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,855,857,858,859,963,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,963,980,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,778,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,855,857,858,859,860,963,1000,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,854,856,857,1000,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,858,963,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,783,784,798,812,813,842,976,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,859,963,980,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,856,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,855,857,858,859,860,963,987,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,854,855,856,857,987,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,800,976,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,855,857,858,859,860,963,993,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,854,855,856,857,993,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,976,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,855,857,858,859,860,963,981,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,855,856,857,981,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,846,969,976,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,854,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,856,857,861,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,855,856,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,856,857,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,856,861,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,819,825,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,816,825,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,881,884,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,778,780,826,863,868,876,877,878,879,882,898,900,909,911,916,917,918,920,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,778,780,816,826,879,895,896,897,920,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,816,825,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,768,769,770,771,772,773,774,775,776,777,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,866,867,869,870,875,876,877,878,879,880,881,882,883,884,885,886,887,888,890,891,892,894,895,896,897,898,899,900,902,903,904,905,908,909,910,911,912,913,914,915,916,919,920,921,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,947,948,949,950,951,952,957,959,960,963,964,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,826,853,854,856,857,858,860,862,863,864,909,911,933,940,941,959,960,961,962,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1005,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,782,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,791,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,768,786,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,799,814,815,904,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,770,786,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,768,774,783,784,785,787,792,793,794,795,796,797,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,841,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,768,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,769,770,771,772,781,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,770,774,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,821,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,769,788,789,790,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,768,774,786,799,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,774,780,782,791,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,773,803,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,770,773,786,833,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,799,805,810,811,814,815,823,828,832,839,840,849,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,770,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,773,774,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,774,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,773,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,827,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,830,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,768,770,774,781,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,806,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,770,774,823,828,832,839,840,844,845,846,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,809,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,830,876,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,876,912,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,818,913,914,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,774,810,816,823,832,839,840,841,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,768,770,799,843,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,843,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,768,769,770,771,772,773,774,781,782,783,784,785,786,787,788,789,790,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,820,821,822,823,824,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,852,853,867,875,876,895,896,897,902,903,904,905,910,912,913,914,915,942,943,968,969,970,971,972,973,974,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,768,769,770,771,772,773,774,781,782,783,784,785,786,787,788,789,790,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,820,821,822,823,824,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,852,867,875,876,895,896,897,902,903,904,905,910,912,913,914,915,942,943,968,969,970,971,972,973,974,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,813,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,814,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,814,815,902,903,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,819,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,902,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,768,770,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,799,810,814,815,820,826,827,828,832,833,839,840,842,847,848,850,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,770,774,817,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,770,774,780,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,820,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,799,805,806,807,808,810,811,812,814,815,820,823,824,828,829,831,832,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,774,816,817,819,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,770,818,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,799,805,810,811,815,823,828,832,839,840,843,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,803,942,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,822,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,825,826,875,876,877,878,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,826,867,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,826,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,776,780,882,952,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,816,826,874,919,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,806,919,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,770,877,878,919,943,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,810,880,882,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,779,780,882,957,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,814,826,884,887,920,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,884,902,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,770,780,816,818,819,826,874,876,878,884,888,915,920,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,775,776,777,779,885,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,786,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,882,900,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,820,826,878,884,900,919,920,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,826,829,919,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,774,780,816,826,881,920,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,877,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,876,920,921,970,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,777,780,882,951,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,843,877,878,919,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,826,830,874,920,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,822,826,950,951,952,953,959,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,855,856,862,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,855,856,862,1011,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,803,875,876,942,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,853,855,856,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,816,826,879,888,899,905,907,920,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,778,826,877,879,898,910,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,822,825,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,776,778,780,825,826,827,850,851,853,854,862,863,864,877,879,882,883,885,888,890,891,894,899,920,921,946,947,949,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,778,780,826,874,878,898,901,908,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,879,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,775,778,780,825,826,827,847,851,853,854,862,863,864,878,885,891,894,920,944,945,946,947,948,949,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,810,825,879,920,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,816,826,913,915,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,779,780,825,826,842,851,853,854,863,864,877,879,882,883,885,891,920,921,944,945,946,947,949,951,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,851,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,825,826,844,878,879,890,920,921,945,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,826,888,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,814,825,886,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,919,920,946,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,825,826,888,899,904,906,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,878,885,946,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,826,833,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,778,780,826,827,831,832,833,851,853,854,862,863,864,874,877,878,879,882,883,885,888,889,890,891,892,893,894,898,899,920,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,777,778,780,825,826,827,848,851,853,854,862,863,864,877,879,882,883,885,888,890,891,894,899,920,921,945,946,947,949,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,879,920,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,853,855,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,767,768,769,770,771,772,773,774,781,782,783,784,785,786,787,788,789,790,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,820,821,822,823,824,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,852,853,854,855,867,875,876,895,896,897,902,903,904,905,910,912,913,914,915,942,943,968,969,970,971,972,973,974,975,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,786,795,798,800,801,802,804,834,835,836,837,838,842,851,852,853,854,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,775,823,862,863,882,885,900,918,950,953,954,955,956,958,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,853,854,855,856,859,861,862,965,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,854,859,862,965,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,853,854,855,856,859,861,862,863,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,863,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,853,854,855,856,859,861,862,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,786,826,853,854,856,862,933,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,934,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,787,825,865,868,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,781,798,825,853,854,863,864,869,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,798,800,825,826,853,854,863,864,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,798,825,826,853,854,863,864,866,868,869,870,871,872,873,922,923,924,925,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,798,825,853,854,863,864,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,769,825,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,781,782,825,826,865,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,800,825,826,853,854,863,864,879,919,921,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,801,825,853,854,863,864,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,802,825,826,853,854,863,864,866,868,869,923,924,925,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,804,825,853,854,863,864,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,825,834,853,854,863,864,900,934,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,795,825,853,854,863,864,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,825,835,853,854,863,864,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,825,836,853,854,863,864,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,825,837,853,854,863,864,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,825,838,853,854,863,864,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,781,788,825,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,789,825,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,825,852,853,854,863,864,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,862,863,926,927,928,929,930,931,932,935,936,937,938,939,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,790,825,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,826,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,775,776,777,779,780,854,864,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,780,854,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,775,776,777,778,779,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2394,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,609,633,1933,1934,1935,1959,1960,1961,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,609,633,1934,1961,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,609,633,1933,1934,1961,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,603,609,633,1933,1935,1961,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1531,1532,1535,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1533,1534,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1530,1531,1535,1536,1537,1541,1545,1546,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1530,1532,1535,1536,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1532,1535,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1528,1529,1530,1531,1532,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1530,1531,1532,1535,1541,1544,1546,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1531,1542,1543,1545,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1530,1532,1536,1545,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1532,1535,1536,1537,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1535,1538,1541,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1530,1531,1538,1539,1540,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1530,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1451,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1454,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1021,1022,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1020,1021,1024,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1020,1021,1026,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1021,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1025,1032,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1020,1021,1022,1023,1024,1025,1027,1028,1029,1030,1031,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1020,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,534,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,536,537,538,539,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,281,283,287,298,487,515,527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,283,293,294,295,297,527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,283,330,332,334,335,338,527,529,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,283,287,289,290,291,321,415,487,505,506,514,527,529,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,294,385,494,503,523,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,283,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,277,385,523,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,340,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,339,527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,485,494,582,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,453,465,503,522,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,396,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,508,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,507,508,509,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,507,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,271,277,283,287,290,292,294,298,299,312,313,340,415,426,504,515,527,531,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,281,283,296,330,331,336,337,527,582,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,296,582,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,281,313,440,527,582,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,582,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,283,296,297,582,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,333,582,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,299,505,513,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,161,523,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,523,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,457,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,383,393,394,523,559,566,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,382,500,560,561,562,563,565,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,499,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,499,500,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,321,385,386,390,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,385,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,385,389,391,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,385,386,387,388,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,564,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,284,553,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,296,375,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,296,515,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,373,377,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,374,533,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1831,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,150,185,269,272,273,531,575,576,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,287,320,371,416,437,439,510,511,515,527,528,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,312,512,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,531,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,282,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,442,455,464,474,476,522,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,161,442,455,473,474,475,522,581,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,467,468,469,470,471,472,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,469,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,473,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[85,90,135,347,348,350,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,341,342,343,344,349,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,347,349,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,345,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,346,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,374,533,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,532,533,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,533,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,416,517,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,517,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,528,533,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,461,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,134,135,460,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,322,385,402,439,448,451,453,454,493,522,525,528,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,367,385,482,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,453,522,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,453,458,459,461,462,463,464,465,466,477,478,479,480,481,483,484,522,523,582,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,447,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,161,284,320,323,344,368,369,416,426,437,438,493,516,527,528,529,531,582,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,522,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,134,135,294,369,426,450,516,518,519,520,521,528,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,453,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,134,135,320,356,402,443,444,445,446,447,448,449,451,452,522,523,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,356,357,443,528,529,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,294,416,426,439,516,522,528,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,527,529,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,167,525,528,529,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,161,178,277,287,296,322,323,325,353,358,363,367,368,369,371,400,402,404,407,409,412,413,414,415,437,439,515,516,523,525,527,528,529,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,167,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,283,284,285,292,525,526,531,533,582,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,281,527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,352,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,167,178,315,338,340,341,342,343,344,350,351,582,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,161,178,277,315,330,362,363,364,400,401,402,407,415,416,422,425,427,437,439,516,523,525,527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,292,299,312,415,426,516,527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,178,284,287,402,420,525,527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,441,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,352,423,424,434,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,525,527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,448,450,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,369,402,515,533,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,161,326,330,401,407,422,425,429,525,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,299,312,330,430,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,283,325,432,515,527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,178,344,527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,296,324,325,326,335,352,431,433,515,527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,271,369,436,531,533,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,399,437,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,161,178,287,298,299,312,322,323,358,362,363,364,368,400,401,402,404,416,417,419,421,437,439,515,516,523,524,525,533,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,167,299,422,428,434,525,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,302,303,304,305,306,307,308,309,310,311,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,353,408,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,410,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,408,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,410,411,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,287,290,320,321,528,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,161,282,284,322,367,368,369,370,398,437,525,529,531,533,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,161,178,286,321,370,402,448,516,524,528,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,443,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,444,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,385,415,493,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,445,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,314,318,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,287,314,322,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,317,318,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,319,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,314,315,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,314,365,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,314,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,353,406,524,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,405,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,315,523,524,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,403,524,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,315,523,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,493,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,287,316,322,369,385,402,436,439,442,448,455,456,486,487,489,492,515,525,528,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,378,381,383,384,393,394,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,274,275,276,488,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,85,90,135,274,275,276,488,491,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,502,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,294,357,369,436,439,453,461,465,495,496,497,498,500,501,504,515,522,527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,393,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,398,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,398,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,322,366,371,395,397,436,525,531,533,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,378,379,380,381,383,384,393,394,532,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,161,178,271,314,315,323,368,369,402,434,435,437,515,516,525,527,528,531,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,357,359,362,516,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,353,527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,356,453,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,355,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,357,358,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,354,356,527,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,150,286,357,359,360,361,527,528,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,385,392,523,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,279,280,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,284,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,382,523,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,271,368,369,531,533,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,284,553,554,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,377,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,161,178,282,337,372,374,376,533,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,296,523,528,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,418,523,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,148,150,161,281,282,332,377,531,532,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,272,273,531,577,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,266,267,268,269,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,140,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,327,328,329,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,327,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,150,152,161,185,269,272,273,274,276,277,282,323,429,473,529,530,533,577,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,541,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,543,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,545,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1832,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,547,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,549,550,551,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,555,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,270,535,540,542,544,546,548,552,556,558,568,569,571,580,581,582,583,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,557,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,567,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,374,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,570,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,134,135,357,359,360,362,572,573,574,577,578,579,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,185,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1638,1640,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1637,1638,1639,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1635,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1631,1632,1633,1636,1637,1638,1640,1641,1642,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1631,1635,1637,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1636,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1632,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1633,1636,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1632,1634,1635,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,611,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,610,611,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,610,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,610,611,612,618,619,622,623,624,625,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,611,619,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,610,611,612,618,619,620,621,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,610,619,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,619,623,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,611,612,613,617,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,612,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,610,611,619,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,185,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492,2498,2499,2500],[90,135,167,185,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492,2498],[90,135,167,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,628,629,630,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,627,631,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,631,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1769,1772,1773,1776,1777,1778,1779,1780,1781,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809,1810,1811,1812,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2239,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2371,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2371,2372,2373,2376,2377,2378,2379,2380,2381,2382,2385,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2371,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2374,2375,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2369,2371,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2366,2367,2369,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2362,2365,2367,2369,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2366,2369,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2357,2358,2359,2362,2363,2364,2366,2367,2368,2369,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2359,2362,2363,2364,2365,2366,2367,2368,2369,2370,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2366,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2360,2366,2367,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2360,2361,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2365,2367,2368,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2365,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2357,2362,2367,2368,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2383,2384,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1823,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1820,1821,1822,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1964,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,608,609,633,639,646,1961,1963,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2397,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2395,2396,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1847,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1864,1865,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1848,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,1850,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1848,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1847,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1863,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1866,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2284,2285,2286,2302,2305,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2284,2285,2286,2295,2303,2323,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2283,2286,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2286,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2284,2285,2286,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2284,2285,2286,2321,2324,2327,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2284,2285,2286,2295,2302,2305,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2284,2285,2286,2295,2303,2315,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2284,2285,2286,2295,2305,2315,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2284,2285,2286,2295,2315,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2284,2285,2286,2290,2296,2302,2307,2325,2326,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2286,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2286,2330,2331,2332,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2286,2329,2330,2331,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2286,2303,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2286,2329,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2286,2295,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2286,2287,2288,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2286,2288,2290,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2279,2280,2284,2285,2286,2287,2289,2290,2291,2292,2293,2294,2295,2296,2297,2298,2302,2303,2304,2305,2306,2307,2308,2309,2310,2311,2312,2313,2314,2316,2317,2318,2319,2320,2321,2322,2324,2325,2326,2327,2333,2334,2335,2336,2337,2338,2339,2340,2341,2342,2343,2344,2345,2346,2347,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2286,2344,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2286,2298,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2286,2305,2309,2310,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2286,2296,2298,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2286,2301,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2286,2324,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2286,2301,2328,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2289,2329,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[84,90,135,2283,2284,2285,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,651,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,609,633,650,1961,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,608,609,633,637,638,639,646,1961,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,603,609,633,637,1961,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,655,656,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,609,633,655,1961,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,647,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,608,609,633,639,645,646,1961,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1961,1962,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,603,608,609,633,639,646,1933,1961,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,167,185,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,642,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,640,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,640,641,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,605,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,178,2413,2417,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,167,178,2413,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2408,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,175,178,2410,2413,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,155,175,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,185,2408,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,155,178,2410,2413,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,147,167,178,2405,2406,2409,2412,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2413,2420,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2405,2411,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2413,2434,2435,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,170,178,185,2409,2413,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,185,2434,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,185,2407,2408,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2413,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2407,2408,2409,2410,2411,2412,2413,2414,2415,2417,2418,2419,2420,2421,2422,2423,2424,2425,2426,2427,2428,2429,2430,2431,2432,2433,2435,2436,2437,2438,2439,2440,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2413,2428,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2413,2420,2421,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2411,2413,2421,2422,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2412,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2405,2408,2413,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2413,2417,2421,2422,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2417,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,178,2411,2413,2416,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2405,2410,2413,2420,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,183,185,2408,2413,2434,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,97,100,103,104,135,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,100,135,167,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,100,104,135,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,94,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,98,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,96,97,100,135,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,94,135,185,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,96,100,135,155,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,91,92,93,95,99,135,147,167,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,100,108,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,92,98,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,100,124,125,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,92,95,100,135,170,178,185,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,100,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,96,100,135,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,91,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,94,95,96,98,99,100,101,102,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,125,126,127,128,129,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,100,117,120,135,143,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,100,108,109,110,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,98,100,109,111,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,99,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,92,94,100,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,100,104,109,111,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,104,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,98,100,103,135,178,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,92,96,100,108,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,100,117,135,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,94,100,124,135,170,183,185,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2463,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,603,607,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,599,603,604,606,608,639,646,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,600,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,601,602,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,599,601,603,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2282,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,2300,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1899,1903,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1899,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1900,1901,1902,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1437,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1428,1429,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1425,1426,1428,1430,1431,1436,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1426,1428,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1436,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1428,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1425,1426,1428,1431,1432,1433,1434,1435,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1425,1426,1427,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,262,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,253,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,253,256,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,248,251,253,254,255,256,257,258,259,260,261,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,187,189,256,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,253,254,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,188,253,255,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,189,191,193,194,195,196,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,191,193,195,196,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,191,193,195,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,188,191,193,194,196,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,187,189,190,191,192,193,194,195,196,197,198,248,249,250,251,252,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,187,189,190,193,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,189,190,193,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,193,196,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,187,188,190,191,192,194,195,196,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,187,188,189,193,253,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,193,194,195,196,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1111,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,195,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1591,1592,1594,1595,1596,1598,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1594,1595,1596,1597,1598,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,1591,1594,1595,1596,1598,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492],[90,135,593,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2454,2455,2461,2462,2464,2465,2466,2467,2468,2469,2473,2474,2475,2477,2478,2484,2485,2486,2487,2488,2489,2490,2491,2492]],"fileInfos":[{"version":"69684132aeb9b5642cbcd9e22dff7818ff0ee1aa831728af0ecf97d3364d5546","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","impliedFormat":1},{"version":"8fd575e12870e9944c7e1d62e1f5a73fcf23dd8d3a321f2a2c74c20d022283fe","impliedFormat":1},{"version":"8bf8b5e44e3c9c36f98e1007e8b7018c0f38d8adc07aecef42f5200114547c70","impliedFormat":1},{"version":"092c2bfe125ce69dbb1223c85d68d4d2397d7d8411867b5cc03cec902c233763","affectsGlobalScope":true,"impliedFormat":1},{"version":"07f073f19d67f74d732b1adea08e1dc66b1b58d77cb5b43931dee3d798a2fd53","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"936e80ad36a2ee83fc3caf008e7c4c5afe45b3cf3d5c24408f039c1d47bdc1df","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"fef8cfad2e2dc5f5b3d97a6f4f2e92848eb1b88e897bb7318cef0e2820bceaab","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"b5ce7a470bc3628408429040c4e3a53a27755022a32fd05e2cb694e7015386c7","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","affectsGlobalScope":true,"impliedFormat":1},{"version":"87dc0f382502f5bbce5129bdc0aea21e19a3abbc19259e0b43ae038a9fc4e326","affectsGlobalScope":true,"impliedFormat":1},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true,"impliedFormat":1},{"version":"56e4ed5aab5f5920980066a9409bfaf53e6d21d3f8d020c17e4de584d29600ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ece9f17b3866cc077099c73f4983bddbcb1dc7ddb943227f1ec070f529dedd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a6282c8827e4b9a95f4bf4f5c205673ada31b982f50572d27103df8ceb8013c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c9319a09485199c1f7b0498f2988d6d2249793ef67edda49d1e584746be9032","affectsGlobalScope":true,"impliedFormat":1},{"version":"e3a2a0cee0f03ffdde24d89660eba2685bfbdeae955a6c67e8c4c9fd28928eeb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811c71eee4aa0ac5f7adf713323a5c41b0cf6c4e17367a34fbce379e12bbf0a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"60037901da1a425516449b9a20073aa03386cce92f7a1fd902d7602be3a7c2e9","affectsGlobalScope":true,"impliedFormat":1},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true,"impliedFormat":1},{"version":"22adec94ef7047a6c9d1af3cb96be87a335908bf9ef386ae9fd50eeb37f44c47","affectsGlobalScope":true,"impliedFormat":1},{"version":"4245fee526a7d1754529d19227ecbf3be066ff79ebb6a380d78e41648f2f224d","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb5b19b86227ace1d29ea4cf81387279d04bb34051e944bc53df69f58914b788","affectsGlobalScope":true,"impliedFormat":1},{"version":"8a8eb4ebffd85e589a1cc7c178e291626c359543403d58c9cd22b81fab5b1fb9","impliedFormat":1},{"version":"87d9d29dbc745f182683f63187bf3d53fd8673e5fca38ad5eaab69798ed29fbc","impliedFormat":1},{"version":"472f5aab7edc498a0a761096e8e254c5bc3323d07a1e7f5f8b8ec0d6395b60a0","affectsGlobalScope":true,"impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"92d5369f7f9480aac66bbcd295c0b2d82077295c8bf0f0fe08fa3c3321225e49","impliedFormat":99},{"version":"878390f2f3d349610300c7603fb9dff16cfb92fcc6f5fc1f9b262e5bbd6479e5","impliedFormat":99},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"c0671b50bb99cc7ad46e9c68fa0e7f15ba4bc898b59c31a17ea4611fab5095da","affectsGlobalScope":true,"impliedFormat":1},{"version":"d802f0e6b5188646d307f070d83512e8eb94651858de8a82d1e47f60fb6da4e2","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"487b694c3de27ddf4ad107d4007ad304d29effccf9800c8ae23c2093638d906a","impliedFormat":1},{"version":"e525f9e67f5ddba7b5548430211cae2479070b70ef1fd93550c96c10529457bd","impliedFormat":1},{"version":"ccf4552357ce3c159ef75f0f0114e80401702228f1898bdc9402214c9499e8c0","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"4bc0794175abedf989547e628949888c1085b1efcd93fc482bccd77ee27f8b7c","impliedFormat":1},{"version":"3c8e93af4d6ce21eb4c8d005ad6dc02e7b5e6781f429d52a35290210f495a674","impliedFormat":1},{"version":"2c9875466123715464539bfd69bcaccb8ff6f3e217809428e0d7bd6323416d01","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"33e981bf6376e939f99bd7f89abec757c64897d33c005036b9a10d9587d80187","impliedFormat":1},{"version":"6c8e442ba33b07892169a14f7757321e49ab0f1032d676d321a1fdab8a67d40c","impliedFormat":1},{"version":"b41767d372275c154c7ea6c9d5449d9a741b8ce080f640155cc88ba1763e35b3","impliedFormat":1},{"version":"1cd673d367293fc5cb31cd7bf03d598eb368e4f31f39cf2b908abbaf120ab85a","impliedFormat":1},{"version":"af13e99445f37022c730bfcafcdc1761e9382ce1ea02afb678e3130b01ce5676","impliedFormat":1},{"version":"3825bf209f1662dfd039010a27747b73d0ef379f79970b1d05601ec8e8a4249f","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"da52342062e70c77213e45107921100ba9f9b3a30dd019444cf349e5fb3470c4","impliedFormat":1},{"version":"e9ace91946385d29192766bf783b8460c7dbcbfc63284aa3c9cae6de5155c8bc","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"1e30c045732e7db8f7a82cf90b516ebe693d2f499ce2250a977ec0d12e44a529","impliedFormat":1},{"version":"84b736594d8760f43400202859cda55607663090a43445a078963031d47e25e7","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"54c3e2371e3d016469ad959697fd257e5621e16296fa67082c2575d0bf8eced0","impliedFormat":1},{"version":"beb8233b2c220cfa0feea31fbe9218d89fa02faa81ef744be8dce5acb89bb1fd","impliedFormat":1},{"version":"78b29846349d4dfdd88bd6650cc5d2baaa67f2e89dc8a80c8e26ef7995386583","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"e38d4fdf79e1eadd92ed7844c331dbaa40f29f21541cfee4e1acff4db09cda33","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"7c10a32ae6f3962672e6869ee2c794e8055d8225ef35c91c0228e354b4e5d2d3","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"99f569b42ea7e7c5fe404b2848c0893f3e1a56e0547c1cd0f74d5dbb9a9de27e","impliedFormat":1},{"version":"f4b4faedc57701ae727d78ba4a83e466a6e3bdcbe40efbf913b17e860642897c","affectsGlobalScope":true,"impliedFormat":1},{"version":"bbcfd9cd76d92c3ee70475270156755346c9086391e1b9cb643d072e0cf576b8","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"72c1f5e0a28e473026074817561d1bc9647909cf253c8d56c41d1df8d95b85f7","impliedFormat":1},{"version":"003ec918ec442c3a4db2c36dc0c9c766977ea1c8bcc1ca7c2085868727c3d3f6","affectsGlobalScope":true,"impliedFormat":1},{"version":"938f94db8400d0b479626b9006245a833d50ce8337f391085fad4af540279567","impliedFormat":1},{"version":"c4e8e8031808b158cfb5ac5c4b38d4a26659aec4b57b6a7e2ba0a141439c208c","impliedFormat":1},{"version":"2c91d8366ff2506296191c26fd97cc1990bab3ee22576275d28b654a21261a44","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"db39d9a16e4ddcd8a8f2b7b3292b362cc5392f92ad7ccd76f00bccf6838ac7de","affectsGlobalScope":true,"impliedFormat":1},{"version":"289e9894a4668c61b5ffed09e196c1f0c2f87ca81efcaebdf6357cfb198dac14","impliedFormat":1},{"version":"25a1105595236f09f5bce42398be9f9ededc8d538c258579ab662d509aa3b98e","impliedFormat":1},{"version":"5078cd62dbdf91ae8b1dc90b1384dec71a9c0932d62bdafb1a811d2a8e26bef2","impliedFormat":1},{"version":"a2e2bbde231b65c53c764c12313897ffdfb6c49183dd31823ee2405f2f7b5378","impliedFormat":1},{"version":"ad1cc0ed328f3f708771272021be61ab146b32ecf2b78f3224959ff1e2cd2a5c","impliedFormat":1},{"version":"71450bbc2d82821d24ca05699a533e72758964e9852062c53b30f31c36978ab8","affectsGlobalScope":true,"impliedFormat":1},{"version":"62f572306e0b173cc5dfc4c583471151f16ef3779cf27ab96922c92ec82a3bc8","affectsGlobalScope":true,"impliedFormat":1},{"version":"067bdd82d9768baddbdc8df51d85f7b96387c47176bf7f895d2e21e2b6b2f1f4","impliedFormat":1},{"version":"42d30e7d04915facc3ded22b4127c9f517973b4c2b1326e56c10ff70daf6800a","impliedFormat":1},{"version":"bd8b644c5861b94926687618ec2c9e60ad054d334d6b7eb4517f23f53cb11f91","impliedFormat":1},{"version":"bcbabfaca3f6b8a76cb2739e57710daf70ab5c9479ab70f5351c9b4932abf6bd","impliedFormat":1},{"version":"77fced47f495f4ff29bb49c52c605c5e73cd9b47d50080133783032769a9d8a6","impliedFormat":1},{"version":"55f370475031b3d36af8dd47fb3934dff02e0f1330d13f1977c9e676af5c2e70","impliedFormat":1},{"version":"c54f0b30a787b3df16280f4675bd3d9d17bf983ae3cd40087409476bc50b922d","affectsGlobalScope":true,"impliedFormat":1},{"version":"0f5cda0282e1d18198e2887387eb2f026372ebc4e11c4e4516fef8a19ee4d514","impliedFormat":1},{"version":"e99b0e71f07128fc32583e88ccd509a1aaa9524c290efb2f48c22f9bf8ba83b1","impliedFormat":1},{"version":"76957a6d92b94b9e2852cf527fea32ad2dc0ef50f67fe2b14bd027c9ceef2d86","impliedFormat":1},{"version":"5e9f8c1e042b0f598a9be018fc8c3cb670fe579e9f2e18e3388b63327544fe16","affectsGlobalScope":true,"impliedFormat":1},{"version":"a8a99a5e6ed33c4a951b67cc1fd5b64fd6ad719f5747845c165ca12f6c21ba16","affectsGlobalScope":true,"impliedFormat":1},{"version":"a58a15da4c5ba3df60c910a043281256fa52d36a0fcdef9b9100c646282e88dd","impliedFormat":1},{"version":"b36beffbf8acdc3ebc58c8bb4b75574b31a2169869c70fc03f82895b93950a12","impliedFormat":1},{"version":"de263f0089aefbfd73c89562fb7254a7468b1f33b61839aafc3f035d60766cb4","impliedFormat":1},{"version":"70b57b5529051497e9f6482b76d91c0dcbb103d9ead8a0549f5bab8f65e5d031","impliedFormat":1},{"version":"8c81fd4a110490c43d7c578e8c6f69b3af01717189196899a6a44f93daa57a3a","impliedFormat":1},{"version":"1013eb2e2547ad8c100aca52ef9df8c3f209edee32bb387121bb3227f7c00088","impliedFormat":1},{"version":"e07c573ac1971ea89e2c56ff5fd096f6f7bba2e6dbcd5681d39257c8d954d4a8","impliedFormat":1},{"version":"363eedb495912790e867da6ff96e81bf792c8cfe386321e8163b71823a35719a","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"125d792ec6c0c0f657d758055c494301cc5fdb327d9d9d5960b3f129aff76093","impliedFormat":1},{"version":"dba28a419aec76ed864ef43e5f577a5c99a010c32e5949fe4e17a4d57c58dd11","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea713aa14a670b1ea0fbaaca4fd204e645f71ca7653a834a8ec07ee889c45de6","impliedFormat":1},{"version":"1e080418e53f9b7a05db81ab517c4e1d71b7194ee26ddd54016bcef3ac474bd4","impliedFormat":1},{"version":"9705cd157ffbb91c5cab48bdd2de5a437a372e63f870f8a8472e72ff634d47c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"ae86f30d5d10e4f75ce8dcb6e1bd3a12ecec3d071a21e8f462c5c85c678efb41","impliedFormat":1},{"version":"982efeb2573605d4e6d5df4dc7e40846bda8b9e678e058fc99522ab6165c479e","impliedFormat":1},{"version":"e03460fe72b259f6d25ad029f085e4bedc3f90477da4401d8fbc1efa9793230e","impliedFormat":1},{"version":"4286a3a6619514fca656089aee160bb6f2e77f4dd53dc5a96b26a0b4fc778055","impliedFormat":1},{"version":"3b63610eaabadf26aadf51a563e4b2a8bf56eeaab1094f2a2b21509008eaef0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"2d5d50cd0667d9710d4d2f6e077cc4e0f9dc75e106cccaea59999b36873c5a0d","affectsGlobalScope":true,"impliedFormat":1},{"version":"784490137935e1e38c49b9289110e74a1622baf8a8907888dcbe9e476d7c5e44","impliedFormat":1},{"version":"42180b657831d1b8fead051698618b31da623fb71ff37f002cb9d932cfa775f1","impliedFormat":1},{"version":"4f98d6fb4fe7cbeaa04635c6eaa119d966285d4d39f0eb55b2654187b0b27446","impliedFormat":1},{"version":"f8529fe0645fd9af7441191a4961497cc7638f75a777a56248eac6a079bb275d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4445f6ce6289c5b2220398138da23752fd84152c5c95bb8b58dedefc1758c036","impliedFormat":1},{"version":"a51f786b9f3c297668f8f322a6c58f85d84948ef69ade32069d5d63ec917221c","impliedFormat":1},{"version":"0c5f112b6d3377b9e8214d8920e1a69d8098b881d941f2ab3ca45234d13d68de","impliedFormat":1},{"version":"c1a2e05eb6d7ca8d7e4a7f4c93ccf0c2857e842a64c98eaee4d85841ee9855e6","impliedFormat":1},{"version":"835fb2909ce458740fb4a49fc61709896c6864f5ce3db7f0a88f06c720d74d02","impliedFormat":1},{"version":"6e5857f38aa297a859cab4ec891408659218a5a2610cd317b6dcbef9979459cc","impliedFormat":1},{"version":"ead8e39c2e11891f286b06ae2aa71f208b1802661fcdb2425cffa4f494a68854","impliedFormat":1},{"version":"82919acbb38870fcf5786ec1292f0f5afe490f9b3060123e48675831bd947192","impliedFormat":1},{"version":"e222701788ec77bd57c28facbbd142eadf5c749a74d586bc2f317db7e33544b1","impliedFormat":1},{"version":"09154713fae0ed7befacdad783e5bd1970c06fc41a5f866f7f933b96312ce764","impliedFormat":1},{"version":"8d67b13da77316a8a2fabc21d340866ddf8a4b99e76a6c951cc45189142df652","impliedFormat":1},{"version":"a91c8d28d10fee7fe717ddf3743f287b68770c813c98f796b6e38d5d164bd459","impliedFormat":1},{"version":"68add36d9632bc096d7245d24d6b0b8ad5f125183016102a3dad4c9c2438ccb0","impliedFormat":1},{"version":"3a819c2928ee06bbcc84e2797fd3558ae2ebb7e0ed8d87f71732fb2e2acc87b4","impliedFormat":1},{"version":"f6f827cd43e92685f194002d6b52a9408309cda1cec46fb7ca8489a95cbd2fd4","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"a270a1a893d1aee5a3c1c8c276cd2778aa970a2741ee2ccf29cc3210d7da80f5","impliedFormat":1},{"version":"add0ce7b77ba5b308492fa68f77f24d1ed1d9148534bdf05ac17c30763fc1a79","impliedFormat":1},{"version":"8926594ee895917e90701d8cbb5fdf77fc238b266ac540f929c7253f8ad6233d","impliedFormat":1},{"version":"2f67911e4bf4e0717dc2ded248ce2d5e4398d945ee13889a6852c1233ea41508","impliedFormat":1},{"version":"d8430c275b0f59417ea8e173cfb888a4477b430ec35b595bf734f3ec7a7d729f","impliedFormat":1},{"version":"69364df1c776372d7df1fb46a6cb3a6bf7f55e700f533a104e3f9d70a32bec18","impliedFormat":1},{"version":"6042774c61ece4ba77b3bf375f15942eb054675b7957882a00c22c0e4fe5865c","impliedFormat":1},{"version":"5a3bd57ed7a9d9afef74c75f77fce79ba3c786401af9810cdf45907c4e93f30e","impliedFormat":1},{"version":"ed8763205f02fb65e84eff7432155258df7f93b7d938f01785cb447d043d53f3","impliedFormat":1},{"version":"30db853bb2e60170ba11e39ab48bacecb32d06d4def89eedf17e58ebab762a65","impliedFormat":1},{"version":"e27451b24234dfed45f6cf22112a04955183a99c42a2691fb4936d63cfe42761","impliedFormat":1},{"version":"2316301dd223d31962d917999acf8e543e0119c5d24ec984c9f22cb23247160c","impliedFormat":1},{"version":"58d65a2803c3b6629b0e18c8bf1bc883a686fcf0333230dd0151ab6e85b74307","impliedFormat":1},{"version":"e818471014c77c103330aee11f00a7a00b37b35500b53ea6f337aefacd6174c9","impliedFormat":1},{"version":"d4a5b1d2ff02c37643e18db302488cd64c342b00e2786e65caac4e12bda9219b","impliedFormat":1},{"version":"29f823cbe0166e10e7176a94afe609a24b9e5af3858628c541ff8ce1727023cd","impliedFormat":1},"108d357ffaa6137eaa9a61814c6efb04d0e5e06bd5b8cd9dc92f94bdbe291df3",{"version":"151da46be65068c00e99f52ee3e47f2a9d7764357d9abc2d874fa6fc5aa6acb2","signature":"e3fd68af9446c93814ce312dbdd1c5bbad5341e324f7acbf2e9d08a8f46b30e1"},{"version":"acd8fd5090ac73902278889c38336ff3f48af6ba03aa665eb34a75e7ba1dccc4","impliedFormat":1},{"version":"d6258883868fb2680d2ca96bc8b1352cab69874581493e6d52680c5ffecdb6cc","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"f258e3960f324a956fc76a3d3d9e964fff2244ff5859dcc6ce5951e5413ca826","impliedFormat":1},{"version":"643f7232d07bf75e15bd8f658f664d6183a0efaca5eb84b48201c7671a266979","impliedFormat":1},{"version":"21da358700a3893281ce0c517a7a30cbd46be020d9f0c3f2834d0a8ad1f5fc75","impliedFormat":1},{"version":"3609e455ffcba8176c8ce0aa57f8258fe10cf03987e27f1fab68f702b4426521","impliedFormat":1},{"version":"d1bd4e51810d159899aad1660ccb859da54e27e08b8c9862b40cd36c1d9ff00f","impliedFormat":1},{"version":"17ed71200119e86ccef2d96b73b02ce8854b76ad6bd21b5021d4269bec527b5f","impliedFormat":1},{"version":"1cfa8647d7d71cb03847d616bd79320abfc01ddea082a49569fda71ac5ece66b","impliedFormat":1},{"version":"bb7a61dd55dc4b9422d13da3a6bb9cc5e89be888ef23bbcf6558aa9726b89a1c","impliedFormat":1},{"version":"413df52d4ea14472c2fa5bee62f7a40abd1eb49be0b9722ee01ee4e52e63beb2","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"446a50749b24d14deac6f8843e057a6355dd6437d1fac4f9e5ce4a5071f34bff","impliedFormat":1},{"version":"182e9fcbe08ac7c012e0a6e2b5798b4352470be29a64fdc114d23c2bab7d5106","impliedFormat":1},{"version":"5c9b31919ea1cb350a7ae5e71c9ced8f11723e4fa258a8cc8d16ae46edd623c7","impliedFormat":1},{"version":"4aa42ce8383b45823b3a1d3811c0fdd5f939f90254bc4874124393febbaf89f6","impliedFormat":1},{"version":"96ffa70b486207241c0fcedb5d9553684f7fa6746bc2b04c519e7ebf41a51205","impliedFormat":1},{"version":"5c24c66b3ba29ce9f2a79c719967e6e944131352a117a0bc43fa5b346b5562b3","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"ad0d1d75d129b1c80f911be438d6b61bfa8703930a8ff2be2f0e1f8a91841c64","impliedFormat":1},{"version":"ce75b1aebb33d510ff28af960a9221410a3eaf7f18fc5f21f9404075fba77256","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"02436d7e9ead85e09a2f8e27d5f47d9464bced31738dec138ca735390815c9f0","impliedFormat":1},{"version":"f4625edcb57b37b84506e8b276eb59ca30d31f88c6656d29d4e90e3bc58e69df","impliedFormat":1},{"version":"78a2869ad0cbf3f9045dda08c0d4562b7e1b2bfe07b19e0db072f5c3c56e9584","impliedFormat":1},{"version":"f8d5ff8eafd37499f2b6a98659dd9b45a321de186b8db6b6142faed0fea3de77","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"c685d9f68c70fe11ce527287526585a06ea13920bb6c18482ca84945a4e433a7","impliedFormat":1},{"version":"540cc83ab772a2c6bc509fe1354f314825b5dba3669efdfbe4693ecd3048e34f","impliedFormat":1},{"version":"121b0696021ab885c570bbeb331be8ad82c6efe2f3b93a6e63874901bebc13e3","impliedFormat":1},{"version":"4e01846df98d478a2a626ec3641524964b38acaac13945c2db198bf9f3df22ee","impliedFormat":1},{"version":"678d6d4c43e5728bf66e92fc2269da9fa709cb60510fed988a27161473c3853f","impliedFormat":1},{"version":"ffa495b17a5ef1d0399586b590bd281056cee6ce3583e34f39926f8dcc6ecdb5","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"aa14cee20aa0db79f8df101fc027d929aec10feb5b8a8da3b9af3895d05b7ba2","impliedFormat":1},{"version":"493c700ac3bd317177b2eb913805c87fe60d4e8af4fb39c41f04ba81fae7e170","impliedFormat":1},{"version":"aeb554d876c6b8c818da2e118d8b11e1e559adbe6bf606cc9a611c1b6c09f670","impliedFormat":1},{"version":"acf5a2ac47b59ca07afa9abbd2b31d001bf7448b041927befae2ea5b1951d9f9","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"d71291eff1e19d8762a908ba947e891af44749f3a2cbc5bd2ec4b72f72ea795f","impliedFormat":1},{"version":"c0480e03db4b816dff2682b347c95f2177699525c54e7e6f6aa8ded890b76be7","impliedFormat":1},{"version":"e2a37ac938c4bede5bb284b9d2d042da299528f1e61f6f57538f1bd37d760869","impliedFormat":1},{"version":"76def37aff8e3a051cf406e10340ffba0f28b6991c5d987474cc11137796e1eb","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"3e7efde639c6a6c3edb9847b3f61e308bf7a69685b92f665048c45132f51c218","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"ee8df1cb8d0faaca4013a1b442e99130769ce06f438d18d510fed95890067563","impliedFormat":1},{"version":"bfb7f8475428637bee12bdd31bd9968c1c8a1cc2c3e426c959e2f3a307f8936f","impliedFormat":1},{"version":"6f491d0108927478d3247bbbc489c78c2da7ef552fd5277f1ab6819986fdf0b1","impliedFormat":1},{"version":"0d8f2b8781c721170b87a6b662b3cb038fd1a721165ecca390352c818d425872","impliedFormat":1},{"version":"7cb0ee103671d1e201cd53dda12bc1cd0a35f1c63d6102720c6eeb322cb8e17e","impliedFormat":1},{"version":"15a234e5031b19c48a69ccc1607522d6e4b50f57d308ecb7fe863d44cd9f9eb3","impliedFormat":1},{"version":"148679c6d0f449210a96e7d2e562d589e56fcde87f843a92808b3ff103f1a774","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"2f9c89cbb29d362290531b48880a4024f258c6033aaeb7e59fbc62db26819650","impliedFormat":1},{"version":"bb37588926aba35c9283fe8d46ebf4e79ffe976343105f5c6d45f282793352b2","impliedFormat":1},{"version":"05c97cddbaf99978f83d96de2d8af86aded9332592f08ce4a284d72d0952c391","impliedFormat":1},{"version":"72179f9dd22a86deaad4cc3490eb0fe69ee084d503b686985965654013f1391b","impliedFormat":1},{"version":"2e6114a7dd6feeef85b2c80120fdbfb59a5529c0dcc5bfa8447b6996c97a69f5","impliedFormat":1},{"version":"7b6ff760c8a240b40dab6e4419b989f06a5b782f4710d2967e67c695ef3e93c4","impliedFormat":1},{"version":"c8f004e6036aa1c764ad4ec543cf89a5c1893a9535c80ef3f2b653e370de45e6","impliedFormat":1},{"version":"dd80b1e600d00f5c6a6ba23f455b84a7db121219e68f89f10552c54ba46e4dc9","impliedFormat":1},{"version":"b064c36f35de7387d71c599bfcf28875849a1dbc733e82bd26cae3d1cd060521","impliedFormat":1},{"version":"05c7280d72f3ed26f346cbe7cbbbb002fb7f15739197cbbee6ab3fd1a6cb9347","impliedFormat":1},{"version":"8de9fe97fa9e00ec00666fa77ab6e91b35d25af8ca75dabcb01e14ad3299b150","impliedFormat":1},{"version":"803cd2aaf1921c218916c2c7ee3fce653e852d767177eb51047ff15b5b253893","impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"7ab12b2f1249187223d11a589f5789c75177a0b597b9eb7f8e2e42d045393347","impliedFormat":1},{"version":"ad37fb4be61c1035b68f532b7220f4e8236cf245381ce3b90ac15449ecfe7305","impliedFormat":1},{"version":"93436bd74c66baba229bfefe1314d122c01f0d4c1d9e35081a0c4f0470ac1a6c","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"50256e9c31318487f3752b7ac12ff365c8949953e04568009c8705db802776fb","impliedFormat":1},{"version":"7d73b24e7bf31dfb8a931ca6c4245f6bb0814dfae17e4b60c9e194a631fe5f7b","impliedFormat":1},{"version":"d130c5f73768de51402351d5dc7d1b36eaec980ca697846e53156e4ea9911476","impliedFormat":1},{"version":"413586add0cfe7369b64979d4ec2ed56c3f771c0667fbde1bf1f10063ede0b08","impliedFormat":1},{"version":"06472528e998d152375ad3bd8ebcb69ff4694fd8d2effaf60a9d9f25a37a097a","impliedFormat":1},{"version":"50b5bc34ce6b12eccb76214b51aadfa56572aa6cc79c2b9455cdbb3d6c76af1d","impliedFormat":1},{"version":"b7e16ef7f646a50991119b205794ebfd3a4d8f8e0f314981ebbe991639023d0e","impliedFormat":1},{"version":"a401617604fa1f6ce437b81689563dfdc377069e4c58465dbd8d16069aede0a5","impliedFormat":1},{"version":"6e9082e91370de5040e415cd9f24e595b490382e8c7402c4e938a8ce4bccc99f","impliedFormat":1},{"version":"8695dec09ad439b0ceef3776ea68a232e381135b516878f0901ed2ea114fd0fe","impliedFormat":1},{"version":"304b44b1e97dd4c94697c3313df89a578dca4930a104454c99863f1784a54357","impliedFormat":1},{"version":"d682336018141807fb602709e2d95a192828fcb8d5ba06dda3833a8ea98f69e3","impliedFormat":1},{"version":"6124e973eab8c52cabf3c07575204efc1784aca6b0a30c79eb85fe240a857efa","impliedFormat":1},{"version":"0d891735a21edc75df51f3eb995e18149e119d1ce22fd40db2b260c5960b914e","impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","impliedFormat":1},{"version":"4fbd3116e00ed3a6410499924b6403cc9367fdca303e34838129b328058ede40","impliedFormat":1},{"version":"b01bd582a6e41457bc56e6f0f9de4cb17f33f5f3843a7cf8210ac9c18472fb0f","impliedFormat":1},{"version":"0a437ae178f999b46b6153d79095b60c42c996bc0458c04955f1c996dc68b971","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"4a7baeb6325920044f66c0f8e5e6f1f52e06e6d87588d837bdf44feb6f35c664","impliedFormat":1},{"version":"12d218a49dbe5655b911e6cc3c13b2c655e4c783471c3b0432137769c79e1b3c","impliedFormat":1},{"version":"7274fbffbd7c9589d8d0ffba68157237afd5cecff1e99881ea3399127e60572f","impliedFormat":1},{"version":"6b0fc04121360f752d196ba35b6567192f422d04a97b2840d7d85f8b79921c92","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"a365c4d3bed3be4e4e20793c999c51f5cd7e6792322f14650949d827fbcd170f","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","impliedFormat":1},{"version":"f374cb24e93e7798c4d9e83ff872fa52d2cdb36306392b840a6ddf46cb925cb6","impliedFormat":1},{"version":"42b81043b00ff27c6bd955aea0f6e741545f2265978bf364b614702b72a027ab","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"97e5ccc7bb88419005cbdf812243a5b3186cdef81b608540acabe1be163fc3e4","affectsGlobalScope":true,"impliedFormat":1},{"version":"3fbdd025f9d4d820414417eeb4107ffa0078d454a033b506e22d3a23bc3d9c41","affectsGlobalScope":true,"impliedFormat":1},{"version":"a8f8e6ab2fa07b45251f403548b78eaf2022f3c2254df3dc186cb2671fe4996d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"9f9bb6755a8ce32d656ffa4763a8144aa4f274d6b69b59d7c32811031467216e","impliedFormat":1},{"version":"5c32bdfbd2d65e8fffbb9fbda04d7165e9181b08dad61154961852366deb7540","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"6b3453eebd474cc8acf6d759f1668e6ce7425a565e2996a20b644c72916ecf75","impliedFormat":1},{"version":"0c05e9842ec4f8b7bfebfd3ca61604bb8c914ba8da9b5337c4f25da427a005f2","impliedFormat":1},{"version":"89cd3444e389e42c56fd0d072afef31387e7f4107651afd2c03950f22dc36f77","impliedFormat":1},{"version":"7f2aa4d4989a82530aaac3f72b3dceca90e9c25bee0b1a327e8a08a1262435ad","impliedFormat":1},{"version":"e39a304f882598138a8022106cb8de332abbbb87f3fee71c5ca6b525c11c51fc","impliedFormat":1},{"version":"faed7a5153215dbd6ebe76dfdcc0af0cfe760f7362bed43284be544308b114cf","impliedFormat":1},{"version":"fcdf3e40e4a01b9a4b70931b8b51476b210c511924fcfe3f0dae19c4d52f1a54","impliedFormat":1},{"version":"345c4327b637d34a15aba4b7091eb068d6ab40a3dedaab9f00986253c9704e53","impliedFormat":1},{"version":"3a788c7fb7b1b1153d69a4d1d9e1d0dfbcf1127e703bdb02b6d12698e683d1fb","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"d38530db0601215d6d767f280e3a3c54b2a83b709e8d9001acb6f61c67e965fc","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"4805f6161c2c8cefb8d3b8bd96a080c0fe8dbc9315f6ad2e53238f9a79e528a6","impliedFormat":1},{"version":"b83cb14474fa60c5f3ec660146b97d122f0735627f80d82dd03e8caa39b4388c","impliedFormat":1},{"version":"2b5b70d7782fe028487a80a1c214e67bd610532b9f978b78fa60f5b4a359f77e","impliedFormat":1},{"version":"7ee86fbb3754388e004de0ef9e6505485ddfb3be7640783d6d015711c03d302d","impliedFormat":1},{"version":"1a82deef4c1d39f6882f28d275cad4c01f907b9b39be9cbc472fcf2cf051e05b","impliedFormat":1},{"version":"162e071992b34bc36ca257d629547f93cb43728d6fe073ad18a237e4f7c52d7d","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"20865ac316b8893c1a0cc383ccfc1801443fbcc2a7255be166cf90d03fac88c9","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"461d0ad8ae5f2ff981778af912ba71b37a8426a33301daa00f21c6ccb27f8156","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"fcafff163ca5e66d3b87126e756e1b6dfa8c526aa9cd2a2b0a9da837d81bbd72","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"8b4327413e5af38cd8cb97c59f48c3c866015d5d642f28518e3a891c469f240e","impliedFormat":1},{"version":"7e6ac205dcb9714f708354fd863bffa45cee90740706cc64b3b39b23ebb84744","impliedFormat":1},{"version":"61dc6e3ac78d64aa864eedd0a208b97b5887cc99c5ba65c03287bf57d83b1eb9","impliedFormat":1},{"version":"4b20fcf10a5413680e39f5666464859fc56b1003e7dfe2405ced82371ebd49b6","impliedFormat":1},{"version":"c06ef3b2569b1c1ad99fcd7fe5fba8d466e2619da5375dfa940a94e0feea899b","impliedFormat":1},{"version":"f7d628893c9fa52ba3ab01bcb5e79191636c4331ee5667ecc6373cbccff8ae12","impliedFormat":1},{"version":"1d879125d1ec570bf04bc1f362fdbe0cb538315c7ac4bcfcdf0c1e9670846aa6","impliedFormat":1},{"version":"8baa8dbdc393e3c6b26e8e31384b938756ce2effdc126648d43e58291ce9869b","impliedFormat":1},{"version":"933aee906d42ea2c53b6892192a8127745f2ec81a90695df4024308ba35a8ff4","impliedFormat":1},{"version":"d663134457d8d669ae0df34eabd57028bddc04fc444c4bc04bc5215afc91e1f4","impliedFormat":1},{"version":"985153f0deb9b4391110331a2f0c114019dbea90cba5ca68a4107700796e0d75","impliedFormat":1},{"version":"a3e3f0efcae272ab8ee3298e4e819f7d9dd9ff411101f45444877e77cfeca9a4","impliedFormat":1},{"version":"43e96a3d5d1411ab40ba2f61d6a3192e58177bcf3b133a80ad2a16591611726d","impliedFormat":1},{"version":"58659b06d33fa430bee1105b75cf876c0a35b2567207487c8578aec51ca2d977","impliedFormat":1},{"version":"71d9eb4c4e99456b78ae182fb20a5dfc20eb1667f091dbb9335b3c017dd1c783","impliedFormat":1},{"version":"cfa846a7b7847a1d973605fbb8c91f47f3a0f0643c18ac05c47077ebc72e71c7","impliedFormat":1},{"version":"30e6520444df1a004f46fdc8096f3fe06f7bbd93d09c53ada9dcdde59919ccca","impliedFormat":1},{"version":"6c800b281b9e89e69165fd11536195488de3ff53004e55905e6c0059a2d8591e","impliedFormat":1},{"version":"7d4254b4c6c67a29d5e7f65e67d72540480ac2cfb041ca484847f5ae70480b62","impliedFormat":1},{"version":"a58beefce74db00dbb60eb5a4bb0c6726fb94c7797c721f629142c0ae9c94306","impliedFormat":1},{"version":"41eeb453ccb75c5b2c3abef97adbbd741bd7e9112a2510e12f03f646dc9ad13d","impliedFormat":1},{"version":"502fa5863df08b806dbf33c54bee8c19f7e2ad466785c0fc35465d7c5ff80995","impliedFormat":1},{"version":"c91a2d08601a1547ffef326201be26db94356f38693bb18db622ae5e9b3d7c92","impliedFormat":1},{"version":"888cda0fa66d7f74e985a3f7b1af1f64b8ff03eb3d5e80d051c3cbdeb7f32ab7","impliedFormat":1},{"version":"60681e13f3545be5e9477acb752b741eae6eaf4cc01658a25ec05bff8b82a2ef","impliedFormat":1},{"version":"8b4b8ebc2d99ae651c5c4169ee8b24e2b0e02a3dfaef84e357d677b663c18fdf","impliedFormat":1},{"version":"a57b1802794433adec9ff3fed12aa79d671faed86c49b09e02e1ac41b4f1d33a","impliedFormat":1},{"version":"ad10d4f0517599cdeca7755b930f148804e3e0e5b5a3847adce0f1f71bbccd74","impliedFormat":1},{"version":"1042064ece5bb47d6aba91648fbe0635c17c600ebdf567588b4ca715602f0a9d","impliedFormat":1},{"version":"c49469a5349b3cc1965710b5b0f98ed6c028686aa8450bcb3796728873eb923e","impliedFormat":1},{"version":"4a889f2c763edb4d55cb624257272ac10d04a1cad2ed2948b10ed4a7fda2a428","impliedFormat":1},{"version":"7bb79aa2fead87d9d56294ef71e056487e848d7b550c9a367523ee5416c44cfa","impliedFormat":1},{"version":"d88ea80a6447d7391f52352ec97e56b52ebec934a4a4af6e2464cfd8b39c3ba8","impliedFormat":1},{"version":"55095860901097726220b6923e35a812afdd49242a1246d7b0942ee7eb34c6e4","impliedFormat":1},{"version":"96171c03c2e7f314d66d38acd581f9667439845865b7f85da8df598ff9617476","impliedFormat":1},{"version":"27ff4196654e6373c9af16b6165120e2dd2169f9ad6abb5c935af5abd8c7938c","impliedFormat":1},{"version":"bb8f2dbc03533abca2066ce4655c119bff353dd4514375beb93c08590c03e023","impliedFormat":1},{"version":"d193c8a86144b3a87b22bc1f5534b9c3e0f5a187873ec337c289a183973a58fe","impliedFormat":1},{"version":"1a6e6ba8a07b74e3ad237717c0299d453f9ceb795dbc2f697d1f2dd07cb782d2","impliedFormat":1},{"version":"58d70c38037fc0f949243388ff7ae20cf43321107152f14a9d36ca79311e0ada","impliedFormat":1},{"version":"f56bdc6884648806d34bc66d31cdb787c4718d04105ce2cd88535db214631f82","impliedFormat":1},{"version":"190da5eac6478d61ab9731ab2146fbc0164af2117a363013249b7e7992f1cccb","impliedFormat":1},{"version":"01479d9d5a5dda16d529b91811375187f61a06e74be294a35ecce77e0b9e8d6c","impliedFormat":1},{"version":"49f95e989b4632c6c2a578cc0078ee19a5831832d79cc59abecf5160ea71abad","impliedFormat":1},{"version":"9666533332f26e8995e4d6fe472bdeec9f15d405693723e6497bf94120c566c8","impliedFormat":1},{"version":"ce0df82a9ae6f914ba08409d4d883983cc08e6d59eb2df02d8e4d68309e7848b","impliedFormat":1},{"version":"796273b2edc72e78a04e86d7c58ae94d370ab93a0ddf40b1aa85a37a1c29ecd7","impliedFormat":1},{"version":"5df15a69187d737d6d8d066e189ae4f97e41f4d53712a46b2710ff9f8563ec9f","impliedFormat":1},{"version":"1a4dc28334a926d90ba6a2d811ba0ff6c22775fcc13679521f034c124269fd40","impliedFormat":1},{"version":"f05315ff85714f0b87cc0b54bcd3dde2716e5a6b99aedcc19cad02bf2403e08c","impliedFormat":1},{"version":"8a8c64dafaba11c806efa56f5c69f611276471bef80a1db1f71316ec4168acef","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"5fad3b31fc17a5bc58095118a8b160f5260964787c52e7eb51e3d4fcf5d4a6f0","impliedFormat":1},{"version":"72105519d0390262cf0abe84cf41c926ade0ff475d35eb21307b2f94de985778","impliedFormat":1},{"version":"d0a4cac61fa080f2be5ebb68b82726be835689b35994ba0e22e3ed4d2bc45e3b","impliedFormat":1},{"version":"c857e0aae3f5f444abd791ec81206020fbcc1223e187316677e026d1c1d6fe08","impliedFormat":1},{"version":"ccf6dd45b708fb74ba9ed0f2478d4eb9195c9dfef0ff83a6092fa3cf2ff53b4f","impliedFormat":1},{"version":"2d7db1d73456e8c5075387d4240c29a2a900847f9c1bff106a2e490da8fbd457","impliedFormat":1},{"version":"2b15c805f48e4e970f8ec0b1915f22d13ca6212375e8987663e2ef5f0205e832","impliedFormat":1},{"version":"205a31b31beb7be73b8df18fcc43109cbc31f398950190a0967afc7a12cb478c","impliedFormat":1},{"version":"8fca3039857709484e5893c05c1f9126ab7451fa6c29e19bb8c2411a2e937345","impliedFormat":1},{"version":"35069c2c417bd7443ae7c7cafd1de02f665bf015479fec998985ffbbf500628c","impliedFormat":1},{"version":"dba6c7006e14a98ec82999c6f89fbbbfd1c642f41db148535f3b77b8018829b8","impliedFormat":1},{"version":"7f897b285f22a57a5c4dc14a27da2747c01084a542b4d90d33897216dceeea2e","impliedFormat":1},{"version":"7e0b7f91c5ab6e33f511efc640d36e6f933510b11be24f98836a20a2dc914c2d","impliedFormat":1},{"version":"045b752f44bf9bbdcaffd882424ab0e15cb8d11fa94e1448942e338c8ef19fba","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"d96b39301d0ded3f1a27b47759676a33a02f6f5049bfcbde81e533fd10f50dcb","impliedFormat":1},{"version":"2ded4f930d6abfaa0625cf55e58f565b7cbd4ab5b574dd2cb19f0a83a2f0be8b","impliedFormat":1},{"version":"0aedb02516baf3e66b2c1db9fef50666d6ed257edac0f866ea32f1aa05aa474f","impliedFormat":1},{"version":"ca0f4d9068d652bad47e326cf6ba424ac71ab866e44b24ddb6c2bd82d129586a","affectsGlobalScope":true,"impliedFormat":1},{"version":"04d36005fcbeac741ac50c421181f4e0316d57d148d37cc321a8ea285472462b","impliedFormat":1},{"version":"9e2739b32f741859263fdba0244c194ca8e96da49b430377930b8f721d77c000","impliedFormat":1},{"version":"56ccb49443bfb72e5952f7012f0de1a8679f9f75fc93a5c1ac0bafb28725fc5f","impliedFormat":1},{"version":"d90b9f1520366d713a73bd30c5a9eb0040d0fb6076aff370796bc776fd705943","impliedFormat":1},{"version":"05321b823dd3781d0b6aac8700bfdc0c9181d56479fe52ba6a40c9196fd661a8","impliedFormat":1},{"version":"736a8712572e21ee73337055ce15edb08142fc0f59cd5410af4466d04beff0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"bef86adb77316505c6b471da1d9b8c9e428867c2566270e8894d4d773a1c4dc2","impliedFormat":1},{"version":"a46dba563f70f32f9e45ae015f3de979225f668075d7a427f874e0f6db584991","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"2652448ac55a2010a1f71dd141f828b682298d39728f9871e1cdf8696ef443fd","impliedFormat":1},{"version":"02c4fc9e6bb27545fa021f6056e88ff5fdf10d9d9f1467f1d10536c6e749ac50","impliedFormat":1},{"version":"120599fd965257b1f4d0ff794bc696162832d9d8467224f4665f713a3119078b","impliedFormat":1},{"version":"5433f33b0a20300cca35d2f229a7fc20b0e8477c44be2affeb21cb464af60c76","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"bd4131091b773973ca5d2326c60b789ab1f5e02d8843b3587effe6e1ea7c9d86","impliedFormat":1},{"version":"c7f6485931085bf010fbaf46880a9b9ec1a285ad9dc8c695a9e936f5a48f34b4","impliedFormat":1},{"version":"14f6b927888a1112d662877a5966b05ac1bf7ed25d6c84386db4c23c95a5363b","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"622694a8522b46f6310c2a9b5d2530dde1e2854cb5829354e6d1ff8f371cf469","impliedFormat":1},{"version":"d24ff95760ea2dfcc7c57d0e269356984e7046b7e0b745c80fea71559f15bdd8","impliedFormat":1},{"version":"a9e6c0ff3f8186fccd05752cf75fc94e147c02645087ac6de5cc16403323d870","impliedFormat":1},{"version":"49c346823ba6d4b12278c12c977fb3a31c06b9ca719015978cb145eb86da1c61","impliedFormat":1},{"version":"bfac6e50eaa7e73bb66b7e052c38fdc8ccfc8dbde2777648642af33cf349f7f1","impliedFormat":1},{"version":"92f7c1a4da7fbfd67a2228d1687d5c2e1faa0ba865a94d3550a3941d7527a45d","impliedFormat":1},{"version":"f53b120213a9289d9a26f5af90c4c686dd71d91487a0aa5451a38366c70dc64b","impliedFormat":1},{"version":"83fe880c090afe485a5c02262c0b7cdd76a299a50c48d9bde02be8e908fb4ae6","impliedFormat":1},{"version":"13c1b657932e827a7ed510395d94fc8b743b9d053ab95b7cd829b2bc46fb06db","impliedFormat":1},{"version":"57d67b72e06059adc5e9454de26bbfe567d412b962a501d263c75c2db430f40e","impliedFormat":1},{"version":"6511e4503cf74c469c60aafd6589e4d14d5eb0a25f9bf043dcbecdf65f261972","impliedFormat":1},{"version":"078131f3a722a8ad3fc0b724cd3497176513cdcb41c80f96a3acbda2a143b58e","impliedFormat":1},{"version":"8c70ddc0c22d85e56011d49fddfaae3405eb53d47b59327b9dd589e82df672e7","impliedFormat":1},{"version":"a67b87d0281c97dfc1197ef28dfe397fc2c865ccd41f7e32b53f647184cc7307","impliedFormat":1},{"version":"771ffb773f1ddd562492a6b9aaca648192ac3f056f0e1d997678ff97dbb6bf9b","impliedFormat":1},{"version":"232f70c0cf2b432f3a6e56a8dc3417103eb162292a9fd376d51a3a9ea5fbbf6f","impliedFormat":1},{"version":"9e155d2255348d950b1f65643fb26c0f14f5109daf8bd9ee24a866ad0a743648","affectsGlobalScope":true,"impliedFormat":1},{"version":"0b103e9abfe82d14c0ad06a55d9f91d6747154ef7cacc73cf27ecad2bfb3afcf","impliedFormat":1},{"version":"7a883e9c84e720810f86ef4388f54938a65caa0f4d181a64e9255e847a7c9f51","impliedFormat":1},{"version":"a0ba218ac1baa3da0d5d9c1ec1a7c2f8676c284e6f5b920d6d049b13fa267377","impliedFormat":1},{"version":"8a0e762ceb20c7e72504feef83d709468a70af4abccb304f32d6b9bac1129b2c","impliedFormat":1},{"version":"d408d6f32de8d1aba2ff4a20f1aa6a6edd7d92c997f63b90f8ad3f9017cf5e46","impliedFormat":1},{"version":"9252d498a77517aab5d8d4b5eb9d71e4b225bbc7123df9713e08181de63180f6","impliedFormat":1},{"version":"221e915caef37c5cbaabd4946418f97dcc20591469e260732b31008321024dd8","impliedFormat":1},{"version":"35e6379c3f7cb27b111ad4c1aa69538fd8e788ab737b8ff7596a1b40e96f4f90","impliedFormat":1},{"version":"1fffe726740f9787f15b532e1dc870af3cd964dbe29e191e76121aa3dd8693f2","impliedFormat":1},{"version":"371bf6127c1d427836de95197155132501cb6b69ef8709176ce6e0b85d059264","impliedFormat":1},{"version":"2bafd700e617d3693d568e972d02b92224b514781f542f70d497a8fdf92d52a2","affectsGlobalScope":true,"impliedFormat":1},{"version":"5542d8a7ea13168cb573be0d1ba0d29460d59430fb12bb7bf4674efd5604e14c","impliedFormat":1},{"version":"af48e58339188d5737b608d41411a9c054685413d8ae88b8c1d0d9bfabdf6e7e","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","impliedFormat":1},{"version":"1de8c302fd35220d8f29dea378a4ae45199dc8ff83ca9923aca1400f2b28848a","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"98a787be42bd92f8c2a37d7df5f13e5992da0d967fab794adbb7ee18370f9849","impliedFormat":1},{"version":"332248ee37cca52903572e66c11bef755ccc6e235835e63d3c3e60ddda3e9b93","impliedFormat":1},{"version":"94e8cc88ae2ef3d920bb3bdc369f48436db123aa2dc07f683309ad8c9968a1e1","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"320f4091e33548b554d2214ce5fc31c96631b513dffa806e2e3a60766c8c49d9","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"d90d5f524de38889d1e1dbc2aeef00060d779f8688c02766ddb9ca195e4a713d","impliedFormat":1},{"version":"a3f41ed1b4f2fc3049394b945a68ae4fdefd49fa1739c32f149d32c0545d67f5","impliedFormat":1},{"version":"b0309e1eda99a9e76f87c18992d9c3689b0938266242835dd4611f2b69efe456","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"6ceb10ca57943be87ff9debe978f4ab73593c0c85ee802c051a93fc96aaf7a20","impliedFormat":1},{"version":"1de3ffe0cc28a9fe2ac761ece075826836b5a02f340b412510a59ba1d41a505a","impliedFormat":1},{"version":"e46d6cc08d243d8d0d83986f609d830991f00450fb234f5b2f861648c42dc0d8","impliedFormat":1},{"version":"1c0a98de1323051010ce5b958ad47bc1c007f7921973123c999300e2b7b0ecc0","impliedFormat":1},{"version":"ff863d17c6c659440f7c5c536e4db7762d8c2565547b2608f36b798a743606ca","impliedFormat":1},{"version":"5412ad0043cd60d1f1406fc12cb4fb987e9a734decbdd4db6f6acf71791e36fe","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"b6c1f64158da02580f55e8a2728eda6805f79419aed46a930f43e68ad66a38fc","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"bc9ee0192f056b3d5527bcd78dc3f9e527a9ba2bdc0a2c296fbc9027147df4b2","impliedFormat":1},{"version":"330896c1a2b9693edd617be24fbf9e5895d6e18c7955d6c08f028f272b37314d","impliedFormat":1},{"version":"1d9c0a9a6df4e8f29dc84c25c5aa0bb1da5456ebede7a03e03df08bb8b27bae6","impliedFormat":1},{"version":"84380af21da938a567c65ef95aefb5354f676368ee1a1cbb4cae81604a4c7d17","impliedFormat":1},{"version":"1af3e1f2a5d1332e136f8b0b95c0e6c0a02aaabd5092b36b64f3042a03debf28","impliedFormat":1},{"version":"30d8da250766efa99490fc02801047c2c6d72dd0da1bba6581c7e80d1d8842a4","impliedFormat":1},{"version":"03566202f5553bd2d9de22dfab0c61aa163cabb64f0223c08431fb3fc8f70280","impliedFormat":1},{"version":"4c0a1233155afb94bd4d7518c75c84f98567cd5f13fc215d258de196cdb40d91","impliedFormat":1},{"version":"e7765aa8bcb74a38b3230d212b4547686eb9796621ffb4367a104451c3f9614f","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"5bf5c7a44e779790d1eb54c234b668b15e34affa95e78eada73e5757f61ed76a","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"5c634644d45a1b6bc7b05e71e05e52ec04f3d73d9ac85d5927f647a5f965181a","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"a68d4b3182e8d776cdede7ac9630c209a7bfbb59191f99a52479151816ef9f9e","impliedFormat":99},{"version":"39644b343e4e3d748344af8182111e3bbc594930fff0170256567e13bbdbebb0","impliedFormat":99},{"version":"ed7fd5160b47b0de3b1571c5c5578e8e7e3314e33ae0b8ea85a895774ee64749","impliedFormat":99},{"version":"63a7595a5015e65262557f883463f934904959da563b4f788306f699411e9bac","impliedFormat":1},{"version":"4ba137d6553965703b6b55fd2000b4e07ba365f8caeb0359162ad7247f9707a6","impliedFormat":1},{"version":"6de125ea94866c736c6d58d68eb15272cf7d1020a5b459fea1c660027eca9a90","affectsGlobalScope":true,"impliedFormat":1},{"version":"8fac4a15690b27612d8474fb2fc7cc00388df52d169791b78d1a3645d60b4c8b","affectsGlobalScope":true,"impliedFormat":1},{"version":"064ac1c2ac4b2867c2ceaa74bbdce0cb6a4c16e7c31a6497097159c18f74aa7c","impliedFormat":1},{"version":"3dc14e1ab45e497e5d5e4295271d54ff689aeae00b4277979fdd10fa563540ae","impliedFormat":1},{"version":"d3b315763d91265d6b0e7e7fa93cfdb8a80ce7cdd2d9f55ba0f37a22db00bdb8","impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","impliedFormat":1},"7ad303e40d4fddf44f156129e397511953a71481c5cfd86b1862649aaaf240cc",{"version":"93cddca2ec6a5a8a1b942af0459cbd78d1b1b48e576cdabecc82811843a2cc28","impliedFormat":1},{"version":"8521ef2eafecd8a02a4aa7fb6e2d977537bcad7247fc4568553abfad801c26be","signature":"9c0eeb8de8dfdfb63ebcef09564c84ae1f6b6187c8e8e274fc5b0ee0a2c8432b"},{"version":"606779a35d17b249511fe72a5c66503ada20e0772cd31f165c35090ac06fde20","signature":"6bc2680fe9c4b4b45eba0548ae7aecb482332ce1a56d8bb250d324b82e31636a"},{"version":"170d4db14678c68178ee8a3d5a990d5afb759ecb6ec44dbd885c50f6da6204f6","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"5e76305d58bcdc924ff2bf14f6a9dc2aa5441ed06464b7e7bd039e611d66a89b","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},"32d33bdc36258d570964d03f51e4f47bd4dc829c238ec3304f1ae32a8fa54ad7",{"version":"62c99d5d7de97c4c8441cc86b8efb1bc5b3284faf70f96ea7fe704cc451a1c83","signature":"62bf6303f60d24103151b02e6b065fe77a22b261a6ed3cb62753327f8f150018"},{"version":"f6d772dde6a58aeaae92ee2b65e02caa315f621d79cac38e5df9b2e8f5249dbc","signature":"87b79fd569bcdf71c08547d45eceadfd769d52e070d3d55e6252790c232d47a6"},{"version":"0759a2bffadfe8f19d9f7693d6703f0dcb5f46aa3fadccac39d41f5052e85e8c","signature":"5f5303388c23daf221df682ba0e6ed13924ad3a925fae5ec71f4cdfb6adf1b79"},{"version":"0af499d2713aef4ec7966d400cfef65a44f757c731937d340d93db85710f2a1e","signature":"39104c0ab7ff0e8994a3da419b2ea6d46a7a9f67529e2d643bafad2d68f3f6ac"},{"version":"89121c1bf2990f5219bfd802a3e7fc557de447c62058d6af68d6b6348d64499a","impliedFormat":1},{"version":"5c5d901a999dfe64746ef4244618ae0628ac8afdb07975e3d5ed66e33c767ed0","impliedFormat":99},{"version":"85d08536e6cd9787f82261674e7d566421a84d286679db1503432a6ccf9e9625","impliedFormat":99},{"version":"5702b3c2f5d248290ed99419d77ca1cc3e6c29db5847172377659c50e6303768","impliedFormat":99},{"version":"9764b2eb5b4fc0b8951468fb3dbd6cd922d7752343ef5fbf1a7cd3dfcd54a75e","impliedFormat":99},{"version":"1fc2d3fe8f31c52c802c4dee6c0157c5a1d1f6be44ece83c49174e316cf931ad","impliedFormat":99},{"version":"dc4aae103a0c812121d9db1f7a5ea98231801ed405bf577d1c9c46a893177e36","impliedFormat":99},{"version":"106d3f40907ba68d2ad8ce143a68358bad476e1cc4a5c710c11c7dbaac878308","impliedFormat":99},{"version":"42ad582d92b058b88570d5be95393cf0a6c09a29ba9aa44609465b41d39d2534","impliedFormat":99},{"version":"36e051a1e0d2f2a808dbb164d846be09b5d98e8b782b37922a3b75f57ee66698","impliedFormat":99},{"version":"79b4369233a12c6fa4a07301ecb7085802c98f3a77cf9ab97eee27e1656f82e6","impliedFormat":1},{"version":"19990350fca066265b2c190c9b6cde1229f35002ea2d4df8c9e397e9942f6c89","impliedFormat":99},{"version":"8fb8fdda477cd7382477ffda92c2bb7d9f7ef583b1aa531eb6b2dc2f0a206c10","impliedFormat":99},{"version":"66995b0c991b5c5d42eff1d950733f85482c7419f7296ab8952e03718169e379","impliedFormat":99},{"version":"9863f888da357e35e013ca3465b794a490a198226bd8232c2f81fb44e16ff323","impliedFormat":99},{"version":"84bc2d80326a83ee4a6e7cba2fd480b86502660770c0e24da96535af597c9f1e","impliedFormat":99},{"version":"ea27768379b866ee3f5da2419650acdb01125479f7af73580a4bceb25b79e372","impliedFormat":99},{"version":"598931eeb4362542cae5845f95c5f0e45ac668925a40ce201e244d7fe808e965","impliedFormat":99},{"version":"da9ef88cde9f715756da642ad80c4cd87a987f465d325462d6bc2a0b11d202c8","impliedFormat":99},{"version":"b4c6184d78303b0816e779a48bef779b15aea4a66028eb819aac0abee8407dea","impliedFormat":99},{"version":"db085d2171d48938a99e851dafe0e486dce9859e5dfa73c21de5ed3d4d6fb0c5","impliedFormat":99},{"version":"62a3ad1ddd1f5974b3bf105680b3e09420f2230711d6520a521fab2be1a32838","impliedFormat":99},{"version":"a77be6fc44c876bc10c897107f84eaba10790913ebdcad40fcda7e47469b2160","impliedFormat":99},{"version":"06cf55b6da5cef54eaaf51cdc3d4e5ebf16adfdd9ebd20cec7fe719be9ced017","impliedFormat":99},{"version":"91f5dbcdb25d145a56cffe957ec665256827892d779ef108eb2f3864faff523b","impliedFormat":99},{"version":"052ba354bab8fb943e0bc05a0769f7b81d7c3b3c6cd0f5cfa53c7b2da2a525c5","impliedFormat":99},{"version":"927955a3de5857e0a1c575ced5a4245e74e6821d720ed213141347dd1870197f","impliedFormat":99},{"version":"fec804d54cd97dd77e956232fc37dc13f53e160d4bbeeb5489e86eeaa91f7ebd","impliedFormat":99},{"version":"2ccdfd33a753c18e8e5fe8a1eadefff968531d920bc9cdc7e4c97b0c6d3dcaf8","impliedFormat":99},{"version":"d64a434d7fb5040dbe7d5f4911145deda53e281b3f1887b9a610defd51b3c1a2","impliedFormat":99},{"version":"927f406568919fd7cd238ef7fe5e9c5e9ec826f1fff89830e480aff8cfd197da","impliedFormat":99},{"version":"a77d742410fe78bb054d325b690fda75459531db005b62ba0e9371c00163353c","impliedFormat":99},{"version":"f8de61dd3e3c4dc193bb341891d67d3979cb5523a57fcacaf46bf1e6284e6c35","impliedFormat":99},{"version":"43866c6355cd21476b3a5cf0dc422e8a56f7acf5010acf7d9c7ea296b3beed44","impliedFormat":99},{"version":"70a8c15739dae81bdd251aecfa667521beac1ca3d62d5510ef1d168367976efd","impliedFormat":99},{"version":"117fc7342e10087d11eea826713624c5ae6b2d886e4a4a592b1cb6a30e3a1eca","impliedFormat":99},{"version":"c9bb4f55c9b9f1942bd206cce9722107b1653236a2c3805a3ffc1b78e60129a7","impliedFormat":99},{"version":"fc610233291d35d0d2c3a0214f4a46d20ad989c74431b9b5d535141065890e41","impliedFormat":99},{"version":"09b7befce01ae33053c59bcdf1c66502af074023fae5a2729b8a0411e1b94788","impliedFormat":99},{"version":"add39e62eed912ab8defd0c6473b6a4d158e22b6343f960a96825924ac87621c","impliedFormat":99},{"version":"64cd32a60b0316be7adc86fad333ec9befbab241486ecf9b0e867b80acc7e9f1","impliedFormat":99},{"version":"a65735a086ae8b401c1c41b51b41546532670c919fd2cedc1606fd186fcee2d7","impliedFormat":99},{"version":"fe021dbde66bd0d6195d4116dcb4c257966ebc8cfba0f34441839415e9e913e1","impliedFormat":99},{"version":"d52a4b1cabee2c94ed18c741c480a45dd9fed32477dd94a9cc8630a8bc263426","impliedFormat":99},{"version":"d059a52684789e6ef30f8052244cb7c52fb786e4066ac415c50642174cc76d14","impliedFormat":99},{"version":"addca1bb7478ebc3f1c67b710755acc945329875207a3c9befd6b5cbcab12574","impliedFormat":99},{"version":"50b565f4771b6b150cbf3ae31eb815c31f15e2e0f45518958a5f4348a1a01660","impliedFormat":99},{"version":"74958763e089797b627dae00e31679752826f7e91c0818e971be919dacb21c1b","impliedFormat":99},{"version":"2cab237ff67a4f114999005809e65fd586f7fdb56e253e1208cabea827df7e42","impliedFormat":99},{"version":"4e3ab6678655e507463a9bfa1aa39a4a5497fac4c75e5f7f7a16c0b7d001c34a","impliedFormat":99},{"version":"49766a3e83c44708ea1357fa410c309837f4420826a5e265e3fb81e0087c7025","impliedFormat":99},{"version":"9f9fba2db9bb11058b153cdede4ec4e3ceed37d51a18a2edfaf7dfbef0a2b241","impliedFormat":99},{"version":"7a8db4befe8f75591173ae1038856ef6ce3ecfee9e22f1b616779018a8779471","impliedFormat":99},{"version":"006ffd4a92ea7050298f50e44dcc03155b943454bb874c0a5a3ad7c8ae92a50b","impliedFormat":99},{"version":"e981c553d5f18821019546df17a5ade9e26316417279a4e478f28d72cfe6c6f8","impliedFormat":99},{"version":"4b29df4bba7b409b5fa9a8db9f4b672eed5ef80664ff09de5d11724aebc5a30b","impliedFormat":99},{"version":"f5282507ffe12947a832a8d9be748df429b3ef61454df95750fb72ff24108a68","impliedFormat":99},{"version":"22872735dfe76e24d30e89624385361d3d077c8d940257aea9b48d57839c906f","impliedFormat":99},{"version":"bd93deb5a61f121242eb17014a20254b517e411b7144769b2389cc56bb254c40","impliedFormat":99},{"version":"0574c7abba287242b2b56e75a37599682fa2ed3d24ff9cc1233a49fffef6cc81","signature":"2ac0937aa98b6c278485c2f01ea1f1a4407ea167fcc064ea361a3f64db054cac"},{"version":"bbcee7849113a4e4439c91e7b55712f1570ff7db4c760253d7693eb2b0541118","signature":"a6861a72bed1cae361b69956a9d680a181b9774d468210f4868ba9a4bd07610d"},{"version":"6e7eb495ef234ffb72dfed736d4526bb91f764e62c1c564b054403432cd523e2","impliedFormat":99},{"version":"767cfb1496df590e9132052dbf9d5a2ed4d7864823eafb4eafcc5f9569f3c152","impliedFormat":99},{"version":"b9f9f77327ef54fc055ab78fc6fc6c93b88410621fd9b2374bb956022018a62a","impliedFormat":99},{"version":"82147ff6ea10d9182855cb50ed6f28d36c1d4f54a792ec2173d42074225caa5d","impliedFormat":99},{"version":"aa015d746ff96c12818a7ad742e45f40bc6f72f4b7df2af0f7200dacfe0ca65e","impliedFormat":99},{"version":"01a6da71fc05a9195f334ab1092b3f4952b0f2e2c460ea55d206ef629309062d","impliedFormat":99},{"version":"66f0b814ca2e007985a604145f5d70793a9f66d2fc7d6c57b2e094c15b7eb83f","impliedFormat":99},{"version":"8d9b0d78ee11670d4caa429ce908ef681663d65e94dc1507b47d6eb44abf903d","impliedFormat":99},{"version":"888cd85dc7c3d5d1fe0c975a8a42387203a6040f6945d9e834044f4cdcaa7177","impliedFormat":99},{"version":"87783efceaa85da75752e808792e347efda30d7aa7d7a76e62398df4a86c31e5","impliedFormat":99},{"version":"6a8ab860d18807ef3ebcbba3c436ae6432640a8e9fd3b3e965d3b923d7ab1043","impliedFormat":99},{"version":"41560bfeef3407476ecbf1e87fe10ad8bb66493854c8810bf1475c1927e9509c","impliedFormat":99},{"version":"6832c3df6998885275fc6cd413f83aab3dfeac3b5b2d9ae0acc29707af0dfde6","impliedFormat":99},{"version":"c3367f21f12005ddb218db94e8f45502526a0c5934456492f39c6c4596b9aa25","impliedFormat":99},{"version":"85985824476f773241686f7b04c9983cef819dcdbea5a6564958898377cc3a50","impliedFormat":99},{"version":"0a0bd6b6fcd60d1165cee9bf6ef911a0d99508214386152d8f2e1e9ad6b0d61e","impliedFormat":99},{"version":"cdd20c5393a901a75302ba47d99425905028f9755c8469cc2641ed8cab9198e4","impliedFormat":99},{"version":"ad4d3582ba33d300a03f929ebc2a14239ab5e1f9aa8b555b4aa6533038ec20c0","impliedFormat":99},{"version":"73b50f5889069014a3063ace1a2ee649b8d64b5664302c10856a7a7ef4c8a404","impliedFormat":99},{"version":"a34c55195ac6e244b0d31f62f0c56545fc8a5e95fcb2b13e5e14fde434536dea","impliedFormat":99},{"version":"f72da096641226a5ab0556e1f4fec5406ac536194a3048d107a10a67e7b9addf","impliedFormat":99},{"version":"52d28c311133f70529f3e34c139fef3976b851b49390217059f743e927ac2aa7","impliedFormat":99},{"version":"6e998866598c2a811ededdb103763750fe3b763d9553ba9750ee8ed4085f6f03","impliedFormat":99},{"version":"e725c7b2ef6718b41be54c450d8983e346a923ecd54ec0d17f53fbfdff6bb11a","impliedFormat":99},{"version":"5b10dc5b06ad30df43ea4df39181eba3c68715af0fde9124fcc0943e40d50c92","impliedFormat":99},{"version":"2567af91666fc79d94fd70a25d7bd13ff340bea3bf677d096e3072c53d55e09f","impliedFormat":99},{"version":"0559c28b4b44c127446a6a528ad2ad5986153c17a667416fcc053f25af64f99d","impliedFormat":99},{"version":"c7ee05e266d06aa759dc6e3e1d6f9fdab337cc39cc3924496b2eb7e472f2f830","impliedFormat":99},{"version":"b06b851cd2fb5d33cfa6c4d12fcb4455898a4c270b20c87c8b119b945c64e1c1","impliedFormat":99},{"version":"a21c78c299f733901aeabd0d9c70115af96ba53c4703362e36acfd5cc32f2d26","impliedFormat":99},{"version":"93ccad407f8b531feeadff9a8337309643fb2b582d4960df8ca33963fe48e345","impliedFormat":99},{"version":"ac036d642dae91d521f48564b87903404bd5d25ea9adcdff3dcde7b34e8120ee","impliedFormat":99},{"version":"033de0ed908f278acc13eb5ad6e7c74ebd6eee9bb952a909b3408300bc0f2aac","impliedFormat":99},{"version":"26b5b5171b6239b01fc878b20a4c46fe6f30ab4c2600f6e0d75f17a0369310e9","impliedFormat":99},{"version":"ad3935c73e8fbcb9b36770aaa55edf61034a8128f434673f2e9770181c4cd517","impliedFormat":99},{"version":"ae46e99d567b4f131dedd3bda872f86836aff70d37a80c7ebc2469cc7ad774ca","impliedFormat":99},{"version":"805544b983979e4ba24049f35b59f1155470c38298c9bf64520d2ba602289d00","impliedFormat":99},{"version":"f8281fad02736e3f7e2475b14bb15990b00b8358b5767a0e7dc3e774f9bf81e7","impliedFormat":99},{"version":"a380cd0a371b5b344c2f679a932593f02445571f9de0014bdf013dddf2a77376","impliedFormat":99},{"version":"dbbcd13911daafc1554acc17dad18ab92f91b5b8f084c6c4370cb8c60520c3b6","impliedFormat":99},{"version":"ab17464cd8391785c29509c629aa8477c8e86d4d3013f4c200b71ac574774ec2","impliedFormat":99},{"version":"b026daf5c00c8f2abdb49008322a7d51fbadcd7b200a99ee7a4452dbd40f0420","impliedFormat":99},{"version":"e130a73d7e1e34953b1964c17c218fd14fccd1df6f15f111352b0d53291311bb","impliedFormat":99},{"version":"4ddecad872558e2b3df434ef0b01114d245e7a18a86afa6e7b5c68e75f9b8f76","impliedFormat":99},{"version":"a0ab7a82c3f844d4d4798f68f7bd6dc304e9ad6130631c90a09fb2636cb62756","impliedFormat":99},{"version":"270ceb915b1304c042b6799de28ff212cfa4baf06900d3a8bc4b79f62f00c8a7","impliedFormat":99},{"version":"1b3174ea6e3b4ae157c88eb28bf8e6d67f044edc9c552daf5488628fd8e5be97","impliedFormat":99},{"version":"6564db88fa4cf3bae5c8e0de01c701c6228eaeef1118b4ad6a49a8aaf8556350","impliedFormat":99},{"version":"9c5a4a21ed5686b2ea080557488b966c3aeea641ef1aa8975a10e5fc59dd569f","impliedFormat":99},{"version":"5585ed538922e2e58655218652dcb262f08afa902f26f490cdec4967887ac31a","impliedFormat":99},{"version":"b46de7238d9d2243b27a21797e4772ba91465caae9c31f21dc43748dc9de9cd0","impliedFormat":99},{"version":"625fdbce788630c62f793cb6c80e0072ce0b8bf1d4d0a9922430671164371e0b","impliedFormat":99},{"version":"b6790300d245377671c085e76e9ef359b3cbba6821b913d6ce6b2739d00b9fb1","impliedFormat":99},{"version":"c92c9dc7415bf7969931ac5339f21c65687909b32d05ac89789b411372e52991","impliedFormat":99},{"version":"a36c717362d06d76e7332d9c1d2744c2c5e4b4a5da6218ef7b4a299a62d23a6d","impliedFormat":99},{"version":"a61f8455fd21cec75a8288cd761f5bcc72441848841eb64aa09569e9d8929ff0","impliedFormat":99},{"version":"b135437aa8444e851e10cb514b4a73141813e0adcfcc06d702df6aa0fd922587","impliedFormat":99},{"version":"cc82fa360f22d73b4cc7f446d08ad52b11f5aba66aa04b1ed8feb11a509e8aff","impliedFormat":99},{"version":"466e7296272b827c55b53a7858502de733733558966e2e3a7cc78274e930210a","impliedFormat":99},{"version":"364a5c527037fdd7d494ab0a97f510d3ceda30b8a4bc598b490c135f959ff3c6","impliedFormat":99},{"version":"f198de1cd91b94acc7f4d72cbccc11abadb1570bedc4ede174810e1f6985e06e","impliedFormat":99},{"version":"83d2dab980f2d1a2fe333f0001de8f42c831a438159d47b77c686ae405891b7f","impliedFormat":99},{"version":"ca369bcbdafc423d1a9dccd69de98044534900ff8236d2dd970b52438afb5355","impliedFormat":99},{"version":"5b90280e84e8eba347caaefc18210de3ce6ac176f5e82705a28e7f497dcc8689","impliedFormat":99},{"version":"34e2f00467aa6f46c1d7955f8d57bffb48ccc6ad2bbc847d0b1ccef1d55a9c3c","impliedFormat":99},{"version":"f09dfae4ff5f84c1341d74208e9b442659c32d039e9d27c09f79a203755e953d","impliedFormat":99},{"version":"e7878d8cd1fd0d0f1c55dcd8f5539f4c22e44993852f588dd194bd666b230727","impliedFormat":99},{"version":"0d22b18cb407b03331edf3413b93e568da71130c906fd69911090df2abf55414","impliedFormat":99},{"version":"2a39f2a7f99dba7a6ec561378d8a1f2e9d2761ac272a006777f78825822fc419","impliedFormat":99},{"version":"ddc0727ff009562bced85c9e7d55b29fb2a5ca23598350eb16299e57a3516a4a","impliedFormat":99},{"version":"b8fefeec475a9f1e78e1e5ae56f7fbc77ae5fb9b8494a8205360ad4b304a511e","impliedFormat":99},{"version":"fad5ca404b01ebde453f9cd5aa80353633878e7e1e1c22ad4a77fd841a49c922","impliedFormat":99},{"version":"64155a3d9cb589603d04c37f1327f91d640f9924c999d4f47284e7a5eec5cf55","impliedFormat":99},{"version":"b1727992696e552fb934bb72673da49085a9eef76050e748448c1e0c2568f63a","impliedFormat":99},{"version":"4eae6595d59a0967571e022dd63f99f3fcfe624e50e17cd26195412e16f22549","impliedFormat":99},{"version":"b62a3b99a2544eb853035484e4e1c71a7675ffc4a4e375fc3c6fa89450f61293","impliedFormat":99},{"version":"b9d571d39d729a623a60d6fb7318e49e41b32e94935ffda36103b43f7d824c5b","impliedFormat":99},{"version":"00f37073c33a43720d2e2236e4dcf43fda3d2113c66703a22a82c066415054e8","impliedFormat":99},{"version":"04bc73f52fd9447154bb396f89640f90434b084939ad22f16b42b2b967d21cde","impliedFormat":99},{"version":"2f6c7cc2633c2a3e4f51aaebe5f07efdfa04405697b7d31c8f7dc19997747521","impliedFormat":99},{"version":"abef69ad896a9e33bf221b4161d7eb3710c30c9cad6a7b6b053df043580a6f43","impliedFormat":99},{"version":"3e7761d82206ddf29dda7f2c1b169b205f4707e043eaf854fa3908dd7e5b0220","impliedFormat":99},{"version":"50ce52224dcdc3d9a431a5dd08f814c76e3cad3100f7a511cf9f0fea3e022245","impliedFormat":99},{"version":"7461a12eefe80a0dd858e8ddbfd3ee2af7627c64aae2c58f8cc3a1f1810a0300","impliedFormat":99},{"version":"d169dee8be7c5c62179b381a7d823d874b0c218604db989d25b8493e3ec932ca","impliedFormat":99},{"version":"26e23b6bbe7b0f8686e4374c69693b0925538608d3af66095fcd5a4debe99cb3","impliedFormat":99},{"version":"20d89ee7150b3ba09c98a1ed855b9dc55d0d883095cc010af0eeafbfe0b60cd8","impliedFormat":99},{"version":"cedfd67cfe921e49f0743aeefc7d93564d2fa7b34d3cf0860f3488ebc4e0e057","impliedFormat":99},{"version":"c048e95315c9725df4687c03b21980f12e9a3ec8c288db45233618f5ce685d27","impliedFormat":99},{"version":"754b10aa3bc061c937bec5e3d2f825e4211ec72ccb94f5aebaefb765ab80c56f","impliedFormat":99},{"version":"95f1648d8c0e1094e2375fb3531a4bad0a99adfc619832fec800fca6934095ce","impliedFormat":99},{"version":"c363c5eaab6f4ead856243f0734d0c320d3748ec777fda05484e17258b515cbe","impliedFormat":99},{"version":"e5bf7408d2d66195b132ae905b89f83d660dde24a588c3d1c43dc000bd5fcf71","impliedFormat":99},{"version":"b3bcacabc80b752dd496e7d154e4707b79e755d2e9d0547e402e6d3fa0326c9e","impliedFormat":99},{"version":"b11aba62a6d6eba8f5487182096d8615ca8d5d5b8a50c99bb0f0717ed7e061b2","impliedFormat":99},{"version":"761bd09a063914a0dddf95c6e9203d769487f1aea46a82ff1fd96cc0ede2d32f","impliedFormat":99},{"version":"cafe5ab7aa61b213d16cef121e65269dcda6607462c5762ef21124c1fb2d644f","impliedFormat":99},{"version":"7a43534274fe9532fee5414984708a56f0baaded902466c2eba2cbfc601ae20e","impliedFormat":99},{"version":"1cff175af3f1ff70546a36d9f2f118451df21e2d59bb837ea6b3cdefd5e2c7bf","impliedFormat":99},{"version":"1998b2e12b3de685ce74eab487748303705062861abe695beae5adb3092cfb98","impliedFormat":99},{"version":"11f999c38bd526002c0809a8157c366d032a0d46314d54c9724302140c5f32fd","impliedFormat":99},{"version":"607751697c8b64c78e2a3e89225a93cfd6a7f2c4399197d7c6f52f2a911d5522","impliedFormat":99},{"version":"c12bfbef4bf57182e240a916ae15d017defdec55d2920a4d08644563970b2036","impliedFormat":99},{"version":"1d7ad8c09354264ec80cf989015bd4bf232c356722d935c5ac38bbb83a6aafbb","impliedFormat":99},{"version":"6b821199f43755f7be315b84ede22b7e51fc91e98b01625cc0610042719cd0f7","impliedFormat":99},{"version":"4e8c57027a125d5d221b6f306eeff8443142bc9a0b1113c7c5de96478c388cc1","impliedFormat":99},{"version":"128dc4bc5697686aa6f876fd5ad6fae6d19a5652eca99f874b40a8afba6057d0","impliedFormat":99},{"version":"df09e59ace0cf7fd8e3c767b0b8f3d5b2212bd40d4e9dbf49a388526ead5e545","impliedFormat":99},{"version":"03fe713f6d3e69f47d4871545b2cd40e1f714b11e3945f6ab1e05361cad19192","impliedFormat":99},{"version":"25ea3ca113eb33c517ace8bd6555e8caf1c99d34dea7b3a48b074bdc5e7cc735","impliedFormat":99},{"version":"d5f83721da8ee61cd1620f0e4abc9d60453b4ef0b25945172bbd478ff6e3adcb","impliedFormat":99},{"version":"597536b4c8e7e309dd823f61206ff169ca90a3d68245343a5226a8d20f7a9ffd","impliedFormat":99},{"version":"cfed3c4118baa2ae69ec0da47b5fa722c49e5915c1cb7ffa8d5b53b678b641ca","impliedFormat":99},{"version":"3b5f9ed01add914f4bf2220544cdd4099583c2a72895b2ea495e9284be88b894","impliedFormat":99},{"version":"575425068684056c113b93ec316870eca50195835f04179630997e7b075164d4","impliedFormat":99},{"version":"aa0af7166f48f67765f96dc70c1d7f9f55ae264b96cadf5b6077b2bc0aa2b5dd","impliedFormat":99},{"version":"2fc9c7c6695b151ffd3ed667d6d793c2f656461978e840eff1d1350fc0bb1ebb","impliedFormat":99},{"version":"4d590f0e0b4abaf693f94d08b5c414928f2571aea5ac6efb97e4646e195dac48","impliedFormat":99},{"version":"bf1655c135bd654637f98f934f9a9eb4d6450194ca2f4968b79263608da59fdd","impliedFormat":99},{"version":"1ebe079cc9ed9ec4cd11d02c70f209caf16e9dd8e1e801a36648ce711bb3c404","impliedFormat":99},{"version":"b9a49ec04f7dac4bdcb928894d3c6bfc15a4ffee5f14d4b2bef69069de2b1d25","impliedFormat":99},{"version":"d2554dc921a526fc29600ef66da5d37577ce64557eaceebdb2b6170776051ffd","impliedFormat":99},{"version":"1fffc635777e6b7edc2caf5e8473fd161ba8fe5605ae84b64746a2a34045e6e3","impliedFormat":99},{"version":"f6f35852e407d4c04e578478e743c8101227037abd405ad71564e8430d4e4c1c","impliedFormat":99},{"version":"0e16f517285c960f284ff5367c239c24e20acfd3b3aced00d7842c7cd867898c","impliedFormat":99},{"version":"063929189f06ac3087123f98ce8736eea5c2a4cecbaa46c54b58118608fb8708","impliedFormat":99},{"version":"95e249a5d61382550bbf6341cb31a3114b0c10c232b0fbac97da74f83275dbab","impliedFormat":99},{"version":"34880c694bdb6f266bf484dbc60bd887890ab1c8d6807459fdf93f6ab0b0c792","impliedFormat":99},{"version":"d490c69a03b97298294b4d2159175f2f9b62a7dfcd81efc63ea3505c60fc0ac8","impliedFormat":99},{"version":"c0511c2a0a6ae726a66a0b25336fdc61c632c6c1505eb0bbbe1556055fd06919","impliedFormat":99},{"version":"bbd577f9d34a868d751e14a904401a6a026de322bd0f7cc7d51ecc585b57c6e6","impliedFormat":99},{"version":"e06f1dbc167fd37808d2f36aee8ef3bfae734d13fc4afcd9994072a30099df0b","impliedFormat":99},{"version":"7e0693b9604ff4fd358a2f21db90982fce11a4ff216eeae85e326f108d648d68","impliedFormat":99},{"version":"69d5a40440e664ec9493c61169414208472583f342346fb31dcbebad23c6a780","impliedFormat":99},{"version":"3d1efa91afa616c307864fa93f9e7777672bf90ac79c16936969efbc6629cbc1","impliedFormat":99},{"version":"722d99f854a33867a3397fe397924c0de326741e9d7830876481ccf87643a133","impliedFormat":99},{"version":"6ddebf5efb47d10d48d734f855b5cd13772de6d14b50779a17590afa30fcaefc","impliedFormat":99},{"version":"0c4e55d5ee917d3efca585b9eb138df9eb9cd38ec5a8ab258fc0e4fe8d54abd6","impliedFormat":99},{"version":"a41283ab6303ab1d21729da5723952e1b7d9d1bf379b9cce09132403f6f0f07d","impliedFormat":99},{"version":"1557f6fa35f1fc5d23911982761a7ddc4fbe9322eda575f6dadca8d5515e0cee","impliedFormat":99},{"version":"ded4768515ff723001ca99221ec8a7b751a4f90af8be95c885ec8a21efa548d4","impliedFormat":99},{"version":"86a6b6352a6a47fa8698f68119eb716ea951f541128d108382137e3d85900162","impliedFormat":99},{"version":"b9e6aa9de64cbd004d2c59ad79cff12a5d34406153456b869eae6b1d8f5bbb61","impliedFormat":99},{"version":"b864079910dbf273badc3367d21910e6f8887705defda6d51882084ef5b6ebb5","impliedFormat":99},{"version":"5e1fbca4c7648ff481cd7dda46ca8651239f4898bbed3d5066b8d6db0f60c555","impliedFormat":99},{"version":"0fd72d2cd43f9da04478aa6cbb922c27fda6b4a0ca7235aee8f6a933941bae92","impliedFormat":99},{"version":"28a5fc04797c6992303791a4b79ed99431bf2af90b77842948e3e8bc0f681d97","impliedFormat":99},{"version":"20a397ebeea68a193465caeb8f1b3225c49c6bdc4545a0ca6521fa684ef2fc5b","impliedFormat":99},{"version":"9edb51982e31faae66cef7076aacce409cab4637f0ec6e6851331fc87d9fa872","impliedFormat":99},{"version":"8a954a3b53672645d10a61e617272cd309bd7dc442f914427291ea8a2cb3c599","impliedFormat":99},{"version":"69e56ac933cbb0a62cbed3436a44068445460c3a246ae5b7b70e010dd3d8e0e5","impliedFormat":99},{"version":"dbafd3b17be2ca81493a6d6daf7afb6d04b83dd3b80c556ae5973e9a375aa6ff","impliedFormat":99},{"version":"8ea7264ce539f97f4720250a14695a1f82c3ac33ed66617991438cfb3d41947f","impliedFormat":99},{"version":"abf4fe7ebd8771a5c9d963754ae0c088c159e0e12ff77ed2e48f69d86ed50be5","impliedFormat":99},{"version":"050985cfd782bb62131bdc040cd444f65bbd35ca1b45e3eefde79ef713ddeb1b","impliedFormat":99},{"version":"cd935cd60191b79134684160cf08d5ab555512b7befd5f88b449681f7e037a3e","impliedFormat":99},{"version":"5e61341da5f93b211c8c11f6ca709a2dda33feded2cb3e1481d0b727aed23cd7","impliedFormat":99},{"version":"3df29fc79cceaa80012df2005f745c1b635a41dda0766ddd29ac8e078c4216f8","impliedFormat":99},{"version":"bb8aac29b972b3826d70b106373ed2600bfa8377ddd5672b0b5e256f361acfb9","impliedFormat":99},{"version":"721e984c6181cf2f1587226d11d2265f5bd1450da58e7598845ae15cee0465b4","impliedFormat":99},{"version":"bfa5dd71d432e277bbf3c44b3dd91db853fd833da21f43cc3a0a2dc324b7af47","impliedFormat":99},{"version":"987f95fafbf1296cd653459ac69b3a73cc4f7f37f38a1c769902bcaabd516452","impliedFormat":99},{"version":"5f3f4c3ea0ce78180d0482cfc808410092b441b727bee9a26d26ef09decb64c3","impliedFormat":99},{"version":"6b1c226b8c0d272c37948df86f81f100157b7cef9d97d2651f1d8e740a2be268","impliedFormat":99},{"version":"3e1e6a7a6e3d0615d046dd62062c557404c9e1c622dfbef4fa83d17c628e314a","impliedFormat":99},{"version":"ded6a76ae5fa6fc3f7f860a187f3b1c81b5bafb953be192501a937307dc739b9","impliedFormat":99},{"version":"8ee4717c4ad7d5976ab1bece799c68045b7b346e274d7774e86ff394cf867d47","impliedFormat":99},{"version":"a9e3299c6098d323bcd9787e2ac5e4d74457e5427c305ebd432e52ed13dd23c0","impliedFormat":99},{"version":"3b8ddc84907c11d89e5240f8299e87fe506a1c18764fb9d546b02f9e347f93e4","impliedFormat":99},{"version":"4b5186a238a3fbfeca1b14fe6b234b8bd5b93ac89a47984715f76f8919e29d96","impliedFormat":99},{"version":"dd693a36f441a1337e9a34b6da2cfdc53d7cd9dbf0d1cde334cab196c3e6f17a","impliedFormat":99},{"version":"e284b0d144003a25e3b7da92c55d80a085531c0c84a77a62f1a3e2bfe82454b1","impliedFormat":99},{"version":"e09a001b1588cde9f5e77560730c5ec6ea346ea09da2d65b3a02629e390d0923","impliedFormat":99},{"version":"77a38dc0e4afa289c6ceaf9121c7718fac9231904ff5f30c0c1201524fb25f23","impliedFormat":99},{"version":"2505ab6e616e35b2a63dcc8cafd3d6092c6e0109b206cec1be7d80cd450cfb7e","impliedFormat":99},{"version":"c2d66e86bd5f13ad098ddb98ab863bcf0d15b323cc9bce6ff56e20eb50648926","impliedFormat":99},{"version":"1959affb5ce54dd86a2bca9ba087da5a29fcd1849eb1710f1d6807512ac513d6","impliedFormat":99},{"version":"8ae1ca3c5186af2c3a6e979998c00c82a363d024ed26a8bcb84a6ef21878d4b4","impliedFormat":99},{"version":"05ec4ee3055f601ad3f932832a0b7fb5842fafa6d3a4f29b200c49e0c8d36bc4","impliedFormat":99},{"version":"cf0430d48488f45abbb7184e36b580c1933cc8078581c038583e16cdfa80863d","impliedFormat":99},{"version":"aeb0806490c71f371a75629556f469419997367aafa673ae2e86705652f39227","impliedFormat":99},{"version":"b445ce66b11750fb2172525591647d30b75ec8d003a93ee3efc12954ffb8d5ec","impliedFormat":99},{"version":"a36860808f7994a6ce2c965e11e7a971ae9b438422ffefe9471ed4f3ba3ef3f6","impliedFormat":99},{"version":"6335dfc1f7c285b25ca643890becb12d22fd24079624d91b660f31a3ef130f8a","impliedFormat":99},{"version":"e0e3a4eb638ceb6583ebfa8a83fb27d5ca98ba93ce00d396dd09d41630834a68","impliedFormat":99},{"version":"54927a5dc7fa2c1d7b8232c756d658d051f2c0620533c4048343e671d18c7983","impliedFormat":99},{"version":"8ff6d24bddad8f2bb8ac0ae129760dd1df427765f4f6199390347c0009ff9b48","impliedFormat":99},{"version":"25a0403b5a28416593fc09c3e5150e981c742afedf0b6e423f8a94f1351352c8","impliedFormat":99},{"version":"0db51b9c351d7c2313296e5879fbad8f20af8b9061885c8590ae0216ff6154a2","impliedFormat":99},{"version":"1c6454d7f733067337f1685a06c4981a78d1925ae0381bcd09f2c43b73755aaf","impliedFormat":99},{"version":"fa7cfbc437c87d93860a4a1304a12e8fa9a28a24695e1f4c36d266b4cbdf4e7b","impliedFormat":99},{"version":"4afe4410e572d10448fb1f7f254bc2eca329e00180e75529be8b22465718b2c4","impliedFormat":99},{"version":"6d57f0ab1e858b12895e1eca5e6f407a835c13038fadd5f85134740e6e32b613","impliedFormat":99},{"version":"5d6ef65ccf14b0d51af503adffccdbaa846848cf0fe82310816cf82eb364d107","impliedFormat":99},{"version":"397a17567b445fbeed01f58163c2b9667638ff815881f349cffec5ad30844905","impliedFormat":99},{"version":"4bbac0cdef9e2f41b2d70826633298df274436caf011a093bfc1bd57dd2269c1","impliedFormat":99},{"version":"38bc1e63328558fecd8e2c3f9159ded06a2fd86059b0a91e93f11aeebeb1f177","impliedFormat":99},{"version":"e4756baf25dba2f6660043ca4fc6e36ab8d31a0c34d507624d8a12cbe6ab9b30","impliedFormat":99},{"version":"4be799bfee1766047c11b3b5d371ca9e3993526d50c3e276e7cdb3943dd680a6","impliedFormat":99},{"version":"513f887178e2df3ea70340c20bffc633827c2f0a4e1340a4176eecb6d3c5a4e7","impliedFormat":99},{"version":"7b6ae8994d485350989d50cae27221f2d9929d60677b8c5de4ceb81baf480d3e","impliedFormat":99},{"version":"7adc87c8c1d3fdf85652694a636c6e032b7ea76335de7a110ffb215970d9b1f0","impliedFormat":99},{"version":"27f60ec60daf015ed0510587fd37d65818c085d47f320f03e2178e80ae1b96d2","impliedFormat":99},{"version":"69f868104a0f83fc97451cb143d1fdee65d6133442e585048983a8e27c6dd4be","impliedFormat":99},{"version":"a4fd1c983504d251988ad2e8629dc110228a591e1bc9f85e57b35895e8141cf3","impliedFormat":99},{"version":"8e82daa0bb799f366cfc646fcd65e507d3db4921082426dc45b78cedcb60f71c","impliedFormat":99},{"version":"a2f99636b48361b7e10c232cdeeb3aede976279f950b7f84e8b970471a0dc606","impliedFormat":99},{"version":"e31bcde9cdc47ff503a45bb8d68c38fd39039b06567c8017021f9116e31ecde2","impliedFormat":99},{"version":"bddecda3271cc5aaf904e94c2b65c23cb7528e27567ef31d1302bc8845374e56","impliedFormat":99},{"version":"23306c0e57b32fdf724bb0aba128d0023fd57b856490b1cd69bdc0de2acc334a","impliedFormat":99},{"version":"4df175087aed418f42fd5cf507c1f57f0972484cadbe0f6c38cabd0c6b3576ce","impliedFormat":99},{"version":"df51e368f1e65529fd132dd67c8312f21b644063ce7950e8b820905d70b28eb7","impliedFormat":99},{"version":"74d10836d7259d3a664ed517a3c8547003e1de056d85b8e324a11dbca522503e","impliedFormat":99},{"version":"b020142c9cb2c061e119c6ebc1712f2e66a68968657f4867332789688c706189","impliedFormat":99},{"version":"df08a61b7eb2b11b4e9c771cc3b538f3fc4260c170f8fd89110843323fac2ba2","impliedFormat":99},{"version":"acc1e28219b83d788ef0e28b8d7ff1f3a8fc7df6a49ec141402e044badc84108","impliedFormat":99},{"version":"3552e9ab64219d9d980f9406c354e620f04ca17944459d4b23c62d472d9be2dc","impliedFormat":99},{"version":"e9f94a086b52c2f522f2bc9ecf7539d7bffc390578cc41db9547005b93df5d88","impliedFormat":99},{"version":"a8f5b86c67a9c2774ccdd1db31968ad888fc41ad166a5d5d066dbe86637b2f66","impliedFormat":99},{"version":"5c25d35c4bbeabb961925d2b114602191800afb4d4f164e45979dd2c83f965c2","impliedFormat":99},{"version":"8b989bf267ff6ef7a00f318e95794bcefd06cda44f3cffde905ca579d17acad5","impliedFormat":99},{"version":"f3983ffdad35bf9234e36a691bf959934b723134c070fa5dffccffe046009e2f","impliedFormat":99},{"version":"2d8c69bb71790264bc61c5858afcfd00d5bd7bca6c9c38185f07896661053790","impliedFormat":99},{"version":"0396db572d4384fab79c5ad3b2cf330f167496da87da0e61a3f55e803f0fcc4b","impliedFormat":99},{"version":"9eb195a59362d107cf8f1d92d704fcd034bd92796f72cf031ad0525d88e36d4f","impliedFormat":99},{"version":"b60a1aeffa207842414020d16c86854f99bebff51832478b00451c1770fc9708","impliedFormat":99},{"version":"178ae1eaa5cd24618fec31c62ee6b66f5f57d76b075d9d8b34cc0db5543c0fec","impliedFormat":99},{"version":"4f31b5dbde1dcb81687ef4e06c2636809f6a0517615876bc937fa266f42e87b4","impliedFormat":99},{"version":"33d7dde5b3614d1f000ab11f9be2cb0f032615b726ca8537afc272db0b522a7e","impliedFormat":99},{"version":"97b8ea7292ec9f0828af2d58952ea17497a73943eaa0a2dd3046a4e07a530c61","impliedFormat":99},{"version":"ebba07507741f7d53d05cbb59a8dea0b3b75aba278810b4d2696776f95b779e1","impliedFormat":99},{"version":"5c840682cf2d211808e4f0dc5cd32da409352e8b3d11d7e9ba25c92796932cdf","impliedFormat":99},{"version":"f278958ac0fde187482abb825e61be120427e8aecb42a9155f0144767a29a358","impliedFormat":99},{"version":"cf35b9aa3b45b2098ec7033db87d004913ac7bf1ce52c9c8154fa86477e9046e","impliedFormat":99},{"version":"8002100726ad65ae695ef88b091b9c8cb73e024eaf23b31d228a5a8ce19af31f","impliedFormat":99},{"version":"f7a7233cef5a86a5c2c5e4dab11b46e0688707bd066a80aa5249e6cd6e0211f6","impliedFormat":99},{"version":"354126b46c623665e6aac86b6deb693dc43499dbd71bf655aa5f6bcd7753dae0","impliedFormat":99},{"version":"1f1d4396e7a118ba9e1f49f290b29d0aa946e91fc8180909b1d64b2998b08953","impliedFormat":99},{"version":"534f6fde91eb366097dbf65174356c88c2726d513c55f0f60b2098fc3f6e87ab","impliedFormat":99},{"version":"bc8d8ff31066bbfb937d223a528d545abfba45a0a646b141e2a15dd1948026e9","impliedFormat":99},{"version":"8a03c390eab5e99798e3d24c1d86a9b0ee851d315c0cb7480b42f6a49338d694","impliedFormat":99},{"version":"46b25c26bc3816d779c0353b3c1f6b83eeda1030b99401472a4341a9ab94138f","impliedFormat":99},{"version":"0ba4da683fe4d459827bca4e9c2befc46b9fe589353a08d874440dc6b1119df9","impliedFormat":99},{"version":"a5ed548aea7fdd435b68149641d20d0c823ae4341afc4c5c027d246a321e358b","impliedFormat":99},{"version":"c831a507ce1fa56d927c77b0296eaebbe63208dc2e10bea30ee19c19d3832ac9","impliedFormat":99},{"version":"0e0775f99d5750b8ea2409b6d5783ecb7465552176022512c4ab695e2cc07b25","impliedFormat":99},{"version":"998b22c2762b4c5dda982144f4273439478216601e1dbf607f8da50f90028302","impliedFormat":99},{"version":"de07059049dbb4d7b800fad4f5919c62c0c572d021f2cde9b4cb92d9d312464b","impliedFormat":99},{"version":"b006db5c3868f18e051c60c815dd8d18b42f0bd1f2a4732e8baab08e56443fa4","impliedFormat":99},{"version":"f6162b05c0a5b26d3446503c022652af632a803010319ff6c65ddb425c88c1da","impliedFormat":99},{"version":"ce99d9af9d4091b04b4c2266c06f38d7f39e5614616973e0d17f01a36212c516","impliedFormat":99},{"version":"d78d9cf92e8957b87ff73a5029aabd2f591a40f4b915af5566122824a328d451","impliedFormat":99},{"version":"afbf6dde412faa5825df13ab00b8eabe1358558603d6e4c34b4ff83c06128500","impliedFormat":99},{"version":"9468d74878264e4ed6214c0127326482d3d81d8b39c2ffb3ef0887348cfc116c","impliedFormat":99},{"version":"5d761b75b6780d20af311198f88e4a894243753acf28338ce7cc0cb1d5e3bfca","impliedFormat":99},{"version":"4c02021950d7f559417399582f74b4f23e7acf96bf4abfc8511ca9f349e7b090","impliedFormat":99},{"version":"f05da64b51a564bd57764011df409a699928b2b9e018a9692e06d5a3e761a8ae","impliedFormat":99},{"version":"448a1d960c3243442ffb6ddec7087a5dbd062a8b2fdbbc39f6ced3f5b7dd1cef","impliedFormat":99},{"version":"c01a88ada696e9f65a4dd8248bd9a568a3f1ce0c2eaa5e7f8696a2c3b3573654","impliedFormat":99},{"version":"6ca73337f9333cbe5e31c1e03fee42dfbdb119e7fdf991e8aa96767f4696d0b3","impliedFormat":99},{"version":"9235105279d1e63c87e3ea34fb6b20eb4968d7696c4783236a2d8ab5e924807f","impliedFormat":99},{"version":"fb3a41f5d7c41e4195119f98b0df0018e7e04a1b025d7b10ed82741f65c0493d","impliedFormat":99},{"version":"bf21257fb910b2a765ad8529cb457d0d9eb99d8d14ef01836bf04422c185f230","impliedFormat":99},{"version":"9dfbe916e48a8172cc0db07e20248214755205bbd16cd5bc0672dc2cbd80a429","impliedFormat":99},{"version":"cfb85bf0a694c3e0630400fdb2c1f135132b500c152aee6b9ae9d842637378f8","impliedFormat":99},{"version":"2067e20e971aec9f9c3b7a80aa3f2ce908fa690d29614956ab939d4137527d46","impliedFormat":99},{"version":"469bd9ff94e8ee2180eaa7be310e626022f4b97990e01fdee2cb681eadfca444","impliedFormat":99},{"version":"9199cfe666c63b59bfc7a0215ad7b834165e2c8678769aa29b6b15d4035dd874","impliedFormat":99},{"version":"75c20bb61d4944a90fd9fc4d79be814af84552497a30059d1f4a5979e338b576","impliedFormat":99},{"version":"e36d57187446a9b265d011815e80c5ac2d31cdd839e5dfed3cadbade9d63084d","impliedFormat":99},{"version":"ff94c04aacb366b85fa01b058ce27e3f2a26305e87a512b6472013f3951a7639","impliedFormat":99},{"version":"f28c4909885c804c93aabb9561ef520ff620576369eb310195a7a6bbb8d9e587","impliedFormat":99},{"version":"e971398a29ade533ddc2730c360a22672144389e077633fc950660f4c4d9d046","impliedFormat":99},{"version":"52677e8175f66b1d6b684e8249f7b12af8932267f0b2f363ab973fb94ee4d319","impliedFormat":99},{"version":"d1e84e4b4b53354c6333f50ae4337b96f1004f28875d3d7b3800cdf79b0ffd4c","impliedFormat":99},{"version":"a9f319a6a5287c95b72c37b461e0f70f490324cc4a55d71378e2a37f1ca67adc","impliedFormat":99},{"version":"df7c9a0919ac54bb69647b916261beffb40d84494f9cc3e68fc7927a79ccb64f","impliedFormat":99},{"version":"30a24fccfdf6a40249949b222a11c755421b9479bd68a96ee97413540443216b","impliedFormat":99},{"version":"da0ed328391d742978219a5ab07decbdcd353bbdbc87708a2176afa971056a3d","impliedFormat":99},{"version":"14a5746786128a83561f1d719290db89ed9b225e42959bbe0023112035a6e0a0","impliedFormat":99},{"version":"71db39385fc6c5eddb6cc70b1b68920b2aa06cd85ca4a95b1de585e127984881","impliedFormat":99},{"version":"c9f95e2f5326df254b2c867de54f7264763065fa4d29f5f9d10960d97352afcf","impliedFormat":99},{"version":"59e6446d76c9c7a1cf516435f942c5e122b3fa9c80d4962c4eeae63f3bb605af","impliedFormat":99},{"version":"ee28654c6c98fe6b359d34465dbe1f9798d1e7a6ed5ece29daf19de11cb8323e","impliedFormat":99},{"version":"68d524e37e75f230102cd0444befe3e17a8c85438060a552322e7e400519f2c3","impliedFormat":99},{"version":"282d6f2f2150a26e8c5cce93eaf68e829a8683171ba13e92ad7dc8b28a12c623","impliedFormat":99},{"version":"34b95fa727e92d0a2fbe45e37352127297aedd9af64e56dfb324ff1280517f3c","impliedFormat":99},{"version":"4d37c6842a01d7310940ed2d39cfd4a8d63e7676d1113070b3cd725131347844","impliedFormat":99},{"version":"38ccba9a0d7f301e4b590f7a52de8a8fb12e9edb1de6d7ede6a8843176abfd54","impliedFormat":99},{"version":"db4e72e734c0d11d2051dea52f678270eb423be4541e3c39ede40bd29e733484","impliedFormat":99},{"version":"b6fa5f1295afc4b3a9bbae107ea00d1fc37956f25b846338916cbb195515a4e2","impliedFormat":99},{"version":"4b3e07d081541bd9f0418c1963a44b02a65923b6de04d6ef7bfaab8e8b1087d7","impliedFormat":99},{"version":"438659c94886ae81435567673c83b61937048729da03385738a023174cbff051","impliedFormat":99},{"version":"63310e90fe40fb7a431b9ff640d54b70ef30d31b23a634890799569521cc2a02","impliedFormat":99},{"version":"911e8affa1bea9b54a8926ce39295e190ba3e5e36182c7808c3d657fbd77d705","impliedFormat":99},{"version":"02353129e38fd07cc487b5f822ac710ec117e43e479e9f9f8039418ed3291ff5","impliedFormat":99},{"version":"54bd44d1d220488406919d2ddbdb92cef690c8ebfe41d2cdc61a8aaf26d6396c","impliedFormat":99},{"version":"790d0cef4abbfedbde0ff29c9a1865d5ed8364e9e251e25bb13794d30c13ac4b","impliedFormat":99},{"version":"88f2b0ad065d1ff42736c1efeb0e14061b3091d9376c272672be3f27d167a152","impliedFormat":99},{"version":"81de941dc5851bd7ce1e8f81d4d31db1e035de774e571947f8d03b351fe9f764","impliedFormat":99},{"version":"fdc68f982f0c7b074e3787534a5137c3f9d746edc323efddde286931ceb9c954","impliedFormat":99},{"version":"be922b6a92064b78554dfbf46decbddf5a0b023f49a656a7865e17ab0bf710c8","impliedFormat":99},{"version":"609d27139ad9aa20641c466c993b2d8f8c413296ee814b1afd79f48bd809a0f6","impliedFormat":99},{"version":"176b071fba5783d15078411e7c0753d2a874dc0ba0bf334c402e358b59e78f54","affectsGlobalScope":true,"impliedFormat":99},{"version":"774d51ea5facd44ed191c2e420f2a0ab4da76a41905e8c1c3aa2a532ec340d97","impliedFormat":99},{"version":"2be23228f8822815f04418af4b3616f6c12d2e8310d3536537ca59c791fd71bd","impliedFormat":99},{"version":"3e9c9a37943e0f4c86e33e47c598a43f25117acf8fdf177e6ff5e0476e6d0cee","impliedFormat":99},{"version":"2c0b6ecefb1757ad6ce6d2414b3c268cb9092cff307f6794c75e27f7b1b3b358","impliedFormat":99},{"version":"10f2d3d9c642a1e734adad62f07866c3217b1f5f89f9f3f47d3fd6a295bef8d9","impliedFormat":99},{"version":"4894b371550ff623685722b8fc3064eec2a1353d0a4f7a4f46707b446d3cd607","impliedFormat":99},{"version":"1306e750b3d9089eddb926279035e88119bff73ba6d12bcd1ac3907eef2bc235","impliedFormat":99},{"version":"f97a6ead0f9c40bd2bf83cdae4aa47655aea300282afd44998180d9cf62b7d76","impliedFormat":99},{"version":"a5d1b10c57d14126c7a312e2bb28763c745a87c4f5c005ec6b9ad79aa16850ae","impliedFormat":99},{"version":"33b25938d78614b92778d270bde2c971ecd7f369ef6c990d13e247eb2c7882ad","impliedFormat":99},{"version":"ef325176a8c255a9a55bec7c23d6289c98b00ebfd67112bd4b915c679d721b1d","impliedFormat":99},{"version":"81c53ecba335a74d626d838f842eaa7cf84289776b413356bcd61560f07c9e51","impliedFormat":99},{"version":"47a102af679f68d61d7754dd8f133fab95c3cf7c2dc6674e70efd1b7b7c815ce","impliedFormat":99},{"version":"41931aa72d208139935cd9537c83679c6ca91fe69fd1bd8ec88bbe8e93fe2a2e","impliedFormat":99},{"version":"793014c8196d3044e2d425d7de6339e130a8f81136c5876e2e88e1b8279dbea5","impliedFormat":99},{"version":"7cfcb8c758cb502085d9128ba3ff999084cafd64b9b7c317f87fc85170d1282c","impliedFormat":99},{"version":"6d0b4092bc1fdd4843d9a4313b470b6b7773b5fb712545d6174fd4d703265ae3","impliedFormat":99},{"version":"b6e63e92b837071df07652d3f5632727214b713881c8416a133f7afa977d6e44","impliedFormat":99},{"version":"b09eefc95f09a9a9c61e4f2ed3c1e22069c703493f8e231bc76d3c6cf7ab1422","impliedFormat":99},{"version":"1b08cc33d8dedfa20adf4a779020ec36d494ab7ea503395c658c5e286a3b50e1","impliedFormat":99},{"version":"2c94d2217244dd31275ca5e404560c5c2105b5f06f8985d0f039f39caa1e9e30","impliedFormat":99},{"version":"e77d73e81f5824767db0df8bdb7b7c72cb1741d5fd74e099bfaa6369b8716491","impliedFormat":99},{"version":"077f88e83a13ea89300c26c39ca8ec3708bfd09a15464c71e2fd762708fbecea","impliedFormat":99},{"version":"fed7941cd69554080ce4abb7f4422dcca93a6021f27cc8d2986875ee2c6bb39e","impliedFormat":99},{"version":"774a6eb661aad32b5f9b73bf9d08af7f9204a4bc67a583494caf43fa09fb2af9","impliedFormat":99},{"version":"0c3d92e5fac4a662f12b04c12ca741b4f34a1bf2824a470414322ddb61b350bb","impliedFormat":99},{"version":"b2035f5bb429bbd282a7c947e80bd344ee948a4f7042a019271d3bd114d09afa","impliedFormat":99},{"version":"a4c05674bf833557741ebbcc3fa4a25ddd052e1c762a91037db94a2d7688ebbe","impliedFormat":99},{"version":"97006e8ef367bc2388ea292d7eb6cf471bd2b53d266642ef8284b66afe6d01bf","impliedFormat":99},{"version":"927c282fb88ca5c95a9aaea00175b56b0a6dc8728b246c9e0ec455de40d54fe9","impliedFormat":99},{"version":"7dac0babb2b88108fc5a5410aaf6fe6e59ef1e158e3b5283a990ea503ac3ea07","impliedFormat":99},{"version":"2f4620eeb98e884074a0ba924445a69b29654da70dcff2e3f84f58092741e6f7","impliedFormat":99},{"version":"669f1bc764b5fd840e483e123909aefdab8dc82c9aeadae194d75129fbba0b53","impliedFormat":99},{"version":"23769d3f0549de671a5ec795a8c2c6c4fd80c22af92348daa349073b65355fe8","impliedFormat":99},{"version":"774182677ec80bd5e58d287833ec21e02bcfccfdd20247780375691f201a0263","impliedFormat":99},{"version":"66bc333acb797d6c079d8830ef73bd4a34c4e1709e6a9d40bc20d5b92141d50d","impliedFormat":99},{"version":"92fe8e78ad5d9f9eca627b40cbc1fcfc095a230aba0911101d1c74f3ef77c32c","impliedFormat":99},{"version":"78dc76bd9afff09e5cea823d477657d25be9b0e1da7582df7df6e713abc6b7e0","impliedFormat":99},{"version":"651be876b03c705ac06a8cddd6ea90f635855d6312702a441e37b710d4291387","impliedFormat":99},{"version":"ae2d7f7735a249142233a5b9624baf7d1076c1b8e30bf6f9ed1f1f96641d368c","impliedFormat":99},{"version":"667514a0b45b9137a724df446646e49c6c5ff8e6c52b51faf7e7d97c07876339","impliedFormat":99},{"version":"dab36fd79d1c298c9252b588fff62da099aebd9b0987650f0268f0e580a6b8ea","impliedFormat":99},{"version":"24e34d276c6b1d5b38adc1a02ebca9d211154478cf875a3d175c12bd2473180d","impliedFormat":99},{"version":"2e7a903e7bad7e1053fd16172349f653b8e1f04b118fa6a0c94f03a05ce66ee8","impliedFormat":99},{"version":"a2b7e8befa2bec65d655a9205fe6ba2c6c313e8af050e0ec6878d667368b0370","impliedFormat":99},{"version":"1fdae95c93d7a258da7b02e50e7c4f4eb2041c4e8c86819c6bab35f1d3b33825","impliedFormat":99},{"version":"8c1eacab9cdc1a87e6142c96994b4679c81c9445557e14fdf3210352b7586209","impliedFormat":99},{"version":"05c811914dd75723518d9e11ab4e406bf49ea23b21ca4b0c014f529630cd6bfc","impliedFormat":99},{"version":"cfb373d580586808b221e2ec5555e381569bbcd65ed91abf6467235796e4011f","impliedFormat":99},{"version":"c01126bfe2cf99a27503a39cb9bd923760f43179322d9ee06640c2e329e00916","impliedFormat":99},{"version":"2b6cf5f7e4b87a9d212aa0b043a4e0203f75aa7a505457838c5c12ef3c5cccf8","impliedFormat":99},{"version":"e9214291673a507e06de72638d08cb77a5a83946ff371fe3118231fd14b66148","impliedFormat":99},{"version":"09949a01affc32445e16e0360269d2a7c3f925c2e67789934fbbaf5658669765","impliedFormat":99},{"version":"03d14a43dbd73bfea94b21aba29128157d81858018a543db71f27cd369903ca6","impliedFormat":99},{"version":"fbed6c54bdcfe0fe3fef66a9998b281f924c1c54813ad24af237cea18fab6f1e","impliedFormat":99},{"version":"469121354c88f9bcc57322407eb5d0d5b8a96cfeb9192494e8b46b2688126e2a","impliedFormat":99},{"version":"e75861934b956453abb77723352171ca00f00ab55e34502eedfe74fba7c6449f","impliedFormat":99},{"version":"450c3dc5526f8e73bba30f955e9e35e42076f82559e4f3ca733e30a99f608fb6","impliedFormat":99},{"version":"840457a9dca7071b074b79ec4bbb07e26daa4899b1939b3bfdffadca62fb157f","impliedFormat":99},{"version":"0bb96d1b7886f8348ee457c22db99c258f563e6e4371410c8c0137c54f8b6332","impliedFormat":99},{"version":"107dec9919e26cd898658841caac2186b3b10ca2e81ba0ecc9407ac989b0b860","impliedFormat":99},{"version":"a6f32c6ebdf43913196c351ed0152695f0d76dbe8226002e2d6654835e0cb685","impliedFormat":99},{"version":"8560d14dc193327f1792881dc467e70e73a74c0623d68adab861f0848619e6ea","impliedFormat":99},{"version":"469827f7ebcc9bea7ea42913b61d3570de20b823081cfa369e37f32f9b6578ce","impliedFormat":99},{"version":"d225636174c86016bb4902443c3cefb17ac3ad480aed999676848dc74df78751","impliedFormat":99},{"version":"ee10a6b8d4948616a923e953b40dd564d87f4c6c960353a4ab40f9ac5953508a","impliedFormat":99},{"version":"616f4301604d5263a177d9d378a417940ee51f4661dc970c446265139b3dc2d7","impliedFormat":99},{"version":"cc8621f4a86f09a9d63af2008516e3284fa8dee2da7ac3e010a7a344267e9fb9","impliedFormat":99},{"version":"318a5c102f218073bb58800a24742df255fef6b4b8b3ad82a0ce2169983331b4","impliedFormat":99},{"version":"88773471c8c8e81e190a0821424744c512320799e0e6596258751e8e7b573b02","impliedFormat":99},{"version":"bdd14f07b4eca0b4b5203b85b8dbc4d084c749fa590bee5ea613e1641dcd3b29","impliedFormat":99},{"version":"b535fbac200d9abb725ff62adc9009598cbac617cf4c94c0111e37b956a51076","impliedFormat":99},{"version":"fe430799297f16036bee1fcf981c81b9c560f339dc87d703102f19347bca922c","impliedFormat":99},{"version":"8eb0d9018c369133b17adcb4eddc1bca329d3be6f82f8e00373b46315ba5a67d","impliedFormat":99},{"version":"dd89def02d5b8071facad4869deb42b2c3284880502ea4f46596606f380bcfef","impliedFormat":99},{"version":"82d5a2d180f3acffab13d4073bfc304cf30b551dd0b8c398f501a5547d0971e9","impliedFormat":99},{"version":"16dce5a31e379fc832306def780d9c7a66b611c9d5680c1d50f3872278afa987","impliedFormat":99},{"version":"8206bc99be58c7e6e541f75ba3ea281bc2e98630025e05eacf121a0312e94d2d","impliedFormat":99},{"version":"4504027d521a810f190f5a1fb6b6235db3e4fcfe4e383361e849157fd4017cc3","impliedFormat":99},{"version":"6cc22fc095dd4bfc296a2abddacf950b999c5ea21032c147d992c1e3bc9f2151","impliedFormat":99},{"version":"107fe9df1e4aa36cdb51032b4f51fe26eacbed9f9452c05b0ed145afb91dafbc","impliedFormat":99},{"version":"6f912d1d946525532c3537fecc0bffe9b2b25fc6b95087d00191b56403c64b02","impliedFormat":99},{"version":"d8d5640bc8ce5f5352324ebf9772c8052948e993f26b41267f78bb0570090367","impliedFormat":99},{"version":"399194d25a36f88cf475f80ae64aaab76f3c404055b582538aeb7888fbfb7aaf","impliedFormat":99},{"version":"334b8c2139f3f2b14517c68ad6e847669a58327d0401fb134480dda0826b21c6","impliedFormat":99},{"version":"646ef0e6b8a5dda70fea93b94c589512cf58b5a4f13ea8f2ad20b63a0207be8b","impliedFormat":99},{"version":"d51e1af504216f68350d001baef42b14de9aa2f8e287c9ff35aacb6c67460947","impliedFormat":99},{"version":"ab6dd1e3fdcc478cb81caef96c7047c2ec0811480f86f71be363baad015e4cd5","impliedFormat":99},{"version":"767cfb1496df590e9132052dbf9d5a2ed4d7864823eafb4eafcc5f9569f3c152","impliedFormat":99},{"version":"b9f9f77327ef54fc055ab78fc6fc6c93b88410621fd9b2374bb956022018a62a","impliedFormat":99},{"version":"82147ff6ea10d9182855cb50ed6f28d36c1d4f54a792ec2173d42074225caa5d","impliedFormat":99},{"version":"aa015d746ff96c12818a7ad742e45f40bc6f72f4b7df2af0f7200dacfe0ca65e","impliedFormat":99},{"version":"01a6da71fc05a9195f334ab1092b3f4952b0f2e2c460ea55d206ef629309062d","impliedFormat":99},{"version":"66f0b814ca2e007985a604145f5d70793a9f66d2fc7d6c57b2e094c15b7eb83f","impliedFormat":99},{"version":"8d9b0d78ee11670d4caa429ce908ef681663d65e94dc1507b47d6eb44abf903d","impliedFormat":99},{"version":"888cd85dc7c3d5d1fe0c975a8a42387203a6040f6945d9e834044f4cdcaa7177","impliedFormat":99},{"version":"87783efceaa85da75752e808792e347efda30d7aa7d7a76e62398df4a86c31e5","impliedFormat":99},{"version":"6a8ab860d18807ef3ebcbba3c436ae6432640a8e9fd3b3e965d3b923d7ab1043","impliedFormat":99},{"version":"41560bfeef3407476ecbf1e87fe10ad8bb66493854c8810bf1475c1927e9509c","impliedFormat":99},{"version":"92a57d4092fa036a6df221404a9263bb67c267aff1d0a9fc6912c720d06d5b2e","impliedFormat":99},{"version":"dbfa5e96e7827fcd26ad524c28d62238de9e58cadc28f347038aed618fdc7a7b","impliedFormat":99},{"version":"596bf741b2e8a7df8738e9210fc36bde96c4006d5737f11fad33c7e366ae40de","impliedFormat":99},{"version":"735ce9068435b7cddb5620d2f4dfa24b1d20914bbf1fb0cdae15e8c9fe946708","impliedFormat":99},{"version":"e1ee6eaef7fb5f2aeaecec5af531fde8e953f1e38b808a31edb1ed667433504a","impliedFormat":99},{"version":"5ee7350ef93ccadb9f50d4faff03ca3287a20daf94457baa7a06b1caccf9895e","impliedFormat":99},{"version":"e1faebf35150fefc223425ebfabb27ea35c8501e8a6cd886349ad4c224599228","impliedFormat":99},{"version":"d96d274e5284eba9927f58c163c0f76f9b42300b2acf690b088ba874bb7c7713","impliedFormat":99},{"version":"d1cb52c406810e8a61028dbddc1c76329a57641f274d85d923d031d6db5b698c","impliedFormat":99},{"version":"44778cc47965a17ca3ddc25b38b477c05412ebd7a11a2400a957de1449cdb5d6","impliedFormat":99},{"version":"090d14278c4f8bb64425275a537241ee6c2423eab2044305f324b7af9d27c539","impliedFormat":99},{"version":"64a821c22015703e99902dfb093bd189db8682b8f59245ee215c335a78b75a3b","impliedFormat":99},{"version":"20d994898c03bab9c60f1d2b8589cd85dbbb4ce8d01fbf13c9695f1b955be534","impliedFormat":99},{"version":"a1a34cf5ba4eea15a74e5333b365586e4054c972d5f1c86ea31d766fac26d22d","impliedFormat":99},{"version":"01541bebe87f69a3eb400ef26ce82339dee293774bf97a12a7d6e0a94b18d07b","impliedFormat":99},{"version":"74aac6d7662936f593c50c8cae13fbf2be2adc6b0f917f949a682e0ba7ba1036","impliedFormat":99},{"version":"6bafdd64b01c1e66f791cdafbd86ff49abf8580d65ae9cee39c2a01918ad2a72","impliedFormat":99},{"version":"343d92ebaf6b63a7a174fb434dd6123f9efc1c27a0e5e39b3c3a6d0bd571cef0","impliedFormat":99},{"version":"fd8a7776f01ed84c7299ecd56635a7c3a977bd2694b2fe1cf98c0e1d8938b7f2","impliedFormat":99},{"version":"5c58006a7bde1813224dc715df836b7f23d12440b65ab1eeaea20ba573b823b8","impliedFormat":99},{"version":"a6668aee0652306f94268fdb12eab4026eb77f177f48496a9df6ff4f2cc50706","impliedFormat":99},{"version":"d0b217bc167cb6d1873c1aedc1891c68c42a7ec55317bff8c52f60f33341021d","impliedFormat":99},{"version":"87547e2163fb58f4ec906f5c81a879e14d5079fb15d74839f841f458fc9caa90","impliedFormat":99},{"version":"db2ab0896701b2cd55281af7132029104263bee659904d65685c33f436bb1bbf","impliedFormat":99},{"version":"357484e7b33f0366b4d918e5623ea38c42e1b79093cf57b37fdd129f08f747b6","impliedFormat":99},{"version":"0ceaf0c8182145c000c6d6ac87d6293bfadc924a893de45515660f4459d29bc6","impliedFormat":99},{"version":"5adfe7cda513ac8ab76c9a42f5fe7dbfcc5f162ad8c0a65275f9dd6c8ed51e3b","impliedFormat":99},{"version":"df75e076d5e167de6a985cc2487779bf5e551430dcdf3b43aa7168391c42e7d3","impliedFormat":99},{"version":"3b794c3c657b91712aef51da5d1f1fa8e96278b7e0068b67c1e4e32dd39052e2","impliedFormat":99},{"version":"e5d521e00e91df030e7ef2db79294f97c3258f213cebac04fb1d7e545e036c92","impliedFormat":99},{"version":"a9c2beeda1479ab211a437d27da7170170f00c080dc0534a234e0f0bfd80d0e4","impliedFormat":99},{"version":"65186b1f46c6306bc4375f2ddf5e17b77213a5e9e0a701bcd901b1d2e46d01a6","impliedFormat":99},{"version":"44bce55aaf52d5b55fd0221a159717679b2b8a1965a31cbb282068c088fc14ec","impliedFormat":99},{"version":"a2577da4ebf625ac5e8d015e1b85a534f324aa42073e5de539b526c1b6ac9368","impliedFormat":99},{"version":"4f1653400fb4ef5c777b5a4ec0b5ba0367129d171259b942f30133bea639175a","impliedFormat":99},{"version":"2c2ea93c9df7a231c01de9ca95b681439ce7e008573432b8a37a634dbd3cd1f8","impliedFormat":99},{"version":"6b6859f495bcd50ae254bd5c0348ce161ab9dc64f315cc96a235c88027b255d5","impliedFormat":99},{"version":"2c0e2a9291d2ae42225fb79483e7ec328823bf9f1fb5d7c7cdae5761e66b91a9","impliedFormat":99},{"version":"fc518fafcc0f671ca4362205bce0d54efc6463b97e667fc1cfaa7e259766a4b2","impliedFormat":99},{"version":"3cdbe580c6d90224bc0bb9cc59101b786773d795a295818eeffad9e9e410618e","impliedFormat":99},{"version":"f13a0aad613324ade17df982376b78e0807cce490ddde44098e64a86be7408f0","impliedFormat":99},{"version":"55af2e7ea5c20ea6b77c6798aab8e6a6ba1af8f03b3ad5699f73e3432904473a","impliedFormat":99},{"version":"ca52b3f0d81184e1aa8ffc3c89e8f85cc0116461b9035bc7f0a137b40a1bf600","impliedFormat":99},{"version":"26a021aeb112c6b6a2f84cb8298586e7b88c82b2aaee38d5fed488b554768507","impliedFormat":99},{"version":"dc72c2a3d5a60b487b2afdee0fd30522c2ab395d5e95a9c61f20533c9e2052b9","impliedFormat":99},{"version":"a8fa80829673c4fe4be9c6bdd2375c74db979b85082001413bbfaa159253b84a","impliedFormat":99},{"version":"38f3aa240e10764cecc0dbb93d103740124aa6086d23d2dd242b27153a5e4954","impliedFormat":99},{"version":"11d819e69caa33068ad0ddb9424d7afc1c39abb48d342e3e1df7700bbffb8b88","impliedFormat":99},{"version":"d8bc0c5487582c6d887c32c92d8b4ffb23310146fcb1d82adf4b15c77f57c4ac","impliedFormat":1},{"version":"8cb31102790372bebfd78dd56d6752913b0f3e2cefbeb08375acd9f5ba737155","impliedFormat":1},{"version":"c6a7b7568c1e9ccd661e7e9a2219f3d41805d79f79d19687de1904edf975e7d5","impliedFormat":99},{"version":"d131d83d3a4f6a640c18169c05957ff4e96264b48fd5d4ef2a87aab682877488","impliedFormat":99},{"version":"e8929e15b0295c54e6a5d0a7f9516585de299b116a5efaefe57dfbe08167cf23","impliedFormat":99},{"version":"e2ffff11859a3c055c87837df65f1c079ca0baf72f7010a99b20229fa658e6fb","impliedFormat":99},{"version":"d25d1708b5c923d5f7b94d183dc7f746f921580b0f2a4fdd4e623882b98642ee","impliedFormat":99},{"version":"12d19496f25ecd6afef2094be494b3b0ae12c02bd631901f6da760c7540a5ec1","impliedFormat":1},{"version":"c6fe327c538417b8dd5b9bb32abcd7911534b10da3a4514f3445cdb28cf3abf2","impliedFormat":99},{"version":"0065cdb7ac9f5b19921632de63f888ec2cc11ad57f7fc868f44bf0faad2fce3e","impliedFormat":99},{"version":"8c1adc3171d0287f3a26f4891a7d1834c89999573a9b444aa5ff519dcc43a2b7","impliedFormat":99},{"version":"27aee784c447854a4719f11058579e49f08faa70d06d8e30abe00f5e25538de6","impliedFormat":99},{"version":"fbc610f9dde70f0bbea39eefec2e31ca1d99f715e9c71fb118bd2306a832bcb5","impliedFormat":99},{"version":"a829052855dca3affb8e2ef0afa0f013b03fa9b55762348b1fba76d9c2741c99","impliedFormat":99},{"version":"1d61288b34b2dd2029b85bc70fabbb1da90c2a370396d5df5f620e62eb47ddbe","impliedFormat":99},{"version":"5a2cf4cd852a58131b320da62269b2143850920ce27e8fdec41fed5c2c54ec95","impliedFormat":99},{"version":"43552100e757fad5a9bb5dabc0ea24ba3b6f2632eb1a4be8915da39d65e83e1c","impliedFormat":99},{"version":"6a99940a8a76a1aa20ae6f2afd8e909e47e0b17df939e7cf5a585171480655ff","impliedFormat":99},{"version":"043195af0b52aadd10713870dd60369df0377ed153104b26e6bac1213b19f63e","impliedFormat":99},{"version":"ad17a36132569045ab97c8e5badf8febb556011a8ed7b2776ff823967d6d5aca","impliedFormat":99},{"version":"698d2b22251dbbfc0735e2d6ed350addead9ad031fac48b8bb316e0103d865db","impliedFormat":99},{"version":"7298d28b75c52e89c0b3e5681eac19e14480132cd30baaba5e5ca10211a740ef","impliedFormat":99},{"version":"ff10facf373a13d2864ff4de38c4892d74be27d9c6468dac49c08adabbf9b0eb","impliedFormat":99},{"version":"97b1cf4599cc3bc2e84b997aa1af60d91ca489d96bea0e20aaff0e52a5504b29","impliedFormat":99},{"version":"853dfbcd0999d3edc6be547d83dc0e0d75bf44530365b9583e75519d35984c35","impliedFormat":99},{"version":"9c80bed388d4ed47080423402db9cb1b35a31449045a83a0487f4dfde3d9d747","impliedFormat":99},{"version":"f29bc6a122a4a26c4e23289daae3aa845a18af10da90989cb8b51987e962b7be","impliedFormat":99},{"version":"3a1f39e098971c10633a064bd7a5dbdec464fcf3864300772763c16aa24457f9","impliedFormat":99},{"version":"20e614d6e045d687c3f7d707561b7655ad6177e859afc0c55649b7e346704c77","impliedFormat":99},{"version":"aa0ae1910ba709bc9db460bdc89a6a24d262be1fbea99451bedac8cbbc5fb0cd","impliedFormat":99},{"version":"161d113c2a8b8484de2916480c7ba505c81633d201200d12678f7f91b7a086f0","impliedFormat":99},{"version":"b998a57d4f43e32ac50a1a11f4505e1d7f71c3b87f155c140debe40df10386c8","impliedFormat":99},{"version":"5710e8ed9797ae0042e815eb8f87df2956cb1bf912939c9b98eeb58494a63c13","impliedFormat":99},{"version":"a6bb421dccfec767dbd3e99180b24c07c4a216c0fd549f54a3313f6ce3f9d2c7","impliedFormat":99},{"version":"3b6f1be46f573b1c1f3e6cd949890bfb96b40ff90b6f313e425a379c1c4d5d77","impliedFormat":99},{"version":"28a2c54d0a78d32c29f7279ca04dc6c7860c008579e4e3033938c0ed0201eb9a","impliedFormat":99},{"version":"c2714a402843287624210a47ebea2b1c8dd3ad1438f448633f6831e31eaf37b8","impliedFormat":99},{"version":"b89945ec6707415d739f3e76f2820982d4927dc6b681910b3c433b5ad261b817","impliedFormat":99},{"version":"a72d5822fb2a2c1ef985b30aed889f4c00342c90e12318762fccc550c6a599cf","impliedFormat":99},{"version":"c8616ab60eda93ca87fbb20aada1d6a6cdbcd2cb181a70a2d7728a3cb0613391","impliedFormat":99},{"version":"eeddfd3e0b09890822068de5248d38144f8328e74b5292847eb4e558d8aba8cb","impliedFormat":99},{"version":"d4dc0b6592543314c8549c71e35ad2ec4a57904662d905ff9585836bde1c855a","impliedFormat":99},{"version":"56e1687a174cd10912a35a4676af434bb213aafa5d4371040986c578afe644ab","impliedFormat":99},{"version":"470c280cc484340b97d0942e0c3aa312399eba3849ceb95312d0d7413bac7458","impliedFormat":99},{"version":"ae183f4a6300aad2be92cdbd4dd12d8bcd36eddf8dd1846f998c237235fe0c33","impliedFormat":99},{"version":"4b0eeffddaf51b967e95926a825a6ba1205b81b3a8fecddbe21eaf0e86bdee91","impliedFormat":99},{"version":"bf3ec0d42e33e487c359a989b30e1c9e90fa06de484dc4751e93fb34a9b5cf90","impliedFormat":99},{"version":"7b9656a61d83df1a46c38c2984dbf96dd057bf48f477ddf3f8990311ab98ec23","impliedFormat":99},{"version":"366b85ddb698f3a035e0caa68dc9fef39a85c4368c0810eaf937c3a3c63ac31e","impliedFormat":99},{"version":"d440ee730bc60a5c605903842e398863e7ecdb7a91fc32a9152f14061bf6cc17","impliedFormat":99},{"version":"a12c86c4a691608d19a75320946c80bbce38bb62c091dda32572aee7158edd38","impliedFormat":99},{"version":"3109cb3f8ab0308d2944c26742b6a8a02b4a4ffc23f479a81f0e945d6a6721dd","impliedFormat":99},{"version":"a2289c12a987f2a06f4cf049afde4fdc9455a4af37913445148865938c6eb613","impliedFormat":99},{"version":"55933c1450edcfaf166429425dbbad0a27c0ae8672d5ab5d427e46946a6f2f63","impliedFormat":99},{"version":"6c684fda6998db4112e82367c9e82e27996dc8086a10d58ac9b51d89770d5f9d","impliedFormat":99},{"version":"5c4b4dd983471fcaed17ad3241c98a1f880729f1ca579ddbcdae7e0bf04035df","impliedFormat":99},{"version":"9e430429c7e9e70071a836ac91a1bf6e6651f91d47d9f4baf0a92eefc6130818","impliedFormat":99},{"version":"b3db7f6d7ef72669dc83fa1ff7b90a2ec31d1d8f82778f2a00ef6d101f5247e5","impliedFormat":99},{"version":"354f61bd2a5acaf20462bc4d61048aa25f8fc0dd04dfe3d2f30bdbabbab54e7d","impliedFormat":99},{"version":"d51756340928e549f076c832d7bc2b4180385597b0b4daaa50e422bed53e1a72","impliedFormat":99},{"version":"32c6e3ef96f2bcbc1db7d7f891459241657633aa663cab6812fb28ade7c90608","impliedFormat":99},{"version":"ac2ea00eb8f73665842e57e729e14c6d3feabe9859dc5e87a1ed451b20b889e4","impliedFormat":99},{"version":"730cb342a128f5a8a036ffbd6dbc1135b623ce2100cefe1e1817bb8845bc7100","impliedFormat":99},{"version":"78e387f16df573a98dd51b3c86d023ddbd5bf68e510711a9fee8340e7ccc3703","impliedFormat":99},{"version":"e2381c64702025b4d57b005e94ed0b994b5592488d76f1e5f67f59d1860ebb70","impliedFormat":99},{"version":"d7dfcb039ff9cff38ccd48d2cc1ba95ca45c316670eddbcf81784e21b7128692","impliedFormat":99},{"version":"acaf0a60eb243938f7742df08bf5d52482fbea033fd27141ee3a6d878bbb0d3d","impliedFormat":99},{"version":"fb89aeecfc8eb28f5677c2c89bced74d13442b7f4ebd01ce2ce92127d1b36d69","impliedFormat":99},{"version":"9e91cb0a5bd7aefa2b94a2872828d6d2321df0ca44412e74d99e8b94e579b7d8","impliedFormat":99},{"version":"3e4f06b464ef1654b91be02777d1773ccc5d43b53c1c8b0a9794ec224cfe8928","impliedFormat":99},{"version":"192c1a207b44af476190ae66920636de5d56c33b57206bbc2421adc23f673e2e","impliedFormat":99},{"version":"e5aa35b3740170492e06e60989d35a222cfda2148507c650ea55753f726c9213","impliedFormat":99},{"version":"057aa42f6983120c35373aed62b219ffcbd7b476b2df08709139a9eb8dfeed26","impliedFormat":99},{"version":"95a0c46b4675d4d02de6a7c167738f1176b53b26ebec9ccfe8e5d9acb0dc7aee","impliedFormat":99},{"version":"94ad4d9745811c482ae3bad61e5b206e0904f77e0dacf783199193a3df9f6ce6","impliedFormat":99},{"version":"407dc18ecd25802296fade17be81d0d4f499ae75fe88ed132f94e7efdad269e2","impliedFormat":99},{"version":"77dabe31d44c48782c529d5c9acddc41f799bf9b424b259596131efc77355478","impliedFormat":99},{"version":"f6dfe21d867aa5e13bc53d536b69b66427f571707a01e7c3604dc51ded097313","impliedFormat":99},{"version":"4ecd02d0e4ccf7befb9c28802c6c208060e33291d56fd1868900ca295c399077","impliedFormat":99},{"version":"37ada75be4b3f6b888f538091020d81b2a0ad721dc42734f70f639fa4703a5c8","impliedFormat":99},{"version":"aa73ff0024d5434a3e87ea2824f6faece7aad7b9f6c22bd399268241ca051dc7","impliedFormat":99},{"version":"4c9fb50b0697756bab3e4095f28839cf5b55430a4744d2ebbaf850ec8dca54d8","impliedFormat":99},{"version":"782868b723c055c5612c4a243f72a78a8b3c0c3b707ae04954e36e8ab966df4c","impliedFormat":99},{"version":"3de9d9ad4876972e7599fc0b3bddb0fddb1923be75787480a599045a30f14292","impliedFormat":99},{"version":"0f4b3c05937bbdb9cf954722ddc97cd72624e3b810f6f2cf4be334adb1796ec1","impliedFormat":99},{"version":"9fc243c4c87d8560348501080341e923be2e70bf7b5e09a1b26c585d97ae8535","impliedFormat":99},{"version":"4f97089fe15655ae448c9d005bb9a87cc4e599b155edc9e115738c87aa788464","impliedFormat":99},{"version":"f948d562d0a8085f1bd17b50798d5032529a75c147f40adfeb4fd3e453368643","impliedFormat":99},{"version":"22929f9874783b059156ee3cfa864d6f718e1abf9c139f298a037ae0274186f6","impliedFormat":99},{"version":"c72a7c316459b2e872ca4a9aca36cc05d1354798cee10077c57ff34a34440ac2","impliedFormat":99},{"version":"3e5bbf8893b975875f5325ebf790ab1ab38a4173f295ffea2ed1f108d9b1512c","impliedFormat":99},{"version":"9e4a38448c1d26d4503cf408cc96f81b7440a3f0a95d2741df2459fe29807f67","impliedFormat":99},{"version":"84124d21216da35986f92d4d7d1192ca54620baeca32b267d6d7f08b5db00df9","impliedFormat":99},{"version":"efba354914a2dc1056a55510188b6ced85ead18c5d10cc8a767b534e2db4b11b","impliedFormat":99},{"version":"25f5bf39f0785a2976d0af5ac02f5c18ca759cde62bc48dd1d0d99871d9ad86f","impliedFormat":99},{"version":"e711fa7718a2060058ff98ac6bff494c1615b9d42c4f03aa9c8270bc34927164","impliedFormat":99},{"version":"e324b2143fa6e32fac37ed9021b88815e181b045a9f17dbb555b72d55e47cdc1","impliedFormat":99},{"version":"3e90ea83e3803a3da248229e3027a01428c3b3de0f3029f86c121dc76c5cdcc2","impliedFormat":99},{"version":"9368c3e26559a30ad3431d461f3e1b9060ab1d59413f9576e37e19aaf2458041","impliedFormat":99},{"version":"915e5bb8e0e5e65f1dc5f5f36b53872ffcdcaef53903e1c5db7338ea0d57587a","impliedFormat":99},{"version":"92cf986f065f18496f7fcb4f135bff8692588c5973e6c270d523191ef13525ad","impliedFormat":99},{"version":"652f2bd447e7135918bc14c74b964e5fe48f0ba10ff05e96ed325c45ac2e65fb","impliedFormat":99},{"version":"cc2156d0ec0f00ff121ce1a91e23bd2f35b5ab310129ad9f920ddaf1a18c2a4d","impliedFormat":99},{"version":"7b371e5d6e44e49b5c4ff88312ae00e11ab798cfcdd629dee13edc97f32133d8","impliedFormat":99},{"version":"e9166dab89930e97bb2ce6fc18bcc328de1287b1d6e42c2349a0f136fc1f73e6","impliedFormat":99},{"version":"6dc0813d9091dfaed7d19df0c5a079ee72e0248ce5e412562c5633913900be25","impliedFormat":99},{"version":"e704c601079399b3f2ec4acdfc4c761f5fe42f533feaaab7d2c1c1528248ca3e","impliedFormat":99},{"version":"49104d28daa32b15716179e61d76b343635c40763d75fe11369f681a8346b976","impliedFormat":99},{"version":"04cd3418706b1851d2c1d394644775626529c23e615a829b8abfe26ec0ee3aef","impliedFormat":99},{"version":"21e459e9485fc48f21708d946c102e4aaa4a87b4c9ad178e1c5667e3ff6bbc59","impliedFormat":99},{"version":"97e685ac984fc93dcdae6c24f733a7a466274c103fdcf5d3b028eaa9245f59d6","impliedFormat":99},{"version":"68526ea8f3bbf75a95f63a3629bebe3eb8a8d2f81af790ce40bc6aad352a0c12","impliedFormat":99},{"version":"bcab57f5fe8791f2576249dfcc21a688ecf2a5929348cfe94bf3eb152cff8205","impliedFormat":99},{"version":"b5428f35f4ebf7ea46652b0158181d9c709e40a0182e93034b291a9dc53718d8","impliedFormat":99},{"version":"0afcd28553038bca2db622646c1e7fcf3fb6a1c4d3b919ef205a6014edeeae0f","impliedFormat":99},{"version":"ee016606dd83ceedc6340f36c9873fbc319a864948bc88837e71bd3b99fdb4f6","impliedFormat":99},{"version":"0e09ffe659fdd2e452e1cbe4159a51059ae4b2de7c9a02227553f69b82303234","impliedFormat":99},{"version":"4126cb6e6864f09ca50c23a6986f74e8744e6216f08c0e1fe91ab16260dab248","impliedFormat":99},{"version":"4927dba9193c224e56aa3e71474d17623d78a236d58711d8f517322bd752b320","impliedFormat":99},{"version":"3d3f189177511d1452e7095471e3e7854b8c44d94443485dc21f6599c2161921","impliedFormat":99},{"version":"bb0519ff5ef245bbf829d51ad1f90002de702b536691f25334136864be259ec5","impliedFormat":99},{"version":"a64e28f2333ea0324632cf81fd73dc0f7090525547a76308cb1dfe5dab96596a","impliedFormat":99},{"version":"883f9faa0229f5d114f8c89dadd186d0bdf60bdafe94d67d886e0e3b81a3372e","impliedFormat":99},{"version":"d204b9ae964f73721d593e97c54fc55f7fd67de826ce9e9f14b1e762190f23d1","impliedFormat":99},{"version":"91830d20b424859e5582a141efe9a799dc520b5cce17d61b579fb053c9a6cd85","impliedFormat":99},{"version":"68115cdc58303bad32e2b6d59e821ccaada2c3fb63f964df7bd4b2ebd6735e80","impliedFormat":99},{"version":"ee27e47098f1d0955c8a70a50ab89eb0d033d28c5f2d76e071d8f52a804afe07","impliedFormat":99},{"version":"7957b11f126c6af955dc2e08a1288013260f9ec2776ff8cc69045270643bf43e","impliedFormat":99},{"version":"d010efe139c8bb78497dc7185dddbbcefc84d3059b5d8549c26221257818a961","impliedFormat":99},{"version":"85059ed9b6605d92c753daf3a534855ba944be69ff1a12ab4eca28cefbabd07a","impliedFormat":99},{"version":"687208233ae7a969baa2d0c565c9f24eb4cb1e64d6cfb30f71afec9e929e58c2","impliedFormat":99},{"version":"ea68a96f4e2ba9ca97d557b7080fbdb7f6e6cf781bb6d2e084e54da2ac2bb36c","impliedFormat":99},{"version":"879de92d0104d490be2f9571face192664ec9b45e87afd3f024dbbf18afb4399","impliedFormat":99},{"version":"424df1d45a2602f93010cb92967dfe76c3fcadad77d59deb9ca9f7ab76995d40","impliedFormat":99},{"version":"21f96085ed19d415725c5a7d665de964f8283cacef43957de10bdd0333721cc4","impliedFormat":99},{"version":"e8d4da9e0859c6d41c4f1c3f4d0e70446554ba6a6ab91e470f01af6a2dcac9bf","impliedFormat":99},{"version":"2e2421a3eec7afefa5a1344a6852d6fee6304678e2d4ee5380b7805f0ac8b58a","impliedFormat":99},{"version":"a10fd5d76a2aaba572bec4143a35ff58912e81f107aa9e6d97f0cd11e4f12483","impliedFormat":99},{"version":"1215f54401c4af167783d0f88f5bfb2dcb6f0dacf48495607920229a84005538","impliedFormat":99},{"version":"476f8eb2ea60d8ad6b2e9a056fdda655b13fd891b73556b85ef0e2af4f764180","impliedFormat":99},{"version":"2fe93aef0ee58eaa1b22a9b93c8d8279fe94490160703e1aabeff026591f8300","impliedFormat":99},{"version":"bbb02e695c037f84947e56da3485bb0d0da9493ed005fa59e4b3c5bc6d448529","impliedFormat":99},{"version":"ba666b3ab51c8bc916c0cebc11a23f4afec6c504c767fd5f0228358f7d285322","impliedFormat":99},{"version":"c10972922d1887fe48ed1722e04ab963e85e1ac12263a167edef9b804a2af097","impliedFormat":99},{"version":"6efeacbd1759ea57a4c7264eb766c531ae0ab2c00385294be58bc5031ef43ad1","impliedFormat":99},{"version":"1c261f5504d0175be4f1b6b99f101f4c3a129a5a29fc768e65c52d6861ca5784","impliedFormat":99},{"version":"f0e69b5877b378d47cbac219992b851e2bbc0f7e3a3d3579d67496dabd341ec4","impliedFormat":99},{"version":"b5ea27f19a54feca5621f5ba36a51026128ea98e7777e5d47f08b79637527cf5","impliedFormat":99},{"version":"b54890769fa3c34ab3eb7e315b474f52d5237c86c35f17d59eb21541e7078f11","impliedFormat":99},{"version":"c133db4b6c17a96db7fa36607c59151dec1e5364d9444cbe15e8c0ea4943861e","impliedFormat":99},{"version":"3a0514f77606d399838431166a0da6dbd9f3c7914eae5bbfbd603e3b6a552959","impliedFormat":99},{"version":"fa568f8d605595e1cffbfca3e8c8c492cf88ae2c6ed151f6c64acf0f9e8c25d8","impliedFormat":99},{"version":"c76fb65cb2eb09a0ee91f02ff5b43a607b94a12c34d16d005b2c0afc62870766","impliedFormat":99},{"version":"cf7af60a0d4308a150df0ab01985aabb1128638df2c22dd81a2f5b74495a3e45","impliedFormat":99},{"version":"913bbf31f6b3a7388b0c92c39aec4e2b5dba6711bf3b04d065bd80c85b6da007","impliedFormat":99},{"version":"42d8c168ca861f0a5b3c4c1a91ff299f07e07c2dd31532cd586fd1ee7b5e3ae6","impliedFormat":99},{"version":"a29faa7cb35193109ec1777562ca52c72e7382ffe9916b26859b5874ad61ff29","impliedFormat":99},{"version":"15bdf2eeef95500ba9f1602896e288cb425e50462b77a07fa4ca23f1068abb21","impliedFormat":99},{"version":"452db58fd828ab87401f6cecc9a44e75fa40716cc4be80a6f66cf0a43c5a60cc","impliedFormat":99},{"version":"54592d0215a3fd239a6aa773b1e1a448dc598b7be6ce9554629cd006ee63a9d6","impliedFormat":99},{"version":"9ee28966bb038151e21e240234f81c6ba5be6fde90b07a9e57d4d84ae8bc030c","impliedFormat":99},{"version":"2fe1c1b2b8a41c22a4e44b0ac7316323d1627d8c72f3f898fa979e8b60d83753","impliedFormat":99},{"version":"956e43b28b5244b27fdb431a1737a90f68c042e162673769330947a8d727d399","impliedFormat":99},{"version":"92a2034da56c329a965c55fd7cffb31ccb293627c7295a114a2ccd19ab558d28","impliedFormat":99},{"version":"c1b7957cd42a98ab392ef9027565404e5826d290a2b3239a81fbac51970b2e63","impliedFormat":99},{"version":"4861ee34a633706bcbba4ea64216f52c82c0b972f3e790b14cf02202994d87c5","impliedFormat":99},{"version":"7af4e33f8b95528de005282d6cca852c48d293655dd7118ad3ce3d4e2790146f","impliedFormat":99},{"version":"df345b8d5bf736526fb45ae28992d043b2716838a128d73a47b18efffe90ffa7","impliedFormat":99},{"version":"d22c5b9861c5fc08ccd129b5fc3dcdc7536e053c0c1d463f3ab39820f751c923","impliedFormat":99},{"version":"dcc38f415a89780b34d827b45493d6dbadb05447d194feb4498172e508c416ac","impliedFormat":99},{"version":"7e917e3b599572a2dd9cfa58ff1f68fda9e659537c077a2c08380b2f2b14f523","impliedFormat":99},{"version":"20b108e922abd1c1966c3f7eb79e530d9ac2140e5f51bfa90f299ad5a3180cf9","impliedFormat":99},{"version":"2bc82315d4e4ed88dc470778e2351a11bc32d57e5141807e4cdb612727848740","impliedFormat":99},{"version":"e2dd1e90801b6cd63705f8e641e41efd1e65abd5fce082ef66d472ba1e7b531b","impliedFormat":99},{"version":"a3cb22545f99760ba147eec92816f8a96222fbb95d62e00706a4c0637176df28","impliedFormat":99},{"version":"287671a0fe52f3e017a58a7395fd8e00f1d7cd9af974a8c4b2baf35cfda63cfa","impliedFormat":99},{"version":"e2cdad7543a43a2fb6ed9b5928821558a03665d3632c95e3212094358ae5896b","impliedFormat":99},{"version":"326a980e72f7b9426be0805774c04838e95195b467bea2072189cefe708e9be7","impliedFormat":99},{"version":"e3588e9db86c6eaa572c313a23bf10f7f2f8370e62972996ac79b99da065acaa","impliedFormat":99},{"version":"1f4700278d1383d6b53ef1f5aecd88e84d1b7e77578761838ffac8e305655c29","impliedFormat":99},{"version":"6362a4854c52419f71f14d3fee88b3b434d1e89dcd58a970e9a82602c0fd707a","impliedFormat":99},{"version":"fb1cc1e09d57dfeb315875453a228948b904cbe1450aaf8fda396ff58364a740","impliedFormat":99},{"version":"50652ed03ea16011bb20e5fa5251301bb7e88c80a6bf0c2ea7ed469be353923b","impliedFormat":99},{"version":"d388e0c1c9a42d59ce88412d3f6ce111f63ce2ff558e0a3f84510092431dfee0","impliedFormat":99},{"version":"35ea0a1e995aef5ae19b1553548a793c76eb31bdf7fef30bc74656660c3a09c3","impliedFormat":99},{"version":"56f4ae4e34cbff1e4158ccada4feea68a357bae86adb3bedaa65260d0af579df","impliedFormat":99},{"version":"6eebdacf8e85b2cf70ad7a2f43ead1f8acccfd214ab57ff1d989e9e35661015d","impliedFormat":99},{"version":"a4f90a12cbfac13b45d256697ce70a6b4227790ca2bf3898ffd2359c19eab4eb","impliedFormat":99},{"version":"4a6c2ac831cff2d8fa846dfb010ee5f7afce3f1b9bd294298ee54fdc555f1161","impliedFormat":99},{"version":"a8d491b4eb728dab387933a518d9e1f32d5c9d5a5225ff134d847b0c8cc9c8ce","impliedFormat":99},{"version":"668f628ae1f164dcf6ea8f334ea6a629d5d1a8e7a2754245720a8326ff7f1dc0","impliedFormat":99},{"version":"5105c00e1ae2c0a17c4061e552fa9ec8c74ec41f69359b8719cb88523781018e","impliedFormat":99},{"version":"d2c033af6f2ea426de4657177f0e548ee5bed6756c618a8b3b296c424e542388","impliedFormat":99},{"version":"2d4530d6228c27906cb4351f0b6af52ff761a7fab728622c5f67e946f55f7f00","impliedFormat":99},{"version":"ec359d001e98bf56b0e06b4882bd1421fd088d4d181dff3138f52175c0582a51","impliedFormat":99},{"version":"45be28de10e6f91aacb29fbd2955ba65a0fd3d1b5fddefece9c381043e91e68d","impliedFormat":99},{"version":"77dabe31d44c48782c529d5c9acddc41f799bf9b424b259596131efc77355478","impliedFormat":99},{"version":"6801ebe0b7ab3b24832bc352e939302f481496b5d90b3bc128c00823990d7c7d","impliedFormat":99},{"version":"0abb1feddc76a0283c7e8e8910c28b366612a71f8bfdd5ca42271d7ad96e50b2","impliedFormat":99},{"version":"ac56b2f316b70d6a727fdbbcfa8d124bcd1798c293487acb2b27a43b5c886bb0","impliedFormat":99},{"version":"d849376baf73ec0b17ffd29de702a2fdbbe0c0390ec91bebf12b6732bf430d29","impliedFormat":99},{"version":"40dcd290c10cc7b04a55f7ee5c76f77250f48022cea1624eba2c0589753993b4","impliedFormat":99},{"version":"0f9c9f7d13a5cf1c63eb56318b6ae4dfa2accef1122b2e88b5ed1c22a4f24e3b","impliedFormat":99},{"version":"9c4178832d47d29c9af3b1377c6b019f7813828887b80bb96777393f700eb260","impliedFormat":99},{"version":"dddb8672a0a6d0e51958d539beb906669a0f1d3be87425aaa0ae3141a9ad6402","impliedFormat":99},{"version":"6b514d5159d0d189675a1d5a707ba068a6da6bc097afb2828aae0c98d8b32f08","impliedFormat":99},{"version":"39d7dbcfec85393fedc8c7cf62ee93f7e97c67605279492b085723b54ccaca8e","impliedFormat":99},{"version":"81882f1fa8d1e43debb7fa1c71f50aa14b81de8c94a7a75db803bb714a9d4e27","impliedFormat":99},{"version":"c727a1218e119f1549b56dd0057e721d67cfa456c060174bac8a5594d95cdb2d","impliedFormat":99},{"version":"bca335fd821572e3f8f1522f6c3999b0bc1fe3782b4d443c317df57c925543ed","impliedFormat":99},{"version":"73332a05f142e33969f9a9b4fb9c12b08b57f09ada25eb3bb94194ca035dc83d","impliedFormat":99},{"version":"c366621e6a8febe9bbca8c26275a1272d99a45440156ca11c860df7aa9d97e6d","impliedFormat":99},{"version":"d9397a54c21d12091a2c9f1d6e40d23baa327ae0b5989462a7a4c6e88e360781","impliedFormat":99},{"version":"dc0e2f7f4d1f850eb20e226de8e751d29d35254b36aa34412509e74d79348b75","impliedFormat":99},{"version":"af3102f6aec26d237c750decefdc7a37d167226bb1f90af80e1e900ceb197659","impliedFormat":99},{"version":"dea1773a15722931fbfe48c14a2a1e1ad4b06a9d9f315b6323ee112c0522c814","impliedFormat":99},{"version":"b26e3175cf5cee8367964e73647d215d1bf38be594ac5362a096c611c0e2eea8","impliedFormat":99},{"version":"4280093ace6386de2a0d941b04cff77dda252f59a0c08282bd3d41ccc79f1a50","impliedFormat":99},{"version":"fe17427083904947a4125a325d5e2afa3a3d34adaedf6630170886a74803f4a2","impliedFormat":99},{"version":"0246f9f332b3c3171dcdd10edafab6eccb918c04b2509a74e251f82e8d423fb7","impliedFormat":99},{"version":"f6ef33c2ff6bbdf1654609a6ca52e74600d16d933fda1893f969fc922160d4d7","impliedFormat":99},{"version":"1abd22816a0d992fd33b3465bf17a5c8066bf13a8c6ca4fc0cd28884b495762d","impliedFormat":99},{"version":"82032a08169ea01cf01aa5fd3f7a02f1f417697df5e42fc27d811d23450bc28d","impliedFormat":99},{"version":"9c8cbd1871126e98602502444cffb28997e6aa9fbc62d85a844d9fd142e9ae1b","impliedFormat":99},{"version":"b0e20abc4a73df8f97b3f1223cc330e9ba3b2062db1908aa2a97754a792139ac","impliedFormat":99},{"version":"bc1f2428d738ab789339030078adf313100471c37d8d69f6cf512a5715333afc","impliedFormat":99},{"version":"dc500c6a23c9432849c82478bdab762fa7bdf9245298c2279a7063dd05ae9f9a","impliedFormat":99},{"version":"cd1b6a2503fc554dcab602e053565c4696e4262b641b897664d840a61f519229","impliedFormat":99},{"version":"af1580cd202df0e33fc592fe1d75d720c15930a4127a87633542b33811316724","impliedFormat":99},{"version":"538608f9242fbf4260d694f19c95b454f855152ab3b882ac72114f19b08984d2","impliedFormat":99},{"version":"cd0e1083bd8ae52661544329c311836abdda5d5dda89fc5d7ab038956c0394e8","impliedFormat":99},{"version":"9ea6fea875302b2bb3976f7431680affc45a4319499d057ce12be04e4f487bf9","impliedFormat":99},{"version":"66e0c3f9875da7be383d0c78c8b8940b6ebae3c6a0fbfd7e77698b5e8ade3b05","impliedFormat":99},{"version":"da38d326fe6a72491cad23ea22c4c94dfc244363b6a3ec8a03b5ad5f4ee6337b","impliedFormat":99},{"version":"da587bf084b08ea4e36a134ec5fb19ae71a0f32ec3ec2a22158029cb2b671e28","impliedFormat":99},{"version":"517a31c520e08c51cfe6d372bc0f5a6bf7bd6287b670bcaa180a1e05c6d4c4da","impliedFormat":99},{"version":"0263d94b7d33716a01d3e3a348b56c2c59e6d897d89b4210bdbf27311127223c","impliedFormat":99},{"version":"d0120e583750834bf1951c8b9936781a98426fe8d3ad3d951f96e12f43090469","impliedFormat":99},{"version":"a2e6a99c0fb4257e9301d592da0834a2cb321b9b1e0a81498424036109295f8b","impliedFormat":99},{"version":"c6b5ae9f99f1fccadc23d56307a28c8490c48e687678f2cafa006b3b9b8e73e4","impliedFormat":99},{"version":"eae178ee8d7292bcd23be2b773dda60b055bc008a0ddce2acc1bfe30cc36cf04","impliedFormat":99},{"version":"e0b5f197fb47b39a4689ad356b8488e335bbf399b283492c0ffae0cfda88837b","impliedFormat":99},{"version":"adb7aa4b8d8b423d0d7e78b6a8affb88c3a32a98e21cd54fcafd570ad8588d0c","impliedFormat":99},{"version":"643e22362c15304f344868ec0e7c0b4a1bc2b56c8b81d5b9f0ee0a6f3c690fff","impliedFormat":99},{"version":"f89e713e33bfcc7cc1d505a1e76f260b7aae72f8ba83f800ab47b5db2fed8653","impliedFormat":99},{"version":"4e095c719ab15aa641872ab286d8be229562c4b3dc4eec79888bc4e8e0426ed8","impliedFormat":99},{"version":"6022afc443d2fe0af44f2f5912a0bdd7d17e32fd1d49e6c5694cbc2c0fa11a8f","impliedFormat":99},{"version":"6dd3f823ac463041d89c84d7bbf74931a38d874a9716040492ac7a16c7d2f023","impliedFormat":99},{"version":"a5bf6d947ce6a4f1935e692c376058493dbfbd9f69d9b60bbaf43fd5d22c324b","impliedFormat":99},{"version":"4927ef881b202105603e8416d63f317a1f1ea47d321e32826b9b20a44caa55e2","impliedFormat":99},{"version":"914d11655546eba92ac24d73e6efdb350738bcf4a9a161a2b96e904bad4de809","impliedFormat":99},{"version":"f9fdd2efc37eefc321338d39b5bd341b2aa82292b72610cb900f205f6803ff66","impliedFormat":99},{"version":"687208233ae7a969baa2d0c565c9f24eb4cb1e64d6cfb30f71afec9e929e58c2","impliedFormat":99},{"version":"62e7bd567baa5bac0771297f45c78365918fb7ba7adba64013b32faa645e5d6d","impliedFormat":99},{"version":"3fb3501967b0f0224023736d0ad41419482b88a69122e5cb46a50ae5635adb6a","impliedFormat":99},{"version":"06d66a6723085295f3f0ecd254a674478c4dba80e7b01c23a9693a586682252f","impliedFormat":99},{"version":"cc411cd97607f993efb008c8b8a67207e50fdd927b7e33657e8e332c2326c9f3","impliedFormat":99},{"version":"b144c6cdf6525af185cd417dc85fd680a386f0840d7135932a8b6839fdee4da6","impliedFormat":99},{"version":"e8dfa804c81c6b3e3dc411ea7cea81adf192fe20b7c6db21bf5574255f1c9c0e","impliedFormat":99},{"version":"572ee8f367fe4068ccb83f44028ddb124c93e3b2dcc20d65e27544d77a0b84d3","impliedFormat":99},{"version":"7d604c1d876ef8b7fec441cf799296fd0d8f66844cf2232d82cf36eb2ddff8fe","impliedFormat":99},{"version":"7b86b536d3e8ca578f8fbc7e48500f89510925aeda67ed82d5b5a3213baf5685","impliedFormat":99},{"version":"861596a3b58ade9e9733374bd6b45e5833b8b80fd2eb9fe504368fc8f73ae257","impliedFormat":99},{"version":"a3da7cf20826f3344ad9a8a56da040186a1531cace94e2788a2db795f277df94","impliedFormat":99},{"version":"900a9da363740d29e4df6298e09fad18ae01771d4639b4024aa73841c6a725da","impliedFormat":99},{"version":"442f6a9e83bb7d79ff61877dc5f221eea37f1d8609d8848dfbc6228ebc7a8e90","impliedFormat":99},{"version":"4e979a85e80e332414f45089ff02f396683c0b5919598032a491eb7b981fedfd","impliedFormat":99},{"version":"6d3496cac1c65b8a645ecbb3e45ec678dd4d39ce360eecbcb6806a33e3d9a7ae","impliedFormat":99},{"version":"9909129eb7301f470e49bbf19f62a6e7dcdfe9c39fdc3f5030fd1578565c1d28","impliedFormat":99},{"version":"7ee8d0a327359e4b13421db97c77a3264e76474d4ee7d1b1ca303a736060dbc6","impliedFormat":99},{"version":"7e4fc245cc369ba9c1a39df427563e008b8bfe5bf73c6c3f5d3a928d926a8708","impliedFormat":99},{"version":"3aa7c4c9a6a658802099fb7f72495b9ba80d8203b2a35c4669ddfcbbe4ff402b","impliedFormat":99},{"version":"d39330cb139d83d5fa5071995bb615ea48aa093018646d4985acd3c04b4e443d","impliedFormat":99},{"version":"663800dc36a836040573a5bb161d044da01e1eaf827ccc71a40721c532125a80","impliedFormat":99},{"version":"f28691d933673efd0f69ac7eae66dea47f44d8aa29ec3f9e8b3210f3337d34df","impliedFormat":99},{"version":"ae89fb16575dc616df3ff907c6338d94cfa731881ecef82155b21ab4134b3826","impliedFormat":99},{"version":"687208233ae7a969baa2d0c565c9f24eb4cb1e64d6cfb30f71afec9e929e58c2","impliedFormat":99},{"version":"f716500cce26a598e550ac0908723b9c452e0929738c55a3c7fe3c348416c3d0","impliedFormat":99},{"version":"6b7c511d20403a5a1e3f5099056bc55973479960ceff56c066ff0dd14174c53c","impliedFormat":99},{"version":"48b83bd0962dac0e99040e91a49f794d341c7271e1744d84e1077e43ecda9e04","impliedFormat":99},{"version":"b8fd98862aa6e7ea8fe0663309f15b15f54add29d610e70d62cbccff39ea5065","impliedFormat":99},{"version":"ffa53626a9de934a9447b4152579a54a61b2ea103dbbf02b0f65519bfef98cdd","impliedFormat":99},{"version":"d171a70a6e5ff6700fa3e2f0569a15b12401ad9bc5f4d650f8b844f7f20ef977","impliedFormat":99},{"version":"b6e9b15869788861fff21ec7f371bda9a2e1a1b15040cc005db4d2e792ece5ca","impliedFormat":99},{"version":"22c844fbe7c52ee4e27da1e33993c3bbb60f378fa27bb8348f32841baecb9086","impliedFormat":99},{"version":"dee6934166088b55fe84eae24de63d2e7aae9bfe918dfe635b252f682ceca95a","impliedFormat":99},{"version":"c39b9c4f5cc37a8ed51bef12075f5d023135e11a9b215739fd0dd87ee8da804a","impliedFormat":99},{"version":"db027bc9edef650cff3cbe542959f0d4ef8532073308c04a5217af25fc4f5860","impliedFormat":99},{"version":"a4e026fe4d88d36f577fbd38a390bd846a698206b6d0ca669a70c226e444af1b","impliedFormat":99},{"version":"b5a0d4f7a2d54acbe0d05f4d9f5c9efaaeddc06c3ee0ca0c66aba037e1dca34b","impliedFormat":99},{"version":"fa910f88f55844718a277ee9519206abce66629de2692676c3e2ad1c9278bdfd","impliedFormat":99},{"version":"a886a5af337cce28fe3e956fd0ed921345933163f5b14f739266ba9400b92484","impliedFormat":99},{"version":"9ae87bd743e93b6384efbfa306bde1fa70b6ff27533983e1e1fe08a4ef7037b8","impliedFormat":99},{"version":"5f7c0a4aad7a3406db65d674a5de9e36e0d08773f638b0f49d70e441de7127c0","impliedFormat":99},{"version":"29062edaa0d16f06627831f95681877b49c576c0a439ccd1a2f2a8173774d6b2","impliedFormat":99},{"version":"49fcfda71ea42a9475b530479a547f93d4e88c2deb0c713845243f5c08af8d76","impliedFormat":99},{"version":"6d640d840f53fb5f1613829a7627096717b9b0d98356fb86bb771b6168299e2e","impliedFormat":99},{"version":"07603bb68d27ff41499e4ed871cde4f6b4bb519c389dcf25d7f0256dfaa56554","impliedFormat":99},{"version":"6bd4aa523d61e94da44cee0ee0f3b6c8d5f1a91ef0bd9e8a8cf14530b0a1a6df","impliedFormat":99},{"version":"e3ee1b2216275817b78d5ae0a448410089bc1bd2ed05951eb1958b66affbdec0","impliedFormat":99},{"version":"17da8f27c18a2a07c1a48feb81887cb69dacc0af77c3257217016dacf9202151","impliedFormat":99},{"version":"8395cc6350a8233a4da1c471bdac6b63d5ed0a0605da9f1e0c50818212145b5b","impliedFormat":99},{"version":"b58dda762d6bd8608d50e1a9cc4b4a1663a9d4aa50a9476d592a6ecdc6194af4","impliedFormat":99},{"version":"bc14cb4f3868dab2a0293f54a8fe10aa23c0428f37aece586270e35631dd6b67","impliedFormat":99},{"version":"946e34a494ec3237c2e2a3cb4320f5d678936845c0acf680b6358acf5ecc7a34","impliedFormat":99},{"version":"b85aa9cc05b0c2d32bec9a10c8329138d9297e3ab76d4dd321d6e08b767b33ed","impliedFormat":99},{"version":"b478cef88033c3b939a6b8a9076af57fc7030e7fd957557f82f2f57eddfc2b51","impliedFormat":99},{"version":"2fac70f99da22181acfda399eed248b47395a8eeb33c9c82d75ca966aee58912","impliedFormat":99},{"version":"54c28c9fc4cae7c4fb9fb7e6245ebe1a405075c190cecd6d9c15d1882473a44e","signature":"369d0e3e9dfada71a1b6dd9ecef32b43ae570f0575996c2d2b71cddf58dd100f"},{"version":"6ad69e42f510ef065216c52cffc2c2f15cf24c20e6da1d622bd32c4123d31bec","signature":"a5c773a8b089f84882db294570016c3c8c314cb829d5dadf810b4b723f927958"},{"version":"d630aaa0315d21c1b78cad038ec90a89ccd4c3c04bcdcc6cca19253413dd41fa","signature":"7546981df71df0ba42aa669a52101937bc2df45aee792832ef26c0d0a2cbbd5d"},{"version":"853aa2acd51cd7300884c400532303bec106892146fbe0b64974bf4dfba845ab","impliedFormat":99},{"version":"5fd06addc09e3585eb52b0d8e731dd7ee79fe59e84f07279f80f5ff9772357ef","signature":"010c7c70c42e3ae1797dd61ea025f3dc9d0470f423be2e57583c6f2b41dbd54a"},{"version":"e64ad9a760a7a4d1fab99793776829d360a7ab34e7e7a0d20fe183c34fb71a11","signature":"a3df50373eaab5122c1384f66eac08aedf5505f13be691f57565e2dc852a3ea0"},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"293eadad9dead44c6fd1db6de552663c33f215c55a1bfa2802a1bceed88ff0ec","impliedFormat":1},{"version":"833e92c058d033cde3f29a6c7603f517001d1ddd8020bc94d2067a3bc69b2a8e","impliedFormat":1},{"version":"08b2fae7b0f553ad9f79faec864b179fc58bc172e295a70943e8585dd85f600c","impliedFormat":1},{"version":"f12edf1672a94c578eca32216839604f1e1c16b40a1896198deabf99c882b340","impliedFormat":1},{"version":"e3498cf5e428e6c6b9e97bd88736f26d6cf147dedbfa5a8ad3ed8e05e059af8a","impliedFormat":1},{"version":"dba3f34531fd9b1b6e072928b6f885aa4d28dd6789cbd0e93563d43f4b62da53","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"e4b03ddcf8563b1c0aee782a185286ed85a255ce8a30df8453aade2188bbc904","impliedFormat":1},{"version":"2329d90062487e1eaca87b5e06abcbbeeecf80a82f65f949fd332cfcf824b87b","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"4fdb529707247a1a917a4626bfb6a293d52cd8ee57ccf03830ec91d39d606d6d","impliedFormat":1},{"version":"a9ebb67d6bbead6044b43714b50dcb77b8f7541ffe803046fdec1714c1eba206","impliedFormat":1},{"version":"5780b706cece027f0d4444fbb4e1af62dc51e19da7c3d3719f67b22b033859b9","impliedFormat":1},{"version":"bcf5edfb2b188348d16a7dd26c83a79856a7e81a7800051109d99aa211ef6947","impliedFormat":99},{"version":"6d870cbc76c9654c649c58dd664a875bab499e3e746b34ff2a6b6d363a3e0686","impliedFormat":99},{"version":"c1da3f540f39091e02624208125e7d825fe6839d153399c21315725acf0adbe1","impliedFormat":1},{"version":"91648eebacde37f34eace96a9d3f269875ae85331e70a4ffb89e76f0382338ae","impliedFormat":1},{"version":"b67aac762d89fc868d6560040d7e83b51fadf4471f7996060897c7344583db7f","signature":"1df7e07dee5c21c5473587ad884f55787fdadcd50af170a9010e7b0b0d795cef"},{"version":"384ef34eda1333e1bdcf728bb0b3cd3a4f67003d4bd1b117b8d2f74554dee0f0","signature":"4ce0f057e01df35065ac049ced53e5a4aff789db91a3f4e871f88bdd9921cc59"},{"version":"e1be4a2a139d2a99071ff9c81d7c3401a04612b032c16b4212a66e254c784217","impliedFormat":99},{"version":"86f417a032f112eef217852657f7dc939cfcb87f7770b0a85360ccc2cb14ce5c","signature":"193f3698143959625b77bab03ef64fae1876dda0ed7438f9da236b49a3b9b826"},{"version":"40eb4eb274d7def754b3352c23328af96a69261fe5e99d20df41f1d5de8d0f5e","impliedFormat":1},{"version":"c57b441e0c0a9cbdfa7d850dae1f8a387d6f81cbffbc3cd0465d530084c2417d","impliedFormat":99},{"version":"26c57c9f839e6d2048d6c25e81f805ba0ca32a28fd4d824399fd5456c9b0575b","impliedFormat":1},{"version":"02a523d3492e97440de9c080a59f3aa9366ca92bb6914078a75ae8a6f3e90a7f","signature":"512960c0e955a2324b34354dac25e3e4d431a1af4cd33077935eda5e95c8b7e1"},{"version":"37c7961117708394f64361ade31a41f96cef7f2a6606300821c72438dd4abda3","impliedFormat":1},{"version":"19feb8551285d9041090b2aac242de4089aa0abe3777a89ff9a804225e561832","affectsGlobalScope":true,"impliedFormat":1},{"version":"a4553f8b822eb81601e9ff242bde19eebc440201e687161598c267892ee84bdf","affectsGlobalScope":true,"impliedFormat":1},{"version":"724dea86a503f9bdf755f5c628cea9de4a63aafdf0f6ca25db5e0172ddc0d883","impliedFormat":1},{"version":"39875f62f78dfcd829f2d38f7b06e8a8d5e39bbb9487abe63b81b41fe3b58c04","impliedFormat":1},{"version":"db9c5f6f63db334bd716569e5354ba232b4b10b7971b9ca57b4b7187b16d4ed5","impliedFormat":1},{"version":"0e139fd856b5953593d16dce2e9246ba9d2cfe0e0ea097e0f3f3c1f39abbdccf","signature":"d0fc15d80d4bd49eab0060e9013580bd40c3d09711b887c6f5b802727bc52630"},{"version":"e83039dc0b4cdf1b92e7c48cba8e00cf26a55db2541034edfe05481816cec202","signature":"a802ec5726974ae76675b9c3a9989a5b2a0cd765ed44124a5f1c8c1b549e17fb"},"e5a5e11a003b49f8b996a57718738f431f2f5c01ab28a866d1c58a9c936e7c8f",{"version":"69e94001c9bd657dc320a0fef9b5a3638e8e127fbee3680066162bce663a0264","signature":"b12f4a22f70ee31bce50622f528a1f7b5b0fe7ce991253ed7c428a748acab351"},{"version":"8da5668b646a09752f09e373a50c1c701037a4fceaed606fae523c436b839267","signature":"3ef125ce9c030718edbe5c69e42a585570b64d61401abea48f5fbfb159558d92"},{"version":"947791420ead2de787f21ccfe43e481460a829d8291533e1d2557a4786099320","signature":"3ffdd8285724697502c4e7e5431281ecd4d8abd99d3e74da01a9076a51711408"},{"version":"ad84aa2aa1f40cb360232de5dd042a167f3c6d9df230f5d45ba5682254a3ee6c","signature":"d0d0b3d9e3ac3534f4e3033589f51ec1ca8505d382dca6b15779c105c7bf9ad2"},{"version":"68ec28272a4f87e783345308c4de9f3e4c31a99bf7ac8a03526a984572f69090","signature":"67aaf9256efc2277f383e89582965e2f9929305c9591bc7092be3e63c3da38ab"},{"version":"5955c00fae6439e230605e2111c0a184753bcb0e204e7d33fd4f73cf1591810b","signature":"9303adb50b794e326768da0980356d1f9999b772876bdb7655701bd567b9db53"},{"version":"dd4c33c6803ee1fab2255ef6aea3c2956da2720b8709e522ccd4161df177af16","signature":"441ba5aac89d55a5b021b2e6a234685afb3b9e2bf0e3f7cfaf6a627552308cdd"},{"version":"fa64b85e3ce81da13ba42ec9a641c91b1c264f36c30f2f7be60c827edc11b523","signature":"dd653302ff9ac172b9efc43fb38a247b99a225a961f1927f59ac43bfa31267ad"},{"version":"fd4adbd0536ed2b88e46a0f9f99841df45a9604aceb6aae8a05f5b59891dbbb0","signature":"2eab959c5749eb603889c6fef43662efcfb14588b5b9fecc912d7eff57f5c11d"},{"version":"d6cf0453674a5d863e055009933dab639e33be1c24d1a498c88510d98aa1483c","signature":"c2b75a7a7fd1a8d6b4131da817f058f89f803d5a1bd71a2628fd8de5522c70f1"},{"version":"0fb1c7b4cd7af758a533dd15270781c84e971f4fd46cdce12aef8b08c09ad06c","signature":"41642e6c042c6a1848edd5954903ddf39a3e93ff3d982cfc52b70c8fdc86eab0"},{"version":"6c5039dee5f9e604c82bafc56889c35f72fa70acc3fc825e491ad5286f6194cd","signature":"854f41282a40dcd7ae4024bd9874935a496fe46d6de35de8f6be1336969ec17e"},{"version":"cf4f62163284ae3d8a7bfd0091e4068ff75dde1cf37cb6447a3411171481b0c8","signature":"0c1abfce570b9969f8bf7118b5dffe70ba43369adba22af3154d89c225ef7fb1"},{"version":"bf4f2229d74a2b8def938a43ddb324daf9b527d9a42d2b6f58b8df2de9b1998d","signature":"914ccf74f18ee798574084c10b93299703fe8e9118d7f3e0e7c0b1d97282db87"},{"version":"13ddffcc1f56698113dbae80fa0eb371b3e0e5b22a86cf351920eaa8c99f5a57","signature":"0cc9ff77c05295b47bff4b2c6d03595fd99b91876656601e9e085306a3d23b4a"},{"version":"d55dd647a31afd67af853f250f794285f3c84f8d934cc1bf24845216cca7d0d3","signature":"42293684b652c3bea06c01bd00da78aaf62776202058b63eec8f1f39c07efd3b"},{"version":"785e659603a3dc19f17ca340c4adcb46f1bfe2311280660d237f0292b3765d87","signature":"ec78ab3cf641624f7d2f7e135acf2c0f4ab0192f67f7ff0d6e1aba33135cecd2"},{"version":"7ea1cbc8f1d66cf4ca1f708ec360e364427adbaefaf4b78351164773fbe58931","signature":"c46ee4bea4e866697b8b78bddd73cc74f511b1fb73c7c434df209391242f85cb"},{"version":"c5779f1371a25486741e2e7772d8320d9a97647d3d6223b9268ca26820661deb","signature":"de63220978c0f91d9e9999976d75db3b610a07460c91b2c9b43b336402eb5a8c"},{"version":"26ef4c32b59e64c1f165ef465a9d47d92d4b63c947d35cd9f132a1bd72e9d7e9","signature":"3c15d8148ca08e01a7284719498e82573afd7a636d974b5ca7b7c567f34427eb"},{"version":"7c60a4c129dbadd911c2d0abaa3c9c0257671c149c5ede9dc885cf3f836c8c39","signature":"1802a255970ffaee6b86ab889a84e2cde4579f071136b20652d9d25920368443"},{"version":"3e1810144e96ab91710c57448594d0d1ba91db25d2146f3c25ffd3c234bef9d9","signature":"332ca7ff77a8ea6b6882dc20715d210def9a804f521c835f693af0de517529cd"},{"version":"9c96de243256bfed19f0a2a2654e37b385089a655783c0d183fb2af74272224e","signature":"0a2050b1a984eeeea3d252e307ab46255646e71838763fb2f42899d6e681caa6"},"8562116c7dc8f01afdf66dee0181a3a0c30b0aa7f3ca59bdfd3935073424c545",{"version":"f9b57b7d7f2a096ce89e8c5e81a725112bcd57642a5d87c2b08b756eb0539383","signature":"0cbe679a9a33573193531d1bf6b6f46a7d21b99f3c7a8e784d1d6ff8b58e1f4d","affectsGlobalScope":true},{"version":"0714ea0ae1ca5e03c1c8e289732488bd3cae7158a1bc4a24074e9d4efb356b42","signature":"55c2696a68a74e32d1fcd2f5a90dd84910ccfbb2aec83f8ffe8b16c2bff84062"},{"version":"2a7580f35d47293ca0e5d1857da35c023515de04501ed02149f5e30e63305db1","signature":"c7bd1b09f85b4bd8b214d205af65111db97c94fcdbbdaeda2cde274bb6c37b5e"},{"version":"672ae324228b7215afd251560a6c1a0943ae8f5fd7a5acb7413fd6ee3b248e23","signature":"2ed125a3f0181ed618ddd13c092f33d1cd22088bc57341fae51c51c87daed6d6"},{"version":"5e8447245195785ad68a25421055841e1f37224a163a94d416707f993fd7fe35","signature":"4697990787d82b43a7860b2815ec2c051ca83dc56288f2e90a7a6a32a145c277"},{"version":"b2df4c7a260ad9da5844b26291c9b8fd656bdc2023d9efbd87e2a74eadb65e8e","signature":"bfd1eb0cb29a18ac66194bba68306bff26536d499f48b78026721e0cb577ae0e"},{"version":"4c750ceb1795c8093bd5c62e434dec15db2ee657225c09e53dd8b053ffb30b12","signature":"0fda42aacca8f228d9d11a7248f7f38bd01e32abd159f5b2d60fe83954f1134b"},{"version":"484696ec6d899e5ae44ace4da931d8e57468c8b8c58bd874ba268ee176f02442","signature":"3f9f60ee24518eeb21de0b3437c092d9c25140412e60d61325f688fc976a56d6"},{"version":"bebc21fd5e8ca60f720bf114bd6a3e6cf2f0f28f34a6fea0abb50641aede2d99","signature":"42bf37bf6d38942111f166530e8f47c5946106c1dd1445cbd6f38891d04bece2"},{"version":"2ba7d8cd1cd949d35d71d9b671769d88d0f18e5491e360a93805931479f6a0b3","signature":"31173d0ffc212ed6bc0619aa4748168843ff21786fbabd40fd64c1693c01fc19"},{"version":"e05284b247ff98755c74264c43a51f1f3481171f16f4202cedc14a13ca2c2389","signature":"1c5aa26007d4f769821837a5ab63113c26faacb64b82941e0fda868e2d075c26"},{"version":"e53c823cdecc247a5b180e38be79756372f4f63e93dc0f540a3d1f22018adb01","signature":"15b0fdec4d28c7cf60c34943d69ecd0dd5e34540ff328335346249bef51e3e41"},{"version":"7c4fc7da6184a91b511b38311bb9c58d316d0db4b797996ca091bba06eb1e642","signature":"8a24e2e3b3d0725c1660921fb95530c886a9c5c278cef47de40a091a02e87d73"},"e35ecebf0e24c37a1a79e00a09d7a9059d3549f6c8cb328f0900c86289d7aa5f",{"version":"ee07be51e6f9fe68b65166e1a6412e86ca13a22f679f89f6f2d15c6fd09dc128","signature":"f025160a6f5342fe0283b8f4fdf94b772c193b14f838b71e69adc626f842e5e5"},"edd6fedc43b0e0bc286b1be00cd065cc4e18a55e85bcf7587cc87ae2f67f2829","6ad8bec672da8779b95ab41a7db8499e867290886bf452c8c21cf8515ed81727",{"version":"b588cbc4ff7110aa2de6c3523ab05675adb92660a381518029697418261c33fe","signature":"3e009be01e991d604aea4037befbd90d045b4ce17bd2fa76c6b4e3e7511fc7fa"},{"version":"9dae4a69a9e8ef7743e7c26a3177717c9480a5a5c7445ced84d6b14c16c26beb","signature":"d3eaa6ad62855b898b718baed188f521a9b844689937a34205422d7a16120252"},{"version":"2bd3ee23d20ffdb309062af641f298b92f19f63efc3662df2a69229e453c08b6","signature":"a575347f08bceafc67ec61295040ebd6d8a8f697126fdb88ccd620ec1824bc08"},{"version":"bfcd0e14df47f0e064e1a4986468629b7f96eb37b3b4a0fe2a3f838b70f00f09","signature":"95871d797695d7e808d51b05b5eb68ff0b675e403cd35e463296b336d299745d"},{"version":"a58be083e01f26481c892acaf164e05ffddaadbb93432ef4621307888d85b906","signature":"bd11709018d7c0be79ed6ce7ccd3eaa9e0c74619b66b6a8c79c6d9407f8ad30c"},{"version":"1c5a5cabdedb944b0dff17ddbad2a09cc334d1943c32a009f9b7b9bbc623630e","signature":"3f02ce38faaa02a97261f60e24286a3abf6b31b93565ad276c32b1fcdd6c144e"},{"version":"3dcef5bcc791db341ec241337e7a9a6d1794b707099bfdf8d66557f5a43fcc9a","signature":"52695dde8f8749f6b5e20a480143d8809df82e70e66df33d07ce8ad4a43a0b63"},{"version":"d085ecf4c41c9698491a6b393a8cd82bc17c5f498999629188b51dc0727f4e7a","signature":"f46610761267deb6fd9fcc1938834193c50c7ecdc7ae3a6612617b8e182844e7"},{"version":"04aa0fc9987bba7035e600c50d174a3cfbc448e6f36662d85721c061cd9b10c0","signature":"e6a6ba05b5c8d27e7a39a7f0231d8b40aa20bdce6d25edb6d9a1314b3ef1bdaf"},{"version":"dfca847ebb25bdf52061760b25bba2c00f5fa9d2cdcbb297308fd98c840e3eff","signature":"08879cd061050170003634aad0ee6fbc5151e0396ae646050284dd744f6d218b"},{"version":"df4bca5517eb6d54f07e2ab809d3167c01894331aeb0c1ac81d20e17e72f5754","signature":"d58e456651d257d90c3c61c198fbf7d10dbc60b11922b0c832ebd4a68411c20a"},{"version":"7e24397ae6da929f14889e183eb0cbe46096df27f863cef367501416a306348d","signature":"e6963e71dc146d13e5c347571c233e413f5f5e0d21eba5c69d879fd94048b447"},"23284bd62b7ffcbbf579f77d93fe5572b364f81d94ce0e1d5d59235198ff32eb",{"version":"b5154dc39f4c3a18b5225a9d9b4d46785972dc2e365e5f788dc121f80f031331","signature":"f2e890f66ae0ca0a78b94014a5db16d0475cd7c8961e5f45395e821f75801799"},{"version":"77b2c2b02fc5591ceb54245f70c434247d3c85673df180b7151498bb1494e1cd","signature":"e8050e1446b8eec1b3c154fb3502c264b749358cacf0f6346d0024ec5d1c457e"},{"version":"78d9b75fd3b33eea3eefd7b1999f8b46f42771113042a56f0fdd98add710982b","signature":"30f003fd12e5bb1fc4939b0c64577bf0ede6de3f1d0ccb80b28b079bbacd1308"},{"version":"5efc64e707bda4626eb9b08beccf34b3673986e897eb9b2f7e8ce1f096a6a51a","signature":"f2574d729531fad0dde200109ebfdd731eb7c4241427b2ca245d596e57e4d797"},{"version":"1119273bed52c96e895ad22b3e8fee9648f1d5d621cb6b7cceccc4638c7b7533","signature":"9e867c1bbc1aa70f581e5095caf3aeec6a320bcdcf14f01b4a83b687890e530d"},{"version":"18807fcc265ed7a189ed2d1b794e571668b3a3eb2810405c1a79b1f3edc5d209","signature":"1860b01d52322d49a4c13672b5835ad2d7be45cc97e20798a9bf303bed0b4f22"},{"version":"6255f8f5f4f919d6caa95a6dec3154a18a11e9147197d8a2d68325c65761f495","signature":"99814c49572093c22c0997eb925518a619f7cf8e990af4b4b8682e9208d023e6"},"4da26603960e508de660ef80c9d1617039bb0603b96b7d2c824add333ddf3068",{"version":"569e133ebdfe508e1f2ac78006fc15f785b0fcf51629cc138b4e2620943a42bb","signature":"3d2be884614ce89df1aba049ad4c89960fed913bc3b34b6c8f6edb4313c70452"},{"version":"e6b8f3cd057e49a50b57a52acc38cff7c224def2249464d489295e0e1d200af6","impliedFormat":1},{"version":"74b89b20ef68166153e24a4ea7a13306c8daa65660815c0a7574f4ef35eb4fc6","signature":"8b23ab4f166c71cd3faa1fa3d6f02e4d626fc6675cc640649a7e8ad401a3015b"},"ccde58ef224ad07068ddeb3533011b986b08b0db6f686e50943f518bbcfbcff0",{"version":"4fa68a3551e639f34e943d51855c4df87d414470356a3ff4ee23adfa02054193","signature":"3545f5333a8c04e118b0bd3a19e7636dcb542d32710d6f1c39cac9a34e7872be"},{"version":"d44c53a5185ae285987268f3f30cdadf3dc7bff19d230bfcb5c6505d4f268299","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7f161bf747d17d98f49d7c2a9e67da87031da2c2cbcf356698fbfc184788b20","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d9ff2131e0cd0d00541853f45cf91d93570f4657717daee949596e828dfecd5","affectsGlobalScope":true,"impliedFormat":1},{"version":"a36dceefb767f35c0e04cd731ce8f302da8f5c4db047afd412749cf9cefab338","impliedFormat":99},{"version":"4fa9937fecb08fd85e2c8a7ffe4ef27fc02cc0ecf5c638133f65ebcc665c155d","impliedFormat":99},{"version":"67500feb82f479142b553d4735477a64838014a335a62033516a93173b2b0d69","impliedFormat":99},{"version":"1bf74c0a319d262ea9b3b7381ca4a05b790ea1fb25e91e4838ca7a03a80e53e8","impliedFormat":99},{"version":"d6f734b1c568ee0b2cc21fdf336ba7e07674d5a244c082961fcb0cfcd1d14d9d","impliedFormat":99},{"version":"3c8e78579c681799fbed1e83c1b42a27497551a66d73620cee86724e16d00c7c","impliedFormat":99},{"version":"d4f1150636fab34b29c20492de16ad8cbfcd165b75fd59f3fc43eb925c3cdda2","impliedFormat":99},{"version":"a423e3eefa50036e6fa64c5559397b2fdd415a635d71f1dc6e6d497fa9ad5733","impliedFormat":99},{"version":"19013bfbe9b85acad69621aee4d4baabea52dd563a55503f4697d31123b4917e","impliedFormat":99},{"version":"c4d678fac1ec9dbe16381c3effdc1e1af1a909eb97c9bc7f6ba8fd832a0c2fbb","impliedFormat":99},{"version":"55605ec08188e2c9183059ab382d904bb0e0e2167edf218a2eeadf755d85c8dc","impliedFormat":99},{"version":"0d6cf4704ba2281eb23e0fb0ef0d9dcb5cad22118515747e5cf62b69a559e198","impliedFormat":99},{"version":"6938a4140037dc171101639cc5601de23db9d150ea2e0ca63149f2ef18c2f943","impliedFormat":99},{"version":"7e05897b8740792b19a75b33f3c18271afcebba23a79a961228422cccf7b107f","impliedFormat":99},{"version":"033ca4aee625d77f77d91ebbb1f2d06c81e6aa5a8e08685b7821164a28ab41f3","impliedFormat":99},{"version":"21a6d8961711f30abcf347ea33621442b239104dad95ca8f6e5c45397f0436ec","impliedFormat":99},{"version":"c1f63a7b774263ad335590fedf52a593a76758fe98c4f28782624c77c4eb59f4","impliedFormat":99},{"version":"4e97d48d7072ad1ce17c5877e8d033c862864b342eed72f951375285411d05df","impliedFormat":99},{"version":"ced21f7dc368280f9928d4f8fa267f04155d9d80dd6e468f17ed1b7b7581d67c","impliedFormat":99},{"version":"71fb51ae6da8a075f61e69a4b8246701c7733a5766d4e895516a13f089788102","impliedFormat":99},{"version":"10467b66b4bfd7c5612004b3efd179333e1fb9108af1cb1a4aca41c4b37d8d9d","signature":"85b9ad1c8e81c0fc2979b37e41ba97e184d91b99d891078f8483750cb12f21b7"},{"version":"88edfe911b08b657b2db83173e56cbe713583dae187ddc869106281b16350bf8","signature":"e76fb79d8346319b35077ecf61bdb65455ad4a2cbec912ded6987f3441393a15"},{"version":"963e329f031c13da20bc9fa1147d1771406630ff92a2ff13491e0ffac0f8e2a7","signature":"2771b190fa173a437f292c7d62adfe0f112a5d298ab4a2959305ea2dd5aef465"},{"version":"9ed19238e79715d3f9c2990785f28240637824a9be27fbdc2b648f073e121c2f","signature":"20ebf4347d0ec57537e4854f496c3b079e60280151a77d1ed4767e59d2c1c781"},{"version":"244e187f8b27c559f429cc7cfa657cca211756223d5513511bd2472d8e8c02f8","signature":"bd74979199c545ec5eef198b3d1b6fea00cca6a299da54e4a8e2ec239a1db3be"},{"version":"f2bf8a98d07f85deeee38fe7123d4584cfe497f9aea4f52e72fb19e540a115ed","signature":"bf7a635f3ff32efe790f51901b961209e068f035e9ba15ff12ca8fc622a6b0a9"},{"version":"b38ba74698e9c65aae350c9235b5b7b64c49518987d52f55efde6e525e0aa802","signature":"4b2a341b2424f7fd6625a872ba05ec2ae70648ff752357868e79867c1c29fb16"},{"version":"f43d6919feff5b1ffe219d212b86b0cfd93056b7ad3d29043f8b1636bb4ba5ab","signature":"f66d247f1974868005ecb8257f60a7d919ae982c1964fe054e7cf54f99dcbcfb"},{"version":"b300c151425070781649957f8232cc12462c7ea871e54d1a113488d62fec0845","signature":"f95c422272723a5e6b7f80fdd2792e4a9bb4e32cc83725d70ebac1759eceb0bb"},{"version":"27679e96d1bd38c5938178aaf4abe8627493090b63d6bae2ce8436e6a87ebe4d","impliedFormat":1},{"version":"d95c8f5d77d5c0cbeff895adc0a5e2230a0e746b58faddeb303dbe0c88c9fa1f","signature":"ea55f358814e5b18a3e1aedaace42db93be906b6948d4cd46c45516867980c51"},{"version":"47086e8e03bc57b1f63d307df4ea211a9a39b25fc0d7235df4f46d41fd5b150f","signature":"fe3798f1ab0d3b67864e33cfe90ce0bd207668acdb794fb0b33abb24aec98b76"},{"version":"448ebc11999f8c4bc677f348b9f02cdab84d5ae940708e96ed3653bb7da5eeb0","signature":"af58445cc7a94f4e967c842bc6bc3d8b99ebcc03b8403c0eb5de711cf23f360f"},{"version":"b2aee1c60cb80700b1e1c0ba938741afc718b5b1ef57ea130cefcd39e38a681c","signature":"3647e4bd2c930df19a43700049e96ce23a10c63405e91a9ecde47e392b2278f4"},{"version":"11870d43a526496e4170ec3bccf9a4f33c7e310c3a1b81fa71ead161b50897ff","signature":"a34f05b827f8b88b33d79c075f55b54bcb8fc833a53143e3b95a9f77b752cbf2"},{"version":"22ac1ebc310cc9deb2fbee76c8ba461070726263be6690f646af410de133cb90","signature":"7136d2ead5ac866e6051b066295e32482530c0c25ebe224d8139381fdf4b119f"},{"version":"fa06fe901eb57b28502f1d03a5c54d7486f255207aa72957d7322b491a7f1633","signature":"c5c57756c6d726b5f590f6268c0d21d3154b91179b6c2134cbbc96e219f613bc"},"6bfa52d6836a8d59c9e506be487c9ce8824da608087278fd3ead167b906d8a11",{"version":"012313a4418195e5ca806962d4df70613c909156d760dc11e9de450c2dbb0561","signature":"fe8df7722f77b779c792cded68cc7769ed89d5e9e50a5dfbdd07ff3f4c420548"},{"version":"aa58edf51b13f8e6dbdb49aecfd69953eb79d14ffb589022567cf80e06af6822","signature":"5de4a9f20b79a71e28aceb8916e26cc12c0d6ca0f442face5b21f98194ce1783"},{"version":"59e907b2109ebe4311b430ed2644a5bea52f2f300fe97ac91a437ef9803195a9","signature":"aa624c5a540fa657676b4c90482d711db9e862659b8bcd5652d43fcffcf5ca7e"},{"version":"b3e9641f3fbbc4cc77cbc8390f4278395b06f3639c6d9559f9a42ad6e17a3d1b","signature":"73aa3daff7523a80b8d229daaa7d127db8a7ba16a5b19986b0bb4c6348888b18"},{"version":"1de2061215e15865b1494f40cb67d017466279bd89a2eb4c5a1dbeeb4e69752c","signature":"a59a49e2d8a3646b5460c7a39e30e96c85dd1fd5855c3d57586987c7284286aa"},{"version":"34105e5bc42e67653de3a238e1648d509e0f818c36c82c457574bf381356f72d","signature":"c9678010531d9ad25826ec1223e8f2b18f97651f9099df4399f9ec6a62af7f5a"},"2440951e1d2dfcd2bbc2073dcc1daa284614e25b9955a9dc60110980e1eff120",{"version":"99942dee5eae15c971e5d008c86e503363f0f1259b0825f8a0bfb8f658ab48f8","signature":"c1e9ae11a135ce5bc4791652c59ae49b1cacaf363f681e5e6cbda84e74002649"},{"version":"d04722411249c2c67672f59b3edc29920ffafe20dc9f29cc860b5ef998f11de5","signature":"9fac5abc302bbcd46367973fd59ce6037560afb28185c5936c8c0ee10baf3f08"},{"version":"81833f8d76c4b585b1083147ffe2e81a819defe5067123b99fac522df2b94697","signature":"2d43165a9050c4e38892846fe05679fcd87ec750ed627b96f43feebfa43aaeba"},{"version":"2b6c7b3f1b1318387ea782aa5be83a9591cc8e1447967f053a0bcd12ad781330","signature":"cea0e10aee11176819ed82281791274ebd65f1befb38d190aac1d74a0b52e000"},{"version":"a6620c7b41052efe5d464acb253a8782dd9eb7e2f3dc2c335a15c3a39e14237b","signature":"51f78abea67d46ede962fd483206a4b536256b0692ab098fca0bca39fcad27ac"},{"version":"11d2f7b4a200c44f382332df87c13bc4ab0041b4947c4b3e55c41287a902ce7d","signature":"355893155b6e80bb1d801e19ecf8b00c1c837681b0876f0571afb9a12325e43f"},{"version":"8d5a652c5bf960a7a7970adcbcc3cdacf86ed7264171aa7dd5231a081c568d5e","signature":"dc3c8ca72eadb931568e0f393a9f6899d13a0dabfe5790000af8a1283a528aa2"},{"version":"df7611f6fe98422dab6c66b43fd70d62c31d190b904e931d944bf9798cde3444","signature":"5256546ff0ee98ff0e9a2094eadb783cf66ce5a46eea777e8215b682b4b10d9d"},{"version":"e425a226e117cb02225226e8d6e41b5184b06e5ce388713fcc00191cc9608356","signature":"00c2b559af7488dc403623c3f0ac67e0fabf5f7b11e93f5a00a2299085a742be"},{"version":"22a6dfb677630fc06f3c1ab654ff56d0e552ae6994c9281c594b593ee6019c86","signature":"6149b5dfb982efadf38000e5956b5fa482362a6e021fb4f3aa08748ab21f900e"},{"version":"f47c7a32b3a33823318cafa457bee692cde6a1d2290a18c446a46f95a29c85ab","signature":"df3c84f2f4c30f7ed7ff9e7bd9ce547b0fe4d901faafbf4295dfd64e7aaa678d"},{"version":"e71080e48e1bba8f91d721bde022001c0fcb8eb73f2b663d859e4a48b2bae81c","signature":"4d48980474af38c429dc09cbd4c4e3ffa8176a8c75e013d1a7101b4feb33b97b"},{"version":"1b80788bfeac9b129e7aa7625134ee25ea5f59486f820cbbbd9c591383edb82d","signature":"6663f58fd684d3191ebc40cf64d66c0078d2722fb6c84967ef285b09f98e7e68"},{"version":"d117c9acaac2e5c8d61f3cd7a574913d8c522b461ee221839afd7dd459558486","signature":"552f04e4d0005c53ba4321cfcf50ca09aeeb45762e87459d635ee0570fcd859e"},{"version":"5394a413e4c01677b46657453bd97c58423ade875920930440397620df53d0ac","signature":"5fb6588a3626c1652d15e2635db10f74c3390c0bbef850d2ceaa1f10e3cce011"},"57948437dcc7024f23c80945cd6dfd8b472614b160d73b2d42ee45cb01c6738e",{"version":"4d7d964609a07368d076ce943b07106c5ebee8138c307d3273ba1cf3a0c3c751","impliedFormat":99},{"version":"0e48c1354203ba2ca366b62a0f22fec9e10c251d9d6420c6d435da1d079e6126","impliedFormat":99},{"version":"0662a451f0584bb3026340c3661c3a89774182976cd373eca502a1d3b5c7b580","impliedFormat":99},{"version":"68219da40672405b0632a0a544d1319b5bfe3fa0401f1283d4c9854b0cc114ce","impliedFormat":99},{"version":"ee40ce45ec7c5888f0c1042abc595649d08f51e509af2c78c77403f1db75482a","impliedFormat":99},{"version":"7841bca23a8296afd82fd036fc8d3b1fed3c1e0c82ee614254693ccd47e916fc","impliedFormat":99},{"version":"b09c433ed46538d0dc7e40f49a9bf532712221219761a0f389e60349c59b3932","impliedFormat":99},{"version":"06360da67958e51b36f6f2545214dca3f1bf61c9aef6e451294fcc9aca230690","impliedFormat":99},{"version":"50bc55fc955fa799a4a1a11c3484077f06cc87033c68f5da6466b520876cb12f","impliedFormat":99},{"version":"0555222d33e7f5be196abdb5e9c1f65ded892bbb8008c7567ec09781e12fdf16","signature":"ba446f853a94d297818b1619d98f0b8c8c3c22eb005992e96ee8a713da8ffa62"},{"version":"bae4b6cc20e2f525e56254d06e774b2b9790b2c5d04e28852b807fde92b0b18d","signature":"f2e048e3ad15cfcd70f8e2303f0d23162f27fa4807757e02683210700339aa4d"},{"version":"ef29dc5b566427383555f9e389db0d17e7a90337e8a59e45fc1ff0004a81714c","signature":"f36a8db2263fc2fe5488bfa8ca7c737df57447f7db39595800298c3c70ca3854"},{"version":"b5f17e563ca43fff78f0238cd86be479546a15cc7091f403184fd912d19b9d87","signature":"963c2a3b2e53abed1302ff62aec14bce2a3f9784060bd3180de5c70d5145d141"},{"version":"6e9be985fc82eca7fd867c803f1baeef8443196062783391672d967e65539239","signature":"72c0cd5e567e7d5faf0621a1838c593231f9a810f4f3c6846f07f122ace09e40"},{"version":"feccb8bfca0e7b7bfc7e68e3a7cd7f4b35089007c56d01718b22ba9335b36c3b","signature":"3451e9ca965c68c5e582dfe07731d12ed49a55dd0b2c4cb131e57ff92486a9ca"},{"version":"90f7a56109b5cca9773cb2514a4aef73fc04e806ef0f13647967c5ee02812125","signature":"877796d8522483f9b57f36306bde0715ed2d519db26cff52cafb35ba2b2032a1"},{"version":"f1c17717b5f6e13311f105b055345940838c4aec26df3564aac864ecadd169b3","signature":"6f5706e199dec5b336c4f714123481e1511174241e232848866660458eb8b1eb"},{"version":"2e821e34791212a2c57b950b58cb8aa1ed1c8cc395d91513bc8392196c537d15","signature":"b3a7da67acca49e318d34f2453e43d62e8b583deba3db902466e5215f44d3403"},{"version":"30a1bb696377e33db61161acdfa3624d4ea1620ad91b1648e99e6cb702613773","signature":"4bce2cef1bf4a5894ea7d588e044cb2f9564e93efeaef9646d55ca1f7a93cf33"},{"version":"62d8e68188f506e2305c52c80e3893c6d2cead3e056ad6a0d403fc19cc75971d","signature":"604b1fd70119593d63f33840d9a8d343ba897cefeff151eaac4afbb7d20e854e"},{"version":"34e8fd52e671fcc6a53b631436464e0a02be5751549dacb542e500c914ccfefe","signature":"e8f86c45a3ba214c82f6349e8a0e124f661124b6be4f0b81887ec8a7beeb9b36"},{"version":"82a53ff4b7db8e943edb08e42c06eb5c41ac7adc3917b0a682032f5833819d4d","signature":"128b66558240654e7d7c9e15bc920647eaa13c9bc7802637ba68e1236bf7afd8"},{"version":"006755bee766f39d1f4f304d9f7e7750444c829dbba0cc7cdae7f26f7af29761","signature":"3d643d640e2fa236a4710079bedf031b2c08fe2ce9becd58642ba1e85f306840"},{"version":"d418151cbf765c1c6e9b465fe1a907eee0f60d18755bb7f47eb8d01ca69b58d9","signature":"41fd46cefa78095ffdb66d546473835cf068ed04d5c05ca0876a8f939b67a385"},{"version":"44ef4381f39e630140a3ef5682dd823a5ed7829f84e4e1b1b43bf96b9f4cf0ec","signature":"ca726a73a928bda6c27d5873f3e3e1327c69c7baa5a9cf9f19bd8a50683c7c88"},{"version":"74b551c80bf0e39e40ccec0583d04b465cb63a4de6135b1ffe0e45859bb3ac31","signature":"f59a391b0a6a9d0493173775e2784bd724e5eddcce7b96b9a1539a1835203b40"},{"version":"6174aec681cca42dddd61a1f5ac5f2633260b81081e771e811078df6c07d776b","signature":"43d354f979393f7a695f29226300cd869b7391efc15827f2d054d2799ce3cf83"},{"version":"992fec81909b0127aa2fd2aa7632e2fc880687210da03e441a14adcf663b6b37","signature":"1988ea4433ed5358f72344f0458036df36384fc9577fbd77d5ba261c46a0a5c1"},{"version":"a969ed96044c8fd0b580eca806037504cb564987d3172335d0469b204cbd8dcf","signature":"9d5ba1b70a3cf01909cdfd9e6665652e85cd320aff06cd18e5c13127900f1009"},{"version":"e8a7d2f46fe31854e8cc006a1eebcc75735dc2acc4baf6bbeacd96374b8af90e","signature":"f4b3a35d21ed9fa28605483d6e14618bdec7215f9693482bac2d0434b236c59b"},{"version":"be2140a7969dab064ebbca0499aa34fce68f3ef929f0da837853efd5bce9f217","signature":"79595033604ac169448461d7371bb79abff9c522ded3101c629249ac2d44f486"},{"version":"ab8583b84ec3374e8f373243086e614689f0b3558dcc0d46bcab5e87c819eeb0","signature":"5b7887ae3c98c0325422ddfd857566abcc96752c11c6e5a6d1961bef330b7676"},{"version":"03ab226323b4c542fe0d9701c0306196239dd5277bc4e63aacd3d8d804a3a2a6","signature":"49e2cf2ef33a42c094abc82aa5344e3506d3843637d748b50c581ead77a36d6f"},{"version":"f0386c9b6afd7a13ee65cd70054f7ae2c3880f537d7374603943dedce2dbfeed","signature":"ef02aefab55e48be91d7b0a51089dc874ff9bdef7fdecd47d4b0b8578325098b"},{"version":"981b479b8d9c4b601ecca190b1daf605f2c3ac71a6a2b79f8735e8f3462607bb","signature":"b1e82efbedf562a6ef7b7f5da47acc1cd2a00577258d5f484f935d702cb8a2b3"},{"version":"a3a9d5742ef909668627cebc3d1015a52f2bad82c0753afa4356f8e0e84cae6d","signature":"40eb508d66c06150f3ab576082189cc2ac348ea1c6043b8662812e1289445523"},{"version":"38d41e008a795cd17529fddc2623e063d32cf7ec976a441fda720f2d20c2f88a","impliedFormat":99},{"version":"91407743d47699adac8c5d7b98886e2557c7ba96c28a0271b186b02acb44c175","impliedFormat":99},{"version":"8b57396f46911d904aadc7eaa47ef54e83880ff2b78052ffe2f81b184f47a36f","signature":"61ed765f05edb47c8df2954354ddda71dd8db974b66e504f0ff1bcf18a4e9eb5"},{"version":"90dc6d5b12dcad3bae4894e97cb5114aad22edbc594ad4c209a152f8e53cc44d","signature":"602d69063fd721a3da0bb5430d4c3d47042bec45fa619e313103e468988e41b9"},{"version":"41a3c233eaf120c47c05501d5eadc56948203af00403838bf74a722ed028a313","impliedFormat":99},{"version":"3670561369ca68a09d52476b583259506a7fb405f8766a1be016d92a795425df","impliedFormat":99},{"version":"c18e72b043d29d83286b33917b380ea78dc695fd31e2834e17133a17539a94bb","impliedFormat":99},{"version":"24f97620d161b9c421246ba30172522e00cb21524f8ece75e275cdf30e22eab5","impliedFormat":99},{"version":"43a1c4c14c1d87ddfeec20e9b044c69db6da86fe868ca8833b8ab5c81b326c68","impliedFormat":99},{"version":"a5a72c4adbbbd805860d5c2b847e25b45b3a6dcbdf471cda1e206c7c4e9f6bd6","impliedFormat":99},{"version":"acca062f4ecb504ba5a1a479b3c2ffb2a6957925f83ac7efdb0b0aba11aefa1c","impliedFormat":99},{"version":"72daabbd93e93b80aa2de51f9e039cf7fcff47c51a9154425cc6a902c3522959","impliedFormat":99},{"version":"2099dbc466f37ac4364a71360471f844929db80aaf41629b260852f075b7e95f","impliedFormat":99},{"version":"38d63c3e7dff35365e72859b041a09b0ba3507e1fb10198c64b2fa31c1f4c6ae","impliedFormat":99},{"version":"24397529aec71e82a879b881f6179fbafb23fdce7aab0de18d4eae73d77d32e8","impliedFormat":99},{"version":"e0e20dda0bb709540cea318a9cec834ad4e8529f2f53234a23094343efc8b19e","impliedFormat":99},{"version":"758359ab39eb58fdc1340f5ddb5743b9bd5f7f5f37d58202b01a0de2fa6e38bf","impliedFormat":99},{"version":"e9cd6b1c0f5b49e2771e96ddcba10cab43d9df34c80bb6ed5a5fd31462cf23c9","impliedFormat":99},{"version":"bb573d0b9de06c261a63b6697dc6148421794a51566690b36af5e616962aa980","impliedFormat":99},{"version":"c9f2ebfd4bdc356a24c0a659efc9ab4832a246fff1cd713729e3997740efb57a","impliedFormat":99},{"version":"0630f136ee5704beb9e4c905abe59ee8cf3feddbf26d9fd0d7d9c226082aafef","impliedFormat":99},{"version":"585638c97811f86301c6396518f349e4f692c7b66e9946a794a828483c804ad6","impliedFormat":99},{"version":"1bf1fcf895db1e2a2221a75cbad91ba1cdf654c0e73018398453f127eb14682a","impliedFormat":99},{"version":"92274791f9af7731fd88b2ded67cd684099f2d268238fe2c099924c6952dc3b6","impliedFormat":99},{"version":"523101fd85530559aa9f0175a8c6ce1d3a80ef2234ebdd13d64d6b81361e929a","impliedFormat":99},{"version":"8685357eaac6a23e2c808cbfe4f73d2454f65c54acadea1579ab4b5755e6b6b3","impliedFormat":99},{"version":"1a0cc12da0de949c4dc73aecd3c3896285f0c1af51a663655e6b71a6130c3e82","impliedFormat":99},{"version":"04004ba4ff682db11df64bf5d5cb1c09616cae3c424e41d18b00bc14ac4f7b6a","impliedFormat":99},{"version":"d95236c56f7f89c2ca50b0573fbdc889609dc87efbf09fa57fcfc2daddfcbf3e","impliedFormat":99},{"version":"8090920245b6bb972deed05e9592ca886a017701fde5aa9e4559714863586823","impliedFormat":99},{"version":"cbbb0bd07f4c7491d2f9af50699a8ec2b22351636335e6275e037ddda7eba7b2","impliedFormat":99},{"version":"61f43d11b4e1548c0197bf72094864d16dedc43371f92c07c44de83473e6369e","impliedFormat":99},{"version":"4d54b2df904582f2b7fdc63a7ea3e0196b74f96cfc72a524770f7a8ba1172d05","impliedFormat":99},{"version":"1a46550a0d8f494566e69a915451468338f1a3331e3ac4afbb52bf0d52bcd647","impliedFormat":99},{"version":"f0d2acb0f37fc5fcf4829220a88bc0e3b5fc8f6d4fd18c30e960c4c31d0998f9","impliedFormat":99},{"version":"62468010f26e1398fa8d2c787a24b1920264497f70b1291697170d51e8c93d15","impliedFormat":99},{"version":"ee40b5746f4aea666abdc84469686077cbb0b1f9e128ca15c7c2f6318fd2faf2","impliedFormat":99},{"version":"11a4440d50d17aae2021ed56bd723619d2f3526966b66b8ee71f99f90aee001b","impliedFormat":99},{"version":"0e7444c2c5c62592662463f953ac5e51f7f7fa02730704e76494bd35659621a1","impliedFormat":99},{"version":"526703c640b52e2a6e362f03dc3c2edc21649a3ca73503d2848ff6f124c2456e","impliedFormat":99},{"version":"5ddd6faa3ef0097ba3c0d64ef86021785d54aca62d672d60f7dac465e15630bb","impliedFormat":99},{"version":"c1f0e5b2a899399fa1788ab79f55b8908841a7dc01baaa8fbc9468fa1a705dc6","impliedFormat":99},{"version":"1f32b1d6620d7831af97596fe6cebc1d4796d65b16592fb6ed542bf2aa580dbe","impliedFormat":99},{"version":"887f38bd46cd7bd27122853c6a3a3f19f93de4138d345bc01f25d97702afb856","impliedFormat":99},{"version":"e95243b24b3a9f410886c29e4a6f05b9174257e0da0151e742f73f65fffe6860","impliedFormat":99},{"version":"606bf8ab190fea5213f45fa8d187f232f5ede10db66c4d2640e466b1eb010723","impliedFormat":99},{"version":"ec66f21c2debd53c8dc23034359ef14fb315cbf911a1c2268af7fd2c61d05538","impliedFormat":99},{"version":"87a87fe8dc65ebaf5d1f296fca81a3a7b02d99d9955dbbd240a6150052c0aafe","impliedFormat":99},{"version":"88cea16c4f051deba1578741810dde95361a3f3d6198829eee1c7aa3b27f5602","impliedFormat":99},{"version":"2f1207758095b2af21b93256eb916a8e3ad916d259c0f8f2bb212b7f00700eb4","impliedFormat":99},{"version":"a5e9081741bf85356d256b96ed191a407e674733ba3c634ff36b2c5498789337","impliedFormat":99},{"version":"0da2fc8007d5d7cba3fe5c9db6a3517e1253409397686f218f7bc780706eb30b","impliedFormat":99},{"version":"b49949a16a4cbe1bca5a73e165f5d46431cb37040f4558df54d50df429c2c38f","impliedFormat":99},{"version":"19b0f0727865e775d21de405d2700c3196f6621bfa619c8c73ef1e1ad964ff55","impliedFormat":99},{"version":"40525b8ec87e8e63e18975722a0f02313942962162fad09caa212f6858effba9","impliedFormat":99},{"version":"cd0e5287f17e1eb6527b1e8b9a706a27e3cbe77f9926430c083fe908a89e2cdd","impliedFormat":99},{"version":"9aacfa9c2cca9598ea4ab8168f77629258e3349292ef281beb8be4948d18228a","impliedFormat":99},{"version":"a3db855047aca46f97fc3b4208b5032ffca77a88985a5e377e85807ecf90bb33","impliedFormat":99},{"version":"e6dbac9ac42976ad196b59756bb60b6588e582307d8c75a4e7971e42e455572b","impliedFormat":99},{"version":"644df01317e55aa6b46b8f3c3c5f9f90eb4947c23dd059bba22d78625005bbfd","impliedFormat":99},{"version":"76a7523ee41103396246de53afb3e103abb7532991de1f3f8fa8384a605ad611","impliedFormat":99},{"version":"1cbcef88c4eecb1de19882fe26349a5cfd44f7b3d5eeaa77f2d0926767cf4495","impliedFormat":99},{"version":"5241e12bc1969d2e0afc0ce91f2da1fc76017ea4e7564b0395f6b8ce35f60d97","impliedFormat":99},{"version":"279ed1bfa8cf4829fbd653c446d786ac5e2a0cef055fe017deabd3076a6fce49","impliedFormat":99},{"version":"bdc8be81152eedd261b069237d84bfdfdd9b5d998a89a3133c8590001df12b6f","impliedFormat":99},{"version":"d308a5d41f15e3a15443ac397c007bd251f4aa046fe4df96db55730b186225c6","impliedFormat":99},{"version":"471d7a10e4396aa877d9cd6292967c95dbfd07fbf62742df615ade1b09e8b435","impliedFormat":99},{"version":"a2a48a2515b88aee6190fa34e149e084ddeb4e9412217ae86fe2949701a835a5","impliedFormat":99},{"version":"8c4a3b1dce4fe45431ca488e303b4d44e4ad66c18eaba9bb3fa6512f4b642a53","impliedFormat":99},{"version":"cba1b57ba0b082720cec477229bcf0d1b701d6a5e3df9d7167e8010bdac15508","impliedFormat":99},{"version":"cfc0c66f91c2c6f41fdc59a658e6e01698c17c13b447911dccc9074167671ee6","impliedFormat":99},{"version":"947ce55683a307873d47ff95a503036b70beb5fed68e4c10dc219faf8ca8b530","impliedFormat":99},{"version":"f0c625c44dcd9eae0094c138637c7e4e333782574df09903050d8dcac006053d","impliedFormat":99},{"version":"d453aa5944b07ef8d739296d9547df75c19a665f0b17ea085640ef9f02caced3","impliedFormat":99},{"version":"e14025e566058d9fd7d9baae9c937a9ad5220ddb87ffb250583b95a85d4d527f","impliedFormat":99},{"version":"0143111a64deefba83c688e35d9b81b6c2688c0323831c411c4fdfba72220b6b","impliedFormat":99},{"version":"9b02af01eaed2a35b94ffd191a27659a70dbe4b9ffb369d8d791c3070eecd3d4","impliedFormat":99},{"version":"adfa5d8d9788fd85749e849cb15277b46a1306c117b4dd73eaa4e35586b67e62","impliedFormat":99},{"version":"c9386c27a10b6a607d8583dd0cc0cbb8dbb5e18e4e0eb3f85f9a45baa9671546","impliedFormat":99},{"version":"a3d092ee18c11b913571df6019a224bbd07ab14830accfa31558598852a7afe0","impliedFormat":99},{"version":"30bdf14c7ed60f3a3d9998fc862800b636afd861089aef669c49266576d5f78c","impliedFormat":99},{"version":"bb3caf11814f7265729f580f52f463d6ffb6b755d9bbe8d4aeb9848c5fef26fd","impliedFormat":99},{"version":"cb9f1584cfdb5f4b9fdf0b838cc2872f0cde4dd7ec6345fd99264ed0bf71831b","impliedFormat":99},{"version":"2454f5d5b0a053c9e65f62bc73905e86e5dc113d86aace26fbea1e74902bcc0a","impliedFormat":99},{"version":"9d5a231dc0db79e4c0381581956d35750956c067812867e3f9d379b9b43248d8","impliedFormat":99},{"version":"378885d39de12f15e3960dbe68916154a6c8dcbdff72df5d09b2b504a9a65565","impliedFormat":99},{"version":"8ff9ad3a9f35597251c664432033f5f9db25a71b887bb7c5f3111fd57755f32b","impliedFormat":99},{"version":"d91b2cf7b3baa8302d63038d8612b1aab48efcc0bf5f9b11d3bdba358b365308","impliedFormat":99},{"version":"59274bbf27983be9810f88969e30c1899cef2f07e2ad37a17aad504466cb6cd7","impliedFormat":99},{"version":"0ea47f42ee816b289fd31ae2dd431203439392f34949180847e910d1f236df5d","impliedFormat":99},{"version":"7f4b0d988fffac25e8b99d53344e6cae20da15912496a892dc113d98982be6f5","impliedFormat":99},{"version":"446401e1e324921d25439a67f39b2356fc78c4f0e0aef23666379d84e9cf89f0","impliedFormat":99},{"version":"3886d0501785ef1a7139fd5360758c4e06fb370768e7abdcb7701dd89ee2c0b5","impliedFormat":99},{"version":"da36e84c3469c725c3f95c88922e3a9a38d75bbc9b5235711b3882609b7bb690","impliedFormat":99},{"version":"fb2331578931cd9b4a074dad8f761f5d7bf4ffe4be1cf42168de580871f81676","impliedFormat":99},{"version":"13445ea5f883f11166f5c1bb7d10f9fb602ca04c8ba37f1fce541b99dbafa440","impliedFormat":99},{"version":"4e69f563abc4910a81f9bd9ba16eab762ad94f1805aaaaa356780fb44d2013e5","impliedFormat":99},{"version":"729f5bda320a664a3b427633d4608fb2de329a54c0c9126cf37bdef529770c98","impliedFormat":99},{"version":"4eed6be4e1f8c3b30b53760af31120d3cefd981a23c94af3c2f33b13ebb6d88d","impliedFormat":99},{"version":"3f2b7367ee831669c773edce5547bcad1159e045f4983c0cbb1acac86d5b5cae","impliedFormat":99},{"version":"ecf6628b6de4ab2097eeb61d810a8e8f83b2f32cbb25aeea3f3e02aa87b4db50","impliedFormat":99},{"version":"b5c1a561b4510dd597fef4380a16e642d6ef3cce51610f0a10fa551ef21633f4","impliedFormat":99},{"version":"bef6e54976d3bc274ef623cc52c323228df7e71a18d7efb8b8326bfe841a8662","impliedFormat":99},{"version":"a112b882bace2440d365474987e0d62657ec0b05821cfa11c5bb69bc51b62634","impliedFormat":99},{"version":"36d2861063b4466b217118725f63fda2001067bfed6407057567d41560c8850f","impliedFormat":99},{"version":"bbc65478104dc679951ff2988d1fdecbd066a9c6ab469eb5b994ea271b7fb724","impliedFormat":99},{"version":"9438e581b06ebf30041297db2ecb993e732b897e29f653eea7719e7904758e93","impliedFormat":99},{"version":"4c413d48eff8f50b8b3f361f231f07707241174eda70dcdff163626dd412e363","impliedFormat":99},{"version":"445653b1ccbbf22009cad4953757fb810f65d313e60526be256665dbffc76a93","impliedFormat":99},{"version":"9cfb34e97729d62aa06bd1f237ec107ea08f7d99874fec5842b4c40ac3767f29","impliedFormat":99},{"version":"358f53870afb50137b0febe4dce1c5e1875553cbd1dfbd59c911d8757d751459","impliedFormat":99},{"version":"5fafb533b24b3cc866b94153881b79815b0cff20969b1b132d266669b6faeff9","impliedFormat":99},{"version":"36cc6e9ec80b9f87389af9dcbffe3554766ebc0eef19eb832c35d3b2b57fa4b7","impliedFormat":99},{"version":"57dcc891c7ce218c3367b8919f93111274361caa23ddf631f6728eb4b69c73b9","impliedFormat":99},{"version":"250c818258ee810d6686d71c3e742803a3cb366064174ab47804921152807c2d","impliedFormat":99},{"version":"0176e8839a101f835597b1ba38e64b2ce7efe98ed63ba893d37968757171c04d","impliedFormat":99},{"version":"aff778492d364e97e647357629660027281eee4ff87761e73cf6d536bc703456","impliedFormat":99},{"version":"90dd653f3958e52ec5e566299e000bc9d4469b0faa40e32931f90d20eb5483df","impliedFormat":99},{"version":"c4272a849d65ce90763bedc02ad31ba374aab2b542d69fe7c79da2c689241678","impliedFormat":99},{"version":"24f01c4507f2c5527b8fe1cbaf25d3439304c503b1825fe0340e06ba319ea1e7","impliedFormat":99},{"version":"072f8a0ccdcb05fc0ef047ab63e057e723caa7b60d193cf0922ef86e42205a52","impliedFormat":99},{"version":"077dceca4e374e246b03df8e185408f22d317ce1b33db22c0cec9a59b5753e7d","impliedFormat":99},{"version":"99d37f9ff252cacfddd4744f3d1f736fbe733b762c6de6dd1d48059ba46beb4f","impliedFormat":99},{"version":"ad61b1b1341264580b5eee0e6e8f107cc1ee47675bf4476f4a05d28b8440b8d6","impliedFormat":99},{"version":"d783a003c0c534585704f16f435ad66185d32fd90fefdd60db3a60da6659336f","impliedFormat":99},{"version":"1f8b4a56d208e8d44879dc87bfc23e37a8d6282f3873b785c87559a2fdcfb5ff","impliedFormat":99},{"version":"5c5602a73d94e9f230a13aa4e20758ed7126f107187c70e4375aef40f6731b56","impliedFormat":99},{"version":"fe3dd75e8dea26d91ab4b5996d38f63994297e955d5060035c35cdea5c4637ac","impliedFormat":99},{"version":"4a5f2bff495d0276b6635d90ac177c0364a67b4fe0399cca1dbf4b4da7d770a3","impliedFormat":99},{"version":"b3bfeaa99d47de0a2acde802241b587d4ce8e3fda00c2d82d11ae63bfa1bd292","impliedFormat":99},{"version":"6d1d185a1db2bc415994b4256a5e62515869add07e7818070ead666168719dea","signature":"028baa65b32f4e285d499dcc7f1277424d731f86f2dd63e8bc9a378858b55839"},{"version":"84e355331be36b794c48e777caafb35afd40a56928d8f3092898fbca149495dc","signature":"cb73bb30cfd3e285e8ad5d9b27c43067a636f4cb82d7a2737ade85ea81d23d2f"},{"version":"324f8860d76fadff9939e31f46ec649efb841446c0199d17ccbd82799d7770b4","signature":"c935537b8d7a55870a339fba8c8317bfa5ba6635e9fcf152f6821724e0ede8b7"},{"version":"c9beadb45ad904ebd3ec1ed112c523828ac9df0171de7a8e1dc4f1d963ef233f","signature":"271d39e82290269846a9d48c6632bf452c85733c9b63d795a94af2ad99c2f565"},{"version":"86a871bbc3bd89cd5f58a5f27fea40ced742d47dd75b4c2d1e258fef3abe2f97","signature":"2768868b308e0bc8124ed7a3815bc7cd68af01d3081e2f49813b5f2a56c492d1"},{"version":"ab4b81fb87b400abe57962ce107cb6b84dfc18d487651745c61910e3c4b4fb73","signature":"093f39c4226cacf0c8b7681419330b3f0838aee56efa5a226d9b23fff24bf105"},{"version":"76b696eb4559199439db04652837e158577b904a03e87692962118928490361e","signature":"ccedbfa657c9e51d4473fcbf080dedcb0c3bc7c78b7d4c31804eace72847a410"},{"version":"abf70f24768cf5e9bb78e45d91e302f51cb18149d436aa2362a3baef5a8beef3","signature":"02d51df70bb92c9090062774118e617480e2cbc8066208068907e00298d0152f"},{"version":"09c80ef840f4b7d31eaf1360dec8404db9976e4a48a3f5293bfe7590cfe6ea80","signature":"10462d3c80d5a3e8570d87f31ce10c1767b67c4e63bf4980a2805b36c1a73d39"},{"version":"7ca5dcede3e372947ce0112245be4f982ab0a1c69eeb40f80467a0e3246a8218","signature":"3320a8f8ccd6d4ad2c8b89e3be7297661beb2c796ea2bb1995f27ef8008fbfb8"},{"version":"42bd37579e6500a66958e0b00e2fffe50e41cffcbde0a5aaad0527efde5f9d4b","signature":"d7c1b98a2382d6fa3ca3ebcd8a157f2a428129bfb362fc7c909bed43a4d198b6"},{"version":"7dc248bfac1a29ee5a3c9a5c7d176fb11fa8b040b3b9fab350690fda029771cf","signature":"4109e8e7f7f80b7091f21f9f3ba27c594f67db0368e1331dd1bf6308a1b1daa7"},{"version":"024829c0b317972acf4f871bf701525f81896ad74015f1a52d46ae6036205cb9","impliedFormat":99},{"version":"a9373d52584b48809ffd61d74f5b3dfd127da846e3c4ee3c415560386df3994b","impliedFormat":99},{"version":"caf4af98bf464ad3e10c46cf7d340556f89197aab0f87f032c7b84eb8ddb24d9","impliedFormat":99},{"version":"0943a6e4e026d0de8a4969ee975a7283e0627bf41aa4635d8502f6f24365ac9b","impliedFormat":99},{"version":"1461efc4aefd3e999244f238f59c9b9753a7e3dfede923ebe2b4a11d6e13a0d0","impliedFormat":99},{"version":"7ec047b73f621c526468517fea779fec2007dd05baa880989def59126c98ef79","impliedFormat":99},{"version":"8dd450de6d756cee0761f277c6dc58b0b5a66b8c274b980949318b8cad26d712","impliedFormat":99},{"version":"904d6ad970b6bd825449480488a73d9b98432357ab38cf8d31ffd651ae376ff5","impliedFormat":99},{"version":"dfcf16e716338e9fe8cf790ac7756f61c85b83b699861df970661e97bf482692","impliedFormat":99},{"version":"31c30cc54e8c3da37c8e2e40e5658471f65915df22d348990d1601901e8c9ff3","impliedFormat":99},{"version":"36d8011f1437aecf0e6e88677d933e4fb3403557f086f4ac00c5a4cb6d028ac2","impliedFormat":99},{"version":"8085954ba165e611c6230596078063627f3656fed3fb68ad1e36a414c4d7599a","impliedFormat":99},{"version":"2c57db2bf2dbd9e8ef4853be7257d62a1cb72845f7b976bb4ee827d362675f96","impliedFormat":99},{"version":"6b5f886fe41e2e767168e491fe6048398ed6439d44e006d9f51cc31265f08978","impliedFormat":99},{"version":"56a87e37f91f5625eb7d5f8394904f3f1e2a90fb08f347161dc94f1ae586bdd0","impliedFormat":99},{"version":"6b863463764ae572b9ada405bf77aac37b5e5089a3ab420d0862e4471051393b","impliedFormat":99},{"version":"68b6a7501a56babd7bcd840e0d638ee7ec582f1e70b3c36ebf32e5e5836913c8","impliedFormat":99},{"version":"89783bd45ab35df55203b522f8271500189c3526976af533a599a86caaf31362","impliedFormat":99},{"version":"6da2e0928bdab05861abc4e4abebea0c7cf0b67e25374ba35a94df2269563dd8","impliedFormat":99},{"version":"e7b00bec016013bcde74268d837a8b57173951add2b23c8fd12ffe57f204d88f","impliedFormat":99},{"version":"26e6c521a290630ea31f0205a46a87cab35faac96e2b30606f37bae7bcda4f9d","impliedFormat":99},{"version":"71acd198e19fa38447a3cbc5c33f2f5a719d933fccf314aaff0e8b0593271324","impliedFormat":99},{"version":"044047026c70439867589d8596ffe417b56158a1f054034f590166dd793b676b","impliedFormat":99},{"version":"89ad9a4e8044299f356f38879a1c2176bc60c997519b442c92cc5a70b731a360","impliedFormat":99},{"version":"fd4f58cd6b5fc8ce8af0d04bfef5142f15c4bafaac9a9899c6daa056f10bb517","impliedFormat":99},{"version":"2a00cea77767cb26393ee6f972fd32941249a0d65b246bfcb20a780a2b919a21","impliedFormat":99},{"version":"440cb5b34e06fabe3dcb13a3f77b98d771bf696857c8e97ce170b4f345f8a26b","impliedFormat":99},{"version":"5bc7f0946c94e23765bd1b8f62dc3ab65d7716285ca7cf45609f57777ddb436f","impliedFormat":99},{"version":"7d5a5e603a68faea3d978630a84cacad7668f11e14164c4dd10224fa1e210f56","impliedFormat":99},{"version":"2535fc1a5fe64892783ff8f61321b181c24f824e688a4a05ae738da33466605b","impliedFormat":99},{"version":"cbfd5ef0c8fdb4983202252b5f5758a579f4500edc3b9ad413da60cffb5c3564","impliedFormat":99},{"version":"9f7a3c434912fd3feb87af4aabdf0d1b614152ecb5e7b2aa1fff3429879cdd51","impliedFormat":99},{"version":"99d1a601593495371e798da1850b52877bf63d0678f15722d5f048e404f002e4","impliedFormat":99},{"version":"1179ef8174e0e4a09d35576199df04803b1db17c0fb35b9326442884bc0b0cce","impliedFormat":99},{"version":"9c580c6eae94f8c9a38373566e59d5c3282dc194aa266b23a50686fe10560159","impliedFormat":99},{"version":"cc3738ba01d9af5ba1206a313896837ff8779791afcd9869e582783550f17f38","impliedFormat":99},{"version":"a80ec72f5e178862476deaeed532c305bdfcd3627014ae7ac2901356d794fc93","impliedFormat":99},{"version":"4a5aa16151dbec524bb043a5cbce2c3fec75957d175475c115a953aca53999a9","impliedFormat":99},{"version":"7a14bf21ae8a29d64c42173c08f026928daf418bed1b97b37ac4bb2aa197b89b","impliedFormat":99},{"version":"c5013d60cbff572255ccc87c314c39e198c8cc6c5aa7855db7a21b79e06a510f","impliedFormat":99},{"version":"69ec8d900cfec3d40e50490fedbbea5c1b49d32c38adbc236e73a3b8978c0b11","impliedFormat":99},{"version":"7fd629484ba6772b686885b443914655089246f75a13dd685845d0abae337671","impliedFormat":99},{"version":"13dcccb62e8537329ac0448f088ab16fe5b0bbed71e56906d28d202072759804","impliedFormat":99},{"version":"233267a4a036c64aee95f66a0d31e3e0ef048cccc57dd66f9cf87582b38691e4","impliedFormat":99},{"version":"ccb9fbe369885d02cf6c2b2948fb5060451565d37b04356bbe753807f98e0682","impliedFormat":99},{"version":"2fbe402f0ee5aa8ab55367f88030f79d46211c0a0f342becaa9f648bf8534e9d","impliedFormat":1},{"version":"b94258ef37e67474ac5522e9c519489a55dcb3d4a8f645e335fc68ea2215fe88","impliedFormat":1},{"version":"b613cee05aa862b1004d8eef06ee56649e8a628b8ba2452ce42ca778d34ee363","signature":"c2b32082ae6fa2fbf67c6e5b69d762ffae35158ed2901b5d16da1e5351fcf735"},{"version":"6c05d0fcee91437571513c404e62396ee798ff37a2d8bef2104accdc79deb9c0","impliedFormat":1},{"version":"44433e18e092339f0f547578cf8da911c99ef9f1ac31cf14c6ab8d05891508ee","signature":"28146598b49f0e770cb4e9a3056d14a8962816c7c8d9031fc36995f65d73f452"},{"version":"9780540210ec95e12504069cc1504296c636d193524f4bbc79e99dddacea69c9","signature":"0a5a9f14c5cf8c76f560e44248244813b1ebbea85f58b3bfacc20e61aabd1327"},{"version":"d04f947114fa00a20ee3c3182bb2863c30869df93293cc673f200defadbd69d9","impliedFormat":1},{"version":"4c629a21fb1b4f2428660f662d5fef6282e359d369f9e5ec5fd6ac197c1906ee","impliedFormat":1},{"version":"785926dee839d0b3f5e479615d5653d77f6a9ef8aa4eea5bbdce2703c860b254","impliedFormat":1},{"version":"66d5c68894bb2975727cd550b53cd6f9d99f7cb77cb0cbecdd4af1c9332b01dd","impliedFormat":1},{"version":"cf62a7edc0f8e389ef165baf6d664cc20eb272de3e5ce056064031ffb0c452f0","impliedFormat":1},{"version":"c991b4ab8278b08f73510d383ae74a9df03a876ba4aa66efa9d87d0bfdbf486b","impliedFormat":1},{"version":"232b55f1b20b63510304141f6c727f30d9e5bcaa0dbc7ca314d3433ae17e66ce","signature":"1043c497f77ed2451a49a4fde80a171b375ea0bc554ac33d929ae2858abba968"},{"version":"e15fd514d3cd04d013dee2da6552007114cd17534c053cd518fade984092e375","signature":"f928b250f56464d321a512ae80ae9a9cdbeec853f3c93f672f426e42d4a64059"},{"version":"65caacfa38aa1a1d355fa23afdfe3348e02d5afea3f9d420d1bf1244d742c535","signature":"edcfbd6f6493084dda84c63c9417cb1d416590b50a6400ef376380dd4b368cf6"},{"version":"cd022c5a34f9de91d8130b4a7ff9600fa17f3129e7b68eb27e8bac396da94e27","signature":"843330ee2523524ab81205761189fbda4045934773deaac8304a653a89c2b26a"},{"version":"afb5c44616d15e04c44ac6ee0abdb32983c1134a5dd3827f1bc701d73fb6199b","affectsGlobalScope":true,"impliedFormat":1},{"version":"fe93c474ab38ac02e30e3af073412b4f92b740152cf3a751fdaee8cbea982341","impliedFormat":1},{"version":"f5705d196b442afbdbd971b6e44bad96f4e32afb53cebfa2e5afe3140017bfc6","impliedFormat":1},{"version":"1e00b8bf9e3766c958218cd6144ffe08418286f89ff44ba5a2cc830c03dd22c7","impliedFormat":1},{"version":"65cbb4a386921bfc89c179edd252af3111ef3b2bf1ed31cdf9c6971753ab284b","signature":"cf48fd24ed6fedc83571c12f4b61772768d19072695ea29872ce333a83b223a6"},{"version":"f16bd9ebe1b8bc63cf0b20b452d54036e7aa8e53ff70020d3fd2e9fe31fa77e0","signature":"325cb56ac5a8ae580ae4ad38545f301d38be5af6e06df0932d26143614b46545"},{"version":"a59526ebfcf02aa335eeef4d7c5ef4ee4ef47f8a702fec5efd22bcf34ca6c05a","signature":"0bc39dc388297e2ce321bbcc9f75407e1afa56a456c14a5deb533c1f6683ee2b"},{"version":"1a9394db306975810471381adabb6e56e9303ee877ada9a231566346a548b2ba","signature":"a4cf55cf0916b2b5b71bc055a596154bc44faf5b4e1f2dcf383cbc03fc4079be"},{"version":"7b3da78fdd3146c85afc94d1237b5ae56472bc48fca17a7502317c183f603118","signature":"33e8cebeb0e9014cddffb1bd6967251edca2c0e6f4b4607ffd054f0f757eb3c6"},{"version":"9e6a0cfebc97aa1f9b7bf2557382ef9f2ecfa91baf2dc5d52d114a6915237205","signature":"9e2b7a970acb37795476eb2e0e6ef9397c03a82abbba1e0bce24e23879279d0e"},{"version":"b64fe8de7ecadcc5b77a2bfb05979f07f687c4ceb371bbea04c63fb29e6e44ca","signature":"70b276f94971173c963d21dcf14c41aa82c0466373cda35f0dcffda72aac78ac"},{"version":"8c7da38e08e4ee7812e78c92b2df464c14df622bea24a30484f1b332a0e79779","signature":"c71f02f55f1f534aa71a54a16e930d47ca35aa9699ae7fe40d0b0024c35b201a"},{"version":"1c8dba38cd2f0b450dea3cbd776b5230436673e3933d21b7e89223ae7964d498","signature":"69cf45f0e77e26059c62a166ef6ca55c4f2c4cd9b4f766efb0eff62d5fcb4a67"},{"version":"e11e18d5b8d1a6fb1cc6eea0bb3fe8676a5b2cf716036feea8b4fff336ca6640","signature":"8fc67ddd42c820c7ce9d330fd429fbf2da5362b5424f80c2a5f3244f5f50f6c5"},{"version":"d71968ed1142146cb1381833baf1f5aa6556e00397af4f1eaccddb35662be6f9","signature":"801e74ce17d94e107be303c816254216fb816277b7d4c584e9f910be669fe0f6"},{"version":"108f68b95944ae7d83b4d099e3f04ec7cb3245e2c1e1caaf2cf642a5097d44d7","signature":"741b040c5ed3e80597053f0472b1cfa6e5f3e6fbbc5961d182327585f5769ee4"},{"version":"5cc035b7c8cd0c6b0c0a7c2e68e54c0e084982b41ba448fe856287682d3b50d0","signature":"6ce028076d966ca2177da2af76b859abc1a6a1f5252ddd2831d3668b81af8095"},{"version":"a81a0eea036dd60a2c2edc52466bb2853bef379c3b9de327fe9fff6e3c38e6c5","impliedFormat":1},{"version":"348c13a1c9160681e41bc5cd3cc519dd8170d38a36a30480b41849f60f5bf8a0","impliedFormat":1},{"version":"c772a37a02356897d6f9872e30fcc2108f43ad943cc112bd1acc5415a876e9f8","impliedFormat":1},{"version":"279248c34ecd223fc46224f86384ebf49c775eb69329ad644d3d99f1205f3e7d","impliedFormat":1},{"version":"74dedffc2d09627f5a4de02bbd7eedf634938c13c2cc4e92f0b4135573432783","impliedFormat":1},{"version":"1f2bbbe38d5e536607b385f04c3d2cbf1e678c5ded7e8c5871ad8ae91ef33c3d","impliedFormat":1},{"version":"3aa3513d5e13d028202e788d763f021d2d113bd673087b42a2606ab50345492d","impliedFormat":1},{"version":"f012173d64d0579875aa60405de21ad379af7971b93bf46bee23acc5fa2b76a4","impliedFormat":1},{"version":"dcf5dc3ce399d472929c170de58422b549130dd540531623c830aaaaf3dd5f93","impliedFormat":1},{"version":"ec35f1490510239b89c745c948007c5dd00a8dca0861a836dcf0db5360679a2d","impliedFormat":1},{"version":"32868e4ec9b6bd4b1d96d24611343404b3a0a37064a7ac514b1d66b48325a911","impliedFormat":1},{"version":"4bbea07f21ff84bf3ceeb218b5a8c367c6e0f08014d3fd09e457d2ffb2826b9c","impliedFormat":1},{"version":"873a07dbeb0f8a3018791d245c0cf10c3289c8f7162cdbbb4a5b9cf723136185","impliedFormat":1},{"version":"43839af7f24edbd4b4e42e861eb7c0d85d80ec497095bb5002c93b451e9fcf88","impliedFormat":1},{"version":"54a7ee56aadecbe8126744f7787f54f79d1e110adab8fe7026ad83a9681f136a","impliedFormat":1},{"version":"6333c727ee2b79cdab55e9e10971e59cbfee26c73dfb350972cfd97712fc2162","impliedFormat":1},{"version":"8743b4356e522c26dc37f20cde4bcdb5ebd0a71a3afe156e81c099db7f34621d","impliedFormat":1},{"version":"af3d97c3a0da9491841efc4e25585247aa76772b840dd279dbff714c69d3a1ec","impliedFormat":1},{"version":"d9ac50fe802967929467413a79631698b8d8f4f2dc692b207e509b6bb3a92524","impliedFormat":1},{"version":"34d017b29ca5107bf2832b992e4cee51ed497f074724a4b4a7b6386b7f8297c9","impliedFormat":1},{"version":"b75d56703daaffcb31a7cdebf190856e07739a9481f01c2919f95bde99be9424","impliedFormat":99},{"version":"b646f38fe19c1d8fa72b4736b329cb3f035410547a9158304ed6020d9d858bc8","signature":"2e92e26a539957daa7a99cd7e4a54b576ab7acf9214997cf9461c0ef866a28b1"},{"version":"6771ef5fbe637e8134f7967dc1238a102d47c4565d55d8aaabaa504e33b883f8","signature":"55028774add71c8a553cf1bc62515f028ccd5a9014b467a3f83515750738070d"},{"version":"1a5c8052279d906e2d5abf2f866b126c1d702d199abe0b0171d9a111aa1bdb49","signature":"4f25ab40289e8e4900dad829c06b6aa102268cb39963790949426e6f6e5508de"},{"version":"17959bea1c07a02e960a6abb6dc2b76fee68b518f6768db33e3bf81211acc99f","signature":"5d5d6411d6a2c9a11baf2b820a2b3d1e37e9f696e9bf5d8f2ffd1fdf172d09c5"},{"version":"c956a8498035db49b3a351fe4f7da36c155ec15d27bb44802b8349ebfc34160a","signature":"370475cfabcb3b420f82852ce94d1a6c4e0ee2478a14f39829ff02bc79ceb028"},{"version":"61548add80e1cfe788b6fd3af8256c097ca0c3a033f861de5662ab0fc9b38d63","signature":"d870380b279ca88c89db97f332a61f051fbc5000b151348df9ace8e2d82bae5a"},{"version":"8a56ac7e11d465fe195cb981a4681b8f500ba0ccc0f2bd584dac6f977e21e9b2","signature":"940d21e60314f263ad5f38def86fa76e8f4eeb8396c0577e0efc3ae9640c5de6"},{"version":"8779ef6b9edb33882a85edaba150a0378a35ff83a888bc332d98f904e0013d51","signature":"de631aa7fd99a8c92707ef269142872004a344fc5cbb35df49aff4658f24b8d6"},{"version":"ee4c1779c27bd58ce25abdca52b5100fbac78f9758b81173428afeb3a06e2928","signature":"901f0d8f7518f1393dc9c80d42a6ea61c5545ec66e7f80a2f17e18833fc89f1a"},{"version":"5730df43d52be31ef007534b70b01b9d3864cba7a223d12c740be782cc8f491c","signature":"7b19c29db78da0b295d52145f5a8c8926bdbe05316cddd45bfa891b869f8089e"},{"version":"afcf96e118455a6b2e65649a2ef28aaa70f0593e693ec75c4ddd8b5a68fadf86","signature":"15f9aa32f0d88edba00004aee54bc55cc0c79da615a6897fc1c63aa516d2fd5b"},{"version":"e10dc6606dec33ddbf49dfb311584fed48258387d4fdc63e2e3099cf3316bc90","signature":"b7826d3f1e7ea2dab860430e9c8ed3d00bdf4465279e957a18bbd404dadc3e31"},{"version":"a950e6602a8b89e221c6696eadd96f1f969d4927c7cd2a8686c664937a291c3c","signature":"ee2856fbc955acbffcc7b08698e5c846a6bbaa1cef107e837100fefdee86299b"},{"version":"7697b5c79fa763b9a27bc7cd33791f81d22a8082a115d38ebd15acbfa954ba78","signature":"fbd698540e016f4ac0c526ec9d71c56347e36373598949562e2f8611bf2ef6eb"},{"version":"bc4b1b3cdcf612882f01cdc68299e2eeaf24e57cbaaa4e6be5474d982f8abc39","signature":"a57465dfce3296d955e0ee5cf4ffd7c9cf95529700125a74614362ed02aee4f5"},{"version":"b64e4e4b1f0c82f25711dd163b84529eca05fa9311670677788ee4e25d6ae005","signature":"eaefd466bec4201cc35417995f31553b8b01b52713d2d856ce7a89b0d92e7e37"},{"version":"1bc53509e1766094e7cbe702ed8820a43e89515a558393dcf273468dfe5babca","signature":"146557827b38b2916cfbefdad876a09388975ebea9ce4023ff7ab64e55ba94eb"},{"version":"2d2a3451d1801b39fde9df004af4d235fa5581ce7d6fa6ab49ff222fc2331eae","signature":"516fa9bdd584eb5dc7ddccef1bf5fcab29973583a34c9a25018aac29920c2023"},{"version":"0b3bbebf0c6a6b5121c9d62d1541113a9ac3db91cd0cc08e2d5705239c16b7c3","signature":"c070695728e82facd438241ae2bd7963f980e6642fbbc23339df02bb05d641ce"},{"version":"0690049208cbb56942c2ad7d2a8dba7b5a0b3803a18861aafe4c5f20d4e1c5f1","signature":"111b7ba046a51d89e2eb536b2d54dd60df204675889970500c566a17dd855dbb"},{"version":"d469338f1463107b33d01775bbc0384f039ef3dd4397ab3c27615f5e243a789f","signature":"525a83b8b4fb505f1e33510cf87d758332a650c79ec819cfc8a44720c9e11556"},{"version":"50eae0983a73214de9bc2d44a275ab7e923b176566a49228f63df7cc2c0dcb64","signature":"b4dff931f0e2b4206119572a07b76c108ffa97b224226119acfa94c74617b1d6"},{"version":"727fa6e475a80c9656269f9793b2ff6eb9ae6f3d7637953b1873c83a59001dea","signature":"b490d52317e53e8de4fd5f554239dc93dd1439e58884193f7e6a986454c6722f"},{"version":"b8270e7c94765534fdfc1e58f6df9ec20ba1eef17f3ebdb1aca06dc50ed6141c","signature":"ad1ac1e6c5b78655198b21dadb508c10092f2eec7e09e9e91728e3a825960f22"},{"version":"a5f4a153510f045c3647f047f2efa4b1506770008272a24fe83d2e6e5e916b18","signature":"449ec902da103543dabfc3d568c5c127a64aa822ffcbdc72e36d989a6c8d3479"},{"version":"aa6aca1b3d4c91707ff6cf9a5088887a8e611db8cf2c2b700f8e58f3be607036","signature":"a013754e9c9372195578014767d9daa25f3a37a2cac34b96228225fbf6ba5c86"},{"version":"7f0cc554e33de28f1bb9e734d161596169694e44e50b0d3d4e011be821134180","signature":"e919070751c17e6c6af648b3c864251cc3e2ef8edd29c3065601d14f7120237f"},{"version":"c9cbcb9de0930c02998a3a46b35c34a684ac54d76a9d3c8feeb9a5edfda3ffda","signature":"4380ba8dda48544e6b271c3b9bbc98802ab7962dad4e47e0e361d87b586df8fe"},{"version":"5720c3159e99ad7e8eb8940a735a8253696f1a7635e209fc2d9c833000ea066a","signature":"5cacd83cc14fc0247cb4c79054b88d800fd2115ebf75393ba8e3329d93573c77"},{"version":"3168c83114c392806aefd84d10f1f7938ed826c4f6769bd0510cddb31270f0c0","signature":"217b72dd3f5c861d2bd45ee2f49b367fdfa225a2ff48f68d09289ebd1d64d617"},{"version":"ccb5f1380c78822e662e4755fb68beb52373330cab2ccbc6714990a42f8c10c9","signature":"bd467a6429a4d7da9c2f4f7baede68cd94764288b0939d222899a14a312e7cec"},{"version":"9edcaa14e241f336ce11fbb13dfad933a5b2bf82dac5adb263dccfc40b890476","impliedFormat":99},{"version":"e3954ab905c6b7b87abd112e8f4c37865e63a7c3e0d089e2a533bd6614e254f2","impliedFormat":99},{"version":"0fb59682975069330ff1ae73ff3536dcd8b3f882d1f499eac023fd5ab22b4662","impliedFormat":99},{"version":"e7a4acd437c3e337f6580360cfe0827f00bfc94641ece1b7af4d5f75dfe4dbdf","impliedFormat":99},{"version":"e64b8b239623500a8d3d43f7b78146b9a9868c559208e603cc6e6199f854173c","impliedFormat":99},{"version":"983f2d62a5fea3f795daea5091142b1deb67ceed9699ea05b5ff0fa9c5722ac8","signature":"d192259e5653779c2dffc50750a2dd891a84e45600c0e9d4eac39eab47737c72"},{"version":"c94f4a3b60c4d56c74dd6f8acf30e452b2703e29f844b697e4f8d420467f1fa1","signature":"f72757c8bfed29b817c1b8ffbfb71499e7957d51a5a672e0624d3ca00b5c80a0"},{"version":"c02e0f4976b734922abb5d68a6c15e3f07e28d84923edfe5739ac3d83dc6405a","signature":"10f861596460a444575746d17a8decd0f63ab4e0c47a995b6dd0b9478ec8dc39"},{"version":"180b5b21b4eb741aa5f065cb983ef2ff8f71a76cb83db4d883020401e09e68df","signature":"2af0673e5797f13598119b43b457f475d32620b9e982c5fc77fcc1d448f95561"},{"version":"af5124076dbe7662b7cb42a8d2d82085205f50ee3ea1bb1983825e064a1f91f7","signature":"d2db95ce6280e925b5b1e91092ccb0ba1f26d82240ce4de17fb7bd14b9b1b8ae"},{"version":"16796d8369a31ed22940f2d5c6ebca441eb6f3970b628913c91b58aff2400dec","signature":"617021a8407f9e18085a76742a472110c990858f4f891c4757d5437464c65a42"},{"version":"14b1d491bea4d6f3ddf6aff63d2f9eb4292effb212338d802d1774aef384514e","signature":"1c7b96efc4934bf375c9532e425db9420b16e60c96d13e8360d5dc0aac1ac50c"},{"version":"f3fad80daae47a4793735e4cae9682e61aa810a6981c96ff9c53540ffe28d0d4","signature":"7fe4d763556414f26f9a105b5aee8559c924f0b994417a4a7e01bd3953cc7098"},{"version":"7cb11cf7ee462913efd70233b29240ae74537af4dcbe6e6ac2bded72b5c25ec7","signature":"c52e333daeb1d1e05ab0205d1b5e7b6ec23f6518d11b56cf0e5c8c69f3de6a27"},{"version":"c3ad67cca5d7ee10867e2ab3408da6680fbbbded54d6313c6107a824a80e63ec","signature":"24823b6d0199de6d6f4b6239435f8c19ef0d35ae2a7cf494d5261fe5ee90be88"},{"version":"f4adb3576376407d1f7a90f21c0d7ec0657b20c10d48239efa51f4562e0dd451","signature":"91947a6885a3f483787bf1c6dbc19ecc2d8976d26942701ac3ff1df939836ac2"},{"version":"cfe6ad7d453686c1a03915bf4926a4293fb7c055eb8e264be0093678eb7e7872","signature":"b035d9ebd86bead78b7681f4e0baddb98f7b3d6781477164cefae6658ed33b7a"},{"version":"e41a1b01a0e5f60cf974a577e964d72a98d48d7678a97ac7b8a4d72c5a9da4e8","signature":"34cad735b48de7b6a9c29ea0760fbf0cb638817af7064e7803fa7d748070d995"},{"version":"3ff992ef7f5cd48fb95d0a8f76e37a1a930c624c43d4e2de4e350e32efbc55f1","signature":"4b6ee5c7fc5778390685788b994fd66f38eecea26e84fefabb7fa1bef0cbe63f"},{"version":"4cac53e726832ba9f802152898bba71e67cce8b71cdf65b97645141eb119b017","signature":"50e6fdafd43addc7f683824647a0a138619ebc2fe750e79d0f2dea6127cf20c0"},{"version":"b9ba180528e0ea34555c1806009666d1410fbb903d9c83af9cb6f7f9529fb29f","signature":"0be0badd6525a1cf42d0812985ebd504bc3b946817197b0a0e3236fe14bf2e3f"},{"version":"fc47aaa9bc9770c09325dfe5079e2acdbaafd52f5034c77f86c9ce64afc88317","impliedFormat":1},{"version":"a776c6d004117c04952de1fe28ed3decd91e7f74f20ead15ecfff8398262d5ec","signature":"cc196972ba073ddf7ae15707bd8747040de3104e196ce1445bb2fac40ced6d85"},{"version":"fecaef0b025e03681adacd216eb0962745a29bdc3b4dc0dfb683b1d536a739d0","signature":"03937876f986bd3c80a25bcde9bedd562e736ad1e39980e12a294f368c0e2157"},{"version":"f7d3613e8996d7e152fa158df1fdbfaf2a994b351193d334d44de26d0eece1b7","signature":"15608aa80f70852d2f51d0a089639de08626a3ab6b49b27261bd045d595d6976"},{"version":"bfe27f03f05badcff7d5fa012a6bcde9d6ed38ffe799ac40a540851ed11d75dc","signature":"b633f945de6ba0936f71fac2077dd6dc3b01f3e8a6670ece054ebce1b3f50b22"},{"version":"570f4ba8501cceea1a57b6be7e47e08a86e1ba0b40aef5f571bdd11e2fbcdf71","signature":"9b0fc84934fe47863b2b614f2855a7aa0e88a38dc2cdb49f2c83983c615a5cb3"},{"version":"2ab06735fa97917a2bd36397e7f7526851f5dee5f21c7b357f7ac11f3f33dbd2","signature":"74f7141a41f4fb646176486a9bda7ad6ce22202a2c46afcb42a691fdc4261c15"},{"version":"2bb574c3d5031b16ea903d791d5064f1c4f4c7bc587672390afaa63a617a83bc","signature":"af84054c9bbd37500c026a1453c398ad1b002598c1e3adea687daa440a08933e"},{"version":"cc38192081a66abb7cb1bd2b22d6f351147e268c2a0f3e9619fdb646509d9d6c","signature":"ee8f7490d800ed0d3034d1323f59c9b3f84bc6eff75eab4659b22e0c24ed91d1"},{"version":"f0f4c585862bb04477dbeb71aa2df8364755565d8fbb78e52dd62be66b229628","signature":"62078e13c0629f619120e6439193760e66dab737560a7bbf73492b17e2b623e6"},{"version":"f8ce8be585eadd785d32dbe087cac8e96503c16004fa6b5ec5cb185805da46a1","signature":"0157a779263bb0ac1edfa28f73bebf5cde42617bd38bac8015e639eddd1a1a44"},{"version":"2cd9d1f20b53d8e2000d63b190d6a702415beadf54d2e8130006c9914bed3c53","signature":"dbcf12cbb3fc513eaa953a2382bda83342b82809560db86b3124269002ca4036"},{"version":"a4562fd63f900a3bba4e0931942781a0d59c2e6dc50f78a4e0433cd6cf796e8c","signature":"efe5302deed61111fa47beb030820be816ff03963bc6aa2b45ea2ed345a65915"},{"version":"d4a22007b481fe2a2e6bfd3a42c00cd62d41edb36d30fc4697df2692e9891fc8","impliedFormat":1},{"version":"a510938c29a2e04183c801a340f0bbb5a0ae091651bd659214a8587d710ddfbb","impliedFormat":99},{"version":"07bcf85b52f652572fc2a7ec58e6de5dd4fcaf9bbc6f4706b124378cedcbb95c","impliedFormat":99},{"version":"4368a800522ca3dd131d3bbc05f2c46a8b7d612eefca41d5c2e5ac0428a45582","impliedFormat":99},{"version":"720e56f06175c21512bcaeed59a4d4173cd635ea7b4df3739901791b83f835b9","impliedFormat":99},{"version":"349949a8894257122f278f418f4ee2d39752c67b1f06162bb59747d8d06bbc51","impliedFormat":99},{"version":"364832fbef8fb60e1fee868343c0b64647ab8a4e6b0421ca6dafb10dff9979ba","impliedFormat":99},{"version":"dfe4d1087854351e45109f87e322a4fb9d3d28d8bd92aa0460f3578320f024e9","impliedFormat":99},{"version":"886051ae2ccc4c5545bedb4f9af372d69c7c3844ae68833ed1fba8cae8d90ef8","impliedFormat":99},{"version":"3f4e5997cb760b0ef04a7110b4dd18407718e7502e4bf6cd8dd8aa97af8456ff","impliedFormat":99},{"version":"381b5f28b29f104bbdd130704f0a0df347f2fc6cb7bab89cfdc2ec637e613f78","impliedFormat":99},{"version":"a52baccd4bf285e633816caffe74e7928870ce064ebc2a702e54d5e908228777","impliedFormat":99},{"version":"c6120582914acd667ce268849283702a625fee9893e9cad5cd27baada5f89f50","impliedFormat":99},{"version":"da1c22fbbf43de3065d227f8acbc10b132dfa2f3c725db415adbe392f6d1359f","impliedFormat":99},{"version":"858880acbe7e15f7e4f06ac82fd8f394dfe2362687271d5860900d584856c205","impliedFormat":99},{"version":"8dfb1bf0a03e4db2371bafe9ac3c5fb2a4481c77e904d2a210f3fed7d2ad243a","impliedFormat":99},{"version":"bc840f0c5e7274e66f61212bb517fb4348d3e25ed57a27e7783fed58301591e0","impliedFormat":99},{"version":"26438d4d1fc8c9923aea60424369c6e9e13f7ce2672e31137aa3d89b7e1ba9af","impliedFormat":99},{"version":"1ace7207aa2566178c72693b145a566f1209677a2d5e9fb948c8be56a1a61ca9","impliedFormat":99},{"version":"a776df294180c0fdb62ba1c56a959b0bb1d2967d25b372abefdb13d6eba14caf","impliedFormat":99},{"version":"6c88ea4c3b86430dd03de268fd178803d22dc6aa85f954f41b1a27c6bb6227f2","impliedFormat":99},{"version":"11e17a3addf249ae2d884b35543d2b40fabf55ddcbc04f8ee3dcdae8a0ce61eb","impliedFormat":99},{"version":"4fd8aac8f684ee9b1a61807c65ee48f217bf12c77eb169a84a3ba8ddf7335a86","impliedFormat":99},{"version":"1d0736a4bfcb9f32de29d6b15ac2fa0049fd447980cf1159d219543aa5266426","impliedFormat":99},{"version":"11083c0a8f45d2ec174df1cb565c7ba9770878d6820bf01d76d4fedb86052a77","impliedFormat":99},{"version":"d8e37104ef452b01cefe43990821adc3c6987423a73a1252aa55fb1d9ebc7e6d","impliedFormat":99},{"version":"f5622423ee5642dcf2b92d71b37967b458e8df3cf90b468675ff9fddaa532a0f","impliedFormat":99},{"version":"21a942886d6b3e372db0504c5ee277285cbe4f517a27fc4763cf8c48bd0f4310","impliedFormat":99},{"version":"41a4b2454b2d3a13b4fc4ec57d6a0a639127369f87da8f28037943019705d619","impliedFormat":99},{"version":"e9b82ac7186490d18dffaafda695f5d975dfee549096c0bf883387a8b6c3ab5a","impliedFormat":99},{"version":"eed9b5f5a6998abe0b408db4b8847a46eb401c9924ddc5b24b1cede3ebf4ee8c","impliedFormat":99},{"version":"af85fde8986fdad68e96e871ae2d5278adaf2922d9879043b9313b18fae920b1","impliedFormat":99},{"version":"8a1f5d2f7cf4bf851cc9baae82056c3316d3c6d29561df28aff525556095554b","impliedFormat":99},{"version":"2c823e44b7aef6502cdbcb8e6b2a3f10300219b5b94a498d98706b66e0ac12c9","signature":"d05cdde5a88e7c3ee725f4783575b81853295b3e32243e8a9b6a60ff073889d2"},{"version":"112b662745cfe4a133ab24cc26489706dc0a6f8ccefd4d09fd8db66873b2db4e","signature":"e489da6130474d966af11ec19ccbc47179d27d4b1ec3ed96b8c3eb640254cb64"},{"version":"a93b141af25e401c20417b04c94e8ca4a81d6faac4472f9da0f3a6a8c8b13b7a","signature":"6597271c8ff378fa6a11192dfcd88c6e11067a502213a2c20c8541a8a21984b8"},{"version":"466e6198d0e8f74258f2af51a9f57c346019b5c10b36b2a56e165c2cf35bb346","signature":"270bd604cb5498de3612dbff8cc7e93c32ce404c0e6809a598e3abf7825ccc75"},{"version":"09cab2cb1f80db4ebb13d4e7b2782f9e993f09af82508f01cc7c023f0db76155","signature":"d4e2ccb01e03009b6d9efb5b9d5f949d296a55966d164d97cd1862cf43a0f220"},{"version":"f26a848f45c71a7d2d0b4bb239d8469788f0a674c286d0d09a5e2953d7a6f248","signature":"5479b9eb983587140f49ba93b8da22defda965082a506c6bce6701e6bd8a07cd"},{"version":"2c6f51d12755e6c90597fd38117271e83c281e2a3c9e1ec2bd8dbbcd5c28069f","signature":"2f51ac914c15319a173b3cc18e93dd70be6935d8cfb75aef178585ae843d9f44"},{"version":"27d713d70bca974cc8f3aa4b6ab15d1df28a57995c690006ec08b1f9698be6fe","signature":"a64b24855b03d171f4132d50ae1f918ace79f36e88438589ab7bb6823884df9a"},{"version":"37c09d6949d80db68bb32167e98d0fa0035283d87de44c9edcf85d7b734c5a74","signature":"681b7b64b80180ceea95447cd309cf2142b50ff83518fd98be41035ed1f6a982"},{"version":"01a2d8ca93b05dbaecae197865f6bc043664487c881f81e5eed0a7e2808a02bc","signature":"430df2d0295d0f521c1a703aa278eb3eba8b6f794fd92e259203ac780091c03c"},{"version":"ac044a7a34f306aee16b19ce75a4cf2715f2979e4f9cdc54fdb72ba47ce81970","signature":"67dabb71f5eabbb97a46f9484a4b13e1af4f7a8add3eb579ffdb5082a6d71b16"},{"version":"233ac954d5d751a459f3af17a105f0050a39169a26831a01c9fb06bddbb26cda","signature":"e1640fe408f9c3c109e626c9d76346861a897d4aa094b3b0639adc0e721008d7"},{"version":"c74fa3619b9b051debc2cf793eeede6bf3d43f8a2342870a224e25c32503e8e9","signature":"90cc0ff80f1f3547017c2f9b11659b3a177da5bf7757d8749c7fbf131c606636"},{"version":"61ade85ce4280272feffcd55c7375d50c21b2ff580396561ba2770c549235d0c","signature":"909b1f35671d97e01b041add10e76d4247a7ed0e478de667ac54b73a2dda6fca"},{"version":"3bac135aab166beb4373ce12017aa4afbffc37d3ef4e803d6fdb72ba5e071070","signature":"c83dccdb6f44e24c1fad277fba1a1cc50be8c0954118cceb82b407051273d227"},{"version":"d981bed1e8964c88dbf9493443d8b0736b9816169dd8a50be95a8be6923b463e","signature":"40e93b7a8225ba3beaeab6c675cc64115ddf43ebbdd490d7578e9d4b6f1d0cc1"},{"version":"b6c0d04e3c19a47f9d1b6a288436eb3a3839bc08dd9fb56e700c6feb2024108c","signature":"0c4fcbb188842315c1999bd9fb0ae5af6e8c9daa068a0ec95458996c4597f528"},{"version":"f4e8f4151c3490cf7b68c685aabe901cbab19f962aaa2f118a97550e22689a76","impliedFormat":1},{"version":"799003c0ab928582fca04977f47b8d85b43a8de610f4eef0ad2d069fbb9f9399","impliedFormat":1},{"version":"d998eea476c695d8e4ff9d007d5b46d49ca2ffa052f74dc20ca516425abd57b1","impliedFormat":1},{"version":"a0bd46d587005aad4819980f6cf2dbcd80ebf584ed1a946202326a27158ba70e","impliedFormat":1},{"version":"07fcbb61a71bd69a92a5bbde69e60654666cf966b5675c2010c3bf9f436f056a","impliedFormat":1},{"version":"88b2eb23d36692162f2bf1e50577ebcde26de017260473e03ed9a0e61e2726a4","impliedFormat":1},{"version":"23ffbd8c0e20a697d2ea5a0cf7513fb6e42c955a7648f021da12541728f62182","impliedFormat":1},{"version":"43fba5fc019a4ce721a6f53ddb97fdc34c55049cfb793bc544d5c864ee5560b9","impliedFormat":1},{"version":"f4e12292c9a7663a13d152195019711c427c552eb0fa02705e0f61370cd5547a","impliedFormat":1},{"version":"c127ebf14d1b59d1604865008fb072865c5ca52277621f566092fe1f42ce0954","impliedFormat":1},{"version":"def638da26d84825a312113a20649d3086861de7c06a18ea13121278702976fd","impliedFormat":1},{"version":"fbaf86f8ba11298dea2727ce0da84b4ab6ae6c265e1919d44aff7d9b2bbc578a","impliedFormat":1},{"version":"c1010caaeaca8e420c6e040c2e822dbe18702459c93a7d2d5de38597d477b8cd","impliedFormat":1},{"version":"e1f0d8392efd9d71f2644eb97d3f33d90827e30ea8051d93b6f92bb11dff520a","impliedFormat":1},{"version":"085211167559ca307d4053bb8d2298d5ad83cbc3d2ae9bb4c8435a4cabf59369","impliedFormat":1},{"version":"55fc49198d8a85a73cdb79e596d9381cfdc9de93c32c77d42e661c1c1e7268ef","impliedFormat":1},{"version":"6a53fb3df8dd32ed1a65502ca30aeae19cfe80990e78ba68162d6cb2a7fed129","impliedFormat":1},{"version":"b5dcc18d7902597a5584a43c1146ca4fe0295ceb5125f724c1348f6a851dd6ed","impliedFormat":1},{"version":"0c6b0f3fbe6eb6a3805170b3766a341118c92ed7b6d1f193b9f35aa82f594846","impliedFormat":1},{"version":"60eaadb36cf157c5cae9c40e84fa367d04f52a150db3920dbe35139780739143","impliedFormat":1},{"version":"4680a32b1098c49dc87881329af1e68af9af94e051e1b9e19fed555a786f6ce6","impliedFormat":1},{"version":"89fcd129ec37f321cddcdb6b258ffe562de4281e90ec3ccbe7c1199ba39359ca","impliedFormat":1},{"version":"4313011f692861c2c1f5205d7f9a473e763adab6444f9853b96937b187fb19f7","impliedFormat":1},{"version":"caa57157e7bdb8d5f1efe56826fb84a6c8f22a1927bba7fa21fd54e2a44ccba2","impliedFormat":1},{"version":"6b74700abfe4a9b88be957fd8e373cfd998efb1a5f6ad122da49a92997e183ad","impliedFormat":1},{"version":"9ef1342f193bd8bae86c64e450c3ac468ef08652110355e1f3cdd45362eb95c4","impliedFormat":1},{"version":"6853c91662c36a2bf4c8371a87177c819007c76a23c293ef3f686ce9157ae4c8","impliedFormat":1},{"version":"9be1c5dabce43380d13fc621100676b03d420b5687b08d1288f479bee68ab7a8","impliedFormat":1},{"version":"8996d218010896712678e6a0337d8ef8b81c1066ab76f637dd8253f0d6ff838d","impliedFormat":1},{"version":"a15603bf387fc45defe28a68f405a6c29105e135c4e8538eeb6d0a1ef5b69a81","impliedFormat":1},{"version":"84e2532e4d42949a2775cdd8bb7b2b97370dd6ddb683d0c199b21bf6978b152d","impliedFormat":1},{"version":"22bf5f19f620db3b8392cfece44bdd587cdbed80ba39c88a53697d427135bf37","impliedFormat":1},{"version":"23ebbd8d484d07e1c1d8783169c20570ed8409966b28f6be6cf8e970d76ef491","impliedFormat":1},{"version":"18b6fa2c778cad6489f2febf76433453f5e2432ec3535f2d45ae7d803b93cc17","impliedFormat":1},{"version":"609d0d7419999cf44529e6ba687e2944b2fc7ad2570d278fd4e6b1683c075149","impliedFormat":1},{"version":"249cf421b8878a3fe948d9c02f6b0bae65491b3bb974c2ffc612341406fa78ff","impliedFormat":1},{"version":"b4aa22522d653428c8148ddbf1dcc1fb3a3471e15eb1964429a67c390d8c7f38","impliedFormat":1},{"version":"30b2cee905b1848b61c7d28082ebfa2675dd5545c0d25d1c093ce21a905cdccc","impliedFormat":1},{"version":"0a2a2eed4137368735205de97c245f2a685af1a7f1bf8d636b918a0ee4ff4326","impliedFormat":1},{"version":"69f342ce86706aa2835a62898e93ea7a1f21b1d89c70845da69371441bb6cd56","impliedFormat":1},{"version":"b5ab4282affcfd860dd1cc3201653f591509a586d110f8e5b1b010508ba79b2c","impliedFormat":1},{"version":"d396233f6cd3edf0d33c2fbfc84ded029c3ea4a05af3c94d09d31a367cced111","impliedFormat":1},{"version":"bc41a726c817624a5136ae893d7aac7c4dc93c771e8d243a670324bccf39b02b","impliedFormat":1},{"version":"710728600e4b3197f834c4dd1956443be787d2e647a72f190bf6519f235aaadd","impliedFormat":1},{"version":"a45097e01ef30ba26640fed365376ab3ccd5faf97d03f20daff3355a7e60286a","impliedFormat":1},{"version":"763cbb7c22199f43fd5c2b1566af5ba96bf7366f125dd31a038a2291cbc89254","impliedFormat":1},{"version":"031933bf279b7563e11100b5e1746397caf3a278596796a87bc0db23cf68dc9e","impliedFormat":1},{"version":"a4a54c1f58fc6e25a82e2c0f651bf680058bd7f72cfb2d43b85ee0ab5fe2e87e","impliedFormat":1},{"version":"9613d789b6f1037f2523a8f70e1b736f1da4566b470593da062be5c9e13dac57","impliedFormat":1},{"version":"0d2a320763a0c9c71493f8f1069971018c8720a6e7e5a8f10c26b6de79aa2f7d","impliedFormat":1},{"version":"817e0df27a237a268dc16e5acffc19f9a74467093af7a0ba164ee927007a4d25","impliedFormat":1},{"version":"43102521b5ca50ff1865188c3c60790feaed94dc9262b25d4adec4dbc76f9035","impliedFormat":1},{"version":"f99947f8d873b960b0115e506ef9c43f4e40c2071b1d20375564538af4a6023b","impliedFormat":1},{"version":"c1e5ad5ca89d18d2a36d25e8ec105623648cf35615825e202c7d8295a49d61ab","impliedFormat":1},{"version":"2b6c9cb81da4e0a2e32a58230e8c0dec49fc5b345efb7f7a3648b98956be4b13","impliedFormat":1},{"version":"99e34af3ede50062dcc826a1c3ce2d45562060dfd0f29f8066381a6ef548bf2a","impliedFormat":1},{"version":"49f5c2a23ea5fc4b2cdb4426f09d1c8b83f8409fa2af13ef38845cc9b9d4bc3d","impliedFormat":1},{"version":"e935227675144b64ecde3489e4a5e242eeb25fdd6b7464b8c21ad1f7a0faa88b","impliedFormat":1},{"version":"b42e6bbe88dc79c2d6dc5605fb9c15184e70f64bdd7b8d4069b802b90ce86df6","impliedFormat":1},{"version":"b9cd712399fdc00fdae07e96c9b39c3cb311e2a8a5425f1bd583f13cab35e44b","impliedFormat":1},{"version":"5a978550ae131b7fef441d67372fd972abab98ea9fdb9fa266e8bdc89edcb8d6","impliedFormat":1},{"version":"4f287919cfc1d26420db9f0457cd5c8780b1ef0a9f949570936abe48d3a43d91","impliedFormat":1},{"version":"496b23b2fd07e614bc01d90dd4388996cb18cd5f3a612d98201e9f683e58ad2e","impliedFormat":1},{"version":"dcfbe42824f37c5fb6dc7b9427ef2500791ec0d30825ecb614f15b8d5bf5a667","impliedFormat":1},{"version":"390124ad2361b46bf01851d25e331cd7eed355d04451d8b2a4aa985c9de4f8ce","impliedFormat":1},{"version":"14d94f17772c3a58eda01b6603490983d845ee2012cd643f7497b4e22566aacb","impliedFormat":1},{"version":"03ef2386c683707ce741a1c30cb126e8c51a908aa0acc01c3471fafb9baaacd5","impliedFormat":1},{"version":"66a372e03c41d2d5e920df5282dadcec2acae4c629cb51cab850825d2a144cea","impliedFormat":1},{"version":"5b48ba9a30a93176a93c87f9e0abf26a9df457eeb808928009439ca578b56f27","impliedFormat":1},{"version":"4707625392316d3c16edbd0716f4ac310e8ff5d346d58f4d01a2b7e0533a23df","impliedFormat":1},{"version":"154d58a4b2d9c552dc864ea39c223d66efd0ed2dd8b55bd13db5225d14322915","impliedFormat":1},{"version":"6a830433fa072931b4ea3eb9aa5fa7d283f470080586a27bfe69837a0f12de9a","impliedFormat":1},{"version":"d25e930e181f4f69b2b128514538f2abb54ef1d48a046ad776ac6f1cda885a72","impliedFormat":1},{"version":"0259b4c21bc93b52ca82c755f97fc90481072bcc44a8010131b2ea7326cf03fe","impliedFormat":1},{"version":"bea43a13a1104a640da0cb049db85c6993f484a6cc03660496b97824719ecc91","impliedFormat":1},{"version":"0224239d61fe66d4900544d912b2e11c2cca24b4707d53fdb94b874a01e29f48","impliedFormat":1},{"version":"2bce8fd2d16a9432110bbe0ba1e663fd02f7d8b8968cd10178ea7bc306c4a5df","impliedFormat":1},{"version":"9c4ad63738346873d685e5c086acbf41199e7022eff5b72bb668931e9ca42404","impliedFormat":1},{"version":"cfb6329bf8ce324e83fe4bbdee537d866a0d5328246f149a0958b75d033de409","impliedFormat":1},{"version":"efc3816f19ea87a7050c84271ea3d3aad9631a517c168013c4f4b6724c287ce0","impliedFormat":1},{"version":"f99f6737336140047e8dd4ade3859f08331aa4b17bc2bd5f156a25c54e0febbc","impliedFormat":1},{"version":"12a2b25c7c9c05c8994adf193e65749926acfcc076381f7166c2f709a97bdf0a","impliedFormat":1},{"version":"0f93a3fdd517c1e45218cd0027c1d6b82237e379dc6b66d693aab1fe74c82e81","impliedFormat":1},{"version":"03c753da0bee80ad0d0f1819b9b42dfe9bf9f436664caf15325aa426246fd891","impliedFormat":1},{"version":"18f5bf1dae429c451f20171427c9e3223fade4346af4dfd817725cbeb247a09d","impliedFormat":1},{"version":"a4eece5fab202e840dd84f7239e511017a8162edb8fc8b54ff2851c5c844125c","impliedFormat":1},{"version":"c4a94af483a63bf947d89f97553a55df5107c605ec8a26f0b9b8bdcc14bd6d89","impliedFormat":1},{"version":"19de2915ccebc0a1482c2337b34cb178d446def2493bf775c4018a4ea355adb8","impliedFormat":1},{"version":"9be8fc03c8b5392cd17d40fd61063d73f08d0ee3457ecf075dcb3768ae1427bd","impliedFormat":1},{"version":"3b568b63f0e8b3873629a4d7a918dce4266ad41461004ab979f8dcdfd13532bb","impliedFormat":1},{"version":"a5e5223c775fe30d606b8aaa521953c925d5ad176a531c2b69437d2461aaabbd","impliedFormat":1},{"version":"8cbf41d2d1ce8ac2066783ae00613c33feef07493796f638e30beaf892e4354a","impliedFormat":1},{"version":"e22ad737718160df198cd428f18da707177d0467934cecdeed4be6e067b0c619","impliedFormat":1},{"version":"15bf5ed8cb7c1a1e1db53fa9b45bc1a1c73c0497735343a8d0c59fdb596a3744","impliedFormat":1},{"version":"791fce84bce8b6948e4f23422d9cbbd7d08c74b3f91cca12dcae83d96079798b","impliedFormat":1},{"version":"8a2619c8e24305f6b9700b35af178394b995dcb28690a57a71cca87ee7e709ae","impliedFormat":1},{"version":"f95fd2fc3cc164921a891f5d6c935fa0d014a576223dd098fc64677e696b0025","impliedFormat":1},{"version":"8c9cecaaa9caba9a8caa47f46dcf24b524b27899b286d8edcc75a81b370d2ba3","impliedFormat":1},{"version":"2b7a82692ecc877c5379df9653902e23f2d0d0bc9f210ec3cf9e47be54413c5c","impliedFormat":1},{"version":"e2ad09c011cf9d7ee128875406bef787eeb504659495f42656a0098c15fe646c","impliedFormat":1},{"version":"eb518567ea6b0b2623f9a6d37c364e1b1ac9d8b508d79e558f64ac05c17e2685","impliedFormat":1},{"version":"630a48fb8f6b07161588e0aee3f9d301c59c97e1532c884118f89368baf4073b","impliedFormat":1},{"version":"14736c608aa46120f8d6d0bc5e0721b46b927bc7eba20e479600571935f27062","impliedFormat":1},{"version":"7574803692d2230db13205a7749b9c3587dccaccdf9e76f003f9e08078bb6d09","impliedFormat":1},{"version":"f3cc1588e666651c51353b1728460bee8acbc6e0f36be8c025eaaf292dca525d","impliedFormat":1},{"version":"0d4ea8a20527dcf3ad6cf1bd188b8ad4e449df174fad09b9e540ed81080af834","impliedFormat":1},{"version":"aa82876d59912d25becff5a79ed7341af04c71bfeb2221cc0417bc34531125e2","impliedFormat":1},{"version":"6f4b0389f439adc84cba35d45428668eabcfbdd351ba17e459d414ca51ab8eb8","impliedFormat":1},{"version":"d5dd33d15fbb07668c264b38065ac542a07a7650af4917727bbc09b58570e862","impliedFormat":1},{"version":"7d90202d0212e9cdc91a20bfddf04a539c89f09fe1d64db3343546fa2eb37e71","impliedFormat":1},{"version":"1a5d073c95a3a4480b17d2fa7fd41862a9df0cb2afaee86834b13649e96bdb45","impliedFormat":1},{"version":"2092495a5b3116c760527a690c4529748f2d8b126cdd5f56b2ce2230b48aba3f","impliedFormat":1},{"version":"620b29d6adbd4061bc0a8fedf145fcc8e8fc9648fb6e0a39726e33babb4e07bc","impliedFormat":1},{"version":"931eda51b5977f7f3fa7a0d9afde01cfd8b0cc1df0bb66dcf8c2cf6e7090384e","impliedFormat":1},{"version":"b084a412374bdd124048c52c4e8a82d64f3adec6c0a9ad5ecbb7317636039b0f","impliedFormat":1},{"version":"11199daa694c3ced3cc2a382a3fa7bd64e95eb40f9bbc3979fc8fb43f5ba38cc","impliedFormat":1},{"version":"2c86f279d7db3c024de0f21cd9c8c2c972972f842357016bfbbd86955723b223","impliedFormat":1},{"version":"dfb53b9d748df3e140b0fddb75f74d21d7623e800bb1f233817a1a2118d4bb24","impliedFormat":1},{"version":"8cfc293b33082003cacbf7856b8b5e2d6dd3bde46abbd575b0c935dc83af4844","impliedFormat":1},{"version":"7730c538d6d35efe95d2c0d246b1371565b13037e893178033360b4c9d2ac863","impliedFormat":1},{"version":"b256694544b0d45495942720852d9597116979d52f2b53c559fda31f635c60df","impliedFormat":1},{"version":"794e8831c68cc471671430ee0998397ea7a62c3b706b30304efdc3eaff77545a","impliedFormat":1},{"version":"9cfc1b227477e31988e3fb18d26b6988618f4a5da9b7da6bc3df7fc12fb2602e","impliedFormat":1},{"version":"264a292b6024567dd901fdabbf3239a8742bea426432cdbda4cf390b224188e1","impliedFormat":1},{"version":"f1556a28bb8e33862dcfa9da7e6f1dca0b149faf433fe6a50153ae76f3362db1","impliedFormat":1},{"version":"1d321aea1c6a77b2a44e02e5c2aeff290e3f1675ead1a86652b6d77f5fea2b32","impliedFormat":1},{"version":"4910efc2ce1f96d6e71a9e7c9437812ffae5764b33ab3831c614663f62294124","impliedFormat":1},{"version":"e3ceab51a36e8b34ab787af1a7cf02b9312b6651bac67c750579b3f05af646c1","impliedFormat":1},{"version":"baf9f145bcee1b765bed6e79fd45e1ff0ca297a81315944de81eb5d6fff2d13d","impliedFormat":1},{"version":"2afd62362b83db93cd20de22489fe4d46c6f51822069802620589a51ccad4b99","impliedFormat":1},{"version":"9f0cd9bd4ab608123b88328c78814738cbdee620f29258b89ef8cd923f07ff9c","impliedFormat":1},{"version":"801186c9e765583c825f28dab63a7ad12db5609e36dc6d9acbdc97d23888a463","impliedFormat":1},{"version":"96c515141c6135ccd6fb655fb9e3500074a9216ba956fb685dc8edc33f689594","impliedFormat":1},{"version":"416af6d65fc76c9ced6795f255cb1096c9d7947bede75b82289732b74d902784","impliedFormat":1},{"version":"a280c68b128ebba35fb044965d67895201c2f83b6b28281bb8b023ade68bf665","impliedFormat":1},{"version":"6fa118f15723b099a41d3beea98ed059bcd1b3eda708acf98c5eff0c7e88832f","impliedFormat":1},{"version":"dcbf582243e20ea50d283f28f4f64e9990b4ed4a608757e996160c63cff6aa99","impliedFormat":1},{"version":"efa432d8fd562529c4e9f859fd936676dd8fef5d3b4bedb06f754e4740056ea9","impliedFormat":1},{"version":"a59b66720b2ccf2e0150fafb49e8da8dabdf4e1be36244a4ccd92f5bd18e1e9e","impliedFormat":1},{"version":"c657fb1ec3b727d6a14a24c71ea20c41cb7d26a503e8e41b726bb919eb964534","impliedFormat":1},{"version":"50d6d3174868f6e974355bf8e8db8c8b3fcf059315282a0c359ecf799d95514a","impliedFormat":1},{"version":"86bf79091014a1424fc55122caa47f08622b721a4d614b97dd620e3037711541","impliedFormat":1},{"version":"7a63313dff3a57f824a926e49a7262f7bd14e0e833cf45fa5af6da25286769c2","impliedFormat":1},{"version":"36dcaeffe1a1aed1cb84d4feba32895bf442795170edccc874fa32232b2354e5","impliedFormat":1},{"version":"686c6962d04d90edafc174aa5940acb9c9db8949c8d425131c01d796cf9a3aef","impliedFormat":1},{"version":"2b1dbc3d5762d6865744b6e7be94b8b9004097698c37e93e06983e42dd8fe93b","impliedFormat":1},{"version":"eb5e8f74826bdf3a6a0644d37a0f48133f8ad0b5298cc2c574102868542ba4eb","impliedFormat":1},{"version":"c6a82a9673ba517cf04dd0803513257d0adf101aed2e3b162a54d840c9a1a3b2","impliedFormat":1},{"version":"fc9f0f415abaa323efcecc4a4e0b6763bfe576e32043546d44f1de6541b6399b","impliedFormat":1},{"version":"2c4d772ac7ac56a44deef82903364eb7c78dd7bc997701123df0ce4639fe39bb","impliedFormat":1},{"version":"9369ef11eed17c1c223fdea9c0fa39e83f3722914ef390b1448db3d71620c93a","impliedFormat":1},{"version":"aa84130dbc9049bba6095f87932138698f53259b642635f6c9e92dd0ddc7512c","impliedFormat":1},{"version":"084ceadd21efabd4b58667dca00d4f644306099151d2ee18cd28a395855b8009","impliedFormat":1},{"version":"b9503e29f06c99b352b7cae052da19e3599fa42899509d32b23a27c9bb5bebf6","impliedFormat":1},{"version":"75188920fe6ccc14070fe9a65c036049f1141d968c627b623d4a897ec3587e15","impliedFormat":1},{"version":"e2e1df7f45013d2b34f8d08e6ae5a9339724b0ea251b5445fcca3e170e640105","impliedFormat":1},{"version":"af06feb5d18a6ea11c088b683bdb571800d1f76b98d848eecdf41e5ec8f317fd","impliedFormat":1},{"version":"0596af52b95e0c8adc2c07f49f109d746b164739c5866fa8bb394dd6329a3725","impliedFormat":1},{"version":"c3365d08fe7a1ccc3b8e8638edc30123007f3241b4604e2585b9f14422ab97d8","impliedFormat":1},{"version":"a7a3d96b04bb0ec8cb7d2669767c4756f97dd70d08548f9e6522dde4de8e8a03","impliedFormat":1},{"version":"745e960e885a4ba04c872225cbb44bd67a7490d169ceaefab7c0dfc444768676","impliedFormat":1},{"version":"0b1ce1768cde3535493a9daf99e3bbb8c7dcc3a7f9d8cd358cb846af71ce5cdf","impliedFormat":1},{"version":"48b9603f6e8a7c94b727277592a089f94261baa64e6c9d18165da0481663a69e","impliedFormat":1},{"version":"3c20a3bb0c50c819419f44aa55acc58476dad4754a16884cef06012d02b0722f","impliedFormat":1},{"version":"4dc64902cb86e677a928293593658fbf53388f9a30d2b934140c70a7267b07ec","impliedFormat":1},{"version":"cb4fd56539a61d163ea9befe6b0292c32aa68a104c1f68f61416f1bc769bcfba","impliedFormat":1},{"version":"0d852bdc2b72b22393a8eebe374ee3efe3e0d44e630037b5e1b6087985388e62","impliedFormat":1},{"version":"b6c9a2deefb6a57ff68d2a38d33c34407b9939487fc9ee9f32ba3ecf2987a88a","impliedFormat":1},{"version":"f6b371377bab3018dac2bca63e27502ecbd5d06f708ad7e312658d3b5315d948","impliedFormat":1},{"version":"faa72893e85cb8ebb1dafde6b427e5204e60bb5f3ee6576bb64c01db1f255bc8","impliedFormat":1},{"version":"95b7ed47b31a6eaddcdd853ee0871f2bb61e39ce36a01d03dfafb83766f6c10c","impliedFormat":1},{"version":"19287d6b76288c2814f1633bdd68d2b76748757ffd355e73e41151644e4773d6","impliedFormat":1},{"version":"fc4e6ec7dade5f9d422b153c5d8f6ad074bd9cc4e280415b7dc58fb5c52b5df1","impliedFormat":1},{"version":"3aea973106e1184db82d8880f0ca134388b6cbc420f7309d1c8947b842886349","impliedFormat":1},{"version":"765e278c464923da94dda7c2b281ece92f58981642421ae097862effe2bd30fa","impliedFormat":1},{"version":"de260bed7f7d25593f59e859bd7c7f8c6e6bb87e8686a0fcafa3774cb5ca02d8","impliedFormat":1},{"version":"d95c4eaad4df9e564859f0c74a177fa0b2e5f8a155939b52580566ab6b311c3f","impliedFormat":1},{"version":"7192a6d17bfa06e83ba14287907b7c671bef9b7111c146f59c6ea753cfc736b9","impliedFormat":1},{"version":"5156d3d392db5d77e1e2f3ea723c0a8bd3ca8acffe3b754b10c84b12f55a6e10","impliedFormat":1},{"version":"a6494e7833ee04386a9f0c686726f7cb05f52f6e069d9293475ccb1e791ee0da","impliedFormat":1},{"version":"d9af0c89a310256851238f509a22aa1071a464d35dc22ea8c2a0bae42dd81bc5","impliedFormat":1},{"version":"291642a66e55e6ca38b029bc6921c7301f5c7b7acf21ae588a5f352e6c1f6d58","impliedFormat":1},{"version":"43cd7c37298b051d1ce0307d94105bcd792c6c7e017282c9d13f1097c27408e8","impliedFormat":1},{"version":"e00d8cce6e2e627654e49c543b582568ad0bf27c1d4ad1018d26aff78d7599df","impliedFormat":1},{"version":"ed13354f0d96fb6d5878655b1fead51722b54875e91d5e53ef16de5b71a0e278","impliedFormat":1},{"version":"fcb934d0fcdee06a8571bd90aa3a63aa288c784b3ebcecfe7ae90d3104d321f4","impliedFormat":1},{"version":"af682dfabe85688289b420d939020a10eb61f0120e393d53c127f1968b3e9f66","impliedFormat":1},{"version":"0dca04006bf13f72240c6a6a502df9c0b49c41c3cab2be75e81e9b592dcd4ea8","impliedFormat":1},{"version":"7dc0b5e3d7be8e1f451f0545448c2eaa02683f230797d24434b36f9820d5a641","impliedFormat":1},{"version":"247af61cdc3f4ec7876b9e993a2ecdd069e10934ff790c9cee5811842bff49eb","impliedFormat":1},{"version":"4be8c2c63d5cd1381081d90021ddfaef106881df4129eddeeaba906f2d0f75d0","impliedFormat":1},{"version":"012f621d6eb28172afb1b2dc23898d8bc74cf35a6d76b63e5581aa8e50fa71b3","impliedFormat":1},{"version":"3a561fa91097e4580c5349ce72e69d247c31c11d29f39e1d0bd3716042ff2c0b","impliedFormat":1},{"version":"bc9981a79dda3badea61d716d368a280c370267e900f43321f828495f4fef23c","impliedFormat":1},{"version":"2ed3b93d55aea416d7be8d49fe25016430caab0fe64c87d641e4c2c551130d17","impliedFormat":1},{"version":"3d66dfc31dd26092c3663d9623b6fc5cec90878606941a19e2b884c4eacd1a24","impliedFormat":1},{"version":"6916c678060af14a8ce8d78a1929d84184e9507fba7ab75142c1bcb646e1c789","impliedFormat":1},{"version":"3eea74afae095028597b3954bde69390f568afc66d457f64fff56e416ea47811","impliedFormat":1},{"version":"549fb2d19deb7d7cae64922918ddddf190109508cc6c7c47033478f7359556d2","impliedFormat":1},{"version":"e7023afc677a74f03f8ccb567532fe9eedd1f5241ee74be7b75ac2336514f6f6","impliedFormat":1},{"version":"ff55505622eac7d104b9ab9570f4cc67166ba47dd8f3badfb85605d55dd6bdc9","impliedFormat":1},{"version":"102fac015b1eebfa13305cb90fd91a4f0bbcabb10f2343556b3483bbb0a04b62","impliedFormat":1},{"version":"18a1f4493f2dbad5fd4f7d9bfba683c98cf5ed5a4fa704fa0d9884e3876e2446","impliedFormat":1},{"version":"f57e6707d035ab89a03797d34faef37deefd3dd90aa17d90de2f33dce46a2c56","impliedFormat":1},{"version":"cc8b559b2cf9380ca72922c64576a43f000275c72042b2af2415ce0fb88d7077","impliedFormat":1},{"version":"1a337ca294c428ba8f2eb01e887b28d080ee4a4307ae87e02e468b1d26af4a74","impliedFormat":1},{"version":"310fe80ff40a158c2de408efbe9de11e249c53d2de5e33ca32798e6f3fbc8822","impliedFormat":1},{"version":"d6ce96c7bb34945c1d444101f44e0f8ba0bba8ab7587a6cc009a9934b538c335","impliedFormat":1},{"version":"1b10a2715917601939a9288d49beccd45b591723256495b229569cd67bbe48a8","impliedFormat":1},{"version":"7498dfdeed2e003ec49cdf726ff6c293002d1d7fdadbc398ce8aafe6d0688de7","impliedFormat":1},{"version":"8492306a4864a1dc6fc7e0cc0de0ae9279cbd37f3aae3e9dc1065afcdc83dddc","impliedFormat":1},{"version":"9c86abbc4fd0248f56abc12aaecd76854517389af405d5ec2eb187fdb00a606f","impliedFormat":1},{"version":"9ffd906f14f8b059d6b95d6640920f530507e596e548f7a595da58ab66e3ce76","impliedFormat":1},{"version":"1884bccc10ce40adca470c2c371c1c938b36824f169c56f7f43d860416ca0a4c","impliedFormat":1},{"version":"986b55b4f920c99d77c1845f2542df6f746cb5adc9ab93eb1545a7e6ef37590d","impliedFormat":1},{"version":"cd00906068b81fbd8a22d021580ac505e272844408174520fafed0ae00627a5d","impliedFormat":1},{"version":"69fab68a769c17a52a24b868aeb644f3ee14abaa5064115f575ddd59231105ce","impliedFormat":1},{"version":"e181eb86b2caf80fe18c72efce6b913bc226e4a69a5456eaf4f859f1c29c6fd6","impliedFormat":1},{"version":"93f7871380478bc6acf02ad9f3dc7da0c21997caebbe782eb93a11b7bd06a46d","impliedFormat":1},{"version":"d00279ab020713264f570d5181c89ca362b7de8abddf96733de86bce0eca082c","impliedFormat":1},{"version":"f7db473f1d5d2a124f14886ac9dbfeccfbb94a98bbe1610a47c30c2933afa279","impliedFormat":1},{"version":"f44cf6c6d608ef925831e550b19841b5d71bd87195bd346604ff05644fb0d29c","impliedFormat":1},{"version":"154f23902d7a3fcdace4c20b654da7355fee4b7f807d1f77d6c9a24a8756013a","impliedFormat":1},{"version":"562f4f3c75a497d3ad7709381f850bb8c7646a9c6e94fdf8e91928e23d155411","impliedFormat":1},{"version":"4583380b676ee59b70a9696b42acfa986cd5f32430f37672e04f31f40b05df74","impliedFormat":1},{"version":"ad0a13f35a0d88803979f8ea9050ad7441e09d21a509abf2f303e18c1267af17","impliedFormat":1},{"version":"ba9781c718ab3d09cbde1216029072698d2da6135f0d2f856ba387d6caceb13e","impliedFormat":1},{"version":"d7c597c14698ba5fc8010076afa426f029b2d8edabb5073270c070cc645ba638","impliedFormat":1},{"version":"bd2afc69cf1d85cd950a99813bc7eff007d8afa496e7c2142a845cd1181d0474","impliedFormat":1},{"version":"558b462b23ea186d094dbff158d652acd58c0988c9fd53af81a8903412aa5901","impliedFormat":1},{"version":"0e984ae642a15973d652fd7b0d2712a284787d0d7a1db99aa49af0121e47f1df","impliedFormat":1},{"version":"0ad53ee208a23eef2a5cb3d85f2a9dc1019fd5e69179c4b0c02dc56c40d611c4","impliedFormat":1},{"version":"7a6898b26947bd356f33f4efef3eb23e61174d85dca19f41a8780d6bb4bfb405","impliedFormat":1},{"version":"9fe30349d26f34e85209fb06340bac34177f7eae3d6bb69dc12cd179d2c13ddf","impliedFormat":1},{"version":"d568c51d2c4360fd407445e39f4d86891dba04083402602bf5f24fd3969cacbb","impliedFormat":1},{"version":"b2483a924349ec835f4d778dd6787447a2f8bfbb651164851bff29d5b3d990a6","impliedFormat":1},{"version":"aae66889332cff4b2f7586c5c8758abc394d8d1c48f9b04b0c257e58f629d285","impliedFormat":1},{"version":"0f86c85130c64d6dbe6a9090bb3df71c4b0987bce4a08afe1ac4ece597655b9c","impliedFormat":1},{"version":"0ce28ad2671baed24517e1c1f4f2a986029137635bce788ee8fb542f002ac5b8","impliedFormat":1},{"version":"cd12e4fe77d24db98d66049360a4269299bcfb9dc3a1b47078ab1b4afac394cb","impliedFormat":1},{"version":"1589e5ac394b2b2e64264da3e1798d0e103b4f408f5bae1527d9e706f98269c7","impliedFormat":1},{"version":"ff8181aa0fde5ec2d737aecc5ebaa9e881379041f13e5ce1745620e17f78dcf9","impliedFormat":1},{"version":"0b2e54504b568c08df1e7db11c105786742866ba51e20486ab9b2286637d268f","impliedFormat":1},{"version":"bc1ffc3a2dca8ee715571739be3ec74d079e60505e1d0d2446e4978f6c75ba5c","impliedFormat":1},{"version":"770a40373470dff27b3f7022937ea2668a0854d7977c9d22073e1c62af537727","impliedFormat":1},{"version":"a0f8ce72cb02247a112ce4a2fa0f122478a8e99c90a5e6b676b41a68b1891ad2","impliedFormat":1},{"version":"6e957ea18b2bf951cf3995d115ad9bfa439e8d891aeb1afc901d793202c0b90d","impliedFormat":1},{"version":"a1c65bd78725f9172b5846c3c58ddf4bcbb43a30ab19e951f0102552fbfd3d5d","impliedFormat":1},{"version":"04718c7325e7df4bac9a6d026a0a2bd5a8b54501f274aaf93a03b5d1d0635bd1","impliedFormat":1},{"version":"405205f932d4e0ce688a380fa3150b1c7ff60e7fc89909e11a33eab7af240edb","impliedFormat":1},{"version":"566fc1a6616a522f8b45082032a33e6d37ff7df3f7d4d63c3cce9017d0345178","impliedFormat":1},{"version":"3b699b08db04559803b85aa0809748e61427b3d831f77834b8206e9f2ed20c93","impliedFormat":1},{"version":"b27242dd3af2a5548d0c7231db7da63d6373636d6c4e72d9b616adaa2acef7e1","impliedFormat":1},{"version":"e0ee7ba0571b83c53a3d6ec761cf391e7128d8f8f590f8832c28661b73c21b68","impliedFormat":1},{"version":"072bfd97fc61c894ef260723f43a416d49ebd8b703696f647c8322671c598873","impliedFormat":1},{"version":"e70875232f5d5528f1650dd6f5c94a5bed344ecf04bdbb998f7f78a3c1317d02","impliedFormat":1},{"version":"8e495129cb6cd8008de6f4ff8ce34fe1302a9e0dcff8d13714bd5593be3f7898","impliedFormat":99},{"version":"8079d851ffa1dbe193fe36643b3d67f4fdf8d360df4c900065788eff44bc15a7","impliedFormat":1},{"version":"da99399a43db3aa559a2d319fe00ee07d54874fa489d1d05c1ee6ea54e2d33b9","signature":"64d1a70b42747a5ff82fc6f3060ed022c404836d60679b338e4ffb8864dd5195"},{"version":"e516240bc1e5e9faef055432b900bc0d3c9ca7edce177fdabbc6c53d728cced8","impliedFormat":99},{"version":"5402765feacf44e052068ccb4535a346716fa1318713e3dae1af46e1e85f29a9","impliedFormat":99},{"version":"e16ec5d4796e7a765810efee80373675cedc4aa4814cf7272025a88addf5f0be","impliedFormat":99},{"version":"1f57157fcd45f9300c6efcfc53e2071fbe43396b0a7ed2701fbd1efb5599f07f","impliedFormat":99},{"version":"9f1886f3efddfac35babcada2d454acd4e23164345d11c979966c594af63468b","impliedFormat":99},{"version":"a3541c308f223863526df064933e408eba640c0208c7345769d7dc330ad90407","impliedFormat":99},{"version":"59af208befeb7b3c9ab0cb6c511e4fec54ede11922f2ffb7b497351deaf8aa2e","impliedFormat":99},{"version":"928b16f344f6cddaba565da8238f4cf2ddf12fe03eb426ab46a7560e9b3078fa","impliedFormat":99},{"version":"120bdf62bccef4ea96562a3d30dd60c9d55481662f5cf31c19725f56c0056b34","impliedFormat":99},{"version":"39e0da933908de42ba76ea1a92e4657305ae195804cfaa8760664e80baac2d6a","impliedFormat":99},{"version":"55ce6ca8df9d774d60cef58dd5d716807d5cc8410b8b065c06d3edac13f2e726","impliedFormat":99},{"version":"788a0faf3f28d43ce3793b4147b7539418a887b4a15a00ffb037214ed8f0b7f6","impliedFormat":99},{"version":"a3e66e7b8ccdab967cd4ada0f178151f1c42746eabb589a06958482fd4ed354e","impliedFormat":99},{"version":"bf45a2964a872c9966d06b971d0823daecbd707f97e927f2368ba54bb1b13a90","impliedFormat":99},{"version":"39973a12c57e06face646fb79462aabe8002e5523eec4e86e399228eb34b32c9","impliedFormat":99},{"version":"f01091e9b5028acfb38208113ae051fad8a0b4b8ec1f7137a2a5cf903c47eefc","impliedFormat":99},{"version":"b3e87824c9e7e3a3be7f76246e45c8d603ce83d116733047200b3aa95875445b","impliedFormat":99},{"version":"7e1f7f9ae14e362d41167dc861be6a8d76eca30dde3a9893c42946dc5a5fc686","impliedFormat":99},{"version":"9308ef3b9433063ac753a55c3f36d6d89fa38a8e6c51e05d9d8329c7f1174f24","impliedFormat":99},{"version":"cd3bb1aa24726a0abd67558fde5759fe968c3c6aa3ec7bad272e718851502894","impliedFormat":99},{"version":"1ae0f22c3b8420b5c2fec118f07b7ebd5ae9716339ab3477f63c603fe7a151c8","impliedFormat":99},{"version":"919ff537fff349930acc8ad8b875fd985a17582fb1beb43e2f558c541fd6ecd9","impliedFormat":99},{"version":"4e67811e45bae6c44bd6f13a160e4188d72fd643665f40c2ac3e8a27552d3fd9","impliedFormat":99},{"version":"3d1450fd1576c1073f6f4db9ebae5104e52e2c4599afb68d7d6c3d283bdbaf4f","impliedFormat":99},{"version":"c072af873c33ff11af126c56a846dfada32461b393983a72b6da7bff373e0002","impliedFormat":99},{"version":"de66e997ea5376d4aeb16d77b86f01c7b7d6d72fbb738241966459d42a4089e0","impliedFormat":99},{"version":"d77ea3b91e4bc44d710b7c9487c2c6158e8e5a3439d25fc578befeb27b03efd7","impliedFormat":99},{"version":"a3d5c695c3d1ebc9b0bd55804afaf2ac7c97328667cbeedf2c0861b933c45d3e","impliedFormat":99},{"version":"270724545d446036f42ddea422ee4d06963db1563ccc5e18b01c76f6e67968ae","impliedFormat":99},{"version":"85441c4f6883f7cfd1c5a211c26e702d33695acbabec8044e7fa6831ed501b45","impliedFormat":99},{"version":"0f268017a6b1891fdeea69c2a11d576646d7fd9cdfc8aac74d003cd7e87e9c5a","impliedFormat":99},{"version":"9ece188c336c80358742a5a0279f2f550175f5a07264349d8e0ce64db9701c0b","impliedFormat":99},{"version":"cf41b0fc7d57643d1a8d21af07b0247db2f2d7e2391c2e55929e9c00fbe6ab9a","impliedFormat":99},{"version":"11e7ddddd9eddaac56a6f23d8699ae7a94c2a55ae8c986fdabc719d3c3e875a1","impliedFormat":99},{"version":"dd129c2d348be7dbf9f15d34661defdfc11ee00628ca6f7161bead46095c6bc3","impliedFormat":99},{"version":"c38d8e7cfc64bbfc14a63346388249c1cfa2cc02166c5f37e5a57da4790ce27f","impliedFormat":99},{"version":"c0ef234e74cabdbaa46b2e5b3d27facd07f949afa402fc381305e885e08416c8","signature":"77a8091fc8ef1f8b7608330b519ca909ac3e9ce1329af32c1153ef8851762ed2"},{"version":"7e3373dde2bba74076250204bd2af3aa44225717435e46396ef076b1954d2729","impliedFormat":1},{"version":"1c3dfad66ff0ba98b41c98c6f41af096fc56e959150bc3f44b2141fb278082fd","impliedFormat":1},{"version":"56208c500dcb5f42be7e18e8cb578f257a1a89b94b3280c506818fed06391805","impliedFormat":1},{"version":"0c94c2e497e1b9bcfda66aea239d5d36cd980d12a6d9d59e66f4be1fa3da5d5a","impliedFormat":1},{"version":"eb9271b3c585ea9dc7b19b906a921bf93f30f22330408ffec6df6a22057f3296","impliedFormat":1},{"version":"0205ee059bd2c4e12dcadc8e2cbd0132e27aeba84082a632681bd6c6c61db710","impliedFormat":1},{"version":"a694d38afadc2f7c20a8b1d150c68ac44d1d6c0229195c4d52947a89980126bc","impliedFormat":1},{"version":"9f1e00eab512de990ba27afa8634ca07362192063315be1f8166bc3dcc7f0e0f","impliedFormat":1},{"version":"9674788d4c5fcbd55c938e6719177ac932c304c94e0906551cc57a7942d2b53b","impliedFormat":1},{"version":"86dac6ce3fcd0a069b67a1ac9abdbce28588ea547fd2b42d73c1a2b7841cf182","impliedFormat":1},{"version":"4d34fbeadba0009ed3a1a5e77c99a1feedec65d88c4d9640910ff905e4e679f7","impliedFormat":1},{"version":"9d90361f495ed7057462bcaa9ae8d8dbad441147c27716d53b3dfeaea5bb7fc8","impliedFormat":1},{"version":"8fcc5571404796a8fe56e5c4d05049acdeac9c7a72205ac15b35cb463916d614","impliedFormat":1},{"version":"a3b3a1712610260c7ab96e270aad82bd7b28a53e5776f25a9a538831057ff44c","impliedFormat":1},{"version":"33a2af54111b3888415e1d81a7a803d37fada1ed2f419c427413742de3948ff5","impliedFormat":1},{"version":"d5a4fca3b69f2f740e447efb9565eecdbbe4e13f170b74dd4a829c5c9a5b8ebf","impliedFormat":1},{"version":"56f1e1a0c56efce87b94501a354729d0a0898508197cb50ab3e18322eb822199","impliedFormat":1},{"version":"8960e8c1730aa7efb87fcf1c02886865229fdbf3a8120dd08bb2305d2241bd7e","impliedFormat":1},{"version":"27bf82d1d38ea76a590cbe56873846103958cae2b6f4023dc59dd8282b66a38a","impliedFormat":1},{"version":"0daaab2afb95d5e1b75f87f59ee26f85a5f8d3005a799ac48b38976b9b521e69","impliedFormat":1},{"version":"2c378d9368abcd2eba8c29b294d40909845f68557bc0b38117e4f04fc56e5f9c","impliedFormat":1},{"version":"bb220eaac1677e2ad82ac4e7fd3e609a0c7b6f2d6d9c673a35068c97f9fcd5cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c60b14c297cc569c648ddaea70bc1540903b7f4da416edd46687e88a543515a1","impliedFormat":1},{"version":"94a802503ca276212549e04e4c6b11c4c14f4fa78722f90f7f0682e8847af434","impliedFormat":1},{"version":"9c0217750253e3bf9c7e3821e51cff04551c00e63258d5e190cf8bd3181d5d4a","impliedFormat":1},{"version":"5c2e7f800b757863f3ddf1a98d7521b8da892a95c1b2eafb48d652a782891677","impliedFormat":1},{"version":"21317aac25f94069dbcaa54492c014574c7e4d680b3b99423510b51c4e36035f","impliedFormat":1},{"version":"c61d8275c35a76cb12c271b5fa8707bb46b1e5778a370fd6037c244c4df6a725","impliedFormat":1},{"version":"c7793cb5cd2bef461059ca340fbcd19d7ddac7ab3dcc6cd1c90432fca260a6ae","impliedFormat":1},{"version":"fd3bf6d545e796ebd31acc33c3b20255a5bc61d963787fc8473035ea1c09d870","impliedFormat":1},{"version":"c7af51101b509721c540c86bb5fc952094404d22e8a18ced30c38a79619916fa","impliedFormat":1},{"version":"59c8f7d68f79c6e3015f8aee218282d47d3f15b85e5defc2d9d1961b6ffed7a0","impliedFormat":1},{"version":"93a2049cbc80c66aa33582ec2648e1df2df59d2b353d6b4a97c9afcbb111ccab","impliedFormat":1},{"version":"d04d359e40db3ae8a8c23d0f096ad3f9f73a9ef980f7cb252a1fdc1e7b3a2fb9","impliedFormat":1},{"version":"84aa4f0c33c729557185805aae6e0df3bd084e311da67a10972bbcf400321ff0","impliedFormat":1},{"version":"cf6cbe50e3f87b2f4fd1f39c0dc746b452d7ce41b48aadfdb724f44da5b6f6ed","impliedFormat":1},{"version":"3cf494506a50b60bf506175dead23f43716a088c031d3aa00f7220b3fbcd56c9","impliedFormat":1},{"version":"f2d47126f1544c40f2b16fc82a66f97a97beac2085053cf89b49730a0e34d231","impliedFormat":1},{"version":"724ac138ba41e752ae562072920ddee03ba69fe4de5dafb812e0a35ef7fb2c7e","impliedFormat":1},{"version":"e4eb3f8a4e2728c3f2c3cb8e6b60cadeb9a189605ee53184d02d265e2820865c","impliedFormat":1},{"version":"f16cb1b503f1a64b371d80a0018949135fbe06fb4c5f78d4f637b17921a49ee8","impliedFormat":1},{"version":"f4808c828723e236a4b35a1415f8f550ff5dec621f81deea79bf3a051a84ffd0","impliedFormat":1},{"version":"3b810aa3410a680b1850ab478d479c2f03ed4318d1e5bf7972b49c4d82bacd8d","impliedFormat":1},{"version":"0ce7166bff5669fcb826bc6b54b246b1cf559837ea9cc87c3414cc70858e6097","impliedFormat":1},{"version":"6ea095c807bc7cc36bc1774bc2a0ef7174bf1c6f7a4f6b499170b802ce214bfe","impliedFormat":1},{"version":"3549400d56ee2625bb5cc51074d3237702f1f9ffa984d61d9a2db2a116786c22","impliedFormat":1},{"version":"5327f9a620d003b202eff5db6be0b44e22079793c9a926e0a7a251b1dbbdd33f","impliedFormat":1},{"version":"b60f6734309d20efb9b0e0c7e6e68282ee451592b9c079dd1a988bb7a5eeb5e7","impliedFormat":1},{"version":"f4187a4e2973251fd9655598aa7e6e8bba879939a73188ee3290bb090cc46b15","impliedFormat":1},{"version":"44c1a26f578277f8ccef3215a4bd642a0a4fbbaf187cf9ae3053591c891fdc9c","impliedFormat":1},{"version":"a5989cd5e1e4ca9b327d2f93f43e7c981f25ee12a81c2ebde85ec7eb30f34213","impliedFormat":1},{"version":"f65b8fa1532dfe0ef2c261d63e72c46fe5f089b28edcd35b3526328d42b412b8","impliedFormat":1},{"version":"1060083aacfc46e7b7b766557bff5dafb99de3128e7bab772240877e5bfe849d","impliedFormat":1},{"version":"d61a3fa4243c8795139e7352694102315f7a6d815ad0aeb29074cfea1eb67e93","impliedFormat":1},{"version":"1f66b80bad5fa29d9597276821375ddf482c84cfb12e8adb718dc893ffce79e0","impliedFormat":1},{"version":"1ed8606c7b3612e15ff2b6541e5a926985cbb4d028813e969c1976b7f4133d73","impliedFormat":1},{"version":"c086ab778e9ba4b8dbb2829f42ef78e2b28204fc1a483e42f54e45d7a96e5737","impliedFormat":1},{"version":"dd0b9b00a39436c1d9f7358be8b1f32571b327c05b5ed0e88cc91f9d6b6bc3c9","impliedFormat":1},{"version":"a951a7b2224a4e48963762f155f5ad44ca1145f23655dde623ae312d8faeb2f2","impliedFormat":1},{"version":"cd960c347c006ace9a821d0a3cffb1d3fbc2518a4630fb3d77fe95f7fd0758b8","impliedFormat":1},{"version":"fe1f3b21a6cc1a6bc37276453bd2ac85910a8bdc16842dc49b711588e89b1b77","impliedFormat":1},{"version":"1a6a21ff41d509ab631dbe1ea14397c518b8551f040e78819f9718ef80f13975","impliedFormat":1},{"version":"0a55c554e9e858e243f714ce25caebb089e5cc7468d5fd022c1e8fa3d8e8173d","impliedFormat":1},{"version":"3a5e0fe9dcd4b1a9af657c487519a3c39b92a67b1b21073ff20e37f7d7852e32","impliedFormat":1},{"version":"977aeb024f773799d20985c6817a4c0db8fed3f601982a52d4093e0c60aba85f","impliedFormat":1},{"version":"d59cf5116848e162c7d3d954694f215b276ad10047c2854ed2ee6d14a481411f","impliedFormat":1},{"version":"50098be78e7cbfc324dfc04983571c80539e55e11a0428f83a090c13c41824a2","impliedFormat":1},{"version":"08e767d9d3a7e704a9ea5f057b0f020fd5880bc63fbb4aa6ffee73be36690014","impliedFormat":1},{"version":"dd6051c7b02af0d521857069c49897adb8595d1f0e94487d53ebc157294ef864","impliedFormat":1},{"version":"79c6a11f75a62151848da39f6098549af0dd13b22206244961048326f451b2a8","impliedFormat":1},{"version":"f410816c0670492da1b23df3fc24b02bdc4710640d8095523b653eb5daeb3c08","signature":"cb4e4c54076f9a3d2a70bbb13dde81b7b451925d7c67502bc9aae6e9cf1b2ef4"},{"version":"731330fd1896d45e2ed00cf09ff0037580d5abc6b83c171f0f3b413142cf49d0","signature":"36b328f629e1ddb764e4620846380d6758f02514308a74eb128378fe6207da55"},{"version":"7ec047b73f621c526468517fea779fec2007dd05baa880989def59126c98ef79","impliedFormat":99},{"version":"dfcf16e716338e9fe8cf790ac7756f61c85b83b699861df970661e97bf482692","impliedFormat":99},{"version":"bb703864a1bc9ca5ac3589ffd83785f6dc86f7f6c485c97d7ffd53438777cb9e","impliedFormat":1},{"version":"c218369b6c0664232b3940fc05c14386c26e6c031a674baf9d1161fda68b8bd5","signature":"41a0db130cfb6020f150fbee6c36ee82a4feacb3922f1cfdece9deac59a47e42"},{"version":"e7441be68f390975c6155c805cea8f54cc1b7f3656b6b9440ecbbbd7753499e6","impliedFormat":99},{"version":"3b1a3d1bedd1c351f54af7e402693480c036f188951d148817ab7e5c17f04f16","signature":"2f72f5f0f83025ca5baf5b2c2eb9b3f0ca8df6bc6c02b8eaa8b7074de0a65120"},{"version":"91b4ce96f6ad631a0a6920eb0ab928159ff01a439ae0e266ecdc9ea83126a195","impliedFormat":1},{"version":"88efe27bebddb62da9655a9f093e0c27719647e96747f16650489dc9671075d6","impliedFormat":1},{"version":"e348f128032c4807ad9359a1fff29fcbc5f551c81be807bfa86db5a45649b7ba","impliedFormat":1},{"version":"8ee6b07974528da39b7835556e12dd3198c0a13e4a9de321217cd2044f3de22e","impliedFormat":1},{"version":"5e1d8a07714f909beaaaf4d0ffe507345a99f2db967493dd8ebbfbb4f18e83ca","impliedFormat":1},{"version":"5f12132800d430adbe59b49c2c0354d85a71ada7d756e34250a655baa8ad4ae5","impliedFormat":1},{"version":"1996d1cd7d585a8359a35878f67abdd73cc35b1f675c9c6b147b202fdd8dfc3f","impliedFormat":1},{"version":"5a50dbfc042633fdb558e53b30b0a005e0b78e142a1fe1147a8d6618ca69ec99","impliedFormat":1},{"version":"a8b0f3d8b205c32395727230bff7849c947150fdd6288855cbe75bc662c4e236","impliedFormat":1},{"version":"6fb55bb881f4a7167649e6925df076f64a1db2f50632df4674e4621a9445c954","impliedFormat":1},{"version":"4374cefdde5c6e9bad52b0436e887b8325b8f407c12035194ad02c28f1553a3a","impliedFormat":1},{"version":"9b70cad270593f676aecfe4d1611dc766464f0b8138527b0ebbf1ff773578d69","impliedFormat":1},{"version":"b4f85bfb7e831703ac81737361842f1ae4d924b42c5d1af2bff93cca521de4d1","impliedFormat":1},{"version":"5fea76008a2d537ca09d569ffae4e08b991b4a5ff90e9f4783bc983584454ede","impliedFormat":1},{"version":"21575cdeaca6a2c2a0beb8c2ecbc981d9deb95f879f82dc7d6e325fe8737b5ba","impliedFormat":1},{"version":"40ec58f0fadd0b3981b3d383e1c12fa0680115ae9f018387fc2cfc0bbcf23204","impliedFormat":1},{"version":"849b9e7283b7309a4556c9b90bb8e2dfc27751f157798065bbc513dcddb09a8c","impliedFormat":1},{"version":"10e109212c7be8a9f66e988e5d6c2a8900c9d14bf6beadf5fa70d32ada3425cf","impliedFormat":1},{"version":"2b821aeb31e690092f8eae671dd961a9d0fd598ff4883ce0a600c90e9e8fa716","impliedFormat":1},{"version":"26602933b613e4df3868a6c82e14fffa2393a08531cb333ed27b151923462981","impliedFormat":1},{"version":"f57a588d8f6b3ce5c8b494f2dc759a8885eaee18e80a4952df47de45403fedbe","impliedFormat":1},{"version":"34735727b3fe7a0ed0651a0f88d06449163d1989a2b2de7f047473adc7c1c383","impliedFormat":1},{"version":"a5b13abc88ab3186e713c445e59e2f6eee20c6167943517bc2f56985d89b8c55","impliedFormat":1},{"version":"3844b45a774bafe226260cf0772376dce72121ebb801d03902c70a7f11da832b","impliedFormat":1},{"version":"7ae65fe95b18205e241e6695cb2c61c0828d660aca7d08f68781b439a800e6b8","impliedFormat":1},{"version":"e025419f23ccceafd7f5ab3141a86a6bb9fc3b33c44fe62b288d7b19baffc95b","impliedFormat":1},{"version":"369b7270eeeb37982203b2cb18c7302947b89bf5818c1d3d2e95a0418f02b74e","impliedFormat":1},{"version":"94f95d223e2783b0aef4d15d7f6990a6a550fe17d099c501395f690337f7105e","impliedFormat":1},{"version":"039bd8d1e0d151570b66e75ee152877fb0e2f42eca43718632ac195e6884be34","impliedFormat":1},{"version":"89fb1e22c3c98cbb86dc3e5949012bdae217f2b5d768a2cc74e1c4b413c25ad2","impliedFormat":1},{"version":"24c371f70751acb269f7fdcb358c4b20ba63e85a4621fae4b562ec0cc0011cb6","signature":"8645fd739fb78ff1e6433f42605595a2a3a1047f5dd87bde6603968b0bfad4cc"},{"version":"92d566eae4dbbe8db9ca1db01911b48e1f53d11dc45b643f8aab6648831dd165","signature":"994534597e772bc135a36815cd936a8a63f27a29879b249eac14caf6e341364d"},{"version":"b843496b17a2bbd79c83809c73fd9c59fab53d3e361e04e52e2d489524eea764","impliedFormat":1},{"version":"a174e474f30d8a13d10d90975f7338efd5bb02c0b1a9c3808719ef3d4d963906","signature":"5edeac74f5feafae2554b5acf4a797463e1f270371f61f89c7cbc5d816b0b41c"},{"version":"82e23bd8aa4c3c9d8825b5d6d7a5f831bde56c51491666d3bb6a6669a933afff","signature":"833875c2409cce5c125064f63cf4cd0b6d43436a961f926d9ee67d1fdaed7c6b"},{"version":"08b059b3ca4faf951bb4b39c17345462ea91bff07d8f8f99c96a71e0a264f326","signature":"bc566a7efbfa5e0f5ae124a0d2c2f66291391387483da8da6278e9c4e642615e"},{"version":"4b983779d67a080b61e66a613c5273c93ec33d0dc7ea885c35bedf9f8e9f0eac","signature":"6147691e841c1e4a3ea56d26412211418dd8d2b77ec0751e5ab830865344c2a7"},{"version":"4d9bffaca7e0f0880868bab5fd351f9e4d57fcc6567654c4c330516fea7932aa","impliedFormat":99},{"version":"867b845defb7e20f8928fdb8c5e2e7c473f701bb54e8f6624cdc3ebfec2e1c50","impliedFormat":99},{"version":"fcebf6064ee19f019cd677748310bbea900dc57fd8646427cf3418af70902d86","impliedFormat":99},{"version":"a93a11986f470d2c99c689ee101a04003d3b08d66e884782c147865017c15484","impliedFormat":99},{"version":"73b48bdb2946a0fa88829e88d32a69daee9b7c22db26524f89e9abe31f9b7cca","impliedFormat":99},{"version":"d3f91fbf8f379271187b8d06fe1900a63853845871c27376b7d37e664a32e930","signature":"6e792f758932bcb89403f0efca4da19a9a8b02bf358f6f1846bdb49d0f1dbbd6"},{"version":"b7467f53d674849e92daa0d5f283c91f316a86a569f3eccace7d3f8f9a7a2944","signature":"f77dac591bbb93fd5024c15a73ccb10025f76c5b33a001760c579e13341ae834"},{"version":"8535cbb3e75a83fa3fb8a261d288fb8685e343cbb814cea806349c69e021475e","signature":"523cbf15f5b12fdc02dbcf3f59602623f8b49c4cc351678ce8b0106575cdddbf"},{"version":"83de4884db640c144f753216c544995b73dad95a0bc36d58ed9da59c0dc75f62","signature":"ca24bbf78a8fd1cc64ae781cc893d96efaffd225c7c4da1087d478843515407e"},{"version":"284e8e8fcfb85ad5c4d6e3f87ecc362a39492cd4b9dfbb8eb2414bf5f51bcbdd","signature":"5bbd9a79fbf27b68134b226d7545e21bd74518258720cf56d833a80fd66073c6"},{"version":"a0b87928e96701063a8e983c83267a3f9a9d4284b424902927279b21bd3e41e0","signature":"43176e4d525a623257cd59078550387aa9495f39389402e2d648e40d95a57925"},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"6e215dac8b234548d91b718f9c07d5b09473cd5cabb29053fcd8be0af190acb6","affectsGlobalScope":true,"impliedFormat":1},{"version":"dbecf494aac7d3ee1b23cdaafae0d0bfea8590567fc153db58fe00ed9fa66c24","impliedFormat":1},{"version":"f3d3e999a323c85c8a63ce90c6e4624ff89fe137a0e2508fddc08e0556d08abf","impliedFormat":1},{"version":"c74b33e4465a03e398a080ec56b8f7cf19edba5d7c4dab482b4be4a5c4efb1ca","impliedFormat":1},{"version":"6edb7a98f0d3483a88041013608fda8e02c9f2f5361c327ad090dc522fcdbeff","affectsGlobalScope":true,"impliedFormat":1},{"version":"53e074a281b50dc3bbdddac7a1c2946100c80a7f5c3161452ab98b31db2e31ba","impliedFormat":1},{"version":"f3d3e999a323c85c8a63ce90c6e4624ff89fe137a0e2508fddc08e0556d08abf","impliedFormat":1},{"version":"6a121c24083c9f164330b85ce7aa8ef97b64fedaf8694ec14cddc34d921ad209","impliedFormat":1},{"version":"49ae37a1b5de16f762c8a151eeaec6b558ce3c27251052ef7a361144af42cad4","impliedFormat":1},{"version":"fc9e630f9302d0414ccd6c8ed2706659cff5ae454a56560c6122fa4a3fac5bbd","affectsGlobalScope":true,"impliedFormat":1},{"version":"7115f1157a00937d712e042a011eb85e9d80b13eff78bac5f210ee852f96879d","impliedFormat":1},{"version":"0ac74c7586880e26b6a599c710b59284a284e084a2bbc82cd40fb3fbfdea71ae","affectsGlobalScope":true,"impliedFormat":1},{"version":"2ce12357dadbb8efc4e4ec4dab709c8071bf992722fc9adfea2fe0bd5b50923f","impliedFormat":1},{"version":"31bd1a31f935276adf90384a35edbd4614018ff008f57d62ffb57ac538e94e51","impliedFormat":1},{"version":"ffd344731abee98a0a85a735b19052817afd2156d97d1410819cd9bcd1bd575e","impliedFormat":1},{"version":"475e07c959f4766f90678425b45cf58ac9b95e50de78367759c1e5118e85d5c3","impliedFormat":1},{"version":"a524ae401b30a1b0814f1bbcdae459da97fa30ae6e22476e506bb3f82e3d9456","impliedFormat":1},{"version":"7375e803c033425e27cb33bae21917c106cb37b508fd242cccd978ef2ee244c7","impliedFormat":1},{"version":"eeb890c7e9218afdad2f30ad8a76b0b0b5161d11ce13b6723879de408e6bc47a","impliedFormat":1},{"version":"8c61ed247159a025ccc4c3702862b97ef3dbac5460e87f57205f6c37c9e7edbd","impliedFormat":1},{"version":"b05b9ef20d18697e468c3ae9cecfff3f47e8976f9522d067047e3f236db06a41","affectsGlobalScope":true,"impliedFormat":1},{"version":"f434b00addaec462abb14aee113fefdb22ba5a60044b782b1b05f7ae489aa335","affectsGlobalScope":true,"impliedFormat":1},{"version":"3ba38f41c6344cc89270450751e89d0cb60279af2db0e20f0b6858994e6785a8","affectsGlobalScope":true,"impliedFormat":1},{"version":"e4b5520626a9a1333f2518839db11f9649ba853e7cc7e9a25eb6addb5d1307e9","impliedFormat":1},{"version":"06e0e96bcdc4bf6032b9e0b83451bcb1148880772b337842de991164e6f00b34","impliedFormat":1},{"version":"c61c37176b7a6c043df76f437e402ea9abc9f19e9652a0d37629dfc8b7e83497","impliedFormat":1},{"version":"5602c92b934a62d674506c40755f3ca46e3c4a6dfbf01674289714a51f238b40","impliedFormat":1},{"version":"fc803e6b01f4365f71f51f9ce13f71396766848204d4f7a1b2b6154434b84b15","impliedFormat":1},{"version":"067f76ab5254b1bdfc94154730b7a30c12e3aad8b9d04ec62c0d6b7a1f40ea0e","affectsGlobalScope":true,"impliedFormat":1},{"version":"8b08cdb4a66b9fe95fa19b3133036c91fe9314ec4e2bff533dd1d60fd81e1bed","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be812b06e518320ba82e2aff3ac2ca37370a9df917db708f081b9043fa3315","impliedFormat":1},{"version":"49ae37a1b5de16f762c8a151eeaec6b558ce3c27251052ef7a361144af42cad4","impliedFormat":1},{"version":"fc9e630f9302d0414ccd6c8ed2706659cff5ae454a56560c6122fa4a3fac5bbd","affectsGlobalScope":true,"impliedFormat":1},{"version":"aa0a44af370a2d7c1aac988a17836f57910a6c52689f52f5b3ac1d4c6cadcb23","impliedFormat":1},{"version":"0ac74c7586880e26b6a599c710b59284a284e084a2bbc82cd40fb3fbfdea71ae","affectsGlobalScope":true,"impliedFormat":1},{"version":"2ce12357dadbb8efc4e4ec4dab709c8071bf992722fc9adfea2fe0bd5b50923f","impliedFormat":1},{"version":"b5a907deaba678e5083ccdd7cc063a3a8c3413c688098f6de29d6e4cefabc85f","impliedFormat":1},{"version":"ffd344731abee98a0a85a735b19052817afd2156d97d1410819cd9bcd1bd575e","impliedFormat":1},{"version":"475e07c959f4766f90678425b45cf58ac9b95e50de78367759c1e5118e85d5c3","impliedFormat":1},{"version":"a524ae401b30a1b0814f1bbcdae459da97fa30ae6e22476e506bb3f82e3d9456","impliedFormat":1},{"version":"7375e803c033425e27cb33bae21917c106cb37b508fd242cccd978ef2ee244c7","impliedFormat":1},{"version":"eeb890c7e9218afdad2f30ad8a76b0b0b5161d11ce13b6723879de408e6bc47a","impliedFormat":1},{"version":"561c795984d06b91091780cebeac616e9e41d83240770e1af14e6ec083b713d5","impliedFormat":1},{"version":"dfbcc400ac6d20b941ccc7bd9031b9d9f54e4d495dd79117334e771959df4805","affectsGlobalScope":true,"impliedFormat":1},{"version":"944d65951e33a13068be5cd525ec42bf9bc180263ba0b723fa236970aa21f611","affectsGlobalScope":true,"impliedFormat":1},{"version":"6b386c7b6ce6f369d18246904fa5eac73566167c88fb6508feba74fa7501a384","affectsGlobalScope":true,"impliedFormat":1},{"version":"592a109e67b907ffd2078cd6f727d5c326e06eaada169eef8fb18546d96f6797","impliedFormat":1},{"version":"f2eb1e35cae499d57e34b4ac3650248776fe7dbd9a3ec34b23754cfd8c22fceb","impliedFormat":1},{"version":"fbed43a6fcf5b675f5ec6fc960328114777862b58a2bb19c109e8fc1906caa09","impliedFormat":1},{"version":"9e98bd421e71f70c75dae7029e316745c89fa7b8bc8b43a91adf9b82c206099c","impliedFormat":1},{"version":"fc803e6b01f4365f71f51f9ce13f71396766848204d4f7a1b2b6154434b84b15","impliedFormat":1},{"version":"f3afcc0d6f77a9ca2d2c5c92eb4b89cd38d6fa4bdc1410d626bd701760a977ec","impliedFormat":1},{"version":"c8109fe76467db6e801d0edfbc50e6826934686467c9418ce6b246232ce7f109","affectsGlobalScope":true,"impliedFormat":1},{"version":"e6f803e4e45915d58e721c04ec17830c6e6678d1e3e00e28edf3d52720909cea","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be812b06e518320ba82e2aff3ac2ca37370a9df917db708f081b9043fa3315","impliedFormat":1},{"version":"f60e3e3060207ac982da13363181fd7ee4beecc19a7c569f0d6bb034331066c2","impliedFormat":1},{"version":"17230b34bb564a3a2e36f9d3985372ccab4ad1722df2c43f7c5c2b553f68e5db","impliedFormat":1},{"version":"6e5c9272f6b3783be7bdddaf207cccdb8e033be3d14c5beacc03ae9d27d50929","impliedFormat":1},{"version":"9b4f7ff9681448c72abe38ea8eefd7ffe0c3aefe495137f02012a08801373f71","impliedFormat":1},{"version":"0dfe35191a04e8f9dc7caeb9f52f2ee07402736563d12cbccd15fb5f31ac877f","impliedFormat":1},{"version":"798367363a3274220cbed839b883fe2f52ba7197b25e8cb2ac59c1e1fd8af6b7","impliedFormat":1},{"version":"fe62b82c98a4d5bca3f8de616b606d20211b18c14e881bb6856807d9ab58131b","impliedFormat":1}],"root":[265,586,588,589,[595,598],658,659,[1419,1421],1423,1424,1443,1444,1446,1450,[1457,1522],[1524,1526],[1550,1558],[1560,1590],[1600,1626],1629,1630,[1757,1768],1816,1818,1819,[1826,1829],[1834,1846],[1868,1898],[1904,1919],[1921,1932],[1966,1982],2241,2278,2349,2350,2354,2356,2387,2388,[2390,2393],[2399,2404]],"options":{"allowJs":true,"esModuleInterop":true,"jsx":4,"module":99,"skipLibCheck":true,"strict":true,"target":4},"referencedMap":[[265,1],[586,2],[588,3],[1423,4],[1424,5],[1444,6],[1827,7],[1842,8],[1840,9],[1845,10],[1846,11],[1968,12],[1834,13],[596,14],[1837,15],[1970,16],[1972,17],[1975,18],[597,14],[1446,19],[659,20],[1976,21],[1977,16],[1870,22],[1890,23],[1869,24],[1889,25],[1875,26],[1888,27],[1882,28],[1884,29],[1879,30],[1924,31],[1874,32],[1911,33],[1931,34],[1930,35],[1979,36],[1967,37],[1966,38],[1878,39],[1921,40],[1891,41],[1898,42],[1886,43],[1897,44],[1892,41],[1913,45],[1908,46],[1904,47],[1917,48],[1918,49],[1919,50],[1909,51],[1905,52],[1907,53],[1916,54],[1915,55],[1914,56],[1906,57],[1826,58],[1844,59],[1819,60],[1922,61],[1835,46],[1836,62],[1457,63],[1932,64],[1923,65],[1978,66],[1818,67],[1969,68],[1980,69],[1981,70],[1873,71],[1838,72],[1973,70],[1982,68],[1816,73],[2241,74],[1843,75],[2278,76],[2349,77],[1929,68],[2350,71],[1896,78],[2354,79],[1871,80],[1881,81],[2356,82],[1872,80],[1894,83],[2387,84],[2388,72],[2390,85],[1883,86],[1880,87],[1926,73],[2391,68],[2392,80],[2393,87],[2399,88],[1925,72],[1928,72],[1841,75],[1927,68],[1974,89],[1868,90],[1876,75],[1885,80],[1839,91],[1912,80],[1971,92],[1895,72],[1828,93],[1910,94],[2400,72],[2401,75],[1877,72],[1893,75],[2402,80],[2404,95],[2403,73],[1829,73],[1458,96],[1460,97],[1462,98],[1463,11],[1466,99],[595,100],[1467,11],[1469,101],[1468,102],[1471,103],[1578,104],[1576,105],[1555,106],[1472,107],[1553,108],[1571,109],[1577,107],[1522,110],[1501,111],[1579,11],[1580,11],[1887,11],[1609,112],[1612,113],[1623,114],[1624,115],[1611,116],[1613,46],[1614,46],[1615,117],[1616,118],[1618,119],[1619,120],[1620,121],[1621,118],[1622,122],[1581,123],[1584,124],[1585,46],[1603,125],[1604,126],[1605,46],[1606,46],[1608,127],[1587,128],[1590,129],[1589,130],[1588,131],[1629,132],[1421,133],[658,134],[1478,11],[1575,135],[1572,136],[1574,137],[1573,138],[1525,139],[1524,140],[1516,141],[1517,141],[1521,142],[1520,143],[1518,141],[1519,143],[1499,144],[1498,145],[1487,146],[1488,147],[1497,148],[1486,149],[1496,150],[1492,151],[1495,152],[1494,152],[1490,153],[1491,153],[1489,153],[1493,153],[1500,154],[1479,147],[1483,155],[1480,146],[1481,146],[1482,146],[1420,156],[1419,157],[1583,158],[1625,159],[1566,160],[1567,161],[1565,11],[1626,11],[1550,162],[1526,163],[1551,164],[1552,165],[1443,166],[1508,167],[1514,168],[1515,169],[1617,170],[1475,171],[1477,172],[1474,173],[1476,55],[1630,174],[589,5],[1554,175],[1556,176],[1557,177],[1569,178],[1568,179],[1562,180],[1558,176],[1564,181],[1563,181],[1561,182],[1570,183],[1560,184],[1502,185],[1505,11],[1511,186],[1507,187],[1509,188],[1510,189],[1506,190],[1503,185],[1512,191],[1484,192],[1758,193],[1757,194],[1759,195],[1760,196],[1602,197],[1761,198],[1607,199],[1763,200],[1764,201],[1610,202],[1485,185],[598,11],[1582,11],[1459,11],[1461,11],[1586,203],[1465,41],[1504,11],[1762,204],[1513,11],[1464,11],[1470,11],[1600,11],[1765,11],[1766,11],[1473,11],[1767,11],[1601,11],[1768,11],[1450,205],[766,206],[1104,207],[1105,208],[1106,209],[1109,210],[1107,209],[1108,211],[688,212],[681,213],[682,213],[683,214],[684,213],[685,214],[686,213],[690,215],[687,216],[689,213],[678,217],[680,218],[1037,213],[673,219],[674,219],[675,219],[679,219],[676,219],[677,219],[1036,220],[1048,221],[1049,221],[1050,222],[691,221],[1084,221],[1085,223],[1039,224],[693,225],[694,226],[730,227],[692,228],[695,225],[696,225],[728,229],[729,230],[731,226],[732,226],[733,226],[734,226],[745,226],[735,226],[736,226],[737,226],[749,226],[739,226],[740,226],[764,231],[757,226],[746,226],[759,226],[747,226],[748,226],[738,226],[758,226],[756,226],[760,226],[761,226],[762,226],[751,226],[752,226],[753,226],[741,226],[742,226],[750,226],[743,226],[744,226],[763,226],[754,226],[755,226],[765,232],[672,233],[660,221],[1035,234],[1018,235],[1033,236],[1038,237],[1097,238],[1098,221],[1099,221],[1102,239],[1100,221],[1101,221],[1110,240],[1019,221],[1649,241],[1664,242],[1663,243],[1665,244],[1652,245],[1644,246],[1656,247],[1658,248],[1659,249],[1657,221],[1655,250],[1662,251],[1670,252],[1685,253],[1730,254],[1667,255],[1686,256],[1687,256],[1688,256],[1672,252],[1689,256],[1690,256],[1673,257],[1691,256],[1692,258],[1693,256],[1694,256],[1695,256],[1696,256],[1671,252],[1684,259],[1731,260],[1674,252],[1732,261],[1697,256],[1698,258],[1733,262],[1699,258],[1734,262],[1729,263],[1700,256],[1735,264],[1701,256],[1736,260],[1737,262],[1702,256],[1704,265],[1703,266],[1738,267],[1705,256],[1706,256],[1707,256],[1708,256],[1709,256],[1675,252],[1739,268],[1710,269],[1712,270],[1711,266],[1740,260],[1741,262],[1676,252],[1713,271],[1742,262],[1714,256],[1743,262],[1755,272],[1715,256],[1677,252],[1744,273],[1716,271],[1745,262],[1717,266],[1718,258],[1746,274],[1719,275],[1747,276],[1720,277],[1678,252],[1721,266],[1722,256],[1723,278],[1748,279],[1679,280],[1680,281],[1749,268],[1724,256],[1750,268],[1725,256],[1726,282],[1727,271],[1751,268],[1681,252],[1752,268],[1753,268],[1682,252],[1683,252],[1660,283],[1754,268],[1728,266],[1668,284],[1654,285],[1756,286],[1666,287],[1645,221],[1653,221],[1646,221],[1647,221],[1648,288],[1661,289],[1650,221],[1651,290],[1669,291],[332,221],[1769,292],[1773,293],[1778,294],[1782,295],[1779,295],[1780,296],[1781,297],[1772,296],[1787,298],[1770,292],[1777,299],[2352,300],[1788,292],[1774,295],[2351,295],[1789,298],[1775,295],[1791,301],[1792,302],[1790,295],[1786,303],[1793,304],[1795,305],[1796,306],[1797,292],[1798,307],[1784,308],[1776,295],[1771,292],[1799,296],[1800,309],[1785,296],[1801,296],[1802,307],[1803,295],[1804,296],[1805,292],[1806,296],[1807,309],[1808,310],[1810,311],[1809,295],[1811,312],[1812,302],[1794,295],[1783,221],[1034,221],[2472,313],[2496,314],[2299,221],[2282,315],[2300,316],[2281,221],[1529,317],[1528,318],[1527,221],[609,319],[1933,319],[132,320],[133,320],[134,207],[90,321],[135,322],[136,323],[137,324],[88,221],[138,325],[139,326],[140,327],[141,328],[142,329],[143,330],[144,330],[146,221],[145,331],[147,332],[148,333],[149,334],[131,335],[89,221],[150,336],[151,337],[152,338],[185,339],[153,340],[154,341],[155,342],[156,343],[157,344],[158,345],[159,346],[160,347],[161,348],[162,349],[163,349],[164,350],[165,221],[166,221],[167,351],[169,352],[168,353],[170,354],[171,355],[172,356],[173,357],[174,358],[175,359],[176,360],[177,361],[178,362],[179,363],[180,364],[181,365],[182,366],[183,367],[184,368],[2503,369],[2502,370],[83,221],[275,371],[490,292],[276,372],[274,292],[491,373],[272,374],[273,375],[81,221],[84,376],[488,292],[85,292],[590,221],[592,377],[593,378],[599,221],[1441,379],[1442,380],[1440,381],[1439,221],[1117,382],[1116,221],[1114,383],[1115,384],[1045,221],[1083,385],[1070,386],[1068,221],[1069,387],[1071,388],[1072,389],[1073,390],[1074,391],[1082,392],[1075,391],[1076,389],[1077,393],[1078,390],[1079,391],[1080,390],[1081,390],[1094,394],[1092,395],[1628,396],[1627,397],[1093,398],[1042,383],[1043,383],[1044,399],[1041,400],[1046,401],[1047,240],[1091,402],[1051,403],[1088,404],[1089,405],[1090,406],[1113,407],[1422,387],[1095,408],[1065,409],[1063,410],[1086,411],[1040,408],[1066,412],[1087,413],[1064,414],[1067,408],[1096,221],[1103,415],[1059,416],[1055,221],[1057,417],[1054,418],[1052,221],[1062,419],[1058,420],[1056,421],[1060,422],[1053,221],[1061,221],[668,423],[664,221],[666,424],[663,425],[661,221],[671,426],[667,427],[665,428],[669,429],[662,221],[670,221],[1830,11],[587,430],[2449,431],[2470,221],[2464,432],[2451,433],[2467,434],[2450,435],[2448,436],[2452,437],[2446,438],[2453,221],[2471,439],[2454,440],[2463,441],[2465,442],[2447,443],[2469,444],[2466,445],[2468,446],[2455,447],[2461,448],[2458,449],[2460,450],[2459,451],[2457,452],[2456,221],[2462,453],[2445,454],[2494,221],[2493,221],[2487,455],[2474,456],[2473,457],[2444,458],[2475,459],[2442,460],[2476,221],[2495,461],[2477,462],[2486,463],[2488,464],[2443,465],[2492,466],[2490,467],[2489,468],[2491,469],[2478,470],[2484,471],[2481,472],[2483,473],[2482,474],[2480,475],[2479,221],[2485,476],[1815,477],[1814,478],[1448,221],[2353,479],[82,221],[591,221],[2070,480],[2049,481],[2146,221],[2050,482],[1986,480],[1987,221],[1988,221],[1989,221],[1990,221],[1991,221],[1992,221],[1993,221],[1994,221],[1995,221],[1996,221],[1997,221],[1998,480],[1999,480],[2000,221],[2001,221],[2002,221],[2003,221],[2004,221],[2005,221],[2006,221],[2007,221],[2008,221],[2010,221],[2009,221],[2011,221],[2012,221],[2013,480],[2014,221],[2015,221],[2016,480],[2017,221],[2018,221],[2019,480],[2020,221],[2021,480],[2022,480],[2023,480],[2024,221],[2025,480],[2026,480],[2027,480],[2028,480],[2029,480],[2031,480],[2032,221],[2033,221],[2030,480],[2034,480],[2035,221],[2036,221],[2037,221],[2038,221],[2039,221],[2040,221],[2041,221],[2042,221],[2043,221],[2044,221],[2045,221],[2046,480],[2047,221],[2048,221],[2051,483],[2052,480],[2053,480],[2054,484],[2055,485],[2056,480],[2057,480],[2058,480],[2059,480],[2062,480],[2060,221],[2061,221],[1984,221],[2063,221],[2064,221],[2065,221],[2066,221],[2067,221],[2068,221],[2069,221],[2071,486],[2072,221],[2073,221],[2074,221],[2076,221],[2075,221],[2077,221],[2078,221],[2079,221],[2080,480],[2081,221],[2082,221],[2083,221],[2084,221],[2085,480],[2086,480],[2088,480],[2087,480],[2089,221],[2090,221],[2091,221],[2092,221],[2239,487],[2093,480],[2094,480],[2095,221],[2096,221],[2097,221],[2098,221],[2099,221],[2100,221],[2101,221],[2102,221],[2103,221],[2104,221],[2105,221],[2106,221],[2107,480],[2108,221],[2109,221],[2110,221],[2111,221],[2112,221],[2113,221],[2114,221],[2115,221],[2116,221],[2117,221],[2118,480],[2119,221],[2120,221],[2121,221],[2122,221],[2123,221],[2124,221],[2125,221],[2126,221],[2127,221],[2128,480],[2129,221],[2130,221],[2131,221],[2132,221],[2133,221],[2134,221],[2135,221],[2136,221],[2137,480],[2138,221],[2139,221],[2140,221],[2141,221],[2142,221],[2143,221],[2144,480],[2145,221],[2147,488],[1983,480],[2148,221],[2149,480],[2150,221],[2151,221],[2152,221],[2153,221],[2154,221],[2155,221],[2156,221],[2157,221],[2158,221],[2159,480],[2160,221],[2161,221],[2162,221],[2163,221],[2164,221],[2165,221],[2166,221],[2171,489],[2169,490],[2170,491],[2168,492],[2167,480],[2172,221],[2173,221],[2174,480],[2175,221],[2176,221],[2177,221],[2178,221],[2179,221],[2180,221],[2181,221],[2182,221],[2183,221],[2184,480],[2185,480],[2186,221],[2187,221],[2188,221],[2189,480],[2190,221],[2191,480],[2192,221],[2193,486],[2194,221],[2195,221],[2196,221],[2197,221],[2198,221],[2199,221],[2200,221],[2201,221],[2202,221],[2203,480],[2204,480],[2205,221],[2206,221],[2207,221],[2208,221],[2209,221],[2210,221],[2211,221],[2212,221],[2213,221],[2214,221],[2215,221],[2216,221],[2217,480],[2218,480],[2219,221],[2220,221],[2221,480],[2222,221],[2223,221],[2224,221],[2225,221],[2226,221],[2227,221],[2228,221],[2229,221],[2230,221],[2231,221],[2232,221],[2233,221],[2234,480],[1985,493],[2235,221],[2236,221],[2237,221],[2238,221],[186,494],[86,495],[87,496],[1407,497],[1411,498],[1356,499],[1171,221],[1121,500],[1405,501],[1406,502],[1119,221],[1408,503],[1193,504],[1136,505],[1159,506],[1168,507],[1139,507],[1140,508],[1141,508],[1167,509],[1142,510],[1143,508],[1149,511],[1144,512],[1145,508],[1146,508],[1169,513],[1138,514],[1147,507],[1148,512],[1150,515],[1151,515],[1152,512],[1153,508],[1154,507],[1155,508],[1156,516],[1157,516],[1158,508],[1180,517],[1188,518],[1166,519],[1196,520],[1160,521],[1162,522],[1163,519],[1174,523],[1182,524],[1187,525],[1184,526],[1189,527],[1177,528],[1178,529],[1185,530],[1186,531],[1192,532],[1183,533],[1161,503],[1194,534],[1137,503],[1181,535],[1179,536],[1165,537],[1164,519],[1195,538],[1170,539],[1190,221],[1191,540],[1410,541],[1120,503],[1231,221],[1248,542],[1197,543],[1222,544],[1229,545],[1198,545],[1199,545],[1200,546],[1228,547],[1201,548],[1216,545],[1202,549],[1203,549],[1204,546],[1205,545],[1206,546],[1207,545],[1230,550],[1208,545],[1209,545],[1210,551],[1211,545],[1212,545],[1213,551],[1214,546],[1215,545],[1217,552],[1218,551],[1219,545],[1220,546],[1221,545],[1243,553],[1239,554],[1227,555],[1251,556],[1223,557],[1224,555],[1240,558],[1232,559],[1241,560],[1238,561],[1236,562],[1242,563],[1235,564],[1247,565],[1237,566],[1249,567],[1244,568],[1233,569],[1226,570],[1225,555],[1250,571],[1234,539],[1245,221],[1246,572],[1123,573],[1313,574],[1252,575],[1287,576],[1296,577],[1253,578],[1254,578],[1255,579],[1256,578],[1295,580],[1257,581],[1258,582],[1259,583],[1260,578],[1297,584],[1298,585],[1261,578],[1263,586],[1264,577],[1266,587],[1267,588],[1268,588],[1269,579],[1270,578],[1271,578],[1272,584],[1273,579],[1274,579],[1275,588],[1276,578],[1277,577],[1278,578],[1279,579],[1280,589],[1265,590],[1281,578],[1282,579],[1283,578],[1284,578],[1285,578],[1286,578],[1415,591],[1308,592],[1294,593],[1318,594],[1288,595],[1290,596],[1291,593],[1412,597],[1301,598],[1307,599],[1303,600],[1309,601],[1413,602],[1414,529],[1304,603],[1306,604],[1312,605],[1302,606],[1289,503],[1314,607],[1262,503],[1300,608],[1305,609],[1293,610],[1292,593],[1315,611],[1316,221],[1317,612],[1299,539],[1310,221],[1311,613],[1417,614],[1418,615],[1416,616],[1132,617],[1125,618],[1175,503],[1172,619],[1176,620],[1173,621],[1367,622],[1344,623],[1350,624],[1319,624],[1320,624],[1321,625],[1349,626],[1322,627],[1337,624],[1323,628],[1324,628],[1325,625],[1326,624],[1327,629],[1328,624],[1351,630],[1329,624],[1330,624],[1331,631],[1332,624],[1333,624],[1334,631],[1335,625],[1336,624],[1338,632],[1339,631],[1340,624],[1341,625],[1342,624],[1343,624],[1364,633],[1355,634],[1370,635],[1345,636],[1346,637],[1359,638],[1352,639],[1363,640],[1354,641],[1362,642],[1361,643],[1366,644],[1353,645],[1368,646],[1365,647],[1360,648],[1348,649],[1347,637],[1369,650],[1358,651],[1357,652],[1128,653],[1130,654],[1129,653],[1131,653],[1134,655],[1133,656],[1135,657],[1126,658],[1403,659],[1371,660],[1396,661],[1400,662],[1399,663],[1372,664],[1401,665],[1392,666],[1393,662],[1394,667],[1395,668],[1380,669],[1388,670],[1398,671],[1404,672],[1373,673],[1374,671],[1377,674],[1383,675],[1387,676],[1385,677],[1389,678],[1378,679],[1381,680],[1386,681],[1402,682],[1384,683],[1382,684],[1379,685],[1397,686],[1375,687],[1391,688],[1376,539],[1390,689],[1124,539],[1122,690],[1127,691],[1409,221],[2276,692],[2277,693],[2242,221],[2250,694],[2244,695],[2251,221],[2273,696],[2248,697],[2272,698],[2269,699],[2252,700],[2253,221],[2246,221],[2243,221],[2274,701],[2270,702],[2254,221],[2271,703],[2255,704],[2257,705],[2258,706],[2247,707],[2259,708],[2260,707],[2262,708],[2263,709],[2264,710],[2266,711],[2261,712],[2267,713],[2268,714],[2245,715],[2265,716],[2249,717],[2256,221],[2275,718],[616,221],[617,719],[614,221],[615,221],[1559,221],[1445,221],[1453,720],[1454,721],[637,722],[634,221],[636,723],[635,724],[633,725],[632,726],[650,727],[649,728],[655,729],[653,728],[654,730],[645,731],[644,732],[2389,292],[727,733],[698,734],[707,734],[699,734],[708,734],[700,734],[701,734],[715,734],[714,734],[716,734],[717,734],[709,734],[702,734],[710,734],[703,734],[711,734],[704,734],[706,734],[713,734],[712,734],[718,734],[705,734],[719,734],[724,734],[725,734],[720,734],[697,221],[726,221],[722,734],[721,734],[723,734],[858,221],[980,735],[859,736],[860,737],[999,738],[1000,739],[1001,740],[1002,741],[1003,742],[1004,743],[992,744],[987,745],[988,746],[989,747],[991,742],[990,748],[986,744],[993,745],[995,749],[994,750],[985,742],[984,751],[998,744],[981,745],[982,752],[983,753],[997,742],[996,754],[861,745],[856,755],[977,756],[857,757],[979,758],[978,759],[884,760],[881,761],[941,762],[919,763],[898,764],[826,765],[1017,766],[963,767],[1006,768],[1005,736],[783,769],[792,770],[796,771],[905,772],[816,773],[787,774],[798,775],[895,773],[875,773],[910,776],[974,773],[769,777],[813,777],[782,778],[770,777],[843,773],[821,779],[822,780],[791,781],[800,782],[801,777],[802,783],[804,784],[834,785],[867,773],[969,773],[771,773],[850,786],[784,787],[793,777],[795,788],[835,777],[836,789],[837,790],[838,790],[828,791],[831,792],[788,793],[805,773],[971,773],[772,773],[806,773],[807,794],[808,773],[768,773],[847,795],[810,796],[914,797],[912,773],[913,798],[915,799],[811,773],[968,773],[973,773],[842,800],[794,769],[812,773],[844,801],[845,802],[809,773],[825,773],[1013,803],[975,804],[767,221],[876,773],[846,773],[896,773],[814,805],[815,806],[839,773],[904,807],[897,773],[902,808],[903,809],[789,810],[942,773],[851,811],[786,773],[818,812],[781,813],[852,790],[785,787],[797,777],[840,814],[773,777],[817,773],[824,773],[833,815],[820,816],[829,773],[819,817],[774,790],[832,773],[972,773],[970,773],[790,810],[848,818],[849,773],[803,773],[830,773],[943,819],[841,773],[799,773],[823,820],[879,821],[901,822],[886,221],[868,823],[865,824],[955,825],[920,826],[889,827],[944,828],[883,829],[958,830],[888,831],[906,832],[921,833],[946,834],[961,835],[918,836],[885,837],[893,838],[882,839],[917,840],[1016,841],[956,842],[945,843],[877,844],[954,845],[1007,846],[1008,846],[1012,847],[1011,848],[862,849],[1010,846],[1009,846],[908,850],[911,851],[953,852],[952,853],[776,221],[909,854],[892,855],[950,856],[775,221],[880,857],[916,858],[957,859],[779,221],[891,860],[948,861],[899,862],[887,863],[949,864],[907,865],[947,866],[874,867],[900,868],[951,869],[777,221],[890,870],[854,871],[976,872],[855,873],[959,874],[966,875],[967,876],[965,877],[933,878],[863,879],[934,880],[964,881],[870,882],[872,883],[922,884],[926,885],[873,886],[871,886],[925,887],[866,888],[927,889],[928,890],[929,891],[937,892],[935,893],[930,894],[931,895],[932,896],[938,897],[936,898],[869,899],[924,900],[939,901],[940,902],[923,903],[878,904],[864,755],[827,905],[1014,906],[1015,221],[960,907],[962,759],[853,221],[894,221],[778,221],[780,908],[2395,909],[2396,909],[2394,221],[1447,292],[1961,910],[1935,911],[1936,912],[1937,912],[1938,912],[1939,912],[1940,912],[1941,912],[1942,912],[1943,912],[1944,912],[1945,912],[1959,913],[1946,912],[1947,912],[1948,912],[1949,912],[1950,912],[1951,912],[1952,912],[1953,912],[1955,912],[1956,912],[1954,912],[1957,912],[1958,912],[1960,912],[1934,914],[1533,915],[1535,916],[1547,917],[1548,918],[1537,919],[1549,920],[1543,221],[1545,921],[1546,922],[1544,923],[1538,924],[1531,221],[1530,221],[1539,925],[1541,926],[1532,221],[1536,927],[1542,927],[1534,221],[1540,221],[1452,928],[1451,221],[1455,929],[1020,221],[1023,930],[1025,931],[1027,932],[1026,933],[1028,934],[1032,935],[1029,930],[1030,933],[1031,933],[1022,933],[1021,936],[1024,221],[1817,292],[535,937],[540,938],[530,939],[296,940],[336,941],[515,942],[331,943],[313,221],[487,221],[294,221],[504,944],[361,945],[295,221],[415,946],[339,947],[340,948],[486,949],[501,950],[397,951],[509,952],[510,953],[508,954],[507,221],[505,955],[338,956],[297,957],[440,221],[441,958],[367,959],[298,960],[368,959],[363,959],[285,959],[334,961],[333,221],[514,962],[526,221],[321,221],[462,963],[463,964],[457,292],[562,221],[465,221],[466,46],[458,965],[567,966],[566,967],[561,221],[382,221],[500,968],[499,221],[560,969],[459,292],[391,970],[387,971],[392,972],[390,221],[389,973],[388,221],[563,221],[559,221],[565,974],[564,221],[386,971],[554,975],[557,976],[376,977],[375,978],[374,979],[570,292],[373,980],[355,221],[573,221],[1832,981],[1831,221],[576,221],[575,292],[577,982],[278,221],[511,983],[512,984],[513,985],[291,221],[324,221],[290,986],[277,221],[478,292],[283,987],[477,988],[476,989],[467,221],[468,221],[475,221],[470,221],[473,990],[469,221],[471,991],[474,992],[472,991],[293,221],[288,221],[289,959],[344,221],[349,993],[350,994],[348,995],[346,996],[347,997],[342,221],[484,46],[370,46],[534,998],[541,999],[545,1000],[518,1001],[517,221],[358,221],[578,1002],[529,1003],[460,1004],[461,1005],[455,1006],[446,221],[483,1007],[520,292],[447,1008],[485,1009],[480,1010],[479,221],[481,221],[452,221],[439,1011],[519,1012],[522,1013],[449,1014],[453,1015],[444,1016],[496,1017],[528,1018],[401,1019],[416,1020],[286,1021],[527,1022],[282,1023],[351,1024],[343,221],[352,1025],[428,1026],[341,221],[427,1027],[271,221],[421,1028],[323,221],[442,1029],[417,221],[287,221],[317,221],[425,1030],[292,221],[353,1031],[451,1032],[516,1033],[450,221],[424,221],[345,221],[430,1034],[431,1035],[506,221],[433,1036],[435,1037],[434,1038],[326,221],[423,1021],[437,1039],[400,1040],[422,1041],[429,1042],[301,221],[305,221],[304,221],[303,221],[308,221],[302,221],[311,221],[310,221],[307,221],[306,221],[309,221],[312,1043],[300,221],[409,1044],[408,221],[413,1045],[410,1046],[412,1047],[414,1045],[411,1046],[322,1048],[371,1049],[525,1050],[579,221],[549,1051],[551,1052],[448,1053],[550,1054],[523,1012],[464,1012],[299,221],[402,1055],[318,1056],[319,1057],[320,1058],[316,1059],[495,1059],[365,1059],[403,1060],[366,1060],[315,1061],[314,221],[407,1062],[406,1063],[405,1064],[404,1065],[524,1066],[494,1067],[493,1068],[456,1069],[489,1070],[492,1071],[503,1072],[502,1073],[498,1074],[399,1075],[396,1076],[398,1077],[395,1078],[436,1079],[426,221],[539,221],[438,1080],[497,221],[354,1081],[445,983],[443,1082],[356,1083],[359,1084],[574,221],[357,1085],[360,1085],[537,221],[536,221],[538,221],[572,221],[362,1086],[521,221],[393,1087],[385,292],[337,221],[281,1088],[369,221],[543,292],[280,221],[553,1089],[384,292],[547,46],[383,1090],[532,1091],[381,1089],[284,221],[555,1092],[379,292],[380,292],[372,221],[279,221],[378,1093],[377,1094],[325,1095],[454,348],[364,348],[432,221],[419,1096],[418,221],[482,971],[394,292],[533,1097],[266,292],[269,1098],[270,1099],[267,292],[268,221],[335,1100],[330,1101],[329,221],[328,1102],[327,221],[531,1103],[542,1104],[544,1105],[546,1106],[1833,1107],[548,1108],[552,1109],[585,1110],[556,1110],[584,1111],[558,1112],[568,1113],[569,1114],[571,1115],[580,1116],[583,986],[582,221],[581,1117],[1639,1118],[1640,1119],[1641,1120],[1643,1121],[1638,1122],[1631,221],[1637,1123],[1633,1124],[1632,1123],[1634,1125],[1636,1126],[1642,221],[1635,221],[612,1127],[625,1128],[610,221],[611,1129],[626,1130],[621,1131],[622,1132],[620,1133],[624,1134],[618,1135],[613,1136],[623,1137],[619,1128],[2501,1138],[2498,1117],[2500,1139],[2499,221],[2497,221],[1118,1140],[631,1141],[628,1142],[629,221],[630,221],[627,1143],[1813,1144],[1456,292],[2240,1145],[2357,221],[2372,1146],[2373,1146],[2386,1147],[2374,1148],[2375,1148],[2376,1149],[2370,1150],[2368,1151],[2359,221],[2363,1152],[2367,1153],[2365,1154],[2371,1155],[2360,1156],[2361,1157],[2362,1158],[2364,1159],[2366,1160],[2369,1161],[2377,1148],[2378,1148],[2379,1148],[2380,1146],[2381,1148],[2382,1148],[2358,1148],[2383,221],[2385,1162],[2384,1148],[1825,1163],[1821,292],[1822,292],[1820,221],[1823,1164],[1824,1163],[1965,1165],[1964,1166],[2398,1167],[2397,1168],[1852,221],[1866,1169],[1847,292],[1849,1170],[1851,1171],[1850,1172],[1848,221],[1853,221],[1854,221],[1855,221],[1856,221],[1857,221],[1858,221],[1859,221],[1860,221],[1861,221],[1862,1173],[1864,1174],[1865,1174],[1863,221],[1867,1175],[2322,1176],[2324,1177],[2314,1178],[2319,1179],[2320,1180],[2326,1181],[2321,1182],[2318,1183],[2317,1184],[2316,1185],[2327,1186],[2284,1179],[2285,1179],[2325,1179],[2330,1187],[2340,1188],[2334,1188],[2342,1188],[2346,1188],[2332,1189],[2333,1188],[2335,1188],[2338,1188],[2341,1188],[2337,1190],[2339,1188],[2343,292],[2336,1179],[2331,1191],[2293,292],[2297,292],[2287,1179],[2290,292],[2295,1179],[2296,1192],[2289,1193],[2292,292],[2294,292],[2291,1194],[2280,292],[2279,292],[2348,1195],[2345,1196],[2311,1197],[2310,1179],[2308,292],[2309,1179],[2312,1198],[2313,1199],[2306,292],[2302,1200],[2305,1179],[2304,1179],[2303,1179],[2298,1179],[2307,1200],[2344,1179],[2323,1201],[2329,1202],[2328,1203],[2347,221],[2315,221],[2288,221],[2286,1204],[652,1205],[651,1206],[639,1207],[638,1208],[657,1209],[656,1210],[648,1211],[647,728],[646,1212],[1963,1213],[1962,1214],[420,1215],[1523,292],[643,1216],[641,1217],[642,1218],[640,221],[1449,221],[606,1219],[605,221],[79,221],[80,221],[13,221],[14,221],[16,221],[15,221],[2,221],[17,221],[18,221],[19,221],[20,221],[21,221],[22,221],[23,221],[24,221],[3,221],[25,221],[26,221],[4,221],[27,221],[31,221],[28,221],[29,221],[30,221],[32,221],[33,221],[34,221],[5,221],[35,221],[36,221],[37,221],[38,221],[6,221],[42,221],[39,221],[40,221],[41,221],[43,221],[7,221],[44,221],[49,221],[50,221],[45,221],[46,221],[47,221],[48,221],[8,221],[54,221],[51,221],[52,221],[53,221],[55,221],[9,221],[56,221],[57,221],[58,221],[60,221],[59,221],[61,221],[62,221],[10,221],[63,221],[64,221],[65,221],[11,221],[66,221],[67,221],[68,221],[69,221],[70,221],[1,221],[71,221],[72,221],[12,221],[76,221],[74,221],[78,221],[73,221],[77,221],[75,221],[2420,1220],[2430,1221],[2419,1220],[2440,1222],[2411,1223],[2410,1224],[2439,1117],[2433,1225],[2438,1226],[2413,1227],[2427,1228],[2412,1229],[2436,1230],[2408,1231],[2407,1117],[2437,1232],[2409,1233],[2414,1234],[2415,221],[2418,1234],[2405,221],[2441,1235],[2431,1236],[2422,1237],[2423,1238],[2425,1239],[2421,1240],[2424,1241],[2434,1117],[2416,1242],[2417,1243],[2426,1244],[2406,1140],[2429,1236],[2428,1234],[2432,221],[2435,1245],[108,1246],[119,1247],[106,1248],[120,1140],[129,1249],[97,1250],[98,1251],[96,1224],[128,1117],[123,1252],[127,1253],[100,1254],[116,1255],[99,1256],[126,1257],[94,1258],[95,1252],[101,1259],[102,221],[107,1260],[105,1259],[92,1261],[130,1262],[121,1263],[111,1264],[110,1259],[112,1265],[114,1266],[109,1267],[113,1268],[124,1117],[103,1269],[104,1270],[115,1271],[93,1140],[118,1272],[117,1259],[122,221],[91,221],[125,1273],[608,1274],[604,221],[607,1275],[1920,292],[2355,479],[601,1276],[600,319],[603,1277],[602,1278],[2283,1279],[2301,1280],[1900,1281],[1901,221],[1899,221],[1902,1282],[1903,1283],[1438,1284],[1430,1285],[1437,1286],[1432,221],[1433,221],[1431,1287],[1434,1288],[1425,221],[1426,221],[1427,1284],[1429,1289],[1435,221],[1436,1290],[1428,1291],[263,1292],[257,1293],[261,1294],[258,1294],[254,1293],[262,1295],[259,1296],[1111,1292],[260,1294],[255,1297],[256,1298],[250,1299],[194,1300],[196,1301],[249,221],[195,1302],[253,1303],[252,1304],[251,1305],[187,221],[197,1300],[198,221],[189,1306],[193,1307],[188,221],[190,1308],[191,1309],[192,221],[1112,1310],[199,1311],[200,1311],[201,1311],[202,1311],[203,1311],[204,1311],[205,1311],[206,1311],[207,1311],[208,1311],[209,1311],[210,1311],[211,1311],[213,1311],[212,1311],[214,1311],[215,1311],[216,1311],[217,1311],[248,1312],[218,1311],[219,1311],[220,1311],[221,1311],[222,1311],[223,1311],[224,1311],[225,1311],[226,1311],[227,1311],[228,1311],[229,1311],[230,1311],[232,1311],[231,1311],[233,1311],[234,1311],[235,1311],[236,1311],[237,1311],[238,1311],[239,1311],[240,1311],[241,1311],[242,1311],[243,1311],[244,1311],[247,1311],[245,1311],[246,1311],[1593,1313],[1599,1314],[1597,1315],[1595,1315],[1598,1315],[1594,1315],[1596,1315],[1592,1315],[1591,221],[264,219],[594,1316]],"affectedFilesPendingEmit":[265,588,1423,1424,1444,1827,1842,1840,1845,1846,1968,1834,596,1837,1970,1972,1975,597,1446,659,1976,1977,1870,1890,1869,1889,1875,1888,1882,1884,1879,1924,1874,1911,1931,1930,1979,1967,1966,1878,1921,1891,1898,1886,1897,1892,1913,1908,1904,1917,1918,1919,1909,1905,1907,1916,1915,1914,1906,1826,1844,1819,1922,1835,1836,1457,1932,1923,1978,1818,1969,1980,1981,1873,1838,1973,1982,1816,2241,1843,2278,2349,1929,2350,1896,2354,1871,1881,2356,1872,1894,2387,2388,2390,1883,1880,1926,2391,2392,2393,2399,1925,1928,1841,1927,1974,1868,1876,1885,1839,1912,1971,1895,1828,1910,2400,2401,1877,1893,2402,2404,2403,1829,1458,1460,1462,1463,1466,595,1467,1469,1468,1471,1578,1576,1555,1472,1553,1571,1577,1522,1501,1579,1580,1887,1609,1612,1623,1624,1611,1613,1614,1615,1616,1618,1619,1620,1621,1622,1581,1584,1585,1603,1604,1605,1606,1608,1587,1590,1589,1588,1629,1421,658,1478,1575,1572,1574,1573,1525,1524,1516,1517,1521,1520,1518,1519,1499,1498,1487,1488,1497,1486,1496,1492,1495,1494,1490,1491,1489,1493,1500,1479,1483,1480,1481,1482,1420,1419,1583,1625,1566,1567,1565,1626,1550,1526,1551,1552,1443,1508,1514,1515,1617,1475,1477,1474,1476,1630,589,1554,1556,1557,1569,1568,1562,1558,1564,1563,1561,1570,1560,1502,1505,1511,1507,1509,1510,1506,1503,1512,1484,1758,1757,1759,1760,1602,1761,1607,1763,1764,1610,1485,598,1582,1459,1461,1586,1465,1504,1762,1513,1464,1470,1600,1765,1766,1473,1767,1601,1768,1450],"version":"5.8.3"} \ No newline at end of file