@hyperframes/studio 0.8.111 → 0.8.113

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 (82) hide show
  1. package/dist/assets/{hyperframes-player-DvJeHBHi.js → hyperframes-player-DzJbtQVO.js} +1 -1
  2. package/dist/assets/{index-SFktf7O1.js → index-DRhF3v4Z.js} +194 -194
  3. package/dist/assets/{index-C7apTvpr.css → index-DbfKVTId.css} +1 -1
  4. package/dist/assets/{index--G6c4Q_x.js → index-DgwDQK5G.js} +1 -1
  5. package/dist/assets/{index-Bt4vDkfw.js → index-n5FUn1xv.js} +1 -1
  6. package/dist/{chunk-PQFEBX32.js → chunk-BIUXP3PG.js} +2 -2
  7. package/dist/{chunk-6LUNLTHR.js → chunk-K47E2ZJA.js} +13 -8
  8. package/dist/chunk-K47E2ZJA.js.map +1 -0
  9. package/dist/{chunk-QUXZVHOJ.js → chunk-VWT7STY3.js} +2 -2
  10. package/dist/{domEditingLayers-JQPQGGT7.js → domEditingLayers-V67MS4WP.js} +3 -3
  11. package/dist/index.d.ts +80 -46
  12. package/dist/index.html +2 -2
  13. package/dist/index.js +7294 -6844
  14. package/dist/index.js.map +1 -1
  15. package/dist/player.js +2 -2
  16. package/dist/styles.css +1 -1
  17. package/package.json +10 -8
  18. package/src/components/nle/useCanvasZOrderTimelineMirror.ts +2 -2
  19. package/src/hooks/timelineEditingHelpers.ts +7 -0
  20. package/src/hooks/timelineMoveAdapter.test.ts +12 -0
  21. package/src/hooks/timelineMoveAdapter.ts +9 -4
  22. package/src/hooks/useTimelineGroupEditing.test.tsx +77 -0
  23. package/src/hooks/useTimelineGroupEditing.ts +5 -1
  24. package/src/index.ts +7 -0
  25. package/src/main.tsx +4 -0
  26. package/src/player/components/ClipBadges.tsx +5 -5
  27. package/src/player/components/Timeline.ghostLane.test.tsx +8 -61
  28. package/src/player/components/Timeline.trackPadding.test.tsx +11 -56
  29. package/src/player/components/TimelineCanvas.tsx +6 -2
  30. package/src/player/components/TimelineClip.filmstrip.test.tsx +156 -0
  31. package/src/player/components/TimelineClip.test.tsx +7 -7
  32. package/src/player/components/TimelineClip.tsx +23 -7
  33. package/src/player/components/TimelineClipFades.tsx +41 -43
  34. package/src/player/components/TimelineGestureOverlay.test.tsx +41 -0
  35. package/src/player/components/TimelineGestureOverlay.tsx +59 -21
  36. package/src/player/components/TimelineLanes.tsx +18 -16
  37. package/src/player/components/timelineCallbacks.ts +6 -4
  38. package/src/player/components/timelineClipChildren.tsx +7 -13
  39. package/src/player/components/timelineClipDragCommit.test.ts +51 -0
  40. package/src/player/components/timelineClipDragCommit.ts +63 -21
  41. package/src/player/components/timelineClipDragGestureLifecycle.ts +18 -2
  42. package/src/player/components/timelineClipDragPreview.test.ts +47 -31
  43. package/src/player/components/timelineClipDragPreview.ts +81 -17
  44. package/src/player/components/timelineClipDragTypes.ts +3 -3
  45. package/src/player/components/timelineClipFadeGeometry.test.ts +29 -0
  46. package/src/player/components/timelineClipFadeGeometry.ts +74 -0
  47. package/src/player/components/timelineCollision.ts +51 -11
  48. package/src/player/components/timelineDragLanding.ts +20 -6
  49. package/src/player/components/timelineLaneProps.ts +3 -8
  50. package/src/player/components/timelineMotionStyles.test.ts +6 -13
  51. package/src/player/components/timelineMountFixtures.tsx +65 -0
  52. package/src/player/components/timelineOptimisticRevision.ts +15 -4
  53. package/src/player/components/timelineTheme.ts +2 -0
  54. package/src/player/components/timelineTrackDisplay.ts +1 -1
  55. package/src/player/components/timelineTrackInsertLayout.test.ts +249 -0
  56. package/src/player/components/timelineTrackInsertLayout.ts +71 -0
  57. package/src/player/components/timelineZMirror.ts +5 -11
  58. package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +163 -0
  59. package/src/player/components/useTimelineClipDrag.ts +116 -12
  60. package/src/player/components/useTimelineDisplayLayout.test.tsx +59 -0
  61. package/src/player/components/useTimelineKeyboardActor.ts +86 -7
  62. package/src/player/components/useTimelineProviderState.tsx +3 -1
  63. package/src/player/components/useTimelineRowVirtualization.test.tsx +56 -0
  64. package/src/player/components/useTimelineRowVirtualization.ts +3 -1
  65. package/src/player/components/useTimelineTrackDerivations.test.ts +14 -1
  66. package/src/player/components/useTimelineTrackDerivations.ts +32 -18
  67. package/src/player/components/useTimelineTrackLayout.ts +43 -14
  68. package/src/player/hooks/timelinePlayerSync.test.ts +7 -0
  69. package/src/player/hooks/timelinePlayerSync.ts +1 -0
  70. package/src/player/lib/automationStoreSync.test.ts +15 -0
  71. package/src/player/lib/automationStoreSync.ts +13 -5
  72. package/src/player/lib/storedVolume.ts +7 -0
  73. package/src/player/lib/thumbnailPageLifecycle.test.ts +123 -0
  74. package/src/player/lib/thumbnailPageLifecycle.ts +27 -0
  75. package/src/player/lib/thumbnailScheduler.ts +20 -2
  76. package/src/player/lib/timelineElementHelpers.ts +2 -8
  77. package/src/styles/components.css +71 -19
  78. package/src/styles/theme.css +4 -1
  79. package/dist/chunk-6LUNLTHR.js.map +0 -1
  80. /package/dist/{chunk-PQFEBX32.js.map → chunk-BIUXP3PG.js.map} +0 -0
  81. /package/dist/{chunk-QUXZVHOJ.js.map → chunk-VWT7STY3.js.map} +0 -0
  82. /package/dist/{domEditingLayers-JQPQGGT7.js.map → domEditingLayers-V67MS4WP.js.map} +0 -0
@@ -1,4 +1,4 @@
1
- import{n as Qi}from"./index-SFktf7O1.js";/*!
1
+ import{n as Qi}from"./index-DRhF3v4Z.js";/*!
2
2
  * Copyright (c) 2026-present, Vanilagy and contributors
3
3
  *
4
4
  * This Source Code Form is subject to the terms of the Mozilla Public
@@ -1 +1 @@
1
- import{g as P}from"./index-SFktf7O1.js";function j(c,d){for(var s=0;s<d.length;s++){const a=d[s];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in c)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(c,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v={},w;function k(){if(w)return v;w=1,Object.defineProperty(v,"__esModule",{value:!0}),v.default=d;var c=window.OfflineAudioContext||window.webkitOfflineAudioContext;function d(e){var r=a(e);return r.start(0),[i,y,O(e.sampleRate),s].reduce(function(t,o){return o(t)},r.buffer.getChannelData(0))}function s(e){return e.sort(function(r,t){return t.count-r.count}).splice(0,5)[0].tempo}function a(e){var r=e.length,t=e.numberOfChannels,o=e.sampleRate,n=new c(t,r,o),u=n.createBufferSource();u.buffer=e;var f=n.createBiquadFilter();return f.type="lowpass",u.connect(f),f.connect(n.destination),u}function i(e){for(var r=[],t=.9,o=.3,n=15;r.length<n&&t>=o;)r=l(e,t),t-=.05;if(r.length<n)throw new Error("Could not find enough samples for a reliable detection.");return r}function l(e,r){for(var t=[],o=0,n=e.length;o<n;o+=1)e[o]>r&&(t.push(o),o+=1e4);return t}function y(e){var r=[];return e.forEach(function(t,o){for(var n=function(x){var g=e[o+x]-t,_=r.some(function(h){if(h.interval===g)return h.count+=1});_||r.push({interval:g,count:1})},u=0;u<10;u+=1)n(u)}),r}function O(e){return function(r){var t=[];return r.forEach(function(o){if(o.interval!==0){for(var n=60/(o.interval/e);n<90;)n*=2;for(;n>180;)n/=2;n=Math.round(n);var u=t.some(function(f){if(f.tempo===n)return f.count+=o.count});u||t.push({tempo:n,count:o.count})}}),t}}return v}var p,b;function q(){return b||(b=1,p=k().default),p}var m=q();const A=P(m),D=j({__proto__:null,default:A},[m]);export{D as i};
1
+ import{g as P}from"./index-DRhF3v4Z.js";function j(c,d){for(var s=0;s<d.length;s++){const a=d[s];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in c)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(c,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v={},w;function k(){if(w)return v;w=1,Object.defineProperty(v,"__esModule",{value:!0}),v.default=d;var c=window.OfflineAudioContext||window.webkitOfflineAudioContext;function d(e){var r=a(e);return r.start(0),[i,y,O(e.sampleRate),s].reduce(function(t,o){return o(t)},r.buffer.getChannelData(0))}function s(e){return e.sort(function(r,t){return t.count-r.count}).splice(0,5)[0].tempo}function a(e){var r=e.length,t=e.numberOfChannels,o=e.sampleRate,n=new c(t,r,o),u=n.createBufferSource();u.buffer=e;var f=n.createBiquadFilter();return f.type="lowpass",u.connect(f),f.connect(n.destination),u}function i(e){for(var r=[],t=.9,o=.3,n=15;r.length<n&&t>=o;)r=l(e,t),t-=.05;if(r.length<n)throw new Error("Could not find enough samples for a reliable detection.");return r}function l(e,r){for(var t=[],o=0,n=e.length;o<n;o+=1)e[o]>r&&(t.push(o),o+=1e4);return t}function y(e){var r=[];return e.forEach(function(t,o){for(var n=function(x){var g=e[o+x]-t,_=r.some(function(h){if(h.interval===g)return h.count+=1});_||r.push({interval:g,count:1})},u=0;u<10;u+=1)n(u)}),r}function O(e){return function(r){var t=[];return r.forEach(function(o){if(o.interval!==0){for(var n=60/(o.interval/e);n<90;)n*=2;for(;n>180;)n/=2;n=Math.round(n);var u=t.some(function(f){if(f.tempo===n)return f.count+=o.count});u||t.push({tempo:n,count:o.count})}}),t}}return v}var p,b;function q(){return b||(b=1,p=k().default),p}var m=q();const A=P(m),D=j({__proto__:null,default:A},[m]);export{D as i};
@@ -3,7 +3,7 @@ import {
3
3
  getSourceScopedSelectorIndex,
4
4
  isAudibleVideoNode,
5
5
  withSelectorIndexPass
6
- } from "./chunk-6LUNLTHR.js";
6
+ } from "./chunk-K47E2ZJA.js";
7
7
 
8
8
  // src/components/editor/probeSourceElement.ts
9
9
  async function probeSourceElement(projectId, sourceFile, target) {
@@ -1063,4 +1063,4 @@ export {
1063
1063
  readHfId,
1064
1064
  buildDomEditPatchTarget
1065
1065
  };
1066
- //# sourceMappingURL=chunk-PQFEBX32.js.map
1066
+ //# sourceMappingURL=chunk-BIUXP3PG.js.map
@@ -173,6 +173,15 @@ function wrapTimeline(tl) {
173
173
  // src/player/lib/timelineElementHelpers.ts
174
174
  import { HF_AUDIO_GROUP_TAG } from "@hyperframes/core/audio-groups";
175
175
  import { readElementFades } from "@hyperframes/core/audio-fade";
176
+
177
+ // src/player/lib/storedVolume.ts
178
+ function parseStoredVolume(text) {
179
+ const volume = Number.parseFloat(text ?? "");
180
+ return Number.isFinite(volume) ? volume : void 0;
181
+ }
182
+ var elementVolume = (el, media) => parseStoredVolume(el.getAttribute("data-volume") ?? media.getAttribute("data-volume"));
183
+
184
+ // src/player/lib/timelineElementHelpers.ts
176
185
  import {
177
186
  clampPlaybackRate,
178
187
  readMediaOffsetSeconds
@@ -281,12 +290,6 @@ function setOptional(entry, key, value) {
281
290
  if (value === void 0) delete entry[key];
282
291
  else entry[key] = value;
283
292
  }
284
- function readVolume(el, media) {
285
- const volume = Number.parseFloat(
286
- el.getAttribute("data-volume") ?? media.getAttribute("data-volume") ?? ""
287
- );
288
- return Number.isFinite(volume) ? volume : void 0;
289
- }
290
293
  function isVideoAudible(opts) {
291
294
  if (opts.hasAudioAttr === "true") return true;
292
295
  if (opts.hasAudioAttr === "false" || opts.hasAudioAttr === "") return false;
@@ -310,7 +313,7 @@ function applyAudioMetadataFromElement(entry, el) {
310
313
  });
311
314
  setOptional(entry, "hasAudio", sound ? true : void 0);
312
315
  setOptional(entry, "muted", muted ? true : void 0);
313
- setOptional(entry, "volume", readVolume(el, media));
316
+ setOptional(entry, "volume", elementVolume(el, media));
314
317
  }
315
318
  function applyFadeMetadataFromElement(entry, el) {
316
319
  const fades = readElementFades(el);
@@ -579,6 +582,8 @@ export {
579
582
  wrapTimeline,
580
583
  withSelectorIndexPass,
581
584
  getSourceScopedSelectorIndex,
585
+ parseStoredVolume,
586
+ elementVolume,
582
587
  LAYER_REVEAL_PRIOR_Z_ATTR,
583
588
  LAYER_REVEAL_PRIOR_POSITION_ATTR,
584
589
  readLayerRevealPriorZ,
@@ -608,4 +613,4 @@ export {
608
613
  findClipElementById,
609
614
  createTimelineDomNodeResolver
610
615
  };
611
- //# sourceMappingURL=chunk-6LUNLTHR.js.map
616
+ //# sourceMappingURL=chunk-K47E2ZJA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/sourceScopedSelectorIndex.ts","../src/player/lib/playbackAdapter.ts","../src/player/lib/timelineElementHelpers.ts","../src/player/lib/storedVolume.ts","../src/utils/projectRouting.ts"],"sourcesContent":["/**\n * Occurrence index for a selector within its source document.\n *\n * The preview flattens multiple composition files into one DOM, so a raw\n * `querySelectorAll` index is not a stable source-file identity. Callers supply\n * their existing source resolver; this helper alone owns occurrence scoping.\n */\n\n/** Every element matching one selector, mapped to the source file it belongs to\n * and its occurrence index within that file. One document query fills it. */\ntype SelectorOccurrences = Map<Element, { scope: string; index: number }>;\n\ninterface SelectorIndexPass {\n doc: Document;\n bySelector: Map<string, SelectorOccurrences>;\n}\n\nlet activePass: SelectorIndexPass | null = null;\n\n/**\n * Share one occurrence index per selector across every\n * `getSourceScopedSelectorIndex` call made synchronously inside `run`.\n *\n * Unshared, each call runs its own whole-document `querySelectorAll(selector)`\n * and resolves the source file of every match. A rebuild that walks n elements\n * sharing a selector therefore does n whole-document queries and n² resolver\n * calls — a composition of repeated cards or tiles is exactly that shape. One\n * index per selector makes the same pass linear.\n *\n * Correctness rests on the pass being ONE synchronous walk of a document that\n * does not change under it: nothing invalidates the index, so `run` must not\n * mutate `doc`, and every call inside it must resolve source files the same way\n * (the resolver may be a fresh closure per call, as long as it answers the\n * same). Outside a pass the index is per-call, exactly as before.\n */\nexport function withSelectorIndexPass<T>(doc: Document, run: () => T): T {\n const previous = activePass;\n activePass = { doc, bySelector: new Map() };\n try {\n return run();\n } finally {\n activePass = previous;\n }\n}\n\nfunction buildOccurrences(\n doc: Document,\n selector: string,\n resolveSourceFile: (candidate: Element) => string | undefined,\n): SelectorOccurrences {\n const occurrences: SelectorOccurrences = new Map();\n const counts = new Map<string, number>();\n for (const candidate of doc.querySelectorAll(selector)) {\n const scope = resolveSourceFile(candidate) ?? \"index.html\";\n const index = counts.get(scope) ?? 0;\n counts.set(scope, index + 1);\n occurrences.set(candidate, { scope, index });\n }\n return occurrences;\n}\n\nexport function getSourceScopedSelectorIndex(\n doc: Document,\n el: Element,\n selector: string | undefined,\n sourceFile: string | undefined,\n resolveSourceFile: (candidate: Element) => string | undefined,\n): number | undefined {\n if (!selector || selector.startsWith(\"#\") || selector.startsWith(\"[data-composition-id=\")) {\n return undefined;\n }\n\n try {\n const pass = activePass?.doc === doc ? activePass : null;\n let occurrences = pass?.bySelector.get(selector);\n if (!occurrences) {\n occurrences = buildOccurrences(doc, selector, resolveSourceFile);\n pass?.bySelector.set(selector, occurrences);\n }\n // An element outside the requested scope is not in that scope's occurrence\n // run at all, which is the `indexOf` miss the filtered form returned before.\n const hit = occurrences.get(el);\n return hit && hit.scope === (sourceFile ?? \"index.html\") ? hit.index : undefined;\n } catch {\n return undefined;\n }\n}\n","/**\n * Playback adapter utilities: factory for the static-seek adapter used when a\n * composition exposes only a `renderSeek` / `seek` API (no native play/pause\n * support), plus a thin wrapper that normalises GSAP-style `TimelineLike`\n * objects to the `PlaybackAdapter` interface.\n */\n\nimport type {\n PlaybackAdapter,\n RuntimePlaybackAdapter,\n StaticSeekPlaybackClock,\n TimelineLike,\n} from \"./playbackTypes\";\n\n// ---------------------------------------------------------------------------\n// Pure numeric helpers\n// ---------------------------------------------------------------------------\n\nexport function isFinitePositive(value: number): boolean {\n return Number.isFinite(value) && value > 0;\n}\n\nfunction clampTime(time: number, duration: number): number {\n const safeDuration = Math.max(0, Number.isFinite(duration) ? duration : 0);\n const safeTime = Math.max(0, Number.isFinite(time) ? time : 0);\n return safeDuration > 0 ? Math.min(safeTime, safeDuration) : safeTime;\n}\n\nexport function getAdapterDuration(adapter: PlaybackAdapter | null | undefined): number {\n if (!adapter) return 0;\n try {\n const duration = Number(adapter.getDuration());\n return isFinitePositive(duration) ? duration : 0;\n } catch {\n return 0;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Clock factory\n// ---------------------------------------------------------------------------\n\nexport function getDefaultStaticSeekPlaybackClock(win: Window): StaticSeekPlaybackClock {\n return {\n now: () => win.performance.now(),\n requestAnimationFrame: (callback) => win.requestAnimationFrame(callback),\n cancelAnimationFrame: (handle) => win.cancelAnimationFrame(handle),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Static-seek adapter\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps a render-only player (exposes `renderSeek`/`seek` but no native\n * play/pause) and drives playback via `requestAnimationFrame`.\n */\nexport function createStaticSeekPlaybackAdapter(\n player: Pick<RuntimePlaybackAdapter, \"getTime\"> &\n Partial<Pick<RuntimePlaybackAdapter, \"renderSeek\" | \"seek\">>,\n duration: number,\n clock: StaticSeekPlaybackClock,\n getPlaybackRate: () => number = () => 1,\n): PlaybackAdapter {\n const safeDuration = Math.max(0, Number.isFinite(duration) ? duration : 0);\n let currentTime = clampTime(Number(player.getTime?.() ?? 0), safeDuration);\n let playing = false;\n let rafId = 0;\n let playStartTime = currentTime;\n let playStartNow = clock.now();\n\n const renderSeek = (time: number) => {\n currentTime = clampTime(time, safeDuration);\n if (typeof player.renderSeek === \"function\") {\n player.renderSeek(currentTime);\n return;\n }\n player.seek?.(currentTime);\n };\n\n const stopTicker = () => {\n if (rafId) {\n clock.cancelAnimationFrame(rafId);\n rafId = 0;\n }\n };\n\n const tick: FrameRequestCallback = (now) => {\n if (!playing) return;\n const playbackRate = Math.max(0.1, Number(getPlaybackRate()) || 1);\n const elapsed = ((now - playStartNow) / 1000) * playbackRate;\n renderSeek(playStartTime + elapsed);\n if (currentTime >= safeDuration) {\n playing = false;\n rafId = 0;\n return;\n }\n rafId = clock.requestAnimationFrame(tick);\n };\n\n return {\n play: () => {\n if (playing || safeDuration <= 0) return;\n if (currentTime >= safeDuration) renderSeek(0);\n playing = true;\n playStartTime = currentTime;\n playStartNow = clock.now();\n stopTicker();\n rafId = clock.requestAnimationFrame(tick);\n },\n pause: () => {\n playing = false;\n stopTicker();\n },\n seek: (time, options) => {\n renderSeek(time);\n if (options?.keepPlaying) {\n if (playing) {\n playStartTime = currentTime;\n playStartNow = clock.now();\n }\n return;\n }\n // Default seek aligns with wrapTimeline: stop the RAF ticker so the\n // adapter's `playing` flag matches the public seek contract instead of\n // silently driving renderSeek in the background.\n playing = false;\n stopTicker();\n },\n getTime: () => currentTime,\n getDuration: () => safeDuration,\n isPlaying: () => playing,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Static-seek fallback cache\n// ---------------------------------------------------------------------------\n\nexport type StaticSeekCacheEntry = {\n player: RuntimePlaybackAdapter | PlaybackAdapter;\n duration: number;\n adapter: PlaybackAdapter;\n};\n\ntype StaticSeekCacheRef = { current: StaticSeekCacheEntry | null };\ntype WarnedRef = { current: boolean };\n\n/**\n * Pause and drop the cached static-seek adapter. Must be called whenever\n * adapter selection switches to a native adapter — a cached static-seek\n * adapter that was mid-play keeps its private rAF loop seeking the player\n * forever otherwise, fighting the native transport. Also re-arms the\n * downgrade warning so a later re-downgrade is surfaced again.\n */\nexport function releaseStaticSeekCache(cache: StaticSeekCacheRef, warned: WarnedRef): void {\n cache.current?.adapter.pause();\n cache.current = null;\n warned.current = false;\n}\n\n/**\n * Resolve (with caching) the seek-driven fallback adapter. Warns once per\n * downgrade streak: seek-driven playback never starts media elements or\n * WebAudio, so without the warning the downgrade silently loses audio.\n */\nexport function resolveStaticSeekFallback(opts: {\n cache: StaticSeekCacheRef;\n warned: WarnedRef;\n bestAdapter: RuntimePlaybackAdapter | PlaybackAdapter;\n effectiveDuration: number;\n docDuration: number;\n clock: StaticSeekPlaybackClock;\n getPlaybackRate: () => number;\n}): PlaybackAdapter {\n const { cache, warned, bestAdapter, effectiveDuration, docDuration } = opts;\n const cached = cache.current;\n if (cached?.player === bestAdapter && cached.duration === effectiveDuration) {\n return cached.adapter;\n }\n cached?.adapter.pause();\n if (!warned.current) {\n warned.current = true;\n console.warn(\n `[useTimelinePlayer] Selected adapter duration (${getAdapterDuration(bestAdapter)}s) does not cover the document duration (${docDuration}s); falling back to seek-driven playback, which never starts media elements or WebAudio. Audio will not play in preview — extend the GSAP timeline to cover the declared data-duration.`,\n );\n }\n const adapter = createStaticSeekPlaybackAdapter(\n bestAdapter,\n effectiveDuration,\n opts.clock,\n opts.getPlaybackRate,\n );\n cache.current = { player: bestAdapter, duration: effectiveDuration, adapter };\n return adapter;\n}\n\n// ---------------------------------------------------------------------------\n// GSAP timeline wrapper\n// ---------------------------------------------------------------------------\n\nexport function wrapTimeline(tl: TimelineLike): PlaybackAdapter {\n return {\n play: () => tl.play(),\n pause: () => tl.pause(),\n seek: (t, options) => {\n const shouldPause = !options?.keepPlaying;\n if (shouldPause) tl.pause();\n tl.seek(t);\n if (shouldPause) tl.pause();\n },\n getTime: () => tl.time(),\n getDuration: () => tl.duration(),\n isPlaying: () => tl.isActive(),\n };\n}\n","/**\n * Low-level helpers for building and identifying TimelineElement objects.\n *\n * Covers: duration reading, media-element metadata extraction, selector/key/\n * identity builders, and DOM node lookup. These are\n * intentionally dependency-free (no store, no hooks) so they can be used in\n * both the React hook and test environments.\n */\n\nimport type { TimelineElement } from \"../store/playerStore\";\nimport type { ClipManifestClip } from \"./playbackTypes\";\nimport { isFinitePositive } from \"./playbackAdapter\";\nimport { getSourceScopedSelectorIndex } from \"../../utils/sourceScopedSelectorIndex\";\nimport { HF_AUDIO_GROUP_TAG } from \"@hyperframes/core/audio-groups\";\nimport { readElementFades } from \"@hyperframes/core/audio-fade\";\nimport { elementVolume } from \"./storedVolume\";\nimport {\n type AttrReader,\n clampPlaybackRate,\n readMediaOffsetSeconds,\n} from \"@hyperframes/parsers/media-duration\";\n\n// ---------------------------------------------------------------------------\n// Layer-reveal lift transparency\n// ---------------------------------------------------------------------------\n\n/**\n * Attributes carrying the pre-lift state of a Layers-panel selection reveal\n * (useLayerRevealOverride): while a layer is selected it PAINTS on top via a\n * temporary inline z-index, but the lift is a purely visual, ephemeral studio\n * affordance — every z reader must keep reporting the element's TRUE z\n * (stored here) so menus, badges, the lane mirror, and the panel sort never\n * reason on the lifted value. A z-reorder commit removes the attributes (the\n * commit is the new truth).\n */\nexport const LAYER_REVEAL_PRIOR_Z_ATTR = \"data-hf-reveal-prior-z\";\nexport const LAYER_REVEAL_PRIOR_POSITION_ATTR = \"data-hf-reveal-prior-pos\";\n\n/** The lifted element's true (pre-lift) z, or null when no lift is active. */\nexport function readLayerRevealPriorZ(el: Element): number | null {\n const raw = el.getAttribute(LAYER_REVEAL_PRIOR_Z_ATTR);\n if (raw == null) return null;\n const n = Number.parseInt(raw, 10);\n return Number.isFinite(n) ? n : null;\n}\n\n// ---------------------------------------------------------------------------\n// Duration attribute helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Read a host element's effective CSS stacking order for the timeline's reverse\n * z→lane mapping. Prefers the inline `style.zIndex` (what the canvas context\n * menu and LayersPanel z-edits write via handleDomZIndexReorderCommit), falls\n * back to computed style; \"auto\" / empty / unparseable ⇒ 0. Works with a\n * detached parse Document (no defaultView) as well as a live iframe. Mirrors\n * canvasContextMenuZOrder.parseZIndex semantics so the two directions agree.\n * Reveal-lift transparent: an active lift reports the stored TRUE z.\n */\nexport function readTimelineElementZIndex(el: Element): number {\n const prior = readLayerRevealPriorZ(el);\n if (prior != null) return prior;\n const html = el as HTMLElement;\n const parseZ = (value: string | null | undefined): number | null => {\n if (value == null || value === \"\" || value === \"auto\") return null;\n const n = Number.parseInt(value, 10);\n return Number.isFinite(n) ? n : null;\n };\n const fromInline = parseZ(html.style?.zIndex);\n if (fromInline != null) return fromInline;\n const view = el.ownerDocument?.defaultView;\n if (view?.getComputedStyle) {\n const fromComputed = parseZ(view.getComputedStyle(html).zIndex);\n if (fromComputed != null) return fromComputed;\n }\n return 0;\n}\n\nfunction readDurationAttribute(el: Element | null | undefined): number {\n if (!el) return 0;\n const duration =\n Number.parseFloat(el.getAttribute(\"data-duration\") ?? \"\") ||\n Number.parseFloat(el.getAttribute(\"data-hf-authored-duration\") ?? \"\");\n return isFinitePositive(duration) ? duration : 0;\n}\n\nexport function isTimelineIgnoredElement(el: Element): boolean {\n // An `<hf-audio-group>` is a mixer bus with no timing of its own, drawn as a group row, never a clip.\n if (el.tagName.toLowerCase() === HF_AUDIO_GROUP_TAG) return true;\n return Boolean(\n el.closest(\n [\n \"[data-hyperframes-ignore]\",\n \"[data-hyperframes-picker-ignore]\",\n \"[data-hf-ignore]\",\n \"[data-hf-color-grading-canvas]\",\n ].join(\",\"),\n ),\n );\n}\n\n/**\n * Furthest clip end (start + RAW `data-duration`) over every non-root clip in the\n * document. Reads the authored attribute, NOT any runtime-computed value — so it\n * is immune to the runtime's clamp that truncates a clip's live duration to the\n * composition length. This is the source of truth for content-driven duration:\n * computing it from the store instead would feed the truncated value back in and\n * make the composition length ratchet down (research HANDOFF-3 §6.1 feedback loop).\n */\nexport function furthestClipEndFromDocument(doc: Document | null | undefined): number {\n if (!doc) return 0;\n const root = doc.querySelector(\"[data-composition-id]\");\n let maxEnd = 0;\n for (const node of Array.from(doc.querySelectorAll(\"[data-start]\"))) {\n if (node === root || isTimelineIgnoredElement(node)) continue;\n const start = Number.parseFloat(node.getAttribute(\"data-start\") ?? \"\");\n const duration = readDurationAttribute(node);\n if (!Number.isFinite(start) || start < 0 || duration <= 0) continue;\n maxEnd = Math.max(maxEnd, start + duration);\n }\n return maxEnd;\n}\n\nexport function readTimelineDurationFromDocument(doc: Document | null | undefined): number {\n if (!doc) return 0;\n const rootDuration = readDurationAttribute(doc.querySelector(\"[data-composition-id]\"));\n if (rootDuration > 0) return rootDuration;\n return furthestClipEndFromDocument(doc);\n}\n\n/**\n * Furthest clip end parsed straight from a composition SOURCE STRING (the HTML\n * being saved). Uses raw `data-duration`, so it is the correct input for syncing\n * the root duration after an edit — reading the store instead would use the\n * runtime-truncated durations and shrink the composition (the feedback loop).\n */\nexport function furthestClipEndFromSource(source: string): number {\n if (!source) return 0;\n return furthestClipEndFromDocument(new DOMParser().parseFromString(source, \"text/html\"));\n}\n\n// ---------------------------------------------------------------------------\n// DOM element type guards\n// ---------------------------------------------------------------------------\n\nfunction isHtmlElement(el: Element): el is HTMLElement {\n const HtmlElementCtor = el.ownerDocument.defaultView?.HTMLElement ?? globalThis.HTMLElement;\n return typeof HtmlElementCtor !== \"undefined\" && el instanceof HtmlElementCtor;\n}\n\nfunction isCompositionHost(el: Element): boolean {\n return (\n el.hasAttribute(\"data-composition-id\") ||\n el.hasAttribute(\"data-composition-src\") ||\n el.hasAttribute(\"data-composition-file\")\n );\n}\n\nexport function resolveMediaElement(el: Element): HTMLMediaElement | HTMLImageElement | null {\n const win = el.ownerDocument.defaultView ?? window;\n const MediaElementCtor = win.HTMLMediaElement ?? globalThis.HTMLMediaElement;\n const ImageElementCtor = win.HTMLImageElement ?? globalThis.HTMLImageElement;\n if (el instanceof MediaElementCtor || el instanceof ImageElementCtor) return el;\n // A composition's media belongs to its own timeline, not the clip's: its length would cap a trim.\n if (isCompositionHost(el)) return null;\n const candidate = el.querySelector(\"video, audio, img\");\n return candidate instanceof MediaElementCtor || candidate instanceof ImageElementCtor\n ? candidate\n : null;\n}\n\n/** The in-point as playback reads it, and the attribute holding it; empty when neither is authored. */\nexport function readPlaybackStartAttributes(\n getAttr: AttrReader,\n): Pick<TimelineElement, \"playbackStart\" | \"playbackStartAttr\"> {\n const playbackStartAttr =\n getAttr(\"data-playback-start\") != null\n ? \"playback-start\"\n : getAttr(\"data-media-start\") != null\n ? \"media-start\"\n : undefined;\n return playbackStartAttr\n ? { playbackStart: readMediaOffsetSeconds(getAttr), playbackStartAttr }\n : {};\n}\n\nexport function playbackStartAttributeForElement(\n element: Pick<TimelineElement, \"kind\" | \"playbackStartAttr\">,\n): \"data-media-start\" | \"data-playback-start\" {\n return element.playbackStartAttr === \"playback-start\" || element.kind === \"composition\"\n ? \"data-playback-start\"\n : \"data-media-start\";\n}\n\nfunction applyPlaybackMetadataFromElement(entry: TimelineElement, el: Element): void {\n Object.assign(\n entry,\n readPlaybackStartAttributes((n) => el.getAttribute(n)),\n );\n\n const authoredPlaybackRate = Number.parseFloat(el.getAttribute(\"data-playback-rate\") ?? \"\");\n if (Number.isFinite(authoredPlaybackRate) && authoredPlaybackRate > 0) {\n entry.playbackRate = clampPlaybackRate(authoredPlaybackRate);\n }\n}\n\n/** Sets or clears an optional field, so a re-parse after the attribute is removed drops it. */\nfunction setOptional<K extends keyof TimelineElement>(\n entry: TimelineElement,\n key: K,\n value: TimelineElement[K] | undefined,\n): void {\n if (value === undefined) delete entry[key];\n else entry[key] = value;\n}\n\n/** The compiler's rule (timingCompiler): explicit data-has-audio wins; otherwise an unmuted <video> is audible. */\nexport function isVideoAudible(opts: {\n tag: string;\n hasAudioAttr: string | null | undefined;\n muted: boolean;\n}): boolean {\n if (opts.hasAudioAttr === \"true\") return true;\n if (opts.hasAudioAttr === \"false\" || opts.hasAudioAttr === \"\") return false;\n return opts.tag.toLowerCase() === \"video\" && !opts.muted;\n}\n\nexport function isAudibleVideoNode(el: Element): boolean {\n if (el.tagName.toLowerCase() !== \"video\" || el.hasAttribute(\"muted\")) return false;\n return isVideoAudible({\n tag: \"video\",\n hasAudioAttr: el.getAttribute(\"data-has-audio\"),\n muted: false,\n });\n}\n\n/** What the mixer gets: the compiler's `data-has-audio` rule, muted and volume. */\nfunction applyAudioMetadataFromElement(entry: TimelineElement, el: Element): void {\n const media = resolveMediaElement(el) ?? el;\n const muted = el.hasAttribute(\"muted\") || media.hasAttribute(\"muted\");\n const sound = isVideoAudible({\n tag: el.tagName,\n hasAudioAttr: el.getAttribute(\"data-has-audio\"),\n muted,\n });\n setOptional(entry, \"hasAudio\", sound ? true : undefined);\n setOptional(entry, \"muted\", muted ? true : undefined);\n setOptional(entry, \"volume\", elementVolume(el, media));\n}\n\nfunction applyFadeMetadataFromElement(entry: TimelineElement, el: Element): void {\n const fades = readElementFades(el);\n setOptional(entry, \"fadeIn\", fades.fadeIn > 0 ? fades.fadeIn : undefined);\n setOptional(entry, \"fadeOut\", fades.fadeOut > 0 ? fades.fadeOut : undefined);\n}\n\nexport function applyMediaMetadataFromElement(entry: TimelineElement, el: Element): void {\n applyPlaybackMetadataFromElement(entry, el);\n applyAudioMetadataFromElement(entry, el);\n applyFadeMetadataFromElement(entry, el);\n\n const mediaEl = resolveMediaElement(el);\n if (!mediaEl) return;\n\n entry.tag = mediaEl.tagName.toLowerCase();\n const src = mediaEl.getAttribute(\"src\");\n if (src) entry.src = src;\n\n const win = mediaEl.ownerDocument.defaultView ?? window;\n const MediaElementCtor = win.HTMLMediaElement ?? globalThis.HTMLMediaElement;\n if (typeof MediaElementCtor === \"undefined\" || !(mediaEl instanceof MediaElementCtor)) return;\n\n const sourceDurationAttr =\n el.getAttribute(\"data-source-duration\") ?? mediaEl.getAttribute(\"data-source-duration\");\n const sourceDuration = sourceDurationAttr ? parseFloat(sourceDurationAttr) : mediaEl.duration;\n if (Number.isFinite(sourceDuration) && sourceDuration > 0) {\n entry.sourceDuration = sourceDuration;\n }\n\n const playbackRate = mediaEl.defaultPlaybackRate;\n if (entry.playbackRate == null && Number.isFinite(playbackRate) && playbackRate > 0) {\n entry.playbackRate = clampPlaybackRate(playbackRate);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Label helpers\n// ---------------------------------------------------------------------------\n\nexport function getTimelineElementDisplayLabel(input: {\n id?: string | null;\n label?: string | null;\n tag?: string | null;\n}): string {\n const label = input.label?.trim();\n if (label) return label;\n const id = input.id?.trim();\n if (id) return id;\n const tag = input.tag?.trim().toLowerCase();\n return tag ? `${tag} clip` : \"Timeline clip\";\n}\n\n// ---------------------------------------------------------------------------\n// Selector / identity / key builders\n// ---------------------------------------------------------------------------\n\nexport function getTimelineElementSelector(el: Element): string | undefined {\n if (isHtmlElement(el) && el.id) return `#${CSS.escape(el.id)}`;\n const compId = el.getAttribute(\"data-composition-id\");\n if (compId) return `[data-composition-id=\"${CSS.escape(compId)}\"]`;\n if (isHtmlElement(el)) {\n const classes = el.className.split(/\\s+/).filter(Boolean);\n const firstClass = classes.find((className) => className !== \"clip\") ?? classes[0];\n if (firstClass) return `.${CSS.escape(firstClass)}`;\n }\n return undefined;\n}\n\nexport function getTimelineElementSourceFile(el: Element): string | undefined {\n const ownerRoot = el.parentElement?.closest(\"[data-composition-id]\");\n return (\n ownerRoot?.getAttribute(\"data-composition-file\") ??\n ownerRoot?.getAttribute(\"data-composition-src\") ??\n undefined\n );\n}\n\nexport function getTimelineElementSelectorIndex(\n doc: Document,\n el: Element,\n selector: string | undefined,\n): number | undefined {\n return getSourceScopedSelectorIndex(\n doc,\n el,\n selector,\n getTimelineElementSourceFile(el),\n getTimelineElementSourceFile,\n );\n}\n\nexport function buildTimelineElementKey(params: {\n id: string;\n fallbackIndex: number;\n domId?: string;\n selector?: string;\n selectorIndex?: number;\n sourceFile?: string;\n}): string {\n const scope = params.sourceFile ?? \"index.html\";\n if (params.domId) return `${scope}#${params.domId}`;\n if (params.selector) return `${scope}:${params.selector}:${params.selectorIndex ?? 0}`;\n return `${scope}:${params.id}:${params.fallbackIndex}`;\n}\n\n/**\n * Inverse of {@link buildTimelineElementKey} for the `sourceFile#domId` form.\n * A key with no `#` is a bare dom id and carries no source file of its own, so\n * the caller supplies the scope it wants to look that id up in.\n */\nexport function splitTimelineElementKey(key: string): {\n sourceFile: string | null;\n domId: string;\n} {\n const hashIndex = key.lastIndexOf(\"#\");\n if (hashIndex < 0) return { sourceFile: null, domId: key };\n return { sourceFile: key.slice(0, hashIndex), domId: key.slice(hashIndex + 1) };\n}\n\nexport function buildTimelineElementIdentity(params: {\n preferredId?: string | null;\n label: string;\n fallbackIndex: number;\n domId?: string;\n selector?: string;\n selectorIndex?: number;\n sourceFile?: string;\n}): { id: string; key: string } {\n const id =\n params.preferredId?.trim() ||\n buildTimelineElementKey({\n id: params.label,\n fallbackIndex: params.fallbackIndex,\n domId: params.domId,\n selector: params.selector,\n selectorIndex: params.selectorIndex,\n sourceFile: params.sourceFile,\n });\n const key = buildTimelineElementKey({\n id,\n fallbackIndex: params.fallbackIndex,\n domId: params.domId,\n selector: params.selector,\n selectorIndex: params.selectorIndex,\n sourceFile: params.sourceFile,\n });\n return { id, key };\n}\n\nexport function getTimelineElementIdentity(element: { key?: string | null; id: string }): string {\n return element.key ?? element.id;\n}\n\n/**\n * The id space the RUNTIME matches on — a bare DOM id, never a store key.\n *\n * Studio addresses rows by `buildTimelineElementKey`'s composite\n * `<sourceFile>#<domId>`, but everything audio in `@hyperframes/core` keys off\n * the live document: `resolveAudioGroups` collects `member.id`,\n * `resolveCarveSourceIds` goes through `getElementById`. Anything crossing into\n * that space — a group membership list, a carve source — has to be\n * converted here first; a composite key silently matches nothing.\n *\n * `null` for a row with no DOM id at all (selector-addressed elements): such an\n * element cannot be grouped, because `resolveAudioGroups` skips\n * members without an `id` and would build a group that is half there.\n */\nexport function runtimeAudioId(element: { domId?: string | null }): string | null {\n return element.domId || null;\n}\n\n/**\n * Timeline store key for a z-reorder entry built OUTSIDE the timeline\n * expansion (canvas context menu / LayersPanel), so the reorder commit can\n * update the store's zIndex synchronously. Matches buildTimelineElementKey's\n * stable branches (`sourceFile#domId`, else the selector-based key). Undefined\n * when the element has neither a DOM id nor a selector — the timeline's\n * fallback branch needs its own fallbackIndex, which these callers don't have,\n * so such an entry simply skips the synchronous store update.\n */\nexport function deriveTimelineStoreKey(params: {\n domId?: string;\n selector?: string;\n selectorIndex?: number;\n sourceFile?: string;\n}): string | undefined {\n if (!params.domId && !params.selector) return undefined;\n return buildTimelineElementKey({ id: \"\", fallbackIndex: 0, ...params });\n}\n\n/**\n * {@link deriveTimelineStoreKey} for a caller that already has a DOM id in\n * hand (e.g. one it just minted), so the undefined branch never applies.\n */\nexport function deriveTimelineStoreKeyForDomId(domId: string, sourceFile?: string): string {\n return deriveTimelineStoreKey({ domId, sourceFile })!;\n}\n\n// ---------------------------------------------------------------------------\n// DOM node querying\n// ---------------------------------------------------------------------------\n\nfunction getTimelineDomNodes(doc: Document): Element[] {\n const rootComp = doc.querySelector(\"[data-composition-id]\");\n return Array.from(doc.querySelectorAll(\"[data-start]\")).filter(\n (node) => node !== rootComp && !isTimelineIgnoredElement(node),\n );\n}\n\nfunction numbersNearlyEqual(a: number, b: number): boolean {\n return Math.abs(a - b) < 0.001;\n}\n\nconst MANIFEST_CLIP_ATTRS: ReadonlyArray<[string, (clip: ClipManifestClip) => number]> = [\n [\"data-start\", (clip) => clip.start],\n [\"data-duration\", (clip) => clip.duration],\n [\"data-track-index\", (clip) => clip.track],\n];\n\nfunction nodeMatchesClipTag(node: Element, clip: ClipManifestClip): boolean {\n return !clip.tagName || node.tagName.toLowerCase() === clip.tagName.toLowerCase();\n}\n\nfunction nodeMatchesManifestClip(node: Element, clip: ClipManifestClip): boolean {\n if (!nodeMatchesClipTag(node, clip)) return false;\n // An attribute only constrains the match when it parses to a finite number:\n // missing or garbled reads as \"unknown\", not \"mismatch\".\n return MANIFEST_CLIP_ATTRS.every(([attr, expected]) => {\n const actual = Number.parseFloat(node.getAttribute(attr) ?? \"\");\n return !Number.isFinite(actual) || numbersNearlyEqual(actual, expected(clip));\n });\n}\n\n/** Whether `node` sits in the composition the clip was read from, the chain the runtime records outermost first\n * (`resolveNearestCompositionContext` in core's runtime/timeline.ts). A clip without that scope accepts any node. */\nfunction nodeInClipScope(node: Element, clip: ClipManifestClip): boolean {\n const scope = clip.compositionAncestors;\n if (!scope) return true;\n const ids: string[] = [];\n for (let cursor = node.parentElement; cursor; cursor = cursor.parentElement) {\n const id = cursor.getAttribute(\"data-composition-id\");\n if (id) ids.unshift(id);\n }\n return ids.length === scope.length && ids.every((id, index) => id === scope[index]);\n}\n\n/** The first match in the clip's composition across `selectors`; a lone match stands only when none is in scope, as a\n * healed host can stale the clip's chain for a pass. An id can repeat in a sub-composition earlier in the document. */\nfunction findInClipScope(\n doc: Document,\n clip: ClipManifestClip,\n selectors: string[],\n): Element | null {\n let lone: Element | null = null;\n for (const selector of selectors) {\n const nodes = Array.from(doc.querySelectorAll(selector));\n const scoped = nodes.find((node) => nodeInClipScope(node, clip));\n if (scoped) return scoped;\n if (nodes.length === 1) lone ??= nodes[0];\n }\n return lone;\n}\n\nexport type PreviewTarget = Pick<TimelineElement, \"hfId\" | \"domId\" | \"id\" | \"sourceFile\">;\n\n/** Finds a row's preview element by `data-hf-id`, then id, preferring one in the row's own file: both repeat across\n * files. Indexes the document once, so a pass over every row costs one scan. */\nexport function previewElementFinder(\n doc: Document,\n selector = \"[data-hf-id], [id]\",\n): (target: PreviewTarget) => Element | null {\n const byKey = new Map<string, Element[]>();\n const add = (key: string, node: Element) => {\n const nodes = byKey.get(key);\n if (nodes) nodes.push(node);\n else byKey.set(key, [node]);\n };\n for (const node of doc.querySelectorAll(selector)) {\n const hfId = node.getAttribute(\"data-hf-id\");\n const id = node.getAttribute(\"id\");\n if (hfId) add(`hf:${hfId}`, node);\n if (id) add(`id:${id}`, node);\n }\n return (target) => {\n const matches = [\n ...((target.hfId && byKey.get(`hf:${target.hfId}`)) || []),\n ...(byKey.get(`id:${target.domId ?? target.id}`) ?? []),\n ];\n return (\n matches.find((node) => getTimelineElementSourceFile(node) === target.sourceFile) ??\n matches[0] ??\n null\n );\n };\n}\n\nexport function findClipElementById(doc: Document, clip: ClipManifestClip): Element | null {\n if (!clip.id) return null;\n const first = doc.getElementById(clip.id);\n if (!first || nodeInClipScope(first, clip)) return first;\n return findInClipScope(doc, clip, [`[id=\"${CSS.escape(clip.id)}\"]`]);\n}\n\nfunction findTimelineDomNode(doc: Document, clip: ClipManifestClip): Element | null {\n if (!clip.id) return null;\n const first = doc.getElementById(clip.id);\n if (first && nodeInClipScope(first, clip)) return first;\n const id = CSS.escape(clip.id);\n const byOtherKeys = [`[data-hf-id=\"${id}\"]`, `[data-composition-id=\"${id}\"]`, `.${id}`];\n return findInClipScope(doc, clip, first ? [`[id=\"${id}\"]`, ...byOtherKeys] : byOtherKeys);\n}\n\nexport function findTimelineDomNodeForClip(\n doc: Document,\n clip: ClipManifestClip,\n fallbackIndex: number,\n usedNodes = new Set<Element>(),\n getCandidates = () => getTimelineDomNodes(doc),\n): Element | null {\n const byIdentity = findTimelineDomNode(doc, clip);\n if (byIdentity && !usedNodes.has(byIdentity) && nodeMatchesClipTag(byIdentity, clip))\n return byIdentity;\n\n const candidates = getCandidates().filter((node) => !usedNodes.has(node));\n const exact = candidates.find((node) => nodeMatchesManifestClip(node, clip));\n if (exact) return exact;\n\n const positional = candidates[fallbackIndex];\n return positional && nodeMatchesClipTag(positional, clip) ? positional : null;\n}\n\n/** One synchronous hydration pass: snapshot only on a miss, never across reloads. */\nexport function createTimelineDomNodeResolver(doc: Document) {\n let candidates: Element[] | undefined;\n const usedNodes = new Set<Element>();\n const getCandidates = () => (candidates ??= getTimelineDomNodes(doc));\n return (clip: ClipManifestClip, index: number): Element | null => {\n const node = findTimelineDomNodeForClip(doc, clip, index, usedNodes, getCandidates);\n if (node) usedNodes.add(node);\n return node;\n };\n}\n","export function parseStoredVolume(text: string | null): number | undefined {\n const volume = Number.parseFloat(text ?? \"\");\n return Number.isFinite(volume) ? volume : undefined;\n}\n\nexport const elementVolume = (el: Element, media: Element) =>\n parseStoredVolume(el.getAttribute(\"data-volume\") ?? media.getAttribute(\"data-volume\"));\n","const PROJECT_HASH_PREFIX = \"#project/\";\n\nexport interface ProjectHashRoute {\n projectId: string;\n params: URLSearchParams;\n}\n\nconst WINDOWS_DRIVE_PREFIX = /^[a-z]:/i;\n\n/** Project names are single path segments, including when received from a hash or server. */\nexport function isValidProjectId(value: string): boolean {\n return (\n value.length > 0 &&\n value !== \".\" &&\n value !== \"..\" &&\n !WINDOWS_DRIVE_PREFIX.test(value) &&\n !value.includes(\"/\") &&\n !value.includes(\"\\\\\") &&\n !Array.from(value).some((char) => char.charCodeAt(0) < 32)\n );\n}\n\nfunction decodeHashProjectId(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\nfunction normalizeHashParams(\n params?: URLSearchParams | Record<string, string | null | undefined>,\n): URLSearchParams {\n if (!params) return new URLSearchParams();\n if (params instanceof URLSearchParams) return params;\n\n const next = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (!key || value == null || value === \"\") continue;\n next.set(key, value);\n }\n return next;\n}\n\nexport function encodeProjectId(projectId: string): string {\n if (!isValidProjectId(projectId)) throw new Error(\"Invalid project ID\");\n return encodeURIComponent(projectId);\n}\n\nexport function buildProjectHash(\n projectId: string,\n params?: URLSearchParams | Record<string, string | null | undefined>,\n): string {\n const search = normalizeHashParams(params).toString();\n return `${PROJECT_HASH_PREFIX}${encodeProjectId(projectId)}${search ? `?${search}` : \"\"}`;\n}\n\nexport function parseProjectHashRoute(hash: string): ProjectHashRoute | null {\n if (!hash.startsWith(PROJECT_HASH_PREFIX)) return null;\n\n const route = hash.slice(PROJECT_HASH_PREFIX.length);\n const queryIndex = route.indexOf(\"?\");\n const encodedProjectId = queryIndex >= 0 ? route.slice(0, queryIndex) : route;\n if (!encodedProjectId || encodedProjectId.includes(\"/\")) return null;\n\n const projectId = decodeHashProjectId(encodedProjectId);\n if (!isValidProjectId(projectId)) return null;\n\n const rawParams = queryIndex >= 0 ? route.slice(queryIndex + 1) : \"\";\n return {\n projectId,\n params: new URLSearchParams(rawParams),\n };\n}\n\nexport function parseProjectIdFromHash(hash: string): string | null {\n return parseProjectHashRoute(hash)?.projectId ?? null;\n}\n\nexport function buildProjectApiPath(projectId: string, suffix = \"\"): string {\n const normalizedSuffix = suffix && !suffix.startsWith(\"/\") ? `/${suffix}` : suffix;\n return `/api/projects/${encodeProjectId(projectId)}${normalizedSuffix}`;\n}\n"],"mappings":"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","names":[]}
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  buildProjectApiPath,
6
6
  splitTimelineElementKey
7
- } from "./chunk-6LUNLTHR.js";
7
+ } from "./chunk-K47E2ZJA.js";
8
8
 
9
9
  // src/player/lib/time.ts
10
10
  var STUDIO_PREVIEW_FPS = 30;
@@ -1406,4 +1406,4 @@ export {
1406
1406
  whenPreviewBooted,
1407
1407
  Player
1408
1408
  };
1409
- //# sourceMappingURL=chunk-QUXZVHOJ.js.map
1409
+ //# sourceMappingURL=chunk-VWT7STY3.js.map
@@ -20,8 +20,8 @@ import {
20
20
  resolveDomEditCapabilities,
21
21
  resolveDomEditSelection,
22
22
  serializeDomEditTextFields
23
- } from "./chunk-PQFEBX32.js";
24
- import "./chunk-6LUNLTHR.js";
23
+ } from "./chunk-BIUXP3PG.js";
24
+ import "./chunk-K47E2ZJA.js";
25
25
  import "./chunk-PZ5AY32C.js";
26
26
  export {
27
27
  buildDefaultDomEditTextField,
@@ -46,4 +46,4 @@ export {
46
46
  resolveDomEditSelection,
47
47
  serializeDomEditTextFields
48
48
  };
49
- //# sourceMappingURL=domEditingLayers-JQPQGGT7.js.map
49
+ //# sourceMappingURL=domEditingLayers-V67MS4WP.js.map
package/dist/index.d.ts CHANGED
@@ -993,6 +993,7 @@ interface TimelineGroupMoveChange {
993
993
  element: TimelineElement;
994
994
  start: number;
995
995
  track?: number;
996
+ audioGroup?: null;
996
997
  }
997
998
  interface TimelineGroupResizeChange {
998
999
  element: TimelineElement;
@@ -1016,6 +1017,9 @@ interface TimelineGroupCommitOptions {
1016
1017
  suppressFailureToast?: boolean;
1017
1018
  }
1018
1019
 
1020
+ type TimelineAtomicMoveUpdates = Pick<TimelineElement, "start" | "track"> & {
1021
+ audioGroup?: null;
1022
+ };
1019
1023
  type TimelineMoveOperation = "timing" | "lane-reorder" | "track-insert";
1020
1024
 
1021
1025
  interface TimelineKeyframeTarget {
@@ -1084,7 +1088,7 @@ type TimelineLinkEdit = {
1084
1088
  mediaStart: number;
1085
1089
  };
1086
1090
  interface TimelineEditCallbacks {
1087
- onMoveElement?: (element: TimelineElement, updates: Pick<TimelineElement, "start" | "track">) => Promise<void> | void;
1091
+ onMoveElement?: (element: TimelineElement, updates: TimelineAtomicMoveUpdates) => Promise<void> | void;
1088
1092
  /** Atomic multi-clip move (single undo) for main-track ripple + track-insert.
1089
1093
  * `coalesceKey` (drag-commit gesture id) merges the move history entry with a
1090
1094
  * lane change's follow-up z-reorder entry into one undo step; `coalesceMs`
@@ -1092,7 +1096,7 @@ interface TimelineEditCallbacks {
1092
1096
  * gesture's records (per-gesture-unique keys keep the fold gesture-scoped). */
1093
1097
  onMoveElements?: (edits: Array<{
1094
1098
  element: TimelineElement;
1095
- updates: Pick<TimelineElement, "start" | "track">;
1099
+ updates: TimelineAtomicMoveUpdates;
1096
1100
  }>, coalesceKey?: string, operation?: TimelineMoveOperation, coalesceMs?: number) => Promise<void> | void;
1097
1101
  onResizeElement?: (element: TimelineElement, updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">) => Promise<void> | void;
1098
1102
  onResizeElements?: (changes: Array<{
@@ -1307,8 +1311,8 @@ interface TimelineSnapTarget {
1307
1311
  }
1308
1312
 
1309
1313
  interface DraggedClipState {
1310
- /** Pointer captured by the stable viewport coordinator for this gesture. */
1311
- pointerId: number;
1314
+ /** Pointer captured by the viewport coordinator; null for a keyboard pickup. */
1315
+ pointerId: number | null;
1312
1316
  element: TimelineElement;
1313
1317
  originClientX: number;
1314
1318
  originClientY: number;
@@ -1329,7 +1333,7 @@ interface DraggedClipState {
1329
1333
  /**
1330
1334
  * When non-null, the drop inserts a NEW track at this visual row boundary
1331
1335
  * (0 = above the top lane, trackOrder.length = below the bottom) instead of
1332
- * landing on previewTrack. Drives the insertion-line indicator.
1336
+ * landing on previewTrack. Drives the opened preview lane.
1333
1337
  */
1334
1338
  insertRow: number | null;
1335
1339
  /** Snap target the clip will land on, for the guide highlight. */
@@ -1379,6 +1383,46 @@ interface BlockedClipState {
1379
1383
 
1380
1384
  type TrackVisualStyle = TimelineTrackStyle;
1381
1385
 
1386
+ /** One resolved audio group, positioned in the row order. */
1387
+ interface TimelineTrackGroupInfo {
1388
+ id: string;
1389
+ label: string;
1390
+ /**
1391
+ * Synthetic sort key for the group's own row — the same fractional-key
1392
+ * convention sub-composition expansion already uses (see
1393
+ * timelineTrackDisplay.ts): the first (lowest) member track's number minus
1394
+ * 0.5, so it slots in immediately above that member.
1395
+ */
1396
+ anchorKey: number;
1397
+ /** Member track numbers, ascending. */
1398
+ memberTracks: number[];
1399
+ /**
1400
+ * Every clip under this group, in member-track order — INDEPENDENT of whether
1401
+ * the group is expanded.
1402
+ *
1403
+ * Collapsing a group stops emitting its member rows into `tracks`, so anything
1404
+ * that recovered member elements by looking them up there got an empty list in
1405
+ * the default (collapsed) state — silently disabling the
1406
+ * automation-lane count, and the bus strip's member labels. Membership is not
1407
+ * a display concern, so it does not travel through the display list.
1408
+ */
1409
+ memberElements: TimelineElement[];
1410
+ /** The group element's `data-volume`, mirrored from a member's parse (B7's slider). */
1411
+ volume: number;
1412
+ /** The group element's `data-hidden`, mirrored from a member's parse (B5's group mute). */
1413
+ hidden: boolean;
1414
+ /** The group element's serialized `data-fx-chain`, mirrored from a member's parse (C1's FX entry). */
1415
+ fxChain?: string;
1416
+ /** The group element's serialized `data-automation` — the group's OWN lanes,
1417
+ * which are what its `∿` discloses (groups doc §5). */
1418
+ automation?: string;
1419
+ }
1420
+ /**
1421
+ * The track number of each timeline row, top to bottom: pass the player store's `elements` and
1422
+ * `collapsedGroupIds`. A group's anchor row is its first member's track minus 0.5.
1423
+ */
1424
+ declare function displayTrackOrder(elements: readonly TimelineElement[], collapsedGroupIds: ReadonlySet<string>): number[];
1425
+
1382
1426
  interface TimelineVirtualRow {
1383
1427
  readonly index: number;
1384
1428
  readonly rowKey: number;
@@ -1417,41 +1461,6 @@ interface MultiDragPreviewInput {
1417
1461
  selectedKeys: ReadonlySet<string>;
1418
1462
  }
1419
1463
 
1420
- /** One resolved audio group, positioned in the row order. */
1421
- interface TimelineTrackGroupInfo {
1422
- id: string;
1423
- label: string;
1424
- /**
1425
- * Synthetic sort key for the group's own row — the same fractional-key
1426
- * convention sub-composition expansion already uses (see
1427
- * timelineTrackDisplay.ts): the first (lowest) member track's number minus
1428
- * 0.5, so it slots in immediately above that member.
1429
- */
1430
- anchorKey: number;
1431
- /** Member track numbers, ascending. */
1432
- memberTracks: number[];
1433
- /**
1434
- * Every clip under this group, in member-track order — INDEPENDENT of whether
1435
- * the group is expanded.
1436
- *
1437
- * Collapsing a group stops emitting its member rows into `tracks`, so anything
1438
- * that recovered member elements by looking them up there got an empty list in
1439
- * the default (collapsed) state — silently disabling the
1440
- * automation-lane count, and the bus strip's member labels. Membership is not
1441
- * a display concern, so it does not travel through the display list.
1442
- */
1443
- memberElements: TimelineElement[];
1444
- /** The group element's `data-volume`, mirrored from a member's parse (B7's slider). */
1445
- volume: number;
1446
- /** The group element's `data-hidden`, mirrored from a member's parse (B5's group mute). */
1447
- hidden: boolean;
1448
- /** The group element's serialized `data-fx-chain`, mirrored from a member's parse (C1's FX entry). */
1449
- fxChain?: string;
1450
- /** The group element's serialized `data-automation` — the group's OWN lanes,
1451
- * which are what its `∿` discloses (groups doc §5). */
1452
- automation?: string;
1453
- }
1454
-
1455
1464
  interface TimelineLogicalItem {
1456
1465
  id: string;
1457
1466
  kind: "clip" | "keyframe" | "ease";
@@ -1514,11 +1523,8 @@ interface TimelineLaneBaseProps {
1514
1523
  blockedClipRef: React.RefObject<BlockedClipState | null>;
1515
1524
  suppressClickRef: React.RefObject<boolean>;
1516
1525
  scrollRef: React.RefObject<HTMLDivElement | null>;
1517
- renderClipContent?: (element: TimelineElement, style: {
1518
- clip: string;
1519
- label: string;
1520
- }, context: TimelineClipRenderContext) => ReactNode;
1521
- renderClipOverlay?: (element: TimelineElement) => ReactNode;
1526
+ renderClipContent?: TimelineProps["renderClipContent"];
1527
+ renderClipOverlay?: TimelineProps["renderClipOverlay"];
1522
1528
  onDrillDown?: (element: TimelineElement) => void;
1523
1529
  onSelectElement?: (element: TimelineElement | null) => void;
1524
1530
  setHoveredClip: (key: string | null) => void;
@@ -2150,6 +2156,34 @@ declare const useAudioGainDialogStore: zustand.UseBoundStore<zustand.StoreApi<Au
2150
2156
  /** Open Audio Gain for the selection (or the clicked clip); false when none of it has sound. */
2151
2157
  declare function openAudioGainDialog(clicked?: TimelineElement): boolean;
2152
2158
 
2159
+ type FadeEdge = "in" | "out";
2160
+ /** The clip as drawn; `toolsLeft` is where its FX badge starts, null when it has none. */
2161
+ interface FadeHandleClipBox {
2162
+ width: number;
2163
+ height: number;
2164
+ radius: number;
2165
+ toolsLeft: number | null;
2166
+ }
2167
+ /** A fade handle's hit box in px from the clip's top left; `tabLeft` is its tab's centre within it. */
2168
+ interface FadeHandleBox {
2169
+ left: number;
2170
+ width: number;
2171
+ height: number;
2172
+ tabLeft: number;
2173
+ top: number;
2174
+ }
2175
+ /**
2176
+ * Where the fade handles sit on a clip `widthPx` wide: each centred on its fade's end, inside the
2177
+ * clip and left of its tools; two drawn handles that would overlap split at the midpoint.
2178
+ */
2179
+ declare function fadeHandleBoxes(input: {
2180
+ widthPx: number;
2181
+ inPx: number;
2182
+ outPx: number;
2183
+ clipBox: FadeHandleClipBox;
2184
+ drawn: Record<FadeEdge, boolean>;
2185
+ }): Record<FadeEdge, FadeHandleBox>;
2186
+
2153
2187
  interface AudioWaveformProps {
2154
2188
  audioUrl: string;
2155
2189
  waveformUrl?: string;
@@ -4061,4 +4095,4 @@ declare function buildStudioTools(depsRef: {
4061
4095
  readonly current: StudioAgentToolsDeps;
4062
4096
  }): ModelContextTool[];
4063
4097
 
4064
- export { type AgentModalAnchorPoint, type AgentPromptElementInfo, AskAgentModal, type AskAgentModalProps, AudioMeterStrip, type AudioMeterStripProps, AudioWaveform, type AudioWaveformProps, type BlockedTimelineEditIntent, type CanEditTimelineElement, CompositionBreadcrumb, type CompositionDimensions, type CompositionLevel, CompositionThumbnail, ConnectedDomEditOverlay, type ConnectedDomEditOverlayProps, DEFAULT_SHORTCUT_SECTIONS, Dock, type DockController, type DomEditCapabilities, type DomEditCommitDeclineReason, type DomEditCommitOutcome, type DomEditGroupPathOffsetCommit, DomEditOverlay, type DomEditOverlayProps, DomEditProvider, type DomEditSelection, type DomEditZOrder, type DomGeometryCommits, type EditHistoryHandle, EditorShell, type EditorShellProps, type ExternalFileChangeBlockedState, type ExternalFileChangeCoordinatorHandle, ExternalFileConflictBanner, type FfmpegStatus, FileTree, GridOverlay, ImageThumbnail, type ImageThumbnailProps, type MarqueeGestures, type MarqueeGesturesDeps, MarqueeOverlay, type MarqueeOverlayProps, type Rect$1 as MarqueeRect, type ModelContextTool, NLEPreview, type PanelId, type PatchOperation, type PersistedRenderSettings, type PickedElement, PlayerControls, type PlayerHandle, type PlayerHandleElement, type PlayerHandleListener, type PlayerHandleTimeListener, type DomEditCompositionRect as PreviewCompositionRect, PreviewGuides, PreviewOverlayProvider, type PreviewOverlayProviderProps, type PreviewPromotion, PreviewReadOnlyProvider, type PreviewSnapPreferences, PropertyPanel, type RenderJob, RenderQueue, type RenderQueueProps, type ResolutionPreset, SNAP_THRESHOLD_PX, type ShortcutHint, type ShortcutSection, ShortcutsButton, type ShortcutsButtonProps, type SnapContext, type SnapEdge, type SnapGuide, SnapGuideOverlay, type SnapGuideOverlayProps, type SnapGuidesState, type Rect as SnapRect, type SnapResult, type SnapTarget, SnapToolbar, SourceEditor, type SpacingGuide, type StartRenderOptions, type StudioAgentToolsDeps, StudioApp, StudioFileConflictError, type StudioLookSnapshot, type StudioPendingEditsDrainResult, TRACK_H, type ThumbnailPriority, Timeline, TimelineClipMenu, type TimelineClipMenuItem, type TimelineClipRenderContext, type TimelineEditCallbacks, type TimelineEditOutcome, type TimelineEditPermission, TimelineEditPopover, TimelineEditProvider, type TimelineElement, TimelineEmptyStatePart, TimelineFrame, TimelineGapMenu, TimelineHistoryButtons, type TimelineHistoryButtonsProps, TimelineKeyframeMenu, TimelineLanes, TimelineOverlays, TimelinePane, type TimelinePaneProps, TimelinePlayhead, TimelineProvider, TimelineRazorGuide, TimelineRuler, TimelineShortcutHint, type TimelineTheme, type TimelineTimeRange$1 as TimelineTimeRange, TimelineToolbar, type TimelineToolbarProps, type TimelineTrackPadding, type TimelineZIndexReorderCommit, type ToolResult, type UseClipboardOptions, type UseDomEditSessionParams, type UseDomGeometryCommitOptions, type UseDomStyleCommitOptions, type UseEditHistoryActionsOptions, type UsePersistentEditHistoryOptions, type UsePreviewPersistenceParams, type UseProjectFileWriterOptions, type UseRenderClipContentOptions, type UseTimelineEditingOptions, VideoThumbnail, type ZOrderAction, applyPatch, buildCompositionSnapTarget, buildGridSnapEdges, buildPickerAgentContextPreview, buildPickerAgentPrompt, buildStudioTools, collectSnapContext, collectStudioLookScene, extractSnapTargets, findElementBlock, flushStudioPendingEdits, formatTime, getPersistedRenderSettings, liveTime, mergeStyleIntoTag, openAudioGainDialog, parseStyleString, persistRenderSettings, resolveDomEditSelection, resolveEquidistanceGuides, resolveGuideLineRect, resolveIframe, resolveSnapAdjustment, resolveSourceFile, revertNewestStudioPendingEdit, snapEngagedForTravel, useAudioGainDialogStore, useAudioMetersVisible, useClipboard, useCompositionStack, useDockLayoutStore, useDomEditSelectionContext, useDomEditSession, useDomEditZOrder, useDomGeometryCommit, useDomStyleCommit, useEditHistoryActions, useElementPicker, useLivePreviewIframe, useMarqueeGestures, usePersistentEditHistory, usePlayerHandle, usePlayerStore, usePreviewCompositionRect, usePreviewGuidesStore, usePreviewIframeStore, usePreviewOverlayContext, usePreviewPersistence, useProjectFileWriter, useRenderClipContent, useRenderQueue, useTimelineContext, useTimelineEditing, useTimelinePlayer };
4098
+ export { type AgentModalAnchorPoint, type AgentPromptElementInfo, AskAgentModal, type AskAgentModalProps, AudioMeterStrip, type AudioMeterStripProps, AudioWaveform, type AudioWaveformProps, type BlockedTimelineEditIntent, type CanEditTimelineElement, CompositionBreadcrumb, type CompositionDimensions, type CompositionLevel, CompositionThumbnail, ConnectedDomEditOverlay, type ConnectedDomEditOverlayProps, DEFAULT_SHORTCUT_SECTIONS, Dock, type DockController, type DomEditCapabilities, type DomEditCommitDeclineReason, type DomEditCommitOutcome, type DomEditGroupPathOffsetCommit, DomEditOverlay, type DomEditOverlayProps, DomEditProvider, type DomEditSelection, type DomEditZOrder, type DomGeometryCommits, type EditHistoryHandle, EditorShell, type EditorShellProps, type ExternalFileChangeBlockedState, type ExternalFileChangeCoordinatorHandle, ExternalFileConflictBanner, type FadeEdge, type FadeHandleBox, type FadeHandleClipBox, type FfmpegStatus, FileTree, GridOverlay, ImageThumbnail, type ImageThumbnailProps, type MarqueeGestures, type MarqueeGesturesDeps, MarqueeOverlay, type MarqueeOverlayProps, type Rect$1 as MarqueeRect, type ModelContextTool, NLEPreview, type PanelId, type PatchOperation, type PersistedRenderSettings, type PickedElement, PlayerControls, type PlayerHandle, type PlayerHandleElement, type PlayerHandleListener, type PlayerHandleTimeListener, type DomEditCompositionRect as PreviewCompositionRect, PreviewGuides, PreviewOverlayProvider, type PreviewOverlayProviderProps, type PreviewPromotion, PreviewReadOnlyProvider, type PreviewSnapPreferences, PropertyPanel, type RenderJob, RenderQueue, type RenderQueueProps, type ResolutionPreset, SNAP_THRESHOLD_PX, type ShortcutHint, type ShortcutSection, ShortcutsButton, type ShortcutsButtonProps, type SnapContext, type SnapEdge, type SnapGuide, SnapGuideOverlay, type SnapGuideOverlayProps, type SnapGuidesState, type Rect as SnapRect, type SnapResult, type SnapTarget, SnapToolbar, SourceEditor, type SpacingGuide, type StartRenderOptions, type StudioAgentToolsDeps, StudioApp, StudioFileConflictError, type StudioLookSnapshot, type StudioPendingEditsDrainResult, TRACK_H, type ThumbnailPriority, Timeline, TimelineClipMenu, type TimelineClipMenuItem, type TimelineClipRenderContext, type TimelineEditCallbacks, type TimelineEditOutcome, type TimelineEditPermission, TimelineEditPopover, TimelineEditProvider, type TimelineElement, TimelineEmptyStatePart, TimelineFrame, TimelineGapMenu, TimelineHistoryButtons, type TimelineHistoryButtonsProps, TimelineKeyframeMenu, TimelineLanes, TimelineOverlays, TimelinePane, type TimelinePaneProps, TimelinePlayhead, TimelineProvider, TimelineRazorGuide, TimelineRuler, TimelineShortcutHint, type TimelineTheme, type TimelineTimeRange$1 as TimelineTimeRange, TimelineToolbar, type TimelineToolbarProps, type TimelineTrackPadding, type TimelineZIndexReorderCommit, type ToolResult, type UseClipboardOptions, type UseDomEditSessionParams, type UseDomGeometryCommitOptions, type UseDomStyleCommitOptions, type UseEditHistoryActionsOptions, type UsePersistentEditHistoryOptions, type UsePreviewPersistenceParams, type UseProjectFileWriterOptions, type UseRenderClipContentOptions, type UseTimelineEditingOptions, VideoThumbnail, type ZOrderAction, applyPatch, buildCompositionSnapTarget, buildGridSnapEdges, buildPickerAgentContextPreview, buildPickerAgentPrompt, buildStudioTools, collectSnapContext, collectStudioLookScene, displayTrackOrder, extractSnapTargets, fadeHandleBoxes, findElementBlock, flushStudioPendingEdits, formatTime, getPersistedRenderSettings, liveTime, mergeStyleIntoTag, openAudioGainDialog, parseStyleString, persistRenderSettings, resolveDomEditSelection, resolveEquidistanceGuides, resolveGuideLineRect, resolveIframe, resolveSnapAdjustment, resolveSourceFile, revertNewestStudioPendingEdit, snapEngagedForTravel, useAudioGainDialogStore, useAudioMetersVisible, useClipboard, useCompositionStack, useDockLayoutStore, useDomEditSelectionContext, useDomEditSession, useDomEditZOrder, useDomGeometryCommit, useDomStyleCommit, useEditHistoryActions, useElementPicker, useLivePreviewIframe, useMarqueeGestures, usePersistentEditHistory, usePlayerHandle, usePlayerStore, usePreviewCompositionRect, usePreviewGuidesStore, usePreviewIframeStore, usePreviewOverlayContext, usePreviewPersistence, useProjectFileWriter, useRenderClipContent, useRenderQueue, useTimelineContext, useTimelineEditing, useTimelinePlayer };
package/dist/index.html CHANGED
@@ -14,8 +14,8 @@
14
14
  document.documentElement.dataset.theme = "paper";
15
15
  }
16
16
  </script>
17
- <script type="module" crossorigin src="/assets/index-SFktf7O1.js"></script>
18
- <link rel="stylesheet" crossorigin href="/assets/index-C7apTvpr.css">
17
+ <script type="module" crossorigin src="/assets/index-DRhF3v4Z.js"></script>
18
+ <link rel="stylesheet" crossorigin href="/assets/index-DbfKVTId.css">
19
19
  </head>
20
20
  <body>
21
21
  <div data-hf-id="hf-aph5" id="root"></div>