@gfazioli/mantine-picker 2.3.15 → 3.0.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Mantine Picker Component
2
2
 
3
- <img alt="Mantine Picker" src="https://github.com/gfazioli/mantine-picker/blob/master/logo.png" />
3
+ <img alt="Mantine Picker" src="https://github.com/gfazioli/mantine-picker/blob/master/logo.jpeg" />
4
4
 
5
5
  <div align="center">
6
6
 
@@ -18,12 +18,24 @@
18
18
  ## Overview
19
19
 
20
20
  This component is created on top of the [Mantine](https://mantine.dev/) library.
21
-
22
- [Mantine Picker](https://gfazioli.github.io/mantine-picker/) is a versatile UI component for building iOS-like wheel pickers in React with Mantine, enabling selection through dragging, mouse wheel, clicking, and keyboard navigation. Developers can fully control state via value and onChange, or render bespoke item content with renderItem (e.g., color swatches, badges).
23
-
24
- The component’s appearance and behavior are highly tunable: itemHeight, visibleItems, cylinderRadius, rotation (rotateY), blur/opacity/scale gradients, mask and highlight, dividers, loop, and animationDuration. Decorative sections (leftSection/rightSection) let you embed icons or labels that remain fixed while the list scrolls, and readOnly mode permits programmatic updates while preventing user changes—handy for counters and clocks.
25
-
26
- Beyond simple lists like cities or months, the Picker supports composite inputs such as time and date pickers by composing multiple instances (hours, minutes, AM/PM; day, month, year) and synchronizing their values. Styling can be adapted with variants, size presets, uppercase transforms, gradients, and custom shadows, and global styles are imported once via the package’s CSS. Altogether, Mantine Picker offers a polished, customizable selection experience that integrates cleanly with Mantine layouts and components for production-ready pickers.
21
+ It requires **Mantine 9.x** and **React 19**.
22
+
23
+ [Mantine Picker](https://gfazioli.github.io/mantine-picker/) is an iOS-style wheel picker for React with Mantine, enabling selection through dragging, mouse wheel, clicking, and keyboard navigation.
24
+
25
+ ## Features
26
+
27
+ - 🎡 **iOS-style wheel picker**: Smooth drag, wheel, click, and keyboard navigation
28
+ - 🎲 **3D rotation effect**: Configurable `perspective`, `maxRotation`, `cylinderRadius`, `rotateY`
29
+ - 🔄 **Loop mode**: Infinite circular scrolling through items
30
+ - 🎯 **Momentum scrolling**: Inertia-based deceleration after drag release
31
+ - 🎨 **Custom item rendering**: `renderItem` for color swatches, badges, icons, or any React content
32
+ - 📐 **Left/Right sections**: Fixed content beside the picker (icons, labels)
33
+ - 🔒 **Read-only mode**: Programmatic updates without user interaction (counters, clocks)
34
+ - 🎭 **Visual effects**: Configurable blur, opacity, scale gradients for non-selected items
35
+ - 🖌 **Mask, highlight, dividers**: Toggle gradient mask, selection highlight, and divider lines
36
+ - ♿ **Accessible**: `aria-label`, keyboard navigation, `focusable` prop, screen reader support
37
+ - 🎨 **Styles API**: Full Mantine Styles API support with 6 selectors
38
+ - 📦 **TypeScript**: Full type safety with generic `Picker<T>` support
27
39
 
28
40
  > [!note]
29
41
  >
@@ -2,6 +2,7 @@
2
2
  'use strict';
3
3
 
4
4
  var React = require('react');
5
+ var hooks = require('@mantine/hooks');
5
6
  var core = require('@mantine/core');
6
7
  var Picker_module = require('./Picker.module.css.cjs');
7
8
 
@@ -62,8 +63,9 @@ const varsResolver = core.createVarsResolver(
62
63
  };
63
64
  }
64
65
  );
65
- const Picker = core.polymorphicFactory((_props, ref) => {
66
- const props = core.useProps("Picker", defaultProps, _props);
66
+ const Picker = core.polymorphicFactory((_props) => {
67
+ const { ref, ...restProps } = _props;
68
+ const props = core.useProps("Picker", defaultProps, restProps);
67
69
  const {
68
70
  data,
69
71
  value,
@@ -155,10 +157,10 @@ const Picker = core.polymorphicFactory((_props, ref) => {
155
157
  const prevValueRef = React.useRef(value);
156
158
  const containerRef = React.useRef(null);
157
159
  const rootRef = React.useRef(null);
160
+ const mergedRootRef = hooks.useMergedRef(ref, rootRef);
158
161
  const [isDragging, setIsDragging] = React.useState(false);
159
- const [lastY, setLastY] = React.useState(0);
160
- const [dragOffset, setDragOffset] = React.useState(0);
161
- const [velocity, setVelocity] = React.useState(0);
162
+ const lastYRef = React.useRef(0);
163
+ const velocityRef = React.useRef(0);
162
164
  const [isMomentumScrolling, setIsMomentumScrolling] = React.useState(false);
163
165
  const lastMoveTime = React.useRef(0);
164
166
  const lastMovePosition = React.useRef(0);
@@ -167,10 +169,14 @@ const Picker = core.polymorphicFactory((_props, ref) => {
167
169
  const wheelTimeoutRef = React.useRef(null);
168
170
  const lastWheelEventTime = React.useRef(0);
169
171
  const [currentPosition, setCurrentPosition] = React.useState(selectedIndex);
172
+ const currentPositionRef = React.useRef(currentPosition);
173
+ currentPositionRef.current = currentPosition;
170
174
  const [isAnimating, setIsAnimating] = React.useState(false);
171
175
  const animationRef = React.useRef(null);
172
176
  const [isFocused, setIsFocused] = React.useState(false);
173
177
  const prevLoopRef = React.useRef(loop);
178
+ const originalOverflowRef = React.useRef(null);
179
+ const isWindows = typeof navigator !== "undefined" && navigator.userAgent.includes("Windows");
174
180
  React.useEffect(() => {
175
181
  if (value !== prevValueRef.current && selectedIndex !== -1) {
176
182
  if (animate) {
@@ -238,58 +244,69 @@ const Picker = core.polymorphicFactory((_props, ref) => {
238
244
  }
239
245
  return currentRoundedPos - directPath;
240
246
  };
241
- const animateToPosition = (targetPosition) => {
242
- if (targetPosition === currentPosition || isAnimating) {
243
- return;
244
- }
245
- let clampedTargetPosition = targetPosition;
246
- if (!loop) {
247
- clampedTargetPosition = Math.max(0, Math.min(data.length - 1, targetPosition));
248
- }
249
- if (animationRef.current !== null) {
250
- cancelAnimationFrame(animationRef.current);
251
- }
252
- if (momentumAnimationRef.current !== null) {
253
- cancelAnimationFrame(momentumAnimationRef.current);
254
- setIsMomentumScrolling(false);
255
- }
256
- setIsAnimating(true);
257
- const startTime = performance.now();
258
- const startPosition = currentPosition;
259
- const distance = Math.abs(clampedTargetPosition - startPosition);
260
- const duration = Math.min(
261
- animationDuration || 300,
262
- Math.max(100, (animationDuration || 300) * Math.min(distance / 3, 1))
263
- );
264
- const animate2 = (time) => {
265
- const elapsed = time - startTime;
266
- const progress = Math.min(elapsed / duration, 1);
267
- const easeProgress = 1 - (1 - progress) * (1 - progress);
268
- const position = startPosition + (clampedTargetPosition - startPosition) * easeProgress;
269
- setCurrentPosition(position);
270
- if (progress < 1) {
271
- animationRef.current = requestAnimationFrame(animate2);
272
- } else {
273
- setIsAnimating(false);
274
- animationRef.current = null;
275
- setCurrentPosition(clampedTargetPosition);
276
- if (!disabled) {
277
- const realIndex2 = loop ? (Math.round(clampedTargetPosition) % data.length + data.length) % data.length : Math.round(clampedTargetPosition);
278
- onChange?.(data[realIndex2]);
279
- }
280
- const realIndex = loop ? (Math.round(clampedTargetPosition) % data.length + data.length) % data.length : Math.round(clampedTargetPosition);
281
- prevValueRef.current = data[realIndex];
247
+ const snapAndNotify = React.useCallback(
248
+ (roundedPosition) => {
249
+ if (data.length === 0) {
250
+ return;
282
251
  }
283
- };
284
- animationRef.current = requestAnimationFrame(animate2);
285
- };
286
- const applyMomentum = () => {
287
- if (velocity === 0 || disabled) {
252
+ const realIndex = loop ? (roundedPosition % data.length + data.length) % data.length : Math.max(0, Math.min(data.length - 1, roundedPosition));
253
+ if (!disabled) {
254
+ onChange?.(data[realIndex]);
255
+ }
256
+ prevValueRef.current = data[realIndex];
257
+ },
258
+ [disabled, loop, data, onChange]
259
+ );
260
+ const animateToPosition = React.useCallback(
261
+ (targetPosition) => {
262
+ if (targetPosition === currentPositionRef.current || isAnimating) {
263
+ return;
264
+ }
265
+ let clampedTargetPosition = targetPosition;
266
+ if (!loop) {
267
+ clampedTargetPosition = Math.max(0, Math.min(data.length - 1, targetPosition));
268
+ }
269
+ if (animationRef.current !== null) {
270
+ cancelAnimationFrame(animationRef.current);
271
+ }
272
+ if (momentumAnimationRef.current !== null) {
273
+ cancelAnimationFrame(momentumAnimationRef.current);
274
+ setIsMomentumScrolling(false);
275
+ }
276
+ setIsAnimating(true);
277
+ const startTime = performance.now();
278
+ const startPosition = currentPositionRef.current;
279
+ const distance = Math.abs(clampedTargetPosition - startPosition);
280
+ const duration = Math.min(
281
+ animationDuration || 300,
282
+ Math.max(100, (animationDuration || 300) * Math.min(distance / 3, 1))
283
+ );
284
+ const animate2 = (time) => {
285
+ const elapsed = time - startTime;
286
+ const progress = Math.min(elapsed / duration, 1);
287
+ const easeProgress = 1 - (1 - progress) * (1 - progress);
288
+ const position = startPosition + (clampedTargetPosition - startPosition) * easeProgress;
289
+ setCurrentPosition(position);
290
+ if (progress < 1) {
291
+ animationRef.current = requestAnimationFrame(animate2);
292
+ } else {
293
+ setIsAnimating(false);
294
+ animationRef.current = null;
295
+ setCurrentPosition(clampedTargetPosition);
296
+ snapAndNotify(Math.round(clampedTargetPosition));
297
+ }
298
+ };
299
+ animationRef.current = requestAnimationFrame(animate2);
300
+ },
301
+ [isAnimating, loop, data.length, animationDuration, snapAndNotify]
302
+ );
303
+ const applyMomentum = React.useCallback(() => {
304
+ if (velocityRef.current === 0 || disabled) {
288
305
  return;
289
306
  }
290
307
  setIsMomentumScrolling(true);
291
- let currentVelocity = velocity * (momentum || 0.95);
292
- let currentPos = currentPosition;
308
+ let currentVelocity = velocityRef.current * (momentum || 0.95);
309
+ let currentPos = currentPositionRef.current;
293
310
  let lastDirection = Math.sign(currentVelocity);
294
311
  const momentumScroll = () => {
295
312
  currentVelocity *= decelerationRate || 0.95;
@@ -329,18 +346,13 @@ const Picker = core.polymorphicFactory((_props, ref) => {
329
346
  }
330
347
  setCurrentPosition(roundedPosition);
331
348
  requestAnimationFrame(() => {
332
- if (!disabled) {
333
- const realIndex2 = loop ? (roundedPosition % data.length + data.length) % data.length : roundedPosition;
334
- onChange?.(data[realIndex2]);
335
- }
336
- const realIndex = loop ? (roundedPosition % data.length + data.length) % data.length : roundedPosition;
337
- prevValueRef.current = data[realIndex];
349
+ snapAndNotify(roundedPosition);
338
350
  });
339
351
  }
340
352
  lastDirection = currentDirection;
341
353
  };
342
354
  momentumAnimationRef.current = requestAnimationFrame(momentumScroll);
343
- };
355
+ }, [disabled, momentum, decelerationRate, loop, data.length, snapAndNotify]);
344
356
  React.useEffect(() => {
345
357
  return () => {
346
358
  if (animationRef.current !== null) {
@@ -357,23 +369,26 @@ const Picker = core.polymorphicFactory((_props, ref) => {
357
369
  const handleMouseEnter = () => {
358
370
  const isInteractionDisabled = disabled || readOnly;
359
371
  if (preventPageScroll && !isInteractionDisabled && typeof document !== "undefined") {
372
+ originalOverflowRef.current = document.body.style.overflow;
360
373
  document.body.style.overflow = "hidden";
361
374
  }
362
375
  };
363
376
  const handleMouseLeave = () => {
364
- if (preventPageScroll && typeof document !== "undefined") {
365
- document.body.style.overflow = "auto";
377
+ if (preventPageScroll && typeof document !== "undefined" && originalOverflowRef.current !== null) {
378
+ document.body.style.overflow = originalOverflowRef.current;
379
+ originalOverflowRef.current = null;
366
380
  }
367
381
  };
368
382
  React.useEffect(() => {
369
383
  return () => {
370
- if (preventPageScroll && typeof document !== "undefined") {
371
- document.body.style.overflow = "auto";
384
+ if (preventPageScroll && typeof document !== "undefined" && originalOverflowRef.current !== null) {
385
+ document.body.style.overflow = originalOverflowRef.current;
386
+ originalOverflowRef.current = null;
372
387
  }
373
388
  };
374
389
  }, [preventPageScroll]);
375
390
  const halfVisible = Math.floor((visibleItems || 5) / 2);
376
- const handleMouseDown = (e) => {
391
+ const startDrag = (clientY) => {
377
392
  const isInteractionDisabled = disabled || readOnly;
378
393
  if (isAnimating || isInteractionDisabled || isMomentumScrolling) {
379
394
  return;
@@ -382,163 +397,118 @@ const Picker = core.polymorphicFactory((_props, ref) => {
382
397
  cancelAnimationFrame(momentumAnimationRef.current);
383
398
  setIsMomentumScrolling(false);
384
399
  }
385
- e.preventDefault();
386
400
  setIsDragging(true);
387
- setLastY(e.clientY);
388
- setDragOffset(0);
389
- setVelocity(0);
401
+ lastYRef.current = clientY;
402
+ velocityRef.current = 0;
390
403
  lastMoveTime.current = performance.now();
391
404
  lastMovePosition.current = currentPosition;
392
405
  };
393
- const handleTouchStart = (e) => {
394
- const isInteractionDisabled = disabled || readOnly;
395
- if (isAnimating || isInteractionDisabled || isMomentumScrolling) {
396
- return;
397
- }
398
- if (momentumAnimationRef.current !== null) {
399
- cancelAnimationFrame(momentumAnimationRef.current);
400
- setIsMomentumScrolling(false);
401
- }
402
- setIsDragging(true);
403
- setLastY(e.touches[0].clientY);
404
- setDragOffset(0);
405
- setVelocity(0);
406
- lastMoveTime.current = performance.now();
407
- lastMovePosition.current = currentPosition;
406
+ const handleMouseDown = (e) => {
407
+ e.preventDefault();
408
+ startDrag(e.clientY);
408
409
  };
409
- const clampPosition = (position) => {
410
- if (loop) {
411
- return position;
412
- }
413
- if (position < 0) {
414
- return position * 0.3;
415
- } else if (position > data.length - 1) {
416
- return data.length - 1 + (position - (data.length - 1)) * 0.3;
417
- }
418
- return position;
410
+ const handleTouchStart = (e) => {
411
+ startDrag(e.touches[0].clientY);
419
412
  };
420
- const handleMouseMove = (e) => {
421
- const isInteractionDisabled = disabled || readOnly;
422
- if (!isDragging || isInteractionDisabled) {
423
- return;
424
- }
425
- const currentTime = performance.now();
426
- const newY = e.clientY;
427
- const deltaY = newY - lastY;
428
- const timeDelta = currentTime - lastMoveTime.current;
429
- const itemOffsetRatio = deltaY / (itemHeight || 40);
430
- setCurrentPosition((prev) => {
431
- let newPosition = prev - itemOffsetRatio;
432
- newPosition = clampPosition(newPosition);
433
- if (timeDelta > 0) {
434
- const positionDelta = newPosition - lastMovePosition.current;
435
- setVelocity(positionDelta / timeDelta * 16);
413
+ const clampPosition = React.useCallback(
414
+ (position) => {
415
+ if (loop) {
416
+ return position;
436
417
  }
437
- lastMovePosition.current = newPosition;
438
- lastMoveTime.current = currentTime;
439
- return newPosition;
440
- });
441
- setLastY(newY);
442
- };
443
- const handleTouchMove = (e) => {
444
- const isInteractionDisabled = disabled || readOnly;
445
- if (!isDragging || isInteractionDisabled) {
446
- return;
447
- }
448
- const currentTime = performance.now();
449
- const newY = e.touches[0].clientY;
450
- const deltaY = newY - lastY;
451
- const timeDelta = currentTime - lastMoveTime.current;
452
- const itemOffsetRatio = deltaY / (itemHeight || 40);
453
- setCurrentPosition((prev) => {
454
- let newPosition = prev - itemOffsetRatio;
455
- newPosition = clampPosition(newPosition);
456
- if (timeDelta > 0) {
457
- const positionDelta = newPosition - lastMovePosition.current;
458
- setVelocity(positionDelta / timeDelta * 16);
418
+ if (position < 0) {
419
+ return position * 0.3;
420
+ } else if (position > data.length - 1) {
421
+ return data.length - 1 + (position - (data.length - 1)) * 0.3;
459
422
  }
460
- lastMovePosition.current = newPosition;
461
- lastMoveTime.current = currentTime;
462
- return newPosition;
463
- });
464
- setLastY(newY);
465
- };
466
- const handleMouseUp = () => {
467
- const isInteractionDisabled = disabled || readOnly;
468
- if (!isDragging || isInteractionDisabled) {
469
- return;
470
- }
471
- setIsDragging(false);
472
- setDragOffset(0);
473
- if (Math.abs(velocity) > 0.05) {
474
- applyMomentum();
475
- } else {
476
- let roundedPosition = Math.round(currentPosition);
477
- if (!loop) {
478
- roundedPosition = Math.max(0, Math.min(data.length - 1, roundedPosition));
423
+ return position;
424
+ },
425
+ [loop, data.length]
426
+ );
427
+ const handleDragMove = React.useCallback(
428
+ (clientY) => {
429
+ const isInteractionDisabled = disabled || readOnly;
430
+ if (!isDragging || isInteractionDisabled) {
431
+ return;
479
432
  }
480
- if (roundedPosition !== currentPosition) {
481
- animateToPosition(roundedPosition);
482
- } else {
483
- if (!disabled) {
484
- const realIndex2 = loop ? (roundedPosition % data.length + data.length) % data.length : roundedPosition;
485
- onChange?.(data[realIndex2]);
433
+ const currentTime = performance.now();
434
+ const deltaY = clientY - lastYRef.current;
435
+ const timeDelta = currentTime - lastMoveTime.current;
436
+ const itemOffsetRatio = deltaY / (itemHeight || 40);
437
+ setCurrentPosition((prev) => {
438
+ let newPosition = prev - itemOffsetRatio;
439
+ newPosition = clampPosition(newPosition);
440
+ if (timeDelta > 0) {
441
+ const positionDelta = newPosition - lastMovePosition.current;
442
+ velocityRef.current = positionDelta / timeDelta * 16;
486
443
  }
487
- const realIndex = loop ? (roundedPosition % data.length + data.length) % data.length : roundedPosition;
488
- prevValueRef.current = data[realIndex];
489
- }
490
- }
491
- };
492
- const handleTouchEnd = () => {
444
+ lastMovePosition.current = newPosition;
445
+ lastMoveTime.current = currentTime;
446
+ return newPosition;
447
+ });
448
+ lastYRef.current = clientY;
449
+ },
450
+ [isDragging, disabled, readOnly, itemHeight, clampPosition]
451
+ );
452
+ const handleDragEnd = React.useCallback(() => {
493
453
  const isInteractionDisabled = disabled || readOnly;
494
454
  if (!isDragging || isInteractionDisabled) {
495
455
  return;
496
456
  }
497
457
  setIsDragging(false);
498
- setDragOffset(0);
499
- if (Math.abs(velocity) > 0.05) {
458
+ if (Math.abs(velocityRef.current) > 0.05) {
500
459
  applyMomentum();
501
460
  } else {
502
- let roundedPosition = Math.round(currentPosition);
461
+ let roundedPosition = Math.round(currentPositionRef.current);
503
462
  if (!loop) {
504
463
  roundedPosition = Math.max(0, Math.min(data.length - 1, roundedPosition));
505
464
  }
506
- if (roundedPosition !== currentPosition) {
465
+ if (roundedPosition !== currentPositionRef.current) {
507
466
  animateToPosition(roundedPosition);
508
467
  } else {
509
- if (!disabled) {
510
- const realIndex2 = loop ? (roundedPosition % data.length + data.length) % data.length : roundedPosition;
511
- onChange?.(data[realIndex2]);
512
- }
513
- const realIndex = loop ? (roundedPosition % data.length + data.length) % data.length : roundedPosition;
514
- prevValueRef.current = data[realIndex];
468
+ snapAndNotify(roundedPosition);
515
469
  }
516
470
  }
517
- };
471
+ }, [
472
+ isDragging,
473
+ disabled,
474
+ readOnly,
475
+ loop,
476
+ data.length,
477
+ applyMomentum,
478
+ animateToPosition,
479
+ snapAndNotify
480
+ ]);
518
481
  React.useEffect(() => {
519
- const handleGlobalMouseMove = (e) => handleMouseMove(e);
520
- const handleGlobalMouseUp = () => handleMouseUp();
521
- const handleGlobalTouchMove = (e) => handleTouchMove(e);
522
- const handleGlobalTouchEnd = () => handleTouchEnd();
482
+ const onMouseMove = (e) => handleDragMove(e.clientY);
483
+ const onMouseUp = () => handleDragEnd();
484
+ const onTouchMove = (e) => handleDragMove(e.touches[0].clientY);
485
+ const onTouchEnd = () => handleDragEnd();
523
486
  if (isDragging) {
524
- window.addEventListener("mousemove", handleGlobalMouseMove, { passive: false });
525
- window.addEventListener("mouseup", handleGlobalMouseUp);
526
- window.addEventListener("touchmove", handleGlobalTouchMove, { passive: false });
527
- window.addEventListener("touchend", handleGlobalTouchEnd);
487
+ window.addEventListener("mousemove", onMouseMove, { passive: false });
488
+ window.addEventListener("mouseup", onMouseUp);
489
+ window.addEventListener("touchmove", onTouchMove, { passive: false });
490
+ window.addEventListener("touchend", onTouchEnd);
528
491
  }
529
492
  return () => {
530
- window.removeEventListener("mousemove", handleGlobalMouseMove);
531
- window.removeEventListener("mouseup", handleGlobalMouseUp);
532
- window.removeEventListener("touchmove", handleGlobalTouchMove);
533
- window.removeEventListener("touchend", handleGlobalTouchEnd);
493
+ window.removeEventListener("mousemove", onMouseMove);
494
+ window.removeEventListener("mouseup", onMouseUp);
495
+ window.removeEventListener("touchmove", onTouchMove);
496
+ window.removeEventListener("touchend", onTouchEnd);
534
497
  };
535
- }, [isDragging, lastY, currentPosition, disabled, readOnly, loop, data.length]);
536
- const isWindows = React.useCallback(() => {
537
- if (typeof navigator !== "undefined") {
538
- return navigator.userAgent.indexOf("Windows") !== -1;
498
+ }, [isDragging, handleDragMove, handleDragEnd]);
499
+ const wheelSnapToNearest = React.useCallback(() => {
500
+ setIsWheeling(false);
501
+ let roundedPosition = Math.round(currentPositionRef.current);
502
+ if (!loop) {
503
+ roundedPosition = Math.max(0, Math.min(data.length - 1, roundedPosition));
539
504
  }
540
- return false;
541
- }, []);
505
+ if (roundedPosition !== currentPositionRef.current) {
506
+ animateToPosition(roundedPosition);
507
+ } else {
508
+ snapAndNotify(roundedPosition);
509
+ }
510
+ wheelTimeoutRef.current = null;
511
+ }, [loop, data.length, snapAndNotify, animateToPosition]);
542
512
  const handleWheel = React.useCallback(
543
513
  (e) => {
544
514
  const isInteractionDisabled = disabled || readOnly;
@@ -559,7 +529,7 @@ const Picker = core.polymorphicFactory((_props, ref) => {
559
529
  } else if (e.deltaMode === 2) {
560
530
  delta *= 100;
561
531
  }
562
- if (isWindows()) {
532
+ if (isWindows) {
563
533
  sensitivity *= 0.2;
564
534
  }
565
535
  delta *= sensitivity;
@@ -580,61 +550,38 @@ const Picker = core.polymorphicFactory((_props, ref) => {
580
550
  lastWheelEventTime.current = Date.now();
581
551
  wheelTimeoutRef.current = setTimeout(() => {
582
552
  if (Date.now() - lastWheelEventTime.current >= 150) {
583
- setIsWheeling(false);
584
- let roundedPosition = Math.round(currentPosition);
585
- if (!loop) {
586
- roundedPosition = Math.max(0, Math.min(data.length - 1, roundedPosition));
587
- }
588
- if (roundedPosition !== currentPosition) {
589
- animateToPosition(roundedPosition);
590
- } else {
591
- if (!disabled) {
592
- const realIndex2 = loop ? (roundedPosition % data.length + data.length) % data.length : roundedPosition;
593
- onChange?.(data[realIndex2]);
594
- }
595
- const realIndex = loop ? (roundedPosition % data.length + data.length) % data.length : roundedPosition;
596
- prevValueRef.current = data[realIndex];
597
- }
598
- wheelTimeoutRef.current = null;
553
+ wheelSnapToNearest();
599
554
  } else {
600
555
  if (wheelTimeoutRef.current) {
601
556
  clearTimeout(wheelTimeoutRef.current);
602
557
  }
603
- wheelTimeoutRef.current = setTimeout(() => {
604
- setIsWheeling(false);
605
- let roundedPosition = Math.round(currentPosition);
606
- if (!loop) {
607
- roundedPosition = Math.max(0, Math.min(data.length - 1, roundedPosition));
608
- }
609
- if (roundedPosition !== currentPosition) {
610
- animateToPosition(roundedPosition);
611
- } else {
612
- if (!disabled) {
613
- const realIndex2 = loop ? (roundedPosition % data.length + data.length) % data.length : roundedPosition;
614
- onChange?.(data[realIndex2]);
615
- }
616
- const realIndex = loop ? (roundedPosition % data.length + data.length) % data.length : roundedPosition;
617
- prevValueRef.current = data[realIndex];
618
- }
619
- wheelTimeoutRef.current = null;
620
- }, 150);
558
+ wheelTimeoutRef.current = setTimeout(wheelSnapToNearest, 150);
621
559
  }
622
560
  }, 150);
623
561
  },
624
562
  [
625
- currentPosition,
626
563
  data,
627
564
  loop,
628
565
  isAnimating,
629
566
  disabled,
630
567
  readOnly,
631
568
  itemHeight,
632
- onChange,
633
569
  wheelSensitivity,
634
570
  isMomentumScrolling,
635
- isWindows
571
+ isWindows,
572
+ wheelSnapToNearest
636
573
  ]
637
574
  );
575
+ React.useEffect(() => {
576
+ const el = containerRef.current;
577
+ if (!el || disabled || readOnly) {
578
+ return;
579
+ }
580
+ el.addEventListener("wheel", handleWheel, { passive: false });
581
+ return () => {
582
+ el.removeEventListener("wheel", handleWheel);
583
+ };
584
+ }, [handleWheel, disabled, readOnly]);
638
585
  const handleKeyDown = React.useCallback(
639
586
  (e) => {
640
587
  const isInteractionDisabled = disabled || readOnly;
@@ -696,7 +643,7 @@ const Picker = core.polymorphicFactory((_props, ref) => {
696
643
  }
697
644
  }
698
645
  },
699
- [currentPosition, data.length, loop, disabled, readOnly, isMomentumScrolling]
646
+ [currentPosition, data.length, loop, disabled, readOnly, animateToPosition]
700
647
  );
701
648
  const handleFocus = () => {
702
649
  if (!disabled) {
@@ -718,33 +665,25 @@ const Picker = core.polymorphicFactory((_props, ref) => {
718
665
  const currentRoundedPos = Math.round(currentPosition);
719
666
  const directVisualPath = virtualIndex - currentRoundedPos;
720
667
  animateToPosition(currentRoundedPos + directVisualPath);
721
- if (!disabled) {
722
- onChange?.(data[clickedIndex]);
723
- }
724
- prevValueRef.current = data[clickedIndex];
668
+ snapAndNotify(clickedIndex);
725
669
  };
726
- const createContinuousIndices = () => {
670
+ const flooredPosition = Math.floor(currentPosition);
671
+ const continuousIndices = React.useMemo(() => {
727
672
  if (!loop || data.length <= 1) {
728
673
  return Array.from({ length: data.length }, (_, i) => ({ dataIndex: i, virtualIndex: i }));
729
674
  }
730
- const duplicatesPerSide = Math.max(
731
- // At least visibleItems * 2 to ensure we have enough items on both sides
732
- (visibleItems || 5) * 2,
733
- // Or data.length * 2 to ensure we have enough duplicates for large datasets
734
- data.length * 2
735
- );
736
- const continuousIndices = [];
737
- const startIndex = Math.floor(currentPosition) - duplicatesPerSide;
738
- const endIndex = Math.floor(currentPosition) + duplicatesPerSide;
675
+ const duplicatesPerSide = Math.max((visibleItems || 5) * 2, data.length * 2);
676
+ const indices = [];
677
+ const startIndex = flooredPosition - duplicatesPerSide;
678
+ const endIndex = flooredPosition + duplicatesPerSide;
739
679
  for (let i = startIndex; i <= endIndex; i++) {
740
680
  const dataIndex = (i % data.length + data.length) % data.length;
741
- continuousIndices.push({ dataIndex, virtualIndex: i });
681
+ indices.push({ dataIndex, virtualIndex: i });
742
682
  }
743
- return continuousIndices;
744
- };
683
+ return indices;
684
+ }, [loop, data.length, visibleItems, flooredPosition]);
745
685
  const renderItems = () => {
746
686
  const items = [];
747
- const continuousIndices = createContinuousIndices();
748
687
  const visibleRange = Math.max(
749
688
  halfVisible + 1,
750
689
  // Add just 1 extra item on each side for smoother scrolling
@@ -754,7 +693,7 @@ const Picker = core.polymorphicFactory((_props, ref) => {
754
693
  for (const { dataIndex, virtualIndex } of continuousIndices) {
755
694
  const relativePos = virtualIndex - currentPosition;
756
695
  if (Math.abs(relativePos) <= visibleRange) {
757
- const itemOffset = relativePos * (itemHeight || 40) + dragOffset;
696
+ const itemOffset = relativePos * (itemHeight || 40);
758
697
  const distanceFromCenter = Math.abs(relativePos);
759
698
  const maxDistance = halfVisible;
760
699
  const normalizedDistance = Math.min(distanceFromCenter / maxDistance, 1);
@@ -829,14 +768,7 @@ const Picker = core.polymorphicFactory((_props, ref) => {
829
768
  return /* @__PURE__ */ React.createElement(
830
769
  core.Box,
831
770
  {
832
- ref: (node) => {
833
- if (typeof ref === "function") {
834
- ref(node);
835
- } else if (ref) {
836
- ref.current = node;
837
- }
838
- rootRef.current = node;
839
- },
771
+ ref: mergedRootRef,
840
772
  ...getStyles("root", {
841
773
  style: {
842
774
  perspective: enable3D ? `${perspective}px` : void 0
@@ -865,7 +797,6 @@ const Picker = core.polymorphicFactory((_props, ref) => {
865
797
  className: Picker_module.container,
866
798
  onMouseDown: disabled || readOnly ? void 0 : handleMouseDown,
867
799
  onTouchStart: disabled || readOnly ? void 0 : handleTouchStart,
868
- onWheel: disabled || readOnly ? void 0 : handleWheel,
869
800
  onKeyDown: disabled || readOnly ? void 0 : handleKeyDown,
870
801
  onFocus: handleFocus,
871
802
  onBlur: handleBlur,