@tremolo-ui/react 0.8.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 (40) hide show
  1. package/dist/index.cjs +416 -252
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +256 -110
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +256 -110
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +368 -203
  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/FileInput/index.tsx +4 -1
  12. package/src/components/Knob/Thumb.tsx +19 -19
  13. package/src/components/Knob/index.tsx +85 -41
  14. package/src/components/NumberInput/InputField.tsx +9 -7
  15. package/src/components/NumberInput/Stepper.tsx +30 -6
  16. package/src/components/NumberInput/StepperButton.tsx +12 -4
  17. package/src/components/NumberInput/context.tsx +24 -2
  18. package/src/components/NumberInput/index.tsx +142 -27
  19. package/src/components/Piano/index.tsx +24 -7
  20. package/src/components/PointsEditor/Container.tsx +1 -1
  21. package/src/components/PointsEditor/Point.tsx +61 -30
  22. package/src/components/PointsEditor/context.tsx +3 -4
  23. package/src/components/PointsEditor/index.tsx +14 -15
  24. package/src/components/Slider/Marks.tsx +3 -2
  25. package/src/components/Slider/MarksOption.tsx +3 -2
  26. package/src/components/Slider/Thumb.tsx +20 -11
  27. package/src/components/Slider/Track.tsx +15 -13
  28. package/src/components/Slider/context.tsx +3 -3
  29. package/src/components/Slider/index.tsx +85 -48
  30. package/src/components/XYPad/Thumb.tsx +18 -8
  31. package/src/components/XYPad/context.tsx +1 -1
  32. package/src/components/XYPad/index.tsx +74 -42
  33. package/src/components/_util/VisuallyHiddenRangeInput.tsx +1 -1
  34. package/src/hooks/_internal/useChangeGesture.ts +93 -0
  35. package/src/hooks/_internal/useCheckSteps.ts +1 -1
  36. package/src/hooks/useAnimationFrame.ts +37 -15
  37. package/src/hooks/useEventListener.ts +93 -22
  38. package/src/hooks/useWheel.ts +4 -4
  39. package/src/index.ts +9 -5
  40. package/src/hooks/useInterval.ts +0 -27
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
  /**
@@ -231,7 +232,7 @@ const FileInput = {
231
232
  disabled
232
233
  }), [inputId, disabled]);
233
234
  function handleChange(event) {
234
- const { accepted, rejected } = (0, _tremolo_ui_dom.partitionByAccept)(Array.from(event.target.files ?? []), accept);
235
+ const { accepted, rejected } = (0, _tremolo_ui_dom_internal.partitionByAccept)(Array.from(event.target.files ?? []), accept);
235
236
  event.target.value = "";
236
237
  if (accepted.length > 0) onChange?.(accepted);
237
238
  if (rejected.length > 0) onReject?.(rejected);
@@ -248,7 +249,7 @@ const FileInput = {
248
249
  accept,
249
250
  multiple,
250
251
  disabled,
251
- style: _tremolo_ui_dom.visuallyHiddenStyle,
252
+ style: _tremolo_ui_dom_internal.visuallyHiddenStyle,
252
253
  "aria-label": ariaLabel,
253
254
  "aria-labelledby": ariaLabelledby,
254
255
  "aria-describedby": ariaDescribedby,
@@ -260,6 +261,57 @@ const FileInput = {
260
261
  Trigger
261
262
  };
262
263
  //#endregion
264
+ //#region src/hooks/_internal/useChangeGesture.ts
265
+ /**
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.
269
+ *
270
+ * Internal
271
+ * @private
272
+ */
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]);
313
+ }
314
+ //#endregion
263
315
  //#region src/hooks/_internal/useCheckSteps.ts
264
316
  /**
265
317
  * Warn, in development only, when a key press or a wheel notch cannot produce
@@ -273,7 +325,7 @@ function useCheckSteps(options) {
273
325
  (0, react.useEffect)(() => {
274
326
  try {
275
327
  if (process.env.NODE_ENV === "production") return;
276
- for (const warning of (0, _tremolo_ui_dom.checkSteps)(options)) console.warn(warning);
328
+ for (const warning of (0, _tremolo_ui_dom_internal.checkSteps)(options)) console.warn(warning);
277
329
  } catch {}
278
330
  }, [
279
331
  component,
@@ -374,7 +426,10 @@ function useWheel(onWheel, { requireFocus = false, target } = {}) {
374
426
  (0, react.useEffect)(() => {
375
427
  const element = target ? target.current : node;
376
428
  if (!element) return;
377
- 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
+ });
378
433
  return () => instance.destroy();
379
434
  }, [
380
435
  node,
@@ -497,7 +552,7 @@ function SVGRoot({ children, style, ...props }) {
497
552
  }
498
553
  //#endregion
499
554
  //#region src/components/Knob/Thumb.tsx
500
- 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 }) {
501
556
  useCheckPlacement("Knob.Thumb", "Knob.SVGRoot");
502
557
  const angleRange = useKnobContext((s) => s.angleRange);
503
558
  const p = useKnobContext((s) => s.p);
@@ -508,16 +563,16 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
508
563
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
509
564
  cx: "50%",
510
565
  cy: "50%",
511
- r: `${thumbSize / 2}%`,
512
- fill: thumb
566
+ r: `${size / 2}%`,
567
+ fill: color
513
568
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
514
- className: classes?.thumbLine,
569
+ className: classes?.line,
515
570
  x1: "50%",
516
- y1: `${(_tremolo_ui_dom.KNOB_VIEWBOX_SIZE - (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}%`,
517
572
  x2: "50%",
518
- y2: `${thumbLineLength}%`,
519
- stroke: thumbLine,
520
- strokeWidth: `${thumbLineWeight}%`,
573
+ y2: `${lineLength}%`,
574
+ stroke: lineColor,
575
+ strokeWidth: `${lineWeight}%`,
521
576
  style: {
522
577
  transform: `rotate(${r1 + p * angleRange}deg)`,
523
578
  transformOrigin: "50% 50%"
@@ -527,7 +582,6 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
527
582
  }
528
583
  //#endregion
529
584
  //#region src/components/Knob/index.tsx
530
- const defaultExternalStyles$2 = { cursor: "grabbing" };
531
585
  /**
532
586
  * The wheel only acts while the focus is inside, so that scrolling a page past
533
587
  * the control does not change its value.
@@ -537,13 +591,9 @@ const WHEEL_OPTIONS$2 = { requireFocus: true };
537
591
  * Interactive rotary knob component implemented in SVG.
538
592
  */
539
593
  const Knob = {
540
- Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, defaultValue = min, startValue = min, size, externalStyles: _externalStyles, wheel = _tremolo_ui_dom.DEFAULT_WHEEL_OPTIONS, keyboard = _tremolo_ui_dom.DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = _tremolo_ui_dom.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) => {
541
595
  const elmRef = (0, react.useRef)(null);
542
- const externalStyles = {
543
- ...defaultExternalStyles$2,
544
- ..._externalStyles
545
- };
546
- const inactive = disabled || readonly;
596
+ const inactive = disabled || readOnly;
547
597
  const range = (0, react.useMemo)(() => ({
548
598
  min,
549
599
  max,
@@ -561,18 +611,30 @@ const Knob = {
561
611
  keyboard,
562
612
  wheel
563
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]);
564
623
  const handleKeyDown = (0, react.useCallback)((event) => {
565
624
  if (!keyboard || !onChange || inactive) return;
566
- const direction = (0, _tremolo_ui_dom.arrowKeyDirection)(event.key);
625
+ const direction = (0, _tremolo_ui_dom_internal.arrowKeyDirection)(event.key);
567
626
  if (direction === null) return;
568
627
  event.preventDefault();
569
- onChange((0, _tremolo_ui_dom.applyDelta)(value, direction, keyboard, range, event));
628
+ gesture.pulse("keyboard");
629
+ change((0, _tremolo_ui_dom_internal.applyDelta)(value, direction, keyboard, range, event));
570
630
  }, [
571
631
  keyboard,
572
632
  onChange,
573
633
  inactive,
574
634
  value,
575
- range
635
+ range,
636
+ gesture,
637
+ change
576
638
  ]);
577
639
  const { refCallback: dragRefCallback, dragging } = useDragValue({
578
640
  axis: (0, react.useMemo)(() => [range, {
@@ -580,25 +642,30 @@ const Knob = {
580
642
  reverse: true
581
643
  }], [range]),
582
644
  getValue: () => [value, value],
583
- sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
645
+ sensitivity: (state) => (0, _tremolo_ui_dom_internal.selectModifier)(dragSensitivity, state.event).value,
584
646
  threshold: 1,
585
- cursor: inactive ? void 0 : externalStyles.cursor,
647
+ cursor: inactive ? void 0 : dragCursor,
586
648
  pointerLock: inactive ? false : pointerLock,
587
649
  shouldStart: () => !inactive,
588
650
  onChange: (v) => {
589
651
  if (inactive) return;
590
- onChange?.(v[1]);
591
- }
652
+ change(v[1]);
653
+ },
654
+ onDragStart: () => {
655
+ if (!inactive) gesture.hold("pointer");
656
+ },
657
+ onDragEnd: () => gesture.end()
592
658
  });
593
659
  const wheelRefCallback = useWheel((event) => {
594
660
  if (!wheel || inactive) return;
595
- const direction = (0, _tremolo_ui_dom.wheelDirection)(event);
661
+ const direction = (0, _tremolo_ui_dom_internal.wheelDirection)(event);
596
662
  if (direction === null) return;
597
663
  event.preventDefault();
598
664
  if (!onChange) return;
599
- onChange((0, _tremolo_ui_dom.applyDelta)(value, direction, wheel, range, event));
665
+ gesture.pulse("wheel");
666
+ change((0, _tremolo_ui_dom_internal.applyDelta)(value, direction, wheel, range, event));
600
667
  }, WHEEL_OPTIONS$2);
601
- const rootRefCallback = require_compose_refs.useComposedRefs(elmRef, dragRefCallback, wheelRefCallback);
668
+ const rootRefCallback = require_compose_refs.useComposedRefs(forwardedRef, elmRef, dragRefCallback, wheelRefCallback);
602
669
  const context = (0, react.useMemo)(() => {
603
670
  const config = {
604
671
  value,
@@ -622,7 +689,7 @@ const Knob = {
622
689
  startValue,
623
690
  angleRange
624
691
  ]);
625
- (0, react.useImperativeHandle)(forwardedRef, () => {
692
+ (0, react.useImperativeHandle)(actionsRef, () => {
626
693
  return {
627
694
  focus() {
628
695
  if (!disabled) elmRef.current?.focus();
@@ -643,17 +710,17 @@ const Knob = {
643
710
  "aria-valuemin": min,
644
711
  "aria-valuemax": max,
645
712
  "aria-disabled": disabled,
646
- "aria-readonly": readonly,
713
+ "aria-readonly": readOnly,
647
714
  "data-disabled": disabled ? "" : void 0,
648
- "data-readonly": readonly ? "" : void 0,
715
+ "data-readonly": readOnly ? "" : void 0,
649
716
  "data-dragging": dragging ? "" : void 0,
650
717
  style: {
651
- "--knob-size": (0, _tremolo_ui_dom.cssLength)(size),
718
+ "--knob-size": (0, _tremolo_ui_dom_internal.cssLength)(size),
652
719
  ...style
653
720
  },
654
721
  onPointerDown,
655
722
  onDoubleClick: (event) => {
656
- if (!inactive && enableDoubleClickDefault && onChange) onChange(defaultValue);
723
+ if (!inactive && resetValue !== null && onChange) gesture.instant("doubleClick", () => change(resetValue ?? startValue));
657
724
  onDoubleClick?.(event);
658
725
  },
659
726
  onKeyDown: (event) => {
@@ -679,6 +746,11 @@ function useNumberInputContext(selector) {
679
746
  if (!context) throw new Error("Missing NumberInputContext.Provider in the tree");
680
747
  return selector ? selector(context) : context;
681
748
  }
749
+ const NumberInputGestureContext = /* @__PURE__ */ (0, react.createContext)(null);
750
+ const NumberInputGestureProvider = NumberInputGestureContext.Provider;
751
+ function useNumberInputGesture() {
752
+ return (0, react.useContext)(NumberInputGestureContext);
753
+ }
682
754
  const StepperContext = /* @__PURE__ */ (0, react.createContext)(null);
683
755
  const StepperProvider = StepperContext.Provider;
684
756
  function useStepperContext() {
@@ -723,8 +795,9 @@ function useLongPress(callback, initialDelay = 500, interval = 40) {
723
795
  * in which way they move the value and which arrow they draw.
724
796
  */
725
797
  function StepperButton({ direction, variant, icon, className, style, children, onPointerDown, ...props }) {
726
- const { step, disabled, readonly, atMin, atMax, nudge } = useNumberInputContext();
798
+ const { step, disabled, readOnly, atMin, atMax, nudge } = useNumberInputContext();
727
799
  const stepper = useStepperContext();
800
+ const gesture = useNumberInputGesture();
728
801
  const press = useLongPress(() => {
729
802
  if (stepper?.moved()) return;
730
803
  nudge(direction, ["raw", step]);
@@ -736,10 +809,13 @@ function StepperButton({ direction, variant, icon, className, style, children, o
736
809
  "aria-label": direction > 0 ? "Increment" : "Decrement",
737
810
  "aria-disabled": disabled || (direction > 0 ? atMax : atMin),
738
811
  "data-disabled": disabled || (direction > 0 ? atMax : atMin) ? "" : void 0,
739
- "data-readonly": readonly ? "" : void 0,
812
+ "data-readonly": readOnly ? "" : void 0,
740
813
  style,
741
814
  onPointerDown: (event) => {
742
- if (!disabled && !readonly) press(event);
815
+ if (!disabled && !readOnly) {
816
+ gesture?.press();
817
+ press(event);
818
+ }
743
819
  onPointerDown?.(event);
744
820
  },
745
821
  ...props,
@@ -795,7 +871,8 @@ function IncrementStepper(props) {
795
871
  * (`selectOnFocus`, `unformatOnFocus`, `keepCaretOnStep`, `blurOnEnter`).
796
872
  */
797
873
  const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ className, style, onChange, onFocus, onBlur, onKeyDown, ...props }, forwardedRef) {
798
- const { value, min, max, step, disabled, readonly, keyboard, selectOnFocus, unformatOnFocus, keepCaretOnStep, blurOnEnter, text, editing, outOfRange, setDraft, commitDraft, nudge, inputRef } = useNumberInputContext();
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();
799
876
  const [focused, setFocused] = (0, react.useState)(false);
800
877
  const shown = unformatOnFocus && focused && !editing ? String(value) : text;
801
878
  (0, react.useEffect)(() => {
@@ -804,7 +881,7 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
804
881
  if (!input) return;
805
882
  if (selectOnFocus === "all") input.setSelectionRange(0, input.value.length);
806
883
  else {
807
- const span = (0, _tremolo_ui_dom.numberSpan)(input.value);
884
+ const span = (0, _tremolo_ui_dom_internal.numberSpan)(input.value);
808
885
  if (span) input.setSelectionRange(span.start, span.end);
809
886
  }
810
887
  }, [
@@ -823,7 +900,7 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
823
900
  caretAfterStep.current = null;
824
901
  const input = inputRef.current;
825
902
  if (!input || input.value === pending.from) return;
826
- const caret = (0, _tremolo_ui_dom.caretAtDecimalOffset)(input.value, pending.offset);
903
+ const caret = (0, _tremolo_ui_dom_internal.caretAtDecimalOffset)(input.value, pending.offset);
827
904
  input.setSelectionRange(caret, caret);
828
905
  });
829
906
  const composedRef = require_compose_refs.useComposedRefs(forwardedRef, inputRef);
@@ -835,11 +912,11 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
835
912
  role: "spinbutton",
836
913
  value: shown,
837
914
  disabled,
838
- readOnly: readonly,
915
+ readOnly,
839
916
  "aria-disabled": disabled,
840
- "aria-readonly": readonly,
917
+ "aria-readonly": readOnly,
841
918
  "data-disabled": disabled ? "" : void 0,
842
- "data-readonly": readonly ? "" : void 0,
919
+ "data-readonly": readOnly ? "" : void 0,
843
920
  "aria-valuenow": value,
844
921
  "aria-valuemin": min,
845
922
  "aria-valuemax": max,
@@ -866,13 +943,14 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
866
943
  if (key === "Enter") {
867
944
  commitDraft();
868
945
  if (blurOnEnter) event.currentTarget.blur();
869
- } 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) {
870
947
  event.preventDefault();
871
948
  const input = event.currentTarget;
872
949
  if (keepCaretOnStep && input.selectionStart !== null) caretAfterStep.current = {
873
- offset: (0, _tremolo_ui_dom.caretDecimalOffset)(input.value, input.selectionStart),
950
+ offset: (0, _tremolo_ui_dom_internal.caretDecimalOffset)(input.value, input.selectionStart),
874
951
  from: input.value
875
952
  };
953
+ gesture?.key();
876
954
  nudge(key === "ArrowUp" ? 1 : -1, keyboard, event);
877
955
  }
878
956
  onKeyDown?.(event);
@@ -890,9 +968,9 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
890
968
  * The drag lives here rather than on `InputField` because `createDrag` turns
891
969
  * off text selection on whatever element it is attached to.
892
970
  */
893
- const Stepper = /* @__PURE__ */ (0, react.forwardRef)(function Stepper({ className, style, children, ...props }, forwardedRef) {
894
- const { value, disabled, readonly, drag, dragSensitivity, pointerLock, rawRange, changeValue } = useNumberInputContext();
895
- const enabled = drag !== null && !disabled && !readonly;
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;
896
974
  const [node, setNode] = (0, react.useState)(null);
897
975
  const instanceRef = (0, react.useRef)(null);
898
976
  const latest = (0, react.useRef)({
@@ -903,7 +981,8 @@ const Stepper = /* @__PURE__ */ (0, react.forwardRef)(function Stepper({ classNa
903
981
  range: rawRange,
904
982
  pixels: drag ?? 1,
905
983
  sensitivity: dragSensitivity,
906
- pointerLock
984
+ pointerLock,
985
+ cursor: dragCursor
907
986
  };
908
987
  const optionsRef = (0, react.useRef)(options);
909
988
  (0, react.useEffect)(() => {
@@ -916,7 +995,7 @@ const Stepper = /* @__PURE__ */ (0, react.forwardRef)(function Stepper({ classNa
916
995
  });
917
996
  (0, react.useEffect)(() => {
918
997
  if (!node || !enabled) return;
919
- const instance = (0, _tremolo_ui_dom.createStepperDrag)(node, {
998
+ const instance = (0, _tremolo_ui_dom_internal.createStepperDrag)(node, {
920
999
  ...optionsRef.current,
921
1000
  getValue: () => latest.current.value,
922
1001
  onChange: (next) => latest.current.changeValue(next)
@@ -927,6 +1006,7 @@ const Stepper = /* @__PURE__ */ (0, react.forwardRef)(function Stepper({ classNa
927
1006
  instance.destroy();
928
1007
  };
929
1008
  }, [node, enabled]);
1009
+ const gesture = useNumberInputGesture();
930
1010
  const composedRef = require_compose_refs.useComposedRefs(forwardedRef, setNode);
931
1011
  const context = (0, react.useMemo)(() => ({ moved: () => instanceRef.current?.moved() ?? false }), []);
932
1012
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepperProvider, {
@@ -935,6 +1015,10 @@ const Stepper = /* @__PURE__ */ (0, react.forwardRef)(function Stepper({ classNa
935
1015
  ref: composedRef,
936
1016
  className,
937
1017
  style,
1018
+ onPointerDown: (event) => {
1019
+ if (enabled) gesture?.press();
1020
+ onPointerDown?.(event);
1021
+ },
938
1022
  ...props,
939
1023
  children
940
1024
  })
@@ -947,17 +1031,22 @@ const defaultFormat = (value) => String(value);
947
1031
  * Input with some useful functions for entering numerical values.
948
1032
  */
949
1033
  const NumberInput = {
950
- 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, 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) => {
951
1035
  const inputRef = (0, react.useRef)(null);
952
1036
  /**
953
1037
  * The text being typed. The only state here: it is not a copy of `value`,
954
1038
  * but the half-finished entry that has no value to be derived from yet.
955
1039
  */
956
1040
  const [draft, setDraft] = (0, react.useState)(null);
957
- const inactive = disabled || readonly;
1041
+ const inactive = disabled || readOnly;
1042
+ const gesture = useChangeGesture(value, {
1043
+ onChangeStart,
1044
+ onChangeEnd,
1045
+ changeEndDelay
1046
+ }, inactive);
958
1047
  const format = formatProp ?? defaultFormat;
959
- const parse = parseProp ?? _tremolo_ui_dom.parseNumberText;
960
- const { normalized: range, raw: rawRange } = (0, react.useMemo)(() => (0, _tremolo_ui_dom.numberInputRanges)({
1048
+ const parse = parseProp ?? _tremolo_ui_dom_internal.parseNumberText;
1049
+ const { normalized: range, raw: rawRange } = (0, react.useMemo)(() => (0, _tremolo_ui_dom_internal.numberInputRanges)({
961
1050
  min,
962
1051
  max,
963
1052
  step,
@@ -993,7 +1082,7 @@ const NumberInput = {
993
1082
  });
994
1083
  const text = draft ?? format(value);
995
1084
  const editing = draft !== null;
996
- const bounds = (0, _tremolo_ui_dom.numberInputBounds)(value, {
1085
+ const bounds = (0, _tremolo_ui_dom_internal.numberInputBounds)(value, {
997
1086
  min,
998
1087
  max,
999
1088
  clampValue
@@ -1004,30 +1093,45 @@ const NumberInput = {
1004
1093
  if (inactive) return;
1005
1094
  setDraft(next);
1006
1095
  const parsed = parse(next);
1007
- 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);
1008
1102
  }, [
1009
1103
  inactive,
1010
1104
  parse,
1011
- onChange
1105
+ onChange,
1106
+ value,
1107
+ gesture
1012
1108
  ]);
1013
1109
  const changeValue = (0, react.useCallback)((next) => {
1014
1110
  if (inactive) return;
1015
1111
  setDraft(null);
1016
- if (next !== value) onChange?.(next);
1112
+ if (next === value) return;
1113
+ gesture.changed(next);
1114
+ onChange?.(next);
1017
1115
  }, [
1018
1116
  inactive,
1019
1117
  value,
1020
- onChange
1118
+ onChange,
1119
+ gesture
1021
1120
  ]);
1022
1121
  const commitDraft = (0, react.useCallback)(() => {
1023
1122
  if (draft === null || inactive) return;
1024
- const committed = (0, _tremolo_ui_dom.commitNumberInputText)(draft, parse, {
1123
+ const committed = (0, _tremolo_ui_dom_internal.commitNumberInputText)(draft, parse, {
1025
1124
  min,
1026
1125
  max,
1027
1126
  clampValue
1028
1127
  });
1029
- if (committed === null) setDraft(null);
1030
- else changeValue(committed);
1128
+ if (committed === null) {
1129
+ setDraft(null);
1130
+ return;
1131
+ }
1132
+ if (committed !== value) gesture.pulse("keyboard");
1133
+ changeValue(committed);
1134
+ gesture.end();
1031
1135
  }, [
1032
1136
  draft,
1033
1137
  inactive,
@@ -1035,19 +1139,22 @@ const NumberInput = {
1035
1139
  clampValue,
1036
1140
  min,
1037
1141
  max,
1038
- changeValue
1142
+ changeValue,
1143
+ value,
1144
+ gesture
1039
1145
  ]);
1040
1146
  const nudge = (0, react.useCallback)((direction, option, modifiers) => {
1041
- changeValue((0, _tremolo_ui_dom.nudgeNumberInput)(value, direction, option, ranges, modifiers));
1147
+ changeValue((0, _tremolo_ui_dom_internal.nudgeNumberInput)(value, direction, option, ranges, modifiers));
1042
1148
  }, [
1043
1149
  changeValue,
1044
1150
  value,
1045
1151
  ranges
1046
1152
  ]);
1047
1153
  const wheelRefCallback = useWheel((event) => {
1048
- const direction = (0, _tremolo_ui_dom.wheelDirection)(event);
1154
+ const direction = (0, _tremolo_ui_dom_internal.wheelDirection)(event);
1049
1155
  if (!wheel || inactive || direction === null) return;
1050
1156
  event.preventDefault();
1157
+ gesture.pulse("wheel");
1051
1158
  nudge(direction, wheel, event);
1052
1159
  }, { requireFocus: true });
1053
1160
  const context = (0, react.useMemo)(() => ({
@@ -1057,7 +1164,7 @@ const NumberInput = {
1057
1164
  step,
1058
1165
  scale,
1059
1166
  disabled,
1060
- readonly,
1167
+ readOnly,
1061
1168
  clampValue,
1062
1169
  range,
1063
1170
  rawRange,
@@ -1067,6 +1174,7 @@ const NumberInput = {
1067
1174
  outOfRange,
1068
1175
  dragSensitivity,
1069
1176
  pointerLock,
1177
+ dragCursor,
1070
1178
  selectOnFocus,
1071
1179
  unformatOnFocus,
1072
1180
  keepCaretOnStep,
@@ -1088,7 +1196,7 @@ const NumberInput = {
1088
1196
  step,
1089
1197
  scale,
1090
1198
  disabled,
1091
- readonly,
1199
+ readOnly,
1092
1200
  clampValue,
1093
1201
  range,
1094
1202
  rawRange,
@@ -1101,6 +1209,7 @@ const NumberInput = {
1101
1209
  drag,
1102
1210
  dragSensitivity,
1103
1211
  pointerLock,
1212
+ dragCursor,
1104
1213
  selectOnFocus,
1105
1214
  unformatOnFocus,
1106
1215
  keepCaretOnStep,
@@ -1112,7 +1221,8 @@ const NumberInput = {
1112
1221
  changeValue,
1113
1222
  nudge
1114
1223
  ]);
1115
- (0, react.useImperativeHandle)(forwardedRef, () => {
1224
+ const rootRefCallback = require_compose_refs.useComposedRefs(forwardedRef, wheelRefCallback);
1225
+ (0, react.useImperativeHandle)(actionsRef, () => {
1116
1226
  return {
1117
1227
  focus() {
1118
1228
  if (!disabled) inputRef.current?.focus();
@@ -1122,16 +1232,39 @@ const NumberInput = {
1122
1232
  }
1123
1233
  };
1124
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]);
1125
1255
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInputProvider, {
1126
1256
  value: context,
1127
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1128
- ref: wheelRefCallback,
1129
- className,
1130
- "data-disabled": disabled ? "" : void 0,
1131
- "data-readonly": readonly ? "" : void 0,
1132
- style,
1133
- ...props,
1134
- 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
+ })
1135
1268
  })
1136
1269
  });
1137
1270
  }),
@@ -1143,12 +1276,12 @@ const NumberInput = {
1143
1276
  /**
1144
1277
  * Customizable piano component.
1145
1278
  */
1146
- 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, ...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) {
1147
1280
  const [node, setNode] = (0, react.useState)(null);
1148
1281
  const [activeNotes, setActiveNotes] = (0, react.useState)([]);
1149
1282
  /** Set while `resizable` is on, measured from the parent. */
1150
1283
  const [resizedKeyWidth, setResizedKeyWidth] = (0, react.useState)(whiteKeyWidth);
1151
- const notes = (0, react.useMemo)(() => (0, _tremolo_ui_dom.getNoteRangeArray)(noteRange), [noteRange]);
1284
+ const notes = (0, react.useMemo)(() => (0, _tremolo_ui_dom_internal.getNoteRangeArray)(noteRange), [noteRange]);
1152
1285
  const layout = (0, react.useMemo)(() => ({
1153
1286
  noteRange,
1154
1287
  whiteKeyWidth: resizable ? resizedKeyWidth : whiteKeyWidth,
@@ -1216,7 +1349,7 @@ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ note
1216
1349
  const parent = node.parentElement;
1217
1350
  if (!parent) throw new Error("doesn't have a parent element.");
1218
1351
  const resizeObserver = new ResizeObserver(() => {
1219
- setResizedKeyWidth((0, _tremolo_ui_dom.fitWhiteKeyWidth)(node.clientWidth, {
1352
+ setResizedKeyWidth((0, _tremolo_ui_dom_internal.fitWhiteKeyWidth)(node.clientWidth, {
1220
1353
  noteRange: {
1221
1354
  first,
1222
1355
  last
@@ -1235,7 +1368,8 @@ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ note
1235
1368
  keyGap,
1236
1369
  blackKeyWidthRatio
1237
1370
  ]);
1238
- (0, react.useImperativeHandle)(forwardedRef, () => ({
1371
+ const rootRefCallback = require_compose_refs.useComposedRefs(forwardedRef, setNode);
1372
+ (0, react.useImperativeHandle)(actionsRef, () => ({
1239
1373
  playNote: (note, velocity) => instanceRef.current?.noteOn(note, {
1240
1374
  source: "api",
1241
1375
  velocity
@@ -1243,13 +1377,13 @@ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ note
1243
1377
  stopNote: (note) => instanceRef.current?.noteOff(note, { source: "api" })
1244
1378
  }), []);
1245
1379
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1246
- ref: setNode,
1380
+ ref: rootRefCallback,
1247
1381
  className,
1248
1382
  "data-resizable": resizable ? "" : void 0,
1249
1383
  role: "group",
1250
1384
  tabIndex: 0,
1251
1385
  style: {
1252
- width: resizable ? "100%" : (0, _tremolo_ui_dom.pianoWidth)(layout),
1386
+ width: resizable ? "100%" : (0, _tremolo_ui_dom_internal.pianoWidth)(layout),
1253
1387
  position: "relative",
1254
1388
  ...style
1255
1389
  },
@@ -1276,7 +1410,7 @@ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ note
1276
1410
  zIndex: keyType === "white" ? 1 : 2,
1277
1411
  ...keyStyle,
1278
1412
  left: (0, _tremolo_ui_dom.notePosition)(note, layout),
1279
- width: keyType === "white" ? layout.whiteKeyWidth : (0, _tremolo_ui_dom.blackKeyWidth)(layout),
1413
+ width: keyType === "white" ? layout.whiteKeyWidth : (0, _tremolo_ui_dom_internal.blackKeyWidth)(layout),
1280
1414
  height: keyType === "white" ? "100%" : `${blackKeyHeightRatio * 100}%`
1281
1415
  },
1282
1416
  children: content !== "" && content !== null && content !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -1319,7 +1453,7 @@ function usePointsEditorContext(selector) {
1319
1453
  const Container = /* @__PURE__ */ (0, react.forwardRef)(function Container({ children, className, style, ...props }, forwardedRef) {
1320
1454
  const { containerRef, disabled, selectable, editor } = usePointsEditorContext();
1321
1455
  const { refCallback: dragRefCallback } = useDragValue({
1322
- axis: _tremolo_ui_dom.POINT_AXIS,
1456
+ axis: _tremolo_ui_dom_internal.POINT_AXIS,
1323
1457
  baseElementRef: containerRef,
1324
1458
  shouldStart: (event) => !disabled && !editor.isPointElement(event.target),
1325
1459
  onDragStart: ([x, y], state) => {
@@ -1335,7 +1469,7 @@ const Container = /* @__PURE__ */ (0, react.forwardRef)(function Container({ chi
1335
1469
  onDragEnd: () => editor.endSelectionBox()
1336
1470
  });
1337
1471
  useWheel((event) => {
1338
- const move = (0, _tremolo_ui_dom.wheelMove)(event);
1472
+ const move = (0, _tremolo_ui_dom_internal.wheelMove)(event);
1339
1473
  if (!move) return;
1340
1474
  const axis = move.axis === 0 ? "x" : "y";
1341
1475
  if (editor.nudgeFocusedPoint(axis, move.direction, event)) event.preventDefault();
@@ -1363,7 +1497,7 @@ const VisuallyHiddenRangeInput = /* @__PURE__ */ (0, react.forwardRef)((props, r
1363
1497
  ...props,
1364
1498
  ref,
1365
1499
  type: "range",
1366
- style: _tremolo_ui_dom.visuallyHiddenStyle
1500
+ style: _tremolo_ui_dom_internal.visuallyHiddenStyle
1367
1501
  }));
1368
1502
  //#endregion
1369
1503
  //#region src/components/PointsEditor/Point.tsx
@@ -1374,17 +1508,27 @@ const VisuallyHiddenRangeInput = /* @__PURE__ */ (0, react.forwardRef)((props, r
1374
1508
  function perAxis(value, axis) {
1375
1509
  return typeof value === "string" ? value : value?.[axis];
1376
1510
  }
1377
- function Point({ value, children, id: idProp, min, max, color, disabled: _disabled, readonly: _readonly, wheel: _wheel, keyboard: _keyboard, "aria-label": ariaLabel, "aria-valuetext": ariaValuetext, onChange, onDragStart, onDragEnd, className, style, onPointerDown, onKeyDown, onFocus, ...props }) {
1378
- const { containerRef, externalStyles, disabled: rootDisabled, readonly: rootReadonly, wheel: rootWheel, keyboard: rootKeyboard, dragSensitivity, selection, editor } = 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();
1379
1513
  const generatedId = (0, react.useId)();
1380
1514
  const id = idProp ?? generatedId;
1381
1515
  const selected = selection.includes(id);
1382
1516
  const disabled = _disabled ?? rootDisabled;
1383
- const readonly = _readonly ?? rootReadonly;
1384
- const inactive = disabled || readonly;
1517
+ const readOnly = _readOnly ?? rootReadOnly;
1518
+ const inactive = disabled || readOnly;
1385
1519
  const wheel = _wheel === void 0 ? rootWheel : _wheel;
1386
1520
  const keyboard = _keyboard === void 0 ? rootKeyboard : _keyboard;
1387
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
+ };
1388
1532
  const [element, setElement] = (0, react.useState)(null);
1389
1533
  const xInputRef = (0, react.useRef)(null);
1390
1534
  const yInputRef = (0, react.useRef)(null);
@@ -1393,9 +1537,10 @@ function Point({ value, children, id: idProp, min, max, color, disabled: _disabl
1393
1537
  min,
1394
1538
  max,
1395
1539
  readonly: inactive,
1396
- onChange,
1540
+ onChange: onChange ? handleChange : void 0,
1397
1541
  element,
1398
- wheel
1542
+ wheel,
1543
+ beforeWheel: () => gesture.pulse("wheel")
1399
1544
  });
1400
1545
  (0, react.useEffect)(() => {
1401
1546
  registration.current = {
@@ -1403,19 +1548,20 @@ function Point({ value, children, id: idProp, min, max, color, disabled: _disabl
1403
1548
  min,
1404
1549
  max,
1405
1550
  readonly: inactive,
1406
- onChange,
1551
+ onChange: onChange ? handleChange : void 0,
1407
1552
  element,
1408
- wheel
1553
+ wheel,
1554
+ beforeWheel: () => gesture.pulse("wheel")
1409
1555
  };
1410
1556
  });
1411
1557
  (0, react.useEffect)(() => editor.registerPoint(id, () => registration.current), [id, editor]);
1412
1558
  /** Where the pointer was when the drag started, to measure the move from. */
1413
1559
  const pointerOrigin = (0, react.useRef)(null);
1414
1560
  const { refCallback: dragRefCallback, dragging } = useDragValue({
1415
- axis: _tremolo_ui_dom.POINT_AXIS,
1561
+ axis: _tremolo_ui_dom_internal.POINT_AXIS,
1416
1562
  baseElementRef: containerRef,
1417
- sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
1418
- 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,
1419
1565
  shouldStart: () => !disabled,
1420
1566
  onChange: ([x, y]) => {
1421
1567
  const origin = pointerOrigin.current;
@@ -1432,29 +1578,27 @@ function Point({ value, children, id: idProp, min, max, color, disabled: _disabl
1432
1578
  y
1433
1579
  };
1434
1580
  xInputRef.current?.focus();
1435
- if (inactive) return;
1436
- onDragStart?.((0, _tremolo_ui_dom.clampPoint)(value, min, max));
1581
+ if (!inactive) gesture.hold("pointer");
1437
1582
  },
1438
1583
  onDragEnd: () => {
1439
1584
  pointerOrigin.current = null;
1440
- if (inactive) return;
1441
- onDragEnd?.((0, _tremolo_ui_dom.clampPoint)(value, min, max));
1585
+ gesture.end();
1442
1586
  }
1443
1587
  });
1444
1588
  const refCallback = require_compose_refs.useComposedRefs(dragRefCallback, setElement);
1445
1589
  const handleKeyDown = (event) => {
1446
- const move = (0, _tremolo_ui_dom.arrowKeyMove)(event.key);
1590
+ const move = (0, _tremolo_ui_dom_internal.arrowKeyMove)(event.key);
1447
1591
  if (!move) return;
1448
1592
  event.preventDefault();
1449
1593
  if (!onChange || inactive || !keyboard) return;
1594
+ gesture.pulse("keyboard");
1450
1595
  editor.nudgePoint(id, move.axis === 0 ? "x" : "y", move.direction, keyboard, event);
1451
1596
  };
1452
- const current = (0, _tremolo_ui_dom.clampPoint)(value, min, max);
1453
1597
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1454
1598
  ref: refCallback,
1455
1599
  className,
1456
1600
  "data-disabled": disabled ? "" : void 0,
1457
- "data-readonly": readonly ? "" : void 0,
1601
+ "data-readonly": readOnly ? "" : void 0,
1458
1602
  tabIndex: -1,
1459
1603
  "data-dragging": dragging ? "" : void 0,
1460
1604
  "data-selected": selected ? "" : void 0,
@@ -1484,12 +1628,12 @@ function Point({ value, children, id: idProp, min, max, color, disabled: _disabl
1484
1628
  max: max?.[axis] ?? 1,
1485
1629
  step: "any",
1486
1630
  disabled,
1487
- "aria-readonly": readonly,
1631
+ "aria-readonly": readOnly,
1488
1632
  "aria-orientation": axis === "x" ? "horizontal" : "vertical",
1489
1633
  "aria-label": perAxis(ariaLabel, axis) ?? axis,
1490
1634
  "aria-valuetext": perAxis(ariaValuetext, axis),
1491
1635
  onChange: (event) => {
1492
- if (readonly) {
1636
+ if (readOnly) {
1493
1637
  event.currentTarget.value = String(current[axis]);
1494
1638
  return;
1495
1639
  }
@@ -1538,9 +1682,8 @@ const EMPTY = [];
1538
1682
  * Multiple Point Controller
1539
1683
  */
1540
1684
  const PointsEditor = {
1541
- Root: /* @__PURE__ */ (0, react.forwardRef)(({ disabled = false, readonly = false, wheel = _tremolo_ui_dom.POINTS_EDITOR_DEFAULT_WHEEL, keyboard = _tremolo_ui_dom.POINTS_EDITOR_DEFAULT_KEYBOARD, dragSensitivity = _tremolo_ui_dom.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) => {
1542
1686
  const containerRef = (0, react.useRef)(null);
1543
- const { cursor = "grabbing" } = externalStyles ?? {};
1544
1687
  const controlled = selectionProp !== void 0;
1545
1688
  const [ownSelection, setOwnSelection] = (0, react.useState)(defaultSelection ?? []);
1546
1689
  const selection = selectable ? selectionProp ?? ownSelection : EMPTY;
@@ -1559,11 +1702,11 @@ const PointsEditor = {
1559
1702
  (0, react.useEffect)(() => () => editor.destroy(), [editor]);
1560
1703
  const context = (0, react.useMemo)(() => ({
1561
1704
  disabled,
1562
- readonly,
1705
+ readOnly,
1563
1706
  wheel,
1564
1707
  keyboard,
1565
1708
  dragSensitivity,
1566
- externalStyles: { cursor },
1709
+ dragCursor,
1567
1710
  containerRef,
1568
1711
  selectable,
1569
1712
  selection,
@@ -1571,11 +1714,11 @@ const PointsEditor = {
1571
1714
  selectionBox
1572
1715
  }), [
1573
1716
  disabled,
1574
- readonly,
1717
+ readOnly,
1575
1718
  wheel,
1576
1719
  keyboard,
1577
1720
  dragSensitivity,
1578
- cursor,
1721
+ dragCursor,
1579
1722
  selectable,
1580
1723
  selection,
1581
1724
  editor,
@@ -1587,7 +1730,7 @@ const PointsEditor = {
1587
1730
  ref: forwardedRef,
1588
1731
  className,
1589
1732
  "data-disabled": disabled ? "" : void 0,
1590
- "data-readonly": readonly ? "" : void 0,
1733
+ "data-readonly": readOnly ? "" : void 0,
1591
1734
  style: {
1592
1735
  position: "relative",
1593
1736
  ...style
@@ -1618,7 +1761,7 @@ function MarksOption({ value, mark = true, label, thickness, length, gap, classe
1618
1761
  const min = useSliderContext((s) => s.min);
1619
1762
  const max = useSliderContext((s) => s.max);
1620
1763
  const scale = useSliderContext((s) => s.scale);
1621
- const vertical = useSliderContext((s) => s.vertical);
1764
+ const vertical = useSliderContext((s) => s.orientation === "vertical");
1622
1765
  const reverse = useSliderContext((s) => s.reverse);
1623
1766
  const percent = (0, _tremolo_ui_dom.valuePercent)(value, {
1624
1767
  min,
@@ -1628,9 +1771,9 @@ function MarksOption({ value, mark = true, label, thickness, length, gap, classe
1628
1771
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1629
1772
  className,
1630
1773
  style: {
1631
- "--thickness": (0, _tremolo_ui_dom.cssLength)(thickness),
1632
- "--length": (0, _tremolo_ui_dom.cssLength)(length),
1633
- "--gap": (0, _tremolo_ui_dom.cssLength)(gap),
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),
1634
1777
  position: "absolute",
1635
1778
  translate: vertical ? "var(--translate, 0 -50%)" : "var(--translate, -50% 0)",
1636
1779
  zIndex: 10,
@@ -1657,15 +1800,15 @@ function Marks({ gap, options, children, className, style, ...props }) {
1657
1800
  const min = useSliderContext((s) => s.min);
1658
1801
  const max = useSliderContext((s) => s.max);
1659
1802
  const step = useSliderContext((s) => s.step);
1660
- const vertical = useSliderContext((s) => s.vertical);
1803
+ const vertical = useSliderContext((s) => s.orientation === "vertical");
1661
1804
  const reverse = useSliderContext((s) => s.reverse);
1662
- const optionsList = options ? (0, _tremolo_ui_dom.sliderMarks)(options, min, max, step) : [];
1805
+ const optionsList = options ? (0, _tremolo_ui_dom_internal.sliderMarks)(options, min, max, step) : [];
1663
1806
  if (vertical !== reverse) optionsList.reverse();
1664
1807
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1665
1808
  className,
1666
1809
  "data-orientation": vertical ? "vertical" : "horizontal",
1667
1810
  style: {
1668
- "--gap": (0, _tremolo_ui_dom.cssLength)(gap),
1811
+ "--gap": (0, _tremolo_ui_dom_internal.cssLength)(gap),
1669
1812
  position: "relative",
1670
1813
  ...style
1671
1814
  },
@@ -1684,9 +1827,9 @@ function Marks({ gap, options, children, className, style, ...props }) {
1684
1827
  }
1685
1828
  //#endregion
1686
1829
  //#region src/components/Slider/Thumb.tsx
1687
- 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) {
1688
1831
  const inputRef = (0, react.useRef)(null);
1689
- 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();
1690
1833
  useCheckPlacement("Slider.Thumb", "Slider.Track");
1691
1834
  const methods = () => ({
1692
1835
  focus() {
@@ -1696,12 +1839,13 @@ const Thumb$1 = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, ch
1696
1839
  inputRef.current?.blur();
1697
1840
  }
1698
1841
  });
1699
- (0, react.useImperativeHandle)(forwardedRef, methods, [disabled]);
1842
+ (0, react.useImperativeHandle)(actionsRef, methods, [disabled]);
1700
1843
  (0, react.useImperativeHandle)(thumbRef, methods, [disabled]);
1701
1844
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1845
+ ref: forwardedRef,
1702
1846
  className,
1703
1847
  "data-disabled": disabled ? "" : void 0,
1704
- "data-readonly": readonly ? "" : void 0,
1848
+ "data-readonly": readOnly ? "" : void 0,
1705
1849
  ...props,
1706
1850
  style: {
1707
1851
  "--color": color,
@@ -1709,8 +1853,8 @@ const Thumb$1 = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, ch
1709
1853
  translate: "var(--translate, -50% -50%)",
1710
1854
  zIndex: 100,
1711
1855
  ...style,
1712
- top: vertical ? `${percent}%` : "50%",
1713
- left: !vertical ? `${percent}%` : "50%"
1856
+ top: orientation === "vertical" ? `${percent}%` : "50%",
1857
+ left: orientation === "vertical" ? "50%" : `${percent}%`
1714
1858
  },
1715
1859
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(VisuallyHiddenRangeInput, {
1716
1860
  ref: inputRef,
@@ -1719,14 +1863,14 @@ const Thumb$1 = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, ch
1719
1863
  max,
1720
1864
  step,
1721
1865
  disabled,
1722
- "aria-readonly": readonly,
1723
- "aria-orientation": vertical ? "vertical" : "horizontal",
1866
+ "aria-readonly": readOnly,
1867
+ "aria-orientation": orientation,
1724
1868
  "aria-label": ariaLabel,
1725
1869
  "aria-labelledby": ariaLabelledby,
1726
1870
  "aria-describedby": ariaDescribedby,
1727
1871
  "aria-valuetext": ariaValuetext,
1728
1872
  onChange: (event) => {
1729
- if (readonly) {
1873
+ if (readOnly) {
1730
1874
  event.currentTarget.value = String(value);
1731
1875
  return;
1732
1876
  }
@@ -1737,20 +1881,20 @@ const Thumb$1 = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, ch
1737
1881
  });
1738
1882
  //#endregion
1739
1883
  //#region src/components/Slider/Track.tsx
1740
- const Track = /* @__PURE__ */ (0, react.forwardRef)(function Track({ length, thickness, active, inactive, children, className, style, ...props }, forwardedRef) {
1741
- 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();
1742
1886
  const composedRef = require_compose_refs.useComposedRefs(forwardedRef, trackRef);
1743
1887
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1744
1888
  ref: composedRef,
1745
1889
  className,
1746
1890
  "data-disabled": disabled ? "" : void 0,
1747
- "data-orientation": vertical ? "vertical" : "horizontal",
1748
- "data-flipped": vertical !== reverse ? "" : void 0,
1891
+ "data-orientation": orientation,
1892
+ "data-flipped": orientation === "vertical" !== reverse ? "" : void 0,
1749
1893
  style: {
1750
- "--active": active,
1751
- "--inactive": inactive,
1752
- "--length": (0, _tremolo_ui_dom.cssLength)(length),
1753
- "--thickness": (0, _tremolo_ui_dom.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),
1754
1898
  "--percent": `${percent}%`,
1755
1899
  position: "relative",
1756
1900
  ...style
@@ -1764,7 +1908,6 @@ const Track = /* @__PURE__ */ (0, react.forwardRef)(function Track({ length, thi
1764
1908
  });
1765
1909
  //#endregion
1766
1910
  //#region src/components/Slider/index.tsx
1767
- const defaultExternalStyles$1 = { cursor: "pointer" };
1768
1911
  /**
1769
1912
  * The wheel only acts while the focus is inside, so that scrolling a page past
1770
1913
  * the control does not change its value.
@@ -1774,14 +1917,11 @@ const WHEEL_OPTIONS$1 = { requireFocus: true };
1774
1917
  * Customizable slider
1775
1918
  */
1776
1919
  const Slider = {
1777
- Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, vertical = false, reverse = false, externalStyles: _externalStyles, 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, 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) => {
1778
1921
  const trackRef = (0, react.useRef)(null);
1779
1922
  const thumbRef = (0, react.useRef)(null);
1780
- const externalStyles = {
1781
- ...defaultExternalStyles$1,
1782
- ..._externalStyles
1783
- };
1784
- const inactive = disabled || readonly;
1923
+ const inactive = disabled || readOnly;
1924
+ const vertical = orientation === "vertical";
1785
1925
  const displayReversed = vertical !== reverse;
1786
1926
  const percent = (0, _tremolo_ui_dom.valuePercent)(value, {
1787
1927
  min,
@@ -1807,60 +1947,70 @@ const Slider = {
1807
1947
  keyboard,
1808
1948
  wheel
1809
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]);
1810
1959
  const handleKeyDown = (0, react.useCallback)((event) => {
1811
- const direction = (0, _tremolo_ui_dom.arrowKeyDirection)(event.key);
1960
+ const direction = (0, _tremolo_ui_dom_internal.arrowKeyDirection)(event.key);
1812
1961
  if (direction === null) return;
1813
1962
  event.preventDefault();
1814
1963
  if (!keyboard || !onChange || inactive) return;
1815
- onChange((0, _tremolo_ui_dom.applyDelta)(value, reverse ? -direction : direction, keyboard, axis, event));
1964
+ gesture.pulse("keyboard");
1965
+ change((0, _tremolo_ui_dom_internal.applyDelta)(value, reverse ? -direction : direction, keyboard, axis, event));
1816
1966
  }, [
1817
1967
  keyboard,
1818
1968
  onChange,
1819
1969
  inactive,
1820
1970
  reverse,
1821
1971
  value,
1822
- axis
1972
+ axis,
1973
+ gesture,
1974
+ change
1823
1975
  ]);
1824
1976
  const valueOf = (v) => v[vertical ? 1 : 0];
1825
1977
  const { refCallback: dragRefCallback } = useDragValue({
1826
1978
  axis,
1827
1979
  baseElementRef: trackRef,
1828
- cursor: inactive ? void 0 : externalStyles.cursor,
1980
+ cursor: inactive ? void 0 : dragCursor,
1829
1981
  shouldStart: () => !inactive,
1830
- sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
1982
+ sensitivity: (state) => (0, _tremolo_ui_dom_internal.selectModifier)(dragSensitivity, state.event).value,
1831
1983
  updateOnPointerDown: true,
1832
1984
  onChange: (v) => {
1833
1985
  if (inactive) return;
1834
- onChange?.(valueOf(v));
1986
+ change(valueOf(v));
1835
1987
  },
1836
- onDragStart: (v) => {
1988
+ onDragStart: () => {
1837
1989
  if (inactive) return;
1990
+ gesture.hold("pointer");
1838
1991
  thumbRef.current?.focus();
1839
- onDragStart?.(valueOf(v));
1840
1992
  },
1841
- onDragEnd: (v) => {
1842
- if (inactive) return;
1843
- onDragEnd?.(valueOf(v));
1844
- }
1993
+ onDragEnd: () => gesture.end()
1845
1994
  });
1846
1995
  const wheelRefCallback = useWheel((event) => {
1847
1996
  if (!wheel || !onChange || inactive) return;
1848
- const direction = (0, _tremolo_ui_dom.wheelDirection)(event, { horizontal: !vertical });
1997
+ const direction = (0, _tremolo_ui_dom_internal.wheelDirection)(event, { horizontal: !vertical });
1849
1998
  if (direction === null) return;
1850
1999
  event.preventDefault();
1851
- onChange((0, _tremolo_ui_dom.applyDelta)(value, reverse ? -direction : direction, wheel, axis, event));
2000
+ gesture.pulse("wheel");
2001
+ change((0, _tremolo_ui_dom_internal.applyDelta)(value, reverse ? -direction : direction, wheel, axis, event));
1852
2002
  }, WHEEL_OPTIONS$1);
1853
- const rootRefCallback = require_compose_refs.useComposedRefs(dragRefCallback, wheelRefCallback);
2003
+ const rootRefCallback = require_compose_refs.useComposedRefs(forwardedRef, dragRefCallback, wheelRefCallback);
1854
2004
  const context = (0, react.useMemo)(() => ({
1855
2005
  value,
1856
2006
  min,
1857
2007
  max,
1858
2008
  step,
1859
2009
  scale,
1860
- vertical,
2010
+ orientation,
1861
2011
  reverse,
1862
2012
  disabled,
1863
- readonly,
2013
+ readOnly,
1864
2014
  onChange,
1865
2015
  percent,
1866
2016
  trackRef,
@@ -1871,14 +2021,14 @@ const Slider = {
1871
2021
  max,
1872
2022
  step,
1873
2023
  scale,
1874
- vertical,
2024
+ orientation,
1875
2025
  reverse,
1876
2026
  disabled,
1877
- readonly,
2027
+ readOnly,
1878
2028
  onChange,
1879
2029
  percent
1880
2030
  ]);
1881
- (0, react.useImperativeHandle)(forwardedRef, () => {
2031
+ (0, react.useImperativeHandle)(actionsRef, () => {
1882
2032
  return {
1883
2033
  focus() {
1884
2034
  if (!disabled) thumbRef.current?.focus();
@@ -1895,9 +2045,9 @@ const Slider = {
1895
2045
  ref: rootRefCallback,
1896
2046
  role: "group",
1897
2047
  tabIndex: -1,
1898
- "data-orientation": vertical ? "vertical" : "horizontal",
2048
+ "data-orientation": orientation,
1899
2049
  "data-disabled": disabled ? "" : void 0,
1900
- "data-readonly": readonly ? "" : void 0,
2050
+ "data-readonly": readOnly ? "" : void 0,
1901
2051
  style,
1902
2052
  onPointerDown,
1903
2053
  onKeyDown: (event) => {
@@ -1952,15 +2102,15 @@ const Area = /* @__PURE__ */ (0, react.forwardRef)(function Area({ children, cla
1952
2102
  });
1953
2103
  //#endregion
1954
2104
  //#region src/components/XYPad/Thumb.tsx
1955
- 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) {
1956
2106
  const xInputRef = (0, react.useRef)(null);
1957
2107
  const yInputRef = (0, react.useRef)(null);
1958
- const { value, min, max, step, disabled, readonly, onChange, percent, thumbRef } = useXYPadContext();
2108
+ const { value, min, max, step, disabled, readOnly, onChange, percent, thumbRef } = useXYPadContext();
1959
2109
  useCheckPlacement("XYPad.Thumb", "XYPad.Area");
1960
- const labels = (0, _tremolo_ui_dom.toXY)(ariaLabel);
1961
- const labelledby = (0, _tremolo_ui_dom.toXY)(ariaLabelledby);
1962
- const describedby = (0, _tremolo_ui_dom.toXY)(ariaDescribedby);
1963
- 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);
1964
2114
  const methods = () => ({
1965
2115
  focus() {
1966
2116
  if (!disabled) xInputRef.current?.focus();
@@ -1970,12 +2120,13 @@ const Thumb = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, chil
1970
2120
  yInputRef.current?.blur();
1971
2121
  }
1972
2122
  });
1973
- (0, react.useImperativeHandle)(forwardedRef, methods, [disabled]);
2123
+ (0, react.useImperativeHandle)(actionsRef, methods, [disabled]);
1974
2124
  (0, react.useImperativeHandle)(thumbRef, methods, [disabled]);
1975
2125
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2126
+ ref: forwardedRef,
1976
2127
  className,
1977
2128
  "data-disabled": disabled ? "" : void 0,
1978
- "data-readonly": readonly ? "" : void 0,
2129
+ "data-readonly": readOnly ? "" : void 0,
1979
2130
  ...props,
1980
2131
  style: {
1981
2132
  "--color": color,
@@ -1994,14 +2145,14 @@ const Thumb = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, chil
1994
2145
  max: max[axis],
1995
2146
  step: step[axis],
1996
2147
  disabled,
1997
- "aria-readonly": readonly,
2148
+ "aria-readonly": readOnly,
1998
2149
  "aria-orientation": axis === 0 ? "horizontal" : "vertical",
1999
2150
  "aria-label": labels[axis] ?? (axis === 0 ? "x" : "y"),
2000
2151
  "aria-labelledby": labelledby[axis],
2001
2152
  "aria-describedby": describedby[axis],
2002
2153
  "aria-valuetext": valueText[axis],
2003
2154
  onChange: (event) => {
2004
- if (readonly) {
2155
+ if (readOnly) {
2005
2156
  event.currentTarget.value = String(value[axis]);
2006
2157
  return;
2007
2158
  }
@@ -2014,7 +2165,6 @@ const Thumb = /* @__PURE__ */ (0, react.forwardRef)(function Thumb({ color, chil
2014
2165
  });
2015
2166
  //#endregion
2016
2167
  //#region src/components/XYPad/index.tsx
2017
- const defaultExternalStyles = { cursor: "pointer" };
2018
2168
  /**
2019
2169
  * The wheel only acts while the focus is inside, so that scrolling a page past
2020
2170
  * the control does not change its value.
@@ -2024,20 +2174,25 @@ const WHEEL_OPTIONS = { requireFocus: true };
2024
2174
  * Simple XYPad
2025
2175
  */
2026
2176
  const XYPad = {
2027
- 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, 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) => {
2028
2178
  const rootRef = (0, react.useRef)(null);
2029
2179
  const areaRef = (0, react.useRef)(null);
2030
2180
  const thumbRef = (0, react.useRef)(null);
2031
- const externalStyles = {
2032
- ...defaultExternalStyles,
2033
- ..._externalStyles
2034
- };
2035
- const inactive = disabled || readonly;
2036
- const min = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_min), [_min]);
2037
- const max = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_max), [_max]);
2038
- const step = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_step), [_step]);
2039
- const scale = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_scale), [_scale]);
2040
- const reverse = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_reverse), [_reverse]);
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]);
2041
2196
  const percent = (0, react.useMemo)(() => [0, 1].map((i) => (0, _tremolo_ui_dom.valuePercent)(value[i], {
2042
2197
  min: min[i],
2043
2198
  max: max[i],
@@ -2078,55 +2233,56 @@ const XYPad = {
2078
2233
  });
2079
2234
  const withAxis = (0, react.useCallback)((axis, next) => axis === 0 ? [next, value[1]] : [value[0], next], [value]);
2080
2235
  /** @param i 0 = x, 1 = y */
2081
- 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)), [
2082
2237
  value,
2083
2238
  axis,
2084
2239
  withAxis
2085
2240
  ]);
2086
2241
  const handleKeyDown = (0, react.useCallback)((event) => {
2087
- const move = (0, _tremolo_ui_dom.arrowKeyMove)(event.key);
2242
+ const move = (0, _tremolo_ui_dom_internal.arrowKeyMove)(event.key);
2088
2243
  if (!move) return;
2089
2244
  event.preventDefault();
2090
2245
  if (!onChange || inactive || !keyboard) return;
2091
2246
  const { axis: i, direction } = move;
2092
- onChange(nudge(i, reverse[i] ? -direction : direction, keyboard, event));
2247
+ gesture.pulse("keyboard");
2248
+ change(nudge(i, reverse[i] ? -direction : direction, keyboard, event));
2093
2249
  }, [
2094
2250
  onChange,
2095
2251
  inactive,
2096
2252
  keyboard,
2097
2253
  reverse,
2098
- nudge
2254
+ nudge,
2255
+ gesture,
2256
+ change
2099
2257
  ]);
2100
2258
  const { refCallback: dragRefCallback } = useDragValue({
2101
2259
  axis,
2102
2260
  baseElementRef: areaRef,
2103
- sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
2261
+ sensitivity: (state) => (0, _tremolo_ui_dom_internal.selectModifier)(dragSensitivity, state.event).value,
2104
2262
  updateOnPointerDown: true,
2105
- cursor: inactive ? void 0 : externalStyles.cursor,
2263
+ cursor: inactive ? void 0 : dragCursor,
2106
2264
  shouldStart: () => !inactive,
2107
2265
  onChange: (v) => {
2108
2266
  if (inactive) return;
2109
- onChange?.(v);
2267
+ change(v);
2110
2268
  },
2111
- onDragStart: (v) => {
2269
+ onDragStart: () => {
2112
2270
  if (inactive) return;
2271
+ gesture.hold("pointer");
2113
2272
  thumbRef.current?.focus();
2114
- onDragStart?.(v);
2115
2273
  },
2116
- onDragEnd: (v) => {
2117
- if (inactive) return;
2118
- onDragEnd?.(v);
2119
- }
2274
+ onDragEnd: () => gesture.end()
2120
2275
  });
2121
2276
  const wheelRefCallback = useWheel((event) => {
2122
2277
  if (!onChange || inactive || !wheel) return;
2123
- const move = (0, _tremolo_ui_dom.wheelMove)(event);
2278
+ const move = (0, _tremolo_ui_dom_internal.wheelMove)(event);
2124
2279
  if (!move) return;
2125
2280
  event.preventDefault();
2126
2281
  const { axis: i, direction } = move;
2127
- onChange(nudge(i, reverse[i] ? -direction : direction, wheel, event));
2282
+ gesture.pulse("wheel");
2283
+ change(nudge(i, reverse[i] ? -direction : direction, wheel, event));
2128
2284
  }, WHEEL_OPTIONS);
2129
- const rootRefCallback = require_compose_refs.useComposedRefs(rootRef, dragRefCallback, wheelRefCallback);
2285
+ const rootRefCallback = require_compose_refs.useComposedRefs(forwardedRef, rootRef, dragRefCallback, wheelRefCallback);
2130
2286
  const context = (0, react.useMemo)(() => ({
2131
2287
  value,
2132
2288
  min,
@@ -2135,7 +2291,7 @@ const XYPad = {
2135
2291
  scale,
2136
2292
  reverse,
2137
2293
  disabled,
2138
- readonly,
2294
+ readOnly,
2139
2295
  onChange,
2140
2296
  percent,
2141
2297
  areaRef,
@@ -2148,19 +2304,18 @@ const XYPad = {
2148
2304
  scale,
2149
2305
  reverse,
2150
2306
  disabled,
2151
- readonly,
2307
+ readOnly,
2152
2308
  onChange,
2153
2309
  percent
2154
2310
  ]);
2155
- (0, react.useImperativeHandle)(forwardedRef, () => {
2311
+ (0, react.useImperativeHandle)(actionsRef, () => {
2156
2312
  return {
2157
2313
  focus() {
2158
2314
  if (!disabled) thumbRef.current?.focus();
2159
2315
  },
2160
2316
  blur() {
2161
2317
  thumbRef.current?.blur();
2162
- },
2163
- original: rootRef
2318
+ }
2164
2319
  };
2165
2320
  }, [disabled]);
2166
2321
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(XYPadProvider, {
@@ -2171,7 +2326,7 @@ const XYPad = {
2171
2326
  role: "group",
2172
2327
  tabIndex: -1,
2173
2328
  "data-disabled": disabled ? "" : void 0,
2174
- "data-readonly": readonly ? "" : void 0,
2329
+ "data-readonly": readOnly ? "" : void 0,
2175
2330
  style,
2176
2331
  onPointerDown,
2177
2332
  onKeyDown: (event) => {
@@ -2196,17 +2351,30 @@ const XYPad = {
2196
2351
  };
2197
2352
  //#endregion
2198
2353
  //#region src/hooks/useAnimationFrame.ts
2199
- function useAnimationFrame(callback = () => {}, deps = []) {
2200
- 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 } = {}) {
2201
2365
  const runCallback = useCallbackRef(callback);
2202
2366
  (0, react.useEffect)(() => {
2203
- const loop = () => {
2204
- reqIdRef.current = requestAnimationFrame(loop);
2205
- 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;
2206
2374
  };
2207
- reqIdRef.current = requestAnimationFrame(loop);
2208
- return () => cancelAnimationFrame(reqIdRef.current);
2209
- }, [runCallback, ...deps]);
2375
+ frameId = requestAnimationFrame(loop);
2376
+ return () => cancelAnimationFrame(frameId);
2377
+ }, [runCallback, disabled]);
2210
2378
  }
2211
2379
  //#endregion
2212
2380
  //#region src/hooks/useDrag.ts
@@ -2246,43 +2414,40 @@ function useDrag({ threshold = 1, cursor, pointerLock, onDrag, onDragStart, onDr
2246
2414
  //#region src/hooks/useEventListener.ts
2247
2415
  function useEventListener(target, event, handler, options) {
2248
2416
  const listener = useCallbackRef(handler);
2249
- const cleanupRef = (0, react.useRef)(() => {});
2417
+ const registrationRef = (0, react.useRef)(null);
2418
+ const { capture = false, once, passive } = typeof options === "boolean" ? { capture: options } : options ?? {};
2250
2419
  (0, react.useEffect)(() => {
2251
- const node = typeof target === "function" ? target() : target ?? document;
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;
2252
2425
  if (!node) return;
2253
- node.addEventListener(event, listener, options);
2254
- const cleanup = () => {
2255
- node.removeEventListener(event, listener, options);
2426
+ const listenerOptions = {
2427
+ capture,
2428
+ once,
2429
+ passive
2256
2430
  };
2257
- cleanupRef.current = cleanup;
2258
- return () => {
2259
- cleanup();
2260
- if (cleanupRef.current === cleanup) cleanupRef.current = () => {};
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)
2261
2439
  };
2262
- }, [
2263
- event,
2264
- target,
2265
- options,
2266
- listener
2267
- ]);
2268
- return (0, react.useCallback)(() => cleanupRef.current(), []);
2269
- }
2270
- //#endregion
2271
- //#region src/hooks/useInterval.ts
2272
- function useInterval(callback, delay) {
2273
- const savedCallback = (0, react.useRef)(callback);
2274
- (0, react.useEffect)(() => {
2275
- savedCallback.current = callback;
2276
- }, [callback]);
2277
- (0, react.useEffect)(() => {
2278
- if (delay === null) return;
2279
- const id = setInterval(() => {
2280
- savedCallback.current();
2281
- }, delay);
2282
- return () => {
2283
- clearInterval(id);
2284
- };
2285
- }, [delay]);
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
+ }, []);
2286
2451
  }
2287
2452
  //#endregion
2288
2453
  //#region src/hooks/useMIDIAccess.ts
@@ -2436,7 +2601,6 @@ exports.useDragValue = useDragValue;
2436
2601
  exports.useDropZone = useDropZone;
2437
2602
  exports.useEventListener = useEventListener;
2438
2603
  exports.useFileInputContext = useFileInputContext;
2439
- exports.useInterval = useInterval;
2440
2604
  exports.useKnobContext = useKnobContext;
2441
2605
  exports.useLongPress = useLongPress;
2442
2606
  exports.useMIDIAccess = useMIDIAccess;