auricui-skill 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -2
  3. package/bin/install.mjs +66 -0
  4. package/package.json +29 -4
  5. package/skill/SKILL.md +64 -0
  6. package/skill/agents/openai.yaml +4 -0
  7. package/skill/assets/examples/ApprovalCard.tsx +8 -0
  8. package/skill/assets/examples/BasicApp.tsx +38 -0
  9. package/skill/assets/examples/BriefingCard.tsx +8 -0
  10. package/skill/assets/examples/ChatControls.tsx +49 -0
  11. package/skill/assets/examples/ChatPanel.tsx +73 -0
  12. package/skill/assets/examples/Checkbox.tsx +13 -0
  13. package/skill/assets/examples/Combobox.tsx +14 -0
  14. package/skill/assets/examples/Drawer.tsx +10 -0
  15. package/skill/assets/examples/MemoryCard.tsx +9 -0
  16. package/skill/assets/examples/MessageList.tsx +8 -0
  17. package/skill/assets/examples/Theme.tsx +14 -0
  18. package/skill/assets/examples/ToolCall.tsx +8 -0
  19. package/skill/assets/examples/Tree.tsx +15 -0
  20. package/skill/assets/examples/examples.css +11 -0
  21. package/skill/assets/library/auricui-0.1.0-preview.1.tgz +0 -0
  22. package/skill/assets/library/auricui-0.1.0-preview.3.tgz +0 -0
  23. package/skill/assets/library/auricui-0.1.0-preview.4.tgz +0 -0
  24. package/skill/assets/upstream/LICENSE +21 -0
  25. package/skill/assets/upstream/apps/desktop/chat/src/App.tsx +1099 -0
  26. package/skill/assets/upstream/apps/desktop/chat/src/app-bridge.ts +154 -0
  27. package/skill/assets/upstream/apps/desktop/chat/src/chat.css +1812 -0
  28. package/skill/assets/upstream/apps/desktop/chat/src/components/AddSkill.tsx +223 -0
  29. package/skill/assets/upstream/apps/desktop/chat/src/components/AgentActivityRow.tsx +716 -0
  30. package/skill/assets/upstream/apps/desktop/chat/src/components/AgentIcons.tsx +46 -0
  31. package/skill/assets/upstream/apps/desktop/chat/src/components/AttachmentCard.tsx +34 -0
  32. package/skill/assets/upstream/apps/desktop/chat/src/components/AttachmentPreview.tsx +331 -0
  33. package/skill/assets/upstream/apps/desktop/chat/src/components/BalanceNotice.tsx +22 -0
  34. package/skill/assets/upstream/apps/desktop/chat/src/components/BrandLogo.tsx +35 -0
  35. package/skill/assets/upstream/apps/desktop/chat/src/components/ComposerCard.tsx +182 -0
  36. package/skill/assets/upstream/apps/desktop/chat/src/components/ConversationSessionBar.tsx +292 -0
  37. package/skill/assets/upstream/apps/desktop/chat/src/components/CopyDiagnostics.tsx +24 -0
  38. package/skill/assets/upstream/apps/desktop/chat/src/components/Divider.tsx +47 -0
  39. package/skill/assets/upstream/apps/desktop/chat/src/components/FileChangesList.tsx +119 -0
  40. package/skill/assets/upstream/apps/desktop/chat/src/components/Markdown.tsx +198 -0
  41. package/skill/assets/upstream/apps/desktop/chat/src/components/ModelPopover.tsx +280 -0
  42. package/skill/assets/upstream/apps/desktop/chat/src/components/NoticeCard.tsx +87 -0
  43. package/skill/assets/upstream/apps/desktop/chat/src/components/Popover.tsx +195 -0
  44. package/skill/assets/upstream/apps/desktop/chat/src/components/PreviewFrame.tsx +129 -0
  45. package/skill/assets/upstream/apps/desktop/chat/src/components/ProjectConversation.tsx +852 -0
  46. package/skill/assets/upstream/apps/desktop/chat/src/components/ProjectStarters.tsx +118 -0
  47. package/skill/assets/upstream/apps/desktop/chat/src/components/Tooltip.tsx +191 -0
  48. package/skill/assets/upstream/apps/desktop/chat/src/components/TurnLog.tsx +583 -0
  49. package/skill/assets/upstream/apps/desktop/chat/src/components/TurnQuestionCard.tsx +133 -0
  50. package/skill/assets/upstream/apps/desktop/chat/src/components/dock-pane-bar.ts +14 -0
  51. package/skill/assets/upstream/apps/desktop/chat/src/components/project-version.ts +37 -0
  52. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/MentionMenuPlugin.tsx +372 -0
  53. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptEditor.tsx +476 -0
  54. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptPill.tsx +399 -0
  55. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptReferenceNode.tsx +164 -0
  56. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/index.ts +31 -0
  57. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/mention-menu.ts +206 -0
  58. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/prompt-text.ts +171 -0
  59. package/skill/assets/upstream/apps/desktop/chat/src/components/useStickToBottom.ts +320 -0
  60. package/skill/assets/upstream/apps/desktop/chat/src/components/useTicker.ts +55 -0
  61. package/skill/assets/upstream/apps/desktop/chat/src/i18n/index.ts +42 -0
  62. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/en-US.ts +408 -0
  63. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/es.ts +407 -0
  64. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/ja.ts +407 -0
  65. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/ko.ts +403 -0
  66. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/words.ts +6 -0
  67. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/zh-CN.ts +407 -0
  68. package/skill/assets/upstream/apps/desktop/chat/src/i18n/lookup.ts +12 -0
  69. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-activity-runs.ts +154 -0
  70. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-menu-rows.ts +41 -0
  71. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-refs.ts +105 -0
  72. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-transcript.ts +440 -0
  73. package/skill/assets/upstream/apps/desktop/chat/src/lib/asset-lists.ts +75 -0
  74. package/skill/assets/upstream/apps/desktop/chat/src/lib/attachment-place.ts +17 -0
  75. package/skill/assets/upstream/apps/desktop/chat/src/lib/balance.ts +16 -0
  76. package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-layer.ts +10 -0
  77. package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-menu.ts +18 -0
  78. package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-store.ts +101 -0
  79. package/skill/assets/upstream/apps/desktop/chat/src/lib/composer-attachments.ts +189 -0
  80. package/skill/assets/upstream/apps/desktop/chat/src/lib/desktop-attachments.ts +178 -0
  81. package/skill/assets/upstream/apps/desktop/chat/src/lib/desktop-bridge.ts +291 -0
  82. package/skill/assets/upstream/apps/desktop/chat/src/lib/developer.ts +39 -0
  83. package/skill/assets/upstream/apps/desktop/chat/src/lib/esc-stop.ts +23 -0
  84. package/skill/assets/upstream/apps/desktop/chat/src/lib/file-changes.ts +32 -0
  85. package/skill/assets/upstream/apps/desktop/chat/src/lib/live-activity.ts +20 -0
  86. package/skill/assets/upstream/apps/desktop/chat/src/lib/local-agent-run.ts +1174 -0
  87. package/skill/assets/upstream/apps/desktop/chat/src/lib/local-sessions.ts +25 -0
  88. package/skill/assets/upstream/apps/desktop/chat/src/lib/model-calls.ts +87 -0
  89. package/skill/assets/upstream/apps/desktop/chat/src/lib/pretty-json.ts +12 -0
  90. package/skill/assets/upstream/apps/desktop/chat/src/lib/preview-top.ts +15 -0
  91. package/skill/assets/upstream/apps/desktop/chat/src/lib/project-resources.ts +66 -0
  92. package/skill/assets/upstream/apps/desktop/chat/src/lib/project-sessions.ts +66 -0
  93. package/skill/assets/upstream/apps/desktop/chat/src/lib/project-versions.ts +116 -0
  94. package/skill/assets/upstream/apps/desktop/chat/src/lib/ref-pictures.ts +119 -0
  95. package/skill/assets/upstream/apps/desktop/chat/src/lib/relative-time.ts +46 -0
  96. package/skill/assets/upstream/apps/desktop/chat/src/lib/skill-pages.ts +29 -0
  97. package/skill/assets/upstream/apps/desktop/chat/src/lib/step-file.ts +23 -0
  98. package/skill/assets/upstream/apps/desktop/chat/src/lib/structural-share.ts +52 -0
  99. package/skill/assets/upstream/apps/desktop/chat/src/lib/studio-film.ts +44 -0
  100. package/skill/assets/upstream/apps/desktop/chat/src/lib/studio-transfer.ts +40 -0
  101. package/skill/assets/upstream/apps/desktop/chat/src/lib/unprompted.ts +27 -0
  102. package/skill/assets/upstream/apps/desktop/chat/src/lib/upload-limits.ts +53 -0
  103. package/skill/assets/upstream/apps/desktop/chat/src/mount.tsx +19 -0
  104. package/skill/assets/upstream/apps/desktop/chat/src/state/local-agent-store.ts +296 -0
  105. package/skill/assets/upstream/apps/desktop/chat/src/studio-refs.ts +307 -0
  106. package/skill/assets/upstream/apps/desktop/package.json +51 -0
  107. package/skill/assets/upstream/apps/desktop/shell/bridge.ts +415 -0
  108. package/skill/assets/upstream/apps/desktop/shell/index.html +27 -0
  109. package/skill/assets/upstream/apps/desktop/shell/main.tsx +17 -0
  110. package/skill/assets/upstream/apps/desktop/shell/shell.css +17 -0
  111. package/skill/assets/upstream/apps/desktop/shell/vite.config.ts +65 -0
  112. package/skill/assets/upstream/apps/desktop/src/agent-definitions.mjs +31 -0
  113. package/skill/assets/upstream/packages/openfilm/LICENSE +21 -0
  114. package/skill/assets/upstream/packages/openfilm/package.json +88 -0
  115. package/skill/assets/upstream/packages/openfilm/src/film-doc.d.mts +125 -0
  116. package/skill/assets/upstream/packages/openfilm/src/film-doc.mjs +986 -0
  117. package/skill/assets/upstream/packages/openfilm/src/overrides.mjs +73 -0
  118. package/skill/assets/upstream/packages/openfilm/src/timeline.mjs +1280 -0
  119. package/skill/assets/upstream/packages/openfilm/studio/server/bridge.js +209 -0
  120. package/skill/assets/upstream/packages/openfilm/studio/server/film-subtitle.mjs +785 -0
  121. package/skill/assets/upstream/packages/openfilm/studio/server/ops.mjs +403 -0
  122. package/skill/assets/upstream/packages/openfilm/studio/server/stage.js +1649 -0
  123. package/skill/assets/upstream/packages/openfilm/studio/server/track-layout.mjs +43 -0
  124. package/skill/assets/upstream/packages/openfilm/studio/ui/src/App.tsx +156 -0
  125. package/skill/assets/upstream/packages/openfilm/studio/ui/src/api.ts +328 -0
  126. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/AudioMeters.tsx +188 -0
  127. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BlockThumbs.tsx +84 -0
  128. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BlockWaveform.tsx +201 -0
  129. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BrandLoadingScreen.tsx +56 -0
  130. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BrandLogo.tsx +25 -0
  131. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Card.tsx +89 -0
  132. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ColorPicker.tsx +204 -0
  133. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ConfirmDialog.tsx +96 -0
  134. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ContextMenu.tsx +422 -0
  135. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/DesktopDialog.tsx +271 -0
  136. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ExamplesGallery.tsx +372 -0
  137. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmFullscreenBar.tsx +147 -0
  138. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmInspector.tsx +2310 -0
  139. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmStageSelect.tsx +3296 -0
  140. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FocusNav.tsx +28 -0
  141. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FontFamilyPicker.tsx +293 -0
  142. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FramePicker.tsx +69 -0
  143. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/InspectorPanel.tsx +25 -0
  144. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Launcher.tsx +38 -0
  145. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/LayersPanel.tsx +277 -0
  146. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/LiveTime.tsx +101 -0
  147. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MediaPool.tsx +1413 -0
  148. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MediaPreview.tsx +188 -0
  149. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MenuSelect.tsx +161 -0
  150. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MgOutline.tsx +24 -0
  151. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/OperationHistory.tsx +184 -0
  152. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PaneDivider.tsx +292 -0
  153. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PasteAttrsDialog.tsx +69 -0
  154. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PlaybackMenu.tsx +110 -0
  155. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Popover.tsx +159 -0
  156. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectCover.tsx +60 -0
  157. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectHistoryControls.tsx +748 -0
  158. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectLayoutMenu.tsx +82 -0
  159. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectMenu.tsx +111 -0
  160. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectSources.tsx +587 -0
  161. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectView.tsx +3222 -0
  162. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectsLibrary.tsx +567 -0
  163. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectsView.tsx +1593 -0
  164. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PropertiesSection.tsx +418 -0
  165. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ResourceInspector.tsx +100 -0
  166. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SelectionToolbar.tsx +157 -0
  167. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SkillsLibrary.tsx +504 -0
  168. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SourcePreview.tsx +464 -0
  169. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SpendAsks.tsx +88 -0
  170. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/StageMaskOverlay.tsx +299 -0
  171. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/StageTransformFrame.tsx +246 -0
  172. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleInspector.module.css +46 -0
  173. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleInspector.tsx +884 -0
  174. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleOverlay.tsx +148 -0
  175. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleRow.tsx +419 -0
  176. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/TabStrip.tsx +188 -0
  177. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Timeline.tsx +4911 -0
  178. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/TimelineMarkers.tsx +329 -0
  179. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Toast.tsx +55 -0
  180. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Tooltip.tsx +193 -0
  181. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/UpdateButton.tsx +104 -0
  182. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ViewerBar.tsx +98 -0
  183. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/WhatsNew.tsx +147 -0
  184. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/dock-pane-bar.ts +16 -0
  185. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/export/ExportCenter.tsx +1502 -0
  186. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/export/ExportTray.tsx +284 -0
  187. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/film-stage-hit.ts +18 -0
  188. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/inspector-fields.tsx +863 -0
  189. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/inspector-look.tsx +1357 -0
  190. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/player-controls/controls-bar.tsx +397 -0
  191. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/player-controls/transport.tsx +294 -0
  192. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/replace-drop.ts +20 -0
  193. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/AgentsSection.tsx +613 -0
  194. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/CustomServices.tsx +104 -0
  195. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/DiagnosticsGroup.tsx +202 -0
  196. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ModelsSection.tsx +179 -0
  197. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ProviderMark.tsx +24 -0
  198. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ProvidersSection.tsx +177 -0
  199. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/SettingsModal.tsx +511 -0
  200. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/SettingsSelect.tsx +84 -0
  201. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ShortcutsSection.tsx +291 -0
  202. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/StorageGroup.tsx +119 -0
  203. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/theme.ts +47 -0
  204. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ui.tsx +139 -0
  205. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/use-settings.ts +10 -0
  206. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/stage-menu.tsx +283 -0
  207. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/timeline-head-extras.tsx +91 -0
  208. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/typing-target.ts +45 -0
  209. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-editor-tools.tsx +408 -0
  210. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-page-changes.ts +48 -0
  211. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-project-history.ts +113 -0
  212. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/useBlockDrag.ts +639 -0
  213. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/Preview.tsx +426 -0
  214. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/player.ts +558 -0
  215. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/preview-frames.ts +36 -0
  216. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/steps.ts +36 -0
  217. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/undo.ts +156 -0
  218. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/use-film.ts +212 -0
  219. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/use-preview-renders.ts +89 -0
  220. package/skill/assets/upstream/packages/openfilm/studio/ui/src/globals.css +559 -0
  221. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/en.ts +1901 -0
  222. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/es.ts +1900 -0
  223. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/index.ts +130 -0
  224. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/ja.ts +1900 -0
  225. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/ko.ts +1896 -0
  226. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/words.ts +6 -0
  227. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/zh-CN.ts +1901 -0
  228. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/agent-row.ts +21 -0
  229. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/asset-timeline.ts +469 -0
  230. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/audio-levels.ts +60 -0
  231. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/batch-map.ts +28 -0
  232. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/browser.ts +10 -0
  233. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/canvas-fit.ts +116 -0
  234. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/chat-marks.ts +114 -0
  235. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-attrs.ts +102 -0
  236. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-fade.ts +70 -0
  237. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-look.ts +520 -0
  238. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-mask.ts +536 -0
  239. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-replace.ts +60 -0
  240. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/component-props.ts +128 -0
  241. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/context-length.ts +12 -0
  242. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/desktop-build.ts +44 -0
  243. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/diagnostics.ts +68 -0
  244. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/double-press.ts +22 -0
  245. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/dropped-overrides.ts +139 -0
  246. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/export-jobs.ts +191 -0
  247. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/export-spec.ts +261 -0
  248. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/field-input.ts +155 -0
  249. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-clip-edits.ts +82 -0
  250. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-frame.ts +134 -0
  251. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-overrides.ts +107 -0
  252. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-shape.ts +172 -0
  253. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film.ts +80 -0
  254. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/font-menu.ts +236 -0
  255. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/font-styles.ts +129 -0
  256. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/freeze-frame.ts +49 -0
  257. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/host.ts +438 -0
  258. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/inspect-scrub.ts +26 -0
  259. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/keymap-store.ts +74 -0
  260. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/keymap.ts +529 -0
  261. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/layers-tree.ts +133 -0
  262. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/look.ts +83 -0
  263. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/loudness.ts +58 -0
  264. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/markers.ts +152 -0
  265. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-bar.ts +58 -0
  266. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-folders.ts +45 -0
  267. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-place.ts +102 -0
  268. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-queue.ts +107 -0
  269. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-selection.ts +64 -0
  270. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/menu-position.ts +77 -0
  271. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/mg-transform.ts +111 -0
  272. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/mixed.ts +3 -0
  273. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/page-changes.ts +45 -0
  274. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/picture-files.ts +19 -0
  275. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/preview-prefs.ts +46 -0
  276. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/preview-renders.ts +71 -0
  277. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-delete.ts +20 -0
  278. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-folders.ts +144 -0
  279. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-history.ts +188 -0
  280. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-sources.ts +283 -0
  281. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/projects-page.ts +40 -0
  282. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/provider-marks.ts +35 -0
  283. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/providers.ts +163 -0
  284. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/resource-drag.ts +131 -0
  285. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/retry-image.ts +17 -0
  286. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/saved-insert.ts +40 -0
  287. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/saved.ts +125 -0
  288. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/selection-toolbar.ts +158 -0
  289. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/shortcut-hint.ts +21 -0
  290. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/similar-layers.ts +46 -0
  291. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/solo.ts +19 -0
  292. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-clip.ts +60 -0
  293. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-crop.ts +232 -0
  294. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-gesture.ts +277 -0
  295. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-layers.ts +71 -0
  296. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-mask.ts +176 -0
  297. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-measure.ts +53 -0
  298. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-types.ts +155 -0
  299. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/studio-refs.ts +248 -0
  300. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/studio-version.ts +41 -0
  301. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/subtitle-cues.ts +238 -0
  302. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/subtitles.ts +263 -0
  303. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/tab-menu.ts +6 -0
  304. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/template-controls.ts +131 -0
  305. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/theme-changes.ts +31 -0
  306. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-cache.ts +63 -0
  307. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-cell.ts +8 -0
  308. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-frame.ts +76 -0
  309. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-pick.ts +42 -0
  310. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timecode.ts +161 -0
  311. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-clipboard.ts +279 -0
  312. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-drag.ts +589 -0
  313. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-edit.ts +1138 -0
  314. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-keys.ts +63 -0
  315. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-layout.ts +1038 -0
  316. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-nav.ts +160 -0
  317. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-prefs.ts +73 -0
  318. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-split.ts +40 -0
  319. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-view.ts +105 -0
  320. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/title.ts +39 -0
  321. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/track-heights.ts +55 -0
  322. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/track-lock.ts +27 -0
  323. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/transitions.ts +459 -0
  324. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/type-metrics.ts +83 -0
  325. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-film-subtitles.ts +111 -0
  326. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-fonts.ts +181 -0
  327. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-loaded-thumbs.ts +91 -0
  328. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-edits.ts +134 -0
  329. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-font.ts +51 -0
  330. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-style.ts +68 -0
  331. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/waveform-columns.ts +48 -0
  332. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/web-update.ts +41 -0
  333. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/whats-new.ts +401 -0
  334. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/workspace-resources.ts +373 -0
  335. package/skill/assets/upstream/packages/openfilm/studio/ui/src/main.tsx +10 -0
  336. package/skill/assets/upstream/packages/openfilm/studio/ui/vite.config.ts +12 -0
  337. package/skill/assets/upstream/packages/shared/package.json +45 -0
  338. package/skill/assets/upstream/packages/shared/src/agent-activity.ts +263 -0
  339. package/skill/assets/upstream/packages/shared/src/contract.ts +285 -0
  340. package/skill/assets/upstream/packages/shared/src/index.ts +17 -0
  341. package/skill/assets/upstream/packages/shared/src/markdown-blocks.ts +252 -0
  342. package/skill/assets/upstream/packages/shared/src/modality.ts +4 -0
  343. package/skill/assets/upstream/packages/shared/src/prompt-reference.ts +113 -0
  344. package/skill/assets/upstream/packages/shared/src/title.ts +76 -0
  345. package/skill/assets/upstream/packages/shared/src/turn.ts +68 -0
  346. package/skill/assets/upstream/packages/shared/src/video.ts +26 -0
  347. package/skill/references/agent-components.md +33 -0
  348. package/skill/references/api-index.json +2693 -0
  349. package/skill/references/chat-components.md +65 -0
  350. package/skill/references/components.md +23 -0
  351. package/skill/references/core-components.md +44 -0
  352. package/skill/references/dependencies.md +22 -0
  353. package/skill/references/esm-package.md +48 -0
  354. package/skill/references/integration.md +43 -0
  355. package/skill/references/library-manifest.json +12 -0
  356. package/skill/references/source-manifest.json +325 -0
  357. package/skill/references/studio-components.md +24 -0
  358. package/skill/references/theme-motion.md +49 -0
  359. package/skill/references/troubleshooting.md +27 -0
  360. package/skill/references/verification.md +13 -0
  361. package/skill/scripts/check_dependencies.py +109 -0
  362. package/skill/scripts/inspect_api.py +60 -0
@@ -0,0 +1,31 @@
1
+ /**
2
+ * How many times the theme (`<html data-theme>`) has changed since the page loaded, for what draws the theme's colors
3
+ * on a canvas (the audio meters, the waveforms): a canvas keeps the colors it was drawn with, so it draws again when
4
+ * this changes. One observer for the whole page, made when the first one asks.
5
+ */
6
+ import React from 'react';
7
+
8
+ let changes = 0;
9
+ const listeners = new Set<() => void>();
10
+ let watching = false;
11
+
12
+ function watch(): void {
13
+ if (watching || typeof MutationObserver === 'undefined' || typeof document === 'undefined') return;
14
+ watching = true;
15
+ let theme = document.documentElement.dataset.theme;
16
+ new MutationObserver(() => {
17
+ if (document.documentElement.dataset.theme === theme) return;
18
+ theme = document.documentElement.dataset.theme;
19
+ changes += 1;
20
+ for (const fn of listeners) fn();
21
+ }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
22
+ }
23
+
24
+ /** A number that changes when the theme does: put it among an effect's dependencies to draw again. */
25
+ export function useThemeChanges(): number {
26
+ return React.useSyncExternalStore(
27
+ (fn) => { watch(); listeners.add(fn); return () => { listeners.delete(fn); }; },
28
+ () => changes,
29
+ () => 0,
30
+ );
31
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Which thumbnails the timeline wants: the cell width shared by shooting and drawing, and the step shots are taken at.
3
+ */
4
+
5
+ /**
6
+ * How wide one thumbnail cell is on the timeline — the one number shooting and drawing share. If they ever differ,
7
+ * shooting follows the film's steps and drawing the clip's, and every cell shows its neighbor's frame.
8
+ */
9
+ export const THUMB_CELL_PX = 80;
10
+
11
+ /**
12
+ * The step snapped up to a fixed ladder (1 · 2 · 2.5 · 5 × 10ⁿ ms). The raw step (cell width / px per ms) changes with
13
+ * every zoom, which would make every zoom a new set of times and keys and shoot the whole timeline again. On the
14
+ * ladder, zooming within a rung keeps the same times; cells are at most 2.5× sparser and still filled (each cell takes
15
+ * the nearest shot).
16
+ */
17
+ const LADDER = [1, 2, 2.5, 5];
18
+ export function ladderStepMs(rawMs: number): number {
19
+ if (!(rawMs > 0)) return rawMs;
20
+ let scale = 10 ** Math.floor(Math.log10(rawMs));
21
+ for (;;) {
22
+ for (const k of LADDER) {
23
+ const step = k * scale;
24
+ if (step >= rawMs - 1e-9) return step;
25
+ }
26
+ scale *= 10;
27
+ }
28
+ }
29
+
30
+ /** The stretch one picture clip takes on the timeline. */
31
+ export interface ThumbSpan {
32
+ startMs: number;
33
+ endMs: number;
34
+ }
35
+
36
+ /**
37
+ * One extra shot for every clip no step falls into (times and spans in the clip's source ms). Zoomed out, a step can
38
+ * be longer than a clip, and that clip would show its neighbors' frames, at that zoom for good. The extra shot is ~28% into the clip (its first frames
39
+ * often still fade in), fixed by the clip alone so it stays cached across zooms.
40
+ */
41
+ export function thumbAnchorTimes(opts: {
42
+ times: readonly number[];
43
+ spans: readonly ThumbSpan[];
44
+ fromMs?: number;
45
+ toMs?: number;
46
+ }): number[] {
47
+ const from = opts.fromMs ?? -Infinity;
48
+ const to = opts.toMs ?? Infinity;
49
+ const out: number[] = [];
50
+ for (const span of opts.spans) {
51
+ const dur = span.endMs - span.startMs;
52
+ if (!(dur > 0)) continue;
53
+ if (span.endMs <= from || span.startMs >= to) continue;
54
+ if (opts.times.some((t) => t >= span.startMs && t < span.endMs)) continue;
55
+ /* 28% in, not before 240 ms, not past the end; clips under 240 ms take the middle */
56
+ const into = dur <= 240
57
+ ? dur / 2
58
+ : Math.min(Math.max(240, dur * 0.28), Math.max(0, dur - 40));
59
+ const t = Math.round(span.startMs + into);
60
+ if (!out.includes(t)) out.push(t);
61
+ }
62
+ return out;
63
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * How a cell of a clip's thumbnail row is drawn (BlockThumbs): at its own width. An <img> takes the page's
3
+ * `max-width: 100%`, so in a clip narrower than a cell every picture was squeezed to the clip's width while the
4
+ * cells kept their steps, and the clip color showed between them, as if the pictures were broken.
5
+ */
6
+ export function thumbCellStyle(left: number, width: number, height: number) {
7
+ return { left, width, maxWidth: 'none', height, opacity: 0.9 } as const;
8
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Thumbnail keys: which clip, and which millisecond within itself, a thumbnail shows.
3
+ *
4
+ * ProjectView builds the keys as `mtime:clipId@intoMs#v` (the source file's mtime, the clip, the in-clip millisecond).
5
+ * Keyed by the clip's own millisecond (counting its trim) rather than film time, moving a clip keeps its pictures;
6
+ * only trimming it or changing its file gives new keys.
7
+ */
8
+
9
+ /** A thumbnail key back to "which clip, which in-clip millisecond". Null for keys it does not recognize. */
10
+ function parseClipFrameKey(key: string): { id: string; intoMs: number } | null {
11
+ const hash = key.lastIndexOf('#');
12
+ const at = key.lastIndexOf('@', hash);
13
+ if (at < 0 || hash <= at) return null;
14
+ const intoMs = Number(key.slice(at + 1, hash));
15
+ if (!Number.isFinite(intoMs)) return null;
16
+ const left = key.slice(0, at);
17
+ const colon = left.lastIndexOf(':');
18
+ if (colon < 0) return null;
19
+ return { id: left.slice(colon + 1), intoMs };
20
+ }
21
+
22
+ /** Which thumbnails belong to a clip on the timeline. */
23
+ export function clipThumbId(block: { clipId?: string; loc?: string; id: string }): string {
24
+ return block.clipId || block.loc || block.id;
25
+ }
26
+
27
+ /**
28
+ * The in-clip millisecond at the clip's left edge. A move keeps the left edge's frame; pulling the left edge reaches
29
+ * deeper into the source, as a video seek would.
30
+ */
31
+ export function thumbIntoOrigin(opts: {
32
+ committedStart: number;
33
+ committedEnd: number;
34
+ visualStart: number;
35
+ visualEnd: number;
36
+ trimFromMs: number;
37
+ /** source ms per film ms: the keys are the source's, so a left trim of a sped-up clip reaches `speed` times deeper */
38
+ speed?: number;
39
+ }): number {
40
+ const visualDur = opts.visualEnd - opts.visualStart;
41
+ const committedDur = opts.committedEnd - opts.committedStart;
42
+ const leftMoved = Math.abs(opts.visualStart - opts.committedStart) > 0.5;
43
+ const durChanged = Math.abs(visualDur - committedDur) > 0.5;
44
+ return opts.trimFromMs + (leftMoved && durChanged ? (opts.visualStart - opts.committedStart) * (opts.speed ?? 1) : 0);
45
+ }
46
+
47
+ /** The thumbnails in memory, per clip: in-clip ms → address. What BlockThumbs draws, detached from film time. */
48
+ export function indexClipThumbs(
49
+ thumbs: ReadonlyMap<string, string>,
50
+ ): Map<string, Map<number, string>> {
51
+ const by = new Map<string, Map<number, string>>();
52
+ for (const [key, url] of thumbs) {
53
+ const parsed = parseClipFrameKey(key);
54
+ if (!parsed) continue;
55
+ let strip = by.get(parsed.id);
56
+ if (!strip) {
57
+ strip = new Map();
58
+ by.set(parsed.id, strip);
59
+ }
60
+ if (!strip.has(parsed.intoMs)) strip.set(parsed.intoMs, url);
61
+ }
62
+ return by;
63
+ }
64
+
65
+ /**
66
+ * A page clip's thumbnails are drawn with its overrides (SPEC §1), the changes made inside it in Studio: as a short
67
+ * token for a frame's address and key, '' when it has none of a layer's (its fades alone change no picture). Another
68
+ * change, another token: the timeline asks for the clip's pictures again.
69
+ */
70
+ export function overridesToken(overrides: readonly unknown[] | undefined): string {
71
+ if (!overrides?.some((o) => typeof o === 'object' && o !== null && 'at' in o)) return '';
72
+ const text = JSON.stringify(overrides);
73
+ let h = 0x811c9dc5;
74
+ for (let i = 0; i < text.length; i++) { h ^= text.charCodeAt(i); h = Math.imul(h, 0x01000193); }
75
+ return (h >>> 0).toString(36);
76
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Which shot a cell of a clip's thumbnail row shows (BlockThumbs). The cells sit on the source's own time grid, the
3
+ * first one straddling the clip's start; a cell stands for the part of it inside the clip, so the straddling one asks
4
+ * for the clip's start, not for the grid time before it (a trimmed clip's row must not open on a frame from before its
5
+ * in-point, often the source's black first frame). A shot from inside the clip beats a nearer one from outside it;
6
+ * with none inside yet, the nearest of any: a far picture beats a half-empty clip, and exact shots replace it as they
7
+ * arrive.
8
+ */
9
+
10
+ /** Index of the sorted time nearest to `want` (binary search: dozens of cells, each scanning would be thousands of steps). */
11
+ function nearestIndex(sorted: readonly number[], want: number, lo = 0, hi = sorted.length - 1): number {
12
+ const first = lo;
13
+ while (lo < hi) {
14
+ const mid = (lo + hi) >> 1;
15
+ if (sorted[mid]! < want) lo = mid + 1; else hi = mid;
16
+ }
17
+ return lo > first && want - sorted[lo - 1]! < sorted[lo]! - want ? lo - 1 : lo;
18
+ }
19
+
20
+ /** First index whose time is at least `at` (sorted.length when none is). */
21
+ function firstAtLeast(sorted: readonly number[], at: number): number {
22
+ let lo = 0;
23
+ let hi = sorted.length;
24
+ while (lo < hi) {
25
+ const mid = (lo + hi) >> 1;
26
+ if (sorted[mid]! < at) lo = mid + 1; else hi = mid;
27
+ }
28
+ return lo;
29
+ }
30
+
31
+ /**
32
+ * The shot for a cell standing at `cellMs` on the grid of a clip that shows `fromMs`–`toMs` of its source (in-clip
33
+ * source ms): `undefined` when there are no shots at all.
34
+ */
35
+ export function pickShot(sorted: readonly number[], cellMs: number, fromMs: number, toMs: number): number | undefined {
36
+ if (!sorted.length) return undefined;
37
+ const want = Math.min(Math.max(cellMs, fromMs), toMs);
38
+ const lo = firstAtLeast(sorted, fromMs);
39
+ const hi = firstAtLeast(sorted, toMs + 1) - 1;
40
+ if (lo <= hi) return sorted[nearestIndex(sorted, want, lo, hi)];
41
+ return sorted[nearestIndex(sorted, want)];
42
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Timecode: `00:00:47:29`, hours:minutes:seconds:frames, the way editors read a position.
3
+ *
4
+ * The film has no frames: time is milliseconds and the picture is drawn live in the browser. The last two digits are
5
+ * a reading scale, not a unit of the document: people cut by frame ("second 47, frame 29"). The scale is the
6
+ * project's frame rate (kept in `.film/settings.json`, see lib/project-settings), 30 until a project says otherwise.
7
+ * It also sets the grid a click, a drag and the arrow keys land on.
8
+ *
9
+ * At 23.976, 29.97 and 59.94 the timecode is the clock's: the seconds are the film's own seconds, and the frames count
10
+ * the frames that start within that second (29 or 30 of them at 29.97). It never drifts from the ruler, which a
11
+ * non-drop SMPTE timecode does (3.6 s an hour), and it skips no numbers the way drop-frame timecode (`;`) does; the
12
+ * price is that the last frame number of a second is missing about once a thousand frames.
13
+ *
14
+ * Fixed width (`00:` even under an hour): a timecode is read while it changes, and a changing width makes it jitter.
15
+ */
16
+ import { useSyncExternalStore } from 'react';
17
+
18
+ /** The frame rate before a project says otherwise, and the grid the ruler's steps are counted in. */
19
+ export const DISPLAY_FPS = 30;
20
+
21
+ /**
22
+ * The rates a project can be set to and a video exported at: the common ones (the NTSC 23.976, 29.97 and 59.94 are
23
+ * what cameras and phones record), and 120 for high frame rate.
24
+ */
25
+ export const FRAME_RATES = [23.976, 24, 25, 29.97, 30, 48, 50, 59.94, 60, 120] as const;
26
+
27
+ /** A rate as it really is: the NTSC ones are 24000/1001, 30000/1001 and 60000/1001. */
28
+ export function exactRate(fps: number): number {
29
+ if (!(fps > 0) || !Number.isFinite(fps)) return DISPLAY_FPS;
30
+ const whole = Math.round(fps);
31
+ /* 23.976, 29.97, 59.94: a whole rate slowed by 1000/1001 */
32
+ if (Math.abs(fps - whole) > 0.001 && Math.abs(fps - (whole * 1000) / 1001) < 0.01) return (whole * 1000) / 1001;
33
+ return fps;
34
+ }
35
+
36
+ /** The rate an editor names: a probed 29.97002997 is 29.97, 24.0 is 24; another rate is kept as it is. */
37
+ export function namedRate(fps: number): number {
38
+ const named = FRAME_RATES.find((r) => Math.abs(r - fps) < 0.01);
39
+ return named ?? Math.round(fps * 1000) / 1000;
40
+ }
41
+
42
+ /**
43
+ * A project's rate before one is chosen: the first footage's that is a rate a project can have (in the order given:
44
+ * the film's footage clips by time, then the project's other videos), else 30.
45
+ */
46
+ export function footageFps(rates: readonly (number | null | undefined)[]): number {
47
+ for (const r of rates) {
48
+ if (r == null || !(r > 0)) continue;
49
+ const named = namedRate(r);
50
+ if ((FRAME_RATES as readonly number[]).includes(named)) return named;
51
+ }
52
+ return DISPLAY_FPS;
53
+ }
54
+
55
+ /** How many frames ⇧ + an arrow steps: a second's worth, whole. */
56
+ export function secondFrames(fps = currentFps): number {
57
+ return Math.max(1, Math.round(fps));
58
+ }
59
+
60
+ /* ── the project's rate, for everything that shows or snaps to frames ── */
61
+
62
+ let currentFps: number = DISPLAY_FPS;
63
+ const listeners = new Set<() => void>();
64
+
65
+ /** The rate frames are counted in now (the project open). */
66
+ export function timecodeFps(): number {
67
+ return currentFps;
68
+ }
69
+
70
+ /** Count frames at this rate from now on (a project opened, or its rate changed). */
71
+ export function setTimecodeFps(fps: number): void {
72
+ const next = fps > 0 && Number.isFinite(fps) ? fps : DISPLAY_FPS;
73
+ if (next === currentFps) return;
74
+ currentFps = next;
75
+ for (const fn of listeners) fn();
76
+ }
77
+
78
+ /** The rate, kept up to date: a component that shows frames renders again when the project's rate changes. */
79
+ export function useTimecodeFps(): number {
80
+ return useSyncExternalStore(
81
+ (fn) => { listeners.add(fn); return () => { listeners.delete(fn); }; },
82
+ timecodeFps,
83
+ () => DISPLAY_FPS,
84
+ );
85
+ }
86
+
87
+ function pad(n: number): string {
88
+ return String(n).padStart(2, '0');
89
+ }
90
+
91
+ /**
92
+ * The frame a time falls on, counted from 0. Rounded to the nearest: a frame at 30 lasts 33.33… ms, which no float or
93
+ * whole millisecond holds exactly — rounded down, 500 ms (14.999… frames) read as frame 14 and a time written to the
94
+ * millisecond (1033 ms) as the frame before its own.
95
+ */
96
+ export function frameAt(ms: number, fps = currentFps): number {
97
+ return Number.isFinite(ms) && ms > 0 ? Math.round((ms * exactRate(fps)) / 1000) : 0;
98
+ }
99
+
100
+ /** The time of a frame. */
101
+ export function frameMs(frame: number, fps = currentFps): number {
102
+ return (frame * 1000) / exactRate(fps);
103
+ }
104
+
105
+ /**
106
+ * The frame nearest `ms`, in ms. What an edit on the timeline writes: a drag lands between frames (a second is 1998 ms
107
+ * of pointer travel), and film.html should hold the moments a person sees on the frame grid.
108
+ */
109
+ export function snapToFrame(ms: number, fps = currentFps): number {
110
+ return frameMs(frameAt(ms, fps), fps);
111
+ }
112
+
113
+ /**
114
+ * `frames` frames on from `ms` (negative = back), landing exactly on a frame: stepping by adding 33.33… ms gathers
115
+ * float error until one frame shows twice and the next is skipped.
116
+ */
117
+ export function stepFrames(ms: number, frames: number, fps = currentFps): number {
118
+ return frameMs(Math.max(0, frameAt(ms, fps) + frames), fps);
119
+ }
120
+
121
+ /** A frame as the clock reads it: the whole second it starts in, and which frame of that second it is. */
122
+ function clockOf(frame: number, fps: number): { secs: number; frames: number } {
123
+ const rate = exactRate(fps);
124
+ const secs = Math.floor(frame / rate + 1e-9);
125
+ /* the first frame that starts in this second */
126
+ const first = Math.ceil(secs * rate - 1e-9);
127
+ return { secs, frames: frame - first };
128
+ }
129
+
130
+ /**
131
+ * The time, in seconds, a timecode's second and frame name: clockOf's way back, so a typed `00:00:17:00` is the frame
132
+ * that shows as it. At 29.97 the 17th second's first frame starts 17 ms after 17 s; `17 + 0 / 29.97` lands a frame
133
+ * early, on the one that reads `00:00:16:29`.
134
+ */
135
+ export function timecodeSeconds(secs: number, frames: number, fps = currentFps): number {
136
+ const rate = exactRate(fps);
137
+ return (Math.ceil(secs * rate - 1e-9) + frames) / rate;
138
+ }
139
+
140
+ export function formatTimecode(ms: number, opts?: { hours?: boolean; fps?: number }): string {
141
+ const { secs, frames } = clockOf(frameAt(ms, opts?.fps), opts?.fps ?? currentFps);
142
+ const tail = `${pad(Math.floor((secs % 3600) / 60))}:${pad(secs % 60)}:${pad(frames)}`;
143
+ /* `hours: false` drops the always-`00:` hour of a film under an hour; the caller decides once per film, so the
144
+ width still never changes */
145
+ if (opts?.hours === false && secs < 3600) return tail;
146
+ return `${pad(Math.floor(secs / 3600))}:${tail}`;
147
+ }
148
+
149
+ /**
150
+ * The short label on the ruler: `mm:ss` on whole seconds (with hours past an hour), `12f` in between. Full timecodes
151
+ * on every tick would be a wall of `00:00:47:29`.
152
+ */
153
+ export function formatRulerLabel(ms: number, fps = currentFps): string {
154
+ /* a tick is a place on the clock, not a frame: a whole second is labeled as one even where no frame starts on it */
155
+ const secs = Math.max(0, Math.floor(ms / 1000 + 1e-6));
156
+ const frames = frameAt(ms, fps) - Math.ceil(secs * exactRate(fps) - 1e-9);
157
+ if (ms - secs * 1000 >= 0.5 && frames > 0) return `${frames}f`;
158
+ const h = Math.floor(secs / 3600);
159
+ const clock = `${pad(Math.floor((secs % 3600) / 60))}:${pad(secs % 60)}`;
160
+ return h > 0 ? `${h}:${clock}` : clock;
161
+ }
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Copy and paste on the timeline.
3
+ *
4
+ * The clipboard holds clips by their id in film.html, not by their place: a split, a delete or an undo renumbers the
5
+ * places (`film.html#t.c`), and a place copied before then would paste whichever clip sits there now. A paste copies
6
+ * the clip as film.html has it then; a clip that is gone by then is reported, not swapped for another.
7
+ *
8
+ * Start times are kept so several clips keep their spacing; tracks are kept so clips copied from several tracks go
9
+ * back to their own tracks. Only clips copied from one track follow a track the person points at.
10
+ */
11
+ import type { Op } from '../api.ts';
12
+ import { newTrackSide } from './timeline-drag.ts';
13
+ import {
14
+ TOUCH_MS, freshLink, insertPointOn, mainTrackOf, magnetOn, planRoom,
15
+ type EditModel, type EditPlan, type Placed, type Switches, type TrackDest,
16
+ } from './timeline-edit.ts';
17
+ import type { TimelineBlock, TimelineTrack } from './timeline-layout';
18
+
19
+ export type TimelineClipboardItem = {
20
+ /** The clip's id in film.html. */
21
+ clipId: string;
22
+ startMs: number;
23
+ /** The clip's length: a paste checks whether the spot is free. */
24
+ durMs: number;
25
+ lane: string;
26
+ };
27
+
28
+ export function laneOfBlock(tracks: readonly TimelineTrack[], id: string): string | null {
29
+ for (const track of tracks) {
30
+ if (track.blocks.some((b) => b.id === id)) return track.lane;
31
+ }
32
+ return null;
33
+ }
34
+
35
+ /** The row by name; when a spread row is gone, the same film.html track. */
36
+ export function trackByLane(
37
+ tracks: readonly TimelineTrack[],
38
+ lane: string,
39
+ ): TimelineTrack | undefined {
40
+ const exact = tracks.find((tr) => tr.lane === lane);
41
+ if (exact) return exact;
42
+ const doc = /^doc-(\d+)/.exec(lane)?.[1];
43
+ if (doc == null) return undefined;
44
+ return tracks.find((tr) => tr.docIndex === Number(doc));
45
+ }
46
+
47
+ export function clipboardItemsOf(
48
+ blocks: readonly TimelineBlock[],
49
+ tracks: readonly TimelineTrack[],
50
+ ): TimelineClipboardItem[] {
51
+ return blocks
52
+ .filter((b) => b.clipId)
53
+ .map((b) => ({
54
+ clipId: b.clipId!,
55
+ startMs: b.startMs,
56
+ durMs: Math.max(1, b.endMs - b.startMs),
57
+ lane: laneOfBlock(tracks, b.id) ?? '',
58
+ }))
59
+ .sort((a, b) => a.startMs - b.startMs);
60
+ }
61
+
62
+ /**
63
+ * The copied clips as the timeline has them now, and how many are gone (deleted, or undone since the copy). Length and
64
+ * track are taken afresh, since the paste copies the clip as it is now; the start times stay as copied, for spacing.
65
+ */
66
+ export function clipboardNow(
67
+ clipboard: readonly TimelineClipboardItem[],
68
+ tracks: readonly TimelineTrack[],
69
+ ): { items: TimelineClipboardItem[]; missing: number } {
70
+ const items: TimelineClipboardItem[] = [];
71
+ for (const item of clipboard) {
72
+ const track = tracks.find((tr) => tr.blocks.some((b) => b.clipId === item.clipId));
73
+ const block = track?.blocks.find((b) => b.clipId === item.clipId);
74
+ if (!track || !block) continue;
75
+ items.push({ ...item, durMs: Math.max(1, block.endMs - block.startMs), lane: track.lane });
76
+ }
77
+ return { items, missing: clipboard.length - items.length };
78
+ }
79
+
80
+ /** The film.html track a row belongs to (`doc-3#2` is a spread row of track 3). */
81
+ function docIndexOfLane(lane: string): number | null {
82
+ const doc = /^doc-(\d+)/.exec(lane)?.[1];
83
+ return doc == null ? null : Number(doc);
84
+ }
85
+
86
+ /**
87
+ * The links copies get: copies of clips linked together and pasted together are linked to each other, with a link
88
+ * of their own (not the originals', or moving a copy would move the original); a copy pasted without the clips its
89
+ * original is linked to is linked to nothing. Clip id → the link for its copy (null: none); unlinked clips are left
90
+ * out.
91
+ */
92
+ export function copyLinks(clipIds: readonly string[], model: EditModel): Map<string, string | null> {
93
+ const linkOf = new Map(model.flatMap((t) => t.clips.flatMap((c) => (c.link ? [[c.id, c.link] as const] : []))));
94
+ const count = new Map<string, number>();
95
+ for (const id of clipIds) {
96
+ const link = linkOf.get(id);
97
+ if (link) count.set(link, (count.get(link) ?? 0) + 1);
98
+ }
99
+ const fresh = new Map<string, string>();
100
+ const taken = new Set<string>();
101
+ const out = new Map<string, string | null>();
102
+ for (const id of clipIds) {
103
+ const link = linkOf.get(id);
104
+ if (!link) continue;
105
+ if ((count.get(link) ?? 0) < 2) { out.set(id, null); continue; }
106
+ if (!fresh.has(link)) {
107
+ const made = freshLink(model, `${id}-copy`, taken);
108
+ taken.add(made);
109
+ fresh.set(link, made);
110
+ }
111
+ out.set(id, fresh.get(link)!);
112
+ }
113
+ return out;
114
+ }
115
+
116
+ /** A copy to put down: of clip `from`, at this time, onto this track; `source` is the track it was copied from. */
117
+ type CopySpan = { from: string; startMs: number; endMs: number; dest: TrackDest; source: number };
118
+
119
+ const overlap = (a: { startMs: number; endMs: number }, b: { startMs: number; endMs: number }): boolean => (
120
+ a.startMs < b.endMs - TOUCH_MS && a.endMs > b.startMs + TOUCH_MS
121
+ );
122
+
123
+ /**
124
+ * Copies put down, never cutting or moving what is there. Onto their track where that time is free; where it is taken
125
+ * (by a clip there, or by another copy) the copies headed there all go to a new track beside it instead (newTrackSide),
126
+ * and one more for each that would still overlap another. `keepOff` (the main track with the magnet) is
127
+ * always taken: copies headed there go to a new track right above it. Copies headed for a new track (`{ insert }`)
128
+ * open one per track they came from, so a group keeps its rows. The inserts onto tracks that are there go first, then
129
+ * the new tracks from the last place up, so every place written holds. `placed`: where each copy lands, by the id it
130
+ * was copied from.
131
+ */
132
+ function placeCopies(
133
+ model: EditModel,
134
+ spans: readonly CopySpan[],
135
+ links: ReadonlyMap<string, string | null>,
136
+ keepOff: number | null = null,
137
+ ): { ops: Op[]; placed: Map<string, Placed> } {
138
+ const there: CopySpan[] = [];
139
+ /* a new track: where it opens, and how low it ends among those opened at that place (higher = lower down) */
140
+ const opened: { at: number; low: number; key: string; spans: CopySpan[] }[] = [];
141
+ const openAt = (at: number, low: number, key: string, span: CopySpan) => {
142
+ const found = opened.find((o) => o.at === at && o.key === key);
143
+ if (found) found.spans.push(span);
144
+ else opened.push({ at, low, key, spans: [span] });
145
+ };
146
+ const byTrack = new Map<number, CopySpan[]>();
147
+ for (const s of spans) {
148
+ if (typeof s.dest === 'object') openAt(s.dest.insert, s.source, `from-${s.source}`, s);
149
+ else byTrack.set(s.dest, [...(byTrack.get(s.dest) ?? []), s]);
150
+ }
151
+ for (const [index, group] of byTrack) {
152
+ const track = model.find((t) => t.index === index);
153
+ const clips = track?.clips ?? [];
154
+ const taken = index === keepOff
155
+ || group.some((s, i) => clips.some((c) => overlap(s, c)) || group.slice(0, i).some((o) => overlap(s, o)));
156
+ if (!taken) {
157
+ there.push(...group);
158
+ continue;
159
+ }
160
+ const side = newTrackSide(index, track?.role);
161
+ const rows: CopySpan[][] = [];
162
+ for (const s of [...group].sort((a, b) => a.startMs - b.startMs)) {
163
+ const row = rows.find((r) => !r.some((o) => overlap(s, o)));
164
+ if (row) row.push(s);
165
+ else rows.push([s]);
166
+ }
167
+ /* the first row next to the track, the others further away: above a picture track, below a sound one */
168
+ rows.forEach((row, n) => {
169
+ for (const s of row) openAt(side, side === index ? -n : n, `beside-${index}-${n}`, s);
170
+ });
171
+ }
172
+ const insertOf = (s: CopySpan, track: TrackDest): Op => ({
173
+ op: 'insert',
174
+ from: s.from,
175
+ at: Math.round(s.startMs) / 1000,
176
+ track,
177
+ ...(links.has(s.from) ? { link: links.get(s.from)! } : {}),
178
+ });
179
+ const ops: Op[] = there.map((s) => insertOf(s, s.dest));
180
+ const placed = new Map<string, Placed>(there.map((s) => [s.from, { startMs: s.startMs, endMs: s.endMs, track: s.dest }]));
181
+ /* a track opened at a place goes above those opened there before it: the lowest of them opens first */
182
+ for (const o of [...opened].sort((a, b) => b.at - a.at || b.low - a.low)) {
183
+ [...o.spans].sort((a, b) => a.startMs - b.startMs).forEach((s, i) => {
184
+ ops.push(insertOf(s, i === 0 ? { insert: o.at } : o.at));
185
+ placed.set(s.from, { startMs: s.startMs, endMs: s.endMs, track: { insert: o.at } });
186
+ });
187
+ }
188
+ return { ops, placed };
189
+ }
190
+
191
+ /**
192
+ * A paste: copied from one track, the copies go onto the track pointed at (`forceLane`) if any; from several, each
193
+ * back onto its own. The group's earliest clip lands at `atMs`, the rest keep their spacing. Put down (`place`), they
194
+ * never cut or move what is there: a taken spot opens a track beside it (placeCopies), and with the magnet a copy
195
+ * headed for the main track goes to a new track right above it. An insert makes room there instead,
196
+ * pushing what follows on, on the tracks they go in on (with linkage, what rides on the main track along). Null when a
197
+ * track it goes onto is locked or gone.
198
+ */
199
+ export function pasteEdit(
200
+ clipboard: readonly TimelineClipboardItem[],
201
+ tracks: readonly TimelineTrack[],
202
+ model: EditModel,
203
+ atMs: number,
204
+ forceLane: string | null,
205
+ opts: { mode: 'place' | 'insert' } & Partial<Switches>,
206
+ ): Op[] | null {
207
+ if (!clipboard.length) return [];
208
+ const base = Math.min(...clipboard.map((item) => item.startMs));
209
+ const copiedTracks = new Set(clipboard.map((item) => docIndexOfLane(item.lane) ?? item.lane));
210
+ const force = forceLane && copiedTracks.size === 1 ? forceLane : null;
211
+ const spans = clipboard.map((item) => {
212
+ const row = trackByLane(tracks, force || item.lane);
213
+ const startMs = Math.round(Math.max(0, atMs) + (item.startMs - base));
214
+ return { item, track: row?.docIndex, locked: Boolean(row?.locked), startMs, endMs: startMs + item.durMs };
215
+ });
216
+ if (spans.some((s) => s.track == null || s.locked)) return null;
217
+ const links = copyLinks(clipboard.map((item) => item.clipId), model);
218
+ if (opts.mode === 'place') {
219
+ return placeCopies(model, spans.map((s) => ({
220
+ from: s.item.clipId, startMs: s.startMs, endMs: s.endMs, dest: s.track!, source: s.track!,
221
+ })), links, opts.magnet ? mainTrackOf(model) : null).ops;
222
+ }
223
+ const insertOf = (s: (typeof spans)[number]): Op => ({
224
+ op: 'insert',
225
+ from: s.item.clipId,
226
+ at: s.startMs / 1000,
227
+ track: s.track!,
228
+ ...(links.has(s.item.clipId) ? { link: links.get(s.item.clipId)! } : {}),
229
+ });
230
+ /* the copies go in first, so a clip pasted inside itself is copied whole before the insert cuts it */
231
+ const room = planRoom(model, spans.map((s) => ({ track: s.track!, startMs: s.startMs, endMs: s.endMs })), { ...opts, mode: 'insert' });
232
+ return [...spans.map(insertOf), ...room.ops];
233
+ }
234
+
235
+ /**
236
+ * Copies of clips put down where a drag lets them go (⌥-drag): the clips stay, a copy of each lands
237
+ * `deltaMs` later (earlier when negative) on its track or the one in `to`, never cutting what is there (placeCopies).
238
+ * With the magnet, when the grabbed one's copy goes onto the main track, the copies headed there go in end to end at
239
+ * the insert point under the hand (`pointerMs`), what follows moving on. Copies of clips linked together are linked to
240
+ * each other. `placed`: where each copy lands, by the clip it copies. Null when a track it goes onto is locked.
241
+ */
242
+ export function planCopies(
243
+ model: EditModel,
244
+ spec: { ids: readonly string[]; deltaMs: number; to?: ReadonlyMap<string, TrackDest>; pointerMs?: number } & Partial<Switches>,
245
+ ): EditPlan | null {
246
+ const clips = model.flatMap((t) => t.clips.map((c) => ({ c, track: t.index })));
247
+ const picked = spec.ids.flatMap((id) => clips.filter((x) => x.c.id === id));
248
+ if (!picked.length) return null;
249
+ /* the group keeps its shape; none goes before 0 */
250
+ const shift = Math.max(spec.deltaMs, -Math.min(...picked.map((x) => x.c.startMs)));
251
+ const spans = picked.map(({ c, track }): CopySpan => {
252
+ const startMs = Math.max(0, c.startMs + shift);
253
+ return { from: c.id, startMs, endMs: startMs + c.endMs - c.startMs, dest: spec.to?.get(c.id) ?? track, source: track };
254
+ });
255
+ if (spans.some((s) => typeof s.dest === 'number' && model.find((t) => t.index === s.dest)?.locked)) return null;
256
+ const links = copyLinks(spec.ids, model);
257
+ const main = mainTrackOf(model);
258
+ const lead = spans[0]!;
259
+ if (!(spec.magnet && main != null && lead.dest === main && magnetOn(model, main, true))) {
260
+ const { ops, placed } = placeCopies(model, spans, links, spec.magnet ? main : null);
261
+ return { ops, placed, shortMs: 0 };
262
+ }
263
+ /* onto the main track with the magnet: in end to end at the insert point, what follows (and rides on it) moving on */
264
+ const going = spans.filter((s) => s.dest === main).sort((a, b) => a.startMs - b.startMs);
265
+ let at = insertPointOn(model, main, spec.pointerMs ?? lead.startMs, lead.startMs);
266
+ const inserts: Op[] = [];
267
+ const placed = new Map<string, Placed>();
268
+ const startAt = at;
269
+ for (const s of going) {
270
+ const len = s.endMs - s.startMs;
271
+ inserts.push({ op: 'insert', from: s.from, at: Math.round(at) / 1000, track: main, ...(links.has(s.from) ? { link: links.get(s.from)! } : {}) });
272
+ placed.set(s.from, { startMs: at, endMs: at + len, track: main });
273
+ at += len;
274
+ }
275
+ const room = planRoom(model, [{ track: main, startMs: startAt, endMs: at }], { mode: 'insert', magnet: spec.magnet, linkage: spec.linkage });
276
+ const rest = placeCopies(model, spans.filter((s) => s.dest !== main), links, main);
277
+ for (const [id, p] of rest.placed) placed.set(id, p);
278
+ return { ops: [...inserts, ...room.ops, ...rest.ops], placed, shortMs: room.shortMs };
279
+ }