dialkit 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +433 -3
  2. package/dist/icons.d.ts +5 -1
  3. package/dist/icons.js +18 -0
  4. package/dist/icons.js.map +1 -1
  5. package/dist/index.cjs +3222 -479
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +274 -6
  8. package/dist/index.d.ts +274 -6
  9. package/dist/index.js +3154 -416
  10. package/dist/index.js.map +1 -1
  11. package/dist/solid/index.d.ts +225 -3
  12. package/dist/solid/index.js +5761 -2500
  13. package/dist/solid/index.js.map +1 -1
  14. package/dist/store/index.cjs +57 -16
  15. package/dist/store/index.cjs.map +1 -1
  16. package/dist/store/index.d.cts +14 -2
  17. package/dist/store/index.d.ts +14 -2
  18. package/dist/store/index.js +52 -16
  19. package/dist/store/index.js.map +1 -1
  20. package/dist/styles.css +788 -0
  21. package/dist/svelte/components/ControlRenderer.svelte +5 -2
  22. package/dist/svelte/components/ControlRenderer.svelte.d.ts +2 -0
  23. package/dist/svelte/components/ControlRenderer.svelte.d.ts.map +1 -1
  24. package/dist/svelte/components/DialRoot.svelte +43 -6
  25. package/dist/svelte/components/DialRoot.svelte.d.ts.map +1 -1
  26. package/dist/svelte/components/Panel.svelte +7 -1
  27. package/dist/svelte/components/Panel.svelte.d.ts +2 -0
  28. package/dist/svelte/components/Panel.svelte.d.ts.map +1 -1
  29. package/dist/svelte/components/Timeline/ClipPopover.svelte +206 -0
  30. package/dist/svelte/components/Timeline/ClipPopover.svelte.d.ts +26 -0
  31. package/dist/svelte/components/Timeline/ClipPopover.svelte.d.ts.map +1 -0
  32. package/dist/svelte/components/Timeline/DialTimeline.svelte +125 -0
  33. package/dist/svelte/components/Timeline/DialTimeline.svelte.d.ts +13 -0
  34. package/dist/svelte/components/Timeline/DialTimeline.svelte.d.ts.map +1 -0
  35. package/dist/svelte/components/Timeline/TimelineClip.svelte +233 -0
  36. package/dist/svelte/components/Timeline/TimelineClip.svelte.d.ts +24 -0
  37. package/dist/svelte/components/Timeline/TimelineClip.svelte.d.ts.map +1 -0
  38. package/dist/svelte/components/Timeline/TimelineSection.svelte +803 -0
  39. package/dist/svelte/components/Timeline/TimelineSection.svelte.d.ts +12 -0
  40. package/dist/svelte/components/Timeline/TimelineSection.svelte.d.ts.map +1 -0
  41. package/dist/svelte/components/Timeline/TimelineToggleButton.svelte +25 -0
  42. package/dist/svelte/components/Timeline/TimelineToggleButton.svelte.d.ts +4 -0
  43. package/dist/svelte/components/Timeline/TimelineToggleButton.svelte.d.ts.map +1 -0
  44. package/dist/svelte/components/TransitionControl.svelte +26 -11
  45. package/dist/svelte/components/TransitionControl.svelte.d.ts +9 -0
  46. package/dist/svelte/components/TransitionControl.svelte.d.ts.map +1 -1
  47. package/dist/svelte/createDialTimeline.svelte.d.ts +4 -0
  48. package/dist/svelte/createDialTimeline.svelte.d.ts.map +1 -0
  49. package/dist/svelte/createDialTimeline.svelte.js +73 -0
  50. package/dist/svelte/index.d.ts +4 -0
  51. package/dist/svelte/index.d.ts.map +1 -1
  52. package/dist/svelte/index.js +3 -0
  53. package/dist/svelte/theme-css.d.ts +1 -1
  54. package/dist/svelte/theme-css.d.ts.map +1 -1
  55. package/dist/svelte/theme-css.js +788 -0
  56. package/dist/timeline/index.cjs +1288 -0
  57. package/dist/timeline/index.cjs.map +1 -0
  58. package/dist/timeline/index.d.cts +443 -0
  59. package/dist/timeline/index.d.ts +443 -0
  60. package/dist/timeline/index.js +1233 -0
  61. package/dist/timeline/index.js.map +1 -0
  62. package/dist/vue/index.d.ts +273 -7
  63. package/dist/vue/index.js +4116 -1517
  64. package/dist/vue/index.js.map +1 -1
  65. package/package.json +23 -13
  66. package/dist/solid/index.cjs +0 -3536
  67. package/dist/solid/index.cjs.map +0 -1
  68. package/dist/solid/index.d.cts +0 -295
  69. package/dist/vue/index.cjs +0 -3497
  70. package/dist/vue/index.cjs.map +0 -1
  71. package/dist/vue/index.d.cts +0 -722
package/dist/index.cjs CHANGED
@@ -23,8 +23,10 @@ var index_exports = {};
23
23
  __export(index_exports, {
24
24
  ButtonGroup: () => ButtonGroup,
25
25
  ColorControl: () => ColorControl,
26
+ ControlRenderer: () => ControlRenderer,
26
27
  DialRoot: () => DialRoot,
27
28
  DialStore: () => DialStore,
29
+ DialTimeline: () => DialTimeline,
28
30
  EasingVisualization: () => EasingVisualization,
29
31
  Folder: () => Folder,
30
32
  PresetManager: () => PresetManager,
@@ -34,15 +36,18 @@ __export(index_exports, {
34
36
  SpringControl: () => SpringControl,
35
37
  SpringVisualization: () => SpringVisualization,
36
38
  TextControl: () => TextControl,
39
+ TimelineStore: () => TimelineStore,
37
40
  Toggle: () => Toggle,
38
41
  TransitionControl: () => TransitionControl,
42
+ formatClock: () => formatClock,
39
43
  useDialKit: () => useDialKit,
40
- useDialKitController: () => useDialKitController
44
+ useDialKitController: () => useDialKitController,
45
+ useDialTimeline: () => useDialTimeline
41
46
  });
42
47
  module.exports = __toCommonJS(index_exports);
43
48
 
44
49
  // src/hooks/useDialKit.ts
45
- var import_react = require("react");
50
+ var import_react2 = require("react");
46
51
 
47
52
  // src/store/DialStore.ts
48
53
  var EMPTY_VALUES = Object.freeze({});
@@ -112,6 +117,19 @@ function isSpringConfigValue(value) {
112
117
  function isEasingConfigValue(value) {
113
118
  return hasType(value, "easing");
114
119
  }
120
+ function isHexColor(value) {
121
+ return /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(value);
122
+ }
123
+ function formatLabel(key) {
124
+ return key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
125
+ }
126
+ function inferStep(min, max) {
127
+ const range = max - min;
128
+ if (range <= 1) return 0.01;
129
+ if (range <= 10) return 0.1;
130
+ if (range <= 100) return 1;
131
+ return 10;
132
+ }
115
133
  function isActionConfigValue(value) {
116
134
  return hasType(value, "action");
117
135
  }
@@ -132,6 +150,9 @@ function getFirstOptionValue(options) {
132
150
  var DialStoreClass = class {
133
151
  constructor() {
134
152
  this.panels = /* @__PURE__ */ new Map();
153
+ this.panelsSnapshot = [];
154
+ this.standardPanelsSnapshot = [];
155
+ this.timelinePanelsSnapshot = [];
135
156
  this.listeners = /* @__PURE__ */ new Map();
136
157
  this.globalListeners = /* @__PURE__ */ new Set();
137
158
  this.snapshots = /* @__PURE__ */ new Map();
@@ -145,6 +166,12 @@ var DialStoreClass = class {
145
166
  this.persistConfigs = /* @__PURE__ */ new Map();
146
167
  }
147
168
  registerPanel(id, name, config, shortcuts, options = {}) {
169
+ const existingPanel = this.panels.get(id);
170
+ if (existingPanel && existingPanel.kind !== options.kind) {
171
+ console.warn(
172
+ `[dialkit] Panel id "${id}" cannot be shared by a timeline and a standard panel; the most recent registration controls where it renders.`
173
+ );
174
+ }
148
175
  this.configurePanelRetention(id, options);
149
176
  this.registrationCounts.set(id, (this.registrationCounts.get(id) ?? 0) + 1);
150
177
  const controls = this.parseConfig(config, "", shortcuts);
@@ -156,7 +183,7 @@ var DialStoreClass = class {
156
183
  const values = this.reconcileValues(defaultValues, previousValues, controlsByPath);
157
184
  const previousBaseValues = this.baseValues.get(id) ?? persisted?.baseValues ?? persisted?.values ?? {};
158
185
  const baseValues = this.reconcileValues(defaultValues, previousBaseValues, controlsByPath);
159
- this.panels.set(id, { id, name, controls, values, shortcuts: shortcuts ?? {} });
186
+ this.panels.set(id, { id, name, controls, values, shortcuts: shortcuts ?? {}, kind: options.kind });
160
187
  this.snapshots.set(id, { ...values });
161
188
  this.baseValues.set(id, baseValues);
162
189
  this.defaultValues.set(id, { ...defaultValues });
@@ -183,7 +210,7 @@ var DialStoreClass = class {
183
210
  const defaultValues = this.flattenValues(config, "");
184
211
  this.initTransitionModes(config, "", defaultValues);
185
212
  const nextValues = this.reconcileValues(defaultValues, existing.values, controlsByPath);
186
- const nextPanel = { id, name, controls, values: nextValues, shortcuts: shortcuts ?? existing.shortcuts };
213
+ const nextPanel = { id, name, controls, values: nextValues, shortcuts: shortcuts ?? existing.shortcuts, kind: options.kind ?? existing.kind };
187
214
  this.panels.set(id, nextPanel);
188
215
  this.snapshots.set(id, { ...nextValues });
189
216
  const previousBaseValues = this.baseValues.get(id) ?? {};
@@ -208,8 +235,8 @@ var DialStoreClass = class {
208
235
  }
209
236
  this.registrationCounts.delete(id);
210
237
  this.panels.delete(id);
211
- this.listeners.delete(id);
212
- this.actionListeners.delete(id);
238
+ if (this.listeners.get(id)?.size === 0) this.listeners.delete(id);
239
+ if (this.actionListeners.get(id)?.size === 0) this.actionListeners.delete(id);
213
240
  if (!this.retainedPanels.has(id)) {
214
241
  this.snapshots.delete(id);
215
242
  this.baseValues.delete(id);
@@ -301,8 +328,10 @@ var DialStoreClass = class {
301
328
  getValues(panelId) {
302
329
  return this.snapshots.get(panelId) ?? EMPTY_VALUES;
303
330
  }
304
- getPanels() {
305
- return Array.from(this.panels.values());
331
+ getPanels(kind) {
332
+ if (kind === "panel") return this.standardPanelsSnapshot;
333
+ if (kind === "timeline") return this.timelinePanelsSnapshot;
334
+ return this.panelsSnapshot;
306
335
  }
307
336
  getPanel(id) {
308
337
  return this.panels.get(id);
@@ -313,7 +342,11 @@ var DialStoreClass = class {
313
342
  }
314
343
  this.listeners.get(panelId).add(listener);
315
344
  return () => {
316
- this.listeners.get(panelId)?.delete(listener);
345
+ const listeners = this.listeners.get(panelId);
346
+ listeners?.delete(listener);
347
+ if (listeners?.size === 0 && !this.panels.has(panelId)) {
348
+ this.listeners.delete(panelId);
349
+ }
317
350
  };
318
351
  }
319
352
  subscribeGlobal(listener) {
@@ -326,7 +359,11 @@ var DialStoreClass = class {
326
359
  }
327
360
  this.actionListeners.get(panelId).add(listener);
328
361
  return () => {
329
- this.actionListeners.get(panelId)?.delete(listener);
362
+ const listeners = this.actionListeners.get(panelId);
363
+ listeners?.delete(listener);
364
+ if (listeners?.size === 0 && !this.panels.has(panelId)) {
365
+ this.actionListeners.delete(panelId);
366
+ }
330
367
  };
331
368
  }
332
369
  triggerAction(panelId, path) {
@@ -521,6 +558,9 @@ var DialStoreClass = class {
521
558
  this.listeners.get(panelId)?.forEach((fn) => fn());
522
559
  }
523
560
  notifyGlobal() {
561
+ this.panelsSnapshot = Array.from(this.panels.values());
562
+ this.standardPanelsSnapshot = this.panelsSnapshot.filter((panel) => panel.kind !== "timeline");
563
+ this.timelinePanelsSnapshot = this.panelsSnapshot.filter((panel) => panel.kind === "timeline");
524
564
  this.globalListeners.forEach((fn) => fn());
525
565
  }
526
566
  initTransitionModes(config, prefix, values) {
@@ -636,10 +676,10 @@ var DialStoreClass = class {
636
676
  return typeof value === "object" && value !== null && "type" in value && value.type === "text";
637
677
  }
638
678
  isHexColor(value) {
639
- return /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(value);
679
+ return isHexColor(value);
640
680
  }
641
681
  formatLabel(key) {
642
- return key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
682
+ return formatLabel(key);
643
683
  }
644
684
  inferRange(value) {
645
685
  if (value >= 0 && value <= 1) {
@@ -655,11 +695,7 @@ var DialStoreClass = class {
655
695
  }
656
696
  }
657
697
  inferStep(min, max) {
658
- const range = max - min;
659
- if (range <= 1) return 0.01;
660
- if (range <= 10) return 0.1;
661
- if (range <= 100) return 1;
662
- return 10;
698
+ return inferStep(min, max);
663
699
  }
664
700
  normalizePreservedValue(existingValue, defaultValue, control) {
665
701
  if (existingValue === void 0 || !control) {
@@ -731,31 +767,33 @@ var DialStoreClass = class {
731
767
  return map;
732
768
  }
733
769
  };
734
- var DialStore = new DialStoreClass();
770
+ var DialStore = /* @__PURE__ */ new DialStoreClass();
735
771
 
736
- // src/hooks/useDialKit.ts
737
- function useDialKit(name, config, options) {
738
- return useDialKitController(name, config, options).values;
772
+ // src/hooks/useDialStorePanel.ts
773
+ var import_react = require("react");
774
+ function useSerialized(value) {
775
+ const ref = (0, import_react.useRef)();
776
+ if (!ref.current || !Object.is(ref.current.value, value)) {
777
+ ref.current = { value, text: JSON.stringify(value) };
778
+ }
779
+ return ref.current.text;
739
780
  }
740
- function useDialKitController(name, config, options) {
781
+ function useDialStorePanel(name, config, options = {}) {
741
782
  const instanceId = (0, import_react.useId)();
742
- const hasStableId = options?.id !== void 0;
743
- const panelId = options?.id ?? `${name}-${instanceId}`;
783
+ const hasStableId = options.id !== void 0;
784
+ const panelId = options.id ?? `${name}-${instanceId}`;
744
785
  const configRef = (0, import_react.useRef)(config);
745
- const serializedConfig = JSON.stringify(config);
746
786
  configRef.current = config;
747
- const onActionRef = (0, import_react.useRef)(options?.onAction);
748
- onActionRef.current = options?.onAction;
749
- const shortcutsRef = (0, import_react.useRef)(options?.shortcuts);
750
- shortcutsRef.current = options?.shortcuts;
751
- const persistRef = (0, import_react.useRef)(options?.persist);
752
- persistRef.current = options?.persist;
753
- const serializedShortcuts = JSON.stringify(options?.shortcuts);
754
- const serializedPersist = JSON.stringify(options?.persist);
787
+ const optionsRef = (0, import_react.useRef)(options);
788
+ optionsRef.current = options;
789
+ const serializedConfig = useSerialized(config);
790
+ const serializedShortcuts = useSerialized(options.shortcuts);
791
+ const serializedPersist = useSerialized(options.persist);
755
792
  (0, import_react.useEffect)(() => {
756
- DialStore.registerPanel(panelId, name, configRef.current, shortcutsRef.current, {
793
+ DialStore.registerPanel(panelId, name, configRef.current, optionsRef.current.shortcuts, {
757
794
  retainOnUnmount: hasStableId,
758
- persist: persistRef.current
795
+ persist: optionsRef.current.persist,
796
+ kind: optionsRef.current.kind
759
797
  });
760
798
  return () => DialStore.unregisterPanel(panelId);
761
799
  }, [hasStableId, panelId, name]);
@@ -765,49 +803,65 @@ function useDialKitController(name, config, options) {
765
803
  mountedRef.current = true;
766
804
  return;
767
805
  }
768
- DialStore.updatePanel(panelId, name, configRef.current, shortcutsRef.current, {
806
+ DialStore.updatePanel(panelId, name, configRef.current, optionsRef.current.shortcuts, {
769
807
  retainOnUnmount: hasStableId,
770
- persist: persistRef.current
808
+ persist: optionsRef.current.persist,
809
+ kind: optionsRef.current.kind
771
810
  });
772
811
  }, [hasStableId, panelId, name, serializedConfig, serializedShortcuts, serializedPersist]);
773
- (0, import_react.useEffect)(() => {
774
- return DialStore.subscribeActions(panelId, (action) => {
775
- onActionRef.current?.(action);
776
- });
777
- }, [panelId]);
778
812
  const subscribe = (0, import_react.useCallback)(
779
813
  (callback) => DialStore.subscribe(panelId, callback),
780
814
  [panelId]
781
815
  );
782
- const getSnapshot = (0, import_react.useCallback)(
783
- () => DialStore.getValues(panelId),
784
- [panelId]
785
- );
816
+ const getSnapshot = (0, import_react.useCallback)(() => DialStore.getValues(panelId), [panelId]);
786
817
  const flatValues = (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
787
- const values = (0, import_react.useMemo)(
818
+ return { panelId, flatValues, serializedConfig };
819
+ }
820
+
821
+ // src/hooks/useDialKit.ts
822
+ function useDialKit(name, config, options) {
823
+ return useDialKitController(name, config, options).values;
824
+ }
825
+ function useDialKitController(name, config, options) {
826
+ const { panelId, flatValues, serializedConfig } = useDialStorePanel(name, config, {
827
+ id: options?.id,
828
+ persist: options?.persist,
829
+ shortcuts: options?.shortcuts
830
+ });
831
+ const configRef = (0, import_react2.useRef)(config);
832
+ configRef.current = config;
833
+ const onActionRef = (0, import_react2.useRef)(options?.onAction);
834
+ onActionRef.current = options?.onAction;
835
+ (0, import_react2.useEffect)(() => {
836
+ return DialStore.subscribeActions(panelId, (action) => {
837
+ onActionRef.current?.(action);
838
+ });
839
+ }, [panelId]);
840
+ const values = (0, import_react2.useMemo)(
788
841
  () => resolveDialValues(configRef.current, flatValues),
842
+ // eslint-disable-next-line react-hooks/exhaustive-deps
789
843
  [flatValues, serializedConfig]
790
844
  );
791
- const setValue = (0, import_react.useCallback)(
845
+ const setValue = (0, import_react2.useCallback)(
792
846
  (path, value) => {
793
847
  DialStore.updateValue(panelId, path, value);
794
848
  },
795
849
  [panelId]
796
850
  );
797
- const setValues = (0, import_react.useCallback)(
851
+ const setValues = (0, import_react2.useCallback)(
798
852
  (nextValues) => {
799
853
  DialStore.updateValues(panelId, flattenDialValueUpdates(configRef.current, nextValues));
800
854
  },
801
855
  [panelId]
802
856
  );
803
- const resetValues = (0, import_react.useCallback)(() => {
857
+ const resetValues = (0, import_react2.useCallback)(() => {
804
858
  DialStore.resetValues(panelId);
805
859
  }, [panelId]);
806
- const getValues = (0, import_react.useCallback)(
860
+ const getValues = (0, import_react2.useCallback)(
807
861
  () => resolveDialValues(configRef.current, DialStore.getValues(panelId)),
808
862
  [panelId]
809
863
  );
810
- return (0, import_react.useMemo)(
864
+ return (0, import_react2.useMemo)(
811
865
  () => ({
812
866
  values,
813
867
  setValue,
@@ -820,16 +874,226 @@ function useDialKitController(name, config, options) {
820
874
  }
821
875
 
822
876
  // src/components/DialRoot.tsx
823
- var import_react17 = require("react");
877
+ var import_react21 = require("react");
824
878
  var import_react_dom3 = require("react-dom");
825
879
 
880
+ // src/store/TimelineStore.ts
881
+ function loopSpan(duration, loopStart) {
882
+ if (!Number.isFinite(duration) || duration <= 0) return 0;
883
+ if (!Number.isFinite(loopStart)) loopStart = 0;
884
+ const start = Math.min(Math.max(0, loopStart), duration);
885
+ return duration - start > 0 ? duration - start : duration;
886
+ }
887
+ function foldLoopTime(time, duration, loopStart = 0) {
888
+ if (!Number.isFinite(time) || !Number.isFinite(duration) || duration <= 0) {
889
+ return { time: 0, wraps: 0 };
890
+ }
891
+ if (time < duration) return { time, wraps: 0 };
892
+ const span = loopSpan(duration, loopStart);
893
+ const base = duration - span;
894
+ const over = time - base;
895
+ return { time: base + over % span, wraps: Math.floor(over / span) };
896
+ }
897
+ var TIMELINE_CLIP_COLORS = [
898
+ "#E8E8E8"
899
+ // neutral white — slightly off-white so the pure-white selection ring still reads
900
+ ];
901
+ var EMPTY_TRANSPORT = Object.freeze({ time: 0, playing: false, duration: 0, wraps: 0 });
902
+ var TimelineStoreClass = class {
903
+ constructor() {
904
+ this.timelines = /* @__PURE__ */ new Map();
905
+ this.transports = /* @__PURE__ */ new Map();
906
+ this.listeners = /* @__PURE__ */ new Map();
907
+ this.globalListeners = /* @__PURE__ */ new Set();
908
+ this.registrationCounts = /* @__PURE__ */ new Map();
909
+ this.listCache = null;
910
+ this.rafId = null;
911
+ this.lastTick = 0;
912
+ this.tick = (now) => {
913
+ const dt = Math.max(0, (now - this.lastTick) / 1e3);
914
+ this.lastTick = now;
915
+ let anyPlaying = false;
916
+ for (const [id, transport] of this.transports) {
917
+ if (!transport.playing) continue;
918
+ const meta = this.timelines.get(id);
919
+ const duration = meta?.duration ?? transport.duration;
920
+ if (!Number.isFinite(duration) || duration <= 0) {
921
+ this.transports.set(id, { time: 0, playing: false, duration: 0, wraps: 0 });
922
+ this.notify(id);
923
+ continue;
924
+ }
925
+ let time = transport.time + dt;
926
+ let playing = true;
927
+ let wraps = transport.wraps;
928
+ if (time >= duration) {
929
+ if (meta?.loop) {
930
+ const folded = foldLoopTime(time, duration, meta.loopStart);
931
+ time = folded.time;
932
+ wraps += folded.wraps;
933
+ } else {
934
+ time = duration;
935
+ playing = false;
936
+ }
937
+ }
938
+ this.transports.set(id, { time, playing, duration, wraps });
939
+ if (playing) anyPlaying = true;
940
+ this.notify(id);
941
+ }
942
+ this.rafId = anyPlaying ? window.requestAnimationFrame(this.tick) : null;
943
+ };
944
+ }
945
+ register(meta, options) {
946
+ const existing = this.timelines.get(meta.id);
947
+ if (existing && existing.name !== meta.name) {
948
+ console.warn(
949
+ `[dialkit] Timeline id "${meta.id}" is already registered by "${existing.name}"; "${meta.name}" will share and overwrite that transport.`
950
+ );
951
+ }
952
+ this.registrationCounts.set(meta.id, (this.registrationCounts.get(meta.id) ?? 0) + 1);
953
+ this.applyMeta(meta, options.autoplay);
954
+ }
955
+ update(meta) {
956
+ if (!this.timelines.has(meta.id)) return;
957
+ this.applyMeta(meta, false);
958
+ }
959
+ unregister(id) {
960
+ const nextCount = (this.registrationCounts.get(id) ?? 1) - 1;
961
+ if (nextCount > 0) {
962
+ this.registrationCounts.set(id, nextCount);
963
+ return;
964
+ }
965
+ this.registrationCounts.delete(id);
966
+ this.timelines.delete(id);
967
+ this.transports.delete(id);
968
+ if (this.listeners.get(id)?.size === 0) this.listeners.delete(id);
969
+ this.listCache = null;
970
+ this.notifyGlobal();
971
+ }
972
+ play(id) {
973
+ const transport = this.transports.get(id);
974
+ if (!transport || transport.duration <= 0 || transport.playing) return;
975
+ const restart = transport.time >= transport.duration;
976
+ this.transports.set(id, {
977
+ ...transport,
978
+ time: restart ? 0 : transport.time,
979
+ wraps: restart ? 0 : transport.wraps,
980
+ playing: true
981
+ });
982
+ this.notify(id);
983
+ this.ensureLoop();
984
+ }
985
+ pause(id) {
986
+ const transport = this.transports.get(id);
987
+ if (!transport || !transport.playing) return;
988
+ this.transports.set(id, { ...transport, playing: false });
989
+ this.notify(id);
990
+ }
991
+ replay(id) {
992
+ const transport = this.transports.get(id);
993
+ if (!transport || transport.duration <= 0) return;
994
+ this.transports.set(id, { ...transport, time: 0, wraps: 0, playing: true });
995
+ this.notify(id);
996
+ this.ensureLoop();
997
+ }
998
+ seek(id, time) {
999
+ const transport = this.transports.get(id);
1000
+ if (!transport || !Number.isFinite(time)) return;
1001
+ const clamped = Math.min(transport.duration, Math.max(0, time));
1002
+ this.transports.set(id, { ...transport, time: clamped, wraps: 0 });
1003
+ this.notify(id);
1004
+ }
1005
+ getTransport(id) {
1006
+ return this.transports.get(id) ?? EMPTY_TRANSPORT;
1007
+ }
1008
+ getTimeline(id) {
1009
+ return this.timelines.get(id);
1010
+ }
1011
+ getTimelines() {
1012
+ if (!this.listCache) {
1013
+ this.listCache = Array.from(this.timelines.values());
1014
+ }
1015
+ return this.listCache;
1016
+ }
1017
+ subscribe(id, listener) {
1018
+ if (!this.listeners.has(id)) {
1019
+ this.listeners.set(id, /* @__PURE__ */ new Set());
1020
+ }
1021
+ this.listeners.get(id).add(listener);
1022
+ return () => {
1023
+ const listeners = this.listeners.get(id);
1024
+ listeners?.delete(listener);
1025
+ if (listeners?.size === 0 && !this.timelines.has(id)) {
1026
+ this.listeners.delete(id);
1027
+ }
1028
+ };
1029
+ }
1030
+ subscribeGlobal(listener) {
1031
+ this.globalListeners.add(listener);
1032
+ return () => {
1033
+ this.globalListeners.delete(listener);
1034
+ };
1035
+ }
1036
+ applyMeta(meta, autoplay) {
1037
+ const duration = Number.isFinite(meta.duration) ? Math.max(0, meta.duration) : 0;
1038
+ const loopStart = Number.isFinite(meta.loopStart) ? Math.min(duration, Math.max(0, meta.loopStart)) : 0;
1039
+ const safeMeta = { ...meta, duration, loopStart };
1040
+ this.timelines.set(meta.id, safeMeta);
1041
+ const existing = this.transports.get(meta.id);
1042
+ if (existing) {
1043
+ this.transports.set(meta.id, {
1044
+ time: Math.min(existing.time, duration),
1045
+ playing: duration > 0 && existing.playing,
1046
+ duration,
1047
+ wraps: existing.wraps
1048
+ });
1049
+ } else {
1050
+ const playing = duration > 0 && autoplay;
1051
+ this.transports.set(meta.id, { time: 0, playing, duration, wraps: 0 });
1052
+ if (playing) this.ensureLoop();
1053
+ }
1054
+ this.listCache = null;
1055
+ this.notify(meta.id);
1056
+ this.notifyGlobal();
1057
+ }
1058
+ ensureLoop() {
1059
+ if (this.rafId !== null || typeof window === "undefined") return;
1060
+ this.lastTick = performance.now();
1061
+ this.rafId = window.requestAnimationFrame(this.tick);
1062
+ }
1063
+ notify(id) {
1064
+ this.listeners.get(id)?.forEach((fn) => fn());
1065
+ }
1066
+ notifyGlobal() {
1067
+ this.globalListeners.forEach((fn) => fn());
1068
+ }
1069
+ };
1070
+ var TimelineStore = /* @__PURE__ */ new TimelineStoreClass();
1071
+
1072
+ // src/env.ts
1073
+ var import_meta = {};
1074
+ 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;
1075
+
826
1076
  // src/components/Folder.tsx
827
- var import_react2 = require("react");
828
- var import_react3 = require("motion/react");
1077
+ var import_react3 = require("react");
1078
+ var import_react4 = require("motion/react");
829
1079
 
830
1080
  // src/icons.ts
831
1081
  var ICON_CHEVRON = "M6 9.5L12 15.5L18 9.5";
832
1082
  var ICON_CHECK = "M5 12.75L10 19L19 5";
1083
+ var ICON_PAUSE = [
1084
+ "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",
1085
+ "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"
1086
+ ];
1087
+ 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";
1088
+ var ICON_REPLAY = [
1089
+ "M12 2.5C17.2466 2.50016 21.5 6.7534 21.5 12C21.5 17.2466 17.2466 21.4998 12 21.5C7.52191 21.5 3.76987 18.4025 2.76465 14.2344C2.63517 13.6975 2.96508 13.1578 3.50195 13.0283C4.03883 12.8988 4.57851 13.2288 4.70801 13.7656C5.5016 17.0563 8.46701 19.5 12 19.5C16.142 19.4998 19.5 16.142 19.5 12C19.5 7.85796 16.142 4.50016 12 4.5C9.32981 4.5 6.98389 5.89541 5.6543 8H7.5C8.05228 8 8.5 8.44772 8.5 9C8.5 9.55228 8.05228 10 7.5 10H3.5C2.94772 10 2.5 9.55228 2.5 9V5C2.5 4.44772 2.94772 4 3.5 4C4.05228 4 4.5 4.44772 4.5 5V6.16797C6.2376 3.93677 8.95063 2.5 12 2.5Z",
1090
+ "M10 9.94043C10 9.33379 10.6826 8.97849 11.1797 9.32617L14.1221 11.3857C14.5486 11.6843 14.5486 12.3157 14.1221 12.6143L11.1797 14.6738C10.6826 15.0215 10 14.6662 10 14.0596V9.94043Z"
1091
+ ];
1092
+ var ICON_TIMELINE = [
1093
+ "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",
1094
+ "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",
1095
+ "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"
1096
+ ];
833
1097
  var ICON_CLIPBOARD = {
834
1098
  board: "M8 6C8 4.34315 9.34315 3 11 3H13C14.6569 3 16 4.34315 16 6V7H8V6Z",
835
1099
  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",
@@ -861,18 +1125,18 @@ var ICON_PANEL = {
861
1125
  // src/components/Folder.tsx
862
1126
  var import_jsx_runtime = require("react/jsx-runtime");
863
1127
  function Folder({ title, children, defaultOpen = true, isRoot = false, inline = false, onOpenChange, toolbar, panelHeightOffset = 10 }) {
864
- const [isOpen, setIsOpen] = (0, import_react2.useState)(defaultOpen);
865
- const [isCollapsed, setIsCollapsed] = (0, import_react2.useState)(!defaultOpen);
866
- const contentRef = (0, import_react2.useRef)(null);
867
- const [contentHeight, setContentHeight] = (0, import_react2.useState)(void 0);
868
- const [windowHeight, setWindowHeight] = (0, import_react2.useState)(typeof window !== "undefined" ? window.innerHeight : 800);
869
- (0, import_react2.useEffect)(() => {
1128
+ const [isOpen, setIsOpen] = (0, import_react3.useState)(defaultOpen);
1129
+ const [isCollapsed, setIsCollapsed] = (0, import_react3.useState)(!defaultOpen);
1130
+ const contentRef = (0, import_react3.useRef)(null);
1131
+ const [contentHeight, setContentHeight] = (0, import_react3.useState)(void 0);
1132
+ const [windowHeight, setWindowHeight] = (0, import_react3.useState)(typeof window !== "undefined" ? window.innerHeight : 800);
1133
+ (0, import_react3.useEffect)(() => {
870
1134
  if (!isRoot) return;
871
1135
  const onResize = () => setWindowHeight(window.innerHeight);
872
1136
  window.addEventListener("resize", onResize);
873
1137
  return () => window.removeEventListener("resize", onResize);
874
1138
  }, [isRoot]);
875
- (0, import_react2.useEffect)(() => {
1139
+ (0, import_react3.useEffect)(() => {
876
1140
  const el = contentRef.current;
877
1141
  if (!el) return;
878
1142
  const ro = new ResizeObserver(() => {
@@ -918,7 +1182,7 @@ function Folder({ title, children, defaultOpen = true, isRoot = false, inline =
918
1182
  }
919
1183
  ),
920
1184
  !isRoot && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
921
- import_react3.motion.svg,
1185
+ import_react4.motion.svg,
922
1186
  {
923
1187
  className: "dialkit-folder-icon",
924
1188
  viewBox: "0 0 24 24",
@@ -936,8 +1200,8 @@ function Folder({ title, children, defaultOpen = true, isRoot = false, inline =
936
1200
  ] }),
937
1201
  isRoot && toolbar && isOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dialkit-panel-toolbar", onClick: (e) => e.stopPropagation(), children: toolbar })
938
1202
  ] }),
939
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react3.AnimatePresence, { initial: false, children: isOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
940
- import_react3.motion.div,
1203
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react4.AnimatePresence, { initial: false, children: isOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1204
+ import_react4.motion.div,
941
1205
  {
942
1206
  className: "dialkit-folder-content",
943
1207
  initial: isRoot ? void 0 : { height: 0, opacity: 0 },
@@ -957,7 +1221,7 @@ function Folder({ title, children, defaultOpen = true, isRoot = false, inline =
957
1221
  }
958
1222
  const panelStyle = isOpen ? { width: 280, height: contentHeight !== void 0 ? Math.min(contentHeight + panelHeightOffset, windowHeight - 32) : "auto", borderRadius: 14, boxShadow: "var(--dial-shadow)", cursor: void 0, overflowY: "auto" } : { width: 42, height: 42, borderRadius: "50%", boxSizing: "border-box", boxShadow: "var(--dial-shadow-collapsed)", overflow: "hidden", cursor: "pointer" };
959
1223
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
960
- import_react3.motion.div,
1224
+ import_react4.motion.div,
961
1225
  {
962
1226
  className: "dialkit-panel-inner",
963
1227
  style: panelStyle,
@@ -973,11 +1237,43 @@ function Folder({ title, children, defaultOpen = true, isRoot = false, inline =
973
1237
  }
974
1238
 
975
1239
  // src/components/Panel.tsx
976
- var import_react15 = require("react");
977
- var import_react16 = require("motion/react");
1240
+ var import_react17 = require("react");
1241
+ var import_react18 = require("motion/react");
1242
+
1243
+ // src/copy-instruction.ts
1244
+ function buildCopyInstruction(hookName, panelName, values) {
1245
+ const jsonStr = JSON.stringify(values, null, 2);
1246
+ if (hookName === "useDialTimeline" || hookName === "createDialTimeline") {
1247
+ return `Update the ${hookName} configuration for "${panelName}" with these values:
1248
+
1249
+ \`\`\`json
1250
+ ${jsonStr}
1251
+ \`\`\`
1252
+
1253
+ 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.
1254
+
1255
+ Add this comment immediately above the ${hookName} call as a production handoff note:
1256
+
1257
+ \`\`\`tsx
1258
+ // TODO(production): DialKit's clip.current values are the scrubbable authoring preview.
1259
+ // Replace them with equivalent real Motion animations using the tuned timeline
1260
+ // timings and transitions, then remove ${hookName} and <DialTimeline />.
1261
+ \`\`\``;
1262
+ }
1263
+ return `Update the ${hookName} configuration for "${panelName}" with these values:
1264
+
1265
+ \`\`\`json
1266
+ ${jsonStr}
1267
+ \`\`\`
1268
+
1269
+ Apply these values as the new defaults in the ${hookName} call.`;
1270
+ }
1271
+
1272
+ // src/components/ControlRenderer.tsx
1273
+ var import_react14 = require("react");
978
1274
 
979
1275
  // src/components/ShortcutListener.tsx
980
- var import_react4 = require("react");
1276
+ var import_react5 = require("react");
981
1277
 
982
1278
  // src/shortcut-utils.ts
983
1279
  function decimalsForStep(step) {
@@ -1066,14 +1362,14 @@ function formatModifier(modifier) {
1066
1362
 
1067
1363
  // src/components/ShortcutListener.tsx
1068
1364
  var import_jsx_runtime2 = require("react/jsx-runtime");
1069
- var ShortcutContext = (0, import_react4.createContext)({ activePanelId: null, activePath: null });
1365
+ var ShortcutContext = (0, import_react5.createContext)({ activePanelId: null, activePath: null });
1070
1366
  function ShortcutListener({ children }) {
1071
- const [activeShortcut, setActiveShortcut] = (0, import_react4.useState)({ activePanelId: null, activePath: null });
1072
- const activeKeysRef = (0, import_react4.useRef)(/* @__PURE__ */ new Set());
1073
- const isDraggingRef = (0, import_react4.useRef)(false);
1074
- const lastMouseXRef = (0, import_react4.useRef)(null);
1075
- const dragAccumulatorRef = (0, import_react4.useRef)(0);
1076
- const resolveActiveTarget = (0, import_react4.useCallback)((interaction) => {
1367
+ const [activeShortcut, setActiveShortcut] = (0, import_react5.useState)({ activePanelId: null, activePath: null });
1368
+ const activeKeysRef = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
1369
+ const isDraggingRef = (0, import_react5.useRef)(false);
1370
+ const lastMouseXRef = (0, import_react5.useRef)(null);
1371
+ const dragAccumulatorRef = (0, import_react5.useRef)(0);
1372
+ const resolveActiveTarget = (0, import_react5.useCallback)((interaction) => {
1077
1373
  for (const key of activeKeysRef.current) {
1078
1374
  const panels = DialStore.getPanels();
1079
1375
  for (const panel of panels) {
@@ -1090,7 +1386,7 @@ function ShortcutListener({ children }) {
1090
1386
  }
1091
1387
  return null;
1092
1388
  }, []);
1093
- (0, import_react4.useEffect)(() => {
1389
+ (0, import_react5.useEffect)(() => {
1094
1390
  const handleKeyDown = (e) => {
1095
1391
  if (isInputFocused()) return;
1096
1392
  const key = e.key.toLowerCase();
@@ -1252,8 +1548,8 @@ function ShortcutListener({ children }) {
1252
1548
  }
1253
1549
 
1254
1550
  // src/components/Slider.tsx
1255
- var import_react5 = require("react");
1256
- var import_react6 = require("motion/react");
1551
+ var import_react6 = require("react");
1552
+ var import_react7 = require("motion/react");
1257
1553
  var import_jsx_runtime3 = require("react/jsx-runtime");
1258
1554
  var CLICK_THRESHOLD = 3;
1259
1555
  var DEAD_ZONE = 32;
@@ -1270,47 +1566,47 @@ function Slider({
1270
1566
  shortcut,
1271
1567
  shortcutActive
1272
1568
  }) {
1273
- const wrapperRef = (0, import_react5.useRef)(null);
1274
- const trackRef = (0, import_react5.useRef)(null);
1275
- const inputRef = (0, import_react5.useRef)(null);
1276
- const labelRef = (0, import_react5.useRef)(null);
1277
- const valueSpanRef = (0, import_react5.useRef)(null);
1278
- const [isInteracting, setIsInteracting] = (0, import_react5.useState)(false);
1279
- const [isDragging, setIsDragging] = (0, import_react5.useState)(false);
1280
- const [isHovered, setIsHovered] = (0, import_react5.useState)(false);
1281
- const [isValueHovered, setIsValueHovered] = (0, import_react5.useState)(false);
1282
- const [isValueEditable, setIsValueEditable] = (0, import_react5.useState)(false);
1283
- const [showInput, setShowInput] = (0, import_react5.useState)(false);
1284
- const [inputValue, setInputValue] = (0, import_react5.useState)("");
1285
- const hoverTimeoutRef = (0, import_react5.useRef)(null);
1286
- const pointerDownPos = (0, import_react5.useRef)(null);
1287
- const isClickRef = (0, import_react5.useRef)(true);
1288
- const animRef = (0, import_react5.useRef)(null);
1289
- const wrapperRectRef = (0, import_react5.useRef)(null);
1290
- const scaleRef = (0, import_react5.useRef)(1);
1569
+ const wrapperRef = (0, import_react6.useRef)(null);
1570
+ const trackRef = (0, import_react6.useRef)(null);
1571
+ const inputRef = (0, import_react6.useRef)(null);
1572
+ const labelRef = (0, import_react6.useRef)(null);
1573
+ const valueSpanRef = (0, import_react6.useRef)(null);
1574
+ const [isInteracting, setIsInteracting] = (0, import_react6.useState)(false);
1575
+ const [isDragging, setIsDragging] = (0, import_react6.useState)(false);
1576
+ const [isHovered, setIsHovered] = (0, import_react6.useState)(false);
1577
+ const [isValueHovered, setIsValueHovered] = (0, import_react6.useState)(false);
1578
+ const [isValueEditable, setIsValueEditable] = (0, import_react6.useState)(false);
1579
+ const [showInput, setShowInput] = (0, import_react6.useState)(false);
1580
+ const [inputValue, setInputValue] = (0, import_react6.useState)("");
1581
+ const hoverTimeoutRef = (0, import_react6.useRef)(null);
1582
+ const pointerDownPos = (0, import_react6.useRef)(null);
1583
+ const isClickRef = (0, import_react6.useRef)(true);
1584
+ const animRef = (0, import_react6.useRef)(null);
1585
+ const wrapperRectRef = (0, import_react6.useRef)(null);
1586
+ const scaleRef = (0, import_react6.useRef)(1);
1291
1587
  const percentage = (value - min) / (max - min) * 100;
1292
1588
  const isActive = isInteracting || isHovered;
1293
- const fillPercent = (0, import_react6.useMotionValue)(percentage);
1294
- const fillWidth = (0, import_react6.useTransform)(fillPercent, (pct) => `${pct}%`);
1295
- const handleLeft = (0, import_react6.useTransform)(
1589
+ const fillPercent = (0, import_react7.useMotionValue)(percentage);
1590
+ const fillWidth = (0, import_react7.useTransform)(fillPercent, (pct) => `${pct}%`);
1591
+ const handleLeft = (0, import_react7.useTransform)(
1296
1592
  fillPercent,
1297
1593
  (pct) => `max(5px, calc(${pct}% - 9px))`
1298
1594
  );
1299
- const rubberStretchPx = (0, import_react6.useMotionValue)(0);
1300
- const rubberBandWidth = (0, import_react6.useTransform)(
1595
+ const rubberStretchPx = (0, import_react7.useMotionValue)(0);
1596
+ const rubberBandWidth = (0, import_react7.useTransform)(
1301
1597
  rubberStretchPx,
1302
1598
  (stretch) => `calc(100% + ${Math.abs(stretch)}px)`
1303
1599
  );
1304
- const rubberBandX = (0, import_react6.useTransform)(
1600
+ const rubberBandX = (0, import_react7.useTransform)(
1305
1601
  rubberStretchPx,
1306
1602
  (stretch) => stretch < 0 ? stretch : 0
1307
1603
  );
1308
- (0, import_react5.useEffect)(() => {
1604
+ (0, import_react6.useEffect)(() => {
1309
1605
  if (!isInteracting && !animRef.current) {
1310
1606
  fillPercent.jump(percentage);
1311
1607
  }
1312
1608
  }, [percentage, isInteracting, fillPercent]);
1313
- const positionToValue = (0, import_react5.useCallback)(
1609
+ const positionToValue = (0, import_react6.useCallback)(
1314
1610
  (clientX) => {
1315
1611
  const rect = wrapperRectRef.current;
1316
1612
  if (!rect) return value;
@@ -1323,11 +1619,11 @@ function Slider({
1323
1619
  },
1324
1620
  [min, max, value]
1325
1621
  );
1326
- const percentFromValue = (0, import_react5.useCallback)(
1622
+ const percentFromValue = (0, import_react6.useCallback)(
1327
1623
  (v) => (v - min) / (max - min) * 100,
1328
1624
  [min, max]
1329
1625
  );
1330
- const computeRubberStretch = (0, import_react5.useCallback)(
1626
+ const computeRubberStretch = (0, import_react6.useCallback)(
1331
1627
  (clientX, sign) => {
1332
1628
  const rect = wrapperRectRef.current;
1333
1629
  if (!rect) return 0;
@@ -1337,7 +1633,7 @@ function Slider({
1337
1633
  },
1338
1634
  []
1339
1635
  );
1340
- const handlePointerDown = (0, import_react5.useCallback)(
1636
+ const handlePointerDown = (0, import_react6.useCallback)(
1341
1637
  (e) => {
1342
1638
  if (showInput) return;
1343
1639
  e.preventDefault();
@@ -1353,7 +1649,7 @@ function Slider({
1353
1649
  },
1354
1650
  [showInput]
1355
1651
  );
1356
- const handlePointerMove = (0, import_react5.useCallback)(
1652
+ const handlePointerMove = (0, import_react6.useCallback)(
1357
1653
  (e) => {
1358
1654
  if (!isInteracting || !pointerDownPos.current) return;
1359
1655
  const dx = e.clientX - pointerDownPos.current.x;
@@ -1394,7 +1690,7 @@ function Slider({
1394
1690
  computeRubberStretch
1395
1691
  ]
1396
1692
  );
1397
- const handlePointerUp = (0, import_react5.useCallback)(
1693
+ const handlePointerUp = (0, import_react6.useCallback)(
1398
1694
  (e) => {
1399
1695
  if (!isInteracting) return;
1400
1696
  if (isClickRef.current) {
@@ -1405,7 +1701,7 @@ function Slider({
1405
1701
  if (animRef.current) {
1406
1702
  animRef.current.stop();
1407
1703
  }
1408
- animRef.current = (0, import_react6.animate)(fillPercent, newPct, {
1704
+ animRef.current = (0, import_react7.animate)(fillPercent, newPct, {
1409
1705
  type: "spring",
1410
1706
  stiffness: 300,
1411
1707
  damping: 25,
@@ -1417,7 +1713,7 @@ function Slider({
1417
1713
  onChange(roundValue(snappedValue, step));
1418
1714
  }
1419
1715
  if (rubberStretchPx.get() !== 0) {
1420
- (0, import_react6.animate)(rubberStretchPx, 0, {
1716
+ (0, import_react7.animate)(rubberStretchPx, 0, {
1421
1717
  type: "spring",
1422
1718
  visualDuration: 0.35,
1423
1719
  bounce: 0.15
@@ -1438,7 +1734,7 @@ function Slider({
1438
1734
  rubberStretchPx
1439
1735
  ]
1440
1736
  );
1441
- (0, import_react5.useEffect)(() => {
1737
+ (0, import_react6.useEffect)(() => {
1442
1738
  if (isValueHovered && !showInput && !isValueEditable) {
1443
1739
  hoverTimeoutRef.current = setTimeout(() => {
1444
1740
  setIsValueEditable(true);
@@ -1456,7 +1752,7 @@ function Slider({
1456
1752
  }
1457
1753
  };
1458
1754
  }, [isValueHovered, showInput, isValueEditable]);
1459
- (0, import_react5.useEffect)(() => {
1755
+ (0, import_react6.useEffect)(() => {
1460
1756
  if (showInput && inputRef.current) {
1461
1757
  inputRef.current.focus();
1462
1758
  inputRef.current.select();
@@ -1534,7 +1830,7 @@ function Slider({
1534
1830
  );
1535
1831
  });
1536
1832
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: wrapperRef, className: "dialkit-slider-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1537
- import_react6.motion.div,
1833
+ import_react7.motion.div,
1538
1834
  {
1539
1835
  ref: trackRef,
1540
1836
  className: `dialkit-slider ${isActive ? "dialkit-slider-active" : ""}`,
@@ -1547,7 +1843,7 @@ function Slider({
1547
1843
  children: [
1548
1844
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dialkit-slider-hashmarks", children: hashMarks }),
1549
1845
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1550
- import_react6.motion.div,
1846
+ import_react7.motion.div,
1551
1847
  {
1552
1848
  className: "dialkit-slider-fill",
1553
1849
  style: {
@@ -1556,7 +1852,7 @@ function Slider({
1556
1852
  }
1557
1853
  ),
1558
1854
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1559
- import_react6.motion.div,
1855
+ import_react7.motion.div,
1560
1856
  {
1561
1857
  className: "dialkit-slider-handle",
1562
1858
  style: {
@@ -1611,17 +1907,17 @@ function Slider({
1611
1907
  }
1612
1908
 
1613
1909
  // src/components/SegmentedControl.tsx
1614
- var import_react7 = require("react");
1910
+ var import_react8 = require("react");
1615
1911
  var import_jsx_runtime4 = require("react/jsx-runtime");
1616
1912
  function SegmentedControl({
1617
1913
  options,
1618
1914
  value,
1619
1915
  onChange
1620
1916
  }) {
1621
- const containerRef = (0, import_react7.useRef)(null);
1622
- const hasAnimated = (0, import_react7.useRef)(false);
1623
- const [pillStyle, setPillStyle] = (0, import_react7.useState)(null);
1624
- const measure = (0, import_react7.useCallback)(() => {
1917
+ const containerRef = (0, import_react8.useRef)(null);
1918
+ const hasAnimated = (0, import_react8.useRef)(false);
1919
+ const [pillStyle, setPillStyle] = (0, import_react8.useState)(null);
1920
+ const measure = (0, import_react8.useCallback)(() => {
1625
1921
  const container = containerRef.current;
1626
1922
  if (!container) return;
1627
1923
  const activeButton = container.querySelector('[data-active="true"]');
@@ -1631,7 +1927,7 @@ function SegmentedControl({
1631
1927
  width: activeButton.offsetWidth
1632
1928
  });
1633
1929
  }, []);
1634
- (0, import_react7.useLayoutEffect)(() => {
1930
+ (0, import_react8.useLayoutEffect)(() => {
1635
1931
  measure();
1636
1932
  }, [value, options.length, measure]);
1637
1933
  const shouldAnimate = hasAnimated.current;
@@ -1686,47 +1982,118 @@ function Toggle({ label, checked, onChange, shortcut, shortcutActive }) {
1686
1982
  ] });
1687
1983
  }
1688
1984
 
1689
- // src/components/SpringVisualization.tsx
1690
- var import_jsx_runtime6 = require("react/jsx-runtime");
1691
- function generateSpringCurve(stiffness, damping, mass, duration) {
1692
- const points = [];
1693
- const steps = 100;
1694
- const dt = duration / steps;
1695
- let position = 0;
1696
- let velocity = 0;
1697
- const target = 1;
1698
- for (let i = 0; i <= steps; i++) {
1699
- const time = i * dt;
1700
- points.push([time, position]);
1701
- const springForce = -stiffness * (position - target);
1702
- const dampingForce = -damping * velocity;
1703
- const acceleration = (springForce + dampingForce) / mass;
1704
- velocity += acceleration * dt;
1705
- position += velocity * dt;
1985
+ // src/transition-math.ts
1986
+ function round2(value) {
1987
+ return Math.round(value * 100) / 100;
1988
+ }
1989
+ function clamp(value, min, max) {
1990
+ return Math.min(max, Math.max(min, value));
1991
+ }
1992
+ function isTransitionConfig(value) {
1993
+ return isSpringConfigValue(value) || isEasingConfigValue(value);
1994
+ }
1995
+ function isPhysicsSpring(transition) {
1996
+ return transition.type === "spring" && (transition.stiffness !== void 0 || transition.damping !== void 0 || transition.mass !== void 0);
1997
+ }
1998
+ function springParams(spring) {
1999
+ if (isPhysicsSpring(spring)) {
2000
+ return { stiffness: spring.stiffness ?? 200, damping: spring.damping ?? 25, mass: spring.mass ?? 1 };
2001
+ }
2002
+ const visualDuration = Math.max(0.05, spring.visualDuration ?? 0.3);
2003
+ const bounce = spring.bounce ?? 0.3;
2004
+ const root = 2 * Math.PI / (visualDuration * 1.2);
2005
+ const stiffness = root * root;
2006
+ const damping = 2 * Math.min(1, Math.max(0.05, 1 - bounce)) * Math.sqrt(stiffness);
2007
+ return { stiffness, damping, mass: 1 };
2008
+ }
2009
+ function springProgress(t, { stiffness, damping, mass }) {
2010
+ if (t <= 0) return 0;
2011
+ const w0 = Math.sqrt(stiffness / mass);
2012
+ const zeta = damping / (2 * Math.sqrt(stiffness * mass));
2013
+ if (zeta < 0.9999) {
2014
+ const wd2 = w0 * Math.sqrt(1 - zeta * zeta);
2015
+ return 1 - Math.exp(-zeta * w0 * t) * (Math.cos(wd2 * t) + zeta * w0 / wd2 * Math.sin(wd2 * t));
2016
+ }
2017
+ if (zeta < 1.0001) {
2018
+ return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
2019
+ }
2020
+ const wd = w0 * Math.sqrt(zeta * zeta - 1);
2021
+ const r1 = -zeta * w0 + wd;
2022
+ const r2 = -zeta * w0 - wd;
2023
+ return 1 + (r2 * Math.exp(r1 * t) - r1 * Math.exp(r2 * t)) / (r1 - r2);
2024
+ }
2025
+ function springSettleDuration(params) {
2026
+ const w0 = Math.sqrt(params.stiffness / params.mass);
2027
+ const zeta = params.damping / (2 * Math.sqrt(params.stiffness * params.mass));
2028
+ const decay = zeta >= 1 ? zeta * w0 - w0 * Math.sqrt(Math.max(0, zeta * zeta - 1)) : zeta * w0;
2029
+ const duration = Math.log(200) / Math.max(decay, 1e-6);
2030
+ return round2(clamp(duration, 0.05, 10));
2031
+ }
2032
+ function cubicBezierProgress(p, [x1, y1, x2, y2]) {
2033
+ if (p <= 0) return 0;
2034
+ if (p >= 1) return 1;
2035
+ const sampleX = (t2) => bezierAxis(t2, x1, x2);
2036
+ const sampleY = (t2) => bezierAxis(t2, y1, y2);
2037
+ let t = p;
2038
+ for (let i = 0; i < 8; i++) {
2039
+ const x = sampleX(t) - p;
2040
+ if (Math.abs(x) < 1e-5) return sampleY(t);
2041
+ const dx = bezierAxisDerivative(t, x1, x2);
2042
+ if (Math.abs(dx) < 1e-6) break;
2043
+ t -= x / dx;
2044
+ }
2045
+ let lo = 0;
2046
+ let hi = 1;
2047
+ t = p;
2048
+ while (hi - lo > 1e-5) {
2049
+ if (sampleX(t) < p) lo = t;
2050
+ else hi = t;
2051
+ t = (lo + hi) / 2;
2052
+ }
2053
+ return sampleY(t);
2054
+ }
2055
+ function bezierAxis(t, a1, a2) {
2056
+ return (1 - 3 * a2 + 3 * a1) * t * t * t + (3 * a2 - 6 * a1) * t * t + 3 * a1 * t;
2057
+ }
2058
+ function bezierAxisDerivative(t, a1, a2) {
2059
+ return 3 * (1 - 3 * a2 + 3 * a1) * t * t + 2 * (3 * a2 - 6 * a1) * t + 3 * a1;
2060
+ }
2061
+ function resolveClipTransition(raw, clipDuration) {
2062
+ const safeDuration = Math.max(0.05, clipDuration);
2063
+ if (raw.type === "easing") {
2064
+ return {
2065
+ transition: { ...raw, duration: safeDuration },
2066
+ duration: safeDuration,
2067
+ isPhysics: false
2068
+ };
2069
+ }
2070
+ if (isPhysicsSpring(raw)) {
2071
+ return {
2072
+ transition: raw,
2073
+ duration: springSettleDuration(springParams(raw)),
2074
+ isPhysics: true
2075
+ };
1706
2076
  }
1707
- return points;
2077
+ return {
2078
+ transition: { type: "spring", bounce: raw.bounce ?? 0.2, visualDuration: safeDuration },
2079
+ duration: safeDuration,
2080
+ isPhysics: false
2081
+ };
1708
2082
  }
2083
+
2084
+ // src/components/SpringVisualization.tsx
2085
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1709
2086
  function SpringVisualization({ spring, isSimpleMode }) {
1710
2087
  const width = 256;
1711
2088
  const height = 140;
1712
- let stiffness;
1713
- let damping;
1714
- let mass;
1715
- if (isSimpleMode) {
1716
- const visualDuration = spring.visualDuration ?? 0.3;
1717
- const bounce = spring.bounce ?? 0.2;
1718
- mass = 1;
1719
- stiffness = 2 * Math.PI / visualDuration;
1720
- stiffness = Math.pow(stiffness, 2);
1721
- const dampingRatio = 1 - bounce;
1722
- damping = 2 * dampingRatio * Math.sqrt(stiffness * mass);
1723
- } else {
1724
- stiffness = spring.stiffness ?? 400;
1725
- damping = spring.damping ?? 17;
1726
- mass = spring.mass ?? 1;
1727
- }
2089
+ 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 });
1728
2090
  const duration = 2;
1729
- const points = generateSpringCurve(stiffness, damping, mass, duration);
2091
+ const steps = 100;
2092
+ const points = [];
2093
+ for (let i = 0; i <= steps; i++) {
2094
+ const time = i / steps * duration;
2095
+ points.push([time, springProgress(time, params)]);
2096
+ }
1730
2097
  const values = points.map(([, value]) => value);
1731
2098
  const minValue = Math.min(...values);
1732
2099
  const maxValue = Math.max(...values);
@@ -1775,20 +2142,20 @@ function SpringVisualization({ spring, isSimpleMode }) {
1775
2142
  }
1776
2143
 
1777
2144
  // src/components/SpringControl.tsx
1778
- var import_react8 = require("react");
2145
+ var import_react9 = require("react");
1779
2146
  var import_jsx_runtime7 = require("react/jsx-runtime");
1780
2147
  function SpringControl({ panelId, path, label, spring, onChange }) {
1781
- const subscribe = (0, import_react8.useCallback)(
2148
+ const subscribe = (0, import_react9.useCallback)(
1782
2149
  (callback) => DialStore.subscribe(panelId, callback),
1783
2150
  [panelId]
1784
2151
  );
1785
- const getSnapshot = (0, import_react8.useCallback)(
2152
+ const getSnapshot = (0, import_react9.useCallback)(
1786
2153
  () => DialStore.getSpringMode(panelId, path),
1787
2154
  [panelId, path]
1788
2155
  );
1789
- const mode = (0, import_react8.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
2156
+ const mode = (0, import_react9.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
1790
2157
  const isSimpleMode = mode === "simple";
1791
- const cache = (0, import_react8.useRef)({
2158
+ const cache = (0, import_react9.useRef)({
1792
2159
  simple: spring.visualDuration !== void 0 ? spring : { type: "spring", visualDuration: 0.3, bounce: 0.2 },
1793
2160
  advanced: spring.stiffness !== void 0 ? spring : { type: "spring", stiffness: 200, damping: 25, mass: 1 }
1794
2161
  });
@@ -1935,21 +2302,29 @@ function EasingVisualization({ easing }) {
1935
2302
  }
1936
2303
 
1937
2304
  // src/components/TransitionControl.tsx
1938
- var import_react9 = require("react");
2305
+ var import_react10 = require("react");
1939
2306
  var import_jsx_runtime9 = require("react/jsx-runtime");
1940
- function TransitionControl({ panelId, path, label, value, onChange }) {
1941
- const subscribe = (0, import_react9.useCallback)(
2307
+ function TransitionControl({
2308
+ panelId,
2309
+ path,
2310
+ label,
2311
+ value,
2312
+ onChange,
2313
+ hideDuration = false,
2314
+ durationControl
2315
+ }) {
2316
+ const subscribe = (0, import_react10.useCallback)(
1942
2317
  (callback) => DialStore.subscribe(panelId, callback),
1943
2318
  [panelId]
1944
2319
  );
1945
- const getSnapshot = (0, import_react9.useCallback)(
2320
+ const getSnapshot = (0, import_react10.useCallback)(
1946
2321
  () => DialStore.getTransitionMode(panelId, path),
1947
2322
  [panelId, path]
1948
2323
  );
1949
- const mode = (0, import_react9.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
2324
+ const mode = (0, import_react10.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
1950
2325
  const isEasing = mode === "easing";
1951
2326
  const isSimpleSpring = mode === "simple";
1952
- const cache = (0, import_react9.useRef)({
2327
+ const cache = (0, import_react10.useRef)({
1953
2328
  easing: value.type === "easing" ? value : { type: "easing", duration: 0.3, ease: [1, -0.4, 0.5, 1] },
1954
2329
  simple: value.type === "spring" && value.visualDuration !== void 0 ? value : { type: "spring", visualDuration: 0.3, bounce: 0.2 },
1955
2330
  advanced: value.type === "spring" && value.stiffness !== void 0 ? value : { type: "spring", stiffness: 200, damping: 25, mass: 1 }
@@ -1987,6 +2362,21 @@ function TransitionControl({ panelId, path, label, value, onChange }) {
1987
2362
  newEase[index] = val;
1988
2363
  onChange({ ...easing, ease: newEase });
1989
2364
  };
2365
+ const durationSlider = !hideDuration && (isEasing || isSimpleSpring) ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2366
+ Slider,
2367
+ {
2368
+ label: "Duration",
2369
+ value: durationControl?.value ?? (isEasing ? easing.duration : spring.visualDuration ?? 0.3),
2370
+ onChange: durationControl?.onChange ?? ((next) => {
2371
+ if (isEasing) onChange({ ...easing, duration: next });
2372
+ else handleSpringUpdate("visualDuration", next);
2373
+ }),
2374
+ min: durationControl?.min ?? 0.1,
2375
+ max: durationControl?.max ?? (isEasing ? 2 : 1),
2376
+ step: durationControl?.step ?? 0.05,
2377
+ unit: "s"
2378
+ }
2379
+ ) : null;
1990
2380
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Folder, { title: label, defaultOpen: true, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
1991
2381
  isEasing ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(EasingVisualization, { easing }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(SpringVisualization, { spring, isSimpleMode: isSimpleSpring }),
1992
2382
  /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dialkit-labeled-control", children: [
@@ -2009,16 +2399,13 @@ function TransitionControl({ panelId, path, label, value, onChange }) {
2009
2399
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Slider, { label: "y1", value: easing.ease[1], onChange: (v) => updateEase(1, v), min: -1, max: 2, step: 0.01 }),
2010
2400
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Slider, { label: "x2", value: easing.ease[2], onChange: (v) => updateEase(2, v), min: 0, max: 1, step: 0.01 }),
2011
2401
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Slider, { label: "y2", value: easing.ease[3], onChange: (v) => updateEase(3, v), min: -1, max: 2, step: 0.01 }),
2012
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Slider, { label: "Duration", value: easing.duration, onChange: (v) => onChange({ ...easing, duration: v }), min: 0.1, max: 2, step: 0.05, unit: "s" }),
2013
2402
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(EaseTextInput, { ease: easing.ease, onChange: (newEase) => onChange({ ...easing, ease: newEase }) })
2014
- ] }) : isSimpleSpring ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
2015
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Slider, { label: "Duration", value: spring.visualDuration ?? 0.3, onChange: (v) => handleSpringUpdate("visualDuration", v), min: 0.1, max: 1, step: 0.05, unit: "s" }),
2016
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Slider, { label: "Bounce", value: spring.bounce ?? 0.2, onChange: (v) => handleSpringUpdate("bounce", v), min: 0, max: 1, step: 0.05 })
2017
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
2403
+ ] }) : isSimpleSpring ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Slider, { label: "Bounce", value: spring.bounce ?? 0.2, onChange: (v) => handleSpringUpdate("bounce", v), min: 0, max: 1, step: 0.05 }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
2018
2404
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Slider, { label: "Stiffness", value: spring.stiffness ?? 400, onChange: (v) => handleSpringUpdate("stiffness", v), min: 1, max: 1e3, step: 10 }),
2019
2405
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Slider, { label: "Damping", value: spring.damping ?? 17, onChange: (v) => handleSpringUpdate("damping", v), min: 1, max: 100, step: 1 }),
2020
2406
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Slider, { label: "Mass", value: spring.mass ?? 1, onChange: (v) => handleSpringUpdate("mass", v), min: 0.1, max: 10, step: 0.1 })
2021
- ] })
2407
+ ] }),
2408
+ durationSlider
2022
2409
  ] }) });
2023
2410
  }
2024
2411
  function formatEase(ease) {
@@ -2032,8 +2419,8 @@ function parseEase(str) {
2032
2419
  return null;
2033
2420
  }
2034
2421
  function EaseTextInput({ ease, onChange }) {
2035
- const [editing, setEditing] = (0, import_react9.useState)(false);
2036
- const [draft, setDraft] = (0, import_react9.useState)("");
2422
+ const [editing, setEditing] = (0, import_react10.useState)(false);
2423
+ const [draft, setDraft] = (0, import_react10.useState)("");
2037
2424
  const handleFocus = () => {
2038
2425
  setDraft(formatEase(ease));
2039
2426
  setEditing(true);
@@ -2085,9 +2472,9 @@ function TextControl({ label, value, onChange, placeholder }) {
2085
2472
  }
2086
2473
 
2087
2474
  // src/components/SelectControl.tsx
2088
- var import_react10 = require("react");
2475
+ var import_react11 = require("react");
2089
2476
  var import_react_dom = require("react-dom");
2090
- var import_react11 = require("motion/react");
2477
+ var import_react12 = require("motion/react");
2091
2478
 
2092
2479
  // src/dropdown-position.ts
2093
2480
  function getDropdownPosition(trigger, portalRoot, options = {}) {
@@ -2118,27 +2505,27 @@ function normalizeOptions(options) {
2118
2505
  );
2119
2506
  }
2120
2507
  function SelectControl({ label, value, options, onChange }) {
2121
- const [isOpen, setIsOpen] = (0, import_react10.useState)(false);
2122
- const triggerRef = (0, import_react10.useRef)(null);
2123
- const dropdownRef = (0, import_react10.useRef)(null);
2124
- const [portalTarget, setPortalTarget] = (0, import_react10.useState)(null);
2125
- const [pos, setPos] = (0, import_react10.useState)(null);
2508
+ const [isOpen, setIsOpen] = (0, import_react11.useState)(false);
2509
+ const triggerRef = (0, import_react11.useRef)(null);
2510
+ const dropdownRef = (0, import_react11.useRef)(null);
2511
+ const [portalTarget, setPortalTarget] = (0, import_react11.useState)(null);
2512
+ const [pos, setPos] = (0, import_react11.useState)(null);
2126
2513
  const normalized = normalizeOptions(options);
2127
2514
  const selectedOption = normalized.find((o) => o.value === value);
2128
- const updatePos = (0, import_react10.useCallback)(() => {
2515
+ const updatePos = (0, import_react11.useCallback)(() => {
2129
2516
  const el = triggerRef.current;
2130
2517
  if (!el || !portalTarget) return;
2131
2518
  const dropdownHeight = 8 + normalized.length * 36;
2132
2519
  setPos(getDropdownPosition(el, portalTarget, { dropdownHeight }));
2133
2520
  }, [normalized.length, portalTarget]);
2134
- (0, import_react10.useEffect)(() => {
2521
+ (0, import_react11.useEffect)(() => {
2135
2522
  setPortalTarget(getDialKitPortalRoot(triggerRef.current) ?? document.body);
2136
2523
  }, []);
2137
- (0, import_react10.useEffect)(() => {
2524
+ (0, import_react11.useEffect)(() => {
2138
2525
  if (!isOpen) return;
2139
2526
  updatePos();
2140
2527
  }, [isOpen, updatePos]);
2141
- (0, import_react10.useEffect)(() => {
2528
+ (0, import_react11.useEffect)(() => {
2142
2529
  if (!isOpen) return;
2143
2530
  const handleClick = (e) => {
2144
2531
  const target = e.target;
@@ -2162,7 +2549,7 @@ function SelectControl({ label, value, options, onChange }) {
2162
2549
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "dialkit-select-right", children: [
2163
2550
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "dialkit-select-value", children: selectedOption?.label ?? value }),
2164
2551
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2165
- import_react11.motion.svg,
2552
+ import_react12.motion.svg,
2166
2553
  {
2167
2554
  className: "dialkit-select-chevron",
2168
2555
  viewBox: "0 0 24 24",
@@ -2181,8 +2568,8 @@ function SelectControl({ label, value, options, onChange }) {
2181
2568
  }
2182
2569
  ),
2183
2570
  portalTarget && (0, import_react_dom.createPortal)(
2184
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react11.AnimatePresence, { children: isOpen && pos && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2185
- import_react11.motion.div,
2571
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react12.AnimatePresence, { children: isOpen && pos && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2572
+ import_react12.motion.div,
2186
2573
  {
2187
2574
  ref: dropdownRef,
2188
2575
  className: "dialkit-select-dropdown",
@@ -2218,14 +2605,14 @@ function SelectControl({ label, value, options, onChange }) {
2218
2605
  }
2219
2606
 
2220
2607
  // src/components/ColorControl.tsx
2221
- var import_react12 = require("react");
2608
+ var import_react13 = require("react");
2222
2609
  var import_jsx_runtime12 = require("react/jsx-runtime");
2223
2610
  var HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
2224
2611
  function ColorControl({ label, value, onChange }) {
2225
- const [isEditing, setIsEditing] = (0, import_react12.useState)(false);
2226
- const [editValue, setEditValue] = (0, import_react12.useState)(value);
2227
- const colorInputRef = (0, import_react12.useRef)(null);
2228
- (0, import_react12.useEffect)(() => {
2612
+ const [isEditing, setIsEditing] = (0, import_react13.useState)(false);
2613
+ const [editValue, setEditValue] = (0, import_react13.useState)(value);
2614
+ const colorInputRef = (0, import_react13.useRef)(null);
2615
+ (0, import_react13.useEffect)(() => {
2229
2616
  if (!isEditing) {
2230
2617
  setEditValue(value);
2231
2618
  }
@@ -2295,266 +2682,105 @@ function expandShorthandHex(hex) {
2295
2682
  return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
2296
2683
  }
2297
2684
 
2298
- // src/components/PresetManager.tsx
2299
- var import_react13 = require("react");
2300
- var import_react_dom2 = require("react-dom");
2301
- var import_react14 = require("motion/react");
2685
+ // src/components/ControlRenderer.tsx
2302
2686
  var import_jsx_runtime13 = require("react/jsx-runtime");
2303
- function PresetManager({ panelId, presets, activePresetId, onAdd }) {
2304
- const [isOpen, setIsOpen] = (0, import_react13.useState)(false);
2305
- const triggerRef = (0, import_react13.useRef)(null);
2306
- const dropdownRef = (0, import_react13.useRef)(null);
2307
- const [pos, setPos] = (0, import_react13.useState)({ top: 0, left: 0, width: 0 });
2308
- const hasPresets = presets.length > 0;
2309
- const activePreset = presets.find((p) => p.id === activePresetId);
2310
- const open = (0, import_react13.useCallback)(() => {
2311
- if (!hasPresets) return;
2312
- const rect = triggerRef.current?.getBoundingClientRect();
2313
- if (rect) {
2314
- setPos({ top: rect.bottom + 4, left: rect.left, width: rect.width });
2315
- }
2316
- setIsOpen(true);
2317
- }, [hasPresets]);
2318
- const close = (0, import_react13.useCallback)(() => setIsOpen(false), []);
2319
- const toggle = (0, import_react13.useCallback)(() => {
2320
- if (isOpen) close();
2321
- else open();
2322
- }, [isOpen, open, close]);
2323
- (0, import_react13.useEffect)(() => {
2324
- if (!isOpen) return;
2325
- const handler = (e) => {
2326
- const target = e.target;
2327
- if (triggerRef.current?.contains(target) || dropdownRef.current?.contains(target)) return;
2328
- close();
2329
- };
2330
- document.addEventListener("mousedown", handler);
2331
- return () => document.removeEventListener("mousedown", handler);
2332
- }, [isOpen, close]);
2333
- const handleSelect = (presetId) => {
2334
- if (presetId) {
2335
- DialStore.loadPreset(panelId, presetId);
2336
- } else {
2337
- DialStore.clearActivePreset(panelId);
2338
- }
2339
- close();
2340
- };
2341
- const handleDelete = (e, presetId) => {
2342
- e.stopPropagation();
2343
- DialStore.deletePreset(panelId, presetId);
2344
- };
2345
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "dialkit-preset-manager", children: [
2346
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2347
- "button",
2348
- {
2349
- ref: triggerRef,
2350
- className: "dialkit-preset-trigger",
2351
- onClick: toggle,
2352
- "data-open": String(isOpen),
2353
- "data-has-preset": String(!!activePreset),
2354
- "data-disabled": String(!hasPresets),
2355
- children: [
2356
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "dialkit-preset-label", children: activePreset ? activePreset.name : "Version 1" }),
2357
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2358
- import_react14.motion.svg,
2359
- {
2360
- className: "dialkit-select-chevron",
2361
- viewBox: "0 0 24 24",
2362
- fill: "none",
2363
- stroke: "currentColor",
2364
- strokeWidth: "2.5",
2365
- strokeLinecap: "round",
2366
- strokeLinejoin: "round",
2367
- animate: { rotate: isOpen ? 180 : 0, opacity: hasPresets ? 0.6 : 0.25 },
2368
- transition: { type: "spring", visualDuration: 0.2, bounce: 0.15 },
2369
- children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("path", { d: ICON_CHEVRON })
2370
- }
2371
- )
2372
- ]
2373
- }
2374
- ),
2375
- (0, import_react_dom2.createPortal)(
2376
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react14.AnimatePresence, { children: isOpen && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2377
- import_react14.motion.div,
2378
- {
2379
- ref: dropdownRef,
2380
- className: "dialkit-root dialkit-preset-dropdown",
2381
- style: { position: "fixed", top: pos.top, left: pos.left, minWidth: pos.width },
2382
- initial: { opacity: 0, y: 4, scale: 0.97 },
2383
- animate: { opacity: 1, y: 0, scale: 1 },
2384
- exit: { opacity: 0, y: 4, scale: 0.97, pointerEvents: "none" },
2385
- transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
2386
- children: [
2387
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2388
- "div",
2389
- {
2390
- className: "dialkit-preset-item",
2391
- "data-active": String(!activePresetId),
2392
- onClick: () => handleSelect(null),
2393
- children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "dialkit-preset-name", children: "Version 1" })
2394
- }
2395
- ),
2396
- presets.map((preset) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2397
- "div",
2398
- {
2399
- className: "dialkit-preset-item",
2400
- "data-active": String(preset.id === activePresetId),
2401
- onClick: () => handleSelect(preset.id),
2402
- children: [
2403
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "dialkit-preset-name", children: preset.name }),
2404
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2405
- "button",
2406
- {
2407
- className: "dialkit-preset-delete",
2408
- onClick: (e) => handleDelete(e, preset.id),
2409
- title: "Delete preset",
2410
- children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: ICON_TRASH.map((d, i) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("path", { d }, i)) })
2411
- }
2412
- )
2413
- ]
2414
- },
2415
- preset.id
2416
- ))
2417
- ]
2418
- }
2419
- ) }),
2420
- document.body
2421
- )
2422
- ] });
2423
- }
2424
-
2425
- // src/components/Panel.tsx
2426
- var import_jsx_runtime14 = require("react/jsx-runtime");
2427
- function Panel({ panel, defaultOpen = true, inline = false, onOpenChange, variant = "root" }) {
2428
- const [copied, setCopied] = (0, import_react15.useState)(false);
2429
- const [isPanelOpen, setIsPanelOpen] = (0, import_react15.useState)(defaultOpen);
2430
- const shortcutCtx = (0, import_react15.useContext)(ShortcutContext);
2431
- const hasShortcuts = Object.keys(panel.shortcuts).length > 0;
2432
- const subscribe = (0, import_react15.useCallback)(
2433
- (callback) => DialStore.subscribe(panel.id, callback),
2434
- [panel.id]
2435
- );
2436
- const getSnapshot = (0, import_react15.useCallback)(
2437
- () => DialStore.getValues(panel.id),
2438
- [panel.id]
2439
- );
2440
- const values = (0, import_react15.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
2441
- const presets = DialStore.getPresets(panel.id);
2442
- const activePresetId = DialStore.getActivePresetId(panel.id);
2443
- const handleAddPreset = () => {
2444
- const nextNum = presets.length + 2;
2445
- DialStore.savePreset(panel.id, `Version ${nextNum}`);
2446
- };
2447
- const handleCopy = () => {
2448
- const jsonStr = JSON.stringify(values, null, 2);
2449
- const instruction = `Update the useDialKit configuration for "${panel.name}" with these values:
2450
-
2451
- \`\`\`json
2452
- ${jsonStr}
2453
- \`\`\`
2454
-
2455
- Apply these values as the new defaults in the useDialKit call.`;
2456
- navigator.clipboard.writeText(instruction);
2457
- setCopied(true);
2458
- setTimeout(() => setCopied(false), 1500);
2459
- };
2460
- const handleOpenChange = (0, import_react15.useCallback)((open) => {
2461
- setIsPanelOpen(open);
2462
- onOpenChange?.(open);
2463
- }, [onOpenChange]);
2687
+ function ControlRenderer({ panelId, controls, values, transitionDuration }) {
2688
+ const shortcutCtx = (0, import_react14.useContext)(ShortcutContext);
2464
2689
  const renderControl = (control) => {
2465
2690
  const value = values[control.path];
2466
2691
  switch (control.type) {
2467
2692
  case "slider":
2468
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2693
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2469
2694
  Slider,
2470
2695
  {
2471
2696
  label: control.label,
2472
2697
  value,
2473
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v),
2698
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v),
2474
2699
  min: control.min,
2475
2700
  max: control.max,
2476
2701
  step: control.step,
2477
2702
  shortcut: control.shortcut,
2478
- shortcutActive: shortcutCtx.activePanelId === panel.id && shortcutCtx.activePath === control.path
2703
+ shortcutActive: shortcutCtx.activePanelId === panelId && shortcutCtx.activePath === control.path
2479
2704
  },
2480
2705
  control.path
2481
2706
  );
2482
2707
  case "toggle":
2483
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2708
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2484
2709
  Toggle,
2485
2710
  {
2486
2711
  label: control.label,
2487
2712
  checked: value,
2488
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v),
2713
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v),
2489
2714
  shortcut: control.shortcut,
2490
- shortcutActive: shortcutCtx.activePanelId === panel.id && shortcutCtx.activePath === control.path
2715
+ shortcutActive: shortcutCtx.activePanelId === panelId && shortcutCtx.activePath === control.path
2491
2716
  },
2492
2717
  control.path
2493
2718
  );
2494
2719
  case "spring":
2495
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2720
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2496
2721
  SpringControl,
2497
2722
  {
2498
- panelId: panel.id,
2723
+ panelId,
2499
2724
  path: control.path,
2500
2725
  label: control.label,
2501
2726
  spring: value,
2502
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
2727
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v)
2503
2728
  },
2504
2729
  control.path
2505
2730
  );
2506
2731
  case "transition":
2507
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2732
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2508
2733
  TransitionControl,
2509
2734
  {
2510
- panelId: panel.id,
2735
+ panelId,
2511
2736
  path: control.path,
2512
2737
  label: control.label,
2513
2738
  value,
2514
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
2739
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v),
2740
+ durationControl: transitionDuration
2515
2741
  },
2516
2742
  control.path
2517
2743
  );
2518
2744
  case "folder":
2519
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Folder, { title: control.label, defaultOpen: control.defaultOpen ?? true, children: control.children?.map(renderControl) }, control.path);
2745
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Folder, { title: control.label, defaultOpen: control.defaultOpen ?? true, children: control.children?.map(renderControl) }, control.path);
2520
2746
  case "text":
2521
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2747
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2522
2748
  TextControl,
2523
2749
  {
2524
2750
  label: control.label,
2525
2751
  value,
2526
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v),
2752
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v),
2527
2753
  placeholder: control.placeholder
2528
2754
  },
2529
2755
  control.path
2530
2756
  );
2531
2757
  case "select":
2532
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2758
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2533
2759
  SelectControl,
2534
2760
  {
2535
2761
  label: control.label,
2536
2762
  value,
2537
2763
  options: control.options ?? [],
2538
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
2764
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v)
2539
2765
  },
2540
2766
  control.path
2541
2767
  );
2542
2768
  case "color":
2543
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2769
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2544
2770
  ColorControl,
2545
2771
  {
2546
2772
  label: control.label,
2547
2773
  value,
2548
- onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
2774
+ onChange: (v) => DialStore.updateValue(panelId, control.path, v)
2549
2775
  },
2550
2776
  control.path
2551
2777
  );
2552
2778
  case "action":
2553
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2779
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2554
2780
  "button",
2555
2781
  {
2556
2782
  className: "dialkit-button",
2557
- onClick: () => DialStore.triggerAction(panel.id, control.path),
2783
+ onClick: () => DialStore.triggerAction(panelId, control.path),
2558
2784
  children: control.label
2559
2785
  },
2560
2786
  control.path
@@ -2563,23 +2789,181 @@ Apply these values as the new defaults in the useDialKit call.`;
2563
2789
  return null;
2564
2790
  }
2565
2791
  };
2566
- const renderControls = () => {
2567
- return panel.controls.map(renderControl);
2792
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_jsx_runtime13.Fragment, { children: controls.map(renderControl) });
2793
+ }
2794
+
2795
+ // src/components/PresetManager.tsx
2796
+ var import_react15 = require("react");
2797
+ var import_react_dom2 = require("react-dom");
2798
+ var import_react16 = require("motion/react");
2799
+ var import_jsx_runtime14 = require("react/jsx-runtime");
2800
+ function PresetManager({ panelId, presets, activePresetId, onAdd }) {
2801
+ const [isOpen, setIsOpen] = (0, import_react15.useState)(false);
2802
+ const triggerRef = (0, import_react15.useRef)(null);
2803
+ const dropdownRef = (0, import_react15.useRef)(null);
2804
+ const [pos, setPos] = (0, import_react15.useState)({ top: 0, left: 0, width: 0 });
2805
+ const hasPresets = presets.length > 0;
2806
+ const activePreset = presets.find((p) => p.id === activePresetId);
2807
+ const open = (0, import_react15.useCallback)(() => {
2808
+ if (!hasPresets) return;
2809
+ const rect = triggerRef.current?.getBoundingClientRect();
2810
+ if (rect) {
2811
+ setPos({ top: rect.bottom + 4, left: rect.left, width: rect.width });
2812
+ }
2813
+ setIsOpen(true);
2814
+ }, [hasPresets]);
2815
+ const close = (0, import_react15.useCallback)(() => setIsOpen(false), []);
2816
+ const toggle = (0, import_react15.useCallback)(() => {
2817
+ if (isOpen) close();
2818
+ else open();
2819
+ }, [isOpen, open, close]);
2820
+ (0, import_react15.useEffect)(() => {
2821
+ if (!isOpen) return;
2822
+ const handler = (e) => {
2823
+ const target = e.target;
2824
+ if (triggerRef.current?.contains(target) || dropdownRef.current?.contains(target)) return;
2825
+ close();
2826
+ };
2827
+ document.addEventListener("mousedown", handler);
2828
+ return () => document.removeEventListener("mousedown", handler);
2829
+ }, [isOpen, close]);
2830
+ const handleSelect = (presetId) => {
2831
+ if (presetId) {
2832
+ DialStore.loadPreset(panelId, presetId);
2833
+ } else {
2834
+ DialStore.clearActivePreset(panelId);
2835
+ }
2836
+ close();
2837
+ };
2838
+ const handleDelete = (e, presetId) => {
2839
+ e.stopPropagation();
2840
+ DialStore.deletePreset(panelId, presetId);
2841
+ };
2842
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "dialkit-preset-manager", children: [
2843
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
2844
+ "button",
2845
+ {
2846
+ ref: triggerRef,
2847
+ className: "dialkit-preset-trigger",
2848
+ onClick: toggle,
2849
+ "data-open": String(isOpen),
2850
+ "data-has-preset": String(!!activePreset),
2851
+ "data-disabled": String(!hasPresets),
2852
+ children: [
2853
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "dialkit-preset-label", children: activePreset ? activePreset.name : "Version 1" }),
2854
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2855
+ import_react16.motion.svg,
2856
+ {
2857
+ className: "dialkit-select-chevron",
2858
+ viewBox: "0 0 24 24",
2859
+ fill: "none",
2860
+ stroke: "currentColor",
2861
+ strokeWidth: "2.5",
2862
+ strokeLinecap: "round",
2863
+ strokeLinejoin: "round",
2864
+ animate: { rotate: isOpen ? 180 : 0, opacity: hasPresets ? 0.6 : 0.25 },
2865
+ transition: { type: "spring", visualDuration: 0.2, bounce: 0.15 },
2866
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("path", { d: ICON_CHEVRON })
2867
+ }
2868
+ )
2869
+ ]
2870
+ }
2871
+ ),
2872
+ (0, import_react_dom2.createPortal)(
2873
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react16.AnimatePresence, { children: isOpen && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
2874
+ import_react16.motion.div,
2875
+ {
2876
+ ref: dropdownRef,
2877
+ className: "dialkit-root dialkit-preset-dropdown",
2878
+ style: { position: "fixed", top: pos.top, left: pos.left, minWidth: pos.width },
2879
+ initial: { opacity: 0, y: 4, scale: 0.97 },
2880
+ animate: { opacity: 1, y: 0, scale: 1 },
2881
+ exit: { opacity: 0, y: 4, scale: 0.97, pointerEvents: "none" },
2882
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
2883
+ children: [
2884
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2885
+ "div",
2886
+ {
2887
+ className: "dialkit-preset-item",
2888
+ "data-active": String(!activePresetId),
2889
+ onClick: () => handleSelect(null),
2890
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "dialkit-preset-name", children: "Version 1" })
2891
+ }
2892
+ ),
2893
+ presets.map((preset) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
2894
+ "div",
2895
+ {
2896
+ className: "dialkit-preset-item",
2897
+ "data-active": String(preset.id === activePresetId),
2898
+ onClick: () => handleSelect(preset.id),
2899
+ children: [
2900
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "dialkit-preset-name", children: preset.name }),
2901
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2902
+ "button",
2903
+ {
2904
+ className: "dialkit-preset-delete",
2905
+ onClick: (e) => handleDelete(e, preset.id),
2906
+ title: "Delete preset",
2907
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: ICON_TRASH.map((d, i) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("path", { d }, i)) })
2908
+ }
2909
+ )
2910
+ ]
2911
+ },
2912
+ preset.id
2913
+ ))
2914
+ ]
2915
+ }
2916
+ ) }),
2917
+ document.body
2918
+ )
2919
+ ] });
2920
+ }
2921
+
2922
+ // src/components/Panel.tsx
2923
+ var import_jsx_runtime15 = require("react/jsx-runtime");
2924
+ function Panel({ panel, defaultOpen = true, inline = false, onOpenChange, variant = "root", toolbarExtra }) {
2925
+ const [copied, setCopied] = (0, import_react17.useState)(false);
2926
+ const [isPanelOpen, setIsPanelOpen] = (0, import_react17.useState)(defaultOpen);
2927
+ const hasShortcuts = Object.keys(panel.shortcuts).length > 0;
2928
+ const subscribe = (0, import_react17.useCallback)(
2929
+ (callback) => DialStore.subscribe(panel.id, callback),
2930
+ [panel.id]
2931
+ );
2932
+ const getSnapshot = (0, import_react17.useCallback)(
2933
+ () => DialStore.getValues(panel.id),
2934
+ [panel.id]
2935
+ );
2936
+ const values = (0, import_react17.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
2937
+ const presets = DialStore.getPresets(panel.id);
2938
+ const activePresetId = DialStore.getActivePresetId(panel.id);
2939
+ const handleAddPreset = () => {
2940
+ const nextNum = presets.length + 2;
2941
+ DialStore.savePreset(panel.id, `Version ${nextNum}`);
2942
+ };
2943
+ const handleCopy = () => {
2944
+ navigator.clipboard.writeText(buildCopyInstruction("useDialKit", panel.name, values));
2945
+ setCopied(true);
2946
+ setTimeout(() => setCopied(false), 1500);
2568
2947
  };
2948
+ const handleOpenChange = (0, import_react17.useCallback)((open) => {
2949
+ setIsPanelOpen(open);
2950
+ onOpenChange?.(open);
2951
+ }, [onOpenChange]);
2952
+ const renderControls = () => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ControlRenderer, { panelId: panel.id, controls: panel.controls, values });
2569
2953
  const iconTransition = { type: "spring", visualDuration: 0.4, bounce: 0.1 };
2570
- const toolbar = /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
2571
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2572
- import_react16.motion.button,
2954
+ const toolbar = /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
2955
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2956
+ import_react18.motion.button,
2573
2957
  {
2574
2958
  className: "dialkit-toolbar-add",
2575
2959
  onClick: handleAddPreset,
2576
2960
  title: "Add preset",
2577
2961
  whileTap: { scale: 0.9 },
2578
2962
  transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
2579
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("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__ */ (0, import_jsx_runtime14.jsx)("path", { d }, i)) })
2963
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("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__ */ (0, import_jsx_runtime15.jsx)("path", { d }, i)) })
2580
2964
  }
2581
2965
  ),
2582
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2966
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2583
2967
  PresetManager,
2584
2968
  {
2585
2969
  panelId: panel.id,
@@ -2588,16 +2972,16 @@ Apply these values as the new defaults in the useDialKit call.`;
2588
2972
  onAdd: handleAddPreset
2589
2973
  }
2590
2974
  ),
2591
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2592
- import_react16.motion.button,
2975
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2976
+ import_react18.motion.button,
2593
2977
  {
2594
2978
  className: "dialkit-toolbar-add",
2595
2979
  onClick: handleCopy,
2596
2980
  title: "Copy parameters",
2597
2981
  whileTap: { scale: 0.9 },
2598
2982
  transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
2599
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { style: { position: "relative", width: 16, height: 16 }, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react16.AnimatePresence, { initial: false, mode: "wait", children: copied ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2600
- import_react16.motion.svg,
2983
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: { position: "relative", width: 16, height: 16 }, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react18.AnimatePresence, { initial: false, mode: "wait", children: copied ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2984
+ import_react18.motion.svg,
2601
2985
  {
2602
2986
  viewBox: "0 0 24 24",
2603
2987
  fill: "none",
@@ -2610,11 +2994,11 @@ Apply these values as the new defaults in the useDialKit call.`;
2610
2994
  animate: { scale: 1, opacity: 1 },
2611
2995
  exit: { scale: 0.8, opacity: 0 },
2612
2996
  transition: { duration: 0.08 },
2613
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("path", { d: ICON_CHECK })
2997
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("path", { d: ICON_CHECK })
2614
2998
  },
2615
2999
  "check"
2616
- ) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
2617
- import_react16.motion.svg,
3000
+ ) : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3001
+ import_react18.motion.svg,
2618
3002
  {
2619
3003
  viewBox: "0 0 24 24",
2620
3004
  fill: "none",
@@ -2624,23 +3008,116 @@ Apply these values as the new defaults in the useDialKit call.`;
2624
3008
  exit: { scale: 0.8, opacity: 0 },
2625
3009
  transition: { duration: 0.08 },
2626
3010
  children: [
2627
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("path", { d: ICON_CLIPBOARD.board, stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round" }),
2628
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("path", { d: ICON_CLIPBOARD.sparkle, fill: "currentColor" }),
2629
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("path", { d: ICON_CLIPBOARD.body, stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })
3011
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("path", { d: ICON_CLIPBOARD.board, stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round" }),
3012
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("path", { d: ICON_CLIPBOARD.sparkle, fill: "currentColor" }),
3013
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("path", { d: ICON_CLIPBOARD.body, stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })
2630
3014
  ]
2631
3015
  },
2632
3016
  "clipboard"
2633
3017
  ) }) })
2634
3018
  }
2635
- )
3019
+ ),
3020
+ toolbarExtra
2636
3021
  ] });
2637
3022
  if (variant === "section") {
2638
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(Folder, { title: panel.name, defaultOpen, onOpenChange: handleOpenChange, children: [
2639
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "dialkit-panel-section-toolbar", onClick: (e) => e.stopPropagation(), children: toolbar }),
3023
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(Folder, { title: panel.name, defaultOpen, onOpenChange: handleOpenChange, children: [
3024
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "dialkit-panel-section-toolbar", onClick: (e) => e.stopPropagation(), children: toolbar }),
2640
3025
  renderControls()
2641
3026
  ] });
2642
3027
  }
2643
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Folder, { title: panel.name, defaultOpen, isRoot: true, inline, onOpenChange: handleOpenChange, toolbar, children: renderControls() }) });
3028
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Folder, { title: panel.name, defaultOpen, isRoot: true, inline, onOpenChange: handleOpenChange, toolbar, children: renderControls() }) });
3029
+ }
3030
+
3031
+ // src/components/Timeline/TimelineToggleButton.tsx
3032
+ var import_react19 = require("react");
3033
+ var import_react20 = require("motion/react");
3034
+
3035
+ // src/store/TimelineUiStore.ts
3036
+ var TimelineUiStoreClass = class {
3037
+ constructor() {
3038
+ this.visible = true;
3039
+ this.initialized = false;
3040
+ this.controllers = /* @__PURE__ */ new Map();
3041
+ this.listeners = /* @__PURE__ */ new Set();
3042
+ }
3043
+ getVisible() {
3044
+ for (const controller of this.controllers.values()) {
3045
+ if (controller.visible !== void 0) return controller.visible;
3046
+ }
3047
+ return this.visible;
3048
+ }
3049
+ registerController(id, controller) {
3050
+ const previous = this.getVisible();
3051
+ if (!this.initialized) {
3052
+ this.visible = controller.defaultVisible;
3053
+ this.initialized = true;
3054
+ }
3055
+ this.controllers.set(id, controller);
3056
+ if (previous !== this.getVisible()) this.notify();
3057
+ return () => {
3058
+ const before = this.getVisible();
3059
+ this.controllers.delete(id);
3060
+ if (this.controllers.size === 0) this.initialized = false;
3061
+ if (before !== this.getVisible()) this.notify();
3062
+ };
3063
+ }
3064
+ updateController(id, controller) {
3065
+ if (!this.controllers.has(id)) return;
3066
+ const previous = this.getVisible();
3067
+ this.controllers.set(id, controller);
3068
+ if (previous !== this.getVisible()) this.notify();
3069
+ }
3070
+ requestVisible(visible) {
3071
+ const current = this.getVisible();
3072
+ if (current === visible) return;
3073
+ const controlled = Array.from(this.controllers.values()).filter(
3074
+ (controller) => controller.visible !== void 0
3075
+ );
3076
+ if (controlled.length > 0) {
3077
+ controlled.forEach((controller) => controller.onVisibilityChange?.(visible));
3078
+ return;
3079
+ }
3080
+ this.visible = visible;
3081
+ this.controllers.forEach((controller) => controller.onVisibilityChange?.(visible));
3082
+ this.notify();
3083
+ }
3084
+ toggle() {
3085
+ this.requestVisible(!this.getVisible());
3086
+ }
3087
+ subscribe(listener) {
3088
+ this.listeners.add(listener);
3089
+ return () => this.listeners.delete(listener);
3090
+ }
3091
+ notify() {
3092
+ this.listeners.forEach((listener) => listener());
3093
+ }
3094
+ };
3095
+ var TimelineUiStore = /* @__PURE__ */ new TimelineUiStoreClass();
3096
+
3097
+ // src/components/Timeline/TimelineToggleButton.tsx
3098
+ var import_jsx_runtime16 = require("react/jsx-runtime");
3099
+ function TimelineToggleButton() {
3100
+ const subscribe = (0, import_react19.useCallback)(
3101
+ (listener) => TimelineUiStore.subscribe(listener),
3102
+ []
3103
+ );
3104
+ const getVisible = (0, import_react19.useCallback)(() => TimelineUiStore.getVisible(), []);
3105
+ const visible = (0, import_react19.useSyncExternalStore)(subscribe, getVisible, getVisible);
3106
+ const label = visible ? "Hide timeline" : "Show timeline";
3107
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3108
+ import_react20.motion.button,
3109
+ {
3110
+ className: "dialkit-toolbar-add dialkit-timeline-toolbar-toggle",
3111
+ "data-active": visible || void 0,
3112
+ "aria-pressed": visible,
3113
+ "aria-label": label,
3114
+ title: label,
3115
+ onClick: () => TimelineUiStore.toggle(),
3116
+ whileTap: { scale: 0.9 },
3117
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
3118
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: ICON_TIMELINE.map((d, i) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("path", { d, fill: "currentColor" }, i)) })
3119
+ }
3120
+ );
2644
3121
  }
2645
3122
 
2646
3123
  // src/panel-drag.ts
@@ -2708,33 +3185,39 @@ function blockPanelDragClick(handle) {
2708
3185
  }
2709
3186
 
2710
3187
  // src/components/DialRoot.tsx
2711
- var import_jsx_runtime15 = require("react/jsx-runtime");
2712
- var import_meta = {};
2713
- 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;
3188
+ var import_jsx_runtime17 = require("react/jsx-runtime");
2714
3189
  function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover", theme = "system", productionEnabled = isDevDefault, onOpenChange }) {
2715
3190
  if (!productionEnabled) return null;
2716
- const [panels, setPanels] = (0, import_react17.useState)([]);
2717
- const [mounted, setMounted] = (0, import_react17.useState)(false);
3191
+ const [panels, setPanels] = (0, import_react21.useState)([]);
3192
+ const [timelineCount, setTimelineCount] = (0, import_react21.useState)(0);
3193
+ const [mounted, setMounted] = (0, import_react21.useState)(false);
2718
3194
  const inline = mode === "inline";
2719
- const panelRef = (0, import_react17.useRef)(null);
2720
- const [dragOffset, setDragOffset] = (0, import_react17.useState)(null);
2721
- const [activePosition, setActivePosition] = (0, import_react17.useState)(position);
2722
- const lastDragOffset = (0, import_react17.useRef)(null);
2723
- const draggingRef = (0, import_react17.useRef)(false);
2724
- const dragStartRef = (0, import_react17.useRef)(null);
2725
- const didDragRef = (0, import_react17.useRef)(false);
2726
- const dragTargetRef = (0, import_react17.useRef)(null);
2727
- const panelOpenStatesRef = (0, import_react17.useRef)(/* @__PURE__ */ new Map());
2728
- const rootOpenRef = (0, import_react17.useRef)(null);
2729
- (0, import_react17.useEffect)(() => {
3195
+ const panelRef = (0, import_react21.useRef)(null);
3196
+ const [dragOffset, setDragOffset] = (0, import_react21.useState)(null);
3197
+ const [activePosition, setActivePosition] = (0, import_react21.useState)(position);
3198
+ const lastDragOffset = (0, import_react21.useRef)(null);
3199
+ const draggingRef = (0, import_react21.useRef)(false);
3200
+ const dragStartRef = (0, import_react21.useRef)(null);
3201
+ const didDragRef = (0, import_react21.useRef)(false);
3202
+ const dragTargetRef = (0, import_react21.useRef)(null);
3203
+ const panelOpenStatesRef = (0, import_react21.useRef)(/* @__PURE__ */ new Map());
3204
+ const rootOpenRef = (0, import_react21.useRef)(null);
3205
+ (0, import_react21.useEffect)(() => {
2730
3206
  setMounted(true);
2731
- setPanels(DialStore.getPanels());
2732
- const unsubscribe = DialStore.subscribeGlobal(() => {
2733
- setPanels(DialStore.getPanels());
3207
+ setPanels(DialStore.getPanels("panel"));
3208
+ setTimelineCount(TimelineStore.getTimelines().length);
3209
+ const unsubscribePanels = DialStore.subscribeGlobal(() => {
3210
+ setPanels(DialStore.getPanels("panel"));
3211
+ });
3212
+ const unsubscribeTimelines = TimelineStore.subscribeGlobal(() => {
3213
+ setTimelineCount(TimelineStore.getTimelines().length);
2734
3214
  });
2735
- return unsubscribe;
3215
+ return () => {
3216
+ unsubscribePanels();
3217
+ unsubscribeTimelines();
3218
+ };
2736
3219
  }, []);
2737
- (0, import_react17.useEffect)(() => {
3220
+ (0, import_react21.useEffect)(() => {
2738
3221
  const fallbackOpen = inline || defaultOpen;
2739
3222
  const nextStates = /* @__PURE__ */ new Map();
2740
3223
  for (const panel of panels) {
@@ -2743,7 +3226,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2743
3226
  panelOpenStatesRef.current = nextStates;
2744
3227
  rootOpenRef.current = Array.from(nextStates.values()).some(Boolean);
2745
3228
  }, [defaultOpen, inline, panels]);
2746
- (0, import_react17.useEffect)(() => {
3229
+ (0, import_react21.useEffect)(() => {
2747
3230
  if (!panelRef.current || inline) return;
2748
3231
  const observer = new MutationObserver(() => {
2749
3232
  const inners = panelRef.current?.querySelectorAll(".dialkit-panel-inner");
@@ -2771,7 +3254,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2771
3254
  observer.observe(panelRef.current, { subtree: true, attributes: true, attributeFilter: ["data-collapsed"] });
2772
3255
  return () => observer.disconnect();
2773
3256
  }, [inline, dragOffset, position]);
2774
- const handlePointerDown = (0, import_react17.useCallback)((e) => {
3257
+ const handlePointerDown = (0, import_react21.useCallback)((e) => {
2775
3258
  const panel = panelRef.current;
2776
3259
  const handle = getPanelDragHandle(e.target, panel);
2777
3260
  if (!panel || !handle) return;
@@ -2781,13 +3264,13 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2781
3264
  draggingRef.current = true;
2782
3265
  handle.setPointerCapture(e.pointerId);
2783
3266
  }, []);
2784
- const handlePointerMove = (0, import_react17.useCallback)((e) => {
3267
+ const handlePointerMove = (0, import_react21.useCallback)((e) => {
2785
3268
  if (!draggingRef.current || !dragStartRef.current) return;
2786
3269
  if (!didDragRef.current && !hasPanelDragMoved(dragStartRef.current, e.clientX, e.clientY)) return;
2787
3270
  didDragRef.current = true;
2788
3271
  setDragOffset(getPanelDragOffset(dragStartRef.current, e.clientX, e.clientY));
2789
3272
  }, []);
2790
- const handlePointerUp = (0, import_react17.useCallback)((e) => {
3273
+ const handlePointerUp = (0, import_react21.useCallback)((e) => {
2791
3274
  if (!draggingRef.current) return;
2792
3275
  draggingRef.current = false;
2793
3276
  dragStartRef.current = null;
@@ -2803,7 +3286,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2803
3286
  }
2804
3287
  dragTargetRef.current = null;
2805
3288
  }, []);
2806
- const handlePanelOpenChange = (0, import_react17.useCallback)((panelId, open) => {
3289
+ const handlePanelOpenChange = (0, import_react21.useCallback)((panelId, open) => {
2807
3290
  panelOpenStatesRef.current.set(panelId, open);
2808
3291
  const fallbackOpen = inline || defaultOpen;
2809
3292
  const nextRootOpen = panels.some((panel) => panelOpenStatesRef.current.get(panel.id) ?? fallbackOpen);
@@ -2811,7 +3294,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2811
3294
  rootOpenRef.current = nextRootOpen;
2812
3295
  onOpenChange?.(nextRootOpen);
2813
3296
  }, [defaultOpen, inline, onOpenChange, panels]);
2814
- const handleRootOpenChange = (0, import_react17.useCallback)((open) => {
3297
+ const handleRootOpenChange = (0, import_react21.useCallback)((open) => {
2815
3298
  if (rootOpenRef.current === open) return;
2816
3299
  rootOpenRef.current = open;
2817
3300
  onOpenChange?.(open);
@@ -2819,7 +3302,7 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2819
3302
  if (!mounted || typeof window === "undefined") {
2820
3303
  return null;
2821
3304
  }
2822
- if (panels.length === 0) {
3305
+ if (panels.length === 0 && timelineCount === 0) {
2823
3306
  return null;
2824
3307
  }
2825
3308
  const dragStyle = dragOffset ? {
@@ -2830,7 +3313,8 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2830
3313
  } : void 0;
2831
3314
  const originX = getPanelOriginX(activePosition, dragOffset);
2832
3315
  const hasMultiplePanels = panels.length > 1;
2833
- const content = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ShortcutListener, { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "dialkit-root", "data-mode": mode, "data-theme": theme, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3316
+ const timelineToggle = timelineCount > 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(TimelineToggleButton, {}) : null;
3317
+ const content = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ShortcutListener, { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "dialkit-root", "data-mode": mode, "data-theme": theme, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2834
3318
  "div",
2835
3319
  {
2836
3320
  ref: panelRef,
@@ -2844,7 +3328,19 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2844
3328
  onPointerMove: !inline ? handlePointerMove : void 0,
2845
3329
  onPointerUp: !inline ? handlePointerUp : void 0,
2846
3330
  onPointerCancel: !inline ? handlePointerUp : void 0,
2847
- children: hasMultiplePanels ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3331
+ children: panels.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3332
+ Folder,
3333
+ {
3334
+ title: "DialKit",
3335
+ defaultOpen: inline || defaultOpen,
3336
+ isRoot: true,
3337
+ inline,
3338
+ onOpenChange: handleRootOpenChange,
3339
+ toolbar: timelineToggle,
3340
+ panelHeightOffset: 2,
3341
+ children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "dialkit-timeline-toolkit-only", children: "Timeline" })
3342
+ }
3343
+ ) }) : hasMultiplePanels ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2848
3344
  Folder,
2849
3345
  {
2850
3346
  title: "DialKit",
@@ -2852,8 +3348,9 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2852
3348
  isRoot: true,
2853
3349
  inline,
2854
3350
  onOpenChange: handleRootOpenChange,
3351
+ toolbar: timelineToggle,
2855
3352
  panelHeightOffset: 2,
2856
- children: panels.map((panel) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3353
+ children: panels.map((panel) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2857
3354
  Panel,
2858
3355
  {
2859
3356
  panel,
@@ -2863,12 +3360,13 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2863
3360
  panel.id
2864
3361
  ))
2865
3362
  }
2866
- ) }) : panels.map((panel) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3363
+ ) }) : panels.map((panel) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2867
3364
  Panel,
2868
3365
  {
2869
3366
  panel,
2870
3367
  defaultOpen: inline || defaultOpen,
2871
3368
  inline,
3369
+ toolbarExtra: timelineToggle,
2872
3370
  onOpenChange: (open) => handlePanelOpenChange(panel.id, open)
2873
3371
  },
2874
3372
  panel.id
@@ -2881,25 +3379,2265 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2881
3379
  return (0, import_react_dom3.createPortal)(content, document.body);
2882
3380
  }
2883
3381
 
2884
- // src/components/ButtonGroup.tsx
2885
- var import_jsx_runtime16 = require("react/jsx-runtime");
2886
- function ButtonGroup({ buttons }) {
2887
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "dialkit-button-group", children: buttons.map((button, index) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2888
- "button",
2889
- {
2890
- className: "dialkit-button",
2891
- onClick: button.onClick,
2892
- children: button.label
2893
- },
2894
- index
2895
- )) });
3382
+ // src/hooks/useDialTimeline.ts
3383
+ var import_react22 = require("react");
3384
+
3385
+ // src/timeline-core.ts
3386
+ var CLIP_VALUE_STEP = 0.01;
3387
+ var TIMELINE_MIN_CLIP_DURATION = 0.05;
3388
+ var DEFAULT_STEP_DURATION = 0.3;
3389
+ var DEFAULT_CLIP_TRANSITION = { type: "spring", bounce: 0.2 };
3390
+ var RESERVED_KEYS = /* @__PURE__ */ new Set(["time", "playing", "duration", "play", "pause", "replay", "seek"]);
3391
+ function isClipConfig(value) {
3392
+ return isPlainObject(value) && Number.isFinite(value.at);
3393
+ }
3394
+ function isGroupConfig(value) {
3395
+ if (!isPlainObject(value) || "at" in value) return false;
3396
+ const entries = Object.values(value);
3397
+ return entries.length > 0 && entries.some(isClipConfig);
3398
+ }
3399
+ function isPlainObject(value) {
3400
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3401
+ }
3402
+ function nonNegativeFinite(value, fallback = 0) {
3403
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : fallback;
3404
+ }
3405
+ function animatedDuration(value, fallback = DEFAULT_STEP_DURATION) {
3406
+ return Math.max(TIMELINE_MIN_CLIP_DURATION, nonNegativeFinite(value, fallback));
3407
+ }
3408
+ function transitionDefaultDuration(transition) {
3409
+ if (transition.type === "easing") return animatedDuration(transition.duration);
3410
+ if (isPhysicsSpring(transition)) {
3411
+ return animatedDuration(springSettleDuration(springParams(transition)));
3412
+ }
3413
+ if (transition.visualDuration !== void 0) return animatedDuration(transition.visualDuration);
3414
+ return animatedDuration(springSettleDuration(springParams(transition)));
3415
+ }
3416
+ function defaultStepDuration(step, inheritedTransition) {
3417
+ const curve = step.transition ?? inheritedTransition;
3418
+ if (curve && isPhysicsSpring(curve)) return transitionDefaultDuration(curve);
3419
+ if (step.duration !== void 0) return animatedDuration(step.duration);
3420
+ if (step.transition) return transitionDefaultDuration(step.transition);
3421
+ return DEFAULT_STEP_DURATION;
3422
+ }
3423
+ function defaultTrackDuration(track, inheritedTransition) {
3424
+ const curve = track.transition ?? inheritedTransition;
3425
+ if (track.steps?.length) {
3426
+ return track.steps.reduce((sum, step) => sum + defaultStepDuration(step, curve), 0);
3427
+ }
3428
+ if (curve && isPhysicsSpring(curve)) return transitionDefaultDuration(curve);
3429
+ if (track.duration !== void 0) return animatedDuration(track.duration);
3430
+ if (track.transition) return transitionDefaultDuration(track.transition);
3431
+ return DEFAULT_STEP_DURATION;
3432
+ }
3433
+ function defaultClipDuration(clip) {
3434
+ const defaultCurve = isTransitionConfig(clip.transition) ? clip.transition : DEFAULT_CLIP_TRANSITION;
3435
+ if (clip.props) {
3436
+ return Object.values(clip.props).reduce(
3437
+ (max, track) => Math.max(
3438
+ max,
3439
+ nonNegativeFinite(track.delay) + defaultTrackDuration(track, defaultCurve)
3440
+ ),
3441
+ 0
3442
+ );
3443
+ }
3444
+ if (clip.steps?.length) {
3445
+ return clip.steps.reduce((sum, step) => sum + defaultStepDuration(step, defaultCurve), 0);
3446
+ }
3447
+ const animating = Boolean(clip.transition || clip.from || clip.to);
3448
+ if (!animating) return nonNegativeFinite(clip.duration);
3449
+ if (isPhysicsSpring(defaultCurve)) return transitionDefaultDuration(defaultCurve);
3450
+ if (clip.duration !== void 0) return animatedDuration(clip.duration);
3451
+ if (isTransitionConfig(clip.transition)) return transitionDefaultDuration(clip.transition);
3452
+ return clip.from || clip.to ? transitionDefaultDuration(DEFAULT_CLIP_TRANSITION) : 0;
3453
+ }
3454
+ function normalizeLoopMode(value) {
3455
+ if (value === true || value === "mirror" || value === "repeat") return "repeat";
3456
+ return "off";
3457
+ }
3458
+ function normalizeStoredTransition(transition, clipDuration) {
3459
+ if (transition.type === "easing") {
3460
+ return { ...transition, duration: clipDuration };
3461
+ }
3462
+ if (isPhysicsSpring(transition)) {
3463
+ return transition;
3464
+ }
3465
+ return { type: "spring", bounce: transition.bounce ?? 0.2 };
3466
+ }
3467
+ function collectClipEntries(config) {
3468
+ const entries = [];
3469
+ for (const [key, value] of Object.entries(config)) {
3470
+ if (key === "duration") continue;
3471
+ if (RESERVED_KEYS.has(key)) {
3472
+ console.warn(`[dialkit] Timeline key "${key}" collides with a reserved key and was skipped.`);
3473
+ continue;
3474
+ }
3475
+ if (isClipConfig(value)) {
3476
+ entries.push({ path: key, childKey: key, clip: value });
3477
+ } else if (isGroupConfig(value)) {
3478
+ for (const [childKey, childClip] of Object.entries(value)) {
3479
+ if (isClipConfig(childClip)) {
3480
+ entries.push({ path: `${key}.${childKey}`, childKey, group: key, clip: childClip });
3481
+ } else {
3482
+ console.warn(
3483
+ `[dialkit] Timeline clip "${key}.${childKey}" is missing a numeric "at" and was skipped.`
3484
+ );
3485
+ }
3486
+ }
3487
+ } else {
3488
+ console.warn(
3489
+ `[dialkit] Timeline entry "${key}" is neither a clip (needs a numeric "at") nor a group of clips and was skipped.`
3490
+ );
3491
+ }
3492
+ }
3493
+ return entries;
3494
+ }
3495
+ function definedValues(values) {
3496
+ if (!values) return void 0;
3497
+ const result = {};
3498
+ for (const [key, value] of Object.entries(values)) {
3499
+ if (value !== void 0) result[key] = value;
3500
+ }
3501
+ return result;
3502
+ }
3503
+ function setDialPath(dialConfig, path, value) {
3504
+ const segments = path.split(".");
3505
+ let node = dialConfig;
3506
+ for (const segment of segments.slice(0, -1)) {
3507
+ node = node[segment] ?? (node[segment] = {});
3508
+ }
3509
+ node[segments[segments.length - 1]] = value;
3510
+ }
3511
+ function parseTimelineConfig(config) {
3512
+ const entries = collectClipEntries(config);
3513
+ let maxEnd = 0;
3514
+ for (const { clip } of entries) {
3515
+ maxEnd = Math.max(maxEnd, nonNegativeFinite(clip.at) + defaultClipDuration(clip));
3516
+ }
3517
+ 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;
3518
+ const dialConfig = {};
3519
+ const clips = [];
3520
+ entries.forEach(({ path, childKey, group, clip }, index) => {
3521
+ const raw = clip;
3522
+ if (raw.props && (raw.steps?.length || raw.from || raw.to)) {
3523
+ console.warn(
3524
+ `[dialkit] Timeline clip "${path}": "props" is mutually exclusive with from/to/steps \u2014 using "props".`
3525
+ );
3526
+ } else if (raw.steps?.length && raw.to) {
3527
+ console.warn(
3528
+ `[dialkit] Timeline clip "${path}": "to" is ignored when "steps" is present \u2014 each leg's "to" defines its targets.`
3529
+ );
3530
+ }
3531
+ const hasSteps = Boolean(clip.steps?.length) && !clip.props;
3532
+ const hasProps = Boolean(clip.props);
3533
+ const single = isTransitionConfig(clip.transition) ? clip.transition : void 0;
3534
+ const total = defaultClipDuration(clip);
3535
+ const defaultCurve = single ?? DEFAULT_CLIP_TRANSITION;
3536
+ const clipAt = nonNegativeFinite(clip.at);
3537
+ const clipDial = {
3538
+ at: [clipAt, 0, duration, CLIP_VALUE_STEP]
3539
+ };
3540
+ if (!hasSteps && !hasProps) {
3541
+ clipDial.duration = [total, 0, duration, CLIP_VALUE_STEP];
3542
+ }
3543
+ if (!hasSteps && !hasProps && (clip.transition || clip.from || clip.to)) {
3544
+ clipDial.transition = normalizeStoredTransition(defaultCurve, total);
3545
+ }
3546
+ let tracks;
3547
+ if (clip.props) {
3548
+ tracks = [];
3549
+ for (const [prop, track] of Object.entries(clip.props)) {
3550
+ if (TRACK_RESERVED.has(prop) || /^step\d+$/.test(prop)) {
3551
+ console.warn(`[dialkit] Timeline property "${prop}" collides with a clip field and was skipped.`);
3552
+ continue;
3553
+ }
3554
+ const trackDuration = defaultTrackDuration(track, defaultCurve);
3555
+ const trackCurve = track.transition ?? defaultCurve;
3556
+ const hasTrackSteps = Boolean(track.steps?.length);
3557
+ const trackDial = {
3558
+ delay: [nonNegativeFinite(track.delay), 0, duration, CLIP_VALUE_STEP]
3559
+ };
3560
+ if (!hasTrackSteps) {
3561
+ trackDial.duration = [trackDuration, 0, duration, CLIP_VALUE_STEP];
3562
+ trackDial.transition = normalizeStoredTransition(trackCurve, trackDuration);
3563
+ }
3564
+ const fromValue = track.from ?? (hasTrackSteps ? void 0 : track.to);
3565
+ if (hasTrackSteps && fromValue === void 0) {
3566
+ console.warn(
3567
+ `[dialkit] Timeline clip "${path}": track "${prop}" has steps but no "from" \u2014 declare its starting value.`
3568
+ );
3569
+ }
3570
+ if (fromValue !== void 0) {
3571
+ trackDial.from = scalarDial(prop, fromValue, hasTrackSteps ? track.steps[0]?.to : track.to);
3572
+ }
3573
+ if (!hasTrackSteps && track.to !== void 0) {
3574
+ trackDial.to = scalarDial(prop, track.to, fromValue);
3575
+ }
3576
+ let trackStepKeys;
3577
+ if (hasTrackSteps) {
3578
+ trackStepKeys = [];
3579
+ let previous = fromValue;
3580
+ track.steps.forEach((step, stepIndex) => {
3581
+ const stepKey = `step${stepIndex + 1}`;
3582
+ trackStepKeys.push(stepKey);
3583
+ const stepDuration = defaultStepDuration(step, trackCurve);
3584
+ const stepDial = {
3585
+ duration: [stepDuration, 0, duration, CLIP_VALUE_STEP],
3586
+ transition: normalizeStoredTransition(step.transition ?? trackCurve, stepDuration)
3587
+ };
3588
+ if (step.to !== void 0) {
3589
+ stepDial.to = scalarDial(prop, step.to, previous);
3590
+ previous = step.to;
3591
+ }
3592
+ trackDial[stepKey] = stepDial;
3593
+ });
3594
+ }
3595
+ clipDial[prop] = trackDial;
3596
+ tracks.push({ prop, stepKeys: trackStepKeys });
3597
+ }
3598
+ }
3599
+ if (clip.from && !hasProps) {
3600
+ clipDial.from = withFromToRanges(
3601
+ clip.from,
3602
+ hasSteps ? definedValues(clip.steps[0]?.to) : clip.to
3603
+ );
3604
+ }
3605
+ if (!hasSteps && !hasProps && clip.to) {
3606
+ clipDial.to = withFromToRanges(clip.to, clip.from);
3607
+ }
3608
+ let stepKeys;
3609
+ if (hasSteps) {
3610
+ stepKeys = [];
3611
+ let running = clip.from;
3612
+ clip.steps.forEach((step, stepIndex) => {
3613
+ const stepKey = `step${stepIndex + 1}`;
3614
+ stepKeys.push(stepKey);
3615
+ const stepDuration = defaultStepDuration(step, defaultCurve);
3616
+ const stepDial = {
3617
+ duration: [stepDuration, 0, duration, CLIP_VALUE_STEP],
3618
+ transition: normalizeStoredTransition(step.transition ?? defaultCurve, stepDuration)
3619
+ };
3620
+ const stepTo = definedValues(step.to);
3621
+ if (stepTo) {
3622
+ for (const prop of Object.keys(stepTo)) {
3623
+ if (!running || !(prop in running)) {
3624
+ console.warn(
3625
+ `[dialkit] Timeline clip "${path}": property "${prop}" first animates in step ${stepIndex + 1} with no starting value \u2014 declare it in "from".`
3626
+ );
3627
+ }
3628
+ }
3629
+ stepDial.to = withFromToRanges(stepTo, running);
3630
+ }
3631
+ clipDial[stepKey] = stepDial;
3632
+ running = { ...running ?? {}, ...stepTo ?? {} };
3633
+ });
3634
+ }
3635
+ setDialPath(dialConfig, path, clipDial);
3636
+ clips.push({
3637
+ key: path,
3638
+ label: formatLabel(childKey),
3639
+ color: TIMELINE_CLIP_COLORS[index % TIMELINE_CLIP_COLORS.length],
3640
+ loop: normalizeLoopMode(clip.loop),
3641
+ group,
3642
+ stepKeys,
3643
+ tracks
3644
+ });
3645
+ });
3646
+ return { duration, dialConfig, clips };
3647
+ }
3648
+ var TRACK_RESERVED = /* @__PURE__ */ new Set(["at", "duration", "loop", "from", "to", "transition", "delay"]);
3649
+ function scalarDial(prop, value, counterpart) {
3650
+ const record = withFromToRanges(
3651
+ { [prop]: value },
3652
+ counterpart === void 0 ? void 0 : { [prop]: counterpart }
3653
+ );
3654
+ return record[prop];
3655
+ }
3656
+ var FROM_TO_RANGE_PRESETS = [
3657
+ [/^(x|y|z|tx|ty|offsetx|offsety|translatex|translatey)$/i, { min: -100, max: 100, step: 1 }],
3658
+ [/rotat|angle|skew/i, { min: -180, max: 180, step: 1 }],
3659
+ [/^scale/i, { min: 0, max: 2, step: 0.01 }],
3660
+ [/opacity|alpha/i, { min: 0, max: 1, step: 0.01 }],
3661
+ [/blur|radius|spread/i, { min: 0, max: 100, step: 1 }]
3662
+ ];
3663
+ function inferFromToRange(key, value, counterpart) {
3664
+ const lo = Math.min(value, counterpart ?? value);
3665
+ const hi = Math.max(value, counterpart ?? value);
3666
+ const preset = FROM_TO_RANGE_PRESETS.find(([pattern]) => pattern.test(key))?.[1];
3667
+ if (preset) {
3668
+ return [value, Math.min(preset.min, lo), Math.max(preset.max, hi), preset.step];
3669
+ }
3670
+ if (lo >= 0 && hi <= 1) {
3671
+ return [value, 0, 1, 0.01];
3672
+ }
3673
+ const extent = Math.max(Math.abs(lo), Math.abs(hi), 1);
3674
+ const min = lo < 0 ? -extent * 2 : 0;
3675
+ const max = Math.max(extent * 2, hi);
3676
+ return [value, min, max, inferStep(min, max)];
3677
+ }
3678
+ function withFromToRanges(config, counterpart) {
3679
+ const result = {};
3680
+ for (const [key, value] of Object.entries(config)) {
3681
+ const other = counterpart?.[key];
3682
+ if (typeof value === "number") {
3683
+ result[key] = inferFromToRange(key, value, typeof other === "number" ? other : void 0);
3684
+ } else if (isPlainObject(value) && !("type" in value)) {
3685
+ result[key] = withFromToRanges(
3686
+ value,
3687
+ isPlainObject(other) && !("type" in other) ? other : void 0
3688
+ );
3689
+ } else {
3690
+ result[key] = value;
3691
+ }
3692
+ }
3693
+ return result;
3694
+ }
3695
+ function curveStatic(transition, duration) {
3696
+ if (!transition) return { duration };
3697
+ if (transition.type === "easing") return { duration, ease: transition.ease };
3698
+ const spring = springParams(transition);
3699
+ return { duration, spring, settle: springSettleDuration(spring) };
3700
+ }
3701
+ function sampleCurve(curve, elapsed) {
3702
+ if (elapsed <= 0) return 0;
3703
+ if (curve.spring) {
3704
+ if (curve.settle !== void 0 && elapsed >= curve.settle) return 1;
3705
+ return springProgress(elapsed, curve.spring);
3706
+ }
3707
+ if (curve.ease) {
3708
+ return cubicBezierProgress(clamp(curve.duration > 0 ? elapsed / curve.duration : 1, 0, 1), curve.ease);
3709
+ }
3710
+ return curve.duration > 0 ? Math.min(1, elapsed / curve.duration) : 1;
3711
+ }
3712
+ function resolvedAtPath(resolved, path) {
3713
+ let node = resolved;
3714
+ for (const segment of path.split(".")) {
3715
+ node = isPlainObject(node) ? node[segment] : void 0;
3716
+ }
3717
+ return isPlainObject(node) ? node : {};
3718
+ }
3719
+ function computeStaticClips(parsed, flatValues) {
3720
+ const resolved = resolveDialValues(parsed.dialConfig, flatValues);
3721
+ return parsed.clips.map(
3722
+ (clip) => buildClipStatic(resolvedAtPath(resolved, clip.key), clip, parsed.duration)
3723
+ );
3724
+ }
3725
+ function computeStaticTimeline(parsed, flatValues) {
3726
+ let clips = computeStaticClips(parsed, flatValues);
3727
+ const maxEnd = clips.reduce(
3728
+ (end, clip) => Math.max(end, clip.at + clip.duration),
3729
+ parsed.duration
3730
+ );
3731
+ const duration = maxEnd > parsed.duration ? Math.ceil(maxEnd * 100 - 1e-4) / 100 : parsed.duration;
3732
+ if (duration !== parsed.duration) {
3733
+ clips = clips.map(
3734
+ (clip) => clip.loop === "repeat" ? { ...clip, end: duration } : clip
3735
+ );
3736
+ }
3737
+ return { duration, clips };
3738
+ }
3739
+ function computeClipStaticFromValues(values, clip, timelineDuration) {
3740
+ return buildClipStatic(unflattenClipValues(values, clip.key), clip, timelineDuration);
3741
+ }
3742
+ function buildClipStatic(clipResolved, clip, timelineDuration) {
3743
+ {
3744
+ const at = typeof clipResolved.at === "number" ? clipResolved.at : 0;
3745
+ const from = isPlainObject(clipResolved.from) ? clipResolved.from : void 0;
3746
+ const single = isTransitionConfig(clipResolved.transition) ? clipResolved.transition : void 0;
3747
+ const staticClip = {
3748
+ key: clip.key,
3749
+ childKey: clip.group ? clip.key.slice(clip.group.length + 1) : clip.key,
3750
+ group: clip.group,
3751
+ at,
3752
+ duration: 0,
3753
+ loop: "off",
3754
+ end: 0,
3755
+ isPhysics: false,
3756
+ from,
3757
+ tracks: [],
3758
+ explicitSteps: Boolean(clip.stepKeys?.length)
3759
+ };
3760
+ if (clip.tracks?.length) {
3761
+ const tracks = clip.tracks.map(({ prop, stepKeys }) => {
3762
+ const trackResolved = isPlainObject(clipResolved[prop]) ? clipResolved[prop] : {};
3763
+ const delay = typeof trackResolved.delay === "number" ? trackResolved.delay : 0;
3764
+ const fromValue = trackResolved.from;
3765
+ let steps;
3766
+ let trackDuration = 0;
3767
+ if (stepKeys?.length) {
3768
+ let running = fromValue;
3769
+ steps = stepKeys.map((stepKey) => {
3770
+ const stepResolved = isPlainObject(trackResolved[stepKey]) ? trackResolved[stepKey] : {};
3771
+ const storedDuration = typeof stepResolved.duration === "number" ? stepResolved.duration : 0;
3772
+ const raw = isTransitionConfig(stepResolved.transition) ? stepResolved.transition : void 0;
3773
+ const effective = raw ? resolveClipTransition(raw, storedDuration) : { transition: void 0, duration: storedDuration, isPhysics: false };
3774
+ const toValue = stepResolved.to;
3775
+ const step = {
3776
+ key: stepKey,
3777
+ offset: trackDuration,
3778
+ duration: effective.duration,
3779
+ isPhysics: effective.isPhysics,
3780
+ start: running === void 0 ? {} : { [prop]: running },
3781
+ to: toValue === void 0 ? {} : { [prop]: toValue },
3782
+ curve: curveStatic(effective.transition, effective.duration)
3783
+ };
3784
+ if (toValue !== void 0) running = toValue;
3785
+ trackDuration += effective.duration;
3786
+ return step;
3787
+ });
3788
+ } else {
3789
+ const storedDuration = typeof trackResolved.duration === "number" ? trackResolved.duration : 0;
3790
+ const raw = isTransitionConfig(trackResolved.transition) ? trackResolved.transition : void 0;
3791
+ const effective = raw ? resolveClipTransition(raw, storedDuration) : { transition: void 0, duration: storedDuration, isPhysics: false };
3792
+ const toValue = trackResolved.to;
3793
+ trackDuration = effective.duration;
3794
+ steps = [
3795
+ {
3796
+ key: null,
3797
+ offset: 0,
3798
+ duration: effective.duration,
3799
+ isPhysics: effective.isPhysics,
3800
+ start: fromValue === void 0 ? {} : { [prop]: fromValue },
3801
+ to: toValue === void 0 ? {} : { [prop]: toValue },
3802
+ curve: curveStatic(effective.transition, effective.duration)
3803
+ }
3804
+ ];
3805
+ }
3806
+ return { prop, delay, duration: trackDuration, steps };
3807
+ });
3808
+ staticClip.tracks = tracks;
3809
+ staticClip.props = tracks.map((track) => track.prop);
3810
+ staticClip.duration = tracks.reduce((max, track) => Math.max(max, track.delay + track.duration), 0);
3811
+ staticClip.from = Object.fromEntries(
3812
+ tracks.map((track) => [track.prop, track.steps[0].start[track.prop]])
3813
+ );
3814
+ staticClip.to = Object.fromEntries(
3815
+ tracks.map((track) => {
3816
+ const last = track.steps[track.steps.length - 1];
3817
+ return [track.prop, last.to[track.prop] ?? last.start[track.prop]];
3818
+ })
3819
+ );
3820
+ staticClip.loop = staticClip.duration > 0 ? clip.loop : "off";
3821
+ staticClip.end = staticClip.loop === "off" ? staticClip.at + staticClip.duration : timelineDuration;
3822
+ return staticClip;
3823
+ }
3824
+ if (clip.stepKeys?.length) {
3825
+ let running = { ...from ?? {} };
3826
+ let offset = 0;
3827
+ const steps = clip.stepKeys.map((stepKey) => {
3828
+ const stepResolved = isPlainObject(clipResolved[stepKey]) ? clipResolved[stepKey] : {};
3829
+ const storedDuration = typeof stepResolved.duration === "number" ? stepResolved.duration : 0;
3830
+ const raw = isTransitionConfig(stepResolved.transition) ? stepResolved.transition : void 0;
3831
+ const effective = raw ? resolveClipTransition(raw, storedDuration) : { transition: void 0, duration: storedDuration, isPhysics: false };
3832
+ const to = isPlainObject(stepResolved.to) ? stepResolved.to : {};
3833
+ const step = {
3834
+ key: stepKey,
3835
+ offset,
3836
+ duration: effective.duration,
3837
+ isPhysics: effective.isPhysics,
3838
+ start: running,
3839
+ to,
3840
+ curve: curveStatic(effective.transition, effective.duration)
3841
+ };
3842
+ running = { ...running, ...to };
3843
+ offset += effective.duration;
3844
+ return step;
3845
+ });
3846
+ staticClip.tracks = [{ delay: 0, duration: offset, steps }];
3847
+ staticClip.duration = offset;
3848
+ staticClip.to = running;
3849
+ } else {
3850
+ const storedDuration = typeof clipResolved.duration === "number" ? clipResolved.duration : 0;
3851
+ const to = isPlainObject(clipResolved.to) ? clipResolved.to : void 0;
3852
+ if (single) {
3853
+ const effective = resolveClipTransition(single, storedDuration);
3854
+ staticClip.duration = effective.duration;
3855
+ staticClip.isPhysics = effective.isPhysics;
3856
+ staticClip.transition = effective.transition;
3857
+ staticClip.css = transitionToCss(effective.transition);
3858
+ staticClip.to = to;
3859
+ if (from && to) {
3860
+ staticClip.tracks = [
3861
+ {
3862
+ delay: 0,
3863
+ duration: effective.duration,
3864
+ steps: [
3865
+ {
3866
+ key: null,
3867
+ offset: 0,
3868
+ duration: effective.duration,
3869
+ isPhysics: effective.isPhysics,
3870
+ start: from,
3871
+ to,
3872
+ curve: curveStatic(effective.transition, effective.duration)
3873
+ }
3874
+ ]
3875
+ }
3876
+ ];
3877
+ }
3878
+ } else {
3879
+ staticClip.duration = storedDuration;
3880
+ staticClip.to = to;
3881
+ if (from && to) {
3882
+ const base = resolveClipTransition(DEFAULT_CLIP_TRANSITION, storedDuration);
3883
+ staticClip.duration = base.duration;
3884
+ staticClip.tracks = [
3885
+ {
3886
+ delay: 0,
3887
+ duration: base.duration,
3888
+ steps: [
3889
+ {
3890
+ key: null,
3891
+ offset: 0,
3892
+ duration: base.duration,
3893
+ isPhysics: false,
3894
+ start: from,
3895
+ to,
3896
+ curve: curveStatic(base.transition, base.duration)
3897
+ }
3898
+ ]
3899
+ }
3900
+ ];
3901
+ }
3902
+ }
3903
+ }
3904
+ if (staticClip.tracks.length) {
3905
+ const props = new Set(Object.keys(from ?? {}));
3906
+ for (const track of staticClip.tracks) {
3907
+ for (const step of track.steps) {
3908
+ for (const prop of Object.keys(step.to)) props.add(prop);
3909
+ }
3910
+ }
3911
+ staticClip.props = Array.from(props);
3912
+ }
3913
+ staticClip.loop = staticClip.duration > 0 ? clip.loop : "off";
3914
+ staticClip.end = staticClip.loop === "off" ? staticClip.at + staticClip.duration : timelineDuration;
3915
+ return staticClip;
3916
+ }
3917
+ }
3918
+ function stepAtPosition(steps, pos) {
3919
+ for (const step of steps) {
3920
+ if (pos < step.offset + step.duration) return step;
3921
+ }
3922
+ return steps[steps.length - 1];
3923
+ }
3924
+ function evalPropAtPos(steps, prop, pos) {
3925
+ const step = stepAtPosition(steps, pos);
3926
+ const within = Math.max(0, pos - step.offset);
3927
+ if (prop in step.to) {
3928
+ const eased = sampleCurve(step.curve, within);
3929
+ return interpolateResolved(step.start[prop], step.to[prop], eased);
3930
+ }
3931
+ return step.start[prop];
3932
+ }
3933
+ function computeClipState(clip, time, cycleTime = time) {
3934
+ const total = clip.duration;
3935
+ const looping = clip.loop === "repeat" && total > 0;
3936
+ const started = time >= clip.at || looping && cycleTime > time;
3937
+ const done = time >= clip.end;
3938
+ const elapsed = time - clip.at;
3939
+ const phaseElapsed = looping ? cycleTime - clip.at : elapsed;
3940
+ const fold = (e) => looping ? e % total : e;
3941
+ const basePos = started ? fold(Math.max(0, phaseElapsed)) : 0;
3942
+ const progress = total > 0 ? clamp(basePos / total, 0, 1) : started ? 1 : 0;
3943
+ let current;
3944
+ let stepIndex = 0;
3945
+ if (clip.tracks.length && clip.props?.length) {
3946
+ current = {};
3947
+ for (const track of clip.tracks) {
3948
+ const props = track.prop !== void 0 ? [track.prop] : clip.props;
3949
+ for (const prop of props) {
3950
+ const startValue = track.steps[0]?.start[prop];
3951
+ if (!started) {
3952
+ if (startValue !== void 0) current[prop] = startValue;
3953
+ continue;
3954
+ }
3955
+ const phase = phaseElapsed - track.delay;
3956
+ if (phase <= 0) {
3957
+ if (startValue !== void 0) current[prop] = startValue;
3958
+ continue;
3959
+ }
3960
+ const pos = looping && track.duration > 0 ? phase % track.duration : phase;
3961
+ const value = evalPropAtPos(track.steps, prop, pos);
3962
+ if (value !== void 0) current[prop] = value;
3963
+ }
3964
+ }
3965
+ const shared = clip.tracks[0];
3966
+ if (started && clip.explicitSteps && shared.prop === void 0) {
3967
+ stepIndex = shared.steps.indexOf(stepAtPosition(shared.steps, basePos));
3968
+ }
3969
+ }
3970
+ return {
3971
+ at: clip.at,
3972
+ duration: clip.duration,
3973
+ loop: clip.loop,
3974
+ started,
3975
+ active: started && !done,
3976
+ done,
3977
+ progress,
3978
+ step: clip.explicitSteps ? stepIndex : void 0,
3979
+ from: clip.from,
3980
+ to: clip.to,
3981
+ animate: started ? clip.to : clip.from,
3982
+ transition: clip.transition,
3983
+ css: clip.css,
3984
+ current
3985
+ };
3986
+ }
3987
+ function interpolateResolved(from, to, p) {
3988
+ if (typeof from === "number" && typeof to === "number") {
3989
+ return from + (to - from) * p;
3990
+ }
3991
+ if (typeof from === "string" && typeof to === "string") {
3992
+ const mixed = mixHexColors(from, to, p);
3993
+ if (mixed) return mixed;
3994
+ }
3995
+ if (isPlainObject(from) && isPlainObject(to)) {
3996
+ const result = {};
3997
+ for (const key of Object.keys(from)) {
3998
+ result[key] = key in to ? interpolateResolved(from[key], to[key], p) : from[key];
3999
+ }
4000
+ for (const key of Object.keys(to)) {
4001
+ if (!(key in from)) result[key] = to[key];
4002
+ }
4003
+ return result;
4004
+ }
4005
+ return p < 0.5 ? from : to;
4006
+ }
4007
+ function parseHex(hex) {
4008
+ if (!isHexColor(hex)) return null;
4009
+ let h = hex.slice(1);
4010
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
4011
+ return [
4012
+ parseInt(h.slice(0, 2), 16),
4013
+ parseInt(h.slice(2, 4), 16),
4014
+ parseInt(h.slice(4, 6), 16),
4015
+ h.length === 8 ? parseInt(h.slice(6, 8), 16) : 255
4016
+ ];
4017
+ }
4018
+ function mixHexColors(a, b, p) {
4019
+ const ca = parseHex(a);
4020
+ const cb = parseHex(b);
4021
+ if (!ca || !cb) return null;
4022
+ const t = clamp(p, 0, 1);
4023
+ const mixed = ca.map((v, i) => Math.round(v + (cb[i] - v) * t));
4024
+ const hex = (n) => n.toString(16).padStart(2, "0");
4025
+ const rgb = `#${hex(mixed[0])}${hex(mixed[1])}${hex(mixed[2])}`;
4026
+ return mixed[3] === 255 ? rgb : `${rgb}${hex(mixed[3])}`;
4027
+ }
4028
+ function transitionToCss(transition) {
4029
+ if (!transition) return void 0;
4030
+ if (transition.type === "easing") {
4031
+ return {
4032
+ transitionDuration: `${round2(transition.duration)}s`,
4033
+ transitionTimingFunction: `cubic-bezier(${transition.ease.map((v) => round2(v)).join(", ")})`
4034
+ };
4035
+ }
4036
+ const params = springParams(transition);
4037
+ const dampingRatio = params.damping / (2 * Math.sqrt(params.stiffness * params.mass));
4038
+ const duration = transition.visualDuration ?? springSettleDuration(params);
4039
+ const bounce = transition.bounce ?? Math.max(0, round2(1 - dampingRatio));
4040
+ return {
4041
+ transitionDuration: `${round2(duration)}s`,
4042
+ transitionTimingFunction: bounce > 0.05 ? `cubic-bezier(0.34, ${round2(1.2 + bounce)}, 0.64, 1)` : "cubic-bezier(0.25, 0.6, 0.35, 1)"
4043
+ };
4044
+ }
4045
+ function timelinePopoverDisplayValues(values, clipKey, stepKeys, stepKey) {
4046
+ const display = { ...values };
4047
+ const swap = (path, duration) => {
4048
+ const raw = display[path];
4049
+ if (isTransitionConfig(raw)) display[path] = resolveClipTransition(raw, duration).transition;
4050
+ };
4051
+ if (stepKey) {
4052
+ swap(`${clipKey}.${stepKey}.transition`, numberValue(values[`${clipKey}.${stepKey}.duration`]));
4053
+ return display;
4054
+ }
4055
+ const cycle = stepKeys?.length ? stepKeys.reduce((sum, sk) => sum + numberValue(values[`${clipKey}.${sk}.duration`]), 0) : numberValue(values[`${clipKey}.duration`]);
4056
+ swap(`${clipKey}.transition`, cycle);
4057
+ return display;
4058
+ }
4059
+ function numberValue(value) {
4060
+ return typeof value === "number" ? value : 0;
4061
+ }
4062
+ function unflattenClipValues(values, clipKey) {
4063
+ const prefix = `${clipKey}.`;
4064
+ const result = {};
4065
+ 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);
4066
+ for (const { segments, value } of entries) {
4067
+ let node = result;
4068
+ for (let i = 0; i < segments.length - 1; i++) {
4069
+ const existing = node[segments[i]];
4070
+ node = isPlainObject(existing) ? existing : node[segments[i]] = {};
4071
+ }
4072
+ node[segments[segments.length - 1]] = cloneTimelineValue(value);
4073
+ }
4074
+ return result;
4075
+ }
4076
+ function cloneTimelineValue(value) {
4077
+ if (Array.isArray(value)) return value.map(cloneTimelineValue);
4078
+ if (!isPlainObject(value)) return value;
4079
+ return Object.fromEntries(
4080
+ Object.entries(value).map(([key, nested]) => [key, cloneTimelineValue(nested)])
4081
+ );
4082
+ }
4083
+ function clampTrackDelay(delay, at, trackDuration, timelineDuration) {
4084
+ return clamp(round2(delay), 0, Math.max(0, round2(timelineDuration - at - trackDuration)));
4085
+ }
4086
+ function clampClipMove(at, duration, timelineDuration) {
4087
+ return clamp(round2(at), 0, Math.max(0, timelineDuration - duration));
4088
+ }
4089
+ function clampClipResizeEnd(duration, at, timelineDuration) {
4090
+ return clamp(round2(duration), TIMELINE_MIN_CLIP_DURATION, timelineDuration - at);
4091
+ }
4092
+ function clampClipResizeStart(newAt, at, duration) {
4093
+ const clampedAt = clamp(round2(newAt), 0, at + duration - TIMELINE_MIN_CLIP_DURATION);
4094
+ return { at: clampedAt, duration: round2(at + duration - clampedAt) };
4095
+ }
4096
+ function clampStepResize(duration, at, otherStepsTotal, timelineDuration) {
4097
+ const max = Math.max(TIMELINE_MIN_CLIP_DURATION, timelineDuration - at - otherStepsTotal);
4098
+ return clamp(round2(duration), TIMELINE_MIN_CLIP_DURATION, max);
4099
+ }
4100
+ function normalizeTimelineValuesForCopy(values, clips) {
4101
+ const normalized = { ...values };
4102
+ for (const path of Object.keys(normalized)) {
4103
+ if (path.endsWith(".__mode")) delete normalized[path];
4104
+ }
4105
+ const normalizeTransitionAt = (transitionPath, durationPath) => {
4106
+ const raw = normalized[transitionPath];
4107
+ if (!isTransitionConfig(raw)) return;
4108
+ if (isPhysicsSpring(raw)) {
4109
+ normalized[durationPath] = transitionDefaultDuration(raw);
4110
+ }
4111
+ normalized[transitionPath] = normalizeStoredTransition(
4112
+ raw,
4113
+ numberValue(normalized[durationPath])
4114
+ );
4115
+ };
4116
+ for (const clip of clips) {
4117
+ for (const stepKey of clip.stepKeys ?? []) {
4118
+ normalizeTransitionAt(
4119
+ `${clip.key}.${stepKey}.transition`,
4120
+ `${clip.key}.${stepKey}.duration`
4121
+ );
4122
+ }
4123
+ normalizeTransitionAt(`${clip.key}.transition`, `${clip.key}.duration`);
4124
+ for (const track of clip.tracks ?? []) {
4125
+ const trackKey = `${clip.key}.${track.prop}`;
4126
+ for (const stepKey of track.stepKeys ?? []) {
4127
+ normalizeTransitionAt(
4128
+ `${trackKey}.${stepKey}.transition`,
4129
+ `${trackKey}.${stepKey}.duration`
4130
+ );
4131
+ }
4132
+ normalizeTransitionAt(`${trackKey}.transition`, `${trackKey}.duration`);
4133
+ if (normalized[`${trackKey}.delay`] === 0) {
4134
+ delete normalized[`${trackKey}.delay`];
4135
+ }
4136
+ }
4137
+ delete normalized[`${clip.key}.loop`];
4138
+ }
4139
+ return normalized;
4140
+ }
4141
+ function formatClock(time, tenths = false) {
4142
+ const safe = Math.max(0, time);
4143
+ const minutes = Math.floor(safe / 60);
4144
+ const seconds = safe - minutes * 60;
4145
+ const secondsText = tenths ? seconds.toFixed(1).padStart(4, "0") : String(Math.floor(seconds)).padStart(2, "0");
4146
+ return `${String(minutes).padStart(2, "0")}:${secondsText}`;
4147
+ }
4148
+ function formatSeconds(value) {
4149
+ return `${round2(value)}s`;
4150
+ }
4151
+ function formatStepLabel(stepKey) {
4152
+ const match = /^step(\d+)$/.exec(stepKey);
4153
+ return match ? `Step ${match[1]}` : formatLabel(stepKey);
2896
4154
  }
2897
4155
 
2898
- // src/components/ShortcutsMenu.tsx
2899
- var import_react18 = require("react");
4156
+ // src/timeline/adapter.ts
4157
+ function resolveTimelineLoop(loop) {
4158
+ if (typeof loop === "object" && loop !== null) {
4159
+ return {
4160
+ enabled: true,
4161
+ start: Number.isFinite(loop.from) ? Math.max(0, loop.from) : 0
4162
+ };
4163
+ }
4164
+ return { enabled: Boolean(loop), start: 0 };
4165
+ }
4166
+ function buildTimelineMeta(id, name, duration, parsed, loop) {
4167
+ const resolvedLoop = resolveTimelineLoop(loop);
4168
+ return {
4169
+ id,
4170
+ name,
4171
+ duration,
4172
+ loop: resolvedLoop.enabled,
4173
+ loopStart: resolvedLoop.start,
4174
+ clips: parsed.clips
4175
+ };
4176
+ }
4177
+ function buildTimelineValues(staticClips, transport, timelineDuration, loopStart, actions) {
4178
+ var _a;
4179
+ const result = {
4180
+ time: transport.time,
4181
+ playing: transport.playing,
4182
+ duration: timelineDuration,
4183
+ ...actions
4184
+ };
4185
+ const span = loopSpan(transport.duration, loopStart);
4186
+ const cycleTime = (span > 0 ? transport.wraps * span : 0) + transport.time;
4187
+ for (const clip of staticClips) {
4188
+ const state = computeClipState(clip, transport.time, cycleTime);
4189
+ if (clip.group) {
4190
+ const bucket = result[_a = clip.group] ?? (result[_a] = {});
4191
+ bucket[clip.childKey] = state;
4192
+ } else {
4193
+ result[clip.key] = state;
4194
+ }
4195
+ }
4196
+ return result;
4197
+ }
4198
+
4199
+ // src/hooks/useDialTimeline.ts
4200
+ function useDialTimeline(name, config, options) {
4201
+ const serializedConfig = useSerialized(config);
4202
+ const parsed = (0, import_react22.useMemo)(() => parseTimelineConfig(config), [serializedConfig]);
4203
+ const { panelId, flatValues } = useDialStorePanel(name, parsed.dialConfig, {
4204
+ id: options?.id,
4205
+ persist: options?.persist,
4206
+ kind: "timeline"
4207
+ });
4208
+ const staticTimeline = (0, import_react22.useMemo)(
4209
+ () => computeStaticTimeline(parsed, flatValues),
4210
+ [parsed, flatValues]
4211
+ );
4212
+ const timelineDuration = staticTimeline.duration;
4213
+ const staticClips = staticTimeline.clips;
4214
+ const parsedRef = (0, import_react22.useRef)(parsed);
4215
+ parsedRef.current = parsed;
4216
+ const optionsRef = (0, import_react22.useRef)(options);
4217
+ optionsRef.current = options;
4218
+ const { start: loopStart } = resolveTimelineLoop(options?.loop);
4219
+ const buildMeta = (0, import_react22.useCallback)(
4220
+ () => buildTimelineMeta(panelId, name, timelineDuration, parsedRef.current, options?.loop),
4221
+ [panelId, name, timelineDuration, options?.loop]
4222
+ );
4223
+ const buildMetaRef = (0, import_react22.useRef)(buildMeta);
4224
+ buildMetaRef.current = buildMeta;
4225
+ (0, import_react22.useEffect)(() => {
4226
+ TimelineStore.register(buildMetaRef.current(), { autoplay: optionsRef.current?.autoplay ?? true });
4227
+ return () => TimelineStore.unregister(panelId);
4228
+ }, [panelId, name]);
4229
+ const mountedRef = (0, import_react22.useRef)(false);
4230
+ (0, import_react22.useEffect)(() => {
4231
+ if (!mountedRef.current) {
4232
+ mountedRef.current = true;
4233
+ return;
4234
+ }
4235
+ TimelineStore.update(buildMeta());
4236
+ }, [buildMeta, parsed]);
4237
+ const subscribeTransport = (0, import_react22.useCallback)(
4238
+ (callback) => TimelineStore.subscribe(panelId, callback),
4239
+ [panelId]
4240
+ );
4241
+ const getTransport = (0, import_react22.useCallback)(() => TimelineStore.getTransport(panelId), [panelId]);
4242
+ const transport = (0, import_react22.useSyncExternalStore)(subscribeTransport, getTransport, getTransport);
4243
+ const play = (0, import_react22.useCallback)(() => TimelineStore.play(panelId), [panelId]);
4244
+ const pause = (0, import_react22.useCallback)(() => TimelineStore.pause(panelId), [panelId]);
4245
+ const replay = (0, import_react22.useCallback)(() => TimelineStore.replay(panelId), [panelId]);
4246
+ const seek = (0, import_react22.useCallback)((time) => TimelineStore.seek(panelId, time), [panelId]);
4247
+ return (0, import_react22.useMemo)(
4248
+ () => buildTimelineValues(staticClips, transport, timelineDuration, loopStart, {
4249
+ play,
4250
+ pause,
4251
+ replay,
4252
+ seek
4253
+ }),
4254
+ [staticClips, transport, timelineDuration, loopStart, play, pause, replay, seek]
4255
+ );
4256
+ }
4257
+
4258
+ // src/components/Timeline/DialTimeline.tsx
4259
+ var import_react23 = require("react");
2900
4260
  var import_react_dom4 = require("react-dom");
2901
- var import_react19 = require("motion/react");
2902
- var import_jsx_runtime17 = require("react/jsx-runtime");
4261
+ var import_react24 = require("motion/react");
4262
+ var import_jsx_runtime18 = require("react/jsx-runtime");
4263
+ var DRAG_THRESHOLD_PX = 3;
4264
+ var MAJOR_TICK_TARGET_PX = 140;
4265
+ var MILLISECOND_STEP = 1e-3;
4266
+ var SECOND_TICK_STEPS = [
4267
+ 1e-3,
4268
+ 2e-3,
4269
+ 5e-3,
4270
+ 0.01,
4271
+ 0.02,
4272
+ 0.05,
4273
+ 0.1,
4274
+ 0.2,
4275
+ 0.5,
4276
+ 1,
4277
+ 2,
4278
+ 5,
4279
+ 10,
4280
+ 15,
4281
+ 30,
4282
+ 60,
4283
+ 120,
4284
+ 300,
4285
+ 600
4286
+ ];
4287
+ var MIN_TIMELINE_MAX_ZOOM = 8;
4288
+ var PLAYHEAD_FLAG_WIDTH = 52;
4289
+ var PLAYHEAD_FLAG_EDGE_OVERHANG = 1;
4290
+ var POPOVER_WIDTH = 280;
4291
+ var ZOOM_DRAG_DISTANCE = 180;
4292
+ var DEFAULT_DOCK_MAX_HEIGHT = 400;
4293
+ var MIN_DOCK_MAX_HEIGHT = 120;
4294
+ var subscribeGlobalTimelines = (callback) => TimelineStore.subscribeGlobal(callback);
4295
+ var getTimelines = () => TimelineStore.getTimelines();
4296
+ var subscribeTimelineVisibility = (callback) => TimelineUiStore.subscribe(callback);
4297
+ var getTimelineVisibility = () => TimelineUiStore.getVisible();
4298
+ var DialTimeline = (0, import_react23.memo)(function DialTimeline2({
4299
+ theme = "system",
4300
+ defaultVisible = true,
4301
+ visible,
4302
+ onVisibilityChange,
4303
+ defaultOpen = true,
4304
+ productionEnabled = isDevDefault
4305
+ }) {
4306
+ if (!productionEnabled) return null;
4307
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4308
+ DialTimelineDock,
4309
+ {
4310
+ theme,
4311
+ defaultVisible,
4312
+ visible,
4313
+ onVisibilityChange,
4314
+ defaultOpen
4315
+ }
4316
+ );
4317
+ });
4318
+ function DialTimelineDock({
4319
+ theme,
4320
+ defaultVisible,
4321
+ visible,
4322
+ onVisibilityChange,
4323
+ defaultOpen
4324
+ }) {
4325
+ const [mounted, setMounted] = (0, import_react23.useState)(false);
4326
+ const [dockMaxHeight, setDockMaxHeight] = (0, import_react23.useState)(DEFAULT_DOCK_MAX_HEIGHT);
4327
+ const visibilityControllerId = (0, import_react23.useRef)(/* @__PURE__ */ Symbol("dialkit-timeline-visibility"));
4328
+ const dockRef = (0, import_react23.useRef)(null);
4329
+ const resizeCleanupRef = (0, import_react23.useRef)(null);
4330
+ (0, import_react23.useEffect)(() => TimelineUiStore.registerController(visibilityControllerId.current, {
4331
+ visible,
4332
+ defaultVisible,
4333
+ onVisibilityChange
4334
+ }), []);
4335
+ (0, import_react23.useEffect)(() => {
4336
+ TimelineUiStore.updateController(visibilityControllerId.current, {
4337
+ visible,
4338
+ defaultVisible,
4339
+ onVisibilityChange
4340
+ });
4341
+ }, [defaultVisible, onVisibilityChange, visible]);
4342
+ (0, import_react23.useEffect)(() => {
4343
+ setMounted(true);
4344
+ }, []);
4345
+ (0, import_react23.useEffect)(() => () => resizeCleanupRef.current?.(), []);
4346
+ const handleResizePointerDown = (0, import_react23.useCallback)((e) => {
4347
+ const dock = dockRef.current;
4348
+ if (!dock) return;
4349
+ e.preventDefault();
4350
+ e.stopPropagation();
4351
+ resizeCleanupRef.current?.();
4352
+ const pointerY = e.clientY;
4353
+ const startHeight = dock.getBoundingClientRect().height;
4354
+ const handlePointerMove = (event) => {
4355
+ event.preventDefault();
4356
+ const viewportMax = Math.max(MIN_DOCK_MAX_HEIGHT, window.innerHeight - 24);
4357
+ setDockMaxHeight(clamp(startHeight + pointerY - event.clientY, MIN_DOCK_MAX_HEIGHT, viewportMax));
4358
+ };
4359
+ const finishResize = () => {
4360
+ window.removeEventListener("pointermove", handlePointerMove);
4361
+ window.removeEventListener("pointerup", finishResize);
4362
+ window.removeEventListener("pointercancel", finishResize);
4363
+ resizeCleanupRef.current = null;
4364
+ };
4365
+ window.addEventListener("pointermove", handlePointerMove, { passive: false });
4366
+ window.addEventListener("pointerup", finishResize);
4367
+ window.addEventListener("pointercancel", finishResize);
4368
+ resizeCleanupRef.current = finishResize;
4369
+ }, []);
4370
+ const timelines = (0, import_react23.useSyncExternalStore)(subscribeGlobalTimelines, getTimelines, getTimelines);
4371
+ const dockVisible = (0, import_react23.useSyncExternalStore)(
4372
+ subscribeTimelineVisibility,
4373
+ getTimelineVisibility,
4374
+ getTimelineVisibility
4375
+ );
4376
+ if (!mounted || typeof window === "undefined" || timelines.length === 0) {
4377
+ return null;
4378
+ }
4379
+ return (0, import_react_dom4.createPortal)(
4380
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dialkit-root dialkit-timeline", "data-theme": theme, hidden: !dockVisible, children: [
4381
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4382
+ "div",
4383
+ {
4384
+ className: "dialkit-timeline-resize-handle",
4385
+ onPointerDown: handleResizePointerDown,
4386
+ role: "separator",
4387
+ "aria-label": "Resize timeline height",
4388
+ "aria-orientation": "horizontal",
4389
+ title: "Drag to resize timeline"
4390
+ }
4391
+ ),
4392
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4393
+ "div",
4394
+ {
4395
+ ref: dockRef,
4396
+ className: "dialkit-timeline-dock",
4397
+ style: { maxHeight: `min(${dockMaxHeight}px, calc(100vh - 24px))` },
4398
+ children: timelines.map((timeline) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4399
+ TimelineSection,
4400
+ {
4401
+ meta: timeline,
4402
+ defaultOpen,
4403
+ theme,
4404
+ dockVisible
4405
+ },
4406
+ timeline.id
4407
+ ))
4408
+ }
4409
+ )
4410
+ ] }),
4411
+ document.body
4412
+ );
4413
+ }
4414
+ function useTransportSubscribe(id) {
4415
+ return (0, import_react23.useCallback)((callback) => TimelineStore.subscribe(id, callback), [id]);
4416
+ }
4417
+ function PlayPauseButton({ id }) {
4418
+ const subscribe = useTransportSubscribe(id);
4419
+ const getPlaying = (0, import_react23.useCallback)(() => TimelineStore.getTransport(id).playing, [id]);
4420
+ const playing = (0, import_react23.useSyncExternalStore)(subscribe, getPlaying, getPlaying);
4421
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4422
+ import_react24.motion.button,
4423
+ {
4424
+ className: "dialkit-toolbar-add",
4425
+ onClick: () => playing ? TimelineStore.pause(id) : TimelineStore.play(id),
4426
+ title: playing ? "Pause" : "Play",
4427
+ "aria-label": playing ? "Pause" : "Play",
4428
+ whileTap: { scale: 0.9 },
4429
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
4430
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { style: { position: "relative", width: 16, height: 16 }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react24.AnimatePresence, { initial: false, mode: "wait", children: playing ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4431
+ import_react24.motion.svg,
4432
+ {
4433
+ viewBox: "0 0 24 24",
4434
+ fill: "none",
4435
+ "aria-hidden": "true",
4436
+ style: { position: "absolute", inset: 0, width: 16, height: 16, color: "var(--dial-text-label)" },
4437
+ initial: { scale: 0.8, opacity: 0 },
4438
+ animate: { scale: 1, opacity: 1 },
4439
+ exit: { scale: 0.8, opacity: 0 },
4440
+ transition: { duration: 0.08 },
4441
+ children: ICON_PAUSE.map((d, i) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("path", { d, fill: "currentColor" }, i))
4442
+ },
4443
+ "pause"
4444
+ ) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4445
+ import_react24.motion.svg,
4446
+ {
4447
+ viewBox: "0 0 24 24",
4448
+ fill: "none",
4449
+ "aria-hidden": "true",
4450
+ style: { position: "absolute", inset: 0, width: 16, height: 16, color: "var(--dial-text-label)" },
4451
+ initial: { scale: 0.8, opacity: 0 },
4452
+ animate: { scale: 1, opacity: 1 },
4453
+ exit: { scale: 0.8, opacity: 0 },
4454
+ transition: { duration: 0.08 },
4455
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("path", { d: ICON_PLAY, fill: "currentColor" })
4456
+ },
4457
+ "play"
4458
+ ) }) })
4459
+ }
4460
+ );
4461
+ }
4462
+ function ReplayButton({ onReplay }) {
4463
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4464
+ import_react24.motion.button,
4465
+ {
4466
+ className: "dialkit-toolbar-add",
4467
+ onClick: onReplay,
4468
+ title: "Replay",
4469
+ "aria-label": "Replay",
4470
+ whileTap: { scale: 0.9 },
4471
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
4472
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: ICON_REPLAY.map((d, i) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("path", { d, fill: "currentColor" }, i)) })
4473
+ }
4474
+ );
4475
+ }
4476
+ function TimelinePlayheadFlag({
4477
+ id,
4478
+ duration,
4479
+ pxPerSecond,
4480
+ viewStart,
4481
+ viewEnd,
4482
+ laneWidth,
4483
+ rulerRef,
4484
+ onResetView
4485
+ }) {
4486
+ const subscribe = useTransportSubscribe(id);
4487
+ const getTime = (0, import_react23.useCallback)(() => TimelineStore.getTransport(id).time, [id]);
4488
+ const time = (0, import_react23.useSyncExternalStore)(subscribe, getTime, getTime);
4489
+ const scrubRef = (0, import_react23.useRef)(null);
4490
+ const cleanupScrubRef = (0, import_react23.useRef)(null);
4491
+ const seekFromClientX = (0, import_react23.useCallback)((clientX) => {
4492
+ const rect = scrubRef.current?.rect;
4493
+ const scrub = scrubRef.current;
4494
+ const contentWidth = rect?.width ?? 0;
4495
+ if (!rect || !scrub || contentWidth <= 0) return;
4496
+ const nextTime = clamp(
4497
+ scrub.viewStart + (clientX - rect.left) / contentWidth * (scrub.viewEnd - scrub.viewStart),
4498
+ scrub.viewStart,
4499
+ scrub.viewEnd
4500
+ );
4501
+ TimelineStore.seek(id, nextTime);
4502
+ }, [id]);
4503
+ const handlePointerDown = (0, import_react23.useCallback)((e) => {
4504
+ const rect = rulerRef.current?.getBoundingClientRect();
4505
+ if (!rect) return;
4506
+ e.preventDefault();
4507
+ e.stopPropagation();
4508
+ cleanupScrubRef.current?.();
4509
+ const resetView = e.shiftKey;
4510
+ scrubRef.current = {
4511
+ wasPlaying: TimelineStore.getTransport(id).playing,
4512
+ rect,
4513
+ viewStart: resetView ? 0 : viewStart,
4514
+ viewEnd: resetView ? duration : viewEnd
4515
+ };
4516
+ if (resetView) onResetView();
4517
+ TimelineStore.pause(id);
4518
+ seekFromClientX(e.clientX);
4519
+ const handleWindowPointerMove = (event) => {
4520
+ event.preventDefault();
4521
+ seekFromClientX(event.clientX);
4522
+ };
4523
+ const finishWindowScrub = () => {
4524
+ window.removeEventListener("pointermove", handleWindowPointerMove);
4525
+ window.removeEventListener("pointerup", finishWindowScrub);
4526
+ window.removeEventListener("pointercancel", finishWindowScrub);
4527
+ if (scrubRef.current?.wasPlaying) TimelineStore.play(id);
4528
+ scrubRef.current = null;
4529
+ cleanupScrubRef.current = null;
4530
+ };
4531
+ window.addEventListener("pointermove", handleWindowPointerMove, { passive: false });
4532
+ window.addEventListener("pointerup", finishWindowScrub);
4533
+ window.addEventListener("pointercancel", finishWindowScrub);
4534
+ cleanupScrubRef.current = finishWindowScrub;
4535
+ }, [duration, id, onResetView, rulerRef, seekFromClientX, viewEnd, viewStart]);
4536
+ (0, import_react23.useEffect)(() => () => cleanupScrubRef.current?.(), []);
4537
+ if (time < viewStart || time > viewEnd || laneWidth <= 0) return null;
4538
+ const x = clamp(
4539
+ (time - viewStart) * pxPerSecond,
4540
+ 0,
4541
+ laneWidth
4542
+ );
4543
+ const flagCenter = clamp(
4544
+ x,
4545
+ PLAYHEAD_FLAG_WIDTH / 2 - PLAYHEAD_FLAG_EDGE_OVERHANG,
4546
+ laneWidth - PLAYHEAD_FLAG_WIDTH / 2 + PLAYHEAD_FLAG_EDGE_OVERHANG
4547
+ );
4548
+ const flagOffset = flagCenter - x;
4549
+ const edge = flagOffset > 0.5 ? "start" : flagOffset < -0.5 ? "end" : "center";
4550
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
4551
+ "div",
4552
+ {
4553
+ className: "dialkit-timeline-playhead-control",
4554
+ "data-edge": edge,
4555
+ style: {
4556
+ left: `calc(var(--dial-timeline-label-w) + ${x}px)`,
4557
+ "--dial-timeline-playhead-flag-offset": `${flagOffset}px`
4558
+ },
4559
+ onPointerDown: handlePointerDown,
4560
+ role: "slider",
4561
+ "aria-label": "Timeline current time",
4562
+ "aria-valuemin": 0,
4563
+ "aria-valuemax": duration,
4564
+ "aria-valuenow": time,
4565
+ title: "Drag to scrub the timeline",
4566
+ children: [
4567
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-playhead-stem" }),
4568
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-playhead-anchor", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-playhead-flag", children: time.toFixed(2) }) })
4569
+ ]
4570
+ }
4571
+ );
4572
+ }
4573
+ function TimelineOverview({
4574
+ id,
4575
+ duration,
4576
+ viewStart,
4577
+ viewEnd,
4578
+ onNavigate
4579
+ }) {
4580
+ const subscribe = useTransportSubscribe(id);
4581
+ const getTime = (0, import_react23.useCallback)(() => TimelineStore.getTransport(id).time, [id]);
4582
+ const time = (0, import_react23.useSyncExternalStore)(subscribe, getTime, getTime);
4583
+ const scrubRef = (0, import_react23.useRef)(null);
4584
+ const seekFromClientX = (0, import_react23.useCallback)((clientX) => {
4585
+ const rect = scrubRef.current?.rect;
4586
+ if (!rect || rect.width <= 0 || duration <= 0) return;
4587
+ const nextTime = clamp((clientX - rect.left) / rect.width * duration, 0, duration);
4588
+ TimelineStore.seek(id, nextTime);
4589
+ onNavigate(nextTime);
4590
+ }, [duration, id, onNavigate]);
4591
+ const handlePointerDown = (0, import_react23.useCallback)((e) => {
4592
+ e.preventDefault();
4593
+ e.currentTarget.setPointerCapture(e.pointerId);
4594
+ scrubRef.current = {
4595
+ wasPlaying: TimelineStore.getTransport(id).playing,
4596
+ rect: e.currentTarget.getBoundingClientRect()
4597
+ };
4598
+ TimelineStore.pause(id);
4599
+ seekFromClientX(e.clientX);
4600
+ }, [id, seekFromClientX]);
4601
+ const handlePointerMove = (0, import_react23.useCallback)((e) => {
4602
+ if (scrubRef.current) seekFromClientX(e.clientX);
4603
+ }, [seekFromClientX]);
4604
+ const finishScrub = (0, import_react23.useCallback)(() => {
4605
+ if (scrubRef.current?.wasPlaying) TimelineStore.play(id);
4606
+ scrubRef.current = null;
4607
+ }, [id]);
4608
+ const viewportLeft = duration > 0 ? viewStart / duration * 100 : 0;
4609
+ const viewportWidth = duration > 0 ? (viewEnd - viewStart) / duration * 100 : 100;
4610
+ const playheadLeft = duration > 0 ? time / duration * 100 : 0;
4611
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
4612
+ "div",
4613
+ {
4614
+ className: "dialkit-timeline-overview",
4615
+ onPointerDown: handlePointerDown,
4616
+ onPointerMove: handlePointerMove,
4617
+ onPointerUp: finishScrub,
4618
+ onPointerCancel: finishScrub,
4619
+ onLostPointerCapture: finishScrub,
4620
+ title: "Drag to scrub the full timeline",
4621
+ children: [
4622
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4623
+ "div",
4624
+ {
4625
+ className: "dialkit-timeline-overview-viewport",
4626
+ "data-zoomed": viewportWidth < 99.999 || void 0,
4627
+ style: { left: `${viewportLeft}%`, width: `${viewportWidth}%` }
4628
+ }
4629
+ ),
4630
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-overview-progress", style: { width: `${playheadLeft}%` } }),
4631
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-overview-playhead", style: { left: `${playheadLeft}%` } })
4632
+ ]
4633
+ }
4634
+ );
4635
+ }
4636
+ function clampViewStart(start, duration, visibleDuration) {
4637
+ return clamp(start, 0, Math.max(0, duration - visibleDuration));
4638
+ }
4639
+ function formatRulerSeconds(time, step) {
4640
+ if (step >= 1 && Number.isInteger(time)) return formatClock(time);
4641
+ const decimals = Math.min(3, Math.max(1, Math.ceil(-Math.log10(step))));
4642
+ return `${time.toFixed(decimals)}s`;
4643
+ }
4644
+ var TimelineSection = (0, import_react23.memo)(function TimelineSection2({
4645
+ meta,
4646
+ defaultOpen,
4647
+ theme,
4648
+ dockVisible
4649
+ }) {
4650
+ const [open, setOpen] = (0, import_react23.useState)(defaultOpen);
4651
+ const [copied, setCopied] = (0, import_react23.useState)(false);
4652
+ const [popover, setPopover] = (0, import_react23.useState)(null);
4653
+ const [collapsedGroups, setCollapsedGroups] = (0, import_react23.useState)(() => /* @__PURE__ */ new Set());
4654
+ const [expandedTracks, setExpandedTracks] = (0, import_react23.useState)(() => /* @__PURE__ */ new Set());
4655
+ const [zoom, setZoom] = (0, import_react23.useState)(1);
4656
+ const [viewStart, setViewStart] = (0, import_react23.useState)(0);
4657
+ const subscribeValues = (0, import_react23.useCallback)(
4658
+ (callback) => DialStore.subscribe(meta.id, callback),
4659
+ [meta.id]
4660
+ );
4661
+ const getValues = (0, import_react23.useCallback)(() => DialStore.getValues(meta.id), [meta.id]);
4662
+ const values = (0, import_react23.useSyncExternalStore)(subscribeValues, getValues, getValues);
4663
+ const presets = DialStore.getPresets(meta.id);
4664
+ const activePresetId = DialStore.getActivePresetId(meta.id);
4665
+ const laneAreaRef = (0, import_react23.useRef)(null);
4666
+ const horizontalScrollRef = (0, import_react23.useRef)(null);
4667
+ const [laneWidth, setLaneWidth] = (0, import_react23.useState)(0);
4668
+ (0, import_react23.useLayoutEffect)(() => {
4669
+ if (!open) return;
4670
+ const ruler = laneAreaRef.current;
4671
+ if (!ruler) return;
4672
+ const measure = () => {
4673
+ setLaneWidth(ruler.getBoundingClientRect().width);
4674
+ };
4675
+ measure();
4676
+ const observer = new ResizeObserver(measure);
4677
+ observer.observe(ruler);
4678
+ return () => observer.disconnect();
4679
+ }, [open]);
4680
+ const visibleDuration = meta.duration > 0 ? meta.duration / zoom : meta.duration;
4681
+ const safeViewStart = clampViewStart(viewStart, meta.duration, visibleDuration);
4682
+ const viewEnd = safeViewStart + visibleDuration;
4683
+ const pxPerSecond = visibleDuration > 0 && laneWidth > 0 ? laneWidth / visibleDuration : 0;
4684
+ const millisecondReadableZoom = laneWidth > 0 && meta.duration > 0 ? MAJOR_TICK_TARGET_PX * meta.duration / (MILLISECOND_STEP * 10 * laneWidth) : MIN_TIMELINE_MAX_ZOOM;
4685
+ const maxZoom = Math.max(MIN_TIMELINE_MAX_ZOOM, millisecondReadableZoom);
4686
+ (0, import_react23.useEffect)(() => {
4687
+ setZoom((current) => clamp(current, 1, maxZoom));
4688
+ }, [maxZoom]);
4689
+ (0, import_react23.useEffect)(() => {
4690
+ setViewStart((current) => clampViewStart(current, meta.duration, meta.duration / zoom));
4691
+ }, [meta.duration, zoom]);
4692
+ (0, import_react23.useLayoutEffect)(() => {
4693
+ const scroller = horizontalScrollRef.current;
4694
+ if (!scroller || pxPerSecond <= 0) return;
4695
+ const nextScrollLeft = safeViewStart * pxPerSecond;
4696
+ if (Math.abs(scroller.scrollLeft - nextScrollLeft) > 0.5) {
4697
+ scroller.scrollLeft = nextScrollLeft;
4698
+ }
4699
+ }, [open, pxPerSecond, safeViewStart]);
4700
+ (0, import_react23.useEffect)(() => {
4701
+ if (!dockVisible) setPopover(null);
4702
+ }, [dockVisible]);
4703
+ const centerViewAt = (0, import_react23.useCallback)((time) => {
4704
+ if (zoom <= 1 || meta.duration <= 0) return;
4705
+ const windowDuration = meta.duration / zoom;
4706
+ setViewStart(clampViewStart(time - windowDuration / 2, meta.duration, windowDuration));
4707
+ }, [meta.duration, zoom]);
4708
+ const resetView = (0, import_react23.useCallback)(() => {
4709
+ setZoom(1);
4710
+ setViewStart(0);
4711
+ }, []);
4712
+ const handleReplay = (0, import_react23.useCallback)(() => {
4713
+ setViewStart(0);
4714
+ TimelineStore.replay(meta.id);
4715
+ }, [meta.id]);
4716
+ const handleHorizontalScroll = (0, import_react23.useCallback)((e) => {
4717
+ if (pxPerSecond <= 0) return;
4718
+ setViewStart(clampViewStart(
4719
+ e.currentTarget.scrollLeft / pxPerSecond,
4720
+ meta.duration,
4721
+ visibleDuration
4722
+ ));
4723
+ }, [meta.duration, pxPerSecond, visibleDuration]);
4724
+ const handleTimelineWheel = (0, import_react23.useCallback)((e) => {
4725
+ const scroller = horizontalScrollRef.current;
4726
+ if (!scroller || zoom <= 1) return;
4727
+ const horizontalDelta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.shiftKey ? e.deltaY : 0;
4728
+ if (horizontalDelta === 0) return;
4729
+ e.preventDefault();
4730
+ scroller.scrollLeft += horizontalDelta;
4731
+ }, [zoom]);
4732
+ const zoomDragRef = (0, import_react23.useRef)(null);
4733
+ const rulerScrubRef = (0, import_react23.useRef)(null);
4734
+ const seekRulerFromClientX = (0, import_react23.useCallback)((clientX) => {
4735
+ const scrub = rulerScrubRef.current;
4736
+ const contentWidth = scrub?.rect.width ?? 0;
4737
+ if (!scrub || contentWidth <= 0) return;
4738
+ TimelineStore.seek(
4739
+ meta.id,
4740
+ clamp(
4741
+ scrub.viewStart + (clientX - scrub.rect.left) / contentWidth * scrub.visibleDuration,
4742
+ scrub.viewStart,
4743
+ scrub.viewStart + scrub.visibleDuration
4744
+ )
4745
+ );
4746
+ }, [meta.id]);
4747
+ const handleRulerPointerDown = (0, import_react23.useCallback)((e) => {
4748
+ e.preventDefault();
4749
+ e.stopPropagation();
4750
+ const rect = e.currentTarget.getBoundingClientRect();
4751
+ const contentWidth = rect.width;
4752
+ if (contentWidth <= 0) return;
4753
+ e.currentTarget.setPointerCapture(e.pointerId);
4754
+ if (!e.altKey) {
4755
+ const resetView2 = e.shiftKey;
4756
+ rulerScrubRef.current = {
4757
+ wasPlaying: TimelineStore.getTransport(meta.id).playing,
4758
+ rect,
4759
+ viewStart: resetView2 ? 0 : safeViewStart,
4760
+ visibleDuration: resetView2 ? meta.duration : visibleDuration
4761
+ };
4762
+ if (resetView2) {
4763
+ setZoom(1);
4764
+ setViewStart(0);
4765
+ }
4766
+ TimelineStore.pause(meta.id);
4767
+ seekRulerFromClientX(e.clientX);
4768
+ return;
4769
+ }
4770
+ const anchorRatio = clamp((e.clientX - rect.left) / contentWidth, 0, 1);
4771
+ zoomDragRef.current = {
4772
+ pointerX: e.clientX,
4773
+ rect,
4774
+ zoom,
4775
+ viewStart: safeViewStart,
4776
+ anchorRatio,
4777
+ anchorTime: safeViewStart + anchorRatio * visibleDuration,
4778
+ moved: false
4779
+ };
4780
+ }, [meta.duration, meta.id, safeViewStart, seekRulerFromClientX, visibleDuration, zoom]);
4781
+ const handleRulerPointerMove = (0, import_react23.useCallback)((e) => {
4782
+ if (rulerScrubRef.current) {
4783
+ seekRulerFromClientX(e.clientX);
4784
+ return;
4785
+ }
4786
+ const drag = zoomDragRef.current;
4787
+ if (!drag || meta.duration <= 0) return;
4788
+ const dx = e.clientX - drag.pointerX;
4789
+ if (!drag.moved && Math.abs(dx) <= DRAG_THRESHOLD_PX) return;
4790
+ drag.moved = true;
4791
+ const nextZoom = clamp(drag.zoom * Math.exp(dx / ZOOM_DRAG_DISTANCE), 1, maxZoom);
4792
+ const nextVisibleDuration = meta.duration / nextZoom;
4793
+ const nextStart = clampViewStart(
4794
+ drag.anchorTime - drag.anchorRatio * nextVisibleDuration,
4795
+ meta.duration,
4796
+ nextVisibleDuration
4797
+ );
4798
+ setZoom(nextZoom);
4799
+ setViewStart(nextStart);
4800
+ }, [maxZoom, meta.duration, seekRulerFromClientX]);
4801
+ const handleRulerPointerUp = (0, import_react23.useCallback)(() => {
4802
+ if (rulerScrubRef.current?.wasPlaying) TimelineStore.play(meta.id);
4803
+ rulerScrubRef.current = null;
4804
+ zoomDragRef.current = null;
4805
+ }, [meta.id]);
4806
+ const handleRulerPointerCancel = (0, import_react23.useCallback)(() => {
4807
+ if (rulerScrubRef.current?.wasPlaying) TimelineStore.play(meta.id);
4808
+ rulerScrubRef.current = null;
4809
+ zoomDragRef.current = null;
4810
+ }, [meta.id]);
4811
+ const trackScrubRef = (0, import_react23.useRef)(null);
4812
+ const seekTrackFromClientX = (0, import_react23.useCallback)((clientX) => {
4813
+ const scrub = trackScrubRef.current;
4814
+ const contentWidth = scrub?.rect.width ?? 0;
4815
+ if (!scrub || contentWidth <= 0) return;
4816
+ const nextTime = clamp(
4817
+ scrub.viewStart + (clientX - scrub.rect.left) / contentWidth * scrub.visibleDuration,
4818
+ scrub.viewStart,
4819
+ scrub.viewStart + scrub.visibleDuration
4820
+ );
4821
+ TimelineStore.seek(meta.id, nextTime);
4822
+ }, [meta.id]);
4823
+ const handleTrackPointerDown = (0, import_react23.useCallback)((e) => {
4824
+ const target = e.target;
4825
+ if (target.closest(".dialkit-timeline-label, button")) return;
4826
+ if (!e.shiftKey && target.closest(".dialkit-timeline-clip")) return;
4827
+ const rect = laneAreaRef.current?.getBoundingClientRect();
4828
+ if (!rect) return;
4829
+ e.preventDefault();
4830
+ e.currentTarget.setPointerCapture(e.pointerId);
4831
+ const resetView2 = e.shiftKey;
4832
+ trackScrubRef.current = {
4833
+ wasPlaying: TimelineStore.getTransport(meta.id).playing,
4834
+ rect,
4835
+ viewStart: resetView2 ? 0 : safeViewStart,
4836
+ visibleDuration: resetView2 ? meta.duration : visibleDuration
4837
+ };
4838
+ if (resetView2) {
4839
+ setZoom(1);
4840
+ setViewStart(0);
4841
+ }
4842
+ setPopover(null);
4843
+ TimelineStore.pause(meta.id);
4844
+ seekTrackFromClientX(e.clientX);
4845
+ }, [meta.duration, meta.id, safeViewStart, seekTrackFromClientX, visibleDuration]);
4846
+ const handleTrackPointerMove = (0, import_react23.useCallback)((e) => {
4847
+ if (trackScrubRef.current) seekTrackFromClientX(e.clientX);
4848
+ }, [seekTrackFromClientX]);
4849
+ const finishTrackScrub = (0, import_react23.useCallback)(() => {
4850
+ if (trackScrubRef.current?.wasPlaying) TimelineStore.play(meta.id);
4851
+ trackScrubRef.current = null;
4852
+ }, [meta.id]);
4853
+ const handleCopy = (0, import_react23.useCallback)(() => {
4854
+ const normalized = normalizeTimelineValuesForCopy(DialStore.getValues(meta.id), meta.clips);
4855
+ navigator.clipboard.writeText(buildCopyInstruction("useDialTimeline", meta.name, normalized));
4856
+ setCopied(true);
4857
+ setTimeout(() => setCopied(false), 1500);
4858
+ }, [meta.clips, meta.id, meta.name]);
4859
+ const handleAddPreset = (0, import_react23.useCallback)(() => {
4860
+ DialStore.savePreset(meta.id, `Version ${presets.length + 2}`);
4861
+ }, [meta.id, presets.length]);
4862
+ const closePopover = (0, import_react23.useCallback)(() => setPopover(null), []);
4863
+ const openClipPopover = (0, import_react23.useCallback)(
4864
+ (clip, rect, stepKey) => {
4865
+ const targetPath = stepKey ? `${clip.key}.${stepKey}` : clip.key;
4866
+ const exclude = stepKey ? void 0 : clipPopoverExclusions(clip);
4867
+ if (getClipControls(meta.id, targetPath, exclude).length === 0) return;
4868
+ setPopover(
4869
+ (prev) => prev?.clip.key === clip.key && prev?.stepKey === stepKey ? null : {
4870
+ clip,
4871
+ stepKey,
4872
+ anchor: {
4873
+ left: rect.left,
4874
+ top: rect.top,
4875
+ right: rect.right,
4876
+ bottom: rect.bottom,
4877
+ width: rect.width,
4878
+ height: rect.height
4879
+ }
4880
+ }
4881
+ );
4882
+ },
4883
+ [meta.id]
4884
+ );
4885
+ const toggleTracks = (0, import_react23.useCallback)((clipKey) => {
4886
+ setExpandedTracks((prev) => {
4887
+ const next = new Set(prev);
4888
+ if (next.has(clipKey)) next.delete(clipKey);
4889
+ else next.add(clipKey);
4890
+ return next;
4891
+ });
4892
+ }, []);
4893
+ const handleBarClick = (0, import_react23.useCallback)(
4894
+ (clip, rect, stepKey) => {
4895
+ if (!stepKey && clip.tracks?.length) {
4896
+ toggleTracks(clip.key);
4897
+ return;
4898
+ }
4899
+ openClipPopover(clip, rect, stepKey);
4900
+ },
4901
+ [openClipPopover, toggleTracks]
4902
+ );
4903
+ const toggleGroup = (0, import_react23.useCallback)((group) => {
4904
+ setCollapsedGroups((prev) => {
4905
+ const next = new Set(prev);
4906
+ if (next.has(group)) next.delete(group);
4907
+ else next.add(group);
4908
+ return next;
4909
+ });
4910
+ }, []);
4911
+ const rawStep = pxPerSecond > 0 ? MAJOR_TICK_TARGET_PX / pxPerSecond : 1;
4912
+ const adaptiveMajorStep = SECOND_TICK_STEPS.find((step) => step >= rawStep) ?? SECOND_TICK_STEPS[SECOND_TICK_STEPS.length - 1];
4913
+ const majorStep = zoom < 1.5 && meta.duration >= 1 ? Math.max(1, adaptiveMajorStep) : adaptiveMajorStep;
4914
+ const fineTickStep = majorStep / 10;
4915
+ const majorTicks = [];
4916
+ const mediumTicks = [];
4917
+ const fineTicks = [];
4918
+ const firstMajorTick = Math.ceil((safeViewStart - 1e-6) / majorStep) * majorStep;
4919
+ for (let t = firstMajorTick; t <= viewEnd + 1e-6; t += majorStep) {
4920
+ majorTicks.push(Number(t.toFixed(4)));
4921
+ }
4922
+ const firstFineIndex = Math.ceil((safeViewStart - 1e-6) / fineTickStep);
4923
+ const lastFineIndex = Math.floor((viewEnd + 1e-6) / fineTickStep);
4924
+ for (let index = firstFineIndex; index <= lastFineIndex; index++) {
4925
+ if (index % 10 === 0) continue;
4926
+ const tick = Number((index * fineTickStep).toFixed(6));
4927
+ if (index % 5 === 0) mediumTicks.push(tick);
4928
+ else fineTicks.push(tick);
4929
+ }
4930
+ const rows = [];
4931
+ let lastGroup;
4932
+ for (const clip of meta.clips) {
4933
+ if (clip.group !== lastGroup) {
4934
+ lastGroup = clip.group;
4935
+ if (clip.group) {
4936
+ const group = clip.group;
4937
+ const isCollapsed = collapsedGroups.has(group);
4938
+ rows.push(
4939
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dialkit-timeline-row dialkit-timeline-group-row", children: [
4940
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dialkit-timeline-label", children: [
4941
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4942
+ "button",
4943
+ {
4944
+ className: "dialkit-timeline-group-toggle",
4945
+ "data-open": !isCollapsed,
4946
+ onClick: () => toggleGroup(group),
4947
+ title: isCollapsed ? "Expand layer" : "Collapse layer",
4948
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("path", { d: ICON_CHEVRON }) })
4949
+ }
4950
+ ),
4951
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: formatLabel(group) })
4952
+ ] }),
4953
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-lane" })
4954
+ ] }, `group:${group}`)
4955
+ );
4956
+ }
4957
+ }
4958
+ if (clip.group && collapsedGroups.has(clip.group)) continue;
4959
+ const isProps = Boolean(clip.tracks?.length);
4960
+ const tracksOpen = isProps && expandedTracks.has(clip.key);
4961
+ const stat = computeClipStaticFromValues(values, clip, meta.duration);
4962
+ rows.push(
4963
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dialkit-timeline-row", "data-grouped": clip.group ? "" : void 0, children: [
4964
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dialkit-timeline-label", children: [
4965
+ isProps ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4966
+ "button",
4967
+ {
4968
+ className: "dialkit-timeline-group-toggle",
4969
+ "data-open": tracksOpen,
4970
+ onClick: (e) => {
4971
+ e.stopPropagation();
4972
+ toggleTracks(clip.key);
4973
+ },
4974
+ title: tracksOpen ? "Collapse properties" : "Expand properties",
4975
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("path", { d: ICON_CHEVRON }) })
4976
+ }
4977
+ ) : null,
4978
+ clip.label
4979
+ ] }),
4980
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-lane", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4981
+ TimelineClip,
4982
+ {
4983
+ timelineId: meta.id,
4984
+ clip,
4985
+ at: stat.at,
4986
+ duration: stat.duration,
4987
+ loop: stat.loop,
4988
+ steps: clip.stepKeys?.length ? stat.tracks[0]?.steps : void 0,
4989
+ fixedDuration: isProps ? true : stat.isPhysics,
4990
+ composite: isProps,
4991
+ pxPerSecond,
4992
+ viewStart: safeViewStart,
4993
+ timelineDuration: meta.duration,
4994
+ selected: popover?.clip.key === clip.key,
4995
+ selectedStepKey: popover?.clip.key === clip.key ? popover.stepKey : void 0,
4996
+ onClick: handleBarClick,
4997
+ onDrag: closePopover
4998
+ }
4999
+ ) })
5000
+ ] }, clip.key)
5001
+ );
5002
+ if (tracksOpen) {
5003
+ for (const trackRef of clip.tracks ?? []) {
5004
+ const track = stat.tracks.find((candidate) => candidate.prop === trackRef.prop);
5005
+ if (!track) continue;
5006
+ const trackKey = `${clip.key}.${trackRef.prop}`;
5007
+ const trackMeta = {
5008
+ key: trackKey,
5009
+ label: `${clip.label} \xB7 ${formatLabel(trackRef.prop)}`,
5010
+ color: clip.color,
5011
+ loop: clip.loop,
5012
+ group: clip.group,
5013
+ stepKeys: trackRef.stepKeys
5014
+ };
5015
+ rows.push(
5016
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
5017
+ "div",
5018
+ {
5019
+ className: "dialkit-timeline-row dialkit-timeline-track-row",
5020
+ "data-grouped": clip.group ? "" : void 0,
5021
+ children: [
5022
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-label", children: formatLabel(trackRef.prop) }),
5023
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-lane", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5024
+ TimelineClip,
5025
+ {
5026
+ timelineId: meta.id,
5027
+ clip: trackMeta,
5028
+ at: stat.at + track.delay,
5029
+ duration: track.duration,
5030
+ loop: stat.loop,
5031
+ steps: trackRef.stepKeys?.length ? track.steps : void 0,
5032
+ fixedDuration: !trackRef.stepKeys?.length && track.steps[0]?.isPhysics === true,
5033
+ baseAt: stat.at,
5034
+ delayMode: true,
5035
+ pxPerSecond,
5036
+ viewStart: safeViewStart,
5037
+ timelineDuration: meta.duration,
5038
+ selected: popover?.clip.key === trackKey,
5039
+ selectedStepKey: popover?.clip.key === trackKey ? popover.stepKey : void 0,
5040
+ onClick: openClipPopover,
5041
+ onDrag: closePopover
5042
+ }
5043
+ ) })
5044
+ ]
5045
+ },
5046
+ trackKey
5047
+ )
5048
+ );
5049
+ }
5050
+ }
5051
+ }
5052
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dialkit-timeline-section", children: [
5053
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dialkit-timeline-header", "data-open": open || void 0, children: [
5054
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-identity", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "dialkit-timeline-title", children: meta.name }) }),
5055
+ !open && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5056
+ TimelineOverview,
5057
+ {
5058
+ id: meta.id,
5059
+ duration: meta.duration,
5060
+ viewStart: safeViewStart,
5061
+ viewEnd,
5062
+ onNavigate: centerViewAt
5063
+ }
5064
+ ),
5065
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dialkit-timeline-actions", children: [
5066
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(PlayPauseButton, { id: meta.id }),
5067
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ReplayButton, { onReplay: handleReplay }),
5068
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5069
+ import_react24.motion.button,
5070
+ {
5071
+ className: "dialkit-toolbar-add",
5072
+ onClick: handleAddPreset,
5073
+ title: "Add timeline version",
5074
+ "aria-label": "Add timeline version",
5075
+ whileTap: { scale: 0.9 },
5076
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
5077
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("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__ */ (0, import_jsx_runtime18.jsx)("path", { d }, i)) })
5078
+ }
5079
+ ),
5080
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5081
+ PresetManager,
5082
+ {
5083
+ panelId: meta.id,
5084
+ presets,
5085
+ activePresetId,
5086
+ onAdd: handleAddPreset
5087
+ }
5088
+ ),
5089
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5090
+ import_react24.motion.button,
5091
+ {
5092
+ className: "dialkit-toolbar-add",
5093
+ onClick: handleCopy,
5094
+ title: "Copy parameters",
5095
+ "aria-label": copied ? "Copied parameters" : "Copy parameters",
5096
+ whileTap: { scale: 0.9 },
5097
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
5098
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { style: { position: "relative", width: 16, height: 16 }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react24.AnimatePresence, { initial: false, mode: "wait", children: copied ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5099
+ import_react24.motion.svg,
5100
+ {
5101
+ viewBox: "0 0 24 24",
5102
+ fill: "none",
5103
+ stroke: "currentColor",
5104
+ strokeWidth: "2",
5105
+ strokeLinecap: "round",
5106
+ strokeLinejoin: "round",
5107
+ "aria-hidden": "true",
5108
+ style: { position: "absolute", inset: 0, width: 16, height: 16, color: "var(--dial-text-label)" },
5109
+ initial: { scale: 0.8, opacity: 0 },
5110
+ animate: { scale: 1, opacity: 1 },
5111
+ exit: { scale: 0.8, opacity: 0 },
5112
+ transition: { duration: 0.08 },
5113
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("path", { d: ICON_CHECK })
5114
+ },
5115
+ "check"
5116
+ ) : /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
5117
+ import_react24.motion.svg,
5118
+ {
5119
+ viewBox: "0 0 24 24",
5120
+ fill: "none",
5121
+ "aria-hidden": "true",
5122
+ style: { position: "absolute", inset: 0, width: 16, height: 16, color: "var(--dial-text-label)" },
5123
+ initial: { scale: 0.8, opacity: 0 },
5124
+ animate: { scale: 1, opacity: 1 },
5125
+ exit: { scale: 0.8, opacity: 0 },
5126
+ transition: { duration: 0.08 },
5127
+ children: [
5128
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("path", { d: ICON_CLIPBOARD.board, stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round" }),
5129
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("path", { d: ICON_CLIPBOARD.sparkle, fill: "currentColor" }),
5130
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("path", { d: ICON_CLIPBOARD.body, stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })
5131
+ ]
5132
+ },
5133
+ "clipboard"
5134
+ ) }) })
5135
+ }
5136
+ ),
5137
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5138
+ "button",
5139
+ {
5140
+ className: "dialkit-timeline-chevron",
5141
+ "data-open": open,
5142
+ "aria-expanded": open,
5143
+ onClick: () => setOpen(!open),
5144
+ title: open ? "Collapse timeline" : "Expand timeline",
5145
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("path", { d: ICON_CHEVRON }) })
5146
+ }
5147
+ )
5148
+ ] })
5149
+ ] }),
5150
+ open && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
5151
+ "div",
5152
+ {
5153
+ className: "dialkit-timeline-body",
5154
+ onWheel: handleTimelineWheel,
5155
+ onPointerDown: handleTrackPointerDown,
5156
+ onPointerMove: handleTrackPointerMove,
5157
+ onPointerUp: finishTrackScrub,
5158
+ onPointerCancel: finishTrackScrub,
5159
+ onLostPointerCapture: finishTrackScrub,
5160
+ children: [
5161
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dialkit-timeline-grid", children: [
5162
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dialkit-timeline-row dialkit-timeline-ruler-row", children: [
5163
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-label" }),
5164
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
5165
+ "div",
5166
+ {
5167
+ ref: laneAreaRef,
5168
+ className: "dialkit-timeline-ruler",
5169
+ onPointerDown: handleRulerPointerDown,
5170
+ onPointerMove: handleRulerPointerMove,
5171
+ onPointerUp: handleRulerPointerUp,
5172
+ onPointerCancel: handleRulerPointerCancel,
5173
+ onLostPointerCapture: handleRulerPointerCancel,
5174
+ title: "Drag to seek \xB7 Option-drag to zoom \xB7 Shift-drag to reset zoom",
5175
+ children: [
5176
+ fineTicks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-tick dialkit-timeline-tick-fine", style: { left: (t - safeViewStart) * pxPerSecond } }, `fine:${t}`)),
5177
+ mediumTicks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-tick dialkit-timeline-tick-medium", style: { left: (t - safeViewStart) * pxPerSecond } }, `medium:${t}`)),
5178
+ majorTicks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-tick", style: { left: (t - safeViewStart) * pxPerSecond }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "dialkit-timeline-tick-label", children: formatRulerSeconds(t, majorStep) }) }, t))
5179
+ ]
5180
+ }
5181
+ )
5182
+ ] }),
5183
+ rows,
5184
+ pxPerSecond > 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5185
+ TimelinePlayheadFlag,
5186
+ {
5187
+ id: meta.id,
5188
+ duration: meta.duration,
5189
+ pxPerSecond,
5190
+ viewStart: safeViewStart,
5191
+ viewEnd,
5192
+ laneWidth,
5193
+ rulerRef: laneAreaRef,
5194
+ onResetView: resetView
5195
+ }
5196
+ )
5197
+ ] }),
5198
+ zoom > 1 && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dialkit-timeline-scroll-row", children: [
5199
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-label" }),
5200
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5201
+ "div",
5202
+ {
5203
+ ref: horizontalScrollRef,
5204
+ className: "dialkit-timeline-horizontal-scroll",
5205
+ onScroll: handleHorizontalScroll,
5206
+ "aria-label": "Timeline horizontal scroll",
5207
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { width: laneWidth * zoom } })
5208
+ }
5209
+ )
5210
+ ] })
5211
+ ]
5212
+ }
5213
+ ),
5214
+ popover && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5215
+ ClipPopover,
5216
+ {
5217
+ panelId: meta.id,
5218
+ popover,
5219
+ values,
5220
+ theme,
5221
+ onClose: closePopover
5222
+ }
5223
+ )
5224
+ ] });
5225
+ });
5226
+ function ClipPopover({
5227
+ panelId,
5228
+ popover,
5229
+ values,
5230
+ theme,
5231
+ onClose
5232
+ }) {
5233
+ const ref = (0, import_react23.useRef)(null);
5234
+ const [naturalHeight, setNaturalHeight] = (0, import_react23.useState)(0);
5235
+ const [viewport, setViewport] = (0, import_react23.useState)(() => ({
5236
+ width: window.visualViewport?.width ?? window.innerWidth,
5237
+ height: window.visualViewport?.height ?? window.innerHeight,
5238
+ offsetLeft: window.visualViewport?.offsetLeft ?? 0,
5239
+ offsetTop: window.visualViewport?.offsetTop ?? 0
5240
+ }));
5241
+ (0, import_react23.useLayoutEffect)(() => {
5242
+ const element = ref.current;
5243
+ if (!element) return;
5244
+ const measure = () => setNaturalHeight(element.scrollHeight + 2);
5245
+ measure();
5246
+ const observer = new ResizeObserver(measure);
5247
+ const body = element.querySelector(".dialkit-timeline-popover-body");
5248
+ observer.observe(body ?? element);
5249
+ return () => observer.disconnect();
5250
+ }, [popover.clip.key, popover.stepKey]);
5251
+ (0, import_react23.useEffect)(() => {
5252
+ const updateViewport = () => setViewport({
5253
+ width: window.visualViewport?.width ?? window.innerWidth,
5254
+ height: window.visualViewport?.height ?? window.innerHeight,
5255
+ offsetLeft: window.visualViewport?.offsetLeft ?? 0,
5256
+ offsetTop: window.visualViewport?.offsetTop ?? 0
5257
+ });
5258
+ window.addEventListener("resize", updateViewport);
5259
+ window.visualViewport?.addEventListener("resize", updateViewport);
5260
+ window.visualViewport?.addEventListener("scroll", updateViewport);
5261
+ return () => {
5262
+ window.removeEventListener("resize", updateViewport);
5263
+ window.visualViewport?.removeEventListener("resize", updateViewport);
5264
+ window.visualViewport?.removeEventListener("scroll", updateViewport);
5265
+ };
5266
+ }, []);
5267
+ (0, import_react23.useEffect)(() => {
5268
+ const handlePointerDown = (e) => {
5269
+ const target = e.target;
5270
+ if (ref.current?.contains(target)) return;
5271
+ if (target.closest?.(".dialkit-timeline-clip")) return;
5272
+ if (target.closest?.(".dialkit-timeline-label")) return;
5273
+ onClose();
5274
+ };
5275
+ const handleKeyDown = (e) => {
5276
+ if (e.key === "Escape") onClose();
5277
+ };
5278
+ document.addEventListener("pointerdown", handlePointerDown, true);
5279
+ document.addEventListener("keydown", handleKeyDown);
5280
+ return () => {
5281
+ document.removeEventListener("pointerdown", handlePointerDown, true);
5282
+ document.removeEventListener("keydown", handleKeyDown);
5283
+ };
5284
+ }, [onClose]);
5285
+ const { clip, stepKey } = popover;
5286
+ let controls;
5287
+ let title;
5288
+ if (stepKey) {
5289
+ controls = getClipControls(panelId, `${clip.key}.${stepKey}`);
5290
+ if (stepKey === clip.stepKeys?.[0]) {
5291
+ const from = getControlAt(panelId, `${clip.key}.from`);
5292
+ if (from) {
5293
+ const toIndex = controls.findIndex((control) => control.path === `${clip.key}.${stepKey}.to`);
5294
+ controls = toIndex >= 0 ? [...controls.slice(0, toIndex), from, ...controls.slice(toIndex)] : [...controls, from];
5295
+ }
5296
+ }
5297
+ title = `${clip.label} \xB7 ${formatStepLabel(stepKey)}`;
5298
+ } else {
5299
+ controls = getClipControls(panelId, clip.key, clipPopoverExclusions(clip));
5300
+ title = clip.label;
5301
+ }
5302
+ if (controls.length === 0) return null;
5303
+ const targetPath = stepKey ? `${clip.key}.${stepKey}` : clip.key;
5304
+ const durationMeta = getControlAt(panelId, `${targetPath}.duration`);
5305
+ const durationValue = durationMeta ? values[durationMeta.path] : void 0;
5306
+ const transitionDuration = durationMeta?.type === "slider" && typeof durationValue === "number" ? {
5307
+ value: durationValue,
5308
+ onChange: (next) => DialStore.updateValue(panelId, durationMeta.path, next),
5309
+ min: Math.max(TIMELINE_MIN_CLIP_DURATION, durationMeta.min ?? 0),
5310
+ max: durationMeta.max,
5311
+ step: durationMeta.step
5312
+ } : void 0;
5313
+ const displayValues = timelinePopoverDisplayValues(values, clip.key, clip.stepKeys, stepKey);
5314
+ const viewportRight = viewport.offsetLeft + viewport.width;
5315
+ const viewportBottom = viewport.offsetTop + viewport.height;
5316
+ const popoverWidth = Math.min(POPOVER_WIDTH, Math.max(220, viewport.width - 24));
5317
+ const left = clamp(
5318
+ popover.anchor.left + popover.anchor.width / 2 - popoverWidth / 2,
5319
+ viewport.offsetLeft + 12,
5320
+ Math.max(viewport.offsetLeft + 12, viewportRight - popoverWidth - 12)
5321
+ );
5322
+ const spaceAbove = Math.max(0, popover.anchor.top - viewport.offsetTop - 22);
5323
+ const spaceBelow = Math.max(0, viewportBottom - popover.anchor.bottom - 22);
5324
+ const placeAbove = naturalHeight === 0 ? spaceAbove >= spaceBelow : naturalHeight <= spaceAbove || naturalHeight > spaceBelow && spaceAbove >= spaceBelow;
5325
+ const availableHeight = placeAbove ? spaceAbove : spaceBelow;
5326
+ const renderedHeight = Math.min(naturalHeight || availableHeight, availableHeight);
5327
+ const unclampedTop = placeAbove ? popover.anchor.top - 10 - renderedHeight : popover.anchor.bottom + 10;
5328
+ const top = clamp(
5329
+ unclampedTop,
5330
+ viewport.offsetTop + 12,
5331
+ Math.max(viewport.offsetTop + 12, viewportBottom - renderedHeight - 12)
5332
+ );
5333
+ return (0, import_react_dom4.createPortal)(
5334
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-root", "data-theme": theme, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
5335
+ "div",
5336
+ {
5337
+ ref,
5338
+ className: "dialkit-timeline-popover",
5339
+ "data-placement": placeAbove ? "above" : "below",
5340
+ style: {
5341
+ left,
5342
+ top,
5343
+ width: popoverWidth,
5344
+ maxHeight: availableHeight,
5345
+ visibility: naturalHeight > 0 ? "visible" : "hidden"
5346
+ },
5347
+ role: "dialog",
5348
+ "aria-label": `Edit ${title}`,
5349
+ children: [
5350
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dialkit-timeline-popover-header", children: [
5351
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "dialkit-timeline-popover-title", children: title }),
5352
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { className: "dialkit-timeline-popover-close", onClick: onClose, title: "Close editor", "aria-label": "Close editor", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("path", { d: "M6 6L18 18M18 6L6 18" }) }) })
5353
+ ] }),
5354
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-popover-body", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5355
+ ControlRenderer,
5356
+ {
5357
+ panelId,
5358
+ controls,
5359
+ values: displayValues,
5360
+ transitionDuration
5361
+ }
5362
+ ) })
5363
+ ]
5364
+ }
5365
+ ) }),
5366
+ document.body
5367
+ );
5368
+ }
5369
+ function clipPopoverExclusions(clip) {
5370
+ return /* @__PURE__ */ new Set([
5371
+ ...clip.stepKeys ?? [],
5372
+ ...clip.tracks?.map((track) => track.prop) ?? []
5373
+ ]);
5374
+ }
5375
+ function getClipControls(panelId, controlPath, excludeChildren) {
5376
+ const panel = DialStore.getPanel(panelId);
5377
+ const folder = panel ? findControl(panel.controls, controlPath) : null;
5378
+ if (!folder?.children) return [];
5379
+ return folder.children.filter((control) => {
5380
+ const childKey = control.path.slice(controlPath.length + 1);
5381
+ if (childKey === "at" || childKey === "duration") return false;
5382
+ return !excludeChildren?.has(childKey);
5383
+ });
5384
+ }
5385
+ function getControlAt(panelId, path) {
5386
+ const panel = DialStore.getPanel(panelId);
5387
+ return panel ? findControl(panel.controls, path) : null;
5388
+ }
5389
+ function TimelineClip({
5390
+ timelineId,
5391
+ clip,
5392
+ at,
5393
+ duration,
5394
+ loop,
5395
+ steps,
5396
+ fixedDuration,
5397
+ composite = false,
5398
+ baseAt = 0,
5399
+ delayMode = false,
5400
+ pxPerSecond,
5401
+ viewStart,
5402
+ timelineDuration,
5403
+ selected,
5404
+ selectedStepKey,
5405
+ onClick,
5406
+ onDrag
5407
+ }) {
5408
+ const dragRef = (0, import_react23.useRef)(null);
5409
+ const [dragging, setDragging] = (0, import_react23.useState)(false);
5410
+ const isSteps = Boolean(steps?.length);
5411
+ const handlePointerDown = (0, import_react23.useCallback)(
5412
+ (e) => {
5413
+ if (e.shiftKey) return;
5414
+ e.stopPropagation();
5415
+ const target = e.target;
5416
+ let mode = "move";
5417
+ let boundaryIndex;
5418
+ const boundary = target.dataset?.boundary;
5419
+ if (boundary !== void 0) {
5420
+ mode = "boundary";
5421
+ boundaryIndex = Number(boundary);
5422
+ } else if (!fixedDuration) {
5423
+ const edge = target.dataset?.edge;
5424
+ if (edge) mode = edge;
5425
+ }
5426
+ dragRef.current = {
5427
+ mode,
5428
+ boundaryIndex,
5429
+ pointerX: e.clientX,
5430
+ at,
5431
+ duration,
5432
+ stepDurations: steps?.map((step) => step.duration),
5433
+ clickEl: target.closest?.("[data-step]") ?? null,
5434
+ moved: false
5435
+ };
5436
+ e.currentTarget.setPointerCapture(e.pointerId);
5437
+ },
5438
+ [at, duration, fixedDuration, steps]
5439
+ );
5440
+ const handlePointerMove = (0, import_react23.useCallback)(
5441
+ (e) => {
5442
+ const drag = dragRef.current;
5443
+ if (!drag || pxPerSecond <= 0) return;
5444
+ const dx = e.clientX - drag.pointerX;
5445
+ if (!drag.moved) {
5446
+ if (Math.abs(dx) <= DRAG_THRESHOLD_PX) return;
5447
+ drag.moved = true;
5448
+ setDragging(true);
5449
+ onDrag();
5450
+ }
5451
+ const dt = dx / pxPerSecond;
5452
+ if (drag.mode === "boundary" && steps && drag.stepDurations) {
5453
+ const index = drag.boundaryIndex ?? 0;
5454
+ const others = drag.stepDurations.reduce((sum, d, j) => j === index ? sum : sum + d, 0);
5455
+ DialStore.updateValue(
5456
+ timelineId,
5457
+ `${clip.key}.${steps[index].key ?? ""}.duration`,
5458
+ clampStepResize(drag.stepDurations[index] + dt, drag.at, others, timelineDuration)
5459
+ );
5460
+ } else if (drag.mode === "move") {
5461
+ if (delayMode) {
5462
+ DialStore.updateValue(
5463
+ timelineId,
5464
+ `${clip.key}.delay`,
5465
+ clampTrackDelay(drag.at + dt - baseAt, baseAt, drag.duration, timelineDuration)
5466
+ );
5467
+ } else {
5468
+ DialStore.updateValue(timelineId, `${clip.key}.at`, clampClipMove(drag.at + dt, drag.duration, timelineDuration));
5469
+ }
5470
+ } else if (drag.mode === "end") {
5471
+ DialStore.updateValue(
5472
+ timelineId,
5473
+ `${clip.key}.duration`,
5474
+ clampClipResizeEnd(drag.duration + dt, drag.at, timelineDuration)
5475
+ );
5476
+ } else if (steps && drag.stepDurations) {
5477
+ const limit = Math.max(baseAt, 0);
5478
+ const next = clampClipResizeStart(Math.max(drag.at + dt, limit), drag.at, drag.stepDurations[0]);
5479
+ DialStore.updateValues(timelineId, {
5480
+ [delayMode ? `${clip.key}.delay` : `${clip.key}.at`]: delayMode ? Math.max(0, next.at - baseAt) : next.at,
5481
+ [`${clip.key}.${steps[0].key ?? ""}.duration`]: next.duration
5482
+ });
5483
+ } else {
5484
+ const limit = Math.max(baseAt, 0);
5485
+ const next = clampClipResizeStart(Math.max(drag.at + dt, limit), drag.at, drag.duration);
5486
+ DialStore.updateValues(timelineId, {
5487
+ [delayMode ? `${clip.key}.delay` : `${clip.key}.at`]: delayMode ? Math.max(0, next.at - baseAt) : next.at,
5488
+ [`${clip.key}.duration`]: next.duration
5489
+ });
5490
+ }
5491
+ },
5492
+ [baseAt, clip.key, delayMode, onDrag, pxPerSecond, steps, timelineId, timelineDuration]
5493
+ );
5494
+ const handlePointerUp = (0, import_react23.useCallback)(
5495
+ (e) => {
5496
+ const drag = dragRef.current;
5497
+ dragRef.current = null;
5498
+ setDragging(false);
5499
+ if (drag && !drag.moved) {
5500
+ const stepKey = drag.clickEl?.dataset?.step;
5501
+ const anchorEl = drag.clickEl ?? e.currentTarget;
5502
+ onClick(clip, anchorEl.getBoundingClientRect(), stepKey);
5503
+ }
5504
+ },
5505
+ [clip, onClick]
5506
+ );
5507
+ const handlePointerCancel = (0, import_react23.useCallback)(() => {
5508
+ dragRef.current = null;
5509
+ setDragging(false);
5510
+ }, []);
5511
+ const width = Math.max(duration * pxPerSecond, 14);
5512
+ const resizable = duration > 0 && !fixedDuration && !composite;
5513
+ const durationText = `${fixedDuration && !composite ? "~" : ""}${formatSeconds(duration)}`;
5514
+ const looping = loop === "repeat" && duration > 0;
5515
+ const ghostCycles = [];
5516
+ if (looping) {
5517
+ const maxGhostCycles = 256;
5518
+ const firstGhostIndex = Math.max(1, Math.floor((viewStart - at) / duration));
5519
+ for (let offset = 0; offset < maxGhostCycles; offset++) {
5520
+ const index = firstGhostIndex + offset;
5521
+ const start = at + duration * index;
5522
+ if (start >= timelineDuration - 1e-6) break;
5523
+ ghostCycles.push({
5524
+ start,
5525
+ duration: Math.min(duration, timelineDuration - start),
5526
+ index
5527
+ });
5528
+ }
5529
+ }
5530
+ const boundaryOffsets = [];
5531
+ if (steps) {
5532
+ let cumulative = 0;
5533
+ for (const step of steps) {
5534
+ cumulative += step.duration;
5535
+ boundaryOffsets.push(cumulative);
5536
+ }
5537
+ }
5538
+ 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" : ""}`;
5539
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
5540
+ ghostCycles.map((cycle) => {
5541
+ const ghostWidth = Math.max(1, cycle.duration * pxPerSecond - 2);
5542
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5543
+ "div",
5544
+ {
5545
+ className: "dialkit-timeline-clip-ghost",
5546
+ "data-steps": isSteps || void 0,
5547
+ "aria-hidden": "true",
5548
+ style: {
5549
+ left: (cycle.start - viewStart) * pxPerSecond + 1,
5550
+ width: ghostWidth,
5551
+ background: clip.color
5552
+ },
5553
+ children: steps?.map((step, stepIndex) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5554
+ "span",
5555
+ {
5556
+ className: "dialkit-timeline-clip-ghost-segment",
5557
+ style: { width: step.duration * pxPerSecond }
5558
+ },
5559
+ step.key ?? `step:${stepIndex}`
5560
+ ))
5561
+ },
5562
+ `ghost:${cycle.index}`
5563
+ );
5564
+ }),
5565
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5566
+ "div",
5567
+ {
5568
+ className: "dialkit-timeline-clip",
5569
+ "data-steps": isSteps || void 0,
5570
+ "data-composite": composite || void 0,
5571
+ "data-selected": selected || void 0,
5572
+ "data-dragging": dragging || void 0,
5573
+ style: {
5574
+ left: (at - viewStart) * pxPerSecond,
5575
+ width,
5576
+ background: composite ? `${clip.color}80` : clip.color
5577
+ },
5578
+ onPointerDown: handlePointerDown,
5579
+ onPointerMove: handlePointerMove,
5580
+ onPointerUp: handlePointerUp,
5581
+ onPointerCancel: handlePointerCancel,
5582
+ onLostPointerCapture: handlePointerCancel,
5583
+ title: barTitle,
5584
+ children: composite ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_jsx_runtime18.Fragment, { children: width > 56 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "dialkit-timeline-clip-duration", children: durationText }) }) : isSteps ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
5585
+ steps.map((step) => {
5586
+ const segmentWidth = step.duration * pxPerSecond;
5587
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5588
+ "div",
5589
+ {
5590
+ className: "dialkit-timeline-clip-segment",
5591
+ "data-step": step.key ?? void 0,
5592
+ "data-selected": selectedStepKey === step.key || void 0,
5593
+ style: { width: segmentWidth },
5594
+ children: segmentWidth > 52 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "dialkit-timeline-clip-duration", children: formatSeconds(step.duration) })
5595
+ },
5596
+ step.key ?? "step"
5597
+ );
5598
+ }),
5599
+ steps.map(
5600
+ (step, index) => step.isPhysics ? null : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5601
+ "div",
5602
+ {
5603
+ className: "dialkit-timeline-clip-handle",
5604
+ "data-boundary": index,
5605
+ style: { left: boundaryOffsets[index] * pxPerSecond - 4 }
5606
+ },
5607
+ `boundary:${step.key}`
5608
+ )
5609
+ ),
5610
+ !steps[0].isPhysics && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-clip-handle", "data-edge": "start" })
5611
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
5612
+ resizable && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-clip-handle", "data-edge": "start" }),
5613
+ width > 56 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "dialkit-timeline-clip-duration", children: durationText }),
5614
+ resizable && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dialkit-timeline-clip-handle", "data-edge": "end" })
5615
+ ] })
5616
+ }
5617
+ ),
5618
+ looping && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "dialkit-timeline-loop-infinity", "aria-hidden": "true", title: "Repeats indefinitely", children: "\u221E" })
5619
+ ] });
5620
+ }
5621
+
5622
+ // src/components/ButtonGroup.tsx
5623
+ var import_jsx_runtime19 = require("react/jsx-runtime");
5624
+ function ButtonGroup({ buttons }) {
5625
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "dialkit-button-group", children: buttons.map((button, index) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5626
+ "button",
5627
+ {
5628
+ className: "dialkit-button",
5629
+ onClick: button.onClick,
5630
+ children: button.label
5631
+ },
5632
+ index
5633
+ )) });
5634
+ }
5635
+
5636
+ // src/components/ShortcutsMenu.tsx
5637
+ var import_react25 = require("react");
5638
+ var import_react_dom5 = require("react-dom");
5639
+ var import_react26 = require("motion/react");
5640
+ var import_jsx_runtime20 = require("react/jsx-runtime");
2903
5641
  function formatShortcutKey(sc) {
2904
5642
  if (!sc.key) return "\u2014";
2905
5643
  const mod = sc.modifier === "alt" ? "\u2325" : sc.modifier === "shift" ? "\u21E7" : sc.modifier === "meta" ? "\u2318" : "";
@@ -2919,23 +5657,23 @@ function formatInteraction(sc) {
2919
5657
  }
2920
5658
  }
2921
5659
  function ShortcutsMenu({ panelId }) {
2922
- const [isOpen, setIsOpen] = (0, import_react18.useState)(false);
2923
- const triggerRef = (0, import_react18.useRef)(null);
2924
- const dropdownRef = (0, import_react18.useRef)(null);
2925
- const [pos, setPos] = (0, import_react18.useState)({ top: 0, right: 0 });
2926
- const open = (0, import_react18.useCallback)(() => {
5660
+ const [isOpen, setIsOpen] = (0, import_react25.useState)(false);
5661
+ const triggerRef = (0, import_react25.useRef)(null);
5662
+ const dropdownRef = (0, import_react25.useRef)(null);
5663
+ const [pos, setPos] = (0, import_react25.useState)({ top: 0, right: 0 });
5664
+ const open = (0, import_react25.useCallback)(() => {
2927
5665
  const rect = triggerRef.current?.getBoundingClientRect();
2928
5666
  if (rect) {
2929
5667
  setPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
2930
5668
  }
2931
5669
  setIsOpen(true);
2932
5670
  }, []);
2933
- const close = (0, import_react18.useCallback)(() => setIsOpen(false), []);
2934
- const toggle = (0, import_react18.useCallback)(() => {
5671
+ const close = (0, import_react25.useCallback)(() => setIsOpen(false), []);
5672
+ const toggle = (0, import_react25.useCallback)(() => {
2935
5673
  if (isOpen) close();
2936
5674
  else open();
2937
5675
  }, [isOpen, open, close]);
2938
- (0, import_react18.useEffect)(() => {
5676
+ (0, import_react25.useEffect)(() => {
2939
5677
  if (!isOpen) return;
2940
5678
  const handler = (e) => {
2941
5679
  const target = e.target;
@@ -2966,9 +5704,9 @@ function ShortcutsMenu({ panelId }) {
2966
5704
  label: findLabel(panel.controls)
2967
5705
  };
2968
5706
  });
2969
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
2970
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2971
- import_react19.motion.button,
5707
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
5708
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
5709
+ import_react26.motion.button,
2972
5710
  {
2973
5711
  ref: triggerRef,
2974
5712
  className: "dialkit-shortcuts-trigger",
@@ -2976,19 +5714,19 @@ function ShortcutsMenu({ panelId }) {
2976
5714
  title: "Keyboard shortcuts",
2977
5715
  whileTap: { scale: 0.9 },
2978
5716
  transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
2979
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2980
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("rect", { x: "2", y: "6", width: "20", height: "12", rx: "2" }),
2981
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("path", { d: "M6 10H6.01" }),
2982
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("path", { d: "M10 10H10.01" }),
2983
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("path", { d: "M14 10H14.01" }),
2984
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("path", { d: "M18 10H18.01" }),
2985
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("path", { d: "M8 14H16" })
5717
+ children: /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
5718
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("rect", { x: "2", y: "6", width: "20", height: "12", rx: "2" }),
5719
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("path", { d: "M6 10H6.01" }),
5720
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("path", { d: "M10 10H10.01" }),
5721
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("path", { d: "M14 10H14.01" }),
5722
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("path", { d: "M18 10H18.01" }),
5723
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("path", { d: "M8 14H16" })
2986
5724
  ] })
2987
5725
  }
2988
5726
  ),
2989
- (0, import_react_dom4.createPortal)(
2990
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react19.AnimatePresence, { children: isOpen && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
2991
- import_react19.motion.div,
5727
+ (0, import_react_dom5.createPortal)(
5728
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react26.AnimatePresence, { children: isOpen && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
5729
+ import_react26.motion.div,
2992
5730
  {
2993
5731
  ref: dropdownRef,
2994
5732
  className: "dialkit-root dialkit-shortcuts-dropdown",
@@ -2998,13 +5736,13 @@ function ShortcutsMenu({ panelId }) {
2998
5736
  exit: { opacity: 0, y: 4, scale: 0.97, pointerEvents: "none" },
2999
5737
  transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
3000
5738
  children: [
3001
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "dialkit-shortcuts-title", children: "Keyboard Shortcuts" }),
3002
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "dialkit-shortcuts-list", children: rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "dialkit-shortcuts-row", children: [
3003
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "dialkit-shortcuts-row-key", children: formatShortcutKey(row.shortcut) }),
3004
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "dialkit-shortcuts-row-label", children: row.label }),
3005
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "dialkit-shortcuts-row-mode", children: formatInteraction(row.shortcut) })
5739
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "dialkit-shortcuts-title", children: "Keyboard Shortcuts" }),
5740
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "dialkit-shortcuts-list", children: rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "dialkit-shortcuts-row", children: [
5741
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "dialkit-shortcuts-row-key", children: formatShortcutKey(row.shortcut) }),
5742
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "dialkit-shortcuts-row-label", children: row.label }),
5743
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "dialkit-shortcuts-row-mode", children: formatInteraction(row.shortcut) })
3006
5744
  ] }, row.path)) }),
3007
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "dialkit-shortcuts-hint", children: "See pill badges on controls for keys" })
5745
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "dialkit-shortcuts-hint", children: "See pill badges on controls for keys" })
3008
5746
  ]
3009
5747
  }
3010
5748
  ) }),
@@ -3016,8 +5754,10 @@ function ShortcutsMenu({ panelId }) {
3016
5754
  0 && (module.exports = {
3017
5755
  ButtonGroup,
3018
5756
  ColorControl,
5757
+ ControlRenderer,
3019
5758
  DialRoot,
3020
5759
  DialStore,
5760
+ DialTimeline,
3021
5761
  EasingVisualization,
3022
5762
  Folder,
3023
5763
  PresetManager,
@@ -3027,9 +5767,12 @@ function ShortcutsMenu({ panelId }) {
3027
5767
  SpringControl,
3028
5768
  SpringVisualization,
3029
5769
  TextControl,
5770
+ TimelineStore,
3030
5771
  Toggle,
3031
5772
  TransitionControl,
5773
+ formatClock,
3032
5774
  useDialKit,
3033
- useDialKitController
5775
+ useDialKitController,
5776
+ useDialTimeline
3034
5777
  });
3035
5778
  //# sourceMappingURL=index.cjs.map