@tremolo-ui/react 0.7.0 → 0.9.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 (55) hide show
  1. package/dist/index.cjs +666 -1171
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +316 -315
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +316 -315
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +643 -1123
  8. package/dist/index.js.map +1 -1
  9. package/package.json +9 -9
  10. package/src/components/AnimationCanvas/index.tsx +4 -18
  11. package/src/components/DropZone/index.tsx +2 -1
  12. package/src/components/FileInput/index.tsx +11 -13
  13. package/src/components/Knob/ActiveLine.tsx +5 -3
  14. package/src/components/Knob/InactiveLine.tsx +6 -4
  15. package/src/components/Knob/SVGRoot.tsx +3 -3
  16. package/src/components/Knob/Thumb.tsx +21 -20
  17. package/src/components/Knob/context.tsx +3 -74
  18. package/src/components/Knob/index.tsx +99 -52
  19. package/src/components/NumberInput/InputField.tsx +20 -33
  20. package/src/components/NumberInput/Stepper.tsx +76 -100
  21. package/src/components/NumberInput/StepperButton.tsx +15 -8
  22. package/src/components/NumberInput/context.tsx +26 -3
  23. package/src/components/NumberInput/index.tsx +175 -91
  24. package/src/components/Piano/index.tsx +54 -144
  25. package/src/components/PointsEditor/Container.tsx +15 -26
  26. package/src/components/PointsEditor/Point.tsx +94 -110
  27. package/src/components/PointsEditor/context.tsx +8 -60
  28. package/src/components/PointsEditor/index.tsx +41 -324
  29. package/src/components/Slider/Marks.tsx +5 -6
  30. package/src/components/Slider/MarksOption.tsx +9 -19
  31. package/src/components/Slider/Thumb.tsx +20 -11
  32. package/src/components/Slider/Track.tsx +16 -13
  33. package/src/components/Slider/context.tsx +3 -3
  34. package/src/components/Slider/index.tsx +110 -72
  35. package/src/components/XYPad/Area.tsx +27 -49
  36. package/src/components/XYPad/Thumb.tsx +20 -8
  37. package/src/components/XYPad/context.tsx +2 -4
  38. package/src/components/XYPad/index.tsx +102 -83
  39. package/src/components/_util/VisuallyHiddenRangeInput.tsx +1 -1
  40. package/src/hooks/_internal/useChangeGesture.ts +93 -0
  41. package/src/hooks/_internal/useCheckSteps.ts +9 -146
  42. package/src/hooks/useAnimationFrame.ts +37 -15
  43. package/src/hooks/useEventListener.ts +93 -22
  44. package/src/hooks/useLongPress.ts +35 -51
  45. package/src/hooks/useMIDIAccess.ts +0 -8
  46. package/src/hooks/useMIDIInput.ts +0 -3
  47. package/src/hooks/useWheel.ts +4 -4
  48. package/src/index.ts +11 -32
  49. package/src/components/Piano/keyboard-shortcuts.ts +0 -61
  50. package/src/components/Slider/type.ts +0 -46
  51. package/src/components/_util/css-length.ts +0 -13
  52. package/src/components/_util/decimal-digits.ts +0 -23
  53. package/src/components/_util/visually-hidden.ts +0 -25
  54. package/src/hooks/useInterval.ts +0 -27
  55. package/src/input-event.ts +0 -32
package/dist/index.cjs CHANGED
@@ -26,6 +26,7 @@ let react = require("react");
26
26
  react = __toESM(react, 1);
27
27
  let _tremolo_ui_dom = require("@tremolo-ui/dom");
28
28
  let react_jsx_runtime = require("react/jsx-runtime");
29
+ let _tremolo_ui_dom_internal = require("@tremolo-ui/dom/internal");
29
30
  let _tremolo_ui_functions = require("@tremolo-ui/functions");
30
31
  //#region src/components/AnimationCanvas/index.tsx
31
32
  /**
@@ -192,31 +193,6 @@ const DropZone = { Root: /* @__PURE__ */ (0, react.forwardRef)(({ accept, multip
192
193
  });
193
194
  }) };
194
195
  //#endregion
195
- //#region src/components/_util/visually-hidden.ts
196
- /**
197
- * Take an element out of sight while leaving it in the accessibility tree and
198
- * in the tab order.
199
- *
200
- * `display: none` and `visibility: hidden` would remove it from both, and the
201
- * native control underneath a headless component is what carries the ARIA and
202
- * the keyboard behaviour. `pointer-events: none` is safe because nothing is
203
- * ever clicked here directly: a `<label>` forwards its click, and a drag is
204
- * handled by the part that is visible.
205
- */
206
- const visuallyHiddenStyle = {
207
- position: "absolute",
208
- width: 1,
209
- height: 1,
210
- padding: 0,
211
- margin: -1,
212
- overflow: "hidden",
213
- clip: "rect(0, 0, 0, 0)",
214
- clipPath: "inset(50%)",
215
- whiteSpace: "nowrap",
216
- border: 0,
217
- pointerEvents: "none"
218
- };
219
- //#endregion
220
196
  //#region src/components/FileInput/context.tsx
221
197
  const FileInputContext = /* @__PURE__ */ (0, react.createContext)(null);
222
198
  const FileInputProvider = FileInputContext.Provider;
@@ -256,14 +232,10 @@ const FileInput = {
256
232
  disabled
257
233
  }), [inputId, disabled]);
258
234
  function handleChange(event) {
259
- const picked = Array.from(event.target.files ?? []);
235
+ const { accepted, rejected } = (0, _tremolo_ui_dom_internal.partitionByAccept)(Array.from(event.target.files ?? []), accept);
260
236
  event.target.value = "";
261
- const accepted = [];
262
- const rejected = [];
263
- for (const file of picked) if ((0, _tremolo_ui_dom.matchesAccept)(file, accept)) accepted.push(file);
264
- else rejected.push(file);
265
- if (rejected.length > 0) onReject?.(rejected);
266
237
  if (accepted.length > 0) onChange?.(accepted);
238
+ if (rejected.length > 0) onReject?.(rejected);
267
239
  }
268
240
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileInputProvider, {
269
241
  value: context,
@@ -277,7 +249,7 @@ const FileInput = {
277
249
  accept,
278
250
  multiple,
279
251
  disabled,
280
- style: visuallyHiddenStyle,
252
+ style: _tremolo_ui_dom_internal.visuallyHiddenStyle,
281
253
  "aria-label": ariaLabel,
282
254
  "aria-labelledby": ariaLabelledby,
283
255
  "aria-describedby": ariaDescribedby,
@@ -289,114 +261,75 @@ const FileInput = {
289
261
  Trigger
290
262
  };
291
263
  //#endregion
292
- //#region src/hooks/_internal/useCheckSteps.ts
293
- /** Positions probed across the travel. The ends are left out so that the
294
- * clamp at `min` and `max` cannot be mistaken for a press that does nothing. */
295
- const PROBES = [
296
- .1,
297
- .2,
298
- .3,
299
- .4,
300
- .5,
301
- .6,
302
- .7,
303
- .8,
304
- .9
305
- ];
306
- const NONE = {
307
- shiftKey: false,
308
- altKey: false,
309
- ctrlKey: false,
310
- metaKey: false
311
- };
312
- const MODIFIER_STATE = {
313
- shift: {
314
- ...NONE,
315
- shiftKey: true
316
- },
317
- alt: {
318
- ...NONE,
319
- altKey: true
320
- },
321
- ctrl: {
322
- ...NONE,
323
- ctrlKey: true
324
- },
325
- meta: {
326
- ...NONE,
327
- metaKey: true
328
- }
329
- };
330
- /** Each entry of a modifier-aware input option, as a name and held keys. */
331
- function entries(options) {
332
- if (Array.isArray(options)) return [{
333
- name: "",
334
- modifiers: NONE
335
- }];
336
- return Object.keys(options).map((key) => ({
337
- name: key === "default" ? "" : key,
338
- modifiers: MODIFIER_STATE[key] ?? NONE
339
- }));
340
- }
264
+ //#region src/hooks/_internal/useChangeGesture.ts
341
265
  /**
342
- * Press every entry of `options` at nine points along the travel and report
343
- * whether anything came of it.
266
+ * The core's change gesture, reporting the control's value to
267
+ * `onChangeStart` / `onChangeEnd`. The start gets the value before the change,
268
+ * the end the last one reported.
344
269
  *
345
- * Run against `applyDelta` itself rather than against a reading of `step`:
346
- * the whole point is that the amount, the step and the scale interact, and
347
- * the pipeline is the only thing that knows how.
270
+ * Internal
271
+ * @private
348
272
  */
349
- function probe(options, range, format) {
350
- const { min, max, scale = _tremolo_ui_functions.linearScale } = range;
351
- const outcome = {
352
- moved: false,
353
- visible: false
354
- };
355
- for (const { modifiers } of entries(options)) for (const position of PROBES) {
356
- const value = scale.denormalize(position, min, max);
357
- const up = (0, _tremolo_ui_dom.applyDelta)(value, 1, options, range, modifiers);
358
- const down = (0, _tremolo_ui_dom.applyDelta)(value, -1, options, range, modifiers);
359
- if (up !== value || down !== value) outcome.moved = true;
360
- if (!format) continue;
361
- const shown = format(value);
362
- if (format(up) !== shown || format(down) !== shown) outcome.visible = true;
363
- }
364
- return outcome;
273
+ function useChangeGesture(value, { onChangeStart, onChangeEnd, changeEndDelay }, inactive) {
274
+ const [instance] = (0, react.useState)(() => (0, _tremolo_ui_dom.createChangeGesture)());
275
+ const latest = (0, react.useRef)({
276
+ value,
277
+ onChangeStart,
278
+ onChangeEnd
279
+ });
280
+ const lastValue = (0, react.useRef)(value);
281
+ (0, react.useInsertionEffect)(() => {
282
+ latest.current = {
283
+ value,
284
+ onChangeStart,
285
+ onChangeEnd
286
+ };
287
+ });
288
+ (0, react.useEffect)(() => {
289
+ instance.update({
290
+ endDelay: changeEndDelay,
291
+ onStart: (source) => {
292
+ lastValue.current = latest.current.value;
293
+ latest.current.onChangeStart?.(latest.current.value, source);
294
+ },
295
+ onEnd: (source) => latest.current.onChangeEnd?.(lastValue.current, source)
296
+ });
297
+ }, [instance, changeEndDelay]);
298
+ (0, react.useEffect)(() => {
299
+ if (inactive) instance.end();
300
+ }, [instance, inactive]);
301
+ (0, react.useEffect)(() => () => instance.end(), [instance]);
302
+ const changed = (0, react.useCallback)((next) => {
303
+ lastValue.current = next;
304
+ }, []);
305
+ return (0, react.useMemo)(() => ({
306
+ hold: instance.hold,
307
+ pulse: instance.pulse,
308
+ instant: instance.instant,
309
+ end: instance.end,
310
+ active: instance.active,
311
+ changed
312
+ }), [instance, changed]);
365
313
  }
314
+ //#endregion
315
+ //#region src/hooks/_internal/useCheckSteps.ts
366
316
  /**
367
317
  * Warn, in development only, when a key press or a wheel notch cannot produce
368
- * a change the user can see.
369
- *
370
- * Two settings that are each fine on their own can cancel out, and nothing
371
- * fails when they do — the control simply sits there:
372
- *
373
- * - **`step` coarser than the amount.** `keyboard={['raw', 0.1]}` with
374
- * `step={1}` rounds every press straight back to where it started
375
- * - **the display coarser than the amount.** A `format` showing two decimals
376
- * of a kHz value cannot show a press worth 1 Hz
377
- *
378
- * The second is only reported when the press is invisible at *every* point
379
- * along the travel. A display that rounds is a deliberate choice and is
380
- * normally right — it is being too coarse everywhere that makes it a mistake.
318
+ * a change the user can see. The probing is `checkSteps` in the core.
381
319
  *
382
320
  * @internal
383
321
  */
384
- function useCheckSteps({ component, axis, range, keyboard, wheel, format }) {
322
+ function useCheckSteps(options) {
323
+ const { component, axis, range, keyboard, wheel } = options;
385
324
  const { min, max, step, scale } = range ?? {};
386
- const where = axis ? `${component} (${axis})` : component;
387
325
  (0, react.useEffect)(() => {
388
326
  try {
389
327
  if (process.env.NODE_ENV === "production") return;
390
- if (!range || min === void 0 || max === void 0 || !(min < max)) return;
391
- for (const [name, options] of [["keyboard", keyboard], ["wheel", wheel]]) {
392
- if (!options) continue;
393
- const { moved, visible } = probe(options, range, format);
394
- if (!moved) console.warn(`[tremolo-ui] ${where}: \`${name}\` cannot move the value.` + (step !== void 0 ? ` Each press is smaller than \`step\` (${step}), so it rounds` : " Each press rounds") + " straight back to where it started.");
395
- else if (format && !visible) console.warn(`[tremolo-ui] ${where}: \`${name}\` moves the value, but \`format\` shows the same text before and after, everywhere in the range. The display is too coarse for it to be seen.`);
396
- }
328
+ for (const warning of (0, _tremolo_ui_dom_internal.checkSteps)(options)) console.warn(warning);
397
329
  } catch {}
398
330
  }, [
399
- where,
331
+ component,
332
+ axis,
400
333
  min,
401
334
  max,
402
335
  step,
@@ -493,7 +426,10 @@ function useWheel(onWheel, { requireFocus = false, target } = {}) {
493
426
  (0, react.useEffect)(() => {
494
427
  const element = target ? target.current : node;
495
428
  if (!element) return;
496
- const instance = (0, _tremolo_ui_dom.createWheel)(element, (event) => wheelHandler(event), { requireFocus });
429
+ const instance = (0, _tremolo_ui_dom.createWheel)(element, {
430
+ onWheel: (event) => wheelHandler(event),
431
+ requireFocus
432
+ });
497
433
  return () => instance.destroy();
498
434
  }, [
499
435
  node,
@@ -504,49 +440,6 @@ function useWheel(onWheel, { requireFocus = false, target } = {}) {
504
440
  return setNode;
505
441
  }
506
442
  //#endregion
507
- //#region src/input-event.ts
508
- /**
509
- * The keyboard amount used by Knob, NumberInput, Slider, and XYPad by default:
510
- * 1 per press in the units of the value, and 0.1 with shift held. That is one
511
- * `step` only while `step` is 1; a coarser `step` rounds 1 straight back, which
512
- * `useCheckSteps` warns about.
513
- *
514
- * A modifier entry is not snapped to `step`, which is what lets the finer
515
- * amount move at all — see `applyDelta` in `@tremolo-ui/dom`.
516
- */
517
- const DEFAULT_KEYBOARD_OPTIONS = {
518
- default: ["raw", 1],
519
- shift: ["raw", .1]
520
- };
521
- /**
522
- * The wheel amount used by Knob, NumberInput, Slider, and XYPad by default.
523
- *
524
- * Browsers turn shift+wheel into horizontal scrolling, which empties `deltaY`
525
- * and fills `deltaX`, so no modifier is bound here.
526
- */
527
- const DEFAULT_WHEEL_OPTIONS = ["raw", 1];
528
- /**
529
- * The drag sensitivity used by value controls by default. Shift makes the
530
- * same movement cover a tenth of the range, matching the arrow keys.
531
- */
532
- const DEFAULT_DRAG_SENSITIVITY = {
533
- default: 1,
534
- shift: .1
535
- };
536
- //#endregion
537
- //#region src/components/_util/css-length.ts
538
- /**
539
- * A number as a CSS length, for a custom property.
540
- *
541
- * React appends `px` to a bare number for the properties it knows take a
542
- * length, and a custom property is never one of them: `--size: 50` would come
543
- * out as the invalid `50`. Anything already a string is passed through, so
544
- * `'3rem'` and `'100%'` still work.
545
- */
546
- function cssLength(value) {
547
- return typeof value === "number" ? `${value}px` : value;
548
- }
549
- //#endregion
550
443
  //#region src/components/_util/Placement.tsx
551
444
  /**
552
445
  * The subcomponent that established the current placement, if any.
@@ -588,49 +481,8 @@ function useCheckPlacement(child, parent) {
588
481
  parent
589
482
  ]);
590
483
  }
591
- /**
592
- * 角度から円弧上の点を求める。
593
- *
594
- * 半径は線の太さの分だけ内側に取る必要があるが、太さは ActiveLine / InactiveLine が
595
- * それぞれ持つため、座標はストアで先に計算せず各コンポーネント側で求める。
596
- */
597
- function pointOnArc(angle, radius) {
598
- return {
599
- x: 50 + radius * Math.cos((0, _tremolo_ui_functions.radian)(angle - 90)),
600
- y: 50 + radius * Math.sin((0, _tremolo_ui_functions.radian)(angle - 90))
601
- };
602
- }
603
- /** 線の太さが viewBox からはみ出さないようにした半径 */
604
- function arcRadius(strokeWidth) {
605
- const width = typeof strokeWidth === "number" ? strokeWidth : Number.parseFloat(String(strokeWidth));
606
- return Number.isFinite(width) ? 50 - width / 2 : 50;
607
- }
608
- /** Build an SVG path for an arc, splitting full turns into drawable segments. */
609
- function arcPath(startAngle, endAngle, radius) {
610
- const start = pointOnArc(startAngle, radius);
611
- const sweep = endAngle - startAngle;
612
- const segmentCount = Math.max(1, Math.ceil(Math.abs(sweep) / 180));
613
- const segmentSweep = sweep / segmentCount;
614
- let path = `M ${start.x} ${start.y}`;
615
- for (let i = 1; i <= segmentCount; i += 1) {
616
- const end = pointOnArc(startAngle + segmentSweep * i, radius);
617
- path += ` A ${radius} ${radius} 0 0 ${segmentSweep >= 0 ? 1 : 0} ${end.x} ${end.y}`;
618
- }
619
- return path;
620
- }
621
- /** 設定から描画に必要な角度を導出する。レンダー中に呼ぶ。 */
622
- function calcAngles({ value, min, max, scale, startValue, angleRange }) {
623
- const p = scale.normalize(value, min, max);
624
- const s = scale.normalize(startValue, min, max);
625
- const r1 = -angleRange / 2;
626
- return {
627
- p,
628
- r1,
629
- r2: r1 + Math.min(p, s) * angleRange,
630
- r3: r1 + Math.max(p, s) * angleRange,
631
- r4: angleRange / 2
632
- };
633
- }
484
+ //#endregion
485
+ //#region src/components/Knob/context.tsx
634
486
  const KnobContext = /* @__PURE__ */ (0, react.createContext)(null);
635
487
  const KnobProvider = KnobContext.Provider;
636
488
  function useKnobContext(selector) {
@@ -644,10 +496,10 @@ function ActiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...pr
644
496
  useCheckPlacement("Knob.ActiveLine", "Knob.SVGRoot");
645
497
  const r2 = useKnobContext((s) => s.r2);
646
498
  const r3 = useKnobContext((s) => s.r3);
647
- const radius = arcRadius(strokeWidth);
499
+ const radius = (0, _tremolo_ui_dom.knobArcRadius)(strokeWidth);
648
500
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
649
501
  className,
650
- d: arcPath(r2, r3, radius),
502
+ d: (0, _tremolo_ui_dom.knobArcPath)(r2, r3, radius),
651
503
  fill: "none",
652
504
  stroke,
653
505
  strokeWidth,
@@ -665,17 +517,17 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
665
517
  const r2 = useKnobContext((s) => s.r2);
666
518
  const r3 = useKnobContext((s) => s.r3);
667
519
  const r4 = useKnobContext((s) => s.r4);
668
- const radius = arcRadius(strokeWidth);
520
+ const radius = (0, _tremolo_ui_dom.knobArcRadius)(strokeWidth);
669
521
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [startValue > min && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
670
522
  className,
671
- d: arcPath(r1, r2, radius),
523
+ d: (0, _tremolo_ui_dom.knobArcPath)(r1, r2, radius),
672
524
  fill: "none",
673
525
  stroke,
674
526
  strokeWidth,
675
527
  ...props
676
528
  }), startValue < max && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
677
529
  className,
678
- d: arcPath(r3, r4, radius),
530
+ d: (0, _tremolo_ui_dom.knobArcPath)(r3, r4, radius),
679
531
  fill: "none",
680
532
  stroke,
681
533
  strokeWidth,
@@ -686,7 +538,7 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
686
538
  //#region src/components/Knob/SVGRoot.tsx
687
539
  function SVGRoot({ children, style, ...props }) {
688
540
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
689
- viewBox: `0 0 100 100`,
541
+ viewBox: `0 0 ${_tremolo_ui_dom.KNOB_VIEWBOX_SIZE} ${_tremolo_ui_dom.KNOB_VIEWBOX_SIZE}`,
690
542
  style: {
691
543
  display: "block",
692
544
  ...style
@@ -700,7 +552,7 @@ function SVGRoot({ children, style, ...props }) {
700
552
  }
701
553
  //#endregion
702
554
  //#region src/components/Knob/Thumb.tsx
703
- function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor", thumbSize = 84, thumbLineWeight = 6, thumbLineLength = 35, classes, ...props }) {
555
+ function Thumb$2({ className, color = "currentColor", lineColor = "currentColor", size = 84, lineWeight = 6, lineLength = 35, classes, ...props }) {
704
556
  useCheckPlacement("Knob.Thumb", "Knob.SVGRoot");
705
557
  const angleRange = useKnobContext((s) => s.angleRange);
706
558
  const p = useKnobContext((s) => s.p);
@@ -711,16 +563,16 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
711
563
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
712
564
  cx: "50%",
713
565
  cy: "50%",
714
- r: `${thumbSize / 2}%`,
715
- fill: thumb
566
+ r: `${size / 2}%`,
567
+ fill: color
716
568
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
717
- className: classes?.thumbLine,
569
+ className: classes?.line,
718
570
  x1: "50%",
719
- y1: `${(100 - (0, _tremolo_ui_functions.clamp)(thumbSize, 0, 100)) / 2}%`,
571
+ y1: `${(_tremolo_ui_dom.KNOB_VIEWBOX_SIZE - (0, _tremolo_ui_functions.clamp)(size, 0, 100)) / 2}%`,
720
572
  x2: "50%",
721
- y2: `${thumbLineLength}%`,
722
- stroke: thumbLine,
723
- strokeWidth: `${thumbLineWeight}%`,
573
+ y2: `${lineLength}%`,
574
+ stroke: lineColor,
575
+ strokeWidth: `${lineWeight}%`,
724
576
  style: {
725
577
  transform: `rotate(${r1 + p * angleRange}deg)`,
726
578
  transformOrigin: "50% 50%"
@@ -730,7 +582,6 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
730
582
  }
731
583
  //#endregion
732
584
  //#region src/components/Knob/index.tsx
733
- const defaultExternalStyles$2 = { cursor: "grabbing" };
734
585
  /**
735
586
  * The wheel only acts while the focus is inside, so that scrolling a page past
736
587
  * the control does not change its value.
@@ -740,13 +591,9 @@ const WHEEL_OPTIONS$2 = { requireFocus: true };
740
591
  * Interactive rotary knob component implemented in SVG.
741
592
  */
742
593
  const Knob = {
743
- Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, defaultValue = min, startValue = min, size, externalStyles: _externalStyles, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, pointerLock = false, enableDoubleClickDefault = true, disabled = false, readonly = false, angleRange = 270, onChange, onKeyDown, onPointerDown, onDoubleClick, className, style, children, ...props }, forwardedRef) => {
594
+ Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, resetValue, startValue = min, size, dragCursor = "grabbing", wheel = _tremolo_ui_dom.DEFAULT_WHEEL_OPTIONS, keyboard = _tremolo_ui_dom.DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = _tremolo_ui_dom.DEFAULT_DRAG_SENSITIVITY, pointerLock = false, disabled = false, readOnly = false, angleRange = 270, onChange, onChangeStart, onChangeEnd, changeEndDelay, onKeyDown, onPointerDown, onDoubleClick, className, style, children, actionsRef, ...props }, forwardedRef) => {
744
595
  const elmRef = (0, react.useRef)(null);
745
- const externalStyles = {
746
- ...defaultExternalStyles$2,
747
- ..._externalStyles
748
- };
749
- const inactive = disabled || readonly;
596
+ const inactive = disabled || readOnly;
750
597
  const range = (0, react.useMemo)(() => ({
751
598
  min,
752
599
  max,
@@ -764,24 +611,30 @@ const Knob = {
764
611
  keyboard,
765
612
  wheel
766
613
  });
614
+ const gesture = useChangeGesture(value, {
615
+ onChangeStart,
616
+ onChangeEnd,
617
+ changeEndDelay
618
+ }, inactive);
619
+ const change = (0, react.useCallback)((next) => {
620
+ gesture.changed(next);
621
+ onChange?.(next);
622
+ }, [gesture, onChange]);
767
623
  const handleKeyDown = (0, react.useCallback)((event) => {
768
624
  if (!keyboard || !onChange || inactive) return;
769
- const key = event.key;
770
- if ([
771
- "ArrowRight",
772
- "ArrowLeft",
773
- "ArrowUp",
774
- "ArrowDown"
775
- ].includes(key)) {
776
- event.preventDefault();
777
- onChange((0, _tremolo_ui_dom.applyDelta)(value, key === "ArrowRight" || key === "ArrowUp" ? 1 : -1, keyboard, range, event));
778
- }
625
+ const direction = (0, _tremolo_ui_dom_internal.arrowKeyDirection)(event.key);
626
+ if (direction === null) return;
627
+ event.preventDefault();
628
+ gesture.pulse("keyboard");
629
+ change((0, _tremolo_ui_dom_internal.applyDelta)(value, direction, keyboard, range, event));
779
630
  }, [
780
631
  keyboard,
781
632
  onChange,
782
633
  inactive,
783
634
  value,
784
- range
635
+ range,
636
+ gesture,
637
+ change
785
638
  ]);
786
639
  const { refCallback: dragRefCallback, dragging } = useDragValue({
787
640
  axis: (0, react.useMemo)(() => [range, {
@@ -789,23 +642,30 @@ const Knob = {
789
642
  reverse: true
790
643
  }], [range]),
791
644
  getValue: () => [value, value],
792
- sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
645
+ sensitivity: (state) => (0, _tremolo_ui_dom_internal.selectModifier)(dragSensitivity, state.event).value,
793
646
  threshold: 1,
794
- cursor: inactive ? void 0 : externalStyles.cursor,
647
+ cursor: inactive ? void 0 : dragCursor,
795
648
  pointerLock: inactive ? false : pointerLock,
796
649
  shouldStart: () => !inactive,
797
650
  onChange: (v) => {
798
651
  if (inactive) return;
799
- onChange?.(v[1]);
800
- }
652
+ change(v[1]);
653
+ },
654
+ onDragStart: () => {
655
+ if (!inactive) gesture.hold("pointer");
656
+ },
657
+ onDragEnd: () => gesture.end()
801
658
  });
802
659
  const wheelRefCallback = useWheel((event) => {
803
660
  if (!wheel || inactive) return;
661
+ const direction = (0, _tremolo_ui_dom_internal.wheelDirection)(event);
662
+ if (direction === null) return;
804
663
  event.preventDefault();
805
- if (!onChange || event.deltaY === 0) return;
806
- onChange((0, _tremolo_ui_dom.applyDelta)(value, -Math.sign(event.deltaY), wheel, range, event));
664
+ if (!onChange) return;
665
+ gesture.pulse("wheel");
666
+ change((0, _tremolo_ui_dom_internal.applyDelta)(value, direction, wheel, range, event));
807
667
  }, WHEEL_OPTIONS$2);
808
- const rootRefCallback = require_compose_refs.useComposedRefs(elmRef, dragRefCallback, wheelRefCallback);
668
+ const rootRefCallback = require_compose_refs.useComposedRefs(forwardedRef, elmRef, dragRefCallback, wheelRefCallback);
809
669
  const context = (0, react.useMemo)(() => {
810
670
  const config = {
811
671
  value,
@@ -818,7 +678,7 @@ const Knob = {
818
678
  };
819
679
  return {
820
680
  ...config,
821
- ...calcAngles(config)
681
+ ...(0, _tremolo_ui_dom.knobAngles)(config)
822
682
  };
823
683
  }, [
824
684
  value,
@@ -829,7 +689,7 @@ const Knob = {
829
689
  startValue,
830
690
  angleRange
831
691
  ]);
832
- (0, react.useImperativeHandle)(forwardedRef, () => {
692
+ (0, react.useImperativeHandle)(actionsRef, () => {
833
693
  return {
834
694
  focus() {
835
695
  if (!disabled) elmRef.current?.focus();
@@ -850,17 +710,17 @@ const Knob = {
850
710
  "aria-valuemin": min,
851
711
  "aria-valuemax": max,
852
712
  "aria-disabled": disabled,
853
- "aria-readonly": readonly,
713
+ "aria-readonly": readOnly,
854
714
  "data-disabled": disabled ? "" : void 0,
855
- "data-readonly": readonly ? "" : void 0,
715
+ "data-readonly": readOnly ? "" : void 0,
856
716
  "data-dragging": dragging ? "" : void 0,
857
717
  style: {
858
- "--knob-size": cssLength(size),
718
+ "--knob-size": (0, _tremolo_ui_dom_internal.cssLength)(size),
859
719
  ...style
860
720
  },
861
721
  onPointerDown,
862
722
  onDoubleClick: (event) => {
863
- if (!inactive && enableDoubleClickDefault && onChange) onChange(defaultValue);
723
+ if (!inactive && resetValue !== null && onChange) gesture.instant("doubleClick", () => change(resetValue ?? startValue));
864
724
  onDoubleClick?.(event);
865
725
  },
866
726
  onKeyDown: (event) => {
@@ -886,88 +746,47 @@ function useNumberInputContext(selector) {
886
746
  if (!context) throw new Error("Missing NumberInputContext.Provider in the tree");
887
747
  return selector ? selector(context) : context;
888
748
  }
749
+ const NumberInputGestureContext = /* @__PURE__ */ (0, react.createContext)(null);
750
+ const NumberInputGestureProvider = NumberInputGestureContext.Provider;
751
+ function useNumberInputGesture() {
752
+ return (0, react.useContext)(NumberInputGestureContext);
753
+ }
889
754
  const StepperContext = /* @__PURE__ */ (0, react.createContext)(null);
890
755
  const StepperProvider = StepperContext.Provider;
891
756
  function useStepperContext() {
892
757
  return (0, react.useContext)(StepperContext);
893
758
  }
894
759
  //#endregion
895
- //#region src/hooks/useEventListener.ts
896
- function useEventListener(target, event, handler, options) {
897
- const listener = useCallbackRef(handler);
898
- const cleanupRef = (0, react.useRef)(() => {});
899
- (0, react.useEffect)(() => {
900
- const node = typeof target === "function" ? target() : target ?? document;
901
- if (!node) return;
902
- node.addEventListener(event, listener, options);
903
- const cleanup = () => {
904
- node.removeEventListener(event, listener, options);
905
- };
906
- cleanupRef.current = cleanup;
907
- return () => {
908
- cleanup();
909
- if (cleanupRef.current === cleanup) cleanupRef.current = () => {};
910
- };
911
- }, [
912
- event,
913
- target,
914
- options,
915
- listener
916
- ]);
917
- return (0, react.useCallback)(() => cleanupRef.current(), []);
918
- }
919
- //#endregion
920
- //#region src/hooks/useInterval.ts
921
- function useInterval(callback, delay) {
922
- const savedCallback = (0, react.useRef)(callback);
760
+ //#region src/hooks/useLongPress.ts
761
+ /**
762
+ * Repeat `callback` while a pointer is held down: once on the press, then
763
+ * every `interval` after `initialDelay`. The repeat itself is
764
+ * `createLongPress` in `@tremolo-ui/dom`.
765
+ *
766
+ * Returns the function that starts a press, for an `onPointerDown`.
767
+ */
768
+ function useLongPress(callback, initialDelay = 500, interval = 40) {
769
+ const onPress = useCallbackRef(callback);
770
+ const instanceRef = (0, react.useRef)(null);
923
771
  (0, react.useEffect)(() => {
924
- savedCallback.current = callback;
925
- }, [callback]);
772
+ instanceRef.current?.update({
773
+ delay: initialDelay,
774
+ interval
775
+ });
776
+ });
926
777
  (0, react.useEffect)(() => {
927
- if (delay === null) return;
928
- const id = setInterval(() => {
929
- savedCallback.current();
930
- }, delay);
778
+ const instance = (0, _tremolo_ui_dom.createLongPress)({
779
+ onPress: () => onPress(),
780
+ delay: initialDelay,
781
+ interval
782
+ });
783
+ instanceRef.current = instance;
931
784
  return () => {
932
- clearInterval(id);
785
+ instanceRef.current = null;
786
+ instance.destroy();
933
787
  };
934
- }, [delay]);
935
- }
936
- //#endregion
937
- //#region src/hooks/useLongPress.ts
938
- function useLongPress(callback, initialDelay = 500, interval = 40) {
939
- const [pressed, setPressed] = (0, react.useState)(false);
940
- const [delay, setDelay] = (0, react.useState)(initialDelay);
941
- const activePointerId = (0, react.useRef)(null);
942
- const isPressed = (0, react.useRef)(false);
943
- useInterval(() => {
944
- callback();
945
- setDelay(interval);
946
- }, pressed ? delay : null);
947
- const stop = (0, react.useCallback)((pointerId) => {
948
- if (!isPressed.current) return;
949
- if (pointerId !== void 0 && activePointerId.current !== null && activePointerId.current !== pointerId) return;
950
- isPressed.current = false;
951
- activePointerId.current = null;
952
- setPressed(false);
953
- setDelay(initialDelay);
954
- }, [initialDelay]);
955
- useEventListener(globalThis.window, "pointerup", (event) => {
956
- stop(event.pointerId);
957
- });
958
- useEventListener(globalThis.window, "pointercancel", (event) => {
959
- stop(event.pointerId);
960
- });
961
- useEventListener(globalThis.window, "blur", () => {
962
- stop();
963
- });
964
- return (0, react.useCallback)((event) => {
965
- if (isPressed.current || event && event.button !== 0) return;
966
- isPressed.current = true;
967
- activePointerId.current = event?.pointerId ?? null;
968
- callback();
969
- setPressed(true);
970
- }, [callback]);
788
+ }, []);
789
+ return (0, react.useCallback)((event) => instanceRef.current?.start(event), []);
971
790
  }
972
791
  //#endregion
973
792
  //#region src/components/NumberInput/StepperButton.tsx
@@ -976,10 +795,11 @@ function useLongPress(callback, initialDelay = 500, interval = 40) {
976
795
  * in which way they move the value and which arrow they draw.
977
796
  */
978
797
  function StepperButton({ direction, variant, icon, className, style, children, onPointerDown, ...props }) {
979
- const { step, disabled, readonly, atMin, atMax, nudge } = useNumberInputContext();
798
+ const { step, disabled, readOnly, atMin, atMax, nudge } = useNumberInputContext();
980
799
  const stepper = useStepperContext();
800
+ const gesture = useNumberInputGesture();
981
801
  const press = useLongPress(() => {
982
- if (stepper?.draggingRef.current) return;
802
+ if (stepper?.moved()) return;
983
803
  nudge(direction, ["raw", step]);
984
804
  });
985
805
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -989,10 +809,13 @@ function StepperButton({ direction, variant, icon, className, style, children, o
989
809
  "aria-label": direction > 0 ? "Increment" : "Decrement",
990
810
  "aria-disabled": disabled || (direction > 0 ? atMax : atMin),
991
811
  "data-disabled": disabled || (direction > 0 ? atMax : atMin) ? "" : void 0,
992
- "data-readonly": readonly ? "" : void 0,
812
+ "data-readonly": readOnly ? "" : void 0,
993
813
  style,
994
814
  onPointerDown: (event) => {
995
- if (!disabled && !readonly) press(event);
815
+ if (!disabled && !readOnly) {
816
+ gesture?.press();
817
+ press(event);
818
+ }
996
819
  onPointerDown?.(event);
997
820
  },
998
821
  ...props,
@@ -1038,21 +861,6 @@ function IncrementStepper(props) {
1038
861
  }
1039
862
  //#endregion
1040
863
  //#region src/components/NumberInput/InputField.tsx
1041
- /** The leading number of the displayed text, whatever the format put around it. */
1042
- const NUMBER_PREFIX = /^\s*-?[\d.,]*/;
1043
- const numberEnd = (text) => text.match(NUMBER_PREFIX)?.[0].length ?? text.length;
1044
- /**
1045
- * The index the caret is measured against: the decimal point, or where one
1046
- * would go if the number has none.
1047
- *
1048
- * Measuring from an end instead would slide the caret across a digit whenever
1049
- * the number changed length — `9.9` to `10.0` gains a character in front, `10`
1050
- * to `9` loses one — which is exactly what stepping does.
1051
- */
1052
- const decimalAnchor = (text) => {
1053
- const dot = text.indexOf(".");
1054
- return dot === -1 ? numberEnd(text) : dot;
1055
- };
1056
864
  /**
1057
865
  * The text field of a `NumberInput`, and the only place the value can be typed.
1058
866
  *
@@ -1062,8 +870,9 @@ const decimalAnchor = (text) => {
1062
870
  * How it behaves on focus, Enter and the arrow keys is set on `Root`
1063
871
  * (`selectOnFocus`, `unformatOnFocus`, `keepCaretOnStep`, `blurOnEnter`).
1064
872
  */
1065
- const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ className, style, onFocus, onBlur, onKeyDown, ...props }, forwardedRef) {
1066
- const { value, min, max, step, disabled, readonly, keyboard, selectOnFocus, unformatOnFocus, keepCaretOnStep, blurOnEnter, text, editing, outOfRange, setDraft, commitDraft, nudge, inputRef } = useNumberInputContext();
873
+ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ className, style, onChange, onFocus, onBlur, onKeyDown, ...props }, forwardedRef) {
874
+ const { value, min, max, step, disabled, readOnly, keyboard, selectOnFocus, unformatOnFocus, keepCaretOnStep, blurOnEnter, text, editing, outOfRange, setDraft, commitDraft, nudge, inputRef } = useNumberInputContext();
875
+ const gesture = useNumberInputGesture();
1067
876
  const [focused, setFocused] = (0, react.useState)(false);
1068
877
  const shown = unformatOnFocus && focused && !editing ? String(value) : text;
1069
878
  (0, react.useEffect)(() => {
@@ -1071,7 +880,10 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
1071
880
  const input = inputRef.current;
1072
881
  if (!input) return;
1073
882
  if (selectOnFocus === "all") input.setSelectionRange(0, input.value.length);
1074
- else input.setSelectionRange(0, input.value.match(NUMBER_PREFIX)?.[0].length ?? 0);
883
+ else {
884
+ const span = (0, _tremolo_ui_dom_internal.numberSpan)(input.value);
885
+ if (span) input.setSelectionRange(span.start, span.end);
886
+ }
1075
887
  }, [
1076
888
  focused,
1077
889
  selectOnFocus,
@@ -1088,8 +900,7 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
1088
900
  caretAfterStep.current = null;
1089
901
  const input = inputRef.current;
1090
902
  if (!input || input.value === pending.from) return;
1091
- const place = decimalAnchor(input.value) + pending.offset;
1092
- const caret = Math.max(0, Math.min(place, numberEnd(input.value)));
903
+ const caret = (0, _tremolo_ui_dom_internal.caretAtDecimalOffset)(input.value, pending.offset);
1093
904
  input.setSelectionRange(caret, caret);
1094
905
  });
1095
906
  const composedRef = require_compose_refs.useComposedRefs(forwardedRef, inputRef);
@@ -1101,11 +912,11 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
1101
912
  role: "spinbutton",
1102
913
  value: shown,
1103
914
  disabled,
1104
- readOnly: readonly,
915
+ readOnly,
1105
916
  "aria-disabled": disabled,
1106
- "aria-readonly": readonly,
917
+ "aria-readonly": readOnly,
1107
918
  "data-disabled": disabled ? "" : void 0,
1108
- "data-readonly": readonly ? "" : void 0,
919
+ "data-readonly": readOnly ? "" : void 0,
1109
920
  "aria-valuenow": value,
1110
921
  "aria-valuemin": min,
1111
922
  "aria-valuemax": max,
@@ -1116,6 +927,7 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
1116
927
  onChange: (event) => {
1117
928
  caretAfterStep.current = null;
1118
929
  setDraft(event.currentTarget.value);
930
+ onChange?.(event);
1119
931
  },
1120
932
  onFocus: (event) => {
1121
933
  setFocused(true);
@@ -1131,13 +943,14 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
1131
943
  if (key === "Enter") {
1132
944
  commitDraft();
1133
945
  if (blurOnEnter) event.currentTarget.blur();
1134
- } else if (keyboard && !disabled && !readonly && (key === "ArrowUp" || key === "ArrowDown") && !event.nativeEvent.isComposing) {
946
+ } else if (keyboard && !disabled && !readOnly && (key === "ArrowUp" || key === "ArrowDown") && !event.nativeEvent.isComposing) {
1135
947
  event.preventDefault();
1136
948
  const input = event.currentTarget;
1137
949
  if (keepCaretOnStep && input.selectionStart !== null) caretAfterStep.current = {
1138
- offset: input.selectionStart - decimalAnchor(input.value),
950
+ offset: (0, _tremolo_ui_dom_internal.caretDecimalOffset)(input.value, input.selectionStart),
1139
951
  from: input.value
1140
952
  };
953
+ gesture?.key();
1141
954
  nudge(key === "ArrowUp" ? 1 : -1, keyboard, event);
1142
955
  }
1143
956
  onKeyDown?.(event);
@@ -1146,123 +959,66 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
1146
959
  });
1147
960
  });
1148
961
  //#endregion
1149
- //#region src/hooks/useDrag.ts
1150
- /**
1151
- * Track a pointer drag on an element.
1152
- *
1153
- * @returns a ref callback to attach to the element being dragged
1154
- */
1155
- function useDrag({ threshold = 1, cursor, pointerLock, onDrag, onDragStart, onDragEnd }) {
1156
- const dragHandler = useCallbackRef(onDrag);
1157
- const dragStartHandler = useCallbackRef(onDragStart);
1158
- const dragEndHandler = useCallbackRef(onDragEnd);
1159
- const [node, setNode] = (0, react.useState)(null);
1160
- (0, react.useEffect)(() => {
1161
- if (!node) return;
1162
- const instance = (0, _tremolo_ui_dom.createDrag)(node, {
1163
- threshold,
1164
- cursor,
1165
- pointerLock,
1166
- onDragStart: (state) => dragStartHandler(state),
1167
- onDrag: (state) => dragHandler(state.x, state.y, state.deltaX, state.deltaY, state),
1168
- onDragEnd: (state) => dragEndHandler(state)
1169
- });
1170
- return () => instance.destroy();
1171
- }, [
1172
- node,
1173
- threshold,
1174
- cursor,
1175
- pointerLock,
1176
- dragHandler,
1177
- dragStartHandler,
1178
- dragEndHandler
1179
- ]);
1180
- return setNode;
1181
- }
1182
- //#endregion
1183
962
  //#region src/components/NumberInput/Stepper.tsx
1184
963
  /**
1185
964
  * The area the steppers sit in, and a drag handle in its own right: dragging it
1186
- * up and down moves the value one `step` every `drag` pixels.
965
+ * up and down moves the value one `step` every `drag` pixels. The counting is
966
+ * `createStepperDrag` in the core.
1187
967
  *
1188
968
  * The drag lives here rather than on `InputField` because `createDrag` turns
1189
969
  * off text selection on whatever element it is attached to.
1190
970
  */
1191
- const Stepper = /* @__PURE__ */ (0, react.forwardRef)(function Stepper({ className, style, children, ...props }, forwardedRef) {
1192
- const { value, step, disabled, readonly, drag, dragSensitivity, pointerLock, rawRange, changeValue } = useNumberInputContext();
1193
- const inactive = disabled || readonly;
1194
- /**
1195
- * The sensitivity as an amount per `drag` pixels, so that the drag goes
1196
- * through the same `applyDelta` the wheel and the arrow keys do. Carrying
1197
- * the modifier map over rather than resolving it here is what keeps `step`
1198
- * out of the pipeline for a modifier entry — naming one is a request to move
1199
- * off the grid.
1200
- */
1201
- const dragOptions = (0, react.useMemo)(() => (0, _tremolo_ui_dom.mapModifier)(dragSensitivity, (factor) => ["raw", step * factor]), [dragSensitivity, step]);
1202
- /**
1203
- * Set once the drag has actually moved the value, so that the press-and-hold
1204
- * repeat of a stepper stands down and leaves the value to the drag. A ref,
1205
- * because it is read from inside the repeat rather than rendered.
1206
- */
1207
- const draggingRef = (0, react.useRef)(false);
1208
- /**
1209
- * Where the drag started, taken on the first move rather than on pointer
1210
- * down: the steppers act on pointer down, so by then the value may already
1211
- * have been nudged once, and the drag should carry on from there.
1212
- */
1213
- const originRef = (0, react.useRef)(null);
1214
- /**
1215
- * Which modifier the drag is currently counting at, and where the previous
1216
- * event was — a key produces no pointer event of its own, so a change is
1217
- * only seen on the next move and has to be dated back to the one before it.
1218
- */
1219
- const factorRef = (0, react.useRef)(1);
1220
- const previousYRef = (0, react.useRef)(0);
1221
- const dragRefCallback = useDrag({
1222
- threshold: 1,
1223
- cursor: inactive ? void 0 : "ns-resize",
1224
- pointerLock: inactive ? false : pointerLock,
1225
- onDragStart: (state) => {
1226
- originRef.current = null;
1227
- draggingRef.current = false;
1228
- factorRef.current = (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value;
1229
- },
1230
- onDrag: (_x, y, _dx, _dy, state) => {
1231
- if (inactive || drag === null) return;
1232
- if (!originRef.current) {
1233
- originRef.current = {
1234
- y,
1235
- value
1236
- };
1237
- previousYRef.current = y;
1238
- return;
1239
- }
1240
- const factor = (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value;
1241
- if (factor !== factorRef.current) {
1242
- originRef.current = {
1243
- y: previousYRef.current,
1244
- value
1245
- };
1246
- factorRef.current = factor;
1247
- }
1248
- previousYRef.current = y;
1249
- const steps = Math.round(-(y - originRef.current.y) / drag);
1250
- if (steps === 0) return;
1251
- draggingRef.current = true;
1252
- changeValue((0, _tremolo_ui_dom.applyDelta)(originRef.current.value, steps, dragOptions, rawRange, state.event));
1253
- },
1254
- onDragEnd: () => {
1255
- draggingRef.current = false;
1256
- }
971
+ const Stepper = /* @__PURE__ */ (0, react.forwardRef)(function Stepper({ className, style, children, onPointerDown, ...props }, forwardedRef) {
972
+ const { value, disabled, readOnly, drag, dragSensitivity, pointerLock, dragCursor, rawRange, changeValue } = useNumberInputContext();
973
+ const enabled = drag !== null && !disabled && !readOnly;
974
+ const [node, setNode] = (0, react.useState)(null);
975
+ const instanceRef = (0, react.useRef)(null);
976
+ const latest = (0, react.useRef)({
977
+ value,
978
+ changeValue
1257
979
  });
1258
- const composedRef = require_compose_refs.useComposedRefs(forwardedRef, drag !== null && !inactive ? dragRefCallback : void 0);
1259
- const context = (0, react.useMemo)(() => ({ draggingRef }), []);
980
+ const options = {
981
+ range: rawRange,
982
+ pixels: drag ?? 1,
983
+ sensitivity: dragSensitivity,
984
+ pointerLock,
985
+ cursor: dragCursor
986
+ };
987
+ const optionsRef = (0, react.useRef)(options);
988
+ (0, react.useEffect)(() => {
989
+ latest.current = {
990
+ value,
991
+ changeValue
992
+ };
993
+ optionsRef.current = options;
994
+ instanceRef.current?.update(options);
995
+ });
996
+ (0, react.useEffect)(() => {
997
+ if (!node || !enabled) return;
998
+ const instance = (0, _tremolo_ui_dom_internal.createStepperDrag)(node, {
999
+ ...optionsRef.current,
1000
+ getValue: () => latest.current.value,
1001
+ onChange: (next) => latest.current.changeValue(next)
1002
+ });
1003
+ instanceRef.current = instance;
1004
+ return () => {
1005
+ instanceRef.current = null;
1006
+ instance.destroy();
1007
+ };
1008
+ }, [node, enabled]);
1009
+ const gesture = useNumberInputGesture();
1010
+ const composedRef = require_compose_refs.useComposedRefs(forwardedRef, setNode);
1011
+ const context = (0, react.useMemo)(() => ({ moved: () => instanceRef.current?.moved() ?? false }), []);
1260
1012
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepperProvider, {
1261
1013
  value: context,
1262
1014
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1263
1015
  ref: composedRef,
1264
1016
  className,
1265
1017
  style,
1018
+ onPointerDown: (event) => {
1019
+ if (enabled) gesture?.press();
1020
+ onPointerDown?.(event);
1021
+ },
1266
1022
  ...props,
1267
1023
  children
1268
1024
  })
@@ -1270,48 +1026,32 @@ const Stepper = /* @__PURE__ */ (0, react.forwardRef)(function Stepper({ classNa
1270
1026
  });
1271
1027
  //#endregion
1272
1028
  //#region src/components/NumberInput/index.tsx
1273
- /**
1274
- * A number at the start of the text, and nothing read after it. A half-typed
1275
- * entry still yields the number in front of it, but text with no number is
1276
- * `NaN` rather than 0, so that "unreadable" and "the user typed 0" stay apart.
1277
- */
1278
- const LEADING_NUMBER = /^\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)/;
1279
1029
  const defaultFormat = (value) => String(value);
1280
- const defaultParse = (text) => {
1281
- const match = text.match(LEADING_NUMBER);
1282
- return match ? Number(match[1]) : NaN;
1283
- };
1284
1030
  /**
1285
1031
  * Input with some useful functions for entering numerical values.
1286
1032
  */
1287
1033
  const NumberInput = {
1288
- Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, format: formatProp, parse: parseProp, clampValue = true, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, drag = 1, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, pointerLock = false, selectOnFocus = "none", unformatOnFocus = false, keepCaretOnStep = false, blurOnEnter = true, disabled = false, readonly = false, className, style, onChange, children, ...props }, forwardedRef) => {
1034
+ Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, format: formatProp, parse: parseProp, clampValue = true, wheel = _tremolo_ui_dom.DEFAULT_WHEEL_OPTIONS, keyboard = _tremolo_ui_dom.DEFAULT_KEYBOARD_OPTIONS, drag = 1, dragSensitivity = _tremolo_ui_dom.DEFAULT_DRAG_SENSITIVITY, pointerLock = false, dragCursor = "ns-resize", selectOnFocus = "none", unformatOnFocus = false, keepCaretOnStep = false, blurOnEnter = true, disabled = false, readOnly = false, className, style, onChange, onChangeStart, onChangeEnd, changeEndDelay, children, actionsRef, ...props }, forwardedRef) => {
1289
1035
  const inputRef = (0, react.useRef)(null);
1290
1036
  /**
1291
1037
  * The text being typed. The only state here: it is not a copy of `value`,
1292
1038
  * but the half-finished entry that has no value to be derived from yet.
1293
1039
  */
1294
1040
  const [draft, setDraft] = (0, react.useState)(null);
1295
- const inactive = disabled || readonly;
1041
+ const inactive = disabled || readOnly;
1042
+ const gesture = useChangeGesture(value, {
1043
+ onChangeStart,
1044
+ onChangeEnd,
1045
+ changeEndDelay
1046
+ }, inactive);
1296
1047
  const format = formatProp ?? defaultFormat;
1297
- const parse = parseProp ?? defaultParse;
1298
- const range = (0, react.useMemo)(() => ({
1299
- min: (clampValue ? min : void 0) ?? Number.MIN_SAFE_INTEGER,
1300
- max: (clampValue ? max : void 0) ?? Number.MAX_SAFE_INTEGER,
1301
- step,
1302
- scale
1303
- }), [
1304
- clampValue,
1048
+ const parse = parseProp ?? _tremolo_ui_dom_internal.parseNumberText;
1049
+ const { normalized: range, raw: rawRange } = (0, react.useMemo)(() => (0, _tremolo_ui_dom_internal.numberInputRanges)({
1305
1050
  min,
1306
1051
  max,
1307
1052
  step,
1308
- scale
1309
- ]);
1310
- const rawRange = (0, react.useMemo)(() => ({
1311
- min: (clampValue ? min : void 0) ?? -Number.MAX_VALUE,
1312
- max: (clampValue ? max : void 0) ?? Number.MAX_VALUE,
1313
- step,
1314
- scale
1053
+ scale,
1054
+ clampValue
1315
1055
  }), [
1316
1056
  clampValue,
1317
1057
  min,
@@ -1319,6 +1059,10 @@ const NumberInput = {
1319
1059
  step,
1320
1060
  scale
1321
1061
  ]);
1062
+ const ranges = (0, react.useMemo)(() => ({
1063
+ normalized: range,
1064
+ raw: rawRange
1065
+ }), [range, rawRange]);
1322
1066
  useCheckSteps({
1323
1067
  component: "NumberInput",
1324
1068
  range: (0, react.useMemo)(() => min !== void 0 && max !== void 0 && min < max ? {
@@ -1338,37 +1082,56 @@ const NumberInput = {
1338
1082
  });
1339
1083
  const text = draft ?? format(value);
1340
1084
  const editing = draft !== null;
1341
- const outOfRange = !editing && (min !== void 0 && value < min || max !== void 0 && value > max);
1085
+ const bounds = (0, _tremolo_ui_dom_internal.numberInputBounds)(value, {
1086
+ min,
1087
+ max,
1088
+ clampValue
1089
+ });
1090
+ const { atMin, atMax } = bounds;
1091
+ const outOfRange = !editing && bounds.outOfRange;
1342
1092
  const handleDraft = (0, react.useCallback)((next) => {
1343
1093
  if (inactive) return;
1344
1094
  setDraft(next);
1345
1095
  const parsed = parse(next);
1346
- if (Number.isFinite(parsed)) onChange?.(parsed);
1096
+ if (!Number.isFinite(parsed)) return;
1097
+ if (parsed !== value) {
1098
+ gesture.pulse("keyboard");
1099
+ gesture.changed(parsed);
1100
+ }
1101
+ onChange?.(parsed);
1347
1102
  }, [
1348
1103
  inactive,
1349
1104
  parse,
1350
- onChange
1105
+ onChange,
1106
+ value,
1107
+ gesture
1351
1108
  ]);
1352
1109
  const changeValue = (0, react.useCallback)((next) => {
1353
1110
  if (inactive) return;
1354
1111
  setDraft(null);
1355
- if (next !== value) onChange?.(next);
1112
+ if (next === value) return;
1113
+ gesture.changed(next);
1114
+ onChange?.(next);
1356
1115
  }, [
1357
1116
  inactive,
1358
1117
  value,
1359
- onChange
1118
+ onChange,
1119
+ gesture
1360
1120
  ]);
1361
1121
  const commitDraft = (0, react.useCallback)(() => {
1362
1122
  if (draft === null || inactive) return;
1363
- const parsed = parse(draft);
1364
- if (!Number.isFinite(parsed)) {
1123
+ const committed = (0, _tremolo_ui_dom_internal.commitNumberInputText)(draft, parse, {
1124
+ min,
1125
+ max,
1126
+ clampValue
1127
+ });
1128
+ if (committed === null) {
1365
1129
  setDraft(null);
1366
1130
  return;
1367
1131
  }
1368
- let committed = parsed;
1369
- if (clampValue && min !== void 0) committed = Math.max(committed, min);
1370
- if (clampValue && max !== void 0) committed = Math.min(committed, max);
1132
+ if (committed !== value) gesture.pulse("keyboard");
1371
1133
  changeValue(committed);
1134
+ gesture.end();
1372
1135
  }, [
1373
1136
  draft,
1374
1137
  inactive,
@@ -1376,21 +1139,23 @@ const NumberInput = {
1376
1139
  clampValue,
1377
1140
  min,
1378
1141
  max,
1379
- changeValue
1142
+ changeValue,
1143
+ value,
1144
+ gesture
1380
1145
  ]);
1381
1146
  const nudge = (0, react.useCallback)((direction, option, modifiers) => {
1382
- const [mode] = (0, _tremolo_ui_dom.selectModifier)(option, modifiers).value;
1383
- changeValue((0, _tremolo_ui_dom.applyDelta)(value, direction, option, mode === "raw" ? rawRange : range, modifiers));
1147
+ changeValue((0, _tremolo_ui_dom_internal.nudgeNumberInput)(value, direction, option, ranges, modifiers));
1384
1148
  }, [
1385
1149
  changeValue,
1386
1150
  value,
1387
- rawRange,
1388
- range
1151
+ ranges
1389
1152
  ]);
1390
1153
  const wheelRefCallback = useWheel((event) => {
1391
- if (!wheel || inactive || event.deltaY === 0) return;
1154
+ const direction = (0, _tremolo_ui_dom_internal.wheelDirection)(event);
1155
+ if (!wheel || inactive || direction === null) return;
1392
1156
  event.preventDefault();
1393
- nudge(-Math.sign(event.deltaY), wheel, event);
1157
+ gesture.pulse("wheel");
1158
+ nudge(direction, wheel, event);
1394
1159
  }, { requireFocus: true });
1395
1160
  const context = (0, react.useMemo)(() => ({
1396
1161
  value,
@@ -1399,7 +1164,7 @@ const NumberInput = {
1399
1164
  step,
1400
1165
  scale,
1401
1166
  disabled,
1402
- readonly,
1167
+ readOnly,
1403
1168
  clampValue,
1404
1169
  range,
1405
1170
  rawRange,
@@ -1409,12 +1174,13 @@ const NumberInput = {
1409
1174
  outOfRange,
1410
1175
  dragSensitivity,
1411
1176
  pointerLock,
1177
+ dragCursor,
1412
1178
  selectOnFocus,
1413
1179
  unformatOnFocus,
1414
1180
  keepCaretOnStep,
1415
1181
  blurOnEnter,
1416
- atMin: clampValue && min !== void 0 && value <= min,
1417
- atMax: clampValue && max !== void 0 && value >= max,
1182
+ atMin,
1183
+ atMax,
1418
1184
  drag,
1419
1185
  format,
1420
1186
  parse,
@@ -1430,7 +1196,7 @@ const NumberInput = {
1430
1196
  step,
1431
1197
  scale,
1432
1198
  disabled,
1433
- readonly,
1199
+ readOnly,
1434
1200
  clampValue,
1435
1201
  range,
1436
1202
  rawRange,
@@ -1438,9 +1204,12 @@ const NumberInput = {
1438
1204
  text,
1439
1205
  editing,
1440
1206
  outOfRange,
1207
+ atMin,
1208
+ atMax,
1441
1209
  drag,
1442
1210
  dragSensitivity,
1443
1211
  pointerLock,
1212
+ dragCursor,
1444
1213
  selectOnFocus,
1445
1214
  unformatOnFocus,
1446
1215
  keepCaretOnStep,
@@ -1452,7 +1221,8 @@ const NumberInput = {
1452
1221
  changeValue,
1453
1222
  nudge
1454
1223
  ]);
1455
- (0, react.useImperativeHandle)(forwardedRef, () => {
1224
+ const rootRefCallback = require_compose_refs.useComposedRefs(forwardedRef, wheelRefCallback);
1225
+ (0, react.useImperativeHandle)(actionsRef, () => {
1456
1226
  return {
1457
1227
  focus() {
1458
1228
  if (!disabled) inputRef.current?.focus();
@@ -1462,16 +1232,39 @@ const NumberInput = {
1462
1232
  }
1463
1233
  };
1464
1234
  }, [disabled]);
1235
+ const pressed = (0, react.useRef)(false);
1236
+ const gestureContext = (0, react.useMemo)(() => ({
1237
+ press: () => {
1238
+ if (inactive) return;
1239
+ gesture.hold("pointer");
1240
+ if (pressed.current) return;
1241
+ pressed.current = true;
1242
+ const release = () => {
1243
+ pressed.current = false;
1244
+ gesture.end();
1245
+ window.removeEventListener("pointerup", release);
1246
+ window.removeEventListener("pointercancel", release);
1247
+ };
1248
+ window.addEventListener("pointerup", release);
1249
+ window.addEventListener("pointercancel", release);
1250
+ },
1251
+ key: () => {
1252
+ if (!inactive) gesture.pulse("keyboard");
1253
+ }
1254
+ }), [gesture, inactive]);
1465
1255
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInputProvider, {
1466
1256
  value: context,
1467
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1468
- ref: wheelRefCallback,
1469
- className,
1470
- "data-disabled": disabled ? "" : void 0,
1471
- "data-readonly": readonly ? "" : void 0,
1472
- style,
1473
- ...props,
1474
- children
1257
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInputGestureProvider, {
1258
+ value: gestureContext,
1259
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1260
+ ref: rootRefCallback,
1261
+ className,
1262
+ "data-disabled": disabled ? "" : void 0,
1263
+ "data-readonly": readOnly ? "" : void 0,
1264
+ style,
1265
+ ...props,
1266
+ children
1267
+ })
1475
1268
  })
1476
1269
  });
1477
1270
  }),
@@ -1480,78 +1273,15 @@ const NumberInput = {
1480
1273
  IncrementStepper,
1481
1274
  DecrementStepper
1482
1275
  };
1483
- //#endregion
1484
- //#region src/components/Piano/keyboard-shortcuts.ts
1485
- /**
1486
- * Ready-made keyboard layouts. Both assume `noteRange.first` is a C.
1487
- */
1488
- const SHORTCUTS = {
1489
- /** Every semitone from C, over the two rows of a QWERTY keyboard. */
1490
- HOME_ROW: { keys: [
1491
- "a",
1492
- "w",
1493
- "s",
1494
- "e",
1495
- "d",
1496
- "f",
1497
- "t",
1498
- "g",
1499
- "y",
1500
- "h",
1501
- "u",
1502
- "j",
1503
- "k",
1504
- "o",
1505
- "l",
1506
- "p",
1507
- ";"
1508
- ] },
1509
- /** The white keys only, on the home row. Black keys have no shortcut. */
1510
- HOME_ROW_NATURAL: { keys: [
1511
- "a",
1512
- "",
1513
- "s",
1514
- "",
1515
- "d",
1516
- "f",
1517
- "",
1518
- "g",
1519
- "",
1520
- "h",
1521
- "",
1522
- "j",
1523
- "k",
1524
- "",
1525
- "l",
1526
- "",
1527
- ";"
1528
- ] }
1529
- };
1530
- //#endregion
1531
- //#region src/components/Piano/index.tsx
1532
- function isEditableTarget(target) {
1533
- if (!(target instanceof HTMLElement)) return false;
1534
- if (target.matches("input, textarea, select")) return true;
1535
- for (let element = target; element;) {
1536
- const contentEditable = element.getAttribute("contenteditable");
1537
- if (contentEditable !== null) return contentEditable.toLowerCase() !== "false";
1538
- element = element.parentElement;
1539
- }
1540
- return false;
1541
- }
1542
- function shortcutKey(event) {
1543
- return event.code || event.key;
1544
- }
1545
1276
  /**
1546
1277
  * Customizable piano component.
1547
1278
  */
1548
- const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, keyboardShortcutsScope = "root", resizable = false, whiteKeyWidth = 40, keyGap = 1, blackKeyWidthRatio = .65, blackKeyHeightRatio = .6, height, style, className, label, keyProps, classes, onPlayNote, onStopNote, ...props }, forwardedRef) {
1279
+ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, keyboardShortcutsScope = "root", resizable = false, whiteKeyWidth = 40, keyGap = 1, blackKeyWidthRatio = .65, blackKeyHeightRatio = .6, style, className, label, keyProps, classes, onPlayNote, onStopNote, actionsRef, ...props }, forwardedRef) {
1549
1280
  const [node, setNode] = (0, react.useState)(null);
1550
1281
  const [activeNotes, setActiveNotes] = (0, react.useState)([]);
1551
1282
  /** Set while `resizable` is on, measured from the parent. */
1552
1283
  const [resizedKeyWidth, setResizedKeyWidth] = (0, react.useState)(whiteKeyWidth);
1553
- const notes = (0, react.useMemo)(() => (0, _tremolo_ui_dom.getNoteRangeArray)(noteRange), [noteRange]);
1554
- const whiteKeyCount = (0, react.useMemo)(() => notes.filter(_tremolo_ui_functions.isWhiteKey).length, [notes]);
1284
+ const notes = (0, react.useMemo)(() => (0, _tremolo_ui_dom_internal.getNoteRangeArray)(noteRange), [noteRange]);
1555
1285
  const layout = (0, react.useMemo)(() => ({
1556
1286
  noteRange,
1557
1287
  whiteKeyWidth: resizable ? resizedKeyWidth : whiteKeyWidth,
@@ -1571,21 +1301,20 @@ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ note
1571
1301
  layout,
1572
1302
  glissando,
1573
1303
  midiMax,
1304
+ keyboardShortcuts,
1305
+ keyboardShortcutsScope,
1574
1306
  onPlayNote,
1575
1307
  onStopNote
1576
1308
  });
1577
1309
  const instanceRef = (0, react.useRef)(null);
1578
- const shortcutNotes = (0, react.useRef)(/* @__PURE__ */ new Map());
1579
- const releaseShortcutNotes = (0, react.useCallback)(() => {
1580
- for (const { note, source } of shortcutNotes.current.values()) instanceRef.current?.noteOff(note, { source });
1581
- shortcutNotes.current.clear();
1582
- }, []);
1583
1310
  (0, react.useEffect)(() => {
1584
1311
  if (!node) return;
1585
1312
  const instance = (0, _tremolo_ui_dom.createPianoInput)(node, {
1586
1313
  layout: latest.current.layout,
1587
1314
  glissando: latest.current.glissando,
1588
1315
  midiMax: latest.current.midiMax,
1316
+ keyboardShortcuts: latest.current.keyboardShortcuts,
1317
+ keyboardShortcutsScope: latest.current.keyboardShortcutsScope,
1589
1318
  onPlayNote: (note, velocity) => latest.current.onPlayNote?.(note, velocity),
1590
1319
  onStopNote: (note) => latest.current.onStopNote?.(note),
1591
1320
  onActiveNotesChange: setActiveNotes
@@ -1601,87 +1330,46 @@ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ note
1601
1330
  layout,
1602
1331
  glissando,
1603
1332
  midiMax,
1333
+ keyboardShortcuts,
1334
+ keyboardShortcutsScope,
1604
1335
  onPlayNote,
1605
1336
  onStopNote
1606
1337
  };
1607
1338
  instanceRef.current?.update({
1608
1339
  layout,
1609
1340
  glissando,
1610
- midiMax
1341
+ midiMax,
1342
+ keyboardShortcuts,
1343
+ keyboardShortcutsScope
1611
1344
  });
1612
1345
  });
1346
+ const { first, last } = noteRange;
1613
1347
  (0, react.useEffect)(() => {
1614
1348
  if (!resizable || !node) return;
1615
1349
  const parent = node.parentElement;
1616
1350
  if (!parent) throw new Error("doesn't have a parent element.");
1617
1351
  const resizeObserver = new ResizeObserver(() => {
1618
- setResizedKeyWidth(node.clientWidth / Math.max(whiteKeyCount, 1) - keyGap);
1352
+ setResizedKeyWidth((0, _tremolo_ui_dom_internal.fitWhiteKeyWidth)(node.clientWidth, {
1353
+ noteRange: {
1354
+ first,
1355
+ last
1356
+ },
1357
+ keyGap,
1358
+ blackKeyWidthRatio
1359
+ }));
1619
1360
  });
1620
1361
  resizeObserver.observe(parent);
1621
1362
  return () => resizeObserver.disconnect();
1622
1363
  }, [
1623
1364
  resizable,
1624
1365
  node,
1625
- whiteKeyCount,
1626
- keyGap
1627
- ]);
1628
- const shortcutKeys = keyboardShortcuts?.keys;
1629
- const hasShortcuts = shortcutKeys !== void 0;
1630
- const shortcutMapping = shortcutKeys?.join("\0");
1631
- (0, react.useEffect)(() => releaseShortcutNotes, [
1632
- releaseShortcutNotes,
1633
- shortcutMapping,
1634
- keyboardShortcutsScope,
1635
- noteRange.first,
1636
- noteRange.last
1637
- ]);
1638
- /** The note a shortcut key plays, or null when it has none. */
1639
- function shortcutNote(key) {
1640
- if (!shortcutKeys || key === "") return null;
1641
- const index = shortcutKeys.indexOf(key);
1642
- const note = noteRange.first + index;
1643
- return index === -1 || note > noteRange.last ? null : note;
1644
- }
1645
- const shortcutTarget = (0, react.useMemo)(() => () => {
1646
- if (!hasShortcuts) return null;
1647
- return keyboardShortcutsScope === "window" ? globalThis.window : node;
1648
- }, [
1649
- hasShortcuts,
1650
- keyboardShortcutsScope,
1651
- node
1652
- ]);
1653
- const focusOutTarget = (0, react.useMemo)(() => () => hasShortcuts && keyboardShortcutsScope === "root" ? node : null, [
1654
- hasShortcuts,
1655
- keyboardShortcutsScope,
1656
- node
1366
+ first,
1367
+ last,
1368
+ keyGap,
1369
+ blackKeyWidthRatio
1657
1370
  ]);
1658
- const windowBlurTarget = (0, react.useMemo)(() => () => hasShortcuts ? globalThis.window : null, [hasShortcuts]);
1659
- useEventListener(shortcutTarget, "keydown", (e) => {
1660
- const key = shortcutKey(e);
1661
- if (e.repeat || shortcutNotes.current.has(key)) return;
1662
- if (isEditableTarget(e.target)) return;
1663
- const note = shortcutNote(e.key);
1664
- if (note === null) return;
1665
- const source = `keyboard:${key}`;
1666
- shortcutNotes.current.set(key, {
1667
- note,
1668
- source
1669
- });
1670
- instanceRef.current?.noteOn(note, { source });
1671
- });
1672
- useEventListener(shortcutTarget, "keyup", (e) => {
1673
- const key = shortcutKey(e);
1674
- const shortcut = shortcutNotes.current.get(key);
1675
- if (!shortcut) return;
1676
- shortcutNotes.current.delete(key);
1677
- instanceRef.current?.noteOff(shortcut.note, { source: shortcut.source });
1678
- });
1679
- useEventListener(focusOutTarget, "focusout", (e) => {
1680
- if (e.relatedTarget instanceof Node && node?.contains(e.relatedTarget)) return;
1681
- releaseShortcutNotes();
1682
- });
1683
- useEventListener(windowBlurTarget, "blur", releaseShortcutNotes);
1684
- (0, react.useImperativeHandle)(forwardedRef, () => ({
1371
+ const rootRefCallback = require_compose_refs.useComposedRefs(forwardedRef, setNode);
1372
+ (0, react.useImperativeHandle)(actionsRef, () => ({
1685
1373
  playNote: (note, velocity) => instanceRef.current?.noteOn(note, {
1686
1374
  source: "api",
1687
1375
  velocity
@@ -1689,14 +1377,13 @@ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ note
1689
1377
  stopNote: (note) => instanceRef.current?.noteOff(note, { source: "api" })
1690
1378
  }), []);
1691
1379
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1692
- ref: setNode,
1380
+ ref: rootRefCallback,
1693
1381
  className,
1694
1382
  "data-resizable": resizable ? "" : void 0,
1695
1383
  role: "group",
1696
1384
  tabIndex: 0,
1697
1385
  style: {
1698
- width: resizable ? "100%" : (0, _tremolo_ui_dom.pianoWidth)(layout),
1699
- "--height": cssLength(height),
1386
+ width: resizable ? "100%" : (0, _tremolo_ui_dom_internal.pianoWidth)(layout),
1700
1387
  position: "relative",
1701
1388
  ...style
1702
1389
  },
@@ -1723,7 +1410,7 @@ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ note
1723
1410
  zIndex: keyType === "white" ? 1 : 2,
1724
1411
  ...keyStyle,
1725
1412
  left: (0, _tremolo_ui_dom.notePosition)(note, layout),
1726
- width: keyType === "white" ? layout.whiteKeyWidth : (0, _tremolo_ui_dom.blackKeyWidth)(layout),
1413
+ width: keyType === "white" ? layout.whiteKeyWidth : (0, _tremolo_ui_dom_internal.blackKeyWidth)(layout),
1727
1414
  height: keyType === "white" ? "100%" : `${blackKeyHeightRatio * 100}%`
1728
1415
  },
1729
1416
  children: content !== "" && content !== null && content !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -1762,30 +1449,58 @@ function usePointsEditorContext(selector) {
1762
1449
  return selector ? selector(context) : context;
1763
1450
  }
1764
1451
  //#endregion
1452
+ //#region src/components/PointsEditor/Container.tsx
1453
+ const Container = /* @__PURE__ */ (0, react.forwardRef)(function Container({ children, className, style, ...props }, forwardedRef) {
1454
+ const { containerRef, disabled, selectable, editor } = usePointsEditorContext();
1455
+ const { refCallback: dragRefCallback } = useDragValue({
1456
+ axis: _tremolo_ui_dom_internal.POINT_AXIS,
1457
+ baseElementRef: containerRef,
1458
+ shouldStart: (event) => !disabled && !editor.isPointElement(event.target),
1459
+ onDragStart: ([x, y], state) => {
1460
+ editor.beginSelectionBox({
1461
+ x,
1462
+ y
1463
+ }, state.event);
1464
+ },
1465
+ onChange: ([x, y]) => editor.moveSelectionBox({
1466
+ x,
1467
+ y
1468
+ }),
1469
+ onDragEnd: () => editor.endSelectionBox()
1470
+ });
1471
+ useWheel((event) => {
1472
+ const move = (0, _tremolo_ui_dom_internal.wheelMove)(event);
1473
+ if (!move) return;
1474
+ const axis = move.axis === 0 ? "x" : "y";
1475
+ if (editor.nudgeFocusedPoint(axis, move.direction, event)) event.preventDefault();
1476
+ }, { target: containerRef });
1477
+ const composedRef = require_compose_refs.useComposedRefs(forwardedRef, containerRef, selectable && !disabled ? dragRefCallback : void 0);
1478
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1479
+ ref: composedRef,
1480
+ className,
1481
+ style: {
1482
+ position: "absolute",
1483
+ inset: 0,
1484
+ zIndex: 10,
1485
+ ...style
1486
+ },
1487
+ ...props,
1488
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Placement, {
1489
+ name: "PointsEditor.Container",
1490
+ children
1491
+ })
1492
+ });
1493
+ });
1494
+ //#endregion
1765
1495
  //#region src/components/_util/VisuallyHiddenRangeInput.tsx
1766
1496
  const VisuallyHiddenRangeInput = /* @__PURE__ */ (0, react.forwardRef)((props, ref) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1767
1497
  ...props,
1768
1498
  ref,
1769
1499
  type: "range",
1770
- style: visuallyHiddenStyle
1500
+ style: _tremolo_ui_dom_internal.visuallyHiddenStyle
1771
1501
  }));
1772
1502
  //#endregion
1773
1503
  //#region src/components/PointsEditor/Point.tsx
1774
- function clampPoint(point, min, max) {
1775
- const { x, y } = point;
1776
- return {
1777
- x: (0, _tremolo_ui_functions.clamp)(x, min?.x ?? 0, max?.x ?? 1),
1778
- y: (0, _tremolo_ui_functions.clamp)(y, min?.y ?? 0, max?.y ?? 1)
1779
- };
1780
- }
1781
- /**
1782
- * A point is placed by its position within the container, so its value is a
1783
- * position: 0..1 on each axis, with y growing downwards.
1784
- */
1785
- const AXIS = {
1786
- min: 0,
1787
- max: 1
1788
- };
1789
1504
  /**
1790
1505
  * One setting for both axes, or one per axis. The point holds a range input
1791
1506
  * for each, so anything that names or describes it comes in a pair.
@@ -1793,17 +1508,27 @@ const AXIS = {
1793
1508
  function perAxis(value, axis) {
1794
1509
  return typeof value === "string" ? value : value?.[axis];
1795
1510
  }
1796
- function Point({ value, children, id: idProp, min, max, size, width, height, color, disabled: _disabled, readonly: _readonly, wheel: _wheel, keyboard: _keyboard, "aria-label": ariaLabel, "aria-valuetext": ariaValuetext, onChange, onDragStart, onDragEnd, className, style, onPointerDown, onKeyDown, onFocus, ...props }) {
1797
- const { containerRef, externalStyles, disabled: rootDisabled, readonly: rootReadonly, wheel: rootWheel, keyboard: rootKeyboard, dragSensitivity, selection, registerPoint, beginPointDrag, movePointDrag, nudgeSelection } = usePointsEditorContext();
1511
+ function Point({ value, children, id: idProp, min, max, color, disabled: _disabled, readOnly: _readOnly, wheel: _wheel, keyboard: _keyboard, "aria-label": ariaLabel, "aria-valuetext": ariaValuetext, onChange, onChangeStart, onChangeEnd, changeEndDelay, className, style, onPointerDown, onKeyDown, onFocus, ...props }) {
1512
+ const { containerRef, dragCursor, disabled: rootDisabled, readOnly: rootReadOnly, wheel: rootWheel, keyboard: rootKeyboard, dragSensitivity, selection, editor } = usePointsEditorContext();
1798
1513
  const generatedId = (0, react.useId)();
1799
1514
  const id = idProp ?? generatedId;
1800
1515
  const selected = selection.includes(id);
1801
1516
  const disabled = _disabled ?? rootDisabled;
1802
- const readonly = _readonly ?? rootReadonly;
1803
- const inactive = disabled || readonly;
1517
+ const readOnly = _readOnly ?? rootReadOnly;
1518
+ const inactive = disabled || readOnly;
1804
1519
  const wheel = _wheel === void 0 ? rootWheel : _wheel;
1805
1520
  const keyboard = _keyboard === void 0 ? rootKeyboard : _keyboard;
1806
1521
  useCheckPlacement("PointsEditor.Point", "PointsEditor.Container");
1522
+ const current = (0, _tremolo_ui_dom_internal.clampPoint)(value, min, max);
1523
+ const gesture = useChangeGesture(current, {
1524
+ onChangeStart,
1525
+ onChangeEnd,
1526
+ changeEndDelay
1527
+ }, inactive);
1528
+ const handleChange = (next) => {
1529
+ gesture.changed(next);
1530
+ onChange?.(next);
1531
+ };
1807
1532
  const [element, setElement] = (0, react.useState)(null);
1808
1533
  const xInputRef = (0, react.useRef)(null);
1809
1534
  const yInputRef = (0, react.useRef)(null);
@@ -1812,9 +1537,10 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
1812
1537
  min,
1813
1538
  max,
1814
1539
  readonly: inactive,
1815
- onChange,
1540
+ onChange: onChange ? handleChange : void 0,
1816
1541
  element,
1817
- wheel
1542
+ wheel,
1543
+ beforeWheel: () => gesture.pulse("wheel")
1818
1544
  });
1819
1545
  (0, react.useEffect)(() => {
1820
1546
  registration.current = {
@@ -1822,82 +1548,62 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
1822
1548
  min,
1823
1549
  max,
1824
1550
  readonly: inactive,
1825
- onChange,
1551
+ onChange: onChange ? handleChange : void 0,
1826
1552
  element,
1827
- wheel
1553
+ wheel,
1554
+ beforeWheel: () => gesture.pulse("wheel")
1828
1555
  };
1829
1556
  });
1830
- (0, react.useEffect)(() => registerPoint(id, registration), [id, registerPoint]);
1557
+ (0, react.useEffect)(() => editor.registerPoint(id, () => registration.current), [id, editor]);
1831
1558
  /** Where the pointer was when the drag started, to measure the move from. */
1832
1559
  const pointerOrigin = (0, react.useRef)(null);
1833
1560
  const { refCallback: dragRefCallback, dragging } = useDragValue({
1834
- axis: AXIS,
1561
+ axis: _tremolo_ui_dom_internal.POINT_AXIS,
1835
1562
  baseElementRef: containerRef,
1836
- sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
1837
- cursor: inactive ? void 0 : externalStyles.cursor,
1563
+ sensitivity: (state) => (0, _tremolo_ui_dom_internal.selectModifier)(dragSensitivity, state.event).value,
1564
+ cursor: inactive ? void 0 : dragCursor,
1838
1565
  shouldStart: () => !disabled,
1839
1566
  onChange: ([x, y]) => {
1840
1567
  const origin = pointerOrigin.current;
1841
1568
  if (!origin) return;
1842
- movePointDrag({
1569
+ editor.movePointDrag({
1843
1570
  x: x - origin.x,
1844
1571
  y: y - origin.y
1845
1572
  });
1846
1573
  },
1847
1574
  onDragStart: ([x, y], state) => {
1848
- beginPointDrag(id, state.event);
1575
+ editor.beginPointDrag(id, state.event);
1849
1576
  pointerOrigin.current = {
1850
1577
  x,
1851
1578
  y
1852
1579
  };
1853
1580
  xInputRef.current?.focus();
1854
- if (inactive) return;
1855
- onDragStart?.(clampPoint(value, min, max));
1581
+ if (!inactive) gesture.hold("pointer");
1856
1582
  },
1857
1583
  onDragEnd: () => {
1858
1584
  pointerOrigin.current = null;
1859
- if (inactive) return;
1860
- onDragEnd?.(clampPoint(value, min, max));
1585
+ gesture.end();
1861
1586
  }
1862
1587
  });
1863
- const nudge = (0, react.useCallback)((axis, direction, option, modifiers) => {
1864
- const next = (0, _tremolo_ui_dom.applyDelta)(value[axis], direction, option, AXIS, modifiers);
1865
- nudgeSelection(id, {
1866
- x: axis === "x" ? next - value.x : 0,
1867
- y: axis === "y" ? next - value.y : 0
1868
- });
1869
- }, [
1870
- value,
1871
- id,
1872
- nudgeSelection
1873
- ]);
1874
1588
  const refCallback = require_compose_refs.useComposedRefs(dragRefCallback, setElement);
1875
1589
  const handleKeyDown = (event) => {
1876
- const key = event.key;
1877
- if (![
1878
- "ArrowRight",
1879
- "ArrowLeft",
1880
- "ArrowUp",
1881
- "ArrowDown"
1882
- ].includes(key)) return;
1883
- const axis = key === "ArrowRight" || key === "ArrowLeft" ? "x" : "y";
1590
+ const move = (0, _tremolo_ui_dom_internal.arrowKeyMove)(event.key);
1591
+ if (!move) return;
1884
1592
  event.preventDefault();
1885
1593
  if (!onChange || inactive || !keyboard) return;
1886
- nudge(axis, key === "ArrowLeft" || key === "ArrowUp" ? -1 : 1, keyboard, event);
1594
+ gesture.pulse("keyboard");
1595
+ editor.nudgePoint(id, move.axis === 0 ? "x" : "y", move.direction, keyboard, event);
1887
1596
  };
1888
- const current = clampPoint(value, min, max);
1889
1597
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1890
1598
  ref: refCallback,
1891
1599
  className,
1892
1600
  "data-disabled": disabled ? "" : void 0,
1893
- "data-readonly": readonly ? "" : void 0,
1601
+ "data-readonly": readOnly ? "" : void 0,
1894
1602
  tabIndex: -1,
1895
1603
  "data-dragging": dragging ? "" : void 0,
1896
1604
  "data-selected": selected ? "" : void 0,
1897
1605
  style: {
1898
1606
  "--color": color,
1899
- "--width": cssLength(size ?? width),
1900
- "--height": cssLength(size ?? height),
1901
1607
  position: "absolute",
1902
1608
  translate: "var(--translate, -50% -50%)",
1903
1609
  ...style,
@@ -1922,17 +1628,17 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
1922
1628
  max: max?.[axis] ?? 1,
1923
1629
  step: "any",
1924
1630
  disabled,
1925
- "aria-readonly": readonly,
1631
+ "aria-readonly": readOnly,
1926
1632
  "aria-orientation": axis === "x" ? "horizontal" : "vertical",
1927
1633
  "aria-label": perAxis(ariaLabel, axis) ?? axis,
1928
1634
  "aria-valuetext": perAxis(ariaValuetext, axis),
1929
1635
  onChange: (event) => {
1930
- if (readonly) {
1636
+ if (readOnly) {
1931
1637
  event.currentTarget.value = String(current[axis]);
1932
1638
  return;
1933
1639
  }
1934
1640
  const delta = event.currentTarget.valueAsNumber - value[axis];
1935
- nudgeSelection(id, {
1641
+ editor.nudgeSelection(id, {
1936
1642
  x: axis === "x" ? delta : 0,
1937
1643
  y: axis === "y" ? delta : 0
1938
1644
  });
@@ -1941,50 +1647,6 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
1941
1647
  });
1942
1648
  }
1943
1649
  //#endregion
1944
- //#region src/components/PointsEditor/Container.tsx
1945
- const Container = /* @__PURE__ */ (0, react.forwardRef)(function Container({ children, className, style, ...props }, forwardedRef) {
1946
- const { containerRef, disabled, selectable, isPointElement, nudgeFocusedPoint, beginSelectionBox, moveSelectionBox, endSelectionBox } = usePointsEditorContext();
1947
- const { refCallback: dragRefCallback } = useDragValue({
1948
- axis: AXIS,
1949
- baseElementRef: containerRef,
1950
- shouldStart: (event) => !disabled && !isPointElement(event.target),
1951
- onDragStart: ([x, y], state) => {
1952
- beginSelectionBox({
1953
- x,
1954
- y
1955
- }, state.event);
1956
- },
1957
- onChange: ([x, y]) => moveSelectionBox({
1958
- x,
1959
- y
1960
- }),
1961
- onDragEnd: endSelectionBox
1962
- });
1963
- useWheel((event) => {
1964
- const horizontal = event.deltaX !== 0;
1965
- const delta = horizontal ? event.deltaX : event.deltaY;
1966
- if (delta === 0) return;
1967
- const axis = horizontal || event.shiftKey ? "x" : "y";
1968
- if (nudgeFocusedPoint(axis, delta < 0 ? -1 : 1, event)) event.preventDefault();
1969
- }, { target: containerRef });
1970
- const composedRef = require_compose_refs.useComposedRefs(forwardedRef, containerRef, selectable && !disabled ? dragRefCallback : void 0);
1971
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1972
- ref: composedRef,
1973
- className,
1974
- style: {
1975
- position: "absolute",
1976
- inset: 0,
1977
- zIndex: 10,
1978
- ...style
1979
- },
1980
- ...props,
1981
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Placement, {
1982
- name: "PointsEditor.Container",
1983
- children
1984
- })
1985
- });
1986
- });
1987
- //#endregion
1988
1650
  //#region src/components/PointsEditor/SelectionBox.tsx
1989
1651
  /**
1990
1652
  * The box a drag on empty space draws, and what it covers is selected.
@@ -2017,220 +1679,50 @@ function SelectionBox({ children, className, style, ...props }) {
2017
1679
  /** One array for every editor with selection turned off, so memos hold still. */
2018
1680
  const EMPTY = [];
2019
1681
  /**
2020
- * How far a point may move before something in the selection leaves its range.
2021
- *
2022
- * Clamping each point on its own would break the shape of the selection: the
2023
- * one that reached the edge would stop while the rest carried on. One amount
2024
- * for all of them means the whole selection stops together.
2025
- */
2026
- function allowedDelta(delta, entries) {
2027
- let loX = -Infinity;
2028
- let hiX = Infinity;
2029
- let loY = -Infinity;
2030
- let hiY = Infinity;
2031
- for (const { start, registration } of entries) {
2032
- loX = Math.max(loX, (registration.min?.x ?? 0) - start.x);
2033
- hiX = Math.min(hiX, (registration.max?.x ?? 1) - start.x);
2034
- loY = Math.max(loY, (registration.min?.y ?? 0) - start.y);
2035
- hiY = Math.min(hiY, (registration.max?.y ?? 1) - start.y);
2036
- }
2037
- return {
2038
- x: hiX < loX ? 0 : (0, _tremolo_ui_functions.clamp)(delta.x, loX, hiX),
2039
- y: hiY < loY ? 0 : (0, _tremolo_ui_functions.clamp)(delta.y, loY, hiY)
2040
- };
2041
- }
2042
- /**
2043
- * A point moves over 0..1 in both axes, so a nudge of 0.01 crosses the editor
2044
- * in a hundred steps whatever its pixel size.
2045
- */
2046
- const DEFAULT_WHEEL = ["normalized", .01];
2047
- /**
2048
- * Shift is the fine-adjustment key everywhere else, so it is bound here too
2049
- * — but only on the keyboard. On the wheel it already means the x axis, and
2050
- * browsers hand shift+wheel over as horizontal scrolling anyway.
2051
- */
2052
- const DEFAULT_KEYBOARD = {
2053
- default: ["normalized", .01],
2054
- shift: ["normalized", .001]
2055
- };
2056
- /**
2057
1682
  * Multiple Point Controller
2058
1683
  */
2059
1684
  const PointsEditor = {
2060
- Root: /* @__PURE__ */ (0, react.forwardRef)(({ width, height, disabled = false, readonly = false, wheel = DEFAULT_WHEEL, keyboard = DEFAULT_KEYBOARD, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, selectable = false, selection: selectionProp, defaultSelection, onSelectionChange, externalStyles, style, className, children, ...props }, forwardedRef) => {
1685
+ Root: /* @__PURE__ */ (0, react.forwardRef)(({ disabled = false, readOnly = false, wheel = _tremolo_ui_dom_internal.POINTS_EDITOR_DEFAULT_WHEEL, keyboard = _tremolo_ui_dom_internal.POINTS_EDITOR_DEFAULT_KEYBOARD, dragSensitivity = _tremolo_ui_dom.DEFAULT_DRAG_SENSITIVITY, selectable = false, selection: selectionProp, defaultSelection, onSelectionChange, dragCursor = "grabbing", style, className, children, ...props }, forwardedRef) => {
2061
1686
  const containerRef = (0, react.useRef)(null);
2062
- const { cursor = "grabbing" } = externalStyles ?? {};
2063
1687
  const controlled = selectionProp !== void 0;
2064
1688
  const [ownSelection, setOwnSelection] = (0, react.useState)(defaultSelection ?? []);
2065
1689
  const selection = selectable ? selectionProp ?? ownSelection : EMPTY;
2066
- const selectionRef = (0, react.useRef)(selection);
2067
- const changeHandlerRef = (0, react.useRef)(onSelectionChange);
2068
- (0, react.useEffect)(() => {
2069
- selectionRef.current = selection;
2070
- changeHandlerRef.current = onSelectionChange;
2071
- });
2072
- const changeSelection = (0, react.useCallback)((next) => {
2073
- selectionRef.current = next;
2074
- if (!controlled) setOwnSelection(next);
2075
- changeHandlerRef.current?.(next);
2076
- }, [controlled]);
2077
- /**
2078
- * Every mounted point, by id. A registration is a ref rather than a value:
2079
- * the value inside changes on every frame of a drag, and a registry keyed
2080
- * on it would be rebuilt just as often.
2081
- */
2082
- const points = (0, react.useRef)(/* @__PURE__ */ new Map());
2083
- const registerPoint = (0, react.useCallback)((id, entry) => {
2084
- points.current.set(id, entry);
2085
- return () => {
2086
- points.current.delete(id);
2087
- };
2088
- }, []);
2089
- /** What the current drag picked up, and where those points started. */
2090
- const dragRef = (0, react.useRef)([]);
2091
- const snapshot = (0, react.useCallback)((ids) => {
2092
- return ids.flatMap((id) => {
2093
- const registration = points.current.get(id)?.current;
2094
- return registration ? [{
2095
- id,
2096
- start: { ...registration.value }
2097
- }] : [];
2098
- });
2099
- }, []);
2100
- const applyDeltaTo = (0, react.useCallback)((entries, delta) => {
2101
- const withRegistration = entries.flatMap((entry) => {
2102
- const registration = points.current.get(entry.id)?.current;
2103
- return registration ? [{
2104
- ...entry,
2105
- registration
2106
- }] : [];
2107
- });
2108
- const allowed = allowedDelta(delta, withRegistration);
2109
- for (const { start, registration } of withRegistration) {
2110
- if (registration.readonly) continue;
2111
- registration.onChange?.({
2112
- x: (0, _tremolo_ui_functions.toPrecision)(start.x + allowed.x),
2113
- y: (0, _tremolo_ui_functions.toPrecision)(start.y + allowed.y)
2114
- });
2115
- }
2116
- }, []);
2117
- const beginPointDrag = (0, react.useCallback)((id, modifiers) => {
2118
- if (!selectable) {
2119
- dragRef.current = snapshot([id]);
2120
- return;
2121
- }
2122
- const current = selectionRef.current;
2123
- const additive = modifiers.ctrlKey || modifiers.metaKey;
2124
- let next;
2125
- if (additive) next = current.includes(id) ? current.filter((x) => x !== id) : [...current, id];
2126
- else if (current.includes(id)) next = current;
2127
- else next = [id];
2128
- changeSelection([...next]);
2129
- dragRef.current = next.includes(id) ? snapshot(next) : [];
2130
- }, [
2131
- selectable,
2132
- changeSelection,
2133
- snapshot
2134
- ]);
2135
- const movePointDrag = (0, react.useCallback)((delta) => applyDeltaTo(dragRef.current, delta), [applyDeltaTo]);
2136
- const nudgeSelection = (0, react.useCallback)((id, delta) => {
2137
- const ids = selectionRef.current.includes(id) ? selectionRef.current : [id];
2138
- applyDeltaTo(snapshot(ids), delta);
2139
- }, [applyDeltaTo, snapshot]);
2140
- const isPointElement = (0, react.useCallback)((element) => {
2141
- if (!element) return false;
2142
- for (const [, entry] of points.current) if (entry.current.element?.contains(element)) return true;
2143
- return false;
2144
- }, []);
2145
- const nudgeFocusedPoint = (0, react.useCallback)((axis, direction, modifiers) => {
2146
- const container = containerRef.current;
2147
- const active = container?.ownerDocument.activeElement;
2148
- if (!container || !active || !container.contains(active)) return false;
2149
- for (const [id, entry] of points.current) {
2150
- const { element, wheel, readonly, onChange, value } = entry.current;
2151
- if (!element?.contains(active)) continue;
2152
- if (!wheel || readonly || !onChange) return false;
2153
- const next = (0, _tremolo_ui_dom.applyDelta)(value[axis], direction, wheel, AXIS, modifiers);
2154
- nudgeSelection(id, {
2155
- x: axis === "x" ? next - value.x : 0,
2156
- y: axis === "y" ? next - value.y : 0
2157
- });
2158
- return true;
2159
- }
2160
- return false;
2161
- }, [nudgeSelection]);
2162
1690
  const [selectionBox, setSelectionBox] = (0, react.useState)(null);
2163
- const selectionBoxRef = (0, react.useRef)(null);
2164
- selectionBoxRef.current ??= (0, _tremolo_ui_dom.createSelectionBox)({
2165
- *items() {
2166
- for (const [id, entry] of points.current) {
2167
- const { x, y } = entry.current.value;
2168
- yield [id, [x, y]];
2169
- }
2170
- },
2171
- onBoxChange: setSelectionBox
2172
- });
2173
- (0, react.useEffect)(() => {
2174
- selectionBoxRef.current?.update({ onSelectionChange: changeSelection });
2175
- }, [changeSelection]);
1691
+ const [editor] = (0, react.useState)(() => (0, _tremolo_ui_dom.createPointsEditor)({ onSelectionBoxChange: setSelectionBox }));
2176
1692
  (0, react.useEffect)(() => {
2177
- const box = selectionBoxRef.current;
2178
- return () => box?.destroy();
2179
- }, []);
2180
- const beginSelectionBox = (0, react.useCallback)((at, modifiers) => {
2181
- if (!selectable) return;
2182
- selectionBoxRef.current?.begin([at.x, at.y], {
2183
- additive: modifiers.ctrlKey || modifiers.metaKey,
2184
- selection: selectionRef.current
1693
+ editor.update({
1694
+ selectable,
1695
+ selection,
1696
+ onSelectionChange: (next) => {
1697
+ if (!controlled) setOwnSelection(next);
1698
+ onSelectionChange?.(next);
1699
+ }
2185
1700
  });
2186
- }, [selectable]);
2187
- const moveSelectionBox = (0, react.useCallback)((to) => {
2188
- selectionBoxRef.current?.move([to.x, to.y]);
2189
- }, []);
2190
- const endSelectionBox = (0, react.useCallback)(() => {
2191
- if (!selectionBoxRef.current?.end()) return;
2192
- const [first] = selectionRef.current;
2193
- if (first) points.current.get(first)?.current.element?.focus();
2194
- }, []);
1701
+ });
1702
+ (0, react.useEffect)(() => () => editor.destroy(), [editor]);
2195
1703
  const context = (0, react.useMemo)(() => ({
2196
1704
  disabled,
2197
- readonly,
1705
+ readOnly,
2198
1706
  wheel,
2199
1707
  keyboard,
2200
1708
  dragSensitivity,
2201
- externalStyles: { cursor },
1709
+ dragCursor,
2202
1710
  containerRef,
2203
1711
  selectable,
2204
1712
  selection,
2205
- registerPoint,
2206
- isPointElement,
2207
- beginPointDrag,
2208
- movePointDrag,
2209
- nudgeSelection,
2210
- nudgeFocusedPoint,
2211
- selectionBox,
2212
- beginSelectionBox,
2213
- moveSelectionBox,
2214
- endSelectionBox
1713
+ editor,
1714
+ selectionBox
2215
1715
  }), [
2216
1716
  disabled,
2217
- readonly,
1717
+ readOnly,
2218
1718
  wheel,
2219
1719
  keyboard,
2220
1720
  dragSensitivity,
2221
- cursor,
1721
+ dragCursor,
2222
1722
  selectable,
2223
1723
  selection,
2224
- registerPoint,
2225
- isPointElement,
2226
- beginPointDrag,
2227
- movePointDrag,
2228
- nudgeSelection,
2229
- nudgeFocusedPoint,
2230
- selectionBox,
2231
- beginSelectionBox,
2232
- moveSelectionBox,
2233
- endSelectionBox
1724
+ editor,
1725
+ selectionBox
2234
1726
  ]);
2235
1727
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PointsEditorProvider, {
2236
1728
  value: context,
@@ -2238,10 +1730,8 @@ const PointsEditor = {
2238
1730
  ref: forwardedRef,
2239
1731
  className,
2240
1732
  "data-disabled": disabled ? "" : void 0,
2241
- "data-readonly": readonly ? "" : void 0,
1733
+ "data-readonly": readOnly ? "" : void 0,
2242
1734
  style: {
2243
- "--width": cssLength(width),
2244
- "--height": cssLength(height),
2245
1735
  position: "relative",
2246
1736
  ...style
2247
1737
  },
@@ -2266,36 +1756,30 @@ function useSliderContext(selector) {
2266
1756
  }
2267
1757
  //#endregion
2268
1758
  //#region src/components/Slider/MarksOption.tsx
2269
- function MarksOption({ value, mark = true, label, thickness, length, gap, labelWidth, classes, styles, className, style, ...props }) {
1759
+ function MarksOption({ value, mark = true, label, thickness, length, gap, classes, styles, className, style, ...props }) {
2270
1760
  useCheckPlacement("Slider.MarksOption", "Slider.Marks");
2271
1761
  const min = useSliderContext((s) => s.min);
2272
1762
  const max = useSliderContext((s) => s.max);
2273
1763
  const scale = useSliderContext((s) => s.scale);
2274
- const vertical = useSliderContext((s) => s.vertical);
1764
+ const vertical = useSliderContext((s) => s.orientation === "vertical");
2275
1765
  const reverse = useSliderContext((s) => s.reverse);
2276
- const calcPercent = (0, react.useCallback)((value) => {
2277
- const percent = scale.normalize(value, min, max) * 100;
2278
- return (0, _tremolo_ui_functions.toFixed)(vertical !== reverse ? 100 - percent : percent);
2279
- }, [
2280
- vertical,
2281
- reverse,
2282
- max,
1766
+ const percent = (0, _tremolo_ui_dom.valuePercent)(value, {
2283
1767
  min,
1768
+ max,
2284
1769
  scale
2285
- ]);
1770
+ }, vertical !== reverse);
2286
1771
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2287
1772
  className,
2288
1773
  style: {
2289
- "--thickness": cssLength(thickness),
2290
- "--length": cssLength(length),
2291
- "--gap": cssLength(gap),
2292
- "--label-width": cssLength(labelWidth),
1774
+ "--thickness": (0, _tremolo_ui_dom_internal.cssLength)(thickness),
1775
+ "--length": (0, _tremolo_ui_dom_internal.cssLength)(length),
1776
+ "--gap": (0, _tremolo_ui_dom_internal.cssLength)(gap),
2293
1777
  position: "absolute",
2294
1778
  translate: vertical ? "var(--translate, 0 -50%)" : "var(--translate, -50% 0)",
2295
1779
  zIndex: 10,
2296
1780
  ...style,
2297
- left: !vertical ? `${calcPercent(value)}%` : void 0,
2298
- top: vertical ? `${calcPercent(value)}%` : void 0
1781
+ left: !vertical ? `${percent}%` : void 0,
1782
+ top: vertical ? `${percent}%` : void 0
2299
1783
  },
2300
1784
  "data-orientation": vertical ? "vertical" : "horizontal",
2301
1785
  ...props,
@@ -2311,58 +1795,20 @@ function MarksOption({ value, mark = true, label, thickness, length, gap, labelW
2311
1795
  });
2312
1796
  }
2313
1797
  //#endregion
2314
- //#region src/components/_util/decimal-digits.ts
2315
- /**
2316
- * How many digits a number carries after the decimal point.
2317
- *
2318
- * `Slider.Marks` builds its labels by multiplying an interval, which leaves
2319
- * binary debris in the last digits (`0.1 * 3` is `0.30000000000000004`). The
2320
- * count is what tells `toFixed` how far to round that back.
2321
- *
2322
- * The exponent form has to be handled separately: `String(1e-7)` is `'1e-7'`,
2323
- * which has no decimal point at all, so reading the text after the point
2324
- * reports no digits and the interval rounds away to whole numbers.
2325
- */
2326
- function decimalDigits(x) {
2327
- if (!Number.isFinite(x)) return 0;
2328
- const text = String(x);
2329
- const e = text.indexOf("e");
2330
- if (e === -1) return text.split(".")[1]?.length ?? 0;
2331
- const fraction = text.slice(0, e).split(".")[1]?.length ?? 0;
2332
- return Math.max(0, fraction - Number(text.slice(e + 1)));
2333
- }
2334
- //#endregion
2335
- //#region src/components/Slider/type.ts
2336
- function generateOptionsList(options, min, max, step) {
2337
- const { per, mark = true, label = true } = typeof options === "object" ? options : { per: options };
2338
- const optionsList = [];
2339
- const interval = per === "step" ? step : per;
2340
- const count = Math.floor(max / interval) - Math.ceil(min / interval) + 1;
2341
- for (let i = 0; i < count; i++) {
2342
- const value = (0, _tremolo_ui_functions.toFixed)(interval * (Math.ceil(min / interval) + i), decimalDigits(interval));
2343
- optionsList.push({
2344
- value,
2345
- mark,
2346
- label
2347
- });
2348
- }
2349
- return optionsList;
2350
- }
2351
- //#endregion
2352
1798
  //#region src/components/Slider/Marks.tsx
2353
1799
  function Marks({ gap, options, children, className, style, ...props }) {
2354
1800
  const min = useSliderContext((s) => s.min);
2355
1801
  const max = useSliderContext((s) => s.max);
2356
1802
  const step = useSliderContext((s) => s.step);
2357
- const vertical = useSliderContext((s) => s.vertical);
1803
+ const vertical = useSliderContext((s) => s.orientation === "vertical");
2358
1804
  const reverse = useSliderContext((s) => s.reverse);
2359
- const optionsList = options ? generateOptionsList(options, min, max, step) : [];
1805
+ const optionsList = options ? (0, _tremolo_ui_dom_internal.sliderMarks)(options, min, max, step) : [];
2360
1806
  if (vertical !== reverse) optionsList.reverse();
2361
1807
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2362
1808
  className,
2363
1809
  "data-orientation": vertical ? "vertical" : "horizontal",
2364
1810
  style: {
2365
- "--gap": cssLength(gap),
1811
+ "--gap": (0, _tremolo_ui_dom_internal.cssLength)(gap),
2366
1812
  position: "relative",
2367
1813
  ...style
2368
1814
  },
@@ -2381,9 +1827,9 @@ function Marks({ gap, options, children, className, style, ...props }) {
2381
1827
  }
2382
1828
  //#endregion
2383
1829
  //#region src/components/Slider/Thumb.tsx
2384
- const Thumb$1 = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, children, className, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-valuetext": ariaValuetext, ...props }, forwardedRef) {
1830
+ const Thumb$1 = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, children, className, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-valuetext": ariaValuetext, actionsRef, ...props }, forwardedRef) {
2385
1831
  const inputRef = (0, react.useRef)(null);
2386
- const { value, min, max, step, vertical, disabled, readonly, onChange, percent, thumbRef } = useSliderContext();
1832
+ const { value, min, max, step, orientation, disabled, readOnly, onChange, percent, thumbRef } = useSliderContext();
2387
1833
  useCheckPlacement("Slider.Thumb", "Slider.Track");
2388
1834
  const methods = () => ({
2389
1835
  focus() {
@@ -2393,12 +1839,13 @@ const Thumb$1 = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, ch
2393
1839
  inputRef.current?.blur();
2394
1840
  }
2395
1841
  });
2396
- (0, react.useImperativeHandle)(forwardedRef, methods, [disabled]);
1842
+ (0, react.useImperativeHandle)(actionsRef, methods, [disabled]);
2397
1843
  (0, react.useImperativeHandle)(thumbRef, methods, [disabled]);
2398
1844
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1845
+ ref: forwardedRef,
2399
1846
  className,
2400
1847
  "data-disabled": disabled ? "" : void 0,
2401
- "data-readonly": readonly ? "" : void 0,
1848
+ "data-readonly": readOnly ? "" : void 0,
2402
1849
  ...props,
2403
1850
  style: {
2404
1851
  "--color": color,
@@ -2406,8 +1853,8 @@ const Thumb$1 = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, ch
2406
1853
  translate: "var(--translate, -50% -50%)",
2407
1854
  zIndex: 100,
2408
1855
  ...style,
2409
- top: vertical ? `${percent}%` : "50%",
2410
- left: !vertical ? `${percent}%` : "50%"
1856
+ top: orientation === "vertical" ? `${percent}%` : "50%",
1857
+ left: orientation === "vertical" ? "50%" : `${percent}%`
2411
1858
  },
2412
1859
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(VisuallyHiddenRangeInput, {
2413
1860
  ref: inputRef,
@@ -2416,14 +1863,14 @@ const Thumb$1 = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, ch
2416
1863
  max,
2417
1864
  step,
2418
1865
  disabled,
2419
- "aria-readonly": readonly,
2420
- "aria-orientation": vertical ? "vertical" : "horizontal",
1866
+ "aria-readonly": readOnly,
1867
+ "aria-orientation": orientation,
2421
1868
  "aria-label": ariaLabel,
2422
1869
  "aria-labelledby": ariaLabelledby,
2423
1870
  "aria-describedby": ariaDescribedby,
2424
1871
  "aria-valuetext": ariaValuetext,
2425
1872
  onChange: (event) => {
2426
- if (readonly) {
1873
+ if (readOnly) {
2427
1874
  event.currentTarget.value = String(value);
2428
1875
  return;
2429
1876
  }
@@ -2434,20 +1881,20 @@ const Thumb$1 = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, ch
2434
1881
  });
2435
1882
  //#endregion
2436
1883
  //#region src/components/Slider/Track.tsx
2437
- const Track = /* @__PURE__ */ (0, react.forwardRef)(function Track({ length, thickness, active, inactive, children, className, style, ...props }, forwardedRef) {
2438
- const { vertical, reverse, disabled, percent, trackRef } = useSliderContext();
1884
+ const Track = /* @__PURE__ */ (0, react.forwardRef)(function Track({ length, thickness, activeColor, inactiveColor, children, className, style, ...props }, forwardedRef) {
1885
+ const { orientation, reverse, disabled, percent, trackRef } = useSliderContext();
2439
1886
  const composedRef = require_compose_refs.useComposedRefs(forwardedRef, trackRef);
2440
1887
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2441
1888
  ref: composedRef,
2442
1889
  className,
2443
1890
  "data-disabled": disabled ? "" : void 0,
2444
- "data-orientation": vertical ? "vertical" : "horizontal",
2445
- "data-flipped": vertical !== reverse ? "" : void 0,
1891
+ "data-orientation": orientation,
1892
+ "data-flipped": orientation === "vertical" !== reverse ? "" : void 0,
2446
1893
  style: {
2447
- "--active": active,
2448
- "--inactive": inactive,
2449
- "--length": cssLength(length),
2450
- "--thickness": cssLength(thickness),
1894
+ "--active-color": activeColor,
1895
+ "--inactive-color": inactiveColor,
1896
+ "--length": (0, _tremolo_ui_dom_internal.cssLength)(length),
1897
+ "--thickness": (0, _tremolo_ui_dom_internal.cssLength)(thickness),
2451
1898
  "--percent": `${percent}%`,
2452
1899
  position: "relative",
2453
1900
  ...style
@@ -2461,7 +1908,6 @@ const Track = /* @__PURE__ */ (0, react.forwardRef)(function Track({ length, thi
2461
1908
  });
2462
1909
  //#endregion
2463
1910
  //#region src/components/Slider/index.tsx
2464
- const defaultExternalStyles$1 = { cursor: "pointer" };
2465
1911
  /**
2466
1912
  * The wheel only acts while the focus is inside, so that scrolling a page past
2467
1913
  * the control does not change its value.
@@ -2471,18 +1917,17 @@ const WHEEL_OPTIONS$1 = { requireFocus: true };
2471
1917
  * Customizable slider
2472
1918
  */
2473
1919
  const Slider = {
2474
- Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, vertical = false, reverse = false, externalStyles: _externalStyles, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, className, style, children, onFocus, onBlur, onPointerDown, onKeyDown, ...props }, forwardedRef) => {
1920
+ Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, orientation = "horizontal", reverse = false, dragCursor = "pointer", wheel = _tremolo_ui_dom.DEFAULT_WHEEL_OPTIONS, keyboard = _tremolo_ui_dom.DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = _tremolo_ui_dom.DEFAULT_DRAG_SENSITIVITY, disabled = false, readOnly = false, onChange, onChangeStart, onChangeEnd, changeEndDelay, className, style, children, onFocus, onBlur, onPointerDown, onKeyDown, actionsRef, ...props }, forwardedRef) => {
2475
1921
  const trackRef = (0, react.useRef)(null);
2476
1922
  const thumbRef = (0, react.useRef)(null);
2477
- const externalStyles = {
2478
- ...defaultExternalStyles$1,
2479
- ..._externalStyles
2480
- };
2481
- const inactive = disabled || readonly;
2482
- const p = (0, _tremolo_ui_functions.toFixed)(scale.normalize(value, min, max) * 100);
2483
- const rev = (0, _tremolo_ui_functions.toFixed)(100 - p);
1923
+ const inactive = disabled || readOnly;
1924
+ const vertical = orientation === "vertical";
2484
1925
  const displayReversed = vertical !== reverse;
2485
- const percent = displayReversed ? rev : p;
1926
+ const percent = (0, _tremolo_ui_dom.valuePercent)(value, {
1927
+ min,
1928
+ max,
1929
+ scale
1930
+ }, displayReversed);
2486
1931
  const axis = (0, react.useMemo)(() => ({
2487
1932
  min,
2488
1933
  max,
@@ -2502,72 +1947,70 @@ const Slider = {
2502
1947
  keyboard,
2503
1948
  wheel
2504
1949
  });
1950
+ const gesture = useChangeGesture(value, {
1951
+ onChangeStart,
1952
+ onChangeEnd,
1953
+ changeEndDelay
1954
+ }, inactive);
1955
+ const change = (0, react.useCallback)((next) => {
1956
+ gesture.changed(next);
1957
+ onChange?.(next);
1958
+ }, [gesture, onChange]);
2505
1959
  const handleKeyDown = (0, react.useCallback)((event) => {
2506
- const key = event.key;
2507
- if (![
2508
- "ArrowRight",
2509
- "ArrowLeft",
2510
- "ArrowUp",
2511
- "ArrowDown"
2512
- ].includes(key)) return;
1960
+ const direction = (0, _tremolo_ui_dom_internal.arrowKeyDirection)(event.key);
1961
+ if (direction === null) return;
2513
1962
  event.preventDefault();
2514
1963
  if (!keyboard || !onChange || inactive) return;
2515
- let direction = key === "ArrowRight" || key === "ArrowUp" ? 1 : -1;
2516
- if (reverse) direction *= -1;
2517
- onChange((0, _tremolo_ui_dom.applyDelta)(value, direction, keyboard, axis, event));
1964
+ gesture.pulse("keyboard");
1965
+ change((0, _tremolo_ui_dom_internal.applyDelta)(value, reverse ? -direction : direction, keyboard, axis, event));
2518
1966
  }, [
2519
1967
  keyboard,
2520
1968
  onChange,
2521
1969
  inactive,
2522
1970
  reverse,
2523
1971
  value,
2524
- axis
1972
+ axis,
1973
+ gesture,
1974
+ change
2525
1975
  ]);
2526
1976
  const valueOf = (v) => v[vertical ? 1 : 0];
2527
1977
  const { refCallback: dragRefCallback } = useDragValue({
2528
1978
  axis,
2529
1979
  baseElementRef: trackRef,
2530
- cursor: inactive ? void 0 : externalStyles.cursor,
1980
+ cursor: inactive ? void 0 : dragCursor,
2531
1981
  shouldStart: () => !inactive,
2532
- sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
1982
+ sensitivity: (state) => (0, _tremolo_ui_dom_internal.selectModifier)(dragSensitivity, state.event).value,
2533
1983
  updateOnPointerDown: true,
2534
1984
  onChange: (v) => {
2535
1985
  if (inactive) return;
2536
- onChange?.(valueOf(v));
1986
+ change(valueOf(v));
2537
1987
  },
2538
- onDragStart: (v) => {
1988
+ onDragStart: () => {
2539
1989
  if (inactive) return;
1990
+ gesture.hold("pointer");
2540
1991
  thumbRef.current?.focus();
2541
- onDragStart?.(valueOf(v));
2542
1992
  },
2543
- onDragEnd: (v) => {
2544
- if (inactive) return;
2545
- onDragEnd?.(valueOf(v));
2546
- }
1993
+ onDragEnd: () => gesture.end()
2547
1994
  });
2548
1995
  const wheelRefCallback = useWheel((event) => {
2549
1996
  if (!wheel || !onChange || inactive) return;
1997
+ const direction = (0, _tremolo_ui_dom_internal.wheelDirection)(event, { horizontal: !vertical });
1998
+ if (direction === null) return;
2550
1999
  event.preventDefault();
2551
- let direction;
2552
- if (!vertical && event.deltaX !== 0) direction = event.deltaX > 0 ? 1 : -1;
2553
- else {
2554
- if (event.deltaY === 0) return;
2555
- direction = event.deltaY > 0 ? -1 : 1;
2556
- }
2557
- if (reverse) direction *= -1;
2558
- onChange((0, _tremolo_ui_dom.applyDelta)(value, direction, wheel, axis, event));
2000
+ gesture.pulse("wheel");
2001
+ change((0, _tremolo_ui_dom_internal.applyDelta)(value, reverse ? -direction : direction, wheel, axis, event));
2559
2002
  }, WHEEL_OPTIONS$1);
2560
- const rootRefCallback = require_compose_refs.useComposedRefs(dragRefCallback, wheelRefCallback);
2003
+ const rootRefCallback = require_compose_refs.useComposedRefs(forwardedRef, dragRefCallback, wheelRefCallback);
2561
2004
  const context = (0, react.useMemo)(() => ({
2562
2005
  value,
2563
2006
  min,
2564
2007
  max,
2565
2008
  step,
2566
2009
  scale,
2567
- vertical,
2010
+ orientation,
2568
2011
  reverse,
2569
2012
  disabled,
2570
- readonly,
2013
+ readOnly,
2571
2014
  onChange,
2572
2015
  percent,
2573
2016
  trackRef,
@@ -2578,14 +2021,14 @@ const Slider = {
2578
2021
  max,
2579
2022
  step,
2580
2023
  scale,
2581
- vertical,
2024
+ orientation,
2582
2025
  reverse,
2583
2026
  disabled,
2584
- readonly,
2027
+ readOnly,
2585
2028
  onChange,
2586
2029
  percent
2587
2030
  ]);
2588
- (0, react.useImperativeHandle)(forwardedRef, () => {
2031
+ (0, react.useImperativeHandle)(actionsRef, () => {
2589
2032
  return {
2590
2033
  focus() {
2591
2034
  if (!disabled) thumbRef.current?.focus();
@@ -2602,9 +2045,9 @@ const Slider = {
2602
2045
  ref: rootRefCallback,
2603
2046
  role: "group",
2604
2047
  tabIndex: -1,
2605
- "data-orientation": vertical ? "vertical" : "horizontal",
2048
+ "data-orientation": orientation,
2606
2049
  "data-disabled": disabled ? "" : void 0,
2607
- "data-readonly": readonly ? "" : void 0,
2050
+ "data-readonly": readOnly ? "" : void 0,
2608
2051
  style,
2609
2052
  onPointerDown,
2610
2053
  onKeyDown: (event) => {
@@ -2640,16 +2083,13 @@ function useXYPadContext(selector) {
2640
2083
  }
2641
2084
  //#endregion
2642
2085
  //#region src/components/XYPad/Area.tsx
2643
- const Area = /* @__PURE__ */ (0, react.forwardRef)(function Area({ width, height, color, children, className, style, ...props }, forwardedRef) {
2086
+ const Area = /* @__PURE__ */ (0, react.forwardRef)(function Area({ children, className, style, ...props }, forwardedRef) {
2644
2087
  const { areaRef } = useXYPadContext();
2645
2088
  const composedRef = require_compose_refs.useComposedRefs(forwardedRef, areaRef);
2646
2089
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2647
2090
  ref: composedRef,
2648
2091
  className,
2649
2092
  style: {
2650
- "--color": color,
2651
- "--width": cssLength(width),
2652
- "--height": cssLength(height),
2653
2093
  position: "relative",
2654
2094
  ...style
2655
2095
  },
@@ -2662,15 +2102,15 @@ const Area = /* @__PURE__ */ (0, react.forwardRef)(function Area({ width, height
2662
2102
  });
2663
2103
  //#endregion
2664
2104
  //#region src/components/XYPad/Thumb.tsx
2665
- const Thumb = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, children, className, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-valuetext": ariaValuetext, ...props }, forwardedRef) {
2105
+ const Thumb = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, children, className, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-valuetext": ariaValuetext, actionsRef, ...props }, forwardedRef) {
2666
2106
  const xInputRef = (0, react.useRef)(null);
2667
2107
  const yInputRef = (0, react.useRef)(null);
2668
- const { value, min, max, step, disabled, readonly, onChange, percent, thumbRef } = useXYPadContext();
2108
+ const { value, min, max, step, disabled, readOnly, onChange, percent, thumbRef } = useXYPadContext();
2669
2109
  useCheckPlacement("XYPad.Thumb", "XYPad.Area");
2670
- const labels = (0, _tremolo_ui_dom.toXY)(ariaLabel);
2671
- const labelledby = (0, _tremolo_ui_dom.toXY)(ariaLabelledby);
2672
- const describedby = (0, _tremolo_ui_dom.toXY)(ariaDescribedby);
2673
- const valueText = (0, _tremolo_ui_dom.toXY)(ariaValuetext);
2110
+ const labels = (0, _tremolo_ui_dom_internal.toXY)(ariaLabel);
2111
+ const labelledby = (0, _tremolo_ui_dom_internal.toXY)(ariaLabelledby);
2112
+ const describedby = (0, _tremolo_ui_dom_internal.toXY)(ariaDescribedby);
2113
+ const valueText = (0, _tremolo_ui_dom_internal.toXY)(ariaValuetext);
2674
2114
  const methods = () => ({
2675
2115
  focus() {
2676
2116
  if (!disabled) xInputRef.current?.focus();
@@ -2680,12 +2120,13 @@ const Thumb = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, chil
2680
2120
  yInputRef.current?.blur();
2681
2121
  }
2682
2122
  });
2683
- (0, react.useImperativeHandle)(forwardedRef, methods, [disabled]);
2123
+ (0, react.useImperativeHandle)(actionsRef, methods, [disabled]);
2684
2124
  (0, react.useImperativeHandle)(thumbRef, methods, [disabled]);
2685
2125
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2126
+ ref: forwardedRef,
2686
2127
  className,
2687
2128
  "data-disabled": disabled ? "" : void 0,
2688
- "data-readonly": readonly ? "" : void 0,
2129
+ "data-readonly": readOnly ? "" : void 0,
2689
2130
  ...props,
2690
2131
  style: {
2691
2132
  "--color": color,
@@ -2704,14 +2145,14 @@ const Thumb = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, chil
2704
2145
  max: max[axis],
2705
2146
  step: step[axis],
2706
2147
  disabled,
2707
- "aria-readonly": readonly,
2148
+ "aria-readonly": readOnly,
2708
2149
  "aria-orientation": axis === 0 ? "horizontal" : "vertical",
2709
2150
  "aria-label": labels[axis] ?? (axis === 0 ? "x" : "y"),
2710
2151
  "aria-labelledby": labelledby[axis],
2711
2152
  "aria-describedby": describedby[axis],
2712
2153
  "aria-valuetext": valueText[axis],
2713
2154
  onChange: (event) => {
2714
- if (readonly) {
2155
+ if (readOnly) {
2715
2156
  event.currentTarget.value = String(value[axis]);
2716
2157
  return;
2717
2158
  }
@@ -2724,7 +2165,6 @@ const Thumb = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, chil
2724
2165
  });
2725
2166
  //#endregion
2726
2167
  //#region src/components/XYPad/index.tsx
2727
- const defaultExternalStyles = { cursor: "pointer" };
2728
2168
  /**
2729
2169
  * The wheel only acts while the focus is inside, so that scrolling a page past
2730
2170
  * the control does not change its value.
@@ -2734,24 +2174,30 @@ const WHEEL_OPTIONS = { requireFocus: true };
2734
2174
  * Simple XYPad
2735
2175
  */
2736
2176
  const XYPad = {
2737
- Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min: _min, max: _max, step: _step = 1, scale: _scale = _tremolo_ui_functions.linearScale, reverse: _reverse = false, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, className, style, externalStyles: _externalStyles, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, onPointerDown, onKeyDown, onFocus, onBlur, children, ...props }, forwardedRef) => {
2177
+ Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min: _min, max: _max, step: _step = 1, scale: _scale = _tremolo_ui_functions.linearScale, reverse: _reverse = false, wheel = _tremolo_ui_dom.DEFAULT_WHEEL_OPTIONS, keyboard = _tremolo_ui_dom.DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = _tremolo_ui_dom.DEFAULT_DRAG_SENSITIVITY, className, style, dragCursor = "pointer", disabled = false, readOnly = false, onChange, onChangeStart, onChangeEnd, changeEndDelay, onPointerDown, onKeyDown, onFocus, onBlur, children, actionsRef, ...props }, forwardedRef) => {
2738
2178
  const rootRef = (0, react.useRef)(null);
2739
2179
  const areaRef = (0, react.useRef)(null);
2740
2180
  const thumbRef = (0, react.useRef)(null);
2741
- const externalStyles = {
2742
- ...defaultExternalStyles,
2743
- ..._externalStyles
2744
- };
2745
- const inactive = disabled || readonly;
2746
- const min = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_min), [_min]);
2747
- const max = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_max), [_max]);
2748
- const step = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_step), [_step]);
2749
- const scale = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_scale), [_scale]);
2750
- const reverse = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_reverse), [_reverse]);
2751
- const percent = (0, react.useMemo)(() => {
2752
- const normalized = [0, 1].map((i) => scale[i].normalize(value[i], min[i], max[i]));
2753
- return [0, 1].map((i) => (0, _tremolo_ui_functions.toFixed)((reverse[i] ? 1 - normalized[i] : normalized[i]) * 100));
2754
- }, [
2181
+ const inactive = disabled || readOnly;
2182
+ const gesture = useChangeGesture(value, {
2183
+ onChangeStart,
2184
+ onChangeEnd,
2185
+ changeEndDelay
2186
+ }, inactive);
2187
+ const change = (0, react.useCallback)((next) => {
2188
+ gesture.changed(next);
2189
+ onChange?.(next);
2190
+ }, [gesture, onChange]);
2191
+ const min = (0, react.useMemo)(() => (0, _tremolo_ui_dom_internal.toXY)(_min), [_min]);
2192
+ const max = (0, react.useMemo)(() => (0, _tremolo_ui_dom_internal.toXY)(_max), [_max]);
2193
+ const step = (0, react.useMemo)(() => (0, _tremolo_ui_dom_internal.toXY)(_step), [_step]);
2194
+ const scale = (0, react.useMemo)(() => (0, _tremolo_ui_dom_internal.toXY)(_scale), [_scale]);
2195
+ const reverse = (0, react.useMemo)(() => (0, _tremolo_ui_dom_internal.toXY)(_reverse), [_reverse]);
2196
+ const percent = (0, react.useMemo)(() => [0, 1].map((i) => (0, _tremolo_ui_dom.valuePercent)(value[i], {
2197
+ min: min[i],
2198
+ max: max[i],
2199
+ scale: scale[i]
2200
+ }, reverse[i])), [
2755
2201
  value,
2756
2202
  min,
2757
2203
  max,
@@ -2787,67 +2233,56 @@ const XYPad = {
2787
2233
  });
2788
2234
  const withAxis = (0, react.useCallback)((axis, next) => axis === 0 ? [next, value[1]] : [value[0], next], [value]);
2789
2235
  /** @param i 0 = x, 1 = y */
2790
- const nudge = (0, react.useCallback)((i, direction, option, modifiers) => withAxis(i, (0, _tremolo_ui_dom.applyDelta)(value[i], direction, option, axis[i], modifiers)), [
2236
+ const nudge = (0, react.useCallback)((i, direction, option, modifiers) => withAxis(i, (0, _tremolo_ui_dom_internal.applyDelta)(value[i], direction, option, axis[i], modifiers)), [
2791
2237
  value,
2792
2238
  axis,
2793
2239
  withAxis
2794
2240
  ]);
2795
2241
  const handleKeyDown = (0, react.useCallback)((event) => {
2796
- const key = event.key;
2797
- if (![
2798
- "ArrowRight",
2799
- "ArrowLeft",
2800
- "ArrowUp",
2801
- "ArrowDown"
2802
- ].includes(key)) return;
2803
- const i = key === "ArrowRight" || key === "ArrowLeft" ? 0 : 1;
2242
+ const move = (0, _tremolo_ui_dom_internal.arrowKeyMove)(event.key);
2243
+ if (!move) return;
2804
2244
  event.preventDefault();
2805
2245
  if (!onChange || inactive || !keyboard) return;
2806
- let direction = 1;
2807
- if (key === "ArrowLeft" || key === "ArrowUp") direction *= -1;
2808
- if (reverse[i]) direction *= -1;
2809
- onChange(nudge(i, direction, keyboard, event));
2246
+ const { axis: i, direction } = move;
2247
+ gesture.pulse("keyboard");
2248
+ change(nudge(i, reverse[i] ? -direction : direction, keyboard, event));
2810
2249
  }, [
2811
2250
  onChange,
2812
2251
  inactive,
2813
2252
  keyboard,
2814
2253
  reverse,
2815
- nudge
2254
+ nudge,
2255
+ gesture,
2256
+ change
2816
2257
  ]);
2817
2258
  const { refCallback: dragRefCallback } = useDragValue({
2818
2259
  axis,
2819
2260
  baseElementRef: areaRef,
2820
- sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
2261
+ sensitivity: (state) => (0, _tremolo_ui_dom_internal.selectModifier)(dragSensitivity, state.event).value,
2821
2262
  updateOnPointerDown: true,
2822
- cursor: inactive ? void 0 : externalStyles.cursor,
2263
+ cursor: inactive ? void 0 : dragCursor,
2823
2264
  shouldStart: () => !inactive,
2824
2265
  onChange: (v) => {
2825
2266
  if (inactive) return;
2826
- onChange?.(v);
2267
+ change(v);
2827
2268
  },
2828
- onDragStart: (v) => {
2269
+ onDragStart: () => {
2829
2270
  if (inactive) return;
2271
+ gesture.hold("pointer");
2830
2272
  thumbRef.current?.focus();
2831
- onDragStart?.(v);
2832
2273
  },
2833
- onDragEnd: (v) => {
2834
- if (inactive) return;
2835
- onDragEnd?.(v);
2836
- }
2274
+ onDragEnd: () => gesture.end()
2837
2275
  });
2838
2276
  const wheelRefCallback = useWheel((event) => {
2839
2277
  if (!onChange || inactive || !wheel) return;
2840
- const horizontal = event.deltaX !== 0;
2841
- const delta = horizontal ? event.deltaX : event.deltaY;
2842
- if (delta === 0) return;
2843
- const i = horizontal || event.shiftKey ? 0 : 1;
2278
+ const move = (0, _tremolo_ui_dom_internal.wheelMove)(event);
2279
+ if (!move) return;
2844
2280
  event.preventDefault();
2845
- let direction = 1;
2846
- if (delta < 0) direction *= -1;
2847
- if (reverse[i]) direction *= -1;
2848
- onChange(nudge(i, direction, wheel, event));
2281
+ const { axis: i, direction } = move;
2282
+ gesture.pulse("wheel");
2283
+ change(nudge(i, reverse[i] ? -direction : direction, wheel, event));
2849
2284
  }, WHEEL_OPTIONS);
2850
- const rootRefCallback = require_compose_refs.useComposedRefs(rootRef, dragRefCallback, wheelRefCallback);
2285
+ const rootRefCallback = require_compose_refs.useComposedRefs(forwardedRef, rootRef, dragRefCallback, wheelRefCallback);
2851
2286
  const context = (0, react.useMemo)(() => ({
2852
2287
  value,
2853
2288
  min,
@@ -2856,7 +2291,7 @@ const XYPad = {
2856
2291
  scale,
2857
2292
  reverse,
2858
2293
  disabled,
2859
- readonly,
2294
+ readOnly,
2860
2295
  onChange,
2861
2296
  percent,
2862
2297
  areaRef,
@@ -2869,19 +2304,18 @@ const XYPad = {
2869
2304
  scale,
2870
2305
  reverse,
2871
2306
  disabled,
2872
- readonly,
2307
+ readOnly,
2873
2308
  onChange,
2874
2309
  percent
2875
2310
  ]);
2876
- (0, react.useImperativeHandle)(forwardedRef, () => {
2311
+ (0, react.useImperativeHandle)(actionsRef, () => {
2877
2312
  return {
2878
2313
  focus() {
2879
2314
  if (!disabled) thumbRef.current?.focus();
2880
2315
  },
2881
2316
  blur() {
2882
2317
  thumbRef.current?.blur();
2883
- },
2884
- original: rootRef
2318
+ }
2885
2319
  };
2886
2320
  }, [disabled]);
2887
2321
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(XYPadProvider, {
@@ -2892,7 +2326,7 @@ const XYPad = {
2892
2326
  role: "group",
2893
2327
  tabIndex: -1,
2894
2328
  "data-disabled": disabled ? "" : void 0,
2895
- "data-readonly": readonly ? "" : void 0,
2329
+ "data-readonly": readOnly ? "" : void 0,
2896
2330
  style,
2897
2331
  onPointerDown,
2898
2332
  onKeyDown: (event) => {
@@ -2917,17 +2351,103 @@ const XYPad = {
2917
2351
  };
2918
2352
  //#endregion
2919
2353
  //#region src/hooks/useAnimationFrame.ts
2920
- function useAnimationFrame(callback = () => {}, deps = []) {
2921
- const reqIdRef = (0, react.useRef)(-1);
2354
+ /**
2355
+ * Call `callback` on every animation frame for as long as the component is
2356
+ * mounted.
2357
+ *
2358
+ * The callback is read on every frame, so it can be written inline and see
2359
+ * the latest render without restarting the loop.
2360
+ *
2361
+ * @param callback receives the frame's timestamp and the milliseconds since
2362
+ * the previous frame, which is `0` on the first one
2363
+ */
2364
+ function useAnimationFrame(callback, { disabled = false } = {}) {
2922
2365
  const runCallback = useCallbackRef(callback);
2923
2366
  (0, react.useEffect)(() => {
2924
- const loop = () => {
2925
- reqIdRef.current = requestAnimationFrame(loop);
2926
- runCallback();
2367
+ if (disabled) return;
2368
+ let frameId = 0;
2369
+ let previous;
2370
+ const loop = (timestamp) => {
2371
+ frameId = requestAnimationFrame(loop);
2372
+ runCallback(timestamp, previous === void 0 ? 0 : timestamp - previous);
2373
+ previous = timestamp;
2927
2374
  };
2928
- reqIdRef.current = requestAnimationFrame(loop);
2929
- return () => cancelAnimationFrame(reqIdRef.current);
2930
- }, [runCallback, ...deps]);
2375
+ frameId = requestAnimationFrame(loop);
2376
+ return () => cancelAnimationFrame(frameId);
2377
+ }, [runCallback, disabled]);
2378
+ }
2379
+ //#endregion
2380
+ //#region src/hooks/useDrag.ts
2381
+ /**
2382
+ * Track a pointer drag on an element.
2383
+ *
2384
+ * @returns a ref callback to attach to the element being dragged
2385
+ */
2386
+ function useDrag({ threshold = 1, cursor, pointerLock, onDrag, onDragStart, onDragEnd }) {
2387
+ const dragHandler = useCallbackRef(onDrag);
2388
+ const dragStartHandler = useCallbackRef(onDragStart);
2389
+ const dragEndHandler = useCallbackRef(onDragEnd);
2390
+ const [node, setNode] = (0, react.useState)(null);
2391
+ (0, react.useEffect)(() => {
2392
+ if (!node) return;
2393
+ const instance = (0, _tremolo_ui_dom.createDrag)(node, {
2394
+ threshold,
2395
+ cursor,
2396
+ pointerLock,
2397
+ onDragStart: (state) => dragStartHandler(state),
2398
+ onDrag: (state) => dragHandler(state.x, state.y, state.deltaX, state.deltaY, state),
2399
+ onDragEnd: (state) => dragEndHandler(state)
2400
+ });
2401
+ return () => instance.destroy();
2402
+ }, [
2403
+ node,
2404
+ threshold,
2405
+ cursor,
2406
+ pointerLock,
2407
+ dragHandler,
2408
+ dragStartHandler,
2409
+ dragEndHandler
2410
+ ]);
2411
+ return setNode;
2412
+ }
2413
+ //#endregion
2414
+ //#region src/hooks/useEventListener.ts
2415
+ function useEventListener(target, event, handler, options) {
2416
+ const listener = useCallbackRef(handler);
2417
+ const registrationRef = (0, react.useRef)(null);
2418
+ const { capture = false, once, passive } = typeof options === "boolean" ? { capture: options } : options ?? {};
2419
+ (0, react.useEffect)(() => {
2420
+ const node = typeof target === "function" ? target() : target;
2421
+ const current = registrationRef.current;
2422
+ if (current && current.node === node && current.event === event && current.capture === capture && current.once === once && current.passive === passive) return;
2423
+ current?.remove();
2424
+ registrationRef.current = null;
2425
+ if (!node) return;
2426
+ const listenerOptions = {
2427
+ capture,
2428
+ once,
2429
+ passive
2430
+ };
2431
+ node.addEventListener(event, listener, listenerOptions);
2432
+ registrationRef.current = {
2433
+ node,
2434
+ event,
2435
+ capture,
2436
+ once,
2437
+ passive,
2438
+ remove: () => node.removeEventListener(event, listener, listenerOptions)
2439
+ };
2440
+ });
2441
+ (0, react.useEffect)(() => () => {
2442
+ registrationRef.current?.remove();
2443
+ registrationRef.current = null;
2444
+ }, []);
2445
+ return (0, react.useCallback)(() => {
2446
+ const current = registrationRef.current;
2447
+ if (!current) return;
2448
+ current.remove();
2449
+ current.remove = () => {};
2450
+ }, []);
2931
2451
  }
2932
2452
  //#endregion
2933
2453
  //#region src/hooks/useMIDIAccess.ts
@@ -3067,45 +2587,20 @@ function useMIDIMessage(midiAccess, onMIDIMessage) {
3067
2587
  }
3068
2588
  //#endregion
3069
2589
  exports.AnimationCanvas = AnimationCanvas;
3070
- exports.DEFAULT_DRAG_SENSITIVITY = DEFAULT_DRAG_SENSITIVITY;
3071
- exports.DEFAULT_KEYBOARD_OPTIONS = DEFAULT_KEYBOARD_OPTIONS;
3072
- exports.DEFAULT_WHEEL_OPTIONS = DEFAULT_WHEEL_OPTIONS;
3073
2590
  exports.DropZone = DropZone;
3074
2591
  exports.FileInput = FileInput;
3075
2592
  exports.Knob = Knob;
3076
- Object.defineProperty(exports, "NOT_SUPPORTED", {
3077
- enumerable: true,
3078
- get: function() {
3079
- return _tremolo_ui_dom.NOT_SUPPORTED;
3080
- }
3081
- });
3082
2593
  exports.NumberInput = NumberInput;
3083
- Object.defineProperty(exports, "PERMISSION_DENIED", {
3084
- enumerable: true,
3085
- get: function() {
3086
- return _tremolo_ui_dom.PERMISSION_DENIED;
3087
- }
3088
- });
3089
- exports.PITCH_BEND_CENTER = _tremolo_ui_dom.PITCH_BEND_CENTER;
3090
2594
  exports.Piano = Piano;
3091
2595
  exports.PointsEditor = PointsEditor;
3092
- exports.SHORTCUTS = SHORTCUTS;
3093
2596
  exports.Slider = Slider;
3094
- Object.defineProperty(exports, "UNAVAILABLE", {
3095
- enumerable: true,
3096
- get: function() {
3097
- return _tremolo_ui_dom.UNAVAILABLE;
3098
- }
3099
- });
3100
2597
  exports.XYPad = XYPad;
3101
- exports.clampPoint = clampPoint;
3102
2598
  exports.useAnimationFrame = useAnimationFrame;
3103
2599
  exports.useDrag = useDrag;
3104
2600
  exports.useDragValue = useDragValue;
3105
2601
  exports.useDropZone = useDropZone;
3106
2602
  exports.useEventListener = useEventListener;
3107
2603
  exports.useFileInputContext = useFileInputContext;
3108
- exports.useInterval = useInterval;
3109
2604
  exports.useKnobContext = useKnobContext;
3110
2605
  exports.useLongPress = useLongPress;
3111
2606
  exports.useMIDIAccess = useMIDIAccess;