@hyperframes/studio 0.6.0-alpha.1 → 0.6.0-alpha.11

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.
@@ -26,11 +26,13 @@ export function resolveSeekPercent(clientX: number, rectLeft: number, rectWidth:
26
26
  interface PlayerControlsProps {
27
27
  onTogglePlay: () => void;
28
28
  onSeek: (time: number) => void;
29
+ disabled?: boolean;
29
30
  }
30
31
 
31
32
  export const PlayerControls = memo(function PlayerControls({
32
33
  onTogglePlay,
33
34
  onSeek,
35
+ disabled = false,
34
36
  }: PlayerControlsProps) {
35
37
  // Subscribe to only the fields we render — each selector prevents cascading re-renders
36
38
  const isPlaying = usePlayerStore((s) => s.isPlaying);
@@ -57,6 +59,7 @@ export const PlayerControls = memo(function PlayerControls({
57
59
 
58
60
  const durationRef = useRef(duration);
59
61
  durationRef.current = duration;
62
+ const controlsDisabled = disabled || !timelineReady;
60
63
  useMountEffect(() => {
61
64
  const updateProgress = (t: number) => {
62
65
  currentTimeRef.current = t;
@@ -115,6 +118,7 @@ export const PlayerControls = memo(function PlayerControls({
115
118
 
116
119
  const seekFromClientX = useCallback(
117
120
  (clientX: number) => {
121
+ if (disabled) return;
118
122
  const bar = seekBarRef.current;
119
123
  if (!bar || duration <= 0) return;
120
124
  const rect = bar.getBoundingClientRect();
@@ -125,7 +129,7 @@ export const PlayerControls = memo(function PlayerControls({
125
129
  if (progressThumbRef.current) progressThumbRef.current.style.left = `${pct}%`;
126
130
  onSeek(percent * duration);
127
131
  },
128
- [duration, onSeek],
132
+ [disabled, duration, onSeek],
129
133
  );
130
134
 
131
135
  const handlePointerDown = useCallback(
@@ -204,7 +208,7 @@ export const PlayerControls = memo(function PlayerControls({
204
208
 
205
209
  const handleKeyDown = useCallback(
206
210
  (e: React.KeyboardEvent) => {
207
- if (!timelineReady || duration <= 0) return;
211
+ if (disabled || !timelineReady || duration <= 0) return;
208
212
  const step = e.shiftKey ? 10 : 1;
209
213
  if (e.key === "ArrowLeft") {
210
214
  e.preventDefault();
@@ -214,14 +218,15 @@ export const PlayerControls = memo(function PlayerControls({
214
218
  onSeek(Math.min(duration, stepFrameTime(currentTimeRef.current, step)));
215
219
  }
216
220
  },
217
- [timelineReady, duration, onSeek],
221
+ [disabled, timelineReady, duration, onSeek],
218
222
  );
219
223
 
220
224
  const commitJumpFrame = useCallback(() => {
225
+ if (disabled) return;
221
226
  const frame = Number.parseInt(jumpFrame, 10);
222
227
  if (!Number.isFinite(frame) || duration <= 0) return;
223
228
  onSeek(Math.min(duration, frameToSeconds(Math.max(0, frame))));
224
- }, [duration, jumpFrame, onSeek]);
229
+ }, [disabled, duration, jumpFrame, onSeek]);
225
230
 
226
231
  const handleJumpSubmit = useCallback(
227
232
  (e: React.FormEvent) => {
@@ -243,6 +248,7 @@ export const PlayerControls = memo(function PlayerControls({
243
248
  return (
244
249
  <div
245
250
  className="px-4 py-2 flex flex-wrap items-center gap-x-2 gap-y-1"
251
+ aria-disabled={disabled || undefined}
246
252
  style={{
247
253
  borderTop: "1px solid rgba(255,255,255,0.04)",
248
254
  // Add iOS safe-area inset so Safari's bottom URL bar doesn't occlude
@@ -256,7 +262,7 @@ export const PlayerControls = memo(function PlayerControls({
256
262
  type="button"
257
263
  aria-label={isPlaying ? "Pause" : "Play"}
258
264
  onClick={onTogglePlay}
259
- disabled={!timelineReady}
265
+ disabled={controlsDisabled}
260
266
  className="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-lg disabled:opacity-30 disabled:pointer-events-none transition-colors"
261
267
  style={{ background: "rgba(255,255,255,0.06)" }}
262
268
  >
@@ -293,12 +299,15 @@ export const PlayerControls = memo(function PlayerControls({
293
299
  (sliderRef as React.MutableRefObject<HTMLDivElement | null>).current = el;
294
300
  }}
295
301
  role="slider"
296
- tabIndex={0}
302
+ tabIndex={disabled ? -1 : 0}
297
303
  aria-label="Seek"
304
+ aria-disabled={disabled || undefined}
298
305
  aria-valuemin={0}
299
306
  aria-valuemax={Math.round(duration)}
300
307
  aria-valuenow={0}
301
- className="min-w-[96px] flex-1 h-6 flex items-center cursor-pointer group"
308
+ className={`min-w-[96px] flex-1 h-6 flex items-center group ${
309
+ disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"
310
+ }`}
302
311
  // `touch-action: none` tells the browser we're handling every
303
312
  // pointer gesture on this element ourselves. Without it, iOS
304
313
  // Safari consumes horizontal swipes for its own swipe-back-to-
@@ -334,6 +343,7 @@ export const PlayerControls = memo(function PlayerControls({
334
343
  <button
335
344
  type="button"
336
345
  onClick={() => setShowSpeedMenu((v) => !v)}
346
+ disabled={disabled}
337
347
  className="w-10 px-2 py-1 rounded-md text-[10px] font-mono tabular-nums transition-colors"
338
348
  style={{ color: "#71717A", background: "rgba(255,255,255,0.04)" }}
339
349
  >
@@ -374,6 +384,7 @@ export const PlayerControls = memo(function PlayerControls({
374
384
  <button
375
385
  type="button"
376
386
  onClick={() => setLoopEnabled(!loopEnabled)}
387
+ disabled={disabled}
377
388
  className={`h-7 w-14 rounded-md border px-2 text-[10px] font-medium transition-colors ${
378
389
  loopEnabled
379
390
  ? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
@@ -389,6 +400,7 @@ export const PlayerControls = memo(function PlayerControls({
389
400
  <button
390
401
  type="button"
391
402
  onClick={() => setTimeDisplayMode((mode) => (mode === "time" ? "frame" : "time"))}
403
+ disabled={disabled}
392
404
  className="h-7 w-14 rounded-md border border-neutral-700 px-2 text-[10px] font-mono text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800"
393
405
  title="Toggle time/frame display"
394
406
  aria-label="Toggle time and frame display"
@@ -403,6 +415,7 @@ export const PlayerControls = memo(function PlayerControls({
403
415
  <input
404
416
  value={jumpFrame}
405
417
  onChange={(e) => setJumpFrame(e.target.value)}
418
+ disabled={disabled}
406
419
  inputMode="numeric"
407
420
  pattern="[0-9]*"
408
421
  aria-label="Jump to frame"
@@ -340,6 +340,7 @@ interface TimelineProps {
340
340
  layerChildCounts?: ReadonlyMap<string, number>;
341
341
  thumbnailedElementIds?: ReadonlySet<string>;
342
342
  onToggleElementThumbnail?: (element: import("../store/playerStore").TimelineElement) => void;
343
+ disabled?: boolean;
343
344
  theme?: Partial<TimelineTheme>;
344
345
  }
345
346
 
@@ -393,6 +394,7 @@ export const Timeline = memo(function Timeline({
393
394
  layerChildCounts,
394
395
  thumbnailedElementIds,
395
396
  onToggleElementThumbnail,
397
+ disabled = false,
396
398
  theme: themeOverrides,
397
399
  }: TimelineProps = {}) {
398
400
  const theme = useMemo(() => ({ ...defaultTimelineTheme, ...themeOverrides }), [themeOverrides]);
@@ -412,6 +414,8 @@ export const Timeline = memo(function Timeline({
412
414
  const scrollRef = useRef<HTMLDivElement>(null);
413
415
  const [hoveredClip, setHoveredClip] = useState<string | null>(null);
414
416
  const isDragging = useRef(false);
417
+ const disabledRef = useRef(disabled);
418
+ disabledRef.current = disabled;
415
419
  const shiftClickClipRef = useRef<{
416
420
  element: TimelineElement;
417
421
  anchorX: number;
@@ -442,7 +446,6 @@ export const Timeline = memo(function Timeline({
442
446
  const resizingClipRef = useRef<ResizingClipState | null>(null);
443
447
  resizingClipRef.current = resizingClip;
444
448
  const blockedClipRef = useRef<BlockedClipState | null>(null);
445
- const deleteInFlightRef = useRef(false);
446
449
  const onMoveElementRef = useRef(onMoveElement);
447
450
  onMoveElementRef.current = onMoveElement;
448
451
  const onResizeElementRef = useRef(onResizeElement);
@@ -498,6 +501,19 @@ export const Timeline = memo(function Timeline({
498
501
  if (shortcutHintRafRef.current) cancelAnimationFrame(shortcutHintRafRef.current);
499
502
  });
500
503
 
504
+ useEffect(() => {
505
+ if (!disabled) return;
506
+ stopClipDragAutoScrollRef.current();
507
+ isDragging.current = false;
508
+ isRangeSelecting.current = false;
509
+ blockedClipRef.current = null;
510
+ setDraggedClip(null);
511
+ setResizingClip(null);
512
+ setRangeSelection(null);
513
+ setShowPopover(false);
514
+ setIsDragOver(false);
515
+ }, [disabled]);
516
+
501
517
  // Effective duration: max of store duration and the furthest element end.
502
518
  // processTimelineMessage updates elements but not duration, so elements can
503
519
  // extend beyond the store's duration — this ensures fit mode shows everything.
@@ -724,6 +740,7 @@ export const Timeline = memo(function Timeline({
724
740
 
725
741
  const seekFromX = useCallback(
726
742
  (clientX: number) => {
743
+ if (disabledRef.current) return;
727
744
  const el = scrollRef.current;
728
745
  if (!el || effectiveDuration <= 0) return;
729
746
  const rect = el.getBoundingClientRect();
@@ -781,6 +798,7 @@ export const Timeline = memo(function Timeline({
781
798
  };
782
799
 
783
800
  const handleWindowPointerMove = (e: PointerEvent) => {
801
+ if (disabledRef.current) return;
784
802
  const drag = draggedClipRef.current;
785
803
  const resize = resizingClipRef.current;
786
804
  const blocked = blockedClipRef.current;
@@ -865,6 +883,7 @@ export const Timeline = memo(function Timeline({
865
883
 
866
884
  const handleWindowPointerUp = () => {
867
885
  stopClipDragAutoScrollRef.current();
886
+ if (disabledRef.current) return;
868
887
  const resize = resizingClipRef.current;
869
888
  if (resize) {
870
889
  resizingClipRef.current = null;
@@ -956,30 +975,24 @@ export const Timeline = memo(function Timeline({
956
975
  };
957
976
  });
958
977
 
959
- useMountEffect(() => {
960
- const handleKeyDown = (event: KeyboardEvent) => {
961
- if (!shouldHandleTimelineDeleteKey(event)) return;
962
- const selected = selectedElementRef.current;
963
- const onDelete = onDeleteElementRef.current;
964
- if (!selected || !onDelete || deleteInFlightRef.current) return;
965
- event.preventDefault();
966
- deleteInFlightRef.current = true;
967
- suppressClickRef.current = true;
978
+ const prevSelectedRef = useRef(selectedElementRef.current);
979
+ // eslint-disable-next-line no-restricted-syntax, react-hooks/exhaustive-deps
980
+ useEffect(() => {
981
+ const prev = prevSelectedRef.current;
982
+ const curr = selectedElementRef.current;
983
+ prevSelectedRef.current = curr;
984
+ if (prev && !curr) {
968
985
  setShowPopover(false);
969
986
  setRangeSelection(null);
970
- Promise.resolve(onDelete(selected)).finally(() => {
971
- deleteInFlightRef.current = false;
972
- requestAnimationFrame(() => {
973
- suppressClickRef.current = false;
974
- });
975
- });
976
- };
977
- window.addEventListener("keydown", handleKeyDown);
978
- return () => window.removeEventListener("keydown", handleKeyDown);
987
+ }
979
988
  });
980
989
 
981
990
  const handlePointerDown = useCallback(
982
991
  (e: React.PointerEvent) => {
992
+ if (disabledRef.current) {
993
+ e.preventDefault();
994
+ return;
995
+ }
983
996
  if (e.button !== 0) return;
984
997
 
985
998
  // Shift+click starts range selection — even on clips
@@ -1011,6 +1024,7 @@ export const Timeline = memo(function Timeline({
1011
1024
  );
1012
1025
  const handlePointerMove = useCallback(
1013
1026
  (e: React.PointerEvent) => {
1027
+ if (disabledRef.current) return;
1014
1028
  if (isRangeSelecting.current) {
1015
1029
  const rect = scrollRef.current?.getBoundingClientRect();
1016
1030
  if (rect) {
@@ -1081,6 +1095,7 @@ export const Timeline = memo(function Timeline({
1081
1095
 
1082
1096
  const [isDragOver, setIsDragOver] = useState(false);
1083
1097
  const handleAssetDragOver = useCallback((e: React.DragEvent) => {
1098
+ if (disabledRef.current) return;
1084
1099
  const hasFiles = e.dataTransfer.files.length > 0;
1085
1100
  const hasAsset = Array.from(e.dataTransfer.types).includes(TIMELINE_ASSET_MIME);
1086
1101
  if (!hasFiles && !hasAsset) return;
@@ -1095,6 +1110,7 @@ export const Timeline = memo(function Timeline({
1095
1110
  (e: React.DragEvent) => {
1096
1111
  e.preventDefault();
1097
1112
  setIsDragOver(false);
1113
+ if (disabledRef.current) return;
1098
1114
  if (onFileDrop && e.dataTransfer.files.length > 0) {
1099
1115
  const scroll = scrollRef.current;
1100
1116
  const rect = scroll?.getBoundingClientRect();
@@ -1151,6 +1167,7 @@ export const Timeline = memo(function Timeline({
1151
1167
 
1152
1168
  const handlePinchWheel = useCallback(
1153
1169
  (e: WheelEvent) => {
1170
+ if (disabledRef.current) return;
1154
1171
  if (!e.ctrlKey) return;
1155
1172
  const scroll = scrollRef.current;
1156
1173
  if (!scroll || durationRef.current <= 0 || fitPpsRef.current <= 0 || ppsRef.current <= 0) {
@@ -1206,6 +1223,7 @@ export const Timeline = memo(function Timeline({
1206
1223
  className={`h-full border-t bg-[#0a0a0b] flex flex-col select-none transition-colors duration-150 ${
1207
1224
  isDragOver ? "border-studio-accent/50 bg-studio-accent/[0.03]" : "border-neutral-800/50"
1208
1225
  }`}
1226
+ aria-disabled={disabled || undefined}
1209
1227
  onDragOver={handleAssetDragOver}
1210
1228
  onDragLeave={() => setIsDragOver(false)}
1211
1229
  onDrop={handleAssetDrop}
@@ -1361,7 +1379,14 @@ export const Timeline = memo(function Timeline({
1361
1379
  <div
1362
1380
  ref={setContainerRef}
1363
1381
  aria-label="Timeline"
1364
- className={`relative border-t select-none h-full overflow-hidden ${shiftHeld ? "cursor-crosshair" : "cursor-default"}`}
1382
+ aria-disabled={disabled || undefined}
1383
+ className={`relative border-t select-none h-full overflow-hidden transition-opacity ${
1384
+ disabled
1385
+ ? "cursor-not-allowed opacity-45"
1386
+ : shiftHeld
1387
+ ? "cursor-crosshair"
1388
+ : "cursor-default"
1389
+ }`}
1365
1390
  style={{
1366
1391
  touchAction: "pan-x pan-y",
1367
1392
  background: theme.shellBackground,
@@ -13,7 +13,7 @@ interface EditableTargetLike {
13
13
  getAttribute?: (name: string) => string | null;
14
14
  }
15
15
 
16
- function isEditableTarget(target: EventTarget | null): boolean {
16
+ export function isEditableTarget(target: EventTarget | null): boolean {
17
17
  if (!target || typeof target !== "object") return false;
18
18
 
19
19
  const element = target as EditableTargetLike;
@@ -1,198 +0,0 @@
1
- var D=Object.defineProperty;var F=(d,b,e)=>b in d?D(d,b,{enumerable:!0,configurable:!0,writable:!0,value:e}):d[b]=e;var p=(d,b,e)=>F(d,typeof b!="symbol"?b+"":b,e);const 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
- `,O='<svg width="24" height="24" viewBox="0 0 18 18" fill="currentColor"><polygon points="4,2 16,9 4,16"/></svg>',U='<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>',j=[.25,.5,1,1.5,2,4];function k(d){return Number.isInteger(d)?`${d}x`:`${d}x`}function R(d){if(!Number.isFinite(d)||d<0)return"0:00";const b=Math.floor(d),e=Math.floor(b/60),t=b%60;return`${e}:${t.toString().padStart(2,"0")}`}function z(d,b,e={}){const t=e.speedPresets??j,s=document.createElement("div");s.className="hfp-controls",s.addEventListener("click",o=>{o.stopPropagation()});const i=document.createElement("button");i.className="hfp-play-btn",i.type="button",i.innerHTML=O,i.setAttribute("aria-label","Play");const r=document.createElement("div");r.className="hfp-scrubber";const n=document.createElement("div");n.className="hfp-progress",n.style.width="0%",r.appendChild(n);const c=document.createElement("span");c.className="hfp-time",c.textContent="0:00 / 0:00";const _=document.createElement("div");_.className="hfp-speed-wrap";const u=document.createElement("button");u.className="hfp-speed-btn",u.type="button",u.textContent="1x",u.setAttribute("aria-label","Playback speed");const a=document.createElement("div");a.className="hfp-speed-menu",a.setAttribute("role","menu");for(const o of t){const h=document.createElement("button");h.className="hfp-speed-option",h.type="button",h.setAttribute("role","menuitem"),h.dataset.speed=String(o),h.textContent=k(o),o===1&&h.classList.add("hfp-active"),a.appendChild(h)}_.appendChild(a),_.appendChild(u),s.appendChild(i),s.appendChild(r),s.appendChild(c),s.appendChild(_),d.appendChild(s);let l=!1,f=null;t.indexOf(1),i.addEventListener("click",o=>{o.stopPropagation(),l?b.onPause():b.onPlay()});const m=o=>{for(const h of a.querySelectorAll(".hfp-speed-option"))h.classList.toggle("hfp-active",h.dataset.speed===String(o))};u.addEventListener("click",o=>{o.stopPropagation();const h=a.classList.toggle("hfp-open");u.setAttribute("aria-expanded",String(h))}),a.addEventListener("click",o=>{o.stopPropagation();const h=o.target.closest(".hfp-speed-option");if(!h)return;const y=parseFloat(h.dataset.speed);t.indexOf(y),u.textContent=k(y),m(y),a.classList.remove("hfp-open"),u.setAttribute("aria-expanded","false"),b.onSpeedChange(y)});const v=()=>{a.classList.remove("hfp-open"),u.setAttribute("aria-expanded","false")};document.addEventListener("click",v);const E=o=>{const h=r.getBoundingClientRect(),y=Math.max(0,Math.min(1,(o-h.left)/h.width));b.onSeek(y)};let g=!1;r.addEventListener("mousedown",o=>{o.stopPropagation(),g=!0,E(o.clientX)});const A=o=>{g&&E(o.clientX)},P=()=>{g=!1};document.addEventListener("mousemove",A),document.addEventListener("mouseup",P),r.addEventListener("touchstart",o=>{g=!0;const h=o.touches[0];h&&E(h.clientX)},{passive:!0});const C=o=>{if(g){const h=o.touches[0];h&&E(h.clientX)}},I=()=>{g=!1};document.addEventListener("touchmove",C,{passive:!0}),document.addEventListener("touchend",I);const T=()=>{f&&clearTimeout(f),f=setTimeout(()=>{l&&s.classList.add("hfp-hidden")},3e3)},L=d instanceof ShadowRoot?d.host:d;return L.addEventListener("mousemove",()=>{s.classList.remove("hfp-hidden"),T()}),L.addEventListener("mouseleave",()=>{l&&s.classList.add("hfp-hidden")}),{updateTime(o,h){const y=h>0?o/h*100:0;n.style.width=`${y}%`,c.textContent=`${R(o)} / ${R(h)}`},updatePlaying(o){l=o,i.innerHTML=o?U:O,i.setAttribute("aria-label",o?"Pause":"Play"),o?T():s.classList.remove("hfp-hidden")},updateSpeed(o){t.indexOf(o),u.textContent=k(o),m(o)},show(){s.style.display=""},hide(){s.style.display="none"},destroy(){document.removeEventListener("mousemove",A),document.removeEventListener("mouseup",P),document.removeEventListener("touchmove",C),document.removeEventListener("touchend",I),document.removeEventListener("click",v),f&&clearTimeout(f)}}}function H(d){return d.hasRuntime||d.runtimeInjected?!1:!!(d.hasNestedCompositions||d.hasTimelines&&d.attempts>=5)}let M=null;function q(){if(M)return M;if(typeof CSSStyleSheet>"u")return null;try{const d=new CSSStyleSheet;return d.replaceSync(N),M=d,d}catch{return null}}const S=30,W="https://cdn.jsdelivr.net/npm/@hyperframes/core/dist/hyperframe.runtime.iife.js",w=class w extends HTMLElement{constructor(){super();p(this,"shadow");p(this,"container");p(this,"iframe");p(this,"posterEl",null);p(this,"controlsApi",null);p(this,"resizeObserver");p(this,"_ready",!1);p(this,"_duration",0);p(this,"_currentTime",0);p(this,"_paused",!0);p(this,"_compositionWidth",1920);p(this,"_compositionHeight",1080);p(this,"_probeInterval",null);p(this,"_lastUpdateMs",0);p(this,"_parentMedia",[]);p(this,"_audioOwner","runtime");p(this,"_mediaObserver");p(this,"_playbackErrorPosted",!1);p(this,"_runtimeInjected",!1);this.shadow=this.attachShadow({mode:"open"});const e=q();if(e)this.shadow.adoptedStyleSheets=[e];else{const t=document.createElement("style");t.textContent=N,this.shadow.appendChild(t)}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","srcdoc","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("srcdoc")&&(this.iframe.srcdoc=this.getAttribute("srcdoc")),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(const t of this._parentMedia)t.el.pause(),t.el.src="";this._parentMedia=[]}attributeChangedCallback(e,t,s){var i,r;switch(e){case"src":s&&(this._ready=!1,this.iframe.src=s);break;case"srcdoc":this._ready=!1,s!==null?this.iframe.srcdoc=s:this.iframe.removeAttribute("srcdoc");break;case"width":this._compositionWidth=parseInt(s||"1920",10),this._updateScale();break;case"height":this._compositionHeight=parseInt(s||"1080",10),this._updateScale();break;case"controls":s!==null?this._setupControls():((i=this.controlsApi)==null||i.destroy(),this.controlsApi=null);break;case"poster":this._setupPoster();break;case"playback-rate":{const n=parseFloat(s||"1");for(const c of this._parentMedia)c.el.playbackRate=n;this._sendControl("set-playback-rate",{playbackRate:n}),(r=this.controlsApi)==null||r.updateSpeed(n),this.dispatchEvent(new Event("ratechange"));break}case"muted":for(const n of this._parentMedia)n.el.muted=s!==null;this._sendControl("set-muted",{muted:s!==null});break;case"audio-src":s&&this._setupParentAudioFromUrl(s);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 t,s;if(!this._trySyncSeek(e)){const i=Math.round(e*S);this._sendControl("seek",{frame:i})}if(this._currentTime=e,this._audioOwner==="parent")for(const i of this._parentMedia){const r=e-i.start;r>=0&&r<i.duration&&(i.el.currentTime=r)}this._paused=!0,(t=this.controlsApi)==null||t.updatePlaying(!1),(s=this.controlsApi)==null||s.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 s;try{(s=this.iframe.contentWindow)==null||s.postMessage({source:"hf-parent",type:"control",action:e,...t},"*")}catch{}}_trySyncSeek(e){try{const t=this.iframe.contentWindow,s=t==null?void 0:t.__player,i=s==null?void 0:s.seek;return typeof i!="function"?!1:(i.call(s,e),!0)}catch{return!1}}_isControlsClick(e){return e.composedPath().some(t=>t instanceof HTMLElement&&t.classList.contains("hfp-controls"))}_onMessage(e){var s,i,r,n;if(e.source!==this.iframe.contentWindow)return;const t=e.data;if(!(!t||t.source!=="hf-preview")){if(t.type==="state"){this._currentTime=(t.frame??0)/S;const c=!this._paused,_=!t.isPlaying,u=this._duration>0&&this._currentTime>=this._duration&&(c||t.isPlaying);if(u&&this.loop){this._audioOwner==="parent"&&this._pauseParentMedia(),this._paused=_,this.seek(0),this.play();return}this._paused=_,this._audioOwner==="parent"&&(c&&this._paused?this._pauseParentMedia():!c&&!this._paused&&this._playParentMedia(),this._mirrorParentMediaTime(this._currentTime));const a=performance.now();(a-this._lastUpdateMs>100||this._paused!==c)&&(this._lastUpdateMs=a,(s=this.controlsApi)==null||s.updateTime(this._currentTime,this._duration),(i=this.controlsApi)==null||i.updatePlaying(!this._paused),this.dispatchEvent(new CustomEvent("timeupdate",{detail:{currentTime:this._currentTime}}))),u&&(this._audioOwner==="parent"&&this._pauseParentMedia(),this._paused=!0,(r=this.controlsApi)==null||r.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/S,(n=this.controlsApi)==null||n.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;const 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 s,i;e++;try{const r=this.iframe.contentWindow;if(!r)return;const n=!!(r.__hf||r.__player),c=!!(r.__timelines&&Object.keys(r.__timelines).length>0),_=!!((s=this.iframe.contentDocument)!=null&&s.querySelector("[data-composition-src]"));if(H({hasRuntime:n,hasTimelines:c,hasNestedCompositions:_,runtimeInjected:this._runtimeInjected,attempts:e})){this._injectRuntime();return}if(this._runtimeInjected&&!n)return;const a=(()=>{var l,f;if(r.__player&&typeof r.__player.getDuration=="function")return r.__player;if(r.__timelines){const m=Object.keys(r.__timelines);if(m.length>0){const v=(f=(l=this.iframe.contentDocument)==null?void 0:l.querySelector("[data-composition-id]"))==null?void 0:f.getAttribute("data-composition-id"),E=v&&v in r.__timelines?v:m[m.length-1],g=r.__timelines[E];return{getDuration:()=>g.duration()}}}return null})();if(a&&a.getDuration()>0){clearInterval(this._probeInterval),this._duration=a.getDuration(),this._ready=!0,(i=this.controlsApi)==null||i.updateTime(0,this._duration),this.dispatchEvent(new CustomEvent("ready",{detail:{duration:this._duration}}));const l=this.iframe.contentDocument,f=l==null?void 0:l.querySelector("[data-composition-id]");if(f){const m=parseInt(f.getAttribute("data-width")||"0",10),v=parseInt(f.getAttribute("data-height")||"0",10);m>0&&v>0&&(this._compositionWidth=m,this._compositionHeight=v,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{const e=this.iframe.contentDocument;if(!e)return;const t=e.createElement("script");t.src=W,t.onload=()=>{},t.onerror=()=>{},(e.head||e.documentElement).appendChild(t)}catch{}}_updateScale(){const e=this.getBoundingClientRect();if(e.width===0||e.height===0)return;const 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;const e={onPlay:()=>this.play(),onPause:()=>this.pause(),onSeek:i=>this.seek(i*this._duration),onSpeedChange:i=>{this.playbackRate=i}},t=this.getAttribute("speed-presets"),s=t?t.split(",").map(Number).filter(i=>!isNaN(i)&&i>0):void 0;this.controlsApi=z(this.shadow,e,{speedPresets:s})}_setupPoster(){var t;const 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(const 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(const e of this._parentMedia)e.el.pause()}_mirrorParentMediaTime(e,t){const s=(t==null?void 0:t.force)===!0,i=w.MIRROR_REQUIRED_CONSECUTIVE_DRIFT_SAMPLES,r=w.MIRROR_DRIFT_THRESHOLD_SECONDS;for(const n of this._parentMedia){const c=e-n.start;if(c<0||c>=n.duration){n.driftSamples=0;continue}Math.abs(n.el.currentTime-c)>r?(n.driftSamples+=1,(s||n.driftSamples>=i)&&(n.el.currentTime=c,n.driftSamples=0)):n.driftSamples=0}}_promoteToParentProxy(){this._audioOwner!=="parent"&&(this._audioOwner="parent",this._sendControl("set-media-output-muted",{muted:!0}),this._mirrorParentMediaTime(this._currentTime,{force:!0}),this._paused||this._playParentMedia(),this.dispatchEvent(new CustomEvent("audioownershipchange",{detail:{owner:"parent",reason:"autoplay-blocked"}})))}_createParentMedia(e,t,s,i){if(this._parentMedia.some(c=>c.el.src===e))return null;const r=t==="video"?document.createElement("video"):new Audio;r.preload="auto",r.src=e,r.load(),r.muted=this.muted,this.playbackRate!==1&&(r.playbackRate=this.playbackRate);const n={el:r,start:s,duration:i,driftSamples:0};return this._parentMedia.push(n),n}_setupParentAudioFromUrl(e){this._createParentMedia(e,"audio",0,1/0)}_setupParentMedia(){try{const e=this.iframe.contentDocument;if(!e)return;const t=e.querySelectorAll("audio[data-start], video[data-start]");for(const s of t)this._adoptIframeMedia(s);this._observeDynamicMedia(e)}catch{}}_adoptIframeMedia(e){var _;const t=e.getAttribute("src")||((_=e.querySelector("source"))==null?void 0:_.getAttribute("src"));if(!t)return;const s=new URL(t,e.ownerDocument.baseURI).href,i=parseFloat(e.getAttribute("data-start")||"0"),r=parseFloat(e.getAttribute("data-duration")||"Infinity"),n=e.tagName==="VIDEO"?"video":"audio",c=this._createParentMedia(s,n,i,r);c&&this._audioOwner==="parent"&&(this._mirrorParentMediaTime(this._currentTime,{force:!0}),!this._paused&&c.el.src&&c.el.play().catch(u=>this._reportPlaybackError(u)))}_observeDynamicMedia(e){if(this._teardownMediaObserver(),typeof MutationObserver>"u"||!e.body)return;const t=new MutationObserver(i=>{var r,n,c,_;for(const u of i){for(const a of u.addedNodes){if(!(a instanceof Element))continue;const l=[];(r=a.matches)!=null&&r.call(a,"audio[data-start], video[data-start]")&&l.push(a);const f=(n=a.querySelectorAll)==null?void 0:n.call(a,"audio[data-start], video[data-start]");if(f)for(const m of f)l.push(m);for(const m of l)this._adoptIframeMedia(m)}for(const a of u.removedNodes){if(!(a instanceof Element))continue;const l=[];(c=a.matches)!=null&&c.call(a,"audio[data-start], video[data-start]")&&l.push(a);const f=(_=a.querySelectorAll)==null?void 0:_.call(a,"audio[data-start], video[data-start]");if(f)for(const m of f)l.push(m);for(const m of l)this._detachIframeMedia(m)}}}),s=e.querySelectorAll("[data-composition-id]");if(s.length>0)for(const i of s)t.observe(i,{childList:!0,subtree:!0});else 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 n;const t=e.getAttribute("src")||((n=e.querySelector("source"))==null?void 0:n.getAttribute("src"));if(!t)return;const s=new URL(t,e.ownerDocument.baseURI).href,i=this._parentMedia.findIndex(c=>c.el.src===s);if(i===-1)return;const r=this._parentMedia[i];r.el.pause(),r.el.src="",this._parentMedia.splice(i,1)}_hidePoster(){var e;(e=this.posterEl)==null||e.remove(),this.posterEl=null}};p(w,"MIRROR_DRIFT_THRESHOLD_SECONDS",.05),p(w,"MIRROR_REQUIRED_CONSECUTIVE_DRIFT_SAMPLES",2);let x=w;customElements.get("hyperframes-player")||customElements.define("hyperframes-player",x);export{x as HyperframesPlayer,j as SPEED_PRESETS,k as formatSpeed,R as formatTime};