@dailies-studio/shot-editor 0.1.0 → 0.2.0

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 (432) hide show
  1. package/dist/.vite/manifest.json +382 -0
  2. package/dist/assets/AgentPanel-CQejLbiX.js +2 -0
  3. package/dist/assets/AgentPanel-DJEphah0.css +1 -0
  4. package/dist/assets/AgentQuestionDialog-BqkD0kXF.js +1 -0
  5. package/dist/assets/CodeStudio-Dv4ihHpz.js +23 -0
  6. package/dist/assets/CommentsPanel-B4yhhT1T.js +1 -0
  7. package/dist/assets/ComponentLibraryRail-Ch_dsAru.js +1 -0
  8. package/dist/assets/ComponentStage-BRhRotyH.js +1 -0
  9. package/dist/assets/CurvePane-BjFVabKX.css +1 -0
  10. package/dist/assets/CurvePane-R22WzllJ.js +1 -0
  11. package/dist/assets/ExportDialog-CWLQWTmG.js +2 -0
  12. package/dist/assets/FontPicker-DWKaYL7F.js +1 -0
  13. package/dist/assets/FontPicker-v0IOIArM.css +1 -0
  14. package/dist/assets/GenerateDrawer-Bh3SqDKt.js +2 -0
  15. package/dist/assets/InsertDrawer-Ce1fod1L.js +1 -0
  16. package/dist/assets/LibraryBrowser-D9sEGz5w.js +2 -0
  17. package/dist/assets/MakeItYoursSection-CMO4319_.js +2 -0
  18. package/dist/assets/MaterialGraphCanvas-BFzl4wWR.js +23 -0
  19. package/dist/assets/MaterialGraphCanvas-BIUD59ik.css +1 -0
  20. package/dist/assets/MaterialWorkspace-CqTWqaRA.js +14 -0
  21. package/dist/assets/PreferencesDialog-CBf9_OoD.js +1 -0
  22. package/dist/assets/PresetsSection-CUEaoCVU.js +1 -0
  23. package/dist/assets/SoundHistory-BeS8EW1h.js +1 -0
  24. package/dist/assets/SoundMode-BmcZru3i.js +3 -0
  25. package/dist/assets/StockSounds-DdAUGRYK.js +1 -0
  26. package/dist/assets/TemplateFieldSection-CaeXpLFo.js +1 -0
  27. package/dist/assets/VersionHistoryPanel-CxG6XhNP.js +1 -0
  28. package/dist/assets/board-B88EPuLO.css +1 -0
  29. package/dist/assets/board-CL-he7SL.js +3 -0
  30. package/dist/assets/components-0oOSPN0l.js +1 -0
  31. package/dist/assets/googleFontRanges-gp32wJMo.js +1 -0
  32. package/dist/assets/googleFonts-BNon6M7X.js +1 -0
  33. package/dist/assets/iconsAndAliases-DL7gXP5j.js +1863 -0
  34. package/dist/assets/index-4F5NQmI8.css +1 -0
  35. package/dist/assets/index-BGHQGV5z.js +117 -0
  36. package/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  37. package/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  38. package/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  39. package/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  40. package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  41. package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  42. package/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  43. package/dist/assets/libraryModel-CpIwfFha.js +1 -0
  44. package/dist/assets/lucide-6iK64tTB.js +9319 -0
  45. package/dist/assets/motionStamp-CH-Hew4E.js +1 -0
  46. package/dist/assets/takeMeasure-Tdg0TxEi.js +1 -0
  47. package/dist/assets/webmcp-BsB9gxWC.js +2 -0
  48. package/dist/index.html +34 -3
  49. package/package.json +12 -9
  50. package/src/addFamilies.ts +66 -0
  51. package/src/api.ts +1631 -55
  52. package/src/app/App.tsx +977 -198
  53. package/src/app/lazyPanel.tsx +137 -0
  54. package/src/app/lazySurfaces.tsx +203 -0
  55. package/src/audio/index.tsx +41 -10
  56. package/src/audio/meter.ts +485 -0
  57. package/src/audio/meterMath.ts +273 -0
  58. package/src/audio/meterStore.ts +155 -0
  59. package/src/audio/offlineMix.ts +289 -0
  60. package/src/audio/preview.ts +928 -118
  61. package/src/bridge.ts +15 -5
  62. package/src/bridgeTypes.ts +1611 -36
  63. package/src/channels.ts +17 -32
  64. package/src/fonts/googleFonts.ts +4 -0
  65. package/src/hostApi.ts +174 -0
  66. package/src/icons.tsx +770 -0
  67. package/src/layerWords.ts +61 -0
  68. package/src/main.tsx +56 -4
  69. package/src/panels/ResizeHandle.tsx +33 -43
  70. package/src/panels/agent/AgentDots.tsx +36 -0
  71. package/src/panels/agent/AgentPanel.tsx +119 -43
  72. package/src/panels/agent/AgentQuestionDialog.tsx +202 -0
  73. package/src/panels/agent/agent.css +113 -97
  74. package/src/panels/agent/agentGate.ts +35 -0
  75. package/src/panels/code/CodeDefaultsPanel.tsx +345 -0
  76. package/src/panels/code/CodeStudio.tsx +373 -0
  77. package/src/panels/code/FileTree.tsx +265 -0
  78. package/src/panels/code/codeBufferGuard.ts +52 -0
  79. package/src/panels/code/codeParts.tsx +321 -0
  80. package/src/panels/comments/CommentComposer.tsx +239 -0
  81. package/src/panels/comments/CommentThread.tsx +346 -0
  82. package/src/panels/comments/CommentsPanel.tsx +336 -0
  83. package/src/panels/comments/InboxButton.tsx +131 -0
  84. package/src/panels/comments/MentionMenu.tsx +98 -0
  85. package/src/panels/comments/parts.tsx +171 -0
  86. package/src/panels/export/ExportDialog.tsx +919 -96
  87. package/src/panels/export/ExportScrim.tsx +57 -0
  88. package/src/panels/files/AssetsTab.tsx +656 -0
  89. package/src/panels/files/FilePanel.tsx +1466 -216
  90. package/src/panels/files/LayerMarks.tsx +40 -0
  91. package/src/panels/files/LeftColumn.tsx +37 -0
  92. package/src/panels/files/McpSection.tsx +690 -47
  93. package/src/panels/files/MissingAssetsNotice.tsx +60 -0
  94. package/src/panels/files/assetDrag.ts +79 -0
  95. package/src/panels/files/resourceParts.tsx +255 -0
  96. package/src/panels/fonts/FontPicker.tsx +762 -0
  97. package/src/panels/fonts/faceCache.ts +207 -0
  98. package/src/panels/fonts/fontPicker.css +271 -0
  99. package/src/panels/fonts/pickerModel.ts +265 -0
  100. package/src/panels/fonts/pickerRequest.ts +46 -0
  101. package/src/panels/header/AccountAvatar.tsx +76 -0
  102. package/src/panels/header/AgentChip.tsx +77 -0
  103. package/src/panels/header/AppMenu.tsx +213 -0
  104. package/src/panels/header/Header.tsx +564 -84
  105. package/src/panels/header/HistoryControls.tsx +31 -5
  106. package/src/panels/header/PresenceStack.tsx +329 -0
  107. package/src/panels/header/ProjectTitle.tsx +232 -0
  108. package/src/panels/header/ShareButton.tsx +59 -0
  109. package/src/panels/header/StatusBadges.tsx +358 -27
  110. package/src/panels/header/SyncDeletedDialog.tsx +88 -0
  111. package/src/panels/header/ZoomControls.tsx +46 -15
  112. package/src/panels/header/agentHandoff.ts +52 -0
  113. package/src/panels/header/leave.ts +26 -0
  114. package/src/panels/help/HelpSection.tsx +189 -163
  115. package/src/panels/insert/InsertDrawer.tsx +723 -0
  116. package/src/panels/insert/iconSet.ts +109 -0
  117. package/src/panels/inspector/ExposeDot.tsx +117 -0
  118. package/src/panels/inspector/FramingRows.tsx +314 -44
  119. package/src/panels/inspector/InspectorFooter.tsx +91 -0
  120. package/src/panels/inspector/InspectorHead.tsx +425 -0
  121. package/src/panels/inspector/InspectorPanel.tsx +408 -124
  122. package/src/panels/inspector/KeyframeDot.tsx +373 -0
  123. package/src/panels/inspector/KeyframeSection.tsx +257 -0
  124. package/src/panels/inspector/MarkerSection.tsx +55 -11
  125. package/src/panels/inspector/ParamRow.tsx +84 -53
  126. package/src/panels/inspector/RightColumn.tsx +152 -0
  127. package/src/panels/inspector/TimeSection.tsx +866 -0
  128. package/src/panels/inspector/animate/EaseControl.tsx +244 -0
  129. package/src/panels/inspector/animate/PresetsSection.tsx +135 -0
  130. package/src/panels/inspector/animate/ShotSection.tsx +562 -48
  131. package/src/panels/inspector/design/AlignRow.tsx +95 -31
  132. package/src/panels/inspector/design/AppearanceSection.tsx +118 -9
  133. package/src/panels/inspector/design/ClonerSection.tsx +323 -62
  134. package/src/panels/inspector/design/ComponentSection.tsx +50 -43
  135. package/src/panels/inspector/design/CornerSection.tsx +45 -14
  136. package/src/panels/inspector/design/DefinitionProps.tsx +315 -0
  137. package/src/panels/inspector/design/DefinitionSection.tsx +136 -0
  138. package/src/panels/inspector/design/DeviceSection.tsx +210 -0
  139. package/src/panels/inspector/design/FillSection.tsx +83 -51
  140. package/src/panels/inspector/design/FontField.tsx +196 -0
  141. package/src/panels/inspector/design/HtmlSection.tsx +23 -12
  142. package/src/panels/inspector/design/InstanceSection.tsx +373 -154
  143. package/src/panels/inspector/design/MaskSection.tsx +84 -0
  144. package/src/panels/inspector/design/MaterialSection.tsx +177 -0
  145. package/src/panels/inspector/design/MediaSection.tsx +273 -120
  146. package/src/panels/inspector/design/MeshSection.tsx +910 -82
  147. package/src/panels/inspector/design/PaintRows.tsx +550 -0
  148. package/src/panels/inspector/design/PlateStyleSection.tsx +177 -0
  149. package/src/panels/inspector/design/ShadowSection.tsx +221 -48
  150. package/src/panels/inspector/design/SplineFollowSection.tsx +235 -0
  151. package/src/panels/inspector/design/StrokeSection.tsx +228 -43
  152. package/src/panels/inspector/design/TemplateFieldSection.tsx +57 -0
  153. package/src/panels/inspector/design/TransformSection.tsx +244 -50
  154. package/src/panels/inspector/design/TypographySection.tsx +679 -110
  155. package/src/panels/inspector/layout.ts +295 -0
  156. package/src/panels/inspector/propValue.ts +199 -0
  157. package/src/panels/inspector/scene/CameraSection.tsx +232 -22
  158. package/src/panels/inspector/scene/EffectsSection.tsx +722 -71
  159. package/src/panels/inspector/scene/MakeItYoursSection.tsx +180 -0
  160. package/src/panels/inspector/scene/SceneDocSection.tsx +191 -0
  161. package/src/panels/inspector/scene/SceneSection.tsx +219 -44
  162. package/src/panels/inspector/scene/TransitionSection.tsx +523 -72
  163. package/src/panels/inspector/storyboard/StoryboardReferenceSection.tsx +593 -0
  164. package/src/panels/inspector/storyboard/VideoStill.tsx +120 -0
  165. package/src/panels/inspector/timeModel.ts +190 -0
  166. package/src/panels/inspector/useDocWrite.ts +438 -146
  167. package/src/panels/library/LibraryBrowser.tsx +435 -0
  168. package/src/panels/library/LibraryGrid.tsx +190 -0
  169. package/src/panels/library/LibraryPreviewPanel.tsx +349 -0
  170. package/src/panels/library/LibraryRail.tsx +84 -0
  171. package/src/panels/library/LibrarySoundPanel.tsx +293 -0
  172. package/src/panels/library/SoundHistory.tsx +245 -0
  173. package/src/panels/library/StockSounds.tsx +390 -0
  174. package/src/panels/library/libraryModel.ts +147 -0
  175. package/src/panels/library/stockSearch.ts +77 -0
  176. package/src/panels/maskMarks.tsx +117 -0
  177. package/src/panels/material/MaterialCommentNode.tsx +54 -0
  178. package/src/panels/material/MaterialGraphCanvas.tsx +861 -0
  179. package/src/panels/material/MaterialGraphMenu.tsx +45 -0
  180. package/src/panels/material/MaterialGroupNode.tsx +56 -0
  181. package/src/panels/material/MaterialLibrary.tsx +253 -0
  182. package/src/panels/material/MaterialNode.tsx +453 -0
  183. package/src/panels/material/MaterialNodePalette.tsx +349 -0
  184. package/src/panels/material/MaterialParameterControls.tsx +525 -0
  185. package/src/panels/material/MaterialRerouteNode.tsx +42 -0
  186. package/src/panels/material/MaterialWorkspace.tsx +501 -0
  187. package/src/panels/material/NodePreview.tsx +70 -0
  188. package/src/panels/material/flowModel.ts +441 -0
  189. package/src/panels/material/parameterValues.ts +231 -0
  190. package/src/panels/material/previewScheduler.ts +235 -0
  191. package/src/panels/material/reactFlow.css +6 -0
  192. package/src/panels/material/socketGlyph.ts +85 -0
  193. package/src/panels/material/viewportMemory.ts +58 -0
  194. package/src/panels/preferences/PreferencesDialog.tsx +215 -0
  195. package/src/panels/share/ShareComponentDialog.tsx +247 -0
  196. package/src/panels/stage/StageMenu.tsx +35 -5
  197. package/src/panels/stage/StagePanel.tsx +338 -23
  198. package/src/panels/stage/cellSlots.tsx +70 -0
  199. package/src/panels/storyboard/BoardCanvas.tsx +739 -0
  200. package/src/panels/storyboard/BoardCursors.tsx +158 -0
  201. package/src/panels/storyboard/BoardDropDialog.tsx +161 -0
  202. package/src/panels/storyboard/BoardPlan.tsx +42 -0
  203. package/src/panels/storyboard/ComponentSheet.tsx +187 -0
  204. package/src/panels/storyboard/ComponentStage.tsx +235 -0
  205. package/src/panels/storyboard/MiniSketch.tsx +173 -0
  206. package/src/panels/storyboard/PartPicker.tsx +64 -0
  207. package/src/panels/storyboard/PlanPanel.tsx +617 -0
  208. package/src/panels/storyboard/ReferencePreview.tsx +217 -0
  209. package/src/panels/storyboard/ShotBrief.tsx +360 -0
  210. package/src/panels/storyboard/ShotCard.tsx +251 -0
  211. package/src/panels/storyboard/StagePreview.tsx +348 -0
  212. package/src/panels/storyboard/StillThumbs.tsx +162 -0
  213. package/src/panels/storyboard/StoryboardView.tsx +79 -0
  214. package/src/panels/storyboard/board.css +1065 -0
  215. package/src/panels/storyboard/board.ts +12 -0
  216. package/src/panels/storyboard/boardClipboard.ts +111 -0
  217. package/src/panels/storyboard/boardOrder.ts +68 -0
  218. package/src/panels/storyboard/boardRows.tsx +425 -0
  219. package/src/panels/storyboard/liveCap.ts +90 -0
  220. package/src/panels/storyboard/plan.css +295 -0
  221. package/src/panels/storyboard/posterCache.ts +133 -0
  222. package/src/panels/storyboard/shotboard/ComponentPicker.tsx +373 -0
  223. package/src/panels/storyboard/shotboard/ImagePicker.tsx +145 -0
  224. package/src/panels/storyboard/shotboard/ShotBoard.tsx +292 -0
  225. package/src/panels/storyboard/shotboard/SketchCanvas.tsx +443 -0
  226. package/src/panels/storyboard/shotboard/SketchInspector.tsx +471 -0
  227. package/src/panels/storyboard/shotboard/SketchItem.tsx +240 -0
  228. package/src/panels/storyboard/shotboard/SketchLayers.tsx +130 -0
  229. package/src/panels/storyboard/shotboard/SketchPlay.tsx +108 -0
  230. package/src/panels/storyboard/shotboard/SketchStrip.tsx +178 -0
  231. package/src/panels/storyboard/shotboard/SketchToolFooter.tsx +99 -0
  232. package/src/panels/storyboard/shotboard/sketch.css +1392 -0
  233. package/src/panels/storyboard/shotboard/sketchModel.ts +219 -0
  234. package/src/panels/storyboard/storyboardDrop.ts +164 -0
  235. package/src/panels/studio/ComponentLibraryRail.tsx +346 -0
  236. package/src/panels/studio/ComponentSidePanel.tsx +355 -0
  237. package/src/panels/studio/ComponentStudio.tsx +243 -0
  238. package/src/panels/studio/ComponentsHome.tsx +318 -0
  239. package/src/panels/studio/ComponentsPreview.tsx +179 -0
  240. package/src/panels/studio/EveryUse.tsx +244 -0
  241. package/src/panels/studio/ThisUsePanel.tsx +679 -0
  242. package/src/panels/studio/componentParts.tsx +341 -0
  243. package/src/panels/studio/components.ts +6 -0
  244. package/src/panels/studio/newCodeComponent.ts +66 -0
  245. package/src/panels/timeline/AudioFxButton.tsx +290 -0
  246. package/src/panels/timeline/AudioLane.tsx +674 -85
  247. package/src/panels/timeline/BlockRow.tsx +154 -85
  248. package/src/panels/timeline/ChannelRow.tsx +378 -129
  249. package/src/panels/timeline/CurvePane.tsx +508 -0
  250. package/src/panels/timeline/Filmstrip.tsx +361 -0
  251. package/src/panels/timeline/GenerateDrawer.tsx +407 -0
  252. package/src/panels/timeline/KeyStretch.tsx +115 -0
  253. package/src/panels/timeline/LifespanRow.tsx +730 -47
  254. package/src/panels/timeline/MarkerLane.tsx +46 -8
  255. package/src/panels/timeline/PlayButton.tsx +91 -0
  256. package/src/panels/timeline/PropertyPicker.tsx +254 -0
  257. package/src/panels/timeline/ShotRow.tsx +400 -0
  258. package/src/panels/timeline/SoundMode.tsx +1406 -0
  259. package/src/panels/timeline/TimelineDock.tsx +2338 -272
  260. package/src/panels/timeline/TransportVolume.tsx +163 -0
  261. package/src/panels/timeline/beatRuler.ts +74 -0
  262. package/src/panels/timeline/curveModel.ts +500 -0
  263. package/src/panels/timeline/duckUnderVoice.ts +68 -0
  264. package/src/panels/timeline/keyframeMenu.ts +138 -0
  265. package/src/panels/timeline/labelWidth.ts +49 -0
  266. package/src/panels/timeline/marquee.ts +155 -0
  267. package/src/panels/timeline/model.ts +1860 -199
  268. package/src/panels/timeline/snap.ts +125 -43
  269. package/src/panels/timeline/staggerPreview.tsx +92 -0
  270. package/src/panels/timeline/takeMeasure.ts +72 -0
  271. package/src/panels/timeline/useLive.ts +48 -0
  272. package/src/panels/timeline/viewport.ts +234 -0
  273. package/src/panels/toolbar/BackChip.tsx +116 -0
  274. package/src/panels/toolbar/CameraViewBar.tsx +118 -49
  275. package/src/panels/toolbar/CanvasBar.tsx +117 -0
  276. package/src/panels/toolbar/InsertButton.tsx +38 -0
  277. package/src/panels/toolbar/IsolationTrail.tsx +156 -0
  278. package/src/panels/toolbar/PanelToggles.tsx +86 -0
  279. package/src/panels/toolbar/ShadingBar.tsx +119 -26
  280. package/src/panels/toolbar/ToolDockBar.tsx +329 -80
  281. package/src/panels/toolbar/ToolFooter.tsx +102 -0
  282. package/src/panels/toolbar/ViewModeBar.tsx +247 -38
  283. package/src/panels/toolbar/WorkPlaneBar.tsx +94 -0
  284. package/src/panels/toolbar/useAddRows.tsx +326 -0
  285. package/src/panels/toolbar/useDismiss.ts +122 -0
  286. package/src/panels/versions/VersionHistoryPanel.tsx +274 -0
  287. package/src/panels/versions/versionModel.ts +103 -0
  288. package/src/panels/virtualWindow.ts +271 -0
  289. package/src/shotTokens.css +151 -60
  290. package/src/stage/FrustumGizmo.tsx +504 -84
  291. package/src/stage/GridPlane.tsx +225 -16
  292. package/src/stage/NodePreviews.tsx +628 -0
  293. package/src/stage/PerfHud.tsx +437 -47
  294. package/src/stage/SplitRenderer.tsx +816 -169
  295. package/src/stage/StageApp.tsx +4274 -486
  296. package/src/stage/StageRootBoundary.tsx +114 -0
  297. package/src/stage/StageTools.tsx +651 -151
  298. package/src/stage/TransformGizmo.tsx +696 -0
  299. package/src/stage/adaptivePresentation.ts +145 -0
  300. package/src/stage/bridgeClient.ts +2522 -41
  301. package/src/stage/exportWorker.ts +9 -0
  302. package/src/stage/feed.ts +305 -14
  303. package/src/stage/fontPreview.ts +176 -0
  304. package/src/stage/frustumGeometry.ts +319 -1
  305. package/src/stage/furnitureTokens.ts +33 -0
  306. package/src/stage/gizmoMath.ts +508 -16
  307. package/src/stage/gpuTimer.ts +276 -0
  308. package/src/stage/instanceModules.ts +194 -33
  309. package/src/stage/interactions/move.ts +24 -5
  310. package/src/stage/isolation.ts +291 -33
  311. package/src/stage/jsSelfProfiler.ts +325 -0
  312. package/src/stage/main.tsx +116 -16
  313. package/src/stage/modalState.ts +222 -23
  314. package/src/stage/moduleEpoch.ts +60 -0
  315. package/src/stage/motionPathStage.ts +279 -0
  316. package/src/stage/motionPreview.ts +74 -0
  317. package/src/stage/overlays/Overlays.tsx +1112 -159
  318. package/src/stage/overlays/Rulers.tsx +495 -0
  319. package/src/stage/panes.ts +159 -10
  320. package/src/stage/project.ts +68 -3
  321. package/src/stage/renderScaleController.ts +730 -0
  322. package/src/stage/rulers.ts +129 -0
  323. package/src/stage/sceneExtent.ts +75 -0
  324. package/src/stage/shading.ts +20 -0
  325. package/src/stage/splineStage.ts +632 -0
  326. package/src/stage/stagePerformanceTelemetry.ts +717 -0
  327. package/src/stage/stageTheme.ts +36 -0
  328. package/src/stage/stageWarmup.ts +11 -0
  329. package/src/stage/textAutoSize.ts +124 -0
  330. package/src/stage/tier1Export.ts +641 -0
  331. package/src/stage/tokenColor.ts +14 -9
  332. package/src/stage/view.ts +56 -9
  333. package/src/stage/workingView.ts +277 -33
  334. package/src/state/accountPrefs.ts +261 -0
  335. package/src/state/agentActions.ts +173 -15
  336. package/src/state/agentActivity.ts +198 -0
  337. package/src/state/agentQuestion.ts +239 -0
  338. package/src/state/bootOpen.ts +42 -0
  339. package/src/state/cameraVerbs.ts +139 -0
  340. package/src/state/clientId.ts +6 -1
  341. package/src/state/comments.ts +1099 -0
  342. package/src/state/context.tsx +19 -1
  343. package/src/state/editorPrefs.ts +220 -0
  344. package/src/state/errorReports.ts +121 -0
  345. package/src/state/fieldWrite.ts +161 -0
  346. package/src/state/fontRecents.ts +70 -0
  347. package/src/state/fonts.ts +656 -0
  348. package/src/state/graphClipboard.ts +414 -0
  349. package/src/state/imageThumbs.ts +216 -0
  350. package/src/state/insert.ts +444 -67
  351. package/src/state/isolationTrail.ts +92 -0
  352. package/src/state/keyMoves.ts +134 -0
  353. package/src/state/keyframeClipboard.ts +177 -0
  354. package/src/state/keymap.ts +1153 -117
  355. package/src/state/layoutPrefs.ts +272 -0
  356. package/src/state/layouts.ts +205 -0
  357. package/src/state/measureMedia.ts +167 -0
  358. package/src/state/menu.ts +2341 -28
  359. package/src/state/motionStamp.ts +7 -0
  360. package/src/state/panelSize.ts +375 -5
  361. package/src/state/pendingChanges.ts +119 -0
  362. package/src/state/playHold.ts +78 -0
  363. package/src/state/playbackPreparation.ts +562 -0
  364. package/src/state/posterCapture.ts +242 -0
  365. package/src/state/presence.ts +254 -0
  366. package/src/state/projectComponents.ts +160 -0
  367. package/src/state/renderEvents.ts +298 -18
  368. package/src/state/runtimeErrors.ts +46 -0
  369. package/src/state/selection.ts +12 -0
  370. package/src/state/shortcuts.ts +1606 -0
  371. package/src/state/sketchGuide.ts +51 -0
  372. package/src/state/splineRefs.ts +87 -0
  373. package/src/state/stageHealth.ts +259 -0
  374. package/src/state/stageWatchdog.ts +47 -0
  375. package/src/state/stillStage.ts +330 -0
  376. package/src/state/stillThumbs.ts +474 -0
  377. package/src/state/store.ts +23591 -2982
  378. package/src/state/storyboardModel.ts +528 -0
  379. package/src/state/sync.ts +1257 -0
  380. package/src/state/syncCache.ts +159 -0
  381. package/src/state/triangles.ts +13 -0
  382. package/src/state/types.ts +1698 -55
  383. package/src/state/unsavedGuard.ts +49 -0
  384. package/src/state/viewMode.ts +200 -0
  385. package/src/state/webmcp.ts +1128 -0
  386. package/src/state/workspace.ts +1147 -0
  387. package/src/styles/assets-tab.css +304 -0
  388. package/src/styles/canvas.css +545 -0
  389. package/src/styles/comments.css +1435 -0
  390. package/src/styles/controls.css +162 -0
  391. package/src/styles/dialogs.css +390 -0
  392. package/src/styles/insert-drawer.css +586 -0
  393. package/src/styles/inspector-board.css +207 -0
  394. package/src/styles/inspector-object.css +486 -0
  395. package/src/styles/inspector-shot.css +252 -0
  396. package/src/styles/inspector.css +360 -0
  397. package/src/styles/left-column.css +1628 -0
  398. package/src/styles/library-preview.css +575 -0
  399. package/src/styles/library.css +512 -0
  400. package/src/styles/modes/code.css +569 -0
  401. package/src/styles/modes/component.css +1561 -0
  402. package/src/styles/modes/material.css +1201 -0
  403. package/src/styles/modes/storyboard.css +518 -0
  404. package/src/styles/share-component.css +49 -0
  405. package/src/styles/shell.css +653 -0
  406. package/src/styles/shot-brief.css +360 -0
  407. package/src/styles/states.css +76 -0
  408. package/src/styles/sync.css +380 -0
  409. package/src/styles/timeline-graph.css +384 -0
  410. package/src/styles/timeline-meters.css +89 -0
  411. package/src/styles/timeline-rows.css +1959 -0
  412. package/src/styles/timeline-sound.css +1003 -0
  413. package/src/styles/timeline.css +754 -0
  414. package/src/styles/tool-footer.css +525 -0
  415. package/src/styles/topbar.css +886 -0
  416. package/src/styles/versions.css +262 -0
  417. package/src/styles.css +72 -2921
  418. package/src/tools.ts +108 -23
  419. package/dist/assets/index-B7notge7.css +0 -1
  420. package/dist/assets/index-BBE6Ta9L.js +0 -52
  421. package/src/panels/files/icons.tsx +0 -118
  422. package/src/panels/header/DocSwitcher.tsx +0 -85
  423. package/src/panels/header/TransportBar.tsx +0 -86
  424. package/src/panels/inspector/animate/BlockTiming.tsx +0 -341
  425. package/src/panels/inspector/animate/DuringBlocks.tsx +0 -85
  426. package/src/panels/inspector/animate/InOutPresets.tsx +0 -57
  427. package/src/panels/inspector/animate/ResolvedChannels.tsx +0 -150
  428. package/src/panels/inspector/animate/useBlocks.ts +0 -43
  429. package/src/panels/material/MaterialPicker.tsx +0 -172
  430. package/src/panels/timeline/SceneStrip.tsx +0 -145
  431. package/src/panels/toolbar/AddMenu.tsx +0 -230
  432. package/src/stage/MoveGizmo.tsx +0 -411
@@ -1,19 +1,24 @@
1
- // A layer's LIFESPAN bar: one rounded bar per layer, at its master position.
1
+ // A layer's LIFESPAN bar: one rounded bar per layer, at its master position —
2
+ // and, for a `kind: "video"` layer, that bar is a CLIP: a filmstrip over the
3
+ // recording's own waveform.
2
4
  //
3
- // ── THE BLOCK IS THE ATOM, AND THE LIFESPAN IS THE LAYER ────────────────
5
+ // ── THE LIFESPAN IS THE LAYER ───────────────────────────────────────────
4
6
  //
5
- // The disclosure triangle expands this row into one DERIVED row per channel
6
- // the layer's blocks write (ChannelRow.tsx) — property rows with keyframe
7
- // diamonds, the Figma/Lottie read. The channel rows are views, never storage:
8
- // every gesture on one drags the whole BLOCK, in one undo entry, and the
9
- // preset's name lives in the span's tooltip and the Animate tab. The block is
10
- // still where this timeline's DOCUMENT stops.
7
+ // The disclosure triangle expands this row into one row per tracked property
8
+ // (ChannelRow.tsx), with keyframe diamonds, the Figma/Lottie read.
11
9
  //
12
10
  // ── THREE GESTURES, AND WHY THE THIRD IS NOT OPTIONAL ───────────────────
13
11
  //
14
- // BODY drags the bar AND every block on the layer with it.
15
- // IN / OUT move one end and touch no block.
16
- // SLIP (⌥ held) moves every block and leaves the bar exactly where it is.
12
+ // BODY drags the bar AND every keyframe on the layer with it.
13
+ // IN / OUT move one end and touch no keyframe.
14
+ // SLIP (⌥ held) moves every keyframe and leaves the bar exactly where it is.
15
+ //
16
+ // ⇧ MEANS NOTHING ON THESE THREE, and that is a decision rather than an
17
+ // omission. ⌥ has been the slip on every row kind since this dock shipped, so
18
+ // a chord that merely adds ⇧ is a slip an author already had in their hands:
19
+ // reading it as some other gesture would retime an ordinary layer from a key
20
+ // combination whose meaning nobody announced changing. ⇧⌥ is therefore the
21
+ // slip too, everywhere except a clip, where the chord is new (below).
17
22
  //
18
23
  // Body-versus-edge is After Effects verbatim, and no tool makes it a preference
19
24
  // — so there is no setting here either. SLIP is the one every two-gesture
@@ -26,140 +31,818 @@
26
31
  // restores the document by reference — every capture re-derives from the base,
27
32
  // which is also why the delta below is measured from the press and never
28
33
  // accumulated.
34
+ //
35
+ // ── AND WHAT A VIDEO BAR ADDS ───────────────────────────────────────────
36
+ //
37
+ // The same three words mean the clip ops for a video layer (`writeLifespan`
38
+ // dispatches; this file only names the gesture), plus two the other kinds have
39
+ // no use for:
40
+ //
41
+ // ⇧⌥ BODY slides THIS bar alone, out of a selection that would otherwise
42
+ // travel with it. A CLIP GESTURE ONLY: on every other kind the
43
+ // chord stays the slip it has always been.
44
+ // ROLL the handle on the waveform half of an abutting out edge: it moves
45
+ // the SEAM, so this clip's tail and the next clip's head trade
46
+ // frames and nothing around the pair shifts.
47
+ //
48
+ // On every kind, the body drag carries the whole selection when the pressed bar
49
+ // is in it, which is what selecting three bars in order to move them together
50
+ // means. ⇧-click adds a bar to the selection or takes it out.
51
+ //
52
+ // ── AND WHY THE BAR IS DOM, LIKE EVERY OTHER BAR IN THIS DOCK ───────────
53
+ //
54
+ // Two canvases hang inside it — `Filmstrip` for the picture and `AudioWaveform`
55
+ // for the sound, both pixel data with no DOM spelling — and everything else,
56
+ // the bar, the edge, the held-frame hatch, the handles and the label, stays
57
+ // elements, because that is what the rest of the dock is and what its
58
+ // hit-testing, its hover and its tests all assume.
29
59
  import { frameToPx, startDrag } from "@dailies-studio/timeline";
30
- import type { PointerEvent as ReactPointerEvent } from "react";
60
+ import {
61
+ IconChevron,
62
+ IconChevronDown,
63
+ IconEye,
64
+ IconEyeOff,
65
+ IconLock,
66
+ IconUnlock,
67
+ IconVolume,
68
+ IconVolumeOff,
69
+ } from "@dailies-studio/ui";
70
+ import type { CSSProperties, PointerEvent as ReactPointerEvent } from "react";
71
+ import type { Rational } from "@dailies-studio/engine";
72
+ import { firstKeyframeFrame, isDefId, legalRollRange, legalTrimRange, setVideoSound } from "@dailies-studio/shot/doc";
73
+ import type { Layer3D, VideoLayer3D } from "@dailies-studio/shot/doc";
74
+ import { MASK_ICON, iconFor, isComponentGlyph } from "../../icons.js";
75
+ import { MaskBracket } from "../maskMarks.js";
76
+ import type { MaskMarks } from "../maskMarks.js";
31
77
  import { useShotEditor, useStore } from "../../state/context.js";
32
- import type { LifespanDrag, MasterRow } from "./model.js";
33
- import { frameDelta } from "./model.js";
78
+ import { AudioWaveform } from "./AudioLane.js";
79
+ import { Filmstrip } from "./Filmstrip.js";
80
+ import type { HeldSpan, LifespanDrag, MasterRow, VideoRow } from "./model.js";
81
+ import {
82
+ CLIP_NAME_GAP_PX,
83
+ VIDEO_EDGE_H,
84
+ VIDEO_PICTURE_H,
85
+ VIDEO_ROW_H,
86
+ VIDEO_WAVE_H,
87
+ clipNamePlace,
88
+ clipRowOf,
89
+ frameDelta,
90
+ lifespanOf,
91
+ trimHandlePx,
92
+ } from "./model.js";
93
+ import { StaggerChip, StaggerGhost, setStaggerPreview, useStaggerGhost, useStaggerReadout } from "./staggerPreview.js";
34
94
  import type { SnapPress } from "./snap.js";
35
95
  import { isSnapSuppressed } from "./snap.js";
96
+ import { useLive } from "./useLive.js";
97
+ import { formatCombo } from "../../state/shortcuts.js";
98
+ import { PeerDots } from "../header/PresenceStack.js";
36
99
 
37
100
  /** ⌥ turns a body drag into a slip. Alt is the modifier both AE and Premiere
38
101
  * put on the timeline's third gesture, and it does not collide with the snap
39
102
  * suppressor (⌘/ctrl) — so a slip can be snapped or not, like every other
40
- * drag. */
103
+ * drag.
104
+ *
105
+ * ⇧ IS NOT READ HERE. Requiring `!shiftKey` would have silently taken ⇧⌥ away
106
+ * from every row kind that already had it as a slip; the one row where the
107
+ * chord means something else is the clip, and `gestureOf` is where that fork
108
+ * lives so it can be read against the row's kind in one place. */
41
109
  function isSlip(event: { altKey: boolean }): boolean {
42
110
  return event.altKey;
43
111
  }
44
112
 
113
+ /** ⇧⌥ is the slide: the same body move, on this bar alone. A CLIP gesture, and
114
+ * `gestureOf` is the only caller because it is the only reader that knows
115
+ * whether this row is one. */
116
+ function isSolo(event: { altKey: boolean; shiftKey: boolean }): boolean {
117
+ return event.altKey && event.shiftKey;
118
+ }
119
+
120
+ /**
121
+ * THE HELD-FRAME HATCH.
122
+ *
123
+ * A frozen tail looks EXACTLY like footage on a filmstrip — the last picture,
124
+ * repeated, is still a picture — so an author who cannot see it ships a shot
125
+ * that stops moving three seconds before it ends. Diagonal hatching over both
126
+ * strips is the mark, because it survives whatever the tile under it happens to
127
+ * be: a light frame, a dark frame, or no strip at all.
128
+ *
129
+ * Built from `--timeline-ink` rather than a fixed colour, so it reads in both
130
+ * appearances against both the picture and the waveform.
131
+ */
132
+ const HELD_HATCH =
133
+ "repeating-linear-gradient(135deg," +
134
+ " color-mix(in srgb, var(--timeline-ink) 34%, transparent) 0 1.5px," +
135
+ " color-mix(in srgb, var(--timeline-ink) 6%, transparent) 1.5px 5px)";
136
+
45
137
  export function LifespanRow({
46
138
  row,
139
+ layer,
47
140
  sceneId,
48
141
  sceneStart,
142
+ originFrames = 0,
49
143
  ppf,
144
+ fps,
50
145
  labelWidth,
146
+ laneW,
51
147
  expanded,
148
+ revealed = false,
52
149
  onToggle,
53
150
  beginSnap,
151
+ mask,
54
152
  }: {
55
153
  row: MasterRow;
154
+ /**
155
+ * The row's own layer, for its KIND GLYPH. A dock row and an outliner row
156
+ * naming the same element drew two different things: a name here, a name and
157
+ * its icon there. The glyph is the taxonomy, so it belongs to both.
158
+ *
159
+ * Optional because `MasterRow` is the dock's projection and carries no layer:
160
+ * the caller resolves it out of the container the row lives in, and a row
161
+ * whose layer has just been deleted under a render draws its name alone
162
+ * rather than throwing.
163
+ */
164
+ layer?: Layer3D | undefined;
56
165
  sceneId: string;
57
- /** The scene's master start. Drawing only: the store takes a scene-local
58
- * DELTA, which is the same number in both clocks. */
166
+ /** The scene's master start. Drawing AND the snap conversion below: the store
167
+ * takes a scene-local DELTA, which is the same number in both clocks. */
59
168
  sceneStart: number;
169
+ /** The shot context's re-zero. DRAWING ONLY — never the snap conversion, or a
170
+ * drag inside a shot would snap `start` frames from where it should. */
171
+ originFrames?: number;
60
172
  ppf: number;
173
+ /** The container's rate. A video row turns source FRAMES into the seconds its
174
+ * strip and its peaks are cut on; every other row ignores it. */
175
+ fps: Rational;
61
176
  labelWidth: number;
177
+ /** The track's drawn width, so a name too long for its clip can go beside it
178
+ * on the side that has room (`clipNamePlace`). Absent: the name stays in. */
179
+ laneW?: number;
62
180
  expanded: boolean;
181
+ /** A pick elsewhere just revealed this row: flash it once (`is-revealed`). */
182
+ revealed?: boolean;
63
183
  onToggle(): void;
64
184
  beginSnap?: SnapPress;
185
+ /** The row's mask marks (gap 7): the glyph, the bracket and the bar's mode
186
+ * label, read by `maskMarksOf` like the outline's. */
187
+ mask?: MaskMarks | undefined;
65
188
  }) {
66
189
  const store = useStore();
190
+ // MEMBERSHIP, not the array. Subscribing to `s.selection` woke every lifespan
191
+ // row on screen for every selection change, when the only rows whose output
192
+ // moved are the two whose membership flipped. The whole selection is read at
193
+ // the PRESS instead, off the store, which is the only moment the drag handler
194
+ // below wants it.
67
195
  const selected = useShotEditor((s) => s.selection.includes(row.layerId));
196
+ const ghostFrom = useStaggerGhost(row.layerId);
197
+ const readout = useStaggerReadout(row.layerId);
68
198
  const { lifespan } = row;
199
+ // THE ONE PREDICATE, and `expandedLayerHeight` in TimelineDock reads the same
200
+ // call: see `clipRowOf` for why the height and the render cannot be allowed
201
+ // two answers. Read once here, for the class, the box and the body alike.
202
+ const video = clipRowOf(layer, row);
69
203
 
70
204
  /** Selecting a row in another scene NAVIGATES first: the stage shows one
71
205
  * scene, so handles on a layer three scenes away would be handles on
72
206
  * nothing — and `select` orders within the current scene, so the selection
73
- * would resolve to empty. */
74
- const pick = (): void => {
207
+ * would resolve to empty.
208
+ *
209
+ * ⇧ (without ⌥, which is the slide) toggles this row in a selection of the
210
+ * same container, as in every design tool. Pressing a row that is already
211
+ * selected keeps the selection, so the group a drag carried is still the
212
+ * selection after it lands. */
213
+ const pick = (e?: { shiftKey: boolean; altKey: boolean }): void => {
214
+ const s = store.getState();
215
+ const here = s.editingDef === null ? sceneId === s.sceneId : sceneId === s.editingDef;
216
+ if (here && e?.shiftKey === true && !e.altKey) {
217
+ store.select(
218
+ s.selection.includes(row.layerId)
219
+ ? s.selection.filter((id) => id !== row.layerId)
220
+ : [...s.selection, row.layerId],
221
+ );
222
+ return;
223
+ }
75
224
  store.focusScene(sceneId);
76
- store.select([row.layerId]);
225
+ if (!(here && s.selection.includes(row.layerId))) store.select([row.layerId]);
77
226
  };
78
227
 
79
228
  const drag =
80
- (mode: LifespanDrag) =>
229
+ (mode: "body" | "in" | "out" | "roll") =>
81
230
  (e: ReactPointerEvent<HTMLElement>): void => {
82
231
  e.preventDefault();
83
232
  e.stopPropagation();
84
- pick();
233
+ // WHAT IS MOVING is decided BEFORE the selection changes. Pressing a bar
234
+ // that is already selected moves the group and keeps it selected;
235
+ // pressing any other bar selects it and moves it alone, which is what
236
+ // every NLE does.
237
+ //
238
+ // NOT INSIDE A DEFINITION: a group move goes through `shiftLayer`, which
239
+ // only finds layers in `doc.scenesById`, so there the plain "body" moves
240
+ // the pressed bar. This list is also the snap exclusion below, so it must
241
+ // name exactly the bars that move.
242
+ //
243
+ // A ROLL IS THE OTHER TWO-BAR GESTURE, and it is the exclusion half of
244
+ // this list rather than the group half. A seam IS the partner's `in` and
245
+ // this bar's `out` at one frame, so a roll that named only its own row
246
+ // offered the snapper a target sitting exactly on the frame being
247
+ // dragged: the pull won every comparison and the drag stuck where it
248
+ // already was. `gestureOf` returns the roll before it ever reads this
249
+ // list, so widening it here cannot turn a roll into a group move.
250
+ //
251
+ // The partner's OTHER edge and its keys go with it, which is the same
252
+ // ceiling the pressed row has always paid: the exclusion's grain is a
253
+ // layer, not a frame.
254
+ const carried =
255
+ mode === "roll"
256
+ ? video?.rollPartner == null
257
+ ? [row.layerId]
258
+ : [row.layerId, video.rollPartner]
259
+ : mode === "body" && selected && !isDefId(sceneId)
260
+ ? store.getState().selection
261
+ : [row.layerId];
262
+ pick(e);
85
263
  const startX = e.clientX;
86
264
  // Read at the PRESS: the pose the drag is measured from, and the two
87
265
  // modifiers. A gesture whose meaning changed under a held key mid-drag
88
266
  // would retime an animation the author was only moving.
89
- const slipping = mode === "body" && isSlip(e);
90
- const effective: LifespanDrag = slipping ? "slip" : mode;
91
- const snap = beginSnap?.({ sceneId, layerId: row.layerId }, isSnapSuppressed(e));
92
- // WHAT THE POINTER IS CARRYING is what snaps: the out edge for an out
93
- // drag, the earliest block for a slip (the bar is not moving), the in
94
- // edge for everything else.
267
+ const effective = gestureOf(mode, e, video ?? null, carried);
268
+ if (effective === null) return;
269
+ // ── WHERE THIS EDGE MAY LEGALLY LAND ──────────────────────────────
270
+ //
271
+ // A clip's edge is bounded by its FOOTAGE as well as by its scene, and
272
+ // `trimLayer` refuses rather than clamps when the constraints do not
273
+ // intersect. The range is read ONCE, at the press, and spends itself
274
+ // twice: it bounds the pointer's own travel below, and it goes to the
275
+ // snapper, which drops every target outside it at collection time. That
276
+ // second half is why an edge cannot stick to a guide the op would then
277
+ // clamp away from.
278
+ //
279
+ // A SEAM IS BOUNDED THE SAME WAY, through `legalRollRange`, and it is
280
+ // wired for the same half: `rollLayers` already clamps the frame it
281
+ // writes, so without this the document was right and the GUIDE was free
282
+ // to point at a frame the op would then clamp away from, which reads as
283
+ // a broken drag even when what lands is legal.
284
+ const doc = store.getState().doc;
285
+ const legal =
286
+ doc === null || video === undefined
287
+ ? null
288
+ : mode === "in" || mode === "out"
289
+ ? legalTrimRange(doc, sceneId, row.layerId, mode)
290
+ : mode === "roll" && video.rollPartner !== null
291
+ ? legalRollRange(doc, sceneId, row.layerId, video.rollPartner)
292
+ : null;
293
+ const clampLegal = (sceneFrame: number): number =>
294
+ legal === null || legal.empty === true
295
+ ? sceneFrame
296
+ : Math.max(legal.min, Math.min(legal.max, sceneFrame));
297
+ // EVERY BAR THIS GESTURE CARRIES is excluded from the target list, not
298
+ // just the pressed one: a bar sits exactly on its own edge, so any one of
299
+ // the others would win every comparison and pin the drag at zero travel.
300
+ //
301
+ // A STAGGER DOES NOT SNAP (Paper page 23): each bar moves a different
302
+ // distance, so a guide under the pointer would describe none of them.
303
+ const stagger = typeof effective !== "string" && "stagger" in effective;
304
+ const snap = stagger
305
+ ? undefined
306
+ : beginSnap?.(
307
+ carried.length > 1
308
+ ? { sceneId, layerId: row.layerId, layerIds: carried }
309
+ : { sceneId, layerId: row.layerId },
310
+ isSnapSuppressed(e),
311
+ // MASTER frames, because that is the base the targets are collected
312
+ // in. An empty range survives the shift as an empty one, so
313
+ // "nowhere legal to go" still filters everything out.
314
+ legal === null ? undefined : { min: legal.min + sceneStart, max: legal.max + sceneStart },
315
+ );
316
+ // WHAT THE POINTER IS CARRYING is what snaps, and for a slip that is
317
+ // never the bar: the bar does not move. A non-video slip carries its
318
+ // earliest keyframe; a video slip carries the source's own t=0 crossing, so
319
+ // "put the start of the take on this beat" is the alignment the pull
320
+ // offers. Everything else carries the edge it is dragging.
95
321
  const anchor =
96
- effective === "out"
322
+ mode === "out" || mode === "roll"
97
323
  ? lifespan.out
98
- : effective === "slip"
99
- ? row.blocks.reduce((min, b) => Math.min(min, b.frame), Infinity)
100
- : lifespan.in;
324
+ : effective !== "slip"
325
+ ? lifespan.in
326
+ : video === undefined
327
+ ? firstKeyframeFrame(layer?.tracks)
328
+ : video.offsetFrames;
329
+ // The ghosts: each moving bar's `in` at the press, in the lane's own
330
+ // drawing clock, which is the clock every row draws its bar in. A stagger
331
+ // never runs inside a definition (`carried` is one bar there), so the
332
+ // container is a scene.
333
+ const scene = doc?.scenesById[sceneId];
334
+ const from: Record<string, number> = {};
335
+ if (stagger && scene !== undefined)
336
+ for (const id of carried) {
337
+ const layer = scene.layersById[id];
338
+ if (layer !== undefined) from[id] = sceneStart - originFrames + lifespanOf(layer, scene.durationFrames).in;
339
+ }
340
+ if (stagger) setStaggerPreview({ from, grabbed: row.layerId, step: 0 });
101
341
  store.beginBlockDrag();
102
342
  startDrag(e, {
103
343
  onMove(ev) {
104
344
  const raw = frameDelta(ev.clientX - startX, ppf);
345
+ const want = clampLegal(anchor + raw);
105
346
  // A signed delta into master space and straight back, so the store
106
347
  // still only ever sees a scene-local number.
107
- const delta =
108
- snap === undefined || !Number.isFinite(anchor)
109
- ? raw
110
- : snap(anchor + raw + sceneStart) - sceneStart - anchor;
111
- store.dragLifespan(sceneId, row.layerId, effective, delta);
348
+ //
349
+ // NO SECOND CLAMP around the snapper: every target it can answer with
350
+ // is inside the range now, and its other answer is the rounded
351
+ // candidate, which is bounded on the line above and cannot round out
352
+ // of a range whose ends are integers.
353
+ const landed = snap === undefined || !Number.isFinite(anchor) ? want : snap(want + sceneStart) - sceneStart;
354
+ store.dragLifespan(sceneId, row.layerId, effective, landed - anchor);
355
+ if (stagger) setStaggerPreview({ from, grabbed: row.layerId, step: Math.round(landed - anchor) });
356
+ },
357
+ onEnd: (commit) => {
358
+ if (stagger) setStaggerPreview(null);
359
+ store.endBlockDrag(commit);
112
360
  },
113
- onEnd: (commit) => store.endBlockDrag(commit),
114
361
  });
115
362
  };
116
363
 
117
- const left = frameToPx(sceneStart + lifespan.in, ppf);
364
+ // Named once and used twice, for `useLive`'s reason: the frame the bar is
365
+ // DRAWN at and the frame it is judged live at must be one arithmetic.
366
+ const startLane = sceneStart - originFrames + lifespan.in;
367
+ const live = useLive(startLane, startLane + (lifespan.out - lifespan.in), originFrames);
368
+ const left = frameToPx(startLane, ppf);
118
369
  const width = Math.max(8, frameToPx(lifespan.out - lifespan.in, ppf));
370
+ // A NAME IS NEVER CUT SHORT. A video clip draws its own name over its picture.
371
+ const namePlace = video !== undefined || laneW === undefined ? "inside" : clipNamePlace(left, width, row.label, laneW);
372
+ /**
373
+ * THE ADAPTIVE GRAB ZONE IS A CLIP AFFORDANCE, and it stays scoped to one.
374
+ *
375
+ * `.lifespan-handle { width: 8px }` in styles.css is what every bar in this
376
+ * dock has always been grabbed by. Setting a width inline on every row
377
+ * silently retuned the hit target on kinds that have nothing to do with
378
+ * video AND left that rule dead for all of them, which is a stylesheet that
379
+ * describes a different machine than the code.
380
+ *
381
+ * ponytail: "two 8px handles ARE a six-frame bar" is true of every kind, so
382
+ * widening this is defensible. It is a deliberate change to every row's hit
383
+ * target and it has to retire the CSS literal in the same move; it is not
384
+ * this fix, and half of it is worse than neither half.
385
+ */
386
+ const clipHandle: CSSProperties | undefined =
387
+ video === undefined ? undefined : { width: trimHandlePx(width) };
388
+ const barStyle: CSSProperties =
389
+ video === undefined
390
+ ? { left, width }
391
+ : // The clip bar owns its own box: no padding, no border and no vertical
392
+ // inset, so the 32 + 1 + 22 budget in model.ts is exactly the row's
393
+ // content height. Selection still shows, because `.is-selected` is an
394
+ // OUTLINE rather than a border.
395
+ {
396
+ left,
397
+ width,
398
+ top: 0,
399
+ bottom: 0,
400
+ padding: 0,
401
+ border: 0,
402
+ borderRadius: 4,
403
+ flexDirection: "column",
404
+ alignItems: "stretch",
405
+ };
119
406
 
120
407
  return (
121
408
  <div
122
- className={`lifespan-row${selected ? " is-selected" : ""}`}
409
+ className={`lifespan-row${selected ? " is-selected" : ""}${live ? " is-live" : ""}${video === undefined ? "" : " is-video"}${revealed ? " is-revealed" : ""}${
410
+ mask?.off === true ? " is-mask-off" : ""
411
+ }`}
123
412
  data-lifespan={row.layerId}
124
413
  data-row-scene={sceneId}
414
+ style={video === undefined ? undefined : { height: VIDEO_ROW_H }}
415
+ // The stage lights this row's layer while the cursor is here:
416
+ // identity-diffed in the store, one SVG repaint stage-side.
417
+ onPointerEnter={() => store.hoverLayer(row.layerId)}
418
+ onPointerLeave={() => store.hoverLayer(null)}
125
419
  >
126
- <div className="lifespan-label-cell" style={{ width: labelWidth, paddingLeft: 4 + row.depth * 12 }}>
420
+ <div
421
+ className="lifespan-label-cell"
422
+ style={{ width: labelWidth, "--depth": row.depth, "--mask-inset": `${mask?.inset ?? 0}px` } as CSSProperties}
423
+ >
424
+ {mask === undefined ? null : <MaskBracket segments={mask.segments} base="var(--space-2)" />}
127
425
  <button
128
426
  type="button"
129
427
  className="lifespan-disclosure"
130
428
  aria-expanded={expanded}
131
- aria-label={expanded ? `Hide ${row.label}'s blocks` : `Show ${row.label}'s blocks`}
429
+ aria-label={expanded ? `Hide ${row.label}'s keyframes` : `Show ${row.label}'s keyframes`}
132
430
  onClick={onToggle}
133
431
  >
134
- {expanded ? "▾" : "▸"}
432
+ {expanded ? <IconChevronDown /> : <IconChevron />}
135
433
  </button>
434
+ {/* OUTSIDE the name button, the rule FilePanel's own kind glyph keeps:
435
+ the button's textContent is the layer's label and nothing else, which
436
+ is what a screen reader announces and what the tests compare. */}
437
+ {layer === undefined ? null : (
438
+ <span
439
+ className={`layer-kind${
440
+ mask?.icon === "mask" ? " is-mask" : mask?.icon == null && isComponentGlyph(layer) ? " is-component" : ""
441
+ }`}
442
+ aria-hidden="true"
443
+ >
444
+ {mask?.icon == null ? iconFor(layer) : MASK_ICON}
445
+ </span>
446
+ )}
136
447
  <button type="button" className="lifespan-name" onClick={pick}>
137
448
  {row.label}
138
449
  </button>
450
+ {/* The mask's mode, the outline's badge (Paper page 25). */}
451
+ {mask?.badge == null ? null : (
452
+ <span className="mask-badge" title={mask.badge}>
453
+ {mask.badge}
454
+ </span>
455
+ )}
456
+ {/* Who else holds this layer (Paper page 21), a dot each. */}
457
+ <PeerDots at={(p) => p.sel.includes(row.layerId)} />
458
+ {layer === undefined ? null : <RowSwitches layer={layer} video={video} sceneId={sceneId} />}
139
459
  </div>
140
460
  <div className="lifespan-lane">
461
+ {ghostFrom === null ? null : (
462
+ <StaggerGhost fromFrame={ghostFrom} width={width} shift={startLane - ghostFrom} ppf={ppf} />
463
+ )}
141
464
  <div
142
- className={`lifespan-bar${selected ? " is-selected" : ""}`}
143
- style={{ left, width }}
465
+ className={`lifespan-bar${live ? " is-live" : ""}${selected ? " is-selected" : ""}${
466
+ video === undefined ? "" : " is-video"
467
+ }${mask?.badge == null ? "" : " is-mask"}`}
468
+ style={barStyle}
144
469
  data-bar={row.layerId}
145
470
  onPointerDown={drag("body")}
146
- title={`${row.label} · ${lifespan.in}–${lifespan.out} · drag to move it and its blocks, ⌥drag to slip the blocks alone, ⌘drag to ignore snapping`}
471
+ title={formatCombo(
472
+ video === undefined
473
+ ? `${row.label} · ${lifespan.in}–${lifespan.out} · drag to move it and its keys, ⌥drag to slip the keys alone or to stagger a selection, ⌘drag to ignore snapping`
474
+ : `${row.label} · ${lifespan.in}–${lifespan.out} · drag to move the clip, ⌥drag to slip the footage under it or to stagger a selection, ⇧⌥drag to move this clip alone, ⌘drag to ignore snapping`,
475
+ )}
147
476
  >
477
+ {video === undefined || layer?.kind !== "video" ? null : (
478
+ <ClipBody
479
+ layer={layer}
480
+ video={video}
481
+ fps={fps}
482
+ ppf={ppf}
483
+ leftPx={left}
484
+ widthPx={width}
485
+ labelWidth={labelWidth}
486
+ lifespanIn={lifespan.in}
487
+ label={row.label}
488
+ />
489
+ )}
148
490
  <span
149
491
  className="lifespan-handle lifespan-handle-in"
150
492
  data-handle="lifespan-in"
493
+ style={clipHandle}
151
494
  onPointerDown={drag("in")}
152
495
  aria-hidden="true"
153
496
  />
154
- <span className="lifespan-bar-name">{row.label}</span>
497
+ {video === undefined && namePlace === "inside" ? <span className="lifespan-bar-name">{row.label}</span> : null}
498
+ {mask?.badge == null ? null : <span className="lifespan-bar-mask">mask · {mask.badge}</span>}
499
+ {/* THE OUT EDGE CARRIES TWO GESTURES WHEN THERE IS A SEAM, so they
500
+ are split by HEIGHT rather than stacked on one 3-to-10px strip:
501
+ the trim keeps the picture half, the roll takes the waveform half.
502
+ Each target is then at least 22px tall, and the seam is where an
503
+ author already looks for a rolling edit. With no abutting
504
+ neighbour the trim handle is full height, as on any other bar. */}
155
505
  <span
156
506
  className="lifespan-handle lifespan-handle-out"
157
507
  data-handle="lifespan-out"
508
+ style={video?.rollPartner == null ? clipHandle : { ...clipHandle, bottom: VIDEO_WAVE_H }}
158
509
  onPointerDown={drag("out")}
159
510
  aria-hidden="true"
160
511
  />
512
+ {video?.rollPartner == null ? null : (
513
+ <span
514
+ className="lifespan-handle lifespan-handle-roll"
515
+ data-handle="lifespan-roll"
516
+ style={{ ...clipHandle, right: 0, top: VIDEO_PICTURE_H + VIDEO_EDGE_H, cursor: "ew-resize" }}
517
+ onPointerDown={drag("roll")}
518
+ aria-hidden="true"
519
+ />
520
+ )}
161
521
  </div>
522
+ {namePlace === "inside" ? null : (
523
+ // Beside the clip, not in it: after it, or before it at the track's
524
+ // end. Not a drag target, so a press lands on the lane as on any gap.
525
+ <span
526
+ className={`lifespan-bar-name is-outside is-${namePlace}`}
527
+ style={
528
+ namePlace === "after"
529
+ ? { left: left + width + CLIP_NAME_GAP_PX }
530
+ : { left: left - CLIP_NAME_GAP_PX, transform: "translateX(-100%)" }
531
+ }
532
+ >
533
+ {row.label}
534
+ </span>
535
+ )}
536
+ {readout === null ? null : <StaggerChip step={readout.step} left={left + width} />}
162
537
  </div>
163
538
  </div>
164
539
  );
165
540
  }
541
+
542
+ /**
543
+ * Which document write a press means, or null when it means nothing.
544
+ *
545
+ * Kept out of the handler so the mapping is one expression a reader can check
546
+ * against the header's table, and so the two modifier predicates are read
547
+ * exactly once, at the press.
548
+ *
549
+ * Null only for a roll with no partner, which the handle's own render condition
550
+ * already prevents: it is guarded here as well because the two conditions would
551
+ * otherwise have to stay in step by hand, and the cost of a stale one is a roll
552
+ * against an empty id.
553
+ */
554
+ function gestureOf(
555
+ mode: "body" | "in" | "out" | "roll",
556
+ event: { altKey: boolean; shiftKey: boolean },
557
+ video: VideoRow | null,
558
+ carried: readonly string[],
559
+ ): LifespanDrag | null {
560
+ if (mode === "roll") return video?.rollPartner == null ? null : { roll: video.rollPartner };
561
+ if (mode !== "body") return mode;
562
+ // THE CLIP FORK IS FIRST AND IT IS THE ONLY ONE ⇧ REACHES. Off a clip the
563
+ // chord falls through to the slip below, which is what ⌥ with or without ⇧
564
+ // has always done on an ordinary row — and `writeLifespan` routes "shift" to
565
+ // `shiftLayer` above its own video fork, so leaking the word here would
566
+ // rewrite a rect's window from a gesture that used to move only its keys.
567
+ if (video !== null && isSolo(event)) return "shift";
568
+ // ⌥ ON A SELECTION FANS IT OUT; on one bar it is still the slip.
569
+ if (isSlip(event)) return carried.length > 1 ? { stagger: carried } : "slip";
570
+ // A group move needs the ids; one bar does not, and the word form is what
571
+ // every existing test already speaks.
572
+ return carried.length > 1 ? { move: carried } : "body";
573
+ }
574
+
575
+ /**
576
+ * The picture, the edge and the wave, stacked.
577
+ *
578
+ * The EDGE is a line and not a lighter fill on purpose: in the light theme
579
+ * `--panel-raised` is `--panel`, so a surface step up between the two strips
580
+ * would be invisible. A 1px rule is what reads in both appearances.
581
+ */
582
+ function ClipBody({
583
+ layer,
584
+ video,
585
+ fps,
586
+ ppf,
587
+ leftPx,
588
+ widthPx,
589
+ labelWidth,
590
+ lifespanIn,
591
+ label,
592
+ }: {
593
+ layer: VideoLayer3D;
594
+ video: VideoRow;
595
+ fps: Rational;
596
+ ppf: number;
597
+ /** The bar's left edge in LANE pixels, exactly as the row placed it. */
598
+ leftPx: number;
599
+ widthPx: number;
600
+ labelWidth: number;
601
+ /** The bar's own left edge, in scene frames, so the held stretches can be
602
+ * placed relative to it. */
603
+ lifespanIn: number;
604
+ label: string;
605
+ }) {
606
+ // ── THE AUDIBLE SPAN IS NOT THE BAR ────────────────────────────────────
607
+ //
608
+ // A recording's inline sound starts where the FOOTAGE starts, not where the
609
+ // window opens: `declaredVideoSoundClip` (timeline.ts) places it at
610
+ // `max(lifetime.in, video.offsetFrames)` and runs it to `lifetime.out`, and
611
+ // every mixer reads that one function. Handing the waveform the whole bar
612
+ // drew peaks across the lead hatch, which is the exact stretch that has no
613
+ // sound in it — the picture and the sound disagreeing about where the take
614
+ // begins, on the one row built to show that they agree.
615
+ //
616
+ // The same expression off the same two fields, rather than a second answer:
617
+ // `video.sound.durationFrames` IS that function's `out - from` over this same
618
+ // window, so the span ends on the bar's own out edge and needs no clamp here.
619
+ // A window that never opens is dropped one step earlier, as a null `sound`.
620
+ const soundFrom = Math.max(lifespanIn, Math.trunc(video.offsetFrames));
621
+ return (
622
+ <>
623
+ {/* THE STRIP'S CONTAINING BLOCK, and that is what this span is for.
624
+ `Filmstrip` places itself absolutely against its positioned ancestor
625
+ and fills it top to bottom; against the BAR that would be all 56px and
626
+ the waveform would be under footage. Against this it is the 32 the
627
+ picture owns. `overflow: hidden` is what cuts a column that straddles
628
+ either end, which is the clip the plan's own header relies on. */}
629
+ <span
630
+ className="lifespan-clip-picture"
631
+ style={{ height: VIDEO_PICTURE_H, position: "relative", overflow: "hidden", flex: "none" }}
632
+ >
633
+ <Filmstrip
634
+ layer={layer}
635
+ asset={video.pictureAsset}
636
+ fps={fps}
637
+ lifetimeIn={lifespanIn}
638
+ ppf={ppf}
639
+ barLeftPx={leftPx}
640
+ barWidthPx={widthPx}
641
+ labelWidth={labelWidth}
642
+ />
643
+ {/* POSITIONED, because the strip is: CSS paints a positioned descendant
644
+ over in-flow content whatever the document order, so a plain span
645
+ here would vanish under the footage. `Filmstrip`'s own mount note
646
+ says so, and this is the row keeping that side of the contract. */}
647
+ <span
648
+ className="lifespan-bar-name"
649
+ style={{
650
+ position: "relative",
651
+ // BLOCK, because `.lifespan-bar-name`'s `overflow: hidden` and
652
+ // `text-overflow: ellipsis` do nothing on an inline box. In an
653
+ // ordinary bar it is a flex ITEM and therefore already blockified;
654
+ // here it is a child of the picture strip and is not.
655
+ display: "block",
656
+ lineHeight: `${VIDEO_PICTURE_H}px`,
657
+ padding: "0 6px",
658
+ }}
659
+ >
660
+ {label}
661
+ </span>
662
+ </span>
663
+ <span
664
+ className="lifespan-clip-edge"
665
+ aria-hidden="true"
666
+ style={{ height: VIDEO_EDGE_H, background: "var(--bar-rest-line)", flex: "none" }}
667
+ />
668
+ <span className="lifespan-clip-sound" style={{ height: VIDEO_WAVE_H, position: "relative", flex: "none" }}>
669
+ {video.sound === null ? null : (
670
+ // POSITIONED, and that is the whole mechanism: `.audio-wave` is
671
+ // `position: absolute; inset: 0`, so the canvas fills its nearest
672
+ // positioned ancestor whatever pixel width it was drawn at. The box
673
+ // is the audible span; `widthPx` only says how many pixels of peaks
674
+ // to put in it.
675
+ <span
676
+ data-clip-wave=""
677
+ style={{
678
+ position: "absolute",
679
+ top: 0,
680
+ bottom: 0,
681
+ left: frameToPx(soundFrom - lifespanIn, ppf),
682
+ width: frameToPx(video.sound.durationFrames, ppf),
683
+ }}
684
+ >
685
+ <AudioWaveform
686
+ clip={video.sound}
687
+ asset={video.soundAsset}
688
+ fps={fps}
689
+ widthPx={frameToPx(video.sound.durationFrames, ppf)}
690
+ heightPx={VIDEO_WAVE_H}
691
+ />
692
+ </span>
693
+ )}
694
+ </span>
695
+ {/* OVER BOTH STRIPS, because the held picture is held in the waveform's
696
+ half of the bar too — there is simply no sound out there either. */}
697
+ <HeldMark span={video.lead} lifespanIn={lifespanIn} ppf={ppf} which="lead" />
698
+ <HeldMark span={video.hold} lifespanIn={lifespanIn} ppf={ppf} which="hold" />
699
+ </>
700
+ );
701
+ }
702
+
703
+ /** One frozen stretch, hatched, with a rule on the crossing itself so the exact
704
+ * frame the picture stops on is readable and not merely implied. */
705
+ function HeldMark({
706
+ span,
707
+ lifespanIn,
708
+ ppf,
709
+ which,
710
+ }: {
711
+ span: HeldSpan | null;
712
+ lifespanIn: number;
713
+ ppf: number;
714
+ which: "lead" | "hold";
715
+ }) {
716
+ if (span === null) return null;
717
+ const from = frameToPx(span.from - lifespanIn, ppf);
718
+ const to = frameToPx(span.to - lifespanIn, ppf);
719
+ // The rule sits on the crossing, which is the INNER end of the stretch: the
720
+ // lead freezes up to its `to` and the hold from its `from`.
721
+ const rule = which === "lead" ? "borderRight" : "borderLeft";
722
+ return (
723
+ <span
724
+ className={`lifespan-held is-${which}`}
725
+ data-held={which}
726
+ aria-hidden="true"
727
+ title={
728
+ which === "lead"
729
+ ? "The recording has not started yet: this stretch is its first frame, frozen"
730
+ : "The recording has run out: this stretch is its last frame, frozen"
731
+ }
732
+ style={{
733
+ position: "absolute",
734
+ top: 0,
735
+ bottom: 0,
736
+ left: from,
737
+ width: Math.max(1, to - from),
738
+ background: HELD_HATCH,
739
+ [rule]: "1px solid color-mix(in srgb, var(--timeline-ink) 45%, transparent)",
740
+ pointerEvents: "none",
741
+ }}
742
+ />
743
+ );
744
+ }
745
+
746
+ /**
747
+ * The switches in a row's label cell: every row carries eye and lock, a clip
748
+ * with sound adds mute.
749
+ *
750
+ * EYE and LOCK are the layer's own flags and MUTE is the recording's inline
751
+ * sound. Mute is a different switch from the eye by design: `hidden` is the
752
+ * picture, `video.sound.muted` is the audio, which is the split every NLE draws
753
+ * and the split `videoSoundClip` already reads.
754
+ *
755
+ * ── ALL THREE NAME THEIR SUBJECT, AND THE EYE USED NOT TO ──────────────
756
+ *
757
+ * The outliner writes its two flags as `select` + `toggleFlag`, and that is
758
+ * sound THERE: its rows are the current container's by construction, so the
759
+ * container `toggleFlag` resolves for itself (`editingDef ?? state.sceneId`) is
760
+ * the one the row is in. This dock is one MASTER strip. `state.sceneId` follows
761
+ * the PLAYHEAD, so a row here is routinely in another scene, and the same pair
762
+ * copied over addressed a container the row is not in: `select` dropped an id
763
+ * that resolves in neither home and cleared the selection, `toggleFlag` then
764
+ * had nothing to write, and in a document whose ids were not minted against
765
+ * `usedIdsOf` it wrote to the playhead scene's layer of the same id instead.
766
+ *
767
+ * So each switch hands the verb this ROW's container and this row's layer, the
768
+ * way the bar's own drags hand `dragLifespan` its `sceneId`. Mute already did;
769
+ * it is now the shape all three share. Pressing one no longer moves the
770
+ * selection or the playhead either, which is what "a switch is not a
771
+ * navigation" means on a strip that shows the whole video at once.
772
+ *
773
+ * SOLO IS NOT HERE, and that is not an omission. Soloing is MONITORING: it
774
+ * changes what the author hears now and must never reach the document, so it
775
+ * belongs in the store's workspace-prefs family beside the other "where the
776
+ * author is standing" state. This file cannot add it — see the handoff note on
777
+ * store.ts — and a button that toggles nothing is worse than no button.
778
+ */
779
+ function RowSwitches({
780
+ layer,
781
+ video,
782
+ sceneId,
783
+ }: {
784
+ layer: Layer3D;
785
+ video: VideoRow | undefined;
786
+ sceneId: string;
787
+ }) {
788
+ const store = useStore();
789
+ const hidden = layer.hidden === true;
790
+ const locked = layer.locked === true;
791
+ return (
792
+ <>
793
+ <button
794
+ type="button"
795
+ className="layer-flag"
796
+ data-switch="hidden"
797
+ aria-pressed={hidden}
798
+ aria-label={`${hidden ? "Show" : "Hide"} ${layer.label}`}
799
+ onPointerDown={(e) => e.stopPropagation()}
800
+ onClick={() => store.toggleFlag("hidden", { containerId: sceneId, layerIds: [layer.id] })}
801
+ >
802
+ {hidden ? <IconEyeOff /> : <IconEye />}
803
+ </button>
804
+ <button
805
+ type="button"
806
+ className="layer-flag"
807
+ data-switch="locked"
808
+ aria-pressed={locked}
809
+ aria-label={`${locked ? "Unlock" : "Lock"} ${layer.label}`}
810
+ onPointerDown={(e) => e.stopPropagation()}
811
+ onClick={() => store.toggleFlag("locked", { containerId: sceneId, layerIds: [layer.id] })}
812
+ >
813
+ {locked ? <IconLock /> : <IconUnlock />}
814
+ </button>
815
+ {/* Only when there IS a soundtrack. A recording that declares none has no
816
+ capability here to keep an affordance visible for. */}
817
+ {video === undefined || !video.hasSound || layer.kind !== "video" ? null : (
818
+ <button
819
+ type="button"
820
+ className="layer-flag"
821
+ data-switch="muted"
822
+ aria-pressed={video.muted}
823
+ aria-label={`${video.muted ? "Unmute" : "Mute"} ${layer.label}`}
824
+ onPointerDown={(e) => e.stopPropagation()}
825
+ onClick={() => {
826
+ if (layer.video.sound === undefined) return;
827
+ // THE RECORD IS RE-READ INSIDE THE EDIT, never the one this row
828
+ // closed over at render. `rowLayer` is the BASE layer (TimelineDock
829
+ // reads `layersById` directly), so the gain here is authored and not
830
+ // a sample of a keyframed curve. That hazard is `SoundRows`'s, and
831
+ // `baseSoundOf` in TimeSection.tsx answers it. The hazard HERE is
832
+ // staleness: a mute pressed after another panel committed a fade
833
+ // would spread this render's record back over it. Scene-addressed
834
+ // like `setVideoSound` itself, so a definition container answers
835
+ // undefined rather than handing back a record the op would refuse.
836
+ store.edit((doc) => {
837
+ const base = doc.scenesById[sceneId]?.layersById[layer.id];
838
+ if (base === undefined || base.kind !== "video" || base.video.sound === undefined) return doc;
839
+ return setVideoSound(doc, sceneId, layer.id, { ...base.video.sound, muted: !video.muted });
840
+ });
841
+ }}
842
+ >
843
+ {video.muted ? <IconVolumeOff /> : <IconVolume />}
844
+ </button>
845
+ )}
846
+ </>
847
+ );
848
+ }