@tremolo-ui/react 0.5.0 → 0.7.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 (86) hide show
  1. package/dist/compose-refs.cjs +72 -0
  2. package/dist/compose-refs.cjs.map +1 -0
  3. package/dist/compose-refs.d.cts +43 -0
  4. package/dist/compose-refs.d.cts.map +1 -0
  5. package/dist/compose-refs.d.ts +43 -0
  6. package/dist/compose-refs.d.ts.map +1 -0
  7. package/dist/compose-refs.js +70 -0
  8. package/dist/compose-refs.js.map +1 -0
  9. package/dist/index.cjs +1669 -541
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1571 -486
  12. package/dist/index.d.cts.map +1 -1
  13. package/dist/index.d.ts +1571 -486
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1640 -538
  16. package/dist/index.js.map +1 -1
  17. package/package.json +29 -27
  18. package/src/components/AnimationCanvas/index.tsx +115 -76
  19. package/src/components/DropZone/index.tsx +100 -0
  20. package/src/components/FileInput/Trigger.tsx +39 -0
  21. package/src/components/FileInput/context.tsx +24 -0
  22. package/src/components/FileInput/index.tsx +144 -0
  23. package/src/components/Knob/ActiveLine.tsx +7 -6
  24. package/src/components/Knob/InactiveLine.tsx +9 -10
  25. package/src/components/Knob/SVGRoot.tsx +5 -3
  26. package/src/components/Knob/Thumb.tsx +33 -11
  27. package/src/components/Knob/context.tsx +17 -1
  28. package/src/components/Knob/index.tsx +156 -60
  29. package/src/components/NumberInput/DecrementStepper.tsx +3 -10
  30. package/src/components/NumberInput/IncrementStepper.tsx +3 -10
  31. package/src/components/NumberInput/InputField.tsx +185 -108
  32. package/src/components/NumberInput/Stepper.tsx +120 -84
  33. package/src/components/NumberInput/StepperButton.tsx +100 -0
  34. package/src/components/NumberInput/context.tsx +34 -8
  35. package/src/components/NumberInput/index.tsx +270 -82
  36. package/src/components/Piano/index.tsx +363 -200
  37. package/src/components/PointsEditor/Background.tsx +13 -10
  38. package/src/components/PointsEditor/Container.tsx +83 -29
  39. package/src/components/PointsEditor/Point.tsx +241 -79
  40. package/src/components/PointsEditor/SelectionBox.tsx +48 -0
  41. package/src/components/PointsEditor/context.tsx +78 -10
  42. package/src/components/PointsEditor/index.tsx +440 -41
  43. package/src/components/Slider/Marks.tsx +48 -19
  44. package/src/components/Slider/MarksOption.tsx +64 -45
  45. package/src/components/Slider/Thumb.tsx +107 -55
  46. package/src/components/Slider/Track.tsx +66 -62
  47. package/src/components/Slider/context.tsx +4 -2
  48. package/src/components/Slider/index.tsx +136 -77
  49. package/src/components/Slider/type.ts +29 -9
  50. package/src/components/XYPad/Area.tsx +46 -41
  51. package/src/components/XYPad/Thumb.tsx +131 -64
  52. package/src/components/XYPad/context.tsx +4 -1
  53. package/src/components/XYPad/index.tsx +183 -53
  54. package/src/components/_util/Placement.tsx +68 -0
  55. package/src/components/_util/VisuallyHiddenRangeInput.tsx +12 -0
  56. package/src/components/_util/css-length.ts +13 -0
  57. package/src/components/_util/decimal-digits.ts +23 -0
  58. package/src/components/_util/visually-hidden.ts +25 -0
  59. package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
  60. package/src/css-variables.ts +23 -0
  61. package/src/hooks/_internal/useCallbackRef.ts +3 -1
  62. package/src/hooks/_internal/useCheckSteps.ts +169 -0
  63. package/src/hooks/useAnimationFrame.ts +17 -7
  64. package/src/hooks/useDrag.ts +51 -12
  65. package/src/hooks/useDragValue.ts +71 -11
  66. package/src/hooks/useDropZone.ts +78 -0
  67. package/src/hooks/useEventListener.ts +10 -6
  68. package/src/hooks/useLongPress.ts +42 -8
  69. package/src/hooks/useMIDIAccess.ts +82 -11
  70. package/src/hooks/useMIDIInput.ts +45 -13
  71. package/src/hooks/useMIDIMessage.ts +23 -5
  72. package/src/hooks/useWheel.ts +4 -1
  73. package/src/index.ts +45 -25
  74. package/src/input-event.ts +32 -0
  75. package/dist/index.css +0 -501
  76. package/src/Globals.d.ts +0 -3
  77. package/src/components/Knob/index.css +0 -81
  78. package/src/components/NumberInput/index.css +0 -121
  79. package/src/components/NumberInput/stepperButton.tsx +0 -90
  80. package/src/components/Piano/index.css +0 -90
  81. package/src/components/PointsEditor/index.css +0 -35
  82. package/src/components/Slider/index.css +0 -125
  83. package/src/components/XYPad/index.css +0 -60
  84. package/src/components/_util/index.ts +0 -28
  85. package/src/styles/global.css +0 -19
  86. /package/src/components/Piano/{keyboardShortcuts.ts → keyboard-shortcuts.ts} +0 -0
@@ -0,0 +1,78 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+
3
+ import {
4
+ createDropZone,
5
+ type DropZoneInstance,
6
+ type DropZoneOptions,
7
+ } from '@tremolo-ui/dom'
8
+
9
+ import { useCallbackRef } from './_internal/useCallbackRef'
10
+
11
+ // `onStateChange` is left out: what it reports comes back from the hook as
12
+ // `over` and `invalid`, which is what a component renders from.
13
+ export interface UseDropZoneOptions extends Omit<
14
+ DropZoneOptions,
15
+ 'onStateChange'
16
+ > {}
17
+
18
+ /**
19
+ * Take files dropped onto an element.
20
+ *
21
+ * Any element can become a drop target this way — a waveform display, a track
22
+ * lane, a canvas — without wrapping it in anything.
23
+ *
24
+ * @returns a ref callback for the element, and whether a drag is over it
25
+ */
26
+ export function useDropZone<T extends Element>({
27
+ accept,
28
+ multiple,
29
+ disabled,
30
+ onDrop,
31
+ onReject,
32
+ }: UseDropZoneOptions = {}): {
33
+ refCallback: (node: T | null) => void
34
+ over: boolean
35
+ invalid: boolean
36
+ } {
37
+ const dropHandler = useCallbackRef(onDrop)
38
+ const rejectHandler = useCallbackRef(onReject)
39
+
40
+ const [state, setState] = useState({ over: false, invalid: false })
41
+
42
+ // See useDrag for why the node is held in state.
43
+ const [node, setNode] = useState<T | null>(null)
44
+
45
+ const instanceRef = useRef<DropZoneInstance | null>(null)
46
+
47
+ // Read when the instance is created. The effect below keeps it current, and
48
+ // runs right after, so a stale setting is replaced within the same commit.
49
+ const latest = useRef({ accept, multiple, disabled })
50
+
51
+ useEffect(() => {
52
+ if (!node) return
53
+
54
+ const instance = createDropZone(node, {
55
+ ...latest.current,
56
+ onDrop: (files, event) => dropHandler(files, event),
57
+ onReject: (files, event) => rejectHandler(files, event),
58
+ onStateChange: (next) => setState(next),
59
+ })
60
+ instanceRef.current = instance
61
+
62
+ return () => {
63
+ instanceRef.current = null
64
+ instance.destroy()
65
+ // The listeners are gone, so no `dragleave` is coming to clear this.
66
+ setState({ over: false, invalid: false })
67
+ }
68
+ }, [node, dropHandler, rejectHandler])
69
+
70
+ // Runs after every render: the settings come from props and are cheap to
71
+ // push, and updating in place leaves a drag in progress untouched.
72
+ useEffect(() => {
73
+ latest.current = { accept, multiple, disabled }
74
+ instanceRef.current?.update({ accept, multiple, disabled })
75
+ })
76
+
77
+ return { refCallback: setNode, over: state.over, invalid: state.invalid }
78
+ }
@@ -1,4 +1,4 @@
1
- import { useEffect } from 'react'
1
+ import { useCallback, useEffect, useRef } from 'react'
2
2
 
3
3
  import { useCallbackRef } from './_internal/useCallbackRef'
4
4
 
@@ -30,6 +30,7 @@ export function useEventListener(
30
30
  options?: Options,
31
31
  ) {
32
32
  const listener = useCallbackRef(handler)
33
+ const cleanupRef = useRef<VoidFunction>(() => {})
33
34
 
34
35
  useEffect(() => {
35
36
  const node = typeof target === 'function' ? target() : (target ?? document)
@@ -37,13 +38,16 @@ export function useEventListener(
37
38
  if (!node) return
38
39
 
39
40
  node.addEventListener(event, listener, options)
40
- return () => {
41
+ const cleanup = () => {
41
42
  node.removeEventListener(event, listener, options)
42
43
  }
44
+ cleanupRef.current = cleanup
45
+
46
+ return () => {
47
+ cleanup()
48
+ if (cleanupRef.current === cleanup) cleanupRef.current = () => {}
49
+ }
43
50
  }, [event, target, options, listener])
44
51
 
45
- return () => {
46
- const node = typeof target === 'function' ? target() : (target ?? document)
47
- node?.removeEventListener(event, listener, options)
48
- }
52
+ return useCallback(() => cleanupRef.current(), [])
49
53
  }
@@ -1,4 +1,4 @@
1
- import { useCallback, useState } from 'react'
1
+ import { useCallback, useRef, useState } from 'react'
2
2
 
3
3
  import { useEventListener } from './useEventListener'
4
4
  import { useInterval } from './useInterval'
@@ -10,6 +10,8 @@ export function useLongPress(
10
10
  ) {
11
11
  const [pressed, setPressed] = useState(false)
12
12
  const [delay, setDelay] = useState(initialDelay)
13
+ const activePointerId = useRef<number | null>(null)
14
+ const isPressed = useRef(false)
13
15
 
14
16
  useInterval(
15
17
  () => {
@@ -19,13 +21,45 @@ export function useLongPress(
19
21
  pressed ? delay : null,
20
22
  )
21
23
 
22
- useEventListener(globalThis.window, 'pointerup', () => {
23
- setPressed(false)
24
- setDelay(initialDelay)
24
+ const stop = useCallback(
25
+ (pointerId?: number) => {
26
+ if (!isPressed.current) return
27
+ if (
28
+ pointerId !== undefined &&
29
+ activePointerId.current !== null &&
30
+ activePointerId.current !== pointerId
31
+ )
32
+ return
33
+
34
+ isPressed.current = false
35
+ activePointerId.current = null
36
+ setPressed(false)
37
+ setDelay(initialDelay)
38
+ },
39
+ [initialDelay],
40
+ )
41
+
42
+ useEventListener(globalThis.window, 'pointerup', (event) => {
43
+ stop(event.pointerId)
44
+ })
45
+
46
+ useEventListener(globalThis.window, 'pointercancel', (event) => {
47
+ stop(event.pointerId)
25
48
  })
26
49
 
27
- return useCallback(() => {
28
- callback()
29
- setPressed(true)
30
- }, [callback])
50
+ useEventListener(globalThis.window, 'blur', () => {
51
+ stop()
52
+ })
53
+
54
+ return useCallback(
55
+ (event?: Pick<PointerEvent, 'button' | 'pointerId'>) => {
56
+ if (isPressed.current || (event && event.button !== 0)) return
57
+
58
+ isPressed.current = true
59
+ activePointerId.current = event?.pointerId ?? null
60
+ callback()
61
+ setPressed(true)
62
+ },
63
+ [callback],
64
+ )
31
65
  }
@@ -1,32 +1,103 @@
1
1
  import { useEffect, useMemo, useSyncExternalStore } from 'react'
2
2
 
3
- import { createMIDIAccess } from '@tremolo-ui/dom'
3
+ import {
4
+ createMIDIAccess,
5
+ type MIDIAccessInstance,
6
+ type MIDIAccessOptions,
7
+ type MIDIAccessState,
8
+ } from '@tremolo-ui/dom'
4
9
 
5
10
  export {
6
11
  NOT_SUPPORTED,
7
12
  PERMISSION_DENIED,
13
+ UNAVAILABLE,
8
14
  type MIDIAccessError,
15
+ type MIDIAccessOptions,
9
16
  } from '@tremolo-ui/dom'
10
17
 
18
+ const INITIAL_STATE: MIDIAccessState = {
19
+ midiAccess: null,
20
+ error: null,
21
+ inputs: [],
22
+ }
23
+
24
+ function createMIDIAccessStore() {
25
+ let instance: MIDIAccessInstance | null = null
26
+ let unsubscribe: VoidFunction | null = null
27
+ let state = INITIAL_STATE
28
+ const listeners = new Set<VoidFunction>()
29
+
30
+ function emit() {
31
+ for (const listener of listeners) listener()
32
+ }
33
+
34
+ function connect() {
35
+ const nextInstance = createMIDIAccess()
36
+ instance = nextInstance
37
+ state = nextInstance.getState()
38
+ unsubscribe = nextInstance.subscribe(() => {
39
+ state = nextInstance.getState()
40
+ emit()
41
+ })
42
+ emit()
43
+ return nextInstance
44
+ }
45
+
46
+ function disconnect(currentInstance: MIDIAccessInstance) {
47
+ if (instance !== currentInstance) return
48
+
49
+ unsubscribe?.()
50
+ unsubscribe = null
51
+ instance = null
52
+ state = INITIAL_STATE
53
+ currentInstance.destroy()
54
+ emit()
55
+ }
56
+
57
+ function request(options?: MIDIAccessOptions) {
58
+ instance?.request(options)
59
+ }
60
+
61
+ return {
62
+ connect,
63
+ disconnect,
64
+ request,
65
+ getState: () => state,
66
+ getServerState: () => INITIAL_STATE,
67
+ subscribe: (listener: VoidFunction) => {
68
+ listeners.add(listener)
69
+ return () => listeners.delete(listener)
70
+ },
71
+ }
72
+ }
73
+
11
74
  /**
12
75
  * Hooks for requesting MIDI access in the browser. The first argument allows you to choose whether to request access on mount.
76
+ *
77
+ * `inputs` follows the devices: it changes as one is plugged in or unplugged,
78
+ * so a list of devices needs no `statechange` listener of its own.
79
+ *
80
+ * `request` takes `{ sysex: true }` when the app needs system exclusive
81
+ * messages. Browsers treat that as a separate, more sensitive permission, so
82
+ * ask for it only when it is actually used.
13
83
  */
14
84
  export function useMIDIAccess(requestOnMount = true) {
15
- const instance = useMemo(() => createMIDIAccess(), [])
85
+ const store = useMemo(() => createMIDIAccessStore(), [])
16
86
 
17
- const { midiAccess, error } = useSyncExternalStore(
18
- instance.subscribe,
19
- instance.getState,
20
- instance.getServerState,
87
+ const { midiAccess, error, inputs } = useSyncExternalStore(
88
+ store.subscribe,
89
+ store.getState,
90
+ store.getServerState,
21
91
  )
22
92
 
23
93
  useEffect(() => {
24
- if (requestOnMount) instance.request()
25
- }, [instance, requestOnMount])
94
+ const instance = store.connect()
95
+ return () => store.disconnect(instance)
96
+ }, [store])
26
97
 
27
98
  useEffect(() => {
28
- return () => instance.destroy()
29
- }, [instance])
99
+ if (requestOnMount) store.request()
100
+ }, [store, requestOnMount])
30
101
 
31
- return { request: instance.request, midiAccess, error }
102
+ return { request: store.request, midiAccess, error, inputs }
32
103
  }
@@ -1,22 +1,54 @@
1
- import { useEffect } from 'react'
1
+ import { useEffect, useRef } from 'react'
2
2
 
3
- import { createMIDIInput } from '@tremolo-ui/dom'
3
+ import {
4
+ createMIDIInput,
5
+ PITCH_BEND_CENTER,
6
+ type MIDIInputHandlers,
7
+ type MIDIInputInstance,
8
+ } from '@tremolo-ui/dom'
9
+
10
+ export { PITCH_BEND_CENTER, type MIDIInputHandlers }
4
11
 
5
12
  /**
6
- * Hooks for handling note on/off events. To be used with useMIDIAccess. Internally uses useMIDIMessage.
13
+ * Handle MIDI input events. To be used with {@link useMIDIAccess}.
14
+ *
15
+ * The handlers are read fresh on every event, so writing them inline is fine:
16
+ * the listeners are attached once per `midiAccess` and stay attached. Devices
17
+ * plugged in later are picked up without anything having to be rebuilt.
18
+ *
19
+ * @example
20
+ * const { midiAccess } = useMIDIAccess()
21
+ * useMIDIInput(midiAccess, {
22
+ * onNoteOnEvent: (note, velocity) => play(note, velocity / 127),
23
+ * onNoteOffEvent: (note) => stop(note),
24
+ * onControlChangeEvent: (controller, value) => {
25
+ * if (controller === 1) setModulation(value / 127)
26
+ * },
27
+ * })
7
28
  */
8
29
  export function useMIDIInput(
9
30
  midiAccess: MIDIAccess | null,
10
- onNoteOnEvent?: (note: number, velocity: number) => void,
11
- onNoteOffEvent?: (note: number) => void,
12
- onPitchBendEvent?: (msb: number, lsb: number) => void,
31
+ handlers: MIDIInputHandlers,
13
32
  ) {
33
+ // Read when the instance is created. The effect below keeps it current, and
34
+ // runs right after, so a stale handler is replaced within the same commit.
35
+ const latest = useRef(handlers)
36
+ const instanceRef = useRef<MIDIInputInstance | null>(null)
37
+
38
+ useEffect(() => {
39
+ const instance = createMIDIInput(midiAccess, latest.current)
40
+ instanceRef.current = instance
41
+
42
+ return () => {
43
+ instanceRef.current = null
44
+ instance.destroy()
45
+ }
46
+ }, [midiAccess])
47
+
48
+ // Runs after every render: handlers come from props and are cheap to push,
49
+ // and updating in place leaves the listeners untouched.
14
50
  useEffect(() => {
15
- const instance = createMIDIInput(midiAccess, {
16
- onNoteOnEvent,
17
- onNoteOffEvent,
18
- onPitchBendEvent,
19
- })
20
- return () => instance.destroy()
21
- }, [midiAccess, onNoteOnEvent, onNoteOffEvent, onPitchBendEvent])
51
+ latest.current = handlers
52
+ instanceRef.current?.update(handlers)
53
+ })
22
54
  }
@@ -1,16 +1,34 @@
1
- import { useEffect } from 'react'
1
+ import { useEffect, useRef } from 'react'
2
2
 
3
- import { createMIDIMessage } from '@tremolo-ui/dom'
3
+ import { createMIDIMessage, type MIDIMessageInstance } from '@tremolo-ui/dom'
4
4
 
5
5
  /**
6
6
  * Hooks for when you want to process MIDI events in more detail than useMIDIInput.
7
+ *
8
+ * The handler is read fresh on every event, so writing it inline is fine: the
9
+ * listeners are attached once per `midiAccess` and stay attached, and devices
10
+ * plugged in later are picked up.
7
11
  */
8
12
  export function useMIDIMessage(
9
13
  midiAccess: MIDIAccess | null,
10
14
  onMIDIMessage: (event: MIDIMessageEvent) => void,
11
15
  ) {
16
+ const latest = useRef(onMIDIMessage)
17
+ const instanceRef = useRef<MIDIMessageInstance | null>(null)
18
+
19
+ useEffect(() => {
20
+ const instance = createMIDIMessage(midiAccess, (event) =>
21
+ latest.current(event),
22
+ )
23
+ instanceRef.current = instance
24
+
25
+ return () => {
26
+ instanceRef.current = null
27
+ instance.destroy()
28
+ }
29
+ }, [midiAccess])
30
+
12
31
  useEffect(() => {
13
- const instance = createMIDIMessage(midiAccess, onMIDIMessage)
14
- return () => instance.destroy()
15
- }, [midiAccess, onMIDIMessage])
32
+ latest.current = onMIDIMessage
33
+ })
16
34
  }
@@ -4,7 +4,10 @@ import { createWheel, type WheelOptions } from '@tremolo-ui/dom'
4
4
 
5
5
  import { useCallbackRef } from './_internal/useCallbackRef'
6
6
 
7
- export interface UseWheelOptions extends WheelOptions {
7
+ // `onWheel` is left out: `createWheel` takes it so that `update()` can replace
8
+ // the callback, but this hook takes the handler as its first argument and the
9
+ // option would be silently ignored.
10
+ export interface UseWheelOptions extends Omit<WheelOptions, 'onWheel'> {
8
11
  /**
9
12
  * Listen on this element rather than on the one the returned ref callback is
10
13
  * attached to, and ignore that callback.
package/src/index.ts CHANGED
@@ -1,31 +1,35 @@
1
- // For bundled CSS (@tremolo-ui/react/styles/index.css)
2
- import './styles/global.css'
3
- import './components/Knob/index.css'
4
- import './components/NumberInput/index.css'
5
- import './components/Piano/index.css'
6
- import './components/PointsEditor/index.css'
7
- import './components/Slider/index.css'
8
- import './components/XYPad/index.css'
9
-
10
1
  export {
11
2
  AnimationCanvas,
12
- type AbsoluteSizingProps,
3
+ type AnimationCanvasCommonProps,
4
+ type AnimationCanvasFixedProps,
13
5
  type AnimationCanvasProps,
14
- type CommonProps,
6
+ type AnimationCanvasResizableProps,
15
7
  type DrawFunction,
16
8
  type InitFunction,
17
- type RelativeSizingProps,
18
9
  } from './components/AnimationCanvas'
19
- export { Knob, type KnobProps, type KnobMethods } from './components/Knob'
10
+ export { DropZone, type DropZoneProps } from './components/DropZone'
11
+ export {
12
+ FileInput,
13
+ useFileInputContext,
14
+ type FileInputContextValue,
15
+ type FileInputProps,
16
+ type FileInputTriggerProps,
17
+ } from './components/FileInput'
18
+ export {
19
+ Knob,
20
+ useKnobContext,
21
+ type KnobContextValue,
22
+ type KnobProps,
23
+ type KnobMethods,
24
+ type KnobSVGRootProps,
25
+ type KnobThumbProps,
26
+ } from './components/Knob'
20
27
  export {
21
28
  NumberInput,
22
29
  useNumberInputContext,
30
+ type NumberInputContextValue,
23
31
  type NumberInputProps,
24
32
  type NumberInputMethods,
25
- type NumberInputFieldProps,
26
- type StepperProps,
27
- type IncrementStepperProps,
28
- type DecrementStepperProps,
29
33
  } from './components/NumberInput'
30
34
  export {
31
35
  Piano,
@@ -34,7 +38,6 @@ export {
34
38
  type PianoMethods,
35
39
  type KeyState,
36
40
  type KeyAttributes,
37
- type CSSVariables,
38
41
  type KeyboardShortcuts,
39
42
  } from './components/Piano'
40
43
  export {
@@ -43,16 +46,16 @@ export {
43
46
  clampPoint,
44
47
  type PointsEditorProps,
45
48
  type PointsEditorContextValue,
46
- type PointsEditorBackgroundProps,
47
- type PointsEditorContainerProps,
48
- type PointProps,
49
+ type PointsEditorPointProps,
49
50
  type PointBaseType,
50
51
  } from './components/PointsEditor'
51
52
  export {
52
53
  Slider,
53
54
  useSliderContext,
54
- type MarksOptionProps,
55
- type MarksProps,
55
+ type SliderContextValue,
56
+ type SliderMarksOptionProps,
57
+ type MarksOptions,
58
+ type SliderMarksProps,
56
59
  type SliderMethods,
57
60
  type SliderProps,
58
61
  type SliderThumbMethods,
@@ -61,6 +64,8 @@ export {
61
64
  } from './components/Slider'
62
65
  export {
63
66
  XYPad,
67
+ useXYPadContext,
68
+ type XYPadContextValue,
64
69
  type XYPadProps,
65
70
  type XYPadMethods,
66
71
  type XYPadAreaProps,
@@ -70,10 +75,13 @@ export {
70
75
  type XYInput,
71
76
  } from './components/XYPad'
72
77
 
78
+ export { type CSSVariables } from './css-variables'
79
+
73
80
  // hooks
74
81
  export { useAnimationFrame } from './hooks/useAnimationFrame'
75
- export { useDrag } from './hooks/useDrag'
82
+ export { useDrag, type UseDragOptions } from './hooks/useDrag'
76
83
  export { useDragValue, type UseDragValueOptions } from './hooks/useDragValue'
84
+ export { useDropZone, type UseDropZoneOptions } from './hooks/useDropZone'
77
85
  export { useEventListener } from './hooks/useEventListener'
78
86
  export { useInterval } from './hooks/useInterval'
79
87
  export { useLongPress } from './hooks/useLongPress'
@@ -81,8 +89,20 @@ export {
81
89
  useMIDIAccess,
82
90
  NOT_SUPPORTED,
83
91
  PERMISSION_DENIED,
92
+ UNAVAILABLE,
84
93
  type MIDIAccessError,
94
+ type MIDIAccessOptions,
85
95
  } from './hooks/useMIDIAccess'
86
- export { useMIDIInput } from './hooks/useMIDIInput'
96
+ export {
97
+ useMIDIInput,
98
+ PITCH_BEND_CENTER,
99
+ type MIDIInputHandlers,
100
+ } from './hooks/useMIDIInput'
87
101
  export { useMIDIMessage } from './hooks/useMIDIMessage'
88
102
  export { useWheel, type UseWheelOptions } from './hooks/useWheel'
103
+
104
+ export {
105
+ DEFAULT_DRAG_SENSITIVITY,
106
+ DEFAULT_KEYBOARD_OPTIONS,
107
+ DEFAULT_WHEEL_OPTIONS,
108
+ } from './input-event'
@@ -0,0 +1,32 @@
1
+ import { type InputEventOption, type ModifierValue } from '@tremolo-ui/dom'
2
+
3
+ /**
4
+ * The keyboard amount used by Knob, NumberInput, Slider, and XYPad by default:
5
+ * 1 per press in the units of the value, and 0.1 with shift held. That is one
6
+ * `step` only while `step` is 1; a coarser `step` rounds 1 straight back, which
7
+ * `useCheckSteps` warns about.
8
+ *
9
+ * A modifier entry is not snapped to `step`, which is what lets the finer
10
+ * amount move at all — see `applyDelta` in `@tremolo-ui/dom`.
11
+ */
12
+ export const DEFAULT_KEYBOARD_OPTIONS: ModifierValue<InputEventOption> = {
13
+ default: ['raw', 1],
14
+ shift: ['raw', 0.1],
15
+ }
16
+
17
+ /**
18
+ * The wheel amount used by Knob, NumberInput, Slider, and XYPad by default.
19
+ *
20
+ * Browsers turn shift+wheel into horizontal scrolling, which empties `deltaY`
21
+ * and fills `deltaX`, so no modifier is bound here.
22
+ */
23
+ export const DEFAULT_WHEEL_OPTIONS: ModifierValue<InputEventOption> = ['raw', 1]
24
+
25
+ /**
26
+ * The drag sensitivity used by value controls by default. Shift makes the
27
+ * same movement cover a tenth of the range, matching the arrow keys.
28
+ */
29
+ export const DEFAULT_DRAG_SENSITIVITY: ModifierValue<number> = {
30
+ default: 1,
31
+ shift: 0.1,
32
+ }