dialkit 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +433 -3
  2. package/dist/icons.d.ts +4 -1
  3. package/dist/icons.js +13 -0
  4. package/dist/icons.js.map +1 -1
  5. package/dist/index.cjs +3118 -480
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +274 -6
  8. package/dist/index.d.ts +274 -6
  9. package/dist/index.js +3050 -417
  10. package/dist/index.js.map +1 -1
  11. package/dist/solid/index.d.ts +225 -3
  12. package/dist/solid/index.js +5697 -2508
  13. package/dist/solid/index.js.map +1 -1
  14. package/dist/store/index.cjs +57 -16
  15. package/dist/store/index.cjs.map +1 -1
  16. package/dist/store/index.d.cts +14 -2
  17. package/dist/store/index.d.ts +14 -2
  18. package/dist/store/index.js +52 -16
  19. package/dist/store/index.js.map +1 -1
  20. package/dist/styles.css +704 -0
  21. package/dist/svelte/components/ControlRenderer.svelte +5 -2
  22. package/dist/svelte/components/ControlRenderer.svelte.d.ts +2 -0
  23. package/dist/svelte/components/ControlRenderer.svelte.d.ts.map +1 -1
  24. package/dist/svelte/components/DialRoot.svelte +43 -6
  25. package/dist/svelte/components/DialRoot.svelte.d.ts.map +1 -1
  26. package/dist/svelte/components/Panel.svelte +7 -1
  27. package/dist/svelte/components/Panel.svelte.d.ts +2 -0
  28. package/dist/svelte/components/Panel.svelte.d.ts.map +1 -1
  29. package/dist/svelte/components/Timeline/ClipPopover.svelte +206 -0
  30. package/dist/svelte/components/Timeline/ClipPopover.svelte.d.ts +26 -0
  31. package/dist/svelte/components/Timeline/ClipPopover.svelte.d.ts.map +1 -0
  32. package/dist/svelte/components/Timeline/DialTimeline.svelte +76 -0
  33. package/dist/svelte/components/Timeline/DialTimeline.svelte.d.ts +13 -0
  34. package/dist/svelte/components/Timeline/DialTimeline.svelte.d.ts.map +1 -0
  35. package/dist/svelte/components/Timeline/TimelineClip.svelte +233 -0
  36. package/dist/svelte/components/Timeline/TimelineClip.svelte.d.ts +24 -0
  37. package/dist/svelte/components/Timeline/TimelineClip.svelte.d.ts.map +1 -0
  38. package/dist/svelte/components/Timeline/TimelineSection.svelte +756 -0
  39. package/dist/svelte/components/Timeline/TimelineSection.svelte.d.ts +12 -0
  40. package/dist/svelte/components/Timeline/TimelineSection.svelte.d.ts.map +1 -0
  41. package/dist/svelte/components/Timeline/TimelineToggleButton.svelte +25 -0
  42. package/dist/svelte/components/Timeline/TimelineToggleButton.svelte.d.ts +4 -0
  43. package/dist/svelte/components/Timeline/TimelineToggleButton.svelte.d.ts.map +1 -0
  44. package/dist/svelte/components/TransitionControl.svelte +26 -11
  45. package/dist/svelte/components/TransitionControl.svelte.d.ts +9 -0
  46. package/dist/svelte/components/TransitionControl.svelte.d.ts.map +1 -1
  47. package/dist/svelte/createDialTimeline.svelte.d.ts +4 -0
  48. package/dist/svelte/createDialTimeline.svelte.d.ts.map +1 -0
  49. package/dist/svelte/createDialTimeline.svelte.js +73 -0
  50. package/dist/svelte/index.d.ts +4 -0
  51. package/dist/svelte/index.d.ts.map +1 -1
  52. package/dist/svelte/index.js +3 -0
  53. package/dist/svelte/theme-css.d.ts +1 -1
  54. package/dist/svelte/theme-css.d.ts.map +1 -1
  55. package/dist/svelte/theme-css.js +704 -0
  56. package/dist/timeline/index.cjs +1288 -0
  57. package/dist/timeline/index.cjs.map +1 -0
  58. package/dist/timeline/index.d.cts +443 -0
  59. package/dist/timeline/index.d.ts +443 -0
  60. package/dist/timeline/index.js +1233 -0
  61. package/dist/timeline/index.js.map +1 -0
  62. package/dist/vue/index.d.ts +273 -7
  63. package/dist/vue/index.js +2867 -361
  64. package/dist/vue/index.js.map +1 -1
  65. package/package.json +23 -13
  66. package/dist/solid/index.cjs +0 -3536
  67. package/dist/solid/index.cjs.map +0 -1
  68. package/dist/solid/index.d.cts +0 -295
  69. package/dist/vue/index.cjs +0 -3497
  70. package/dist/vue/index.cjs.map +0 -1
  71. package/dist/vue/index.d.cts +0 -722
@@ -0,0 +1,1233 @@
1
+ // src/store/TimelineStore.ts
2
+ function loopSpan(duration, loopStart) {
3
+ if (!Number.isFinite(duration) || duration <= 0) return 0;
4
+ if (!Number.isFinite(loopStart)) loopStart = 0;
5
+ const start = Math.min(Math.max(0, loopStart), duration);
6
+ return duration - start > 0 ? duration - start : duration;
7
+ }
8
+ function foldLoopTime(time, duration, loopStart = 0) {
9
+ if (!Number.isFinite(time) || !Number.isFinite(duration) || duration <= 0) {
10
+ return { time: 0, wraps: 0 };
11
+ }
12
+ if (time < duration) return { time, wraps: 0 };
13
+ const span = loopSpan(duration, loopStart);
14
+ const base = duration - span;
15
+ const over = time - base;
16
+ return { time: base + over % span, wraps: Math.floor(over / span) };
17
+ }
18
+ var TIMELINE_CLIP_COLORS = [
19
+ "#E8E8E8"
20
+ // neutral white — slightly off-white so the pure-white selection ring still reads
21
+ ];
22
+ var EMPTY_TRANSPORT = Object.freeze({ time: 0, playing: false, duration: 0, wraps: 0 });
23
+ var TimelineStoreClass = class {
24
+ constructor() {
25
+ this.timelines = /* @__PURE__ */ new Map();
26
+ this.transports = /* @__PURE__ */ new Map();
27
+ this.listeners = /* @__PURE__ */ new Map();
28
+ this.globalListeners = /* @__PURE__ */ new Set();
29
+ this.registrationCounts = /* @__PURE__ */ new Map();
30
+ this.listCache = null;
31
+ this.rafId = null;
32
+ this.lastTick = 0;
33
+ this.tick = (now) => {
34
+ const dt = Math.max(0, (now - this.lastTick) / 1e3);
35
+ this.lastTick = now;
36
+ let anyPlaying = false;
37
+ for (const [id, transport] of this.transports) {
38
+ if (!transport.playing) continue;
39
+ const meta = this.timelines.get(id);
40
+ const duration = meta?.duration ?? transport.duration;
41
+ if (!Number.isFinite(duration) || duration <= 0) {
42
+ this.transports.set(id, { time: 0, playing: false, duration: 0, wraps: 0 });
43
+ this.notify(id);
44
+ continue;
45
+ }
46
+ let time = transport.time + dt;
47
+ let playing = true;
48
+ let wraps = transport.wraps;
49
+ if (time >= duration) {
50
+ if (meta?.loop) {
51
+ const folded = foldLoopTime(time, duration, meta.loopStart);
52
+ time = folded.time;
53
+ wraps += folded.wraps;
54
+ } else {
55
+ time = duration;
56
+ playing = false;
57
+ }
58
+ }
59
+ this.transports.set(id, { time, playing, duration, wraps });
60
+ if (playing) anyPlaying = true;
61
+ this.notify(id);
62
+ }
63
+ this.rafId = anyPlaying ? window.requestAnimationFrame(this.tick) : null;
64
+ };
65
+ }
66
+ register(meta, options) {
67
+ const existing = this.timelines.get(meta.id);
68
+ if (existing && existing.name !== meta.name) {
69
+ console.warn(
70
+ `[dialkit] Timeline id "${meta.id}" is already registered by "${existing.name}"; "${meta.name}" will share and overwrite that transport.`
71
+ );
72
+ }
73
+ this.registrationCounts.set(meta.id, (this.registrationCounts.get(meta.id) ?? 0) + 1);
74
+ this.applyMeta(meta, options.autoplay);
75
+ }
76
+ update(meta) {
77
+ if (!this.timelines.has(meta.id)) return;
78
+ this.applyMeta(meta, false);
79
+ }
80
+ unregister(id) {
81
+ const nextCount = (this.registrationCounts.get(id) ?? 1) - 1;
82
+ if (nextCount > 0) {
83
+ this.registrationCounts.set(id, nextCount);
84
+ return;
85
+ }
86
+ this.registrationCounts.delete(id);
87
+ this.timelines.delete(id);
88
+ this.transports.delete(id);
89
+ if (this.listeners.get(id)?.size === 0) this.listeners.delete(id);
90
+ this.listCache = null;
91
+ this.notifyGlobal();
92
+ }
93
+ play(id) {
94
+ const transport = this.transports.get(id);
95
+ if (!transport || transport.duration <= 0 || transport.playing) return;
96
+ const restart = transport.time >= transport.duration;
97
+ this.transports.set(id, {
98
+ ...transport,
99
+ time: restart ? 0 : transport.time,
100
+ wraps: restart ? 0 : transport.wraps,
101
+ playing: true
102
+ });
103
+ this.notify(id);
104
+ this.ensureLoop();
105
+ }
106
+ pause(id) {
107
+ const transport = this.transports.get(id);
108
+ if (!transport || !transport.playing) return;
109
+ this.transports.set(id, { ...transport, playing: false });
110
+ this.notify(id);
111
+ }
112
+ replay(id) {
113
+ const transport = this.transports.get(id);
114
+ if (!transport || transport.duration <= 0) return;
115
+ this.transports.set(id, { ...transport, time: 0, wraps: 0, playing: true });
116
+ this.notify(id);
117
+ this.ensureLoop();
118
+ }
119
+ seek(id, time) {
120
+ const transport = this.transports.get(id);
121
+ if (!transport || !Number.isFinite(time)) return;
122
+ const clamped = Math.min(transport.duration, Math.max(0, time));
123
+ this.transports.set(id, { ...transport, time: clamped, wraps: 0 });
124
+ this.notify(id);
125
+ }
126
+ getTransport(id) {
127
+ return this.transports.get(id) ?? EMPTY_TRANSPORT;
128
+ }
129
+ getTimeline(id) {
130
+ return this.timelines.get(id);
131
+ }
132
+ getTimelines() {
133
+ if (!this.listCache) {
134
+ this.listCache = Array.from(this.timelines.values());
135
+ }
136
+ return this.listCache;
137
+ }
138
+ subscribe(id, listener) {
139
+ if (!this.listeners.has(id)) {
140
+ this.listeners.set(id, /* @__PURE__ */ new Set());
141
+ }
142
+ this.listeners.get(id).add(listener);
143
+ return () => {
144
+ const listeners = this.listeners.get(id);
145
+ listeners?.delete(listener);
146
+ if (listeners?.size === 0 && !this.timelines.has(id)) {
147
+ this.listeners.delete(id);
148
+ }
149
+ };
150
+ }
151
+ subscribeGlobal(listener) {
152
+ this.globalListeners.add(listener);
153
+ return () => {
154
+ this.globalListeners.delete(listener);
155
+ };
156
+ }
157
+ applyMeta(meta, autoplay) {
158
+ const duration = Number.isFinite(meta.duration) ? Math.max(0, meta.duration) : 0;
159
+ const loopStart = Number.isFinite(meta.loopStart) ? Math.min(duration, Math.max(0, meta.loopStart)) : 0;
160
+ const safeMeta = { ...meta, duration, loopStart };
161
+ this.timelines.set(meta.id, safeMeta);
162
+ const existing = this.transports.get(meta.id);
163
+ if (existing) {
164
+ this.transports.set(meta.id, {
165
+ time: Math.min(existing.time, duration),
166
+ playing: duration > 0 && existing.playing,
167
+ duration,
168
+ wraps: existing.wraps
169
+ });
170
+ } else {
171
+ const playing = duration > 0 && autoplay;
172
+ this.transports.set(meta.id, { time: 0, playing, duration, wraps: 0 });
173
+ if (playing) this.ensureLoop();
174
+ }
175
+ this.listCache = null;
176
+ this.notify(meta.id);
177
+ this.notifyGlobal();
178
+ }
179
+ ensureLoop() {
180
+ if (this.rafId !== null || typeof window === "undefined") return;
181
+ this.lastTick = performance.now();
182
+ this.rafId = window.requestAnimationFrame(this.tick);
183
+ }
184
+ notify(id) {
185
+ this.listeners.get(id)?.forEach((fn) => fn());
186
+ }
187
+ notifyGlobal() {
188
+ this.globalListeners.forEach((fn) => fn());
189
+ }
190
+ };
191
+ var TimelineStore = /* @__PURE__ */ new TimelineStoreClass();
192
+
193
+ // src/timeline-core.ts
194
+ import { formatLabel, inferStep, isHexColor, resolveDialValues } from "dialkit/store";
195
+
196
+ // src/transition-math.ts
197
+ import { isEasingConfigValue, isSpringConfigValue } from "dialkit/store";
198
+ function round2(value) {
199
+ return Math.round(value * 100) / 100;
200
+ }
201
+ function clamp(value, min, max) {
202
+ return Math.min(max, Math.max(min, value));
203
+ }
204
+ function isTransitionConfig(value) {
205
+ return isSpringConfigValue(value) || isEasingConfigValue(value);
206
+ }
207
+ function isPhysicsSpring(transition) {
208
+ return transition.type === "spring" && (transition.stiffness !== void 0 || transition.damping !== void 0 || transition.mass !== void 0);
209
+ }
210
+ function springParams(spring) {
211
+ if (isPhysicsSpring(spring)) {
212
+ return { stiffness: spring.stiffness ?? 200, damping: spring.damping ?? 25, mass: spring.mass ?? 1 };
213
+ }
214
+ const visualDuration = Math.max(0.05, spring.visualDuration ?? 0.3);
215
+ const bounce = spring.bounce ?? 0.3;
216
+ const root = 2 * Math.PI / (visualDuration * 1.2);
217
+ const stiffness = root * root;
218
+ const damping = 2 * Math.min(1, Math.max(0.05, 1 - bounce)) * Math.sqrt(stiffness);
219
+ return { stiffness, damping, mass: 1 };
220
+ }
221
+ function springProgress(t, { stiffness, damping, mass }) {
222
+ if (t <= 0) return 0;
223
+ const w0 = Math.sqrt(stiffness / mass);
224
+ const zeta = damping / (2 * Math.sqrt(stiffness * mass));
225
+ if (zeta < 0.9999) {
226
+ const wd2 = w0 * Math.sqrt(1 - zeta * zeta);
227
+ return 1 - Math.exp(-zeta * w0 * t) * (Math.cos(wd2 * t) + zeta * w0 / wd2 * Math.sin(wd2 * t));
228
+ }
229
+ if (zeta < 1.0001) {
230
+ return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
231
+ }
232
+ const wd = w0 * Math.sqrt(zeta * zeta - 1);
233
+ const r1 = -zeta * w0 + wd;
234
+ const r2 = -zeta * w0 - wd;
235
+ return 1 + (r2 * Math.exp(r1 * t) - r1 * Math.exp(r2 * t)) / (r1 - r2);
236
+ }
237
+ function springSettleDuration(params) {
238
+ const w0 = Math.sqrt(params.stiffness / params.mass);
239
+ const zeta = params.damping / (2 * Math.sqrt(params.stiffness * params.mass));
240
+ const decay = zeta >= 1 ? zeta * w0 - w0 * Math.sqrt(Math.max(0, zeta * zeta - 1)) : zeta * w0;
241
+ const duration = Math.log(200) / Math.max(decay, 1e-6);
242
+ return round2(clamp(duration, 0.05, 10));
243
+ }
244
+ function cubicBezierProgress(p, [x1, y1, x2, y2]) {
245
+ if (p <= 0) return 0;
246
+ if (p >= 1) return 1;
247
+ const sampleX = (t2) => bezierAxis(t2, x1, x2);
248
+ const sampleY = (t2) => bezierAxis(t2, y1, y2);
249
+ let t = p;
250
+ for (let i = 0; i < 8; i++) {
251
+ const x = sampleX(t) - p;
252
+ if (Math.abs(x) < 1e-5) return sampleY(t);
253
+ const dx = bezierAxisDerivative(t, x1, x2);
254
+ if (Math.abs(dx) < 1e-6) break;
255
+ t -= x / dx;
256
+ }
257
+ let lo = 0;
258
+ let hi = 1;
259
+ t = p;
260
+ while (hi - lo > 1e-5) {
261
+ if (sampleX(t) < p) lo = t;
262
+ else hi = t;
263
+ t = (lo + hi) / 2;
264
+ }
265
+ return sampleY(t);
266
+ }
267
+ function bezierAxis(t, a1, a2) {
268
+ return (1 - 3 * a2 + 3 * a1) * t * t * t + (3 * a2 - 6 * a1) * t * t + 3 * a1 * t;
269
+ }
270
+ function bezierAxisDerivative(t, a1, a2) {
271
+ return 3 * (1 - 3 * a2 + 3 * a1) * t * t + 2 * (3 * a2 - 6 * a1) * t + 3 * a1;
272
+ }
273
+ function resolveClipTransition(raw, clipDuration) {
274
+ const safeDuration = Math.max(0.05, clipDuration);
275
+ if (raw.type === "easing") {
276
+ return {
277
+ transition: { ...raw, duration: safeDuration },
278
+ duration: safeDuration,
279
+ isPhysics: false
280
+ };
281
+ }
282
+ if (isPhysicsSpring(raw)) {
283
+ return {
284
+ transition: raw,
285
+ duration: springSettleDuration(springParams(raw)),
286
+ isPhysics: true
287
+ };
288
+ }
289
+ return {
290
+ transition: { type: "spring", bounce: raw.bounce ?? 0.2, visualDuration: safeDuration },
291
+ duration: safeDuration,
292
+ isPhysics: false
293
+ };
294
+ }
295
+
296
+ // src/timeline-core.ts
297
+ var CLIP_VALUE_STEP = 0.01;
298
+ var TIMELINE_MIN_CLIP_DURATION = 0.05;
299
+ var DEFAULT_STEP_DURATION = 0.3;
300
+ var DEFAULT_CLIP_TRANSITION = { type: "spring", bounce: 0.2 };
301
+ var RESERVED_KEYS = /* @__PURE__ */ new Set(["time", "playing", "duration", "play", "pause", "replay", "seek"]);
302
+ function isClipConfig(value) {
303
+ return isPlainObject(value) && Number.isFinite(value.at);
304
+ }
305
+ function isGroupConfig(value) {
306
+ if (!isPlainObject(value) || "at" in value) return false;
307
+ const entries = Object.values(value);
308
+ return entries.length > 0 && entries.some(isClipConfig);
309
+ }
310
+ function isPlainObject(value) {
311
+ return typeof value === "object" && value !== null && !Array.isArray(value);
312
+ }
313
+ function nonNegativeFinite(value, fallback = 0) {
314
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : fallback;
315
+ }
316
+ function animatedDuration(value, fallback = DEFAULT_STEP_DURATION) {
317
+ return Math.max(TIMELINE_MIN_CLIP_DURATION, nonNegativeFinite(value, fallback));
318
+ }
319
+ function transitionDefaultDuration(transition) {
320
+ if (transition.type === "easing") return animatedDuration(transition.duration);
321
+ if (isPhysicsSpring(transition)) {
322
+ return animatedDuration(springSettleDuration(springParams(transition)));
323
+ }
324
+ if (transition.visualDuration !== void 0) return animatedDuration(transition.visualDuration);
325
+ return animatedDuration(springSettleDuration(springParams(transition)));
326
+ }
327
+ function defaultStepDuration(step, inheritedTransition) {
328
+ const curve = step.transition ?? inheritedTransition;
329
+ if (curve && isPhysicsSpring(curve)) return transitionDefaultDuration(curve);
330
+ if (step.duration !== void 0) return animatedDuration(step.duration);
331
+ if (step.transition) return transitionDefaultDuration(step.transition);
332
+ return DEFAULT_STEP_DURATION;
333
+ }
334
+ function defaultTrackDuration(track, inheritedTransition) {
335
+ const curve = track.transition ?? inheritedTransition;
336
+ if (track.steps?.length) {
337
+ return track.steps.reduce((sum, step) => sum + defaultStepDuration(step, curve), 0);
338
+ }
339
+ if (curve && isPhysicsSpring(curve)) return transitionDefaultDuration(curve);
340
+ if (track.duration !== void 0) return animatedDuration(track.duration);
341
+ if (track.transition) return transitionDefaultDuration(track.transition);
342
+ return DEFAULT_STEP_DURATION;
343
+ }
344
+ function defaultClipDuration(clip) {
345
+ const defaultCurve = isTransitionConfig(clip.transition) ? clip.transition : DEFAULT_CLIP_TRANSITION;
346
+ if (clip.props) {
347
+ return Object.values(clip.props).reduce(
348
+ (max, track) => Math.max(
349
+ max,
350
+ nonNegativeFinite(track.delay) + defaultTrackDuration(track, defaultCurve)
351
+ ),
352
+ 0
353
+ );
354
+ }
355
+ if (clip.steps?.length) {
356
+ return clip.steps.reduce((sum, step) => sum + defaultStepDuration(step, defaultCurve), 0);
357
+ }
358
+ const animating = Boolean(clip.transition || clip.from || clip.to);
359
+ if (!animating) return nonNegativeFinite(clip.duration);
360
+ if (isPhysicsSpring(defaultCurve)) return transitionDefaultDuration(defaultCurve);
361
+ if (clip.duration !== void 0) return animatedDuration(clip.duration);
362
+ if (isTransitionConfig(clip.transition)) return transitionDefaultDuration(clip.transition);
363
+ return clip.from || clip.to ? transitionDefaultDuration(DEFAULT_CLIP_TRANSITION) : 0;
364
+ }
365
+ function normalizeLoopMode(value) {
366
+ if (value === true || value === "mirror" || value === "repeat") return "repeat";
367
+ return "off";
368
+ }
369
+ function normalizeStoredTransition(transition, clipDuration) {
370
+ if (transition.type === "easing") {
371
+ return { ...transition, duration: clipDuration };
372
+ }
373
+ if (isPhysicsSpring(transition)) {
374
+ return transition;
375
+ }
376
+ return { type: "spring", bounce: transition.bounce ?? 0.2 };
377
+ }
378
+ function collectClipEntries(config) {
379
+ const entries = [];
380
+ for (const [key, value] of Object.entries(config)) {
381
+ if (key === "duration") continue;
382
+ if (RESERVED_KEYS.has(key)) {
383
+ console.warn(`[dialkit] Timeline key "${key}" collides with a reserved key and was skipped.`);
384
+ continue;
385
+ }
386
+ if (isClipConfig(value)) {
387
+ entries.push({ path: key, childKey: key, clip: value });
388
+ } else if (isGroupConfig(value)) {
389
+ for (const [childKey, childClip] of Object.entries(value)) {
390
+ if (isClipConfig(childClip)) {
391
+ entries.push({ path: `${key}.${childKey}`, childKey, group: key, clip: childClip });
392
+ } else {
393
+ console.warn(
394
+ `[dialkit] Timeline clip "${key}.${childKey}" is missing a numeric "at" and was skipped.`
395
+ );
396
+ }
397
+ }
398
+ } else {
399
+ console.warn(
400
+ `[dialkit] Timeline entry "${key}" is neither a clip (needs a numeric "at") nor a group of clips and was skipped.`
401
+ );
402
+ }
403
+ }
404
+ return entries;
405
+ }
406
+ function definedValues(values) {
407
+ if (!values) return void 0;
408
+ const result = {};
409
+ for (const [key, value] of Object.entries(values)) {
410
+ if (value !== void 0) result[key] = value;
411
+ }
412
+ return result;
413
+ }
414
+ function setDialPath(dialConfig, path, value) {
415
+ const segments = path.split(".");
416
+ let node = dialConfig;
417
+ for (const segment of segments.slice(0, -1)) {
418
+ node = node[segment] ?? (node[segment] = {});
419
+ }
420
+ node[segments[segments.length - 1]] = value;
421
+ }
422
+ function parseTimelineConfig(config) {
423
+ const entries = collectClipEntries(config);
424
+ let maxEnd = 0;
425
+ for (const { clip } of entries) {
426
+ maxEnd = Math.max(maxEnd, nonNegativeFinite(clip.at) + defaultClipDuration(clip));
427
+ }
428
+ 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;
429
+ const dialConfig = {};
430
+ const clips = [];
431
+ entries.forEach(({ path, childKey, group, clip }, index) => {
432
+ const raw = clip;
433
+ if (raw.props && (raw.steps?.length || raw.from || raw.to)) {
434
+ console.warn(
435
+ `[dialkit] Timeline clip "${path}": "props" is mutually exclusive with from/to/steps \u2014 using "props".`
436
+ );
437
+ } else if (raw.steps?.length && raw.to) {
438
+ console.warn(
439
+ `[dialkit] Timeline clip "${path}": "to" is ignored when "steps" is present \u2014 each leg's "to" defines its targets.`
440
+ );
441
+ }
442
+ const hasSteps = Boolean(clip.steps?.length) && !clip.props;
443
+ const hasProps = Boolean(clip.props);
444
+ const single = isTransitionConfig(clip.transition) ? clip.transition : void 0;
445
+ const total = defaultClipDuration(clip);
446
+ const defaultCurve = single ?? DEFAULT_CLIP_TRANSITION;
447
+ const clipAt = nonNegativeFinite(clip.at);
448
+ const clipDial = {
449
+ at: [clipAt, 0, duration, CLIP_VALUE_STEP]
450
+ };
451
+ if (!hasSteps && !hasProps) {
452
+ clipDial.duration = [total, 0, duration, CLIP_VALUE_STEP];
453
+ }
454
+ if (!hasSteps && !hasProps && (clip.transition || clip.from || clip.to)) {
455
+ clipDial.transition = normalizeStoredTransition(defaultCurve, total);
456
+ }
457
+ let tracks;
458
+ if (clip.props) {
459
+ tracks = [];
460
+ for (const [prop, track] of Object.entries(clip.props)) {
461
+ if (TRACK_RESERVED.has(prop) || /^step\d+$/.test(prop)) {
462
+ console.warn(`[dialkit] Timeline property "${prop}" collides with a clip field and was skipped.`);
463
+ continue;
464
+ }
465
+ const trackDuration = defaultTrackDuration(track, defaultCurve);
466
+ const trackCurve = track.transition ?? defaultCurve;
467
+ const hasTrackSteps = Boolean(track.steps?.length);
468
+ const trackDial = {
469
+ delay: [nonNegativeFinite(track.delay), 0, duration, CLIP_VALUE_STEP]
470
+ };
471
+ if (!hasTrackSteps) {
472
+ trackDial.duration = [trackDuration, 0, duration, CLIP_VALUE_STEP];
473
+ trackDial.transition = normalizeStoredTransition(trackCurve, trackDuration);
474
+ }
475
+ const fromValue = track.from ?? (hasTrackSteps ? void 0 : track.to);
476
+ if (hasTrackSteps && fromValue === void 0) {
477
+ console.warn(
478
+ `[dialkit] Timeline clip "${path}": track "${prop}" has steps but no "from" \u2014 declare its starting value.`
479
+ );
480
+ }
481
+ if (fromValue !== void 0) {
482
+ trackDial.from = scalarDial(prop, fromValue, hasTrackSteps ? track.steps[0]?.to : track.to);
483
+ }
484
+ if (!hasTrackSteps && track.to !== void 0) {
485
+ trackDial.to = scalarDial(prop, track.to, fromValue);
486
+ }
487
+ let trackStepKeys;
488
+ if (hasTrackSteps) {
489
+ trackStepKeys = [];
490
+ let previous = fromValue;
491
+ track.steps.forEach((step, stepIndex) => {
492
+ const stepKey = `step${stepIndex + 1}`;
493
+ trackStepKeys.push(stepKey);
494
+ const stepDuration = defaultStepDuration(step, trackCurve);
495
+ const stepDial = {
496
+ duration: [stepDuration, 0, duration, CLIP_VALUE_STEP],
497
+ transition: normalizeStoredTransition(step.transition ?? trackCurve, stepDuration)
498
+ };
499
+ if (step.to !== void 0) {
500
+ stepDial.to = scalarDial(prop, step.to, previous);
501
+ previous = step.to;
502
+ }
503
+ trackDial[stepKey] = stepDial;
504
+ });
505
+ }
506
+ clipDial[prop] = trackDial;
507
+ tracks.push({ prop, stepKeys: trackStepKeys });
508
+ }
509
+ }
510
+ if (clip.from && !hasProps) {
511
+ clipDial.from = withFromToRanges(
512
+ clip.from,
513
+ hasSteps ? definedValues(clip.steps[0]?.to) : clip.to
514
+ );
515
+ }
516
+ if (!hasSteps && !hasProps && clip.to) {
517
+ clipDial.to = withFromToRanges(clip.to, clip.from);
518
+ }
519
+ let stepKeys;
520
+ if (hasSteps) {
521
+ stepKeys = [];
522
+ let running = clip.from;
523
+ clip.steps.forEach((step, stepIndex) => {
524
+ const stepKey = `step${stepIndex + 1}`;
525
+ stepKeys.push(stepKey);
526
+ const stepDuration = defaultStepDuration(step, defaultCurve);
527
+ const stepDial = {
528
+ duration: [stepDuration, 0, duration, CLIP_VALUE_STEP],
529
+ transition: normalizeStoredTransition(step.transition ?? defaultCurve, stepDuration)
530
+ };
531
+ const stepTo = definedValues(step.to);
532
+ if (stepTo) {
533
+ for (const prop of Object.keys(stepTo)) {
534
+ if (!running || !(prop in running)) {
535
+ console.warn(
536
+ `[dialkit] Timeline clip "${path}": property "${prop}" first animates in step ${stepIndex + 1} with no starting value \u2014 declare it in "from".`
537
+ );
538
+ }
539
+ }
540
+ stepDial.to = withFromToRanges(stepTo, running);
541
+ }
542
+ clipDial[stepKey] = stepDial;
543
+ running = { ...running ?? {}, ...stepTo ?? {} };
544
+ });
545
+ }
546
+ setDialPath(dialConfig, path, clipDial);
547
+ clips.push({
548
+ key: path,
549
+ label: formatLabel(childKey),
550
+ color: TIMELINE_CLIP_COLORS[index % TIMELINE_CLIP_COLORS.length],
551
+ loop: normalizeLoopMode(clip.loop),
552
+ group,
553
+ stepKeys,
554
+ tracks
555
+ });
556
+ });
557
+ return { duration, dialConfig, clips };
558
+ }
559
+ var TRACK_RESERVED = /* @__PURE__ */ new Set(["at", "duration", "loop", "from", "to", "transition", "delay"]);
560
+ function scalarDial(prop, value, counterpart) {
561
+ const record = withFromToRanges(
562
+ { [prop]: value },
563
+ counterpart === void 0 ? void 0 : { [prop]: counterpart }
564
+ );
565
+ return record[prop];
566
+ }
567
+ var FROM_TO_RANGE_PRESETS = [
568
+ [/^(x|y|z|tx|ty|offsetx|offsety|translatex|translatey)$/i, { min: -100, max: 100, step: 1 }],
569
+ [/rotat|angle|skew/i, { min: -180, max: 180, step: 1 }],
570
+ [/^scale/i, { min: 0, max: 2, step: 0.01 }],
571
+ [/opacity|alpha/i, { min: 0, max: 1, step: 0.01 }],
572
+ [/blur|radius|spread/i, { min: 0, max: 100, step: 1 }]
573
+ ];
574
+ function inferFromToRange(key, value, counterpart) {
575
+ const lo = Math.min(value, counterpart ?? value);
576
+ const hi = Math.max(value, counterpart ?? value);
577
+ const preset = FROM_TO_RANGE_PRESETS.find(([pattern]) => pattern.test(key))?.[1];
578
+ if (preset) {
579
+ return [value, Math.min(preset.min, lo), Math.max(preset.max, hi), preset.step];
580
+ }
581
+ if (lo >= 0 && hi <= 1) {
582
+ return [value, 0, 1, 0.01];
583
+ }
584
+ const extent = Math.max(Math.abs(lo), Math.abs(hi), 1);
585
+ const min = lo < 0 ? -extent * 2 : 0;
586
+ const max = Math.max(extent * 2, hi);
587
+ return [value, min, max, inferStep(min, max)];
588
+ }
589
+ function withFromToRanges(config, counterpart) {
590
+ const result = {};
591
+ for (const [key, value] of Object.entries(config)) {
592
+ const other = counterpart?.[key];
593
+ if (typeof value === "number") {
594
+ result[key] = inferFromToRange(key, value, typeof other === "number" ? other : void 0);
595
+ } else if (isPlainObject(value) && !("type" in value)) {
596
+ result[key] = withFromToRanges(
597
+ value,
598
+ isPlainObject(other) && !("type" in other) ? other : void 0
599
+ );
600
+ } else {
601
+ result[key] = value;
602
+ }
603
+ }
604
+ return result;
605
+ }
606
+ function curveStatic(transition, duration) {
607
+ if (!transition) return { duration };
608
+ if (transition.type === "easing") return { duration, ease: transition.ease };
609
+ const spring = springParams(transition);
610
+ return { duration, spring, settle: springSettleDuration(spring) };
611
+ }
612
+ function sampleCurve(curve, elapsed) {
613
+ if (elapsed <= 0) return 0;
614
+ if (curve.spring) {
615
+ if (curve.settle !== void 0 && elapsed >= curve.settle) return 1;
616
+ return springProgress(elapsed, curve.spring);
617
+ }
618
+ if (curve.ease) {
619
+ return cubicBezierProgress(clamp(curve.duration > 0 ? elapsed / curve.duration : 1, 0, 1), curve.ease);
620
+ }
621
+ return curve.duration > 0 ? Math.min(1, elapsed / curve.duration) : 1;
622
+ }
623
+ function resolvedAtPath(resolved, path) {
624
+ let node = resolved;
625
+ for (const segment of path.split(".")) {
626
+ node = isPlainObject(node) ? node[segment] : void 0;
627
+ }
628
+ return isPlainObject(node) ? node : {};
629
+ }
630
+ function computeStaticClips(parsed, flatValues) {
631
+ const resolved = resolveDialValues(parsed.dialConfig, flatValues);
632
+ return parsed.clips.map(
633
+ (clip) => buildClipStatic(resolvedAtPath(resolved, clip.key), clip, parsed.duration)
634
+ );
635
+ }
636
+ function computeStaticTimeline(parsed, flatValues) {
637
+ let clips = computeStaticClips(parsed, flatValues);
638
+ const maxEnd = clips.reduce(
639
+ (end, clip) => Math.max(end, clip.at + clip.duration),
640
+ parsed.duration
641
+ );
642
+ const duration = maxEnd > parsed.duration ? Math.ceil(maxEnd * 100 - 1e-4) / 100 : parsed.duration;
643
+ if (duration !== parsed.duration) {
644
+ clips = clips.map(
645
+ (clip) => clip.loop === "repeat" ? { ...clip, end: duration } : clip
646
+ );
647
+ }
648
+ return { duration, clips };
649
+ }
650
+ function computeClipStaticFromValues(values, clip, timelineDuration) {
651
+ return buildClipStatic(unflattenClipValues(values, clip.key), clip, timelineDuration);
652
+ }
653
+ function buildClipStatic(clipResolved, clip, timelineDuration) {
654
+ {
655
+ const at = typeof clipResolved.at === "number" ? clipResolved.at : 0;
656
+ const from = isPlainObject(clipResolved.from) ? clipResolved.from : void 0;
657
+ const single = isTransitionConfig(clipResolved.transition) ? clipResolved.transition : void 0;
658
+ const staticClip = {
659
+ key: clip.key,
660
+ childKey: clip.group ? clip.key.slice(clip.group.length + 1) : clip.key,
661
+ group: clip.group,
662
+ at,
663
+ duration: 0,
664
+ loop: "off",
665
+ end: 0,
666
+ isPhysics: false,
667
+ from,
668
+ tracks: [],
669
+ explicitSteps: Boolean(clip.stepKeys?.length)
670
+ };
671
+ if (clip.tracks?.length) {
672
+ const tracks = clip.tracks.map(({ prop, stepKeys }) => {
673
+ const trackResolved = isPlainObject(clipResolved[prop]) ? clipResolved[prop] : {};
674
+ const delay = typeof trackResolved.delay === "number" ? trackResolved.delay : 0;
675
+ const fromValue = trackResolved.from;
676
+ let steps;
677
+ let trackDuration = 0;
678
+ if (stepKeys?.length) {
679
+ let running = fromValue;
680
+ steps = stepKeys.map((stepKey) => {
681
+ const stepResolved = isPlainObject(trackResolved[stepKey]) ? trackResolved[stepKey] : {};
682
+ const storedDuration = typeof stepResolved.duration === "number" ? stepResolved.duration : 0;
683
+ const raw = isTransitionConfig(stepResolved.transition) ? stepResolved.transition : void 0;
684
+ const effective = raw ? resolveClipTransition(raw, storedDuration) : { transition: void 0, duration: storedDuration, isPhysics: false };
685
+ const toValue = stepResolved.to;
686
+ const step = {
687
+ key: stepKey,
688
+ offset: trackDuration,
689
+ duration: effective.duration,
690
+ isPhysics: effective.isPhysics,
691
+ start: running === void 0 ? {} : { [prop]: running },
692
+ to: toValue === void 0 ? {} : { [prop]: toValue },
693
+ curve: curveStatic(effective.transition, effective.duration)
694
+ };
695
+ if (toValue !== void 0) running = toValue;
696
+ trackDuration += effective.duration;
697
+ return step;
698
+ });
699
+ } else {
700
+ const storedDuration = typeof trackResolved.duration === "number" ? trackResolved.duration : 0;
701
+ const raw = isTransitionConfig(trackResolved.transition) ? trackResolved.transition : void 0;
702
+ const effective = raw ? resolveClipTransition(raw, storedDuration) : { transition: void 0, duration: storedDuration, isPhysics: false };
703
+ const toValue = trackResolved.to;
704
+ trackDuration = effective.duration;
705
+ steps = [
706
+ {
707
+ key: null,
708
+ offset: 0,
709
+ duration: effective.duration,
710
+ isPhysics: effective.isPhysics,
711
+ start: fromValue === void 0 ? {} : { [prop]: fromValue },
712
+ to: toValue === void 0 ? {} : { [prop]: toValue },
713
+ curve: curveStatic(effective.transition, effective.duration)
714
+ }
715
+ ];
716
+ }
717
+ return { prop, delay, duration: trackDuration, steps };
718
+ });
719
+ staticClip.tracks = tracks;
720
+ staticClip.props = tracks.map((track) => track.prop);
721
+ staticClip.duration = tracks.reduce((max, track) => Math.max(max, track.delay + track.duration), 0);
722
+ staticClip.from = Object.fromEntries(
723
+ tracks.map((track) => [track.prop, track.steps[0].start[track.prop]])
724
+ );
725
+ staticClip.to = Object.fromEntries(
726
+ tracks.map((track) => {
727
+ const last = track.steps[track.steps.length - 1];
728
+ return [track.prop, last.to[track.prop] ?? last.start[track.prop]];
729
+ })
730
+ );
731
+ staticClip.loop = staticClip.duration > 0 ? clip.loop : "off";
732
+ staticClip.end = staticClip.loop === "off" ? staticClip.at + staticClip.duration : timelineDuration;
733
+ return staticClip;
734
+ }
735
+ if (clip.stepKeys?.length) {
736
+ let running = { ...from ?? {} };
737
+ let offset = 0;
738
+ const steps = clip.stepKeys.map((stepKey) => {
739
+ const stepResolved = isPlainObject(clipResolved[stepKey]) ? clipResolved[stepKey] : {};
740
+ const storedDuration = typeof stepResolved.duration === "number" ? stepResolved.duration : 0;
741
+ const raw = isTransitionConfig(stepResolved.transition) ? stepResolved.transition : void 0;
742
+ const effective = raw ? resolveClipTransition(raw, storedDuration) : { transition: void 0, duration: storedDuration, isPhysics: false };
743
+ const to = isPlainObject(stepResolved.to) ? stepResolved.to : {};
744
+ const step = {
745
+ key: stepKey,
746
+ offset,
747
+ duration: effective.duration,
748
+ isPhysics: effective.isPhysics,
749
+ start: running,
750
+ to,
751
+ curve: curveStatic(effective.transition, effective.duration)
752
+ };
753
+ running = { ...running, ...to };
754
+ offset += effective.duration;
755
+ return step;
756
+ });
757
+ staticClip.tracks = [{ delay: 0, duration: offset, steps }];
758
+ staticClip.duration = offset;
759
+ staticClip.to = running;
760
+ } else {
761
+ const storedDuration = typeof clipResolved.duration === "number" ? clipResolved.duration : 0;
762
+ const to = isPlainObject(clipResolved.to) ? clipResolved.to : void 0;
763
+ if (single) {
764
+ const effective = resolveClipTransition(single, storedDuration);
765
+ staticClip.duration = effective.duration;
766
+ staticClip.isPhysics = effective.isPhysics;
767
+ staticClip.transition = effective.transition;
768
+ staticClip.css = transitionToCss(effective.transition);
769
+ staticClip.to = to;
770
+ if (from && to) {
771
+ staticClip.tracks = [
772
+ {
773
+ delay: 0,
774
+ duration: effective.duration,
775
+ steps: [
776
+ {
777
+ key: null,
778
+ offset: 0,
779
+ duration: effective.duration,
780
+ isPhysics: effective.isPhysics,
781
+ start: from,
782
+ to,
783
+ curve: curveStatic(effective.transition, effective.duration)
784
+ }
785
+ ]
786
+ }
787
+ ];
788
+ }
789
+ } else {
790
+ staticClip.duration = storedDuration;
791
+ staticClip.to = to;
792
+ if (from && to) {
793
+ const base = resolveClipTransition(DEFAULT_CLIP_TRANSITION, storedDuration);
794
+ staticClip.duration = base.duration;
795
+ staticClip.tracks = [
796
+ {
797
+ delay: 0,
798
+ duration: base.duration,
799
+ steps: [
800
+ {
801
+ key: null,
802
+ offset: 0,
803
+ duration: base.duration,
804
+ isPhysics: false,
805
+ start: from,
806
+ to,
807
+ curve: curveStatic(base.transition, base.duration)
808
+ }
809
+ ]
810
+ }
811
+ ];
812
+ }
813
+ }
814
+ }
815
+ if (staticClip.tracks.length) {
816
+ const props = new Set(Object.keys(from ?? {}));
817
+ for (const track of staticClip.tracks) {
818
+ for (const step of track.steps) {
819
+ for (const prop of Object.keys(step.to)) props.add(prop);
820
+ }
821
+ }
822
+ staticClip.props = Array.from(props);
823
+ }
824
+ staticClip.loop = staticClip.duration > 0 ? clip.loop : "off";
825
+ staticClip.end = staticClip.loop === "off" ? staticClip.at + staticClip.duration : timelineDuration;
826
+ return staticClip;
827
+ }
828
+ }
829
+ function stepAtPosition(steps, pos) {
830
+ for (const step of steps) {
831
+ if (pos < step.offset + step.duration) return step;
832
+ }
833
+ return steps[steps.length - 1];
834
+ }
835
+ function evalPropAtPos(steps, prop, pos) {
836
+ const step = stepAtPosition(steps, pos);
837
+ const within = Math.max(0, pos - step.offset);
838
+ if (prop in step.to) {
839
+ const eased = sampleCurve(step.curve, within);
840
+ return interpolateResolved(step.start[prop], step.to[prop], eased);
841
+ }
842
+ return step.start[prop];
843
+ }
844
+ function computeClipState(clip, time, cycleTime = time) {
845
+ const total = clip.duration;
846
+ const looping = clip.loop === "repeat" && total > 0;
847
+ const started = time >= clip.at || looping && cycleTime > time;
848
+ const done = time >= clip.end;
849
+ const elapsed = time - clip.at;
850
+ const phaseElapsed = looping ? cycleTime - clip.at : elapsed;
851
+ const fold = (e) => looping ? e % total : e;
852
+ const basePos = started ? fold(Math.max(0, phaseElapsed)) : 0;
853
+ const progress = total > 0 ? clamp(basePos / total, 0, 1) : started ? 1 : 0;
854
+ let current;
855
+ let stepIndex = 0;
856
+ if (clip.tracks.length && clip.props?.length) {
857
+ current = {};
858
+ for (const track of clip.tracks) {
859
+ const props = track.prop !== void 0 ? [track.prop] : clip.props;
860
+ for (const prop of props) {
861
+ const startValue = track.steps[0]?.start[prop];
862
+ if (!started) {
863
+ if (startValue !== void 0) current[prop] = startValue;
864
+ continue;
865
+ }
866
+ const phase = phaseElapsed - track.delay;
867
+ if (phase <= 0) {
868
+ if (startValue !== void 0) current[prop] = startValue;
869
+ continue;
870
+ }
871
+ const pos = looping && track.duration > 0 ? phase % track.duration : phase;
872
+ const value = evalPropAtPos(track.steps, prop, pos);
873
+ if (value !== void 0) current[prop] = value;
874
+ }
875
+ }
876
+ const shared = clip.tracks[0];
877
+ if (started && clip.explicitSteps && shared.prop === void 0) {
878
+ stepIndex = shared.steps.indexOf(stepAtPosition(shared.steps, basePos));
879
+ }
880
+ }
881
+ return {
882
+ at: clip.at,
883
+ duration: clip.duration,
884
+ loop: clip.loop,
885
+ started,
886
+ active: started && !done,
887
+ done,
888
+ progress,
889
+ step: clip.explicitSteps ? stepIndex : void 0,
890
+ from: clip.from,
891
+ to: clip.to,
892
+ animate: started ? clip.to : clip.from,
893
+ transition: clip.transition,
894
+ css: clip.css,
895
+ current
896
+ };
897
+ }
898
+ function interpolateResolved(from, to, p) {
899
+ if (typeof from === "number" && typeof to === "number") {
900
+ return from + (to - from) * p;
901
+ }
902
+ if (typeof from === "string" && typeof to === "string") {
903
+ const mixed = mixHexColors(from, to, p);
904
+ if (mixed) return mixed;
905
+ }
906
+ if (isPlainObject(from) && isPlainObject(to)) {
907
+ const result = {};
908
+ for (const key of Object.keys(from)) {
909
+ result[key] = key in to ? interpolateResolved(from[key], to[key], p) : from[key];
910
+ }
911
+ for (const key of Object.keys(to)) {
912
+ if (!(key in from)) result[key] = to[key];
913
+ }
914
+ return result;
915
+ }
916
+ return p < 0.5 ? from : to;
917
+ }
918
+ function parseHex(hex) {
919
+ if (!isHexColor(hex)) return null;
920
+ let h = hex.slice(1);
921
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
922
+ return [
923
+ parseInt(h.slice(0, 2), 16),
924
+ parseInt(h.slice(2, 4), 16),
925
+ parseInt(h.slice(4, 6), 16),
926
+ h.length === 8 ? parseInt(h.slice(6, 8), 16) : 255
927
+ ];
928
+ }
929
+ function mixHexColors(a, b, p) {
930
+ const ca = parseHex(a);
931
+ const cb = parseHex(b);
932
+ if (!ca || !cb) return null;
933
+ const t = clamp(p, 0, 1);
934
+ const mixed = ca.map((v, i) => Math.round(v + (cb[i] - v) * t));
935
+ const hex = (n) => n.toString(16).padStart(2, "0");
936
+ const rgb = `#${hex(mixed[0])}${hex(mixed[1])}${hex(mixed[2])}`;
937
+ return mixed[3] === 255 ? rgb : `${rgb}${hex(mixed[3])}`;
938
+ }
939
+ function transitionToCss(transition) {
940
+ if (!transition) return void 0;
941
+ if (transition.type === "easing") {
942
+ return {
943
+ transitionDuration: `${round2(transition.duration)}s`,
944
+ transitionTimingFunction: `cubic-bezier(${transition.ease.map((v) => round2(v)).join(", ")})`
945
+ };
946
+ }
947
+ const params = springParams(transition);
948
+ const dampingRatio = params.damping / (2 * Math.sqrt(params.stiffness * params.mass));
949
+ const duration = transition.visualDuration ?? springSettleDuration(params);
950
+ const bounce = transition.bounce ?? Math.max(0, round2(1 - dampingRatio));
951
+ return {
952
+ transitionDuration: `${round2(duration)}s`,
953
+ transitionTimingFunction: bounce > 0.05 ? `cubic-bezier(0.34, ${round2(1.2 + bounce)}, 0.64, 1)` : "cubic-bezier(0.25, 0.6, 0.35, 1)"
954
+ };
955
+ }
956
+ function timelinePopoverDisplayValues(values, clipKey, stepKeys, stepKey) {
957
+ const display = { ...values };
958
+ const swap = (path, duration) => {
959
+ const raw = display[path];
960
+ if (isTransitionConfig(raw)) display[path] = resolveClipTransition(raw, duration).transition;
961
+ };
962
+ if (stepKey) {
963
+ swap(`${clipKey}.${stepKey}.transition`, numberValue(values[`${clipKey}.${stepKey}.duration`]));
964
+ return display;
965
+ }
966
+ const cycle = stepKeys?.length ? stepKeys.reduce((sum, sk) => sum + numberValue(values[`${clipKey}.${sk}.duration`]), 0) : numberValue(values[`${clipKey}.duration`]);
967
+ swap(`${clipKey}.transition`, cycle);
968
+ return display;
969
+ }
970
+ function numberValue(value) {
971
+ return typeof value === "number" ? value : 0;
972
+ }
973
+ function unflattenClipValues(values, clipKey) {
974
+ const prefix = `${clipKey}.`;
975
+ const result = {};
976
+ 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);
977
+ for (const { segments, value } of entries) {
978
+ let node = result;
979
+ for (let i = 0; i < segments.length - 1; i++) {
980
+ const existing = node[segments[i]];
981
+ node = isPlainObject(existing) ? existing : node[segments[i]] = {};
982
+ }
983
+ node[segments[segments.length - 1]] = cloneTimelineValue(value);
984
+ }
985
+ return result;
986
+ }
987
+ function cloneTimelineValue(value) {
988
+ if (Array.isArray(value)) return value.map(cloneTimelineValue);
989
+ if (!isPlainObject(value)) return value;
990
+ return Object.fromEntries(
991
+ Object.entries(value).map(([key, nested]) => [key, cloneTimelineValue(nested)])
992
+ );
993
+ }
994
+ function clampTrackDelay(delay, at, trackDuration, timelineDuration) {
995
+ return clamp(round2(delay), 0, Math.max(0, round2(timelineDuration - at - trackDuration)));
996
+ }
997
+ function clampClipMove(at, duration, timelineDuration) {
998
+ return clamp(round2(at), 0, Math.max(0, timelineDuration - duration));
999
+ }
1000
+ function clampClipResizeEnd(duration, at, timelineDuration) {
1001
+ return clamp(round2(duration), TIMELINE_MIN_CLIP_DURATION, timelineDuration - at);
1002
+ }
1003
+ function clampClipResizeStart(newAt, at, duration) {
1004
+ const clampedAt = clamp(round2(newAt), 0, at + duration - TIMELINE_MIN_CLIP_DURATION);
1005
+ return { at: clampedAt, duration: round2(at + duration - clampedAt) };
1006
+ }
1007
+ function clampStepResize(duration, at, otherStepsTotal, timelineDuration) {
1008
+ const max = Math.max(TIMELINE_MIN_CLIP_DURATION, timelineDuration - at - otherStepsTotal);
1009
+ return clamp(round2(duration), TIMELINE_MIN_CLIP_DURATION, max);
1010
+ }
1011
+ function normalizeTimelineValuesForCopy(values, clips) {
1012
+ const normalized = { ...values };
1013
+ for (const path of Object.keys(normalized)) {
1014
+ if (path.endsWith(".__mode")) delete normalized[path];
1015
+ }
1016
+ const normalizeTransitionAt = (transitionPath, durationPath) => {
1017
+ const raw = normalized[transitionPath];
1018
+ if (!isTransitionConfig(raw)) return;
1019
+ if (isPhysicsSpring(raw)) {
1020
+ normalized[durationPath] = transitionDefaultDuration(raw);
1021
+ }
1022
+ normalized[transitionPath] = normalizeStoredTransition(
1023
+ raw,
1024
+ numberValue(normalized[durationPath])
1025
+ );
1026
+ };
1027
+ for (const clip of clips) {
1028
+ for (const stepKey of clip.stepKeys ?? []) {
1029
+ normalizeTransitionAt(
1030
+ `${clip.key}.${stepKey}.transition`,
1031
+ `${clip.key}.${stepKey}.duration`
1032
+ );
1033
+ }
1034
+ normalizeTransitionAt(`${clip.key}.transition`, `${clip.key}.duration`);
1035
+ for (const track of clip.tracks ?? []) {
1036
+ const trackKey = `${clip.key}.${track.prop}`;
1037
+ for (const stepKey of track.stepKeys ?? []) {
1038
+ normalizeTransitionAt(
1039
+ `${trackKey}.${stepKey}.transition`,
1040
+ `${trackKey}.${stepKey}.duration`
1041
+ );
1042
+ }
1043
+ normalizeTransitionAt(`${trackKey}.transition`, `${trackKey}.duration`);
1044
+ if (normalized[`${trackKey}.delay`] === 0) {
1045
+ delete normalized[`${trackKey}.delay`];
1046
+ }
1047
+ }
1048
+ delete normalized[`${clip.key}.loop`];
1049
+ }
1050
+ return normalized;
1051
+ }
1052
+ function formatClock(time, tenths = false) {
1053
+ const safe = Math.max(0, time);
1054
+ const minutes = Math.floor(safe / 60);
1055
+ const seconds = safe - minutes * 60;
1056
+ const secondsText = tenths ? seconds.toFixed(1).padStart(4, "0") : String(Math.floor(seconds)).padStart(2, "0");
1057
+ return `${String(minutes).padStart(2, "0")}:${secondsText}`;
1058
+ }
1059
+ function formatSeconds(value) {
1060
+ return `${round2(value)}s`;
1061
+ }
1062
+ function formatStepLabel(stepKey) {
1063
+ const match = /^step(\d+)$/.exec(stepKey);
1064
+ return match ? `Step ${match[1]}` : formatLabel(stepKey);
1065
+ }
1066
+
1067
+ // src/timeline/adapter.ts
1068
+ function resolveTimelineLoop(loop) {
1069
+ if (typeof loop === "object" && loop !== null) {
1070
+ return {
1071
+ enabled: true,
1072
+ start: Number.isFinite(loop.from) ? Math.max(0, loop.from) : 0
1073
+ };
1074
+ }
1075
+ return { enabled: Boolean(loop), start: 0 };
1076
+ }
1077
+ function buildTimelineMeta(id, name, duration, parsed, loop) {
1078
+ const resolvedLoop = resolveTimelineLoop(loop);
1079
+ return {
1080
+ id,
1081
+ name,
1082
+ duration,
1083
+ loop: resolvedLoop.enabled,
1084
+ loopStart: resolvedLoop.start,
1085
+ clips: parsed.clips
1086
+ };
1087
+ }
1088
+ function buildTimelineValues(staticClips, transport, timelineDuration, loopStart, actions) {
1089
+ var _a;
1090
+ const result = {
1091
+ time: transport.time,
1092
+ playing: transport.playing,
1093
+ duration: timelineDuration,
1094
+ ...actions
1095
+ };
1096
+ const span = loopSpan(transport.duration, loopStart);
1097
+ const cycleTime = (span > 0 ? transport.wraps * span : 0) + transport.time;
1098
+ for (const clip of staticClips) {
1099
+ const state = computeClipState(clip, transport.time, cycleTime);
1100
+ if (clip.group) {
1101
+ const bucket = result[_a = clip.group] ?? (result[_a] = {});
1102
+ bucket[clip.childKey] = state;
1103
+ } else {
1104
+ result[clip.key] = state;
1105
+ }
1106
+ }
1107
+ return result;
1108
+ }
1109
+
1110
+ // src/store/TimelineUiStore.ts
1111
+ var TimelineUiStoreClass = class {
1112
+ constructor() {
1113
+ this.visible = true;
1114
+ this.initialized = false;
1115
+ this.controllers = /* @__PURE__ */ new Map();
1116
+ this.listeners = /* @__PURE__ */ new Set();
1117
+ }
1118
+ getVisible() {
1119
+ for (const controller of this.controllers.values()) {
1120
+ if (controller.visible !== void 0) return controller.visible;
1121
+ }
1122
+ return this.visible;
1123
+ }
1124
+ registerController(id, controller) {
1125
+ const previous = this.getVisible();
1126
+ if (!this.initialized) {
1127
+ this.visible = controller.defaultVisible;
1128
+ this.initialized = true;
1129
+ }
1130
+ this.controllers.set(id, controller);
1131
+ if (previous !== this.getVisible()) this.notify();
1132
+ return () => {
1133
+ const before = this.getVisible();
1134
+ this.controllers.delete(id);
1135
+ if (this.controllers.size === 0) this.initialized = false;
1136
+ if (before !== this.getVisible()) this.notify();
1137
+ };
1138
+ }
1139
+ updateController(id, controller) {
1140
+ if (!this.controllers.has(id)) return;
1141
+ const previous = this.getVisible();
1142
+ this.controllers.set(id, controller);
1143
+ if (previous !== this.getVisible()) this.notify();
1144
+ }
1145
+ requestVisible(visible) {
1146
+ const current = this.getVisible();
1147
+ if (current === visible) return;
1148
+ const controlled = Array.from(this.controllers.values()).filter(
1149
+ (controller) => controller.visible !== void 0
1150
+ );
1151
+ if (controlled.length > 0) {
1152
+ controlled.forEach((controller) => controller.onVisibilityChange?.(visible));
1153
+ return;
1154
+ }
1155
+ this.visible = visible;
1156
+ this.controllers.forEach((controller) => controller.onVisibilityChange?.(visible));
1157
+ this.notify();
1158
+ }
1159
+ toggle() {
1160
+ this.requestVisible(!this.getVisible());
1161
+ }
1162
+ subscribe(listener) {
1163
+ this.listeners.add(listener);
1164
+ return () => this.listeners.delete(listener);
1165
+ }
1166
+ notify() {
1167
+ this.listeners.forEach((listener) => listener());
1168
+ }
1169
+ };
1170
+ var TimelineUiStore = /* @__PURE__ */ new TimelineUiStoreClass();
1171
+
1172
+ // src/copy-instruction.ts
1173
+ function buildCopyInstruction(hookName, panelName, values) {
1174
+ const jsonStr = JSON.stringify(values, null, 2);
1175
+ if (hookName === "useDialTimeline" || hookName === "createDialTimeline") {
1176
+ return `Update the ${hookName} configuration for "${panelName}" with these values:
1177
+
1178
+ \`\`\`json
1179
+ ${jsonStr}
1180
+ \`\`\`
1181
+
1182
+ 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.
1183
+
1184
+ Add this comment immediately above the ${hookName} call as a production handoff note:
1185
+
1186
+ \`\`\`tsx
1187
+ // TODO(production): DialKit's clip.current values are the scrubbable authoring preview.
1188
+ // Replace them with equivalent real Motion animations using the tuned timeline
1189
+ // timings and transitions, then remove ${hookName} and <DialTimeline />.
1190
+ \`\`\``;
1191
+ }
1192
+ return `Update the ${hookName} configuration for "${panelName}" with these values:
1193
+
1194
+ \`\`\`json
1195
+ ${jsonStr}
1196
+ \`\`\`
1197
+
1198
+ Apply these values as the new defaults in the ${hookName} call.`;
1199
+ }
1200
+
1201
+ // src/env.ts
1202
+ 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;
1203
+ export {
1204
+ TIMELINE_CLIP_COLORS,
1205
+ TIMELINE_MIN_CLIP_DURATION,
1206
+ TimelineStore,
1207
+ TimelineUiStore,
1208
+ buildCopyInstruction,
1209
+ buildTimelineMeta,
1210
+ buildTimelineValues,
1211
+ clamp,
1212
+ clampClipMove,
1213
+ clampClipResizeEnd,
1214
+ clampClipResizeStart,
1215
+ clampStepResize,
1216
+ clampTrackDelay,
1217
+ computeClipState,
1218
+ computeClipStaticFromValues,
1219
+ computeStaticClips,
1220
+ computeStaticTimeline,
1221
+ foldLoopTime,
1222
+ formatClock,
1223
+ formatSeconds,
1224
+ formatStepLabel,
1225
+ isDevDefault,
1226
+ loopSpan,
1227
+ normalizeTimelineValuesForCopy,
1228
+ parseTimelineConfig,
1229
+ resolveTimelineLoop,
1230
+ timelinePopoverDisplayValues,
1231
+ transitionToCss
1232
+ };
1233
+ //# sourceMappingURL=index.js.map