@tremolo-ui/react 0.5.0 → 0.6.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 (76) 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 +1468 -534
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1103 -435
  12. package/dist/index.d.cts.map +1 -1
  13. package/dist/index.d.ts +1103 -435
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1457 -534
  16. package/dist/index.js.map +1 -1
  17. package/package.json +28 -26
  18. package/src/components/AnimationCanvas/index.tsx +115 -76
  19. package/src/components/Knob/ActiveLine.tsx +7 -6
  20. package/src/components/Knob/InactiveLine.tsx +9 -10
  21. package/src/components/Knob/SVGRoot.tsx +5 -3
  22. package/src/components/Knob/Thumb.tsx +33 -11
  23. package/src/components/Knob/context.tsx +17 -1
  24. package/src/components/Knob/index.tsx +148 -55
  25. package/src/components/NumberInput/DecrementStepper.tsx +8 -4
  26. package/src/components/NumberInput/IncrementStepper.tsx +9 -4
  27. package/src/components/NumberInput/InputField.tsx +188 -105
  28. package/src/components/NumberInput/Stepper.tsx +123 -72
  29. package/src/components/NumberInput/{stepperButton.tsx → StepperButton.tsx} +27 -22
  30. package/src/components/NumberInput/context.tsx +35 -8
  31. package/src/components/NumberInput/index.tsx +272 -75
  32. package/src/components/Piano/index.tsx +354 -188
  33. package/src/components/PointsEditor/Background.tsx +11 -2
  34. package/src/components/PointsEditor/Container.tsx +87 -27
  35. package/src/components/PointsEditor/Point.tsx +239 -79
  36. package/src/components/PointsEditor/SelectionBox.tsx +54 -0
  37. package/src/components/PointsEditor/context.tsx +78 -10
  38. package/src/components/PointsEditor/index.tsx +440 -39
  39. package/src/components/Slider/Marks.tsx +48 -16
  40. package/src/components/Slider/MarksOption.tsx +57 -43
  41. package/src/components/Slider/Thumb.tsx +108 -50
  42. package/src/components/Slider/Track.tsx +69 -58
  43. package/src/components/Slider/context.tsx +4 -2
  44. package/src/components/Slider/index.tsx +131 -65
  45. package/src/components/Slider/type.ts +26 -8
  46. package/src/components/XYPad/Area.tsx +45 -35
  47. package/src/components/XYPad/Thumb.tsx +135 -62
  48. package/src/components/XYPad/context.tsx +4 -1
  49. package/src/components/XYPad/index.tsx +180 -48
  50. package/src/components/_util/Placement.tsx +68 -0
  51. package/src/components/_util/VisuallyHiddenRangeInput.tsx +24 -0
  52. package/src/components/_util/css-length.ts +13 -0
  53. package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
  54. package/src/hooks/_internal/useCallbackRef.ts +3 -1
  55. package/src/hooks/_internal/useCheckSteps.ts +170 -0
  56. package/src/hooks/useAnimationFrame.ts +17 -7
  57. package/src/hooks/useDrag.ts +38 -8
  58. package/src/hooks/useDragValue.ts +54 -9
  59. package/src/hooks/useEventListener.ts +10 -6
  60. package/src/hooks/useLongPress.ts +42 -8
  61. package/src/hooks/useMIDIAccess.ts +82 -11
  62. package/src/hooks/useMIDIInput.ts +45 -13
  63. package/src/hooks/useMIDIMessage.ts +23 -5
  64. package/src/index.ts +38 -21
  65. package/src/input-event.ts +32 -0
  66. package/dist/index.css +0 -501
  67. package/src/Globals.d.ts +0 -3
  68. package/src/components/Knob/index.css +0 -81
  69. package/src/components/NumberInput/index.css +0 -121
  70. package/src/components/Piano/index.css +0 -90
  71. package/src/components/PointsEditor/index.css +0 -35
  72. package/src/components/Slider/index.css +0 -125
  73. package/src/components/XYPad/index.css +0 -60
  74. package/src/components/_util/index.ts +0 -28
  75. package/src/styles/global.css +0 -19
  76. /package/src/components/Piano/{keyboardShortcuts.ts → keyboard-shortcuts.ts} +0 -0
@@ -1,4 +1,3 @@
1
- import clsx from 'clsx'
2
1
  import React, {
3
2
  ComponentPropsWithoutRef,
4
3
  CSSProperties,
@@ -15,15 +14,22 @@ import {
15
14
  applyDelta,
16
15
  linearScale,
17
16
  toFixed,
18
- InputEventOption,
17
+ type InputEventOption,
18
+ type ModifierValue,
19
+ selectModifier,
19
20
  xor,
20
21
  type Scale,
21
22
  } from '@tremolo-ui/functions'
22
23
 
24
+ import { useComposedRefs } from '../../compose-refs'
25
+ import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
23
26
  import { useDragValue } from '../../hooks/useDragValue'
24
27
  import { useWheel } from '../../hooks/useWheel'
25
- import { addUserSelectNone, Cursor, removeUserSelectNone } from '../_util'
26
- import { useComposedRefs } from '../_util/composeRefs'
28
+ import {
29
+ DEFAULT_DRAG_SENSITIVITY,
30
+ DEFAULT_KEYBOARD_OPTIONS,
31
+ DEFAULT_WHEEL_OPTIONS,
32
+ } from '../../input-event'
27
33
 
28
34
  import { SliderProvider } from './context'
29
35
  import { Marks } from './Marks'
@@ -32,17 +38,23 @@ import { Thumb, SliderThumbMethods } from './Thumb'
32
38
  import { Track } from './Track'
33
39
 
34
40
  const defaultExternalStyles: SliderProps['externalStyles'] = {
35
- userSelectNone: true,
36
41
  cursor: 'pointer',
37
42
  }
38
43
 
39
44
  export interface SliderProps {
40
- // required
45
+ /** The current value. The slider shows only this, so update it from `onChange`. */
41
46
  value: number
47
+ /** The value at the start of the travel. */
42
48
  min: number
49
+ /** The value at the end of the travel. */
43
50
  max: number
44
51
 
45
- // optional
52
+ /**
53
+ * Granularity of the value. A drag, the wheel and the arrow keys snap it to
54
+ * multiples of `step`.
55
+ *
56
+ * @default 1
57
+ */
46
58
  step?: number
47
59
  /**
48
60
  * How the value is distributed across the travel.
@@ -55,43 +67,93 @@ export interface SliderProps {
55
67
  */
56
68
  scale?: Scale
57
69
  /**
58
- * slider orientation
59
- * aria-orientation property is also applied.
70
+ * Run the slider vertically, with the value growing upwards. The range input
71
+ * inside the thumb takes its `aria-orientation` from this.
72
+ *
73
+ * @default false
60
74
  */
61
75
  vertical?: boolean
76
+ /**
77
+ * Grow the value the other way: leftwards, or downwards when `vertical`.
78
+ * The arrow keys follow the direction on screen.
79
+ *
80
+ * @default false
81
+ */
62
82
  reverse?: boolean
63
83
 
64
- /** Global style to apply when dragged */
84
+ /**
85
+ * The cursor to show while dragging. It is set on the dragged element, so it
86
+ * stays while the pointer is outside the slider.
87
+ *
88
+ * @default { cursor: 'pointer' }
89
+ */
65
90
  externalStyles?: {
66
- userSelectNone?: boolean
67
- cursor?: Cursor
91
+ cursor?: CSSProperties['cursor']
68
92
  }
69
93
  /**
70
- * wheel control option
71
- * If null, no event will be triggered
94
+ * How much one notch of the wheel moves the value. It only acts while the
95
+ * focus is inside, so that scrolling the page past the slider leaves it
96
+ * alone.
97
+ *
98
+ * `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
99
+ * range between `min` and `max`. The result is snapped to `step`, except for
100
+ * an amount set on a modifier key (`{ default: …, shift: … }`). `null` turns
101
+ * the wheel off.
102
+ *
103
+ * @default ['raw', 1]
72
104
  */
73
- wheel?: InputEventOption | null
105
+ wheel?: ModifierValue<InputEventOption> | null
74
106
  /**
75
- * keyboard control option
76
- * If null, no event will be triggered
107
+ * How much a drag moves the value, per modifier key.
108
+ *
109
+ * `1` is the pointer position itself, which is what a drag normally is here.
110
+ * **Anything else turns the drag relative**: `0.1` makes the same movement
111
+ * cover a tenth of the travel, so the value stops following the pointer and
112
+ * starts moving a tenth as fast. Shift is bound to `0.1` by default, to
113
+ * match what it does on the arrow keys.
114
+ *
115
+ * Pressing or releasing the key mid-drag does not disturb the value: the
116
+ * travel so far is kept and the new sensitivity applies from there. **The
117
+ * pointer and the value stay apart for the rest of the drag** — snapping
118
+ * them back together on release would move the value nobody asked to move.
119
+ *
120
+ * @default { default: 1, shift: 0.1 }
77
121
  */
78
- keyboard?: InputEventOption | null
122
+ dragSensitivity?: ModifierValue<number>
79
123
 
80
124
  /**
81
- * Only the appearance will change.
82
- * Please consider using with readonly.
83
- * aria-disabled property is also applied.
125
+ * How much one arrow key press moves the value.
126
+ *
127
+ * `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
128
+ * range between `min` and `max`. The result is snapped to `step`, except for
129
+ * an amount set on a modifier key, which is what lets shift move off the
130
+ * grid. `null` turns the arrow keys off.
131
+ *
132
+ * The default moves by 1, and by 0.1 with shift. With a `step` above 1, raise
133
+ * the amount to match: 1 would round straight back to where it started, and
134
+ * a development build warns about it.
135
+ *
136
+ * @default { default: ['raw', 1], shift: ['raw', 0.1] }
137
+ */
138
+ keyboard?: ModifierValue<InputEventOption> | null
139
+
140
+ /**
141
+ * Make the slider unchangeable and remove it from the tab order.
142
+ * The parts carry `data-disabled` while it is set.
84
143
  */
85
144
  disabled?: boolean
86
145
  /**
87
146
  * Make the value unchangeable.
88
- * aria-readonly property is also applied.
147
+ * The parts carry `data-readonly` while it is set.
89
148
  */
90
149
  readonly?: boolean
91
150
  className?: string
92
151
  style?: CSSProperties
152
+ /** Called with the new value when a drag, the wheel or an arrow key moves it. */
93
153
  onChange?: (value: number) => void
154
+ /** Called when a drag starts, with the value where the track was pressed. */
94
155
  onDragStart?: (value: number) => void
156
+ /** Called when the drag ends, with the value it ended on. */
95
157
  onDragEnd?: (value: number) => void
96
158
  /**
97
159
  * The slider renders exactly what you compose here; there is no default
@@ -100,7 +162,7 @@ export interface SliderProps {
100
162
  * @example
101
163
  * <Slider.Root value={value} min={0} max={100} onChange={setValue}>
102
164
  * <Slider.Track>
103
- * <Slider.Thumb />
165
+ * <Slider.Thumb aria-label="Level" />
104
166
  * </Slider.Track>
105
167
  * </Slider.Root>
106
168
  */
@@ -121,7 +183,7 @@ type Props = SliderProps &
121
183
  */
122
184
  const WHEEL_OPTIONS = { requireFocus: true }
123
185
 
124
- export const Root = forwardRef<SliderMethods, Props>(
186
+ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
125
187
  (
126
188
  {
127
189
  value,
@@ -132,8 +194,9 @@ export const Root = forwardRef<SliderMethods, Props>(
132
194
  vertical = false,
133
195
  reverse = false,
134
196
  externalStyles: _externalStyles,
135
- wheel = ['raw', 1],
136
- keyboard = ['raw', 1],
197
+ wheel = DEFAULT_WHEEL_OPTIONS,
198
+ keyboard = DEFAULT_KEYBOARD_OPTIONS,
199
+ dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
137
200
  disabled = false,
138
201
  readonly = false,
139
202
  onChange,
@@ -153,9 +216,9 @@ export const Root = forwardRef<SliderMethods, Props>(
153
216
  // -- state and ref ---
154
217
  const trackRef = useRef<HTMLDivElement>(null)
155
218
  const thumbRef = useRef<SliderThumbMethods>(null)
156
-
157
219
  // --- interpret props ---
158
220
  const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
221
+ const inactive = disabled || readonly
159
222
 
160
223
  const p = toFixed(scale.normalize(value, min, max) * 100)
161
224
  const rev = toFixed(100 - p)
@@ -179,18 +242,20 @@ export const Root = forwardRef<SliderMethods, Props>(
179
242
  [min, max, step, scale, displayReversed],
180
243
  )
181
244
 
245
+ useCheckSteps({ component: 'Slider', range: axis, keyboard, wheel })
246
+
182
247
  const handleKeyDown = useCallback(
183
248
  (event: React.KeyboardEvent<HTMLDivElement>) => {
184
- if (!keyboard || !onChange || readonly) return
185
249
  const key = event.key
186
- if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
187
- event.preventDefault()
188
- let direction = key == 'ArrowRight' || key == 'ArrowUp' ? 1 : -1
189
- if (reverse) direction *= -1
190
- onChange(applyDelta(value, direction, keyboard, axis))
191
- }
250
+ if (!['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key))
251
+ return
252
+ event.preventDefault()
253
+ if (!keyboard || !onChange || inactive) return
254
+ let direction = key === 'ArrowRight' || key === 'ArrowUp' ? 1 : -1
255
+ if (reverse) direction *= -1
256
+ onChange(applyDelta(value, direction, keyboard, axis, event))
192
257
  },
193
- [keyboard, onChange, readonly, reverse, value, axis],
258
+ [keyboard, onChange, inactive, reverse, value, axis],
194
259
  )
195
260
 
196
261
  // --- hooks ---
@@ -199,40 +264,38 @@ export const Root = forwardRef<SliderMethods, Props>(
199
264
  const { refCallback: dragRefCallback } = useDragValue<HTMLDivElement>({
200
265
  axis,
201
266
  baseElementRef: trackRef,
202
- cursor: readonly ? undefined : externalStyles.cursor,
267
+ cursor: inactive ? undefined : externalStyles.cursor,
268
+ shouldStart: () => !inactive,
269
+ sensitivity: (state) =>
270
+ selectModifier(dragSensitivity, state.event).value,
203
271
  updateOnPointerDown: true,
204
272
  onChange: (v) => {
205
- if (readonly) return
273
+ if (inactive) return
206
274
  onChange?.(valueOf(v))
207
275
  },
208
276
  onDragStart: (v) => {
209
- if (readonly) return
210
- if (externalStyles.userSelectNone) addUserSelectNone()
211
-
277
+ if (inactive) return
212
278
  thumbRef.current?.focus()
213
279
  onDragStart?.(valueOf(v))
214
280
  },
215
281
  onDragEnd: (v) => {
216
- if (readonly) return
217
-
218
- if (externalStyles.userSelectNone) removeUserSelectNone()
219
-
282
+ if (inactive) return
220
283
  onDragEnd?.(valueOf(v))
221
284
  },
222
285
  })
223
286
 
224
287
  const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
225
- if (!wheel || !onChange || readonly) return
288
+ if (!wheel || !onChange || inactive) return
226
289
  event.preventDefault()
227
290
  let direction
228
- if (!vertical && event.deltaX != 0) {
291
+ if (!vertical && event.deltaX !== 0) {
229
292
  direction = event.deltaX > 0 ? 1 : -1
230
293
  } else {
231
- if (event.deltaY == 0) return
294
+ if (event.deltaY === 0) return
232
295
  direction = event.deltaY > 0 ? -1 : 1
233
296
  }
234
- if (vertical && reverse) direction *= -1
235
- onChange(applyDelta(value, direction, wheel, axis))
297
+ if (reverse) direction *= -1
298
+ onChange(applyDelta(value, direction, wheel, axis, event))
236
299
  }, WHEEL_OPTIONS)
237
300
 
238
301
  // Composed once, so React attaches the refs a single time instead of
@@ -253,6 +316,7 @@ export const Root = forwardRef<SliderMethods, Props>(
253
316
  reverse,
254
317
  disabled,
255
318
  readonly,
319
+ onChange,
256
320
  percent,
257
321
  trackRef,
258
322
  thumbRef,
@@ -267,6 +331,7 @@ export const Root = forwardRef<SliderMethods, Props>(
267
331
  reverse,
268
332
  disabled,
269
333
  readonly,
334
+ onChange,
270
335
  percent,
271
336
  ],
272
337
  )
@@ -274,28 +339,29 @@ export const Root = forwardRef<SliderMethods, Props>(
274
339
  useImperativeHandle(forwardedRef, () => {
275
340
  return {
276
341
  focus() {
277
- thumbRef.current?.focus()
342
+ if (!disabled) thumbRef.current?.focus()
278
343
  },
279
344
  blur() {
280
345
  thumbRef.current?.blur()
281
346
  },
282
347
  }
283
- }, [])
348
+ }, [disabled])
284
349
 
285
350
  return (
286
351
  <SliderProvider value={context}>
352
+ {/* oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the group is the pointer and keyboard event area, while the nested range input carries its control semantics */}
287
353
  <div
288
- className={clsx('tremolo-slider', className)}
354
+ className={className}
289
355
  ref={rootRefCallback}
356
+ role="group"
357
+ // A press lands on the track or the thumb, neither of which can hold
358
+ // focus, and the browser answers that by clearing the focus to the
359
+ // body — undoing the focus the drag just gave the input. Taking the
360
+ // focus here keeps it inside, and `onFocus` passes it to the input.
290
361
  tabIndex={-1}
291
- role="slider"
292
- aria-valuenow={value}
293
- aria-valuemin={min}
294
- aria-valuemax={max}
295
- aria-orientation={vertical ? 'vertical' : 'horizontal'}
296
- aria-disabled={disabled}
297
- aria-readonly={readonly}
298
- data-vertical={vertical}
362
+ data-orientation={vertical ? 'vertical' : 'horizontal'}
363
+ data-disabled={disabled ? '' : undefined}
364
+ data-readonly={readonly ? '' : undefined}
299
365
  style={style}
300
366
  onPointerDown={onPointerDown}
301
367
  onKeyDown={(event) => {
@@ -303,7 +369,7 @@ export const Root = forwardRef<SliderMethods, Props>(
303
369
  onKeyDown?.(event)
304
370
  }}
305
371
  onFocus={(event) => {
306
- thumbRef.current?.focus()
372
+ if (!disabled) thumbRef.current?.focus()
307
373
  onFocus?.(event)
308
374
  }}
309
375
  onBlur={(event) => {
@@ -330,9 +396,9 @@ export const Slider = {
330
396
  MarksOption,
331
397
  }
332
398
 
333
- export { useSliderContext } from './context'
399
+ export { useSliderContext, type SliderContextValue } from './context'
334
400
  export { type SliderThumbMethods, type SliderThumbProps } from './Thumb'
335
401
  export { type SliderTrackProps } from './Track'
336
- export { type MarksProps } from './Marks'
337
- export { type MarksOptionProps } from './MarksOption'
338
- export { type MarksOptions, type MarksType } from './type'
402
+ export { type SliderMarksProps } from './Marks'
403
+ export { type SliderMarksOptionProps } from './MarksOption'
404
+ export { type MarksOptions } from './type'
@@ -1,7 +1,19 @@
1
1
  import { decimalPart, toFixed } from '@tremolo-ui/functions'
2
2
 
3
- export type MarksType = 'mark' | 'mark-number' | 'number'
4
- export type MarksOptions = ['step', MarksType] | [number, MarksType]
3
+ /**
4
+ * How `Slider.Marks` fills itself in when it is given no children: one option
5
+ * every `per`, or every `step` of the slider. The object form turns off the
6
+ * mark or the label for the whole set; a single option is customized by
7
+ * writing `Slider.MarksOption` out instead.
8
+ */
9
+ export type MarksOptions =
10
+ | 'step'
11
+ | number
12
+ | {
13
+ per: 'step' | number
14
+ mark?: boolean
15
+ label?: boolean
16
+ }
5
17
 
6
18
  export function generateOptionsList(
7
19
  options: MarksOptions,
@@ -9,18 +21,24 @@ export function generateOptionsList(
9
21
  max: number,
10
22
  step: number,
11
23
  ) {
24
+ const {
25
+ per,
26
+ mark = true,
27
+ label = true,
28
+ } = typeof options === 'object' ? options : { per: options }
12
29
  const optionsList: {
13
30
  value: number
14
- type: MarksType
31
+ mark: boolean
32
+ label: boolean
15
33
  }[] = []
16
- const per = options[0] == 'step' ? step : options[0]
17
- const count = Math.floor(max / per) - Math.ceil(min / per) + 1
34
+ const interval = per === 'step' ? step : per
35
+ const count = Math.floor(max / interval) - Math.ceil(min / interval) + 1
18
36
  for (let i = 0; i < count; i++) {
19
37
  const value = toFixed(
20
- per * (Math.ceil(min / per) + i),
21
- decimalPart(per)?.length,
38
+ interval * (Math.ceil(min / interval) + i),
39
+ decimalPart(interval)?.length,
22
40
  )
23
- optionsList.push({ value: value, type: options[1] })
41
+ optionsList.push({ value: value, mark: mark, label: label })
24
42
  }
25
43
  return optionsList
26
44
  }
@@ -1,51 +1,61 @@
1
- import clsx from 'clsx'
2
- import { ComponentPropsWithoutRef, CSSProperties, ReactNode, Ref } from 'react'
1
+ import {
2
+ ComponentPropsWithoutRef,
3
+ CSSProperties,
4
+ forwardRef,
5
+ ReactNode,
6
+ } from 'react'
3
7
 
4
- import { useComposedRefs } from '../_util/composeRefs'
8
+ import { useComposedRefs } from '../../compose-refs'
9
+ import { cssLength } from '../_util/css-length'
10
+ import { Placement } from '../_util/Placement'
5
11
 
6
12
  import { useXYPadContext } from './context'
7
13
 
8
14
  export interface XYPadAreaProps {
15
+ /** Sets `--width`; the size the theme gives it stands when omitted. */
9
16
  width?: number | string
17
+ /** Sets `--height`; the size the theme gives it stands when omitted. */
10
18
  height?: number | string
19
+ /** Sets `--color`, for the theme to colour the area with. */
11
20
  color?: string
12
21
  className?: string
13
22
  style?: CSSProperties
14
23
  /** `<XYPad.Thumb />` goes here. */
15
24
  children?: ReactNode
16
- ref?: Ref<HTMLDivElement>
17
25
  }
18
26
 
19
- export function Area({
20
- width = 120,
21
- height = 120,
22
- color,
23
- children,
24
- className,
25
- style,
26
- ref,
27
- ...props
28
- }: XYPadAreaProps &
29
- Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadAreaProps>) {
30
- const { areaRef } = useXYPadContext()
27
+ type Props = XYPadAreaProps &
28
+ Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadAreaProps>
31
29
 
32
- // The area is what the pointer position is normalized against, so the
33
- // context ref is composed with any ref the caller passed.
34
- const composedRef = useComposedRefs<HTMLDivElement>(ref, areaRef)
30
+ export const Area = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
31
+ function Area(
32
+ { width, height, color, children, className, style, ...props },
33
+ forwardedRef,
34
+ ) {
35
+ const { areaRef } = useXYPadContext()
35
36
 
36
- return (
37
- <div
38
- ref={composedRef}
39
- className={clsx('tremolo-xy-pad-area', className)}
40
- style={{
41
- ...{ '--color': color },
42
- width: width,
43
- height: height,
44
- ...style,
45
- }}
46
- {...props}
47
- >
48
- {children}
49
- </div>
50
- )
51
- }
37
+ // The area is what the pointer position is normalized against, so the
38
+ // context ref is composed with any ref the caller passed.
39
+ const composedRef = useComposedRefs<HTMLDivElement>(forwardedRef, areaRef)
40
+
41
+ return (
42
+ <div
43
+ ref={composedRef}
44
+ className={className}
45
+ style={
46
+ {
47
+ '--color': color,
48
+ '--width': cssLength(width),
49
+ '--height': cssLength(height),
50
+ // The thumb inside is placed against this box.
51
+ position: 'relative',
52
+ ...style,
53
+ } as CSSProperties
54
+ }
55
+ {...props}
56
+ >
57
+ <Placement name="XYPad.Area">{children}</Placement>
58
+ </div>
59
+ )
60
+ },
61
+ )