dialkit 1.3.0 → 1.4.0

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 +4 -1
  3. package/dist/icons.js +13 -0
  4. package/dist/icons.js.map +1 -1
  5. package/dist/index.cjs +3118 -480
  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 +3050 -417
  10. package/dist/index.js.map +1 -1
  11. package/dist/solid/index.d.ts +225 -3
  12. package/dist/solid/index.js +5697 -2508
  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 +704 -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 +76 -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 +756 -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 +704 -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 +2867 -361
  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,221 @@ 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_TIMELINE = [
1042
+ "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",
1043
+ "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",
1044
+ "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"
1045
+ ];
792
1046
  var ICON_CLIPBOARD = {
793
1047
  board: "M8 6C8 4.34315 9.34315 3 11 3H13C14.6569 3 16 4.34315 16 6V7H8V6Z",
794
1048
  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 +1076,16 @@ import { jsx, jsxs } from "react/jsx-runtime";
822
1076
  function Folder({ title, children, defaultOpen = true, isRoot = false, inline = false, onOpenChange, toolbar, panelHeightOffset = 10 }) {
823
1077
  const [isOpen, setIsOpen] = useState(defaultOpen);
824
1078
  const [isCollapsed, setIsCollapsed] = useState(!defaultOpen);
825
- const contentRef = useRef2(null);
1079
+ const contentRef = useRef3(null);
826
1080
  const [contentHeight, setContentHeight] = useState(void 0);
827
1081
  const [windowHeight, setWindowHeight] = useState(typeof window !== "undefined" ? window.innerHeight : 800);
828
- useEffect2(() => {
1082
+ useEffect3(() => {
829
1083
  if (!isRoot) return;
830
1084
  const onResize = () => setWindowHeight(window.innerHeight);
831
1085
  window.addEventListener("resize", onResize);
832
1086
  return () => window.removeEventListener("resize", onResize);
833
1087
  }, [isRoot]);
834
- useEffect2(() => {
1088
+ useEffect3(() => {
835
1089
  const el = contentRef.current;
836
1090
  if (!el) return;
837
1091
  const ro = new ResizeObserver(() => {
@@ -932,11 +1186,43 @@ function Folder({ title, children, defaultOpen = true, isRoot = false, inline =
932
1186
  }
933
1187
 
934
1188
  // src/components/Panel.tsx
935
- import { useCallback as useCallback9, useContext, useState as useState9, useSyncExternalStore as useSyncExternalStore4 } from "react";
1189
+ import { useCallback as useCallback10, useState as useState9, useSyncExternalStore as useSyncExternalStore4 } from "react";
936
1190
  import { motion as motion5, AnimatePresence as AnimatePresence4 } from "motion/react";
937
1191
 
1192
+ // src/copy-instruction.ts
1193
+ function buildCopyInstruction(hookName, panelName, values) {
1194
+ const jsonStr = JSON.stringify(values, null, 2);
1195
+ if (hookName === "useDialTimeline" || hookName === "createDialTimeline") {
1196
+ return `Update the ${hookName} configuration for "${panelName}" with these values:
1197
+
1198
+ \`\`\`json
1199
+ ${jsonStr}
1200
+ \`\`\`
1201
+
1202
+ 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.
1203
+
1204
+ Add this comment immediately above the ${hookName} call as a production handoff note:
1205
+
1206
+ \`\`\`tsx
1207
+ // TODO(production): DialKit's clip.current values are the scrubbable authoring preview.
1208
+ // Replace them with equivalent real Motion animations using the tuned timeline
1209
+ // timings and transitions, then remove ${hookName} and <DialTimeline />.
1210
+ \`\`\``;
1211
+ }
1212
+ return `Update the ${hookName} configuration for "${panelName}" with these values:
1213
+
1214
+ \`\`\`json
1215
+ ${jsonStr}
1216
+ \`\`\`
1217
+
1218
+ Apply these values as the new defaults in the ${hookName} call.`;
1219
+ }
1220
+
1221
+ // src/components/ControlRenderer.tsx
1222
+ import { useContext } from "react";
1223
+
938
1224
  // src/components/ShortcutListener.tsx
939
- import { createContext, useEffect as useEffect3, useRef as useRef3, useState as useState2, useCallback as useCallback2 } from "react";
1225
+ import { createContext, useEffect as useEffect4, useRef as useRef4, useState as useState2, useCallback as useCallback3 } from "react";
940
1226
 
941
1227
  // src/shortcut-utils.ts
942
1228
  function decimalsForStep(step) {
@@ -1028,11 +1314,11 @@ import { jsx as jsx2 } from "react/jsx-runtime";
1028
1314
  var ShortcutContext = createContext({ activePanelId: null, activePath: null });
1029
1315
  function ShortcutListener({ children }) {
1030
1316
  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) => {
1317
+ const activeKeysRef = useRef4(/* @__PURE__ */ new Set());
1318
+ const isDraggingRef = useRef4(false);
1319
+ const lastMouseXRef = useRef4(null);
1320
+ const dragAccumulatorRef = useRef4(0);
1321
+ const resolveActiveTarget = useCallback3((interaction) => {
1036
1322
  for (const key of activeKeysRef.current) {
1037
1323
  const panels = DialStore.getPanels();
1038
1324
  for (const panel of panels) {
@@ -1049,7 +1335,7 @@ function ShortcutListener({ children }) {
1049
1335
  }
1050
1336
  return null;
1051
1337
  }, []);
1052
- useEffect3(() => {
1338
+ useEffect4(() => {
1053
1339
  const handleKeyDown = (e) => {
1054
1340
  if (isInputFocused()) return;
1055
1341
  const key = e.key.toLowerCase();
@@ -1211,7 +1497,7 @@ function ShortcutListener({ children }) {
1211
1497
  }
1212
1498
 
1213
1499
  // src/components/Slider.tsx
1214
- import { useRef as useRef4, useState as useState3, useCallback as useCallback3, useEffect as useEffect4 } from "react";
1500
+ import { useRef as useRef5, useState as useState3, useCallback as useCallback4, useEffect as useEffect5 } from "react";
1215
1501
  import { motion as motion2, useMotionValue, useTransform, animate } from "motion/react";
1216
1502
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1217
1503
  var CLICK_THRESHOLD = 3;
@@ -1229,11 +1515,11 @@ function Slider({
1229
1515
  shortcut,
1230
1516
  shortcutActive
1231
1517
  }) {
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);
1518
+ const wrapperRef = useRef5(null);
1519
+ const trackRef = useRef5(null);
1520
+ const inputRef = useRef5(null);
1521
+ const labelRef = useRef5(null);
1522
+ const valueSpanRef = useRef5(null);
1237
1523
  const [isInteracting, setIsInteracting] = useState3(false);
1238
1524
  const [isDragging, setIsDragging] = useState3(false);
1239
1525
  const [isHovered, setIsHovered] = useState3(false);
@@ -1241,12 +1527,12 @@ function Slider({
1241
1527
  const [isValueEditable, setIsValueEditable] = useState3(false);
1242
1528
  const [showInput, setShowInput] = useState3(false);
1243
1529
  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);
1530
+ const hoverTimeoutRef = useRef5(null);
1531
+ const pointerDownPos = useRef5(null);
1532
+ const isClickRef = useRef5(true);
1533
+ const animRef = useRef5(null);
1534
+ const wrapperRectRef = useRef5(null);
1535
+ const scaleRef = useRef5(1);
1250
1536
  const percentage = (value - min) / (max - min) * 100;
1251
1537
  const isActive = isInteracting || isHovered;
1252
1538
  const fillPercent = useMotionValue(percentage);
@@ -1264,12 +1550,12 @@ function Slider({
1264
1550
  rubberStretchPx,
1265
1551
  (stretch) => stretch < 0 ? stretch : 0
1266
1552
  );
1267
- useEffect4(() => {
1553
+ useEffect5(() => {
1268
1554
  if (!isInteracting && !animRef.current) {
1269
1555
  fillPercent.jump(percentage);
1270
1556
  }
1271
1557
  }, [percentage, isInteracting, fillPercent]);
1272
- const positionToValue = useCallback3(
1558
+ const positionToValue = useCallback4(
1273
1559
  (clientX) => {
1274
1560
  const rect = wrapperRectRef.current;
1275
1561
  if (!rect) return value;
@@ -1282,11 +1568,11 @@ function Slider({
1282
1568
  },
1283
1569
  [min, max, value]
1284
1570
  );
1285
- const percentFromValue = useCallback3(
1571
+ const percentFromValue = useCallback4(
1286
1572
  (v) => (v - min) / (max - min) * 100,
1287
1573
  [min, max]
1288
1574
  );
1289
- const computeRubberStretch = useCallback3(
1575
+ const computeRubberStretch = useCallback4(
1290
1576
  (clientX, sign) => {
1291
1577
  const rect = wrapperRectRef.current;
1292
1578
  if (!rect) return 0;
@@ -1296,7 +1582,7 @@ function Slider({
1296
1582
  },
1297
1583
  []
1298
1584
  );
1299
- const handlePointerDown = useCallback3(
1585
+ const handlePointerDown = useCallback4(
1300
1586
  (e) => {
1301
1587
  if (showInput) return;
1302
1588
  e.preventDefault();
@@ -1312,7 +1598,7 @@ function Slider({
1312
1598
  },
1313
1599
  [showInput]
1314
1600
  );
1315
- const handlePointerMove = useCallback3(
1601
+ const handlePointerMove = useCallback4(
1316
1602
  (e) => {
1317
1603
  if (!isInteracting || !pointerDownPos.current) return;
1318
1604
  const dx = e.clientX - pointerDownPos.current.x;
@@ -1353,7 +1639,7 @@ function Slider({
1353
1639
  computeRubberStretch
1354
1640
  ]
1355
1641
  );
1356
- const handlePointerUp = useCallback3(
1642
+ const handlePointerUp = useCallback4(
1357
1643
  (e) => {
1358
1644
  if (!isInteracting) return;
1359
1645
  if (isClickRef.current) {
@@ -1397,7 +1683,7 @@ function Slider({
1397
1683
  rubberStretchPx
1398
1684
  ]
1399
1685
  );
1400
- useEffect4(() => {
1686
+ useEffect5(() => {
1401
1687
  if (isValueHovered && !showInput && !isValueEditable) {
1402
1688
  hoverTimeoutRef.current = setTimeout(() => {
1403
1689
  setIsValueEditable(true);
@@ -1415,7 +1701,7 @@ function Slider({
1415
1701
  }
1416
1702
  };
1417
1703
  }, [isValueHovered, showInput, isValueEditable]);
1418
- useEffect4(() => {
1704
+ useEffect5(() => {
1419
1705
  if (showInput && inputRef.current) {
1420
1706
  inputRef.current.focus();
1421
1707
  inputRef.current.select();
@@ -1570,17 +1856,17 @@ function Slider({
1570
1856
  }
1571
1857
 
1572
1858
  // src/components/SegmentedControl.tsx
1573
- import { useRef as useRef5, useState as useState4, useLayoutEffect, useCallback as useCallback4 } from "react";
1859
+ import { useRef as useRef6, useState as useState4, useLayoutEffect, useCallback as useCallback5 } from "react";
1574
1860
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1575
1861
  function SegmentedControl({
1576
1862
  options,
1577
1863
  value,
1578
1864
  onChange
1579
1865
  }) {
1580
- const containerRef = useRef5(null);
1581
- const hasAnimated = useRef5(false);
1866
+ const containerRef = useRef6(null);
1867
+ const hasAnimated = useRef6(false);
1582
1868
  const [pillStyle, setPillStyle] = useState4(null);
1583
- const measure = useCallback4(() => {
1869
+ const measure = useCallback5(() => {
1584
1870
  const container = containerRef.current;
1585
1871
  if (!container) return;
1586
1872
  const activeButton = container.querySelector('[data-active="true"]');
@@ -1645,47 +1931,118 @@ function Toggle({ label, checked, onChange, shortcut, shortcutActive }) {
1645
1931
  ] });
1646
1932
  }
1647
1933
 
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;
1934
+ // src/transition-math.ts
1935
+ function round2(value) {
1936
+ return Math.round(value * 100) / 100;
1937
+ }
1938
+ function clamp(value, min, max) {
1939
+ return Math.min(max, Math.max(min, value));
1940
+ }
1941
+ function isTransitionConfig(value) {
1942
+ return isSpringConfigValue(value) || isEasingConfigValue(value);
1943
+ }
1944
+ function isPhysicsSpring(transition) {
1945
+ return transition.type === "spring" && (transition.stiffness !== void 0 || transition.damping !== void 0 || transition.mass !== void 0);
1946
+ }
1947
+ function springParams(spring) {
1948
+ if (isPhysicsSpring(spring)) {
1949
+ return { stiffness: spring.stiffness ?? 200, damping: spring.damping ?? 25, mass: spring.mass ?? 1 };
1950
+ }
1951
+ const visualDuration = Math.max(0.05, spring.visualDuration ?? 0.3);
1952
+ const bounce = spring.bounce ?? 0.3;
1953
+ const root = 2 * Math.PI / (visualDuration * 1.2);
1954
+ const stiffness = root * root;
1955
+ const damping = 2 * Math.min(1, Math.max(0.05, 1 - bounce)) * Math.sqrt(stiffness);
1956
+ return { stiffness, damping, mass: 1 };
1957
+ }
1958
+ function springProgress(t, { stiffness, damping, mass }) {
1959
+ if (t <= 0) return 0;
1960
+ const w0 = Math.sqrt(stiffness / mass);
1961
+ const zeta = damping / (2 * Math.sqrt(stiffness * mass));
1962
+ if (zeta < 0.9999) {
1963
+ const wd2 = w0 * Math.sqrt(1 - zeta * zeta);
1964
+ return 1 - Math.exp(-zeta * w0 * t) * (Math.cos(wd2 * t) + zeta * w0 / wd2 * Math.sin(wd2 * t));
1965
+ }
1966
+ if (zeta < 1.0001) {
1967
+ return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
1968
+ }
1969
+ const wd = w0 * Math.sqrt(zeta * zeta - 1);
1970
+ const r1 = -zeta * w0 + wd;
1971
+ const r2 = -zeta * w0 - wd;
1972
+ return 1 + (r2 * Math.exp(r1 * t) - r1 * Math.exp(r2 * t)) / (r1 - r2);
1973
+ }
1974
+ function springSettleDuration(params) {
1975
+ const w0 = Math.sqrt(params.stiffness / params.mass);
1976
+ const zeta = params.damping / (2 * Math.sqrt(params.stiffness * params.mass));
1977
+ const decay = zeta >= 1 ? zeta * w0 - w0 * Math.sqrt(Math.max(0, zeta * zeta - 1)) : zeta * w0;
1978
+ const duration = Math.log(200) / Math.max(decay, 1e-6);
1979
+ return round2(clamp(duration, 0.05, 10));
1980
+ }
1981
+ function cubicBezierProgress(p, [x1, y1, x2, y2]) {
1982
+ if (p <= 0) return 0;
1983
+ if (p >= 1) return 1;
1984
+ const sampleX = (t2) => bezierAxis(t2, x1, x2);
1985
+ const sampleY = (t2) => bezierAxis(t2, y1, y2);
1986
+ let t = p;
1987
+ for (let i = 0; i < 8; i++) {
1988
+ const x = sampleX(t) - p;
1989
+ if (Math.abs(x) < 1e-5) return sampleY(t);
1990
+ const dx = bezierAxisDerivative(t, x1, x2);
1991
+ if (Math.abs(dx) < 1e-6) break;
1992
+ t -= x / dx;
1993
+ }
1994
+ let lo = 0;
1995
+ let hi = 1;
1996
+ t = p;
1997
+ while (hi - lo > 1e-5) {
1998
+ if (sampleX(t) < p) lo = t;
1999
+ else hi = t;
2000
+ t = (lo + hi) / 2;
2001
+ }
2002
+ return sampleY(t);
2003
+ }
2004
+ function bezierAxis(t, a1, a2) {
2005
+ return (1 - 3 * a2 + 3 * a1) * t * t * t + (3 * a2 - 6 * a1) * t * t + 3 * a1 * t;
2006
+ }
2007
+ function bezierAxisDerivative(t, a1, a2) {
2008
+ return 3 * (1 - 3 * a2 + 3 * a1) * t * t + 2 * (3 * a2 - 6 * a1) * t + 3 * a1;
2009
+ }
2010
+ function resolveClipTransition(raw, clipDuration) {
2011
+ const safeDuration = Math.max(0.05, clipDuration);
2012
+ if (raw.type === "easing") {
2013
+ return {
2014
+ transition: { ...raw, duration: safeDuration },
2015
+ duration: safeDuration,
2016
+ isPhysics: false
2017
+ };
1665
2018
  }
1666
- return points;
2019
+ if (isPhysicsSpring(raw)) {
2020
+ return {
2021
+ transition: raw,
2022
+ duration: springSettleDuration(springParams(raw)),
2023
+ isPhysics: true
2024
+ };
2025
+ }
2026
+ return {
2027
+ transition: { type: "spring", bounce: raw.bounce ?? 0.2, visualDuration: safeDuration },
2028
+ duration: safeDuration,
2029
+ isPhysics: false
2030
+ };
1667
2031
  }
2032
+
2033
+ // src/components/SpringVisualization.tsx
2034
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1668
2035
  function SpringVisualization({ spring, isSimpleMode }) {
1669
2036
  const width = 256;
1670
2037
  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
- }
2038
+ 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
2039
  const duration = 2;
1688
- const points = generateSpringCurve(stiffness, damping, mass, duration);
2040
+ const steps = 100;
2041
+ const points = [];
2042
+ for (let i = 0; i <= steps; i++) {
2043
+ const time = i / steps * duration;
2044
+ points.push([time, springProgress(time, params)]);
2045
+ }
1689
2046
  const values = points.map(([, value]) => value);
1690
2047
  const minValue = Math.min(...values);
1691
2048
  const maxValue = Math.max(...values);
@@ -1734,20 +2091,20 @@ function SpringVisualization({ spring, isSimpleMode }) {
1734
2091
  }
1735
2092
 
1736
2093
  // src/components/SpringControl.tsx
1737
- import { useCallback as useCallback5, useRef as useRef6, useSyncExternalStore as useSyncExternalStore2 } from "react";
2094
+ import { useCallback as useCallback6, useRef as useRef7, useSyncExternalStore as useSyncExternalStore2 } from "react";
1738
2095
  import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1739
2096
  function SpringControl({ panelId, path, label, spring, onChange }) {
1740
- const subscribe = useCallback5(
2097
+ const subscribe = useCallback6(
1741
2098
  (callback) => DialStore.subscribe(panelId, callback),
1742
2099
  [panelId]
1743
2100
  );
1744
- const getSnapshot = useCallback5(
2101
+ const getSnapshot = useCallback6(
1745
2102
  () => DialStore.getSpringMode(panelId, path),
1746
2103
  [panelId, path]
1747
2104
  );
1748
2105
  const mode = useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
1749
2106
  const isSimpleMode = mode === "simple";
1750
- const cache = useRef6({
2107
+ const cache = useRef7({
1751
2108
  simple: spring.visualDuration !== void 0 ? spring : { type: "spring", visualDuration: 0.3, bounce: 0.2 },
1752
2109
  advanced: spring.stiffness !== void 0 ? spring : { type: "spring", stiffness: 200, damping: 25, mass: 1 }
1753
2110
  });
@@ -1894,21 +2251,29 @@ function EasingVisualization({ easing }) {
1894
2251
  }
1895
2252
 
1896
2253
  // src/components/TransitionControl.tsx
1897
- import { useCallback as useCallback6, useRef as useRef7, useState as useState5, useSyncExternalStore as useSyncExternalStore3 } from "react";
2254
+ import { useCallback as useCallback7, useRef as useRef8, useState as useState5, useSyncExternalStore as useSyncExternalStore3 } from "react";
1898
2255
  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(
2256
+ function TransitionControl({
2257
+ panelId,
2258
+ path,
2259
+ label,
2260
+ value,
2261
+ onChange,
2262
+ hideDuration = false,
2263
+ durationControl
2264
+ }) {
2265
+ const subscribe = useCallback7(
1901
2266
  (callback) => DialStore.subscribe(panelId, callback),
1902
2267
  [panelId]
1903
2268
  );
1904
- const getSnapshot = useCallback6(
2269
+ const getSnapshot = useCallback7(
1905
2270
  () => DialStore.getTransitionMode(panelId, path),
1906
2271
  [panelId, path]
1907
2272
  );
1908
2273
  const mode = useSyncExternalStore3(subscribe, getSnapshot, getSnapshot);
1909
2274
  const isEasing = mode === "easing";
1910
2275
  const isSimpleSpring = mode === "simple";
1911
- const cache = useRef7({
2276
+ const cache = useRef8({
1912
2277
  easing: value.type === "easing" ? value : { type: "easing", duration: 0.3, ease: [1, -0.4, 0.5, 1] },
1913
2278
  simple: value.type === "spring" && value.visualDuration !== void 0 ? value : { type: "spring", visualDuration: 0.3, bounce: 0.2 },
1914
2279
  advanced: value.type === "spring" && value.stiffness !== void 0 ? value : { type: "spring", stiffness: 200, damping: 25, mass: 1 }
@@ -1946,6 +2311,21 @@ function TransitionControl({ panelId, path, label, value, onChange }) {
1946
2311
  newEase[index] = val;
1947
2312
  onChange({ ...easing, ease: newEase });
1948
2313
  };
2314
+ const durationSlider = !hideDuration && (isEasing || isSimpleSpring) ? /* @__PURE__ */ jsx9(
2315
+ Slider,
2316
+ {
2317
+ label: "Duration",
2318
+ value: durationControl?.value ?? (isEasing ? easing.duration : spring.visualDuration ?? 0.3),
2319
+ onChange: durationControl?.onChange ?? ((next) => {
2320
+ if (isEasing) onChange({ ...easing, duration: next });
2321
+ else handleSpringUpdate("visualDuration", next);
2322
+ }),
2323
+ min: durationControl?.min ?? 0.1,
2324
+ max: durationControl?.max ?? (isEasing ? 2 : 1),
2325
+ step: durationControl?.step ?? 0.05,
2326
+ unit: "s"
2327
+ }
2328
+ ) : null;
1949
2329
  return /* @__PURE__ */ jsx9(Folder, { title: label, defaultOpen: true, children: /* @__PURE__ */ jsxs8("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
1950
2330
  isEasing ? /* @__PURE__ */ jsx9(EasingVisualization, { easing }) : /* @__PURE__ */ jsx9(SpringVisualization, { spring, isSimpleMode: isSimpleSpring }),
1951
2331
  /* @__PURE__ */ jsxs8("div", { className: "dialkit-labeled-control", children: [
@@ -1968,16 +2348,13 @@ function TransitionControl({ panelId, path, label, value, onChange }) {
1968
2348
  /* @__PURE__ */ jsx9(Slider, { label: "y1", value: easing.ease[1], onChange: (v) => updateEase(1, v), min: -1, max: 2, step: 0.01 }),
1969
2349
  /* @__PURE__ */ jsx9(Slider, { label: "x2", value: easing.ease[2], onChange: (v) => updateEase(2, v), min: 0, max: 1, step: 0.01 }),
1970
2350
  /* @__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
2351
  /* @__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: [
2352
+ ] }) : 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
2353
  /* @__PURE__ */ jsx9(Slider, { label: "Stiffness", value: spring.stiffness ?? 400, onChange: (v) => handleSpringUpdate("stiffness", v), min: 1, max: 1e3, step: 10 }),
1978
2354
  /* @__PURE__ */ jsx9(Slider, { label: "Damping", value: spring.damping ?? 17, onChange: (v) => handleSpringUpdate("damping", v), min: 1, max: 100, step: 1 }),
1979
2355
  /* @__PURE__ */ jsx9(Slider, { label: "Mass", value: spring.mass ?? 1, onChange: (v) => handleSpringUpdate("mass", v), min: 0.1, max: 10, step: 0.1 })
1980
- ] })
2356
+ ] }),
2357
+ durationSlider
1981
2358
  ] }) });
1982
2359
  }
1983
2360
  function formatEase(ease) {
@@ -2044,7 +2421,7 @@ function TextControl({ label, value, onChange, placeholder }) {
2044
2421
  }
2045
2422
 
2046
2423
  // src/components/SelectControl.tsx
2047
- import { useState as useState6, useRef as useRef8, useEffect as useEffect5, useCallback as useCallback7 } from "react";
2424
+ import { useState as useState6, useRef as useRef9, useEffect as useEffect6, useCallback as useCallback8 } from "react";
2048
2425
  import { createPortal } from "react-dom";
2049
2426
  import { motion as motion3, AnimatePresence as AnimatePresence2 } from "motion/react";
2050
2427
 
@@ -2078,26 +2455,26 @@ function normalizeOptions(options) {
2078
2455
  }
2079
2456
  function SelectControl({ label, value, options, onChange }) {
2080
2457
  const [isOpen, setIsOpen] = useState6(false);
2081
- const triggerRef = useRef8(null);
2082
- const dropdownRef = useRef8(null);
2458
+ const triggerRef = useRef9(null);
2459
+ const dropdownRef = useRef9(null);
2083
2460
  const [portalTarget, setPortalTarget] = useState6(null);
2084
2461
  const [pos, setPos] = useState6(null);
2085
2462
  const normalized = normalizeOptions(options);
2086
2463
  const selectedOption = normalized.find((o) => o.value === value);
2087
- const updatePos = useCallback7(() => {
2464
+ const updatePos = useCallback8(() => {
2088
2465
  const el = triggerRef.current;
2089
2466
  if (!el || !portalTarget) return;
2090
2467
  const dropdownHeight = 8 + normalized.length * 36;
2091
2468
  setPos(getDropdownPosition(el, portalTarget, { dropdownHeight }));
2092
2469
  }, [normalized.length, portalTarget]);
2093
- useEffect5(() => {
2470
+ useEffect6(() => {
2094
2471
  setPortalTarget(getDialKitPortalRoot(triggerRef.current) ?? document.body);
2095
2472
  }, []);
2096
- useEffect5(() => {
2473
+ useEffect6(() => {
2097
2474
  if (!isOpen) return;
2098
2475
  updatePos();
2099
2476
  }, [isOpen, updatePos]);
2100
- useEffect5(() => {
2477
+ useEffect6(() => {
2101
2478
  if (!isOpen) return;
2102
2479
  const handleClick = (e) => {
2103
2480
  const target = e.target;
@@ -2177,14 +2554,14 @@ function SelectControl({ label, value, options, onChange }) {
2177
2554
  }
2178
2555
 
2179
2556
  // src/components/ColorControl.tsx
2180
- import { useState as useState7, useRef as useRef9, useEffect as useEffect6 } from "react";
2557
+ import { useState as useState7, useRef as useRef10, useEffect as useEffect7 } from "react";
2181
2558
  import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2182
2559
  var HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
2183
2560
  function ColorControl({ label, value, onChange }) {
2184
2561
  const [isEditing, setIsEditing] = useState7(false);
2185
2562
  const [editValue, setEditValue] = useState7(value);
2186
- const colorInputRef = useRef9(null);
2187
- useEffect6(() => {
2563
+ const colorInputRef = useRef10(null);
2564
+ useEffect7(() => {
2188
2565
  if (!isEditing) {
2189
2566
  setEditValue(value);
2190
2567
  }
@@ -2254,266 +2631,105 @@ function expandShorthandHex(hex) {
2254
2631
  return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
2255
2632
  }
2256
2633
 
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);
2634
+ // src/components/ControlRenderer.tsx
2635
+ import { Fragment as Fragment3, jsx as jsx13 } from "react/jsx-runtime";
2636
+ function ControlRenderer({ panelId, controls, values, transitionDuration }) {
2389
2637
  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
2638
  const renderControl = (control) => {
2424
2639
  const value = values[control.path];
2425
2640
  switch (control.type) {
2426
2641
  case "slider":
2427
- return /* @__PURE__ */ jsx14(
2642
+ return /* @__PURE__ */ jsx13(
2428
2643
  Slider,
2429
2644
  {
2430
2645
  label: control.label,
2431
2646
  value,
2432
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v),
2647
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v),
2433
2648
  min: control.min,
2434
2649
  max: control.max,
2435
2650
  step: control.step,
2436
2651
  shortcut: control.shortcut,
2437
- shortcutActive: shortcutCtx.activePanelId === panel.id && shortcutCtx.activePath === control.path
2652
+ shortcutActive: shortcutCtx.activePanelId === panelId && shortcutCtx.activePath === control.path
2438
2653
  },
2439
2654
  control.path
2440
2655
  );
2441
2656
  case "toggle":
2442
- return /* @__PURE__ */ jsx14(
2657
+ return /* @__PURE__ */ jsx13(
2443
2658
  Toggle,
2444
2659
  {
2445
2660
  label: control.label,
2446
2661
  checked: value,
2447
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v),
2662
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v),
2448
2663
  shortcut: control.shortcut,
2449
- shortcutActive: shortcutCtx.activePanelId === panel.id && shortcutCtx.activePath === control.path
2664
+ shortcutActive: shortcutCtx.activePanelId === panelId && shortcutCtx.activePath === control.path
2450
2665
  },
2451
2666
  control.path
2452
2667
  );
2453
2668
  case "spring":
2454
- return /* @__PURE__ */ jsx14(
2669
+ return /* @__PURE__ */ jsx13(
2455
2670
  SpringControl,
2456
2671
  {
2457
- panelId: panel.id,
2672
+ panelId,
2458
2673
  path: control.path,
2459
2674
  label: control.label,
2460
2675
  spring: value,
2461
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
2676
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v)
2462
2677
  },
2463
2678
  control.path
2464
2679
  );
2465
2680
  case "transition":
2466
- return /* @__PURE__ */ jsx14(
2681
+ return /* @__PURE__ */ jsx13(
2467
2682
  TransitionControl,
2468
2683
  {
2469
- panelId: panel.id,
2684
+ panelId,
2470
2685
  path: control.path,
2471
2686
  label: control.label,
2472
2687
  value,
2473
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
2688
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v),
2689
+ durationControl: transitionDuration
2474
2690
  },
2475
2691
  control.path
2476
2692
  );
2477
2693
  case "folder":
2478
- return /* @__PURE__ */ jsx14(Folder, { title: control.label, defaultOpen: control.defaultOpen ?? true, children: control.children?.map(renderControl) }, control.path);
2694
+ return /* @__PURE__ */ jsx13(Folder, { title: control.label, defaultOpen: control.defaultOpen ?? true, children: control.children?.map(renderControl) }, control.path);
2479
2695
  case "text":
2480
- return /* @__PURE__ */ jsx14(
2696
+ return /* @__PURE__ */ jsx13(
2481
2697
  TextControl,
2482
2698
  {
2483
2699
  label: control.label,
2484
2700
  value,
2485
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v),
2701
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v),
2486
2702
  placeholder: control.placeholder
2487
2703
  },
2488
2704
  control.path
2489
2705
  );
2490
2706
  case "select":
2491
- return /* @__PURE__ */ jsx14(
2707
+ return /* @__PURE__ */ jsx13(
2492
2708
  SelectControl,
2493
2709
  {
2494
2710
  label: control.label,
2495
2711
  value,
2496
2712
  options: control.options ?? [],
2497
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
2713
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v)
2498
2714
  },
2499
2715
  control.path
2500
2716
  );
2501
2717
  case "color":
2502
- return /* @__PURE__ */ jsx14(
2718
+ return /* @__PURE__ */ jsx13(
2503
2719
  ColorControl,
2504
2720
  {
2505
2721
  label: control.label,
2506
2722
  value,
2507
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
2723
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v)
2508
2724
  },
2509
2725
  control.path
2510
2726
  );
2511
2727
  case "action":
2512
- return /* @__PURE__ */ jsx14(
2728
+ return /* @__PURE__ */ jsx13(
2513
2729
  "button",
2514
2730
  {
2515
2731
  className: "dialkit-button",
2516
- onClick: () => DialStore.triggerAction(panel.id, control.path),
2732
+ onClick: () => DialStore.triggerAction(panelId, control.path),
2517
2733
  children: control.label
2518
2734
  },
2519
2735
  control.path
@@ -2522,12 +2738,170 @@ Apply these values as the new defaults in the useDialKit call.`;
2522
2738
  return null;
2523
2739
  }
2524
2740
  };
2525
- const renderControls = () => {
2526
- return panel.controls.map(renderControl);
2741
+ return /* @__PURE__ */ jsx13(Fragment3, { children: controls.map(renderControl) });
2742
+ }
2743
+
2744
+ // src/components/PresetManager.tsx
2745
+ import { useState as useState8, useRef as useRef11, useEffect as useEffect8, useCallback as useCallback9 } from "react";
2746
+ import { createPortal as createPortal2 } from "react-dom";
2747
+ import { motion as motion4, AnimatePresence as AnimatePresence3 } from "motion/react";
2748
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2749
+ function PresetManager({ panelId, presets, activePresetId, onAdd }) {
2750
+ const [isOpen, setIsOpen] = useState8(false);
2751
+ const triggerRef = useRef11(null);
2752
+ const dropdownRef = useRef11(null);
2753
+ const [pos, setPos] = useState8({ top: 0, left: 0, width: 0 });
2754
+ const hasPresets = presets.length > 0;
2755
+ const activePreset = presets.find((p) => p.id === activePresetId);
2756
+ const open = useCallback9(() => {
2757
+ if (!hasPresets) return;
2758
+ const rect = triggerRef.current?.getBoundingClientRect();
2759
+ if (rect) {
2760
+ setPos({ top: rect.bottom + 4, left: rect.left, width: rect.width });
2761
+ }
2762
+ setIsOpen(true);
2763
+ }, [hasPresets]);
2764
+ const close = useCallback9(() => setIsOpen(false), []);
2765
+ const toggle = useCallback9(() => {
2766
+ if (isOpen) close();
2767
+ else open();
2768
+ }, [isOpen, open, close]);
2769
+ useEffect8(() => {
2770
+ if (!isOpen) return;
2771
+ const handler = (e) => {
2772
+ const target = e.target;
2773
+ if (triggerRef.current?.contains(target) || dropdownRef.current?.contains(target)) return;
2774
+ close();
2775
+ };
2776
+ document.addEventListener("mousedown", handler);
2777
+ return () => document.removeEventListener("mousedown", handler);
2778
+ }, [isOpen, close]);
2779
+ const handleSelect = (presetId) => {
2780
+ if (presetId) {
2781
+ DialStore.loadPreset(panelId, presetId);
2782
+ } else {
2783
+ DialStore.clearActivePreset(panelId);
2784
+ }
2785
+ close();
2786
+ };
2787
+ const handleDelete = (e, presetId) => {
2788
+ e.stopPropagation();
2789
+ DialStore.deletePreset(panelId, presetId);
2790
+ };
2791
+ return /* @__PURE__ */ jsxs12("div", { className: "dialkit-preset-manager", children: [
2792
+ /* @__PURE__ */ jsxs12(
2793
+ "button",
2794
+ {
2795
+ ref: triggerRef,
2796
+ className: "dialkit-preset-trigger",
2797
+ onClick: toggle,
2798
+ "data-open": String(isOpen),
2799
+ "data-has-preset": String(!!activePreset),
2800
+ "data-disabled": String(!hasPresets),
2801
+ children: [
2802
+ /* @__PURE__ */ jsx14("span", { className: "dialkit-preset-label", children: activePreset ? activePreset.name : "Version 1" }),
2803
+ /* @__PURE__ */ jsx14(
2804
+ motion4.svg,
2805
+ {
2806
+ className: "dialkit-select-chevron",
2807
+ viewBox: "0 0 24 24",
2808
+ fill: "none",
2809
+ stroke: "currentColor",
2810
+ strokeWidth: "2.5",
2811
+ strokeLinecap: "round",
2812
+ strokeLinejoin: "round",
2813
+ animate: { rotate: isOpen ? 180 : 0, opacity: hasPresets ? 0.6 : 0.25 },
2814
+ transition: { type: "spring", visualDuration: 0.2, bounce: 0.15 },
2815
+ children: /* @__PURE__ */ jsx14("path", { d: ICON_CHEVRON })
2816
+ }
2817
+ )
2818
+ ]
2819
+ }
2820
+ ),
2821
+ createPortal2(
2822
+ /* @__PURE__ */ jsx14(AnimatePresence3, { children: isOpen && /* @__PURE__ */ jsxs12(
2823
+ motion4.div,
2824
+ {
2825
+ ref: dropdownRef,
2826
+ className: "dialkit-root dialkit-preset-dropdown",
2827
+ style: { position: "fixed", top: pos.top, left: pos.left, minWidth: pos.width },
2828
+ initial: { opacity: 0, y: 4, scale: 0.97 },
2829
+ animate: { opacity: 1, y: 0, scale: 1 },
2830
+ exit: { opacity: 0, y: 4, scale: 0.97, pointerEvents: "none" },
2831
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
2832
+ children: [
2833
+ /* @__PURE__ */ jsx14(
2834
+ "div",
2835
+ {
2836
+ className: "dialkit-preset-item",
2837
+ "data-active": String(!activePresetId),
2838
+ onClick: () => handleSelect(null),
2839
+ children: /* @__PURE__ */ jsx14("span", { className: "dialkit-preset-name", children: "Version 1" })
2840
+ }
2841
+ ),
2842
+ presets.map((preset) => /* @__PURE__ */ jsxs12(
2843
+ "div",
2844
+ {
2845
+ className: "dialkit-preset-item",
2846
+ "data-active": String(preset.id === activePresetId),
2847
+ onClick: () => handleSelect(preset.id),
2848
+ children: [
2849
+ /* @__PURE__ */ jsx14("span", { className: "dialkit-preset-name", children: preset.name }),
2850
+ /* @__PURE__ */ jsx14(
2851
+ "button",
2852
+ {
2853
+ className: "dialkit-preset-delete",
2854
+ onClick: (e) => handleDelete(e, preset.id),
2855
+ title: "Delete preset",
2856
+ 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)) })
2857
+ }
2858
+ )
2859
+ ]
2860
+ },
2861
+ preset.id
2862
+ ))
2863
+ ]
2864
+ }
2865
+ ) }),
2866
+ document.body
2867
+ )
2868
+ ] });
2869
+ }
2870
+
2871
+ // src/components/Panel.tsx
2872
+ import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
2873
+ function Panel({ panel, defaultOpen = true, inline = false, onOpenChange, variant = "root", toolbarExtra }) {
2874
+ const [copied, setCopied] = useState9(false);
2875
+ const [isPanelOpen, setIsPanelOpen] = useState9(defaultOpen);
2876
+ const hasShortcuts = Object.keys(panel.shortcuts).length > 0;
2877
+ const subscribe = useCallback10(
2878
+ (callback) => DialStore.subscribe(panel.id, callback),
2879
+ [panel.id]
2880
+ );
2881
+ const getSnapshot = useCallback10(
2882
+ () => DialStore.getValues(panel.id),
2883
+ [panel.id]
2884
+ );
2885
+ const values = useSyncExternalStore4(subscribe, getSnapshot, getSnapshot);
2886
+ const presets = DialStore.getPresets(panel.id);
2887
+ const activePresetId = DialStore.getActivePresetId(panel.id);
2888
+ const handleAddPreset = () => {
2889
+ const nextNum = presets.length + 2;
2890
+ DialStore.savePreset(panel.id, `Version ${nextNum}`);
2891
+ };
2892
+ const handleCopy = () => {
2893
+ navigator.clipboard.writeText(buildCopyInstruction("useDialKit", panel.name, values));
2894
+ setCopied(true);
2895
+ setTimeout(() => setCopied(false), 1500);
2527
2896
  };
2897
+ const handleOpenChange = useCallback10((open) => {
2898
+ setIsPanelOpen(open);
2899
+ onOpenChange?.(open);
2900
+ }, [onOpenChange]);
2901
+ const renderControls = () => /* @__PURE__ */ jsx15(ControlRenderer, { panelId: panel.id, controls: panel.controls, values });
2528
2902
  const iconTransition = { type: "spring", visualDuration: 0.4, bounce: 0.1 };
2529
- const toolbar = /* @__PURE__ */ jsxs13(Fragment3, { children: [
2530
- /* @__PURE__ */ jsx14(
2903
+ const toolbar = /* @__PURE__ */ jsxs13(Fragment4, { children: [
2904
+ /* @__PURE__ */ jsx15(
2531
2905
  motion5.button,
2532
2906
  {
2533
2907
  className: "dialkit-toolbar-add",
@@ -2535,10 +2909,10 @@ Apply these values as the new defaults in the useDialKit call.`;
2535
2909
  title: "Add preset",
2536
2910
  whileTap: { scale: 0.9 },
2537
2911
  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)) })
2912
+ 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
2913
  }
2540
2914
  ),
2541
- /* @__PURE__ */ jsx14(
2915
+ /* @__PURE__ */ jsx15(
2542
2916
  PresetManager,
2543
2917
  {
2544
2918
  panelId: panel.id,
@@ -2547,7 +2921,7 @@ Apply these values as the new defaults in the useDialKit call.`;
2547
2921
  onAdd: handleAddPreset
2548
2922
  }
2549
2923
  ),
2550
- /* @__PURE__ */ jsx14(
2924
+ /* @__PURE__ */ jsx15(
2551
2925
  motion5.button,
2552
2926
  {
2553
2927
  className: "dialkit-toolbar-add",
@@ -2555,7 +2929,7 @@ Apply these values as the new defaults in the useDialKit call.`;
2555
2929
  title: "Copy parameters",
2556
2930
  whileTap: { scale: 0.9 },
2557
2931
  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(
2932
+ children: /* @__PURE__ */ jsx15("span", { style: { position: "relative", width: 16, height: 16 }, children: /* @__PURE__ */ jsx15(AnimatePresence4, { initial: false, mode: "wait", children: copied ? /* @__PURE__ */ jsx15(
2559
2933
  motion5.svg,
2560
2934
  {
2561
2935
  viewBox: "0 0 24 24",
@@ -2569,7 +2943,7 @@ Apply these values as the new defaults in the useDialKit call.`;
2569
2943
  animate: { scale: 1, opacity: 1 },
2570
2944
  exit: { scale: 0.8, opacity: 0 },
2571
2945
  transition: { duration: 0.08 },
2572
- children: /* @__PURE__ */ jsx14("path", { d: ICON_CHECK })
2946
+ children: /* @__PURE__ */ jsx15("path", { d: ICON_CHECK })
2573
2947
  },
2574
2948
  "check"
2575
2949
  ) : /* @__PURE__ */ jsxs13(
@@ -2583,23 +2957,116 @@ Apply these values as the new defaults in the useDialKit call.`;
2583
2957
  exit: { scale: 0.8, opacity: 0 },
2584
2958
  transition: { duration: 0.08 },
2585
2959
  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" })
2960
+ /* @__PURE__ */ jsx15("path", { d: ICON_CLIPBOARD.board, stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round" }),
2961
+ /* @__PURE__ */ jsx15("path", { d: ICON_CLIPBOARD.sparkle, fill: "currentColor" }),
2962
+ /* @__PURE__ */ jsx15("path", { d: ICON_CLIPBOARD.body, stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })
2589
2963
  ]
2590
2964
  },
2591
2965
  "clipboard"
2592
2966
  ) }) })
2593
2967
  }
2594
- )
2968
+ ),
2969
+ toolbarExtra
2595
2970
  ] });
2596
2971
  if (variant === "section") {
2597
2972
  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 }),
2973
+ /* @__PURE__ */ jsx15("div", { className: "dialkit-panel-section-toolbar", onClick: (e) => e.stopPropagation(), children: toolbar }),
2599
2974
  renderControls()
2600
2975
  ] });
2601
2976
  }
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() }) });
2977
+ return /* @__PURE__ */ jsx15("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ jsx15(Folder, { title: panel.name, defaultOpen, isRoot: true, inline, onOpenChange: handleOpenChange, toolbar, children: renderControls() }) });
2978
+ }
2979
+
2980
+ // src/components/Timeline/TimelineToggleButton.tsx
2981
+ import { useCallback as useCallback11, useSyncExternalStore as useSyncExternalStore5 } from "react";
2982
+ import { motion as motion6 } from "motion/react";
2983
+
2984
+ // src/store/TimelineUiStore.ts
2985
+ var TimelineUiStoreClass = class {
2986
+ constructor() {
2987
+ this.visible = true;
2988
+ this.initialized = false;
2989
+ this.controllers = /* @__PURE__ */ new Map();
2990
+ this.listeners = /* @__PURE__ */ new Set();
2991
+ }
2992
+ getVisible() {
2993
+ for (const controller of this.controllers.values()) {
2994
+ if (controller.visible !== void 0) return controller.visible;
2995
+ }
2996
+ return this.visible;
2997
+ }
2998
+ registerController(id, controller) {
2999
+ const previous = this.getVisible();
3000
+ if (!this.initialized) {
3001
+ this.visible = controller.defaultVisible;
3002
+ this.initialized = true;
3003
+ }
3004
+ this.controllers.set(id, controller);
3005
+ if (previous !== this.getVisible()) this.notify();
3006
+ return () => {
3007
+ const before = this.getVisible();
3008
+ this.controllers.delete(id);
3009
+ if (this.controllers.size === 0) this.initialized = false;
3010
+ if (before !== this.getVisible()) this.notify();
3011
+ };
3012
+ }
3013
+ updateController(id, controller) {
3014
+ if (!this.controllers.has(id)) return;
3015
+ const previous = this.getVisible();
3016
+ this.controllers.set(id, controller);
3017
+ if (previous !== this.getVisible()) this.notify();
3018
+ }
3019
+ requestVisible(visible) {
3020
+ const current = this.getVisible();
3021
+ if (current === visible) return;
3022
+ const controlled = Array.from(this.controllers.values()).filter(
3023
+ (controller) => controller.visible !== void 0
3024
+ );
3025
+ if (controlled.length > 0) {
3026
+ controlled.forEach((controller) => controller.onVisibilityChange?.(visible));
3027
+ return;
3028
+ }
3029
+ this.visible = visible;
3030
+ this.controllers.forEach((controller) => controller.onVisibilityChange?.(visible));
3031
+ this.notify();
3032
+ }
3033
+ toggle() {
3034
+ this.requestVisible(!this.getVisible());
3035
+ }
3036
+ subscribe(listener) {
3037
+ this.listeners.add(listener);
3038
+ return () => this.listeners.delete(listener);
3039
+ }
3040
+ notify() {
3041
+ this.listeners.forEach((listener) => listener());
3042
+ }
3043
+ };
3044
+ var TimelineUiStore = /* @__PURE__ */ new TimelineUiStoreClass();
3045
+
3046
+ // src/components/Timeline/TimelineToggleButton.tsx
3047
+ import { jsx as jsx16 } from "react/jsx-runtime";
3048
+ function TimelineToggleButton() {
3049
+ const subscribe = useCallback11(
3050
+ (listener) => TimelineUiStore.subscribe(listener),
3051
+ []
3052
+ );
3053
+ const getVisible = useCallback11(() => TimelineUiStore.getVisible(), []);
3054
+ const visible = useSyncExternalStore5(subscribe, getVisible, getVisible);
3055
+ const label = visible ? "Hide timeline" : "Show timeline";
3056
+ return /* @__PURE__ */ jsx16(
3057
+ motion6.button,
3058
+ {
3059
+ className: "dialkit-toolbar-add dialkit-timeline-toolbar-toggle",
3060
+ "data-active": visible || void 0,
3061
+ "aria-pressed": visible,
3062
+ "aria-label": label,
3063
+ title: label,
3064
+ onClick: () => TimelineUiStore.toggle(),
3065
+ whileTap: { scale: 0.9 },
3066
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
3067
+ 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)) })
3068
+ }
3069
+ );
2603
3070
  }
2604
3071
 
2605
3072
  // src/panel-drag.ts
@@ -2667,32 +3134,39 @@ function blockPanelDragClick(handle) {
2667
3134
  }
2668
3135
 
2669
3136
  // 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;
3137
+ import { jsx as jsx17 } from "react/jsx-runtime";
2672
3138
  function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover", theme = "system", productionEnabled = isDevDefault, onOpenChange }) {
2673
3139
  if (!productionEnabled) return null;
2674
3140
  const [panels, setPanels] = useState10([]);
3141
+ const [timelineCount, setTimelineCount] = useState10(0);
2675
3142
  const [mounted, setMounted] = useState10(false);
2676
3143
  const inline = mode === "inline";
2677
- const panelRef = useRef11(null);
3144
+ const panelRef = useRef12(null);
2678
3145
  const [dragOffset, setDragOffset] = useState10(null);
2679
3146
  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(() => {
3147
+ const lastDragOffset = useRef12(null);
3148
+ const draggingRef = useRef12(false);
3149
+ const dragStartRef = useRef12(null);
3150
+ const didDragRef = useRef12(false);
3151
+ const dragTargetRef = useRef12(null);
3152
+ const panelOpenStatesRef = useRef12(/* @__PURE__ */ new Map());
3153
+ const rootOpenRef = useRef12(null);
3154
+ useEffect9(() => {
2688
3155
  setMounted(true);
2689
- setPanels(DialStore.getPanels());
2690
- const unsubscribe = DialStore.subscribeGlobal(() => {
2691
- setPanels(DialStore.getPanels());
3156
+ setPanels(DialStore.getPanels("panel"));
3157
+ setTimelineCount(TimelineStore.getTimelines().length);
3158
+ const unsubscribePanels = DialStore.subscribeGlobal(() => {
3159
+ setPanels(DialStore.getPanels("panel"));
3160
+ });
3161
+ const unsubscribeTimelines = TimelineStore.subscribeGlobal(() => {
3162
+ setTimelineCount(TimelineStore.getTimelines().length);
2692
3163
  });
2693
- return unsubscribe;
3164
+ return () => {
3165
+ unsubscribePanels();
3166
+ unsubscribeTimelines();
3167
+ };
2694
3168
  }, []);
2695
- useEffect8(() => {
3169
+ useEffect9(() => {
2696
3170
  const fallbackOpen = inline || defaultOpen;
2697
3171
  const nextStates = /* @__PURE__ */ new Map();
2698
3172
  for (const panel of panels) {
@@ -2701,7 +3175,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2701
3175
  panelOpenStatesRef.current = nextStates;
2702
3176
  rootOpenRef.current = Array.from(nextStates.values()).some(Boolean);
2703
3177
  }, [defaultOpen, inline, panels]);
2704
- useEffect8(() => {
3178
+ useEffect9(() => {
2705
3179
  if (!panelRef.current || inline) return;
2706
3180
  const observer = new MutationObserver(() => {
2707
3181
  const inners = panelRef.current?.querySelectorAll(".dialkit-panel-inner");
@@ -2729,7 +3203,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2729
3203
  observer.observe(panelRef.current, { subtree: true, attributes: true, attributeFilter: ["data-collapsed"] });
2730
3204
  return () => observer.disconnect();
2731
3205
  }, [inline, dragOffset, position]);
2732
- const handlePointerDown = useCallback10((e) => {
3206
+ const handlePointerDown = useCallback12((e) => {
2733
3207
  const panel = panelRef.current;
2734
3208
  const handle = getPanelDragHandle(e.target, panel);
2735
3209
  if (!panel || !handle) return;
@@ -2739,13 +3213,13 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2739
3213
  draggingRef.current = true;
2740
3214
  handle.setPointerCapture(e.pointerId);
2741
3215
  }, []);
2742
- const handlePointerMove = useCallback10((e) => {
3216
+ const handlePointerMove = useCallback12((e) => {
2743
3217
  if (!draggingRef.current || !dragStartRef.current) return;
2744
3218
  if (!didDragRef.current && !hasPanelDragMoved(dragStartRef.current, e.clientX, e.clientY)) return;
2745
3219
  didDragRef.current = true;
2746
3220
  setDragOffset(getPanelDragOffset(dragStartRef.current, e.clientX, e.clientY));
2747
3221
  }, []);
2748
- const handlePointerUp = useCallback10((e) => {
3222
+ const handlePointerUp = useCallback12((e) => {
2749
3223
  if (!draggingRef.current) return;
2750
3224
  draggingRef.current = false;
2751
3225
  dragStartRef.current = null;
@@ -2761,7 +3235,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2761
3235
  }
2762
3236
  dragTargetRef.current = null;
2763
3237
  }, []);
2764
- const handlePanelOpenChange = useCallback10((panelId, open) => {
3238
+ const handlePanelOpenChange = useCallback12((panelId, open) => {
2765
3239
  panelOpenStatesRef.current.set(panelId, open);
2766
3240
  const fallbackOpen = inline || defaultOpen;
2767
3241
  const nextRootOpen = panels.some((panel) => panelOpenStatesRef.current.get(panel.id) ?? fallbackOpen);
@@ -2769,7 +3243,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2769
3243
  rootOpenRef.current = nextRootOpen;
2770
3244
  onOpenChange?.(nextRootOpen);
2771
3245
  }, [defaultOpen, inline, onOpenChange, panels]);
2772
- const handleRootOpenChange = useCallback10((open) => {
3246
+ const handleRootOpenChange = useCallback12((open) => {
2773
3247
  if (rootOpenRef.current === open) return;
2774
3248
  rootOpenRef.current = open;
2775
3249
  onOpenChange?.(open);
@@ -2777,7 +3251,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2777
3251
  if (!mounted || typeof window === "undefined") {
2778
3252
  return null;
2779
3253
  }
2780
- if (panels.length === 0) {
3254
+ if (panels.length === 0 && timelineCount === 0) {
2781
3255
  return null;
2782
3256
  }
2783
3257
  const dragStyle = dragOffset ? {
@@ -2788,7 +3262,8 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2788
3262
  } : void 0;
2789
3263
  const originX = getPanelOriginX(activePosition, dragOffset);
2790
3264
  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(
3265
+ const timelineToggle = timelineCount > 0 ? /* @__PURE__ */ jsx17(TimelineToggleButton, {}) : null;
3266
+ const content = /* @__PURE__ */ jsx17(ShortcutListener, { children: /* @__PURE__ */ jsx17("div", { className: "dialkit-root", "data-mode": mode, "data-theme": theme, children: /* @__PURE__ */ jsx17(
2792
3267
  "div",
2793
3268
  {
2794
3269
  ref: panelRef,
@@ -2802,7 +3277,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2802
3277
  onPointerMove: !inline ? handlePointerMove : void 0,
2803
3278
  onPointerUp: !inline ? handlePointerUp : void 0,
2804
3279
  onPointerCancel: !inline ? handlePointerUp : void 0,
2805
- children: hasMultiplePanels ? /* @__PURE__ */ jsx15("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ jsx15(
3280
+ children: panels.length === 0 ? /* @__PURE__ */ jsx17("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ jsx17(
2806
3281
  Folder,
2807
3282
  {
2808
3283
  title: "DialKit",
@@ -2810,23 +3285,37 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2810
3285
  isRoot: true,
2811
3286
  inline,
2812
3287
  onOpenChange: handleRootOpenChange,
3288
+ toolbar: timelineToggle,
2813
3289
  panelHeightOffset: 2,
2814
- children: panels.map((panel) => /* @__PURE__ */ jsx15(
2815
- Panel,
2816
- {
2817
- panel,
2818
- defaultOpen: true,
2819
- variant: "section"
2820
- },
2821
- panel.id
2822
- ))
3290
+ children: /* @__PURE__ */ jsx17("div", { className: "dialkit-timeline-toolkit-only", children: "Timeline" })
2823
3291
  }
2824
- ) }) : panels.map((panel) => /* @__PURE__ */ jsx15(
3292
+ ) }) : hasMultiplePanels ? /* @__PURE__ */ jsx17("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ jsx17(
3293
+ Folder,
3294
+ {
3295
+ title: "DialKit",
3296
+ defaultOpen: inline || defaultOpen,
3297
+ isRoot: true,
3298
+ inline,
3299
+ onOpenChange: handleRootOpenChange,
3300
+ toolbar: timelineToggle,
3301
+ panelHeightOffset: 2,
3302
+ children: panels.map((panel) => /* @__PURE__ */ jsx17(
3303
+ Panel,
3304
+ {
3305
+ panel,
3306
+ defaultOpen: true,
3307
+ variant: "section"
3308
+ },
3309
+ panel.id
3310
+ ))
3311
+ }
3312
+ ) }) : panels.map((panel) => /* @__PURE__ */ jsx17(
2825
3313
  Panel,
2826
3314
  {
2827
3315
  panel,
2828
3316
  defaultOpen: inline || defaultOpen,
2829
3317
  inline,
3318
+ toolbarExtra: timelineToggle,
2830
3319
  onOpenChange: (open) => handlePanelOpenChange(panel.id, open)
2831
3320
  },
2832
3321
  panel.id
@@ -2839,10 +3328,2149 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2839
3328
  return createPortal3(content, document.body);
2840
3329
  }
2841
3330
 
3331
+ // src/hooks/useDialTimeline.ts
3332
+ import { useCallback as useCallback13, useEffect as useEffect10, useMemo as useMemo2, useRef as useRef13, useSyncExternalStore as useSyncExternalStore6 } from "react";
3333
+
3334
+ // src/timeline-core.ts
3335
+ var CLIP_VALUE_STEP = 0.01;
3336
+ var TIMELINE_MIN_CLIP_DURATION = 0.05;
3337
+ var DEFAULT_STEP_DURATION = 0.3;
3338
+ var DEFAULT_CLIP_TRANSITION = { type: "spring", bounce: 0.2 };
3339
+ var RESERVED_KEYS = /* @__PURE__ */ new Set(["time", "playing", "duration", "play", "pause", "replay", "seek"]);
3340
+ function isClipConfig(value) {
3341
+ return isPlainObject(value) && Number.isFinite(value.at);
3342
+ }
3343
+ function isGroupConfig(value) {
3344
+ if (!isPlainObject(value) || "at" in value) return false;
3345
+ const entries = Object.values(value);
3346
+ return entries.length > 0 && entries.some(isClipConfig);
3347
+ }
3348
+ function isPlainObject(value) {
3349
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3350
+ }
3351
+ function nonNegativeFinite(value, fallback = 0) {
3352
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : fallback;
3353
+ }
3354
+ function animatedDuration(value, fallback = DEFAULT_STEP_DURATION) {
3355
+ return Math.max(TIMELINE_MIN_CLIP_DURATION, nonNegativeFinite(value, fallback));
3356
+ }
3357
+ function transitionDefaultDuration(transition) {
3358
+ if (transition.type === "easing") return animatedDuration(transition.duration);
3359
+ if (isPhysicsSpring(transition)) {
3360
+ return animatedDuration(springSettleDuration(springParams(transition)));
3361
+ }
3362
+ if (transition.visualDuration !== void 0) return animatedDuration(transition.visualDuration);
3363
+ return animatedDuration(springSettleDuration(springParams(transition)));
3364
+ }
3365
+ function defaultStepDuration(step, inheritedTransition) {
3366
+ const curve = step.transition ?? inheritedTransition;
3367
+ if (curve && isPhysicsSpring(curve)) return transitionDefaultDuration(curve);
3368
+ if (step.duration !== void 0) return animatedDuration(step.duration);
3369
+ if (step.transition) return transitionDefaultDuration(step.transition);
3370
+ return DEFAULT_STEP_DURATION;
3371
+ }
3372
+ function defaultTrackDuration(track, inheritedTransition) {
3373
+ const curve = track.transition ?? inheritedTransition;
3374
+ if (track.steps?.length) {
3375
+ return track.steps.reduce((sum, step) => sum + defaultStepDuration(step, curve), 0);
3376
+ }
3377
+ if (curve && isPhysicsSpring(curve)) return transitionDefaultDuration(curve);
3378
+ if (track.duration !== void 0) return animatedDuration(track.duration);
3379
+ if (track.transition) return transitionDefaultDuration(track.transition);
3380
+ return DEFAULT_STEP_DURATION;
3381
+ }
3382
+ function defaultClipDuration(clip) {
3383
+ const defaultCurve = isTransitionConfig(clip.transition) ? clip.transition : DEFAULT_CLIP_TRANSITION;
3384
+ if (clip.props) {
3385
+ return Object.values(clip.props).reduce(
3386
+ (max, track) => Math.max(
3387
+ max,
3388
+ nonNegativeFinite(track.delay) + defaultTrackDuration(track, defaultCurve)
3389
+ ),
3390
+ 0
3391
+ );
3392
+ }
3393
+ if (clip.steps?.length) {
3394
+ return clip.steps.reduce((sum, step) => sum + defaultStepDuration(step, defaultCurve), 0);
3395
+ }
3396
+ const animating = Boolean(clip.transition || clip.from || clip.to);
3397
+ if (!animating) return nonNegativeFinite(clip.duration);
3398
+ if (isPhysicsSpring(defaultCurve)) return transitionDefaultDuration(defaultCurve);
3399
+ if (clip.duration !== void 0) return animatedDuration(clip.duration);
3400
+ if (isTransitionConfig(clip.transition)) return transitionDefaultDuration(clip.transition);
3401
+ return clip.from || clip.to ? transitionDefaultDuration(DEFAULT_CLIP_TRANSITION) : 0;
3402
+ }
3403
+ function normalizeLoopMode(value) {
3404
+ if (value === true || value === "mirror" || value === "repeat") return "repeat";
3405
+ return "off";
3406
+ }
3407
+ function normalizeStoredTransition(transition, clipDuration) {
3408
+ if (transition.type === "easing") {
3409
+ return { ...transition, duration: clipDuration };
3410
+ }
3411
+ if (isPhysicsSpring(transition)) {
3412
+ return transition;
3413
+ }
3414
+ return { type: "spring", bounce: transition.bounce ?? 0.2 };
3415
+ }
3416
+ function collectClipEntries(config) {
3417
+ const entries = [];
3418
+ for (const [key, value] of Object.entries(config)) {
3419
+ if (key === "duration") continue;
3420
+ if (RESERVED_KEYS.has(key)) {
3421
+ console.warn(`[dialkit] Timeline key "${key}" collides with a reserved key and was skipped.`);
3422
+ continue;
3423
+ }
3424
+ if (isClipConfig(value)) {
3425
+ entries.push({ path: key, childKey: key, clip: value });
3426
+ } else if (isGroupConfig(value)) {
3427
+ for (const [childKey, childClip] of Object.entries(value)) {
3428
+ if (isClipConfig(childClip)) {
3429
+ entries.push({ path: `${key}.${childKey}`, childKey, group: key, clip: childClip });
3430
+ } else {
3431
+ console.warn(
3432
+ `[dialkit] Timeline clip "${key}.${childKey}" is missing a numeric "at" and was skipped.`
3433
+ );
3434
+ }
3435
+ }
3436
+ } else {
3437
+ console.warn(
3438
+ `[dialkit] Timeline entry "${key}" is neither a clip (needs a numeric "at") nor a group of clips and was skipped.`
3439
+ );
3440
+ }
3441
+ }
3442
+ return entries;
3443
+ }
3444
+ function definedValues(values) {
3445
+ if (!values) return void 0;
3446
+ const result = {};
3447
+ for (const [key, value] of Object.entries(values)) {
3448
+ if (value !== void 0) result[key] = value;
3449
+ }
3450
+ return result;
3451
+ }
3452
+ function setDialPath(dialConfig, path, value) {
3453
+ const segments = path.split(".");
3454
+ let node = dialConfig;
3455
+ for (const segment of segments.slice(0, -1)) {
3456
+ node = node[segment] ?? (node[segment] = {});
3457
+ }
3458
+ node[segments[segments.length - 1]] = value;
3459
+ }
3460
+ function parseTimelineConfig(config) {
3461
+ const entries = collectClipEntries(config);
3462
+ let maxEnd = 0;
3463
+ for (const { clip } of entries) {
3464
+ maxEnd = Math.max(maxEnd, nonNegativeFinite(clip.at) + defaultClipDuration(clip));
3465
+ }
3466
+ 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;
3467
+ const dialConfig = {};
3468
+ const clips = [];
3469
+ entries.forEach(({ path, childKey, group, clip }, index) => {
3470
+ const raw = clip;
3471
+ if (raw.props && (raw.steps?.length || raw.from || raw.to)) {
3472
+ console.warn(
3473
+ `[dialkit] Timeline clip "${path}": "props" is mutually exclusive with from/to/steps \u2014 using "props".`
3474
+ );
3475
+ } else if (raw.steps?.length && raw.to) {
3476
+ console.warn(
3477
+ `[dialkit] Timeline clip "${path}": "to" is ignored when "steps" is present \u2014 each leg's "to" defines its targets.`
3478
+ );
3479
+ }
3480
+ const hasSteps = Boolean(clip.steps?.length) && !clip.props;
3481
+ const hasProps = Boolean(clip.props);
3482
+ const single = isTransitionConfig(clip.transition) ? clip.transition : void 0;
3483
+ const total = defaultClipDuration(clip);
3484
+ const defaultCurve = single ?? DEFAULT_CLIP_TRANSITION;
3485
+ const clipAt = nonNegativeFinite(clip.at);
3486
+ const clipDial = {
3487
+ at: [clipAt, 0, duration, CLIP_VALUE_STEP]
3488
+ };
3489
+ if (!hasSteps && !hasProps) {
3490
+ clipDial.duration = [total, 0, duration, CLIP_VALUE_STEP];
3491
+ }
3492
+ if (!hasSteps && !hasProps && (clip.transition || clip.from || clip.to)) {
3493
+ clipDial.transition = normalizeStoredTransition(defaultCurve, total);
3494
+ }
3495
+ let tracks;
3496
+ if (clip.props) {
3497
+ tracks = [];
3498
+ for (const [prop, track] of Object.entries(clip.props)) {
3499
+ if (TRACK_RESERVED.has(prop) || /^step\d+$/.test(prop)) {
3500
+ console.warn(`[dialkit] Timeline property "${prop}" collides with a clip field and was skipped.`);
3501
+ continue;
3502
+ }
3503
+ const trackDuration = defaultTrackDuration(track, defaultCurve);
3504
+ const trackCurve = track.transition ?? defaultCurve;
3505
+ const hasTrackSteps = Boolean(track.steps?.length);
3506
+ const trackDial = {
3507
+ delay: [nonNegativeFinite(track.delay), 0, duration, CLIP_VALUE_STEP]
3508
+ };
3509
+ if (!hasTrackSteps) {
3510
+ trackDial.duration = [trackDuration, 0, duration, CLIP_VALUE_STEP];
3511
+ trackDial.transition = normalizeStoredTransition(trackCurve, trackDuration);
3512
+ }
3513
+ const fromValue = track.from ?? (hasTrackSteps ? void 0 : track.to);
3514
+ if (hasTrackSteps && fromValue === void 0) {
3515
+ console.warn(
3516
+ `[dialkit] Timeline clip "${path}": track "${prop}" has steps but no "from" \u2014 declare its starting value.`
3517
+ );
3518
+ }
3519
+ if (fromValue !== void 0) {
3520
+ trackDial.from = scalarDial(prop, fromValue, hasTrackSteps ? track.steps[0]?.to : track.to);
3521
+ }
3522
+ if (!hasTrackSteps && track.to !== void 0) {
3523
+ trackDial.to = scalarDial(prop, track.to, fromValue);
3524
+ }
3525
+ let trackStepKeys;
3526
+ if (hasTrackSteps) {
3527
+ trackStepKeys = [];
3528
+ let previous = fromValue;
3529
+ track.steps.forEach((step, stepIndex) => {
3530
+ const stepKey = `step${stepIndex + 1}`;
3531
+ trackStepKeys.push(stepKey);
3532
+ const stepDuration = defaultStepDuration(step, trackCurve);
3533
+ const stepDial = {
3534
+ duration: [stepDuration, 0, duration, CLIP_VALUE_STEP],
3535
+ transition: normalizeStoredTransition(step.transition ?? trackCurve, stepDuration)
3536
+ };
3537
+ if (step.to !== void 0) {
3538
+ stepDial.to = scalarDial(prop, step.to, previous);
3539
+ previous = step.to;
3540
+ }
3541
+ trackDial[stepKey] = stepDial;
3542
+ });
3543
+ }
3544
+ clipDial[prop] = trackDial;
3545
+ tracks.push({ prop, stepKeys: trackStepKeys });
3546
+ }
3547
+ }
3548
+ if (clip.from && !hasProps) {
3549
+ clipDial.from = withFromToRanges(
3550
+ clip.from,
3551
+ hasSteps ? definedValues(clip.steps[0]?.to) : clip.to
3552
+ );
3553
+ }
3554
+ if (!hasSteps && !hasProps && clip.to) {
3555
+ clipDial.to = withFromToRanges(clip.to, clip.from);
3556
+ }
3557
+ let stepKeys;
3558
+ if (hasSteps) {
3559
+ stepKeys = [];
3560
+ let running = clip.from;
3561
+ clip.steps.forEach((step, stepIndex) => {
3562
+ const stepKey = `step${stepIndex + 1}`;
3563
+ stepKeys.push(stepKey);
3564
+ const stepDuration = defaultStepDuration(step, defaultCurve);
3565
+ const stepDial = {
3566
+ duration: [stepDuration, 0, duration, CLIP_VALUE_STEP],
3567
+ transition: normalizeStoredTransition(step.transition ?? defaultCurve, stepDuration)
3568
+ };
3569
+ const stepTo = definedValues(step.to);
3570
+ if (stepTo) {
3571
+ for (const prop of Object.keys(stepTo)) {
3572
+ if (!running || !(prop in running)) {
3573
+ console.warn(
3574
+ `[dialkit] Timeline clip "${path}": property "${prop}" first animates in step ${stepIndex + 1} with no starting value \u2014 declare it in "from".`
3575
+ );
3576
+ }
3577
+ }
3578
+ stepDial.to = withFromToRanges(stepTo, running);
3579
+ }
3580
+ clipDial[stepKey] = stepDial;
3581
+ running = { ...running ?? {}, ...stepTo ?? {} };
3582
+ });
3583
+ }
3584
+ setDialPath(dialConfig, path, clipDial);
3585
+ clips.push({
3586
+ key: path,
3587
+ label: formatLabel(childKey),
3588
+ color: TIMELINE_CLIP_COLORS[index % TIMELINE_CLIP_COLORS.length],
3589
+ loop: normalizeLoopMode(clip.loop),
3590
+ group,
3591
+ stepKeys,
3592
+ tracks
3593
+ });
3594
+ });
3595
+ return { duration, dialConfig, clips };
3596
+ }
3597
+ var TRACK_RESERVED = /* @__PURE__ */ new Set(["at", "duration", "loop", "from", "to", "transition", "delay"]);
3598
+ function scalarDial(prop, value, counterpart) {
3599
+ const record = withFromToRanges(
3600
+ { [prop]: value },
3601
+ counterpart === void 0 ? void 0 : { [prop]: counterpart }
3602
+ );
3603
+ return record[prop];
3604
+ }
3605
+ var FROM_TO_RANGE_PRESETS = [
3606
+ [/^(x|y|z|tx|ty|offsetx|offsety|translatex|translatey)$/i, { min: -100, max: 100, step: 1 }],
3607
+ [/rotat|angle|skew/i, { min: -180, max: 180, step: 1 }],
3608
+ [/^scale/i, { min: 0, max: 2, step: 0.01 }],
3609
+ [/opacity|alpha/i, { min: 0, max: 1, step: 0.01 }],
3610
+ [/blur|radius|spread/i, { min: 0, max: 100, step: 1 }]
3611
+ ];
3612
+ function inferFromToRange(key, value, counterpart) {
3613
+ const lo = Math.min(value, counterpart ?? value);
3614
+ const hi = Math.max(value, counterpart ?? value);
3615
+ const preset = FROM_TO_RANGE_PRESETS.find(([pattern]) => pattern.test(key))?.[1];
3616
+ if (preset) {
3617
+ return [value, Math.min(preset.min, lo), Math.max(preset.max, hi), preset.step];
3618
+ }
3619
+ if (lo >= 0 && hi <= 1) {
3620
+ return [value, 0, 1, 0.01];
3621
+ }
3622
+ const extent = Math.max(Math.abs(lo), Math.abs(hi), 1);
3623
+ const min = lo < 0 ? -extent * 2 : 0;
3624
+ const max = Math.max(extent * 2, hi);
3625
+ return [value, min, max, inferStep(min, max)];
3626
+ }
3627
+ function withFromToRanges(config, counterpart) {
3628
+ const result = {};
3629
+ for (const [key, value] of Object.entries(config)) {
3630
+ const other = counterpart?.[key];
3631
+ if (typeof value === "number") {
3632
+ result[key] = inferFromToRange(key, value, typeof other === "number" ? other : void 0);
3633
+ } else if (isPlainObject(value) && !("type" in value)) {
3634
+ result[key] = withFromToRanges(
3635
+ value,
3636
+ isPlainObject(other) && !("type" in other) ? other : void 0
3637
+ );
3638
+ } else {
3639
+ result[key] = value;
3640
+ }
3641
+ }
3642
+ return result;
3643
+ }
3644
+ function curveStatic(transition, duration) {
3645
+ if (!transition) return { duration };
3646
+ if (transition.type === "easing") return { duration, ease: transition.ease };
3647
+ const spring = springParams(transition);
3648
+ return { duration, spring, settle: springSettleDuration(spring) };
3649
+ }
3650
+ function sampleCurve(curve, elapsed) {
3651
+ if (elapsed <= 0) return 0;
3652
+ if (curve.spring) {
3653
+ if (curve.settle !== void 0 && elapsed >= curve.settle) return 1;
3654
+ return springProgress(elapsed, curve.spring);
3655
+ }
3656
+ if (curve.ease) {
3657
+ return cubicBezierProgress(clamp(curve.duration > 0 ? elapsed / curve.duration : 1, 0, 1), curve.ease);
3658
+ }
3659
+ return curve.duration > 0 ? Math.min(1, elapsed / curve.duration) : 1;
3660
+ }
3661
+ function resolvedAtPath(resolved, path) {
3662
+ let node = resolved;
3663
+ for (const segment of path.split(".")) {
3664
+ node = isPlainObject(node) ? node[segment] : void 0;
3665
+ }
3666
+ return isPlainObject(node) ? node : {};
3667
+ }
3668
+ function computeStaticClips(parsed, flatValues) {
3669
+ const resolved = resolveDialValues(parsed.dialConfig, flatValues);
3670
+ return parsed.clips.map(
3671
+ (clip) => buildClipStatic(resolvedAtPath(resolved, clip.key), clip, parsed.duration)
3672
+ );
3673
+ }
3674
+ function computeStaticTimeline(parsed, flatValues) {
3675
+ let clips = computeStaticClips(parsed, flatValues);
3676
+ const maxEnd = clips.reduce(
3677
+ (end, clip) => Math.max(end, clip.at + clip.duration),
3678
+ parsed.duration
3679
+ );
3680
+ const duration = maxEnd > parsed.duration ? Math.ceil(maxEnd * 100 - 1e-4) / 100 : parsed.duration;
3681
+ if (duration !== parsed.duration) {
3682
+ clips = clips.map(
3683
+ (clip) => clip.loop === "repeat" ? { ...clip, end: duration } : clip
3684
+ );
3685
+ }
3686
+ return { duration, clips };
3687
+ }
3688
+ function computeClipStaticFromValues(values, clip, timelineDuration) {
3689
+ return buildClipStatic(unflattenClipValues(values, clip.key), clip, timelineDuration);
3690
+ }
3691
+ function buildClipStatic(clipResolved, clip, timelineDuration) {
3692
+ {
3693
+ const at = typeof clipResolved.at === "number" ? clipResolved.at : 0;
3694
+ const from = isPlainObject(clipResolved.from) ? clipResolved.from : void 0;
3695
+ const single = isTransitionConfig(clipResolved.transition) ? clipResolved.transition : void 0;
3696
+ const staticClip = {
3697
+ key: clip.key,
3698
+ childKey: clip.group ? clip.key.slice(clip.group.length + 1) : clip.key,
3699
+ group: clip.group,
3700
+ at,
3701
+ duration: 0,
3702
+ loop: "off",
3703
+ end: 0,
3704
+ isPhysics: false,
3705
+ from,
3706
+ tracks: [],
3707
+ explicitSteps: Boolean(clip.stepKeys?.length)
3708
+ };
3709
+ if (clip.tracks?.length) {
3710
+ const tracks = clip.tracks.map(({ prop, stepKeys }) => {
3711
+ const trackResolved = isPlainObject(clipResolved[prop]) ? clipResolved[prop] : {};
3712
+ const delay = typeof trackResolved.delay === "number" ? trackResolved.delay : 0;
3713
+ const fromValue = trackResolved.from;
3714
+ let steps;
3715
+ let trackDuration = 0;
3716
+ if (stepKeys?.length) {
3717
+ let running = fromValue;
3718
+ steps = stepKeys.map((stepKey) => {
3719
+ const stepResolved = isPlainObject(trackResolved[stepKey]) ? trackResolved[stepKey] : {};
3720
+ const storedDuration = typeof stepResolved.duration === "number" ? stepResolved.duration : 0;
3721
+ const raw = isTransitionConfig(stepResolved.transition) ? stepResolved.transition : void 0;
3722
+ const effective = raw ? resolveClipTransition(raw, storedDuration) : { transition: void 0, duration: storedDuration, isPhysics: false };
3723
+ const toValue = stepResolved.to;
3724
+ const step = {
3725
+ key: stepKey,
3726
+ offset: trackDuration,
3727
+ duration: effective.duration,
3728
+ isPhysics: effective.isPhysics,
3729
+ start: running === void 0 ? {} : { [prop]: running },
3730
+ to: toValue === void 0 ? {} : { [prop]: toValue },
3731
+ curve: curveStatic(effective.transition, effective.duration)
3732
+ };
3733
+ if (toValue !== void 0) running = toValue;
3734
+ trackDuration += effective.duration;
3735
+ return step;
3736
+ });
3737
+ } else {
3738
+ const storedDuration = typeof trackResolved.duration === "number" ? trackResolved.duration : 0;
3739
+ const raw = isTransitionConfig(trackResolved.transition) ? trackResolved.transition : void 0;
3740
+ const effective = raw ? resolveClipTransition(raw, storedDuration) : { transition: void 0, duration: storedDuration, isPhysics: false };
3741
+ const toValue = trackResolved.to;
3742
+ trackDuration = effective.duration;
3743
+ steps = [
3744
+ {
3745
+ key: null,
3746
+ offset: 0,
3747
+ duration: effective.duration,
3748
+ isPhysics: effective.isPhysics,
3749
+ start: fromValue === void 0 ? {} : { [prop]: fromValue },
3750
+ to: toValue === void 0 ? {} : { [prop]: toValue },
3751
+ curve: curveStatic(effective.transition, effective.duration)
3752
+ }
3753
+ ];
3754
+ }
3755
+ return { prop, delay, duration: trackDuration, steps };
3756
+ });
3757
+ staticClip.tracks = tracks;
3758
+ staticClip.props = tracks.map((track) => track.prop);
3759
+ staticClip.duration = tracks.reduce((max, track) => Math.max(max, track.delay + track.duration), 0);
3760
+ staticClip.from = Object.fromEntries(
3761
+ tracks.map((track) => [track.prop, track.steps[0].start[track.prop]])
3762
+ );
3763
+ staticClip.to = Object.fromEntries(
3764
+ tracks.map((track) => {
3765
+ const last = track.steps[track.steps.length - 1];
3766
+ return [track.prop, last.to[track.prop] ?? last.start[track.prop]];
3767
+ })
3768
+ );
3769
+ staticClip.loop = staticClip.duration > 0 ? clip.loop : "off";
3770
+ staticClip.end = staticClip.loop === "off" ? staticClip.at + staticClip.duration : timelineDuration;
3771
+ return staticClip;
3772
+ }
3773
+ if (clip.stepKeys?.length) {
3774
+ let running = { ...from ?? {} };
3775
+ let offset = 0;
3776
+ const steps = clip.stepKeys.map((stepKey) => {
3777
+ const stepResolved = isPlainObject(clipResolved[stepKey]) ? clipResolved[stepKey] : {};
3778
+ const storedDuration = typeof stepResolved.duration === "number" ? stepResolved.duration : 0;
3779
+ const raw = isTransitionConfig(stepResolved.transition) ? stepResolved.transition : void 0;
3780
+ const effective = raw ? resolveClipTransition(raw, storedDuration) : { transition: void 0, duration: storedDuration, isPhysics: false };
3781
+ const to = isPlainObject(stepResolved.to) ? stepResolved.to : {};
3782
+ const step = {
3783
+ key: stepKey,
3784
+ offset,
3785
+ duration: effective.duration,
3786
+ isPhysics: effective.isPhysics,
3787
+ start: running,
3788
+ to,
3789
+ curve: curveStatic(effective.transition, effective.duration)
3790
+ };
3791
+ running = { ...running, ...to };
3792
+ offset += effective.duration;
3793
+ return step;
3794
+ });
3795
+ staticClip.tracks = [{ delay: 0, duration: offset, steps }];
3796
+ staticClip.duration = offset;
3797
+ staticClip.to = running;
3798
+ } else {
3799
+ const storedDuration = typeof clipResolved.duration === "number" ? clipResolved.duration : 0;
3800
+ const to = isPlainObject(clipResolved.to) ? clipResolved.to : void 0;
3801
+ if (single) {
3802
+ const effective = resolveClipTransition(single, storedDuration);
3803
+ staticClip.duration = effective.duration;
3804
+ staticClip.isPhysics = effective.isPhysics;
3805
+ staticClip.transition = effective.transition;
3806
+ staticClip.css = transitionToCss(effective.transition);
3807
+ staticClip.to = to;
3808
+ if (from && to) {
3809
+ staticClip.tracks = [
3810
+ {
3811
+ delay: 0,
3812
+ duration: effective.duration,
3813
+ steps: [
3814
+ {
3815
+ key: null,
3816
+ offset: 0,
3817
+ duration: effective.duration,
3818
+ isPhysics: effective.isPhysics,
3819
+ start: from,
3820
+ to,
3821
+ curve: curveStatic(effective.transition, effective.duration)
3822
+ }
3823
+ ]
3824
+ }
3825
+ ];
3826
+ }
3827
+ } else {
3828
+ staticClip.duration = storedDuration;
3829
+ staticClip.to = to;
3830
+ if (from && to) {
3831
+ const base = resolveClipTransition(DEFAULT_CLIP_TRANSITION, storedDuration);
3832
+ staticClip.duration = base.duration;
3833
+ staticClip.tracks = [
3834
+ {
3835
+ delay: 0,
3836
+ duration: base.duration,
3837
+ steps: [
3838
+ {
3839
+ key: null,
3840
+ offset: 0,
3841
+ duration: base.duration,
3842
+ isPhysics: false,
3843
+ start: from,
3844
+ to,
3845
+ curve: curveStatic(base.transition, base.duration)
3846
+ }
3847
+ ]
3848
+ }
3849
+ ];
3850
+ }
3851
+ }
3852
+ }
3853
+ if (staticClip.tracks.length) {
3854
+ const props = new Set(Object.keys(from ?? {}));
3855
+ for (const track of staticClip.tracks) {
3856
+ for (const step of track.steps) {
3857
+ for (const prop of Object.keys(step.to)) props.add(prop);
3858
+ }
3859
+ }
3860
+ staticClip.props = Array.from(props);
3861
+ }
3862
+ staticClip.loop = staticClip.duration > 0 ? clip.loop : "off";
3863
+ staticClip.end = staticClip.loop === "off" ? staticClip.at + staticClip.duration : timelineDuration;
3864
+ return staticClip;
3865
+ }
3866
+ }
3867
+ function stepAtPosition(steps, pos) {
3868
+ for (const step of steps) {
3869
+ if (pos < step.offset + step.duration) return step;
3870
+ }
3871
+ return steps[steps.length - 1];
3872
+ }
3873
+ function evalPropAtPos(steps, prop, pos) {
3874
+ const step = stepAtPosition(steps, pos);
3875
+ const within = Math.max(0, pos - step.offset);
3876
+ if (prop in step.to) {
3877
+ const eased = sampleCurve(step.curve, within);
3878
+ return interpolateResolved(step.start[prop], step.to[prop], eased);
3879
+ }
3880
+ return step.start[prop];
3881
+ }
3882
+ function computeClipState(clip, time, cycleTime = time) {
3883
+ const total = clip.duration;
3884
+ const looping = clip.loop === "repeat" && total > 0;
3885
+ const started = time >= clip.at || looping && cycleTime > time;
3886
+ const done = time >= clip.end;
3887
+ const elapsed = time - clip.at;
3888
+ const phaseElapsed = looping ? cycleTime - clip.at : elapsed;
3889
+ const fold = (e) => looping ? e % total : e;
3890
+ const basePos = started ? fold(Math.max(0, phaseElapsed)) : 0;
3891
+ const progress = total > 0 ? clamp(basePos / total, 0, 1) : started ? 1 : 0;
3892
+ let current;
3893
+ let stepIndex = 0;
3894
+ if (clip.tracks.length && clip.props?.length) {
3895
+ current = {};
3896
+ for (const track of clip.tracks) {
3897
+ const props = track.prop !== void 0 ? [track.prop] : clip.props;
3898
+ for (const prop of props) {
3899
+ const startValue = track.steps[0]?.start[prop];
3900
+ if (!started) {
3901
+ if (startValue !== void 0) current[prop] = startValue;
3902
+ continue;
3903
+ }
3904
+ const phase = phaseElapsed - track.delay;
3905
+ if (phase <= 0) {
3906
+ if (startValue !== void 0) current[prop] = startValue;
3907
+ continue;
3908
+ }
3909
+ const pos = looping && track.duration > 0 ? phase % track.duration : phase;
3910
+ const value = evalPropAtPos(track.steps, prop, pos);
3911
+ if (value !== void 0) current[prop] = value;
3912
+ }
3913
+ }
3914
+ const shared = clip.tracks[0];
3915
+ if (started && clip.explicitSteps && shared.prop === void 0) {
3916
+ stepIndex = shared.steps.indexOf(stepAtPosition(shared.steps, basePos));
3917
+ }
3918
+ }
3919
+ return {
3920
+ at: clip.at,
3921
+ duration: clip.duration,
3922
+ loop: clip.loop,
3923
+ started,
3924
+ active: started && !done,
3925
+ done,
3926
+ progress,
3927
+ step: clip.explicitSteps ? stepIndex : void 0,
3928
+ from: clip.from,
3929
+ to: clip.to,
3930
+ animate: started ? clip.to : clip.from,
3931
+ transition: clip.transition,
3932
+ css: clip.css,
3933
+ current
3934
+ };
3935
+ }
3936
+ function interpolateResolved(from, to, p) {
3937
+ if (typeof from === "number" && typeof to === "number") {
3938
+ return from + (to - from) * p;
3939
+ }
3940
+ if (typeof from === "string" && typeof to === "string") {
3941
+ const mixed = mixHexColors(from, to, p);
3942
+ if (mixed) return mixed;
3943
+ }
3944
+ if (isPlainObject(from) && isPlainObject(to)) {
3945
+ const result = {};
3946
+ for (const key of Object.keys(from)) {
3947
+ result[key] = key in to ? interpolateResolved(from[key], to[key], p) : from[key];
3948
+ }
3949
+ for (const key of Object.keys(to)) {
3950
+ if (!(key in from)) result[key] = to[key];
3951
+ }
3952
+ return result;
3953
+ }
3954
+ return p < 0.5 ? from : to;
3955
+ }
3956
+ function parseHex(hex) {
3957
+ if (!isHexColor(hex)) return null;
3958
+ let h = hex.slice(1);
3959
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
3960
+ return [
3961
+ parseInt(h.slice(0, 2), 16),
3962
+ parseInt(h.slice(2, 4), 16),
3963
+ parseInt(h.slice(4, 6), 16),
3964
+ h.length === 8 ? parseInt(h.slice(6, 8), 16) : 255
3965
+ ];
3966
+ }
3967
+ function mixHexColors(a, b, p) {
3968
+ const ca = parseHex(a);
3969
+ const cb = parseHex(b);
3970
+ if (!ca || !cb) return null;
3971
+ const t = clamp(p, 0, 1);
3972
+ const mixed = ca.map((v, i) => Math.round(v + (cb[i] - v) * t));
3973
+ const hex = (n) => n.toString(16).padStart(2, "0");
3974
+ const rgb = `#${hex(mixed[0])}${hex(mixed[1])}${hex(mixed[2])}`;
3975
+ return mixed[3] === 255 ? rgb : `${rgb}${hex(mixed[3])}`;
3976
+ }
3977
+ function transitionToCss(transition) {
3978
+ if (!transition) return void 0;
3979
+ if (transition.type === "easing") {
3980
+ return {
3981
+ transitionDuration: `${round2(transition.duration)}s`,
3982
+ transitionTimingFunction: `cubic-bezier(${transition.ease.map((v) => round2(v)).join(", ")})`
3983
+ };
3984
+ }
3985
+ const params = springParams(transition);
3986
+ const dampingRatio = params.damping / (2 * Math.sqrt(params.stiffness * params.mass));
3987
+ const duration = transition.visualDuration ?? springSettleDuration(params);
3988
+ const bounce = transition.bounce ?? Math.max(0, round2(1 - dampingRatio));
3989
+ return {
3990
+ transitionDuration: `${round2(duration)}s`,
3991
+ transitionTimingFunction: bounce > 0.05 ? `cubic-bezier(0.34, ${round2(1.2 + bounce)}, 0.64, 1)` : "cubic-bezier(0.25, 0.6, 0.35, 1)"
3992
+ };
3993
+ }
3994
+ function timelinePopoverDisplayValues(values, clipKey, stepKeys, stepKey) {
3995
+ const display = { ...values };
3996
+ const swap = (path, duration) => {
3997
+ const raw = display[path];
3998
+ if (isTransitionConfig(raw)) display[path] = resolveClipTransition(raw, duration).transition;
3999
+ };
4000
+ if (stepKey) {
4001
+ swap(`${clipKey}.${stepKey}.transition`, numberValue(values[`${clipKey}.${stepKey}.duration`]));
4002
+ return display;
4003
+ }
4004
+ const cycle = stepKeys?.length ? stepKeys.reduce((sum, sk) => sum + numberValue(values[`${clipKey}.${sk}.duration`]), 0) : numberValue(values[`${clipKey}.duration`]);
4005
+ swap(`${clipKey}.transition`, cycle);
4006
+ return display;
4007
+ }
4008
+ function numberValue(value) {
4009
+ return typeof value === "number" ? value : 0;
4010
+ }
4011
+ function unflattenClipValues(values, clipKey) {
4012
+ const prefix = `${clipKey}.`;
4013
+ const result = {};
4014
+ 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);
4015
+ for (const { segments, value } of entries) {
4016
+ let node = result;
4017
+ for (let i = 0; i < segments.length - 1; i++) {
4018
+ const existing = node[segments[i]];
4019
+ node = isPlainObject(existing) ? existing : node[segments[i]] = {};
4020
+ }
4021
+ node[segments[segments.length - 1]] = cloneTimelineValue(value);
4022
+ }
4023
+ return result;
4024
+ }
4025
+ function cloneTimelineValue(value) {
4026
+ if (Array.isArray(value)) return value.map(cloneTimelineValue);
4027
+ if (!isPlainObject(value)) return value;
4028
+ return Object.fromEntries(
4029
+ Object.entries(value).map(([key, nested]) => [key, cloneTimelineValue(nested)])
4030
+ );
4031
+ }
4032
+ function clampTrackDelay(delay, at, trackDuration, timelineDuration) {
4033
+ return clamp(round2(delay), 0, Math.max(0, round2(timelineDuration - at - trackDuration)));
4034
+ }
4035
+ function clampClipMove(at, duration, timelineDuration) {
4036
+ return clamp(round2(at), 0, Math.max(0, timelineDuration - duration));
4037
+ }
4038
+ function clampClipResizeEnd(duration, at, timelineDuration) {
4039
+ return clamp(round2(duration), TIMELINE_MIN_CLIP_DURATION, timelineDuration - at);
4040
+ }
4041
+ function clampClipResizeStart(newAt, at, duration) {
4042
+ const clampedAt = clamp(round2(newAt), 0, at + duration - TIMELINE_MIN_CLIP_DURATION);
4043
+ return { at: clampedAt, duration: round2(at + duration - clampedAt) };
4044
+ }
4045
+ function clampStepResize(duration, at, otherStepsTotal, timelineDuration) {
4046
+ const max = Math.max(TIMELINE_MIN_CLIP_DURATION, timelineDuration - at - otherStepsTotal);
4047
+ return clamp(round2(duration), TIMELINE_MIN_CLIP_DURATION, max);
4048
+ }
4049
+ function normalizeTimelineValuesForCopy(values, clips) {
4050
+ const normalized = { ...values };
4051
+ for (const path of Object.keys(normalized)) {
4052
+ if (path.endsWith(".__mode")) delete normalized[path];
4053
+ }
4054
+ const normalizeTransitionAt = (transitionPath, durationPath) => {
4055
+ const raw = normalized[transitionPath];
4056
+ if (!isTransitionConfig(raw)) return;
4057
+ if (isPhysicsSpring(raw)) {
4058
+ normalized[durationPath] = transitionDefaultDuration(raw);
4059
+ }
4060
+ normalized[transitionPath] = normalizeStoredTransition(
4061
+ raw,
4062
+ numberValue(normalized[durationPath])
4063
+ );
4064
+ };
4065
+ for (const clip of clips) {
4066
+ for (const stepKey of clip.stepKeys ?? []) {
4067
+ normalizeTransitionAt(
4068
+ `${clip.key}.${stepKey}.transition`,
4069
+ `${clip.key}.${stepKey}.duration`
4070
+ );
4071
+ }
4072
+ normalizeTransitionAt(`${clip.key}.transition`, `${clip.key}.duration`);
4073
+ for (const track of clip.tracks ?? []) {
4074
+ const trackKey = `${clip.key}.${track.prop}`;
4075
+ for (const stepKey of track.stepKeys ?? []) {
4076
+ normalizeTransitionAt(
4077
+ `${trackKey}.${stepKey}.transition`,
4078
+ `${trackKey}.${stepKey}.duration`
4079
+ );
4080
+ }
4081
+ normalizeTransitionAt(`${trackKey}.transition`, `${trackKey}.duration`);
4082
+ if (normalized[`${trackKey}.delay`] === 0) {
4083
+ delete normalized[`${trackKey}.delay`];
4084
+ }
4085
+ }
4086
+ delete normalized[`${clip.key}.loop`];
4087
+ }
4088
+ return normalized;
4089
+ }
4090
+ function formatClock(time, tenths = false) {
4091
+ const safe = Math.max(0, time);
4092
+ const minutes = Math.floor(safe / 60);
4093
+ const seconds = safe - minutes * 60;
4094
+ const secondsText = tenths ? seconds.toFixed(1).padStart(4, "0") : String(Math.floor(seconds)).padStart(2, "0");
4095
+ return `${String(minutes).padStart(2, "0")}:${secondsText}`;
4096
+ }
4097
+ function formatSeconds(value) {
4098
+ return `${round2(value)}s`;
4099
+ }
4100
+ function formatStepLabel(stepKey) {
4101
+ const match = /^step(\d+)$/.exec(stepKey);
4102
+ return match ? `Step ${match[1]}` : formatLabel(stepKey);
4103
+ }
4104
+
4105
+ // src/timeline/adapter.ts
4106
+ function resolveTimelineLoop(loop) {
4107
+ if (typeof loop === "object" && loop !== null) {
4108
+ return {
4109
+ enabled: true,
4110
+ start: Number.isFinite(loop.from) ? Math.max(0, loop.from) : 0
4111
+ };
4112
+ }
4113
+ return { enabled: Boolean(loop), start: 0 };
4114
+ }
4115
+ function buildTimelineMeta(id, name, duration, parsed, loop) {
4116
+ const resolvedLoop = resolveTimelineLoop(loop);
4117
+ return {
4118
+ id,
4119
+ name,
4120
+ duration,
4121
+ loop: resolvedLoop.enabled,
4122
+ loopStart: resolvedLoop.start,
4123
+ clips: parsed.clips
4124
+ };
4125
+ }
4126
+ function buildTimelineValues(staticClips, transport, timelineDuration, loopStart, actions) {
4127
+ var _a;
4128
+ const result = {
4129
+ time: transport.time,
4130
+ playing: transport.playing,
4131
+ duration: timelineDuration,
4132
+ ...actions
4133
+ };
4134
+ const span = loopSpan(transport.duration, loopStart);
4135
+ const cycleTime = (span > 0 ? transport.wraps * span : 0) + transport.time;
4136
+ for (const clip of staticClips) {
4137
+ const state = computeClipState(clip, transport.time, cycleTime);
4138
+ if (clip.group) {
4139
+ const bucket = result[_a = clip.group] ?? (result[_a] = {});
4140
+ bucket[clip.childKey] = state;
4141
+ } else {
4142
+ result[clip.key] = state;
4143
+ }
4144
+ }
4145
+ return result;
4146
+ }
4147
+
4148
+ // src/hooks/useDialTimeline.ts
4149
+ function useDialTimeline(name, config, options) {
4150
+ const serializedConfig = useSerialized(config);
4151
+ const parsed = useMemo2(() => parseTimelineConfig(config), [serializedConfig]);
4152
+ const { panelId, flatValues } = useDialStorePanel(name, parsed.dialConfig, {
4153
+ id: options?.id,
4154
+ persist: options?.persist,
4155
+ kind: "timeline"
4156
+ });
4157
+ const staticTimeline = useMemo2(
4158
+ () => computeStaticTimeline(parsed, flatValues),
4159
+ [parsed, flatValues]
4160
+ );
4161
+ const timelineDuration = staticTimeline.duration;
4162
+ const staticClips = staticTimeline.clips;
4163
+ const parsedRef = useRef13(parsed);
4164
+ parsedRef.current = parsed;
4165
+ const optionsRef = useRef13(options);
4166
+ optionsRef.current = options;
4167
+ const { start: loopStart } = resolveTimelineLoop(options?.loop);
4168
+ const buildMeta = useCallback13(
4169
+ () => buildTimelineMeta(panelId, name, timelineDuration, parsedRef.current, options?.loop),
4170
+ [panelId, name, timelineDuration, options?.loop]
4171
+ );
4172
+ const buildMetaRef = useRef13(buildMeta);
4173
+ buildMetaRef.current = buildMeta;
4174
+ useEffect10(() => {
4175
+ TimelineStore.register(buildMetaRef.current(), { autoplay: optionsRef.current?.autoplay ?? true });
4176
+ return () => TimelineStore.unregister(panelId);
4177
+ }, [panelId, name]);
4178
+ const mountedRef = useRef13(false);
4179
+ useEffect10(() => {
4180
+ if (!mountedRef.current) {
4181
+ mountedRef.current = true;
4182
+ return;
4183
+ }
4184
+ TimelineStore.update(buildMeta());
4185
+ }, [buildMeta, parsed]);
4186
+ const subscribeTransport = useCallback13(
4187
+ (callback) => TimelineStore.subscribe(panelId, callback),
4188
+ [panelId]
4189
+ );
4190
+ const getTransport = useCallback13(() => TimelineStore.getTransport(panelId), [panelId]);
4191
+ const transport = useSyncExternalStore6(subscribeTransport, getTransport, getTransport);
4192
+ const play = useCallback13(() => TimelineStore.play(panelId), [panelId]);
4193
+ const pause = useCallback13(() => TimelineStore.pause(panelId), [panelId]);
4194
+ const replay = useCallback13(() => TimelineStore.replay(panelId), [panelId]);
4195
+ const seek = useCallback13((time) => TimelineStore.seek(panelId, time), [panelId]);
4196
+ return useMemo2(
4197
+ () => buildTimelineValues(staticClips, transport, timelineDuration, loopStart, {
4198
+ play,
4199
+ pause,
4200
+ replay,
4201
+ seek
4202
+ }),
4203
+ [staticClips, transport, timelineDuration, loopStart, play, pause, replay, seek]
4204
+ );
4205
+ }
4206
+
4207
+ // src/components/Timeline/DialTimeline.tsx
4208
+ import { memo, useCallback as useCallback14, useEffect as useEffect11, useLayoutEffect as useLayoutEffect2, useRef as useRef14, useState as useState11, useSyncExternalStore as useSyncExternalStore7 } from "react";
4209
+ import { createPortal as createPortal4 } from "react-dom";
4210
+ import { AnimatePresence as AnimatePresence5, motion as motion7 } from "motion/react";
4211
+ import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
4212
+ var DRAG_THRESHOLD_PX = 3;
4213
+ var MAJOR_TICK_TARGET_PX = 140;
4214
+ var MILLISECOND_STEP = 1e-3;
4215
+ var SECOND_TICK_STEPS = [
4216
+ 1e-3,
4217
+ 2e-3,
4218
+ 5e-3,
4219
+ 0.01,
4220
+ 0.02,
4221
+ 0.05,
4222
+ 0.1,
4223
+ 0.2,
4224
+ 0.5,
4225
+ 1,
4226
+ 2,
4227
+ 5,
4228
+ 10,
4229
+ 15,
4230
+ 30,
4231
+ 60,
4232
+ 120,
4233
+ 300,
4234
+ 600
4235
+ ];
4236
+ var MIN_TIMELINE_MAX_ZOOM = 8;
4237
+ var PLAYHEAD_FLAG_WIDTH = 52;
4238
+ var POPOVER_WIDTH = 280;
4239
+ var ZOOM_DRAG_DISTANCE = 180;
4240
+ var subscribeGlobalTimelines = (callback) => TimelineStore.subscribeGlobal(callback);
4241
+ var getTimelines = () => TimelineStore.getTimelines();
4242
+ var subscribeTimelineVisibility = (callback) => TimelineUiStore.subscribe(callback);
4243
+ var getTimelineVisibility = () => TimelineUiStore.getVisible();
4244
+ var DialTimeline = memo(function DialTimeline2({
4245
+ theme = "system",
4246
+ defaultVisible = true,
4247
+ visible,
4248
+ onVisibilityChange,
4249
+ defaultOpen = true,
4250
+ productionEnabled = isDevDefault
4251
+ }) {
4252
+ if (!productionEnabled) return null;
4253
+ return /* @__PURE__ */ jsx18(
4254
+ DialTimelineDock,
4255
+ {
4256
+ theme,
4257
+ defaultVisible,
4258
+ visible,
4259
+ onVisibilityChange,
4260
+ defaultOpen
4261
+ }
4262
+ );
4263
+ });
4264
+ function DialTimelineDock({
4265
+ theme,
4266
+ defaultVisible,
4267
+ visible,
4268
+ onVisibilityChange,
4269
+ defaultOpen
4270
+ }) {
4271
+ const [mounted, setMounted] = useState11(false);
4272
+ const visibilityControllerId = useRef14(/* @__PURE__ */ Symbol("dialkit-timeline-visibility"));
4273
+ useEffect11(() => TimelineUiStore.registerController(visibilityControllerId.current, {
4274
+ visible,
4275
+ defaultVisible,
4276
+ onVisibilityChange
4277
+ }), []);
4278
+ useEffect11(() => {
4279
+ TimelineUiStore.updateController(visibilityControllerId.current, {
4280
+ visible,
4281
+ defaultVisible,
4282
+ onVisibilityChange
4283
+ });
4284
+ }, [defaultVisible, onVisibilityChange, visible]);
4285
+ useEffect11(() => {
4286
+ setMounted(true);
4287
+ }, []);
4288
+ const timelines = useSyncExternalStore7(subscribeGlobalTimelines, getTimelines, getTimelines);
4289
+ const dockVisible = useSyncExternalStore7(
4290
+ subscribeTimelineVisibility,
4291
+ getTimelineVisibility,
4292
+ getTimelineVisibility
4293
+ );
4294
+ if (!mounted || typeof window === "undefined" || timelines.length === 0) {
4295
+ return null;
4296
+ }
4297
+ return createPortal4(
4298
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-root dialkit-timeline", "data-theme": theme, hidden: !dockVisible, children: /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-dock", children: timelines.map((timeline) => /* @__PURE__ */ jsx18(
4299
+ TimelineSection,
4300
+ {
4301
+ meta: timeline,
4302
+ defaultOpen,
4303
+ theme,
4304
+ dockVisible
4305
+ },
4306
+ timeline.id
4307
+ )) }) }),
4308
+ document.body
4309
+ );
4310
+ }
4311
+ function useTransportSubscribe(id) {
4312
+ return useCallback14((callback) => TimelineStore.subscribe(id, callback), [id]);
4313
+ }
4314
+ function PlayPauseButton({ id }) {
4315
+ const subscribe = useTransportSubscribe(id);
4316
+ const getPlaying = useCallback14(() => TimelineStore.getTransport(id).playing, [id]);
4317
+ const playing = useSyncExternalStore7(subscribe, getPlaying, getPlaying);
4318
+ return /* @__PURE__ */ jsx18(
4319
+ motion7.button,
4320
+ {
4321
+ className: "dialkit-toolbar-add",
4322
+ onClick: () => playing ? TimelineStore.pause(id) : TimelineStore.play(id),
4323
+ title: playing ? "Pause" : "Play",
4324
+ "aria-label": playing ? "Pause" : "Play",
4325
+ whileTap: { scale: 0.9 },
4326
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
4327
+ children: /* @__PURE__ */ jsx18("span", { style: { position: "relative", width: 16, height: 16 }, children: /* @__PURE__ */ jsx18(AnimatePresence5, { initial: false, mode: "wait", children: playing ? /* @__PURE__ */ jsx18(
4328
+ motion7.svg,
4329
+ {
4330
+ viewBox: "0 0 24 24",
4331
+ fill: "none",
4332
+ "aria-hidden": "true",
4333
+ style: { position: "absolute", inset: 0, width: 16, height: 16, color: "var(--dial-text-label)" },
4334
+ initial: { scale: 0.8, opacity: 0 },
4335
+ animate: { scale: 1, opacity: 1 },
4336
+ exit: { scale: 0.8, opacity: 0 },
4337
+ transition: { duration: 0.08 },
4338
+ children: ICON_PAUSE.map((d, i) => /* @__PURE__ */ jsx18("path", { d, fill: "currentColor" }, i))
4339
+ },
4340
+ "pause"
4341
+ ) : /* @__PURE__ */ jsx18(
4342
+ motion7.svg,
4343
+ {
4344
+ viewBox: "0 0 24 24",
4345
+ fill: "none",
4346
+ "aria-hidden": "true",
4347
+ style: { position: "absolute", inset: 0, width: 16, height: 16, color: "var(--dial-text-label)" },
4348
+ initial: { scale: 0.8, opacity: 0 },
4349
+ animate: { scale: 1, opacity: 1 },
4350
+ exit: { scale: 0.8, opacity: 0 },
4351
+ transition: { duration: 0.08 },
4352
+ children: /* @__PURE__ */ jsx18("path", { d: ICON_PLAY, fill: "currentColor" })
4353
+ },
4354
+ "play"
4355
+ ) }) })
4356
+ }
4357
+ );
4358
+ }
4359
+ function TimelinePlayheadFlag({
4360
+ id,
4361
+ duration,
4362
+ pxPerSecond,
4363
+ viewStart,
4364
+ viewEnd,
4365
+ laneWidth,
4366
+ rulerRef,
4367
+ headerClearStart,
4368
+ headerClearEnd,
4369
+ onResetView
4370
+ }) {
4371
+ const subscribe = useTransportSubscribe(id);
4372
+ const getTime = useCallback14(() => TimelineStore.getTransport(id).time, [id]);
4373
+ const time = useSyncExternalStore7(subscribe, getTime, getTime);
4374
+ const scrubRef = useRef14(null);
4375
+ const cleanupScrubRef = useRef14(null);
4376
+ const seekFromClientX = useCallback14((clientX) => {
4377
+ const rect = scrubRef.current?.rect;
4378
+ const scrub = scrubRef.current;
4379
+ const contentWidth = rect?.width ?? 0;
4380
+ if (!rect || !scrub || contentWidth <= 0) return;
4381
+ const nextTime = clamp(
4382
+ scrub.viewStart + (clientX - rect.left) / contentWidth * (scrub.viewEnd - scrub.viewStart),
4383
+ scrub.viewStart,
4384
+ scrub.viewEnd
4385
+ );
4386
+ TimelineStore.seek(id, nextTime);
4387
+ }, [id]);
4388
+ const handlePointerDown = useCallback14((e) => {
4389
+ const rect = rulerRef.current?.getBoundingClientRect();
4390
+ if (!rect) return;
4391
+ e.preventDefault();
4392
+ e.stopPropagation();
4393
+ cleanupScrubRef.current?.();
4394
+ const resetView = e.shiftKey;
4395
+ scrubRef.current = {
4396
+ wasPlaying: TimelineStore.getTransport(id).playing,
4397
+ rect,
4398
+ viewStart: resetView ? 0 : viewStart,
4399
+ viewEnd: resetView ? duration : viewEnd
4400
+ };
4401
+ if (resetView) onResetView();
4402
+ TimelineStore.pause(id);
4403
+ seekFromClientX(e.clientX);
4404
+ const handleWindowPointerMove = (event) => {
4405
+ event.preventDefault();
4406
+ seekFromClientX(event.clientX);
4407
+ };
4408
+ const finishWindowScrub = () => {
4409
+ window.removeEventListener("pointermove", handleWindowPointerMove);
4410
+ window.removeEventListener("pointerup", finishWindowScrub);
4411
+ window.removeEventListener("pointercancel", finishWindowScrub);
4412
+ if (scrubRef.current?.wasPlaying) TimelineStore.play(id);
4413
+ scrubRef.current = null;
4414
+ cleanupScrubRef.current = null;
4415
+ };
4416
+ window.addEventListener("pointermove", handleWindowPointerMove, { passive: false });
4417
+ window.addEventListener("pointerup", finishWindowScrub);
4418
+ window.addEventListener("pointercancel", finishWindowScrub);
4419
+ cleanupScrubRef.current = finishWindowScrub;
4420
+ }, [duration, id, onResetView, rulerRef, seekFromClientX, viewEnd, viewStart]);
4421
+ useEffect11(() => () => cleanupScrubRef.current?.(), []);
4422
+ if (time < viewStart || time > viewEnd || laneWidth <= 0) return null;
4423
+ const x = clamp(
4424
+ (time - viewStart) * pxPerSecond,
4425
+ 0,
4426
+ laneWidth
4427
+ );
4428
+ const flagLeft = x - PLAYHEAD_FLAG_WIDTH / 2;
4429
+ const flagRight = flagLeft + PLAYHEAD_FLAG_WIDTH;
4430
+ const placement = flagLeft >= headerClearStart && flagRight <= headerClearEnd ? "raised" : "lowered";
4431
+ return /* @__PURE__ */ jsxs14(
4432
+ "div",
4433
+ {
4434
+ className: "dialkit-timeline-playhead-control",
4435
+ "data-edge": "center",
4436
+ "data-placement": placement,
4437
+ style: { left: `calc(var(--dial-timeline-label-w) + ${x}px)` },
4438
+ onPointerDown: handlePointerDown,
4439
+ role: "slider",
4440
+ "aria-label": "Timeline current time",
4441
+ "aria-valuemin": 0,
4442
+ "aria-valuemax": duration,
4443
+ "aria-valuenow": time,
4444
+ title: "Drag to scrub the timeline",
4445
+ children: [
4446
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-playhead-flag", children: time.toFixed(2) }),
4447
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-playhead-stem" })
4448
+ ]
4449
+ }
4450
+ );
4451
+ }
4452
+ function TimelineOverview({
4453
+ id,
4454
+ duration,
4455
+ viewStart,
4456
+ viewEnd,
4457
+ onNavigate
4458
+ }) {
4459
+ const subscribe = useTransportSubscribe(id);
4460
+ const getTime = useCallback14(() => TimelineStore.getTransport(id).time, [id]);
4461
+ const time = useSyncExternalStore7(subscribe, getTime, getTime);
4462
+ const scrubRef = useRef14(null);
4463
+ const seekFromClientX = useCallback14((clientX) => {
4464
+ const rect = scrubRef.current?.rect;
4465
+ if (!rect || rect.width <= 0 || duration <= 0) return;
4466
+ const nextTime = clamp((clientX - rect.left) / rect.width * duration, 0, duration);
4467
+ TimelineStore.seek(id, nextTime);
4468
+ onNavigate(nextTime);
4469
+ }, [duration, id, onNavigate]);
4470
+ const handlePointerDown = useCallback14((e) => {
4471
+ e.preventDefault();
4472
+ e.currentTarget.setPointerCapture(e.pointerId);
4473
+ scrubRef.current = {
4474
+ wasPlaying: TimelineStore.getTransport(id).playing,
4475
+ rect: e.currentTarget.getBoundingClientRect()
4476
+ };
4477
+ TimelineStore.pause(id);
4478
+ seekFromClientX(e.clientX);
4479
+ }, [id, seekFromClientX]);
4480
+ const handlePointerMove = useCallback14((e) => {
4481
+ if (scrubRef.current) seekFromClientX(e.clientX);
4482
+ }, [seekFromClientX]);
4483
+ const finishScrub = useCallback14(() => {
4484
+ if (scrubRef.current?.wasPlaying) TimelineStore.play(id);
4485
+ scrubRef.current = null;
4486
+ }, [id]);
4487
+ const viewportLeft = duration > 0 ? viewStart / duration * 100 : 0;
4488
+ const viewportWidth = duration > 0 ? (viewEnd - viewStart) / duration * 100 : 100;
4489
+ const playheadLeft = duration > 0 ? time / duration * 100 : 0;
4490
+ return /* @__PURE__ */ jsxs14(
4491
+ "div",
4492
+ {
4493
+ className: "dialkit-timeline-overview",
4494
+ onPointerDown: handlePointerDown,
4495
+ onPointerMove: handlePointerMove,
4496
+ onPointerUp: finishScrub,
4497
+ onPointerCancel: finishScrub,
4498
+ onLostPointerCapture: finishScrub,
4499
+ title: "Drag to scrub the full timeline",
4500
+ children: [
4501
+ /* @__PURE__ */ jsx18(
4502
+ "div",
4503
+ {
4504
+ className: "dialkit-timeline-overview-viewport",
4505
+ "data-zoomed": viewportWidth < 99.999 || void 0,
4506
+ style: { left: `${viewportLeft}%`, width: `${viewportWidth}%` }
4507
+ }
4508
+ ),
4509
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-overview-progress", style: { width: `${playheadLeft}%` } }),
4510
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-overview-playhead", style: { left: `${playheadLeft}%` } })
4511
+ ]
4512
+ }
4513
+ );
4514
+ }
4515
+ function clampViewStart(start, duration, visibleDuration) {
4516
+ return clamp(start, 0, Math.max(0, duration - visibleDuration));
4517
+ }
4518
+ function formatRulerSeconds(time, step) {
4519
+ if (step >= 1 && Number.isInteger(time)) return formatClock(time);
4520
+ const decimals = Math.min(3, Math.max(1, Math.ceil(-Math.log10(step))));
4521
+ return `${time.toFixed(decimals)}s`;
4522
+ }
4523
+ var TimelineSection = memo(function TimelineSection2({
4524
+ meta,
4525
+ defaultOpen,
4526
+ theme,
4527
+ dockVisible
4528
+ }) {
4529
+ const [open, setOpen] = useState11(defaultOpen);
4530
+ const [copied, setCopied] = useState11(false);
4531
+ const [popover, setPopover] = useState11(null);
4532
+ const [collapsedGroups, setCollapsedGroups] = useState11(() => /* @__PURE__ */ new Set());
4533
+ const [expandedTracks, setExpandedTracks] = useState11(() => /* @__PURE__ */ new Set());
4534
+ const [zoom, setZoom] = useState11(1);
4535
+ const [viewStart, setViewStart] = useState11(0);
4536
+ const subscribeValues = useCallback14(
4537
+ (callback) => DialStore.subscribe(meta.id, callback),
4538
+ [meta.id]
4539
+ );
4540
+ const getValues = useCallback14(() => DialStore.getValues(meta.id), [meta.id]);
4541
+ const values = useSyncExternalStore7(subscribeValues, getValues, getValues);
4542
+ const presets = DialStore.getPresets(meta.id);
4543
+ const activePresetId = DialStore.getActivePresetId(meta.id);
4544
+ const laneAreaRef = useRef14(null);
4545
+ const titleRef = useRef14(null);
4546
+ const actionsRef = useRef14(null);
4547
+ const [laneWidth, setLaneWidth] = useState11(0);
4548
+ const [flagClearRange, setFlagClearRange] = useState11({ start: 0, end: 0 });
4549
+ useLayoutEffect2(() => {
4550
+ if (!open) return;
4551
+ const ruler = laneAreaRef.current;
4552
+ const title = titleRef.current;
4553
+ const actions = actionsRef.current;
4554
+ if (!ruler || !title || !actions) return;
4555
+ const measure = () => {
4556
+ const rulerRect = ruler.getBoundingClientRect();
4557
+ const titleRect = title.getBoundingClientRect();
4558
+ const actionsRect = actions.getBoundingClientRect();
4559
+ setLaneWidth(rulerRect.width);
4560
+ const start = Math.round(titleRect.right + 10 - rulerRect.left);
4561
+ const end = Math.round(actionsRect.left - 10 - rulerRect.left);
4562
+ setFlagClearRange((current) => current.start === start && current.end === end ? current : { start, end });
4563
+ };
4564
+ measure();
4565
+ const observer = new ResizeObserver(measure);
4566
+ observer.observe(ruler);
4567
+ observer.observe(title);
4568
+ observer.observe(actions);
4569
+ return () => observer.disconnect();
4570
+ }, [open]);
4571
+ const visibleDuration = meta.duration > 0 ? meta.duration / zoom : meta.duration;
4572
+ const safeViewStart = clampViewStart(viewStart, meta.duration, visibleDuration);
4573
+ const viewEnd = safeViewStart + visibleDuration;
4574
+ const pxPerSecond = visibleDuration > 0 && laneWidth > 0 ? laneWidth / visibleDuration : 0;
4575
+ const millisecondReadableZoom = laneWidth > 0 && meta.duration > 0 ? MAJOR_TICK_TARGET_PX * meta.duration / (MILLISECOND_STEP * 10 * laneWidth) : MIN_TIMELINE_MAX_ZOOM;
4576
+ const maxZoom = Math.max(MIN_TIMELINE_MAX_ZOOM, millisecondReadableZoom);
4577
+ useEffect11(() => {
4578
+ setZoom((current) => clamp(current, 1, maxZoom));
4579
+ }, [maxZoom]);
4580
+ useEffect11(() => {
4581
+ setViewStart((current) => clampViewStart(current, meta.duration, meta.duration / zoom));
4582
+ }, [meta.duration, zoom]);
4583
+ useEffect11(() => {
4584
+ if (!dockVisible) setPopover(null);
4585
+ }, [dockVisible]);
4586
+ const centerViewAt = useCallback14((time) => {
4587
+ if (zoom <= 1 || meta.duration <= 0) return;
4588
+ const windowDuration = meta.duration / zoom;
4589
+ setViewStart(clampViewStart(time - windowDuration / 2, meta.duration, windowDuration));
4590
+ }, [meta.duration, zoom]);
4591
+ const resetView = useCallback14(() => {
4592
+ setZoom(1);
4593
+ setViewStart(0);
4594
+ }, []);
4595
+ const zoomDragRef = useRef14(null);
4596
+ const rulerScrubRef = useRef14(null);
4597
+ const seekRulerFromClientX = useCallback14((clientX) => {
4598
+ const scrub = rulerScrubRef.current;
4599
+ const contentWidth = scrub?.rect.width ?? 0;
4600
+ if (!scrub || contentWidth <= 0) return;
4601
+ TimelineStore.seek(
4602
+ meta.id,
4603
+ clamp(
4604
+ scrub.viewStart + (clientX - scrub.rect.left) / contentWidth * scrub.visibleDuration,
4605
+ scrub.viewStart,
4606
+ scrub.viewStart + scrub.visibleDuration
4607
+ )
4608
+ );
4609
+ }, [meta.id]);
4610
+ const handleRulerPointerDown = useCallback14((e) => {
4611
+ e.preventDefault();
4612
+ e.stopPropagation();
4613
+ const rect = e.currentTarget.getBoundingClientRect();
4614
+ const contentWidth = rect.width;
4615
+ if (contentWidth <= 0) return;
4616
+ e.currentTarget.setPointerCapture(e.pointerId);
4617
+ if (!e.altKey) {
4618
+ const resetView2 = e.shiftKey;
4619
+ rulerScrubRef.current = {
4620
+ wasPlaying: TimelineStore.getTransport(meta.id).playing,
4621
+ rect,
4622
+ viewStart: resetView2 ? 0 : safeViewStart,
4623
+ visibleDuration: resetView2 ? meta.duration : visibleDuration
4624
+ };
4625
+ if (resetView2) {
4626
+ setZoom(1);
4627
+ setViewStart(0);
4628
+ }
4629
+ TimelineStore.pause(meta.id);
4630
+ seekRulerFromClientX(e.clientX);
4631
+ return;
4632
+ }
4633
+ const anchorRatio = clamp((e.clientX - rect.left) / contentWidth, 0, 1);
4634
+ zoomDragRef.current = {
4635
+ pointerX: e.clientX,
4636
+ rect,
4637
+ zoom,
4638
+ viewStart: safeViewStart,
4639
+ anchorRatio,
4640
+ anchorTime: safeViewStart + anchorRatio * visibleDuration,
4641
+ moved: false
4642
+ };
4643
+ }, [meta.duration, meta.id, safeViewStart, seekRulerFromClientX, visibleDuration, zoom]);
4644
+ const handleRulerPointerMove = useCallback14((e) => {
4645
+ if (rulerScrubRef.current) {
4646
+ seekRulerFromClientX(e.clientX);
4647
+ return;
4648
+ }
4649
+ const drag = zoomDragRef.current;
4650
+ if (!drag || meta.duration <= 0) return;
4651
+ const dx = e.clientX - drag.pointerX;
4652
+ if (!drag.moved && Math.abs(dx) <= DRAG_THRESHOLD_PX) return;
4653
+ drag.moved = true;
4654
+ const nextZoom = clamp(drag.zoom * Math.exp(dx / ZOOM_DRAG_DISTANCE), 1, maxZoom);
4655
+ const nextVisibleDuration = meta.duration / nextZoom;
4656
+ const nextStart = clampViewStart(
4657
+ drag.anchorTime - drag.anchorRatio * nextVisibleDuration,
4658
+ meta.duration,
4659
+ nextVisibleDuration
4660
+ );
4661
+ setZoom(nextZoom);
4662
+ setViewStart(nextStart);
4663
+ }, [maxZoom, meta.duration, seekRulerFromClientX]);
4664
+ const handleRulerPointerUp = useCallback14(() => {
4665
+ if (rulerScrubRef.current?.wasPlaying) TimelineStore.play(meta.id);
4666
+ rulerScrubRef.current = null;
4667
+ zoomDragRef.current = null;
4668
+ }, [meta.id]);
4669
+ const handleRulerPointerCancel = useCallback14(() => {
4670
+ if (rulerScrubRef.current?.wasPlaying) TimelineStore.play(meta.id);
4671
+ rulerScrubRef.current = null;
4672
+ zoomDragRef.current = null;
4673
+ }, [meta.id]);
4674
+ const trackScrubRef = useRef14(null);
4675
+ const seekTrackFromClientX = useCallback14((clientX) => {
4676
+ const scrub = trackScrubRef.current;
4677
+ const contentWidth = scrub?.rect.width ?? 0;
4678
+ if (!scrub || contentWidth <= 0) return;
4679
+ const nextTime = clamp(
4680
+ scrub.viewStart + (clientX - scrub.rect.left) / contentWidth * scrub.visibleDuration,
4681
+ scrub.viewStart,
4682
+ scrub.viewStart + scrub.visibleDuration
4683
+ );
4684
+ TimelineStore.seek(meta.id, nextTime);
4685
+ }, [meta.id]);
4686
+ const handleTrackPointerDown = useCallback14((e) => {
4687
+ const target = e.target;
4688
+ if (target.closest(".dialkit-timeline-label, button")) return;
4689
+ if (!e.shiftKey && target.closest(".dialkit-timeline-clip")) return;
4690
+ const rect = laneAreaRef.current?.getBoundingClientRect();
4691
+ if (!rect) return;
4692
+ e.preventDefault();
4693
+ e.currentTarget.setPointerCapture(e.pointerId);
4694
+ const resetView2 = e.shiftKey;
4695
+ trackScrubRef.current = {
4696
+ wasPlaying: TimelineStore.getTransport(meta.id).playing,
4697
+ rect,
4698
+ viewStart: resetView2 ? 0 : safeViewStart,
4699
+ visibleDuration: resetView2 ? meta.duration : visibleDuration
4700
+ };
4701
+ if (resetView2) {
4702
+ setZoom(1);
4703
+ setViewStart(0);
4704
+ }
4705
+ setPopover(null);
4706
+ TimelineStore.pause(meta.id);
4707
+ seekTrackFromClientX(e.clientX);
4708
+ }, [meta.duration, meta.id, safeViewStart, seekTrackFromClientX, visibleDuration]);
4709
+ const handleTrackPointerMove = useCallback14((e) => {
4710
+ if (trackScrubRef.current) seekTrackFromClientX(e.clientX);
4711
+ }, [seekTrackFromClientX]);
4712
+ const finishTrackScrub = useCallback14(() => {
4713
+ if (trackScrubRef.current?.wasPlaying) TimelineStore.play(meta.id);
4714
+ trackScrubRef.current = null;
4715
+ }, [meta.id]);
4716
+ const handleCopy = useCallback14(() => {
4717
+ const normalized = normalizeTimelineValuesForCopy(DialStore.getValues(meta.id), meta.clips);
4718
+ navigator.clipboard.writeText(buildCopyInstruction("useDialTimeline", meta.name, normalized));
4719
+ setCopied(true);
4720
+ setTimeout(() => setCopied(false), 1500);
4721
+ }, [meta.clips, meta.id, meta.name]);
4722
+ const handleAddPreset = useCallback14(() => {
4723
+ DialStore.savePreset(meta.id, `Version ${presets.length + 2}`);
4724
+ }, [meta.id, presets.length]);
4725
+ const closePopover = useCallback14(() => setPopover(null), []);
4726
+ const openClipPopover = useCallback14(
4727
+ (clip, rect, stepKey) => {
4728
+ const targetPath = stepKey ? `${clip.key}.${stepKey}` : clip.key;
4729
+ const exclude = stepKey ? void 0 : clipPopoverExclusions(clip);
4730
+ if (getClipControls(meta.id, targetPath, exclude).length === 0) return;
4731
+ setPopover(
4732
+ (prev) => prev?.clip.key === clip.key && prev?.stepKey === stepKey ? null : {
4733
+ clip,
4734
+ stepKey,
4735
+ anchor: {
4736
+ left: rect.left,
4737
+ top: rect.top,
4738
+ right: rect.right,
4739
+ bottom: rect.bottom,
4740
+ width: rect.width,
4741
+ height: rect.height
4742
+ }
4743
+ }
4744
+ );
4745
+ },
4746
+ [meta.id]
4747
+ );
4748
+ const toggleTracks = useCallback14((clipKey) => {
4749
+ setExpandedTracks((prev) => {
4750
+ const next = new Set(prev);
4751
+ if (next.has(clipKey)) next.delete(clipKey);
4752
+ else next.add(clipKey);
4753
+ return next;
4754
+ });
4755
+ }, []);
4756
+ const handleBarClick = useCallback14(
4757
+ (clip, rect, stepKey) => {
4758
+ if (!stepKey && clip.tracks?.length) {
4759
+ toggleTracks(clip.key);
4760
+ return;
4761
+ }
4762
+ openClipPopover(clip, rect, stepKey);
4763
+ },
4764
+ [openClipPopover, toggleTracks]
4765
+ );
4766
+ const toggleGroup = useCallback14((group) => {
4767
+ setCollapsedGroups((prev) => {
4768
+ const next = new Set(prev);
4769
+ if (next.has(group)) next.delete(group);
4770
+ else next.add(group);
4771
+ return next;
4772
+ });
4773
+ }, []);
4774
+ const rawStep = pxPerSecond > 0 ? MAJOR_TICK_TARGET_PX / pxPerSecond : 1;
4775
+ const adaptiveMajorStep = SECOND_TICK_STEPS.find((step) => step >= rawStep) ?? SECOND_TICK_STEPS[SECOND_TICK_STEPS.length - 1];
4776
+ const majorStep = zoom < 1.5 && meta.duration >= 1 ? Math.max(1, adaptiveMajorStep) : adaptiveMajorStep;
4777
+ const fineTickStep = majorStep / 10;
4778
+ const majorTicks = [];
4779
+ const mediumTicks = [];
4780
+ const fineTicks = [];
4781
+ const firstMajorTick = Math.ceil((safeViewStart - 1e-6) / majorStep) * majorStep;
4782
+ for (let t = firstMajorTick; t <= viewEnd + 1e-6; t += majorStep) {
4783
+ majorTicks.push(Number(t.toFixed(4)));
4784
+ }
4785
+ const firstFineIndex = Math.ceil((safeViewStart - 1e-6) / fineTickStep);
4786
+ const lastFineIndex = Math.floor((viewEnd + 1e-6) / fineTickStep);
4787
+ for (let index = firstFineIndex; index <= lastFineIndex; index++) {
4788
+ if (index % 10 === 0) continue;
4789
+ const tick = Number((index * fineTickStep).toFixed(6));
4790
+ if (index % 5 === 0) mediumTicks.push(tick);
4791
+ else fineTicks.push(tick);
4792
+ }
4793
+ const rows = [];
4794
+ let lastGroup;
4795
+ for (const clip of meta.clips) {
4796
+ if (clip.group !== lastGroup) {
4797
+ lastGroup = clip.group;
4798
+ if (clip.group) {
4799
+ const group = clip.group;
4800
+ const isCollapsed = collapsedGroups.has(group);
4801
+ rows.push(
4802
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-row dialkit-timeline-group-row", children: [
4803
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-label", children: [
4804
+ /* @__PURE__ */ jsx18(
4805
+ "button",
4806
+ {
4807
+ className: "dialkit-timeline-group-toggle",
4808
+ "data-open": !isCollapsed,
4809
+ onClick: () => toggleGroup(group),
4810
+ title: isCollapsed ? "Expand layer" : "Collapse layer",
4811
+ 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 }) })
4812
+ }
4813
+ ),
4814
+ /* @__PURE__ */ jsx18("span", { children: formatLabel(group) })
4815
+ ] }),
4816
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-lane" })
4817
+ ] }, `group:${group}`)
4818
+ );
4819
+ }
4820
+ }
4821
+ if (clip.group && collapsedGroups.has(clip.group)) continue;
4822
+ const isProps = Boolean(clip.tracks?.length);
4823
+ const tracksOpen = isProps && expandedTracks.has(clip.key);
4824
+ const stat = computeClipStaticFromValues(values, clip, meta.duration);
4825
+ rows.push(
4826
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-row", "data-grouped": clip.group ? "" : void 0, children: [
4827
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-label", children: [
4828
+ isProps ? /* @__PURE__ */ jsx18(
4829
+ "button",
4830
+ {
4831
+ className: "dialkit-timeline-group-toggle",
4832
+ "data-open": tracksOpen,
4833
+ onClick: (e) => {
4834
+ e.stopPropagation();
4835
+ toggleTracks(clip.key);
4836
+ },
4837
+ title: tracksOpen ? "Collapse properties" : "Expand properties",
4838
+ 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 }) })
4839
+ }
4840
+ ) : null,
4841
+ clip.label
4842
+ ] }),
4843
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-lane", children: /* @__PURE__ */ jsx18(
4844
+ TimelineClip,
4845
+ {
4846
+ timelineId: meta.id,
4847
+ clip,
4848
+ at: stat.at,
4849
+ duration: stat.duration,
4850
+ loop: stat.loop,
4851
+ steps: clip.stepKeys?.length ? stat.tracks[0]?.steps : void 0,
4852
+ fixedDuration: isProps ? true : stat.isPhysics,
4853
+ composite: isProps,
4854
+ pxPerSecond,
4855
+ viewStart: safeViewStart,
4856
+ timelineDuration: meta.duration,
4857
+ selected: popover?.clip.key === clip.key,
4858
+ selectedStepKey: popover?.clip.key === clip.key ? popover.stepKey : void 0,
4859
+ onClick: handleBarClick,
4860
+ onDrag: closePopover
4861
+ }
4862
+ ) })
4863
+ ] }, clip.key)
4864
+ );
4865
+ if (tracksOpen) {
4866
+ for (const trackRef of clip.tracks ?? []) {
4867
+ const track = stat.tracks.find((candidate) => candidate.prop === trackRef.prop);
4868
+ if (!track) continue;
4869
+ const trackKey = `${clip.key}.${trackRef.prop}`;
4870
+ const trackMeta = {
4871
+ key: trackKey,
4872
+ label: `${clip.label} \xB7 ${formatLabel(trackRef.prop)}`,
4873
+ color: clip.color,
4874
+ loop: clip.loop,
4875
+ group: clip.group,
4876
+ stepKeys: trackRef.stepKeys
4877
+ };
4878
+ rows.push(
4879
+ /* @__PURE__ */ jsxs14(
4880
+ "div",
4881
+ {
4882
+ className: "dialkit-timeline-row dialkit-timeline-track-row",
4883
+ "data-grouped": clip.group ? "" : void 0,
4884
+ children: [
4885
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-label", children: formatLabel(trackRef.prop) }),
4886
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-lane", children: /* @__PURE__ */ jsx18(
4887
+ TimelineClip,
4888
+ {
4889
+ timelineId: meta.id,
4890
+ clip: trackMeta,
4891
+ at: stat.at + track.delay,
4892
+ duration: track.duration,
4893
+ loop: stat.loop,
4894
+ steps: trackRef.stepKeys?.length ? track.steps : void 0,
4895
+ fixedDuration: !trackRef.stepKeys?.length && track.steps[0]?.isPhysics === true,
4896
+ baseAt: stat.at,
4897
+ delayMode: true,
4898
+ pxPerSecond,
4899
+ viewStart: safeViewStart,
4900
+ timelineDuration: meta.duration,
4901
+ selected: popover?.clip.key === trackKey,
4902
+ selectedStepKey: popover?.clip.key === trackKey ? popover.stepKey : void 0,
4903
+ onClick: openClipPopover,
4904
+ onDrag: closePopover
4905
+ }
4906
+ ) })
4907
+ ]
4908
+ },
4909
+ trackKey
4910
+ )
4911
+ );
4912
+ }
4913
+ }
4914
+ }
4915
+ return /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-section", children: [
4916
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-header", "data-open": open || void 0, children: [
4917
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-identity", children: /* @__PURE__ */ jsx18("span", { ref: titleRef, className: "dialkit-timeline-title", children: meta.name }) }),
4918
+ !open && /* @__PURE__ */ jsx18(
4919
+ TimelineOverview,
4920
+ {
4921
+ id: meta.id,
4922
+ duration: meta.duration,
4923
+ viewStart: safeViewStart,
4924
+ viewEnd,
4925
+ onNavigate: centerViewAt
4926
+ }
4927
+ ),
4928
+ /* @__PURE__ */ jsxs14("div", { ref: actionsRef, className: "dialkit-timeline-actions", children: [
4929
+ /* @__PURE__ */ jsx18(PlayPauseButton, { id: meta.id }),
4930
+ /* @__PURE__ */ jsx18(
4931
+ motion7.button,
4932
+ {
4933
+ className: "dialkit-toolbar-add",
4934
+ onClick: handleAddPreset,
4935
+ title: "Add timeline version",
4936
+ "aria-label": "Add timeline version",
4937
+ whileTap: { scale: 0.9 },
4938
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
4939
+ 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)) })
4940
+ }
4941
+ ),
4942
+ /* @__PURE__ */ jsx18(
4943
+ PresetManager,
4944
+ {
4945
+ panelId: meta.id,
4946
+ presets,
4947
+ activePresetId,
4948
+ onAdd: handleAddPreset
4949
+ }
4950
+ ),
4951
+ /* @__PURE__ */ jsx18(
4952
+ motion7.button,
4953
+ {
4954
+ className: "dialkit-toolbar-add",
4955
+ onClick: handleCopy,
4956
+ title: "Copy parameters",
4957
+ "aria-label": copied ? "Copied parameters" : "Copy parameters",
4958
+ whileTap: { scale: 0.9 },
4959
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
4960
+ children: /* @__PURE__ */ jsx18("span", { style: { position: "relative", width: 16, height: 16 }, children: /* @__PURE__ */ jsx18(AnimatePresence5, { initial: false, mode: "wait", children: copied ? /* @__PURE__ */ jsx18(
4961
+ motion7.svg,
4962
+ {
4963
+ viewBox: "0 0 24 24",
4964
+ fill: "none",
4965
+ stroke: "currentColor",
4966
+ strokeWidth: "2",
4967
+ strokeLinecap: "round",
4968
+ strokeLinejoin: "round",
4969
+ "aria-hidden": "true",
4970
+ style: { position: "absolute", inset: 0, width: 16, height: 16, color: "var(--dial-text-label)" },
4971
+ initial: { scale: 0.8, opacity: 0 },
4972
+ animate: { scale: 1, opacity: 1 },
4973
+ exit: { scale: 0.8, opacity: 0 },
4974
+ transition: { duration: 0.08 },
4975
+ children: /* @__PURE__ */ jsx18("path", { d: ICON_CHECK })
4976
+ },
4977
+ "check"
4978
+ ) : /* @__PURE__ */ jsxs14(
4979
+ motion7.svg,
4980
+ {
4981
+ viewBox: "0 0 24 24",
4982
+ fill: "none",
4983
+ "aria-hidden": "true",
4984
+ style: { position: "absolute", inset: 0, width: 16, height: 16, color: "var(--dial-text-label)" },
4985
+ initial: { scale: 0.8, opacity: 0 },
4986
+ animate: { scale: 1, opacity: 1 },
4987
+ exit: { scale: 0.8, opacity: 0 },
4988
+ transition: { duration: 0.08 },
4989
+ children: [
4990
+ /* @__PURE__ */ jsx18("path", { d: ICON_CLIPBOARD.board, stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round" }),
4991
+ /* @__PURE__ */ jsx18("path", { d: ICON_CLIPBOARD.sparkle, fill: "currentColor" }),
4992
+ /* @__PURE__ */ jsx18("path", { d: ICON_CLIPBOARD.body, stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })
4993
+ ]
4994
+ },
4995
+ "clipboard"
4996
+ ) }) })
4997
+ }
4998
+ ),
4999
+ /* @__PURE__ */ jsx18(
5000
+ "button",
5001
+ {
5002
+ className: "dialkit-timeline-chevron",
5003
+ "data-open": open,
5004
+ "aria-expanded": open,
5005
+ onClick: () => setOpen(!open),
5006
+ title: open ? "Collapse timeline" : "Expand timeline",
5007
+ 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 }) })
5008
+ }
5009
+ )
5010
+ ] })
5011
+ ] }),
5012
+ open && /* @__PURE__ */ jsx18(
5013
+ "div",
5014
+ {
5015
+ className: "dialkit-timeline-body",
5016
+ onPointerDown: handleTrackPointerDown,
5017
+ onPointerMove: handleTrackPointerMove,
5018
+ onPointerUp: finishTrackScrub,
5019
+ onPointerCancel: finishTrackScrub,
5020
+ onLostPointerCapture: finishTrackScrub,
5021
+ children: /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-grid", children: [
5022
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-row dialkit-timeline-ruler-row", children: [
5023
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-label" }),
5024
+ /* @__PURE__ */ jsxs14(
5025
+ "div",
5026
+ {
5027
+ ref: laneAreaRef,
5028
+ className: "dialkit-timeline-ruler",
5029
+ onPointerDown: handleRulerPointerDown,
5030
+ onPointerMove: handleRulerPointerMove,
5031
+ onPointerUp: handleRulerPointerUp,
5032
+ onPointerCancel: handleRulerPointerCancel,
5033
+ onLostPointerCapture: handleRulerPointerCancel,
5034
+ title: "Drag to seek \xB7 Option-drag to zoom \xB7 Shift-drag to reset zoom",
5035
+ children: [
5036
+ fineTicks.map((t) => /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-tick dialkit-timeline-tick-fine", style: { left: (t - safeViewStart) * pxPerSecond } }, `fine:${t}`)),
5037
+ mediumTicks.map((t) => /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-tick dialkit-timeline-tick-medium", style: { left: (t - safeViewStart) * pxPerSecond } }, `medium:${t}`)),
5038
+ 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))
5039
+ ]
5040
+ }
5041
+ )
5042
+ ] }),
5043
+ rows,
5044
+ pxPerSecond > 0 && /* @__PURE__ */ jsx18(
5045
+ TimelinePlayheadFlag,
5046
+ {
5047
+ id: meta.id,
5048
+ duration: meta.duration,
5049
+ pxPerSecond,
5050
+ viewStart: safeViewStart,
5051
+ viewEnd,
5052
+ laneWidth,
5053
+ rulerRef: laneAreaRef,
5054
+ headerClearStart: flagClearRange.start,
5055
+ headerClearEnd: flagClearRange.end,
5056
+ onResetView: resetView
5057
+ }
5058
+ )
5059
+ ] })
5060
+ }
5061
+ ),
5062
+ popover && /* @__PURE__ */ jsx18(
5063
+ ClipPopover,
5064
+ {
5065
+ panelId: meta.id,
5066
+ popover,
5067
+ values,
5068
+ theme,
5069
+ onClose: closePopover
5070
+ }
5071
+ )
5072
+ ] });
5073
+ });
5074
+ function ClipPopover({
5075
+ panelId,
5076
+ popover,
5077
+ values,
5078
+ theme,
5079
+ onClose
5080
+ }) {
5081
+ const ref = useRef14(null);
5082
+ const [naturalHeight, setNaturalHeight] = useState11(0);
5083
+ const [viewport, setViewport] = useState11(() => ({
5084
+ width: window.visualViewport?.width ?? window.innerWidth,
5085
+ height: window.visualViewport?.height ?? window.innerHeight,
5086
+ offsetLeft: window.visualViewport?.offsetLeft ?? 0,
5087
+ offsetTop: window.visualViewport?.offsetTop ?? 0
5088
+ }));
5089
+ useLayoutEffect2(() => {
5090
+ const element = ref.current;
5091
+ if (!element) return;
5092
+ const measure = () => setNaturalHeight(element.scrollHeight + 2);
5093
+ measure();
5094
+ const observer = new ResizeObserver(measure);
5095
+ const body = element.querySelector(".dialkit-timeline-popover-body");
5096
+ observer.observe(body ?? element);
5097
+ return () => observer.disconnect();
5098
+ }, [popover.clip.key, popover.stepKey]);
5099
+ useEffect11(() => {
5100
+ const updateViewport = () => setViewport({
5101
+ width: window.visualViewport?.width ?? window.innerWidth,
5102
+ height: window.visualViewport?.height ?? window.innerHeight,
5103
+ offsetLeft: window.visualViewport?.offsetLeft ?? 0,
5104
+ offsetTop: window.visualViewport?.offsetTop ?? 0
5105
+ });
5106
+ window.addEventListener("resize", updateViewport);
5107
+ window.visualViewport?.addEventListener("resize", updateViewport);
5108
+ window.visualViewport?.addEventListener("scroll", updateViewport);
5109
+ return () => {
5110
+ window.removeEventListener("resize", updateViewport);
5111
+ window.visualViewport?.removeEventListener("resize", updateViewport);
5112
+ window.visualViewport?.removeEventListener("scroll", updateViewport);
5113
+ };
5114
+ }, []);
5115
+ useEffect11(() => {
5116
+ const handlePointerDown = (e) => {
5117
+ const target = e.target;
5118
+ if (ref.current?.contains(target)) return;
5119
+ if (target.closest?.(".dialkit-timeline-clip")) return;
5120
+ if (target.closest?.(".dialkit-timeline-label")) return;
5121
+ onClose();
5122
+ };
5123
+ const handleKeyDown = (e) => {
5124
+ if (e.key === "Escape") onClose();
5125
+ };
5126
+ document.addEventListener("pointerdown", handlePointerDown, true);
5127
+ document.addEventListener("keydown", handleKeyDown);
5128
+ return () => {
5129
+ document.removeEventListener("pointerdown", handlePointerDown, true);
5130
+ document.removeEventListener("keydown", handleKeyDown);
5131
+ };
5132
+ }, [onClose]);
5133
+ const { clip, stepKey } = popover;
5134
+ let controls;
5135
+ let title;
5136
+ if (stepKey) {
5137
+ controls = getClipControls(panelId, `${clip.key}.${stepKey}`);
5138
+ if (stepKey === clip.stepKeys?.[0]) {
5139
+ const from = getControlAt(panelId, `${clip.key}.from`);
5140
+ if (from) {
5141
+ const toIndex = controls.findIndex((control) => control.path === `${clip.key}.${stepKey}.to`);
5142
+ controls = toIndex >= 0 ? [...controls.slice(0, toIndex), from, ...controls.slice(toIndex)] : [...controls, from];
5143
+ }
5144
+ }
5145
+ title = `${clip.label} \xB7 ${formatStepLabel(stepKey)}`;
5146
+ } else {
5147
+ controls = getClipControls(panelId, clip.key, clipPopoverExclusions(clip));
5148
+ title = clip.label;
5149
+ }
5150
+ if (controls.length === 0) return null;
5151
+ const targetPath = stepKey ? `${clip.key}.${stepKey}` : clip.key;
5152
+ const durationMeta = getControlAt(panelId, `${targetPath}.duration`);
5153
+ const durationValue = durationMeta ? values[durationMeta.path] : void 0;
5154
+ const transitionDuration = durationMeta?.type === "slider" && typeof durationValue === "number" ? {
5155
+ value: durationValue,
5156
+ onChange: (next) => DialStore.updateValue(panelId, durationMeta.path, next),
5157
+ min: Math.max(TIMELINE_MIN_CLIP_DURATION, durationMeta.min ?? 0),
5158
+ max: durationMeta.max,
5159
+ step: durationMeta.step
5160
+ } : void 0;
5161
+ const displayValues = timelinePopoverDisplayValues(values, clip.key, clip.stepKeys, stepKey);
5162
+ const viewportRight = viewport.offsetLeft + viewport.width;
5163
+ const viewportBottom = viewport.offsetTop + viewport.height;
5164
+ const popoverWidth = Math.min(POPOVER_WIDTH, Math.max(220, viewport.width - 24));
5165
+ const left = clamp(
5166
+ popover.anchor.left + popover.anchor.width / 2 - popoverWidth / 2,
5167
+ viewport.offsetLeft + 12,
5168
+ Math.max(viewport.offsetLeft + 12, viewportRight - popoverWidth - 12)
5169
+ );
5170
+ const spaceAbove = Math.max(0, popover.anchor.top - viewport.offsetTop - 22);
5171
+ const spaceBelow = Math.max(0, viewportBottom - popover.anchor.bottom - 22);
5172
+ const placeAbove = naturalHeight === 0 ? spaceAbove >= spaceBelow : naturalHeight <= spaceAbove || naturalHeight > spaceBelow && spaceAbove >= spaceBelow;
5173
+ const availableHeight = placeAbove ? spaceAbove : spaceBelow;
5174
+ const renderedHeight = Math.min(naturalHeight || availableHeight, availableHeight);
5175
+ const unclampedTop = placeAbove ? popover.anchor.top - 10 - renderedHeight : popover.anchor.bottom + 10;
5176
+ const top = clamp(
5177
+ unclampedTop,
5178
+ viewport.offsetTop + 12,
5179
+ Math.max(viewport.offsetTop + 12, viewportBottom - renderedHeight - 12)
5180
+ );
5181
+ return createPortal4(
5182
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-root", "data-theme": theme, children: /* @__PURE__ */ jsxs14(
5183
+ "div",
5184
+ {
5185
+ ref,
5186
+ className: "dialkit-timeline-popover",
5187
+ "data-placement": placeAbove ? "above" : "below",
5188
+ style: {
5189
+ left,
5190
+ top,
5191
+ width: popoverWidth,
5192
+ maxHeight: availableHeight,
5193
+ visibility: naturalHeight > 0 ? "visible" : "hidden"
5194
+ },
5195
+ role: "dialog",
5196
+ "aria-label": `Edit ${title}`,
5197
+ children: [
5198
+ /* @__PURE__ */ jsxs14("div", { className: "dialkit-timeline-popover-header", children: [
5199
+ /* @__PURE__ */ jsx18("span", { className: "dialkit-timeline-popover-title", children: title }),
5200
+ /* @__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" }) }) })
5201
+ ] }),
5202
+ /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-popover-body", children: /* @__PURE__ */ jsx18(
5203
+ ControlRenderer,
5204
+ {
5205
+ panelId,
5206
+ controls,
5207
+ values: displayValues,
5208
+ transitionDuration
5209
+ }
5210
+ ) })
5211
+ ]
5212
+ }
5213
+ ) }),
5214
+ document.body
5215
+ );
5216
+ }
5217
+ function clipPopoverExclusions(clip) {
5218
+ return /* @__PURE__ */ new Set([
5219
+ ...clip.stepKeys ?? [],
5220
+ ...clip.tracks?.map((track) => track.prop) ?? []
5221
+ ]);
5222
+ }
5223
+ function getClipControls(panelId, controlPath, excludeChildren) {
5224
+ const panel = DialStore.getPanel(panelId);
5225
+ const folder = panel ? findControl(panel.controls, controlPath) : null;
5226
+ if (!folder?.children) return [];
5227
+ return folder.children.filter((control) => {
5228
+ const childKey = control.path.slice(controlPath.length + 1);
5229
+ if (childKey === "at" || childKey === "duration") return false;
5230
+ return !excludeChildren?.has(childKey);
5231
+ });
5232
+ }
5233
+ function getControlAt(panelId, path) {
5234
+ const panel = DialStore.getPanel(panelId);
5235
+ return panel ? findControl(panel.controls, path) : null;
5236
+ }
5237
+ function TimelineClip({
5238
+ timelineId,
5239
+ clip,
5240
+ at,
5241
+ duration,
5242
+ loop,
5243
+ steps,
5244
+ fixedDuration,
5245
+ composite = false,
5246
+ baseAt = 0,
5247
+ delayMode = false,
5248
+ pxPerSecond,
5249
+ viewStart,
5250
+ timelineDuration,
5251
+ selected,
5252
+ selectedStepKey,
5253
+ onClick,
5254
+ onDrag
5255
+ }) {
5256
+ const dragRef = useRef14(null);
5257
+ const [dragging, setDragging] = useState11(false);
5258
+ const isSteps = Boolean(steps?.length);
5259
+ const handlePointerDown = useCallback14(
5260
+ (e) => {
5261
+ if (e.shiftKey) return;
5262
+ e.stopPropagation();
5263
+ const target = e.target;
5264
+ let mode = "move";
5265
+ let boundaryIndex;
5266
+ const boundary = target.dataset?.boundary;
5267
+ if (boundary !== void 0) {
5268
+ mode = "boundary";
5269
+ boundaryIndex = Number(boundary);
5270
+ } else if (!fixedDuration) {
5271
+ const edge = target.dataset?.edge;
5272
+ if (edge) mode = edge;
5273
+ }
5274
+ dragRef.current = {
5275
+ mode,
5276
+ boundaryIndex,
5277
+ pointerX: e.clientX,
5278
+ at,
5279
+ duration,
5280
+ stepDurations: steps?.map((step) => step.duration),
5281
+ clickEl: target.closest?.("[data-step]") ?? null,
5282
+ moved: false
5283
+ };
5284
+ e.currentTarget.setPointerCapture(e.pointerId);
5285
+ },
5286
+ [at, duration, fixedDuration, steps]
5287
+ );
5288
+ const handlePointerMove = useCallback14(
5289
+ (e) => {
5290
+ const drag = dragRef.current;
5291
+ if (!drag || pxPerSecond <= 0) return;
5292
+ const dx = e.clientX - drag.pointerX;
5293
+ if (!drag.moved) {
5294
+ if (Math.abs(dx) <= DRAG_THRESHOLD_PX) return;
5295
+ drag.moved = true;
5296
+ setDragging(true);
5297
+ onDrag();
5298
+ }
5299
+ const dt = dx / pxPerSecond;
5300
+ if (drag.mode === "boundary" && steps && drag.stepDurations) {
5301
+ const index = drag.boundaryIndex ?? 0;
5302
+ const others = drag.stepDurations.reduce((sum, d, j) => j === index ? sum : sum + d, 0);
5303
+ DialStore.updateValue(
5304
+ timelineId,
5305
+ `${clip.key}.${steps[index].key ?? ""}.duration`,
5306
+ clampStepResize(drag.stepDurations[index] + dt, drag.at, others, timelineDuration)
5307
+ );
5308
+ } else if (drag.mode === "move") {
5309
+ if (delayMode) {
5310
+ DialStore.updateValue(
5311
+ timelineId,
5312
+ `${clip.key}.delay`,
5313
+ clampTrackDelay(drag.at + dt - baseAt, baseAt, drag.duration, timelineDuration)
5314
+ );
5315
+ } else {
5316
+ DialStore.updateValue(timelineId, `${clip.key}.at`, clampClipMove(drag.at + dt, drag.duration, timelineDuration));
5317
+ }
5318
+ } else if (drag.mode === "end") {
5319
+ DialStore.updateValue(
5320
+ timelineId,
5321
+ `${clip.key}.duration`,
5322
+ clampClipResizeEnd(drag.duration + dt, drag.at, timelineDuration)
5323
+ );
5324
+ } else if (steps && drag.stepDurations) {
5325
+ const limit = Math.max(baseAt, 0);
5326
+ const next = clampClipResizeStart(Math.max(drag.at + dt, limit), drag.at, drag.stepDurations[0]);
5327
+ DialStore.updateValues(timelineId, {
5328
+ [delayMode ? `${clip.key}.delay` : `${clip.key}.at`]: delayMode ? Math.max(0, next.at - baseAt) : next.at,
5329
+ [`${clip.key}.${steps[0].key ?? ""}.duration`]: next.duration
5330
+ });
5331
+ } else {
5332
+ const limit = Math.max(baseAt, 0);
5333
+ const next = clampClipResizeStart(Math.max(drag.at + dt, limit), drag.at, drag.duration);
5334
+ DialStore.updateValues(timelineId, {
5335
+ [delayMode ? `${clip.key}.delay` : `${clip.key}.at`]: delayMode ? Math.max(0, next.at - baseAt) : next.at,
5336
+ [`${clip.key}.duration`]: next.duration
5337
+ });
5338
+ }
5339
+ },
5340
+ [baseAt, clip.key, delayMode, onDrag, pxPerSecond, steps, timelineId, timelineDuration]
5341
+ );
5342
+ const handlePointerUp = useCallback14(
5343
+ (e) => {
5344
+ const drag = dragRef.current;
5345
+ dragRef.current = null;
5346
+ setDragging(false);
5347
+ if (drag && !drag.moved) {
5348
+ const stepKey = drag.clickEl?.dataset?.step;
5349
+ const anchorEl = drag.clickEl ?? e.currentTarget;
5350
+ onClick(clip, anchorEl.getBoundingClientRect(), stepKey);
5351
+ }
5352
+ },
5353
+ [clip, onClick]
5354
+ );
5355
+ const handlePointerCancel = useCallback14(() => {
5356
+ dragRef.current = null;
5357
+ setDragging(false);
5358
+ }, []);
5359
+ const width = Math.max(duration * pxPerSecond, 14);
5360
+ const resizable = duration > 0 && !fixedDuration && !composite;
5361
+ const durationText = `${fixedDuration && !composite ? "~" : ""}${formatSeconds(duration)}`;
5362
+ const looping = loop === "repeat" && duration > 0;
5363
+ const ghostCycles = [];
5364
+ if (looping) {
5365
+ const maxGhostCycles = 256;
5366
+ const firstGhostIndex = Math.max(1, Math.floor((viewStart - at) / duration));
5367
+ for (let offset = 0; offset < maxGhostCycles; offset++) {
5368
+ const index = firstGhostIndex + offset;
5369
+ const start = at + duration * index;
5370
+ if (start >= timelineDuration - 1e-6) break;
5371
+ ghostCycles.push({
5372
+ start,
5373
+ duration: Math.min(duration, timelineDuration - start),
5374
+ index
5375
+ });
5376
+ }
5377
+ }
5378
+ const boundaryOffsets = [];
5379
+ if (steps) {
5380
+ let cumulative = 0;
5381
+ for (const step of steps) {
5382
+ cumulative += step.duration;
5383
+ boundaryOffsets.push(cumulative);
5384
+ }
5385
+ }
5386
+ 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" : ""}`;
5387
+ return /* @__PURE__ */ jsxs14(Fragment5, { children: [
5388
+ ghostCycles.map((cycle) => {
5389
+ const ghostWidth = Math.max(1, cycle.duration * pxPerSecond - 2);
5390
+ return /* @__PURE__ */ jsx18(
5391
+ "div",
5392
+ {
5393
+ className: "dialkit-timeline-clip-ghost",
5394
+ "data-steps": isSteps || void 0,
5395
+ "aria-hidden": "true",
5396
+ style: {
5397
+ left: (cycle.start - viewStart) * pxPerSecond + 1,
5398
+ width: ghostWidth,
5399
+ background: clip.color
5400
+ },
5401
+ children: steps?.map((step, stepIndex) => /* @__PURE__ */ jsx18(
5402
+ "span",
5403
+ {
5404
+ className: "dialkit-timeline-clip-ghost-segment",
5405
+ style: { width: step.duration * pxPerSecond }
5406
+ },
5407
+ step.key ?? `step:${stepIndex}`
5408
+ ))
5409
+ },
5410
+ `ghost:${cycle.index}`
5411
+ );
5412
+ }),
5413
+ /* @__PURE__ */ jsx18(
5414
+ "div",
5415
+ {
5416
+ className: "dialkit-timeline-clip",
5417
+ "data-steps": isSteps || void 0,
5418
+ "data-composite": composite || void 0,
5419
+ "data-selected": selected || void 0,
5420
+ "data-dragging": dragging || void 0,
5421
+ style: {
5422
+ left: (at - viewStart) * pxPerSecond,
5423
+ width,
5424
+ background: composite ? `${clip.color}80` : clip.color
5425
+ },
5426
+ onPointerDown: handlePointerDown,
5427
+ onPointerMove: handlePointerMove,
5428
+ onPointerUp: handlePointerUp,
5429
+ onPointerCancel: handlePointerCancel,
5430
+ onLostPointerCapture: handlePointerCancel,
5431
+ title: barTitle,
5432
+ children: composite ? /* @__PURE__ */ jsx18(Fragment5, { children: width > 56 && /* @__PURE__ */ jsx18("span", { className: "dialkit-timeline-clip-duration", children: durationText }) }) : isSteps ? /* @__PURE__ */ jsxs14(Fragment5, { children: [
5433
+ steps.map((step) => {
5434
+ const segmentWidth = step.duration * pxPerSecond;
5435
+ return /* @__PURE__ */ jsx18(
5436
+ "div",
5437
+ {
5438
+ className: "dialkit-timeline-clip-segment",
5439
+ "data-step": step.key ?? void 0,
5440
+ "data-selected": selectedStepKey === step.key || void 0,
5441
+ style: { width: segmentWidth },
5442
+ children: segmentWidth > 52 && /* @__PURE__ */ jsx18("span", { className: "dialkit-timeline-clip-duration", children: formatSeconds(step.duration) })
5443
+ },
5444
+ step.key ?? "step"
5445
+ );
5446
+ }),
5447
+ steps.map(
5448
+ (step, index) => step.isPhysics ? null : /* @__PURE__ */ jsx18(
5449
+ "div",
5450
+ {
5451
+ className: "dialkit-timeline-clip-handle",
5452
+ "data-boundary": index,
5453
+ style: { left: boundaryOffsets[index] * pxPerSecond - 4 }
5454
+ },
5455
+ `boundary:${step.key}`
5456
+ )
5457
+ ),
5458
+ !steps[0].isPhysics && /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-clip-handle", "data-edge": "start" })
5459
+ ] }) : /* @__PURE__ */ jsxs14(Fragment5, { children: [
5460
+ resizable && /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-clip-handle", "data-edge": "start" }),
5461
+ width > 56 && /* @__PURE__ */ jsx18("span", { className: "dialkit-timeline-clip-duration", children: durationText }),
5462
+ resizable && /* @__PURE__ */ jsx18("div", { className: "dialkit-timeline-clip-handle", "data-edge": "end" })
5463
+ ] })
5464
+ }
5465
+ ),
5466
+ looping && /* @__PURE__ */ jsx18("span", { className: "dialkit-timeline-loop-infinity", "aria-hidden": "true", title: "Repeats indefinitely", children: "\u221E" })
5467
+ ] });
5468
+ }
5469
+
2842
5470
  // src/components/ButtonGroup.tsx
2843
- import { jsx as jsx16 } from "react/jsx-runtime";
5471
+ import { jsx as jsx19 } from "react/jsx-runtime";
2844
5472
  function ButtonGroup({ buttons }) {
2845
- return /* @__PURE__ */ jsx16("div", { className: "dialkit-button-group", children: buttons.map((button, index) => /* @__PURE__ */ jsx16(
5473
+ return /* @__PURE__ */ jsx19("div", { className: "dialkit-button-group", children: buttons.map((button, index) => /* @__PURE__ */ jsx19(
2846
5474
  "button",
2847
5475
  {
2848
5476
  className: "dialkit-button",
@@ -2854,10 +5482,10 @@ function ButtonGroup({ buttons }) {
2854
5482
  }
2855
5483
 
2856
5484
  // 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";
5485
+ import { useState as useState12, useRef as useRef15, useEffect as useEffect12, useCallback as useCallback15 } from "react";
5486
+ import { createPortal as createPortal5 } from "react-dom";
5487
+ import { motion as motion8, AnimatePresence as AnimatePresence6 } from "motion/react";
5488
+ import { Fragment as Fragment6, jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
2861
5489
  function formatShortcutKey(sc) {
2862
5490
  if (!sc.key) return "\u2014";
2863
5491
  const mod = sc.modifier === "alt" ? "\u2325" : sc.modifier === "shift" ? "\u21E7" : sc.modifier === "meta" ? "\u2318" : "";
@@ -2877,23 +5505,23 @@ function formatInteraction(sc) {
2877
5505
  }
2878
5506
  }
2879
5507
  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(() => {
5508
+ const [isOpen, setIsOpen] = useState12(false);
5509
+ const triggerRef = useRef15(null);
5510
+ const dropdownRef = useRef15(null);
5511
+ const [pos, setPos] = useState12({ top: 0, right: 0 });
5512
+ const open = useCallback15(() => {
2885
5513
  const rect = triggerRef.current?.getBoundingClientRect();
2886
5514
  if (rect) {
2887
5515
  setPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
2888
5516
  }
2889
5517
  setIsOpen(true);
2890
5518
  }, []);
2891
- const close = useCallback11(() => setIsOpen(false), []);
2892
- const toggle = useCallback11(() => {
5519
+ const close = useCallback15(() => setIsOpen(false), []);
5520
+ const toggle = useCallback15(() => {
2893
5521
  if (isOpen) close();
2894
5522
  else open();
2895
5523
  }, [isOpen, open, close]);
2896
- useEffect9(() => {
5524
+ useEffect12(() => {
2897
5525
  if (!isOpen) return;
2898
5526
  const handler = (e) => {
2899
5527
  const target = e.target;
@@ -2924,9 +5552,9 @@ function ShortcutsMenu({ panelId }) {
2924
5552
  label: findLabel(panel.controls)
2925
5553
  };
2926
5554
  });
2927
- return /* @__PURE__ */ jsxs14(Fragment4, { children: [
2928
- /* @__PURE__ */ jsx17(
2929
- motion6.button,
5555
+ return /* @__PURE__ */ jsxs15(Fragment6, { children: [
5556
+ /* @__PURE__ */ jsx20(
5557
+ motion8.button,
2930
5558
  {
2931
5559
  ref: triggerRef,
2932
5560
  className: "dialkit-shortcuts-trigger",
@@ -2934,19 +5562,19 @@ function ShortcutsMenu({ panelId }) {
2934
5562
  title: "Keyboard shortcuts",
2935
5563
  whileTap: { scale: 0.9 },
2936
5564
  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" })
5565
+ children: /* @__PURE__ */ jsxs15("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
5566
+ /* @__PURE__ */ jsx20("rect", { x: "2", y: "6", width: "20", height: "12", rx: "2" }),
5567
+ /* @__PURE__ */ jsx20("path", { d: "M6 10H6.01" }),
5568
+ /* @__PURE__ */ jsx20("path", { d: "M10 10H10.01" }),
5569
+ /* @__PURE__ */ jsx20("path", { d: "M14 10H14.01" }),
5570
+ /* @__PURE__ */ jsx20("path", { d: "M18 10H18.01" }),
5571
+ /* @__PURE__ */ jsx20("path", { d: "M8 14H16" })
2944
5572
  ] })
2945
5573
  }
2946
5574
  ),
2947
- createPortal4(
2948
- /* @__PURE__ */ jsx17(AnimatePresence5, { children: isOpen && /* @__PURE__ */ jsxs14(
2949
- motion6.div,
5575
+ createPortal5(
5576
+ /* @__PURE__ */ jsx20(AnimatePresence6, { children: isOpen && /* @__PURE__ */ jsxs15(
5577
+ motion8.div,
2950
5578
  {
2951
5579
  ref: dropdownRef,
2952
5580
  className: "dialkit-root dialkit-shortcuts-dropdown",
@@ -2956,13 +5584,13 @@ function ShortcutsMenu({ panelId }) {
2956
5584
  exit: { opacity: 0, y: 4, scale: 0.97, pointerEvents: "none" },
2957
5585
  transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
2958
5586
  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) })
5587
+ /* @__PURE__ */ jsx20("div", { className: "dialkit-shortcuts-title", children: "Keyboard Shortcuts" }),
5588
+ /* @__PURE__ */ jsx20("div", { className: "dialkit-shortcuts-list", children: rows.map((row) => /* @__PURE__ */ jsxs15("div", { className: "dialkit-shortcuts-row", children: [
5589
+ /* @__PURE__ */ jsx20("span", { className: "dialkit-shortcuts-row-key", children: formatShortcutKey(row.shortcut) }),
5590
+ /* @__PURE__ */ jsx20("span", { className: "dialkit-shortcuts-row-label", children: row.label }),
5591
+ /* @__PURE__ */ jsx20("span", { className: "dialkit-shortcuts-row-mode", children: formatInteraction(row.shortcut) })
2964
5592
  ] }, row.path)) }),
2965
- /* @__PURE__ */ jsx17("div", { className: "dialkit-shortcuts-hint", children: "See pill badges on controls for keys" })
5593
+ /* @__PURE__ */ jsx20("div", { className: "dialkit-shortcuts-hint", children: "See pill badges on controls for keys" })
2966
5594
  ]
2967
5595
  }
2968
5596
  ) }),
@@ -2973,8 +5601,10 @@ function ShortcutsMenu({ panelId }) {
2973
5601
  export {
2974
5602
  ButtonGroup,
2975
5603
  ColorControl,
5604
+ ControlRenderer,
2976
5605
  DialRoot,
2977
5606
  DialStore,
5607
+ DialTimeline,
2978
5608
  EasingVisualization,
2979
5609
  Folder,
2980
5610
  PresetManager,
@@ -2984,9 +5614,12 @@ export {
2984
5614
  SpringControl,
2985
5615
  SpringVisualization,
2986
5616
  TextControl,
5617
+ TimelineStore,
2987
5618
  Toggle,
2988
5619
  TransitionControl,
5620
+ formatClock,
2989
5621
  useDialKit,
2990
- useDialKitController
5622
+ useDialKitController,
5623
+ useDialTimeline
2991
5624
  };
2992
5625
  //# sourceMappingURL=index.js.map