@zag-js/number-input 1.41.1 → 2.0.0-next.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.
@@ -1,18 +1,18 @@
1
1
  // src/number-input.dom.ts
2
- import { isSafari, MAX_Z_INDEX } from "@zag-js/dom-query";
3
2
  import { roundToDpr, wrap } from "@zag-js/utils";
4
- var getRootId = (ctx) => ctx.ids?.root ?? `number-input:${ctx.id}`;
5
- var getInputId = (ctx) => ctx.ids?.input ?? `number-input:${ctx.id}:input`;
6
- var getIncrementTriggerId = (ctx) => ctx.ids?.incrementTrigger ?? `number-input:${ctx.id}:inc`;
7
- var getDecrementTriggerId = (ctx) => ctx.ids?.decrementTrigger ?? `number-input:${ctx.id}:dec`;
8
- var getScrubberId = (ctx) => ctx.ids?.scrubber ?? `number-input:${ctx.id}:scrubber`;
9
- var getCursorId = (ctx) => `number-input:${ctx.id}:cursor`;
10
- var getLabelId = (ctx) => ctx.ids?.label ?? `number-input:${ctx.id}:label`;
11
- var getInputEl = (ctx) => ctx.getById(getInputId(ctx));
12
- var getIncrementTriggerEl = (ctx) => ctx.getById(getIncrementTriggerId(ctx));
13
- var getDecrementTriggerEl = (ctx) => ctx.getById(getDecrementTriggerId(ctx));
14
- var getScrubberEl = (ctx) => ctx.getById(getScrubberId(ctx));
15
- var getCursorEl = (ctx) => ctx.getDoc().getElementById(getCursorId(ctx));
3
+ import { parts } from "./number-input.anatomy.mjs";
4
+ var getRootId = (ctx) => ctx.ids?.root ?? `${ctx.id}`;
5
+ var getInputId = (ctx) => ctx.ids?.input ?? `${ctx.id}:input`;
6
+ var getIncrementTriggerId = (ctx) => ctx.ids?.incrementTrigger ?? `${ctx.id}:inc`;
7
+ var getDecrementTriggerId = (ctx) => ctx.ids?.decrementTrigger ?? `${ctx.id}:dec`;
8
+ var getScrubberId = (ctx) => ctx.ids?.scrubber ?? `${ctx.id}:scrubber`;
9
+ var getScrubberCursorId = (ctx) => ctx.ids?.scrubberCursor ?? `${ctx.id}:scrubber-cursor`;
10
+ var getLabelId = (ctx) => ctx.ids?.label ?? `${ctx.id}:label`;
11
+ var getInputEl = (ctx) => ctx.query(ctx.selector(parts.input));
12
+ var getIncrementTriggerEl = (ctx) => ctx.query(ctx.selector(parts.incrementTrigger));
13
+ var getDecrementTriggerEl = (ctx) => ctx.query(ctx.selector(parts.decrementTrigger));
14
+ var getScrubberEl = (ctx) => ctx.query(ctx.selector(parts.scrubber));
15
+ var getScrubberCursorEl = (ctx) => ctx.query(ctx.selector(parts.scrubberCursor));
16
16
  var getPressedTriggerEl = (ctx, hint) => {
17
17
  let btnEl = null;
18
18
  if (hint === "increment") {
@@ -23,20 +23,13 @@ var getPressedTriggerEl = (ctx, hint) => {
23
23
  }
24
24
  return btnEl;
25
25
  };
26
- var setupVirtualCursor = (ctx, point) => {
27
- if (isSafari()) return;
28
- createVirtualCursor(ctx, point);
29
- return () => {
30
- getCursorEl(ctx)?.remove();
31
- };
32
- };
33
- var preventTextSelection = (ctx) => {
26
+ var preventTextSelection = (ctx, direction) => {
34
27
  const doc = ctx.getDoc();
35
28
  const html = doc.documentElement;
36
29
  const body = doc.body;
37
30
  body.style.pointerEvents = "none";
38
31
  html.style.userSelect = "none";
39
- html.style.cursor = "ew-resize";
32
+ html.style.cursor = direction === "vertical" ? "ns-resize" : "ew-resize";
40
33
  return () => {
41
34
  body.style.pointerEvents = "";
42
35
  html.style.userSelect = "";
@@ -50,48 +43,52 @@ var preventTextSelection = (ctx) => {
50
43
  };
51
44
  };
52
45
  var getMousemoveValue = (ctx, opts) => {
53
- const { point, isRtl, event } = opts;
46
+ const { point, isRtl, event, direction, teleportDistance } = opts;
54
47
  const win = ctx.getWin();
55
48
  const x = roundToDpr(event.movementX, win.devicePixelRatio);
56
49
  const y = roundToDpr(event.movementY, win.devicePixelRatio);
57
- let hint = x > 0 ? "increment" : x < 0 ? "decrement" : null;
58
- if (isRtl && hint === "increment") hint = "decrement";
59
- if (isRtl && hint === "decrement") hint = "increment";
50
+ const isVertical = direction === "vertical";
51
+ const delta = isVertical ? y : x;
52
+ let hint;
53
+ if (isVertical) {
54
+ hint = delta < 0 ? "increment" : delta > 0 ? "decrement" : null;
55
+ } else {
56
+ hint = delta > 0 ? "increment" : delta < 0 ? "decrement" : null;
57
+ }
58
+ if (!isVertical && isRtl) {
59
+ if (hint === "increment") hint = "decrement";
60
+ else if (hint === "decrement") hint = "increment";
61
+ }
60
62
  const newPoint = { x: point.x + x, y: point.y + y };
61
- const width = win.innerWidth;
62
63
  const half = roundToDpr(7.5, win.devicePixelRatio);
63
- newPoint.x = wrap(newPoint.x + half, width) - half;
64
- return { hint, point: newPoint };
65
- };
66
- var createVirtualCursor = (ctx, point) => {
67
- const doc = ctx.getDoc();
68
- const el = doc.createElement("div");
69
- el.className = "scrubber--cursor";
70
- el.id = getCursorId(ctx);
71
- Object.assign(el.style, {
72
- width: "15px",
73
- height: "15px",
74
- position: "fixed",
75
- pointerEvents: "none",
76
- left: "0px",
77
- top: "0px",
78
- zIndex: MAX_Z_INDEX,
79
- transform: point ? `translate3d(${point.x}px, ${point.y}px, 0px)` : void 0,
80
- willChange: "transform"
81
- });
82
- el.innerHTML = `
83
- <svg width="46" height="15" style="left: -15.5px; position: absolute; top: 0; filter: drop-shadow(rgba(0, 0, 0, 0.4) 0px 1px 1.1px);">
84
- <g transform="translate(2 3)">
85
- <path fill-rule="evenodd" d="M 15 4.5L 15 2L 11.5 5.5L 15 9L 15 6.5L 31 6.5L 31 9L 34.5 5.5L 31 2L 31 4.5Z" style="stroke-width: 2px; stroke: white;"></path>
86
- <path fill-rule="evenodd" d="M 15 4.5L 15 2L 11.5 5.5L 15 9L 15 6.5L 31 6.5L 31 9L 34.5 5.5L 31 2L 31 4.5Z"></path>
87
- </g>
88
- </svg>`;
89
- doc.body.appendChild(el);
64
+ if (teleportDistance != null) {
65
+ const scrubberEl = getScrubberEl(ctx);
66
+ if (scrubberEl) {
67
+ const rect = scrubberEl.getBoundingClientRect();
68
+ if (isVertical) {
69
+ const center = rect.top + rect.height / 2;
70
+ const min = center - teleportDistance / 2;
71
+ const max = center + teleportDistance / 2;
72
+ newPoint.y = wrap(newPoint.y + half - min, max - min) + min - half;
73
+ } else {
74
+ const center = rect.left + rect.width / 2;
75
+ const min = center - teleportDistance / 2;
76
+ const max = center + teleportDistance / 2;
77
+ newPoint.x = wrap(newPoint.x + half - min, max - min) + min - half;
78
+ }
79
+ }
80
+ } else {
81
+ if (isVertical) {
82
+ const height = win.innerHeight;
83
+ newPoint.y = wrap(newPoint.y + half, height) - half;
84
+ } else {
85
+ const width = win.innerWidth;
86
+ newPoint.x = wrap(newPoint.x + half, width) - half;
87
+ }
88
+ }
89
+ return { hint, point: newPoint, delta };
90
90
  };
91
91
  export {
92
- createVirtualCursor,
93
- getCursorEl,
94
- getCursorId,
95
92
  getDecrementTriggerEl,
96
93
  getDecrementTriggerId,
97
94
  getIncrementTriggerEl,
@@ -102,8 +99,9 @@ export {
102
99
  getMousemoveValue,
103
100
  getPressedTriggerEl,
104
101
  getRootId,
102
+ getScrubberCursorEl,
103
+ getScrubberCursorId,
105
104
  getScrubberEl,
106
105
  getScrubberId,
107
- preventTextSelection,
108
- setupVirtualCursor
106
+ preventTextSelection
109
107
  };
@@ -54,9 +54,14 @@ var machine = createMachine({
54
54
  pattern: "-?[0-9]*(.[0-9]+)?",
55
55
  defaultValue: "",
56
56
  step,
57
+ largeStep: props.largeStep ?? step * 10,
58
+ smallStep: props.smallStep ?? step * 0.1,
57
59
  min: Number.MIN_SAFE_INTEGER,
58
60
  max: Number.MAX_SAFE_INTEGER,
59
61
  spinOnPress: true,
62
+ scrubberPixelSensitivity: 2,
63
+ scrubberDirection: "horizontal",
64
+ snapOnStep: false,
60
65
  ...props,
61
66
  translations: {
62
67
  incrementLabel: "increment value",
@@ -86,7 +91,10 @@ var machine = createMachine({
86
91
  return value ? `x:${value.x}, y:${value.y}` : "";
87
92
  }
88
93
  })),
89
- fieldsetDisabled: bindable(() => ({ defaultValue: false }))
94
+ fieldsetDisabled: bindable(() => ({ defaultValue: false })),
95
+ cumulativeDelta: bindable(() => ({ defaultValue: 0 })),
96
+ isPointerLockSupported: bindable(() => ({ defaultValue: false })),
97
+ visualScale: bindable(() => ({ defaultValue: 1 }))
90
98
  };
91
99
  },
92
100
  computed: {
@@ -121,7 +129,7 @@ var machine = createMachine({
121
129
  action(["setVirtualCursorPosition"]);
122
130
  });
123
131
  },
124
- effects: ["trackFormControl"],
132
+ effects: ["trackFormControl", "detectPointerLock"],
125
133
  on: {
126
134
  "VALUE.SET": {
127
135
  actions: ["setRawValue"]
@@ -244,22 +252,16 @@ var machine = createMachine({
244
252
  },
245
253
  scrubbing: {
246
254
  tags: ["focus"],
247
- effects: ["activatePointerLock", "trackMousemove", "setupVirtualCursor", "preventTextSelection"],
255
+ effects: ["activatePointerLock", "trackMousemove", "preventTextSelection", "trackVisualViewport"],
256
+ entry: ["clearCumulativeDelta"],
248
257
  on: {
249
258
  "SCRUBBER.POINTER_UP": {
250
259
  target: "focused",
251
- actions: ["focusInput", "clearCursorPoint"]
260
+ actions: ["focusInput", "clearCursorPoint", "clearCumulativeDelta"]
252
261
  },
253
- "SCRUBBER.POINTER_MOVE": [
254
- {
255
- guard: "isIncrementHint",
256
- actions: ["increment", "setCursorPoint"]
257
- },
258
- {
259
- guard: "isDecrementHint",
260
- actions: ["decrement", "setCursorPoint"]
261
- }
262
- ]
262
+ "SCRUBBER.POINTER_MOVE": {
263
+ actions: ["accumulateDelta", "setCursorPoint"]
264
+ }
263
265
  }
264
266
  }
265
267
  },
@@ -286,6 +288,9 @@ var machine = createMachine({
286
288
  }, 50);
287
289
  return () => clearInterval(id);
288
290
  },
291
+ detectPointerLock({ context }) {
292
+ context.set("isPointerLockSupported", !(0, import_dom_query.isSafari)());
293
+ },
289
294
  trackFormControl({ context, scope }) {
290
295
  const inputEl = dom.getInputEl(scope);
291
296
  return (0, import_dom_query.trackFormControl)(inputEl, {
@@ -297,12 +302,8 @@ var machine = createMachine({
297
302
  }
298
303
  });
299
304
  },
300
- setupVirtualCursor({ context, scope }) {
301
- const point = context.get("scrubberCursorPoint");
302
- return dom.setupVirtualCursor(scope, point);
303
- },
304
- preventTextSelection({ scope }) {
305
- return dom.preventTextSelection(scope);
305
+ preventTextSelection({ scope, prop }) {
306
+ return dom.preventTextSelection(scope, prop("scrubberDirection"));
306
307
  },
307
308
  trackButtonDisabled({ context, scope, send }) {
308
309
  const hint = context.get("hint");
@@ -328,21 +329,39 @@ var machine = createMachine({
328
329
  }
329
330
  return (0, import_dom_query.addDomEvent)(inputEl, "wheel", onWheel, { passive: false });
330
331
  },
332
+ trackVisualViewport({ scope, context }) {
333
+ const vV = scope.getWin().visualViewport;
334
+ if (!vV) return;
335
+ function onResize() {
336
+ context.set("visualScale", vV.scale);
337
+ }
338
+ onResize();
339
+ return (0, import_dom_query.addDomEvent)(vV, "resize", onResize);
340
+ },
331
341
  activatePointerLock({ scope }) {
332
342
  if ((0, import_dom_query.isSafari)()) return;
333
- return (0, import_dom_query.requestPointerLock)(scope.getDoc());
343
+ const cleanup = (0, import_dom_query.requestPointerLock)(scope.getDoc());
344
+ return () => {
345
+ if ((0, import_dom_query.isFirefox)()) {
346
+ setTimeout(() => cleanup?.(), 20);
347
+ } else {
348
+ cleanup?.();
349
+ }
350
+ };
334
351
  },
335
- trackMousemove({ scope, send, context, computed }) {
352
+ trackMousemove({ scope, send, context, computed, prop }) {
336
353
  const doc = scope.getDoc();
337
354
  function onMousemove(event) {
338
355
  const point = context.get("scrubberCursorPoint");
339
356
  const isRtl = computed("isRtl");
340
- const value = dom.getMousemoveValue(scope, { point, isRtl, event });
341
- if (!value.hint) return;
357
+ const direction = prop("scrubberDirection");
358
+ const teleportDistance = prop("scrubberTeleportDistance");
359
+ const value = dom.getMousemoveValue(scope, { point, isRtl, event, direction, teleportDistance });
342
360
  send({
343
361
  type: "SCRUBBER.POINTER_MOVE",
344
362
  hint: value.hint,
345
- point: value.point
363
+ point: value.point,
364
+ delta: value.delta
346
365
  });
347
366
  }
348
367
  function onMouseup() {
@@ -361,11 +380,15 @@ var machine = createMachine({
361
380
  increment({ context, event, prop, computed }) {
362
381
  let nextValue = (0, import_utils.incrementValue)(computed("valueAsNumber"), event.step ?? prop("step"));
363
382
  if (!prop("allowOverflow")) nextValue = (0, import_utils.clampValue)(nextValue, prop("min"), prop("max"));
383
+ if (prop("snapOnStep"))
384
+ nextValue = (0, import_utils.snapValueToStep)(nextValue, prop("min"), prop("max"), event.step ?? prop("step"));
364
385
  context.set("value", (0, import_number_input.formatValue)(nextValue, { computed, prop }));
365
386
  },
366
387
  decrement({ context, event, prop, computed }) {
367
388
  let nextValue = (0, import_utils.decrementValue)(computed("valueAsNumber"), event.step ?? prop("step"));
368
389
  if (!prop("allowOverflow")) nextValue = (0, import_utils.clampValue)(nextValue, prop("min"), prop("max"));
390
+ if (prop("snapOnStep"))
391
+ nextValue = (0, import_utils.snapValueToStep)(nextValue, prop("min"), prop("max"), event.step ?? prop("step"));
369
392
  context.set("value", (0, import_number_input.formatValue)(nextValue, { computed, prop }));
370
393
  },
371
394
  setClampedValue({ context, prop, computed }) {
@@ -447,10 +470,31 @@ var machine = createMachine({
447
470
  context.set("scrubberCursorPoint", null);
448
471
  },
449
472
  setVirtualCursorPosition({ context, scope }) {
450
- const cursorEl = dom.getCursorEl(scope);
473
+ const scrubberEl = dom.getScrubberEl(scope);
451
474
  const point = context.get("scrubberCursorPoint");
452
- if (!cursorEl || !point) return;
453
- cursorEl.style.transform = `translate3d(${point.x}px, ${point.y}px, 0px)`;
475
+ if (!scrubberEl || !point) return;
476
+ scrubberEl.style.setProperty("--scrubber-x", `${point.x}px`);
477
+ scrubberEl.style.setProperty("--scrubber-y", `${point.y}px`);
478
+ },
479
+ accumulateDelta({ context, event, prop, send }) {
480
+ const delta = event.delta ?? 0;
481
+ const newDelta = context.get("cumulativeDelta") + delta;
482
+ const sensitivity = prop("scrubberPixelSensitivity");
483
+ if (Math.abs(newDelta) >= sensitivity) {
484
+ const step = prop("step");
485
+ const hint = event.hint;
486
+ if (hint === "increment") {
487
+ send({ type: "VALUE.INCREMENT", step });
488
+ } else if (hint === "decrement") {
489
+ send({ type: "VALUE.DECREMENT", step });
490
+ }
491
+ context.set("cumulativeDelta", newDelta % sensitivity);
492
+ } else {
493
+ context.set("cumulativeDelta", newDelta);
494
+ }
495
+ },
496
+ clearCumulativeDelta({ context }) {
497
+ context.set("cumulativeDelta", 0);
454
498
  }
455
499
  }
456
500
  }
@@ -2,6 +2,7 @@
2
2
  import { memo, setup } from "@zag-js/core";
3
3
  import {
4
4
  addDomEvent,
5
+ isFirefox,
5
6
  isSafari,
6
7
  observeAttributes,
7
8
  raf,
@@ -16,7 +17,8 @@ import {
16
17
  incrementValue,
17
18
  isValueAtMax,
18
19
  isValueAtMin,
19
- isValueWithinRange
20
+ isValueWithinRange,
21
+ snapValueToStep
20
22
  } from "@zag-js/utils";
21
23
  import { recordCursor, restoreCursor } from "./cursor.mjs";
22
24
  import * as dom from "./number-input.dom.mjs";
@@ -36,9 +38,14 @@ var machine = createMachine({
36
38
  pattern: "-?[0-9]*(.[0-9]+)?",
37
39
  defaultValue: "",
38
40
  step,
41
+ largeStep: props.largeStep ?? step * 10,
42
+ smallStep: props.smallStep ?? step * 0.1,
39
43
  min: Number.MIN_SAFE_INTEGER,
40
44
  max: Number.MAX_SAFE_INTEGER,
41
45
  spinOnPress: true,
46
+ scrubberPixelSensitivity: 2,
47
+ scrubberDirection: "horizontal",
48
+ snapOnStep: false,
42
49
  ...props,
43
50
  translations: {
44
51
  incrementLabel: "increment value",
@@ -68,7 +75,10 @@ var machine = createMachine({
68
75
  return value ? `x:${value.x}, y:${value.y}` : "";
69
76
  }
70
77
  })),
71
- fieldsetDisabled: bindable(() => ({ defaultValue: false }))
78
+ fieldsetDisabled: bindable(() => ({ defaultValue: false })),
79
+ cumulativeDelta: bindable(() => ({ defaultValue: 0 })),
80
+ isPointerLockSupported: bindable(() => ({ defaultValue: false })),
81
+ visualScale: bindable(() => ({ defaultValue: 1 }))
72
82
  };
73
83
  },
74
84
  computed: {
@@ -103,7 +113,7 @@ var machine = createMachine({
103
113
  action(["setVirtualCursorPosition"]);
104
114
  });
105
115
  },
106
- effects: ["trackFormControl"],
116
+ effects: ["trackFormControl", "detectPointerLock"],
107
117
  on: {
108
118
  "VALUE.SET": {
109
119
  actions: ["setRawValue"]
@@ -226,22 +236,16 @@ var machine = createMachine({
226
236
  },
227
237
  scrubbing: {
228
238
  tags: ["focus"],
229
- effects: ["activatePointerLock", "trackMousemove", "setupVirtualCursor", "preventTextSelection"],
239
+ effects: ["activatePointerLock", "trackMousemove", "preventTextSelection", "trackVisualViewport"],
240
+ entry: ["clearCumulativeDelta"],
230
241
  on: {
231
242
  "SCRUBBER.POINTER_UP": {
232
243
  target: "focused",
233
- actions: ["focusInput", "clearCursorPoint"]
244
+ actions: ["focusInput", "clearCursorPoint", "clearCumulativeDelta"]
234
245
  },
235
- "SCRUBBER.POINTER_MOVE": [
236
- {
237
- guard: "isIncrementHint",
238
- actions: ["increment", "setCursorPoint"]
239
- },
240
- {
241
- guard: "isDecrementHint",
242
- actions: ["decrement", "setCursorPoint"]
243
- }
244
- ]
246
+ "SCRUBBER.POINTER_MOVE": {
247
+ actions: ["accumulateDelta", "setCursorPoint"]
248
+ }
245
249
  }
246
250
  }
247
251
  },
@@ -268,6 +272,9 @@ var machine = createMachine({
268
272
  }, 50);
269
273
  return () => clearInterval(id);
270
274
  },
275
+ detectPointerLock({ context }) {
276
+ context.set("isPointerLockSupported", !isSafari());
277
+ },
271
278
  trackFormControl({ context, scope }) {
272
279
  const inputEl = dom.getInputEl(scope);
273
280
  return trackFormControl(inputEl, {
@@ -279,12 +286,8 @@ var machine = createMachine({
279
286
  }
280
287
  });
281
288
  },
282
- setupVirtualCursor({ context, scope }) {
283
- const point = context.get("scrubberCursorPoint");
284
- return dom.setupVirtualCursor(scope, point);
285
- },
286
- preventTextSelection({ scope }) {
287
- return dom.preventTextSelection(scope);
289
+ preventTextSelection({ scope, prop }) {
290
+ return dom.preventTextSelection(scope, prop("scrubberDirection"));
288
291
  },
289
292
  trackButtonDisabled({ context, scope, send }) {
290
293
  const hint = context.get("hint");
@@ -310,21 +313,39 @@ var machine = createMachine({
310
313
  }
311
314
  return addDomEvent(inputEl, "wheel", onWheel, { passive: false });
312
315
  },
316
+ trackVisualViewport({ scope, context }) {
317
+ const vV = scope.getWin().visualViewport;
318
+ if (!vV) return;
319
+ function onResize() {
320
+ context.set("visualScale", vV.scale);
321
+ }
322
+ onResize();
323
+ return addDomEvent(vV, "resize", onResize);
324
+ },
313
325
  activatePointerLock({ scope }) {
314
326
  if (isSafari()) return;
315
- return requestPointerLock(scope.getDoc());
327
+ const cleanup = requestPointerLock(scope.getDoc());
328
+ return () => {
329
+ if (isFirefox()) {
330
+ setTimeout(() => cleanup?.(), 20);
331
+ } else {
332
+ cleanup?.();
333
+ }
334
+ };
316
335
  },
317
- trackMousemove({ scope, send, context, computed }) {
336
+ trackMousemove({ scope, send, context, computed, prop }) {
318
337
  const doc = scope.getDoc();
319
338
  function onMousemove(event) {
320
339
  const point = context.get("scrubberCursorPoint");
321
340
  const isRtl = computed("isRtl");
322
- const value = dom.getMousemoveValue(scope, { point, isRtl, event });
323
- if (!value.hint) return;
341
+ const direction = prop("scrubberDirection");
342
+ const teleportDistance = prop("scrubberTeleportDistance");
343
+ const value = dom.getMousemoveValue(scope, { point, isRtl, event, direction, teleportDistance });
324
344
  send({
325
345
  type: "SCRUBBER.POINTER_MOVE",
326
346
  hint: value.hint,
327
- point: value.point
347
+ point: value.point,
348
+ delta: value.delta
328
349
  });
329
350
  }
330
351
  function onMouseup() {
@@ -343,11 +364,15 @@ var machine = createMachine({
343
364
  increment({ context, event, prop, computed }) {
344
365
  let nextValue = incrementValue(computed("valueAsNumber"), event.step ?? prop("step"));
345
366
  if (!prop("allowOverflow")) nextValue = clampValue(nextValue, prop("min"), prop("max"));
367
+ if (prop("snapOnStep"))
368
+ nextValue = snapValueToStep(nextValue, prop("min"), prop("max"), event.step ?? prop("step"));
346
369
  context.set("value", formatValue(nextValue, { computed, prop }));
347
370
  },
348
371
  decrement({ context, event, prop, computed }) {
349
372
  let nextValue = decrementValue(computed("valueAsNumber"), event.step ?? prop("step"));
350
373
  if (!prop("allowOverflow")) nextValue = clampValue(nextValue, prop("min"), prop("max"));
374
+ if (prop("snapOnStep"))
375
+ nextValue = snapValueToStep(nextValue, prop("min"), prop("max"), event.step ?? prop("step"));
351
376
  context.set("value", formatValue(nextValue, { computed, prop }));
352
377
  },
353
378
  setClampedValue({ context, prop, computed }) {
@@ -429,10 +454,31 @@ var machine = createMachine({
429
454
  context.set("scrubberCursorPoint", null);
430
455
  },
431
456
  setVirtualCursorPosition({ context, scope }) {
432
- const cursorEl = dom.getCursorEl(scope);
457
+ const scrubberEl = dom.getScrubberEl(scope);
433
458
  const point = context.get("scrubberCursorPoint");
434
- if (!cursorEl || !point) return;
435
- cursorEl.style.transform = `translate3d(${point.x}px, ${point.y}px, 0px)`;
459
+ if (!scrubberEl || !point) return;
460
+ scrubberEl.style.setProperty("--scrubber-x", `${point.x}px`);
461
+ scrubberEl.style.setProperty("--scrubber-y", `${point.y}px`);
462
+ },
463
+ accumulateDelta({ context, event, prop, send }) {
464
+ const delta = event.delta ?? 0;
465
+ const newDelta = context.get("cumulativeDelta") + delta;
466
+ const sensitivity = prop("scrubberPixelSensitivity");
467
+ if (Math.abs(newDelta) >= sensitivity) {
468
+ const step = prop("step");
469
+ const hint = event.hint;
470
+ if (hint === "increment") {
471
+ send({ type: "VALUE.INCREMENT", step });
472
+ } else if (hint === "decrement") {
473
+ send({ type: "VALUE.DECREMENT", step });
474
+ }
475
+ context.set("cumulativeDelta", newDelta % sensitivity);
476
+ } else {
477
+ context.set("cumulativeDelta", newDelta);
478
+ }
479
+ },
480
+ clearCumulativeDelta({ context }) {
481
+ context.set("cumulativeDelta", 0);
436
482
  }
437
483
  }
438
484
  }
@@ -40,6 +40,7 @@ var props = (0, import_types.createProps)()([
40
40
  "ids",
41
41
  "inputMode",
42
42
  "invalid",
43
+ "largeStep",
43
44
  "locale",
44
45
  "max",
45
46
  "min",
@@ -51,6 +52,11 @@ var props = (0, import_types.createProps)()([
51
52
  "pattern",
52
53
  "required",
53
54
  "readOnly",
55
+ "scrubberDirection",
56
+ "scrubberPixelSensitivity",
57
+ "scrubberTeleportDistance",
58
+ "smallStep",
59
+ "snapOnStep",
54
60
  "spinOnPress",
55
61
  "step",
56
62
  "translations",
@@ -15,6 +15,7 @@ var props = createProps()([
15
15
  "ids",
16
16
  "inputMode",
17
17
  "invalid",
18
+ "largeStep",
18
19
  "locale",
19
20
  "max",
20
21
  "min",
@@ -26,6 +27,11 @@ var props = createProps()([
26
27
  "pattern",
27
28
  "required",
28
29
  "readOnly",
30
+ "scrubberDirection",
31
+ "scrubberPixelSensitivity",
32
+ "scrubberTeleportDistance",
33
+ "smallStep",
34
+ "snapOnStep",
29
35
  "spinOnPress",
30
36
  "step",
31
37
  "translations",
@@ -21,6 +21,7 @@ type ElementIds = Partial<{
21
21
  incrementTrigger: string;
22
22
  decrementTrigger: string;
23
23
  scrubber: string;
24
+ scrubberCursor: string;
24
25
  }>;
25
26
  interface IntlTranslations {
26
27
  /**
@@ -150,8 +151,39 @@ interface NumberInputProps extends LocaleProperties, CommonProperties {
150
151
  * @default true
151
152
  */
152
153
  spinOnPress?: boolean | undefined;
154
+ /**
155
+ * Step value when Shift key is held (large increments).
156
+ * @default step * 10
157
+ */
158
+ largeStep?: number | undefined;
159
+ /**
160
+ * Step value when Alt/Option key is held (fine control).
161
+ * @default step * 0.1
162
+ */
163
+ smallStep?: number | undefined;
164
+ /**
165
+ * How many pixels the pointer must move to change the value by one step.
166
+ * Higher values mean more precise control. Lower values mean more sensitivity.
167
+ * @default 2
168
+ */
169
+ scrubberPixelSensitivity?: number | undefined;
170
+ /**
171
+ * Distance in pixels before the virtual cursor wraps around.
172
+ * When not set, the viewport bounds are used (default behavior).
173
+ */
174
+ scrubberTeleportDistance?: number | undefined;
175
+ /**
176
+ * The direction of scrubbing.
177
+ * @default "horizontal"
178
+ */
179
+ scrubberDirection?: "horizontal" | "vertical" | undefined;
180
+ /**
181
+ * Whether values snap to step multiples when scrubbing.
182
+ * @default false
183
+ */
184
+ snapOnStep?: boolean | undefined;
153
185
  }
154
- type PropsWithDefault = "dir" | "locale" | "focusInputOnChange" | "clampValueOnBlur" | "allowOverflow" | "inputMode" | "pattern" | "translations" | "step" | "spinOnPress" | "min" | "max" | "step" | "translations";
186
+ type PropsWithDefault = "dir" | "locale" | "focusInputOnChange" | "clampValueOnBlur" | "allowOverflow" | "inputMode" | "pattern" | "translations" | "step" | "spinOnPress" | "min" | "max" | "largeStep" | "smallStep" | "scrubberPixelSensitivity" | "scrubberDirection" | "snapOnStep";
155
187
  type ComputedContext = Readonly<{
156
188
  /**
157
189
  * The value of the input as a number
@@ -227,6 +259,18 @@ interface PrivateContext {
227
259
  * The value of the input
228
260
  */
229
261
  value: string;
262
+ /**
263
+ * The accumulated delta for pixel sensitivity tracking
264
+ */
265
+ cumulativeDelta: number;
266
+ /**
267
+ * Whether the browser supports pointer lock (false on Safari, false during SSR)
268
+ */
269
+ isPointerLockSupported: boolean;
270
+ /**
271
+ * The visual viewport scale (for pinch-zoom compensation on the cursor)
272
+ */
273
+ visualScale: number;
230
274
  }
231
275
  interface NumberInputSchema {
232
276
  state: "idle" | "focused" | "spinning" | "before:spin" | "scrubbing";
@@ -246,6 +290,10 @@ interface NumberInputApi<T extends PropTypes = PropTypes> {
246
290
  * Whether the input is focused.
247
291
  */
248
292
  focused: boolean;
293
+ /**
294
+ * Whether the input is being scrubbed.
295
+ */
296
+ scrubbing: boolean;
249
297
  /**
250
298
  * Whether the input is invalid.
251
299
  */
@@ -298,6 +346,7 @@ interface NumberInputApi<T extends PropTypes = PropTypes> {
298
346
  getDecrementTriggerProps: () => T["button"];
299
347
  getIncrementTriggerProps: () => T["button"];
300
348
  getScrubberProps: () => T["element"];
349
+ getScrubberCursorProps: () => T["element"];
301
350
  }
302
351
 
303
352
  export type { ElementIds, FocusChangeDetails, HintValue, InputMode, IntlTranslations, NumberInputApi, NumberInputMachine, NumberInputProps, NumberInputSchema, NumberInputService, ValidityState, ValueChangeDetails, ValueInvalidDetails };