@signalsandsorcery/plugin-sdk 2.28.1 → 2.34.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.
package/dist/index.js CHANGED
@@ -30,6 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AUDIO_EFFECTS: () => AUDIO_EFFECTS,
34
+ AUDIO_EFFECT_LABEL: () => AUDIO_EFFECT_LABEL,
33
35
  ConfirmDialog: () => ConfirmDialog,
34
36
  CrossfadeModal: () => CrossfadeModal,
35
37
  CrossfadeTrackRow: () => CrossfadeTrackRow,
@@ -68,34 +70,49 @@ __export(index_exports, {
68
70
  ScrollingWaveform: () => ScrollingWaveform,
69
71
  SorceryProgressBar: () => SorceryProgressBar,
70
72
  TEXTURAL_ROLES: () => TEXTURAL_ROLES,
73
+ TRANSITION_DESIGNER_DRAFT_KEY: () => TRANSITION_DESIGNER_DRAFT_KEY,
71
74
  TrackDrawer: () => TrackDrawer,
72
75
  TrackMeterStrip: () => TrackMeterStrip,
73
76
  TrackRow: () => TrackRow,
77
+ TransitionDesigner: () => TransitionDesigner,
74
78
  VolumeSlider: () => VolumeSlider,
75
79
  WaveformView: () => WaveformView,
76
80
  analyzeWavPeak: () => analyzeWavPeak,
81
+ asAudioEffect: () => asAudioEffect,
77
82
  asCrossfadeMeta: () => asCrossfadeMeta,
78
83
  asFadeMeta: () => asFadeMeta,
84
+ asTransitionDesignerDraft: () => asTransitionDesignerDraft,
79
85
  buildCrossfadeInpaintPrompt: () => buildCrossfadeInpaintPrompt,
80
86
  buildCrossfadeVolumeCurves: () => buildCrossfadeVolumeCurves,
81
87
  buildFadeVolumeCurve: () => buildFadeVolumeCurve,
88
+ buildRowSlots: () => buildRowSlots,
82
89
  calculateTimeBasedTarget: () => calculateTimeBasedTarget,
83
90
  cellToPx: () => cellToPx,
84
91
  centerScrollTop: () => centerScrollTop,
85
92
  computePeaks: () => computePeaks,
93
+ dbIdsFromKeys: () => dbIdsFromKeys,
86
94
  dbToSlider: () => dbToSlider,
87
95
  defaultFadeGesture: () => defaultFadeGesture,
88
96
  drawWaveform: () => drawWaveform,
89
97
  formatConcurrentTracks: () => formatConcurrentTracks,
98
+ hashString: () => hashString,
90
99
  moveItem: () => moveItem,
100
+ normalizeSlots: () => normalizeSlots,
101
+ padPair: () => padPair,
102
+ padSlots: () => padSlots,
91
103
  parseCrossfadePairs: () => parseCrossfadePairs,
92
104
  parseFades: () => parseFades,
93
105
  pickTopKWeighted: () => pickTopKWeighted,
94
106
  pitchToName: () => pitchToName,
95
107
  pxToCell: () => pxToCell,
108
+ reconcileSlots: () => reconcileSlots,
96
109
  resizeNoteDuration: () => resizeNoteDuration,
110
+ rowKey: () => rowKey,
111
+ rowType: () => rowType,
97
112
  scorePromptMatch: () => scorePromptMatch,
98
113
  sliderToDb: () => sliderToDb,
114
+ slotsEqual: () => slotsEqual,
115
+ soundIdentity: () => soundIdentity,
99
116
  synthesizeCuePoints: () => synthesizeCuePoints,
100
117
  tokenizePrompt: () => tokenizePrompt,
101
118
  transposeNotes: () => transposeNotes,
@@ -1455,6 +1472,7 @@ var LevelMeter = ({
1455
1472
 
1456
1473
  // src/hooks/useTrackLevels.ts
1457
1474
  var import_react5 = require("react");
1475
+ var meterDiagRLast = /* @__PURE__ */ new Map();
1458
1476
  var POLL_INTERVAL_MS = 33;
1459
1477
  var HIDDEN_RECHECK_MS = 250;
1460
1478
  var METER_FLOOR_DB = -120;
@@ -1586,6 +1604,11 @@ function useTrackMeter(handle, trackId) {
1586
1604
  }
1587
1605
  const update = () => {
1588
1606
  const level = handle.getLevel(trackId);
1607
+ const dNow = Date.now();
1608
+ if ((meterDiagRLast.get(trackId) ?? 0) < dNow - 3e3) {
1609
+ meterDiagRLast.set(trackId, dNow);
1610
+ console.log(`[MeterDiagR] lookup trackId=${trackId} \u2192 ${level === null ? "MISS (no level for this id)" : "hit"}`);
1611
+ }
1589
1612
  const now = performance.now();
1590
1613
  const dtSec = lastTickRef.current ? Math.max(0, (now - lastTickRef.current) / 1e3) : 0;
1591
1614
  lastTickRef.current = now;
@@ -2287,8 +2310,7 @@ function TrackRow({
2287
2310
  {
2288
2311
  "data-testid": "sdk-mute-button",
2289
2312
  onClick: onMuteToggle,
2290
- disabled: isGenerating,
2291
- className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isGenerating ? "bg-sas-panel text-sas-muted/50 cursor-not-allowed" : isMuted ? "bg-red-600 text-white" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
2313
+ className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isMuted ? "bg-red-600 text-white" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
2292
2314
  title: isMuted ? "Unmute track" : "Mute track",
2293
2315
  children: "M"
2294
2316
  }
@@ -2539,6 +2561,18 @@ function CrossfadeTrackRow({
2539
2561
  }
2540
2562
 
2541
2563
  // src/crossfade-meta.ts
2564
+ function hashString(s) {
2565
+ let h = 5381;
2566
+ for (let i = 0; i < s.length; i++) h = (h << 5) + h ^ s.charCodeAt(i) | 0;
2567
+ return (h >>> 0).toString(36);
2568
+ }
2569
+ function soundIdentity(snap) {
2570
+ if (!snap) return "";
2571
+ if (snap.kind === "preset") return `p:${hashString(snap.state)}`;
2572
+ if (snap.kind === "sample") return `s:${snap.samplePath}`;
2573
+ if (snap.kind === "instrument") return `i:${snap.instrumentId ?? ""}:${hashString(JSON.stringify(snap.zones))}`;
2574
+ return "";
2575
+ }
2542
2576
  var EQUAL_POWER_GAIN = 0.707;
2543
2577
  function asCrossfadeMeta(val) {
2544
2578
  if (!val || typeof val !== "object") return null;
@@ -2688,9 +2722,11 @@ function asFadeMeta(val) {
2688
2722
  const m = val;
2689
2723
  if (m.direction !== "in" && m.direction !== "out") return null;
2690
2724
  if (m.gesture !== "volume" && m.gesture !== "build") return null;
2725
+ const effect = m.effect === "stutter" || m.effect === "chopped" || m.effect === "delay" || m.effect === "fade" ? m.effect : void 0;
2691
2726
  return {
2692
2727
  direction: m.direction,
2693
2728
  gesture: m.gesture,
2729
+ effect,
2694
2730
  sourceTrackDbId: typeof m.sourceTrackDbId === "string" ? m.sourceTrackDbId : "",
2695
2731
  sourceSceneId: typeof m.sourceSceneId === "string" ? m.sourceSceneId : "",
2696
2732
  sourceName: typeof m.sourceName === "string" ? m.sourceName : "",
@@ -2777,6 +2813,7 @@ function FadeTrackRow({
2777
2813
  layer,
2778
2814
  direction,
2779
2815
  gesture,
2816
+ effect,
2780
2817
  sliderPos = 0.5,
2781
2818
  onMuteToggle,
2782
2819
  onSoloToggle,
@@ -2790,7 +2827,8 @@ function FadeTrackRow({
2790
2827
  const [confirmDelete, setConfirmDelete] = import_react11.default.useState(false);
2791
2828
  const leftLabel = direction === "in" ? "(silent)" : layer.sourceName ?? layer.name;
2792
2829
  const rightLabel = direction === "in" ? layer.sourceName ?? layer.name : "(silent)";
2793
- const badge = direction === "in" ? "\u2197 Fade in" : "\u2198 Fade out";
2830
+ const verb = effect && effect !== "fade" ? effect.charAt(0).toUpperCase() + effect.slice(1) : "Fade";
2831
+ const badge = direction === "in" ? `\u2197 ${verb} in` : `\u2198 ${verb} out`;
2794
2832
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2795
2833
  "div",
2796
2834
  {
@@ -3525,9 +3563,701 @@ function CrossfadeModal({
3525
3563
  ) });
3526
3564
  }
3527
3565
 
3528
- // src/components/DownloadPackButton.tsx
3566
+ // src/components/TransitionDesigner.tsx
3567
+ var import_react16 = require("react");
3568
+
3569
+ // src/hooks/useTrackReorder.ts
3529
3570
  var import_react15 = require("react");
3571
+ function moveItem(arr, from, to) {
3572
+ const next = arr.slice();
3573
+ if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
3574
+ return next;
3575
+ }
3576
+ const [moved] = next.splice(from, 1);
3577
+ next.splice(to, 0, moved);
3578
+ return next;
3579
+ }
3580
+ function useTrackReorder({
3581
+ host,
3582
+ items,
3583
+ setItems,
3584
+ getId,
3585
+ onError
3586
+ }) {
3587
+ const [draggingIndex, setDraggingIndex] = (0, import_react15.useState)(null);
3588
+ const [dragOverIndex, setDragOverIndex] = (0, import_react15.useState)(null);
3589
+ const fromRef = (0, import_react15.useRef)(null);
3590
+ const itemsRef = (0, import_react15.useRef)(items);
3591
+ itemsRef.current = items;
3592
+ const dragPropsFor = (0, import_react15.useCallback)(
3593
+ (index) => ({
3594
+ handleProps: {
3595
+ draggable: true,
3596
+ onDragStart: (e) => {
3597
+ fromRef.current = index;
3598
+ setDraggingIndex(index);
3599
+ if (e.dataTransfer) {
3600
+ e.dataTransfer.effectAllowed = "move";
3601
+ try {
3602
+ e.dataTransfer.setData("text/plain", String(index));
3603
+ } catch {
3604
+ }
3605
+ }
3606
+ },
3607
+ onDragEnd: () => {
3608
+ fromRef.current = null;
3609
+ setDraggingIndex(null);
3610
+ setDragOverIndex(null);
3611
+ }
3612
+ },
3613
+ rowProps: {
3614
+ onDragEnter: (e) => {
3615
+ if (fromRef.current === null) return;
3616
+ e.preventDefault();
3617
+ setDragOverIndex(index);
3618
+ },
3619
+ onDragOver: (e) => {
3620
+ if (fromRef.current === null) return;
3621
+ e.preventDefault();
3622
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
3623
+ setDragOverIndex((cur) => cur === index ? cur : index);
3624
+ },
3625
+ onDragLeave: () => {
3626
+ setDragOverIndex((cur) => cur === index ? null : cur);
3627
+ },
3628
+ onDrop: (e) => {
3629
+ e.preventDefault();
3630
+ const from = fromRef.current;
3631
+ fromRef.current = null;
3632
+ setDraggingIndex(null);
3633
+ setDragOverIndex(null);
3634
+ if (from === null || from === index) return;
3635
+ const prev = itemsRef.current;
3636
+ const next = moveItem(prev, from, index);
3637
+ setItems(next);
3638
+ const ids = next.map(getId);
3639
+ Promise.resolve(host.reorderTracks(ids)).catch((err) => {
3640
+ setItems(prev);
3641
+ onError?.(err);
3642
+ });
3643
+ }
3644
+ },
3645
+ isDragging: draggingIndex === index,
3646
+ isDragTarget: dragOverIndex === index && draggingIndex !== index
3647
+ }),
3648
+ [host, setItems, getId, onError, draggingIndex, dragOverIndex]
3649
+ );
3650
+ return { dragPropsFor, draggingIndex, dragOverIndex };
3651
+ }
3652
+
3653
+ // src/transition-designer-meta.ts
3654
+ var TRANSITION_DESIGNER_DRAFT_KEY = "transitionDesigner:draft";
3655
+ var AUDIO_EFFECTS = ["fade", "stutter", "chopped", "delay"];
3656
+ var AUDIO_EFFECT_LABEL = {
3657
+ fade: "Fade",
3658
+ stutter: "Stutter",
3659
+ chopped: "Chopped",
3660
+ delay: "Delay"
3661
+ };
3662
+ function asAudioEffect(v) {
3663
+ return v === "fade" || v === "stutter" || v === "chopped" || v === "delay" ? v : null;
3664
+ }
3665
+ function rowType(hasOrigin, hasTarget) {
3666
+ if (hasOrigin && hasTarget) return "crossfade";
3667
+ if (hasOrigin) return "fade-out";
3668
+ if (hasTarget) return "fade-in";
3669
+ return null;
3670
+ }
3671
+ function asTransitionDesignerDraft(val) {
3672
+ if (!val || typeof val !== "object") return null;
3673
+ const d = val;
3674
+ const clean = (a) => Array.isArray(a) ? a.filter((x) => x === null || typeof x === "string") : [];
3675
+ const cleanEffects = (e) => {
3676
+ const out = {};
3677
+ if (e && typeof e === "object") {
3678
+ for (const [k, v] of Object.entries(e)) {
3679
+ const eff = asAudioEffect(v);
3680
+ if (eff) out[k] = eff;
3681
+ }
3682
+ }
3683
+ return out;
3684
+ };
3685
+ return {
3686
+ originOrder: clean(d.originOrder),
3687
+ targetOrder: clean(d.targetOrder),
3688
+ rowEffects: cleanEffects(d.rowEffects)
3689
+ };
3690
+ }
3691
+ function reconcileSlots(saved, poolIds) {
3692
+ const pool = new Set(poolIds);
3693
+ const seen = /* @__PURE__ */ new Set();
3694
+ const out = [];
3695
+ for (const slot of saved ?? []) {
3696
+ if (slot === null) {
3697
+ out.push(null);
3698
+ continue;
3699
+ }
3700
+ if (pool.has(slot) && !seen.has(slot)) {
3701
+ out.push(slot);
3702
+ seen.add(slot);
3703
+ }
3704
+ }
3705
+ for (const id of poolIds) {
3706
+ if (!seen.has(id)) {
3707
+ out.push(id);
3708
+ seen.add(id);
3709
+ }
3710
+ }
3711
+ return out;
3712
+ }
3713
+ function buildRowSlots(originSlots, targetSlots) {
3714
+ const n = Math.max(originSlots.length, targetSlots.length);
3715
+ const rows = [];
3716
+ for (let i = 0; i < n; i++) {
3717
+ const originId = originSlots[i] ?? null;
3718
+ const targetId = targetSlots[i] ?? null;
3719
+ rows.push({ originId, targetId, type: rowType(originId !== null, targetId !== null) });
3720
+ }
3721
+ return rows;
3722
+ }
3723
+ function normalizeSlots(originSlots, targetSlots) {
3724
+ const rows = buildRowSlots(originSlots, targetSlots).filter(
3725
+ (r) => r.originId !== null || r.targetId !== null
3726
+ );
3727
+ const trimTrailing = (a) => {
3728
+ let end = a.length;
3729
+ while (end > 0 && a[end - 1] === null) end--;
3730
+ return a.slice(0, end);
3731
+ };
3732
+ return {
3733
+ originOrder: trimTrailing(rows.map((r) => r.originId)),
3734
+ targetOrder: trimTrailing(rows.map((r) => r.targetId))
3735
+ };
3736
+ }
3737
+ function padSlots(slots, n) {
3738
+ if (slots.length >= n) return slots.slice();
3739
+ return [...slots, ...new Array(n - slots.length).fill(null)];
3740
+ }
3741
+ function padPair(originSlots, targetSlots) {
3742
+ const n = Math.max(originSlots.length, targetSlots.length);
3743
+ return [padSlots(originSlots, n), padSlots(targetSlots, n)];
3744
+ }
3745
+ function slotsEqual(a, b) {
3746
+ if (a.length !== b.length) return false;
3747
+ for (let i = 0; i < a.length; i++) {
3748
+ if (a[i] !== b[i]) return false;
3749
+ }
3750
+ return true;
3751
+ }
3752
+ function rowKey(row) {
3753
+ if (row.type === "crossfade") return `xf:${row.originId}|${row.targetId}`;
3754
+ if (row.type === "fade-out") return `fo:${row.originId}`;
3755
+ if (row.type === "fade-in") return `fi:${row.targetId}`;
3756
+ return null;
3757
+ }
3758
+ function dbIdsFromKeys(keys) {
3759
+ const out = /* @__PURE__ */ new Set();
3760
+ for (const k of keys) {
3761
+ const body = k.slice(3);
3762
+ if (k.startsWith("xf:")) {
3763
+ const sep = body.indexOf("|");
3764
+ out.add(body.slice(0, sep));
3765
+ out.add(body.slice(sep + 1));
3766
+ } else {
3767
+ out.add(body);
3768
+ }
3769
+ }
3770
+ return out;
3771
+ }
3772
+
3773
+ // src/components/TransitionDesigner.tsx
3530
3774
  var import_jsx_runtime17 = require("react/jsx-runtime");
3775
+ var CROSSFADE_ESTIMATE_MS = 15e3;
3776
+ var FADE_ESTIMATE_MS = 11e3;
3777
+ var CREATE_ALL_CONCURRENCY = 5;
3778
+ var TYPE_LABEL = {
3779
+ crossfade: "Crossfade",
3780
+ "fade-out": "Fade out",
3781
+ "fade-in": "Fade in"
3782
+ };
3783
+ function shortId3(dbId) {
3784
+ return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
3785
+ }
3786
+ function TransitionDesigner({
3787
+ host,
3788
+ fromSceneId,
3789
+ toSceneId,
3790
+ transitionSceneId,
3791
+ excludeSourceDbIds,
3792
+ onCreateCrossfade,
3793
+ onCreateFade,
3794
+ onCreateAudioTransition,
3795
+ familyLabel,
3796
+ testIdPrefix = "transition-designer"
3797
+ }) {
3798
+ const [load, setLoad] = (0, import_react16.useState)({ status: "loading" });
3799
+ const [fromName, setFromName] = (0, import_react16.useState)(null);
3800
+ const [toName, setToName] = (0, import_react16.useState)(null);
3801
+ const [originSlots, setOriginSlots] = (0, import_react16.useState)([]);
3802
+ const [targetSlots, setTargetSlots] = (0, import_react16.useState)([]);
3803
+ const [creatingKeys, setCreatingKeys] = (0, import_react16.useState)(() => /* @__PURE__ */ new Set());
3804
+ const [rowErrors, setRowErrors] = (0, import_react16.useState)({});
3805
+ const [rowEffects, setRowEffects] = (0, import_react16.useState)({});
3806
+ const rowEffectsRef = (0, import_react16.useRef)(rowEffects);
3807
+ rowEffectsRef.current = rowEffects;
3808
+ const audioEffectsEnabled = !!onCreateAudioTransition;
3809
+ const excludeRef = (0, import_react16.useRef)(excludeSourceDbIds);
3810
+ excludeRef.current = excludeSourceDbIds;
3811
+ const originSlotsRef = (0, import_react16.useRef)(originSlots);
3812
+ originSlotsRef.current = originSlots;
3813
+ const targetSlotsRef = (0, import_react16.useRef)(targetSlots);
3814
+ targetSlotsRef.current = targetSlots;
3815
+ const creatingKeysRef = (0, import_react16.useRef)(creatingKeys);
3816
+ creatingKeysRef.current = creatingKeys;
3817
+ const dragRef = (0, import_react16.useRef)(null);
3818
+ const [dragging, setDragging] = (0, import_react16.useState)(null);
3819
+ const [dragOver, setDragOver] = (0, import_react16.useState)(null);
3820
+ const excludeSet = (0, import_react16.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3821
+ const originPool = (0, import_react16.useMemo)(
3822
+ () => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
3823
+ [load, excludeSet]
3824
+ );
3825
+ const targetPool = (0, import_react16.useMemo)(
3826
+ () => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
3827
+ [load, excludeSet]
3828
+ );
3829
+ const originById = (0, import_react16.useMemo)(() => new Map(originPool.map((t) => [t.dbId, t])), [originPool]);
3830
+ const targetById = (0, import_react16.useMemo)(() => new Map(targetPool.map((t) => [t.dbId, t])), [targetPool]);
3831
+ const originByIdRef = (0, import_react16.useRef)(originById);
3832
+ originByIdRef.current = originById;
3833
+ const targetByIdRef = (0, import_react16.useRef)(targetById);
3834
+ targetByIdRef.current = targetById;
3835
+ const refresh = (0, import_react16.useCallback)(async () => {
3836
+ if (!host.listSceneFamilyTracks) {
3837
+ setLoad({ status: "error", message: "This host does not support transition tracks." });
3838
+ return;
3839
+ }
3840
+ setLoad({ status: "loading" });
3841
+ try {
3842
+ const [origin, target, fName, tName, draftRaw] = await Promise.all([
3843
+ host.listSceneFamilyTracks(fromSceneId),
3844
+ host.listSceneFamilyTracks(toSceneId),
3845
+ host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),
3846
+ host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null),
3847
+ host.getSceneData ? host.getSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY) : Promise.resolve(null)
3848
+ ]);
3849
+ const draft = asTransitionDesignerDraft(draftRaw);
3850
+ const exSet = new Set(excludeRef.current ?? []);
3851
+ const originIds = origin.filter((t) => !exSet.has(t.dbId)).map((t) => t.dbId);
3852
+ const targetIds = target.filter((t) => !exSet.has(t.dbId)).map((t) => t.dbId);
3853
+ const [po, pt] = padPair(
3854
+ reconcileSlots(draft?.originOrder, originIds),
3855
+ reconcileSlots(draft?.targetOrder, targetIds)
3856
+ );
3857
+ setOriginSlots(po);
3858
+ setTargetSlots(pt);
3859
+ setRowEffects(draft?.rowEffects ?? {});
3860
+ setFromName(fName);
3861
+ setToName(tName);
3862
+ setLoad({ status: "ready", origin, target });
3863
+ } catch (err) {
3864
+ setLoad({
3865
+ status: "error",
3866
+ message: err instanceof Error ? err.message : "Failed to load tracks."
3867
+ });
3868
+ }
3869
+ }, [host, fromSceneId, toSceneId, transitionSceneId]);
3870
+ (0, import_react16.useEffect)(() => {
3871
+ void refresh();
3872
+ }, [refresh]);
3873
+ (0, import_react16.useEffect)(() => {
3874
+ if (load.status !== "ready") return;
3875
+ const [po, pt] = padPair(
3876
+ reconcileSlots(originSlotsRef.current, originPool.map((t) => t.dbId)),
3877
+ reconcileSlots(targetSlotsRef.current, targetPool.map((t) => t.dbId))
3878
+ );
3879
+ if (!slotsEqual(po, originSlotsRef.current)) setOriginSlots(po);
3880
+ if (!slotsEqual(pt, targetSlotsRef.current)) setTargetSlots(pt);
3881
+ }, [originPool, targetPool, load.status]);
3882
+ const mutate = (0, import_react16.useCallback)(
3883
+ (nextOrigin, nextTarget) => {
3884
+ const norm = normalizeSlots(nextOrigin, nextTarget);
3885
+ const [po, pt] = padPair(norm.originOrder, norm.targetOrder);
3886
+ setOriginSlots(po);
3887
+ setTargetSlots(pt);
3888
+ if (host.setSceneData) {
3889
+ host.setSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY, { ...norm, rowEffects: rowEffectsRef.current }).catch(() => {
3890
+ });
3891
+ }
3892
+ },
3893
+ [host, transitionSceneId]
3894
+ );
3895
+ const setRowEffect = (0, import_react16.useCallback)(
3896
+ (sourceDbId, effect) => {
3897
+ setRowEffects((prev) => {
3898
+ const next = { ...prev, [sourceDbId]: effect };
3899
+ if (host.setSceneData) {
3900
+ const norm = normalizeSlots(originSlotsRef.current, targetSlotsRef.current);
3901
+ host.setSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY, { ...norm, rowEffects: next }).catch(() => {
3902
+ });
3903
+ }
3904
+ return next;
3905
+ });
3906
+ },
3907
+ [host, transitionSceneId]
3908
+ );
3909
+ const insertGapAbove = (0, import_react16.useCallback)(
3910
+ (col, index) => {
3911
+ const slots = col === "origin" ? originSlots : targetSlots;
3912
+ const next = [...slots.slice(0, index), null, ...slots.slice(index)];
3913
+ if (col === "origin") mutate(next, targetSlots);
3914
+ else mutate(originSlots, next);
3915
+ },
3916
+ [originSlots, targetSlots, mutate]
3917
+ );
3918
+ const removeGap = (0, import_react16.useCallback)(
3919
+ (col, index) => {
3920
+ const slots = col === "origin" ? originSlots : targetSlots;
3921
+ const next = slots.filter((_, i) => i !== index);
3922
+ if (col === "origin") mutate(next, targetSlots);
3923
+ else mutate(originSlots, next);
3924
+ },
3925
+ [originSlots, targetSlots, mutate]
3926
+ );
3927
+ const handleDrop = (0, import_react16.useCallback)(
3928
+ (col, to) => {
3929
+ const from = dragRef.current;
3930
+ dragRef.current = null;
3931
+ setDragging(null);
3932
+ setDragOver(null);
3933
+ if (!from || from.col !== col || from.index === to) return;
3934
+ if (col === "origin") mutate(moveItem(originSlots, from.index, to), targetSlots);
3935
+ else mutate(originSlots, moveItem(targetSlots, from.index, to));
3936
+ },
3937
+ [originSlots, targetSlots, mutate]
3938
+ );
3939
+ const rows = (0, import_react16.useMemo)(() => buildRowSlots(originSlots, targetSlots), [originSlots, targetSlots]);
3940
+ const creatingDbIds = (0, import_react16.useMemo)(() => dbIdsFromKeys(creatingKeys), [creatingKeys]);
3941
+ const eligibleCount = (0, import_react16.useMemo)(
3942
+ () => rows.filter((r) => {
3943
+ const k = rowKey(r);
3944
+ return k !== null && !creatingKeys.has(k);
3945
+ }).length,
3946
+ [rows, creatingKeys]
3947
+ );
3948
+ const createRow = (0, import_react16.useCallback)(
3949
+ async (row) => {
3950
+ const key = rowKey(row);
3951
+ if (!key || !row.type || creatingKeysRef.current.has(key)) return;
3952
+ setCreatingKeys((prev) => new Set(prev).add(key));
3953
+ setRowErrors((prev) => {
3954
+ if (!(key in prev)) return prev;
3955
+ const next = { ...prev };
3956
+ delete next[key];
3957
+ return next;
3958
+ });
3959
+ try {
3960
+ if (row.type === "crossfade") {
3961
+ const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
3962
+ const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
3963
+ if (!o || !t) throw new Error("Track is no longer available \u2014 refresh and retry.");
3964
+ await onCreateCrossfade(
3965
+ { dbId: o.dbId, name: o.name, role: o.role },
3966
+ { dbId: t.dbId, name: t.name, role: t.role }
3967
+ );
3968
+ } else if (row.type === "fade-out") {
3969
+ const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
3970
+ if (!o) throw new Error("Track is no longer available \u2014 refresh and retry.");
3971
+ const eff = rowEffectsRef.current[o.dbId] ?? "fade";
3972
+ if (eff !== "fade" && onCreateAudioTransition) {
3973
+ await onCreateAudioTransition({ dbId: o.dbId, name: o.name, role: o.role }, "out", eff);
3974
+ } else {
3975
+ await onCreateFade({ dbId: o.dbId, name: o.name, role: o.role }, "out", defaultFadeGesture(o.role));
3976
+ }
3977
+ } else {
3978
+ const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
3979
+ if (!t) throw new Error("Track is no longer available \u2014 refresh and retry.");
3980
+ const eff = rowEffectsRef.current[t.dbId] ?? "fade";
3981
+ if (eff !== "fade" && onCreateAudioTransition) {
3982
+ await onCreateAudioTransition({ dbId: t.dbId, name: t.name, role: t.role }, "in", eff);
3983
+ } else {
3984
+ await onCreateFade({ dbId: t.dbId, name: t.name, role: t.role }, "in", defaultFadeGesture(t.role));
3985
+ }
3986
+ }
3987
+ } catch (err) {
3988
+ setRowErrors((prev) => ({
3989
+ ...prev,
3990
+ [key]: err instanceof Error ? err.message : "Failed to create transition."
3991
+ }));
3992
+ } finally {
3993
+ setCreatingKeys((prev) => {
3994
+ const next = new Set(prev);
3995
+ next.delete(key);
3996
+ return next;
3997
+ });
3998
+ }
3999
+ },
4000
+ [onCreateCrossfade, onCreateFade, onCreateAudioTransition]
4001
+ );
4002
+ const createAll = (0, import_react16.useCallback)(async () => {
4003
+ const eligible = buildRowSlots(originSlotsRef.current, targetSlotsRef.current).filter((r) => {
4004
+ const k = rowKey(r);
4005
+ return k !== null && !creatingKeysRef.current.has(k);
4006
+ });
4007
+ if (eligible.length === 0) return;
4008
+ let cursor = 0;
4009
+ const worker = async () => {
4010
+ while (cursor < eligible.length) {
4011
+ const row = eligible[cursor];
4012
+ cursor += 1;
4013
+ await createRow(row);
4014
+ }
4015
+ };
4016
+ await Promise.all(
4017
+ Array.from({ length: Math.min(CREATE_ALL_CONCURRENCY, eligible.length) }, () => worker())
4018
+ );
4019
+ }, [createRow]);
4020
+ const fromLabel = fromName ?? "origin";
4021
+ const toLabel = toName ?? "target";
4022
+ const cellDragProps = (col, index, locked) => ({
4023
+ draggable: !locked,
4024
+ onDragStart: (e) => {
4025
+ if (locked) return;
4026
+ dragRef.current = { col, index };
4027
+ setDragging({ col, index });
4028
+ if (e.dataTransfer) {
4029
+ e.dataTransfer.effectAllowed = "move";
4030
+ try {
4031
+ e.dataTransfer.setData("text/plain", String(index));
4032
+ } catch {
4033
+ }
4034
+ }
4035
+ },
4036
+ onDragEnd: () => {
4037
+ dragRef.current = null;
4038
+ setDragging(null);
4039
+ setDragOver(null);
4040
+ },
4041
+ onDragEnter: (e) => {
4042
+ const d = dragRef.current;
4043
+ if (!d || d.col !== col) return;
4044
+ e.preventDefault();
4045
+ setDragOver({ col, index });
4046
+ },
4047
+ onDragOver: (e) => {
4048
+ const d = dragRef.current;
4049
+ if (!d || d.col !== col) return;
4050
+ e.preventDefault();
4051
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
4052
+ },
4053
+ onDragLeave: () => {
4054
+ setDragOver((cur) => cur && cur.col === col && cur.index === index ? null : cur);
4055
+ },
4056
+ onDrop: (e) => {
4057
+ e.preventDefault();
4058
+ handleDrop(col, index);
4059
+ }
4060
+ });
4061
+ const renderCell = (col, index, slotId) => {
4062
+ const byId = col === "origin" ? originById : targetById;
4063
+ const track = slotId ? byId.get(slotId) : void 0;
4064
+ const locked = slotId !== null && creatingDbIds.has(slotId);
4065
+ const isDragging = dragging?.col === col && dragging.index === index;
4066
+ const isDragTarget = dragOver?.col === col && dragOver.index === index && !isDragging;
4067
+ const base = "group relative rounded-sm border px-2 py-1.5 text-left transition-colors select-none";
4068
+ const tone = isDragTarget ? "border-sas-accent bg-sas-accent/10" : "border-sas-border bg-sas-panel";
4069
+ if (slotId === null) {
4070
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
4071
+ "div",
4072
+ {
4073
+ ...cellDragProps(col, index, false),
4074
+ "data-testid": `${testIdPrefix}-${col}-gap-${index}`,
4075
+ className: `${base} ${tone} border-dashed flex items-center justify-between ${isDragging ? "opacity-40" : "opacity-70"}`,
4076
+ children: [
4077
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
4078
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4079
+ "button",
4080
+ {
4081
+ type: "button",
4082
+ "data-testid": `${testIdPrefix}-${col}-remove-gap-${index}`,
4083
+ onClick: () => removeGap(col, index),
4084
+ title: "Remove gap",
4085
+ className: "text-[10px] text-sas-muted hover:text-sas-danger",
4086
+ children: "\u2715"
4087
+ }
4088
+ )
4089
+ ]
4090
+ }
4091
+ );
4092
+ }
4093
+ const primary = track ? track.prompt?.trim() || track.name : slotId;
4094
+ const meta = track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing";
4095
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4096
+ "div",
4097
+ {
4098
+ ...cellDragProps(col, index, locked),
4099
+ "data-testid": `${testIdPrefix}-${col}-cell-${slotId}`,
4100
+ "data-value": slotId,
4101
+ className: `${base} ${tone} ${isDragging ? "opacity-40" : ""} ${locked ? "opacity-60" : "cursor-grab active:cursor-grabbing"}`,
4102
+ title: track ? track.dbId : "Track no longer available",
4103
+ children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-start gap-1", children: [
4104
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
4105
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "min-w-0 flex-1", children: [
4106
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-text truncate", children: primary }),
4107
+ meta && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
4108
+ ] }),
4109
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4110
+ "button",
4111
+ {
4112
+ type: "button",
4113
+ "data-testid": `${testIdPrefix}-${col}-insert-gap-${index}`,
4114
+ onClick: () => insertGapAbove(col, index),
4115
+ disabled: locked,
4116
+ title: "Insert a gap above (make this a fade)",
4117
+ className: "text-[10px] text-sas-muted opacity-0 group-hover:opacity-100 hover:text-sas-accent disabled:opacity-30",
4118
+ children: "+gap"
4119
+ }
4120
+ )
4121
+ ] })
4122
+ }
4123
+ );
4124
+ };
4125
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
4126
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
4127
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
4128
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-text", children: fromLabel }),
4129
+ " \u2192",
4130
+ " ",
4131
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-text", children: toLabel }),
4132
+ familyLabel ? ` \xB7 ${familyLabel}` : "",
4133
+ " \xB7 line up a track on each side to crossfade; leave one blank (or insert a gap) to fade."
4134
+ ] }),
4135
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center gap-2 shrink-0", children: [
4136
+ creatingKeys.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
4137
+ creatingKeys.size,
4138
+ " creating\u2026"
4139
+ ] }),
4140
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
4141
+ "button",
4142
+ {
4143
+ type: "button",
4144
+ "data-testid": `${testIdPrefix}-create-all`,
4145
+ onClick: createAll,
4146
+ disabled: eligibleCount === 0,
4147
+ title: "Create every staged transition at once (runs several concurrently)",
4148
+ className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors whitespace-nowrap ${eligibleCount > 0 ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
4149
+ children: [
4150
+ "Create all",
4151
+ eligibleCount > 0 ? ` (${eligibleCount})` : ""
4152
+ ]
4153
+ }
4154
+ )
4155
+ ] })
4156
+ ] }),
4157
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
4158
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
4159
+ "Origin (",
4160
+ fromLabel,
4161
+ ")"
4162
+ ] }),
4163
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
4164
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
4165
+ "Target (",
4166
+ toLabel,
4167
+ ")"
4168
+ ] })
4169
+ ] }),
4170
+ load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4171
+ load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4172
+ load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
4173
+ "No tracks to arrange in this panel for either scene. Add tracks to ",
4174
+ fromLabel,
4175
+ " or ",
4176
+ toLabel,
4177
+ " ",
4178
+ "first (or free one by deleting an existing crossfade/fade)."
4179
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "space-y-2", children: rows.map((row, i) => {
4180
+ const key = rowKey(row);
4181
+ const isCreatingThis = key !== null && creatingKeys.has(key);
4182
+ const errMsg = key !== null ? rowErrors[key] : void 0;
4183
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
4184
+ "div",
4185
+ {
4186
+ "data-testid": `${testIdPrefix}-row-${i}`,
4187
+ className: "grid grid-cols-[1fr_auto_1fr] gap-2 items-center",
4188
+ children: [
4189
+ renderCell("origin", i, row.originId),
4190
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4191
+ !row.type ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4192
+ "span",
4193
+ {
4194
+ "data-testid": `${testIdPrefix}-type-${i}`,
4195
+ className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-accent/50 text-sas-accent",
4196
+ children: TYPE_LABEL[row.type]
4197
+ }
4198
+ ) : audioEffectsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
4199
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4200
+ "select",
4201
+ {
4202
+ "data-testid": `${testIdPrefix}-effect-${i}`,
4203
+ value: rowEffects[row.originId ?? row.targetId] ?? "fade",
4204
+ onChange: (e) => {
4205
+ const id = row.originId ?? row.targetId;
4206
+ if (id) setRowEffect(id, e.target.value);
4207
+ },
4208
+ className: "text-[10px] bg-sas-panel border border-sas-border rounded-sm px-1 py-0.5 text-sas-text",
4209
+ children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
4210
+ }
4211
+ ),
4212
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
4213
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4214
+ "span",
4215
+ {
4216
+ "data-testid": `${testIdPrefix}-type-${i}`,
4217
+ className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-border text-sas-muted",
4218
+ children: TYPE_LABEL[row.type]
4219
+ }
4220
+ ),
4221
+ isCreatingThis ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4222
+ SorceryProgressBar,
4223
+ {
4224
+ isLoading: true,
4225
+ heightClass: "h-5",
4226
+ statusText: "CREATING",
4227
+ estimatedDurationMs: row.type === "crossfade" ? CROSSFADE_ESTIMATE_MS : FADE_ESTIMATE_MS
4228
+ }
4229
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4230
+ "button",
4231
+ {
4232
+ type: "button",
4233
+ "data-testid": `${testIdPrefix}-create-${i}`,
4234
+ onClick: () => createRow(row),
4235
+ disabled: !row.type,
4236
+ className: `w-full px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors ${row.type ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
4237
+ children: "Create"
4238
+ }
4239
+ ),
4240
+ errMsg && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4241
+ "span",
4242
+ {
4243
+ "data-testid": `${testIdPrefix}-row-error-${i}`,
4244
+ className: "text-[10px] text-sas-danger text-center leading-tight",
4245
+ children: errMsg
4246
+ }
4247
+ )
4248
+ ] }),
4249
+ renderCell("target", i, row.targetId)
4250
+ ]
4251
+ },
4252
+ i
4253
+ );
4254
+ }) }))
4255
+ ] });
4256
+ }
4257
+
4258
+ // src/components/DownloadPackButton.tsx
4259
+ var import_react17 = require("react");
4260
+ var import_jsx_runtime18 = require("react/jsx-runtime");
3531
4261
  function formatSize(bytes) {
3532
4262
  if (!bytes || bytes <= 0) return "";
3533
4263
  const gb = bytes / 1024 ** 3;
@@ -3543,10 +4273,10 @@ var DownloadPackButton = ({
3543
4273
  variant = "compact",
3544
4274
  onDownloadComplete
3545
4275
  }) => {
3546
- const [status, setStatus] = (0, import_react15.useState)("idle");
3547
- const [progress, setProgress] = (0, import_react15.useState)(0);
3548
- const [errorMessage, setErrorMessage] = (0, import_react15.useState)(null);
3549
- (0, import_react15.useEffect)(() => {
4276
+ const [status, setStatus] = (0, import_react17.useState)("idle");
4277
+ const [progress, setProgress] = (0, import_react17.useState)(0);
4278
+ const [errorMessage, setErrorMessage] = (0, import_react17.useState)(null);
4279
+ (0, import_react17.useEffect)(() => {
3550
4280
  const unsub = host.onSamplePackProgress(packId, (p) => {
3551
4281
  setStatus(p.status);
3552
4282
  setProgress(p.progress);
@@ -3561,7 +4291,7 @@ var DownloadPackButton = ({
3561
4291
  });
3562
4292
  return unsub;
3563
4293
  }, [host, packId, onDownloadComplete]);
3564
- const handleClick = (0, import_react15.useCallback)(async () => {
4294
+ const handleClick = (0, import_react17.useCallback)(async () => {
3565
4295
  if (status !== "idle" && status !== "error") return;
3566
4296
  try {
3567
4297
  setStatus("downloading");
@@ -3615,8 +4345,8 @@ var DownloadPackButton = ({
3615
4345
  } else {
3616
4346
  className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
3617
4347
  }
3618
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
3619
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4348
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
4349
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3620
4350
  "button",
3621
4351
  {
3622
4352
  "data-testid": `download-pack-button-${packId}`,
@@ -3627,12 +4357,12 @@ var DownloadPackButton = ({
3627
4357
  children: buttonLabel
3628
4358
  }
3629
4359
  ),
3630
- variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
4360
+ variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
3631
4361
  ] });
3632
4362
  };
3633
4363
 
3634
4364
  // src/components/SamplePackCTACard.tsx
3635
- var import_jsx_runtime18 = require("react/jsx-runtime");
4365
+ var import_jsx_runtime19 = require("react/jsx-runtime");
3636
4366
  var SamplePackCTACard = ({
3637
4367
  host,
3638
4368
  pack,
@@ -3640,7 +4370,7 @@ var SamplePackCTACard = ({
3640
4370
  onDownloadComplete
3641
4371
  }) => {
3642
4372
  if (status === "checking") {
3643
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4373
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3644
4374
  "div",
3645
4375
  {
3646
4376
  "data-testid": `sample-pack-cta-checking-${pack.packId}`,
@@ -3651,16 +4381,16 @@ var SamplePackCTACard = ({
3651
4381
  }
3652
4382
  const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
3653
4383
  const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
3654
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
4384
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
3655
4385
  "div",
3656
4386
  {
3657
4387
  "data-testid": `sample-pack-cta-${pack.packId}`,
3658
4388
  className: "flex flex-col items-center justify-center py-12 px-6 text-center",
3659
4389
  children: [
3660
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
3661
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-base text-sas-text mb-1", children: headline }),
3662
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
3663
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4390
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
4391
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-base text-sas-text mb-1", children: headline }),
4392
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
4393
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3664
4394
  DownloadPackButton,
3665
4395
  {
3666
4396
  host,
@@ -3677,7 +4407,7 @@ var SamplePackCTACard = ({
3677
4407
  };
3678
4408
 
3679
4409
  // src/components/WaveformView.tsx
3680
- var import_react16 = require("react");
4410
+ var import_react18 = require("react");
3681
4411
 
3682
4412
  // src/components/waveform.ts
3683
4413
  function computePeaks(audioBuffer, bins, targetSamples) {
@@ -3740,7 +4470,7 @@ function drawWaveform(canvas, peaks, options = {}) {
3740
4470
  }
3741
4471
 
3742
4472
  // src/components/WaveformView.tsx
3743
- var import_jsx_runtime19 = require("react/jsx-runtime");
4473
+ var import_jsx_runtime20 = require("react/jsx-runtime");
3744
4474
  var WaveformView = ({
3745
4475
  host,
3746
4476
  filePath,
@@ -3749,9 +4479,9 @@ var WaveformView = ({
3749
4479
  fillStyle,
3750
4480
  targetSamples
3751
4481
  }) => {
3752
- const canvasRef = (0, import_react16.useRef)(null);
3753
- const [peaks, setPeaks] = (0, import_react16.useState)(null);
3754
- (0, import_react16.useEffect)(() => {
4482
+ const canvasRef = (0, import_react18.useRef)(null);
4483
+ const [peaks, setPeaks] = (0, import_react18.useState)(null);
4484
+ (0, import_react18.useEffect)(() => {
3755
4485
  let cancelled = false;
3756
4486
  let audioContext = null;
3757
4487
  (async () => {
@@ -3777,7 +4507,7 @@ var WaveformView = ({
3777
4507
  cancelled = true;
3778
4508
  };
3779
4509
  }, [host, filePath, bins, targetSamples]);
3780
- (0, import_react16.useEffect)(() => {
4510
+ (0, import_react18.useEffect)(() => {
3781
4511
  if (!peaks) return;
3782
4512
  const canvas = canvasRef.current;
3783
4513
  if (!canvas) return;
@@ -3788,7 +4518,7 @@ var WaveformView = ({
3788
4518
  observer.observe(canvas);
3789
4519
  return () => observer.disconnect();
3790
4520
  }, [peaks, fillStyle]);
3791
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4521
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
3792
4522
  "canvas",
3793
4523
  {
3794
4524
  ref: canvasRef,
@@ -3799,8 +4529,8 @@ var WaveformView = ({
3799
4529
  };
3800
4530
 
3801
4531
  // src/components/ScrollingWaveform.tsx
3802
- var import_react17 = require("react");
3803
- var import_jsx_runtime20 = require("react/jsx-runtime");
4532
+ var import_react19 = require("react");
4533
+ var import_jsx_runtime21 = require("react/jsx-runtime");
3804
4534
  var ScrollingWaveform = ({
3805
4535
  getPeakDb,
3806
4536
  active,
@@ -3808,11 +4538,11 @@ var ScrollingWaveform = ({
3808
4538
  className,
3809
4539
  fillStyle
3810
4540
  }) => {
3811
- const canvasRef = (0, import_react17.useRef)(null);
3812
- const ringRef = (0, import_react17.useRef)(new Float32Array(columns));
3813
- const writeIdxRef = (0, import_react17.useRef)(0);
3814
- const rafRef = (0, import_react17.useRef)(null);
3815
- (0, import_react17.useEffect)(() => {
4541
+ const canvasRef = (0, import_react19.useRef)(null);
4542
+ const ringRef = (0, import_react19.useRef)(new Float32Array(columns));
4543
+ const writeIdxRef = (0, import_react19.useRef)(0);
4544
+ const rafRef = (0, import_react19.useRef)(null);
4545
+ (0, import_react19.useEffect)(() => {
3816
4546
  if (ringRef.current.length !== columns) {
3817
4547
  const next = new Float32Array(columns);
3818
4548
  const prev = ringRef.current;
@@ -3824,7 +4554,7 @@ var ScrollingWaveform = ({
3824
4554
  writeIdxRef.current = writeIdxRef.current % columns;
3825
4555
  }
3826
4556
  }, [columns]);
3827
- (0, import_react17.useEffect)(() => {
4557
+ (0, import_react19.useEffect)(() => {
3828
4558
  if (!active) {
3829
4559
  if (rafRef.current !== null) {
3830
4560
  cancelAnimationFrame(rafRef.current);
@@ -3876,7 +4606,7 @@ var ScrollingWaveform = ({
3876
4606
  }
3877
4607
  };
3878
4608
  }, [active, getPeakDb, fillStyle]);
3879
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4609
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3880
4610
  "canvas",
3881
4611
  {
3882
4612
  ref: canvasRef,
@@ -3887,8 +4617,8 @@ var ScrollingWaveform = ({
3887
4617
  };
3888
4618
 
3889
4619
  // src/components/OffsetScrubber.tsx
3890
- var import_react18 = require("react");
3891
- var import_jsx_runtime21 = require("react/jsx-runtime");
4620
+ var import_react20 = require("react");
4621
+ var import_jsx_runtime22 = require("react/jsx-runtime");
3892
4622
  var SLIDER_HEIGHT_PX = 28;
3893
4623
  var TICK_HEIGHT_PX = 14;
3894
4624
  var DOWNBEAT_TICK_HEIGHT_PX = 22;
@@ -3901,40 +4631,40 @@ function OffsetScrubber({
3901
4631
  onChange,
3902
4632
  disabled = false
3903
4633
  }) {
3904
- const trackRef = (0, import_react18.useRef)(null);
3905
- const [draftOffset, setDraftOffset] = (0, import_react18.useState)(offsetSamples);
3906
- const [isDragging, setIsDragging] = (0, import_react18.useState)(false);
3907
- (0, import_react18.useEffect)(() => {
4634
+ const trackRef = (0, import_react20.useRef)(null);
4635
+ const [draftOffset, setDraftOffset] = (0, import_react20.useState)(offsetSamples);
4636
+ const [isDragging, setIsDragging] = (0, import_react20.useState)(false);
4637
+ (0, import_react20.useEffect)(() => {
3908
4638
  if (!isDragging) setDraftOffset(offsetSamples);
3909
4639
  }, [offsetSamples, isDragging]);
3910
4640
  const sampleRate = cuePoints?.sample_rate ?? 44100;
3911
4641
  const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
3912
- const beatsForRange = (0, import_react18.useMemo)(() => {
4642
+ const beatsForRange = (0, import_react20.useMemo)(() => {
3913
4643
  return Math.round(60 / projectBpm * sampleRate);
3914
4644
  }, [projectBpm, sampleRate]);
3915
4645
  const rangeSamples = beatsForRange * meter;
3916
- const sampleToFraction = (0, import_react18.useCallback)(
4646
+ const sampleToFraction = (0, import_react20.useCallback)(
3917
4647
  (sample) => {
3918
4648
  const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
3919
4649
  return (clamped + rangeSamples) / (2 * rangeSamples);
3920
4650
  },
3921
4651
  [rangeSamples]
3922
4652
  );
3923
- const fractionToSample = (0, import_react18.useCallback)(
4653
+ const fractionToSample = (0, import_react20.useCallback)(
3924
4654
  (fraction) => {
3925
4655
  const clamped = Math.max(0, Math.min(1, fraction));
3926
4656
  return Math.round(clamped * 2 * rangeSamples - rangeSamples);
3927
4657
  },
3928
4658
  [rangeSamples]
3929
4659
  );
3930
- const snapTargets = (0, import_react18.useMemo)(() => {
4660
+ const snapTargets = (0, import_react20.useMemo)(() => {
3931
4661
  if (!cuePoints || cuePoints.beats.length === 0) return [];
3932
4662
  const downbeat = cuePoints.beats[0];
3933
4663
  const positives = cuePoints.beats.map((b) => b - downbeat);
3934
4664
  const negatives = positives.slice(1).map((p) => -p);
3935
4665
  return [...negatives, ...positives].sort((a, b) => a - b);
3936
4666
  }, [cuePoints]);
3937
- const snapToBeat = (0, import_react18.useCallback)(
4667
+ const snapToBeat = (0, import_react20.useCallback)(
3938
4668
  (sample) => {
3939
4669
  if (snapTargets.length === 0) return sample;
3940
4670
  let best = snapTargets[0];
@@ -3950,7 +4680,7 @@ function OffsetScrubber({
3950
4680
  },
3951
4681
  [snapTargets]
3952
4682
  );
3953
- const handlePointerDown = (0, import_react18.useCallback)(
4683
+ const handlePointerDown = (0, import_react20.useCallback)(
3954
4684
  (e) => {
3955
4685
  if (disabled || !cuePoints) return;
3956
4686
  e.preventDefault();
@@ -3984,7 +4714,7 @@ function OffsetScrubber({
3984
4714
  },
3985
4715
  [disabled, cuePoints, fractionToSample, onChange, snapToBeat]
3986
4716
  );
3987
- const handleResetToZero = (0, import_react18.useCallback)(() => {
4717
+ const handleResetToZero = (0, import_react20.useCallback)(() => {
3988
4718
  if (disabled) return;
3989
4719
  setDraftOffset(0);
3990
4720
  onChange(0);
@@ -3992,7 +4722,7 @@ function OffsetScrubber({
3992
4722
  const thumbFraction = sampleToFraction(draftOffset);
3993
4723
  const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;
3994
4724
  const bpmMismatch = cuePoints?.detected_bpm != null && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;
3995
- const ticks = (0, import_react18.useMemo)(() => {
4725
+ const ticks = (0, import_react20.useMemo)(() => {
3996
4726
  if (!cuePoints) return [];
3997
4727
  const downbeat = cuePoints.beats[0] ?? 0;
3998
4728
  return cuePoints.beats.map((b, i) => {
@@ -4003,9 +4733,9 @@ function OffsetScrubber({
4003
4733
  });
4004
4734
  }, [cuePoints, sampleToFraction]);
4005
4735
  const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
4006
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
4007
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
4008
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
4736
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
4737
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
4738
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
4009
4739
  "div",
4010
4740
  {
4011
4741
  ref: trackRef,
@@ -4021,7 +4751,7 @@ function OffsetScrubber({
4021
4751
  "aria-valuenow": draftOffset,
4022
4752
  "aria-disabled": isDisabled,
4023
4753
  children: [
4024
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4754
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4025
4755
  "div",
4026
4756
  {
4027
4757
  "aria-hidden": "true",
@@ -4029,7 +4759,7 @@ function OffsetScrubber({
4029
4759
  style: { left: "50%" }
4030
4760
  }
4031
4761
  ),
4032
- ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4762
+ ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4033
4763
  "div",
4034
4764
  {
4035
4765
  "data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
@@ -4044,7 +4774,7 @@ function OffsetScrubber({
4044
4774
  },
4045
4775
  t.i
4046
4776
  )),
4047
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4777
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4048
4778
  "div",
4049
4779
  {
4050
4780
  "data-testid": "offset-scrubber-thumb",
@@ -4061,7 +4791,7 @@ function OffsetScrubber({
4061
4791
  ]
4062
4792
  }
4063
4793
  ),
4064
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4794
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4065
4795
  "span",
4066
4796
  {
4067
4797
  "data-testid": "offset-scrubber-readout",
@@ -4069,7 +4799,7 @@ function OffsetScrubber({
4069
4799
  children: formatOffset(draftOffset, sampleRate)
4070
4800
  }
4071
4801
  ),
4072
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4802
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4073
4803
  "button",
4074
4804
  {
4075
4805
  type: "button",
@@ -4081,7 +4811,7 @@ function OffsetScrubber({
4081
4811
  children: "\u2316"
4082
4812
  }
4083
4813
  ),
4084
- bpmMismatch && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4814
+ bpmMismatch && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4085
4815
  "span",
4086
4816
  {
4087
4817
  "data-testid": "offset-bpm-mismatch",
@@ -4153,13 +4883,13 @@ function synthesizeCuePoints({
4153
4883
  }
4154
4884
 
4155
4885
  // src/hooks/useSceneState.ts
4156
- var import_react19 = require("react");
4886
+ var import_react21 = require("react");
4157
4887
  function useSceneState(activeSceneId, initialValue) {
4158
- const [stateMap, setStateMap] = (0, import_react19.useState)(() => /* @__PURE__ */ new Map());
4159
- const activeSceneIdRef = (0, import_react19.useRef)(activeSceneId);
4888
+ const [stateMap, setStateMap] = (0, import_react21.useState)(() => /* @__PURE__ */ new Map());
4889
+ const activeSceneIdRef = (0, import_react21.useRef)(activeSceneId);
4160
4890
  activeSceneIdRef.current = activeSceneId;
4161
4891
  const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
4162
- const setForCurrentScene = (0, import_react19.useCallback)((value) => {
4892
+ const setForCurrentScene = (0, import_react21.useCallback)((value) => {
4163
4893
  const sid = activeSceneIdRef.current;
4164
4894
  if (sid === null) return;
4165
4895
  setStateMap((prev) => {
@@ -4170,7 +4900,7 @@ function useSceneState(activeSceneId, initialValue) {
4170
4900
  return newMap;
4171
4901
  });
4172
4902
  }, [initialValue]);
4173
- const setForScene = (0, import_react19.useCallback)((sceneId, value) => {
4903
+ const setForScene = (0, import_react21.useCallback)((sceneId, value) => {
4174
4904
  setStateMap((prev) => {
4175
4905
  const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
4176
4906
  const next = typeof value === "function" ? value(current) : value;
@@ -4183,10 +4913,10 @@ function useSceneState(activeSceneId, initialValue) {
4183
4913
  }
4184
4914
 
4185
4915
  // src/hooks/useAnySolo.ts
4186
- var import_react20 = require("react");
4916
+ var import_react22 = require("react");
4187
4917
  function useAnySolo(host) {
4188
- const [anySolo, setAnySolo] = (0, import_react20.useState)(false);
4189
- (0, import_react20.useEffect)(() => {
4918
+ const [anySolo, setAnySolo] = (0, import_react22.useState)(false);
4919
+ (0, import_react22.useEffect)(() => {
4190
4920
  let active = true;
4191
4921
  const refresh = () => {
4192
4922
  host.isAnySoloActive().then((v) => {
@@ -4205,7 +4935,7 @@ function useAnySolo(host) {
4205
4935
  }
4206
4936
 
4207
4937
  // src/hooks/useSoundHistory.ts
4208
- var import_react21 = require("react");
4938
+ var import_react23 = require("react");
4209
4939
  var EMPTY = { entries: [], cursor: -1 };
4210
4940
  function sameDescriptor(a, b) {
4211
4941
  if (a === b) return true;
@@ -4217,14 +4947,14 @@ function sameDescriptor(a, b) {
4217
4947
  }
4218
4948
  function useSoundHistory(applySound, opts = {}) {
4219
4949
  const max = Math.max(2, opts.max ?? 24);
4220
- const applyRef = (0, import_react21.useRef)(applySound);
4950
+ const applyRef = (0, import_react23.useRef)(applySound);
4221
4951
  applyRef.current = applySound;
4222
- const onChangeRef = (0, import_react21.useRef)(opts.onChange);
4952
+ const onChangeRef = (0, import_react23.useRef)(opts.onChange);
4223
4953
  onChangeRef.current = opts.onChange;
4224
- const dataRef = (0, import_react21.useRef)({});
4225
- const [, setVersion] = (0, import_react21.useState)(0);
4226
- const bump = (0, import_react21.useCallback)(() => setVersion((v) => v + 1), []);
4227
- const commit = (0, import_react21.useCallback)(
4954
+ const dataRef = (0, import_react23.useRef)({});
4955
+ const [, setVersion] = (0, import_react23.useState)(0);
4956
+ const bump = (0, import_react23.useCallback)(() => setVersion((v) => v + 1), []);
4957
+ const commit = (0, import_react23.useCallback)(
4228
4958
  (trackId, next, notify) => {
4229
4959
  dataRef.current = { ...dataRef.current, [trackId]: next };
4230
4960
  bump();
@@ -4232,7 +4962,7 @@ function useSoundHistory(applySound, opts = {}) {
4232
4962
  },
4233
4963
  [bump]
4234
4964
  );
4235
- const record = (0, import_react21.useCallback)(
4965
+ const record = (0, import_react23.useCallback)(
4236
4966
  (trackId, descriptor, label) => {
4237
4967
  const h = dataRef.current[trackId];
4238
4968
  const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
@@ -4247,7 +4977,7 @@ function useSoundHistory(applySound, opts = {}) {
4247
4977
  },
4248
4978
  [max, commit]
4249
4979
  );
4250
- const restoreTo = (0, import_react21.useCallback)(
4980
+ const restoreTo = (0, import_react23.useCallback)(
4251
4981
  async (trackId, index) => {
4252
4982
  const h = dataRef.current[trackId];
4253
4983
  if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
@@ -4257,7 +4987,7 @@ function useSoundHistory(applySound, opts = {}) {
4257
4987
  },
4258
4988
  [commit]
4259
4989
  );
4260
- const undo = (0, import_react21.useCallback)(
4990
+ const undo = (0, import_react23.useCallback)(
4261
4991
  (trackId) => {
4262
4992
  const h = dataRef.current[trackId];
4263
4993
  if (!h || h.cursor <= 0) return Promise.resolve(false);
@@ -4265,7 +4995,7 @@ function useSoundHistory(applySound, opts = {}) {
4265
4995
  },
4266
4996
  [restoreTo]
4267
4997
  );
4268
- const toggleFavorite = (0, import_react21.useCallback)(
4998
+ const toggleFavorite = (0, import_react23.useCallback)(
4269
4999
  (trackId, index) => {
4270
5000
  const h = dataRef.current[trackId];
4271
5001
  if (!h || index < 0 || index >= h.entries.length) return;
@@ -4274,7 +5004,7 @@ function useSoundHistory(applySound, opts = {}) {
4274
5004
  },
4275
5005
  [commit]
4276
5006
  );
4277
- const restore = (0, import_react21.useCallback)(
5007
+ const restore = (0, import_react23.useCallback)(
4278
5008
  (trackId, state) => {
4279
5009
  const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
4280
5010
  const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
@@ -4283,15 +5013,15 @@ function useSoundHistory(applySound, opts = {}) {
4283
5013
  },
4284
5014
  [commit]
4285
5015
  );
4286
- const list = (0, import_react21.useCallback)(
5016
+ const list = (0, import_react23.useCallback)(
4287
5017
  (trackId) => dataRef.current[trackId] ?? EMPTY,
4288
5018
  []
4289
5019
  );
4290
- const canUndo = (0, import_react21.useCallback)((trackId) => {
5020
+ const canUndo = (0, import_react23.useCallback)((trackId) => {
4291
5021
  const h = dataRef.current[trackId];
4292
5022
  return !!h && h.cursor > 0;
4293
5023
  }, []);
4294
- const clear = (0, import_react21.useCallback)(
5024
+ const clear = (0, import_react23.useCallback)(
4295
5025
  (trackId) => {
4296
5026
  if (dataRef.current[trackId]) {
4297
5027
  const next = { ...dataRef.current };
@@ -4303,102 +5033,18 @@ function useSoundHistory(applySound, opts = {}) {
4303
5033
  },
4304
5034
  [bump]
4305
5035
  );
4306
- const reset = (0, import_react21.useCallback)(() => {
5036
+ const reset = (0, import_react23.useCallback)(() => {
4307
5037
  dataRef.current = {};
4308
5038
  bump();
4309
5039
  }, [bump]);
4310
- return (0, import_react21.useMemo)(
5040
+ return (0, import_react23.useMemo)(
4311
5041
  () => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),
4312
5042
  [record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite]
4313
5043
  );
4314
5044
  }
4315
5045
 
4316
- // src/hooks/useTrackReorder.ts
4317
- var import_react22 = require("react");
4318
- function moveItem(arr, from, to) {
4319
- const next = arr.slice();
4320
- if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
4321
- return next;
4322
- }
4323
- const [moved] = next.splice(from, 1);
4324
- next.splice(to, 0, moved);
4325
- return next;
4326
- }
4327
- function useTrackReorder({
4328
- host,
4329
- items,
4330
- setItems,
4331
- getId,
4332
- onError
4333
- }) {
4334
- const [draggingIndex, setDraggingIndex] = (0, import_react22.useState)(null);
4335
- const [dragOverIndex, setDragOverIndex] = (0, import_react22.useState)(null);
4336
- const fromRef = (0, import_react22.useRef)(null);
4337
- const itemsRef = (0, import_react22.useRef)(items);
4338
- itemsRef.current = items;
4339
- const dragPropsFor = (0, import_react22.useCallback)(
4340
- (index) => ({
4341
- handleProps: {
4342
- draggable: true,
4343
- onDragStart: (e) => {
4344
- fromRef.current = index;
4345
- setDraggingIndex(index);
4346
- if (e.dataTransfer) {
4347
- e.dataTransfer.effectAllowed = "move";
4348
- try {
4349
- e.dataTransfer.setData("text/plain", String(index));
4350
- } catch {
4351
- }
4352
- }
4353
- },
4354
- onDragEnd: () => {
4355
- fromRef.current = null;
4356
- setDraggingIndex(null);
4357
- setDragOverIndex(null);
4358
- }
4359
- },
4360
- rowProps: {
4361
- onDragEnter: (e) => {
4362
- if (fromRef.current === null) return;
4363
- e.preventDefault();
4364
- setDragOverIndex(index);
4365
- },
4366
- onDragOver: (e) => {
4367
- if (fromRef.current === null) return;
4368
- e.preventDefault();
4369
- if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
4370
- setDragOverIndex((cur) => cur === index ? cur : index);
4371
- },
4372
- onDragLeave: () => {
4373
- setDragOverIndex((cur) => cur === index ? null : cur);
4374
- },
4375
- onDrop: (e) => {
4376
- e.preventDefault();
4377
- const from = fromRef.current;
4378
- fromRef.current = null;
4379
- setDraggingIndex(null);
4380
- setDragOverIndex(null);
4381
- if (from === null || from === index) return;
4382
- const prev = itemsRef.current;
4383
- const next = moveItem(prev, from, index);
4384
- setItems(next);
4385
- const ids = next.map(getId);
4386
- Promise.resolve(host.reorderTracks(ids)).catch((err) => {
4387
- setItems(prev);
4388
- onError?.(err);
4389
- });
4390
- }
4391
- },
4392
- isDragging: draggingIndex === index,
4393
- isDragTarget: dragOverIndex === index && draggingIndex !== index
4394
- }),
4395
- [host, setItems, getId, onError, draggingIndex, dragOverIndex]
4396
- );
4397
- return { dragPropsFor, draggingIndex, dragOverIndex };
4398
- }
4399
-
4400
5046
  // src/constants/sdk-version.ts
4401
- var PLUGIN_SDK_VERSION = "2.28.0";
5047
+ var PLUGIN_SDK_VERSION = "2.34.0";
4402
5048
 
4403
5049
  // src/utils/format-concurrent-tracks.ts
4404
5050
  function formatConcurrentTracks(ctx) {
@@ -4542,6 +5188,8 @@ function pickTopKWeighted(scored, options = {}) {
4542
5188
  }
4543
5189
  // Annotate the CommonJS export names for ESM import in node:
4544
5190
  0 && (module.exports = {
5191
+ AUDIO_EFFECTS,
5192
+ AUDIO_EFFECT_LABEL,
4545
5193
  ConfirmDialog,
4546
5194
  CrossfadeModal,
4547
5195
  CrossfadeTrackRow,
@@ -4580,34 +5228,49 @@ function pickTopKWeighted(scored, options = {}) {
4580
5228
  ScrollingWaveform,
4581
5229
  SorceryProgressBar,
4582
5230
  TEXTURAL_ROLES,
5231
+ TRANSITION_DESIGNER_DRAFT_KEY,
4583
5232
  TrackDrawer,
4584
5233
  TrackMeterStrip,
4585
5234
  TrackRow,
5235
+ TransitionDesigner,
4586
5236
  VolumeSlider,
4587
5237
  WaveformView,
4588
5238
  analyzeWavPeak,
5239
+ asAudioEffect,
4589
5240
  asCrossfadeMeta,
4590
5241
  asFadeMeta,
5242
+ asTransitionDesignerDraft,
4591
5243
  buildCrossfadeInpaintPrompt,
4592
5244
  buildCrossfadeVolumeCurves,
4593
5245
  buildFadeVolumeCurve,
5246
+ buildRowSlots,
4594
5247
  calculateTimeBasedTarget,
4595
5248
  cellToPx,
4596
5249
  centerScrollTop,
4597
5250
  computePeaks,
5251
+ dbIdsFromKeys,
4598
5252
  dbToSlider,
4599
5253
  defaultFadeGesture,
4600
5254
  drawWaveform,
4601
5255
  formatConcurrentTracks,
5256
+ hashString,
4602
5257
  moveItem,
5258
+ normalizeSlots,
5259
+ padPair,
5260
+ padSlots,
4603
5261
  parseCrossfadePairs,
4604
5262
  parseFades,
4605
5263
  pickTopKWeighted,
4606
5264
  pitchToName,
4607
5265
  pxToCell,
5266
+ reconcileSlots,
4608
5267
  resizeNoteDuration,
5268
+ rowKey,
5269
+ rowType,
4609
5270
  scorePromptMatch,
4610
5271
  sliderToDb,
5272
+ slotsEqual,
5273
+ soundIdentity,
4611
5274
  synthesizeCuePoints,
4612
5275
  tokenizePrompt,
4613
5276
  transposeNotes,