@hyperframes/studio 0.8.123 → 0.8.125

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 (48) hide show
  1. package/dist/assets/{hyperframes-player-yABdb6yk.js → hyperframes-player-CmIh-wq9.js} +1 -1
  2. package/dist/assets/{index-D77MIjSJ.js → index-4LRR6o2F.js} +1 -1
  3. package/dist/assets/{index-Dt7lWtY7.js → index-CrYoEZ5p.js} +139 -139
  4. package/dist/assets/{index-LrNlT5Tg.css → index-D11atkBt.css} +1 -1
  5. package/dist/assets/{index-DWjF2Kxw.js → index-yCK8osBg.js} +1 -1
  6. package/dist/index.d.ts +43 -35
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +283 -143
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles.css +1 -1
  11. package/package.json +8 -8
  12. package/src/components/OpenInDesktopButton.dom.test.tsx +27 -10
  13. package/src/components/OpenInDesktopButton.tsx +22 -15
  14. package/src/components/editor/domEditOverlayGestures.ts +2 -0
  15. package/src/components/editor/domEditOverlayStartGesture.ts +6 -0
  16. package/src/components/editor/dragUndoPaint.test.ts +170 -2
  17. package/src/components/editor/gestureUndoRevert.ts +104 -0
  18. package/src/components/editor/manualOffsetDrag.ts +1 -12
  19. package/src/components/editor/useDomEditNudge.ts +1 -1
  20. package/src/components/editor/useDomEditOverlayGestures.ts +9 -5
  21. package/src/hooks/gestureTransaction.test.ts +60 -0
  22. package/src/hooks/gestureTransaction.ts +5 -2
  23. package/src/hooks/gsapGestureOutcome.ts +9 -2
  24. package/src/hooks/gsapResizeDropPoint.test.ts +39 -1
  25. package/src/hooks/gsapResizeElementSize.test.tsx +27 -0
  26. package/src/hooks/gsapResizeIntercept.ts +55 -68
  27. package/src/hooks/gsapScriptCommitTypes.ts +2 -0
  28. package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +29 -1
  29. package/src/hooks/useGsapAwareEditing.test.tsx +1 -0
  30. package/src/hooks/useGsapAwareEditing.ts +21 -25
  31. package/src/hooks/useGsapAwareGroupMove.ts +4 -6
  32. package/src/hooks/useGsapScriptCommits.test.tsx +42 -1
  33. package/src/hooks/useGsapScriptCommits.ts +12 -3
  34. package/src/player/components/AudibleVideoClipContent.test.tsx +67 -0
  35. package/src/player/components/AudibleVideoClipContent.tsx +5 -4
  36. package/src/player/components/AudioWaveform.tsx +2 -1
  37. package/src/player/components/ClipPeakMarks.tsx +5 -2
  38. package/src/player/components/OutOfSyncBadge.tsx +1 -1
  39. package/src/player/components/Timeline.trackPadding.test.tsx +1 -3
  40. package/src/player/components/TimelineCanvas.tsx +1 -0
  41. package/src/player/components/TimelineClip.filmstrip.test.tsx +27 -0
  42. package/src/player/components/TimelineClipFades.test.tsx +2 -1
  43. package/src/player/components/timelineClipDragPreview.test.ts +31 -0
  44. package/src/player/components/timelineClipDragPreview.ts +42 -37
  45. package/src/player/hooks/useTimelinePlayer.seek.test.ts +11 -0
  46. package/src/styles/components.css +33 -3
  47. package/src/utils/studioPendingEdits.test.ts +52 -0
  48. package/src/utils/studioPendingEdits.ts +55 -10
package/dist/index.js CHANGED
@@ -4187,7 +4187,7 @@ function computeDragPreview(drag, clientX, clientY, ctx3) {
4187
4187
  drag.element.key ?? drag.element.id,
4188
4188
  !isMusicTrack(drag.element)
4189
4189
  );
4190
- const snap = snapMoveToTargets(
4190
+ const snapped = snapMoveToTargets(
4191
4191
  nextMove.start,
4192
4192
  drag.element.duration,
4193
4193
  targets,
@@ -4197,6 +4197,9 @@ function computeDragPreview(drag, clientX, clientY, ctx3) {
4197
4197
  dragMaxStart + drag.element.duration,
4198
4198
  ctx3.gridStep
4199
4199
  );
4200
+ const target = snapTarget(snapped);
4201
+ const missed = target !== null && !guideIfSaved(drag.element, { start: snapped.start }, target, pps);
4202
+ const snap = missed ? { start: nextMove.start, snapTime: null, snapType: null } : snapped;
4200
4203
  const group = resolveGroupDrag(drag, ctx3);
4201
4204
  const dragKey = drag.element.key ?? drag.element.id;
4202
4205
  const passengers = ctx3.elements.filter(
@@ -4213,8 +4216,7 @@ function computeDragPreview(drag, clientX, clientY, ctx3) {
4213
4216
  return { ...drag, started: true };
4214
4217
  }
4215
4218
  const { track: previewTrack, insertRow } = placement;
4216
- const target = placement.start === snap.start && snap.snapTime !== null && snap.snapType !== null ? { time: snap.snapTime, type: snap.snapType } : null;
4217
- const guide = guideIfSaved(drag.element, { start: snap.start }, target, pps);
4219
+ const guide = placement.start === snap.start ? snapTarget(snap) : null;
4218
4220
  return {
4219
4221
  ...drag,
4220
4222
  started: true,
@@ -4230,6 +4232,7 @@ function computeDragPreview(drag, clientX, clientY, ctx3) {
4230
4232
  snapType: guide?.type ?? null
4231
4233
  };
4232
4234
  }
4235
+ var snapTarget = (s) => s.snapTime !== null && s.snapType !== null ? { time: s.snapTime, type: s.snapType } : null;
4233
4236
  function guideIfSaved(element, clip, target, pps) {
4234
4237
  if (!target) return null;
4235
4238
  const saved2 = savedClipEdges(element, clip.start, clip.duration);
@@ -4273,30 +4276,30 @@ function computeResizePreview(resize2, clientX, ctx3) {
4273
4276
  );
4274
4277
  const gridStep = ctx3.gridStep ?? 0;
4275
4278
  let snap = null;
4276
- if (trimTargets.length > 0 || gridStep > 0) {
4277
- const snapSecs = TIMELINE_SNAP_PX / Math.max(pps, 1);
4278
- if (resize2.edge === "end") {
4279
- const edgeTime2 = nextResize.start + nextResize.duration;
4280
- const { time: snapped, target } = snapTimelineTime(edgeTime2, trimTargets, snapSecs, gridStep);
4281
- const snappedDuration = Math.round((snapped - nextResize.start) * 1e3) / 1e3;
4282
- if (target && snapped <= maxEnd + 1e-6 && snappedDuration >= resolveTimelineMinDuration() - 1e-6) {
4283
- if (snapped !== edgeTime2) nextResize = { ...nextResize, duration: snappedDuration };
4284
- snap = guideIfSaved(resize2.element, nextResize, target, pps);
4285
- }
4286
- } else {
4287
- const { time: snapped, target } = snapTimelineTime(
4288
- nextResize.start,
4289
- trimTargets,
4290
- snapSecs,
4291
- gridStep
4292
- );
4293
- const clip = { ...nextResize, playbackRate: resize2.element.playbackRate };
4294
- const delta = snapped - nextResize.start;
4295
- const bounds = clipStartTrimDeltaBounds(clip, minStart, resolveTimelineMinDuration());
4296
- if (target && delta >= bounds.minDelta - 1e-6 && delta <= bounds.maxDelta + 1e-6) {
4297
- if (snapped !== nextResize.start) nextResize = applyClipStartTrimDelta(clip, delta);
4298
- snap = guideIfSaved(resize2.element, nextResize, target, pps);
4299
- }
4279
+ const snapSecs = TIMELINE_SNAP_PX / Math.max(pps, 1);
4280
+ if (resize2.edge === "end") {
4281
+ const edgeTime2 = nextResize.start + nextResize.duration;
4282
+ const { time: snapped, target } = snapTimelineTime(edgeTime2, trimTargets, snapSecs, gridStep);
4283
+ const snappedDuration = Math.round((snapped - nextResize.start) * 1e3) / 1e3;
4284
+ if (target && snapped <= maxEnd + 1e-6 && snappedDuration >= resolveTimelineMinDuration() - 1e-6) {
4285
+ const onTarget = snapped === edgeTime2 ? nextResize : { ...nextResize, duration: snappedDuration };
4286
+ snap = guideIfSaved(resize2.element, onTarget, target, pps);
4287
+ if (snap) nextResize = onTarget;
4288
+ }
4289
+ } else {
4290
+ const { time: snapped, target } = snapTimelineTime(
4291
+ nextResize.start,
4292
+ trimTargets,
4293
+ snapSecs,
4294
+ gridStep
4295
+ );
4296
+ const clip = { ...nextResize, playbackRate: resize2.element.playbackRate };
4297
+ const delta = snapped - nextResize.start;
4298
+ const bounds = clipStartTrimDeltaBounds(clip, minStart, resolveTimelineMinDuration());
4299
+ if (target && delta >= bounds.minDelta - 1e-6 && delta <= bounds.maxDelta + 1e-6) {
4300
+ const onTarget = snapped === nextResize.start ? nextResize : applyClipStartTrimDelta(clip, delta);
4301
+ snap = guideIfSaved(resize2.element, onTarget, target, pps);
4302
+ if (snap) nextResize = onTarget;
4300
4303
  }
4301
4304
  }
4302
4305
  return {
@@ -11831,7 +11834,10 @@ function asElement(target) {
11831
11834
  var STUDIO_FLUSH_PENDING_EDITS_EVENT = "hf-studio-flush-pending-edits";
11832
11835
  var pendingEdits = /* @__PURE__ */ new Map();
11833
11836
  var NOT_SAVED = () => Promise.resolve(false);
11834
- var adopting = false;
11837
+ var adopting = null;
11838
+ function adoptingStudioPendingEdit() {
11839
+ return adopting;
11840
+ }
11835
11841
  function waitForPostBlurEffects() {
11836
11842
  return new Promise((resolve) => setTimeout(resolve, 0));
11837
11843
  }
@@ -11874,7 +11880,7 @@ function trackStudioPendingEdit(result) {
11874
11880
  if (!result) return void 0;
11875
11881
  const promise = Promise.resolve(result);
11876
11882
  if (adopting) return promise;
11877
- pendingEdits.set(promise, { revert: null, landed: NOT_SAVED });
11883
+ pendingEdits.set(promise, { revert: null, landed: NOT_SAVED, redraws: [], showAgain: null });
11878
11884
  promise.then(
11879
11885
  () => pendingEdits.delete(promise),
11880
11886
  () => pendingEdits.delete(promise)
@@ -11894,25 +11900,49 @@ function beginStudioPendingEdit(revert) {
11894
11900
  const entry2 = pendingEdits.get(promise);
11895
11901
  entry2.revert = revert;
11896
11902
  let landed = Promise.resolve(false);
11903
+ let saved2 = false;
11897
11904
  entry2.landed = () => landed;
11905
+ const inFlight = {
11906
+ reverted: () => entry2.revert === null && revert !== null,
11907
+ within(run) {
11908
+ const outer = adopting;
11909
+ adopting = inFlight;
11910
+ try {
11911
+ return run();
11912
+ } finally {
11913
+ adopting = outer;
11914
+ }
11915
+ },
11916
+ drawUnlessUndone(draw) {
11917
+ if (inFlight.reverted()) entry2.redraws.push(draw);
11918
+ else draw();
11919
+ },
11920
+ drawKeepingUndone(draw) {
11921
+ if (!inFlight.reverted()) return draw();
11922
+ entry2.showAgain?.();
11923
+ try {
11924
+ return draw();
11925
+ } finally {
11926
+ entry2.showAgain = revert();
11927
+ }
11928
+ },
11929
+ markSaved: () => void (saved2 = true)
11930
+ };
11898
11931
  return {
11899
11932
  settle,
11900
- reverted: () => entry2.revert === null && revert !== null,
11901
- // Only what `start` registers synchronously is adopted; a registration after an await is a newer edit.
11933
+ reverted: inFlight.reverted,
11934
+ // Only what `start` registers synchronously is adopted; a later write joins only through `within`.
11902
11935
  adopt(start) {
11903
- adopting = true;
11904
11936
  try {
11905
- const committed = start();
11937
+ const committed = inFlight.within(start);
11906
11938
  landed = Promise.resolve(committed).then(
11907
11939
  () => true,
11908
- () => false
11940
+ () => saved2
11909
11941
  );
11910
11942
  return committed;
11911
11943
  } catch (error) {
11912
11944
  settle();
11913
11945
  throw error;
11914
- } finally {
11915
- adopting = false;
11916
11946
  }
11917
11947
  }
11918
11948
  };
@@ -11922,7 +11952,14 @@ function paintBackNewestStudioPendingEdit() {
11922
11952
  const revert = newest?.revert;
11923
11953
  if (!newest || !revert) return null;
11924
11954
  newest.revert = null;
11925
- return { showAgain: revert(), landed: newest.landed };
11955
+ newest.showAgain = revert();
11956
+ return {
11957
+ showAgain: () => {
11958
+ newest.showAgain?.();
11959
+ for (const redraw of newest.redraws.splice(0)) redraw();
11960
+ },
11961
+ landed: newest.landed
11962
+ };
11926
11963
  }
11927
11964
  function revertNewestStudioPendingEdit() {
11928
11965
  return paintBackNewestStudioPendingEdit()?.showAgain ?? null;
@@ -15955,6 +15992,7 @@ import { jsx as jsx30, jsxs as jsxs17 } from "react/jsx-runtime";
15955
15992
  var BAR_STEP = 3;
15956
15993
  var rendersWaveform = (el) => el.tag === "audio";
15957
15994
  var FADE_GHOST_OPACITY = 0.27;
15995
+ var WAVEFORM_LAYER_Z = 10;
15958
15996
  function paintWaveformBars(context, bars, height, waveformBarRgb, waveformBaselineRgb, amplitudes) {
15959
15997
  bars.forEach((bar, index) => {
15960
15998
  const opacity = loudnessToOpacity(amplitudes[index] ?? 0);
@@ -16113,7 +16151,7 @@ var AudioWaveform = memo10(function AudioWaveform2({
16113
16151
  else clip.removeAttribute("data-audio-muted");
16114
16152
  return () => clip.removeAttribute("data-audio-muted");
16115
16153
  }, [muted]);
16116
- return /* @__PURE__ */ jsx30("div", { ref: rootRef, className: "absolute inset-0", children: /* @__PURE__ */ jsxs17("div", { className: "absolute inset-0 overflow-hidden", style: { zIndex: 10 }, children: [
16154
+ return /* @__PURE__ */ jsx30("div", { ref: rootRef, className: "absolute inset-0", children: /* @__PURE__ */ jsxs17("div", { className: "absolute inset-0 overflow-hidden", style: { zIndex: WAVEFORM_LAYER_Z }, children: [
16117
16155
  /* @__PURE__ */ jsx30(
16118
16156
  "canvas",
16119
16157
  {
@@ -18468,7 +18506,7 @@ var OutOfSyncBadge = memo12(function OutOfSyncBadge2({ el }) {
18468
18506
  "aria-label": `Out of sync with ${partnerName} by ${label} frames`,
18469
18507
  title: `Out of sync with ${partnerName} by ${label} \u2014 click for Move or Slip into Sync`,
18470
18508
  "data-testid": "out-of-sync-badge",
18471
- className: "absolute bottom-0.5 left-1 z-[7] cursor-pointer rounded-[3px] bg-red-600 px-1 font-mono text-[9px] leading-[14px] text-white tabular-nums",
18509
+ className: "absolute bottom-0.5 left-1 z-[12] cursor-pointer rounded-[3px] bg-red-600 px-1 font-mono text-[9px] leading-[14px] text-white tabular-nums",
18472
18510
  onPointerDown: stop,
18473
18511
  onDoubleClick: stop,
18474
18512
  onClick: open,
@@ -24016,6 +24054,7 @@ var TimelineCanvas = memo16(function TimelineCanvas2() {
24016
24054
  draggedClip?.started && draggedClip.insertRow == null && draggedRowIndex >= 0 && /* @__PURE__ */ jsx59(
24017
24055
  "div",
24018
24056
  {
24057
+ "data-testid": "timeline-drag-landing",
24019
24058
  className: "absolute pointer-events-none",
24020
24059
  style: {
24021
24060
  top: props.rowGeometry.getRowTop(draggedRowIndex) + CLIP_Y,
@@ -37145,14 +37184,6 @@ function restoreManualOffsetDragMember(member) {
37145
37184
  }
37146
37185
  endStudioManualEditGesture(member.element, member.gestureToken);
37147
37186
  }
37148
- function manualOffsetMoveRevert(members) {
37149
- if (!members.every((member) => member.plainTranslate)) return null;
37150
- return () => {
37151
- const shown2 = members.map((member) => captureStudioPathOffset(member.element));
37152
- for (const member of members) restoreStudioPathOffset(member.element, member.initialPathOffset);
37153
- return () => members.forEach((member, i) => restoreStudioPathOffset(member.element, shown2[i]));
37154
- };
37155
- }
37156
37187
  function restoreManualOffsetDragMembers(members) {
37157
37188
  for (const member of members) {
37158
37189
  restoreManualOffsetDragMember(member);
@@ -37197,6 +37228,70 @@ function resumeGsapTimelines(element) {
37197
37228
  win.__player?.seek?.(t2);
37198
37229
  }
37199
37230
 
37231
+ // src/components/editor/gestureUndoRevert.ts
37232
+ var PLACEMENT = ["position", "left", "top"];
37233
+ var placementOf = (element) => PLACEMENT.map((prop) => element.style.getPropertyValue(prop));
37234
+ function showPlacement(element, placement) {
37235
+ PLACEMENT.forEach((prop, i) => {
37236
+ if (placement[i]) element.style.setProperty(prop, placement[i]);
37237
+ else element.style.removeProperty(prop);
37238
+ });
37239
+ }
37240
+ function readMemberPosition(member) {
37241
+ const gsap3 = member.plainTranslate ? null : getOffsetDragGsap(member.element);
37242
+ return {
37243
+ offset: captureStudioPathOffset(member.element),
37244
+ gsap: gsap3 && {
37245
+ x: Number(gsap3.getProperty(member.element, "x")),
37246
+ y: Number(gsap3.getProperty(member.element, "y"))
37247
+ },
37248
+ placement: placementOf(member.element)
37249
+ };
37250
+ }
37251
+ function showMemberPosition(member, position) {
37252
+ restoreStudioPathOffset(member.element, position.offset);
37253
+ showPlacement(member.element, position.placement);
37254
+ if (position.gsap) getOffsetDragGsap(member.element)?.set(member.element, { ...position.gsap });
37255
+ if (!member.plainTranslate)
37256
+ stampGestureBase(member.element, member.initialOffset, member.baseGsap);
37257
+ }
37258
+ function manualOffsetMoveRevert(members) {
37259
+ const startPlacement = members.map((member) => placementOf(member.element));
37260
+ return () => {
37261
+ const shown2 = members.map(readMemberPosition);
37262
+ members.forEach(
37263
+ (member, i) => showMemberPosition(member, {
37264
+ offset: member.initialPathOffset,
37265
+ gsap: member.plainTranslate ? null : member.baseGsap,
37266
+ placement: startPlacement[i]
37267
+ })
37268
+ );
37269
+ return () => members.forEach((member, i) => showMemberPosition(member, shown2[i]));
37270
+ };
37271
+ }
37272
+ var GSAP_LOOK_PROPS = ["x", "y", "rotation", "scaleX", "scaleY"];
37273
+ function readElementLook(element, gsapOwned) {
37274
+ const gsap3 = gsapOwned ? getOffsetDragGsap(element) : null;
37275
+ return {
37276
+ style: element.getAttribute("style"),
37277
+ gsap: gsap3 && Object.fromEntries(
37278
+ GSAP_LOOK_PROPS.map((prop) => [prop, Number(gsap3.getProperty(element, prop))])
37279
+ )
37280
+ };
37281
+ }
37282
+ function showElementLook(element, look) {
37283
+ if (look.style === null) element.removeAttribute("style");
37284
+ else element.setAttribute("style", look.style);
37285
+ if (look.gsap) getOffsetDragGsap(element)?.set(element, { ...look.gsap });
37286
+ }
37287
+ function elementLookRevert(element, start) {
37288
+ return () => {
37289
+ const shown2 = readElementLook(element, start.gsap !== null);
37290
+ showElementLook(element, start);
37291
+ return () => showElementLook(element, shown2);
37292
+ };
37293
+ }
37294
+
37200
37295
  // src/components/editor/rotationDraft.ts
37201
37296
  function lastRuleTransform(element) {
37202
37297
  let value = "";
@@ -37975,6 +38070,10 @@ function startGesture(kind, e, opts, options) {
37975
38070
  const rawContentScaleY = rect.height / (rect.editScaleY * actualHeight);
37976
38071
  const contentScaleX = Number.isFinite(rawContentScaleX) && rawContentScaleX > 0 ? rawContentScaleX : 1;
37977
38072
  const contentScaleY = Number.isFinite(rawContentScaleY) && rawContentScaleY > 0 ? rawContentScaleY : 1;
38073
+ const initialLook = readElementLook(
38074
+ sel.element,
38075
+ kind !== "drag" && !editsPlainCss(sel.element, kind === "rotate" ? "rotate" : "resize")
38076
+ );
37978
38077
  let initialPathOffset = captureStudioPathOffset(sel.element);
37979
38078
  let manualEditDragToken;
37980
38079
  let pathOffsetMember;
@@ -38054,6 +38153,7 @@ function startGesture(kind, e, opts, options) {
38054
38153
  initialPathOffset,
38055
38154
  initialRotation: captureStudioRotation(sel.element),
38056
38155
  initialBoxSize: captureStudioBoxSize(sel.element),
38156
+ initialLook,
38057
38157
  pathOffsetMember,
38058
38158
  originLeft: rect.left,
38059
38159
  originTop: rect.top,
@@ -38555,11 +38655,13 @@ function createDomEditOverlayGestureHandlers(opts) {
38555
38655
  }
38556
38656
  applyRotationDraft(sel.element, finalRotation.angle, g.plainRotation);
38557
38657
  const commit = { ...finalRotation, plain: g.plainRotation };
38558
- void Promise.resolve(opts.onRotationCommitRef.current(sel, commit)).then((result) => trackPreviewEditResult("rotate", "drag", result)).catch((error) => {
38658
+ const edit = beginStudioPendingEdit(elementLookRevert(sel.element, g.initialLook));
38659
+ const saved2 = Promise.resolve(edit.adopt(() => opts.onRotationCommitRef.current(sel, commit))).then((result) => trackPreviewEditResult("rotate", "drag", result)).catch((error) => {
38559
38660
  logGestureCommitFailure("rotate commit failed", error);
38560
38661
  if (g.manualEditDragToken && isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken))
38561
38662
  restoreRotation();
38562
38663
  }).finally(() => endStudioManualEditGesture(sel.element, g.manualEditDragToken));
38664
+ edit.settle(saved2);
38563
38665
  } else if (g.kind === "drag") {
38564
38666
  opts.suppressNextBoxClickRef.current = true;
38565
38667
  const dx = g.lastSnappedDx ?? e.clientX - g.startX;
@@ -38619,14 +38721,15 @@ function createDomEditOverlayGestureHandlers(opts) {
38619
38721
  restoreStudioBoxSize(sel.element, g.initialBoxSize);
38620
38722
  if (finalOffset) restoreStudioPathOffset(sel.element, g.initialPathOffset);
38621
38723
  };
38622
- void Promise.resolve(
38623
- opts.onBoxSizeCommitRef.current(sel, finalSize, finalOffset ?? void 0, restore2, member)
38624
- ).then((result) => trackPreviewEditResult("resize", "drag", result)).catch((error) => {
38724
+ const commitSize = () => opts.onBoxSizeCommitRef.current(sel, finalSize, finalOffset ?? void 0, restore2, member);
38725
+ const edit = beginStudioPendingEdit(elementLookRevert(sel.element, g.initialLook));
38726
+ const saved2 = Promise.resolve(edit.adopt(commitSize)).then((result) => trackPreviewEditResult("resize", "drag", result)).catch((error) => {
38625
38727
  logGestureCommitFailure("resize commit failed", error);
38626
38728
  }).finally(() => {
38627
38729
  if (member) endManualOffsetDragMembers([member]);
38628
38730
  else endStudioManualEditGesture(sel.element, g.manualEditDragToken);
38629
38731
  });
38732
+ edit.settle(saved2);
38630
38733
  logResizeSettle(sel.element, "post-release");
38631
38734
  }
38632
38735
  };
@@ -44460,6 +44563,8 @@ function trackKeyframeCommit(mutations, result, options, memberOptions) {
44460
44563
 
44461
44564
  // src/hooks/gsapGestureOutcome.ts
44462
44565
  function observeGsapGesture(writer) {
44566
+ const edit = adoptingStudioPendingEdit();
44567
+ const join = (run) => edit ? edit.within(run) : run();
44463
44568
  let changed = false;
44464
44569
  let pendingResults = 0;
44465
44570
  const actions = /* @__PURE__ */ new Set();
@@ -44467,12 +44572,14 @@ function observeGsapGesture(writer) {
44467
44572
  pendingResults += 1;
44468
44573
  return {
44469
44574
  ...options,
44575
+ ...edit && { pendingEdit: edit },
44470
44576
  keyframeTelemetry: false,
44471
44577
  onResult: (result) => {
44472
44578
  pendingResults -= 1;
44473
44579
  options.onResult?.(result);
44474
44580
  if (!result.ok || result.changed !== true) return;
44475
44581
  changed = true;
44582
+ edit?.markSaved();
44476
44583
  const members = changedMutationIndices(
44477
44584
  result,
44478
44585
  calls.length,
@@ -44488,16 +44595,18 @@ function observeGsapGesture(writer) {
44488
44595
  };
44489
44596
  let commit = null;
44490
44597
  if (writer) {
44491
- commit = (selection, mutation, options) => writer(selection, mutation, observe([{ selection, mutation, options }], options));
44598
+ commit = (selection, mutation, options) => join(() => writer(selection, mutation, observe([{ selection, mutation, options }], options)));
44492
44599
  if (writer.batch) {
44493
44600
  const batch = writer.batch;
44494
- commit.batch = (calls, options) => batch(calls, observe(calls, options));
44601
+ commit.batch = (calls, options) => join(() => batch(calls, observe(calls, options)));
44495
44602
  }
44496
44603
  }
44497
44604
  return {
44498
44605
  commit,
44606
+ drawKeepingUndone: (draw) => edit ? edit.drawKeepingUndone(draw) : draw(),
44499
44607
  recordDomResult: (result) => {
44500
44608
  changed ||= result?.changed === true;
44609
+ if (result?.changed) edit?.markSaved();
44501
44610
  },
44502
44611
  finish: (domChanged = false) => {
44503
44612
  if (pendingResults !== 0) return { ok: true, changed: false };
@@ -44741,7 +44850,8 @@ function runGestureTransaction(tx) {
44741
44850
  logCommit("start", { label: tx.label, coalesceKey });
44742
44851
  tx.settle();
44743
44852
  logCommit("settled", { label: tx.label, coalesceKey });
44744
- const before = !tx.skipPixelAssert ? readPixelRect(tx.element) : null;
44853
+ const measure = () => tx.draw ? tx.draw(() => readPixelRect(tx.element)) : readPixelRect(tx.element);
44854
+ const before = !tx.skipPixelAssert ? measure() : null;
44745
44855
  const commit = (commitMutation) => {
44746
44856
  const wrapped = (selection, mutation, options) => {
44747
44857
  mutationCount += 1;
@@ -44762,7 +44872,7 @@ function runGestureTransaction(tx) {
44762
44872
  const durationMs = Math.round(performance.now() - startedAt2);
44763
44873
  logCommit("persisted", { label: tx.label, coalesceKey });
44764
44874
  if (before) {
44765
- const after = readPixelRect(tx.element);
44875
+ const after = measure();
44766
44876
  const delta = pixelDelta(before, after);
44767
44877
  if (exceedsPixelTolerance(delta)) {
44768
44878
  logCommit("persist-changed-pixels", { label: tx.label, before, after, delta });
@@ -50698,7 +50808,8 @@ function TextClipContent({ text }) {
50698
50808
 
50699
50809
  // src/player/components/AudibleVideoClipContent.tsx
50700
50810
  import { jsx as jsx115, jsxs as jsxs84 } from "react/jsx-runtime";
50701
- var AUDIBLE_VIDEO_WAVE_SHARE = 0.38;
50811
+ var AUDIBLE_VIDEO_WAVE_SHARE = 0.5;
50812
+ var AUDIO_CLIP_SURFACE = "var(--timeline-clip-audio-bg)";
50702
50813
  function AudibleVideoClipContent({
50703
50814
  thumbnail,
50704
50815
  waveform
@@ -50709,9 +50820,9 @@ function AudibleVideoClipContent({
50709
50820
  /* @__PURE__ */ jsx115(
50710
50821
  "div",
50711
50822
  {
50712
- className: "absolute inset-x-0 bottom-0 bg-black/30",
50823
+ className: "audible-video-wave absolute inset-x-0 bottom-0",
50713
50824
  "data-testid": "audible-video-wave",
50714
- style: { height: waveHeight },
50825
+ style: { height: waveHeight, backgroundColor: AUDIO_CLIP_SURFACE },
50715
50826
  children: waveform
50716
50827
  }
50717
50828
  )
@@ -50760,10 +50871,20 @@ function ClipPeakMarks({
50760
50871
  },
50761
50872
  run.from
50762
50873
  )),
50763
- /* @__PURE__ */ jsxs85("span", { className: "absolute top-0 right-1 font-mono text-[9px] leading-none text-red-400", children: [
50764
- "\u25B2 peaks ",
50765
- dbText2(peakDbfs)
50766
- ] })
50874
+ /* @__PURE__ */ jsxs85(
50875
+ "span",
50876
+ {
50877
+ className: "absolute top-0 right-1 font-mono text-[9px] leading-none text-red-400",
50878
+ "data-peak-badge": true,
50879
+ children: [
50880
+ "\u25B2",
50881
+ /* @__PURE__ */ jsxs85("span", { "data-peak-text": true, children: [
50882
+ " peaks ",
50883
+ dbText2(peakDbfs)
50884
+ ] })
50885
+ ]
50886
+ }
50887
+ )
50767
50888
  ]
50768
50889
  }
50769
50890
  )
@@ -81164,13 +81285,22 @@ async function runMutationRequest(mutations, options, showToast, request) {
81164
81285
  function finishUnchangedMutation(iframe, result, options, reloadPreview) {
81165
81286
  if (result.changed !== false) return false;
81166
81287
  if (!options.skipReload && (instantPatchesFor(options).length > 0 || options.previewFallbackLatch?.pending)) {
81167
- applyPreviewSync(iframe, result, options, reloadPreview);
81288
+ redrawUnlessPaintedBack(
81289
+ options,
81290
+ () => applyPreviewSync(iframe, result, options, reloadPreview)
81291
+ );
81168
81292
  }
81169
81293
  return true;
81170
81294
  }
81295
+ function redrawUnlessPaintedBack(options, redraw) {
81296
+ if (options.pendingEdit) options.pendingEdit.drawUnlessUndone(redraw);
81297
+ else redraw();
81298
+ }
81171
81299
  function refreshMutationPreview(iframe, result, options, reloadPreview, onCacheInvalidate, nestedFiles) {
81172
- options.beforeReload?.();
81173
- applyPreviewSync(iframe, result, options, reloadPreview, nestedFiles);
81300
+ redrawUnlessPaintedBack(options, () => {
81301
+ options.beforeReload?.();
81302
+ applyPreviewSync(iframe, result, options, reloadPreview, nestedFiles);
81303
+ });
81174
81304
  onCacheInvalidate();
81175
81305
  }
81176
81306
  function isActiveCommitTarget(projectIdRef, activeCompPathRef, projectId, compositionPath) {
@@ -82192,12 +82322,10 @@ function useGsapAwareGroupMove({
82192
82322
  renderOnCommit = index === lastScriptWrite;
82193
82323
  const plain = offsetMembers.get(selection);
82194
82324
  if (plain !== void 0) {
82195
- const result = await stageElementPositionOffset(
82196
- selection,
82197
- next,
82198
- plain,
82199
- coalesceKey
82200
- ).save();
82325
+ const staged = writes.drawKeepingUndone(
82326
+ () => stageElementPositionOffset(selection, next, plain, coalesceKey)
82327
+ );
82328
+ const result = await staged.save();
82201
82329
  writes.recordDomResult(result);
82202
82330
  continue;
82203
82331
  }
@@ -82325,7 +82453,7 @@ function tweenUsesScaleLonghands(anim) {
82325
82453
  );
82326
82454
  return inKeyframes || Object.keys(anim?.properties ?? {}).some(isLonghand);
82327
82455
  }
82328
- async function tryGsapResizeIntercept(selection, size, animations, iframe, commitMutation, fetchFallbackAnimations, dragOffset) {
82456
+ async function tryGsapResizeIntercept(selection, size, animations, iframe, commitMutation, fetchFallbackAnimations, dragOffset, draw = (run) => run()) {
82329
82457
  const fetchedAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : [];
82330
82458
  const outcome = preflightGsapResizeIntercept(selection, animations, iframe, fetchedAnimations);
82331
82459
  if (outcome.status === "blocked") return outcome;
@@ -82421,7 +82549,7 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82421
82549
  scaleDraftEl = el;
82422
82550
  committedScale = useScaleLonghands ? { x: newScaleX, y: newScaleY } : { x: newScaleX, y: newScaleX };
82423
82551
  if (el) {
82424
- const dropRect = el.getBoundingClientRect();
82552
+ const dropRect = draw(() => el.getBoundingClientRect());
82425
82553
  scaleDraftDropPoint = { x: dropRect.x, y: dropRect.y };
82426
82554
  }
82427
82555
  } else {
@@ -82433,39 +82561,50 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82433
82561
  }
82434
82562
  const finalizeScaleResizeCommit = async () => {
82435
82563
  if (!scaleDraftEl) return false;
82436
- clearStudioBoxSize(scaleDraftEl);
82437
- if (!scaleDraftDropPoint || !selector) return false;
82438
- if (committedScale) {
82439
- setElementGsapScale(scaleDraftEl, committedScale.x, committedScale.y);
82440
- }
82441
- const gsapPos = readGsapPositionFromIframe(iframe, selector) ?? { x: 0, y: 0 };
82442
- const { baseGsapX, baseGsapY } = computeDraggedGsapPosition(
82443
- selection.element,
82444
- { x: 0, y: 0 },
82445
- gsapPos
82446
- );
82447
- const base2 = { x: baseGsapX, y: baseGsapY };
82448
- setElementGsapPosition(scaleDraftEl, base2.x, base2.y);
82449
- const post2 = scaleDraftEl.getBoundingClientRect();
82450
- const residual = { x: scaleDraftDropPoint.x - post2.x, y: scaleDraftDropPoint.y - post2.y };
82451
- if (!Number.isFinite(residual.x) || !Number.isFinite(residual.y)) return false;
82452
- if (Math.abs(residual.x) < 0.5 && Math.abs(residual.y) < 0.5) {
82453
- logResize("scale-finalize", { skipped: "already-on-drop-point", residual, base: base2 });
82454
- return true;
82564
+ const draftEl = scaleDraftEl;
82565
+ if (!scaleDraftDropPoint || !selector) {
82566
+ draw(() => clearStudioBoxSize(draftEl));
82567
+ return false;
82455
82568
  }
82456
- const corrected = {
82457
- x: roundTo3(base2.x + residual.x),
82458
- y: roundTo3(base2.y + residual.y)
82459
- };
82460
- logResize("scale-finalize", {
82461
- dropPoint: scaleDraftDropPoint,
82462
- post: { x: post2.x, y: post2.y },
82463
- residual,
82464
- gsapPos,
82465
- base: base2,
82466
- corrected
82569
+ const dropPoint = scaleDraftDropPoint;
82570
+ const measured = draw(() => {
82571
+ clearStudioBoxSize(draftEl);
82572
+ if (committedScale) {
82573
+ setElementGsapScale(draftEl, committedScale.x, committedScale.y);
82574
+ }
82575
+ const gsapPos = readGsapPositionFromIframe(iframe, selector) ?? { x: 0, y: 0 };
82576
+ const { baseGsapX, baseGsapY } = computeDraggedGsapPosition(
82577
+ selection.element,
82578
+ { x: 0, y: 0 },
82579
+ gsapPos
82580
+ );
82581
+ const base3 = { x: baseGsapX, y: baseGsapY };
82582
+ setElementGsapPosition(draftEl, base3.x, base3.y);
82583
+ const post2 = draftEl.getBoundingClientRect();
82584
+ const residual = { x: dropPoint.x - post2.x, y: dropPoint.y - post2.y };
82585
+ if (!Number.isFinite(residual.x) || !Number.isFinite(residual.y)) return null;
82586
+ if (Math.abs(residual.x) < 0.5 && Math.abs(residual.y) < 0.5) {
82587
+ logResize("scale-finalize", { skipped: "already-on-drop-point", residual, base: base3 });
82588
+ return "settled";
82589
+ }
82590
+ const corrected2 = {
82591
+ x: roundTo3(base3.x + residual.x),
82592
+ y: roundTo3(base3.y + residual.y)
82593
+ };
82594
+ logResize("scale-finalize", {
82595
+ dropPoint,
82596
+ post: { x: post2.x, y: post2.y },
82597
+ residual,
82598
+ gsapPos,
82599
+ base: base3,
82600
+ corrected: corrected2
82601
+ });
82602
+ setElementGsapPosition(draftEl, corrected2.x, corrected2.y);
82603
+ return { base: base3, corrected: corrected2 };
82467
82604
  });
82468
- setElementGsapPosition(scaleDraftEl, corrected.x, corrected.y);
82605
+ if (measured === null) return false;
82606
+ if (measured === "settled") return true;
82607
+ const { base: base2, corrected } = measured;
82469
82608
  const currentAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : resolved2?.animations ?? animations;
82470
82609
  const delta = { x: corrected.x - base2.x, y: corrected.y - base2.y };
82471
82610
  const positionTween = pickClosestToPlayhead(
@@ -82916,8 +83055,10 @@ function useGsapAwareEditing({
82916
83055
  modifiers
82917
83056
  );
82918
83057
  await saveMove(outcome, async () => {
82919
- const result = await stageElementPositionOffset(selection, next, false).save();
82920
- writes.recordDomResult(result);
83058
+ const staged = writes.drawKeepingUndone(
83059
+ () => stageElementPositionOffset(selection, next, false)
83060
+ );
83061
+ writes.recordDomResult(await staged.save());
82921
83062
  });
82922
83063
  return writes.finish();
82923
83064
  } catch (error) {
@@ -82990,11 +83131,8 @@ function useGsapAwareEditing({
82990
83131
  );
82991
83132
  await saveMove(dragOutcome, async () => {
82992
83133
  const plainAfterSettle = editsPlainCss(selection.element, "move");
82993
- anchorMove = stageElementPositionOffset(
82994
- selection,
82995
- offset,
82996
- plainAfterSettle,
82997
- coalesceKey
83134
+ anchorMove = writes.drawKeepingUndone(
83135
+ () => stageElementPositionOffset(selection, offset, plainAfterSettle, coalesceKey)
82998
83136
  );
82999
83137
  });
83000
83138
  }
@@ -83002,15 +83140,18 @@ function useGsapAwareEditing({
83002
83140
  await runGestureTransaction({
83003
83141
  element: selection.element,
83004
83142
  label: "Resize layer",
83143
+ draw: writes.drawKeepingUndone,
83005
83144
  settle: () => {
83006
83145
  if (!offset || scaleRoute || !selector) return;
83007
- const gsapPos = readGsapPositionFromIframe(previewIframeRef.current, selector) ?? {
83008
- x: 0,
83009
- y: 0
83010
- };
83011
- const { newX, newY } = computeDraggedGsapPosition(selection.element, offset, gsapPos);
83012
- logResize("sync-settle", { gsapPos, offset, newX, newY });
83013
- setElementGsapPosition(selection.element, newX, newY);
83146
+ writes.drawKeepingUndone(() => {
83147
+ const gsapPos = readGsapPositionFromIframe(previewIframeRef.current, selector) ?? {
83148
+ x: 0,
83149
+ y: 0
83150
+ };
83151
+ const { newX, newY } = computeDraggedGsapPosition(selection.element, offset, gsapPos);
83152
+ logResize("sync-settle", { gsapPos, offset, newX, newY });
83153
+ setElementGsapPosition(selection.element, newX, newY);
83154
+ });
83014
83155
  },
83015
83156
  persist: async (commit, coalesceKey) => {
83016
83157
  if (writes.commit) {
@@ -83023,16 +83164,13 @@ function useGsapAwareEditing({
83023
83164
  previewIframeRef.current,
83024
83165
  commitMutation2,
83025
83166
  makeFetchFallback(selection),
83026
- offset
83167
+ offset,
83168
+ writes.drawKeepingUndone
83027
83169
  );
83028
83170
  assertGsapEditPersisted(outcome);
83029
83171
  if (outcome.status === "element-size") {
83030
- const result = await handleDomBoxSizeCommit(
83031
- selection,
83032
- next,
83033
- void 0,
83034
- void 0,
83035
- coalesceKey
83172
+ const result = await writes.drawKeepingUndone(
83173
+ () => handleDomBoxSizeCommit(selection, next, void 0, void 0, coalesceKey)
83036
83174
  );
83037
83175
  writes.recordDomResult(result);
83038
83176
  } else cropUndoKey = coalesceKey;
@@ -83056,11 +83194,8 @@ function useGsapAwareEditing({
83056
83194
  const anchorResult = await anchorMove?.save();
83057
83195
  writes.recordDomResult(anchorResult);
83058
83196
  if (cropUndoKey) {
83059
- const cropResult = await saveCropResize(
83060
- stageCrop,
83061
- selection,
83062
- commitPositionPatchToHtml,
83063
- cropUndoKey
83197
+ const cropResult = await writes.drawKeepingUndone(
83198
+ () => saveCropResize(stageCrop, selection, commitPositionPatchToHtml, cropUndoKey)
83064
83199
  );
83065
83200
  writes.recordDomResult(cropResult);
83066
83201
  }
@@ -87441,7 +87576,6 @@ function FrameyGlyph({
87441
87576
  // src/components/OpenInDesktopButton.tsx
87442
87577
  import { jsx as jsx203, jsxs as jsxs161 } from "react/jsx-runtime";
87443
87578
  var ROUTE = "/api/open-in-desktop";
87444
- var DOWNLOAD_URL = "https://hyperframes.dev/studio/download";
87445
87579
  var GAZE = 2.2;
87446
87580
  var OPENING_MS = 1600;
87447
87581
  function useDesktopRoute() {
@@ -87449,7 +87583,10 @@ function useDesktopRoute() {
87449
87583
  useEffect133(() => {
87450
87584
  let live = true;
87451
87585
  studioApiFetch(ROUTE).then((res) => res.ok ? res.json() : null).then((body) => {
87452
- if (live && body?.available === true) setRoute({ handoff: body.handoff === true });
87586
+ if (!live || body?.available !== true) return;
87587
+ const handoff = body.handoff === true;
87588
+ const downloadUrl = typeof body.downloadUrl === "string" ? body.downloadUrl : null;
87589
+ if (handoff || downloadUrl) setRoute({ handoff, downloadUrl });
87453
87590
  }).catch(() => {
87454
87591
  });
87455
87592
  return () => {
@@ -87491,7 +87628,7 @@ function OpenInDesktopButton() {
87491
87628
  const open = async () => {
87492
87629
  if (!route.handoff) {
87493
87630
  trackStudioEvent("toolbar_action", { action: "open_in_desktop" });
87494
- setDownloadUrl(DOWNLOAD_URL);
87631
+ setDownloadUrl(route.downloadUrl);
87495
87632
  return;
87496
87633
  }
87497
87634
  if (opening) return;
@@ -87501,7 +87638,7 @@ function OpenInDesktopButton() {
87501
87638
  const res = await studioApiFetch(ROUTE, { method: "POST" });
87502
87639
  const result = await res.json();
87503
87640
  if (result.opened) {
87504
- showToast("Framey is opening this project in HyperFrames Studio", "info");
87641
+ showToast("Framey is opening this project in the HyperFrames desktop app", "info");
87505
87642
  await new Promise((done) => setTimeout(done, OPENING_MS));
87506
87643
  } else setDownloadUrl(result.downloadUrl);
87507
87644
  } catch {
@@ -87523,17 +87660,20 @@ function OpenInDesktopButton() {
87523
87660
  arrow: true,
87524
87661
  "aria-label": "Meet Framey",
87525
87662
  className: "w-72",
87526
- trigger: /* @__PURE__ */ jsx203(
87663
+ trigger: /* @__PURE__ */ jsxs161(
87527
87664
  Button,
87528
87665
  {
87529
87666
  ref: trigger,
87530
87667
  variant: "ghost",
87531
87668
  "data-testid": "header-open-in-desktop",
87532
87669
  "data-opening": opening || void 0,
87533
- title: "Open this project in HyperFrames Studio, the desktop app, and edit it with Framey",
87534
- className: "hf-framey-trigger gap-1.5 pl-1.5",
87535
- icon: /* @__PURE__ */ jsx203(FrameyGlyph, { size: 20, className: "hf-framey", eyeRef: eye }),
87536
- children: opening ? "Opening in the app\u2026" : "Edit with Framey"
87670
+ title: "Open this project in the HyperFrames desktop app and edit it with Framey",
87671
+ className: "hf-framey-trigger gap-1.5 border border-accent/40 bg-accent/10 pl-1 font-semibold text-text-0 enabled:hover:bg-accent/20",
87672
+ icon: /* @__PURE__ */ jsx203(FrameyGlyph, { size: 24, className: "hf-framey", eyeRef: eye }),
87673
+ children: [
87674
+ opening ? "Opening in the app\u2026" : "Edit with Framey",
87675
+ !opening && /* @__PURE__ */ jsx203("span", { className: "rounded-full bg-accent px-1.5 text-[10px] leading-4 font-bold text-on-accent", children: "New" })
87676
+ ]
87537
87677
  }
87538
87678
  ),
87539
87679
  children: [
@@ -87541,7 +87681,7 @@ function OpenInDesktopButton() {
87541
87681
  /* @__PURE__ */ jsx203(FrameyGlyph, { size: 36 }),
87542
87682
  /* @__PURE__ */ jsx203("p", { className: "text-step-12 font-medium text-text-0", children: "Meet Framey" })
87543
87683
  ] }),
87544
- /* @__PURE__ */ jsx203("p", { className: "mt-2 text-text-3", children: "I live in HyperFrames Studio, the free desktop app for macOS:" }),
87684
+ /* @__PURE__ */ jsx203("p", { className: "mt-2 text-balance text-text-3", children: "I live in the free HyperFrames desktop app:" }),
87545
87685
  /* @__PURE__ */ jsxs161("ul", { className: "mt-1.5 grid gap-1 text-text-1", children: [
87546
87686
  /* @__PURE__ */ jsx203("li", { children: "Chat to change anything in the video" }),
87547
87687
  /* @__PURE__ */ jsx203("li", { children: "Draw on a frame to point at it" }),
@@ -87557,7 +87697,7 @@ function OpenInDesktopButton() {
87557
87697
  rel: "noopener noreferrer",
87558
87698
  className: cn(buttonBase, buttonVariants.primary, buttonSizes.md),
87559
87699
  onClick: () => trackStudioEvent("toolbar_action", { action: "get_desktop_app" }),
87560
- children: "Get HyperFrames Studio"
87700
+ children: "Get the desktop app"
87561
87701
  }
87562
87702
  )
87563
87703
  ] })
@@ -92969,7 +93109,7 @@ var RenderQueueItem = memo54(function RenderQueueItem2({
92969
93109
  // src/components/renders/FfmpegRequiredNotice.tsx
92970
93110
  import { memo as memo55, useEffect as useEffect148, useRef as useRef181, useState as useState148 } from "react";
92971
93111
  import { jsx as jsx228, jsxs as jsxs183 } from "react/jsx-runtime";
92972
- var DOWNLOAD_URL2 = "https://ffmpeg.org/download.html";
93112
+ var DOWNLOAD_URL = "https://ffmpeg.org/download.html";
92973
93113
  var CUE_MS = 1600;
92974
93114
  var FOCUS_RING = "outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-studio-accent";
92975
93115
  var FfmpegRequiredNotice = memo55(function FfmpegRequiredNotice2({
@@ -93033,7 +93173,7 @@ var FfmpegRequiredNotice = memo55(function FfmpegRequiredNotice2({
93033
93173
  /* @__PURE__ */ jsx228(
93034
93174
  "a",
93035
93175
  {
93036
- href: DOWNLOAD_URL2,
93176
+ href: DOWNLOAD_URL,
93037
93177
  target: "_blank",
93038
93178
  rel: "noreferrer",
93039
93179
  className: `rounded-xs py-0.5 text-[10px] text-panel-text-2 underline-offset-2 transition-colors hover:text-panel-text-0 hover:underline ${FOCUS_RING}`,