@tremolo-ui/react 0.0.11 → 0.1.0-alpha.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 (153) hide show
  1. package/dist/cjs/index.js +711 -395
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/AnimationCanvas/index.d.ts +2 -2
  4. package/dist/cjs/types/components/AnimationCanvas/index.d.ts.map +1 -1
  5. package/dist/cjs/types/components/AnimationKnob/index.d.ts +10 -3
  6. package/dist/cjs/types/components/AnimationKnob/index.d.ts.map +1 -1
  7. package/dist/cjs/types/components/Knob/index.d.ts +25 -3
  8. package/dist/cjs/types/components/Knob/index.d.ts.map +1 -1
  9. package/dist/cjs/types/components/NumberInput/DecrementStepper.d.ts +2 -0
  10. package/dist/cjs/types/components/NumberInput/DecrementStepper.d.ts.map +1 -1
  11. package/dist/cjs/types/components/NumberInput/IncrementStepper.d.ts +2 -0
  12. package/dist/cjs/types/components/NumberInput/IncrementStepper.d.ts.map +1 -1
  13. package/dist/cjs/types/components/NumberInput/InternalInput.d.ts +18 -0
  14. package/dist/cjs/types/components/NumberInput/InternalInput.d.ts.map +1 -0
  15. package/dist/cjs/types/components/NumberInput/Stepper.d.ts +2 -0
  16. package/dist/cjs/types/components/NumberInput/Stepper.d.ts.map +1 -1
  17. package/dist/cjs/types/components/NumberInput/context.d.ts +3 -2
  18. package/dist/cjs/types/components/NumberInput/context.d.ts.map +1 -1
  19. package/dist/cjs/types/components/NumberInput/index.d.ts +12 -3
  20. package/dist/cjs/types/components/NumberInput/index.d.ts.map +1 -1
  21. package/dist/cjs/types/components/Piano/KeyLabel.d.ts +1 -3
  22. package/dist/cjs/types/components/Piano/KeyLabel.d.ts.map +1 -1
  23. package/dist/cjs/types/components/Piano/context.d.ts +24 -0
  24. package/dist/cjs/types/components/Piano/context.d.ts.map +1 -0
  25. package/dist/cjs/types/components/Piano/index.d.ts +15 -12
  26. package/dist/cjs/types/components/Piano/index.d.ts.map +1 -1
  27. package/dist/cjs/types/components/Piano/key.d.ts +14 -19
  28. package/dist/cjs/types/components/Piano/key.d.ts.map +1 -1
  29. package/dist/cjs/types/components/Piano/keyboardShortcuts.d.ts +2 -2
  30. package/dist/cjs/types/components/Slider/Scale.d.ts +1 -3
  31. package/dist/cjs/types/components/Slider/Scale.d.ts.map +1 -1
  32. package/dist/cjs/types/components/Slider/Thumb.d.ts +1 -0
  33. package/dist/cjs/types/components/Slider/Thumb.d.ts.map +1 -1
  34. package/dist/cjs/types/components/Slider/Track.d.ts +2 -2
  35. package/dist/cjs/types/components/Slider/Track.d.ts.map +1 -1
  36. package/dist/cjs/types/components/Slider/index.d.ts +5 -1
  37. package/dist/cjs/types/components/Slider/index.d.ts.map +1 -1
  38. package/dist/cjs/types/components/WheelObserver/index.d.ts +3 -3
  39. package/dist/cjs/types/components/WheelObserver/index.d.ts.map +1 -1
  40. package/dist/cjs/types/components/XYPad/Area.d.ts +3 -3
  41. package/dist/cjs/types/components/XYPad/Area.d.ts.map +1 -1
  42. package/dist/cjs/types/components/XYPad/Thumb.d.ts +1 -0
  43. package/dist/cjs/types/components/XYPad/Thumb.d.ts.map +1 -1
  44. package/dist/cjs/types/components/XYPad/index.d.ts +6 -1
  45. package/dist/cjs/types/components/XYPad/index.d.ts.map +1 -1
  46. package/dist/cjs/types/hooks/useDrag.d.ts.map +1 -1
  47. package/dist/cjs/types/hooks/useDragWithElement.d.ts +16 -0
  48. package/dist/cjs/types/hooks/useDragWithElement.d.ts.map +1 -0
  49. package/dist/cjs/types/hooks/useInterval.d.ts +2 -0
  50. package/dist/cjs/types/hooks/useInterval.d.ts.map +1 -0
  51. package/dist/cjs/types/hooks/useLongPress.d.ts +2 -0
  52. package/dist/cjs/types/hooks/useLongPress.d.ts.map +1 -0
  53. package/dist/cjs/types/index.d.ts +5 -0
  54. package/dist/cjs/types/index.d.ts.map +1 -1
  55. package/dist/esm/index.js +706 -396
  56. package/dist/esm/index.js.map +1 -1
  57. package/dist/esm/types/components/AnimationCanvas/index.d.ts +2 -2
  58. package/dist/esm/types/components/AnimationCanvas/index.d.ts.map +1 -1
  59. package/dist/esm/types/components/AnimationKnob/index.d.ts +10 -3
  60. package/dist/esm/types/components/AnimationKnob/index.d.ts.map +1 -1
  61. package/dist/esm/types/components/Knob/index.d.ts +25 -3
  62. package/dist/esm/types/components/Knob/index.d.ts.map +1 -1
  63. package/dist/esm/types/components/NumberInput/DecrementStepper.d.ts +2 -0
  64. package/dist/esm/types/components/NumberInput/DecrementStepper.d.ts.map +1 -1
  65. package/dist/esm/types/components/NumberInput/IncrementStepper.d.ts +2 -0
  66. package/dist/esm/types/components/NumberInput/IncrementStepper.d.ts.map +1 -1
  67. package/dist/esm/types/components/NumberInput/InternalInput.d.ts +18 -0
  68. package/dist/esm/types/components/NumberInput/InternalInput.d.ts.map +1 -0
  69. package/dist/esm/types/components/NumberInput/Stepper.d.ts +2 -0
  70. package/dist/esm/types/components/NumberInput/Stepper.d.ts.map +1 -1
  71. package/dist/esm/types/components/NumberInput/context.d.ts +3 -2
  72. package/dist/esm/types/components/NumberInput/context.d.ts.map +1 -1
  73. package/dist/esm/types/components/NumberInput/index.d.ts +12 -3
  74. package/dist/esm/types/components/NumberInput/index.d.ts.map +1 -1
  75. package/dist/esm/types/components/Piano/KeyLabel.d.ts +1 -3
  76. package/dist/esm/types/components/Piano/KeyLabel.d.ts.map +1 -1
  77. package/dist/esm/types/components/Piano/context.d.ts +24 -0
  78. package/dist/esm/types/components/Piano/context.d.ts.map +1 -0
  79. package/dist/esm/types/components/Piano/index.d.ts +15 -12
  80. package/dist/esm/types/components/Piano/index.d.ts.map +1 -1
  81. package/dist/esm/types/components/Piano/key.d.ts +14 -19
  82. package/dist/esm/types/components/Piano/key.d.ts.map +1 -1
  83. package/dist/esm/types/components/Piano/keyboardShortcuts.d.ts +2 -2
  84. package/dist/esm/types/components/Slider/Scale.d.ts +1 -3
  85. package/dist/esm/types/components/Slider/Scale.d.ts.map +1 -1
  86. package/dist/esm/types/components/Slider/Thumb.d.ts +1 -0
  87. package/dist/esm/types/components/Slider/Thumb.d.ts.map +1 -1
  88. package/dist/esm/types/components/Slider/Track.d.ts +2 -2
  89. package/dist/esm/types/components/Slider/Track.d.ts.map +1 -1
  90. package/dist/esm/types/components/Slider/index.d.ts +5 -1
  91. package/dist/esm/types/components/Slider/index.d.ts.map +1 -1
  92. package/dist/esm/types/components/WheelObserver/index.d.ts +3 -3
  93. package/dist/esm/types/components/WheelObserver/index.d.ts.map +1 -1
  94. package/dist/esm/types/components/XYPad/Area.d.ts +3 -3
  95. package/dist/esm/types/components/XYPad/Area.d.ts.map +1 -1
  96. package/dist/esm/types/components/XYPad/Thumb.d.ts +1 -0
  97. package/dist/esm/types/components/XYPad/Thumb.d.ts.map +1 -1
  98. package/dist/esm/types/components/XYPad/index.d.ts +6 -1
  99. package/dist/esm/types/components/XYPad/index.d.ts.map +1 -1
  100. package/dist/esm/types/hooks/useDrag.d.ts.map +1 -1
  101. package/dist/esm/types/hooks/useDragWithElement.d.ts +16 -0
  102. package/dist/esm/types/hooks/useDragWithElement.d.ts.map +1 -0
  103. package/dist/esm/types/hooks/useInterval.d.ts +2 -0
  104. package/dist/esm/types/hooks/useInterval.d.ts.map +1 -0
  105. package/dist/esm/types/hooks/useLongPress.d.ts +2 -0
  106. package/dist/esm/types/hooks/useLongPress.d.ts.map +1 -0
  107. package/dist/esm/types/index.d.ts +5 -0
  108. package/dist/esm/types/index.d.ts.map +1 -1
  109. package/dist/styles/index.css +117 -33
  110. package/package.json +3 -2
  111. package/src/components/AnimationCanvas/index.tsx +14 -6
  112. package/src/components/AnimationKnob/index.tsx +216 -187
  113. package/src/components/Knob/index.tsx +244 -171
  114. package/src/components/NumberInput/DecrementStepper.tsx +9 -2
  115. package/src/components/NumberInput/IncrementStepper.tsx +9 -2
  116. package/src/components/NumberInput/InternalInput.tsx +219 -0
  117. package/src/components/NumberInput/Stepper.tsx +2 -0
  118. package/src/components/NumberInput/context.tsx +20 -8
  119. package/src/components/NumberInput/index.css +76 -22
  120. package/src/components/NumberInput/index.tsx +94 -162
  121. package/src/components/Piano/KeyLabel.tsx +9 -5
  122. package/src/components/Piano/context.tsx +108 -0
  123. package/src/components/Piano/index.css +24 -6
  124. package/src/components/Piano/index.tsx +192 -122
  125. package/src/components/Piano/key.tsx +215 -0
  126. package/src/components/Piano/keyboardShortcuts.ts +2 -2
  127. package/src/components/Slider/Scale.tsx +1 -4
  128. package/src/components/Slider/Thumb.tsx +4 -0
  129. package/src/components/Slider/Track.tsx +5 -2
  130. package/src/components/Slider/index.css +5 -2
  131. package/src/components/Slider/index.tsx +84 -71
  132. package/src/components/WheelObserver/index.tsx +4 -2
  133. package/src/components/XYPad/Area.tsx +7 -6
  134. package/src/components/XYPad/Thumb.tsx +6 -0
  135. package/src/components/XYPad/index.css +12 -2
  136. package/src/components/XYPad/index.tsx +116 -90
  137. package/src/hooks/useDrag.ts +6 -7
  138. package/src/hooks/useDragWithElement.ts +76 -0
  139. package/src/hooks/useInterval.ts +27 -0
  140. package/src/hooks/useLongPress.ts +31 -0
  141. package/src/index.ts +6 -0
  142. package/src/styles/index.css +0 -1
  143. package/dist/cjs/types/components/Piano/BlackKey.d.ts +0 -5
  144. package/dist/cjs/types/components/Piano/BlackKey.d.ts.map +0 -1
  145. package/dist/cjs/types/components/Piano/WhiteKey.d.ts +0 -7
  146. package/dist/cjs/types/components/Piano/WhiteKey.d.ts.map +0 -1
  147. package/dist/esm/types/components/Piano/BlackKey.d.ts +0 -5
  148. package/dist/esm/types/components/Piano/BlackKey.d.ts.map +0 -1
  149. package/dist/esm/types/components/Piano/WhiteKey.d.ts +0 -7
  150. package/dist/esm/types/components/Piano/WhiteKey.d.ts.map +0 -1
  151. package/src/components/Piano/BlackKey.tsx +0 -118
  152. package/src/components/Piano/WhiteKey.tsx +0 -120
  153. package/src/components/Piano/key.ts +0 -46
package/dist/cjs/index.js CHANGED
@@ -69,7 +69,7 @@ function setDprConfig(canvas, context, width, height, dpr) {
69
69
  canvas.style.height = `${height}px`;
70
70
  }
71
71
  function AnimationCanvas(_a) {
72
- var { width = 100, height = 100, relativeSize = false, reduceFlickering = true, options, init, draw, className } = _a, props = __rest(_a, ["width", "height", "relativeSize", "reduceFlickering", "options", "init", "draw", "className"]);
72
+ var { width = 100, height = 100, relativeSize = false, reduceFlickering = true, options, init, draw, className, onContextMenu = (event) => event.preventDefault() } = _a, props = __rest(_a, ["width", "height", "relativeSize", "reduceFlickering", "options", "init", "draw", "className", "onContextMenu"]);
73
73
  const canvasRef = React.useRef(null);
74
74
  const memoCanvasRef = React.useRef(null);
75
75
  const reqIdRef = React.useRef(-1);
@@ -118,7 +118,11 @@ function AnimationCanvas(_a) {
118
118
  for (const prop of drawingState) {
119
119
  context[prop] = contextMemo[prop];
120
120
  }
121
- if (reduceFlickering && memoContext) {
121
+ if (reduceFlickering &&
122
+ memoContext &&
123
+ memoCanvas &&
124
+ memoCanvas.width > 0 &&
125
+ memoCanvas.height > 0) {
122
126
  context.drawImage(memoContext.canvas, 0, 0);
123
127
  }
124
128
  }
@@ -150,7 +154,7 @@ function AnimationCanvas(_a) {
150
154
  };
151
155
  }
152
156
  }, [loop, init, options, reduceFlickering, relativeSize]);
153
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("canvas", Object.assign({ className: clsx('tremolo-animation-canvas', className), width: relativeSize ? 0 : width, height: relativeSize ? 0 : height, ref: canvasRef }, props)), relativeSize && reduceFlickering && (jsxRuntime.jsx("canvas", { style: { display: 'none' }, ref: memoCanvasRef }))] }));
157
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("canvas", Object.assign({ className: clsx('tremolo-animation-canvas', className), width: relativeSize ? 0 : width, height: relativeSize ? 0 : height, ref: canvasRef, onContextMenu: onContextMenu }, props)), relativeSize && reduceFlickering && (jsxRuntime.jsx("canvas", { style: { display: 'none' }, ref: memoCanvasRef }))] }));
154
158
  }
155
159
 
156
160
  /**
@@ -258,7 +262,7 @@ function xor(a = false, b = false) {
258
262
  /**
259
263
  * @category MIDI
260
264
  */
261
- const noteNames = [
265
+ const noteKeys = [
262
266
  'C',
263
267
  'C#',
264
268
  'D',
@@ -292,18 +296,16 @@ function parseNoteName(noteName) {
292
296
  */
293
297
  function noteNumber(noteName) {
294
298
  const { letter, accidental, octave } = parseNoteName(noteName);
295
- const noteIndex = noteNames.indexOf(letter.toLocaleUpperCase());
299
+ const noteIndex = noteKeys.indexOf(letter.toLocaleUpperCase());
296
300
  const accidentalValue = (accidental[0] == 'b' ? -1 : 1) * accidental.length;
297
301
  return noteIndex + 12 * (Number(octave) + 1) + accidentalValue;
298
302
  }
299
303
  /**
300
- * Convert noteNumber to noteName
304
+ * Convert noteNumber to noteKey
301
305
  * @category MIDI
302
306
  */
303
- function noteName(noteNumber) {
304
- const noteIndex = mod(noteNumber, 12);
305
- const octave = Math.floor(noteNumber / 12) - 1;
306
- return noteNames[noteIndex] + octave;
307
+ function noteKey(noteNumber) {
308
+ return noteKeys[mod(noteNumber, 12)];
307
309
  }
308
310
  /**
309
311
  * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'
@@ -319,6 +321,13 @@ function isWhiteKey(note) {
319
321
  mod(n, 12) == 9 ||
320
322
  mod(n, 12) == 11);
321
323
  }
324
+ /**
325
+ * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'
326
+ * @category MIDI
327
+ */
328
+ function isBlackKey(note) {
329
+ return !isWhiteKey(note);
330
+ }
322
331
 
323
332
  /**
324
333
  * This hook is user-land implementation of the experimental `useEffectEvent` hook.
@@ -399,8 +408,7 @@ function useDrag({ threshold = 1, onDrag, onDragStart, onDragEnd, }) {
399
408
  }
400
409
  if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold)
401
410
  return;
402
- if (onDrag)
403
- onDrag(screenX - dragStartX.current, screenY - dragStartY.current, deltaX, deltaY);
411
+ onDrag(screenX - dragStartX.current, screenY - dragStartY.current, deltaX, deltaY);
404
412
  }, [threshold, onDrag]);
405
413
  const refHandler = useRefCallbackEvent('touchmove', handleDrag, { passive: false }, [onDrag]);
406
414
  const pointerDownHandler = React.useCallback((event) => {
@@ -426,13 +434,14 @@ function removeNoSelect() {
426
434
  }
427
435
 
428
436
  /**
429
- * simple rotary Knob.
437
+ * Animatable rotary knob.
430
438
  * @category AnimationKnob
431
439
  */
432
- function AnimationKnob(_a) {
440
+ const AnimationKnob = React.forwardRef((_a, ref) => {
433
441
  var { value, min, max, defaultValue = min, startValue = min, step = 1, skew = 1, size, width = 50, height = 50, activeColor = '#7998ec', inactiveColor = '#eee', bg = '#ccc', thumb = '#4e76e6', lineWeight = 6, thumbWeight = 4, draw, bodyNoSelect = true, wheel = ['raw', 1], keyboard = ['raw', 1], enableDoubleClickDefault = true, style, className, onChange } = _a, props = __rest(_a, ["value", "min", "max", "defaultValue", "startValue", "step", "skew", "size", "width", "height", "activeColor", "inactiveColor", "bg", "thumb", "lineWeight", "thumbWeight", "draw", "bodyNoSelect", "wheel", "keyboard", "enableDoubleClickDefault", "style", "className", "onChange"]);
434
442
  // -- state and ref ---
435
443
  const valueRef = React.useRef(0);
444
+ const elmRef = React.useRef(null);
436
445
  // --- interpret props ---
437
446
  const isRelativeSize = typeof (size !== null && size !== void 0 ? size : width) == 'string' || typeof height == 'string';
438
447
  const onDrag = React.useCallback((_x, y) => {
@@ -489,14 +498,27 @@ function AnimationKnob(_a) {
489
498
  if (onChange)
490
499
  onChange(clamp(stepValue(v, step), min, max));
491
500
  }, { passive: false }, [wheel, value, min, max, skew, step, onChange]);
501
+ React.useImperativeHandle(ref, () => {
502
+ return {
503
+ focus() {
504
+ var _a;
505
+ (_a = elmRef.current) === null || _a === void 0 ? void 0 : _a.focus();
506
+ },
507
+ blur() {
508
+ var _a;
509
+ (_a = elmRef.current) === null || _a === void 0 ? void 0 : _a.blur();
510
+ },
511
+ };
512
+ }, []);
492
513
  return (jsxRuntime.jsx("div", Object.assign({ className: clsx('tremolo-animation-knob', className), ref: (div) => {
514
+ elmRef.current = div;
493
515
  wheelRefCallback(div);
494
516
  touchMoveRefCallback(div);
495
517
  }, role: "slider", tabIndex: 0, "aria-valuenow": value, "aria-valuemax": max, "aria-valuemin": min, style: Object.assign({ display: 'inline-block', width: size !== null && size !== void 0 ? size : width, height: size !== null && size !== void 0 ? size : height, cursor: 'pointer', WebkitTapHighlightColor: 'transparent' }, style), onPointerDown: pointerDownHandler, onDoubleClick: () => {
496
518
  if (enableDoubleClickDefault && onChange) {
497
519
  onChange(defaultValue);
498
520
  }
499
- }, onKeyDown: handleKeyDown, onContextMenu: (e) => e.preventDefault() }, props, { children: jsxRuntime.jsx(AnimationCanvas, Object.assign({}, (isRelativeSize
521
+ }, onKeyDown: handleKeyDown }, props, { children: jsxRuntime.jsx(AnimationCanvas, Object.assign({}, (isRelativeSize
500
522
  ? { relativeSize: true }
501
523
  : {
502
524
  width: size !== null && size !== void 0 ? size : width,
@@ -543,7 +565,7 @@ function AnimationKnob(_a) {
543
565
  ctx.lineWidth = thumbWeight;
544
566
  ctx.stroke();
545
567
  } })) })));
546
- }
568
+ });
547
569
 
548
570
  /** @category DragObserver */
549
571
  function DragObserver(props) {
@@ -557,15 +579,20 @@ function DragObserver(props) {
557
579
  return (jsxRuntime.jsx(Component, Object.assign({ ref: refCallback, onPointerDown: pointerDownHandler }, attributes, { children: children })));
558
580
  }
559
581
 
560
- /** @category Knob */
561
- function Knob(_a) {
562
- var { value, min, max, step = 1, skew = 1, defaultValue = min, startValue = min, size = 50, bodyNoSelect = true, wheel = ['raw', 1], keyboard = ['raw', 1], enableDoubleClickDefault = true, disabled = false, readonly = false, activeLine, inactiveLine, thumb, thumbLine, onChange, className, classes } = _a, props = __rest(_a, ["value", "min", "max", "step", "skew", "defaultValue", "startValue", "size", "bodyNoSelect", "wheel", "keyboard", "enableDoubleClickDefault", "disabled", "readonly", "activeLine", "inactiveLine", "thumb", "thumbLine", "onChange", "className", "classes"]);
582
+ /**
583
+ * Interactive rotary knob component implemented in SVG.
584
+ *
585
+ * @category Knob
586
+ */
587
+ const Knob = React.forwardRef((_a, ref) => {
588
+ var { value, min, max, step = 1, skew = 1, defaultValue = min, startValue = min, size = 50, bodyNoSelect = true, wheel = ['raw', 1], keyboard = ['raw', 1], enableDoubleClickDefault = true, disabled = false, readonly = false, activeLine, inactiveLine, thumb, thumbLine, onChange, onKeyDown, onPointerDown, onDoubleClick, className, classes } = _a, props = __rest(_a, ["value", "min", "max", "step", "skew", "defaultValue", "startValue", "size", "bodyNoSelect", "wheel", "keyboard", "enableDoubleClickDefault", "disabled", "readonly", "activeLine", "inactiveLine", "thumb", "thumbLine", "onChange", "onKeyDown", "onPointerDown", "onDoubleClick", "className", "classes"]);
563
589
  const valueRef = React.useRef(0);
590
+ const elmRef = React.useRef(null);
564
591
  // --- interpret props ---
565
592
  const padding = 8; // %
566
593
  const thumbLineWeight = 6; // %
567
- const thumbLength = 35; // %
568
- const lineWeight = 3; // px
594
+ const thumbLineLength = 35; // %
595
+ const lineWeight = size * 0.06; // px
569
596
  const p = normalizeValue(value, min, max, skew);
570
597
  const s = normalizeValue(startValue, min, max, skew);
571
598
  // --- internal functions ---
@@ -576,6 +603,17 @@ function Knob(_a) {
576
603
  const v2 = clamp(stepValue(v, step), min, max);
577
604
  onChange(v2);
578
605
  }, [max, min, onChange, readonly, skew, step]);
606
+ const updateValueByEvent = React.useCallback((eventType, x) => {
607
+ let newValue;
608
+ if (eventType == 'normalized') {
609
+ const n = normalizeValue(value, min, max, skew);
610
+ newValue = rawValue(n + x, min, max, skew);
611
+ }
612
+ else {
613
+ newValue = value + x;
614
+ }
615
+ return clamp(stepValue(newValue, step), min, max);
616
+ }, [max, min, skew, step, value]);
579
617
  const handleKeyDown = React.useCallback((event) => {
580
618
  if (!keyboard || !onChange || readonly)
581
619
  return;
@@ -583,17 +621,9 @@ function Knob(_a) {
583
621
  if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
584
622
  event.preventDefault();
585
623
  const x = key == 'ArrowRight' || key == 'ArrowUp' ? keyboard[1] : -keyboard[1];
586
- let v;
587
- if (keyboard[0] == 'normalized') {
588
- const n = normalizeValue(value, min, max, skew);
589
- v = rawValue(n + x, min, max, skew);
590
- }
591
- else {
592
- v = value + x;
593
- }
594
- onChange(clamp(stepValue(v, step), min, max));
624
+ onChange(updateValueByEvent(keyboard[0], x));
595
625
  }
596
- }, [value, min, max, step, skew, keyboard, readonly, onChange]);
626
+ }, [keyboard, onChange, readonly, updateValueByEvent]);
597
627
  // --- hooks ---
598
628
  const [touchMoveRefCallback, pointerDownHandler] = useDrag({
599
629
  onDrag: onDrag,
@@ -608,21 +638,26 @@ function Knob(_a) {
608
638
  },
609
639
  });
610
640
  const wheelRefCallback = useRefCallbackEvent('wheel', (event) => {
611
- if (!wheel)
641
+ if (!wheel || readonly)
612
642
  return;
613
643
  event.preventDefault();
614
- const x = event.deltaY > 0 ? -wheel[1] : wheel[1];
615
- let v;
616
- if (wheel[0] == 'normalized') {
617
- const n = normalizeValue(value, min, max, skew);
618
- v = rawValue(n + x, min, max, skew);
619
- }
620
- else {
621
- v = value + x;
622
- }
623
- if (onChange)
624
- onChange(clamp(stepValue(v, step), min, max));
625
- }, { passive: false }, [wheel, value, min, max, skew, step, onChange]);
644
+ if (!onChange || event.deltaY == 0)
645
+ return;
646
+ const x = Math.sign(event.deltaY) * -wheel[1];
647
+ onChange(updateValueByEvent(wheel[0], x));
648
+ }, { passive: false }, [wheel, onChange, readonly, updateValueByEvent]);
649
+ React.useImperativeHandle(ref, () => {
650
+ return {
651
+ focus() {
652
+ var _a;
653
+ (_a = elmRef.current) === null || _a === void 0 ? void 0 : _a.focus();
654
+ },
655
+ blur() {
656
+ var _a;
657
+ (_a = elmRef.current) === null || _a === void 0 ? void 0 : _a.blur();
658
+ },
659
+ };
660
+ }, []);
626
661
  const center = size / 2;
627
662
  const r1 = -135;
628
663
  const r2 = -135 + Math.min(p, s) * 270;
@@ -637,13 +672,62 @@ function Knob(_a) {
637
672
  const x4 = center + center * Math.cos(radian(r4 - 90));
638
673
  const y4 = center + center * Math.sin(radian(r4 - 90));
639
674
  return (jsxRuntime.jsxs("svg", Object.assign({ className: clsx('tremolo-knob', className), ref: (div) => {
675
+ elmRef.current = div;
640
676
  wheelRefCallback(div);
641
677
  touchMoveRefCallback(div);
642
- }, width: size, height: size, tabIndex: 0, "aria-valuenow": value, "aria-valuemin": min, "aria-valuemax": max, "aria-disabled": disabled, "aria-readonly": readonly, onPointerDown: pointerDownHandler, onDoubleClick: () => {
678
+ }, width: size, height: size, tabIndex: 0, role: "slider", "aria-valuenow": value, "aria-valuemin": min, "aria-valuemax": max, "aria-disabled": disabled, "aria-readonly": readonly, onPointerDown: (event) => {
679
+ pointerDownHandler(event);
680
+ onPointerDown === null || onPointerDown === void 0 ? void 0 : onPointerDown(event);
681
+ }, onDoubleClick: (event) => {
643
682
  if (enableDoubleClickDefault && onChange) {
644
683
  onChange(defaultValue);
645
684
  }
646
- }, onKeyDown: handleKeyDown, onContextMenu: (e) => e.preventDefault() }, props, { children: [startValue > min && (jsxRuntime.jsx("path", { className: clsx('tremolo-knob-inactive-line', classes === null || classes === void 0 ? void 0 : classes.inactiveLine), d: `M ${x1} ${y1} A ${center} ${center} -135 ${r2 - r1 > 180 ? 1 : 0} 1 ${x2}, ${y2}`, fill: "none", stroke: inactiveLine || 'currentColor', strokeWidth: lineWeight })), jsxRuntime.jsx("path", { className: clsx('tremolo-knob-active-line', classes === null || classes === void 0 ? void 0 : classes.activeLine), d: `M ${x2} ${y2} A ${center} ${center} -135 ${r3 - r2 > 180 ? 1 : 0} 1 ${x3}, ${y3}`, fill: "none", stroke: activeLine || 'currentColor', strokeWidth: lineWeight }), startValue < max && (jsxRuntime.jsx("path", { className: clsx('tremolo-knob-inactive-line', classes === null || classes === void 0 ? void 0 : classes.inactiveLine), d: `M ${x3} ${y3} A ${center} ${center} -135 ${r4 - r3 > 180 ? 1 : 0} 1 ${x4}, ${y4}`, fill: "none", stroke: inactiveLine || 'currentColor', strokeWidth: lineWeight })), jsxRuntime.jsxs("svg", { className: clsx('tremolo-knob-thumb', classes === null || classes === void 0 ? void 0 : classes.thumb), children: [jsxRuntime.jsx("circle", { cx: "50%", cy: "50%", r: `${50 - padding}%`, fill: thumb || 'currentColor' }), jsxRuntime.jsx("line", { className: clsx('tremolo-knob-thumb-line', classes === null || classes === void 0 ? void 0 : classes.thumbLine), x1: "50%", y1: `${padding}%`, x2: "50%", y2: `${thumbLength}%`, stroke: thumbLine || 'currentColor', strokeWidth: `${thumbLineWeight}%`, transform: `rotate(${ -135 + p * 270})`, "transform-origin": "50% 50%" })] })] })));
685
+ onDoubleClick === null || onDoubleClick === void 0 ? void 0 : onDoubleClick(event);
686
+ }, onKeyDown: (event) => {
687
+ handleKeyDown(event);
688
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
689
+ } }, props, { children: [startValue > min && (jsxRuntime.jsx("path", { className: clsx('tremolo-knob-inactive-line', classes === null || classes === void 0 ? void 0 : classes.inactiveLine), d: `M ${x1} ${y1} A ${center} ${center} -135 ${r2 - r1 > 180 ? 1 : 0} 1 ${x2}, ${y2}`, fill: "none", stroke: inactiveLine || 'currentColor', strokeWidth: lineWeight })), jsxRuntime.jsx("path", { className: clsx('tremolo-knob-active-line', classes === null || classes === void 0 ? void 0 : classes.activeLine), d: `M ${x2} ${y2} A ${center} ${center} -135 ${r3 - r2 > 180 ? 1 : 0} 1 ${x3}, ${y3}`, fill: "none", stroke: activeLine || 'currentColor', strokeWidth: lineWeight }), startValue < max && (jsxRuntime.jsx("path", { className: clsx('tremolo-knob-inactive-line', classes === null || classes === void 0 ? void 0 : classes.inactiveLine), d: `M ${x3} ${y3} A ${center} ${center} -135 ${r4 - r3 > 180 ? 1 : 0} 1 ${x4}, ${y4}`, fill: "none", stroke: inactiveLine || 'currentColor', strokeWidth: lineWeight })), jsxRuntime.jsxs("svg", { className: clsx('tremolo-knob-thumb', classes === null || classes === void 0 ? void 0 : classes.thumb), children: [jsxRuntime.jsx("circle", { cx: "50%", cy: "50%", r: `${50 - padding}%`, fill: thumb || 'currentColor' }), jsxRuntime.jsx("line", { className: clsx('tremolo-knob-thumb-line', classes === null || classes === void 0 ? void 0 : classes.thumbLine), x1: "50%", y1: `${padding}%`, x2: "50%", y2: `${thumbLineLength}%`, stroke: thumbLine || 'currentColor', strokeWidth: `${thumbLineWeight}%`,
690
+ // NOTE
691
+ // https://bugs.webkit.org/show_bug.cgi?id=201854
692
+ // https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/transform-origin#browser_compatibility
693
+ style: {
694
+ transform: `rotate(${ -135 + p * 270}deg)`,
695
+ transformOrigin: '50% 50%',
696
+ } })] })] })));
697
+ });
698
+
699
+ const createStoreImpl = (createState) => {
700
+ let state;
701
+ const listeners = /* @__PURE__ */ new Set();
702
+ const setState = (partial, replace) => {
703
+ const nextState = typeof partial === "function" ? partial(state) : partial;
704
+ if (!Object.is(nextState, state)) {
705
+ const previousState = state;
706
+ state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
707
+ listeners.forEach((listener) => listener(state, previousState));
708
+ }
709
+ };
710
+ const getState = () => state;
711
+ const getInitialState = () => initialState;
712
+ const subscribe = (listener) => {
713
+ listeners.add(listener);
714
+ return () => listeners.delete(listener);
715
+ };
716
+ const api = { setState, getState, getInitialState, subscribe };
717
+ const initialState = state = createState(setState, getState, api);
718
+ return api;
719
+ };
720
+ const createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;
721
+
722
+ const identity = (arg) => arg;
723
+ function useStore(api, selector = identity) {
724
+ const slice = React.useSyncExternalStore(
725
+ api.subscribe,
726
+ () => selector(api.getState()),
727
+ () => selector(api.getInitialState())
728
+ );
729
+ React.useDebugValue(slice);
730
+ return slice;
647
731
  }
648
732
 
649
733
  /**
@@ -692,40 +776,6 @@ function parseValue(inputString, units, digit) {
692
776
  };
693
777
  }
694
778
 
695
- const createStoreImpl = (createState) => {
696
- let state;
697
- const listeners = /* @__PURE__ */ new Set();
698
- const setState = (partial, replace) => {
699
- const nextState = typeof partial === "function" ? partial(state) : partial;
700
- if (!Object.is(nextState, state)) {
701
- const previousState = state;
702
- state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
703
- listeners.forEach((listener) => listener(state, previousState));
704
- }
705
- };
706
- const getState = () => state;
707
- const getInitialState = () => initialState;
708
- const subscribe = (listener) => {
709
- listeners.add(listener);
710
- return () => listeners.delete(listener);
711
- };
712
- const api = { setState, getState, getInitialState, subscribe };
713
- const initialState = state = createState(setState, getState, api);
714
- return api;
715
- };
716
- const createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;
717
-
718
- const identity = (arg) => arg;
719
- function useStore(api, selector = identity) {
720
- const slice = React.useSyncExternalStore(
721
- api.subscribe,
722
- () => selector(api.getState()),
723
- () => selector(api.getInitialState())
724
- );
725
- React.useDebugValue(slice);
726
- return slice;
727
- }
728
-
729
779
  function safeClamp(value, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
730
780
  return clamp(value, min, max);
731
781
  }
@@ -736,6 +786,7 @@ const createNumberInputStore = (initProps) => {
736
786
  min: 0,
737
787
  max: 0,
738
788
  step: 1,
789
+ readonly: false,
739
790
  keepWithinRange: true,
740
791
  onChange: (_v, _t) => { },
741
792
  };
@@ -744,8 +795,10 @@ const createNumberInputStore = (initProps) => {
744
795
  valueAsNumber: parseValue(String((initProps === null || initProps === void 0 ? void 0 : initProps.value) || ''), initProps === null || initProps === void 0 ? void 0 : initProps.units, initProps === null || initProps === void 0 ? void 0 : initProps.digit).rawValue,
745
796
  }), { increment: () => set((state) => {
746
797
  var _a, _b;
747
- let next = parseValue(state.value, state.units, state.digit).rawValue +
748
- ((_a = state.step) !== null && _a !== void 0 ? _a : 1);
798
+ if (state.readonly)
799
+ return {};
800
+ const current = parseValue(state.value, state.units, state.digit).rawValue;
801
+ let next = current + ((_a = state.step) !== null && _a !== void 0 ? _a : 1);
749
802
  if (state.keepWithinRange) {
750
803
  next = safeClamp(next, state.min, state.max);
751
804
  }
@@ -757,8 +810,10 @@ const createNumberInputStore = (initProps) => {
757
810
  };
758
811
  }), decrement: () => set((state) => {
759
812
  var _a, _b;
760
- let next = parseValue(state.value, state.units, state.digit).rawValue -
761
- ((_a = state.step) !== null && _a !== void 0 ? _a : 1);
813
+ if (state.readonly)
814
+ return {};
815
+ const current = parseValue(state.value, state.units, state.digit).rawValue;
816
+ let next = current - ((_a = state.step) !== null && _a !== void 0 ? _a : 1);
762
817
  if (state.keepWithinRange) {
763
818
  next = safeClamp(next, state.min, state.max);
764
819
  }
@@ -769,6 +824,8 @@ const createNumberInputStore = (initProps) => {
769
824
  valueAsNumber: next,
770
825
  };
771
826
  }), change: (value) => set((state) => {
827
+ if (state.readonly)
828
+ return {};
772
829
  const v = value !== null && value !== void 0 ? value : '';
773
830
  return {
774
831
  value: v,
@@ -805,36 +862,17 @@ function useNumberInputContext(selector) {
805
862
  return useStore(store, selector);
806
863
  }
807
864
 
808
- function Stepper(_a) {
809
- var { dynamic = true, children, className } = _a, props = __rest(_a, ["dynamic", "children", "className"]);
810
- return (jsxRuntime.jsx("div", Object.assign({ className: clsx('tremolo-number-input-stepper', className), "data-dynamic": dynamic }, props, { children: children })));
811
- }
812
-
813
- function IncrementStepper(_a) {
814
- var { size = 12, children, className } = _a, props = __rest(_a, ["size", "children", "className"]);
815
- const max = useNumberInputContext((s) => s.max);
816
- const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
817
- const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
818
- const increment = useNumberInputContext((s) => s.increment);
819
- // TODO: add long press event
820
- return (jsxRuntime.jsx("div", Object.assign({ className: clsx('tremolo-number-input-increment-stepper', className), role: "button", tabIndex: -1, "aria-disabled": keepWithinRange && valueAsNumber >= (max !== null && max !== void 0 ? max : Number.MAX_SAFE_INTEGER), onPointerDown: increment }, props, { children: children || (jsxRuntime.jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsxRuntime.jsx("polyline", { points: "18 15 12 9 6 15" }) })) })));
821
- }
822
-
823
- function DecrementStepper(_a) {
824
- var { size = 12, children, className } = _a, props = __rest(_a, ["size", "children", "className"]);
825
- const min = useNumberInputContext((s) => s.min);
826
- const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
827
- const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
828
- const decrement = useNumberInputContext((s) => s.decrement);
829
- // TODO: add long press event
830
- return (jsxRuntime.jsx("div", Object.assign({ className: clsx('tremolo-number-input-decrement-stepper', className), role: "button", tabIndex: -1, "aria-disabled": keepWithinRange && valueAsNumber <= (min !== null && min !== void 0 ? min : Number.MIN_SAFE_INTEGER), onPointerDown: decrement }, props, { children: children || (jsxRuntime.jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsxRuntime.jsx("polyline", { points: "6 9 12 15 18 9" }) })) })));
831
- }
832
-
833
- function InternalInput(_a) {
834
- var { min, max, step, defaultValue, units, disabled, digit, selectWithFocus, blurOnEnter, keepWithinRange, clampValueOnBlur, wheel, keyboard, wrapperClassName, className, onChange, onFocus, onBlur, children: _children } = _a, props = __rest(_a, ["min", "max", "step", "defaultValue", "units", "disabled", "digit", "selectWithFocus", "blurOnEnter", "keepWithinRange", "clampValueOnBlur", "wheel", "keyboard", "wrapperClassName", "className", "onChange", "onFocus", "onBlur", "children"]);
865
+ const InternalInput = React.forwardRef((_a, ref) => {
866
+ var { disabled, readonly, selectWithFocus, blurOnEnter, clampValueOnBlur, wheel, keyboard, className, onChange, onFocus, onBlur, onKeyDown, children: _children } = _a, props = __rest(_a, ["disabled", "readonly", "selectWithFocus", "blurOnEnter", "clampValueOnBlur", "wheel", "keyboard", "className", "onChange", "onFocus", "onBlur", "onKeyDown", "children"]);
867
+ const elmRef = React.useRef(null);
835
868
  const [editing, setEditing] = React.useState(false);
836
869
  const value = useNumberInputContext((s) => s.value);
837
870
  const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
871
+ const min = useNumberInputContext((s) => s.min);
872
+ const max = useNumberInputContext((s) => s.max);
873
+ const step = useNumberInputContext((s) => s.step);
874
+ const units = useNumberInputContext((s) => s.units);
875
+ const digit = useNumberInputContext((s) => s.digit);
838
876
  const change = useNumberInputContext((s) => s.change);
839
877
  const error = React.useMemo(() => {
840
878
  if (editing)
@@ -842,11 +880,71 @@ function InternalInput(_a) {
842
880
  const v = valueAsNumber;
843
881
  return (min !== null && min !== void 0 ? min : -Infinity) > v || v > (max !== null && max !== void 0 ? max : Infinity);
844
882
  }, [editing, max, min, valueAsNumber]);
845
- return (jsxRuntime.jsx("input", Object.assign({ className: clsx('tremolo-number-input', className), value: value, min: min, max: max, step: step, type: "text", disabled: disabled, tabIndex: -1, "data-error": error, onChange: (event) => {
883
+ const updateValueByEvent = React.useCallback((eventType, x) => {
884
+ let newValue;
885
+ if (eventType == 'normalized') {
886
+ if (!min || !max) {
887
+ throw new Error('required parameter: min, max'); // TODO
888
+ }
889
+ const n = normalizeValue(valueAsNumber, min, max);
890
+ newValue = rawValue(n + x, min, max);
891
+ return clamp(stepValue(newValue, step), min, max);
892
+ }
893
+ else {
894
+ newValue = valueAsNumber + x;
895
+ return stepValue(newValue, step);
896
+ }
897
+ }, [max, min, step, valueAsNumber]);
898
+ const handleKeyDown = React.useCallback((event) => {
899
+ if (!keyboard || !onChange || readonly)
900
+ return;
901
+ const key = event.key;
902
+ if (!['ArrowUp', 'ArrowDown'].includes(key))
903
+ return;
904
+ event.preventDefault();
905
+ const x = key == 'ArrowUp' ? keyboard[1] : -keyboard[1];
906
+ const parsed = parseValue(String(updateValueByEvent(keyboard[0], x)));
907
+ console.log('key control');
908
+ onChange === null || onChange === void 0 ? void 0 : onChange(parsed.rawValue, parsed.formatValue);
909
+ }, [keyboard, readonly, onChange, updateValueByEvent]);
910
+ const wheelRefCallback = useRefCallbackEvent('wheel', (event) => {
911
+ if (!wheel || readonly)
912
+ return;
913
+ event.preventDefault();
914
+ if (!onChange || event.deltaY == 0)
915
+ return;
916
+ const x = Math.sign(event.deltaY) * -wheel[1];
917
+ const parsed = parseValue(String(updateValueByEvent(wheel[0], x)));
918
+ console.log('wheel control');
919
+ onChange === null || onChange === void 0 ? void 0 : onChange(parsed.rawValue, parsed.formatValue);
920
+ }, { passive: false }, [wheel, readonly, onChange, updateValueByEvent]);
921
+ React.useImperativeHandle(ref, () => {
922
+ return {
923
+ focus() {
924
+ var _a;
925
+ (_a = elmRef.current) === null || _a === void 0 ? void 0 : _a.focus();
926
+ },
927
+ blur() {
928
+ var _a;
929
+ (_a = elmRef.current) === null || _a === void 0 ? void 0 : _a.blur();
930
+ },
931
+ };
932
+ }, []);
933
+ return (jsxRuntime.jsx("input", Object.assign({ ref: (input) => {
934
+ elmRef.current = input;
935
+ wheelRefCallback(input);
936
+ }, className: clsx('tremolo-number-input', className), value: value, min: min, max: max, step: step, type: "text", "aria-disabled": disabled, "aria-readonly": readonly, tabIndex: -1, "data-error": error, onChange: (event) => {
937
+ if (readonly)
938
+ return;
846
939
  const v = event.currentTarget.value;
940
+ const cursorPos = event.target.selectionStart;
847
941
  change(v);
848
942
  const valueAsNumber = parseValue(v, units, digit).rawValue;
849
943
  onChange === null || onChange === void 0 ? void 0 : onChange(valueAsNumber, v);
944
+ if (cursorPos) {
945
+ event.target.selectionStart = cursorPos;
946
+ event.target.selectionEnd = cursorPos;
947
+ }
850
948
  }, onFocus: (event) => {
851
949
  setEditing(true);
852
950
  const v = event.currentTarget.value;
@@ -875,53 +973,190 @@ function InternalInput(_a) {
875
973
  if (blurOnEnter && event.key == 'Enter') {
876
974
  event.currentTarget.blur();
877
975
  }
976
+ handleKeyDown(event);
977
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
878
978
  } }, props)));
979
+ });
980
+
981
+ /** @category NumberInput */
982
+ function Stepper(_a) {
983
+ var { dynamic = true, children, className } = _a, props = __rest(_a, ["dynamic", "children", "className"]);
984
+ return (jsxRuntime.jsx("div", Object.assign({ className: clsx('tremolo-number-input-stepper', className), "data-dynamic": dynamic }, props, { children: children })));
985
+ }
986
+
987
+ function useInterval(callback, delay) {
988
+ const savedCallback = React.useRef(callback);
989
+ // Remember the latest callback if it changes.
990
+ React.useEffect(() => {
991
+ savedCallback.current = callback;
992
+ }, [callback]);
993
+ // Set up the interval.
994
+ React.useEffect(() => {
995
+ // Don't schedule if no delay is specified.
996
+ // Note: 0 is a valid value for delay.
997
+ if (delay === null) {
998
+ return;
999
+ }
1000
+ const id = setInterval(() => {
1001
+ savedCallback.current();
1002
+ }, delay);
1003
+ return () => {
1004
+ clearInterval(id);
1005
+ };
1006
+ }, [delay]);
879
1007
  }
1008
+
1009
+ function useLongPress(callback, initialDelay = 500, interval = 40) {
1010
+ const [pressed, setPressed] = React.useState(false);
1011
+ const [delay, setDelay] = React.useState(initialDelay);
1012
+ useInterval(() => {
1013
+ callback();
1014
+ setDelay(interval);
1015
+ }, pressed ? delay : null);
1016
+ useEventListener(globalThis.window, 'pointerup', () => {
1017
+ setPressed(false);
1018
+ setDelay(initialDelay);
1019
+ });
1020
+ return React.useCallback(() => {
1021
+ callback();
1022
+ setPressed(true);
1023
+ }, [callback]);
1024
+ }
1025
+
1026
+ /** @category NumberInput */
1027
+ function IncrementStepper(_a) {
1028
+ var { size = 12, children, className } = _a, props = __rest(_a, ["size", "children", "className"]);
1029
+ const max = useNumberInputContext((s) => s.max);
1030
+ const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
1031
+ const readonly = useNumberInputContext((s) => s.readonly);
1032
+ const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
1033
+ const increment = useNumberInputContext((s) => s.increment);
1034
+ const press = useLongPress(increment);
1035
+ return (
1036
+ // eslint-disable-next-line jsx-a11y/role-supports-aria-props
1037
+ jsxRuntime.jsx("div", Object.assign({ className: clsx('tremolo-number-input-increment-stepper', className), role: "button", tabIndex: -1, "aria-disabled": keepWithinRange && valueAsNumber >= (max !== null && max !== void 0 ? max : Number.MAX_SAFE_INTEGER), "aria-readonly": readonly, onPointerDown: press }, props, { children: children || (jsxRuntime.jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsxRuntime.jsx("polyline", { points: "18 15 12 9 6 15" }) })) })));
1038
+ }
1039
+
1040
+ /** @category NumberInput */
1041
+ function DecrementStepper(_a) {
1042
+ var { size = 12, children, className } = _a, props = __rest(_a, ["size", "children", "className"]);
1043
+ const min = useNumberInputContext((s) => s.min);
1044
+ const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
1045
+ const readonly = useNumberInputContext((s) => s.readonly);
1046
+ const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
1047
+ const decrement = useNumberInputContext((s) => s.decrement);
1048
+ const press = useLongPress(decrement);
1049
+ return (
1050
+ // eslint-disable-next-line jsx-a11y/role-supports-aria-props
1051
+ jsxRuntime.jsx("div", Object.assign({ className: clsx('tremolo-number-input-decrement-stepper', className), role: "button", tabIndex: -1, "aria-disabled": keepWithinRange && valueAsNumber <= (min !== null && min !== void 0 ? min : Number.MIN_SAFE_INTEGER), "aria-readonly": readonly, onPointerDown: press }, props, { children: children || (jsxRuntime.jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsxRuntime.jsx("polyline", { points: "6 9 12 15 18 9" }) })) })));
1052
+ }
1053
+
880
1054
  /**
881
1055
  * Input with some useful functions for entering numerical values.
882
1056
  * @category NumberInput
883
1057
  */
884
- function NumberInput(_a) {
885
- var { value, min, max, step = 1, defaultValue, units, disabled = false, digit, selectWithFocus = 'none', blurOnEnter = true, keepWithinRange = true, clampValueOnBlur = true, wheel = ['raw', 1], keyboard = ['raw', 1], wrapperClassName, className, onChange, onFocus, onBlur, children } = _a, props = __rest(_a, ["value", "min", "max", "step", "defaultValue", "units", "disabled", "digit", "selectWithFocus", "blurOnEnter", "keepWithinRange", "clampValueOnBlur", "wheel", "keyboard", "wrapperClassName", "className", "onChange", "onFocus", "onBlur", "children"]);
886
- return (jsxRuntime.jsx(NumberInputProvider, { value: String(value), min: min, max: max, step: step, units: units, digit: digit, keepWithinRange: keepWithinRange, onChange: onChange, children: jsxRuntime.jsxs("div", { className: clsx('tremolo-number-input-wrapper', wrapperClassName),
1058
+ const NumberInput = React.forwardRef((_a, ref) => {
1059
+ var { value, min, max, step = 1, units, readonly = false, disabled = false, digit, variant = 'outline', selectWithFocus = 'none', blurOnEnter = true, keepWithinRange = true, clampValueOnBlur = true, wheel = ['raw', 1], keyboard = ['raw', 1], activeColor, wrapperClassName, className, onChange, onFocus, onBlur, children } = _a, props = __rest(_a, ["value", "min", "max", "step", "units", "readonly", "disabled", "digit", "variant", "selectWithFocus", "blurOnEnter", "keepWithinRange", "clampValueOnBlur", "wheel", "keyboard", "activeColor", "wrapperClassName", "className", "onChange", "onFocus", "onBlur", "children"]);
1060
+ const colors = {
1061
+ '--active-color': activeColor,
1062
+ };
1063
+ return (jsxRuntime.jsx(NumberInputProvider, { value: String(value), min: min, max: max, step: step, units: units, digit: digit, readonly: readonly, keepWithinRange: keepWithinRange, onChange: onChange, children: jsxRuntime.jsxs("div", { className: clsx('tremolo-number-input-wrapper', wrapperClassName),
887
1064
  // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
888
- tabIndex: 0, "data-stepper": !!children, children: [jsxRuntime.jsx(InternalInput, Object.assign({ min: min, max: max, step: step, defaultValue: defaultValue, units: units, disabled: disabled, digit: digit, selectWithFocus: selectWithFocus, blurOnEnter: blurOnEnter, keepWithinRange: keepWithinRange, clampValueOnBlur: clampValueOnBlur, wheel: wheel, keyboard: keyboard, wrapperClassName: wrapperClassName, className: className, onChange: onChange, onFocus: onFocus, onBlur: onBlur }, props)), children] }) }));
1065
+ tabIndex: 0, style: Object.assign({}, colors), "data-stepper": !!children, "data-variant": variant, children: [jsxRuntime.jsx(InternalInput, Object.assign({ ref: ref, readonly: readonly, disabled: disabled, selectWithFocus: selectWithFocus, blurOnEnter: blurOnEnter, clampValueOnBlur: clampValueOnBlur, wheel: wheel, keyboard: keyboard, className: className, onChange: onChange, onFocus: onFocus, onBlur: onBlur }, props)), children] }) }));
1066
+ });
1067
+
1068
+ /**
1069
+ * @returns [refCallback, pointerDownHandler]
1070
+ */
1071
+ function useDragWithElement({ baseElementRef, onDrag, onDragStart, onDragEnd, }) {
1072
+ const dragged = React.useRef(false);
1073
+ const normalizedX = React.useRef(0);
1074
+ const normalizedY = React.useRef(0);
1075
+ const handleDrag = React.useCallback((event) => {
1076
+ if (!baseElementRef.current || !dragged.current) {
1077
+ return;
1078
+ }
1079
+ const isTouch = event instanceof TouchEvent;
1080
+ if (isTouch && event.cancelable)
1081
+ event.preventDefault();
1082
+ const { left, top, right, bottom } = baseElementRef.current.getBoundingClientRect();
1083
+ const mouseX = isTouch ? event.touches[0].clientX : event.clientX;
1084
+ const mouseY = isTouch ? event.touches[0].clientY : event.clientY;
1085
+ const nx = normalizeValue(mouseX, left, right);
1086
+ const ny = normalizeValue(mouseY, top, bottom);
1087
+ normalizedX.current = nx;
1088
+ normalizedY.current = ny;
1089
+ onDrag(nx, ny);
1090
+ }, [onDrag, baseElementRef]);
1091
+ const refHandler = useRefCallbackEvent('touchmove', handleDrag, { passive: false }, [onDrag]);
1092
+ const pointerDownHandler = React.useCallback((event) => {
1093
+ dragged.current = true;
1094
+ handleDrag(event);
1095
+ onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart(normalizedX.current, normalizedY.current);
1096
+ }, [handleDrag, onDragStart]);
1097
+ useEventListener(globalThis.window, 'pointermove', handleDrag);
1098
+ useEventListener(globalThis.window, 'pointerup', () => {
1099
+ if (!dragged.current)
1100
+ return;
1101
+ dragged.current = false;
1102
+ onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(normalizedX.current, normalizedY.current);
1103
+ });
1104
+ return [refHandler, pointerDownHandler];
889
1105
  }
890
1106
 
891
1107
  /** @category Piano */
892
1108
  function KeyLabel(_a) {
893
- var { label, className, wrapperClassName, wrapperStyle, __note, __index, __label } = _a, props = __rest(_a, ["label", "className", "wrapperClassName", "wrapperStyle", "__note", "__index", "__label"]);
1109
+ var { label, className, wrapperClassName, wrapperStyle, __note, __label } = _a, props = __rest(_a, ["label", "className", "wrapperClassName", "wrapperStyle", "__note", "__label"]);
1110
+ const noteRange = usePianoContext((s) => s.noteRange);
1111
+ const noteRangeArray = getNoteRangeArray(noteRange);
1112
+ const index = noteRangeArray.indexOf(__note);
894
1113
  const content = label
895
- ? label(__note, __index)
896
- : __label && __label(__note, __index);
1114
+ ? label(__note, index)
1115
+ : __label && __label(__note, index);
897
1116
  return ((content != undefined || content != null) && (jsxRuntime.jsx("div", { className: clsx('tremolo-piano-key-label-wrapper', wrapperClassName), style: wrapperStyle, children: jsxRuntime.jsx("div", Object.assign({ className: clsx('tremolo-piano-key-label', className) }, props, { children: content })) })));
898
1117
  }
899
1118
 
900
1119
  /** @category Piano */
901
1120
  const defaultWhiteKeyWidth = 40;
902
1121
  /** @category Piano */
903
- const WhiteKey = React.forwardRef((_a, ref) => {
904
- var { width = defaultWhiteKeyWidth, height = '100%', className, style, children, __glissando, __position, __note, __disabled, __index, __fill, __width, __onPlayNote, __onStopNote, __label } = _a, props = __rest(_a, ["width", "height", "className", "style", "children", "__glissando", "__position", "__note", "__disabled", "__index", "__fill", "__width", "__onPlayNote", "__onStopNote", "__label"]);
1122
+ const defaultBlackKeyWidth = 26;
1123
+ const KeyImpl = React.forwardRef((_a, ref) => {
1124
+ var { keyType, noteNumber, width, height, bg, color, activeBg, activeColor, className, style, children, __width } = _a, props = __rest(_a, ["keyType", "noteNumber", "width", "height", "bg", "color", "activeBg", "activeColor", "className", "style", "children", "__width"]);
905
1125
  const [played, setPlayed] = React.useState(false);
1126
+ const midiMax = usePianoContext((s) => s.midiMax);
1127
+ const fill = usePianoContext((s) => s.fill);
1128
+ // const glissando = usePianoContext(s => s.glissando)
1129
+ const onPlayNote = usePianoContext((s) => s.onPlayNote);
1130
+ const onStopNote = usePianoContext((s) => s.onStopNote);
1131
+ const label = usePianoContext((s) => s.label);
1132
+ const notePosition = usePianoContext((s) => s.notePosition);
1133
+ const position = notePosition(noteNumber);
1134
+ const disabled = noteNumber > midiMax;
1135
+ const colors = {
1136
+ '--bg': bg,
1137
+ '--color': color,
1138
+ '--active-bg': activeBg,
1139
+ '--active-color': activeColor,
1140
+ };
906
1141
  React.useImperativeHandle(ref, () => {
907
1142
  return {
908
1143
  play() {
909
- if (__disabled)
1144
+ if (disabled)
910
1145
  return;
911
1146
  setPlayed(true);
912
- if (__onPlayNote)
913
- __onPlayNote(__note);
1147
+ if (onPlayNote)
1148
+ onPlayNote(noteNumber);
914
1149
  },
915
1150
  stop() {
916
1151
  setPlayed(false);
917
- if (__onStopNote)
918
- __onStopNote(__note);
1152
+ if (onStopNote)
1153
+ onStopNote(noteNumber);
919
1154
  },
920
1155
  played() {
921
1156
  return played;
922
1157
  },
923
1158
  };
924
- }, [__disabled, __note, __onPlayNote, __onStopNote, played]);
1159
+ }, [disabled, noteNumber, onPlayNote, onStopNote, played]);
925
1160
  let keyLabelProps = {};
926
1161
  if (children != undefined) {
927
1162
  React.Children.forEach(children, (child) => {
@@ -938,109 +1173,85 @@ const WhiteKey = React.forwardRef((_a, ref) => {
938
1173
  }
939
1174
  });
940
1175
  }
941
- return (jsxRuntime.jsx("div", Object.assign({ className: clsx(`tremolo-piano-white-key`, className), "aria-disabled": __disabled, "data-active": played, style: Object.assign({ left: __position, width: __fill ? __width : (width !== null && width !== void 0 ? width : __width), height: height }, style), onPointerDown: () => {
942
- if (__disabled)
943
- return;
944
- setPlayed(true);
945
- if (__onPlayNote)
946
- __onPlayNote(__note);
947
- }, onPointerEnter: () => {
948
- if (__disabled)
949
- return;
950
- if (__glissando) {
951
- setPlayed(true);
952
- if (__onPlayNote)
953
- __onPlayNote(__note);
954
- }
955
- }, onPointerUp: () => {
956
- if (__disabled)
957
- return;
958
- setPlayed(false);
959
- if (__onStopNote)
960
- __onStopNote(__note);
961
- }, onPointerLeave: () => {
962
- if (__disabled)
963
- return;
964
- if (__glissando) {
965
- setPlayed(false);
966
- if (__onStopNote)
967
- __onStopNote(__note);
968
- }
969
- } }, props, { children: jsxRuntime.jsx(KeyLabel, Object.assign({ __note: __note, __label: __label, __index: __index }, keyLabelProps)) })));
1176
+ return (jsxRuntime.jsx("div", Object.assign({ className: clsx(`tremolo-piano-${keyType}-key`, className), "aria-disabled": disabled, "data-active": played, style: Object.assign(Object.assign(Object.assign({}, colors), { left: position, width: fill ? __width : width, height: height }), style) }, props, { children: jsxRuntime.jsx(KeyLabel, Object.assign({ label: label, __note: noteNumber }, keyLabelProps)) })));
1177
+ });
1178
+ /** @category Piano */
1179
+ const WhiteKey = React.forwardRef((_a, ref) => {
1180
+ var { width = defaultWhiteKeyWidth, height = '100%', onContextMenu = (e) => e.preventDefault() } = _a, props = __rest(_a, ["width", "height", "onContextMenu"]);
1181
+ return (jsxRuntime.jsx(KeyImpl, Object.assign({ ref: ref, keyType: "white", width: width, height: height, onContextMenu: onContextMenu }, props)));
970
1182
  });
971
-
972
1183
  /** @category Piano */
973
1184
  const BlackKey = React.forwardRef((_a, ref) => {
974
- var { width = defaultWhiteKeyWidth * 0.65, height = '60%', className, style, children, __glissando, __position, __note, __disabled, __index, __fill, __width, __onPlayNote, __onStopNote, __label } = _a, props = __rest(_a, ["width", "height", "className", "style", "children", "__glissando", "__position", "__note", "__disabled", "__index", "__fill", "__width", "__onPlayNote", "__onStopNote", "__label"]);
975
- const [played, setPlayed] = React.useState(false);
976
- React.useImperativeHandle(ref, () => {
977
- return {
978
- play() {
979
- if (__disabled)
980
- return;
981
- setPlayed(true);
982
- if (__onPlayNote)
983
- __onPlayNote(__note);
984
- },
985
- stop() {
986
- setPlayed(false);
987
- if (__onStopNote)
988
- __onStopNote(__note);
989
- },
990
- played() {
991
- return played;
992
- },
993
- };
994
- }, [__disabled, __note, __onPlayNote, __onStopNote, played]);
995
- let keyLabelProps = {};
996
- if (children != undefined) {
997
- React.Children.forEach(children, (child) => {
998
- if (React.isValidElement(child)) {
999
- if (child.type == KeyLabel) {
1000
- keyLabelProps = child.props;
1001
- }
1002
- else {
1003
- throw new Error('only <KeyLabel>');
1004
- }
1005
- }
1006
- else {
1007
- throw new Error('children is an invalid element.');
1008
- }
1009
- });
1010
- }
1011
- return (jsxRuntime.jsx("div", Object.assign({ className: clsx(`tremolo-piano-black-key`, className), "aria-disabled": __disabled, "data-active": played, style: Object.assign({ left: __position, width: __fill ? __width : (width !== null && width !== void 0 ? width : __width), height: height }, style), onPointerDown: () => {
1012
- if (__disabled)
1013
- return;
1014
- setPlayed(true);
1015
- if (__onPlayNote)
1016
- __onPlayNote(__note);
1017
- }, onPointerEnter: () => {
1018
- if (__disabled)
1019
- return;
1020
- if (__glissando) {
1021
- setPlayed(true);
1022
- if (__onPlayNote)
1023
- __onPlayNote(__note);
1024
- }
1025
- }, onPointerUp: () => {
1026
- if (__disabled)
1027
- return;
1028
- setPlayed(false);
1029
- if (__onStopNote)
1030
- __onStopNote(__note);
1031
- }, onPointerLeave: () => {
1032
- if (__disabled)
1033
- return;
1034
- if (__glissando) {
1035
- setPlayed(false);
1036
- if (__onStopNote)
1037
- __onStopNote(__note);
1038
- }
1039
- } }, props, { children: jsxRuntime.jsx(KeyLabel, Object.assign({ __note: __note, __label: __label, __index: __index }, keyLabelProps)) })));
1185
+ var { width = defaultBlackKeyWidth, height = '60%', onContextMenu = (e) => e.preventDefault() } = _a, props = __rest(_a, ["width", "height", "onContextMenu"]);
1186
+ return (jsxRuntime.jsx(KeyImpl, Object.assign({ ref: ref, keyType: "black", width: width, height: height, onContextMenu: onContextMenu }, props)));
1040
1187
  });
1041
1188
 
1189
+ const createPianoStore = (initProps) => {
1190
+ const noteRange = (initProps === null || initProps === void 0 ? void 0 : initProps.noteRange) || { first: 0};
1191
+ const DEFAULT_PROPS = {
1192
+ noteRange: { first: 0, last: 127 },
1193
+ glissando: true,
1194
+ midiMax: 127,
1195
+ fill: false,
1196
+ label: () => undefined,
1197
+ onPlayNote: () => { },
1198
+ onStopNote: () => { },
1199
+ };
1200
+ return createStore()(() => (Object.assign(Object.assign(Object.assign({}, DEFAULT_PROPS), initProps), { notePosition: (note) => {
1201
+ const pitchPositions = {
1202
+ C: 0,
1203
+ 'C#': 1,
1204
+ D: 1,
1205
+ 'D#': 2,
1206
+ E: 2,
1207
+ F: 3,
1208
+ 'F#': 4,
1209
+ G: 4,
1210
+ 'G#': 5,
1211
+ A: 5,
1212
+ 'A#': 6,
1213
+ B: 6,
1214
+ };
1215
+ const whiteNoteWidth = defaultWhiteKeyWidth;
1216
+ const blackNoteWidth = defaultBlackKeyWidth;
1217
+ const padding = 1;
1218
+ const targetNoteKey = noteKey(note);
1219
+ const firstNoteKey = noteKey(noteRange.first);
1220
+ const octave = Math.floor((note - noteRange.first) / 12);
1221
+ const octaveOffset = noteKeys.indexOf(firstNoteKey) > noteKeys.indexOf(targetNoteKey) ? 1 : 0;
1222
+ const w = whiteNoteWidth + padding;
1223
+ const pos = pitchPositions[targetNoteKey] - pitchPositions[firstNoteKey];
1224
+ const blackKeyOffset = isBlackKey(note) ? blackNoteWidth / 2 : 0;
1225
+ return pos * w + (octave + octaveOffset) * 7 * w - blackKeyOffset;
1226
+ } })));
1227
+ };
1228
+ const PianoContext = React.createContext(null);
1229
+ function PianoProvider(_a) {
1230
+ var { children } = _a, props = __rest(_a, ["children"]);
1231
+ const storeRef = React.useRef(null);
1232
+ if (!storeRef.current) {
1233
+ storeRef.current = createPianoStore(props);
1234
+ }
1235
+ React.useEffect(() => {
1236
+ if (storeRef.current) {
1237
+ storeRef.current.setState(props);
1238
+ }
1239
+ else {
1240
+ storeRef.current = createPianoStore(props);
1241
+ }
1242
+ }, [props]);
1243
+ return (jsxRuntime.jsx(PianoContext.Provider, { value: storeRef.current, children: children }));
1244
+ }
1245
+ /** @category Piano */
1246
+ function usePianoContext(selector) {
1247
+ const store = React.useContext(PianoContext);
1248
+ if (!store)
1249
+ throw new Error('Missing PianoContext.Provider in the tree');
1250
+ return useStore(store, selector);
1251
+ }
1252
+
1042
1253
  /** @category Piano */
1043
- const Shortcuts = {
1254
+ const SHORTCUTS = {
1044
1255
  HOME_ROW: {
1045
1256
  keys: [
1046
1257
  'a',
@@ -1064,67 +1275,121 @@ const Shortcuts = {
1064
1275
  },
1065
1276
  };
1066
1277
 
1278
+ /**
1279
+ * Piano component
1280
+ *
1281
+ * TODO:
1282
+ * - add scale highlight
1283
+ */
1067
1284
  /** @category Piano */
1285
+ function getNoteRangeArray(noteRange) {
1286
+ return Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
1287
+ }
1288
+ /**
1289
+ * Customizable piano component.
1290
+ * @category Piano
1291
+ */
1068
1292
  function Piano(_a) {
1069
- var { noteRange, glissando = true, midiMax = 127, keyboardShortcuts, fill = false, height = fill ? '100%' : 160, style, className, onPlayNote, onStopNote, label, children } = _a, props = __rest(_a, ["noteRange", "glissando", "midiMax", "keyboardShortcuts", "fill", "height", "style", "className", "onPlayNote", "onStopNote", "label", "children"]);
1293
+ var { noteRange, glissando = true, midiMax = 127, keyboardShortcuts, fill = false, height = fill ? '100%' : 160, whiteNoteWidth: _whiteNoteWidth = defaultWhiteKeyWidth, style, className, onPlayNote, onStopNote, label, children, onPointerDown } = _a, props = __rest(_a, ["noteRange", "glissando", "midiMax", "keyboardShortcuts", "fill", "height", "whiteNoteWidth", "style", "className", "onPlayNote", "onStopNote", "label", "children", "onPointerDown"]);
1070
1294
  // -- state and ref ---
1071
- const [pressed, setPressed] = React.useState(false);
1072
- const [whiteNoteWidth, setWhiteNoteWidth] = React.useState(-1);
1295
+ const [whiteNoteWidth, setWhiteNoteWidth] = React.useState(_whiteNoteWidth);
1073
1296
  const keyRefs = React.useRef([]);
1074
1297
  for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) {
1075
1298
  keyRefs.current[i] = React.createRef();
1076
1299
  }
1077
1300
  const pianoRef = React.useRef(null);
1301
+ const hitKeyIndex = React.useRef(-1);
1078
1302
  // --- interpret props ---
1079
- const noteRangeArray = Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
1080
- const whiteNoteCount = noteRangeArray.filter((v) => isWhiteKey(v)).length;
1081
- let whiteKeyProps = {}, blackKeyProps = {};
1082
- if (children != undefined) {
1083
- React.Children.forEach(children, (child) => {
1084
- if (React.isValidElement(child)) {
1085
- if (child.type == WhiteKey) {
1086
- whiteKeyProps = child.props;
1087
- }
1088
- else if (child.type == BlackKey) {
1089
- blackKeyProps = child.props;
1090
- }
1091
- else {
1092
- throw new Error('only <WhiteKey> or <BlackKey>');
1303
+ const noteRangeArray = getNoteRangeArray(noteRange);
1304
+ const whiteNotes = noteRangeArray.filter((v) => isWhiteKey(v));
1305
+ const blackNotes = noteRangeArray.filter((v) => isBlackKey(v));
1306
+ const whiteNoteCount = whiteNotes.length;
1307
+ const padding = 1;
1308
+ const staticWidth = (whiteNoteWidth + padding) * whiteNoteCount;
1309
+ // const blackNoteShiftPercent = 0.3
1310
+ // TODO: if fill setWhiteNoteWidth
1311
+ // useMemo
1312
+ // __width
1313
+ const childrenWithProps = React.useMemo(() => {
1314
+ return (children &&
1315
+ React.Children.map(children, (child, index) => {
1316
+ if (React.isValidElement(child)) {
1317
+ const props = { ref: keyRefs.current[index] };
1318
+ return React.cloneElement(child, props);
1093
1319
  }
1094
- }
1095
- else {
1096
- throw new Error('children is an invalid element.');
1097
- }
1098
- });
1099
- }
1320
+ return child;
1321
+ }));
1322
+ }, [children]);
1100
1323
  // --- internal functions ---
1101
- function notePosition(note) {
1324
+ const notePosition = React.useCallback((note) => {
1102
1325
  const pitchPositions = {
1103
1326
  C: 0,
1104
- 'C#': 0.55,
1327
+ 'C#': 1,
1105
1328
  D: 1,
1106
- 'D#': 1.8,
1329
+ 'D#': 2,
1107
1330
  E: 2,
1108
1331
  F: 3,
1109
- 'F#': 3.5,
1332
+ 'F#': 4,
1110
1333
  G: 4,
1111
- 'G#': 4.7,
1334
+ 'G#': 5,
1112
1335
  A: 5,
1113
- 'A#': 5.85,
1336
+ 'A#': 6,
1114
1337
  B: 6,
1115
1338
  };
1116
- const toNoteName = (n) => {
1117
- const { letter, accidental } = parseNoteName(noteName(n));
1118
- return (letter + accidental);
1119
- };
1120
- const _noteName = toNoteName(note);
1121
- const firstNoteName = toNoteName(noteRange.first);
1122
- const pos = pitchPositions[_noteName] - pitchPositions[firstNoteName];
1339
+ const whiteNoteWidth = defaultWhiteKeyWidth;
1340
+ const blackNoteWidth = defaultBlackKeyWidth;
1341
+ const padding = 1;
1342
+ const targetNoteKey = noteKey(note);
1343
+ const firstNoteKey = noteKey(noteRange.first);
1123
1344
  const octave = Math.floor((note - noteRange.first) / 12);
1124
- const octaveOffset = noteNames.indexOf(firstNoteName) > noteNames.indexOf(_noteName) ? 1 : 0;
1125
- const length = whiteNoteWidth + 3;
1126
- return pos * length + (octave + octaveOffset) * 7 * length;
1127
- }
1345
+ const octaveOffset = noteKeys.indexOf(firstNoteKey) > noteKeys.indexOf(targetNoteKey) ? 1 : 0;
1346
+ const w = whiteNoteWidth + padding;
1347
+ const pos = pitchPositions[targetNoteKey] - pitchPositions[firstNoteKey];
1348
+ const blackKeyOffset = isBlackKey(note) ? blackNoteWidth / 2 : 0;
1349
+ return pos * w + (octave + octaveOffset) * 7 * w - blackKeyOffset;
1350
+ }, [noteRange.first]);
1351
+ const getHitKeyIndex = React.useCallback((x, y) => {
1352
+ if (!pianoRef.current)
1353
+ return -1;
1354
+ const containerHeight = pianoRef.current.clientHeight;
1355
+ const notes = [...blackNotes, ...whiteNotes];
1356
+ for (let i = 0; i < notes.length; i++) {
1357
+ const note = notes[i];
1358
+ const pos = notePosition(note);
1359
+ const w = isWhiteKey(note) ? defaultWhiteKeyWidth : defaultBlackKeyWidth;
1360
+ const h = isWhiteKey(note) ? containerHeight : containerHeight * 0.6;
1361
+ if (pos <= x && x < pos + w && 0 <= y && y < h) {
1362
+ return note;
1363
+ }
1364
+ }
1365
+ return -1;
1366
+ }, [blackNotes, notePosition, whiteNotes]);
1367
+ // TODO: 単一のポインターに対しては、useDragで対応可能だが、
1368
+ // マルチタッチに対しては、TouchEventを使う必要がありそう
1369
+ // 取り敢えず、シングルタッチだけ対応
1370
+ const onDrag = React.useCallback((perX, perY) => {
1371
+ var _a, _b, _c, _d;
1372
+ if (!pianoRef.current)
1373
+ return;
1374
+ const x = perX * staticWidth;
1375
+ const y = perY * pianoRef.current.clientHeight;
1376
+ const note = getHitKeyIndex(x, y);
1377
+ const index = noteRangeArray.indexOf(note);
1378
+ if (index == -1)
1379
+ return;
1380
+ if (hitKeyIndex.current != index) {
1381
+ (_b = (_a = keyRefs.current[hitKeyIndex.current]) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.stop();
1382
+ (_d = (_c = keyRefs.current[index]) === null || _c === void 0 ? void 0 : _c.current) === null || _d === void 0 ? void 0 : _d.play();
1383
+ hitKeyIndex.current = index;
1384
+ }
1385
+ }, [getHitKeyIndex, noteRangeArray, staticWidth]);
1386
+ const onDragEnd = React.useCallback(() => {
1387
+ var _a, _b, _c, _d;
1388
+ if ((_b = (_a = keyRefs.current[hitKeyIndex.current]) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.played()) {
1389
+ (_d = (_c = keyRefs.current[hitKeyIndex.current]) === null || _c === void 0 ? void 0 : _c.current) === null || _d === void 0 ? void 0 : _d.stop();
1390
+ }
1391
+ hitKeyIndex.current = -1;
1392
+ }, [keyRefs]);
1128
1393
  // --- hooks ---
1129
1394
  React.useEffect(() => {
1130
1395
  if (fill && pianoRef.current) {
@@ -1133,14 +1398,22 @@ function Piano(_a) {
1133
1398
  throw new Error("doesn't have a parent element.");
1134
1399
  const resizeObserver = new ResizeObserver(() => {
1135
1400
  const w = pianoRef.current.clientWidth;
1136
- setWhiteNoteWidth(w / whiteNoteCount - 3);
1401
+ setWhiteNoteWidth(w / whiteNoteCount - padding);
1137
1402
  });
1138
1403
  resizeObserver.observe(parent);
1404
+ return () => {
1405
+ resizeObserver.unobserve(parent);
1406
+ };
1139
1407
  }
1140
1408
  else {
1141
- setWhiteNoteWidth((whiteKeyProps === null || whiteKeyProps === void 0 ? void 0 : whiteKeyProps.width) || defaultWhiteKeyWidth);
1409
+ setWhiteNoteWidth(_whiteNoteWidth);
1142
1410
  }
1143
- }, [fill, whiteKeyProps === null || whiteKeyProps === void 0 ? void 0 : whiteKeyProps.width, whiteNoteCount]);
1411
+ }, [fill, _whiteNoteWidth, whiteNoteCount]);
1412
+ const [touchMoveRefCallback, pointerDownHandler] = useDragWithElement({
1413
+ baseElementRef: pianoRef,
1414
+ onDrag: onDrag,
1415
+ onDragEnd: onDragEnd,
1416
+ });
1144
1417
  useEventListener(globalThis.window, 'keydown', (e) => {
1145
1418
  var _a, _b, _c, _d;
1146
1419
  if (e.repeat)
@@ -1165,17 +1438,14 @@ function Piano(_a) {
1165
1438
  (_b = (_a = keyRefs.current[index]) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.stop();
1166
1439
  }
1167
1440
  });
1168
- return (jsxRuntime.jsx("div", Object.assign({ ref: pianoRef, className: clsx('tremolo-piano', className), style: Object.assign({ width: fill ? '100%' : (whiteNoteWidth + 3) * whiteNoteCount, height: height }, style), onPointerDown: (e) => {
1169
- if (props.onPointerDown)
1170
- props.onPointerDown(e);
1171
- setPressed(true);
1172
- }, onPointerUp: (e) => {
1173
- if (props.onPointerUp)
1174
- props.onPointerUp(e);
1175
- setPressed(false);
1176
- } }, props, { children: noteRangeArray.map((note, index) => {
1177
- return isWhiteKey(note) ? (jsxRuntime.jsx(WhiteKey, Object.assign({ ref: keyRefs.current[index], __glissando: glissando && pressed, __position: notePosition(note), __note: note, __disabled: note > midiMax, __index: index, __fill: fill, __width: whiteNoteWidth, __onPlayNote: onPlayNote, __onStopNote: onStopNote, __label: label }, whiteKeyProps), note)) : (jsxRuntime.jsx(BlackKey, Object.assign({ ref: keyRefs.current[index], __glissando: glissando && pressed, __position: notePosition(note), __note: note, __disabled: note > midiMax, __index: index, __fill: fill, __width: whiteNoteWidth * 0.65, __onPlayNote: onPlayNote, __onStopNote: onStopNote, __label: label }, blackKeyProps), note));
1178
- }) })));
1441
+ return (jsxRuntime.jsx(PianoProvider, { noteRange: noteRange, glissando: glissando, midiMax: midiMax, fill: fill, onPlayNote: onPlayNote, onStopNote: onStopNote, label: label, children: jsxRuntime.jsx("div", Object.assign({ ref: (div) => {
1442
+ pianoRef.current = div;
1443
+ touchMoveRefCallback(div);
1444
+ }, className: clsx('tremolo-piano', className), style: Object.assign({ width: fill ? '100%' : staticWidth, height: height }, style), onPointerDown: (event) => {
1445
+ pointerDownHandler(event);
1446
+ onPointerDown === null || onPointerDown === void 0 ? void 0 : onPointerDown(event);
1447
+ } }, props, { children: childrenWithProps ||
1448
+ getNoteRangeArray(noteRange).map((note, index) => isWhiteKey(note) ? (jsxRuntime.jsx(WhiteKey, { ref: keyRefs.current[index], noteNumber: note }, note)) : (jsxRuntime.jsx(BlackKey, { ref: keyRefs.current[index], noteNumber: note }, note))) })) }));
1179
1449
  }
1180
1450
 
1181
1451
  const createSliderStore = (initProps) => {
@@ -1289,7 +1559,7 @@ function Scale(_a) {
1289
1559
  /** @category Slider */
1290
1560
  const defaultThumbSize = 22;
1291
1561
  /** @category Slider */
1292
- const SliderThumb = React.forwardRef(({ size, width = defaultThumbSize, height = defaultThumbSize, children, className, style, __percent, }, ref) => {
1562
+ const SliderThumb = React.forwardRef(({ size, width = defaultThumbSize, height = defaultThumbSize, color, children, className, style, __percent, }, ref) => {
1293
1563
  const wrapperRef = React.useRef(null);
1294
1564
  const vertical = useSliderContext((s) => s.vertical);
1295
1565
  const disabled = useSliderContext((s) => s.disabled);
@@ -1313,13 +1583,14 @@ const SliderThumb = React.forwardRef(({ size, width = defaultThumbSize, height =
1313
1583
  // default slider thumb
1314
1584
  jsxRuntime.jsx("div", { ref: wrapperRef, className: clsx('tremolo-slider-thumb', className),
1315
1585
  // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
1316
- tabIndex: 0, "aria-disabled": disabled, "aria-readonly": readonly, style: Object.assign({ width: size !== null && size !== void 0 ? size : width, height: size !== null && size !== void 0 ? size : height }, style) })) }));
1586
+ tabIndex: 0, "aria-disabled": disabled, "aria-readonly": readonly, style: Object.assign(Object.assign({ '--color': color }, { width: size !== null && size !== void 0 ? size : width, height: size !== null && size !== void 0 ? size : height }), style) })) }));
1317
1587
  });
1318
1588
 
1319
1589
  const defaultLength = 140;
1320
1590
  const defaultThickness = 10;
1321
1591
  /** @category Slider */
1322
- function SliderTrack({ length = defaultLength, thickness = defaultThickness, active, inactive, children, className, style, defaultStyle = true, __thumb, __percent = 0, }) {
1592
+ function SliderTrack(_a) {
1593
+ var { length = defaultLength, thickness = defaultThickness, active, inactive, children, className, style, defaultStyle = true, __thumb, __percent = 0 } = _a, props = __rest(_a, ["length", "thickness", "active", "inactive", "children", "className", "style", "defaultStyle", "__thumb", "__percent"]);
1323
1594
  const vertical = useSliderContext((s) => s.vertical);
1324
1595
  const reverse = useSliderContext((s) => s.reverse);
1325
1596
  const disabled = useSliderContext((s) => s.disabled);
@@ -1328,11 +1599,11 @@ function SliderTrack({ length = defaultLength, thickness = defaultThickness, act
1328
1599
  '--active': active,
1329
1600
  '--inactive': inactive,
1330
1601
  };
1331
- return (jsxRuntime.jsxs("div", { className: clsx('tremolo-slider-track', className), "aria-disabled": disabled, "data-vertical": vertical, style: !defaultStyle
1602
+ return (jsxRuntime.jsxs("div", Object.assign({ className: clsx('tremolo-slider-track', className), "aria-disabled": disabled, "data-vertical": vertical, style: !defaultStyle
1332
1603
  ? style
1333
1604
  : Object.assign(Object.assign(Object.assign({}, colors), { background: xor(vertical, reverse)
1334
1605
  ? `linear-gradient(to ${direction}, var(--inactive, #eee) ${__percent}%, var(--active, #7998ec) ${__percent}%)`
1335
- : `linear-gradient(to ${direction}, var(--active, #7998ec) ${__percent}%, var(--inactive, #eee) ${__percent}%)`, borderRadius: styleHelper(thickness, '/', 2), width: !vertical ? length : thickness, height: vertical ? length : thickness }), style), children: [children, __thumb] }));
1606
+ : `linear-gradient(to ${direction}, var(--active, #7998ec) ${__percent}%, var(--inactive, #eee) ${__percent}%)`, borderRadius: styleHelper(thickness, '/', 2), width: !vertical ? length : thickness, height: vertical ? length : thickness }), style) }, props, { children: [children, __thumb] })));
1336
1607
  }
1337
1608
 
1338
1609
  /**
@@ -1340,12 +1611,11 @@ function SliderTrack({ length = defaultLength, thickness = defaultThickness, act
1340
1611
  * @category Slider
1341
1612
  */
1342
1613
  const Slider = React.forwardRef((_a, ref) => {
1343
- var _b, _c, _d;
1344
- var { value, min, max, step = 1, skew = 1, vertical = false, reverse = false, className, style, bodyNoSelect = true, wheel = ['raw', 1], keyboard = ['raw', 1], disabled = false, readonly = false, onChange, children } = _a, props = __rest(_a, ["value", "min", "max", "step", "skew", "vertical", "reverse", "className", "style", "bodyNoSelect", "wheel", "keyboard", "disabled", "readonly", "onChange", "children"]);
1614
+ var _b;
1615
+ var { value, min, max, step = 1, skew = 1, vertical = false, reverse = false, bodyNoSelect = true, wheel = ['raw', 1], keyboard = ['raw', 1], disabled = false, readonly = false, onChange, onDragStart, onDragEnd, className, style, children, onFocus, onBlur, onPointerDown, onKeyDown } = _a, props = __rest(_a, ["value", "min", "max", "step", "skew", "vertical", "reverse", "bodyNoSelect", "wheel", "keyboard", "disabled", "readonly", "onChange", "onDragStart", "onDragEnd", "className", "style", "children", "onFocus", "onBlur", "onPointerDown", "onKeyDown"]);
1345
1616
  // -- state and ref ---
1346
1617
  const trackElementRef = React.useRef(null);
1347
1618
  const thumbRef = React.useRef(null);
1348
- const thumbDragged = React.useRef(false);
1349
1619
  // --- interpret props ---
1350
1620
  const p = toFixed$1(normalizeValue(value, min, max, skew) * 100);
1351
1621
  const rev = toFixed$1(100 - p);
@@ -1381,27 +1651,25 @@ const Slider = React.forwardRef((_a, ref) => {
1381
1651
  });
1382
1652
  }
1383
1653
  // --- internal functions ---
1384
- const handleValue = (event) => {
1385
- if (!trackElementRef.current ||
1386
- !thumbDragged.current ||
1387
- !onChange ||
1388
- readonly) {
1654
+ const onDrag = React.useCallback((nx, ny) => {
1655
+ if (!onChange || readonly)
1389
1656
  return;
1390
- }
1391
- const isTouch = event instanceof TouchEvent;
1392
- if (isTouch && event.cancelable)
1393
- event.preventDefault();
1394
- if (bodyNoSelect)
1395
- addNoSelect();
1396
- const { left: x1, top: y1, right: x2, bottom: y2, } = trackElementRef.current.getBoundingClientRect();
1397
- const mouseX = isTouch ? event.touches[0].clientX : event.clientX;
1398
- const mouseY = isTouch ? event.touches[0].clientY : event.clientY;
1399
- const n = vertical
1400
- ? normalizeValue(mouseY, y1, y2)
1401
- : normalizeValue(mouseX, x1, x2);
1657
+ const n = vertical ? ny : nx;
1402
1658
  const v = rawValue(displayReversed ? 1 - n : n, min, max, skew);
1403
- onChange(clamp(stepValue(v, step), min, max));
1404
- };
1659
+ const v2 = clamp(stepValue(v, step), min, max);
1660
+ onChange(v2);
1661
+ }, [displayReversed, max, min, onChange, readonly, skew, step, vertical]);
1662
+ const updateValueByEvent = React.useCallback((eventType, x) => {
1663
+ let newValue;
1664
+ if (eventType == 'normalized') {
1665
+ const n = normalizeValue(value, min, max, skew);
1666
+ newValue = rawValue(n + x, min, max, skew);
1667
+ }
1668
+ else {
1669
+ newValue = value + x;
1670
+ }
1671
+ return clamp(stepValue(newValue, step), min, max);
1672
+ }, [max, min, skew, step, value]);
1405
1673
  const handleKeyDown = React.useCallback((event) => {
1406
1674
  if (!keyboard || !onChange || readonly)
1407
1675
  return;
@@ -1411,19 +1679,30 @@ const Slider = React.forwardRef((_a, ref) => {
1411
1679
  let x = key == 'ArrowRight' || key == 'ArrowUp' ? keyboard[1] : -keyboard[1];
1412
1680
  if (reverse)
1413
1681
  x *= -1;
1414
- let v;
1415
- if (keyboard[0] == 'normalized') {
1416
- const n = normalizeValue(value, min, max, skew);
1417
- v = rawValue(n + x, min, max, skew);
1418
- }
1419
- else {
1420
- v = value + x;
1421
- }
1422
- onChange(clamp(stepValue(v, step), min, max));
1682
+ onChange(updateValueByEvent(keyboard[0], x));
1423
1683
  }
1424
- }, [value, min, max, step, skew, reverse, keyboard, onChange, readonly]);
1684
+ }, [keyboard, onChange, readonly, reverse, updateValueByEvent]);
1425
1685
  // --- hooks ---
1426
- const touchMoveRefCallback = useRefCallbackEvent('touchmove', handleValue, { passive: false }, [min, max, skew, step, vertical, reverse, onChange, readonly]);
1686
+ const [touchMoveRefCallback, pointerDownHandler] = useDragWithElement({
1687
+ baseElementRef: trackElementRef,
1688
+ onDrag: onDrag,
1689
+ onDragStart: (nx, ny) => {
1690
+ var _a;
1691
+ if (readonly)
1692
+ return;
1693
+ if (bodyNoSelect)
1694
+ addNoSelect();
1695
+ (_a = thumbRef.current) === null || _a === void 0 ? void 0 : _a.focus();
1696
+ onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart(clamp(stepValue(rawValue(vertical ? ny : nx, min, max, skew), step), min, max));
1697
+ },
1698
+ onDragEnd: (nx, ny) => {
1699
+ if (readonly)
1700
+ return;
1701
+ if (bodyNoSelect)
1702
+ removeNoSelect();
1703
+ onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(clamp(stepValue(rawValue(vertical ? ny : nx, min, max, skew), step), min, max));
1704
+ },
1705
+ });
1427
1706
  const wheelRefCallback = useRefCallbackEvent('wheel', (event) => {
1428
1707
  if (!wheel || !onChange || readonly)
1429
1708
  return;
@@ -1439,26 +1718,10 @@ const Slider = React.forwardRef((_a, ref) => {
1439
1718
  }
1440
1719
  if (vertical && reverse)
1441
1720
  x *= -1;
1442
- let v;
1443
- if (wheel[0] == 'normalized') {
1444
- const n = normalizeValue(value, min, max, skew);
1445
- v = rawValue(n + x, min, max, skew);
1446
- }
1447
- else {
1448
- v = value + x;
1449
- }
1450
- onChange(clamp(stepValue(v, step), min, max));
1721
+ onChange(updateValueByEvent(wheel[0], x));
1451
1722
  }, {
1452
1723
  passive: false,
1453
- }, [wheel, value, min, max, skew, step]);
1454
- useEventListener(globalThis.window, 'mousemove', (event) => {
1455
- handleValue(event);
1456
- });
1457
- useEventListener(globalThis.window, 'mouseup', () => {
1458
- thumbDragged.current = false;
1459
- if (bodyNoSelect)
1460
- removeNoSelect();
1461
- });
1724
+ }, [wheel, vertical, readonly, onChange, updateValueByEvent]);
1462
1725
  React.useImperativeHandle(ref, () => {
1463
1726
  return {
1464
1727
  focus() {
@@ -1475,9 +1738,20 @@ const Slider = React.forwardRef((_a, ref) => {
1475
1738
  wheelRefCallback(div);
1476
1739
  touchMoveRefCallback(div);
1477
1740
  }, tabIndex: -1, role: "slider", "aria-valuenow": value, "aria-valuemin": min, "aria-valuemax": max, "aria-orientation": vertical ? 'vertical' : 'horizontal', "aria-disabled": disabled, "aria-readonly": readonly, style: Object.assign({ margin: `calc(${styleHelper((_b = thumbProps === null || thumbProps === void 0 ? void 0 : thumbProps.size) !== null && _b !== void 0 ? _b : 22)} / 2)` }, style), onPointerDown: (event) => {
1478
- thumbDragged.current = true;
1479
- handleValue(event);
1480
- }, onKeyDown: handleKeyDown, onFocus: (_c = thumbRef.current) === null || _c === void 0 ? void 0 : _c.focus, onBlur: (_d = thumbRef.current) === null || _d === void 0 ? void 0 : _d.blur }, props, { children: jsxRuntime.jsxs("div", { style: {
1741
+ pointerDownHandler(event);
1742
+ onPointerDown === null || onPointerDown === void 0 ? void 0 : onPointerDown(event);
1743
+ }, onKeyDown: (event) => {
1744
+ handleKeyDown(event);
1745
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
1746
+ }, onFocus: (event) => {
1747
+ var _a;
1748
+ (_a = thumbRef.current) === null || _a === void 0 ? void 0 : _a.focus();
1749
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
1750
+ }, onBlur: (event) => {
1751
+ var _a;
1752
+ (_a = thumbRef.current) === null || _a === void 0 ? void 0 : _a.blur();
1753
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
1754
+ } }, props, { children: jsxRuntime.jsxs("div", { style: {
1481
1755
  display: 'flex',
1482
1756
  flexDirection: vertical ? 'row' : 'column',
1483
1757
  width: !vertical ? '100%' : undefined,
@@ -1496,12 +1770,14 @@ function WheelObserver(props) {
1496
1770
  }
1497
1771
 
1498
1772
  /** @category XYPad */
1499
- function XYPadArea({ width = 120, height = 120, bg = '#eee', children, className, style, __thumb, }) {
1500
- return (jsxRuntime.jsxs("div", { className: clsx('tremolo-xy-pad-area', className), style: Object.assign({ position: 'relative', width: width, height: height, background: bg }, style), children: [children, __thumb] }));
1773
+ function XYPadArea(_a) {
1774
+ var { width = 120, height = 120, color, children, className, style, __thumb } = _a, props = __rest(_a, ["width", "height", "color", "children", "className", "style", "__thumb"]);
1775
+ return (jsxRuntime.jsxs("div", Object.assign({ className: clsx('tremolo-xy-pad-area', className), style: Object.assign(Object.assign({ '--color': color }, { width: width, height: height }), style) }, props, { children: [children, __thumb] })));
1501
1776
  }
1502
1777
 
1503
1778
  /** @category XYPad */
1504
- const XYPadThumb = React.forwardRef(({ size, width = 22, height = 22, children, wrapperClassName, wrapperStyle, className, style, __disabled, __css, }, ref) => {
1779
+ const XYPadThumb = React.forwardRef((_a, ref) => {
1780
+ var { size, width = 22, height = 22, color, children, wrapperClassName, wrapperStyle, className, style, __disabled, __css } = _a, props = __rest(_a, ["size", "width", "height", "color", "children", "wrapperClassName", "wrapperStyle", "className", "style", "__disabled", "__css"]);
1505
1781
  const wrapperRef = React.useRef(null);
1506
1782
  React.useImperativeHandle(ref, () => {
1507
1783
  return {
@@ -1515,11 +1791,11 @@ const XYPadThumb = React.forwardRef(({ size, width = 22, height = 22, children,
1515
1791
  },
1516
1792
  };
1517
1793
  }, []);
1518
- return (jsxRuntime.jsx("div", { className: clsx('tremolo-xy-pad-thumb-wrapper', wrapperClassName), style: Object.assign(Object.assign({}, __css), wrapperStyle), children: children ? (children) : (
1794
+ return (jsxRuntime.jsx("div", Object.assign({ className: clsx('tremolo-xy-pad-thumb-wrapper', wrapperClassName), style: Object.assign(Object.assign({}, __css), wrapperStyle) }, props, { children: children ? (children) : (
1519
1795
  // default thumb
1520
1796
  jsxRuntime.jsx("div", { ref: wrapperRef, className: clsx('tremolo-xy-pad-thumb', className),
1521
1797
  // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
1522
- tabIndex: 0, "aria-disabled": __disabled, style: Object.assign({ width: size !== null && size !== void 0 ? size : width, height: size !== null && size !== void 0 ? size : height }, style) })) }));
1798
+ tabIndex: 0, "aria-disabled": __disabled, style: Object.assign(Object.assign({ '--color': color }, { width: size !== null && size !== void 0 ? size : width, height: size !== null && size !== void 0 ? size : height }), style) })) })));
1523
1799
  });
1524
1800
 
1525
1801
  const defaultValueOptions = {
@@ -1534,8 +1810,8 @@ const defaultValueOptions = {
1534
1810
  * @category XYPad
1535
1811
  */
1536
1812
  const XYPad = React.forwardRef((_a, ref) => {
1537
- var _b, _c, _d;
1538
- var { x: _x, y: _y, className, style, bodyNoSelect = true, disabled = false, readonly = false, onChange, children } = _a, props = __rest(_a, ["x", "y", "className", "style", "bodyNoSelect", "disabled", "readonly", "onChange", "children"]);
1813
+ var _b;
1814
+ var { x: _x, y: _y, className, style, bodyNoSelect = true, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, onPointerDown, onKeyDown, onFocus, onBlur, children } = _a, props = __rest(_a, ["x", "y", "className", "style", "bodyNoSelect", "disabled", "readonly", "onChange", "onDragStart", "onDragEnd", "onPointerDown", "onKeyDown", "onFocus", "onBlur", "children"]);
1539
1815
  const x = React.useMemo(() => {
1540
1816
  return Object.assign(Object.assign({}, defaultValueOptions), _x);
1541
1817
  }, [_x]);
@@ -1545,13 +1821,13 @@ const XYPad = React.forwardRef((_a, ref) => {
1545
1821
  // -- state and ref ---
1546
1822
  const areaElementRef = React.useRef(null);
1547
1823
  const thumbRef = React.useRef(null);
1548
- const thumbDragged = React.useRef(false);
1549
1824
  // --- interpret props ---
1550
1825
  const nx = normalizeValue(x.value, x.min, x.max, x.skew);
1551
1826
  const ny = normalizeValue(y.value, y.min, y.max, y.skew);
1552
1827
  const percentX = toFixed$1((x.reverse ? 1 - nx : nx) * 100);
1553
1828
  const percentY = toFixed$1((y.reverse ? 1 - ny : ny) * 100);
1554
- let areaProps = {}, thumbProps = {};
1829
+ let areaProps = {};
1830
+ let thumbProps = {};
1555
1831
  if (children != undefined) {
1556
1832
  React.Children.forEach(children, (child) => {
1557
1833
  if (React.isValidElement(child)) {
@@ -1571,28 +1847,39 @@ const XYPad = React.forwardRef((_a, ref) => {
1571
1847
  });
1572
1848
  }
1573
1849
  // --- internal functions ---
1574
- const handleValue = (event) => {
1575
- if (!areaElementRef.current ||
1576
- !thumbDragged.current ||
1577
- !onChange ||
1578
- readonly)
1850
+ const onDrag = React.useCallback((nx, ny) => {
1851
+ if (!onChange || readonly)
1579
1852
  return;
1580
- const isTouch = event instanceof TouchEvent;
1581
- if (isTouch && event.cancelable)
1582
- event.preventDefault();
1583
- if (bodyNoSelect)
1584
- addNoSelect();
1585
- const { left: x1, top: y1, right: x2, bottom: y2, } = areaElementRef.current.getBoundingClientRect();
1586
- const mouseX = isTouch ? event.touches[0].clientX : event.clientX;
1587
- const mouseY = isTouch ? event.touches[0].clientY : event.clientY;
1588
- const nx = normalizeValue(mouseX, x1, x2);
1589
- const ny = normalizeValue(mouseY, y1, y2);
1590
1853
  const vx = rawValue(x.reverse ? 1 - nx : nx, x.min, x.max, x.skew);
1591
1854
  const vy = rawValue(y.reverse ? 1 - ny : ny, y.min, y.max, y.skew);
1592
1855
  const newX = clamp(stepValue(vx, x.step), x.min, x.max);
1593
1856
  const newY = clamp(stepValue(vy, y.step), y.min, y.max);
1594
1857
  onChange(newX, newY);
1595
- };
1858
+ }, [
1859
+ onChange,
1860
+ readonly,
1861
+ x.max,
1862
+ x.min,
1863
+ x.reverse,
1864
+ x.skew,
1865
+ x.step,
1866
+ y.max,
1867
+ y.min,
1868
+ y.reverse,
1869
+ y.skew,
1870
+ y.step,
1871
+ ]);
1872
+ const updateValueByEvent = React.useCallback((eventType, target, x) => {
1873
+ let v;
1874
+ if (eventType == 'normalized') {
1875
+ const n = normalizeValue(target.value, target.min, target.max, target.skew);
1876
+ v = rawValue(n + x, target.min, target.max, target.skew);
1877
+ }
1878
+ else {
1879
+ v = target.value + x;
1880
+ }
1881
+ return clamp(stepValue(v, target.step), target.min, target.max);
1882
+ }, []);
1596
1883
  const handleKeyDown = React.useCallback((event) => {
1597
1884
  if (!onChange || readonly)
1598
1885
  return;
@@ -1608,15 +1895,7 @@ const XYPad = React.forwardRef((_a, ref) => {
1608
1895
  delta *= -1;
1609
1896
  if (target.reverse)
1610
1897
  delta *= -1;
1611
- let v;
1612
- if (target.keyboard[0] == 'normalized') {
1613
- const n = normalizeValue(target.value, target.min, target.max, target.skew);
1614
- v = rawValue(n + delta, target.min, target.max, target.skew);
1615
- }
1616
- else {
1617
- v = target.value + delta;
1618
- }
1619
- const newValue = clamp(stepValue(v, target.step), target.min, target.max);
1898
+ const newValue = updateValueByEvent(target.keyboard[0], target, delta);
1620
1899
  if (isHorizontal) {
1621
1900
  onChange(newValue, y.value);
1622
1901
  }
@@ -1624,9 +1903,32 @@ const XYPad = React.forwardRef((_a, ref) => {
1624
1903
  onChange(x.value, newValue);
1625
1904
  }
1626
1905
  }
1627
- }, [x, y, onChange, readonly]);
1906
+ }, [onChange, readonly, x, y, updateValueByEvent]);
1628
1907
  // --- hooks ---
1629
- const touchMoveRefCallback = useRefCallbackEvent('touchmove', handleValue, { passive: false }, [x, onChange, readonly]);
1908
+ const [touchMoveRefCallback, pointerDownHandler] = useDragWithElement({
1909
+ baseElementRef: areaElementRef,
1910
+ onDrag: onDrag,
1911
+ onDragStart: (nx, ny) => {
1912
+ var _a;
1913
+ if (readonly)
1914
+ return;
1915
+ if (bodyNoSelect)
1916
+ addNoSelect();
1917
+ (_a = thumbRef.current) === null || _a === void 0 ? void 0 : _a.focus();
1918
+ const valueX = clamp(stepValue(rawValue(nx, x.min, x.max, x.skew), x.step), x.min, x.max);
1919
+ const valueY = clamp(stepValue(rawValue(ny, y.min, y.max, y.skew), y.step), y.min, y.max);
1920
+ onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart(valueX, valueY);
1921
+ },
1922
+ onDragEnd: (nx, ny) => {
1923
+ if (readonly)
1924
+ return;
1925
+ if (bodyNoSelect)
1926
+ removeNoSelect();
1927
+ const valueX = clamp(stepValue(rawValue(nx, x.min, x.max, x.skew), x.step), x.min, x.max);
1928
+ const valueY = clamp(stepValue(rawValue(ny, y.min, y.max, y.skew), y.step), y.min, y.max);
1929
+ onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(valueX, valueY);
1930
+ },
1931
+ });
1630
1932
  const wheelRefCallback = useRefCallbackEvent('wheel', (event) => {
1631
1933
  if (!onChange || readonly)
1632
1934
  return;
@@ -1639,15 +1941,7 @@ const XYPad = React.forwardRef((_a, ref) => {
1639
1941
  delta *= -1;
1640
1942
  if (target.reverse)
1641
1943
  delta *= -1;
1642
- let v;
1643
- if (target.wheel[0] == 'normalized') {
1644
- const n = normalizeValue(target.value, target.min, target.max, target.skew);
1645
- v = rawValue(n + delta, target.min, target.max, target.skew);
1646
- }
1647
- else {
1648
- v = target.value + delta;
1649
- }
1650
- const newValue = clamp(stepValue(v, target.step), target.min, target.max);
1944
+ const newValue = updateValueByEvent(target.wheel[0], target, delta);
1651
1945
  if (event.shiftKey) {
1652
1946
  onChange(newValue, y.value);
1653
1947
  }
@@ -1656,15 +1950,7 @@ const XYPad = React.forwardRef((_a, ref) => {
1656
1950
  }
1657
1951
  }, {
1658
1952
  passive: false,
1659
- }, [x, y]);
1660
- useEventListener(globalThis.window, 'mousemove', (event) => {
1661
- handleValue(event);
1662
- });
1663
- useEventListener(globalThis.window, 'mouseup', () => {
1664
- thumbDragged.current = false;
1665
- if (bodyNoSelect)
1666
- removeNoSelect();
1667
- });
1953
+ }, [x, y, onChange, readonly, updateValueByEvent]);
1668
1954
  React.useImperativeHandle(ref, () => {
1669
1955
  return {
1670
1956
  focus() {
@@ -1683,14 +1969,38 @@ const XYPad = React.forwardRef((_a, ref) => {
1683
1969
  wheelRefCallback(div);
1684
1970
  touchMoveRefCallback(div);
1685
1971
  }, tabIndex: -1, "aria-disabled": disabled, "aria-readonly": readonly, style: Object.assign({ margin: `calc(${styleHelper((_b = thumbProps === null || thumbProps === void 0 ? void 0 : thumbProps.size) !== null && _b !== void 0 ? _b : 22)} / 2)`, cursor: readonly ? 'not-allowed' : 'pointer', WebkitTapHighlightColor: 'transparent' }, style), onPointerDown: (event) => {
1686
- thumbDragged.current = true;
1687
- handleValue(event);
1688
- }, onKeyDown: handleKeyDown, onFocus: (_c = thumbRef.current) === null || _c === void 0 ? void 0 : _c.focus, onBlur: (_d = thumbRef.current) === null || _d === void 0 ? void 0 : _d.blur }, props, { children: jsxRuntime.jsx("div", { className: "tremolo-xy-pad-area-wrapper", ref: areaElementRef, children: jsxRuntime.jsx(XYPadArea, Object.assign({ __thumb: jsxRuntime.jsx(XYPadThumb, Object.assign({ ref: thumbRef, __disabled: disabled, __css: {
1972
+ pointerDownHandler(event);
1973
+ onPointerDown === null || onPointerDown === void 0 ? void 0 : onPointerDown(event);
1974
+ }, onKeyDown: (event) => {
1975
+ handleKeyDown(event);
1976
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
1977
+ }, onFocus: (event) => {
1978
+ var _a;
1979
+ (_a = thumbRef.current) === null || _a === void 0 ? void 0 : _a.focus();
1980
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
1981
+ }, onBlur: (event) => {
1982
+ var _a;
1983
+ (_a = thumbRef.current) === null || _a === void 0 ? void 0 : _a.blur();
1984
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
1985
+ } }, props, { children: jsxRuntime.jsx("div", { className: "tremolo-xy-pad-area-wrapper", ref: areaElementRef, children: jsxRuntime.jsx(XYPadArea, Object.assign({ __thumb: jsxRuntime.jsx(XYPadThumb, Object.assign({ ref: thumbRef, __disabled: disabled, __css: {
1689
1986
  top: `${percentY}%`,
1690
1987
  left: `${percentX}%`,
1691
1988
  } }, thumbProps)) }, areaProps)) }) })));
1692
1989
  });
1693
1990
 
1991
+ const useAnimationFrame = (callback = () => { }, deps = []) => {
1992
+ const reqIdRef = React.useRef(-1);
1993
+ const loop = React.useCallback(() => {
1994
+ reqIdRef.current = requestAnimationFrame(loop);
1995
+ callback();
1996
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1997
+ }, [callback, ...deps]);
1998
+ React.useEffect(() => {
1999
+ reqIdRef.current = requestAnimationFrame(loop);
2000
+ return () => cancelAnimationFrame(reqIdRef.current);
2001
+ }, [loop]);
2002
+ };
2003
+
1694
2004
  exports.AnimationCanvas = AnimationCanvas;
1695
2005
  exports.AnimationKnob = AnimationKnob;
1696
2006
  exports.BlackKey = BlackKey;
@@ -1701,9 +2011,9 @@ exports.KeyLabel = KeyLabel;
1701
2011
  exports.Knob = Knob;
1702
2012
  exports.NumberInput = NumberInput;
1703
2013
  exports.Piano = Piano;
2014
+ exports.SHORTCUTS = SHORTCUTS;
1704
2015
  exports.Scale = Scale;
1705
2016
  exports.ScaleOption = ScaleOption;
1706
- exports.Shortcuts = Shortcuts;
1707
2017
  exports.Slider = Slider;
1708
2018
  exports.SliderThumb = SliderThumb;
1709
2019
  exports.SliderTrack = SliderTrack;
@@ -1713,5 +2023,11 @@ exports.WhiteKey = WhiteKey;
1713
2023
  exports.XYPad = XYPad;
1714
2024
  exports.XYPadArea = XYPadArea;
1715
2025
  exports.XYPadThumb = XYPadThumb;
2026
+ exports.getNoteRangeArray = getNoteRangeArray;
2027
+ exports.useAnimationFrame = useAnimationFrame;
2028
+ exports.useDrag = useDrag;
2029
+ exports.useEventListener = useEventListener;
2030
+ exports.useInterval = useInterval;
2031
+ exports.useLongPress = useLongPress;
1716
2032
  exports.useSliderContext = useSliderContext;
1717
2033
  //# sourceMappingURL=index.js.map