@hyperframes/studio 0.4.12 → 0.4.13-alpha.1

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 (33) hide show
  1. package/dist/assets/hyperframes-player-BOs_kypk.js +198 -0
  2. package/dist/assets/index-BKkR67xb.css +1 -0
  3. package/dist/assets/index-rN5doSq1.js +93 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/App.tsx +289 -11
  7. package/src/components/nle/NLELayout.tsx +24 -7
  8. package/src/components/nle/NLEPreview.test.ts +32 -0
  9. package/src/components/nle/NLEPreview.tsx +12 -1
  10. package/src/player/components/CompositionThumbnail.tsx +94 -17
  11. package/src/player/components/EditModal.tsx +48 -29
  12. package/src/player/components/Player.tsx +5 -2
  13. package/src/player/components/PlayerControls.test.ts +20 -0
  14. package/src/player/components/PlayerControls.tsx +12 -1
  15. package/src/player/components/Timeline.test.ts +44 -1
  16. package/src/player/components/Timeline.tsx +686 -169
  17. package/src/player/components/TimelineClip.tsx +112 -16
  18. package/src/player/components/timelineEditing.test.ts +310 -0
  19. package/src/player/components/timelineEditing.ts +213 -0
  20. package/src/player/components/timelineTheme.test.ts +56 -0
  21. package/src/player/components/timelineTheme.ts +141 -0
  22. package/src/player/components/timelineZoom.test.ts +62 -0
  23. package/src/player/components/timelineZoom.ts +38 -0
  24. package/src/player/hooks/useTimelinePlayer.test.ts +96 -0
  25. package/src/player/hooks/useTimelinePlayer.ts +313 -59
  26. package/src/player/store/playerStore.test.ts +30 -12
  27. package/src/player/store/playerStore.ts +23 -9
  28. package/src/types/hyperframes-player.d.ts +1 -0
  29. package/src/utils/sourcePatcher.test.ts +84 -0
  30. package/src/utils/sourcePatcher.ts +143 -0
  31. package/dist/assets/hyperframes-player-5iD9BZnx.js +0 -198
  32. package/dist/assets/index-CVDXfFQ6.js +0 -93
  33. package/dist/assets/index-jmDaI2F7.css +0 -1
@@ -2,12 +2,23 @@ import { create } from "zustand";
2
2
 
3
3
  export interface TimelineElement {
4
4
  id: string;
5
+ key?: string;
5
6
  tag: string;
6
7
  start: number;
7
8
  duration: number;
8
9
  track: number;
10
+ domId?: string;
11
+ /** Best-effort selector used when patching source HTML back from timeline edits */
12
+ selector?: string;
13
+ /** Zero-based occurrence index for non-unique selectors */
14
+ selectorIndex?: number;
15
+ /** Source composition file that owns this element, when known */
16
+ sourceFile?: string;
9
17
  src?: string;
10
18
  playbackStart?: number;
19
+ playbackStartAttr?: "media-start" | "playback-start";
20
+ playbackRate?: number;
21
+ sourceDuration?: number;
11
22
  volume?: number;
12
23
  /** Path from data-composition-src — identifies sub-composition elements */
13
24
  compositionSrc?: string;
@@ -23,10 +34,10 @@ interface PlayerState {
23
34
  elements: TimelineElement[];
24
35
  selectedElementId: string | null;
25
36
  playbackRate: number;
26
- /** Timeline zoom: 'fit' auto-scales to viewport, 'manual' uses pixelsPerSecond */
37
+ /** Timeline zoom: 'fit' auto-scales to viewport, 'manual' uses manualZoomPercent */
27
38
  zoomMode: ZoomMode;
28
- /** Pixels per second when in manual zoom mode */
29
- pixelsPerSecond: number;
39
+ /** Timeline zoom percent relative to the fit width when in manual mode */
40
+ manualZoomPercent: number;
30
41
 
31
42
  setIsPlaying: (playing: boolean) => void;
32
43
  setCurrentTime: (time: number) => void;
@@ -37,10 +48,10 @@ interface PlayerState {
37
48
  setSelectedElementId: (id: string | null) => void;
38
49
  updateElement: (
39
50
  elementId: string,
40
- updates: Partial<Pick<TimelineElement, "start" | "duration" | "track">>,
51
+ updates: Partial<Pick<TimelineElement, "start" | "duration" | "track" | "playbackStart">>,
41
52
  ) => void;
42
53
  setZoomMode: (mode: ZoomMode) => void;
43
- setPixelsPerSecond: (pps: number) => void;
54
+ setManualZoomPercent: (percent: number) => void;
44
55
  reset: () => void;
45
56
  }
46
57
 
@@ -66,12 +77,13 @@ export const usePlayerStore = create<PlayerState>((set) => ({
66
77
  selectedElementId: null,
67
78
  playbackRate: 1,
68
79
  zoomMode: "fit",
69
- pixelsPerSecond: 100,
80
+ manualZoomPercent: 100,
70
81
 
71
82
  setIsPlaying: (playing) => set({ isPlaying: playing }),
72
83
  setPlaybackRate: (rate) => set({ playbackRate: rate }),
73
84
  setZoomMode: (mode) => set({ zoomMode: mode }),
74
- setPixelsPerSecond: (pps) => set({ pixelsPerSecond: Math.max(10, pps) }),
85
+ setManualZoomPercent: (percent) =>
86
+ set({ manualZoomPercent: Math.max(10, Math.min(2000, Math.round(percent))) }),
75
87
  setCurrentTime: (time) => set({ currentTime: Number.isFinite(time) ? time : 0 }),
76
88
  setDuration: (duration) => set({ duration: Number.isFinite(duration) ? duration : 0 }),
77
89
  setTimelineReady: (ready) => set({ timelineReady: ready }),
@@ -79,10 +91,12 @@ export const usePlayerStore = create<PlayerState>((set) => ({
79
91
  setSelectedElementId: (id) => set({ selectedElementId: id }),
80
92
  updateElement: (elementId, updates) =>
81
93
  set((state) => ({
82
- elements: state.elements.map((el) => (el.id === elementId ? { ...el, ...updates } : el)),
94
+ elements: state.elements.map((el) =>
95
+ (el.key ?? el.id) === elementId ? { ...el, ...updates } : el,
96
+ ),
83
97
  })),
84
98
  // Resets project-specific state when switching compositions.
85
- // playbackRate, zoomMode, and pixelsPerSecond are intentionally preserved
99
+ // playbackRate, zoomMode, and manualZoomPercent are intentionally preserved
86
100
  // because they are user preferences that should survive project switches.
87
101
  reset: () =>
88
102
  set({
@@ -0,0 +1 @@
1
+ declare module "@hyperframes/player";
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { applyPatchByTarget, readAttributeByTarget, type PatchOperation } from "./sourcePatcher";
3
+
4
+ describe("applyPatchByTarget", () => {
5
+ it("updates a composition host by data-composition-id selector", () => {
6
+ const html = `<div data-composition-id="intro" data-start="0" data-track-index="1"></div>`;
7
+ const op: PatchOperation = { type: "attribute", property: "start", value: "2.5" };
8
+
9
+ expect(applyPatchByTarget(html, { selector: '[data-composition-id="intro"]' }, op)).toContain(
10
+ 'data-start="2.5"',
11
+ );
12
+ });
13
+
14
+ it("updates a class-based layer when the clip has no DOM id", () => {
15
+ const html = `<div class="headline clip" data-start="0" data-track-index="1"></div>`;
16
+ const op: PatchOperation = { type: "attribute", property: "track-index", value: "3" };
17
+
18
+ expect(applyPatchByTarget(html, { selector: ".headline" }, op)).toContain(
19
+ 'data-track-index="3"',
20
+ );
21
+ });
22
+
23
+ it("updates inline z-index by selector when the clip has no DOM id", () => {
24
+ const html = `<div class="headline clip" style="position: absolute; opacity: 1" data-start="0"></div>`;
25
+ const op: PatchOperation = { type: "inline-style", property: "z-index", value: "3" };
26
+
27
+ expect(applyPatchByTarget(html, { selector: ".headline" }, op)).toContain(
28
+ 'style="position: absolute; opacity: 1; z-index: 3"',
29
+ );
30
+ });
31
+
32
+ it("updates media timing attributes by selector", () => {
33
+ const html = `<video class="hero clip" data-start="0.2" data-duration="1.4" data-media-start="0.4"></video>`;
34
+
35
+ const withDuration = applyPatchByTarget(
36
+ html,
37
+ { selector: ".hero" },
38
+ {
39
+ type: "attribute",
40
+ property: "duration",
41
+ value: "1.1",
42
+ },
43
+ );
44
+ const withMediaStart = applyPatchByTarget(
45
+ withDuration,
46
+ { selector: ".hero" },
47
+ {
48
+ type: "attribute",
49
+ property: "media-start",
50
+ value: "0.7",
51
+ },
52
+ );
53
+
54
+ expect(withMediaStart).toContain('data-duration="1.1"');
55
+ expect(withMediaStart).toContain('data-media-start="0.7"');
56
+ });
57
+
58
+ it("reads media timing attributes by selector", () => {
59
+ const html = `<div class="hero clip" data-start="0.2" data-duration="1.4" data-media-start="0.4"></div>`;
60
+
61
+ expect(readAttributeByTarget(html, { selector: ".hero" }, "media-start")).toBe("0.4");
62
+ expect(readAttributeByTarget(html, { selector: ".hero" }, "duration")).toBe("1.4");
63
+ });
64
+
65
+ it("patches the correct duplicate selector occurrence", () => {
66
+ const html = [
67
+ `<div class="headline clip" data-start="0"></div>`,
68
+ `<div class="headline clip" data-start="1"></div>`,
69
+ ].join("");
70
+
71
+ const patched = applyPatchByTarget(
72
+ html,
73
+ { selector: ".headline", selectorIndex: 1 },
74
+ {
75
+ type: "attribute",
76
+ property: "start",
77
+ value: "2.5",
78
+ },
79
+ );
80
+
81
+ expect(patched).toContain(`<div class="headline clip" data-start="0"></div>`);
82
+ expect(patched).toContain(`<div class="headline clip" data-start="2.5"></div>`);
83
+ });
84
+ });
@@ -13,6 +13,12 @@ export interface PatchOperation {
13
13
  value: string;
14
14
  }
15
15
 
16
+ export interface PatchTarget {
17
+ id?: string | null;
18
+ selector?: string;
19
+ selectorIndex?: number;
20
+ }
21
+
16
22
  /**
17
23
  * Find which source file contains an element by its ID.
18
24
  */
@@ -73,6 +79,11 @@ function patchInlineStyle(html: string, elementId: string, prop: string, value:
73
79
  if (!match) return html;
74
80
 
75
81
  const tag = match[1];
82
+ return patchInlineStyleInTag(html, tag, prop, value);
83
+ }
84
+
85
+ function patchInlineStyleInTag(html: string, tag: string, prop: string, value: string): string {
86
+ if (!tag) return html;
76
87
 
77
88
  // Check if there's an existing style attribute
78
89
  const styleMatch = /\bstyle="([^"]*)"/.exec(tag);
@@ -102,6 +113,120 @@ function patchInlineStyle(html: string, elementId: string, prop: string, value:
102
113
  }
103
114
  }
104
115
 
116
+ function patchInlineStyleByTarget(
117
+ html: string,
118
+ target: PatchTarget,
119
+ prop: string,
120
+ value: string,
121
+ ): string {
122
+ const match = findTagByTarget(html, target);
123
+ if (!match) return html;
124
+ const newTag = patchInlineStyleInTag(match.tag, match.tag, prop, value);
125
+ return replaceTagAtMatch(html, match, newTag);
126
+ }
127
+
128
+ interface TagMatch {
129
+ tag: string;
130
+ start: number;
131
+ end: number;
132
+ }
133
+
134
+ function replaceTagAtMatch(html: string, match: TagMatch, newTag: string): string {
135
+ return `${html.slice(0, match.start)}${newTag}${html.slice(match.end)}`;
136
+ }
137
+
138
+ function findTagByTarget(html: string, target: PatchTarget): TagMatch | null {
139
+ if (target.id) {
140
+ const idPattern = new RegExp(`(<[^>]*\\bid="${escapeRegex(target.id)}"[^>]*)>`, "i");
141
+ const match = idPattern.exec(html);
142
+ if (match?.index != null) {
143
+ return {
144
+ tag: match[1],
145
+ start: match.index,
146
+ end: match.index + match[1].length,
147
+ };
148
+ }
149
+ }
150
+
151
+ if (!target.selector) return null;
152
+
153
+ const compositionIdMatch = target.selector.match(/^\[data-composition-id="([^"]+)"\]$/);
154
+ if (compositionIdMatch) {
155
+ const compId = compositionIdMatch[1];
156
+ const pattern = new RegExp(
157
+ `(<[^>]*\\bdata-composition-id="${escapeRegex(compId)}"[^>]*)>`,
158
+ "i",
159
+ );
160
+ const match = pattern.exec(html);
161
+ if (match?.index != null) {
162
+ return {
163
+ tag: match[1],
164
+ start: match.index,
165
+ end: match.index + match[1].length,
166
+ };
167
+ }
168
+ }
169
+
170
+ const classMatch = target.selector.match(/^\.([a-zA-Z0-9_-]+)$/);
171
+ if (classMatch) {
172
+ const cls = classMatch[1];
173
+ const pattern = new RegExp(
174
+ `(<[^>]*\\bclass=(["'])[^"']*\\b${escapeRegex(cls)}\\b[^"']*\\2[^>]*)>`,
175
+ "gi",
176
+ );
177
+ const selectorIndex = target.selectorIndex ?? 0;
178
+ let match: RegExpExecArray | null;
179
+ let currentIndex = 0;
180
+ while ((match = pattern.exec(html)) !== null) {
181
+ if (currentIndex === selectorIndex && match.index != null) {
182
+ return {
183
+ tag: match[1],
184
+ start: match.index,
185
+ end: match.index + match[1].length,
186
+ };
187
+ }
188
+ currentIndex += 1;
189
+ }
190
+ }
191
+
192
+ return null;
193
+ }
194
+
195
+ export function readAttributeByTarget(
196
+ html: string,
197
+ target: PatchTarget,
198
+ attr: string,
199
+ ): string | undefined {
200
+ const match = findTagByTarget(html, target);
201
+ if (!match) return undefined;
202
+
203
+ const fullAttr = attr.startsWith("data-") ? attr : `data-${attr}`;
204
+ const valueMatch = new RegExp(`\\b${fullAttr}="([^"]*)"`).exec(match.tag);
205
+ return valueMatch?.[1];
206
+ }
207
+
208
+ function patchAttributeByTarget(
209
+ html: string,
210
+ target: PatchTarget,
211
+ attr: string,
212
+ value: string,
213
+ ): string {
214
+ const match = findTagByTarget(html, target);
215
+ if (!match) return html;
216
+
217
+ const fullAttr = attr.startsWith("data-") ? attr : `data-${attr}`;
218
+ const attrPattern = new RegExp(`\\b${fullAttr}="[^"]*"`);
219
+ const tag = match.tag;
220
+
221
+ if (attrPattern.test(tag)) {
222
+ const newTag = tag.replace(attrPattern, `${fullAttr}="${value}"`);
223
+ return replaceTagAtMatch(html, match, newTag);
224
+ }
225
+
226
+ const newTag = tag + ` ${fullAttr}="${value}"`;
227
+ return replaceTagAtMatch(html, match, newTag);
228
+ }
229
+
105
230
  /**
106
231
  * Apply an attribute change to an element in the HTML source.
107
232
  */
@@ -151,3 +276,21 @@ export function applyPatch(html: string, elementId: string, op: PatchOperation):
151
276
  return html;
152
277
  }
153
278
  }
279
+
280
+ export function applyPatchByTarget(html: string, target: PatchTarget, op: PatchOperation): string {
281
+ if (target.id) {
282
+ const patchedById = applyPatch(html, target.id, op);
283
+ if (patchedById !== html || !target.selector) {
284
+ return patchedById;
285
+ }
286
+ }
287
+
288
+ switch (op.type) {
289
+ case "inline-style":
290
+ return patchInlineStyleByTarget(html, target, op.property, op.value);
291
+ case "attribute":
292
+ return patchAttributeByTarget(html, target, op.property, op.value);
293
+ default:
294
+ return html;
295
+ }
296
+ }
@@ -1,198 +0,0 @@
1
- var O=Object.defineProperty;var R=(l,f,e)=>f in l?O(l,f,{enumerable:!0,configurable:!0,writable:!0,value:e}):l[f]=e;var u=(l,f,e)=>R(l,typeof f!="symbol"?f+"":f,e);var N=`
2
- :host {
3
- display: block;
4
- position: relative;
5
- overflow: hidden;
6
- background: #000;
7
- contain: layout style;
8
- }
9
-
10
- .hfp-container {
11
- position: absolute;
12
- inset: 0;
13
- overflow: hidden;
14
- pointer-events: none;
15
- }
16
-
17
-
18
- .hfp-iframe {
19
- position: absolute;
20
- top: 50%;
21
- left: 50%;
22
- border: none;
23
- pointer-events: none;
24
- }
25
-
26
- .hfp-poster {
27
- position: absolute;
28
- inset: 0;
29
- object-fit: contain;
30
- z-index: 1;
31
- pointer-events: none;
32
- }
33
-
34
- /* ── Theming via CSS custom properties ──
35
- *
36
- * Override from outside the shadow DOM:
37
- * hyperframes-player {
38
- * --hfp-controls-bg: linear-gradient(transparent, rgba(0,0,0,0.9));
39
- * --hfp-accent: #ff6b6b;
40
- * --hfp-font: "Inter", sans-serif;
41
- * }
42
- */
43
-
44
- .hfp-controls {
45
- position: absolute;
46
- bottom: 0;
47
- left: 0;
48
- right: 0;
49
- display: flex;
50
- align-items: center;
51
- gap: var(--hfp-controls-gap, 12px);
52
- padding: var(--hfp-controls-padding, 8px 16px);
53
- background: var(--hfp-controls-bg, linear-gradient(transparent, rgba(0, 0, 0, 0.7)));
54
- color: var(--hfp-color, #fff);
55
- font-family: var(--hfp-font, system-ui, -apple-system, sans-serif);
56
- font-size: var(--hfp-font-size, 13px);
57
- z-index: 10;
58
- pointer-events: auto;
59
- opacity: 1;
60
- transition: opacity 0.3s ease;
61
- user-select: none;
62
- }
63
-
64
- .hfp-controls.hfp-hidden {
65
- opacity: 0;
66
- pointer-events: none;
67
- }
68
-
69
- .hfp-play-btn {
70
- background: none;
71
- border: none;
72
- color: var(--hfp-color, #fff);
73
- cursor: pointer;
74
- padding: 8px;
75
- display: flex;
76
- align-items: center;
77
- justify-content: center;
78
- width: 40px;
79
- height: 40px;
80
- flex-shrink: 0;
81
- z-index: 10;
82
- }
83
-
84
- .hfp-play-btn:hover {
85
- opacity: 0.8;
86
- }
87
-
88
- .hfp-play-btn svg,
89
- .hfp-play-btn svg * {
90
- pointer-events: none;
91
- }
92
-
93
- .hfp-scrubber {
94
- flex: 1;
95
- height: var(--hfp-scrubber-height, 4px);
96
- background: var(--hfp-scrubber-bg, rgba(255, 255, 255, 0.3));
97
- border-radius: var(--hfp-scrubber-radius, 2px);
98
- cursor: pointer;
99
- position: relative;
100
- }
101
-
102
- .hfp-scrubber:hover {
103
- height: var(--hfp-scrubber-height-hover, 6px);
104
- }
105
-
106
- .hfp-progress {
107
- position: absolute;
108
- top: 0;
109
- left: 0;
110
- height: 100%;
111
- background: var(--hfp-accent, #fff);
112
- border-radius: var(--hfp-scrubber-radius, 2px);
113
- pointer-events: none;
114
- }
115
-
116
- .hfp-time {
117
- flex-shrink: 0;
118
- font-variant-numeric: tabular-nums;
119
- opacity: 0.9;
120
- }
121
-
122
- .hfp-speed-wrap {
123
- position: relative;
124
- flex-shrink: 0;
125
- }
126
-
127
- .hfp-speed-btn {
128
- background: var(--hfp-speed-btn-bg, rgba(255, 255, 255, 0.15));
129
- border: none;
130
- border-radius: var(--hfp-speed-btn-radius, 4px);
131
- color: var(--hfp-color, #fff);
132
- cursor: pointer;
133
- font-family: var(--hfp-font, system-ui, -apple-system, sans-serif);
134
- font-size: 12px;
135
- font-variant-numeric: tabular-nums;
136
- font-weight: 600;
137
- padding: 4px 8px;
138
- min-width: 40px;
139
- text-align: center;
140
- transition: background 0.15s ease;
141
- }
142
-
143
- .hfp-speed-btn:hover {
144
- background: var(--hfp-speed-btn-bg-hover, rgba(255, 255, 255, 0.3));
145
- }
146
-
147
- .hfp-speed-menu {
148
- position: absolute;
149
- bottom: calc(100% + 8px);
150
- right: 0;
151
- background: var(--hfp-menu-bg, rgba(20, 20, 20, 0.95));
152
- backdrop-filter: blur(12px);
153
- -webkit-backdrop-filter: blur(12px);
154
- border: 1px solid var(--hfp-menu-border, rgba(255, 255, 255, 0.1));
155
- border-radius: var(--hfp-menu-radius, 8px);
156
- padding: 4px;
157
- display: flex;
158
- flex-direction: column;
159
- gap: 2px;
160
- min-width: 80px;
161
- opacity: 0;
162
- visibility: hidden;
163
- transform: translateY(4px);
164
- transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
165
- box-shadow: var(--hfp-menu-shadow, 0 8px 24px rgba(0, 0, 0, 0.4));
166
- }
167
-
168
- .hfp-speed-menu.hfp-open {
169
- opacity: 1;
170
- visibility: visible;
171
- transform: translateY(0);
172
- }
173
-
174
- .hfp-speed-option {
175
- background: none;
176
- border: none;
177
- border-radius: 4px;
178
- color: var(--hfp-menu-color, rgba(255, 255, 255, 0.7));
179
- cursor: pointer;
180
- font-family: var(--hfp-font, system-ui, -apple-system, sans-serif);
181
- font-size: 13px;
182
- font-variant-numeric: tabular-nums;
183
- padding: 6px 12px;
184
- text-align: left;
185
- transition: background 0.1s ease, color 0.1s ease;
186
- white-space: nowrap;
187
- }
188
-
189
- .hfp-speed-option:hover {
190
- background: var(--hfp-menu-hover-bg, rgba(255, 255, 255, 0.1));
191
- color: var(--hfp-color, #fff);
192
- }
193
-
194
- .hfp-speed-option.hfp-active {
195
- color: var(--hfp-accent, #fff);
196
- font-weight: 600;
197
- }
198
- `,T='<svg width="24" height="24" viewBox="0 0 18 18" fill="currentColor"><polygon points="4,2 16,9 4,16"/></svg>',D='<svg width="24" height="24" viewBox="0 0 18 18" fill="currentColor"><rect x="3" y="2" width="4" height="14"/><rect x="11" y="2" width="4" height="14"/></svg>',F=[.25,.5,1,1.5,2,4];function x(l){return Number.isInteger(l)?`${l}x`:`${l}x`}function S(l){if(!Number.isFinite(l)||l<0)return"0:00";let f=Math.floor(l),e=Math.floor(f/60),t=f%60;return`${e}:${t.toString().padStart(2,"0")}`}function H(l,f,e={}){let t=e.speedPresets??F,i=document.createElement("div");i.className="hfp-controls",i.addEventListener("click",s=>{s.stopPropagation()});let a=document.createElement("button");a.className="hfp-play-btn",a.type="button",a.innerHTML=T,a.setAttribute("aria-label","Play");let n=document.createElement("div");n.className="hfp-scrubber";let r=document.createElement("div");r.className="hfp-progress",r.style.width="0%",n.appendChild(r);let p=document.createElement("span");p.className="hfp-time",p.textContent="0:00 / 0:00";let b=document.createElement("div");b.className="hfp-speed-wrap";let o=document.createElement("button");o.className="hfp-speed-btn",o.type="button",o.textContent="1x",o.setAttribute("aria-label","Playback speed");let h=document.createElement("div");h.className="hfp-speed-menu",h.setAttribute("role","menu");for(let s of t){let d=document.createElement("button");d.className="hfp-speed-option",d.type="button",d.setAttribute("role","menuitem"),d.dataset.speed=String(s),d.textContent=x(s),s===1&&d.classList.add("hfp-active"),h.appendChild(d)}b.appendChild(h),b.appendChild(o),i.appendChild(a),i.appendChild(n),i.appendChild(p),i.appendChild(b),l.appendChild(i);let m=!1,c=null;t.indexOf(1),a.addEventListener("click",s=>{s.stopPropagation(),m?f.onPause():f.onPlay()});let _=s=>{for(let d of h.querySelectorAll(".hfp-speed-option"))d.classList.toggle("hfp-active",d.dataset.speed===String(s))};o.addEventListener("click",s=>{s.stopPropagation();let d=h.classList.toggle("hfp-open");o.setAttribute("aria-expanded",String(d))}),h.addEventListener("click",s=>{s.stopPropagation();let d=s.target.closest(".hfp-speed-option");if(!d)return;let g=parseFloat(d.dataset.speed);t.indexOf(g),o.textContent=x(g),_(g),h.classList.remove("hfp-open"),o.setAttribute("aria-expanded","false"),f.onSpeedChange(g)});let E=()=>{h.classList.remove("hfp-open"),o.setAttribute("aria-expanded","false")};document.addEventListener("click",E);let w=s=>{let d=n.getBoundingClientRect(),g=Math.max(0,Math.min(1,(s-d.left)/d.width));f.onSeek(g)},v=!1;n.addEventListener("mousedown",s=>{s.stopPropagation(),v=!0,w(s.clientX)});let M=s=>{v&&w(s.clientX)},P=()=>{v=!1};document.addEventListener("mousemove",M),document.addEventListener("mouseup",P),n.addEventListener("touchstart",s=>{v=!0;let d=s.touches[0];d&&w(d.clientX)},{passive:!0});let A=s=>{if(v){let d=s.touches[0];d&&w(d.clientX)}},C=()=>{v=!1};document.addEventListener("touchmove",A,{passive:!0}),document.addEventListener("touchend",C);let I=()=>{c&&clearTimeout(c),c=setTimeout(()=>{m&&i.classList.add("hfp-hidden")},3e3)},L=l instanceof ShadowRoot?l.host:l;return L.addEventListener("mousemove",()=>{i.classList.remove("hfp-hidden"),I()}),L.addEventListener("mouseleave",()=>{m&&i.classList.add("hfp-hidden")}),{updateTime(s,d){let g=d>0?s/d*100:0;r.style.width=`${g}%`,p.textContent=`${S(s)} / ${S(d)}`},updatePlaying(s){m=s,a.innerHTML=s?D:T,a.setAttribute("aria-label",s?"Pause":"Play"),s?I():i.classList.remove("hfp-hidden")},updateSpeed(s){t.indexOf(s),o.textContent=x(s),_(s)},show(){i.style.display=""},hide(){i.style.display="none"},destroy(){document.removeEventListener("mousemove",M),document.removeEventListener("mouseup",P),document.removeEventListener("touchmove",A),document.removeEventListener("touchend",C),document.removeEventListener("click",E),c&&clearTimeout(c)}}}function j(l){return l.hasRuntime||l.runtimeInjected?!1:!!(l.hasNestedCompositions||l.hasTimelines&&l.attempts>=5)}var k=30,z="https://cdn.jsdelivr.net/npm/@hyperframes/core/dist/hyperframe.runtime.iife.js",y,U=(y=class extends HTMLElement{constructor(){super();u(this,"shadow");u(this,"container");u(this,"iframe");u(this,"posterEl",null);u(this,"controlsApi",null);u(this,"resizeObserver");u(this,"_ready",!1);u(this,"_duration",0);u(this,"_currentTime",0);u(this,"_paused",!0);u(this,"_compositionWidth",1920);u(this,"_compositionHeight",1080);u(this,"_probeInterval",null);u(this,"_lastUpdateMs",0);u(this,"_parentMedia",[]);u(this,"_audioOwner","runtime");u(this,"_mediaObserver");u(this,"_playbackErrorPosted",!1);u(this,"_runtimeInjected",!1);this.shadow=this.attachShadow({mode:"open"});let e=document.createElement("style");e.textContent=N,this.shadow.appendChild(e),this.container=document.createElement("div"),this.container.className="hfp-container",this.iframe=document.createElement("iframe"),this.iframe.className="hfp-iframe",this.iframe.sandbox.add("allow-scripts","allow-same-origin"),this.iframe.allow="autoplay; fullscreen",this.iframe.referrerPolicy="no-referrer",this.iframe.title="HyperFrames Composition",this.container.appendChild(this.iframe),this.shadow.appendChild(this.container),this.addEventListener("click",t=>{this._isControlsClick(t)||(this._paused?this.play():this.pause())}),this.resizeObserver=new ResizeObserver(()=>this._updateScale()),this._onMessage=this._onMessage.bind(this),this._onIframeLoad=this._onIframeLoad.bind(this)}static get observedAttributes(){return["src","width","height","controls","muted","poster","playback-rate","audio-src"]}connectedCallback(){this.resizeObserver.observe(this),window.addEventListener("message",this._onMessage),this.iframe.addEventListener("load",this._onIframeLoad),this.hasAttribute("controls")&&this._setupControls(),this.hasAttribute("poster")&&this._setupPoster(),this.hasAttribute("audio-src")&&this._setupParentAudioFromUrl(this.getAttribute("audio-src")),this.hasAttribute("src")&&(this.iframe.src=this.getAttribute("src"))}disconnectedCallback(){var e;this.resizeObserver.disconnect(),window.removeEventListener("message",this._onMessage),this.iframe.removeEventListener("load",this._onIframeLoad),this._probeInterval&&clearInterval(this._probeInterval),this._teardownMediaObserver(),(e=this.controlsApi)==null||e.destroy();for(let t of this._parentMedia)t.el.pause(),t.el.src="";this._parentMedia=[]}attributeChangedCallback(e,t,i){var a,n;switch(e){case"src":i&&(this._ready=!1,this.iframe.src=i);break;case"width":this._compositionWidth=parseInt(i||"1920",10),this._updateScale();break;case"height":this._compositionHeight=parseInt(i||"1080",10),this._updateScale();break;case"controls":i!==null?this._setupControls():((a=this.controlsApi)==null||a.destroy(),this.controlsApi=null);break;case"poster":this._setupPoster();break;case"playback-rate":{let r=parseFloat(i||"1");for(let p of this._parentMedia)p.el.playbackRate=r;this._sendControl("set-playback-rate",{playbackRate:r}),(n=this.controlsApi)==null||n.updateSpeed(r),this.dispatchEvent(new Event("ratechange"));break}case"muted":for(let r of this._parentMedia)r.el.muted=i!==null;this._sendControl("set-muted",{muted:i!==null});break;case"audio-src":i&&this._setupParentAudioFromUrl(i);break}}get iframeElement(){return this.iframe}play(){var e;this._hidePoster(),this._sendControl("play"),this._audioOwner==="parent"&&this._playParentMedia(),this._paused=!1,(e=this.controlsApi)==null||e.updatePlaying(!0),this.dispatchEvent(new Event("play"))}pause(){var e;this._sendControl("pause"),this._audioOwner==="parent"&&this._pauseParentMedia(),this._paused=!0,(e=this.controlsApi)==null||e.updatePlaying(!1),this.dispatchEvent(new Event("pause"))}seek(e){var i,a;let t=Math.round(e*k);if(this._sendControl("seek",{frame:t}),this._currentTime=e,this._audioOwner==="parent")for(let n of this._parentMedia){let r=e-n.start;r>=0&&r<n.duration&&(n.el.currentTime=r)}this._paused=!0,(i=this.controlsApi)==null||i.updatePlaying(!1),(a=this.controlsApi)==null||a.updateTime(this._currentTime,this._duration)}get currentTime(){return this._currentTime}set currentTime(e){this.seek(e)}get duration(){return this._duration}get paused(){return this._paused}get ready(){return this._ready}get playbackRate(){return parseFloat(this.getAttribute("playback-rate")||"1")}set playbackRate(e){this.setAttribute("playback-rate",String(e))}get muted(){return this.hasAttribute("muted")}set muted(e){e?this.setAttribute("muted",""):this.removeAttribute("muted")}get loop(){return this.hasAttribute("loop")}set loop(e){e?this.setAttribute("loop",""):this.removeAttribute("loop")}_sendControl(e,t={}){var i;try{(i=this.iframe.contentWindow)==null||i.postMessage({source:"hf-parent",type:"control",action:e,...t},"*")}catch{}}_isControlsClick(e){return e.composedPath().some(t=>t instanceof HTMLElement&&t.classList.contains("hfp-controls"))}_onMessage(e){var i,a,n,r;if(e.source!==this.iframe.contentWindow)return;let t=e.data;if(!(!t||t.source!=="hf-preview")){if(t.type==="state"){this._currentTime=(t.frame??0)/k;let p=!this._paused;this._paused=!t.isPlaying,this._audioOwner==="parent"&&(p&&this._paused?this._pauseParentMedia():!p&&!this._paused&&this._playParentMedia(),this._mirrorParentMediaTime(this._currentTime));let b=performance.now();(b-this._lastUpdateMs>100||this._paused!==p)&&(this._lastUpdateMs=b,(i=this.controlsApi)==null||i.updateTime(this._currentTime,this._duration),(a=this.controlsApi)==null||a.updatePlaying(!this._paused),this.dispatchEvent(new CustomEvent("timeupdate",{detail:{currentTime:this._currentTime}}))),this._currentTime>=this._duration&&!this._paused&&(this._audioOwner==="parent"&&this._pauseParentMedia(),this.loop?(this.seek(0),this.play()):(this._paused=!0,(n=this.controlsApi)==null||n.updatePlaying(!1),this.dispatchEvent(new Event("ended"))))}t.type==="media-autoplay-blocked"&&this._promoteToParentProxy(),t.type==="timeline"&&t.durationInFrames>0&&Number.isFinite(t.durationInFrames)&&(this._duration=t.durationInFrames/k,(r=this.controlsApi)==null||r.updateTime(this._currentTime,this._duration)),t.type==="stage-size"&&t.width>0&&t.height>0&&(this._compositionWidth=t.width,this._compositionHeight=t.height,this._updateScale())}}_onIframeLoad(){let e=0;this._runtimeInjected=!1;let t=this._audioOwner==="parent";this._audioOwner="runtime",this._playbackErrorPosted=!1,this._pauseParentMedia(),this._teardownMediaObserver(),t&&this.dispatchEvent(new CustomEvent("audioownershipchange",{detail:{owner:"runtime",reason:"iframe-reload"}})),this._probeInterval&&clearInterval(this._probeInterval),this._probeInterval=setInterval(()=>{var i,a,n;e++;try{let r=this.iframe.contentWindow;if(!r)return;let p=!!(r.__hf||r.__player),b=!!(r.__timelines&&Object.keys(r.__timelines).length>0),o=!!((i=this.iframe.contentDocument)!=null&&i.querySelector("[data-composition-src]"));if(j({hasRuntime:p,hasTimelines:b,hasNestedCompositions:o,runtimeInjected:this._runtimeInjected,attempts:e})){this._injectRuntime();return}if(this._runtimeInjected&&!p)return;let h=(()=>{var m,c;if(r.__player&&typeof r.__player.getDuration=="function")return r.__player;if(r.__timelines){let _=Object.keys(r.__timelines);if(_.length>0){let E=(c=(m=this.iframe.contentDocument)==null?void 0:m.querySelector("[data-composition-id]"))==null?void 0:c.getAttribute("data-composition-id"),w=E&&E in r.__timelines?E:_[_.length-1],v=r.__timelines[w];return{getDuration:()=>v.duration()}}}return null})();if(h&&h.getDuration()>0){clearInterval(this._probeInterval),this._duration=h.getDuration(),this._ready=!0,(a=this.controlsApi)==null||a.updateTime(0,this._duration),this.dispatchEvent(new CustomEvent("ready",{detail:{duration:this._duration}}));let m=(n=this.iframe.contentDocument)==null?void 0:n.querySelector("[data-composition-id]");if(m){let c=parseInt(m.getAttribute("data-width")||"0",10),_=parseInt(m.getAttribute("data-height")||"0",10);c>0&&_>0&&(this._compositionWidth=c,this._compositionHeight=_,this._updateScale())}this._setupParentMedia(),this.hasAttribute("autoplay")&&this.play();return}}catch{}e>=40&&(clearInterval(this._probeInterval),this.dispatchEvent(new CustomEvent("error",{detail:{message:"Composition timeline not found after 8s"}})))},200)}_injectRuntime(){this._runtimeInjected=!0;try{let e=this.iframe.contentDocument;if(!e)return;let t=e.createElement("script");t.src=z,t.onload=()=>{},t.onerror=()=>{},(e.head||e.documentElement).appendChild(t)}catch{}}_updateScale(){let e=this.getBoundingClientRect();if(e.width===0||e.height===0)return;let t=Math.min(e.width/this._compositionWidth,e.height/this._compositionHeight);this.iframe.style.width=`${this._compositionWidth}px`,this.iframe.style.height=`${this._compositionHeight}px`,this.iframe.style.transform=`translate(-50%, -50%) scale(${t})`}_setupControls(){if(this.controlsApi)return;let e={onPlay:()=>this.play(),onPause:()=>this.pause(),onSeek:a=>this.seek(a*this._duration),onSpeedChange:a=>{this.playbackRate=a}},t=this.getAttribute("speed-presets"),i=t?t.split(",").map(Number).filter(a=>!isNaN(a)&&a>0):void 0;this.controlsApi=H(this.shadow,e,{speedPresets:i})}_setupPoster(){var t;let e=this.getAttribute("poster");if(!e){(t=this.posterEl)==null||t.remove(),this.posterEl=null;return}this.posterEl||(this.posterEl=document.createElement("img"),this.posterEl.className="hfp-poster",this.shadow.appendChild(this.posterEl)),this.posterEl.src=e}_playParentMedia(){for(let e of this._parentMedia)e.el.src&&e.el.play().catch(t=>this._reportPlaybackError(t))}_reportPlaybackError(e){this._playbackErrorPosted||(this._playbackErrorPosted=!0,this.dispatchEvent(new CustomEvent("playbackerror",{detail:{source:"parent-proxy",error:e}})))}_pauseParentMedia(){for(let e of this._parentMedia)e.el.pause()}_mirrorParentMediaTime(e){for(let t of this._parentMedia){let i=e-t.start;i<0||i>=t.duration||Math.abs(t.el.currentTime-i)>y.MIRROR_DRIFT_THRESHOLD_SECONDS&&(t.el.currentTime=i)}}_promoteToParentProxy(){this._audioOwner!=="parent"&&(this._audioOwner="parent",this._sendControl("set-media-output-muted",{muted:!0}),this._mirrorParentMediaTime(this._currentTime),this._paused||this._playParentMedia(),this.dispatchEvent(new CustomEvent("audioownershipchange",{detail:{owner:"parent",reason:"autoplay-blocked"}})))}_createParentMedia(e,t,i,a){if(this._parentMedia.some(p=>p.el.src===e))return null;let n=t==="video"?document.createElement("video"):new Audio;n.preload="auto",n.src=e,n.load(),n.muted=this.muted,this.playbackRate!==1&&(n.playbackRate=this.playbackRate);let r={el:n,start:i,duration:a};return this._parentMedia.push(r),r}_setupParentAudioFromUrl(e){this._createParentMedia(e,"audio",0,1/0)}_setupParentMedia(){try{let e=this.iframe.contentDocument;if(!e)return;let t=e.querySelectorAll("audio[data-start], video[data-start]");for(let i of t)this._adoptIframeMedia(i);this._observeDynamicMedia(e)}catch{}}_adoptIframeMedia(e){var b;let t=e.getAttribute("src")||((b=e.querySelector("source"))==null?void 0:b.getAttribute("src"));if(!t)return;let i=new URL(t,e.ownerDocument.baseURI).href,a=parseFloat(e.getAttribute("data-start")||"0"),n=parseFloat(e.getAttribute("data-duration")||"Infinity"),r=e.tagName==="VIDEO"?"video":"audio",p=this._createParentMedia(i,r,a,n);p&&this._audioOwner==="parent"&&(this._mirrorParentMediaTime(this._currentTime),!this._paused&&p.el.src&&p.el.play().catch(o=>this._reportPlaybackError(o)))}_observeDynamicMedia(e){if(this._teardownMediaObserver(),typeof MutationObserver>"u"||!e.body)return;let t=new MutationObserver(i=>{var a,n,r,p;for(let b of i){for(let o of b.addedNodes){if(!(o instanceof Element))continue;let h=[];(a=o.matches)!=null&&a.call(o,"audio[data-start], video[data-start]")&&h.push(o);let m=(n=o.querySelectorAll)==null?void 0:n.call(o,"audio[data-start], video[data-start]");if(m)for(let c of m)h.push(c);for(let c of h)this._adoptIframeMedia(c)}for(let o of b.removedNodes){if(!(o instanceof Element))continue;let h=[];(r=o.matches)!=null&&r.call(o,"audio[data-start], video[data-start]")&&h.push(o);let m=(p=o.querySelectorAll)==null?void 0:p.call(o,"audio[data-start], video[data-start]");if(m)for(let c of m)h.push(c);for(let c of h)this._detachIframeMedia(c)}}});t.observe(e.body,{childList:!0,subtree:!0}),this._mediaObserver=t}_teardownMediaObserver(){var e;(e=this._mediaObserver)==null||e.disconnect(),this._mediaObserver=void 0}_detachIframeMedia(e){var r;let t=e.getAttribute("src")||((r=e.querySelector("source"))==null?void 0:r.getAttribute("src"));if(!t)return;let i=new URL(t,e.ownerDocument.baseURI).href,a=this._parentMedia.findIndex(p=>p.el.src===i);if(a===-1)return;let n=this._parentMedia[a];n.el.pause(),n.el.src="",this._parentMedia.splice(a,1)}_hidePoster(){var e;(e=this.posterEl)==null||e.remove(),this.posterEl=null}},u(y,"MIRROR_DRIFT_THRESHOLD_SECONDS",.05),y);customElements.get("hyperframes-player")||customElements.define("hyperframes-player",U);export{U as HyperframesPlayer,F as SPEED_PRESETS,x as formatSpeed,S as formatTime};