@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.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { useComposedRefs } from "./compose-refs.js";
2
2
  import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
- import { DEFAULT_DRAG_SENSITIVITY, DEFAULT_KEYBOARD_OPTIONS, DEFAULT_WHEEL_OPTIONS, KNOB_VIEWBOX_SIZE, POINTS_EDITOR_DEFAULT_KEYBOARD, POINTS_EDITOR_DEFAULT_WHEEL, POINT_AXIS, applyDelta, arrowKeyDirection, arrowKeyMove, blackKeyWidth, caretAtDecimalOffset, caretDecimalOffset, checkSteps, clampPoint, commitNumberInputText, createAnimationCanvas, createDrag, createDragValue, createDropZone, createLongPress, createMIDIAccess, createMIDIInput, createMIDIMessage, createPianoInput, createPointsEditor, createStepperDrag, createWheel, cssLength, elementMapping, fitWhiteKeyWidth, getNoteRangeArray, knobAngles, knobArcPath, knobArcRadius, notePosition, nudgeNumberInput, numberInputBounds, numberInputRanges, numberSpan, parseNumberText, partitionByAccept, pianoWidth, relativeMapping, selectModifier, sliderMarks, toXY, valuePercent, visuallyHiddenStyle, wheelDirection, wheelMove } from "@tremolo-ui/dom";
3
+ import { DEFAULT_DRAG_SENSITIVITY, DEFAULT_KEYBOARD_OPTIONS, DEFAULT_WHEEL_OPTIONS, KNOB_VIEWBOX_SIZE, createAnimationCanvas, createChangeGesture, createDrag, createDragValue, createDropZone, createLongPress, createMIDIAccess, createMIDIInput, createMIDIMessage, createPianoInput, createPointsEditor, createWheel, elementMapping, knobAngles, knobArcPath, knobArcRadius, notePosition, relativeMapping, valuePercent } from "@tremolo-ui/dom";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { POINTS_EDITOR_DEFAULT_KEYBOARD, POINTS_EDITOR_DEFAULT_WHEEL, POINT_AXIS, applyDelta, arrowKeyDirection, arrowKeyMove, blackKeyWidth, caretAtDecimalOffset, caretDecimalOffset, checkSteps, clampPoint, commitNumberInputText, createStepperDrag, cssLength, fitWhiteKeyWidth, getNoteRangeArray, nudgeNumberInput, numberInputBounds, numberInputRanges, numberSpan, parseNumberText, partitionByAccept, pianoWidth, selectModifier, sliderMarks, toXY, visuallyHiddenStyle, wheelDirection, wheelMove } from "@tremolo-ui/dom/internal";
5
6
  import { clamp, isWhiteKey, linearScale, noteKey } from "@tremolo-ui/functions";
6
7
  //#region src/components/AnimationCanvas/index.tsx
7
8
  /**
@@ -225,6 +226,57 @@ const FileInput = {
225
226
  })
226
227
  };
227
228
  //#endregion
229
+ //#region src/hooks/_internal/useChangeGesture.ts
230
+ /**
231
+ * The core's change gesture, reporting the control's value to
232
+ * `onChangeStart` / `onChangeEnd`. The start gets the value before the change,
233
+ * the end the last one reported.
234
+ *
235
+ * Internal
236
+ * @private
237
+ */
238
+ function useChangeGesture(value, { onChangeStart, onChangeEnd, changeEndDelay }, inactive) {
239
+ const [instance] = useState(() => createChangeGesture());
240
+ const latest = useRef({
241
+ value,
242
+ onChangeStart,
243
+ onChangeEnd
244
+ });
245
+ const lastValue = useRef(value);
246
+ useInsertionEffect(() => {
247
+ latest.current = {
248
+ value,
249
+ onChangeStart,
250
+ onChangeEnd
251
+ };
252
+ });
253
+ useEffect(() => {
254
+ instance.update({
255
+ endDelay: changeEndDelay,
256
+ onStart: (source) => {
257
+ lastValue.current = latest.current.value;
258
+ latest.current.onChangeStart?.(latest.current.value, source);
259
+ },
260
+ onEnd: (source) => latest.current.onChangeEnd?.(lastValue.current, source)
261
+ });
262
+ }, [instance, changeEndDelay]);
263
+ useEffect(() => {
264
+ if (inactive) instance.end();
265
+ }, [instance, inactive]);
266
+ useEffect(() => () => instance.end(), [instance]);
267
+ const changed = useCallback((next) => {
268
+ lastValue.current = next;
269
+ }, []);
270
+ return useMemo(() => ({
271
+ hold: instance.hold,
272
+ pulse: instance.pulse,
273
+ instant: instance.instant,
274
+ end: instance.end,
275
+ active: instance.active,
276
+ changed
277
+ }), [instance, changed]);
278
+ }
279
+ //#endregion
228
280
  //#region src/hooks/_internal/useCheckSteps.ts
229
281
  /**
230
282
  * Warn, in development only, when a key press or a wheel notch cannot produce
@@ -339,7 +391,10 @@ function useWheel(onWheel, { requireFocus = false, target } = {}) {
339
391
  useEffect(() => {
340
392
  const element = target ? target.current : node;
341
393
  if (!element) return;
342
- const instance = createWheel(element, (event) => wheelHandler(event), { requireFocus });
394
+ const instance = createWheel(element, {
395
+ onWheel: (event) => wheelHandler(event),
396
+ requireFocus
397
+ });
343
398
  return () => instance.destroy();
344
399
  }, [
345
400
  node,
@@ -462,7 +517,7 @@ function SVGRoot({ children, style, ...props }) {
462
517
  }
463
518
  //#endregion
464
519
  //#region src/components/Knob/Thumb.tsx
465
- function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor", thumbSize = 84, thumbLineWeight = 6, thumbLineLength = 35, classes, ...props }) {
520
+ function Thumb$2({ className, color = "currentColor", lineColor = "currentColor", size = 84, lineWeight = 6, lineLength = 35, classes, ...props }) {
466
521
  useCheckPlacement("Knob.Thumb", "Knob.SVGRoot");
467
522
  const angleRange = useKnobContext((s) => s.angleRange);
468
523
  const p = useKnobContext((s) => s.p);
@@ -473,16 +528,16 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
473
528
  children: [/* @__PURE__ */ jsx("circle", {
474
529
  cx: "50%",
475
530
  cy: "50%",
476
- r: `${thumbSize / 2}%`,
477
- fill: thumb
531
+ r: `${size / 2}%`,
532
+ fill: color
478
533
  }), /* @__PURE__ */ jsx("line", {
479
- className: classes?.thumbLine,
534
+ className: classes?.line,
480
535
  x1: "50%",
481
- y1: `${(KNOB_VIEWBOX_SIZE - clamp(thumbSize, 0, 100)) / 2}%`,
536
+ y1: `${(KNOB_VIEWBOX_SIZE - clamp(size, 0, 100)) / 2}%`,
482
537
  x2: "50%",
483
- y2: `${thumbLineLength}%`,
484
- stroke: thumbLine,
485
- strokeWidth: `${thumbLineWeight}%`,
538
+ y2: `${lineLength}%`,
539
+ stroke: lineColor,
540
+ strokeWidth: `${lineWeight}%`,
486
541
  style: {
487
542
  transform: `rotate(${r1 + p * angleRange}deg)`,
488
543
  transformOrigin: "50% 50%"
@@ -492,7 +547,6 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
492
547
  }
493
548
  //#endregion
494
549
  //#region src/components/Knob/index.tsx
495
- const defaultExternalStyles$2 = { cursor: "grabbing" };
496
550
  /**
497
551
  * The wheel only acts while the focus is inside, so that scrolling a page past
498
552
  * the control does not change its value.
@@ -502,13 +556,9 @@ const WHEEL_OPTIONS$2 = { requireFocus: true };
502
556
  * Interactive rotary knob component implemented in SVG.
503
557
  */
504
558
  const Knob = {
505
- Root: /* @__PURE__ */ forwardRef(({ value, min, max, step = 1, scale = 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) => {
559
+ Root: /* @__PURE__ */ forwardRef(({ value, min, max, step = 1, scale = linearScale, resetValue, startValue = min, size, dragCursor = "grabbing", wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, pointerLock = false, disabled = false, readOnly = false, angleRange = 270, onChange, onChangeStart, onChangeEnd, changeEndDelay, onKeyDown, onPointerDown, onDoubleClick, className, style, children, actionsRef, ...props }, forwardedRef) => {
506
560
  const elmRef = useRef(null);
507
- const externalStyles = {
508
- ...defaultExternalStyles$2,
509
- ..._externalStyles
510
- };
511
- const inactive = disabled || readonly;
561
+ const inactive = disabled || readOnly;
512
562
  const range = useMemo(() => ({
513
563
  min,
514
564
  max,
@@ -526,18 +576,30 @@ const Knob = {
526
576
  keyboard,
527
577
  wheel
528
578
  });
579
+ const gesture = useChangeGesture(value, {
580
+ onChangeStart,
581
+ onChangeEnd,
582
+ changeEndDelay
583
+ }, inactive);
584
+ const change = useCallback((next) => {
585
+ gesture.changed(next);
586
+ onChange?.(next);
587
+ }, [gesture, onChange]);
529
588
  const handleKeyDown = useCallback((event) => {
530
589
  if (!keyboard || !onChange || inactive) return;
531
590
  const direction = arrowKeyDirection(event.key);
532
591
  if (direction === null) return;
533
592
  event.preventDefault();
534
- onChange(applyDelta(value, direction, keyboard, range, event));
593
+ gesture.pulse("keyboard");
594
+ change(applyDelta(value, direction, keyboard, range, event));
535
595
  }, [
536
596
  keyboard,
537
597
  onChange,
538
598
  inactive,
539
599
  value,
540
- range
600
+ range,
601
+ gesture,
602
+ change
541
603
  ]);
542
604
  const { refCallback: dragRefCallback, dragging } = useDragValue({
543
605
  axis: useMemo(() => [range, {
@@ -547,13 +609,17 @@ const Knob = {
547
609
  getValue: () => [value, value],
548
610
  sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
549
611
  threshold: 1,
550
- cursor: inactive ? void 0 : externalStyles.cursor,
612
+ cursor: inactive ? void 0 : dragCursor,
551
613
  pointerLock: inactive ? false : pointerLock,
552
614
  shouldStart: () => !inactive,
553
615
  onChange: (v) => {
554
616
  if (inactive) return;
555
- onChange?.(v[1]);
556
- }
617
+ change(v[1]);
618
+ },
619
+ onDragStart: () => {
620
+ if (!inactive) gesture.hold("pointer");
621
+ },
622
+ onDragEnd: () => gesture.end()
557
623
  });
558
624
  const wheelRefCallback = useWheel((event) => {
559
625
  if (!wheel || inactive) return;
@@ -561,9 +627,10 @@ const Knob = {
561
627
  if (direction === null) return;
562
628
  event.preventDefault();
563
629
  if (!onChange) return;
564
- onChange(applyDelta(value, direction, wheel, range, event));
630
+ gesture.pulse("wheel");
631
+ change(applyDelta(value, direction, wheel, range, event));
565
632
  }, WHEEL_OPTIONS$2);
566
- const rootRefCallback = useComposedRefs(elmRef, dragRefCallback, wheelRefCallback);
633
+ const rootRefCallback = useComposedRefs(forwardedRef, elmRef, dragRefCallback, wheelRefCallback);
567
634
  const context = useMemo(() => {
568
635
  const config = {
569
636
  value,
@@ -587,7 +654,7 @@ const Knob = {
587
654
  startValue,
588
655
  angleRange
589
656
  ]);
590
- useImperativeHandle(forwardedRef, () => {
657
+ useImperativeHandle(actionsRef, () => {
591
658
  return {
592
659
  focus() {
593
660
  if (!disabled) elmRef.current?.focus();
@@ -608,9 +675,9 @@ const Knob = {
608
675
  "aria-valuemin": min,
609
676
  "aria-valuemax": max,
610
677
  "aria-disabled": disabled,
611
- "aria-readonly": readonly,
678
+ "aria-readonly": readOnly,
612
679
  "data-disabled": disabled ? "" : void 0,
613
- "data-readonly": readonly ? "" : void 0,
680
+ "data-readonly": readOnly ? "" : void 0,
614
681
  "data-dragging": dragging ? "" : void 0,
615
682
  style: {
616
683
  "--knob-size": cssLength(size),
@@ -618,7 +685,7 @@ const Knob = {
618
685
  },
619
686
  onPointerDown,
620
687
  onDoubleClick: (event) => {
621
- if (!inactive && enableDoubleClickDefault && onChange) onChange(defaultValue);
688
+ if (!inactive && resetValue !== null && onChange) gesture.instant("doubleClick", () => change(resetValue ?? startValue));
622
689
  onDoubleClick?.(event);
623
690
  },
624
691
  onKeyDown: (event) => {
@@ -644,6 +711,11 @@ function useNumberInputContext(selector) {
644
711
  if (!context) throw new Error("Missing NumberInputContext.Provider in the tree");
645
712
  return selector ? selector(context) : context;
646
713
  }
714
+ const NumberInputGestureContext = /* @__PURE__ */ createContext(null);
715
+ const NumberInputGestureProvider = NumberInputGestureContext.Provider;
716
+ function useNumberInputGesture() {
717
+ return useContext(NumberInputGestureContext);
718
+ }
647
719
  const StepperContext = /* @__PURE__ */ createContext(null);
648
720
  const StepperProvider = StepperContext.Provider;
649
721
  function useStepperContext() {
@@ -688,8 +760,9 @@ function useLongPress(callback, initialDelay = 500, interval = 40) {
688
760
  * in which way they move the value and which arrow they draw.
689
761
  */
690
762
  function StepperButton({ direction, variant, icon, className, style, children, onPointerDown, ...props }) {
691
- const { step, disabled, readonly, atMin, atMax, nudge } = useNumberInputContext();
763
+ const { step, disabled, readOnly, atMin, atMax, nudge } = useNumberInputContext();
692
764
  const stepper = useStepperContext();
765
+ const gesture = useNumberInputGesture();
693
766
  const press = useLongPress(() => {
694
767
  if (stepper?.moved()) return;
695
768
  nudge(direction, ["raw", step]);
@@ -701,10 +774,13 @@ function StepperButton({ direction, variant, icon, className, style, children, o
701
774
  "aria-label": direction > 0 ? "Increment" : "Decrement",
702
775
  "aria-disabled": disabled || (direction > 0 ? atMax : atMin),
703
776
  "data-disabled": disabled || (direction > 0 ? atMax : atMin) ? "" : void 0,
704
- "data-readonly": readonly ? "" : void 0,
777
+ "data-readonly": readOnly ? "" : void 0,
705
778
  style,
706
779
  onPointerDown: (event) => {
707
- if (!disabled && !readonly) press(event);
780
+ if (!disabled && !readOnly) {
781
+ gesture?.press();
782
+ press(event);
783
+ }
708
784
  onPointerDown?.(event);
709
785
  },
710
786
  ...props,
@@ -760,7 +836,8 @@ function IncrementStepper(props) {
760
836
  * (`selectOnFocus`, `unformatOnFocus`, `keepCaretOnStep`, `blurOnEnter`).
761
837
  */
762
838
  const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, style, onChange, onFocus, onBlur, onKeyDown, ...props }, forwardedRef) {
763
- const { value, min, max, step, disabled, readonly, keyboard, selectOnFocus, unformatOnFocus, keepCaretOnStep, blurOnEnter, text, editing, outOfRange, setDraft, commitDraft, nudge, inputRef } = useNumberInputContext();
839
+ const { value, min, max, step, disabled, readOnly, keyboard, selectOnFocus, unformatOnFocus, keepCaretOnStep, blurOnEnter, text, editing, outOfRange, setDraft, commitDraft, nudge, inputRef } = useNumberInputContext();
840
+ const gesture = useNumberInputGesture();
764
841
  const [focused, setFocused] = useState(false);
765
842
  const shown = unformatOnFocus && focused && !editing ? String(value) : text;
766
843
  useEffect(() => {
@@ -800,11 +877,11 @@ const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, s
800
877
  role: "spinbutton",
801
878
  value: shown,
802
879
  disabled,
803
- readOnly: readonly,
880
+ readOnly,
804
881
  "aria-disabled": disabled,
805
- "aria-readonly": readonly,
882
+ "aria-readonly": readOnly,
806
883
  "data-disabled": disabled ? "" : void 0,
807
- "data-readonly": readonly ? "" : void 0,
884
+ "data-readonly": readOnly ? "" : void 0,
808
885
  "aria-valuenow": value,
809
886
  "aria-valuemin": min,
810
887
  "aria-valuemax": max,
@@ -831,13 +908,14 @@ const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, s
831
908
  if (key === "Enter") {
832
909
  commitDraft();
833
910
  if (blurOnEnter) event.currentTarget.blur();
834
- } else if (keyboard && !disabled && !readonly && (key === "ArrowUp" || key === "ArrowDown") && !event.nativeEvent.isComposing) {
911
+ } else if (keyboard && !disabled && !readOnly && (key === "ArrowUp" || key === "ArrowDown") && !event.nativeEvent.isComposing) {
835
912
  event.preventDefault();
836
913
  const input = event.currentTarget;
837
914
  if (keepCaretOnStep && input.selectionStart !== null) caretAfterStep.current = {
838
915
  offset: caretDecimalOffset(input.value, input.selectionStart),
839
916
  from: input.value
840
917
  };
918
+ gesture?.key();
841
919
  nudge(key === "ArrowUp" ? 1 : -1, keyboard, event);
842
920
  }
843
921
  onKeyDown?.(event);
@@ -855,9 +933,9 @@ const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, s
855
933
  * The drag lives here rather than on `InputField` because `createDrag` turns
856
934
  * off text selection on whatever element it is attached to.
857
935
  */
858
- const Stepper = /* @__PURE__ */ forwardRef(function Stepper({ className, style, children, ...props }, forwardedRef) {
859
- const { value, disabled, readonly, drag, dragSensitivity, pointerLock, rawRange, changeValue } = useNumberInputContext();
860
- const enabled = drag !== null && !disabled && !readonly;
936
+ const Stepper = /* @__PURE__ */ forwardRef(function Stepper({ className, style, children, onPointerDown, ...props }, forwardedRef) {
937
+ const { value, disabled, readOnly, drag, dragSensitivity, pointerLock, dragCursor, rawRange, changeValue } = useNumberInputContext();
938
+ const enabled = drag !== null && !disabled && !readOnly;
861
939
  const [node, setNode] = useState(null);
862
940
  const instanceRef = useRef(null);
863
941
  const latest = useRef({
@@ -868,7 +946,8 @@ const Stepper = /* @__PURE__ */ forwardRef(function Stepper({ className, style,
868
946
  range: rawRange,
869
947
  pixels: drag ?? 1,
870
948
  sensitivity: dragSensitivity,
871
- pointerLock
949
+ pointerLock,
950
+ cursor: dragCursor
872
951
  };
873
952
  const optionsRef = useRef(options);
874
953
  useEffect(() => {
@@ -892,6 +971,7 @@ const Stepper = /* @__PURE__ */ forwardRef(function Stepper({ className, style,
892
971
  instance.destroy();
893
972
  };
894
973
  }, [node, enabled]);
974
+ const gesture = useNumberInputGesture();
895
975
  const composedRef = useComposedRefs(forwardedRef, setNode);
896
976
  const context = useMemo(() => ({ moved: () => instanceRef.current?.moved() ?? false }), []);
897
977
  return /* @__PURE__ */ jsx(StepperProvider, {
@@ -900,6 +980,10 @@ const Stepper = /* @__PURE__ */ forwardRef(function Stepper({ className, style,
900
980
  ref: composedRef,
901
981
  className,
902
982
  style,
983
+ onPointerDown: (event) => {
984
+ if (enabled) gesture?.press();
985
+ onPointerDown?.(event);
986
+ },
903
987
  ...props,
904
988
  children
905
989
  })
@@ -912,14 +996,19 @@ const defaultFormat = (value) => String(value);
912
996
  * Input with some useful functions for entering numerical values.
913
997
  */
914
998
  const NumberInput = {
915
- Root: /* @__PURE__ */ forwardRef(({ value, min, max, step = 1, scale = 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) => {
999
+ Root: /* @__PURE__ */ forwardRef(({ value, min, max, step = 1, scale = linearScale, format: formatProp, parse: parseProp, clampValue = true, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, drag = 1, dragSensitivity = 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) => {
916
1000
  const inputRef = useRef(null);
917
1001
  /**
918
1002
  * The text being typed. The only state here: it is not a copy of `value`,
919
1003
  * but the half-finished entry that has no value to be derived from yet.
920
1004
  */
921
1005
  const [draft, setDraft] = useState(null);
922
- const inactive = disabled || readonly;
1006
+ const inactive = disabled || readOnly;
1007
+ const gesture = useChangeGesture(value, {
1008
+ onChangeStart,
1009
+ onChangeEnd,
1010
+ changeEndDelay
1011
+ }, inactive);
923
1012
  const format = formatProp ?? defaultFormat;
924
1013
  const parse = parseProp ?? parseNumberText;
925
1014
  const { normalized: range, raw: rawRange } = useMemo(() => numberInputRanges({
@@ -969,20 +1058,30 @@ const NumberInput = {
969
1058
  if (inactive) return;
970
1059
  setDraft(next);
971
1060
  const parsed = parse(next);
972
- if (Number.isFinite(parsed)) onChange?.(parsed);
1061
+ if (!Number.isFinite(parsed)) return;
1062
+ if (parsed !== value) {
1063
+ gesture.pulse("keyboard");
1064
+ gesture.changed(parsed);
1065
+ }
1066
+ onChange?.(parsed);
973
1067
  }, [
974
1068
  inactive,
975
1069
  parse,
976
- onChange
1070
+ onChange,
1071
+ value,
1072
+ gesture
977
1073
  ]);
978
1074
  const changeValue = useCallback((next) => {
979
1075
  if (inactive) return;
980
1076
  setDraft(null);
981
- if (next !== value) onChange?.(next);
1077
+ if (next === value) return;
1078
+ gesture.changed(next);
1079
+ onChange?.(next);
982
1080
  }, [
983
1081
  inactive,
984
1082
  value,
985
- onChange
1083
+ onChange,
1084
+ gesture
986
1085
  ]);
987
1086
  const commitDraft = useCallback(() => {
988
1087
  if (draft === null || inactive) return;
@@ -991,8 +1090,13 @@ const NumberInput = {
991
1090
  max,
992
1091
  clampValue
993
1092
  });
994
- if (committed === null) setDraft(null);
995
- else changeValue(committed);
1093
+ if (committed === null) {
1094
+ setDraft(null);
1095
+ return;
1096
+ }
1097
+ if (committed !== value) gesture.pulse("keyboard");
1098
+ changeValue(committed);
1099
+ gesture.end();
996
1100
  }, [
997
1101
  draft,
998
1102
  inactive,
@@ -1000,7 +1104,9 @@ const NumberInput = {
1000
1104
  clampValue,
1001
1105
  min,
1002
1106
  max,
1003
- changeValue
1107
+ changeValue,
1108
+ value,
1109
+ gesture
1004
1110
  ]);
1005
1111
  const nudge = useCallback((direction, option, modifiers) => {
1006
1112
  changeValue(nudgeNumberInput(value, direction, option, ranges, modifiers));
@@ -1013,6 +1119,7 @@ const NumberInput = {
1013
1119
  const direction = wheelDirection(event);
1014
1120
  if (!wheel || inactive || direction === null) return;
1015
1121
  event.preventDefault();
1122
+ gesture.pulse("wheel");
1016
1123
  nudge(direction, wheel, event);
1017
1124
  }, { requireFocus: true });
1018
1125
  const context = useMemo(() => ({
@@ -1022,7 +1129,7 @@ const NumberInput = {
1022
1129
  step,
1023
1130
  scale,
1024
1131
  disabled,
1025
- readonly,
1132
+ readOnly,
1026
1133
  clampValue,
1027
1134
  range,
1028
1135
  rawRange,
@@ -1032,6 +1139,7 @@ const NumberInput = {
1032
1139
  outOfRange,
1033
1140
  dragSensitivity,
1034
1141
  pointerLock,
1142
+ dragCursor,
1035
1143
  selectOnFocus,
1036
1144
  unformatOnFocus,
1037
1145
  keepCaretOnStep,
@@ -1053,7 +1161,7 @@ const NumberInput = {
1053
1161
  step,
1054
1162
  scale,
1055
1163
  disabled,
1056
- readonly,
1164
+ readOnly,
1057
1165
  clampValue,
1058
1166
  range,
1059
1167
  rawRange,
@@ -1066,6 +1174,7 @@ const NumberInput = {
1066
1174
  drag,
1067
1175
  dragSensitivity,
1068
1176
  pointerLock,
1177
+ dragCursor,
1069
1178
  selectOnFocus,
1070
1179
  unformatOnFocus,
1071
1180
  keepCaretOnStep,
@@ -1077,7 +1186,8 @@ const NumberInput = {
1077
1186
  changeValue,
1078
1187
  nudge
1079
1188
  ]);
1080
- useImperativeHandle(forwardedRef, () => {
1189
+ const rootRefCallback = useComposedRefs(forwardedRef, wheelRefCallback);
1190
+ useImperativeHandle(actionsRef, () => {
1081
1191
  return {
1082
1192
  focus() {
1083
1193
  if (!disabled) inputRef.current?.focus();
@@ -1087,16 +1197,39 @@ const NumberInput = {
1087
1197
  }
1088
1198
  };
1089
1199
  }, [disabled]);
1200
+ const pressed = useRef(false);
1201
+ const gestureContext = useMemo(() => ({
1202
+ press: () => {
1203
+ if (inactive) return;
1204
+ gesture.hold("pointer");
1205
+ if (pressed.current) return;
1206
+ pressed.current = true;
1207
+ const release = () => {
1208
+ pressed.current = false;
1209
+ gesture.end();
1210
+ window.removeEventListener("pointerup", release);
1211
+ window.removeEventListener("pointercancel", release);
1212
+ };
1213
+ window.addEventListener("pointerup", release);
1214
+ window.addEventListener("pointercancel", release);
1215
+ },
1216
+ key: () => {
1217
+ if (!inactive) gesture.pulse("keyboard");
1218
+ }
1219
+ }), [gesture, inactive]);
1090
1220
  return /* @__PURE__ */ jsx(NumberInputProvider, {
1091
1221
  value: context,
1092
- children: /* @__PURE__ */ jsx("div", {
1093
- ref: wheelRefCallback,
1094
- className,
1095
- "data-disabled": disabled ? "" : void 0,
1096
- "data-readonly": readonly ? "" : void 0,
1097
- style,
1098
- ...props,
1099
- children
1222
+ children: /* @__PURE__ */ jsx(NumberInputGestureProvider, {
1223
+ value: gestureContext,
1224
+ children: /* @__PURE__ */ jsx("div", {
1225
+ ref: rootRefCallback,
1226
+ className,
1227
+ "data-disabled": disabled ? "" : void 0,
1228
+ "data-readonly": readOnly ? "" : void 0,
1229
+ style,
1230
+ ...props,
1231
+ children
1232
+ })
1100
1233
  })
1101
1234
  });
1102
1235
  }),
@@ -1108,7 +1241,7 @@ const NumberInput = {
1108
1241
  /**
1109
1242
  * Customizable piano component.
1110
1243
  */
1111
- const Piano = { Root: /* @__PURE__ */ 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) {
1244
+ const Piano = { Root: /* @__PURE__ */ 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) {
1112
1245
  const [node, setNode] = useState(null);
1113
1246
  const [activeNotes, setActiveNotes] = useState([]);
1114
1247
  /** Set while `resizable` is on, measured from the parent. */
@@ -1200,7 +1333,8 @@ const Piano = { Root: /* @__PURE__ */ forwardRef(function Root({ noteRange, glis
1200
1333
  keyGap,
1201
1334
  blackKeyWidthRatio
1202
1335
  ]);
1203
- useImperativeHandle(forwardedRef, () => ({
1336
+ const rootRefCallback = useComposedRefs(forwardedRef, setNode);
1337
+ useImperativeHandle(actionsRef, () => ({
1204
1338
  playNote: (note, velocity) => instanceRef.current?.noteOn(note, {
1205
1339
  source: "api",
1206
1340
  velocity
@@ -1208,7 +1342,7 @@ const Piano = { Root: /* @__PURE__ */ forwardRef(function Root({ noteRange, glis
1208
1342
  stopNote: (note) => instanceRef.current?.noteOff(note, { source: "api" })
1209
1343
  }), []);
1210
1344
  return /* @__PURE__ */ jsx("div", {
1211
- ref: setNode,
1345
+ ref: rootRefCallback,
1212
1346
  className,
1213
1347
  "data-resizable": resizable ? "" : void 0,
1214
1348
  role: "group",
@@ -1339,17 +1473,27 @@ const VisuallyHiddenRangeInput = /* @__PURE__ */ forwardRef((props, ref) => /* @
1339
1473
  function perAxis(value, axis) {
1340
1474
  return typeof value === "string" ? value : value?.[axis];
1341
1475
  }
1342
- 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 }) {
1343
- const { containerRef, externalStyles, disabled: rootDisabled, readonly: rootReadonly, wheel: rootWheel, keyboard: rootKeyboard, dragSensitivity, selection, editor } = usePointsEditorContext();
1476
+ 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 }) {
1477
+ const { containerRef, dragCursor, disabled: rootDisabled, readOnly: rootReadOnly, wheel: rootWheel, keyboard: rootKeyboard, dragSensitivity, selection, editor } = usePointsEditorContext();
1344
1478
  const generatedId = useId();
1345
1479
  const id = idProp ?? generatedId;
1346
1480
  const selected = selection.includes(id);
1347
1481
  const disabled = _disabled ?? rootDisabled;
1348
- const readonly = _readonly ?? rootReadonly;
1349
- const inactive = disabled || readonly;
1482
+ const readOnly = _readOnly ?? rootReadOnly;
1483
+ const inactive = disabled || readOnly;
1350
1484
  const wheel = _wheel === void 0 ? rootWheel : _wheel;
1351
1485
  const keyboard = _keyboard === void 0 ? rootKeyboard : _keyboard;
1352
1486
  useCheckPlacement("PointsEditor.Point", "PointsEditor.Container");
1487
+ const current = clampPoint(value, min, max);
1488
+ const gesture = useChangeGesture(current, {
1489
+ onChangeStart,
1490
+ onChangeEnd,
1491
+ changeEndDelay
1492
+ }, inactive);
1493
+ const handleChange = (next) => {
1494
+ gesture.changed(next);
1495
+ onChange?.(next);
1496
+ };
1353
1497
  const [element, setElement] = useState(null);
1354
1498
  const xInputRef = useRef(null);
1355
1499
  const yInputRef = useRef(null);
@@ -1358,9 +1502,10 @@ function Point({ value, children, id: idProp, min, max, color, disabled: _disabl
1358
1502
  min,
1359
1503
  max,
1360
1504
  readonly: inactive,
1361
- onChange,
1505
+ onChange: onChange ? handleChange : void 0,
1362
1506
  element,
1363
- wheel
1507
+ wheel,
1508
+ beforeWheel: () => gesture.pulse("wheel")
1364
1509
  });
1365
1510
  useEffect(() => {
1366
1511
  registration.current = {
@@ -1368,9 +1513,10 @@ function Point({ value, children, id: idProp, min, max, color, disabled: _disabl
1368
1513
  min,
1369
1514
  max,
1370
1515
  readonly: inactive,
1371
- onChange,
1516
+ onChange: onChange ? handleChange : void 0,
1372
1517
  element,
1373
- wheel
1518
+ wheel,
1519
+ beforeWheel: () => gesture.pulse("wheel")
1374
1520
  };
1375
1521
  });
1376
1522
  useEffect(() => editor.registerPoint(id, () => registration.current), [id, editor]);
@@ -1380,7 +1526,7 @@ function Point({ value, children, id: idProp, min, max, color, disabled: _disabl
1380
1526
  axis: POINT_AXIS,
1381
1527
  baseElementRef: containerRef,
1382
1528
  sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
1383
- cursor: inactive ? void 0 : externalStyles.cursor,
1529
+ cursor: inactive ? void 0 : dragCursor,
1384
1530
  shouldStart: () => !disabled,
1385
1531
  onChange: ([x, y]) => {
1386
1532
  const origin = pointerOrigin.current;
@@ -1397,13 +1543,11 @@ function Point({ value, children, id: idProp, min, max, color, disabled: _disabl
1397
1543
  y
1398
1544
  };
1399
1545
  xInputRef.current?.focus();
1400
- if (inactive) return;
1401
- onDragStart?.(clampPoint(value, min, max));
1546
+ if (!inactive) gesture.hold("pointer");
1402
1547
  },
1403
1548
  onDragEnd: () => {
1404
1549
  pointerOrigin.current = null;
1405
- if (inactive) return;
1406
- onDragEnd?.(clampPoint(value, min, max));
1550
+ gesture.end();
1407
1551
  }
1408
1552
  });
1409
1553
  const refCallback = useComposedRefs(dragRefCallback, setElement);
@@ -1412,14 +1556,14 @@ function Point({ value, children, id: idProp, min, max, color, disabled: _disabl
1412
1556
  if (!move) return;
1413
1557
  event.preventDefault();
1414
1558
  if (!onChange || inactive || !keyboard) return;
1559
+ gesture.pulse("keyboard");
1415
1560
  editor.nudgePoint(id, move.axis === 0 ? "x" : "y", move.direction, keyboard, event);
1416
1561
  };
1417
- const current = clampPoint(value, min, max);
1418
1562
  return /* @__PURE__ */ jsxs("div", {
1419
1563
  ref: refCallback,
1420
1564
  className,
1421
1565
  "data-disabled": disabled ? "" : void 0,
1422
- "data-readonly": readonly ? "" : void 0,
1566
+ "data-readonly": readOnly ? "" : void 0,
1423
1567
  tabIndex: -1,
1424
1568
  "data-dragging": dragging ? "" : void 0,
1425
1569
  "data-selected": selected ? "" : void 0,
@@ -1449,12 +1593,12 @@ function Point({ value, children, id: idProp, min, max, color, disabled: _disabl
1449
1593
  max: max?.[axis] ?? 1,
1450
1594
  step: "any",
1451
1595
  disabled,
1452
- "aria-readonly": readonly,
1596
+ "aria-readonly": readOnly,
1453
1597
  "aria-orientation": axis === "x" ? "horizontal" : "vertical",
1454
1598
  "aria-label": perAxis(ariaLabel, axis) ?? axis,
1455
1599
  "aria-valuetext": perAxis(ariaValuetext, axis),
1456
1600
  onChange: (event) => {
1457
- if (readonly) {
1601
+ if (readOnly) {
1458
1602
  event.currentTarget.value = String(current[axis]);
1459
1603
  return;
1460
1604
  }
@@ -1503,9 +1647,8 @@ const EMPTY = [];
1503
1647
  * Multiple Point Controller
1504
1648
  */
1505
1649
  const PointsEditor = {
1506
- Root: /* @__PURE__ */ forwardRef(({ disabled = false, readonly = false, wheel = POINTS_EDITOR_DEFAULT_WHEEL, keyboard = POINTS_EDITOR_DEFAULT_KEYBOARD, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, selectable = false, selection: selectionProp, defaultSelection, onSelectionChange, externalStyles, style, className, children, ...props }, forwardedRef) => {
1650
+ Root: /* @__PURE__ */ forwardRef(({ disabled = false, readOnly = false, wheel = POINTS_EDITOR_DEFAULT_WHEEL, keyboard = POINTS_EDITOR_DEFAULT_KEYBOARD, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, selectable = false, selection: selectionProp, defaultSelection, onSelectionChange, dragCursor = "grabbing", style, className, children, ...props }, forwardedRef) => {
1507
1651
  const containerRef = useRef(null);
1508
- const { cursor = "grabbing" } = externalStyles ?? {};
1509
1652
  const controlled = selectionProp !== void 0;
1510
1653
  const [ownSelection, setOwnSelection] = useState(defaultSelection ?? []);
1511
1654
  const selection = selectable ? selectionProp ?? ownSelection : EMPTY;
@@ -1524,11 +1667,11 @@ const PointsEditor = {
1524
1667
  useEffect(() => () => editor.destroy(), [editor]);
1525
1668
  const context = useMemo(() => ({
1526
1669
  disabled,
1527
- readonly,
1670
+ readOnly,
1528
1671
  wheel,
1529
1672
  keyboard,
1530
1673
  dragSensitivity,
1531
- externalStyles: { cursor },
1674
+ dragCursor,
1532
1675
  containerRef,
1533
1676
  selectable,
1534
1677
  selection,
@@ -1536,11 +1679,11 @@ const PointsEditor = {
1536
1679
  selectionBox
1537
1680
  }), [
1538
1681
  disabled,
1539
- readonly,
1682
+ readOnly,
1540
1683
  wheel,
1541
1684
  keyboard,
1542
1685
  dragSensitivity,
1543
- cursor,
1686
+ dragCursor,
1544
1687
  selectable,
1545
1688
  selection,
1546
1689
  editor,
@@ -1552,7 +1695,7 @@ const PointsEditor = {
1552
1695
  ref: forwardedRef,
1553
1696
  className,
1554
1697
  "data-disabled": disabled ? "" : void 0,
1555
- "data-readonly": readonly ? "" : void 0,
1698
+ "data-readonly": readOnly ? "" : void 0,
1556
1699
  style: {
1557
1700
  position: "relative",
1558
1701
  ...style
@@ -1583,7 +1726,7 @@ function MarksOption({ value, mark = true, label, thickness, length, gap, classe
1583
1726
  const min = useSliderContext((s) => s.min);
1584
1727
  const max = useSliderContext((s) => s.max);
1585
1728
  const scale = useSliderContext((s) => s.scale);
1586
- const vertical = useSliderContext((s) => s.vertical);
1729
+ const vertical = useSliderContext((s) => s.orientation === "vertical");
1587
1730
  const reverse = useSliderContext((s) => s.reverse);
1588
1731
  const percent = valuePercent(value, {
1589
1732
  min,
@@ -1622,7 +1765,7 @@ function Marks({ gap, options, children, className, style, ...props }) {
1622
1765
  const min = useSliderContext((s) => s.min);
1623
1766
  const max = useSliderContext((s) => s.max);
1624
1767
  const step = useSliderContext((s) => s.step);
1625
- const vertical = useSliderContext((s) => s.vertical);
1768
+ const vertical = useSliderContext((s) => s.orientation === "vertical");
1626
1769
  const reverse = useSliderContext((s) => s.reverse);
1627
1770
  const optionsList = options ? sliderMarks(options, min, max, step) : [];
1628
1771
  if (vertical !== reverse) optionsList.reverse();
@@ -1649,9 +1792,9 @@ function Marks({ gap, options, children, className, style, ...props }) {
1649
1792
  }
1650
1793
  //#endregion
1651
1794
  //#region src/components/Slider/Thumb.tsx
1652
- const Thumb$1 = /* @__PURE__ */ forwardRef(function Thumb({ color, children, className, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-valuetext": ariaValuetext, ...props }, forwardedRef) {
1795
+ const Thumb$1 = /* @__PURE__ */ forwardRef(function Thumb({ color, children, className, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-valuetext": ariaValuetext, actionsRef, ...props }, forwardedRef) {
1653
1796
  const inputRef = useRef(null);
1654
- const { value, min, max, step, vertical, disabled, readonly, onChange, percent, thumbRef } = useSliderContext();
1797
+ const { value, min, max, step, orientation, disabled, readOnly, onChange, percent, thumbRef } = useSliderContext();
1655
1798
  useCheckPlacement("Slider.Thumb", "Slider.Track");
1656
1799
  const methods = () => ({
1657
1800
  focus() {
@@ -1661,12 +1804,13 @@ const Thumb$1 = /* @__PURE__ */ forwardRef(function Thumb({ color, children, cla
1661
1804
  inputRef.current?.blur();
1662
1805
  }
1663
1806
  });
1664
- useImperativeHandle(forwardedRef, methods, [disabled]);
1807
+ useImperativeHandle(actionsRef, methods, [disabled]);
1665
1808
  useImperativeHandle(thumbRef, methods, [disabled]);
1666
1809
  return /* @__PURE__ */ jsxs("div", {
1810
+ ref: forwardedRef,
1667
1811
  className,
1668
1812
  "data-disabled": disabled ? "" : void 0,
1669
- "data-readonly": readonly ? "" : void 0,
1813
+ "data-readonly": readOnly ? "" : void 0,
1670
1814
  ...props,
1671
1815
  style: {
1672
1816
  "--color": color,
@@ -1674,8 +1818,8 @@ const Thumb$1 = /* @__PURE__ */ forwardRef(function Thumb({ color, children, cla
1674
1818
  translate: "var(--translate, -50% -50%)",
1675
1819
  zIndex: 100,
1676
1820
  ...style,
1677
- top: vertical ? `${percent}%` : "50%",
1678
- left: !vertical ? `${percent}%` : "50%"
1821
+ top: orientation === "vertical" ? `${percent}%` : "50%",
1822
+ left: orientation === "vertical" ? "50%" : `${percent}%`
1679
1823
  },
1680
1824
  children: [/* @__PURE__ */ jsx(VisuallyHiddenRangeInput, {
1681
1825
  ref: inputRef,
@@ -1684,14 +1828,14 @@ const Thumb$1 = /* @__PURE__ */ forwardRef(function Thumb({ color, children, cla
1684
1828
  max,
1685
1829
  step,
1686
1830
  disabled,
1687
- "aria-readonly": readonly,
1688
- "aria-orientation": vertical ? "vertical" : "horizontal",
1831
+ "aria-readonly": readOnly,
1832
+ "aria-orientation": orientation,
1689
1833
  "aria-label": ariaLabel,
1690
1834
  "aria-labelledby": ariaLabelledby,
1691
1835
  "aria-describedby": ariaDescribedby,
1692
1836
  "aria-valuetext": ariaValuetext,
1693
1837
  onChange: (event) => {
1694
- if (readonly) {
1838
+ if (readOnly) {
1695
1839
  event.currentTarget.value = String(value);
1696
1840
  return;
1697
1841
  }
@@ -1702,18 +1846,18 @@ const Thumb$1 = /* @__PURE__ */ forwardRef(function Thumb({ color, children, cla
1702
1846
  });
1703
1847
  //#endregion
1704
1848
  //#region src/components/Slider/Track.tsx
1705
- const Track = /* @__PURE__ */ forwardRef(function Track({ length, thickness, active, inactive, children, className, style, ...props }, forwardedRef) {
1706
- const { vertical, reverse, disabled, percent, trackRef } = useSliderContext();
1849
+ const Track = /* @__PURE__ */ forwardRef(function Track({ length, thickness, activeColor, inactiveColor, children, className, style, ...props }, forwardedRef) {
1850
+ const { orientation, reverse, disabled, percent, trackRef } = useSliderContext();
1707
1851
  const composedRef = useComposedRefs(forwardedRef, trackRef);
1708
1852
  return /* @__PURE__ */ jsx("div", {
1709
1853
  ref: composedRef,
1710
1854
  className,
1711
1855
  "data-disabled": disabled ? "" : void 0,
1712
- "data-orientation": vertical ? "vertical" : "horizontal",
1713
- "data-flipped": vertical !== reverse ? "" : void 0,
1856
+ "data-orientation": orientation,
1857
+ "data-flipped": orientation === "vertical" !== reverse ? "" : void 0,
1714
1858
  style: {
1715
- "--active": active,
1716
- "--inactive": inactive,
1859
+ "--active-color": activeColor,
1860
+ "--inactive-color": inactiveColor,
1717
1861
  "--length": cssLength(length),
1718
1862
  "--thickness": cssLength(thickness),
1719
1863
  "--percent": `${percent}%`,
@@ -1729,7 +1873,6 @@ const Track = /* @__PURE__ */ forwardRef(function Track({ length, thickness, act
1729
1873
  });
1730
1874
  //#endregion
1731
1875
  //#region src/components/Slider/index.tsx
1732
- const defaultExternalStyles$1 = { cursor: "pointer" };
1733
1876
  /**
1734
1877
  * The wheel only acts while the focus is inside, so that scrolling a page past
1735
1878
  * the control does not change its value.
@@ -1739,14 +1882,11 @@ const WHEEL_OPTIONS$1 = { requireFocus: true };
1739
1882
  * Customizable slider
1740
1883
  */
1741
1884
  const Slider = {
1742
- Root: /* @__PURE__ */ forwardRef(({ value, min, max, step = 1, scale = 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) => {
1885
+ Root: /* @__PURE__ */ forwardRef(({ value, min, max, step = 1, scale = linearScale, orientation = "horizontal", reverse = false, dragCursor = "pointer", wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, disabled = false, readOnly = false, onChange, onChangeStart, onChangeEnd, changeEndDelay, className, style, children, onFocus, onBlur, onPointerDown, onKeyDown, actionsRef, ...props }, forwardedRef) => {
1743
1886
  const trackRef = useRef(null);
1744
1887
  const thumbRef = useRef(null);
1745
- const externalStyles = {
1746
- ...defaultExternalStyles$1,
1747
- ..._externalStyles
1748
- };
1749
- const inactive = disabled || readonly;
1888
+ const inactive = disabled || readOnly;
1889
+ const vertical = orientation === "vertical";
1750
1890
  const displayReversed = vertical !== reverse;
1751
1891
  const percent = valuePercent(value, {
1752
1892
  min,
@@ -1772,60 +1912,70 @@ const Slider = {
1772
1912
  keyboard,
1773
1913
  wheel
1774
1914
  });
1915
+ const gesture = useChangeGesture(value, {
1916
+ onChangeStart,
1917
+ onChangeEnd,
1918
+ changeEndDelay
1919
+ }, inactive);
1920
+ const change = useCallback((next) => {
1921
+ gesture.changed(next);
1922
+ onChange?.(next);
1923
+ }, [gesture, onChange]);
1775
1924
  const handleKeyDown = useCallback((event) => {
1776
1925
  const direction = arrowKeyDirection(event.key);
1777
1926
  if (direction === null) return;
1778
1927
  event.preventDefault();
1779
1928
  if (!keyboard || !onChange || inactive) return;
1780
- onChange(applyDelta(value, reverse ? -direction : direction, keyboard, axis, event));
1929
+ gesture.pulse("keyboard");
1930
+ change(applyDelta(value, reverse ? -direction : direction, keyboard, axis, event));
1781
1931
  }, [
1782
1932
  keyboard,
1783
1933
  onChange,
1784
1934
  inactive,
1785
1935
  reverse,
1786
1936
  value,
1787
- axis
1937
+ axis,
1938
+ gesture,
1939
+ change
1788
1940
  ]);
1789
1941
  const valueOf = (v) => v[vertical ? 1 : 0];
1790
1942
  const { refCallback: dragRefCallback } = useDragValue({
1791
1943
  axis,
1792
1944
  baseElementRef: trackRef,
1793
- cursor: inactive ? void 0 : externalStyles.cursor,
1945
+ cursor: inactive ? void 0 : dragCursor,
1794
1946
  shouldStart: () => !inactive,
1795
1947
  sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
1796
1948
  updateOnPointerDown: true,
1797
1949
  onChange: (v) => {
1798
1950
  if (inactive) return;
1799
- onChange?.(valueOf(v));
1951
+ change(valueOf(v));
1800
1952
  },
1801
- onDragStart: (v) => {
1953
+ onDragStart: () => {
1802
1954
  if (inactive) return;
1955
+ gesture.hold("pointer");
1803
1956
  thumbRef.current?.focus();
1804
- onDragStart?.(valueOf(v));
1805
1957
  },
1806
- onDragEnd: (v) => {
1807
- if (inactive) return;
1808
- onDragEnd?.(valueOf(v));
1809
- }
1958
+ onDragEnd: () => gesture.end()
1810
1959
  });
1811
1960
  const wheelRefCallback = useWheel((event) => {
1812
1961
  if (!wheel || !onChange || inactive) return;
1813
1962
  const direction = wheelDirection(event, { horizontal: !vertical });
1814
1963
  if (direction === null) return;
1815
1964
  event.preventDefault();
1816
- onChange(applyDelta(value, reverse ? -direction : direction, wheel, axis, event));
1965
+ gesture.pulse("wheel");
1966
+ change(applyDelta(value, reverse ? -direction : direction, wheel, axis, event));
1817
1967
  }, WHEEL_OPTIONS$1);
1818
- const rootRefCallback = useComposedRefs(dragRefCallback, wheelRefCallback);
1968
+ const rootRefCallback = useComposedRefs(forwardedRef, dragRefCallback, wheelRefCallback);
1819
1969
  const context = useMemo(() => ({
1820
1970
  value,
1821
1971
  min,
1822
1972
  max,
1823
1973
  step,
1824
1974
  scale,
1825
- vertical,
1975
+ orientation,
1826
1976
  reverse,
1827
1977
  disabled,
1828
- readonly,
1978
+ readOnly,
1829
1979
  onChange,
1830
1980
  percent,
1831
1981
  trackRef,
@@ -1836,14 +1986,14 @@ const Slider = {
1836
1986
  max,
1837
1987
  step,
1838
1988
  scale,
1839
- vertical,
1989
+ orientation,
1840
1990
  reverse,
1841
1991
  disabled,
1842
- readonly,
1992
+ readOnly,
1843
1993
  onChange,
1844
1994
  percent
1845
1995
  ]);
1846
- useImperativeHandle(forwardedRef, () => {
1996
+ useImperativeHandle(actionsRef, () => {
1847
1997
  return {
1848
1998
  focus() {
1849
1999
  if (!disabled) thumbRef.current?.focus();
@@ -1860,9 +2010,9 @@ const Slider = {
1860
2010
  ref: rootRefCallback,
1861
2011
  role: "group",
1862
2012
  tabIndex: -1,
1863
- "data-orientation": vertical ? "vertical" : "horizontal",
2013
+ "data-orientation": orientation,
1864
2014
  "data-disabled": disabled ? "" : void 0,
1865
- "data-readonly": readonly ? "" : void 0,
2015
+ "data-readonly": readOnly ? "" : void 0,
1866
2016
  style,
1867
2017
  onPointerDown,
1868
2018
  onKeyDown: (event) => {
@@ -1917,10 +2067,10 @@ const Area = /* @__PURE__ */ forwardRef(function Area({ children, className, sty
1917
2067
  });
1918
2068
  //#endregion
1919
2069
  //#region src/components/XYPad/Thumb.tsx
1920
- const Thumb = /* @__PURE__ */ forwardRef(function Thumb({ color, children, className, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-valuetext": ariaValuetext, ...props }, forwardedRef) {
2070
+ const Thumb = /* @__PURE__ */ forwardRef(function Thumb({ color, children, className, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-valuetext": ariaValuetext, actionsRef, ...props }, forwardedRef) {
1921
2071
  const xInputRef = useRef(null);
1922
2072
  const yInputRef = useRef(null);
1923
- const { value, min, max, step, disabled, readonly, onChange, percent, thumbRef } = useXYPadContext();
2073
+ const { value, min, max, step, disabled, readOnly, onChange, percent, thumbRef } = useXYPadContext();
1924
2074
  useCheckPlacement("XYPad.Thumb", "XYPad.Area");
1925
2075
  const labels = toXY(ariaLabel);
1926
2076
  const labelledby = toXY(ariaLabelledby);
@@ -1935,12 +2085,13 @@ const Thumb = /* @__PURE__ */ forwardRef(function Thumb({ color, children, class
1935
2085
  yInputRef.current?.blur();
1936
2086
  }
1937
2087
  });
1938
- useImperativeHandle(forwardedRef, methods, [disabled]);
2088
+ useImperativeHandle(actionsRef, methods, [disabled]);
1939
2089
  useImperativeHandle(thumbRef, methods, [disabled]);
1940
2090
  return /* @__PURE__ */ jsxs("div", {
2091
+ ref: forwardedRef,
1941
2092
  className,
1942
2093
  "data-disabled": disabled ? "" : void 0,
1943
- "data-readonly": readonly ? "" : void 0,
2094
+ "data-readonly": readOnly ? "" : void 0,
1944
2095
  ...props,
1945
2096
  style: {
1946
2097
  "--color": color,
@@ -1959,14 +2110,14 @@ const Thumb = /* @__PURE__ */ forwardRef(function Thumb({ color, children, class
1959
2110
  max: max[axis],
1960
2111
  step: step[axis],
1961
2112
  disabled,
1962
- "aria-readonly": readonly,
2113
+ "aria-readonly": readOnly,
1963
2114
  "aria-orientation": axis === 0 ? "horizontal" : "vertical",
1964
2115
  "aria-label": labels[axis] ?? (axis === 0 ? "x" : "y"),
1965
2116
  "aria-labelledby": labelledby[axis],
1966
2117
  "aria-describedby": describedby[axis],
1967
2118
  "aria-valuetext": valueText[axis],
1968
2119
  onChange: (event) => {
1969
- if (readonly) {
2120
+ if (readOnly) {
1970
2121
  event.currentTarget.value = String(value[axis]);
1971
2122
  return;
1972
2123
  }
@@ -1979,7 +2130,6 @@ const Thumb = /* @__PURE__ */ forwardRef(function Thumb({ color, children, class
1979
2130
  });
1980
2131
  //#endregion
1981
2132
  //#region src/components/XYPad/index.tsx
1982
- const defaultExternalStyles = { cursor: "pointer" };
1983
2133
  /**
1984
2134
  * The wheel only acts while the focus is inside, so that scrolling a page past
1985
2135
  * the control does not change its value.
@@ -1989,15 +2139,20 @@ const WHEEL_OPTIONS = { requireFocus: true };
1989
2139
  * Simple XYPad
1990
2140
  */
1991
2141
  const XYPad = {
1992
- Root: /* @__PURE__ */ forwardRef(({ value, min: _min, max: _max, step: _step = 1, scale: _scale = 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) => {
2142
+ Root: /* @__PURE__ */ forwardRef(({ value, min: _min, max: _max, step: _step = 1, scale: _scale = linearScale, reverse: _reverse = false, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, className, style, dragCursor = "pointer", disabled = false, readOnly = false, onChange, onChangeStart, onChangeEnd, changeEndDelay, onPointerDown, onKeyDown, onFocus, onBlur, children, actionsRef, ...props }, forwardedRef) => {
1993
2143
  const rootRef = useRef(null);
1994
2144
  const areaRef = useRef(null);
1995
2145
  const thumbRef = useRef(null);
1996
- const externalStyles = {
1997
- ...defaultExternalStyles,
1998
- ..._externalStyles
1999
- };
2000
- const inactive = disabled || readonly;
2146
+ const inactive = disabled || readOnly;
2147
+ const gesture = useChangeGesture(value, {
2148
+ onChangeStart,
2149
+ onChangeEnd,
2150
+ changeEndDelay
2151
+ }, inactive);
2152
+ const change = useCallback((next) => {
2153
+ gesture.changed(next);
2154
+ onChange?.(next);
2155
+ }, [gesture, onChange]);
2001
2156
  const min = useMemo(() => toXY(_min), [_min]);
2002
2157
  const max = useMemo(() => toXY(_max), [_max]);
2003
2158
  const step = useMemo(() => toXY(_step), [_step]);
@@ -2054,34 +2209,34 @@ const XYPad = {
2054
2209
  event.preventDefault();
2055
2210
  if (!onChange || inactive || !keyboard) return;
2056
2211
  const { axis: i, direction } = move;
2057
- onChange(nudge(i, reverse[i] ? -direction : direction, keyboard, event));
2212
+ gesture.pulse("keyboard");
2213
+ change(nudge(i, reverse[i] ? -direction : direction, keyboard, event));
2058
2214
  }, [
2059
2215
  onChange,
2060
2216
  inactive,
2061
2217
  keyboard,
2062
2218
  reverse,
2063
- nudge
2219
+ nudge,
2220
+ gesture,
2221
+ change
2064
2222
  ]);
2065
2223
  const { refCallback: dragRefCallback } = useDragValue({
2066
2224
  axis,
2067
2225
  baseElementRef: areaRef,
2068
2226
  sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
2069
2227
  updateOnPointerDown: true,
2070
- cursor: inactive ? void 0 : externalStyles.cursor,
2228
+ cursor: inactive ? void 0 : dragCursor,
2071
2229
  shouldStart: () => !inactive,
2072
2230
  onChange: (v) => {
2073
2231
  if (inactive) return;
2074
- onChange?.(v);
2232
+ change(v);
2075
2233
  },
2076
- onDragStart: (v) => {
2234
+ onDragStart: () => {
2077
2235
  if (inactive) return;
2236
+ gesture.hold("pointer");
2078
2237
  thumbRef.current?.focus();
2079
- onDragStart?.(v);
2080
2238
  },
2081
- onDragEnd: (v) => {
2082
- if (inactive) return;
2083
- onDragEnd?.(v);
2084
- }
2239
+ onDragEnd: () => gesture.end()
2085
2240
  });
2086
2241
  const wheelRefCallback = useWheel((event) => {
2087
2242
  if (!onChange || inactive || !wheel) return;
@@ -2089,9 +2244,10 @@ const XYPad = {
2089
2244
  if (!move) return;
2090
2245
  event.preventDefault();
2091
2246
  const { axis: i, direction } = move;
2092
- onChange(nudge(i, reverse[i] ? -direction : direction, wheel, event));
2247
+ gesture.pulse("wheel");
2248
+ change(nudge(i, reverse[i] ? -direction : direction, wheel, event));
2093
2249
  }, WHEEL_OPTIONS);
2094
- const rootRefCallback = useComposedRefs(rootRef, dragRefCallback, wheelRefCallback);
2250
+ const rootRefCallback = useComposedRefs(forwardedRef, rootRef, dragRefCallback, wheelRefCallback);
2095
2251
  const context = useMemo(() => ({
2096
2252
  value,
2097
2253
  min,
@@ -2100,7 +2256,7 @@ const XYPad = {
2100
2256
  scale,
2101
2257
  reverse,
2102
2258
  disabled,
2103
- readonly,
2259
+ readOnly,
2104
2260
  onChange,
2105
2261
  percent,
2106
2262
  areaRef,
@@ -2113,19 +2269,18 @@ const XYPad = {
2113
2269
  scale,
2114
2270
  reverse,
2115
2271
  disabled,
2116
- readonly,
2272
+ readOnly,
2117
2273
  onChange,
2118
2274
  percent
2119
2275
  ]);
2120
- useImperativeHandle(forwardedRef, () => {
2276
+ useImperativeHandle(actionsRef, () => {
2121
2277
  return {
2122
2278
  focus() {
2123
2279
  if (!disabled) thumbRef.current?.focus();
2124
2280
  },
2125
2281
  blur() {
2126
2282
  thumbRef.current?.blur();
2127
- },
2128
- original: rootRef
2283
+ }
2129
2284
  };
2130
2285
  }, [disabled]);
2131
2286
  return /* @__PURE__ */ jsx(XYPadProvider, {
@@ -2136,7 +2291,7 @@ const XYPad = {
2136
2291
  role: "group",
2137
2292
  tabIndex: -1,
2138
2293
  "data-disabled": disabled ? "" : void 0,
2139
- "data-readonly": readonly ? "" : void 0,
2294
+ "data-readonly": readOnly ? "" : void 0,
2140
2295
  style,
2141
2296
  onPointerDown,
2142
2297
  onKeyDown: (event) => {
@@ -2161,17 +2316,30 @@ const XYPad = {
2161
2316
  };
2162
2317
  //#endregion
2163
2318
  //#region src/hooks/useAnimationFrame.ts
2164
- function useAnimationFrame(callback = () => {}, deps = []) {
2165
- const reqIdRef = useRef(-1);
2319
+ /**
2320
+ * Call `callback` on every animation frame for as long as the component is
2321
+ * mounted.
2322
+ *
2323
+ * The callback is read on every frame, so it can be written inline and see
2324
+ * the latest render without restarting the loop.
2325
+ *
2326
+ * @param callback receives the frame's timestamp and the milliseconds since
2327
+ * the previous frame, which is `0` on the first one
2328
+ */
2329
+ function useAnimationFrame(callback, { disabled = false } = {}) {
2166
2330
  const runCallback = useCallbackRef(callback);
2167
2331
  useEffect(() => {
2168
- const loop = () => {
2169
- reqIdRef.current = requestAnimationFrame(loop);
2170
- runCallback();
2332
+ if (disabled) return;
2333
+ let frameId = 0;
2334
+ let previous;
2335
+ const loop = (timestamp) => {
2336
+ frameId = requestAnimationFrame(loop);
2337
+ runCallback(timestamp, previous === void 0 ? 0 : timestamp - previous);
2338
+ previous = timestamp;
2171
2339
  };
2172
- reqIdRef.current = requestAnimationFrame(loop);
2173
- return () => cancelAnimationFrame(reqIdRef.current);
2174
- }, [runCallback, ...deps]);
2340
+ frameId = requestAnimationFrame(loop);
2341
+ return () => cancelAnimationFrame(frameId);
2342
+ }, [runCallback, disabled]);
2175
2343
  }
2176
2344
  //#endregion
2177
2345
  //#region src/hooks/useDrag.ts
@@ -2211,43 +2379,40 @@ function useDrag({ threshold = 1, cursor, pointerLock, onDrag, onDragStart, onDr
2211
2379
  //#region src/hooks/useEventListener.ts
2212
2380
  function useEventListener(target, event, handler, options) {
2213
2381
  const listener = useCallbackRef(handler);
2214
- const cleanupRef = useRef(() => {});
2382
+ const registrationRef = useRef(null);
2383
+ const { capture = false, once, passive } = typeof options === "boolean" ? { capture: options } : options ?? {};
2215
2384
  useEffect(() => {
2216
- const node = typeof target === "function" ? target() : target ?? document;
2385
+ const node = typeof target === "function" ? target() : target;
2386
+ const current = registrationRef.current;
2387
+ if (current && current.node === node && current.event === event && current.capture === capture && current.once === once && current.passive === passive) return;
2388
+ current?.remove();
2389
+ registrationRef.current = null;
2217
2390
  if (!node) return;
2218
- node.addEventListener(event, listener, options);
2219
- const cleanup = () => {
2220
- node.removeEventListener(event, listener, options);
2221
- };
2222
- cleanupRef.current = cleanup;
2223
- return () => {
2224
- cleanup();
2225
- if (cleanupRef.current === cleanup) cleanupRef.current = () => {};
2391
+ const listenerOptions = {
2392
+ capture,
2393
+ once,
2394
+ passive
2226
2395
  };
2227
- }, [
2228
- event,
2229
- target,
2230
- options,
2231
- listener
2232
- ]);
2233
- return useCallback(() => cleanupRef.current(), []);
2234
- }
2235
- //#endregion
2236
- //#region src/hooks/useInterval.ts
2237
- function useInterval(callback, delay) {
2238
- const savedCallback = useRef(callback);
2239
- useEffect(() => {
2240
- savedCallback.current = callback;
2241
- }, [callback]);
2242
- useEffect(() => {
2243
- if (delay === null) return;
2244
- const id = setInterval(() => {
2245
- savedCallback.current();
2246
- }, delay);
2247
- return () => {
2248
- clearInterval(id);
2396
+ node.addEventListener(event, listener, listenerOptions);
2397
+ registrationRef.current = {
2398
+ node,
2399
+ event,
2400
+ capture,
2401
+ once,
2402
+ passive,
2403
+ remove: () => node.removeEventListener(event, listener, listenerOptions)
2249
2404
  };
2250
- }, [delay]);
2405
+ });
2406
+ useEffect(() => () => {
2407
+ registrationRef.current?.remove();
2408
+ registrationRef.current = null;
2409
+ }, []);
2410
+ return useCallback(() => {
2411
+ const current = registrationRef.current;
2412
+ if (!current) return;
2413
+ current.remove();
2414
+ current.remove = () => {};
2415
+ }, []);
2251
2416
  }
2252
2417
  //#endregion
2253
2418
  //#region src/hooks/useMIDIAccess.ts
@@ -2386,6 +2551,6 @@ function useMIDIMessage(midiAccess, onMIDIMessage) {
2386
2551
  });
2387
2552
  }
2388
2553
  //#endregion
2389
- export { AnimationCanvas, DropZone, FileInput, Knob, NumberInput, Piano, PointsEditor, Slider, XYPad, useAnimationFrame, useDrag, useDragValue, useDropZone, useEventListener, useFileInputContext, useInterval, useKnobContext, useLongPress, useMIDIAccess, useMIDIInput, useMIDIMessage, useNumberInputContext, usePointsEditorContext, useSliderContext, useWheel, useXYPadContext };
2554
+ export { AnimationCanvas, DropZone, FileInput, Knob, NumberInput, Piano, PointsEditor, Slider, XYPad, useAnimationFrame, useDrag, useDragValue, useDropZone, useEventListener, useFileInputContext, useKnobContext, useLongPress, useMIDIAccess, useMIDIInput, useMIDIMessage, useNumberInputContext, usePointsEditorContext, useSliderContext, useWheel, useXYPadContext };
2390
2555
 
2391
2556
  //# sourceMappingURL=index.js.map