dialkit 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +433 -3
  2. package/dist/icons.d.ts +5 -1
  3. package/dist/icons.js +18 -0
  4. package/dist/icons.js.map +1 -1
  5. package/dist/index.cjs +3222 -479
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +274 -6
  8. package/dist/index.d.ts +274 -6
  9. package/dist/index.js +3154 -416
  10. package/dist/index.js.map +1 -1
  11. package/dist/solid/index.d.ts +225 -3
  12. package/dist/solid/index.js +5761 -2500
  13. package/dist/solid/index.js.map +1 -1
  14. package/dist/store/index.cjs +57 -16
  15. package/dist/store/index.cjs.map +1 -1
  16. package/dist/store/index.d.cts +14 -2
  17. package/dist/store/index.d.ts +14 -2
  18. package/dist/store/index.js +52 -16
  19. package/dist/store/index.js.map +1 -1
  20. package/dist/styles.css +788 -0
  21. package/dist/svelte/components/ControlRenderer.svelte +5 -2
  22. package/dist/svelte/components/ControlRenderer.svelte.d.ts +2 -0
  23. package/dist/svelte/components/ControlRenderer.svelte.d.ts.map +1 -1
  24. package/dist/svelte/components/DialRoot.svelte +43 -6
  25. package/dist/svelte/components/DialRoot.svelte.d.ts.map +1 -1
  26. package/dist/svelte/components/Panel.svelte +7 -1
  27. package/dist/svelte/components/Panel.svelte.d.ts +2 -0
  28. package/dist/svelte/components/Panel.svelte.d.ts.map +1 -1
  29. package/dist/svelte/components/Timeline/ClipPopover.svelte +206 -0
  30. package/dist/svelte/components/Timeline/ClipPopover.svelte.d.ts +26 -0
  31. package/dist/svelte/components/Timeline/ClipPopover.svelte.d.ts.map +1 -0
  32. package/dist/svelte/components/Timeline/DialTimeline.svelte +125 -0
  33. package/dist/svelte/components/Timeline/DialTimeline.svelte.d.ts +13 -0
  34. package/dist/svelte/components/Timeline/DialTimeline.svelte.d.ts.map +1 -0
  35. package/dist/svelte/components/Timeline/TimelineClip.svelte +233 -0
  36. package/dist/svelte/components/Timeline/TimelineClip.svelte.d.ts +24 -0
  37. package/dist/svelte/components/Timeline/TimelineClip.svelte.d.ts.map +1 -0
  38. package/dist/svelte/components/Timeline/TimelineSection.svelte +803 -0
  39. package/dist/svelte/components/Timeline/TimelineSection.svelte.d.ts +12 -0
  40. package/dist/svelte/components/Timeline/TimelineSection.svelte.d.ts.map +1 -0
  41. package/dist/svelte/components/Timeline/TimelineToggleButton.svelte +25 -0
  42. package/dist/svelte/components/Timeline/TimelineToggleButton.svelte.d.ts +4 -0
  43. package/dist/svelte/components/Timeline/TimelineToggleButton.svelte.d.ts.map +1 -0
  44. package/dist/svelte/components/TransitionControl.svelte +26 -11
  45. package/dist/svelte/components/TransitionControl.svelte.d.ts +9 -0
  46. package/dist/svelte/components/TransitionControl.svelte.d.ts.map +1 -1
  47. package/dist/svelte/createDialTimeline.svelte.d.ts +4 -0
  48. package/dist/svelte/createDialTimeline.svelte.d.ts.map +1 -0
  49. package/dist/svelte/createDialTimeline.svelte.js +73 -0
  50. package/dist/svelte/index.d.ts +4 -0
  51. package/dist/svelte/index.d.ts.map +1 -1
  52. package/dist/svelte/index.js +3 -0
  53. package/dist/svelte/theme-css.d.ts +1 -1
  54. package/dist/svelte/theme-css.d.ts.map +1 -1
  55. package/dist/svelte/theme-css.js +788 -0
  56. package/dist/timeline/index.cjs +1288 -0
  57. package/dist/timeline/index.cjs.map +1 -0
  58. package/dist/timeline/index.d.cts +443 -0
  59. package/dist/timeline/index.d.ts +443 -0
  60. package/dist/timeline/index.js +1233 -0
  61. package/dist/timeline/index.js.map +1 -0
  62. package/dist/vue/index.d.ts +273 -7
  63. package/dist/vue/index.js +4116 -1517
  64. package/dist/vue/index.js.map +1 -1
  65. package/package.json +23 -13
  66. package/dist/solid/index.cjs +0 -3536
  67. package/dist/solid/index.cjs.map +0 -1
  68. package/dist/solid/index.d.cts +0 -295
  69. package/dist/vue/index.cjs +0 -3497
  70. package/dist/vue/index.cjs.map +0 -1
  71. package/dist/vue/index.d.cts +0 -722
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/hooks/useDialKit.ts
4
- import { useCallback, useEffect, useId, useMemo, useRef, useSyncExternalStore } from "react";
4
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo, useRef as useRef2 } from "react";
5
5
 
6
6
  // src/store/DialStore.ts
7
7
  var EMPTY_VALUES = Object.freeze({});
@@ -71,6 +71,19 @@ function isSpringConfigValue(value) {
71
71
  function isEasingConfigValue(value) {
72
72
  return hasType(value, "easing");
73
73
  }
74
+ function isHexColor(value) {
75
+ return /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(value);
76
+ }
77
+ function formatLabel(key) {
78
+ return key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
79
+ }
80
+ function inferStep(min, max) {
81
+ const range = max - min;
82
+ if (range <= 1) return 0.01;
83
+ if (range <= 10) return 0.1;
84
+ if (range <= 100) return 1;
85
+ return 10;
86
+ }
74
87
  function isActionConfigValue(value) {
75
88
  return hasType(value, "action");
76
89
  }
@@ -91,6 +104,9 @@ function getFirstOptionValue(options) {
91
104
  var DialStoreClass = class {
92
105
  constructor() {
93
106
  this.panels = /* @__PURE__ */ new Map();
107
+ this.panelsSnapshot = [];
108
+ this.standardPanelsSnapshot = [];
109
+ this.timelinePanelsSnapshot = [];
94
110
  this.listeners = /* @__PURE__ */ new Map();
95
111
  this.globalListeners = /* @__PURE__ */ new Set();
96
112
  this.snapshots = /* @__PURE__ */ new Map();
@@ -104,6 +120,12 @@ var DialStoreClass = class {
104
120
  this.persistConfigs = /* @__PURE__ */ new Map();
105
121
  }
106
122
  registerPanel(id, name, config, shortcuts, options = {}) {
123
+ const existingPanel = this.panels.get(id);
124
+ if (existingPanel && existingPanel.kind !== options.kind) {
125
+ console.warn(
126
+ `[dialkit] Panel id "${id}" cannot be shared by a timeline and a standard panel; the most recent registration controls where it renders.`
127
+ );
128
+ }
107
129
  this.configurePanelRetention(id, options);
108
130
  this.registrationCounts.set(id, (this.registrationCounts.get(id) ?? 0) + 1);
109
131
  const controls = this.parseConfig(config, "", shortcuts);
@@ -115,7 +137,7 @@ var DialStoreClass = class {
115
137
  const values = this.reconcileValues(defaultValues, previousValues, controlsByPath);
116
138
  const previousBaseValues = this.baseValues.get(id) ?? persisted?.baseValues ?? persisted?.values ?? {};
117
139
  const baseValues = this.reconcileValues(defaultValues, previousBaseValues, controlsByPath);
118
- this.panels.set(id, { id, name, controls, values, shortcuts: shortcuts ?? {} });
140
+ this.panels.set(id, { id, name, controls, values, shortcuts: shortcuts ?? {}, kind: options.kind });
119
141
  this.snapshots.set(id, { ...values });
120
142
  this.baseValues.set(id, baseValues);
121
143
  this.defaultValues.set(id, { ...defaultValues });
@@ -142,7 +164,7 @@ var DialStoreClass = class {
142
164
  const defaultValues = this.flattenValues(config, "");
143
165
  this.initTransitionModes(config, "", defaultValues);
144
166
  const nextValues = this.reconcileValues(defaultValues, existing.values, controlsByPath);
145
- const nextPanel = { id, name, controls, values: nextValues, shortcuts: shortcuts ?? existing.shortcuts };
167
+ const nextPanel = { id, name, controls, values: nextValues, shortcuts: shortcuts ?? existing.shortcuts, kind: options.kind ?? existing.kind };
146
168
  this.panels.set(id, nextPanel);
147
169
  this.snapshots.set(id, { ...nextValues });
148
170
  const previousBaseValues = this.baseValues.get(id) ?? {};
@@ -167,8 +189,8 @@ var DialStoreClass = class {
167
189
  }
168
190
  this.registrationCounts.delete(id);
169
191
  this.panels.delete(id);
170
- this.listeners.delete(id);
171
- this.actionListeners.delete(id);
192
+ if (this.listeners.get(id)?.size === 0) this.listeners.delete(id);
193
+ if (this.actionListeners.get(id)?.size === 0) this.actionListeners.delete(id);
172
194
  if (!this.retainedPanels.has(id)) {
173
195
  this.snapshots.delete(id);
174
196
  this.baseValues.delete(id);
@@ -260,8 +282,10 @@ var DialStoreClass = class {
260
282
  getValues(panelId) {
261
283
  return this.snapshots.get(panelId) ?? EMPTY_VALUES;
262
284
  }
263
- getPanels() {
264
- return Array.from(this.panels.values());
285
+ getPanels(kind) {
286
+ if (kind === "panel") return this.standardPanelsSnapshot;
287
+ if (kind === "timeline") return this.timelinePanelsSnapshot;
288
+ return this.panelsSnapshot;
265
289
  }
266
290
  getPanel(id) {
267
291
  return this.panels.get(id);
@@ -272,7 +296,11 @@ var DialStoreClass = class {
272
296
  }
273
297
  this.listeners.get(panelId).add(listener);
274
298
  return () => {
275
- this.listeners.get(panelId)?.delete(listener);
299
+ const listeners = this.listeners.get(panelId);
300
+ listeners?.delete(listener);
301
+ if (listeners?.size === 0 && !this.panels.has(panelId)) {
302
+ this.listeners.delete(panelId);
303
+ }
276
304
  };
277
305
  }
278
306
  subscribeGlobal(listener) {
@@ -285,7 +313,11 @@ var DialStoreClass = class {
285
313
  }
286
314
  this.actionListeners.get(panelId).add(listener);
287
315
  return () => {
288
- this.actionListeners.get(panelId)?.delete(listener);
316
+ const listeners = this.actionListeners.get(panelId);
317
+ listeners?.delete(listener);
318
+ if (listeners?.size === 0 && !this.panels.has(panelId)) {
319
+ this.actionListeners.delete(panelId);
320
+ }
289
321
  };
290
322
  }
291
323
  triggerAction(panelId, path) {
@@ -480,6 +512,9 @@ var DialStoreClass = class {
480
512
  this.listeners.get(panelId)?.forEach((fn) => fn());
481
513
  }
482
514
  notifyGlobal() {
515
+ this.panelsSnapshot = Array.from(this.panels.values());
516
+ this.standardPanelsSnapshot = this.panelsSnapshot.filter((panel) => panel.kind !== "timeline");
517
+ this.timelinePanelsSnapshot = this.panelsSnapshot.filter((panel) => panel.kind === "timeline");
483
518
  this.globalListeners.forEach((fn) => fn());
484
519
  }
485
520
  initTransitionModes(config, prefix, values) {
@@ -595,10 +630,10 @@ var DialStoreClass = class {
595
630
  return typeof value === "object" && value !== null && "type" in value && value.type === "text";
596
631
  }
597
632
  isHexColor(value) {
598
- return /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(value);
633
+ return isHexColor(value);
599
634
  }
600
635
  formatLabel(key) {
601
- return key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
636
+ return formatLabel(key);
602
637
  }
603
638
  inferRange(value) {
604
639
  if (value >= 0 && value <= 1) {
@@ -614,11 +649,7 @@ var DialStoreClass = class {
614
649
  }
615
650
  }
616
651
  inferStep(min, max) {
617
- const range = max - min;
618
- if (range <= 1) return 0.01;
619
- if (range <= 10) return 0.1;
620
- if (range <= 100) return 1;
621
- return 10;
652
+ return inferStep(min, max);
622
653
  }
623
654
  normalizePreservedValue(existingValue, defaultValue, control) {
624
655
  if (existingValue === void 0 || !control) {
@@ -690,31 +721,33 @@ var DialStoreClass = class {
690
721
  return map;
691
722
  }
692
723
  };
693
- var DialStore = new DialStoreClass();
724
+ var DialStore = /* @__PURE__ */ new DialStoreClass();
694
725
 
695
- // src/hooks/useDialKit.ts
696
- function useDialKit(name, config, options) {
697
- return useDialKitController(name, config, options).values;
726
+ // src/hooks/useDialStorePanel.ts
727
+ import { useCallback, useEffect, useId, useRef, useSyncExternalStore } from "react";
728
+ function useSerialized(value) {
729
+ const ref = useRef();
730
+ if (!ref.current || !Object.is(ref.current.value, value)) {
731
+ ref.current = { value, text: JSON.stringify(value) };
732
+ }
733
+ return ref.current.text;
698
734
  }
699
- function useDialKitController(name, config, options) {
735
+ function useDialStorePanel(name, config, options = {}) {
700
736
  const instanceId = useId();
701
- const hasStableId = options?.id !== void 0;
702
- const panelId = options?.id ?? `${name}-${instanceId}`;
737
+ const hasStableId = options.id !== void 0;
738
+ const panelId = options.id ?? `${name}-${instanceId}`;
703
739
  const configRef = useRef(config);
704
- const serializedConfig = JSON.stringify(config);
705
740
  configRef.current = config;
706
- const onActionRef = useRef(options?.onAction);
707
- onActionRef.current = options?.onAction;
708
- const shortcutsRef = useRef(options?.shortcuts);
709
- shortcutsRef.current = options?.shortcuts;
710
- const persistRef = useRef(options?.persist);
711
- persistRef.current = options?.persist;
712
- const serializedShortcuts = JSON.stringify(options?.shortcuts);
713
- const serializedPersist = JSON.stringify(options?.persist);
741
+ const optionsRef = useRef(options);
742
+ optionsRef.current = options;
743
+ const serializedConfig = useSerialized(config);
744
+ const serializedShortcuts = useSerialized(options.shortcuts);
745
+ const serializedPersist = useSerialized(options.persist);
714
746
  useEffect(() => {
715
- DialStore.registerPanel(panelId, name, configRef.current, shortcutsRef.current, {
747
+ DialStore.registerPanel(panelId, name, configRef.current, optionsRef.current.shortcuts, {
716
748
  retainOnUnmount: hasStableId,
717
- persist: persistRef.current
749
+ persist: optionsRef.current.persist,
750
+ kind: optionsRef.current.kind
718
751
  });
719
752
  return () => DialStore.unregisterPanel(panelId);
720
753
  }, [hasStableId, panelId, name]);
@@ -724,45 +757,61 @@ function useDialKitController(name, config, options) {
724
757
  mountedRef.current = true;
725
758
  return;
726
759
  }
727
- DialStore.updatePanel(panelId, name, configRef.current, shortcutsRef.current, {
760
+ DialStore.updatePanel(panelId, name, configRef.current, optionsRef.current.shortcuts, {
728
761
  retainOnUnmount: hasStableId,
729
- persist: persistRef.current
762
+ persist: optionsRef.current.persist,
763
+ kind: optionsRef.current.kind
730
764
  });
731
765
  }, [hasStableId, panelId, name, serializedConfig, serializedShortcuts, serializedPersist]);
732
- useEffect(() => {
733
- return DialStore.subscribeActions(panelId, (action) => {
734
- onActionRef.current?.(action);
735
- });
736
- }, [panelId]);
737
766
  const subscribe = useCallback(
738
767
  (callback) => DialStore.subscribe(panelId, callback),
739
768
  [panelId]
740
769
  );
741
- const getSnapshot = useCallback(
742
- () => DialStore.getValues(panelId),
743
- [panelId]
744
- );
770
+ const getSnapshot = useCallback(() => DialStore.getValues(panelId), [panelId]);
745
771
  const flatValues = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
772
+ return { panelId, flatValues, serializedConfig };
773
+ }
774
+
775
+ // src/hooks/useDialKit.ts
776
+ function useDialKit(name, config, options) {
777
+ return useDialKitController(name, config, options).values;
778
+ }
779
+ function useDialKitController(name, config, options) {
780
+ const { panelId, flatValues, serializedConfig } = useDialStorePanel(name, config, {
781
+ id: options?.id,
782
+ persist: options?.persist,
783
+ shortcuts: options?.shortcuts
784
+ });
785
+ const configRef = useRef2(config);
786
+ configRef.current = config;
787
+ const onActionRef = useRef2(options?.onAction);
788
+ onActionRef.current = options?.onAction;
789
+ useEffect2(() => {
790
+ return DialStore.subscribeActions(panelId, (action) => {
791
+ onActionRef.current?.(action);
792
+ });
793
+ }, [panelId]);
746
794
  const values = useMemo(
747
795
  () => resolveDialValues(configRef.current, flatValues),
796
+ // eslint-disable-next-line react-hooks/exhaustive-deps
748
797
  [flatValues, serializedConfig]
749
798
  );
750
- const setValue = useCallback(
799
+ const setValue = useCallback2(
751
800
  (path, value) => {
752
801
  DialStore.updateValue(panelId, path, value);
753
802
  },
754
803
  [panelId]
755
804
  );
756
- const setValues = useCallback(
805
+ const setValues = useCallback2(
757
806
  (nextValues) => {
758
807
  DialStore.updateValues(panelId, flattenDialValueUpdates(configRef.current, nextValues));
759
808
  },
760
809
  [panelId]
761
810
  );
762
- const resetValues = useCallback(() => {
811
+ const resetValues = useCallback2(() => {
763
812
  DialStore.resetValues(panelId);
764
813
  }, [panelId]);
765
- const getValues = useCallback(
814
+ const getValues = useCallback2(
766
815
  () => resolveDialValues(configRef.current, DialStore.getValues(panelId)),
767
816
  [panelId]
768
817
  );
@@ -779,16 +828,225 @@ function useDialKitController(name, config, options) {
779
828
  }
780
829
 
781
830
  // src/components/DialRoot.tsx
782
- import { useEffect as useEffect8, useState as useState10, useRef as useRef11, useCallback as useCallback10 } from "react";
831
+ import { useEffect as useEffect9, useState as useState10, useRef as useRef12, useCallback as useCallback12 } from "react";
783
832
  import { createPortal as createPortal3 } from "react-dom";
784
833
 
834
+ // src/store/TimelineStore.ts
835
+ function loopSpan(duration, loopStart) {
836
+ if (!Number.isFinite(duration) || duration <= 0) return 0;
837
+ if (!Number.isFinite(loopStart)) loopStart = 0;
838
+ const start = Math.min(Math.max(0, loopStart), duration);
839
+ return duration - start > 0 ? duration - start : duration;
840
+ }
841
+ function foldLoopTime(time, duration, loopStart = 0) {
842
+ if (!Number.isFinite(time) || !Number.isFinite(duration) || duration <= 0) {
843
+ return { time: 0, wraps: 0 };
844
+ }
845
+ if (time < duration) return { time, wraps: 0 };
846
+ const span = loopSpan(duration, loopStart);
847
+ const base = duration - span;
848
+ const over = time - base;
849
+ return { time: base + over % span, wraps: Math.floor(over / span) };
850
+ }
851
+ var TIMELINE_CLIP_COLORS = [
852
+ "#E8E8E8"
853
+ // neutral white — slightly off-white so the pure-white selection ring still reads
854
+ ];
855
+ var EMPTY_TRANSPORT = Object.freeze({ time: 0, playing: false, duration: 0, wraps: 0 });
856
+ var TimelineStoreClass = class {
857
+ constructor() {
858
+ this.timelines = /* @__PURE__ */ new Map();
859
+ this.transports = /* @__PURE__ */ new Map();
860
+ this.listeners = /* @__PURE__ */ new Map();
861
+ this.globalListeners = /* @__PURE__ */ new Set();
862
+ this.registrationCounts = /* @__PURE__ */ new Map();
863
+ this.listCache = null;
864
+ this.rafId = null;
865
+ this.lastTick = 0;
866
+ this.tick = (now) => {
867
+ const dt = Math.max(0, (now - this.lastTick) / 1e3);
868
+ this.lastTick = now;
869
+ let anyPlaying = false;
870
+ for (const [id, transport] of this.transports) {
871
+ if (!transport.playing) continue;
872
+ const meta = this.timelines.get(id);
873
+ const duration = meta?.duration ?? transport.duration;
874
+ if (!Number.isFinite(duration) || duration <= 0) {
875
+ this.transports.set(id, { time: 0, playing: false, duration: 0, wraps: 0 });
876
+ this.notify(id);
877
+ continue;
878
+ }
879
+ let time = transport.time + dt;
880
+ let playing = true;
881
+ let wraps = transport.wraps;
882
+ if (time >= duration) {
883
+ if (meta?.loop) {
884
+ const folded = foldLoopTime(time, duration, meta.loopStart);
885
+ time = folded.time;
886
+ wraps += folded.wraps;
887
+ } else {
888
+ time = duration;
889
+ playing = false;
890
+ }
891
+ }
892
+ this.transports.set(id, { time, playing, duration, wraps });
893
+ if (playing) anyPlaying = true;
894
+ this.notify(id);
895
+ }
896
+ this.rafId = anyPlaying ? window.requestAnimationFrame(this.tick) : null;
897
+ };
898
+ }
899
+ register(meta, options) {
900
+ const existing = this.timelines.get(meta.id);
901
+ if (existing && existing.name !== meta.name) {
902
+ console.warn(
903
+ `[dialkit] Timeline id "${meta.id}" is already registered by "${existing.name}"; "${meta.name}" will share and overwrite that transport.`
904
+ );
905
+ }
906
+ this.registrationCounts.set(meta.id, (this.registrationCounts.get(meta.id) ?? 0) + 1);
907
+ this.applyMeta(meta, options.autoplay);
908
+ }
909
+ update(meta) {
910
+ if (!this.timelines.has(meta.id)) return;
911
+ this.applyMeta(meta, false);
912
+ }
913
+ unregister(id) {
914
+ const nextCount = (this.registrationCounts.get(id) ?? 1) - 1;
915
+ if (nextCount > 0) {
916
+ this.registrationCounts.set(id, nextCount);
917
+ return;
918
+ }
919
+ this.registrationCounts.delete(id);
920
+ this.timelines.delete(id);
921
+ this.transports.delete(id);
922
+ if (this.listeners.get(id)?.size === 0) this.listeners.delete(id);
923
+ this.listCache = null;
924
+ this.notifyGlobal();
925
+ }
926
+ play(id) {
927
+ const transport = this.transports.get(id);
928
+ if (!transport || transport.duration <= 0 || transport.playing) return;
929
+ const restart = transport.time >= transport.duration;
930
+ this.transports.set(id, {
931
+ ...transport,
932
+ time: restart ? 0 : transport.time,
933
+ wraps: restart ? 0 : transport.wraps,
934
+ playing: true
935
+ });
936
+ this.notify(id);
937
+ this.ensureLoop();
938
+ }
939
+ pause(id) {
940
+ const transport = this.transports.get(id);
941
+ if (!transport || !transport.playing) return;
942
+ this.transports.set(id, { ...transport, playing: false });
943
+ this.notify(id);
944
+ }
945
+ replay(id) {
946
+ const transport = this.transports.get(id);
947
+ if (!transport || transport.duration <= 0) return;
948
+ this.transports.set(id, { ...transport, time: 0, wraps: 0, playing: true });
949
+ this.notify(id);
950
+ this.ensureLoop();
951
+ }
952
+ seek(id, time) {
953
+ const transport = this.transports.get(id);
954
+ if (!transport || !Number.isFinite(time)) return;
955
+ const clamped = Math.min(transport.duration, Math.max(0, time));
956
+ this.transports.set(id, { ...transport, time: clamped, wraps: 0 });
957
+ this.notify(id);
958
+ }
959
+ getTransport(id) {
960
+ return this.transports.get(id) ?? EMPTY_TRANSPORT;
961
+ }
962
+ getTimeline(id) {
963
+ return this.timelines.get(id);
964
+ }
965
+ getTimelines() {
966
+ if (!this.listCache) {
967
+ this.listCache = Array.from(this.timelines.values());
968
+ }
969
+ return this.listCache;
970
+ }
971
+ subscribe(id, listener) {
972
+ if (!this.listeners.has(id)) {
973
+ this.listeners.set(id, /* @__PURE__ */ new Set());
974
+ }
975
+ this.listeners.get(id).add(listener);
976
+ return () => {
977
+ const listeners = this.listeners.get(id);
978
+ listeners?.delete(listener);
979
+ if (listeners?.size === 0 && !this.timelines.has(id)) {
980
+ this.listeners.delete(id);
981
+ }
982
+ };
983
+ }
984
+ subscribeGlobal(listener) {
985
+ this.globalListeners.add(listener);
986
+ return () => {
987
+ this.globalListeners.delete(listener);
988
+ };
989
+ }
990
+ applyMeta(meta, autoplay) {
991
+ const duration = Number.isFinite(meta.duration) ? Math.max(0, meta.duration) : 0;
992
+ const loopStart = Number.isFinite(meta.loopStart) ? Math.min(duration, Math.max(0, meta.loopStart)) : 0;
993
+ const safeMeta = { ...meta, duration, loopStart };
994
+ this.timelines.set(meta.id, safeMeta);
995
+ const existing = this.transports.get(meta.id);
996
+ if (existing) {
997
+ this.transports.set(meta.id, {
998
+ time: Math.min(existing.time, duration),
999
+ playing: duration > 0 && existing.playing,
1000
+ duration,
1001
+ wraps: existing.wraps
1002
+ });
1003
+ } else {
1004
+ const playing = duration > 0 && autoplay;
1005
+ this.transports.set(meta.id, { time: 0, playing, duration, wraps: 0 });
1006
+ if (playing) this.ensureLoop();
1007
+ }
1008
+ this.listCache = null;
1009
+ this.notify(meta.id);
1010
+ this.notifyGlobal();
1011
+ }
1012
+ ensureLoop() {
1013
+ if (this.rafId !== null || typeof window === "undefined") return;
1014
+ this.lastTick = performance.now();
1015
+ this.rafId = window.requestAnimationFrame(this.tick);
1016
+ }
1017
+ notify(id) {
1018
+ this.listeners.get(id)?.forEach((fn) => fn());
1019
+ }
1020
+ notifyGlobal() {
1021
+ this.globalListeners.forEach((fn) => fn());
1022
+ }
1023
+ };
1024
+ var TimelineStore = /* @__PURE__ */ new TimelineStoreClass();
1025
+
1026
+ // src/env.ts
1027
+ var isDevDefault = typeof process !== "undefined" && process?.env?.NODE_ENV ? process.env.NODE_ENV !== "production" : typeof import.meta !== "undefined" && import.meta.env?.MODE ? import.meta.env.MODE !== "production" : true;
1028
+
785
1029
  // src/components/Folder.tsx
786
- import { useState, useRef as useRef2, useEffect as useEffect2 } from "react";
1030
+ import { useState, useRef as useRef3, useEffect as useEffect3 } from "react";
787
1031
  import { motion, AnimatePresence } from "motion/react";
788
1032
 
789
1033
  // src/icons.ts
790
1034
  var ICON_CHEVRON = "M6 9.5L12 15.5L18 9.5";
791
1035
  var ICON_CHECK = "M5 12.75L10 19L19 5";
1036
+ var ICON_PAUSE = [
1037
+ "M6.75 3C5.23122 3 4 4.23122 4 5.75V18.25C4 19.7688 5.23122 21 6.75 21H7.25C8.76878 21 10 19.7688 10 18.25V5.75C10 4.23122 8.76878 3 7.25 3H6.75Z",
1038
+ "M16.75 3C15.2312 3 14 4.23122 14 5.75V18.25C14 19.7688 15.2312 21 16.75 21H17.25C18.7688 21 20 19.7688 20 18.25V5.75C20 4.23122 18.7688 3 17.25 3H16.75Z"
1039
+ ];
1040
+ var ICON_PLAY = "M9.24394 2.36758C7.41419 1.18362 5 2.49701 5 4.67639V19.3238C5 21.5032 7.41419 22.8166 9.24394 21.6326L20.5624 14.3089C22.2371 13.2253 22.2372 10.775 20.5624 9.69129L9.24394 2.36758Z";
1041
+ var ICON_REPLAY = [
1042
+ "M12 2.5C17.2466 2.50016 21.5 6.7534 21.5 12C21.5 17.2466 17.2466 21.4998 12 21.5C7.52191 21.5 3.76987 18.4025 2.76465 14.2344C2.63517 13.6975 2.96508 13.1578 3.50195 13.0283C4.03883 12.8988 4.57851 13.2288 4.70801 13.7656C5.5016 17.0563 8.46701 19.5 12 19.5C16.142 19.4998 19.5 16.142 19.5 12C19.5 7.85796 16.142 4.50016 12 4.5C9.32981 4.5 6.98389 5.89541 5.6543 8H7.5C8.05228 8 8.5 8.44772 8.5 9C8.5 9.55228 8.05228 10 7.5 10H3.5C2.94772 10 2.5 9.55228 2.5 9V5C2.5 4.44772 2.94772 4 3.5 4C4.05228 4 4.5 4.44772 4.5 5V6.16797C6.2376 3.93677 8.95063 2.5 12 2.5Z",
1043
+ "M10 9.94043C10 9.33379 10.6826 8.97849 11.1797 9.32617L14.1221 11.3857C14.5486 11.6843 14.5486 12.3157 14.1221 12.6143L11.1797 14.6738C10.6826 15.0215 10 14.6662 10 14.0596V9.94043Z"
1044
+ ];
1045
+ var ICON_TIMELINE = [
1046
+ "M18.868 10C20.8517 10.0003 22.2886 11.8914 21.7577 13.8027L20.369 18.8027C20.0083 20.1012 18.826 20.9999 17.4784 21H6.51941C5.17179 21 3.98948 20.1012 3.62878 18.8027L2.24011 13.8027C1.7092 11.8913 3.14603 10.0003 5.12976 10H18.868Z",
1047
+ "M18.9989 6.5C19.5511 6.50007 19.9989 6.94776 19.9989 7.5C19.9989 8.05224 19.5511 8.49993 18.9989 8.5H4.9989C4.44661 8.5 3.9989 8.05228 3.9989 7.5C3.9989 6.94772 4.44661 6.5 4.9989 6.5H18.9989Z",
1048
+ "M16.9989 3C17.5511 3.00007 17.9989 3.44776 17.9989 4C17.9989 4.55224 17.5511 4.99993 16.9989 5H6.9989C6.44661 5 5.9989 4.55228 5.9989 4C5.9989 3.44772 6.44661 3 6.9989 3H16.9989Z"
1049
+ ];
792
1050
  var ICON_CLIPBOARD = {
793
1051
  board: "M8 6C8 4.34315 9.34315 3 11 3H13C14.6569 3 16 4.34315 16 6V7H8V6Z",
794
1052
  sparkle: "M19.2405 16.1852L18.5436 14.3733C18.4571 14.1484 18.241 14 18 14C17.759 14 17.5429 14.1484 17.4564 14.3733L16.7595 16.1852C16.658 16.4493 16.4493 16.658 16.1852 16.7595L14.3733 17.4564C14.1484 17.5429 14 17.759 14 18C14 18.241 14.1484 18.4571 14.3733 18.5436L16.1852 19.2405C16.4493 19.342 16.658 19.5507 16.7595 19.8148L17.4564 21.6267C17.5429 21.8516 17.759 22 18 22C18.241 22 18.4571 21.8516 18.5436 21.6267L19.2405 19.8148C19.342 19.5507 19.5507 19.342 19.8148 19.2405L21.6267 18.5436C21.8516 18.4571 22 18.241 22 18C22 17.759 21.8516 17.5429 21.6267 17.4564L19.8148 16.7595C19.5507 16.658 19.342 16.4493 19.2405 16.1852Z",
@@ -822,16 +1080,16 @@ import { jsx, jsxs } from "react/jsx-runtime";
822
1080
  function Folder({ title, children, defaultOpen = true, isRoot = false, inline = false, onOpenChange, toolbar, panelHeightOffset = 10 }) {
823
1081
  const [isOpen, setIsOpen] = useState(defaultOpen);
824
1082
  const [isCollapsed, setIsCollapsed] = useState(!defaultOpen);
825
- const contentRef = useRef2(null);
1083
+ const contentRef = useRef3(null);
826
1084
  const [contentHeight, setContentHeight] = useState(void 0);
827
1085
  const [windowHeight, setWindowHeight] = useState(typeof window !== "undefined" ? window.innerHeight : 800);
828
- useEffect2(() => {
1086
+ useEffect3(() => {
829
1087
  if (!isRoot) return;
830
1088
  const onResize = () => setWindowHeight(window.innerHeight);
831
1089
  window.addEventListener("resize", onResize);
832
1090
  return () => window.removeEventListener("resize", onResize);
833
1091
  }, [isRoot]);
834
- useEffect2(() => {
1092
+ useEffect3(() => {
835
1093
  const el = contentRef.current;
836
1094
  if (!el) return;
837
1095
  const ro = new ResizeObserver(() => {
@@ -932,11 +1190,43 @@ function Folder({ title, children, defaultOpen = true, isRoot = false, inline =
932
1190
  }
933
1191
 
934
1192
  // src/components/Panel.tsx
935
- import { useCallback as useCallback9, useContext, useState as useState9, useSyncExternalStore as useSyncExternalStore4 } from "react";
1193
+ import { useCallback as useCallback10, useState as useState9, useSyncExternalStore as useSyncExternalStore4 } from "react";
936
1194
  import { motion as motion5, AnimatePresence as AnimatePresence4 } from "motion/react";
937
1195
 
1196
+ // src/copy-instruction.ts
1197
+ function buildCopyInstruction(hookName, panelName, values) {
1198
+ const jsonStr = JSON.stringify(values, null, 2);
1199
+ if (hookName === "useDialTimeline" || hookName === "createDialTimeline") {
1200
+ return `Update the ${hookName} configuration for "${panelName}" with these values:
1201
+
1202
+ \`\`\`json
1203
+ ${jsonStr}
1204
+ \`\`\`
1205
+
1206
+ Apply these values as the new defaults in the ${hookName} call. Keep the existing \`clip.current\` bindings while this timeline is being authored; do not convert the animation or remove DialKit yet.
1207
+
1208
+ Add this comment immediately above the ${hookName} call as a production handoff note:
1209
+
1210
+ \`\`\`tsx
1211
+ // TODO(production): DialKit's clip.current values are the scrubbable authoring preview.
1212
+ // Replace them with equivalent real Motion animations using the tuned timeline
1213
+ // timings and transitions, then remove ${hookName} and <DialTimeline />.
1214
+ \`\`\``;
1215
+ }
1216
+ return `Update the ${hookName} configuration for "${panelName}" with these values:
1217
+
1218
+ \`\`\`json
1219
+ ${jsonStr}
1220
+ \`\`\`
1221
+
1222
+ Apply these values as the new defaults in the ${hookName} call.`;
1223
+ }
1224
+
1225
+ // src/components/ControlRenderer.tsx
1226
+ import { useContext } from "react";
1227
+
938
1228
  // src/components/ShortcutListener.tsx
939
- import { createContext, useEffect as useEffect3, useRef as useRef3, useState as useState2, useCallback as useCallback2 } from "react";
1229
+ import { createContext, useEffect as useEffect4, useRef as useRef4, useState as useState2, useCallback as useCallback3 } from "react";
940
1230
 
941
1231
  // src/shortcut-utils.ts
942
1232
  function decimalsForStep(step) {
@@ -1028,11 +1318,11 @@ import { jsx as jsx2 } from "react/jsx-runtime";
1028
1318
  var ShortcutContext = createContext({ activePanelId: null, activePath: null });
1029
1319
  function ShortcutListener({ children }) {
1030
1320
  const [activeShortcut, setActiveShortcut] = useState2({ activePanelId: null, activePath: null });
1031
- const activeKeysRef = useRef3(/* @__PURE__ */ new Set());
1032
- const isDraggingRef = useRef3(false);
1033
- const lastMouseXRef = useRef3(null);
1034
- const dragAccumulatorRef = useRef3(0);
1035
- const resolveActiveTarget = useCallback2((interaction) => {
1321
+ const activeKeysRef = useRef4(/* @__PURE__ */ new Set());
1322
+ const isDraggingRef = useRef4(false);
1323
+ const lastMouseXRef = useRef4(null);
1324
+ const dragAccumulatorRef = useRef4(0);
1325
+ const resolveActiveTarget = useCallback3((interaction) => {
1036
1326
  for (const key of activeKeysRef.current) {
1037
1327
  const panels = DialStore.getPanels();
1038
1328
  for (const panel of panels) {
@@ -1049,7 +1339,7 @@ function ShortcutListener({ children }) {
1049
1339
  }
1050
1340
  return null;
1051
1341
  }, []);
1052
- useEffect3(() => {
1342
+ useEffect4(() => {
1053
1343
  const handleKeyDown = (e) => {
1054
1344
  if (isInputFocused()) return;
1055
1345
  const key = e.key.toLowerCase();
@@ -1211,7 +1501,7 @@ function ShortcutListener({ children }) {
1211
1501
  }
1212
1502
 
1213
1503
  // src/components/Slider.tsx
1214
- import { useRef as useRef4, useState as useState3, useCallback as useCallback3, useEffect as useEffect4 } from "react";
1504
+ import { useRef as useRef5, useState as useState3, useCallback as useCallback4, useEffect as useEffect5 } from "react";
1215
1505
  import { motion as motion2, useMotionValue, useTransform, animate } from "motion/react";
1216
1506
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1217
1507
  var CLICK_THRESHOLD = 3;
@@ -1229,11 +1519,11 @@ function Slider({
1229
1519
  shortcut,
1230
1520
  shortcutActive
1231
1521
  }) {
1232
- const wrapperRef = useRef4(null);
1233
- const trackRef = useRef4(null);
1234
- const inputRef = useRef4(null);
1235
- const labelRef = useRef4(null);
1236
- const valueSpanRef = useRef4(null);
1522
+ const wrapperRef = useRef5(null);
1523
+ const trackRef = useRef5(null);
1524
+ const inputRef = useRef5(null);
1525
+ const labelRef = useRef5(null);
1526
+ const valueSpanRef = useRef5(null);
1237
1527
  const [isInteracting, setIsInteracting] = useState3(false);
1238
1528
  const [isDragging, setIsDragging] = useState3(false);
1239
1529
  const [isHovered, setIsHovered] = useState3(false);
@@ -1241,12 +1531,12 @@ function Slider({
1241
1531
  const [isValueEditable, setIsValueEditable] = useState3(false);
1242
1532
  const [showInput, setShowInput] = useState3(false);
1243
1533
  const [inputValue, setInputValue] = useState3("");
1244
- const hoverTimeoutRef = useRef4(null);
1245
- const pointerDownPos = useRef4(null);
1246
- const isClickRef = useRef4(true);
1247
- const animRef = useRef4(null);
1248
- const wrapperRectRef = useRef4(null);
1249
- const scaleRef = useRef4(1);
1534
+ const hoverTimeoutRef = useRef5(null);
1535
+ const pointerDownPos = useRef5(null);
1536
+ const isClickRef = useRef5(true);
1537
+ const animRef = useRef5(null);
1538
+ const wrapperRectRef = useRef5(null);
1539
+ const scaleRef = useRef5(1);
1250
1540
  const percentage = (value - min) / (max - min) * 100;
1251
1541
  const isActive = isInteracting || isHovered;
1252
1542
  const fillPercent = useMotionValue(percentage);
@@ -1264,12 +1554,12 @@ function Slider({
1264
1554
  rubberStretchPx,
1265
1555
  (stretch) => stretch < 0 ? stretch : 0
1266
1556
  );
1267
- useEffect4(() => {
1557
+ useEffect5(() => {
1268
1558
  if (!isInteracting && !animRef.current) {
1269
1559
  fillPercent.jump(percentage);
1270
1560
  }
1271
1561
  }, [percentage, isInteracting, fillPercent]);
1272
- const positionToValue = useCallback3(
1562
+ const positionToValue = useCallback4(
1273
1563
  (clientX) => {
1274
1564
  const rect = wrapperRectRef.current;
1275
1565
  if (!rect) return value;
@@ -1282,11 +1572,11 @@ function Slider({
1282
1572
  },
1283
1573
  [min, max, value]
1284
1574
  );
1285
- const percentFromValue = useCallback3(
1575
+ const percentFromValue = useCallback4(
1286
1576
  (v) => (v - min) / (max - min) * 100,
1287
1577
  [min, max]
1288
1578
  );
1289
- const computeRubberStretch = useCallback3(
1579
+ const computeRubberStretch = useCallback4(
1290
1580
  (clientX, sign) => {
1291
1581
  const rect = wrapperRectRef.current;
1292
1582
  if (!rect) return 0;
@@ -1296,7 +1586,7 @@ function Slider({
1296
1586
  },
1297
1587
  []
1298
1588
  );
1299
- const handlePointerDown = useCallback3(
1589
+ const handlePointerDown = useCallback4(
1300
1590
  (e) => {
1301
1591
  if (showInput) return;
1302
1592
  e.preventDefault();
@@ -1312,7 +1602,7 @@ function Slider({
1312
1602
  },
1313
1603
  [showInput]
1314
1604
  );
1315
- const handlePointerMove = useCallback3(
1605
+ const handlePointerMove = useCallback4(
1316
1606
  (e) => {
1317
1607
  if (!isInteracting || !pointerDownPos.current) return;
1318
1608
  const dx = e.clientX - pointerDownPos.current.x;
@@ -1353,7 +1643,7 @@ function Slider({
1353
1643
  computeRubberStretch
1354
1644
  ]
1355
1645
  );
1356
- const handlePointerUp = useCallback3(
1646
+ const handlePointerUp = useCallback4(
1357
1647
  (e) => {
1358
1648
  if (!isInteracting) return;
1359
1649
  if (isClickRef.current) {
@@ -1397,7 +1687,7 @@ function Slider({
1397
1687
  rubberStretchPx
1398
1688
  ]
1399
1689
  );
1400
- useEffect4(() => {
1690
+ useEffect5(() => {
1401
1691
  if (isValueHovered && !showInput && !isValueEditable) {
1402
1692
  hoverTimeoutRef.current = setTimeout(() => {
1403
1693
  setIsValueEditable(true);
@@ -1415,7 +1705,7 @@ function Slider({
1415
1705
  }
1416
1706
  };
1417
1707
  }, [isValueHovered, showInput, isValueEditable]);
1418
- useEffect4(() => {
1708
+ useEffect5(() => {
1419
1709
  if (showInput && inputRef.current) {
1420
1710
  inputRef.current.focus();
1421
1711
  inputRef.current.select();
@@ -1570,17 +1860,17 @@ function Slider({
1570
1860
  }
1571
1861
 
1572
1862
  // src/components/SegmentedControl.tsx
1573
- import { useRef as useRef5, useState as useState4, useLayoutEffect, useCallback as useCallback4 } from "react";
1863
+ import { useRef as useRef6, useState as useState4, useLayoutEffect, useCallback as useCallback5 } from "react";
1574
1864
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1575
1865
  function SegmentedControl({
1576
1866
  options,
1577
1867
  value,
1578
1868
  onChange
1579
1869
  }) {
1580
- const containerRef = useRef5(null);
1581
- const hasAnimated = useRef5(false);
1870
+ const containerRef = useRef6(null);
1871
+ const hasAnimated = useRef6(false);
1582
1872
  const [pillStyle, setPillStyle] = useState4(null);
1583
- const measure = useCallback4(() => {
1873
+ const measure = useCallback5(() => {
1584
1874
  const container = containerRef.current;
1585
1875
  if (!container) return;
1586
1876
  const activeButton = container.querySelector('[data-active="true"]');
@@ -1645,47 +1935,118 @@ function Toggle({ label, checked, onChange, shortcut, shortcutActive }) {
1645
1935
  ] });
1646
1936
  }
1647
1937
 
1648
- // src/components/SpringVisualization.tsx
1649
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1650
- function generateSpringCurve(stiffness, damping, mass, duration) {
1651
- const points = [];
1652
- const steps = 100;
1653
- const dt = duration / steps;
1654
- let position = 0;
1655
- let velocity = 0;
1656
- const target = 1;
1657
- for (let i = 0; i <= steps; i++) {
1658
- const time = i * dt;
1659
- points.push([time, position]);
1660
- const springForce = -stiffness * (position - target);
1661
- const dampingForce = -damping * velocity;
1662
- const acceleration = (springForce + dampingForce) / mass;
1663
- velocity += acceleration * dt;
1664
- position += velocity * dt;
1938
+ // src/transition-math.ts
1939
+ function round2(value) {
1940
+ return Math.round(value * 100) / 100;
1941
+ }
1942
+ function clamp(value, min, max) {
1943
+ return Math.min(max, Math.max(min, value));
1944
+ }
1945
+ function isTransitionConfig(value) {
1946
+ return isSpringConfigValue(value) || isEasingConfigValue(value);
1947
+ }
1948
+ function isPhysicsSpring(transition) {
1949
+ return transition.type === "spring" && (transition.stiffness !== void 0 || transition.damping !== void 0 || transition.mass !== void 0);
1950
+ }
1951
+ function springParams(spring) {
1952
+ if (isPhysicsSpring(spring)) {
1953
+ return { stiffness: spring.stiffness ?? 200, damping: spring.damping ?? 25, mass: spring.mass ?? 1 };
1954
+ }
1955
+ const visualDuration = Math.max(0.05, spring.visualDuration ?? 0.3);
1956
+ const bounce = spring.bounce ?? 0.3;
1957
+ const root = 2 * Math.PI / (visualDuration * 1.2);
1958
+ const stiffness = root * root;
1959
+ const damping = 2 * Math.min(1, Math.max(0.05, 1 - bounce)) * Math.sqrt(stiffness);
1960
+ return { stiffness, damping, mass: 1 };
1961
+ }
1962
+ function springProgress(t, { stiffness, damping, mass }) {
1963
+ if (t <= 0) return 0;
1964
+ const w0 = Math.sqrt(stiffness / mass);
1965
+ const zeta = damping / (2 * Math.sqrt(stiffness * mass));
1966
+ if (zeta < 0.9999) {
1967
+ const wd2 = w0 * Math.sqrt(1 - zeta * zeta);
1968
+ return 1 - Math.exp(-zeta * w0 * t) * (Math.cos(wd2 * t) + zeta * w0 / wd2 * Math.sin(wd2 * t));
1969
+ }
1970
+ if (zeta < 1.0001) {
1971
+ return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
1972
+ }
1973
+ const wd = w0 * Math.sqrt(zeta * zeta - 1);
1974
+ const r1 = -zeta * w0 + wd;
1975
+ const r2 = -zeta * w0 - wd;
1976
+ return 1 + (r2 * Math.exp(r1 * t) - r1 * Math.exp(r2 * t)) / (r1 - r2);
1977
+ }
1978
+ function springSettleDuration(params) {
1979
+ const w0 = Math.sqrt(params.stiffness / params.mass);
1980
+ const zeta = params.damping / (2 * Math.sqrt(params.stiffness * params.mass));
1981
+ const decay = zeta >= 1 ? zeta * w0 - w0 * Math.sqrt(Math.max(0, zeta * zeta - 1)) : zeta * w0;
1982
+ const duration = Math.log(200) / Math.max(decay, 1e-6);
1983
+ return round2(clamp(duration, 0.05, 10));
1984
+ }
1985
+ function cubicBezierProgress(p, [x1, y1, x2, y2]) {
1986
+ if (p <= 0) return 0;
1987
+ if (p >= 1) return 1;
1988
+ const sampleX = (t2) => bezierAxis(t2, x1, x2);
1989
+ const sampleY = (t2) => bezierAxis(t2, y1, y2);
1990
+ let t = p;
1991
+ for (let i = 0; i < 8; i++) {
1992
+ const x = sampleX(t) - p;
1993
+ if (Math.abs(x) < 1e-5) return sampleY(t);
1994
+ const dx = bezierAxisDerivative(t, x1, x2);
1995
+ if (Math.abs(dx) < 1e-6) break;
1996
+ t -= x / dx;
1997
+ }
1998
+ let lo = 0;
1999
+ let hi = 1;
2000
+ t = p;
2001
+ while (hi - lo > 1e-5) {
2002
+ if (sampleX(t) < p) lo = t;
2003
+ else hi = t;
2004
+ t = (lo + hi) / 2;
2005
+ }
2006
+ return sampleY(t);
2007
+ }
2008
+ function bezierAxis(t, a1, a2) {
2009
+ return (1 - 3 * a2 + 3 * a1) * t * t * t + (3 * a2 - 6 * a1) * t * t + 3 * a1 * t;
2010
+ }
2011
+ function bezierAxisDerivative(t, a1, a2) {
2012
+ return 3 * (1 - 3 * a2 + 3 * a1) * t * t + 2 * (3 * a2 - 6 * a1) * t + 3 * a1;
2013
+ }
2014
+ function resolveClipTransition(raw, clipDuration) {
2015
+ const safeDuration = Math.max(0.05, clipDuration);
2016
+ if (raw.type === "easing") {
2017
+ return {
2018
+ transition: { ...raw, duration: safeDuration },
2019
+ duration: safeDuration,
2020
+ isPhysics: false
2021
+ };
2022
+ }
2023
+ if (isPhysicsSpring(raw)) {
2024
+ return {
2025
+ transition: raw,
2026
+ duration: springSettleDuration(springParams(raw)),
2027
+ isPhysics: true
2028
+ };
1665
2029
  }
1666
- return points;
2030
+ return {
2031
+ transition: { type: "spring", bounce: raw.bounce ?? 0.2, visualDuration: safeDuration },
2032
+ duration: safeDuration,
2033
+ isPhysics: false
2034
+ };
1667
2035
  }
2036
+
2037
+ // src/components/SpringVisualization.tsx
2038
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1668
2039
  function SpringVisualization({ spring, isSimpleMode }) {
1669
2040
  const width = 256;
1670
2041
  const height = 140;
1671
- let stiffness;
1672
- let damping;
1673
- let mass;
1674
- if (isSimpleMode) {
1675
- const visualDuration = spring.visualDuration ?? 0.3;
1676
- const bounce = spring.bounce ?? 0.2;
1677
- mass = 1;
1678
- stiffness = 2 * Math.PI / visualDuration;
1679
- stiffness = Math.pow(stiffness, 2);
1680
- const dampingRatio = 1 - bounce;
1681
- damping = 2 * dampingRatio * Math.sqrt(stiffness * mass);
1682
- } else {
1683
- stiffness = spring.stiffness ?? 400;
1684
- damping = spring.damping ?? 17;
1685
- mass = spring.mass ?? 1;
1686
- }
2042
+ const params = isSimpleMode ? springParams({ type: "spring", visualDuration: spring.visualDuration ?? 0.3, bounce: spring.bounce ?? 0.2 }) : springParams({ type: "spring", stiffness: spring.stiffness ?? 400, damping: spring.damping ?? 17, mass: spring.mass ?? 1 });
1687
2043
  const duration = 2;
1688
- const points = generateSpringCurve(stiffness, damping, mass, duration);
2044
+ const steps = 100;
2045
+ const points = [];
2046
+ for (let i = 0; i <= steps; i++) {
2047
+ const time = i / steps * duration;
2048
+ points.push([time, springProgress(time, params)]);
2049
+ }
1689
2050
  const values = points.map(([, value]) => value);
1690
2051
  const minValue = Math.min(...values);
1691
2052
  const maxValue = Math.max(...values);
@@ -1734,20 +2095,20 @@ function SpringVisualization({ spring, isSimpleMode }) {
1734
2095
  }
1735
2096
 
1736
2097
  // src/components/SpringControl.tsx
1737
- import { useCallback as useCallback5, useRef as useRef6, useSyncExternalStore as useSyncExternalStore2 } from "react";
2098
+ import { useCallback as useCallback6, useRef as useRef7, useSyncExternalStore as useSyncExternalStore2 } from "react";
1738
2099
  import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1739
2100
  function SpringControl({ panelId, path, label, spring, onChange }) {
1740
- const subscribe = useCallback5(
2101
+ const subscribe = useCallback6(
1741
2102
  (callback) => DialStore.subscribe(panelId, callback),
1742
2103
  [panelId]
1743
2104
  );
1744
- const getSnapshot = useCallback5(
2105
+ const getSnapshot = useCallback6(
1745
2106
  () => DialStore.getSpringMode(panelId, path),
1746
2107
  [panelId, path]
1747
2108
  );
1748
2109
  const mode = useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
1749
2110
  const isSimpleMode = mode === "simple";
1750
- const cache = useRef6({
2111
+ const cache = useRef7({
1751
2112
  simple: spring.visualDuration !== void 0 ? spring : { type: "spring", visualDuration: 0.3, bounce: 0.2 },
1752
2113
  advanced: spring.stiffness !== void 0 ? spring : { type: "spring", stiffness: 200, damping: 25, mass: 1 }
1753
2114
  });
@@ -1894,21 +2255,29 @@ function EasingVisualization({ easing }) {
1894
2255
  }
1895
2256
 
1896
2257
  // src/components/TransitionControl.tsx
1897
- import { useCallback as useCallback6, useRef as useRef7, useState as useState5, useSyncExternalStore as useSyncExternalStore3 } from "react";
2258
+ import { useCallback as useCallback7, useRef as useRef8, useState as useState5, useSyncExternalStore as useSyncExternalStore3 } from "react";
1898
2259
  import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1899
- function TransitionControl({ panelId, path, label, value, onChange }) {
1900
- const subscribe = useCallback6(
2260
+ function TransitionControl({
2261
+ panelId,
2262
+ path,
2263
+ label,
2264
+ value,
2265
+ onChange,
2266
+ hideDuration = false,
2267
+ durationControl
2268
+ }) {
2269
+ const subscribe = useCallback7(
1901
2270
  (callback) => DialStore.subscribe(panelId, callback),
1902
2271
  [panelId]
1903
2272
  );
1904
- const getSnapshot = useCallback6(
2273
+ const getSnapshot = useCallback7(
1905
2274
  () => DialStore.getTransitionMode(panelId, path),
1906
2275
  [panelId, path]
1907
2276
  );
1908
2277
  const mode = useSyncExternalStore3(subscribe, getSnapshot, getSnapshot);
1909
2278
  const isEasing = mode === "easing";
1910
2279
  const isSimpleSpring = mode === "simple";
1911
- const cache = useRef7({
2280
+ const cache = useRef8({
1912
2281
  easing: value.type === "easing" ? value : { type: "easing", duration: 0.3, ease: [1, -0.4, 0.5, 1] },
1913
2282
  simple: value.type === "spring" && value.visualDuration !== void 0 ? value : { type: "spring", visualDuration: 0.3, bounce: 0.2 },
1914
2283
  advanced: value.type === "spring" && value.stiffness !== void 0 ? value : { type: "spring", stiffness: 200, damping: 25, mass: 1 }
@@ -1946,6 +2315,21 @@ function TransitionControl({ panelId, path, label, value, onChange }) {
1946
2315
  newEase[index] = val;
1947
2316
  onChange({ ...easing, ease: newEase });
1948
2317
  };
2318
+ const durationSlider = !hideDuration && (isEasing || isSimpleSpring) ? /* @__PURE__ */ jsx9(
2319
+ Slider,
2320
+ {
2321
+ label: "Duration",
2322
+ value: durationControl?.value ?? (isEasing ? easing.duration : spring.visualDuration ?? 0.3),
2323
+ onChange: durationControl?.onChange ?? ((next) => {
2324
+ if (isEasing) onChange({ ...easing, duration: next });
2325
+ else handleSpringUpdate("visualDuration", next);
2326
+ }),
2327
+ min: durationControl?.min ?? 0.1,
2328
+ max: durationControl?.max ?? (isEasing ? 2 : 1),
2329
+ step: durationControl?.step ?? 0.05,
2330
+ unit: "s"
2331
+ }
2332
+ ) : null;
1949
2333
  return /* @__PURE__ */ jsx9(Folder, { title: label, defaultOpen: true, children: /* @__PURE__ */ jsxs8("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
1950
2334
  isEasing ? /* @__PURE__ */ jsx9(EasingVisualization, { easing }) : /* @__PURE__ */ jsx9(SpringVisualization, { spring, isSimpleMode: isSimpleSpring }),
1951
2335
  /* @__PURE__ */ jsxs8("div", { className: "dialkit-labeled-control", children: [
@@ -1968,16 +2352,13 @@ function TransitionControl({ panelId, path, label, value, onChange }) {
1968
2352
  /* @__PURE__ */ jsx9(Slider, { label: "y1", value: easing.ease[1], onChange: (v) => updateEase(1, v), min: -1, max: 2, step: 0.01 }),
1969
2353
  /* @__PURE__ */ jsx9(Slider, { label: "x2", value: easing.ease[2], onChange: (v) => updateEase(2, v), min: 0, max: 1, step: 0.01 }),
1970
2354
  /* @__PURE__ */ jsx9(Slider, { label: "y2", value: easing.ease[3], onChange: (v) => updateEase(3, v), min: -1, max: 2, step: 0.01 }),
1971
- /* @__PURE__ */ jsx9(Slider, { label: "Duration", value: easing.duration, onChange: (v) => onChange({ ...easing, duration: v }), min: 0.1, max: 2, step: 0.05, unit: "s" }),
1972
2355
  /* @__PURE__ */ jsx9(EaseTextInput, { ease: easing.ease, onChange: (newEase) => onChange({ ...easing, ease: newEase }) })
1973
- ] }) : isSimpleSpring ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
1974
- /* @__PURE__ */ jsx9(Slider, { label: "Duration", value: spring.visualDuration ?? 0.3, onChange: (v) => handleSpringUpdate("visualDuration", v), min: 0.1, max: 1, step: 0.05, unit: "s" }),
1975
- /* @__PURE__ */ jsx9(Slider, { label: "Bounce", value: spring.bounce ?? 0.2, onChange: (v) => handleSpringUpdate("bounce", v), min: 0, max: 1, step: 0.05 })
1976
- ] }) : /* @__PURE__ */ jsxs8(Fragment2, { children: [
2356
+ ] }) : isSimpleSpring ? /* @__PURE__ */ jsx9(Slider, { label: "Bounce", value: spring.bounce ?? 0.2, onChange: (v) => handleSpringUpdate("bounce", v), min: 0, max: 1, step: 0.05 }) : /* @__PURE__ */ jsxs8(Fragment2, { children: [
1977
2357
  /* @__PURE__ */ jsx9(Slider, { label: "Stiffness", value: spring.stiffness ?? 400, onChange: (v) => handleSpringUpdate("stiffness", v), min: 1, max: 1e3, step: 10 }),
1978
2358
  /* @__PURE__ */ jsx9(Slider, { label: "Damping", value: spring.damping ?? 17, onChange: (v) => handleSpringUpdate("damping", v), min: 1, max: 100, step: 1 }),
1979
2359
  /* @__PURE__ */ jsx9(Slider, { label: "Mass", value: spring.mass ?? 1, onChange: (v) => handleSpringUpdate("mass", v), min: 0.1, max: 10, step: 0.1 })
1980
- ] })
2360
+ ] }),
2361
+ durationSlider
1981
2362
  ] }) });
1982
2363
  }
1983
2364
  function formatEase(ease) {
@@ -2044,7 +2425,7 @@ function TextControl({ label, value, onChange, placeholder }) {
2044
2425
  }
2045
2426
 
2046
2427
  // src/components/SelectControl.tsx
2047
- import { useState as useState6, useRef as useRef8, useEffect as useEffect5, useCallback as useCallback7 } from "react";
2428
+ import { useState as useState6, useRef as useRef9, useEffect as useEffect6, useCallback as useCallback8 } from "react";
2048
2429
  import { createPortal } from "react-dom";
2049
2430
  import { motion as motion3, AnimatePresence as AnimatePresence2 } from "motion/react";
2050
2431
 
@@ -2078,26 +2459,26 @@ function normalizeOptions(options) {
2078
2459
  }
2079
2460
  function SelectControl({ label, value, options, onChange }) {
2080
2461
  const [isOpen, setIsOpen] = useState6(false);
2081
- const triggerRef = useRef8(null);
2082
- const dropdownRef = useRef8(null);
2462
+ const triggerRef = useRef9(null);
2463
+ const dropdownRef = useRef9(null);
2083
2464
  const [portalTarget, setPortalTarget] = useState6(null);
2084
2465
  const [pos, setPos] = useState6(null);
2085
2466
  const normalized = normalizeOptions(options);
2086
2467
  const selectedOption = normalized.find((o) => o.value === value);
2087
- const updatePos = useCallback7(() => {
2468
+ const updatePos = useCallback8(() => {
2088
2469
  const el = triggerRef.current;
2089
2470
  if (!el || !portalTarget) return;
2090
2471
  const dropdownHeight = 8 + normalized.length * 36;
2091
2472
  setPos(getDropdownPosition(el, portalTarget, { dropdownHeight }));
2092
2473
  }, [normalized.length, portalTarget]);
2093
- useEffect5(() => {
2474
+ useEffect6(() => {
2094
2475
  setPortalTarget(getDialKitPortalRoot(triggerRef.current) ?? document.body);
2095
2476
  }, []);
2096
- useEffect5(() => {
2477
+ useEffect6(() => {
2097
2478
  if (!isOpen) return;
2098
2479
  updatePos();
2099
2480
  }, [isOpen, updatePos]);
2100
- useEffect5(() => {
2481
+ useEffect6(() => {
2101
2482
  if (!isOpen) return;
2102
2483
  const handleClick = (e) => {
2103
2484
  const target = e.target;
@@ -2177,14 +2558,14 @@ function SelectControl({ label, value, options, onChange }) {
2177
2558
  }
2178
2559
 
2179
2560
  // src/components/ColorControl.tsx
2180
- import { useState as useState7, useRef as useRef9, useEffect as useEffect6 } from "react";
2561
+ import { useState as useState7, useRef as useRef10, useEffect as useEffect7 } from "react";
2181
2562
  import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2182
2563
  var HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
2183
2564
  function ColorControl({ label, value, onChange }) {
2184
2565
  const [isEditing, setIsEditing] = useState7(false);
2185
2566
  const [editValue, setEditValue] = useState7(value);
2186
- const colorInputRef = useRef9(null);
2187
- useEffect6(() => {
2567
+ const colorInputRef = useRef10(null);
2568
+ useEffect7(() => {
2188
2569
  if (!isEditing) {
2189
2570
  setEditValue(value);
2190
2571
  }
@@ -2254,266 +2635,105 @@ function expandShorthandHex(hex) {
2254
2635
  return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
2255
2636
  }
2256
2637
 
2257
- // src/components/PresetManager.tsx
2258
- import { useState as useState8, useRef as useRef10, useEffect as useEffect7, useCallback as useCallback8 } from "react";
2259
- import { createPortal as createPortal2 } from "react-dom";
2260
- import { motion as motion4, AnimatePresence as AnimatePresence3 } from "motion/react";
2261
- import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
2262
- function PresetManager({ panelId, presets, activePresetId, onAdd }) {
2263
- const [isOpen, setIsOpen] = useState8(false);
2264
- const triggerRef = useRef10(null);
2265
- const dropdownRef = useRef10(null);
2266
- const [pos, setPos] = useState8({ top: 0, left: 0, width: 0 });
2267
- const hasPresets = presets.length > 0;
2268
- const activePreset = presets.find((p) => p.id === activePresetId);
2269
- const open = useCallback8(() => {
2270
- if (!hasPresets) return;
2271
- const rect = triggerRef.current?.getBoundingClientRect();
2272
- if (rect) {
2273
- setPos({ top: rect.bottom + 4, left: rect.left, width: rect.width });
2274
- }
2275
- setIsOpen(true);
2276
- }, [hasPresets]);
2277
- const close = useCallback8(() => setIsOpen(false), []);
2278
- const toggle = useCallback8(() => {
2279
- if (isOpen) close();
2280
- else open();
2281
- }, [isOpen, open, close]);
2282
- useEffect7(() => {
2283
- if (!isOpen) return;
2284
- const handler = (e) => {
2285
- const target = e.target;
2286
- if (triggerRef.current?.contains(target) || dropdownRef.current?.contains(target)) return;
2287
- close();
2288
- };
2289
- document.addEventListener("mousedown", handler);
2290
- return () => document.removeEventListener("mousedown", handler);
2291
- }, [isOpen, close]);
2292
- const handleSelect = (presetId) => {
2293
- if (presetId) {
2294
- DialStore.loadPreset(panelId, presetId);
2295
- } else {
2296
- DialStore.clearActivePreset(panelId);
2297
- }
2298
- close();
2299
- };
2300
- const handleDelete = (e, presetId) => {
2301
- e.stopPropagation();
2302
- DialStore.deletePreset(panelId, presetId);
2303
- };
2304
- return /* @__PURE__ */ jsxs12("div", { className: "dialkit-preset-manager", children: [
2305
- /* @__PURE__ */ jsxs12(
2306
- "button",
2307
- {
2308
- ref: triggerRef,
2309
- className: "dialkit-preset-trigger",
2310
- onClick: toggle,
2311
- "data-open": String(isOpen),
2312
- "data-has-preset": String(!!activePreset),
2313
- "data-disabled": String(!hasPresets),
2314
- children: [
2315
- /* @__PURE__ */ jsx13("span", { className: "dialkit-preset-label", children: activePreset ? activePreset.name : "Version 1" }),
2316
- /* @__PURE__ */ jsx13(
2317
- motion4.svg,
2318
- {
2319
- className: "dialkit-select-chevron",
2320
- viewBox: "0 0 24 24",
2321
- fill: "none",
2322
- stroke: "currentColor",
2323
- strokeWidth: "2.5",
2324
- strokeLinecap: "round",
2325
- strokeLinejoin: "round",
2326
- animate: { rotate: isOpen ? 180 : 0, opacity: hasPresets ? 0.6 : 0.25 },
2327
- transition: { type: "spring", visualDuration: 0.2, bounce: 0.15 },
2328
- children: /* @__PURE__ */ jsx13("path", { d: ICON_CHEVRON })
2329
- }
2330
- )
2331
- ]
2332
- }
2333
- ),
2334
- createPortal2(
2335
- /* @__PURE__ */ jsx13(AnimatePresence3, { children: isOpen && /* @__PURE__ */ jsxs12(
2336
- motion4.div,
2337
- {
2338
- ref: dropdownRef,
2339
- className: "dialkit-root dialkit-preset-dropdown",
2340
- style: { position: "fixed", top: pos.top, left: pos.left, minWidth: pos.width },
2341
- initial: { opacity: 0, y: 4, scale: 0.97 },
2342
- animate: { opacity: 1, y: 0, scale: 1 },
2343
- exit: { opacity: 0, y: 4, scale: 0.97, pointerEvents: "none" },
2344
- transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
2345
- children: [
2346
- /* @__PURE__ */ jsx13(
2347
- "div",
2348
- {
2349
- className: "dialkit-preset-item",
2350
- "data-active": String(!activePresetId),
2351
- onClick: () => handleSelect(null),
2352
- children: /* @__PURE__ */ jsx13("span", { className: "dialkit-preset-name", children: "Version 1" })
2353
- }
2354
- ),
2355
- presets.map((preset) => /* @__PURE__ */ jsxs12(
2356
- "div",
2357
- {
2358
- className: "dialkit-preset-item",
2359
- "data-active": String(preset.id === activePresetId),
2360
- onClick: () => handleSelect(preset.id),
2361
- children: [
2362
- /* @__PURE__ */ jsx13("span", { className: "dialkit-preset-name", children: preset.name }),
2363
- /* @__PURE__ */ jsx13(
2364
- "button",
2365
- {
2366
- className: "dialkit-preset-delete",
2367
- onClick: (e) => handleDelete(e, preset.id),
2368
- title: "Delete preset",
2369
- children: /* @__PURE__ */ jsx13("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: ICON_TRASH.map((d, i) => /* @__PURE__ */ jsx13("path", { d }, i)) })
2370
- }
2371
- )
2372
- ]
2373
- },
2374
- preset.id
2375
- ))
2376
- ]
2377
- }
2378
- ) }),
2379
- document.body
2380
- )
2381
- ] });
2382
- }
2383
-
2384
- // src/components/Panel.tsx
2385
- import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
2386
- function Panel({ panel, defaultOpen = true, inline = false, onOpenChange, variant = "root" }) {
2387
- const [copied, setCopied] = useState9(false);
2388
- const [isPanelOpen, setIsPanelOpen] = useState9(defaultOpen);
2638
+ // src/components/ControlRenderer.tsx
2639
+ import { Fragment as Fragment3, jsx as jsx13 } from "react/jsx-runtime";
2640
+ function ControlRenderer({ panelId, controls, values, transitionDuration }) {
2389
2641
  const shortcutCtx = useContext(ShortcutContext);
2390
- const hasShortcuts = Object.keys(panel.shortcuts).length > 0;
2391
- const subscribe = useCallback9(
2392
- (callback) => DialStore.subscribe(panel.id, callback),
2393
- [panel.id]
2394
- );
2395
- const getSnapshot = useCallback9(
2396
- () => DialStore.getValues(panel.id),
2397
- [panel.id]
2398
- );
2399
- const values = useSyncExternalStore4(subscribe, getSnapshot, getSnapshot);
2400
- const presets = DialStore.getPresets(panel.id);
2401
- const activePresetId = DialStore.getActivePresetId(panel.id);
2402
- const handleAddPreset = () => {
2403
- const nextNum = presets.length + 2;
2404
- DialStore.savePreset(panel.id, `Version ${nextNum}`);
2405
- };
2406
- const handleCopy = () => {
2407
- const jsonStr = JSON.stringify(values, null, 2);
2408
- const instruction = `Update the useDialKit configuration for "${panel.name}" with these values:
2409
-
2410
- \`\`\`json
2411
- ${jsonStr}
2412
- \`\`\`
2413
-
2414
- Apply these values as the new defaults in the useDialKit call.`;
2415
- navigator.clipboard.writeText(instruction);
2416
- setCopied(true);
2417
- setTimeout(() => setCopied(false), 1500);
2418
- };
2419
- const handleOpenChange = useCallback9((open) => {
2420
- setIsPanelOpen(open);
2421
- onOpenChange?.(open);
2422
- }, [onOpenChange]);
2423
2642
  const renderControl = (control) => {
2424
2643
  const value = values[control.path];
2425
2644
  switch (control.type) {
2426
2645
  case "slider":
2427
- return /* @__PURE__ */ jsx14(
2646
+ return /* @__PURE__ */ jsx13(
2428
2647
  Slider,
2429
2648
  {
2430
2649
  label: control.label,
2431
2650
  value,
2432
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v),
2651
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v),
2433
2652
  min: control.min,
2434
2653
  max: control.max,
2435
2654
  step: control.step,
2436
2655
  shortcut: control.shortcut,
2437
- shortcutActive: shortcutCtx.activePanelId === panel.id && shortcutCtx.activePath === control.path
2656
+ shortcutActive: shortcutCtx.activePanelId === panelId && shortcutCtx.activePath === control.path
2438
2657
  },
2439
2658
  control.path
2440
2659
  );
2441
2660
  case "toggle":
2442
- return /* @__PURE__ */ jsx14(
2661
+ return /* @__PURE__ */ jsx13(
2443
2662
  Toggle,
2444
2663
  {
2445
2664
  label: control.label,
2446
2665
  checked: value,
2447
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v),
2666
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v),
2448
2667
  shortcut: control.shortcut,
2449
- shortcutActive: shortcutCtx.activePanelId === panel.id && shortcutCtx.activePath === control.path
2668
+ shortcutActive: shortcutCtx.activePanelId === panelId && shortcutCtx.activePath === control.path
2450
2669
  },
2451
2670
  control.path
2452
2671
  );
2453
2672
  case "spring":
2454
- return /* @__PURE__ */ jsx14(
2673
+ return /* @__PURE__ */ jsx13(
2455
2674
  SpringControl,
2456
2675
  {
2457
- panelId: panel.id,
2676
+ panelId,
2458
2677
  path: control.path,
2459
2678
  label: control.label,
2460
2679
  spring: value,
2461
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
2680
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v)
2462
2681
  },
2463
2682
  control.path
2464
2683
  );
2465
2684
  case "transition":
2466
- return /* @__PURE__ */ jsx14(
2685
+ return /* @__PURE__ */ jsx13(
2467
2686
  TransitionControl,
2468
2687
  {
2469
- panelId: panel.id,
2688
+ panelId,
2470
2689
  path: control.path,
2471
2690
  label: control.label,
2472
2691
  value,
2473
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
2692
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v),
2693
+ durationControl: transitionDuration
2474
2694
  },
2475
2695
  control.path
2476
2696
  );
2477
2697
  case "folder":
2478
- return /* @__PURE__ */ jsx14(Folder, { title: control.label, defaultOpen: control.defaultOpen ?? true, children: control.children?.map(renderControl) }, control.path);
2698
+ return /* @__PURE__ */ jsx13(Folder, { title: control.label, defaultOpen: control.defaultOpen ?? true, children: control.children?.map(renderControl) }, control.path);
2479
2699
  case "text":
2480
- return /* @__PURE__ */ jsx14(
2700
+ return /* @__PURE__ */ jsx13(
2481
2701
  TextControl,
2482
2702
  {
2483
2703
  label: control.label,
2484
2704
  value,
2485
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v),
2705
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v),
2486
2706
  placeholder: control.placeholder
2487
2707
  },
2488
2708
  control.path
2489
2709
  );
2490
2710
  case "select":
2491
- return /* @__PURE__ */ jsx14(
2711
+ return /* @__PURE__ */ jsx13(
2492
2712
  SelectControl,
2493
2713
  {
2494
2714
  label: control.label,
2495
2715
  value,
2496
2716
  options: control.options ?? [],
2497
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
2717
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v)
2498
2718
  },
2499
2719
  control.path
2500
2720
  );
2501
2721
  case "color":
2502
- return /* @__PURE__ */ jsx14(
2722
+ return /* @__PURE__ */ jsx13(
2503
2723
  ColorControl,
2504
2724
  {
2505
2725
  label: control.label,
2506
2726
  value,
2507
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
2727
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v)
2508
2728
  },
2509
2729
  control.path
2510
2730
  );
2511
2731
  case "action":
2512
- return /* @__PURE__ */ jsx14(
2732
+ return /* @__PURE__ */ jsx13(
2513
2733
  "button",
2514
2734
  {
2515
2735
  className: "dialkit-button",
2516
- onClick: () => DialStore.triggerAction(panel.id, control.path),
2736
+ onClick: () => DialStore.triggerAction(panelId, control.path),
2517
2737
  children: control.label
2518
2738
  },
2519
2739
  control.path
@@ -2522,12 +2742,170 @@ Apply these values as the new defaults in the useDialKit call.`;
2522
2742
  return null;
2523
2743
  }
2524
2744
  };
2525
- const renderControls = () => {
2526
- return panel.controls.map(renderControl);
2745
+ return /* @__PURE__ */ jsx13(Fragment3, { children: controls.map(renderControl) });
2746
+ }
2747
+
2748
+ // src/components/PresetManager.tsx
2749
+ import { useState as useState8, useRef as useRef11, useEffect as useEffect8, useCallback as useCallback9 } from "react";
2750
+ import { createPortal as createPortal2 } from "react-dom";
2751
+ import { motion as motion4, AnimatePresence as AnimatePresence3 } from "motion/react";
2752
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2753
+ function PresetManager({ panelId, presets, activePresetId, onAdd }) {
2754
+ const [isOpen, setIsOpen] = useState8(false);
2755
+ const triggerRef = useRef11(null);
2756
+ const dropdownRef = useRef11(null);
2757
+ const [pos, setPos] = useState8({ top: 0, left: 0, width: 0 });
2758
+ const hasPresets = presets.length > 0;
2759
+ const activePreset = presets.find((p) => p.id === activePresetId);
2760
+ const open = useCallback9(() => {
2761
+ if (!hasPresets) return;
2762
+ const rect = triggerRef.current?.getBoundingClientRect();
2763
+ if (rect) {
2764
+ setPos({ top: rect.bottom + 4, left: rect.left, width: rect.width });
2765
+ }
2766
+ setIsOpen(true);
2767
+ }, [hasPresets]);
2768
+ const close = useCallback9(() => setIsOpen(false), []);
2769
+ const toggle = useCallback9(() => {
2770
+ if (isOpen) close();
2771
+ else open();
2772
+ }, [isOpen, open, close]);
2773
+ useEffect8(() => {
2774
+ if (!isOpen) return;
2775
+ const handler = (e) => {
2776
+ const target = e.target;
2777
+ if (triggerRef.current?.contains(target) || dropdownRef.current?.contains(target)) return;
2778
+ close();
2779
+ };
2780
+ document.addEventListener("mousedown", handler);
2781
+ return () => document.removeEventListener("mousedown", handler);
2782
+ }, [isOpen, close]);
2783
+ const handleSelect = (presetId) => {
2784
+ if (presetId) {
2785
+ DialStore.loadPreset(panelId, presetId);
2786
+ } else {
2787
+ DialStore.clearActivePreset(panelId);
2788
+ }
2789
+ close();
2790
+ };
2791
+ const handleDelete = (e, presetId) => {
2792
+ e.stopPropagation();
2793
+ DialStore.deletePreset(panelId, presetId);
2794
+ };
2795
+ return /* @__PURE__ */ jsxs12("div", { className: "dialkit-preset-manager", children: [
2796
+ /* @__PURE__ */ jsxs12(
2797
+ "button",
2798
+ {
2799
+ ref: triggerRef,
2800
+ className: "dialkit-preset-trigger",
2801
+ onClick: toggle,
2802
+ "data-open": String(isOpen),
2803
+ "data-has-preset": String(!!activePreset),
2804
+ "data-disabled": String(!hasPresets),
2805
+ children: [
2806
+ /* @__PURE__ */ jsx14("span", { className: "dialkit-preset-label", children: activePreset ? activePreset.name : "Version 1" }),
2807
+ /* @__PURE__ */ jsx14(
2808
+ motion4.svg,
2809
+ {
2810
+ className: "dialkit-select-chevron",
2811
+ viewBox: "0 0 24 24",
2812
+ fill: "none",
2813
+ stroke: "currentColor",
2814
+ strokeWidth: "2.5",
2815
+ strokeLinecap: "round",
2816
+ strokeLinejoin: "round",
2817
+ animate: { rotate: isOpen ? 180 : 0, opacity: hasPresets ? 0.6 : 0.25 },
2818
+ transition: { type: "spring", visualDuration: 0.2, bounce: 0.15 },
2819
+ children: /* @__PURE__ */ jsx14("path", { d: ICON_CHEVRON })
2820
+ }
2821
+ )
2822
+ ]
2823
+ }
2824
+ ),
2825
+ createPortal2(
2826
+ /* @__PURE__ */ jsx14(AnimatePresence3, { children: isOpen && /* @__PURE__ */ jsxs12(
2827
+ motion4.div,
2828
+ {
2829
+ ref: dropdownRef,
2830
+ className: "dialkit-root dialkit-preset-dropdown",
2831
+ style: { position: "fixed", top: pos.top, left: pos.left, minWidth: pos.width },
2832
+ initial: { opacity: 0, y: 4, scale: 0.97 },
2833
+ animate: { opacity: 1, y: 0, scale: 1 },
2834
+ exit: { opacity: 0, y: 4, scale: 0.97, pointerEvents: "none" },
2835
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
2836
+ children: [
2837
+ /* @__PURE__ */ jsx14(
2838
+ "div",
2839
+ {
2840
+ className: "dialkit-preset-item",
2841
+ "data-active": String(!activePresetId),
2842
+ onClick: () => handleSelect(null),
2843
+ children: /* @__PURE__ */ jsx14("span", { className: "dialkit-preset-name", children: "Version 1" })
2844
+ }
2845
+ ),
2846
+ presets.map((preset) => /* @__PURE__ */ jsxs12(
2847
+ "div",
2848
+ {
2849
+ className: "dialkit-preset-item",
2850
+ "data-active": String(preset.id === activePresetId),
2851
+ onClick: () => handleSelect(preset.id),
2852
+ children: [
2853
+ /* @__PURE__ */ jsx14("span", { className: "dialkit-preset-name", children: preset.name }),
2854
+ /* @__PURE__ */ jsx14(
2855
+ "button",
2856
+ {
2857
+ className: "dialkit-preset-delete",
2858
+ onClick: (e) => handleDelete(e, preset.id),
2859
+ title: "Delete preset",
2860
+ children: /* @__PURE__ */ jsx14("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: ICON_TRASH.map((d, i) => /* @__PURE__ */ jsx14("path", { d }, i)) })
2861
+ }
2862
+ )
2863
+ ]
2864
+ },
2865
+ preset.id
2866
+ ))
2867
+ ]
2868
+ }
2869
+ ) }),
2870
+ document.body
2871
+ )
2872
+ ] });
2873
+ }
2874
+
2875
+ // src/components/Panel.tsx
2876
+ import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
2877
+ function Panel({ panel, defaultOpen = true, inline = false, onOpenChange, variant = "root", toolbarExtra }) {
2878
+ const [copied, setCopied] = useState9(false);
2879
+ const [isPanelOpen, setIsPanelOpen] = useState9(defaultOpen);
2880
+ const hasShortcuts = Object.keys(panel.shortcuts).length > 0;
2881
+ const subscribe = useCallback10(
2882
+ (callback) => DialStore.subscribe(panel.id, callback),
2883
+ [panel.id]
2884
+ );
2885
+ const getSnapshot = useCallback10(
2886
+ () => DialStore.getValues(panel.id),
2887
+ [panel.id]
2888
+ );
2889
+ const values = useSyncExternalStore4(subscribe, getSnapshot, getSnapshot);
2890
+ const presets = DialStore.getPresets(panel.id);
2891
+ const activePresetId = DialStore.getActivePresetId(panel.id);
2892
+ const handleAddPreset = () => {
2893
+ const nextNum = presets.length + 2;
2894
+ DialStore.savePreset(panel.id, `Version ${nextNum}`);
2895
+ };
2896
+ const handleCopy = () => {
2897
+ navigator.clipboard.writeText(buildCopyInstruction("useDialKit", panel.name, values));
2898
+ setCopied(true);
2899
+ setTimeout(() => setCopied(false), 1500);
2527
2900
  };
2901
+ const handleOpenChange = useCallback10((open) => {
2902
+ setIsPanelOpen(open);
2903
+ onOpenChange?.(open);
2904
+ }, [onOpenChange]);
2905
+ const renderControls = () => /* @__PURE__ */ jsx15(ControlRenderer, { panelId: panel.id, controls: panel.controls, values });
2528
2906
  const iconTransition = { type: "spring", visualDuration: 0.4, bounce: 0.1 };
2529
- const toolbar = /* @__PURE__ */ jsxs13(Fragment3, { children: [
2530
- /* @__PURE__ */ jsx14(
2907
+ const toolbar = /* @__PURE__ */ jsxs13(Fragment4, { children: [
2908
+ /* @__PURE__ */ jsx15(
2531
2909
  motion5.button,
2532
2910
  {
2533
2911
  className: "dialkit-toolbar-add",
@@ -2535,10 +2913,10 @@ Apply these values as the new defaults in the useDialKit call.`;
2535
2913
  title: "Add preset",
2536
2914
  whileTap: { scale: 0.9 },
2537
2915
  transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
2538
- children: /* @__PURE__ */ jsx14("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: ICON_ADD_PRESET.map((d, i) => /* @__PURE__ */ jsx14("path", { d }, i)) })
2916
+ children: /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: ICON_ADD_PRESET.map((d, i) => /* @__PURE__ */ jsx15("path", { d }, i)) })
2539
2917
  }
2540
2918
  ),
2541
- /* @__PURE__ */ jsx14(
2919
+ /* @__PURE__ */ jsx15(
2542
2920
  PresetManager,
2543
2921
  {
2544
2922
  panelId: panel.id,
@@ -2547,7 +2925,7 @@ Apply these values as the new defaults in the useDialKit call.`;
2547
2925
  onAdd: handleAddPreset
2548
2926
  }
2549
2927
  ),
2550
- /* @__PURE__ */ jsx14(
2928
+ /* @__PURE__ */ jsx15(
2551
2929
  motion5.button,
2552
2930
  {
2553
2931
  className: "dialkit-toolbar-add",
@@ -2555,7 +2933,7 @@ Apply these values as the new defaults in the useDialKit call.`;
2555
2933
  title: "Copy parameters",
2556
2934
  whileTap: { scale: 0.9 },
2557
2935
  transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
2558
- children: /* @__PURE__ */ jsx14("span", { style: { position: "relative", width: 16, height: 16 }, children: /* @__PURE__ */ jsx14(AnimatePresence4, { initial: false, mode: "wait", children: copied ? /* @__PURE__ */ jsx14(
2936
+ children: /* @__PURE__ */ jsx15("span", { style: { position: "relative", width: 16, height: 16 }, children: /* @__PURE__ */ jsx15(AnimatePresence4, { initial: false, mode: "wait", children: copied ? /* @__PURE__ */ jsx15(
2559
2937
  motion5.svg,
2560
2938
  {
2561
2939
  viewBox: "0 0 24 24",
@@ -2569,7 +2947,7 @@ Apply these values as the new defaults in the useDialKit call.`;
2569
2947
  animate: { scale: 1, opacity: 1 },
2570
2948
  exit: { scale: 0.8, opacity: 0 },
2571
2949
  transition: { duration: 0.08 },
2572
- children: /* @__PURE__ */ jsx14("path", { d: ICON_CHECK })
2950
+ children: /* @__PURE__ */ jsx15("path", { d: ICON_CHECK })
2573
2951
  },
2574
2952
  "check"
2575
2953
  ) : /* @__PURE__ */ jsxs13(
@@ -2583,23 +2961,116 @@ Apply these values as the new defaults in the useDialKit call.`;
2583
2961
  exit: { scale: 0.8, opacity: 0 },
2584
2962
  transition: { duration: 0.08 },
2585
2963
  children: [
2586
- /* @__PURE__ */ jsx14("path", { d: ICON_CLIPBOARD.board, stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round" }),
2587
- /* @__PURE__ */ jsx14("path", { d: ICON_CLIPBOARD.sparkle, fill: "currentColor" }),
2588
- /* @__PURE__ */ jsx14("path", { d: ICON_CLIPBOARD.body, stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })
2964
+ /* @__PURE__ */ jsx15("path", { d: ICON_CLIPBOARD.board, stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round" }),
2965
+ /* @__PURE__ */ jsx15("path", { d: ICON_CLIPBOARD.sparkle, fill: "currentColor" }),
2966
+ /* @__PURE__ */ jsx15("path", { d: ICON_CLIPBOARD.body, stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })
2589
2967
  ]
2590
2968
  },
2591
2969
  "clipboard"
2592
2970
  ) }) })
2593
2971
  }
2594
- )
2972
+ ),
2973
+ toolbarExtra
2595
2974
  ] });
2596
2975
  if (variant === "section") {
2597
2976
  return /* @__PURE__ */ jsxs13(Folder, { title: panel.name, defaultOpen, onOpenChange: handleOpenChange, children: [
2598
- /* @__PURE__ */ jsx14("div", { className: "dialkit-panel-section-toolbar", onClick: (e) => e.stopPropagation(), children: toolbar }),
2977
+ /* @__PURE__ */ jsx15("div", { className: "dialkit-panel-section-toolbar", onClick: (e) => e.stopPropagation(), children: toolbar }),
2599
2978
  renderControls()
2600
2979
  ] });
2601
2980
  }
2602
- return /* @__PURE__ */ jsx14("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ jsx14(Folder, { title: panel.name, defaultOpen, isRoot: true, inline, onOpenChange: handleOpenChange, toolbar, children: renderControls() }) });
2981
+ return /* @__PURE__ */ jsx15("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ jsx15(Folder, { title: panel.name, defaultOpen, isRoot: true, inline, onOpenChange: handleOpenChange, toolbar, children: renderControls() }) });
2982
+ }
2983
+
2984
+ // src/components/Timeline/TimelineToggleButton.tsx
2985
+ import { useCallback as useCallback11, useSyncExternalStore as useSyncExternalStore5 } from "react";
2986
+ import { motion as motion6 } from "motion/react";
2987
+
2988
+ // src/store/TimelineUiStore.ts
2989
+ var TimelineUiStoreClass = class {
2990
+ constructor() {
2991
+ this.visible = true;
2992
+ this.initialized = false;
2993
+ this.controllers = /* @__PURE__ */ new Map();
2994
+ this.listeners = /* @__PURE__ */ new Set();
2995
+ }
2996
+ getVisible() {
2997
+ for (const controller of this.controllers.values()) {
2998
+ if (controller.visible !== void 0) return controller.visible;
2999
+ }
3000
+ return this.visible;
3001
+ }
3002
+ registerController(id, controller) {
3003
+ const previous = this.getVisible();
3004
+ if (!this.initialized) {
3005
+ this.visible = controller.defaultVisible;
3006
+ this.initialized = true;
3007
+ }
3008
+ this.controllers.set(id, controller);
3009
+ if (previous !== this.getVisible()) this.notify();
3010
+ return () => {
3011
+ const before = this.getVisible();
3012
+ this.controllers.delete(id);
3013
+ if (this.controllers.size === 0) this.initialized = false;
3014
+ if (before !== this.getVisible()) this.notify();
3015
+ };
3016
+ }
3017
+ updateController(id, controller) {
3018
+ if (!this.controllers.has(id)) return;
3019
+ const previous = this.getVisible();
3020
+ this.controllers.set(id, controller);
3021
+ if (previous !== this.getVisible()) this.notify();
3022
+ }
3023
+ requestVisible(visible) {
3024
+ const current = this.getVisible();
3025
+ if (current === visible) return;
3026
+ const controlled = Array.from(this.controllers.values()).filter(
3027
+ (controller) => controller.visible !== void 0
3028
+ );
3029
+ if (controlled.length > 0) {
3030
+ controlled.forEach((controller) => controller.onVisibilityChange?.(visible));
3031
+ return;
3032
+ }
3033
+ this.visible = visible;
3034
+ this.controllers.forEach((controller) => controller.onVisibilityChange?.(visible));
3035
+ this.notify();
3036
+ }
3037
+ toggle() {
3038
+ this.requestVisible(!this.getVisible());
3039
+ }
3040
+ subscribe(listener) {
3041
+ this.listeners.add(listener);
3042
+ return () => this.listeners.delete(listener);
3043
+ }
3044
+ notify() {
3045
+ this.listeners.forEach((listener) => listener());
3046
+ }
3047
+ };
3048
+ var TimelineUiStore = /* @__PURE__ */ new TimelineUiStoreClass();
3049
+
3050
+ // src/components/Timeline/TimelineToggleButton.tsx
3051
+ import { jsx as jsx16 } from "react/jsx-runtime";
3052
+ function TimelineToggleButton() {
3053
+ const subscribe = useCallback11(
3054
+ (listener) => TimelineUiStore.subscribe(listener),
3055
+ []
3056
+ );
3057
+ const getVisible = useCallback11(() => TimelineUiStore.getVisible(), []);
3058
+ const visible = useSyncExternalStore5(subscribe, getVisible, getVisible);
3059
+ const label = visible ? "Hide timeline" : "Show timeline";
3060
+ return /* @__PURE__ */ jsx16(
3061
+ motion6.button,
3062
+ {
3063
+ className: "dialkit-toolbar-add dialkit-timeline-toolbar-toggle",
3064
+ "data-active": visible || void 0,
3065
+ "aria-pressed": visible,
3066
+ "aria-label": label,
3067
+ title: label,
3068
+ onClick: () => TimelineUiStore.toggle(),
3069
+ whileTap: { scale: 0.9 },
3070
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
3071
+ children: /* @__PURE__ */ jsx16("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: ICON_TIMELINE.map((d, i) => /* @__PURE__ */ jsx16("path", { d, fill: "currentColor" }, i)) })
3072
+ }
3073
+ );
2603
3074
  }
2604
3075
 
2605
3076
  // src/panel-drag.ts
@@ -2667,32 +3138,39 @@ function blockPanelDragClick(handle) {
2667
3138
  }
2668
3139
 
2669
3140
  // src/components/DialRoot.tsx
2670
- import { jsx as jsx15 } from "react/jsx-runtime";
2671
- var isDevDefault = typeof process !== "undefined" && process?.env?.NODE_ENV ? process.env.NODE_ENV !== "production" : typeof import.meta !== "undefined" && import.meta.env?.MODE ? import.meta.env.MODE !== "production" : true;
3141
+ import { jsx as jsx17 } from "react/jsx-runtime";
2672
3142
  function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover", theme = "system", productionEnabled = isDevDefault, onOpenChange }) {
2673
3143
  if (!productionEnabled) return null;
2674
3144
  const [panels, setPanels] = useState10([]);
3145
+ const [timelineCount, setTimelineCount] = useState10(0);
2675
3146
  const [mounted, setMounted] = useState10(false);
2676
3147
  const inline = mode === "inline";
2677
- const panelRef = useRef11(null);
3148
+ const panelRef = useRef12(null);
2678
3149
  const [dragOffset, setDragOffset] = useState10(null);
2679
3150
  const [activePosition, setActivePosition] = useState10(position);
2680
- const lastDragOffset = useRef11(null);
2681
- const draggingRef = useRef11(false);
2682
- const dragStartRef = useRef11(null);
2683
- const didDragRef = useRef11(false);
2684
- const dragTargetRef = useRef11(null);
2685
- const panelOpenStatesRef = useRef11(/* @__PURE__ */ new Map());
2686
- const rootOpenRef = useRef11(null);
2687
- useEffect8(() => {
3151
+ const lastDragOffset = useRef12(null);
3152
+ const draggingRef = useRef12(false);
3153
+ const dragStartRef = useRef12(null);
3154
+ const didDragRef = useRef12(false);
3155
+ const dragTargetRef = useRef12(null);
3156
+ const panelOpenStatesRef = useRef12(/* @__PURE__ */ new Map());
3157
+ const rootOpenRef = useRef12(null);
3158
+ useEffect9(() => {
2688
3159
  setMounted(true);
2689
- setPanels(DialStore.getPanels());
2690
- const unsubscribe = DialStore.subscribeGlobal(() => {
2691
- setPanels(DialStore.getPanels());
3160
+ setPanels(DialStore.getPanels("panel"));
3161
+ setTimelineCount(TimelineStore.getTimelines().length);
3162
+ const unsubscribePanels = DialStore.subscribeGlobal(() => {
3163
+ setPanels(DialStore.getPanels("panel"));
2692
3164
  });
2693
- return unsubscribe;
3165
+ const unsubscribeTimelines = TimelineStore.subscribeGlobal(() => {
3166
+ setTimelineCount(TimelineStore.getTimelines().length);
3167
+ });
3168
+ return () => {
3169
+ unsubscribePanels();
3170
+ unsubscribeTimelines();
3171
+ };
2694
3172
  }, []);
2695
- useEffect8(() => {
3173
+ useEffect9(() => {
2696
3174
  const fallbackOpen = inline || defaultOpen;
2697
3175
  const nextStates = /* @__PURE__ */ new Map();
2698
3176
  for (const panel of panels) {
@@ -2701,7 +3179,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2701
3179
  panelOpenStatesRef.current = nextStates;
2702
3180
  rootOpenRef.current = Array.from(nextStates.values()).some(Boolean);
2703
3181
  }, [defaultOpen, inline, panels]);
2704
- useEffect8(() => {
3182
+ useEffect9(() => {
2705
3183
  if (!panelRef.current || inline) return;
2706
3184
  const observer = new MutationObserver(() => {
2707
3185
  const inners = panelRef.current?.querySelectorAll(".dialkit-panel-inner");
@@ -2729,7 +3207,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2729
3207
  observer.observe(panelRef.current, { subtree: true, attributes: true, attributeFilter: ["data-collapsed"] });
2730
3208
  return () => observer.disconnect();
2731
3209
  }, [inline, dragOffset, position]);
2732
- const handlePointerDown = useCallback10((e) => {
3210
+ const handlePointerDown = useCallback12((e) => {
2733
3211
  const panel = panelRef.current;
2734
3212
  const handle = getPanelDragHandle(e.target, panel);
2735
3213
  if (!panel || !handle) return;
@@ -2739,13 +3217,13 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2739
3217
  draggingRef.current = true;
2740
3218
  handle.setPointerCapture(e.pointerId);
2741
3219
  }, []);
2742
- const handlePointerMove = useCallback10((e) => {
3220
+ const handlePointerMove = useCallback12((e) => {
2743
3221
  if (!draggingRef.current || !dragStartRef.current) return;
2744
3222
  if (!didDragRef.current && !hasPanelDragMoved(dragStartRef.current, e.clientX, e.clientY)) return;
2745
3223
  didDragRef.current = true;
2746
3224
  setDragOffset(getPanelDragOffset(dragStartRef.current, e.clientX, e.clientY));
2747
3225
  }, []);
2748
- const handlePointerUp = useCallback10((e) => {
3226
+ const handlePointerUp = useCallback12((e) => {
2749
3227
  if (!draggingRef.current) return;
2750
3228
  draggingRef.current = false;
2751
3229
  dragStartRef.current = null;
@@ -2761,7 +3239,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2761
3239
  }
2762
3240
  dragTargetRef.current = null;
2763
3241
  }, []);
2764
- const handlePanelOpenChange = useCallback10((panelId, open) => {
3242
+ const handlePanelOpenChange = useCallback12((panelId, open) => {
2765
3243
  panelOpenStatesRef.current.set(panelId, open);
2766
3244
  const fallbackOpen = inline || defaultOpen;
2767
3245
  const nextRootOpen = panels.some((panel) => panelOpenStatesRef.current.get(panel.id) ?? fallbackOpen);
@@ -2769,7 +3247,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2769
3247
  rootOpenRef.current = nextRootOpen;
2770
3248
  onOpenChange?.(nextRootOpen);
2771
3249
  }, [defaultOpen, inline, onOpenChange, panels]);
2772
- const handleRootOpenChange = useCallback10((open) => {
3250
+ const handleRootOpenChange = useCallback12((open) => {
2773
3251
  if (rootOpenRef.current === open) return;
2774
3252
  rootOpenRef.current = open;
2775
3253
  onOpenChange?.(open);
@@ -2777,7 +3255,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2777
3255
  if (!mounted || typeof window === "undefined") {
2778
3256
  return null;
2779
3257
  }
2780
- if (panels.length === 0) {
3258
+ if (panels.length === 0 && timelineCount === 0) {
2781
3259
  return null;
2782
3260
  }
2783
3261
  const dragStyle = dragOffset ? {
@@ -2788,7 +3266,8 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2788
3266
  } : void 0;
2789
3267
  const originX = getPanelOriginX(activePosition, dragOffset);
2790
3268
  const hasMultiplePanels = panels.length > 1;
2791
- const content = /* @__PURE__ */ jsx15(ShortcutListener, { children: /* @__PURE__ */ jsx15("div", { className: "dialkit-root", "data-mode": mode, "data-theme": theme, children: /* @__PURE__ */ jsx15(
3269
+ const timelineToggle = timelineCount > 0 ? /* @__PURE__ */ jsx17(TimelineToggleButton, {}) : null;
3270
+ const content = /* @__PURE__ */ jsx17(ShortcutListener, { children: /* @__PURE__ */ jsx17("div", { className: "dialkit-root", "data-mode": mode, "data-theme": theme, children: /* @__PURE__ */ jsx17(
2792
3271
  "div",
2793
3272
  {
2794
3273
  ref: panelRef,
@@ -2802,7 +3281,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2802
3281
  onPointerMove: !inline ? handlePointerMove : void 0,
2803
3282
  onPointerUp: !inline ? handlePointerUp : void 0,
2804
3283
  onPointerCancel: !inline ? handlePointerUp : void 0,
2805
- children: hasMultiplePanels ? /* @__PURE__ */ jsx15("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ jsx15(
3284
+ children: panels.length === 0 ? /* @__PURE__ */ jsx17("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ jsx17(
2806
3285
  Folder,
2807
3286
  {
2808
3287
  title: "DialKit",
@@ -2810,23 +3289,37 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2810
3289
  isRoot: true,
2811
3290
  inline,
2812
3291
  onOpenChange: handleRootOpenChange,
3292
+ toolbar: timelineToggle,
2813
3293
  panelHeightOffset: 2,
2814
- children: panels.map((panel) => /* @__PURE__ */ jsx15(
2815
- Panel,
2816
- {
2817
- panel,
2818
- defaultOpen: true,
2819
- variant: "section"
2820
- },
2821
- panel.id
3294
+ children: /* @__PURE__ */ jsx17("div", { className: "dialkit-timeline-toolkit-only", children: "Timeline" })
3295
+ }
3296
+ ) }) : hasMultiplePanels ? /* @__PURE__ */ jsx17("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ jsx17(
3297
+ Folder,
3298
+ {
3299
+ title: "DialKit",
3300
+ defaultOpen: inline || defaultOpen,
3301
+ isRoot: true,
3302
+ inline,
3303
+ onOpenChange: handleRootOpenChange,
3304
+ toolbar: timelineToggle,
3305
+ panelHeightOffset: 2,
3306
+ children: panels.map((panel) => /* @__PURE__ */ jsx17(
3307
+ Panel,
3308
+ {
3309
+ panel,
3310
+ defaultOpen: true,
3311
+ variant: "section"
3312
+ },
3313
+ panel.id
2822
3314
  ))
2823
3315
  }
2824
- ) }) : panels.map((panel) => /* @__PURE__ */ jsx15(
3316
+ ) }) : panels.map((panel) => /* @__PURE__ */ jsx17(
2825
3317
  Panel,
2826
3318
  {
2827
3319
  panel,
2828
3320
  defaultOpen: inline || defaultOpen,
2829
3321
  inline,
3322
+ toolbarExtra: timelineToggle,
2830
3323
  onOpenChange: (open) => handlePanelOpenChange(panel.id, open)
2831
3324
  },
2832
3325
  panel.id
@@ -2839,10 +3332,2250 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2839
3332
  return createPortal3(content, document.body);
2840
3333
  }
2841
3334
 
3335
+ // src/hooks/useDialTimeline.ts
3336
+ import { useCallback as useCallback13, useEffect as useEffect10, useMemo as useMemo2, useRef as useRef13, useSyncExternalStore as useSyncExternalStore6 } from "react";
3337
+
3338
+ // src/timeline-core.ts
3339
+ var CLIP_VALUE_STEP = 0.01;
3340
+ var TIMELINE_MIN_CLIP_DURATION = 0.05;
3341
+ var DEFAULT_STEP_DURATION = 0.3;
3342
+ var DEFAULT_CLIP_TRANSITION = { type: "spring", bounce: 0.2 };
3343
+ var RESERVED_KEYS = /* @__PURE__ */ new Set(["time", "playing", "duration", "play", "pause", "replay", "seek"]);
3344
+ function isClipConfig(value) {
3345
+ return isPlainObject(value) && Number.isFinite(value.at);
3346
+ }
3347
+ function isGroupConfig(value) {
3348
+ if (!isPlainObject(value) || "at" in value) return false;
3349
+ const entries = Object.values(value);
3350
+ return entries.length > 0 && entries.some(isClipConfig);
3351
+ }
3352
+ function isPlainObject(value) {
3353
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3354
+ }
3355
+ function nonNegativeFinite(value, fallback = 0) {
3356
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : fallback;
3357
+ }
3358
+ function animatedDuration(value, fallback = DEFAULT_STEP_DURATION) {
3359
+ return Math.max(TIMELINE_MIN_CLIP_DURATION, nonNegativeFinite(value, fallback));
3360
+ }
3361
+ function transitionDefaultDuration(transition) {
3362
+ if (transition.type === "easing") return animatedDuration(transition.duration);
3363
+ if (isPhysicsSpring(transition)) {
3364
+ return animatedDuration(springSettleDuration(springParams(transition)));
3365
+ }
3366
+ if (transition.visualDuration !== void 0) return animatedDuration(transition.visualDuration);
3367
+ return animatedDuration(springSettleDuration(springParams(transition)));
3368
+ }
3369
+ function defaultStepDuration(step, inheritedTransition) {
3370
+ const curve = step.transition ?? inheritedTransition;
3371
+ if (curve && isPhysicsSpring(curve)) return transitionDefaultDuration(curve);
3372
+ if (step.duration !== void 0) return animatedDuration(step.duration);
3373
+ if (step.transition) return transitionDefaultDuration(step.transition);
3374
+ return DEFAULT_STEP_DURATION;
3375
+ }
3376
+ function defaultTrackDuration(track, inheritedTransition) {
3377
+ const curve = track.transition ?? inheritedTransition;
3378
+ if (track.steps?.length) {
3379
+ return track.steps.reduce((sum, step) => sum + defaultStepDuration(step, curve), 0);
3380
+ }
3381
+ if (curve && isPhysicsSpring(curve)) return transitionDefaultDuration(curve);
3382
+ if (track.duration !== void 0) return animatedDuration(track.duration);
3383
+ if (track.transition) return transitionDefaultDuration(track.transition);
3384
+ return DEFAULT_STEP_DURATION;
3385
+ }
3386
+ function defaultClipDuration(clip) {
3387
+ const defaultCurve = isTransitionConfig(clip.transition) ? clip.transition : DEFAULT_CLIP_TRANSITION;
3388
+ if (clip.props) {
3389
+ return Object.values(clip.props).reduce(
3390
+ (max, track) => Math.max(
3391
+ max,
3392
+ nonNegativeFinite(track.delay) + defaultTrackDuration(track, defaultCurve)
3393
+ ),
3394
+ 0
3395
+ );
3396
+ }
3397
+ if (clip.steps?.length) {
3398
+ return clip.steps.reduce((sum, step) => sum + defaultStepDuration(step, defaultCurve), 0);
3399
+ }
3400
+ const animating = Boolean(clip.transition || clip.from || clip.to);
3401
+ if (!animating) return nonNegativeFinite(clip.duration);
3402
+ if (isPhysicsSpring(defaultCurve)) return transitionDefaultDuration(defaultCurve);
3403
+ if (clip.duration !== void 0) return animatedDuration(clip.duration);
3404
+ if (isTransitionConfig(clip.transition)) return transitionDefaultDuration(clip.transition);
3405
+ return clip.from || clip.to ? transitionDefaultDuration(DEFAULT_CLIP_TRANSITION) : 0;
3406
+ }
3407
+ function normalizeLoopMode(value) {
3408
+ if (value === true || value === "mirror" || value === "repeat") return "repeat";
3409
+ return "off";
3410
+ }
3411
+ function normalizeStoredTransition(transition, clipDuration) {
3412
+ if (transition.type === "easing") {
3413
+ return { ...transition, duration: clipDuration };
3414
+ }
3415
+ if (isPhysicsSpring(transition)) {
3416
+ return transition;
3417
+ }
3418
+ return { type: "spring", bounce: transition.bounce ?? 0.2 };
3419
+ }
3420
+ function collectClipEntries(config) {
3421
+ const entries = [];
3422
+ for (const [key, value] of Object.entries(config)) {
3423
+ if (key === "duration") continue;
3424
+ if (RESERVED_KEYS.has(key)) {
3425
+ console.warn(`[dialkit] Timeline key "${key}" collides with a reserved key and was skipped.`);
3426
+ continue;
3427
+ }
3428
+ if (isClipConfig(value)) {
3429
+ entries.push({ path: key, childKey: key, clip: value });
3430
+ } else if (isGroupConfig(value)) {
3431
+ for (const [childKey, childClip] of Object.entries(value)) {
3432
+ if (isClipConfig(childClip)) {
3433
+ entries.push({ path: `${key}.${childKey}`, childKey, group: key, clip: childClip });
3434
+ } else {
3435
+ console.warn(
3436
+ `[dialkit] Timeline clip "${key}.${childKey}" is missing a numeric "at" and was skipped.`
3437
+ );
3438
+ }
3439
+ }
3440
+ } else {
3441
+ console.warn(
3442
+ `[dialkit] Timeline entry "${key}" is neither a clip (needs a numeric "at") nor a group of clips and was skipped.`
3443
+ );
3444
+ }
3445
+ }
3446
+ return entries;
3447
+ }
3448
+ function definedValues(values) {
3449
+ if (!values) return void 0;
3450
+ const result = {};
3451
+ for (const [key, value] of Object.entries(values)) {
3452
+ if (value !== void 0) result[key] = value;
3453
+ }
3454
+ return result;
3455
+ }
3456
+ function setDialPath(dialConfig, path, value) {
3457
+ const segments = path.split(".");
3458
+ let node = dialConfig;
3459
+ for (const segment of segments.slice(0, -1)) {
3460
+ node = node[segment] ?? (node[segment] = {});
3461
+ }
3462
+ node[segments[segments.length - 1]] = value;
3463
+ }
3464
+ function parseTimelineConfig(config) {
3465
+ const entries = collectClipEntries(config);
3466
+ let maxEnd = 0;
3467
+ for (const { clip } of entries) {
3468
+ maxEnd = Math.max(maxEnd, nonNegativeFinite(clip.at) + defaultClipDuration(clip));
3469
+ }
3470
+ const duration = typeof config.duration === "number" && Number.isFinite(config.duration) && config.duration > 0 ? config.duration : maxEnd > 0 ? Math.ceil(maxEnd * 100 - 1e-4) / 100 : 1;
3471
+ const dialConfig = {};
3472
+ const clips = [];
3473
+ entries.forEach(({ path, childKey, group, clip }, index) => {
3474
+ const raw = clip;
3475
+ if (raw.props && (raw.steps?.length || raw.from || raw.to)) {
3476
+ console.warn(
3477
+ `[dialkit] Timeline clip "${path}": "props" is mutually exclusive with from/to/steps \u2014 using "props".`
3478
+ );
3479
+ } else if (raw.steps?.length && raw.to) {
3480
+ console.warn(
3481
+ `[dialkit] Timeline clip "${path}": "to" is ignored when "steps" is present \u2014 each leg's "to" defines its targets.`
3482
+ );
3483
+ }
3484
+ const hasSteps = Boolean(clip.steps?.length) && !clip.props;
3485
+ const hasProps = Boolean(clip.props);
3486
+ const single = isTransitionConfig(clip.transition) ? clip.transition : void 0;
3487
+ const total = defaultClipDuration(clip);
3488
+ const defaultCurve = single ?? DEFAULT_CLIP_TRANSITION;
3489
+ const clipAt = nonNegativeFinite(clip.at);
3490
+ const clipDial = {
3491
+ at: [clipAt, 0, duration, CLIP_VALUE_STEP]
3492
+ };
3493
+ if (!hasSteps && !hasProps) {
3494
+ clipDial.duration = [total, 0, duration, CLIP_VALUE_STEP];
3495
+ }
3496
+ if (!hasSteps && !hasProps && (clip.transition || clip.from || clip.to)) {
3497
+ clipDial.transition = normalizeStoredTransition(defaultCurve, total);
3498
+ }
3499
+ let tracks;
3500
+ if (clip.props) {
3501
+ tracks = [];
3502
+ for (const [prop, track] of Object.entries(clip.props)) {
3503
+ if (TRACK_RESERVED.has(prop) || /^step\d+$/.test(prop)) {
3504
+ console.warn(`[dialkit] Timeline property "${prop}" collides with a clip field and was skipped.`);
3505
+ continue;
3506
+ }
3507
+ const trackDuration = defaultTrackDuration(track, defaultCurve);
3508
+ const trackCurve = track.transition ?? defaultCurve;
3509
+ const hasTrackSteps = Boolean(track.steps?.length);
3510
+ const trackDial = {
3511
+ delay: [nonNegativeFinite(track.delay), 0, duration, CLIP_VALUE_STEP]
3512
+ };
3513
+ if (!hasTrackSteps) {
3514
+ trackDial.duration = [trackDuration, 0, duration, CLIP_VALUE_STEP];
3515
+ trackDial.transition = normalizeStoredTransition(trackCurve, trackDuration);
3516
+ }
3517
+ const fromValue = track.from ?? (hasTrackSteps ? void 0 : track.to);
3518
+ if (hasTrackSteps && fromValue === void 0) {
3519
+ console.warn(
3520
+ `[dialkit] Timeline clip "${path}": track "${prop}" has steps but no "from" \u2014 declare its starting value.`
3521
+ );
3522
+ }
3523
+ if (fromValue !== void 0) {
3524
+ trackDial.from = scalarDial(prop, fromValue, hasTrackSteps ? track.steps[0]?.to : track.to);
3525
+ }
3526
+ if (!hasTrackSteps && track.to !== void 0) {
3527
+ trackDial.to = scalarDial(prop, track.to, fromValue);
3528
+ }
3529
+ let trackStepKeys;
3530
+ if (hasTrackSteps) {
3531
+ trackStepKeys = [];
3532
+ let previous = fromValue;
3533
+ track.steps.forEach((step, stepIndex) => {
3534
+ const stepKey = `step${stepIndex + 1}`;
3535
+ trackStepKeys.push(stepKey);
3536
+ const stepDuration = defaultStepDuration(step, trackCurve);
3537
+ const stepDial = {
3538
+ duration: [stepDuration, 0, duration, CLIP_VALUE_STEP],
3539
+ transition: normalizeStoredTransition(step.transition ?? trackCurve, stepDuration)
3540
+ };
3541
+ if (step.to !== void 0) {
3542
+ stepDial.to = scalarDial(prop, step.to, previous);
3543
+ previous = step.to;
3544
+ }
3545
+ trackDial[stepKey] = stepDial;
3546
+ });
3547
+ }
3548
+ clipDial[prop] = trackDial;
3549
+ tracks.push({ prop, stepKeys: trackStepKeys });
3550
+ }
3551
+ }
3552
+ if (clip.from && !hasProps) {
3553
+ clipDial.from = withFromToRanges(
3554
+ clip.from,
3555
+ hasSteps ? definedValues(clip.steps[0]?.to) : clip.to
3556
+ );
3557
+ }
3558
+ if (!hasSteps && !hasProps && clip.to) {
3559
+ clipDial.to = withFromToRanges(clip.to, clip.from);
3560
+ }
3561
+ let stepKeys;
3562
+ if (hasSteps) {
3563
+ stepKeys = [];
3564
+ let running = clip.from;
3565
+ clip.steps.forEach((step, stepIndex) => {
3566
+ const stepKey = `step${stepIndex + 1}`;
3567
+ stepKeys.push(stepKey);
3568
+ const stepDuration = defaultStepDuration(step, defaultCurve);
3569
+ const stepDial = {
3570
+ duration: [stepDuration, 0, duration, CLIP_VALUE_STEP],
3571
+ transition: normalizeStoredTransition(step.transition ?? defaultCurve, stepDuration)
3572
+ };
3573
+ const stepTo = definedValues(step.to);
3574
+ if (stepTo) {
3575
+ for (const prop of Object.keys(stepTo)) {
3576
+ if (!running || !(prop in running)) {
3577
+ console.warn(
3578
+ `[dialkit] Timeline clip "${path}": property "${prop}" first animates in step ${stepIndex + 1} with no starting value \u2014 declare it in "from".`
3579
+ );
3580
+ }
3581
+ }
3582
+ stepDial.to = withFromToRanges(stepTo, running);
3583
+ }
3584
+ clipDial[stepKey] = stepDial;
3585
+ running = { ...running ?? {}, ...stepTo ?? {} };
3586
+ });
3587
+ }
3588
+ setDialPath(dialConfig, path, clipDial);
3589
+ clips.push({
3590
+ key: path,
3591
+ label: formatLabel(childKey),
3592
+ color: TIMELINE_CLIP_COLORS[index % TIMELINE_CLIP_COLORS.length],
3593
+ loop: normalizeLoopMode(clip.loop),
3594
+ group,
3595
+ stepKeys,
3596
+ tracks
3597
+ });
3598
+ });
3599
+ return { duration, dialConfig, clips };
3600
+ }
3601
+ var TRACK_RESERVED = /* @__PURE__ */ new Set(["at", "duration", "loop", "from", "to", "transition", "delay"]);
3602
+ function scalarDial(prop, value, counterpart) {
3603
+ const record = withFromToRanges(
3604
+ { [prop]: value },
3605
+ counterpart === void 0 ? void 0 : { [prop]: counterpart }
3606
+ );
3607
+ return record[prop];
3608
+ }
3609
+ var FROM_TO_RANGE_PRESETS = [
3610
+ [/^(x|y|z|tx|ty|offsetx|offsety|translatex|translatey)$/i, { min: -100, max: 100, step: 1 }],
3611
+ [/rotat|angle|skew/i, { min: -180, max: 180, step: 1 }],
3612
+ [/^scale/i, { min: 0, max: 2, step: 0.01 }],
3613
+ [/opacity|alpha/i, { min: 0, max: 1, step: 0.01 }],
3614
+ [/blur|radius|spread/i, { min: 0, max: 100, step: 1 }]
3615
+ ];
3616
+ function inferFromToRange(key, value, counterpart) {
3617
+ const lo = Math.min(value, counterpart ?? value);
3618
+ const hi = Math.max(value, counterpart ?? value);
3619
+ const preset = FROM_TO_RANGE_PRESETS.find(([pattern]) => pattern.test(key))?.[1];
3620
+ if (preset) {
3621
+ return [value, Math.min(preset.min, lo), Math.max(preset.max, hi), preset.step];
3622
+ }
3623
+ if (lo >= 0 && hi <= 1) {
3624
+ return [value, 0, 1, 0.01];
3625
+ }
3626
+ const extent = Math.max(Math.abs(lo), Math.abs(hi), 1);
3627
+ const min = lo < 0 ? -extent * 2 : 0;
3628
+ const max = Math.max(extent * 2, hi);
3629
+ return [value, min, max, inferStep(min, max)];
3630
+ }
3631
+ function withFromToRanges(config, counterpart) {
3632
+ const result = {};
3633
+ for (const [key, value] of Object.entries(config)) {
3634
+ const other = counterpart?.[key];
3635
+ if (typeof value === "number") {
3636
+ result[key] = inferFromToRange(key, value, typeof other === "number" ? other : void 0);
3637
+ } else if (isPlainObject(value) && !("type" in value)) {
3638
+ result[key] = withFromToRanges(
3639
+ value,
3640
+ isPlainObject(other) && !("type" in other) ? other : void 0
3641
+ );
3642
+ } else {
3643
+ result[key] = value;
3644
+ }
3645
+ }
3646
+ return result;
3647
+ }
3648
+ function curveStatic(transition, duration) {
3649
+ if (!transition) return { duration };
3650
+ if (transition.type === "easing") return { duration, ease: transition.ease };
3651
+ const spring = springParams(transition);
3652
+ return { duration, spring, settle: springSettleDuration(spring) };
3653
+ }
3654
+ function sampleCurve(curve, elapsed) {
3655
+ if (elapsed <= 0) return 0;
3656
+ if (curve.spring) {
3657
+ if (curve.settle !== void 0 && elapsed >= curve.settle) return 1;
3658
+ return springProgress(elapsed, curve.spring);
3659
+ }
3660
+ if (curve.ease) {
3661
+ return cubicBezierProgress(clamp(curve.duration > 0 ? elapsed / curve.duration : 1, 0, 1), curve.ease);
3662
+ }
3663
+ return curve.duration > 0 ? Math.min(1, elapsed / curve.duration) : 1;
3664
+ }
3665
+ function resolvedAtPath(resolved, path) {
3666
+ let node = resolved;
3667
+ for (const segment of path.split(".")) {
3668
+ node = isPlainObject(node) ? node[segment] : void 0;
3669
+ }
3670
+ return isPlainObject(node) ? node : {};
3671
+ }
3672
+ function computeStaticClips(parsed, flatValues) {
3673
+ const resolved = resolveDialValues(parsed.dialConfig, flatValues);
3674
+ return parsed.clips.map(
3675
+ (clip) => buildClipStatic(resolvedAtPath(resolved, clip.key), clip, parsed.duration)
3676
+ );
3677
+ }
3678
+ function computeStaticTimeline(parsed, flatValues) {
3679
+ let clips = computeStaticClips(parsed, flatValues);
3680
+ const maxEnd = clips.reduce(
3681
+ (end, clip) => Math.max(end, clip.at + clip.duration),
3682
+ parsed.duration
3683
+ );
3684
+ const duration = maxEnd > parsed.duration ? Math.ceil(maxEnd * 100 - 1e-4) / 100 : parsed.duration;
3685
+ if (duration !== parsed.duration) {
3686
+ clips = clips.map(
3687
+ (clip) => clip.loop === "repeat" ? { ...clip, end: duration } : clip
3688
+ );
3689
+ }
3690
+ return { duration, clips };
3691
+ }
3692
+ function computeClipStaticFromValues(values, clip, timelineDuration) {
3693
+ return buildClipStatic(unflattenClipValues(values, clip.key), clip, timelineDuration);
3694
+ }
3695
+ function buildClipStatic(clipResolved, clip, timelineDuration) {
3696
+ {
3697
+ const at = typeof clipResolved.at === "number" ? clipResolved.at : 0;
3698
+ const from = isPlainObject(clipResolved.from) ? clipResolved.from : void 0;
3699
+ const single = isTransitionConfig(clipResolved.transition) ? clipResolved.transition : void 0;
3700
+ const staticClip = {
3701
+ key: clip.key,
3702
+ childKey: clip.group ? clip.key.slice(clip.group.length + 1) : clip.key,
3703
+ group: clip.group,
3704
+ at,
3705
+ duration: 0,
3706
+ loop: "off",
3707
+ end: 0,
3708
+ isPhysics: false,
3709
+ from,
3710
+ tracks: [],
3711
+ explicitSteps: Boolean(clip.stepKeys?.length)
3712
+ };
3713
+ if (clip.tracks?.length) {
3714
+ const tracks = clip.tracks.map(({ prop, stepKeys }) => {
3715
+ const trackResolved = isPlainObject(clipResolved[prop]) ? clipResolved[prop] : {};
3716
+ const delay = typeof trackResolved.delay === "number" ? trackResolved.delay : 0;
3717
+ const fromValue = trackResolved.from;
3718
+ let steps;
3719
+ let trackDuration = 0;
3720
+ if (stepKeys?.length) {
3721
+ let running = fromValue;
3722
+ steps = stepKeys.map((stepKey) => {
3723
+ const stepResolved = isPlainObject(trackResolved[stepKey]) ? trackResolved[stepKey] : {};
3724
+ const storedDuration = typeof stepResolved.duration === "number" ? stepResolved.duration : 0;
3725
+ const raw = isTransitionConfig(stepResolved.transition) ? stepResolved.transition : void 0;
3726
+ const effective = raw ? resolveClipTransition(raw, storedDuration) : { transition: void 0, duration: storedDuration, isPhysics: false };
3727
+ const toValue = stepResolved.to;
3728
+ const step = {
3729
+ key: stepKey,
3730
+ offset: trackDuration,
3731
+ duration: effective.duration,
3732
+ isPhysics: effective.isPhysics,
3733
+ start: running === void 0 ? {} : { [prop]: running },
3734
+ to: toValue === void 0 ? {} : { [prop]: toValue },
3735
+ curve: curveStatic(effective.transition, effective.duration)
3736
+ };
3737
+ if (toValue !== void 0) running = toValue;
3738
+ trackDuration += effective.duration;
3739
+ return step;
3740
+ });
3741
+ } else {
3742
+ const storedDuration = typeof trackResolved.duration === "number" ? trackResolved.duration : 0;
3743
+ const raw = isTransitionConfig(trackResolved.transition) ? trackResolved.transition : void 0;
3744
+ const effective = raw ? resolveClipTransition(raw, storedDuration) : { transition: void 0, duration: storedDuration, isPhysics: false };
3745
+ const toValue = trackResolved.to;
3746
+ trackDuration = effective.duration;
3747
+ steps = [
3748
+ {
3749
+ key: null,
3750
+ offset: 0,
3751
+ duration: effective.duration,
3752
+ isPhysics: effective.isPhysics,
3753
+ start: fromValue === void 0 ? {} : { [prop]: fromValue },
3754
+ to: toValue === void 0 ? {} : { [prop]: toValue },
3755
+ curve: curveStatic(effective.transition, effective.duration)
3756
+ }
3757
+ ];
3758
+ }
3759
+ return { prop, delay, duration: trackDuration, steps };
3760
+ });
3761
+ staticClip.tracks = tracks;
3762
+ staticClip.props = tracks.map((track) => track.prop);
3763
+ staticClip.duration = tracks.reduce((max, track) => Math.max(max, track.delay + track.duration), 0);
3764
+ staticClip.from = Object.fromEntries(
3765
+ tracks.map((track) => [track.prop, track.steps[0].start[track.prop]])
3766
+ );
3767
+ staticClip.to = Object.fromEntries(
3768
+ tracks.map((track) => {
3769
+ const last = track.steps[track.steps.length - 1];
3770
+ return [track.prop, last.to[track.prop] ?? last.start[track.prop]];
3771
+ })
3772
+ );
3773
+ staticClip.loop = staticClip.duration > 0 ? clip.loop : "off";
3774
+ staticClip.end = staticClip.loop === "off" ? staticClip.at + staticClip.duration : timelineDuration;
3775
+ return staticClip;
3776
+ }
3777
+ if (clip.stepKeys?.length) {
3778
+ let running = { ...from ?? {} };
3779
+ let offset = 0;
3780
+ const steps = clip.stepKeys.map((stepKey) => {
3781
+ const stepResolved = isPlainObject(clipResolved[stepKey]) ? clipResolved[stepKey] : {};
3782
+ const storedDuration = typeof stepResolved.duration === "number" ? stepResolved.duration : 0;
3783
+ const raw = isTransitionConfig(stepResolved.transition) ? stepResolved.transition : void 0;
3784
+ const effective = raw ? resolveClipTransition(raw, storedDuration) : { transition: void 0, duration: storedDuration, isPhysics: false };
3785
+ const to = isPlainObject(stepResolved.to) ? stepResolved.to : {};
3786
+ const step = {
3787
+ key: stepKey,
3788
+ offset,
3789
+ duration: effective.duration,
3790
+ isPhysics: effective.isPhysics,
3791
+ start: running,
3792
+ to,
3793
+ curve: curveStatic(effective.transition, effective.duration)
3794
+ };
3795
+ running = { ...running, ...to };
3796
+ offset += effective.duration;
3797
+ return step;
3798
+ });
3799
+ staticClip.tracks = [{ delay: 0, duration: offset, steps }];
3800
+ staticClip.duration = offset;
3801
+ staticClip.to = running;
3802
+ } else {
3803
+ const storedDuration = typeof clipResolved.duration === "number" ? clipResolved.duration : 0;
3804
+ const to = isPlainObject(clipResolved.to) ? clipResolved.to : void 0;
3805
+ if (single) {
3806
+ const effective = resolveClipTransition(single, storedDuration);
3807
+ staticClip.duration = effective.duration;
3808
+ staticClip.isPhysics = effective.isPhysics;
3809
+ staticClip.transition = effective.transition;
3810
+ staticClip.css = transitionToCss(effective.transition);
3811
+ staticClip.to = to;
3812
+ if (from && to) {
3813
+ staticClip.tracks = [
3814
+ {
3815
+ delay: 0,
3816
+ duration: effective.duration,
3817
+ steps: [
3818
+ {
3819
+ key: null,
3820
+ offset: 0,
3821
+ duration: effective.duration,
3822
+ isPhysics: effective.isPhysics,
3823
+ start: from,
3824
+ to,
3825
+ curve: curveStatic(effective.transition, effective.duration)
3826
+ }
3827
+ ]
3828
+ }
3829
+ ];
3830
+ }
3831
+ } else {
3832
+ staticClip.duration = storedDuration;
3833
+ staticClip.to = to;
3834
+ if (from && to) {
3835
+ const base = resolveClipTransition(DEFAULT_CLIP_TRANSITION, storedDuration);
3836
+ staticClip.duration = base.duration;
3837
+ staticClip.tracks = [
3838
+ {
3839
+ delay: 0,
3840
+ duration: base.duration,
3841
+ steps: [
3842
+ {
3843
+ key: null,
3844
+ offset: 0,
3845
+ duration: base.duration,
3846
+ isPhysics: false,
3847
+ start: from,
3848
+ to,
3849
+ curve: curveStatic(base.transition, base.duration)
3850
+ }
3851
+ ]
3852
+ }
3853
+ ];
3854
+ }
3855
+ }
3856
+ }
3857
+ if (staticClip.tracks.length) {
3858
+ const props = new Set(Object.keys(from ?? {}));
3859
+ for (const track of staticClip.tracks) {
3860
+ for (const step of track.steps) {
3861
+ for (const prop of Object.keys(step.to)) props.add(prop);
3862
+ }
3863
+ }
3864
+ staticClip.props = Array.from(props);
3865
+ }
3866
+ staticClip.loop = staticClip.duration > 0 ? clip.loop : "off";
3867
+ staticClip.end = staticClip.loop === "off" ? staticClip.at + staticClip.duration : timelineDuration;
3868
+ return staticClip;
3869
+ }
3870
+ }
3871
+ function stepAtPosition(steps, pos) {
3872
+ for (const step of steps) {
3873
+ if (pos < step.offset + step.duration) return step;
3874
+ }
3875
+ return steps[steps.length - 1];
3876
+ }
3877
+ function evalPropAtPos(steps, prop, pos) {
3878
+ const step = stepAtPosition(steps, pos);
3879
+ const within = Math.max(0, pos - step.offset);
3880
+ if (prop in step.to) {
3881
+ const eased = sampleCurve(step.curve, within);
3882
+ return interpolateResolved(step.start[prop], step.to[prop], eased);
3883
+ }
3884
+ return step.start[prop];
3885
+ }
3886
+ function computeClipState(clip, time, cycleTime = time) {
3887
+ const total = clip.duration;
3888
+ const looping = clip.loop === "repeat" && total > 0;
3889
+ const started = time >= clip.at || looping && cycleTime > time;
3890
+ const done = time >= clip.end;
3891
+ const elapsed = time - clip.at;
3892
+ const phaseElapsed = looping ? cycleTime - clip.at : elapsed;
3893
+ const fold = (e) => looping ? e % total : e;
3894
+ const basePos = started ? fold(Math.max(0, phaseElapsed)) : 0;
3895
+ const progress = total > 0 ? clamp(basePos / total, 0, 1) : started ? 1 : 0;
3896
+ let current;
3897
+ let stepIndex = 0;
3898
+ if (clip.tracks.length && clip.props?.length) {
3899
+ current = {};
3900
+ for (const track of clip.tracks) {
3901
+ const props = track.prop !== void 0 ? [track.prop] : clip.props;
3902
+ for (const prop of props) {
3903
+ const startValue = track.steps[0]?.start[prop];
3904
+ if (!started) {
3905
+ if (startValue !== void 0) current[prop] = startValue;
3906
+ continue;
3907
+ }
3908
+ const phase = phaseElapsed - track.delay;
3909
+ if (phase <= 0) {
3910
+ if (startValue !== void 0) current[prop] = startValue;
3911
+ continue;
3912
+ }
3913
+ const pos = looping && track.duration > 0 ? phase % track.duration : phase;
3914
+ const value = evalPropAtPos(track.steps, prop, pos);
3915
+ if (value !== void 0) current[prop] = value;
3916
+ }
3917
+ }
3918
+ const shared = clip.tracks[0];
3919
+ if (started && clip.explicitSteps && shared.prop === void 0) {
3920
+ stepIndex = shared.steps.indexOf(stepAtPosition(shared.steps, basePos));
3921
+ }
3922
+ }
3923
+ return {
3924
+ at: clip.at,
3925
+ duration: clip.duration,
3926
+ loop: clip.loop,
3927
+ started,
3928
+ active: started && !done,
3929
+ done,
3930
+ progress,
3931
+ step: clip.explicitSteps ? stepIndex : void 0,
3932
+ from: clip.from,
3933
+ to: clip.to,
3934
+ animate: started ? clip.to : clip.from,
3935
+ transition: clip.transition,
3936
+ css: clip.css,
3937
+ current
3938
+ };
3939
+ }
3940
+ function interpolateResolved(from, to, p) {
3941
+ if (typeof from === "number" && typeof to === "number") {
3942
+ return from + (to - from) * p;
3943
+ }
3944
+ if (typeof from === "string" && typeof to === "string") {
3945
+ const mixed = mixHexColors(from, to, p);
3946
+ if (mixed) return mixed;
3947
+ }
3948
+ if (isPlainObject(from) && isPlainObject(to)) {
3949
+ const result = {};
3950
+ for (const key of Object.keys(from)) {
3951
+ result[key] = key in to ? interpolateResolved(from[key], to[key], p) : from[key];
3952
+ }
3953
+ for (const key of Object.keys(to)) {
3954
+ if (!(key in from)) result[key] = to[key];
3955
+ }
3956
+ return result;
3957
+ }
3958
+ return p < 0.5 ? from : to;
3959
+ }
3960
+ function parseHex(hex) {
3961
+ if (!isHexColor(hex)) return null;
3962
+ let h = hex.slice(1);
3963
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
3964
+ return [
3965
+ parseInt(h.slice(0, 2), 16),
3966
+ parseInt(h.slice(2, 4), 16),
3967
+ parseInt(h.slice(4, 6), 16),
3968
+ h.length === 8 ? parseInt(h.slice(6, 8), 16) : 255
3969
+ ];
3970
+ }
3971
+ function mixHexColors(a, b, p) {
3972
+ const ca = parseHex(a);
3973
+ const cb = parseHex(b);
3974
+ if (!ca || !cb) return null;
3975
+ const t = clamp(p, 0, 1);
3976
+ const mixed = ca.map((v, i) => Math.round(v + (cb[i] - v) * t));
3977
+ const hex = (n) => n.toString(16).padStart(2, "0");
3978
+ const rgb = `#${hex(mixed[0])}${hex(mixed[1])}${hex(mixed[2])}`;
3979
+ return mixed[3] === 255 ? rgb : `${rgb}${hex(mixed[3])}`;
3980
+ }
3981
+ function transitionToCss(transition) {
3982
+ if (!transition) return void 0;
3983
+ if (transition.type === "easing") {
3984
+ return {
3985
+ transitionDuration: `${round2(transition.duration)}s`,
3986
+ transitionTimingFunction: `cubic-bezier(${transition.ease.map((v) => round2(v)).join(", ")})`
3987
+ };
3988
+ }
3989
+ const params = springParams(transition);
3990
+ const dampingRatio = params.damping / (2 * Math.sqrt(params.stiffness * params.mass));
3991
+ const duration = transition.visualDuration ?? springSettleDuration(params);
3992
+ const bounce = transition.bounce ?? Math.max(0, round2(1 - dampingRatio));
3993
+ return {
3994
+ transitionDuration: `${round2(duration)}s`,
3995
+ transitionTimingFunction: bounce > 0.05 ? `cubic-bezier(0.34, ${round2(1.2 + bounce)}, 0.64, 1)` : "cubic-bezier(0.25, 0.6, 0.35, 1)"
3996
+ };
3997
+ }
3998
+ function timelinePopoverDisplayValues(values, clipKey, stepKeys, stepKey) {
3999
+ const display = { ...values };
4000
+ const swap = (path, duration) => {
4001
+ const raw = display[path];
4002
+ if (isTransitionConfig(raw)) display[path] = resolveClipTransition(raw, duration).transition;
4003
+ };
4004
+ if (stepKey) {
4005
+ swap(`${clipKey}.${stepKey}.transition`, numberValue(values[`${clipKey}.${stepKey}.duration`]));
4006
+ return display;
4007
+ }
4008
+ const cycle = stepKeys?.length ? stepKeys.reduce((sum, sk) => sum + numberValue(values[`${clipKey}.${sk}.duration`]), 0) : numberValue(values[`${clipKey}.duration`]);
4009
+ swap(`${clipKey}.transition`, cycle);
4010
+ return display;
4011
+ }
4012
+ function numberValue(value) {
4013
+ return typeof value === "number" ? value : 0;
4014
+ }
4015
+ function unflattenClipValues(values, clipKey) {
4016
+ const prefix = `${clipKey}.`;
4017
+ const result = {};
4018
+ const entries = Object.entries(values).filter(([path]) => path.startsWith(prefix)).map(([path, value]) => ({ segments: path.slice(prefix.length).split("."), value })).sort((a, b) => a.segments.length - b.segments.length);
4019
+ for (const { segments, value } of entries) {
4020
+ let node = result;
4021
+ for (let i = 0; i < segments.length - 1; i++) {
4022
+ const existing = node[segments[i]];
4023
+ node = isPlainObject(existing) ? existing : node[segments[i]] = {};
4024
+ }
4025
+ node[segments[segments.length - 1]] = cloneTimelineValue(value);
4026
+ }
4027
+ return result;
4028
+ }
4029
+ function cloneTimelineValue(value) {
4030
+ if (Array.isArray(value)) return value.map(cloneTimelineValue);
4031
+ if (!isPlainObject(value)) return value;
4032
+ return Object.fromEntries(
4033
+ Object.entries(value).map(([key, nested]) => [key, cloneTimelineValue(nested)])
4034
+ );
4035
+ }
4036
+ function clampTrackDelay(delay, at, trackDuration, timelineDuration) {
4037
+ return clamp(round2(delay), 0, Math.max(0, round2(timelineDuration - at - trackDuration)));
4038
+ }
4039
+ function clampClipMove(at, duration, timelineDuration) {
4040
+ return clamp(round2(at), 0, Math.max(0, timelineDuration - duration));
4041
+ }
4042
+ function clampClipResizeEnd(duration, at, timelineDuration) {
4043
+ return clamp(round2(duration), TIMELINE_MIN_CLIP_DURATION, timelineDuration - at);
4044
+ }
4045
+ function clampClipResizeStart(newAt, at, duration) {
4046
+ const clampedAt = clamp(round2(newAt), 0, at + duration - TIMELINE_MIN_CLIP_DURATION);
4047
+ return { at: clampedAt, duration: round2(at + duration - clampedAt) };
4048
+ }
4049
+ function clampStepResize(duration, at, otherStepsTotal, timelineDuration) {
4050
+ const max = Math.max(TIMELINE_MIN_CLIP_DURATION, timelineDuration - at - otherStepsTotal);
4051
+ return clamp(round2(duration), TIMELINE_MIN_CLIP_DURATION, max);
4052
+ }
4053
+ function normalizeTimelineValuesForCopy(values, clips) {
4054
+ const normalized = { ...values };
4055
+ for (const path of Object.keys(normalized)) {
4056
+ if (path.endsWith(".__mode")) delete normalized[path];
4057
+ }
4058
+ const normalizeTransitionAt = (transitionPath, durationPath) => {
4059
+ const raw = normalized[transitionPath];
4060
+ if (!isTransitionConfig(raw)) return;
4061
+ if (isPhysicsSpring(raw)) {
4062
+ normalized[durationPath] = transitionDefaultDuration(raw);
4063
+ }
4064
+ normalized[transitionPath] = normalizeStoredTransition(
4065
+ raw,
4066
+ numberValue(normalized[durationPath])
4067
+ );
4068
+ };
4069
+ for (const clip of clips) {
4070
+ for (const stepKey of clip.stepKeys ?? []) {
4071
+ normalizeTransitionAt(
4072
+ `${clip.key}.${stepKey}.transition`,
4073
+ `${clip.key}.${stepKey}.duration`
4074
+ );
4075
+ }
4076
+ normalizeTransitionAt(`${clip.key}.transition`, `${clip.key}.duration`);
4077
+ for (const track of clip.tracks ?? []) {
4078
+ const trackKey = `${clip.key}.${track.prop}`;
4079
+ for (const stepKey of track.stepKeys ?? []) {
4080
+ normalizeTransitionAt(
4081
+ `${trackKey}.${stepKey}.transition`,
4082
+ `${trackKey}.${stepKey}.duration`
4083
+ );
4084
+ }
4085
+ normalizeTransitionAt(`${trackKey}.transition`, `${trackKey}.duration`);
4086
+ if (normalized[`${trackKey}.delay`] === 0) {
4087
+ delete normalized[`${trackKey}.delay`];
4088
+ }
4089
+ }
4090
+ delete normalized[`${clip.key}.loop`];
4091
+ }
4092
+ return normalized;
4093
+ }
4094
+ function formatClock(time, tenths = false) {
4095
+ const safe = Math.max(0, time);
4096
+ const minutes = Math.floor(safe / 60);
4097
+ const seconds = safe - minutes * 60;
4098
+ const secondsText = tenths ? seconds.toFixed(1).padStart(4, "0") : String(Math.floor(seconds)).padStart(2, "0");
4099
+ return `${String(minutes).padStart(2, "0")}:${secondsText}`;
4100
+ }
4101
+ function formatSeconds(value) {
4102
+ return `${round2(value)}s`;
4103
+ }
4104
+ function formatStepLabel(stepKey) {
4105
+ const match = /^step(\d+)$/.exec(stepKey);
4106
+ return match ? `Step ${match[1]}` : formatLabel(stepKey);
4107
+ }
4108
+
4109
+ // src/timeline/adapter.ts
4110
+ function resolveTimelineLoop(loop) {
4111
+ if (typeof loop === "object" && loop !== null) {
4112
+ return {
4113
+ enabled: true,
4114
+ start: Number.isFinite(loop.from) ? Math.max(0, loop.from) : 0
4115
+ };
4116
+ }
4117
+ return { enabled: Boolean(loop), start: 0 };
4118
+ }
4119
+ function buildTimelineMeta(id, name, duration, parsed, loop) {
4120
+ const resolvedLoop = resolveTimelineLoop(loop);
4121
+ return {
4122
+ id,
4123
+ name,
4124
+ duration,
4125
+ loop: resolvedLoop.enabled,
4126
+ loopStart: resolvedLoop.start,
4127
+ clips: parsed.clips
4128
+ };
4129
+ }
4130
+ function buildTimelineValues(staticClips, transport, timelineDuration, loopStart, actions) {
4131
+ var _a;
4132
+ const result = {
4133
+ time: transport.time,
4134
+ playing: transport.playing,
4135
+ duration: timelineDuration,
4136
+ ...actions
4137
+ };
4138
+ const span = loopSpan(transport.duration, loopStart);
4139
+ const cycleTime = (span > 0 ? transport.wraps * span : 0) + transport.time;
4140
+ for (const clip of staticClips) {
4141
+ const state = computeClipState(clip, transport.time, cycleTime);
4142
+ if (clip.group) {
4143
+ const bucket = result[_a = clip.group] ?? (result[_a] = {});
4144
+ bucket[clip.childKey] = state;
4145
+ } else {
4146
+ result[clip.key] = state;
4147
+ }
4148
+ }
4149
+ return result;
4150
+ }
4151
+
4152
+ // src/hooks/useDialTimeline.ts
4153
+ function useDialTimeline(name, config, options) {
4154
+ const serializedConfig = useSerialized(config);
4155
+ const parsed = useMemo2(() => parseTimelineConfig(config), [serializedConfig]);
4156
+ const { panelId, flatValues } = useDialStorePanel(name, parsed.dialConfig, {
4157
+ id: options?.id,
4158
+ persist: options?.persist,
4159
+ kind: "timeline"
4160
+ });
4161
+ const staticTimeline = useMemo2(
4162
+ () => computeStaticTimeline(parsed, flatValues),
4163
+ [parsed, flatValues]
4164
+ );
4165
+ const timelineDuration = staticTimeline.duration;
4166
+ const staticClips = staticTimeline.clips;
4167
+ const parsedRef = useRef13(parsed);
4168
+ parsedRef.current = parsed;
4169
+ const optionsRef = useRef13(options);
4170
+ optionsRef.current = options;
4171
+ const { start: loopStart } = resolveTimelineLoop(options?.loop);
4172
+ const buildMeta = useCallback13(
4173
+ () => buildTimelineMeta(panelId, name, timelineDuration, parsedRef.current, options?.loop),
4174
+ [panelId, name, timelineDuration, options?.loop]
4175
+ );
4176
+ const buildMetaRef = useRef13(buildMeta);
4177
+ buildMetaRef.current = buildMeta;
4178
+ useEffect10(() => {
4179
+ TimelineStore.register(buildMetaRef.current(), { autoplay: optionsRef.current?.autoplay ?? true });
4180
+ return () => TimelineStore.unregister(panelId);
4181
+ }, [panelId, name]);
4182
+ const mountedRef = useRef13(false);
4183
+ useEffect10(() => {
4184
+ if (!mountedRef.current) {
4185
+ mountedRef.current = true;
4186
+ return;
4187
+ }
4188
+ TimelineStore.update(buildMeta());
4189
+ }, [buildMeta, parsed]);
4190
+ const subscribeTransport = useCallback13(
4191
+ (callback) => TimelineStore.subscribe(panelId, callback),
4192
+ [panelId]
4193
+ );
4194
+ const getTransport = useCallback13(() => TimelineStore.getTransport(panelId), [panelId]);
4195
+ const transport = useSyncExternalStore6(subscribeTransport, getTransport, getTransport);
4196
+ const play = useCallback13(() => TimelineStore.play(panelId), [panelId]);
4197
+ const pause = useCallback13(() => TimelineStore.pause(panelId), [panelId]);
4198
+ const replay = useCallback13(() => TimelineStore.replay(panelId), [panelId]);
4199
+ const seek = useCallback13((time) => TimelineStore.seek(panelId, time), [panelId]);
4200
+ return useMemo2(
4201
+ () => buildTimelineValues(staticClips, transport, timelineDuration, loopStart, {
4202
+ play,
4203
+ pause,
4204
+ replay,
4205
+ seek
4206
+ }),
4207
+ [staticClips, transport, timelineDuration, loopStart, play, pause, replay, seek]
4208
+ );
4209
+ }
4210
+
4211
+ // src/components/Timeline/DialTimeline.tsx
4212
+ import { memo, useCallback as useCallback14, useEffect as useEffect11, useLayoutEffect as useLayoutEffect2, useRef as useRef14, useState as useState11, useSyncExternalStore as useSyncExternalStore7 } from "react";
4213
+ import { createPortal as createPortal4 } from "react-dom";
4214
+ import { AnimatePresence as AnimatePresence5, motion as motion7 } from "motion/react";
4215
+ import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
4216
+ var DRAG_THRESHOLD_PX = 3;
4217
+ var MAJOR_TICK_TARGET_PX = 140;
4218
+ var MILLISECOND_STEP = 1e-3;
4219
+ var SECOND_TICK_STEPS = [
4220
+ 1e-3,
4221
+ 2e-3,
4222
+ 5e-3,
4223
+ 0.01,
4224
+ 0.02,
4225
+ 0.05,
4226
+ 0.1,
4227
+ 0.2,
4228
+ 0.5,
4229
+ 1,
4230
+ 2,
4231
+ 5,
4232
+ 10,
4233
+ 15,
4234
+ 30,
4235
+ 60,
4236
+ 120,
4237
+ 300,
4238
+ 600
4239
+ ];
4240
+ var MIN_TIMELINE_MAX_ZOOM = 8;
4241
+ var PLAYHEAD_FLAG_WIDTH = 52;
4242
+ var PLAYHEAD_FLAG_EDGE_OVERHANG = 1;
4243
+ var POPOVER_WIDTH = 280;
4244
+ var ZOOM_DRAG_DISTANCE = 180;
4245
+ var DEFAULT_DOCK_MAX_HEIGHT = 400;
4246
+ var MIN_DOCK_MAX_HEIGHT = 120;
4247
+ var subscribeGlobalTimelines = (callback) => TimelineStore.subscribeGlobal(callback);
4248
+ var getTimelines = () => TimelineStore.getTimelines();
4249
+ var subscribeTimelineVisibility = (callback) => TimelineUiStore.subscribe(callback);
4250
+ var getTimelineVisibility = () => TimelineUiStore.getVisible();
4251
+ var DialTimeline = memo(function DialTimeline2({
4252
+ theme = "system",
4253
+ defaultVisible = true,
4254
+ visible,
4255
+ onVisibilityChange,
4256
+ defaultOpen = true,
4257
+ productionEnabled = isDevDefault
4258
+ }) {
4259
+ if (!productionEnabled) return null;
4260
+ return /* @__PURE__ */ jsx18(
4261
+ DialTimelineDock,
4262
+ {
4263
+ theme,
4264
+ defaultVisible,
4265
+ visible,
4266
+ onVisibilityChange,
4267
+ defaultOpen
4268
+ }
4269
+ );
4270
+ });
4271
+ function DialTimelineDock({
4272
+ theme,
4273
+ defaultVisible,
4274
+ visible,
4275
+ onVisibilityChange,
4276
+ defaultOpen
4277
+ }) {
4278
+ const [mounted, setMounted] = useState11(false);
4279
+ const [dockMaxHeight, setDockMaxHeight] = useState11(DEFAULT_DOCK_MAX_HEIGHT);
4280
+ const visibilityControllerId = useRef14(/* @__PURE__ */ Symbol("dialkit-timeline-visibility"));
4281
+ const dockRef = useRef14(null);
4282
+ const resizeCleanupRef = useRef14(null);
4283
+ useEffect11(() => TimelineUiStore.registerController(visibilityControllerId.current, {
4284
+ visible,
4285
+ defaultVisible,
4286
+ onVisibilityChange
4287
+ }), []);
4288
+ useEffect11(() => {
4289
+ TimelineUiStore.updateController(visibilityControllerId.current, {
4290
+ visible,
4291
+ defaultVisible,
4292
+ onVisibilityChange
4293
+ });
4294
+ }, [defaultVisible, onVisibilityChange, visible]);
4295
+ useEffect11(() => {
4296
+ setMounted(true);
4297
+ }, []);
4298
+ useEffect11(() => () => resizeCleanupRef.current?.(), []);
4299
+ const handleResizePointerDown = useCallback14((e) => {
4300
+ const dock = dockRef.current;
4301
+ if (!dock) return;
4302
+ e.preventDefault();
4303
+ e.stopPropagation();
4304
+ resizeCleanupRef.current?.();
4305
+ const pointerY = e.clientY;
4306
+ const startHeight = dock.getBoundingClientRect().height;
4307
+ const handlePointerMove = (event) => {
4308
+ event.preventDefault();
4309
+ const viewportMax = Math.max(MIN_DOCK_MAX_HEIGHT, window.innerHeight - 24);
4310
+ setDockMaxHeight(clamp(startHeight + pointerY - event.clientY, MIN_DOCK_MAX_HEIGHT, viewportMax));
4311
+ };
4312
+ const finishResize = () => {
4313
+ window.removeEventListener("pointermove", handlePointerMove);
4314
+ window.removeEventListener("pointerup", finishResize);
4315
+ window.removeEventListener("pointercancel", finishResize);
4316
+ resizeCleanupRef.current = null;
4317
+ };
4318
+ window.addEventListener("pointermove", handlePointerMove, { passive: false });
4319
+ window.addEventListener("pointerup", finishResize);
4320
+ window.addEventListener("pointercancel", finishResize);
4321
+ resizeCleanupRef.current = finishResize;
4322
+ }, []);
4323
+ const timelines = useSyncExternalStore7(subscribeGlobalTimelines, getTimelines, getTimelines);
4324
+ const dockVisible = useSyncExternalStore7(
4325
+ subscribeTimelineVisibility,
4326
+ getTimelineVisibility,
4327
+ getTimelineVisibility
4328
+ );
4329
+ if (!mounted || typeof window === "undefined" || timelines.length === 0) {
4330
+ return null;
4331
+ }
4332
+ return createPortal4(
4333
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-root dialkit-timeline", "data-theme": theme, hidden: !dockVisible, children: [
4334
+ /* @__PURE__ */ jsx18(
4335
+ "div",
4336
+ {
4337
+ className: "dialkit-timeline-resize-handle",
4338
+ onPointerDown: handleResizePointerDown,
4339
+ role: "separator",
4340
+ "aria-label": "Resize timeline height",
4341
+ "aria-orientation": "horizontal",
4342
+ title: "Drag to resize timeline"
4343
+ }
4344
+ ),
4345
+ /* @__PURE__ */ jsx18(
4346
+ "div",
4347
+ {
4348
+ ref: dockRef,
4349
+ className: "dialkit-timeline-dock",
4350
+ style: { maxHeight: `min(${dockMaxHeight}px, calc(100vh - 24px))` },
4351
+ children: timelines.map((timeline) => /* @__PURE__ */ jsx18(
4352
+ TimelineSection,
4353
+ {
4354
+ meta: timeline,
4355
+ defaultOpen,
4356
+ theme,
4357
+ dockVisible
4358
+ },
4359
+ timeline.id
4360
+ ))
4361
+ }
4362
+ )
4363
+ ] }),
4364
+ document.body
4365
+ );
4366
+ }
4367
+ function useTransportSubscribe(id) {
4368
+ return useCallback14((callback) => TimelineStore.subscribe(id, callback), [id]);
4369
+ }
4370
+ function PlayPauseButton({ id }) {
4371
+ const subscribe = useTransportSubscribe(id);
4372
+ const getPlaying = useCallback14(() => TimelineStore.getTransport(id).playing, [id]);
4373
+ const playing = useSyncExternalStore7(subscribe, getPlaying, getPlaying);
4374
+ return /* @__PURE__ */ jsx18(
4375
+ motion7.button,
4376
+ {
4377
+ className: "dialkit-toolbar-add",
4378
+ onClick: () => playing ? TimelineStore.pause(id) : TimelineStore.play(id),
4379
+ title: playing ? "Pause" : "Play",
4380
+ "aria-label": playing ? "Pause" : "Play",
4381
+ whileTap: { scale: 0.9 },
4382
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
4383
+ children: /* @__PURE__ */ jsx18("span", { style: { position: "relative", width: 16, height: 16 }, children: /* @__PURE__ */ jsx18(AnimatePresence5, { initial: false, mode: "wait", children: playing ? /* @__PURE__ */ jsx18(
4384
+ motion7.svg,
4385
+ {
4386
+ viewBox: "0 0 24 24",
4387
+ fill: "none",
4388
+ "aria-hidden": "true",
4389
+ style: { position: "absolute", inset: 0, width: 16, height: 16, color: "var(--dial-text-label)" },
4390
+ initial: { scale: 0.8, opacity: 0 },
4391
+ animate: { scale: 1, opacity: 1 },
4392
+ exit: { scale: 0.8, opacity: 0 },
4393
+ transition: { duration: 0.08 },
4394
+ children: ICON_PAUSE.map((d, i) => /* @__PURE__ */ jsx18("path", { d, fill: "currentColor" }, i))
4395
+ },
4396
+ "pause"
4397
+ ) : /* @__PURE__ */ jsx18(
4398
+ motion7.svg,
4399
+ {
4400
+ viewBox: "0 0 24 24",
4401
+ fill: "none",
4402
+ "aria-hidden": "true",
4403
+ style: { position: "absolute", inset: 0, width: 16, height: 16, color: "var(--dial-text-label)" },
4404
+ initial: { scale: 0.8, opacity: 0 },
4405
+ animate: { scale: 1, opacity: 1 },
4406
+ exit: { scale: 0.8, opacity: 0 },
4407
+ transition: { duration: 0.08 },
4408
+ children: /* @__PURE__ */ jsx18("path", { d: ICON_PLAY, fill: "currentColor" })
4409
+ },
4410
+ "play"
4411
+ ) }) })
4412
+ }
4413
+ );
4414
+ }
4415
+ function ReplayButton({ onReplay }) {
4416
+ return /* @__PURE__ */ jsx18(
4417
+ motion7.button,
4418
+ {
4419
+ className: "dialkit-toolbar-add",
4420
+ onClick: onReplay,
4421
+ title: "Replay",
4422
+ "aria-label": "Replay",
4423
+ whileTap: { scale: 0.9 },
4424
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
4425
+ children: /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: ICON_REPLAY.map((d, i) => /* @__PURE__ */ jsx18("path", { d, fill: "currentColor" }, i)) })
4426
+ }
4427
+ );
4428
+ }
4429
+ function TimelinePlayheadFlag({
4430
+ id,
4431
+ duration,
4432
+ pxPerSecond,
4433
+ viewStart,
4434
+ viewEnd,
4435
+ laneWidth,
4436
+ rulerRef,
4437
+ onResetView
4438
+ }) {
4439
+ const subscribe = useTransportSubscribe(id);
4440
+ const getTime = useCallback14(() => TimelineStore.getTransport(id).time, [id]);
4441
+ const time = useSyncExternalStore7(subscribe, getTime, getTime);
4442
+ const scrubRef = useRef14(null);
4443
+ const cleanupScrubRef = useRef14(null);
4444
+ const seekFromClientX = useCallback14((clientX) => {
4445
+ const rect = scrubRef.current?.rect;
4446
+ const scrub = scrubRef.current;
4447
+ const contentWidth = rect?.width ?? 0;
4448
+ if (!rect || !scrub || contentWidth <= 0) return;
4449
+ const nextTime = clamp(
4450
+ scrub.viewStart + (clientX - rect.left) / contentWidth * (scrub.viewEnd - scrub.viewStart),
4451
+ scrub.viewStart,
4452
+ scrub.viewEnd
4453
+ );
4454
+ TimelineStore.seek(id, nextTime);
4455
+ }, [id]);
4456
+ const handlePointerDown = useCallback14((e) => {
4457
+ const rect = rulerRef.current?.getBoundingClientRect();
4458
+ if (!rect) return;
4459
+ e.preventDefault();
4460
+ e.stopPropagation();
4461
+ cleanupScrubRef.current?.();
4462
+ const resetView = e.shiftKey;
4463
+ scrubRef.current = {
4464
+ wasPlaying: TimelineStore.getTransport(id).playing,
4465
+ rect,
4466
+ viewStart: resetView ? 0 : viewStart,
4467
+ viewEnd: resetView ? duration : viewEnd
4468
+ };
4469
+ if (resetView) onResetView();
4470
+ TimelineStore.pause(id);
4471
+ seekFromClientX(e.clientX);
4472
+ const handleWindowPointerMove = (event) => {
4473
+ event.preventDefault();
4474
+ seekFromClientX(event.clientX);
4475
+ };
4476
+ const finishWindowScrub = () => {
4477
+ window.removeEventListener("pointermove", handleWindowPointerMove);
4478
+ window.removeEventListener("pointerup", finishWindowScrub);
4479
+ window.removeEventListener("pointercancel", finishWindowScrub);
4480
+ if (scrubRef.current?.wasPlaying) TimelineStore.play(id);
4481
+ scrubRef.current = null;
4482
+ cleanupScrubRef.current = null;
4483
+ };
4484
+ window.addEventListener("pointermove", handleWindowPointerMove, { passive: false });
4485
+ window.addEventListener("pointerup", finishWindowScrub);
4486
+ window.addEventListener("pointercancel", finishWindowScrub);
4487
+ cleanupScrubRef.current = finishWindowScrub;
4488
+ }, [duration, id, onResetView, rulerRef, seekFromClientX, viewEnd, viewStart]);
4489
+ useEffect11(() => () => cleanupScrubRef.current?.(), []);
4490
+ if (time < viewStart || time > viewEnd || laneWidth <= 0) return null;
4491
+ const x = clamp(
4492
+ (time - viewStart) * pxPerSecond,
4493
+ 0,
4494
+ laneWidth
4495
+ );
4496
+ const flagCenter = clamp(
4497
+ x,
4498
+ PLAYHEAD_FLAG_WIDTH / 2 - PLAYHEAD_FLAG_EDGE_OVERHANG,
4499
+ laneWidth - PLAYHEAD_FLAG_WIDTH / 2 + PLAYHEAD_FLAG_EDGE_OVERHANG
4500
+ );
4501
+ const flagOffset = flagCenter - x;
4502
+ const edge = flagOffset > 0.5 ? "start" : flagOffset < -0.5 ? "end" : "center";
4503
+ return /* @__PURE__ */ jsxs14(
4504
+ "div",
4505
+ {
4506
+ className: "dialkit-timeline-playhead-control",
4507
+ "data-edge": edge,
4508
+ style: {
4509
+ left: `calc(var(--dial-timeline-label-w) + ${x}px)`,
4510
+ "--dial-timeline-playhead-flag-offset": `${flagOffset}px`
4511
+ },
4512
+ onPointerDown: handlePointerDown,
4513
+ role: "slider",
4514
+ "aria-label": "Timeline current time",
4515
+ "aria-valuemin": 0,
4516
+ "aria-valuemax": duration,
4517
+ "aria-valuenow": time,
4518
+ title: "Drag to scrub the timeline",
4519
+ children: [
4520
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-playhead-stem" }),
4521
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-playhead-anchor", children: /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-playhead-flag", children: time.toFixed(2) }) })
4522
+ ]
4523
+ }
4524
+ );
4525
+ }
4526
+ function TimelineOverview({
4527
+ id,
4528
+ duration,
4529
+ viewStart,
4530
+ viewEnd,
4531
+ onNavigate
4532
+ }) {
4533
+ const subscribe = useTransportSubscribe(id);
4534
+ const getTime = useCallback14(() => TimelineStore.getTransport(id).time, [id]);
4535
+ const time = useSyncExternalStore7(subscribe, getTime, getTime);
4536
+ const scrubRef = useRef14(null);
4537
+ const seekFromClientX = useCallback14((clientX) => {
4538
+ const rect = scrubRef.current?.rect;
4539
+ if (!rect || rect.width <= 0 || duration <= 0) return;
4540
+ const nextTime = clamp((clientX - rect.left) / rect.width * duration, 0, duration);
4541
+ TimelineStore.seek(id, nextTime);
4542
+ onNavigate(nextTime);
4543
+ }, [duration, id, onNavigate]);
4544
+ const handlePointerDown = useCallback14((e) => {
4545
+ e.preventDefault();
4546
+ e.currentTarget.setPointerCapture(e.pointerId);
4547
+ scrubRef.current = {
4548
+ wasPlaying: TimelineStore.getTransport(id).playing,
4549
+ rect: e.currentTarget.getBoundingClientRect()
4550
+ };
4551
+ TimelineStore.pause(id);
4552
+ seekFromClientX(e.clientX);
4553
+ }, [id, seekFromClientX]);
4554
+ const handlePointerMove = useCallback14((e) => {
4555
+ if (scrubRef.current) seekFromClientX(e.clientX);
4556
+ }, [seekFromClientX]);
4557
+ const finishScrub = useCallback14(() => {
4558
+ if (scrubRef.current?.wasPlaying) TimelineStore.play(id);
4559
+ scrubRef.current = null;
4560
+ }, [id]);
4561
+ const viewportLeft = duration > 0 ? viewStart / duration * 100 : 0;
4562
+ const viewportWidth = duration > 0 ? (viewEnd - viewStart) / duration * 100 : 100;
4563
+ const playheadLeft = duration > 0 ? time / duration * 100 : 0;
4564
+ return /* @__PURE__ */ jsxs14(
4565
+ "div",
4566
+ {
4567
+ className: "dialkit-timeline-overview",
4568
+ onPointerDown: handlePointerDown,
4569
+ onPointerMove: handlePointerMove,
4570
+ onPointerUp: finishScrub,
4571
+ onPointerCancel: finishScrub,
4572
+ onLostPointerCapture: finishScrub,
4573
+ title: "Drag to scrub the full timeline",
4574
+ children: [
4575
+ /* @__PURE__ */ jsx18(
4576
+ "div",
4577
+ {
4578
+ className: "dialkit-timeline-overview-viewport",
4579
+ "data-zoomed": viewportWidth < 99.999 || void 0,
4580
+ style: { left: `${viewportLeft}%`, width: `${viewportWidth}%` }
4581
+ }
4582
+ ),
4583
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-overview-progress", style: { width: `${playheadLeft}%` } }),
4584
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-overview-playhead", style: { left: `${playheadLeft}%` } })
4585
+ ]
4586
+ }
4587
+ );
4588
+ }
4589
+ function clampViewStart(start, duration, visibleDuration) {
4590
+ return clamp(start, 0, Math.max(0, duration - visibleDuration));
4591
+ }
4592
+ function formatRulerSeconds(time, step) {
4593
+ if (step >= 1 && Number.isInteger(time)) return formatClock(time);
4594
+ const decimals = Math.min(3, Math.max(1, Math.ceil(-Math.log10(step))));
4595
+ return `${time.toFixed(decimals)}s`;
4596
+ }
4597
+ var TimelineSection = memo(function TimelineSection2({
4598
+ meta,
4599
+ defaultOpen,
4600
+ theme,
4601
+ dockVisible
4602
+ }) {
4603
+ const [open, setOpen] = useState11(defaultOpen);
4604
+ const [copied, setCopied] = useState11(false);
4605
+ const [popover, setPopover] = useState11(null);
4606
+ const [collapsedGroups, setCollapsedGroups] = useState11(() => /* @__PURE__ */ new Set());
4607
+ const [expandedTracks, setExpandedTracks] = useState11(() => /* @__PURE__ */ new Set());
4608
+ const [zoom, setZoom] = useState11(1);
4609
+ const [viewStart, setViewStart] = useState11(0);
4610
+ const subscribeValues = useCallback14(
4611
+ (callback) => DialStore.subscribe(meta.id, callback),
4612
+ [meta.id]
4613
+ );
4614
+ const getValues = useCallback14(() => DialStore.getValues(meta.id), [meta.id]);
4615
+ const values = useSyncExternalStore7(subscribeValues, getValues, getValues);
4616
+ const presets = DialStore.getPresets(meta.id);
4617
+ const activePresetId = DialStore.getActivePresetId(meta.id);
4618
+ const laneAreaRef = useRef14(null);
4619
+ const horizontalScrollRef = useRef14(null);
4620
+ const [laneWidth, setLaneWidth] = useState11(0);
4621
+ useLayoutEffect2(() => {
4622
+ if (!open) return;
4623
+ const ruler = laneAreaRef.current;
4624
+ if (!ruler) return;
4625
+ const measure = () => {
4626
+ setLaneWidth(ruler.getBoundingClientRect().width);
4627
+ };
4628
+ measure();
4629
+ const observer = new ResizeObserver(measure);
4630
+ observer.observe(ruler);
4631
+ return () => observer.disconnect();
4632
+ }, [open]);
4633
+ const visibleDuration = meta.duration > 0 ? meta.duration / zoom : meta.duration;
4634
+ const safeViewStart = clampViewStart(viewStart, meta.duration, visibleDuration);
4635
+ const viewEnd = safeViewStart + visibleDuration;
4636
+ const pxPerSecond = visibleDuration > 0 && laneWidth > 0 ? laneWidth / visibleDuration : 0;
4637
+ const millisecondReadableZoom = laneWidth > 0 && meta.duration > 0 ? MAJOR_TICK_TARGET_PX * meta.duration / (MILLISECOND_STEP * 10 * laneWidth) : MIN_TIMELINE_MAX_ZOOM;
4638
+ const maxZoom = Math.max(MIN_TIMELINE_MAX_ZOOM, millisecondReadableZoom);
4639
+ useEffect11(() => {
4640
+ setZoom((current) => clamp(current, 1, maxZoom));
4641
+ }, [maxZoom]);
4642
+ useEffect11(() => {
4643
+ setViewStart((current) => clampViewStart(current, meta.duration, meta.duration / zoom));
4644
+ }, [meta.duration, zoom]);
4645
+ useLayoutEffect2(() => {
4646
+ const scroller = horizontalScrollRef.current;
4647
+ if (!scroller || pxPerSecond <= 0) return;
4648
+ const nextScrollLeft = safeViewStart * pxPerSecond;
4649
+ if (Math.abs(scroller.scrollLeft - nextScrollLeft) > 0.5) {
4650
+ scroller.scrollLeft = nextScrollLeft;
4651
+ }
4652
+ }, [open, pxPerSecond, safeViewStart]);
4653
+ useEffect11(() => {
4654
+ if (!dockVisible) setPopover(null);
4655
+ }, [dockVisible]);
4656
+ const centerViewAt = useCallback14((time) => {
4657
+ if (zoom <= 1 || meta.duration <= 0) return;
4658
+ const windowDuration = meta.duration / zoom;
4659
+ setViewStart(clampViewStart(time - windowDuration / 2, meta.duration, windowDuration));
4660
+ }, [meta.duration, zoom]);
4661
+ const resetView = useCallback14(() => {
4662
+ setZoom(1);
4663
+ setViewStart(0);
4664
+ }, []);
4665
+ const handleReplay = useCallback14(() => {
4666
+ setViewStart(0);
4667
+ TimelineStore.replay(meta.id);
4668
+ }, [meta.id]);
4669
+ const handleHorizontalScroll = useCallback14((e) => {
4670
+ if (pxPerSecond <= 0) return;
4671
+ setViewStart(clampViewStart(
4672
+ e.currentTarget.scrollLeft / pxPerSecond,
4673
+ meta.duration,
4674
+ visibleDuration
4675
+ ));
4676
+ }, [meta.duration, pxPerSecond, visibleDuration]);
4677
+ const handleTimelineWheel = useCallback14((e) => {
4678
+ const scroller = horizontalScrollRef.current;
4679
+ if (!scroller || zoom <= 1) return;
4680
+ const horizontalDelta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.shiftKey ? e.deltaY : 0;
4681
+ if (horizontalDelta === 0) return;
4682
+ e.preventDefault();
4683
+ scroller.scrollLeft += horizontalDelta;
4684
+ }, [zoom]);
4685
+ const zoomDragRef = useRef14(null);
4686
+ const rulerScrubRef = useRef14(null);
4687
+ const seekRulerFromClientX = useCallback14((clientX) => {
4688
+ const scrub = rulerScrubRef.current;
4689
+ const contentWidth = scrub?.rect.width ?? 0;
4690
+ if (!scrub || contentWidth <= 0) return;
4691
+ TimelineStore.seek(
4692
+ meta.id,
4693
+ clamp(
4694
+ scrub.viewStart + (clientX - scrub.rect.left) / contentWidth * scrub.visibleDuration,
4695
+ scrub.viewStart,
4696
+ scrub.viewStart + scrub.visibleDuration
4697
+ )
4698
+ );
4699
+ }, [meta.id]);
4700
+ const handleRulerPointerDown = useCallback14((e) => {
4701
+ e.preventDefault();
4702
+ e.stopPropagation();
4703
+ const rect = e.currentTarget.getBoundingClientRect();
4704
+ const contentWidth = rect.width;
4705
+ if (contentWidth <= 0) return;
4706
+ e.currentTarget.setPointerCapture(e.pointerId);
4707
+ if (!e.altKey) {
4708
+ const resetView2 = e.shiftKey;
4709
+ rulerScrubRef.current = {
4710
+ wasPlaying: TimelineStore.getTransport(meta.id).playing,
4711
+ rect,
4712
+ viewStart: resetView2 ? 0 : safeViewStart,
4713
+ visibleDuration: resetView2 ? meta.duration : visibleDuration
4714
+ };
4715
+ if (resetView2) {
4716
+ setZoom(1);
4717
+ setViewStart(0);
4718
+ }
4719
+ TimelineStore.pause(meta.id);
4720
+ seekRulerFromClientX(e.clientX);
4721
+ return;
4722
+ }
4723
+ const anchorRatio = clamp((e.clientX - rect.left) / contentWidth, 0, 1);
4724
+ zoomDragRef.current = {
4725
+ pointerX: e.clientX,
4726
+ rect,
4727
+ zoom,
4728
+ viewStart: safeViewStart,
4729
+ anchorRatio,
4730
+ anchorTime: safeViewStart + anchorRatio * visibleDuration,
4731
+ moved: false
4732
+ };
4733
+ }, [meta.duration, meta.id, safeViewStart, seekRulerFromClientX, visibleDuration, zoom]);
4734
+ const handleRulerPointerMove = useCallback14((e) => {
4735
+ if (rulerScrubRef.current) {
4736
+ seekRulerFromClientX(e.clientX);
4737
+ return;
4738
+ }
4739
+ const drag = zoomDragRef.current;
4740
+ if (!drag || meta.duration <= 0) return;
4741
+ const dx = e.clientX - drag.pointerX;
4742
+ if (!drag.moved && Math.abs(dx) <= DRAG_THRESHOLD_PX) return;
4743
+ drag.moved = true;
4744
+ const nextZoom = clamp(drag.zoom * Math.exp(dx / ZOOM_DRAG_DISTANCE), 1, maxZoom);
4745
+ const nextVisibleDuration = meta.duration / nextZoom;
4746
+ const nextStart = clampViewStart(
4747
+ drag.anchorTime - drag.anchorRatio * nextVisibleDuration,
4748
+ meta.duration,
4749
+ nextVisibleDuration
4750
+ );
4751
+ setZoom(nextZoom);
4752
+ setViewStart(nextStart);
4753
+ }, [maxZoom, meta.duration, seekRulerFromClientX]);
4754
+ const handleRulerPointerUp = useCallback14(() => {
4755
+ if (rulerScrubRef.current?.wasPlaying) TimelineStore.play(meta.id);
4756
+ rulerScrubRef.current = null;
4757
+ zoomDragRef.current = null;
4758
+ }, [meta.id]);
4759
+ const handleRulerPointerCancel = useCallback14(() => {
4760
+ if (rulerScrubRef.current?.wasPlaying) TimelineStore.play(meta.id);
4761
+ rulerScrubRef.current = null;
4762
+ zoomDragRef.current = null;
4763
+ }, [meta.id]);
4764
+ const trackScrubRef = useRef14(null);
4765
+ const seekTrackFromClientX = useCallback14((clientX) => {
4766
+ const scrub = trackScrubRef.current;
4767
+ const contentWidth = scrub?.rect.width ?? 0;
4768
+ if (!scrub || contentWidth <= 0) return;
4769
+ const nextTime = clamp(
4770
+ scrub.viewStart + (clientX - scrub.rect.left) / contentWidth * scrub.visibleDuration,
4771
+ scrub.viewStart,
4772
+ scrub.viewStart + scrub.visibleDuration
4773
+ );
4774
+ TimelineStore.seek(meta.id, nextTime);
4775
+ }, [meta.id]);
4776
+ const handleTrackPointerDown = useCallback14((e) => {
4777
+ const target = e.target;
4778
+ if (target.closest(".dialkit-timeline-label, button")) return;
4779
+ if (!e.shiftKey && target.closest(".dialkit-timeline-clip")) return;
4780
+ const rect = laneAreaRef.current?.getBoundingClientRect();
4781
+ if (!rect) return;
4782
+ e.preventDefault();
4783
+ e.currentTarget.setPointerCapture(e.pointerId);
4784
+ const resetView2 = e.shiftKey;
4785
+ trackScrubRef.current = {
4786
+ wasPlaying: TimelineStore.getTransport(meta.id).playing,
4787
+ rect,
4788
+ viewStart: resetView2 ? 0 : safeViewStart,
4789
+ visibleDuration: resetView2 ? meta.duration : visibleDuration
4790
+ };
4791
+ if (resetView2) {
4792
+ setZoom(1);
4793
+ setViewStart(0);
4794
+ }
4795
+ setPopover(null);
4796
+ TimelineStore.pause(meta.id);
4797
+ seekTrackFromClientX(e.clientX);
4798
+ }, [meta.duration, meta.id, safeViewStart, seekTrackFromClientX, visibleDuration]);
4799
+ const handleTrackPointerMove = useCallback14((e) => {
4800
+ if (trackScrubRef.current) seekTrackFromClientX(e.clientX);
4801
+ }, [seekTrackFromClientX]);
4802
+ const finishTrackScrub = useCallback14(() => {
4803
+ if (trackScrubRef.current?.wasPlaying) TimelineStore.play(meta.id);
4804
+ trackScrubRef.current = null;
4805
+ }, [meta.id]);
4806
+ const handleCopy = useCallback14(() => {
4807
+ const normalized = normalizeTimelineValuesForCopy(DialStore.getValues(meta.id), meta.clips);
4808
+ navigator.clipboard.writeText(buildCopyInstruction("useDialTimeline", meta.name, normalized));
4809
+ setCopied(true);
4810
+ setTimeout(() => setCopied(false), 1500);
4811
+ }, [meta.clips, meta.id, meta.name]);
4812
+ const handleAddPreset = useCallback14(() => {
4813
+ DialStore.savePreset(meta.id, `Version ${presets.length + 2}`);
4814
+ }, [meta.id, presets.length]);
4815
+ const closePopover = useCallback14(() => setPopover(null), []);
4816
+ const openClipPopover = useCallback14(
4817
+ (clip, rect, stepKey) => {
4818
+ const targetPath = stepKey ? `${clip.key}.${stepKey}` : clip.key;
4819
+ const exclude = stepKey ? void 0 : clipPopoverExclusions(clip);
4820
+ if (getClipControls(meta.id, targetPath, exclude).length === 0) return;
4821
+ setPopover(
4822
+ (prev) => prev?.clip.key === clip.key && prev?.stepKey === stepKey ? null : {
4823
+ clip,
4824
+ stepKey,
4825
+ anchor: {
4826
+ left: rect.left,
4827
+ top: rect.top,
4828
+ right: rect.right,
4829
+ bottom: rect.bottom,
4830
+ width: rect.width,
4831
+ height: rect.height
4832
+ }
4833
+ }
4834
+ );
4835
+ },
4836
+ [meta.id]
4837
+ );
4838
+ const toggleTracks = useCallback14((clipKey) => {
4839
+ setExpandedTracks((prev) => {
4840
+ const next = new Set(prev);
4841
+ if (next.has(clipKey)) next.delete(clipKey);
4842
+ else next.add(clipKey);
4843
+ return next;
4844
+ });
4845
+ }, []);
4846
+ const handleBarClick = useCallback14(
4847
+ (clip, rect, stepKey) => {
4848
+ if (!stepKey && clip.tracks?.length) {
4849
+ toggleTracks(clip.key);
4850
+ return;
4851
+ }
4852
+ openClipPopover(clip, rect, stepKey);
4853
+ },
4854
+ [openClipPopover, toggleTracks]
4855
+ );
4856
+ const toggleGroup = useCallback14((group) => {
4857
+ setCollapsedGroups((prev) => {
4858
+ const next = new Set(prev);
4859
+ if (next.has(group)) next.delete(group);
4860
+ else next.add(group);
4861
+ return next;
4862
+ });
4863
+ }, []);
4864
+ const rawStep = pxPerSecond > 0 ? MAJOR_TICK_TARGET_PX / pxPerSecond : 1;
4865
+ const adaptiveMajorStep = SECOND_TICK_STEPS.find((step) => step >= rawStep) ?? SECOND_TICK_STEPS[SECOND_TICK_STEPS.length - 1];
4866
+ const majorStep = zoom < 1.5 && meta.duration >= 1 ? Math.max(1, adaptiveMajorStep) : adaptiveMajorStep;
4867
+ const fineTickStep = majorStep / 10;
4868
+ const majorTicks = [];
4869
+ const mediumTicks = [];
4870
+ const fineTicks = [];
4871
+ const firstMajorTick = Math.ceil((safeViewStart - 1e-6) / majorStep) * majorStep;
4872
+ for (let t = firstMajorTick; t <= viewEnd + 1e-6; t += majorStep) {
4873
+ majorTicks.push(Number(t.toFixed(4)));
4874
+ }
4875
+ const firstFineIndex = Math.ceil((safeViewStart - 1e-6) / fineTickStep);
4876
+ const lastFineIndex = Math.floor((viewEnd + 1e-6) / fineTickStep);
4877
+ for (let index = firstFineIndex; index <= lastFineIndex; index++) {
4878
+ if (index % 10 === 0) continue;
4879
+ const tick = Number((index * fineTickStep).toFixed(6));
4880
+ if (index % 5 === 0) mediumTicks.push(tick);
4881
+ else fineTicks.push(tick);
4882
+ }
4883
+ const rows = [];
4884
+ let lastGroup;
4885
+ for (const clip of meta.clips) {
4886
+ if (clip.group !== lastGroup) {
4887
+ lastGroup = clip.group;
4888
+ if (clip.group) {
4889
+ const group = clip.group;
4890
+ const isCollapsed = collapsedGroups.has(group);
4891
+ rows.push(
4892
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-row dialkit-timeline-group-row", children: [
4893
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-label", children: [
4894
+ /* @__PURE__ */ jsx18(
4895
+ "button",
4896
+ {
4897
+ className: "dialkit-timeline-group-toggle",
4898
+ "data-open": !isCollapsed,
4899
+ onClick: () => toggleGroup(group),
4900
+ title: isCollapsed ? "Expand layer" : "Collapse layer",
4901
+ children: /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx18("path", { d: ICON_CHEVRON }) })
4902
+ }
4903
+ ),
4904
+ /* @__PURE__ */ jsx18("span", { children: formatLabel(group) })
4905
+ ] }),
4906
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-lane" })
4907
+ ] }, `group:${group}`)
4908
+ );
4909
+ }
4910
+ }
4911
+ if (clip.group && collapsedGroups.has(clip.group)) continue;
4912
+ const isProps = Boolean(clip.tracks?.length);
4913
+ const tracksOpen = isProps && expandedTracks.has(clip.key);
4914
+ const stat = computeClipStaticFromValues(values, clip, meta.duration);
4915
+ rows.push(
4916
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-row", "data-grouped": clip.group ? "" : void 0, children: [
4917
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-label", children: [
4918
+ isProps ? /* @__PURE__ */ jsx18(
4919
+ "button",
4920
+ {
4921
+ className: "dialkit-timeline-group-toggle",
4922
+ "data-open": tracksOpen,
4923
+ onClick: (e) => {
4924
+ e.stopPropagation();
4925
+ toggleTracks(clip.key);
4926
+ },
4927
+ title: tracksOpen ? "Collapse properties" : "Expand properties",
4928
+ children: /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx18("path", { d: ICON_CHEVRON }) })
4929
+ }
4930
+ ) : null,
4931
+ clip.label
4932
+ ] }),
4933
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-lane", children: /* @__PURE__ */ jsx18(
4934
+ TimelineClip,
4935
+ {
4936
+ timelineId: meta.id,
4937
+ clip,
4938
+ at: stat.at,
4939
+ duration: stat.duration,
4940
+ loop: stat.loop,
4941
+ steps: clip.stepKeys?.length ? stat.tracks[0]?.steps : void 0,
4942
+ fixedDuration: isProps ? true : stat.isPhysics,
4943
+ composite: isProps,
4944
+ pxPerSecond,
4945
+ viewStart: safeViewStart,
4946
+ timelineDuration: meta.duration,
4947
+ selected: popover?.clip.key === clip.key,
4948
+ selectedStepKey: popover?.clip.key === clip.key ? popover.stepKey : void 0,
4949
+ onClick: handleBarClick,
4950
+ onDrag: closePopover
4951
+ }
4952
+ ) })
4953
+ ] }, clip.key)
4954
+ );
4955
+ if (tracksOpen) {
4956
+ for (const trackRef of clip.tracks ?? []) {
4957
+ const track = stat.tracks.find((candidate) => candidate.prop === trackRef.prop);
4958
+ if (!track) continue;
4959
+ const trackKey = `${clip.key}.${trackRef.prop}`;
4960
+ const trackMeta = {
4961
+ key: trackKey,
4962
+ label: `${clip.label} \xB7 ${formatLabel(trackRef.prop)}`,
4963
+ color: clip.color,
4964
+ loop: clip.loop,
4965
+ group: clip.group,
4966
+ stepKeys: trackRef.stepKeys
4967
+ };
4968
+ rows.push(
4969
+ /* @__PURE__ */ jsxs14(
4970
+ "div",
4971
+ {
4972
+ className: "dialkit-timeline-row dialkit-timeline-track-row",
4973
+ "data-grouped": clip.group ? "" : void 0,
4974
+ children: [
4975
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-label", children: formatLabel(trackRef.prop) }),
4976
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-lane", children: /* @__PURE__ */ jsx18(
4977
+ TimelineClip,
4978
+ {
4979
+ timelineId: meta.id,
4980
+ clip: trackMeta,
4981
+ at: stat.at + track.delay,
4982
+ duration: track.duration,
4983
+ loop: stat.loop,
4984
+ steps: trackRef.stepKeys?.length ? track.steps : void 0,
4985
+ fixedDuration: !trackRef.stepKeys?.length && track.steps[0]?.isPhysics === true,
4986
+ baseAt: stat.at,
4987
+ delayMode: true,
4988
+ pxPerSecond,
4989
+ viewStart: safeViewStart,
4990
+ timelineDuration: meta.duration,
4991
+ selected: popover?.clip.key === trackKey,
4992
+ selectedStepKey: popover?.clip.key === trackKey ? popover.stepKey : void 0,
4993
+ onClick: openClipPopover,
4994
+ onDrag: closePopover
4995
+ }
4996
+ ) })
4997
+ ]
4998
+ },
4999
+ trackKey
5000
+ )
5001
+ );
5002
+ }
5003
+ }
5004
+ }
5005
+ return /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-section", children: [
5006
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-header", "data-open": open || void 0, children: [
5007
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-identity", children: /* @__PURE__ */ jsx18("span", { className: "dialkit-timeline-title", children: meta.name }) }),
5008
+ !open && /* @__PURE__ */ jsx18(
5009
+ TimelineOverview,
5010
+ {
5011
+ id: meta.id,
5012
+ duration: meta.duration,
5013
+ viewStart: safeViewStart,
5014
+ viewEnd,
5015
+ onNavigate: centerViewAt
5016
+ }
5017
+ ),
5018
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-actions", children: [
5019
+ /* @__PURE__ */ jsx18(PlayPauseButton, { id: meta.id }),
5020
+ /* @__PURE__ */ jsx18(ReplayButton, { onReplay: handleReplay }),
5021
+ /* @__PURE__ */ jsx18(
5022
+ motion7.button,
5023
+ {
5024
+ className: "dialkit-toolbar-add",
5025
+ onClick: handleAddPreset,
5026
+ title: "Add timeline version",
5027
+ "aria-label": "Add timeline version",
5028
+ whileTap: { scale: 0.9 },
5029
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
5030
+ children: /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: ICON_ADD_PRESET.map((d, i) => /* @__PURE__ */ jsx18("path", { d }, i)) })
5031
+ }
5032
+ ),
5033
+ /* @__PURE__ */ jsx18(
5034
+ PresetManager,
5035
+ {
5036
+ panelId: meta.id,
5037
+ presets,
5038
+ activePresetId,
5039
+ onAdd: handleAddPreset
5040
+ }
5041
+ ),
5042
+ /* @__PURE__ */ jsx18(
5043
+ motion7.button,
5044
+ {
5045
+ className: "dialkit-toolbar-add",
5046
+ onClick: handleCopy,
5047
+ title: "Copy parameters",
5048
+ "aria-label": copied ? "Copied parameters" : "Copy parameters",
5049
+ whileTap: { scale: 0.9 },
5050
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
5051
+ children: /* @__PURE__ */ jsx18("span", { style: { position: "relative", width: 16, height: 16 }, children: /* @__PURE__ */ jsx18(AnimatePresence5, { initial: false, mode: "wait", children: copied ? /* @__PURE__ */ jsx18(
5052
+ motion7.svg,
5053
+ {
5054
+ viewBox: "0 0 24 24",
5055
+ fill: "none",
5056
+ stroke: "currentColor",
5057
+ strokeWidth: "2",
5058
+ strokeLinecap: "round",
5059
+ strokeLinejoin: "round",
5060
+ "aria-hidden": "true",
5061
+ style: { position: "absolute", inset: 0, width: 16, height: 16, color: "var(--dial-text-label)" },
5062
+ initial: { scale: 0.8, opacity: 0 },
5063
+ animate: { scale: 1, opacity: 1 },
5064
+ exit: { scale: 0.8, opacity: 0 },
5065
+ transition: { duration: 0.08 },
5066
+ children: /* @__PURE__ */ jsx18("path", { d: ICON_CHECK })
5067
+ },
5068
+ "check"
5069
+ ) : /* @__PURE__ */ jsxs14(
5070
+ motion7.svg,
5071
+ {
5072
+ viewBox: "0 0 24 24",
5073
+ fill: "none",
5074
+ "aria-hidden": "true",
5075
+ style: { position: "absolute", inset: 0, width: 16, height: 16, color: "var(--dial-text-label)" },
5076
+ initial: { scale: 0.8, opacity: 0 },
5077
+ animate: { scale: 1, opacity: 1 },
5078
+ exit: { scale: 0.8, opacity: 0 },
5079
+ transition: { duration: 0.08 },
5080
+ children: [
5081
+ /* @__PURE__ */ jsx18("path", { d: ICON_CLIPBOARD.board, stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round" }),
5082
+ /* @__PURE__ */ jsx18("path", { d: ICON_CLIPBOARD.sparkle, fill: "currentColor" }),
5083
+ /* @__PURE__ */ jsx18("path", { d: ICON_CLIPBOARD.body, stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })
5084
+ ]
5085
+ },
5086
+ "clipboard"
5087
+ ) }) })
5088
+ }
5089
+ ),
5090
+ /* @__PURE__ */ jsx18(
5091
+ "button",
5092
+ {
5093
+ className: "dialkit-timeline-chevron",
5094
+ "data-open": open,
5095
+ "aria-expanded": open,
5096
+ onClick: () => setOpen(!open),
5097
+ title: open ? "Collapse timeline" : "Expand timeline",
5098
+ children: /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx18("path", { d: ICON_CHEVRON }) })
5099
+ }
5100
+ )
5101
+ ] })
5102
+ ] }),
5103
+ open && /* @__PURE__ */ jsxs14(
5104
+ "div",
5105
+ {
5106
+ className: "dialkit-timeline-body",
5107
+ onWheel: handleTimelineWheel,
5108
+ onPointerDown: handleTrackPointerDown,
5109
+ onPointerMove: handleTrackPointerMove,
5110
+ onPointerUp: finishTrackScrub,
5111
+ onPointerCancel: finishTrackScrub,
5112
+ onLostPointerCapture: finishTrackScrub,
5113
+ children: [
5114
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-grid", children: [
5115
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-row dialkit-timeline-ruler-row", children: [
5116
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-label" }),
5117
+ /* @__PURE__ */ jsxs14(
5118
+ "div",
5119
+ {
5120
+ ref: laneAreaRef,
5121
+ className: "dialkit-timeline-ruler",
5122
+ onPointerDown: handleRulerPointerDown,
5123
+ onPointerMove: handleRulerPointerMove,
5124
+ onPointerUp: handleRulerPointerUp,
5125
+ onPointerCancel: handleRulerPointerCancel,
5126
+ onLostPointerCapture: handleRulerPointerCancel,
5127
+ title: "Drag to seek \xB7 Option-drag to zoom \xB7 Shift-drag to reset zoom",
5128
+ children: [
5129
+ fineTicks.map((t) => /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-tick dialkit-timeline-tick-fine", style: { left: (t - safeViewStart) * pxPerSecond } }, `fine:${t}`)),
5130
+ mediumTicks.map((t) => /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-tick dialkit-timeline-tick-medium", style: { left: (t - safeViewStart) * pxPerSecond } }, `medium:${t}`)),
5131
+ majorTicks.map((t) => /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-tick", style: { left: (t - safeViewStart) * pxPerSecond }, children: /* @__PURE__ */ jsx18("span", { className: "dialkit-timeline-tick-label", children: formatRulerSeconds(t, majorStep) }) }, t))
5132
+ ]
5133
+ }
5134
+ )
5135
+ ] }),
5136
+ rows,
5137
+ pxPerSecond > 0 && /* @__PURE__ */ jsx18(
5138
+ TimelinePlayheadFlag,
5139
+ {
5140
+ id: meta.id,
5141
+ duration: meta.duration,
5142
+ pxPerSecond,
5143
+ viewStart: safeViewStart,
5144
+ viewEnd,
5145
+ laneWidth,
5146
+ rulerRef: laneAreaRef,
5147
+ onResetView: resetView
5148
+ }
5149
+ )
5150
+ ] }),
5151
+ zoom > 1 && /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-scroll-row", children: [
5152
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-label" }),
5153
+ /* @__PURE__ */ jsx18(
5154
+ "div",
5155
+ {
5156
+ ref: horizontalScrollRef,
5157
+ className: "dialkit-timeline-horizontal-scroll",
5158
+ onScroll: handleHorizontalScroll,
5159
+ "aria-label": "Timeline horizontal scroll",
5160
+ children: /* @__PURE__ */ jsx18("div", { style: { width: laneWidth * zoom } })
5161
+ }
5162
+ )
5163
+ ] })
5164
+ ]
5165
+ }
5166
+ ),
5167
+ popover && /* @__PURE__ */ jsx18(
5168
+ ClipPopover,
5169
+ {
5170
+ panelId: meta.id,
5171
+ popover,
5172
+ values,
5173
+ theme,
5174
+ onClose: closePopover
5175
+ }
5176
+ )
5177
+ ] });
5178
+ });
5179
+ function ClipPopover({
5180
+ panelId,
5181
+ popover,
5182
+ values,
5183
+ theme,
5184
+ onClose
5185
+ }) {
5186
+ const ref = useRef14(null);
5187
+ const [naturalHeight, setNaturalHeight] = useState11(0);
5188
+ const [viewport, setViewport] = useState11(() => ({
5189
+ width: window.visualViewport?.width ?? window.innerWidth,
5190
+ height: window.visualViewport?.height ?? window.innerHeight,
5191
+ offsetLeft: window.visualViewport?.offsetLeft ?? 0,
5192
+ offsetTop: window.visualViewport?.offsetTop ?? 0
5193
+ }));
5194
+ useLayoutEffect2(() => {
5195
+ const element = ref.current;
5196
+ if (!element) return;
5197
+ const measure = () => setNaturalHeight(element.scrollHeight + 2);
5198
+ measure();
5199
+ const observer = new ResizeObserver(measure);
5200
+ const body = element.querySelector(".dialkit-timeline-popover-body");
5201
+ observer.observe(body ?? element);
5202
+ return () => observer.disconnect();
5203
+ }, [popover.clip.key, popover.stepKey]);
5204
+ useEffect11(() => {
5205
+ const updateViewport = () => setViewport({
5206
+ width: window.visualViewport?.width ?? window.innerWidth,
5207
+ height: window.visualViewport?.height ?? window.innerHeight,
5208
+ offsetLeft: window.visualViewport?.offsetLeft ?? 0,
5209
+ offsetTop: window.visualViewport?.offsetTop ?? 0
5210
+ });
5211
+ window.addEventListener("resize", updateViewport);
5212
+ window.visualViewport?.addEventListener("resize", updateViewport);
5213
+ window.visualViewport?.addEventListener("scroll", updateViewport);
5214
+ return () => {
5215
+ window.removeEventListener("resize", updateViewport);
5216
+ window.visualViewport?.removeEventListener("resize", updateViewport);
5217
+ window.visualViewport?.removeEventListener("scroll", updateViewport);
5218
+ };
5219
+ }, []);
5220
+ useEffect11(() => {
5221
+ const handlePointerDown = (e) => {
5222
+ const target = e.target;
5223
+ if (ref.current?.contains(target)) return;
5224
+ if (target.closest?.(".dialkit-timeline-clip")) return;
5225
+ if (target.closest?.(".dialkit-timeline-label")) return;
5226
+ onClose();
5227
+ };
5228
+ const handleKeyDown = (e) => {
5229
+ if (e.key === "Escape") onClose();
5230
+ };
5231
+ document.addEventListener("pointerdown", handlePointerDown, true);
5232
+ document.addEventListener("keydown", handleKeyDown);
5233
+ return () => {
5234
+ document.removeEventListener("pointerdown", handlePointerDown, true);
5235
+ document.removeEventListener("keydown", handleKeyDown);
5236
+ };
5237
+ }, [onClose]);
5238
+ const { clip, stepKey } = popover;
5239
+ let controls;
5240
+ let title;
5241
+ if (stepKey) {
5242
+ controls = getClipControls(panelId, `${clip.key}.${stepKey}`);
5243
+ if (stepKey === clip.stepKeys?.[0]) {
5244
+ const from = getControlAt(panelId, `${clip.key}.from`);
5245
+ if (from) {
5246
+ const toIndex = controls.findIndex((control) => control.path === `${clip.key}.${stepKey}.to`);
5247
+ controls = toIndex >= 0 ? [...controls.slice(0, toIndex), from, ...controls.slice(toIndex)] : [...controls, from];
5248
+ }
5249
+ }
5250
+ title = `${clip.label} \xB7 ${formatStepLabel(stepKey)}`;
5251
+ } else {
5252
+ controls = getClipControls(panelId, clip.key, clipPopoverExclusions(clip));
5253
+ title = clip.label;
5254
+ }
5255
+ if (controls.length === 0) return null;
5256
+ const targetPath = stepKey ? `${clip.key}.${stepKey}` : clip.key;
5257
+ const durationMeta = getControlAt(panelId, `${targetPath}.duration`);
5258
+ const durationValue = durationMeta ? values[durationMeta.path] : void 0;
5259
+ const transitionDuration = durationMeta?.type === "slider" && typeof durationValue === "number" ? {
5260
+ value: durationValue,
5261
+ onChange: (next) => DialStore.updateValue(panelId, durationMeta.path, next),
5262
+ min: Math.max(TIMELINE_MIN_CLIP_DURATION, durationMeta.min ?? 0),
5263
+ max: durationMeta.max,
5264
+ step: durationMeta.step
5265
+ } : void 0;
5266
+ const displayValues = timelinePopoverDisplayValues(values, clip.key, clip.stepKeys, stepKey);
5267
+ const viewportRight = viewport.offsetLeft + viewport.width;
5268
+ const viewportBottom = viewport.offsetTop + viewport.height;
5269
+ const popoverWidth = Math.min(POPOVER_WIDTH, Math.max(220, viewport.width - 24));
5270
+ const left = clamp(
5271
+ popover.anchor.left + popover.anchor.width / 2 - popoverWidth / 2,
5272
+ viewport.offsetLeft + 12,
5273
+ Math.max(viewport.offsetLeft + 12, viewportRight - popoverWidth - 12)
5274
+ );
5275
+ const spaceAbove = Math.max(0, popover.anchor.top - viewport.offsetTop - 22);
5276
+ const spaceBelow = Math.max(0, viewportBottom - popover.anchor.bottom - 22);
5277
+ const placeAbove = naturalHeight === 0 ? spaceAbove >= spaceBelow : naturalHeight <= spaceAbove || naturalHeight > spaceBelow && spaceAbove >= spaceBelow;
5278
+ const availableHeight = placeAbove ? spaceAbove : spaceBelow;
5279
+ const renderedHeight = Math.min(naturalHeight || availableHeight, availableHeight);
5280
+ const unclampedTop = placeAbove ? popover.anchor.top - 10 - renderedHeight : popover.anchor.bottom + 10;
5281
+ const top = clamp(
5282
+ unclampedTop,
5283
+ viewport.offsetTop + 12,
5284
+ Math.max(viewport.offsetTop + 12, viewportBottom - renderedHeight - 12)
5285
+ );
5286
+ return createPortal4(
5287
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-root", "data-theme": theme, children: /* @__PURE__ */ jsxs14(
5288
+ "div",
5289
+ {
5290
+ ref,
5291
+ className: "dialkit-timeline-popover",
5292
+ "data-placement": placeAbove ? "above" : "below",
5293
+ style: {
5294
+ left,
5295
+ top,
5296
+ width: popoverWidth,
5297
+ maxHeight: availableHeight,
5298
+ visibility: naturalHeight > 0 ? "visible" : "hidden"
5299
+ },
5300
+ role: "dialog",
5301
+ "aria-label": `Edit ${title}`,
5302
+ children: [
5303
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-popover-header", children: [
5304
+ /* @__PURE__ */ jsx18("span", { className: "dialkit-timeline-popover-title", children: title }),
5305
+ /* @__PURE__ */ jsx18("button", { className: "dialkit-timeline-popover-close", onClick: onClose, title: "Close editor", "aria-label": "Close editor", children: /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: /* @__PURE__ */ jsx18("path", { d: "M6 6L18 18M18 6L6 18" }) }) })
5306
+ ] }),
5307
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-popover-body", children: /* @__PURE__ */ jsx18(
5308
+ ControlRenderer,
5309
+ {
5310
+ panelId,
5311
+ controls,
5312
+ values: displayValues,
5313
+ transitionDuration
5314
+ }
5315
+ ) })
5316
+ ]
5317
+ }
5318
+ ) }),
5319
+ document.body
5320
+ );
5321
+ }
5322
+ function clipPopoverExclusions(clip) {
5323
+ return /* @__PURE__ */ new Set([
5324
+ ...clip.stepKeys ?? [],
5325
+ ...clip.tracks?.map((track) => track.prop) ?? []
5326
+ ]);
5327
+ }
5328
+ function getClipControls(panelId, controlPath, excludeChildren) {
5329
+ const panel = DialStore.getPanel(panelId);
5330
+ const folder = panel ? findControl(panel.controls, controlPath) : null;
5331
+ if (!folder?.children) return [];
5332
+ return folder.children.filter((control) => {
5333
+ const childKey = control.path.slice(controlPath.length + 1);
5334
+ if (childKey === "at" || childKey === "duration") return false;
5335
+ return !excludeChildren?.has(childKey);
5336
+ });
5337
+ }
5338
+ function getControlAt(panelId, path) {
5339
+ const panel = DialStore.getPanel(panelId);
5340
+ return panel ? findControl(panel.controls, path) : null;
5341
+ }
5342
+ function TimelineClip({
5343
+ timelineId,
5344
+ clip,
5345
+ at,
5346
+ duration,
5347
+ loop,
5348
+ steps,
5349
+ fixedDuration,
5350
+ composite = false,
5351
+ baseAt = 0,
5352
+ delayMode = false,
5353
+ pxPerSecond,
5354
+ viewStart,
5355
+ timelineDuration,
5356
+ selected,
5357
+ selectedStepKey,
5358
+ onClick,
5359
+ onDrag
5360
+ }) {
5361
+ const dragRef = useRef14(null);
5362
+ const [dragging, setDragging] = useState11(false);
5363
+ const isSteps = Boolean(steps?.length);
5364
+ const handlePointerDown = useCallback14(
5365
+ (e) => {
5366
+ if (e.shiftKey) return;
5367
+ e.stopPropagation();
5368
+ const target = e.target;
5369
+ let mode = "move";
5370
+ let boundaryIndex;
5371
+ const boundary = target.dataset?.boundary;
5372
+ if (boundary !== void 0) {
5373
+ mode = "boundary";
5374
+ boundaryIndex = Number(boundary);
5375
+ } else if (!fixedDuration) {
5376
+ const edge = target.dataset?.edge;
5377
+ if (edge) mode = edge;
5378
+ }
5379
+ dragRef.current = {
5380
+ mode,
5381
+ boundaryIndex,
5382
+ pointerX: e.clientX,
5383
+ at,
5384
+ duration,
5385
+ stepDurations: steps?.map((step) => step.duration),
5386
+ clickEl: target.closest?.("[data-step]") ?? null,
5387
+ moved: false
5388
+ };
5389
+ e.currentTarget.setPointerCapture(e.pointerId);
5390
+ },
5391
+ [at, duration, fixedDuration, steps]
5392
+ );
5393
+ const handlePointerMove = useCallback14(
5394
+ (e) => {
5395
+ const drag = dragRef.current;
5396
+ if (!drag || pxPerSecond <= 0) return;
5397
+ const dx = e.clientX - drag.pointerX;
5398
+ if (!drag.moved) {
5399
+ if (Math.abs(dx) <= DRAG_THRESHOLD_PX) return;
5400
+ drag.moved = true;
5401
+ setDragging(true);
5402
+ onDrag();
5403
+ }
5404
+ const dt = dx / pxPerSecond;
5405
+ if (drag.mode === "boundary" && steps && drag.stepDurations) {
5406
+ const index = drag.boundaryIndex ?? 0;
5407
+ const others = drag.stepDurations.reduce((sum, d, j) => j === index ? sum : sum + d, 0);
5408
+ DialStore.updateValue(
5409
+ timelineId,
5410
+ `${clip.key}.${steps[index].key ?? ""}.duration`,
5411
+ clampStepResize(drag.stepDurations[index] + dt, drag.at, others, timelineDuration)
5412
+ );
5413
+ } else if (drag.mode === "move") {
5414
+ if (delayMode) {
5415
+ DialStore.updateValue(
5416
+ timelineId,
5417
+ `${clip.key}.delay`,
5418
+ clampTrackDelay(drag.at + dt - baseAt, baseAt, drag.duration, timelineDuration)
5419
+ );
5420
+ } else {
5421
+ DialStore.updateValue(timelineId, `${clip.key}.at`, clampClipMove(drag.at + dt, drag.duration, timelineDuration));
5422
+ }
5423
+ } else if (drag.mode === "end") {
5424
+ DialStore.updateValue(
5425
+ timelineId,
5426
+ `${clip.key}.duration`,
5427
+ clampClipResizeEnd(drag.duration + dt, drag.at, timelineDuration)
5428
+ );
5429
+ } else if (steps && drag.stepDurations) {
5430
+ const limit = Math.max(baseAt, 0);
5431
+ const next = clampClipResizeStart(Math.max(drag.at + dt, limit), drag.at, drag.stepDurations[0]);
5432
+ DialStore.updateValues(timelineId, {
5433
+ [delayMode ? `${clip.key}.delay` : `${clip.key}.at`]: delayMode ? Math.max(0, next.at - baseAt) : next.at,
5434
+ [`${clip.key}.${steps[0].key ?? ""}.duration`]: next.duration
5435
+ });
5436
+ } else {
5437
+ const limit = Math.max(baseAt, 0);
5438
+ const next = clampClipResizeStart(Math.max(drag.at + dt, limit), drag.at, drag.duration);
5439
+ DialStore.updateValues(timelineId, {
5440
+ [delayMode ? `${clip.key}.delay` : `${clip.key}.at`]: delayMode ? Math.max(0, next.at - baseAt) : next.at,
5441
+ [`${clip.key}.duration`]: next.duration
5442
+ });
5443
+ }
5444
+ },
5445
+ [baseAt, clip.key, delayMode, onDrag, pxPerSecond, steps, timelineId, timelineDuration]
5446
+ );
5447
+ const handlePointerUp = useCallback14(
5448
+ (e) => {
5449
+ const drag = dragRef.current;
5450
+ dragRef.current = null;
5451
+ setDragging(false);
5452
+ if (drag && !drag.moved) {
5453
+ const stepKey = drag.clickEl?.dataset?.step;
5454
+ const anchorEl = drag.clickEl ?? e.currentTarget;
5455
+ onClick(clip, anchorEl.getBoundingClientRect(), stepKey);
5456
+ }
5457
+ },
5458
+ [clip, onClick]
5459
+ );
5460
+ const handlePointerCancel = useCallback14(() => {
5461
+ dragRef.current = null;
5462
+ setDragging(false);
5463
+ }, []);
5464
+ const width = Math.max(duration * pxPerSecond, 14);
5465
+ const resizable = duration > 0 && !fixedDuration && !composite;
5466
+ const durationText = `${fixedDuration && !composite ? "~" : ""}${formatSeconds(duration)}`;
5467
+ const looping = loop === "repeat" && duration > 0;
5468
+ const ghostCycles = [];
5469
+ if (looping) {
5470
+ const maxGhostCycles = 256;
5471
+ const firstGhostIndex = Math.max(1, Math.floor((viewStart - at) / duration));
5472
+ for (let offset = 0; offset < maxGhostCycles; offset++) {
5473
+ const index = firstGhostIndex + offset;
5474
+ const start = at + duration * index;
5475
+ if (start >= timelineDuration - 1e-6) break;
5476
+ ghostCycles.push({
5477
+ start,
5478
+ duration: Math.min(duration, timelineDuration - start),
5479
+ index
5480
+ });
5481
+ }
5482
+ }
5483
+ const boundaryOffsets = [];
5484
+ if (steps) {
5485
+ let cumulative = 0;
5486
+ for (const step of steps) {
5487
+ cumulative += step.duration;
5488
+ boundaryOffsets.push(cumulative);
5489
+ }
5490
+ }
5491
+ const barTitle = composite ? `${clip.label} \u2014 composite of its property tracks${looping ? " \xB7 repeats through timeline" : ""} \xB7 click to expand` : `${clip.label} \u2014 ${formatSeconds(at)} for ${durationText}${fixedDuration ? " (duration set by spring physics)" : ""}${looping ? " \xB7 repeats through timeline" : ""}${delayMode ? " \xB7 drag to phase-shift" : ""}`;
5492
+ return /* @__PURE__ */ jsxs14(Fragment5, { children: [
5493
+ ghostCycles.map((cycle) => {
5494
+ const ghostWidth = Math.max(1, cycle.duration * pxPerSecond - 2);
5495
+ return /* @__PURE__ */ jsx18(
5496
+ "div",
5497
+ {
5498
+ className: "dialkit-timeline-clip-ghost",
5499
+ "data-steps": isSteps || void 0,
5500
+ "aria-hidden": "true",
5501
+ style: {
5502
+ left: (cycle.start - viewStart) * pxPerSecond + 1,
5503
+ width: ghostWidth,
5504
+ background: clip.color
5505
+ },
5506
+ children: steps?.map((step, stepIndex) => /* @__PURE__ */ jsx18(
5507
+ "span",
5508
+ {
5509
+ className: "dialkit-timeline-clip-ghost-segment",
5510
+ style: { width: step.duration * pxPerSecond }
5511
+ },
5512
+ step.key ?? `step:${stepIndex}`
5513
+ ))
5514
+ },
5515
+ `ghost:${cycle.index}`
5516
+ );
5517
+ }),
5518
+ /* @__PURE__ */ jsx18(
5519
+ "div",
5520
+ {
5521
+ className: "dialkit-timeline-clip",
5522
+ "data-steps": isSteps || void 0,
5523
+ "data-composite": composite || void 0,
5524
+ "data-selected": selected || void 0,
5525
+ "data-dragging": dragging || void 0,
5526
+ style: {
5527
+ left: (at - viewStart) * pxPerSecond,
5528
+ width,
5529
+ background: composite ? `${clip.color}80` : clip.color
5530
+ },
5531
+ onPointerDown: handlePointerDown,
5532
+ onPointerMove: handlePointerMove,
5533
+ onPointerUp: handlePointerUp,
5534
+ onPointerCancel: handlePointerCancel,
5535
+ onLostPointerCapture: handlePointerCancel,
5536
+ title: barTitle,
5537
+ children: composite ? /* @__PURE__ */ jsx18(Fragment5, { children: width > 56 && /* @__PURE__ */ jsx18("span", { className: "dialkit-timeline-clip-duration", children: durationText }) }) : isSteps ? /* @__PURE__ */ jsxs14(Fragment5, { children: [
5538
+ steps.map((step) => {
5539
+ const segmentWidth = step.duration * pxPerSecond;
5540
+ return /* @__PURE__ */ jsx18(
5541
+ "div",
5542
+ {
5543
+ className: "dialkit-timeline-clip-segment",
5544
+ "data-step": step.key ?? void 0,
5545
+ "data-selected": selectedStepKey === step.key || void 0,
5546
+ style: { width: segmentWidth },
5547
+ children: segmentWidth > 52 && /* @__PURE__ */ jsx18("span", { className: "dialkit-timeline-clip-duration", children: formatSeconds(step.duration) })
5548
+ },
5549
+ step.key ?? "step"
5550
+ );
5551
+ }),
5552
+ steps.map(
5553
+ (step, index) => step.isPhysics ? null : /* @__PURE__ */ jsx18(
5554
+ "div",
5555
+ {
5556
+ className: "dialkit-timeline-clip-handle",
5557
+ "data-boundary": index,
5558
+ style: { left: boundaryOffsets[index] * pxPerSecond - 4 }
5559
+ },
5560
+ `boundary:${step.key}`
5561
+ )
5562
+ ),
5563
+ !steps[0].isPhysics && /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-clip-handle", "data-edge": "start" })
5564
+ ] }) : /* @__PURE__ */ jsxs14(Fragment5, { children: [
5565
+ resizable && /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-clip-handle", "data-edge": "start" }),
5566
+ width > 56 && /* @__PURE__ */ jsx18("span", { className: "dialkit-timeline-clip-duration", children: durationText }),
5567
+ resizable && /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-clip-handle", "data-edge": "end" })
5568
+ ] })
5569
+ }
5570
+ ),
5571
+ looping && /* @__PURE__ */ jsx18("span", { className: "dialkit-timeline-loop-infinity", "aria-hidden": "true", title: "Repeats indefinitely", children: "\u221E" })
5572
+ ] });
5573
+ }
5574
+
2842
5575
  // src/components/ButtonGroup.tsx
2843
- import { jsx as jsx16 } from "react/jsx-runtime";
5576
+ import { jsx as jsx19 } from "react/jsx-runtime";
2844
5577
  function ButtonGroup({ buttons }) {
2845
- return /* @__PURE__ */ jsx16("div", { className: "dialkit-button-group", children: buttons.map((button, index) => /* @__PURE__ */ jsx16(
5578
+ return /* @__PURE__ */ jsx19("div", { className: "dialkit-button-group", children: buttons.map((button, index) => /* @__PURE__ */ jsx19(
2846
5579
  "button",
2847
5580
  {
2848
5581
  className: "dialkit-button",
@@ -2854,10 +5587,10 @@ function ButtonGroup({ buttons }) {
2854
5587
  }
2855
5588
 
2856
5589
  // src/components/ShortcutsMenu.tsx
2857
- import { useState as useState11, useRef as useRef12, useEffect as useEffect9, useCallback as useCallback11 } from "react";
2858
- import { createPortal as createPortal4 } from "react-dom";
2859
- import { motion as motion6, AnimatePresence as AnimatePresence5 } from "motion/react";
2860
- import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
5590
+ import { useState as useState12, useRef as useRef15, useEffect as useEffect12, useCallback as useCallback15 } from "react";
5591
+ import { createPortal as createPortal5 } from "react-dom";
5592
+ import { motion as motion8, AnimatePresence as AnimatePresence6 } from "motion/react";
5593
+ import { Fragment as Fragment6, jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
2861
5594
  function formatShortcutKey(sc) {
2862
5595
  if (!sc.key) return "\u2014";
2863
5596
  const mod = sc.modifier === "alt" ? "\u2325" : sc.modifier === "shift" ? "\u21E7" : sc.modifier === "meta" ? "\u2318" : "";
@@ -2877,23 +5610,23 @@ function formatInteraction(sc) {
2877
5610
  }
2878
5611
  }
2879
5612
  function ShortcutsMenu({ panelId }) {
2880
- const [isOpen, setIsOpen] = useState11(false);
2881
- const triggerRef = useRef12(null);
2882
- const dropdownRef = useRef12(null);
2883
- const [pos, setPos] = useState11({ top: 0, right: 0 });
2884
- const open = useCallback11(() => {
5613
+ const [isOpen, setIsOpen] = useState12(false);
5614
+ const triggerRef = useRef15(null);
5615
+ const dropdownRef = useRef15(null);
5616
+ const [pos, setPos] = useState12({ top: 0, right: 0 });
5617
+ const open = useCallback15(() => {
2885
5618
  const rect = triggerRef.current?.getBoundingClientRect();
2886
5619
  if (rect) {
2887
5620
  setPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
2888
5621
  }
2889
5622
  setIsOpen(true);
2890
5623
  }, []);
2891
- const close = useCallback11(() => setIsOpen(false), []);
2892
- const toggle = useCallback11(() => {
5624
+ const close = useCallback15(() => setIsOpen(false), []);
5625
+ const toggle = useCallback15(() => {
2893
5626
  if (isOpen) close();
2894
5627
  else open();
2895
5628
  }, [isOpen, open, close]);
2896
- useEffect9(() => {
5629
+ useEffect12(() => {
2897
5630
  if (!isOpen) return;
2898
5631
  const handler = (e) => {
2899
5632
  const target = e.target;
@@ -2924,9 +5657,9 @@ function ShortcutsMenu({ panelId }) {
2924
5657
  label: findLabel(panel.controls)
2925
5658
  };
2926
5659
  });
2927
- return /* @__PURE__ */ jsxs14(Fragment4, { children: [
2928
- /* @__PURE__ */ jsx17(
2929
- motion6.button,
5660
+ return /* @__PURE__ */ jsxs15(Fragment6, { children: [
5661
+ /* @__PURE__ */ jsx20(
5662
+ motion8.button,
2930
5663
  {
2931
5664
  ref: triggerRef,
2932
5665
  className: "dialkit-shortcuts-trigger",
@@ -2934,19 +5667,19 @@ function ShortcutsMenu({ panelId }) {
2934
5667
  title: "Keyboard shortcuts",
2935
5668
  whileTap: { scale: 0.9 },
2936
5669
  transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
2937
- children: /* @__PURE__ */ jsxs14("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2938
- /* @__PURE__ */ jsx17("rect", { x: "2", y: "6", width: "20", height: "12", rx: "2" }),
2939
- /* @__PURE__ */ jsx17("path", { d: "M6 10H6.01" }),
2940
- /* @__PURE__ */ jsx17("path", { d: "M10 10H10.01" }),
2941
- /* @__PURE__ */ jsx17("path", { d: "M14 10H14.01" }),
2942
- /* @__PURE__ */ jsx17("path", { d: "M18 10H18.01" }),
2943
- /* @__PURE__ */ jsx17("path", { d: "M8 14H16" })
5670
+ children: /* @__PURE__ */ jsxs15("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
5671
+ /* @__PURE__ */ jsx20("rect", { x: "2", y: "6", width: "20", height: "12", rx: "2" }),
5672
+ /* @__PURE__ */ jsx20("path", { d: "M6 10H6.01" }),
5673
+ /* @__PURE__ */ jsx20("path", { d: "M10 10H10.01" }),
5674
+ /* @__PURE__ */ jsx20("path", { d: "M14 10H14.01" }),
5675
+ /* @__PURE__ */ jsx20("path", { d: "M18 10H18.01" }),
5676
+ /* @__PURE__ */ jsx20("path", { d: "M8 14H16" })
2944
5677
  ] })
2945
5678
  }
2946
5679
  ),
2947
- createPortal4(
2948
- /* @__PURE__ */ jsx17(AnimatePresence5, { children: isOpen && /* @__PURE__ */ jsxs14(
2949
- motion6.div,
5680
+ createPortal5(
5681
+ /* @__PURE__ */ jsx20(AnimatePresence6, { children: isOpen && /* @__PURE__ */ jsxs15(
5682
+ motion8.div,
2950
5683
  {
2951
5684
  ref: dropdownRef,
2952
5685
  className: "dialkit-root dialkit-shortcuts-dropdown",
@@ -2956,13 +5689,13 @@ function ShortcutsMenu({ panelId }) {
2956
5689
  exit: { opacity: 0, y: 4, scale: 0.97, pointerEvents: "none" },
2957
5690
  transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
2958
5691
  children: [
2959
- /* @__PURE__ */ jsx17("div", { className: "dialkit-shortcuts-title", children: "Keyboard Shortcuts" }),
2960
- /* @__PURE__ */ jsx17("div", { className: "dialkit-shortcuts-list", children: rows.map((row) => /* @__PURE__ */ jsxs14("div", { className: "dialkit-shortcuts-row", children: [
2961
- /* @__PURE__ */ jsx17("span", { className: "dialkit-shortcuts-row-key", children: formatShortcutKey(row.shortcut) }),
2962
- /* @__PURE__ */ jsx17("span", { className: "dialkit-shortcuts-row-label", children: row.label }),
2963
- /* @__PURE__ */ jsx17("span", { className: "dialkit-shortcuts-row-mode", children: formatInteraction(row.shortcut) })
5692
+ /* @__PURE__ */ jsx20("div", { className: "dialkit-shortcuts-title", children: "Keyboard Shortcuts" }),
5693
+ /* @__PURE__ */ jsx20("div", { className: "dialkit-shortcuts-list", children: rows.map((row) => /* @__PURE__ */ jsxs15("div", { className: "dialkit-shortcuts-row", children: [
5694
+ /* @__PURE__ */ jsx20("span", { className: "dialkit-shortcuts-row-key", children: formatShortcutKey(row.shortcut) }),
5695
+ /* @__PURE__ */ jsx20("span", { className: "dialkit-shortcuts-row-label", children: row.label }),
5696
+ /* @__PURE__ */ jsx20("span", { className: "dialkit-shortcuts-row-mode", children: formatInteraction(row.shortcut) })
2964
5697
  ] }, row.path)) }),
2965
- /* @__PURE__ */ jsx17("div", { className: "dialkit-shortcuts-hint", children: "See pill badges on controls for keys" })
5698
+ /* @__PURE__ */ jsx20("div", { className: "dialkit-shortcuts-hint", children: "See pill badges on controls for keys" })
2966
5699
  ]
2967
5700
  }
2968
5701
  ) }),
@@ -2973,8 +5706,10 @@ function ShortcutsMenu({ panelId }) {
2973
5706
  export {
2974
5707
  ButtonGroup,
2975
5708
  ColorControl,
5709
+ ControlRenderer,
2976
5710
  DialRoot,
2977
5711
  DialStore,
5712
+ DialTimeline,
2978
5713
  EasingVisualization,
2979
5714
  Folder,
2980
5715
  PresetManager,
@@ -2984,9 +5719,12 @@ export {
2984
5719
  SpringControl,
2985
5720
  SpringVisualization,
2986
5721
  TextControl,
5722
+ TimelineStore,
2987
5723
  Toggle,
2988
5724
  TransitionControl,
5725
+ formatClock,
2989
5726
  useDialKit,
2990
- useDialKitController
5727
+ useDialKitController,
5728
+ useDialTimeline
2991
5729
  };
2992
5730
  //# sourceMappingURL=index.js.map