@tremolo-ui/react 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/index.cjs +416 -252
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +256 -110
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +256 -110
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +368 -203
  8. package/dist/index.js.map +1 -1
  9. package/package.json +9 -9
  10. package/src/components/AnimationCanvas/index.tsx +4 -18
  11. package/src/components/FileInput/index.tsx +4 -1
  12. package/src/components/Knob/Thumb.tsx +19 -19
  13. package/src/components/Knob/index.tsx +85 -41
  14. package/src/components/NumberInput/InputField.tsx +9 -7
  15. package/src/components/NumberInput/Stepper.tsx +30 -6
  16. package/src/components/NumberInput/StepperButton.tsx +12 -4
  17. package/src/components/NumberInput/context.tsx +24 -2
  18. package/src/components/NumberInput/index.tsx +142 -27
  19. package/src/components/Piano/index.tsx +24 -7
  20. package/src/components/PointsEditor/Container.tsx +1 -1
  21. package/src/components/PointsEditor/Point.tsx +61 -30
  22. package/src/components/PointsEditor/context.tsx +3 -4
  23. package/src/components/PointsEditor/index.tsx +14 -15
  24. package/src/components/Slider/Marks.tsx +3 -2
  25. package/src/components/Slider/MarksOption.tsx +3 -2
  26. package/src/components/Slider/Thumb.tsx +20 -11
  27. package/src/components/Slider/Track.tsx +15 -13
  28. package/src/components/Slider/context.tsx +3 -3
  29. package/src/components/Slider/index.tsx +85 -48
  30. package/src/components/XYPad/Thumb.tsx +18 -8
  31. package/src/components/XYPad/context.tsx +1 -1
  32. package/src/components/XYPad/index.tsx +74 -42
  33. package/src/components/_util/VisuallyHiddenRangeInput.tsx +1 -1
  34. package/src/hooks/_internal/useChangeGesture.ts +93 -0
  35. package/src/hooks/_internal/useCheckSteps.ts +1 -1
  36. package/src/hooks/useAnimationFrame.ts +37 -15
  37. package/src/hooks/useEventListener.ts +93 -22
  38. package/src/hooks/useWheel.ts +4 -4
  39. package/src/index.ts +9 -5
  40. package/src/hooks/useInterval.ts +0 -27
@@ -9,16 +9,15 @@ import {
9
9
 
10
10
  export type PointsEditorContextValue = {
11
11
  disabled: boolean
12
- readonly: boolean
12
+ readOnly: boolean
13
13
  /** Inherited by every `Point`; `null` turns the wheel off. */
14
14
  wheel: ModifierValue<InputEventOption> | null
15
15
  /** Inherited by every `Point`; `null` turns the keyboard off. */
16
16
  keyboard: ModifierValue<InputEventOption> | null
17
17
  /** Inherited by every `Point`. See `PointsEditorProps.dragSensitivity`. */
18
18
  dragSensitivity: ModifierValue<number>
19
- externalStyles: {
20
- cursor?: CSSProperties['cursor']
21
- }
19
+ /** Inherited by every `Point`. See `PointsEditorProps.dragCursor`. */
20
+ dragCursor: CSSProperties['cursor']
22
21
  /**
23
22
  * `Container` registers its element here; `Point` normalizes the pointer
24
23
  * against it, so a point is placed by its position within the container.
@@ -14,10 +14,12 @@ import {
14
14
  DEFAULT_DRAG_SENSITIVITY,
15
15
  type InputEventOption,
16
16
  type ModifierValue,
17
- POINTS_EDITOR_DEFAULT_KEYBOARD,
18
- POINTS_EDITOR_DEFAULT_WHEEL,
19
17
  type SelectionBoxRect,
20
18
  } from '@tremolo-ui/dom'
19
+ import {
20
+ POINTS_EDITOR_DEFAULT_KEYBOARD,
21
+ POINTS_EDITOR_DEFAULT_WHEEL,
22
+ } from '@tremolo-ui/dom/internal'
21
23
 
22
24
  import { Background } from './Background'
23
25
  import { Container } from './Container'
@@ -38,17 +40,15 @@ export interface PointsEditorProps {
38
40
  * Make the points unmovable.
39
41
  * The parts carry `data-readonly` while it is set.
40
42
  */
41
- readonly?: boolean
43
+ readOnly?: boolean
42
44
 
43
45
  /**
44
46
  * The cursor to show while dragging a point. It is set on the dragged
45
47
  * point, so it stays while the pointer is outside it.
46
48
  *
47
- * @default { cursor: 'grabbing' }
49
+ * @default 'grabbing'
48
50
  */
49
- externalStyles?: {
50
- cursor?: CSSProperties['cursor']
51
- }
51
+ dragCursor?: CSSProperties['cursor']
52
52
 
53
53
  /**
54
54
  * How much one notch of the wheel moves the focused `Point`. Scrolling
@@ -155,7 +155,7 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
155
155
  (
156
156
  {
157
157
  disabled = false,
158
- readonly = false,
158
+ readOnly = false,
159
159
  wheel = POINTS_EDITOR_DEFAULT_WHEEL,
160
160
  keyboard = POINTS_EDITOR_DEFAULT_KEYBOARD,
161
161
  dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
@@ -163,7 +163,7 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
163
163
  selection: selectionProp,
164
164
  defaultSelection,
165
165
  onSelectionChange,
166
- externalStyles,
166
+ dragCursor = 'grabbing',
167
167
  style,
168
168
  className,
169
169
  children,
@@ -174,7 +174,6 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
174
174
  const containerRef = useRef<HTMLDivElement>(null)
175
175
  // Picked apart so that the memo below depends on values rather than on the
176
176
  // object literal a caller writes inline, which is new on every render.
177
- const { cursor = 'grabbing' } = externalStyles ?? {}
178
177
 
179
178
  // --- selection ---
180
179
  // The selection lives here, where React state can hold it; which points a
@@ -214,11 +213,11 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
214
213
  const context = useMemo(
215
214
  () => ({
216
215
  disabled,
217
- readonly,
216
+ readOnly,
218
217
  wheel,
219
218
  keyboard,
220
219
  dragSensitivity,
221
- externalStyles: { cursor },
220
+ dragCursor,
222
221
  containerRef,
223
222
  selectable,
224
223
  selection,
@@ -227,11 +226,11 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
227
226
  }),
228
227
  [
229
228
  disabled,
230
- readonly,
229
+ readOnly,
231
230
  wheel,
232
231
  keyboard,
233
232
  dragSensitivity,
234
- cursor,
233
+ dragCursor,
235
234
  selectable,
236
235
  selection,
237
236
  editor,
@@ -245,7 +244,7 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
245
244
  ref={forwardedRef}
246
245
  className={className}
247
246
  data-disabled={disabled ? '' : undefined}
248
- data-readonly={readonly ? '' : undefined}
247
+ data-readonly={readOnly ? '' : undefined}
249
248
  style={
250
249
  {
251
250
  // The layers inside are placed against this box.
@@ -1,6 +1,7 @@
1
1
  import { ComponentPropsWithoutRef, CSSProperties } from 'react'
2
2
 
3
- import { cssLength, type MarksOptions, sliderMarks } from '@tremolo-ui/dom'
3
+ import { type MarksOptions } from '@tremolo-ui/dom'
4
+ import { cssLength, sliderMarks } from '@tremolo-ui/dom/internal'
4
5
 
5
6
  import { Placement } from '../_util/Placement'
6
7
 
@@ -40,7 +41,7 @@ export function Marks({
40
41
  const min = useSliderContext((s) => s.min)
41
42
  const max = useSliderContext((s) => s.max)
42
43
  const step = useSliderContext((s) => s.step)
43
- const vertical = useSliderContext((s) => s.vertical)
44
+ const vertical = useSliderContext((s) => s.orientation === 'vertical')
44
45
  const reverse = useSliderContext((s) => s.reverse)
45
46
 
46
47
  const optionsList = options ? sliderMarks(options, min, max, step) : []
@@ -1,6 +1,7 @@
1
1
  import { ComponentPropsWithoutRef, CSSProperties } from 'react'
2
2
 
3
- import { cssLength, valuePercent } from '@tremolo-ui/dom'
3
+ import { valuePercent } from '@tremolo-ui/dom'
4
+ import { cssLength } from '@tremolo-ui/dom/internal'
4
5
 
5
6
  import { useCheckPlacement } from '../_util/Placement'
6
7
 
@@ -62,7 +63,7 @@ export function MarksOption({
62
63
  const min = useSliderContext((s) => s.min)
63
64
  const max = useSliderContext((s) => s.max)
64
65
  const scale = useSliderContext((s) => s.scale)
65
- const vertical = useSliderContext((s) => s.vertical)
66
+ const vertical = useSliderContext((s) => s.orientation === 'vertical')
66
67
  const reverse = useSliderContext((s) => s.reverse)
67
68
 
68
69
  // The marks have to sit on the same curve the thumb runs along.
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  ComponentPropsWithoutRef,
3
+ CSSProperties,
3
4
  forwardRef,
5
+ Ref,
4
6
  useImperativeHandle,
5
7
  useRef,
6
- CSSProperties,
7
8
  } from 'react'
8
9
 
9
10
  import { useCheckPlacement } from '../_util/Placement'
@@ -22,6 +23,12 @@ export interface SliderThumbProps {
22
23
  color?: string
23
24
 
24
25
  style?: CSSProperties & CSSVariables<'color' | 'translate'>
26
+
27
+ /**
28
+ * Receives `focus` and `blur` for the range input inside, which do nothing
29
+ * while the slider is disabled. `ref` reaches the thumb element itself.
30
+ */
31
+ actionsRef?: Ref<SliderThumbMethods>
25
32
  }
26
33
 
27
34
  export interface SliderThumbMethods {
@@ -32,7 +39,7 @@ export interface SliderThumbMethods {
32
39
  type Props = SliderThumbProps &
33
40
  Omit<ComponentPropsWithoutRef<'div'>, keyof SliderThumbProps>
34
41
 
35
- export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
42
+ export const Thumb = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
36
43
  function Thumb(
37
44
  {
38
45
  color,
@@ -48,6 +55,7 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
48
55
  'aria-labelledby': ariaLabelledby,
49
56
  'aria-describedby': ariaDescribedby,
50
57
  'aria-valuetext': ariaValuetext,
58
+ actionsRef,
51
59
  ...props
52
60
  },
53
61
  forwardedRef,
@@ -58,9 +66,9 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
58
66
  min,
59
67
  max,
60
68
  step,
61
- vertical,
69
+ orientation,
62
70
  disabled,
63
- readonly,
71
+ readOnly,
64
72
  onChange,
65
73
  percent,
66
74
  thumbRef,
@@ -78,15 +86,16 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
78
86
  },
79
87
  })
80
88
 
81
- useImperativeHandle(forwardedRef, methods, [disabled])
89
+ useImperativeHandle(actionsRef, methods, [disabled])
82
90
  // Root focuses the thumb when a drag starts, wherever the user placed it.
83
91
  useImperativeHandle(thumbRef, methods, [disabled])
84
92
 
85
93
  return (
86
94
  <div
95
+ ref={forwardedRef}
87
96
  className={className}
88
97
  data-disabled={disabled ? '' : undefined}
89
- data-readonly={readonly ? '' : undefined}
98
+ data-readonly={readOnly ? '' : undefined}
90
99
  {...props}
91
100
  style={{
92
101
  ...{ '--color': color },
@@ -101,8 +110,8 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
101
110
  // Where the thumb sits is the component's decision, not a style: a
102
111
  // `left` from the caller would take it off the track, so it is
103
112
  // written after theirs.
104
- top: vertical ? `${percent}%` : '50%',
105
- left: !vertical ? `${percent}%` : '50%',
113
+ top: orientation === 'vertical' ? `${percent}%` : '50%',
114
+ left: orientation === 'vertical' ? '50%' : `${percent}%`,
106
115
  }}
107
116
  >
108
117
  <VisuallyHiddenRangeInput
@@ -112,14 +121,14 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
112
121
  max={max}
113
122
  step={step}
114
123
  disabled={disabled}
115
- aria-readonly={readonly}
116
- aria-orientation={vertical ? 'vertical' : 'horizontal'}
124
+ aria-readonly={readOnly}
125
+ aria-orientation={orientation}
117
126
  aria-label={ariaLabel}
118
127
  aria-labelledby={ariaLabelledby}
119
128
  aria-describedby={ariaDescribedby}
120
129
  aria-valuetext={ariaValuetext}
121
130
  onChange={(event) => {
122
- if (readonly) {
131
+ if (readOnly) {
123
132
  event.currentTarget.value = String(value)
124
133
  return
125
134
  }
@@ -1,6 +1,6 @@
1
1
  import { ComponentPropsWithoutRef, CSSProperties, forwardRef } from 'react'
2
2
 
3
- import { cssLength } from '@tremolo-ui/dom'
3
+ import { cssLength } from '@tremolo-ui/dom/internal'
4
4
 
5
5
  import { useComposedRefs } from '../../compose-refs'
6
6
  import { Placement } from '../_util/Placement'
@@ -18,13 +18,15 @@ export interface SliderTrackProps {
18
18
  /** How thick the track is across that axis. Sets `--thickness`. */
19
19
  thickness?: number | string
20
20
 
21
- /** Colour of the part from `min` to the value. Sets `--active`. */
22
- active?: string
23
- /** Colour of the rest of the track. Sets `--inactive`. */
24
- inactive?: string
21
+ /** Colour of the part from `min` to the value. Sets `--active-color`. */
22
+ activeColor?: string
23
+ /** Colour of the rest of the track. Sets `--inactive-color`. */
24
+ inactiveColor?: string
25
25
 
26
26
  style?: CSSProperties &
27
- CSSVariables<'length' | 'thickness' | 'active' | 'inactive' | 'percent'>
27
+ CSSVariables<
28
+ 'length' | 'thickness' | 'active-color' | 'inactive-color' | 'percent'
29
+ >
28
30
  }
29
31
 
30
32
  type Props = SliderTrackProps &
@@ -35,8 +37,8 @@ export const Track = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
35
37
  {
36
38
  length,
37
39
  thickness,
38
- active,
39
- inactive,
40
+ activeColor,
41
+ inactiveColor,
40
42
  children,
41
43
  className,
42
44
  style,
@@ -44,7 +46,7 @@ export const Track = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
44
46
  },
45
47
  forwardedRef,
46
48
  ) {
47
- const { vertical, reverse, disabled, percent, trackRef } =
49
+ const { orientation, reverse, disabled, percent, trackRef } =
48
50
  useSliderContext()
49
51
 
50
52
  // The track is what the pointer position is normalized against, so the
@@ -56,14 +58,14 @@ export const Track = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
56
58
  ref={composedRef}
57
59
  className={className}
58
60
  data-disabled={disabled ? '' : undefined}
59
- data-orientation={vertical ? 'vertical' : 'horizontal'}
61
+ data-orientation={orientation}
60
62
  // Which end the value grows from. `percent` is already the position on
61
63
  // screen, so this only says which side of it is the filled one.
62
- data-flipped={vertical !== reverse ? '' : undefined}
64
+ data-flipped={(orientation === 'vertical') !== reverse ? '' : undefined}
63
65
  style={
64
66
  {
65
- '--active': active,
66
- '--inactive': inactive,
67
+ '--active-color': activeColor,
68
+ '--inactive-color': inactiveColor,
67
69
  '--length': cssLength(length),
68
70
  '--thickness': cssLength(thickness),
69
71
  // Where the value sits, for the theme to paint the fill with. The
@@ -11,14 +11,14 @@ export type SliderContextValue = {
11
11
  step: number
12
12
  /** How the value is distributed across the travel. */
13
13
  scale: Scale
14
- vertical: boolean
14
+ orientation: 'horizontal' | 'vertical'
15
15
  reverse: boolean
16
16
  disabled: boolean
17
- readonly: boolean
17
+ readOnly: boolean
18
18
  onChange?: (value: number) => void
19
19
  /**
20
20
  * Position of the thumb, 0-100, already accounting for the display direction
21
- * implied by `vertical` and `reverse`.
21
+ * implied by `orientation` and `reverse`.
22
22
  */
23
23
  percent: number
24
24
 
@@ -3,6 +3,7 @@ import React, {
3
3
  CSSProperties,
4
4
  forwardRef,
5
5
  ReactNode,
6
+ Ref,
6
7
  useCallback,
7
8
  useImperativeHandle,
8
9
  useMemo,
@@ -10,22 +11,26 @@ import React, {
10
11
  } from 'react'
11
12
 
12
13
  import {
13
- applyDelta,
14
- arrowKeyDirection,
15
14
  type AxisOptions,
15
+ type ChangeSource,
16
16
  DEFAULT_DRAG_SENSITIVITY,
17
17
  DEFAULT_KEYBOARD_OPTIONS,
18
18
  DEFAULT_WHEEL_OPTIONS,
19
19
  type InputEventOption,
20
20
  type ModifierValue,
21
- selectModifier,
22
21
  valuePercent,
23
- wheelDirection,
24
22
  type XY,
25
23
  } from '@tremolo-ui/dom'
24
+ import {
25
+ applyDelta,
26
+ arrowKeyDirection,
27
+ selectModifier,
28
+ wheelDirection,
29
+ } from '@tremolo-ui/dom/internal'
26
30
  import { linearScale, type Scale } from '@tremolo-ui/functions'
27
31
 
28
32
  import { useComposedRefs } from '../../compose-refs'
33
+ import { useChangeGesture } from '../../hooks/_internal/useChangeGesture'
29
34
  import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
30
35
  import { useDragValue } from '../../hooks/useDragValue'
31
36
  import { useWheel } from '../../hooks/useWheel'
@@ -36,10 +41,6 @@ import { MarksOption } from './MarksOption'
36
41
  import { Thumb, SliderThumbMethods } from './Thumb'
37
42
  import { Track } from './Track'
38
43
 
39
- const defaultExternalStyles: SliderProps['externalStyles'] = {
40
- cursor: 'pointer',
41
- }
42
-
43
44
  export interface SliderProps {
44
45
  /** The current value. The slider shows only this, so update it from `onChange`. */
45
46
  value: number
@@ -66,14 +67,15 @@ export interface SliderProps {
66
67
  */
67
68
  scale?: Scale
68
69
  /**
69
- * Run the slider vertically, with the value growing upwards. The range input
70
- * inside the thumb takes its `aria-orientation` from this.
70
+ * Which way the slider runs. A vertical slider grows upwards. Every part
71
+ * carries the same value as `data-orientation`, and the range input inside
72
+ * the thumb takes its `aria-orientation` from it.
71
73
  *
72
- * @default false
74
+ * @default 'horizontal'
73
75
  */
74
- vertical?: boolean
76
+ orientation?: 'horizontal' | 'vertical'
75
77
  /**
76
- * Grow the value the other way: leftwards, or downwards when `vertical`.
78
+ * Grow the value the other way: leftwards, or downwards when vertical.
77
79
  * The arrow keys follow the direction on screen.
78
80
  *
79
81
  * @default false
@@ -84,11 +86,9 @@ export interface SliderProps {
84
86
  * The cursor to show while dragging. It is set on the dragged element, so it
85
87
  * stays while the pointer is outside the slider.
86
88
  *
87
- * @default { cursor: 'pointer' }
89
+ * @default 'pointer'
88
90
  */
89
- externalStyles?: {
90
- cursor?: CSSProperties['cursor']
91
- }
91
+ dragCursor?: CSSProperties['cursor']
92
92
  /**
93
93
  * How much one notch of the wheel moves the value. It only acts while the
94
94
  * focus is inside, so that scrolling the page past the slider leaves it
@@ -145,13 +145,28 @@ export interface SliderProps {
145
145
  * Make the value unchangeable.
146
146
  * The parts carry `data-readonly` while it is set.
147
147
  */
148
- readonly?: boolean
148
+ readOnly?: boolean
149
149
  /** Called with the new value when a drag, the wheel or an arrow key moves it. */
150
150
  onChange?: (value: number) => void
151
- /** Called when a drag starts, with the value where the track was pressed. */
152
- onDragStart?: (value: number) => void
153
- /** Called when the drag ends, with the value it ended on. */
154
- onDragEnd?: (value: number) => void
151
+ /**
152
+ * Called when a change of the value starts — a press on the track, the first wheel notch
153
+ * or arrow key — with the value
154
+ * before it and what it is made with. A host recording automation can treat
155
+ * the control as touched from here until `onChangeEnd`.
156
+ */
157
+ onChangeStart?: (value: number, source: ChangeSource) => void
158
+ /**
159
+ * Called when the change ends, with the value it ended on: on release, or
160
+ * `changeEndDelay` after the last wheel notch or arrow key.
161
+ */
162
+ onChangeEnd?: (value: number, source: ChangeSource) => void
163
+ /**
164
+ * How long after the last wheel notch or arrow key the change counts as
165
+ * over, in milliseconds. Neither has an event that says it is done.
166
+ *
167
+ * @default 500
168
+ */
169
+ changeEndDelay?: number
155
170
  /**
156
171
  * The slider renders exactly what you compose here; there is no default
157
172
  * markup to fall back to.
@@ -164,6 +179,13 @@ export interface SliderProps {
164
179
  * </Slider.Root>
165
180
  */
166
181
  children: ReactNode
182
+
183
+ /**
184
+ * Receives `focus` and `blur`, which act on the range input inside the thumb —
185
+ * the element that takes the focus — and do nothing while the slider is
186
+ * disabled. `ref` reaches the root element itself.
187
+ */
188
+ actionsRef?: Ref<SliderMethods>
167
189
  }
168
190
 
169
191
  export interface SliderMethods {
@@ -180,7 +202,7 @@ type Props = SliderProps &
180
202
  */
181
203
  const WHEEL_OPTIONS = { requireFocus: true }
182
204
 
183
- export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
205
+ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
184
206
  (
185
207
  {
186
208
  value,
@@ -188,17 +210,18 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
188
210
  max,
189
211
  step = 1,
190
212
  scale = linearScale,
191
- vertical = false,
213
+ orientation = 'horizontal',
192
214
  reverse = false,
193
- externalStyles: _externalStyles,
215
+ dragCursor = 'pointer',
194
216
  wheel = DEFAULT_WHEEL_OPTIONS,
195
217
  keyboard = DEFAULT_KEYBOARD_OPTIONS,
196
218
  dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
197
219
  disabled = false,
198
- readonly = false,
220
+ readOnly = false,
199
221
  onChange,
200
- onDragStart,
201
- onDragEnd,
222
+ onChangeStart,
223
+ onChangeEnd,
224
+ changeEndDelay,
202
225
  className,
203
226
  style,
204
227
  children,
@@ -206,6 +229,7 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
206
229
  onBlur,
207
230
  onPointerDown,
208
231
  onKeyDown,
232
+ actionsRef,
209
233
  ...props
210
234
  }: Props,
211
235
  forwardedRef,
@@ -214,14 +238,14 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
214
238
  const trackRef = useRef<HTMLDivElement>(null)
215
239
  const thumbRef = useRef<SliderThumbMethods>(null)
216
240
  // --- interpret props ---
217
- const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
218
- const inactive = disabled || readonly
241
+ const inactive = disabled || readOnly
219
242
 
220
243
  // Measured from the left or the top, as CSS places things:
221
244
  // normal -> normal (right)
222
245
  // vertical -> reversed (up)
223
246
  // reverse -> reversed (left)
224
247
  // vertical & reverse -> normal (down)
248
+ const vertical = orientation === 'vertical'
225
249
  const displayReversed = vertical !== reverse
226
250
  const percent = valuePercent(value, { min, max, scale }, displayReversed)
227
251
 
@@ -236,13 +260,27 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
236
260
 
237
261
  useCheckSteps({ component: 'Slider', range: axis, keyboard, wheel })
238
262
 
263
+ const gesture = useChangeGesture(
264
+ value,
265
+ { onChangeStart, onChangeEnd, changeEndDelay },
266
+ inactive,
267
+ )
268
+ const change = useCallback(
269
+ (next: number) => {
270
+ gesture.changed(next)
271
+ onChange?.(next)
272
+ },
273
+ [gesture, onChange],
274
+ )
275
+
239
276
  const handleKeyDown = useCallback(
240
277
  (event: React.KeyboardEvent<HTMLDivElement>) => {
241
278
  const direction = arrowKeyDirection(event.key)
242
279
  if (direction === null) return
243
280
  event.preventDefault()
244
281
  if (!keyboard || !onChange || inactive) return
245
- onChange(
282
+ gesture.pulse('keyboard')
283
+ change(
246
284
  applyDelta(
247
285
  value,
248
286
  reverse ? -direction : direction,
@@ -252,7 +290,7 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
252
290
  ),
253
291
  )
254
292
  },
255
- [keyboard, onChange, inactive, reverse, value, axis],
293
+ [keyboard, onChange, inactive, reverse, value, axis, gesture, change],
256
294
  )
257
295
 
258
296
  // --- hooks ---
@@ -261,24 +299,21 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
261
299
  const { refCallback: dragRefCallback } = useDragValue<HTMLDivElement>({
262
300
  axis,
263
301
  baseElementRef: trackRef,
264
- cursor: inactive ? undefined : externalStyles.cursor,
302
+ cursor: inactive ? undefined : dragCursor,
265
303
  shouldStart: () => !inactive,
266
304
  sensitivity: (state) =>
267
305
  selectModifier(dragSensitivity, state.event).value,
268
306
  updateOnPointerDown: true,
269
307
  onChange: (v) => {
270
308
  if (inactive) return
271
- onChange?.(valueOf(v))
309
+ change(valueOf(v))
272
310
  },
273
- onDragStart: (v) => {
311
+ onDragStart: () => {
274
312
  if (inactive) return
313
+ gesture.hold('pointer')
275
314
  thumbRef.current?.focus()
276
- onDragStart?.(valueOf(v))
277
- },
278
- onDragEnd: (v) => {
279
- if (inactive) return
280
- onDragEnd?.(valueOf(v))
281
315
  },
316
+ onDragEnd: () => gesture.end(),
282
317
  })
283
318
 
284
319
  const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
@@ -288,7 +323,8 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
288
323
  const direction = wheelDirection(event, { horizontal: !vertical })
289
324
  if (direction === null) return
290
325
  event.preventDefault()
291
- onChange(
326
+ gesture.pulse('wheel')
327
+ change(
292
328
  applyDelta(value, reverse ? -direction : direction, wheel, axis, event),
293
329
  )
294
330
  }, WHEEL_OPTIONS)
@@ -296,6 +332,7 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
296
332
  // Composed once, so React attaches the refs a single time instead of
297
333
  // detaching and re-attaching on every render.
298
334
  const rootRefCallback = useComposedRefs<HTMLDivElement>(
335
+ forwardedRef,
299
336
  dragRefCallback,
300
337
  wheelRefCallback,
301
338
  )
@@ -307,10 +344,10 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
307
344
  max,
308
345
  step,
309
346
  scale,
310
- vertical,
347
+ orientation,
311
348
  reverse,
312
349
  disabled,
313
- readonly,
350
+ readOnly,
314
351
  onChange,
315
352
  percent,
316
353
  trackRef,
@@ -322,16 +359,16 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
322
359
  max,
323
360
  step,
324
361
  scale,
325
- vertical,
362
+ orientation,
326
363
  reverse,
327
364
  disabled,
328
- readonly,
365
+ readOnly,
329
366
  onChange,
330
367
  percent,
331
368
  ],
332
369
  )
333
370
 
334
- useImperativeHandle(forwardedRef, () => {
371
+ useImperativeHandle(actionsRef, () => {
335
372
  return {
336
373
  focus() {
337
374
  if (!disabled) thumbRef.current?.focus()
@@ -354,9 +391,9 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
354
391
  // body — undoing the focus the drag just gave the input. Taking the
355
392
  // focus here keeps it inside, and `onFocus` passes it to the input.
356
393
  tabIndex={-1}
357
- data-orientation={vertical ? 'vertical' : 'horizontal'}
394
+ data-orientation={orientation}
358
395
  data-disabled={disabled ? '' : undefined}
359
- data-readonly={readonly ? '' : undefined}
396
+ data-readonly={readOnly ? '' : undefined}
360
397
  style={style}
361
398
  onPointerDown={onPointerDown}
362
399
  onKeyDown={(event) => {