@aviala-design/spiral 2.6.0 → 2.6.2

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/dist/index.cjs CHANGED
@@ -6304,7 +6304,70 @@ var import_react43 = require("react");
6304
6304
  // src/lib/sticky-wheel.ts
6305
6305
  var WHEEL_STICKY_AMOUNT = 0.45;
6306
6306
  var WHEEL_SCROLL_END_MS = 100;
6307
- var WHEEL_STEP_THRESHOLD_FACTOR = 0.5 + WHEEL_STICKY_AMOUNT * 0.35;
6307
+ var WHEEL_STEP_THRESHOLD_FACTOR = 0.35;
6308
+ var WHEEL_ACCUM_IDLE_MS = 140;
6309
+ var WHEEL_LOOP_SECTIONS = 3;
6310
+ var WHEEL_LOOP_MIDDLE_SECTION = 1;
6311
+ function consumeWheelSteps(accum, deltaY, threshold) {
6312
+ if (!(threshold > 0) || !Number.isFinite(deltaY) || deltaY === 0) {
6313
+ return { nextAccum: accum, steps: 0 };
6314
+ }
6315
+ let nextAccum = accum;
6316
+ if (nextAccum !== 0 && Math.sign(nextAccum) !== Math.sign(deltaY)) {
6317
+ nextAccum = 0;
6318
+ }
6319
+ nextAccum += deltaY;
6320
+ const steps = Math.trunc(nextAccum / threshold);
6321
+ if (steps === 0) {
6322
+ return { nextAccum, steps: 0 };
6323
+ }
6324
+ nextAccum -= steps * threshold;
6325
+ return { nextAccum, steps };
6326
+ }
6327
+ function normalizeWheelIndex(index, length) {
6328
+ if (length <= 0) return 0;
6329
+ return (index % length + length) % length;
6330
+ }
6331
+ function recenterLoopIndex(rawIndex, length, middleSection = WHEEL_LOOP_MIDDLE_SECTION) {
6332
+ if (length <= 0) return 0;
6333
+ return middleSection * length + normalizeWheelIndex(rawIndex, length);
6334
+ }
6335
+ function nearestLoopIndex(currentRawIndex, valueIndex, length, sections = WHEEL_LOOP_SECTIONS, preferDelta = 0) {
6336
+ if (length <= 0 || sections <= 0) return 0;
6337
+ const slot = normalizeWheelIndex(valueIndex, length);
6338
+ let best = slot;
6339
+ let bestScore = Number.POSITIVE_INFINITY;
6340
+ for (let section = 0; section < sections; section += 1) {
6341
+ const candidate = section * length + slot;
6342
+ const distance = Math.abs(candidate - currentRawIndex);
6343
+ const directionBias = preferDelta === 0 ? 0 : Math.sign(candidate - currentRawIndex) === Math.sign(preferDelta) ? -0.25 : 0.25;
6344
+ const score = distance + directionBias;
6345
+ if (score < bestScore) {
6346
+ bestScore = score;
6347
+ best = candidate;
6348
+ }
6349
+ }
6350
+ return best;
6351
+ }
6352
+ function stepLoopRawIndex(currentRawIndex, steps, length, sections = WHEEL_LOOP_SECTIONS, middleSection = WHEEL_LOOP_MIDDLE_SECTION) {
6353
+ if (length <= 0) return { rawIndex: 0, didRecenter: false };
6354
+ const maxIndex = sections * length - 1;
6355
+ let rawIndex = currentRawIndex;
6356
+ let didRecenter = false;
6357
+ const section = Math.floor(rawIndex / length);
6358
+ if (section !== middleSection) {
6359
+ rawIndex = recenterLoopIndex(rawIndex, length, middleSection);
6360
+ didRecenter = true;
6361
+ }
6362
+ const next = rawIndex + steps;
6363
+ if (next < 0 || next > maxIndex) {
6364
+ return {
6365
+ rawIndex: recenterLoopIndex(next, length, middleSection),
6366
+ didRecenter: true
6367
+ };
6368
+ }
6369
+ return { rawIndex: next, didRecenter };
6370
+ }
6308
6371
  function stickyRemapProgress(t, amount) {
6309
6372
  const clamped = Math.max(0, Math.min(1, t));
6310
6373
  if (amount <= 0) return clamped;
@@ -6323,14 +6386,11 @@ function stickyRoundIndex(fractionalIndex, stickiness = WHEEL_STICKY_AMOUNT) {
6323
6386
 
6324
6387
  // src/components/date-picker/date-picker-time-wheel.tsx
6325
6388
  var import_jsx_runtime40 = require("react/jsx-runtime");
6326
- var LOOP_SECTIONS = 3;
6327
- var MIDDLE_SECTION = 1;
6389
+ var LOOP_SECTIONS = WHEEL_LOOP_SECTIONS;
6390
+ var MIDDLE_SECTION = WHEEL_LOOP_MIDDLE_SECTION;
6328
6391
  function formatWheelValue(value) {
6329
6392
  return String(value).padStart(2, "0");
6330
6393
  }
6331
- function normalizeIndex(index, length) {
6332
- return (index % length + length) % length;
6333
- }
6334
6394
  function getValueIndex(values, value) {
6335
6395
  const index = values.indexOf(value);
6336
6396
  if (index >= 0) return index;
@@ -6368,17 +6428,28 @@ function DatePickerTimeWheelColumn({
6368
6428
  layoutKey = 0
6369
6429
  }) {
6370
6430
  const scrollRef = (0, import_react43.useRef)(null);
6431
+ const clipTrackRef = (0, import_react43.useRef)(null);
6371
6432
  const isUserScrollingRef = (0, import_react43.useRef)(false);
6372
6433
  const isProgrammaticScrollRef = (0, import_react43.useRef)(false);
6373
6434
  const smoothTargetValueRef = (0, import_react43.useRef)(null);
6374
6435
  const scrollEndTimerRef = (0, import_react43.useRef)(void 0);
6375
6436
  const wheelAccumRef = (0, import_react43.useRef)(0);
6437
+ const wheelAccumIdleTimerRef = (0, import_react43.useRef)(void 0);
6376
6438
  const selectedValueRef = (0, import_react43.useRef)(value);
6377
6439
  selectedValueRef.current = value;
6378
6440
  const repeatedValues = (0, import_react43.useMemo)(() => {
6379
6441
  if (!loop) return [...values];
6380
6442
  return Array.from({ length: LOOP_SECTIONS }, () => values).flat();
6381
6443
  }, [loop, values]);
6444
+ const readRawIndex2 = (0, import_react43.useCallback)((container, itemHeight) => {
6445
+ return Math.round(container.scrollTop / itemHeight);
6446
+ }, []);
6447
+ const syncClipTrack = (0, import_react43.useCallback)(() => {
6448
+ const container = scrollRef.current;
6449
+ const track = clipTrackRef.current;
6450
+ if (!container || !track) return;
6451
+ track.style.transform = `translate3d(0, ${-container.scrollTop}px, 0)`;
6452
+ }, []);
6382
6453
  const scrollToIndex = (0, import_react43.useCallback)(
6383
6454
  (index, behavior = "auto") => {
6384
6455
  const container = scrollRef.current;
@@ -6387,21 +6458,13 @@ function DatePickerTimeWheelColumn({
6387
6458
  if (itemHeight <= 0) return;
6388
6459
  isProgrammaticScrollRef.current = true;
6389
6460
  container.scrollTo({ top: index * itemHeight, behavior });
6390
- },
6391
- []
6392
- );
6393
- const scrollToValue = (0, import_react43.useCallback)(
6394
- (nextValue, behavior = "smooth") => {
6395
- if (behavior === "smooth") {
6396
- smoothTargetValueRef.current = nextValue;
6461
+ if (behavior === "auto") {
6462
+ syncClipTrack();
6397
6463
  } else {
6398
- smoothTargetValueRef.current = null;
6464
+ requestAnimationFrame(syncClipTrack);
6399
6465
  }
6400
- const valueIndex = getValueIndex(values, nextValue);
6401
- const targetIndex = loop ? values.length * MIDDLE_SECTION + valueIndex : valueIndex;
6402
- scrollToIndex(targetIndex, behavior);
6403
6466
  },
6404
- [loop, scrollToIndex, values]
6467
+ [syncClipTrack]
6405
6468
  );
6406
6469
  const syncScrollToValue = (0, import_react43.useCallback)(
6407
6470
  (behavior = "auto") => {
@@ -6414,24 +6477,27 @@ function DatePickerTimeWheelColumn({
6414
6477
  const itemHeight = readItemHeight(container);
6415
6478
  if (itemHeight <= 0) return;
6416
6479
  const valueIndex = getValueIndex(values, selectedValueRef.current);
6417
- const targetIndex = loop ? values.length * MIDDLE_SECTION + valueIndex : valueIndex;
6418
- const currentIndex = Math.round(container.scrollTop / itemHeight);
6419
- const currentValueIndex = loop ? normalizeIndex(currentIndex, values.length) : Math.min(Math.max(currentIndex, 0), values.length - 1);
6420
- const inMiddleSection = !loop || Math.floor(currentIndex / values.length) === MIDDLE_SECTION;
6421
- if (currentValueIndex === valueIndex && inMiddleSection) return;
6480
+ const currentIndex = readRawIndex2(container, itemHeight);
6481
+ const currentValueIndex = loop ? normalizeWheelIndex(currentIndex, values.length) : Math.min(Math.max(currentIndex, 0), values.length - 1);
6482
+ if (currentValueIndex === valueIndex) {
6483
+ syncClipTrack();
6484
+ return;
6485
+ }
6486
+ const targetIndex = loop ? nearestLoopIndex(currentIndex, valueIndex, values.length, LOOP_SECTIONS) : valueIndex;
6422
6487
  scrollToIndex(targetIndex, behavior);
6423
6488
  },
6424
- [loop, scrollToIndex, values]
6489
+ [loop, readRawIndex2, scrollToIndex, syncClipTrack, values]
6425
6490
  );
6426
6491
  const settleScroll = (0, import_react43.useCallback)(() => {
6427
6492
  const container = scrollRef.current;
6428
- if (!container || isProgrammaticScrollRef.current) {
6429
- isProgrammaticScrollRef.current = false;
6493
+ if (!container) return;
6494
+ const wasProgrammatic = isProgrammaticScrollRef.current;
6495
+ isProgrammaticScrollRef.current = false;
6496
+ const itemHeight = readItemHeight(container);
6497
+ if (itemHeight <= 0 || values.length === 0) {
6430
6498
  smoothTargetValueRef.current = null;
6431
6499
  return;
6432
6500
  }
6433
- const itemHeight = readItemHeight(container);
6434
- if (itemHeight <= 0 || values.length === 0) return;
6435
6501
  const lastIndex = values.length - 1;
6436
6502
  const maxScroll = Math.max(container.scrollHeight - container.clientHeight, 0);
6437
6503
  const fractionalIndex = container.scrollTop / itemHeight;
@@ -6443,12 +6509,21 @@ function DatePickerTimeWheelColumn({
6443
6509
  rawIndex = 0;
6444
6510
  }
6445
6511
  }
6446
- const valueIndex = loop ? normalizeIndex(rawIndex, values.length) : Math.min(Math.max(rawIndex, 0), lastIndex);
6512
+ const valueIndex = loop ? normalizeWheelIndex(rawIndex, values.length) : Math.min(Math.max(rawIndex, 0), lastIndex);
6447
6513
  const nextValue = values[valueIndex];
6448
- const targetIndex = loop ? values.length * MIDDLE_SECTION + valueIndex : valueIndex;
6514
+ const targetIndex = loop ? recenterLoopIndex(rawIndex, values.length, MIDDLE_SECTION) : valueIndex;
6449
6515
  const targetTop = targetIndex * itemHeight;
6450
6516
  const section = loop ? Math.floor(rawIndex / values.length) : MIDDLE_SECTION;
6451
6517
  const needsLoopRecenter = loop && section !== MIDDLE_SECTION;
6518
+ if (wasProgrammatic) {
6519
+ if (needsLoopRecenter) {
6520
+ isProgrammaticScrollRef.current = true;
6521
+ container.scrollTop = targetTop;
6522
+ }
6523
+ smoothTargetValueRef.current = null;
6524
+ syncClipTrack();
6525
+ return;
6526
+ }
6452
6527
  const needsSnap = Math.abs(container.scrollTop - targetTop) > 1;
6453
6528
  if (needsLoopRecenter) {
6454
6529
  isProgrammaticScrollRef.current = true;
@@ -6466,22 +6541,26 @@ function DatePickerTimeWheelColumn({
6466
6541
  }
6467
6542
  isUserScrollingRef.current = false;
6468
6543
  wheelAccumRef.current = 0;
6469
- }, [loop, onChange, values]);
6544
+ syncClipTrack();
6545
+ }, [loop, onChange, syncClipTrack, values]);
6470
6546
  const handleScroll = (0, import_react43.useCallback)(() => {
6547
+ syncClipTrack();
6471
6548
  if (isProgrammaticScrollRef.current) return;
6472
6549
  isUserScrollingRef.current = true;
6473
6550
  if (scrollEndTimerRef.current) {
6474
6551
  clearTimeout(scrollEndTimerRef.current);
6475
6552
  }
6476
6553
  scrollEndTimerRef.current = setTimeout(settleScroll, WHEEL_SCROLL_END_MS);
6477
- }, [settleScroll]);
6554
+ }, [settleScroll, syncClipTrack]);
6478
6555
  (0, import_react43.useLayoutEffect)(() => {
6479
6556
  syncScrollToValue("auto");
6557
+ syncClipTrack();
6480
6558
  const raf = requestAnimationFrame(() => {
6481
6559
  syncScrollToValue("auto");
6560
+ syncClipTrack();
6482
6561
  });
6483
6562
  return () => cancelAnimationFrame(raf);
6484
- }, [layoutKey, syncScrollToValue, value]);
6563
+ }, [layoutKey, syncClipTrack, syncScrollToValue, value]);
6485
6564
  (0, import_react43.useEffect)(() => {
6486
6565
  const container = scrollRef.current;
6487
6566
  if (!container) return;
@@ -6509,23 +6588,48 @@ function DatePickerTimeWheelColumn({
6509
6588
  if (!container) return;
6510
6589
  const onWheel = (event) => {
6511
6590
  event.preventDefault();
6512
- if (isProgrammaticScrollRef.current || values.length === 0) return;
6591
+ if (values.length === 0) return;
6513
6592
  const itemHeight = readItemHeight(container);
6514
6593
  if (itemHeight <= 0) return;
6515
- wheelAccumRef.current += event.deltaY;
6594
+ if (wheelAccumIdleTimerRef.current) {
6595
+ clearTimeout(wheelAccumIdleTimerRef.current);
6596
+ }
6597
+ wheelAccumIdleTimerRef.current = setTimeout(() => {
6598
+ wheelAccumRef.current = 0;
6599
+ }, WHEEL_ACCUM_IDLE_MS);
6516
6600
  const threshold = itemHeight * WHEEL_STEP_THRESHOLD_FACTOR;
6517
- if (Math.abs(wheelAccumRef.current) < threshold) return;
6518
- const direction = wheelAccumRef.current > 0 ? 1 : -1;
6519
- wheelAccumRef.current = 0;
6520
- const currentIndex = getValueIndex(values, selectedValueRef.current);
6521
- const nextIndex = loop ? normalizeIndex(currentIndex + direction, values.length) : Math.min(Math.max(currentIndex + direction, 0), values.length - 1);
6522
- if (nextIndex === currentIndex) return;
6601
+ const { nextAccum, steps } = consumeWheelSteps(
6602
+ wheelAccumRef.current,
6603
+ event.deltaY,
6604
+ threshold
6605
+ );
6606
+ wheelAccumRef.current = nextAccum;
6607
+ if (steps === 0) return;
6608
+ const currentRaw = readRawIndex2(container, itemHeight);
6609
+ let targetRaw;
6610
+ if (loop) {
6611
+ if (Math.floor(currentRaw / values.length) !== MIDDLE_SECTION) {
6612
+ isProgrammaticScrollRef.current = true;
6613
+ container.scrollTop = recenterLoopIndex(currentRaw, values.length, MIDDLE_SECTION) * itemHeight;
6614
+ }
6615
+ targetRaw = stepLoopRawIndex(
6616
+ readRawIndex2(container, itemHeight),
6617
+ steps,
6618
+ values.length,
6619
+ LOOP_SECTIONS,
6620
+ MIDDLE_SECTION
6621
+ ).rawIndex;
6622
+ } else {
6623
+ targetRaw = Math.min(Math.max(currentRaw + steps, 0), values.length - 1);
6624
+ if (targetRaw === currentRaw) return;
6625
+ }
6523
6626
  if (scrollEndTimerRef.current) {
6524
6627
  clearTimeout(scrollEndTimerRef.current);
6525
6628
  }
6526
- const nextValue = values[nextIndex];
6629
+ const nextValue = values[normalizeWheelIndex(targetRaw, values.length)];
6527
6630
  isUserScrollingRef.current = false;
6528
- scrollToValue(nextValue, "smooth");
6631
+ smoothTargetValueRef.current = nextValue;
6632
+ scrollToIndex(targetRaw, "smooth");
6529
6633
  if (nextValue !== selectedValueRef.current) {
6530
6634
  onChange(nextValue);
6531
6635
  }
@@ -6533,35 +6637,49 @@ function DatePickerTimeWheelColumn({
6533
6637
  container.addEventListener("wheel", onWheel, { passive: false });
6534
6638
  return () => {
6535
6639
  container.removeEventListener("wheel", onWheel);
6640
+ if (wheelAccumIdleTimerRef.current) {
6641
+ clearTimeout(wheelAccumIdleTimerRef.current);
6642
+ }
6536
6643
  };
6537
- }, [loop, onChange, scrollToValue, values]);
6644
+ }, [loop, onChange, readRawIndex2, scrollToIndex, values]);
6538
6645
  const handleKeyDown = (event) => {
6539
- const currentIndex = getValueIndex(values, selectedValueRef.current);
6540
- let nextIndex = null;
6646
+ const container = scrollRef.current;
6647
+ if (!container || values.length === 0) return;
6648
+ const itemHeight = readItemHeight(container);
6649
+ if (itemHeight <= 0) return;
6650
+ const currentRaw = readRawIndex2(container, itemHeight);
6651
+ let targetRaw = null;
6652
+ let preferDelta = 0;
6541
6653
  switch (event.key) {
6542
6654
  case "ArrowUp":
6543
- nextIndex = loop ? normalizeIndex(currentIndex - 1, values.length) : Math.max(currentIndex - 1, 0);
6655
+ preferDelta = -1;
6656
+ targetRaw = loop ? stepLoopRawIndex(currentRaw, -1, values.length, LOOP_SECTIONS, MIDDLE_SECTION).rawIndex : Math.max(currentRaw - 1, 0);
6544
6657
  break;
6545
6658
  case "ArrowDown":
6546
- nextIndex = loop ? normalizeIndex(currentIndex + 1, values.length) : Math.min(currentIndex + 1, values.length - 1);
6659
+ preferDelta = 1;
6660
+ targetRaw = loop ? stepLoopRawIndex(currentRaw, 1, values.length, LOOP_SECTIONS, MIDDLE_SECTION).rawIndex : Math.min(currentRaw + 1, values.length - 1);
6547
6661
  break;
6548
6662
  case "Home":
6549
- nextIndex = 0;
6663
+ targetRaw = loop ? nearestLoopIndex(currentRaw, 0, values.length, LOOP_SECTIONS, -1) : 0;
6550
6664
  break;
6551
6665
  case "End":
6552
- nextIndex = values.length - 1;
6666
+ targetRaw = loop ? nearestLoopIndex(currentRaw, values.length - 1, values.length, LOOP_SECTIONS, 1) : values.length - 1;
6553
6667
  break;
6554
6668
  default:
6555
6669
  return;
6556
6670
  }
6557
6671
  event.preventDefault();
6558
- const nextValue = values[nextIndex];
6559
- scrollToValue(nextValue, "smooth");
6672
+ if (targetRaw === currentRaw && preferDelta === 0) return;
6673
+ const nextValue = values[normalizeWheelIndex(targetRaw, values.length)];
6674
+ isUserScrollingRef.current = false;
6675
+ smoothTargetValueRef.current = nextValue;
6676
+ scrollToIndex(targetRaw, "smooth");
6560
6677
  if (nextValue !== selectedValueRef.current) {
6561
6678
  onChange(nextValue);
6562
6679
  }
6563
6680
  };
6564
- const selectedOptionId = loop ? `${ariaLabel}-${value}-${values.length * MIDDLE_SECTION + getValueIndex(values, value)}` : `${ariaLabel}-${value}`;
6681
+ const committedIndex = getValueIndex(values, value);
6682
+ const selectedOptionId = loop ? `${ariaLabel}-${value}-${values.length * MIDDLE_SECTION + committedIndex}` : `${ariaLabel}-${value}`;
6565
6683
  return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: cn("aviala-datepicker-time__wheel", className), children: [
6566
6684
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "aviala-datepicker-time__wheel-highlight", "aria-hidden": true }),
6567
6685
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
@@ -6590,15 +6708,15 @@ function DatePickerTimeWheelColumn({
6590
6708
  tabIndex: 0,
6591
6709
  onKeyDown: handleKeyDown,
6592
6710
  children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("ul", { className: "aviala-datepicker-time__wheel-list", children: repeatedValues.map((itemValue, index) => {
6593
- const isSelected = itemValue === value;
6711
+ const isCommitted = itemValue === value;
6594
6712
  const optionId = loop ? `${ariaLabel}-${itemValue}-${index}` : `${ariaLabel}-${itemValue}`;
6595
6713
  return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
6596
6714
  "li",
6597
6715
  {
6598
- id: optionId,
6716
+ id: loop && isCommitted && Math.floor(index / values.length) === MIDDLE_SECTION ? selectedOptionId : optionId,
6599
6717
  className: "aviala-datepicker-time__wheel-item",
6600
6718
  role: "option",
6601
- "aria-selected": isSelected,
6719
+ "aria-selected": isCommitted,
6602
6720
  children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
6603
6721
  "button",
6604
6722
  {
@@ -6607,10 +6725,12 @@ function DatePickerTimeWheelColumn({
6607
6725
  "aviala-datepicker-time__wheel-option",
6608
6726
  typographyVariants({ level: "text" })
6609
6727
  ),
6610
- "data-selected": isSelected ? "true" : void 0,
6728
+ "data-selected": isCommitted ? "true" : void 0,
6611
6729
  tabIndex: -1,
6612
6730
  onClick: () => {
6613
- scrollToValue(itemValue, "smooth");
6731
+ isUserScrollingRef.current = false;
6732
+ smoothTargetValueRef.current = itemValue;
6733
+ scrollToIndex(index, "smooth");
6614
6734
  if (itemValue !== selectedValueRef.current) {
6615
6735
  onChange(itemValue);
6616
6736
  }
@@ -6623,7 +6743,18 @@ function DatePickerTimeWheelColumn({
6623
6743
  );
6624
6744
  }) })
6625
6745
  }
6626
- )
6746
+ ),
6747
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "aviala-datepicker-time__wheel-clip", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { ref: clipTrackRef, className: "aviala-datepicker-time__wheel-clip-track", children: repeatedValues.map((itemValue, index) => /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
6748
+ "div",
6749
+ {
6750
+ className: cn(
6751
+ "aviala-datepicker-time__wheel-clip-item",
6752
+ typographyVariants({ level: "text" })
6753
+ ),
6754
+ children: formatValue(itemValue)
6755
+ },
6756
+ `clip-${loop ? `${itemValue}-${index}` : itemValue}`
6757
+ )) }) })
6627
6758
  ] });
6628
6759
  }
6629
6760
 
@@ -7359,8 +7490,8 @@ function DatePickerCalendar({ className }) {
7359
7490
  const { code } = useLocale();
7360
7491
  const locale = useLocaleMessages("DatePicker");
7361
7492
  const rtl = useRtl();
7362
- const PrevIcon = rtl ? import_icons12.DirectionArrowRight : import_icons12.DirectionArrowLeft;
7363
- const NextIcon = rtl ? import_icons12.DirectionArrowLeft : import_icons12.DirectionArrowRight;
7493
+ const PrevIcon = rtl ? import_icons12.DirectionArrowRightLight : import_icons12.DirectionArrowLeftLight;
7494
+ const NextIcon = rtl ? import_icons12.DirectionArrowLeftLight : import_icons12.DirectionArrowRightLight;
7364
7495
  const {
7365
7496
  mode,
7366
7497
  viewMonth,
@@ -7382,6 +7513,10 @@ function DatePickerCalendar({ className }) {
7382
7513
  const dayGridRef = (0, import_react45.useRef)(null);
7383
7514
  const pendingDayFocusRef = (0, import_react45.useRef)(false);
7384
7515
  const [monthSlide, setMonthSlide] = (0, import_react45.useState)(null);
7516
+ const [prevNavAnimKey, setPrevNavAnimKey] = (0, import_react45.useState)(0);
7517
+ const [nextNavAnimKey, setNextNavAnimKey] = (0, import_react45.useState)(0);
7518
+ const [prevNavAnimating, setPrevNavAnimating] = (0, import_react45.useState)(false);
7519
+ const [nextNavAnimating, setNextNavAnimating] = (0, import_react45.useState)(false);
7385
7520
  const isMonthPanel = activePanel === "month";
7386
7521
  const isTimePanel = activePanel === "time" && enableTime;
7387
7522
  const targetContentView = isMonthPanel ? "month" : "date";
@@ -7632,8 +7767,23 @@ function DatePickerCalendar({ className }) {
7632
7767
  type: "button",
7633
7768
  className: "aviala-datepicker-calendar__nav aviala-focus-ring",
7634
7769
  "aria-label": isMonthPanel ? locale.previousYear : locale.previousMonth,
7635
- onClick: () => setViewMonth(addMonths(viewMonth, -stepMonth)),
7636
- children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(PrevIcon, { level: "text", biggerSize: true, "aria-hidden": true })
7770
+ onClick: () => {
7771
+ if (!prefersReducedMotion2()) {
7772
+ setPrevNavAnimKey((n) => n + 1);
7773
+ setPrevNavAnimating(true);
7774
+ }
7775
+ setViewMonth(addMonths(viewMonth, -stepMonth));
7776
+ },
7777
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "aviala-datepicker-calendar__nav-icon", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
7778
+ "span",
7779
+ {
7780
+ className: "aviala-datepicker-calendar__nav-icon-inner",
7781
+ "data-anim": prevNavAnimating ? "prev" : void 0,
7782
+ onAnimationEnd: () => setPrevNavAnimating(false),
7783
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(PrevIcon, { level: "text", biggerSize: true, "aria-hidden": true })
7784
+ },
7785
+ prevNavAnimKey
7786
+ ) })
7637
7787
  }
7638
7788
  ),
7639
7789
  /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
@@ -7666,8 +7816,23 @@ function DatePickerCalendar({ className }) {
7666
7816
  type: "button",
7667
7817
  className: "aviala-datepicker-calendar__nav aviala-focus-ring",
7668
7818
  "aria-label": isMonthPanel ? locale.nextYear : locale.nextMonth,
7669
- onClick: () => setViewMonth(addMonths(viewMonth, stepMonth)),
7670
- children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(NextIcon, { level: "text", biggerSize: true, "aria-hidden": true })
7819
+ onClick: () => {
7820
+ if (!prefersReducedMotion2()) {
7821
+ setNextNavAnimKey((n) => n + 1);
7822
+ setNextNavAnimating(true);
7823
+ }
7824
+ setViewMonth(addMonths(viewMonth, stepMonth));
7825
+ },
7826
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "aviala-datepicker-calendar__nav-icon", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
7827
+ "span",
7828
+ {
7829
+ className: "aviala-datepicker-calendar__nav-icon-inner",
7830
+ "data-anim": nextNavAnimating ? "next" : void 0,
7831
+ onAnimationEnd: () => setNextNavAnimating(false),
7832
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(NextIcon, { level: "text", biggerSize: true, "aria-hidden": true })
7833
+ },
7834
+ nextNavAnimKey
7835
+ ) })
7671
7836
  }
7672
7837
  )
7673
7838
  ] }),
@@ -10444,11 +10609,8 @@ Upload.displayName = "Upload";
10444
10609
  // src/components/scroll-picker.tsx
10445
10610
  var import_react60 = require("react");
10446
10611
  var import_jsx_runtime57 = require("react/jsx-runtime");
10447
- var LOOP_SECTIONS2 = 3;
10448
- var MIDDLE_SECTION2 = 1;
10449
- function normalizeIndex2(index, length) {
10450
- return (index % length + length) % length;
10451
- }
10612
+ var LOOP_SECTIONS2 = WHEEL_LOOP_SECTIONS;
10613
+ var MIDDLE_SECTION2 = WHEEL_LOOP_MIDDLE_SECTION;
10452
10614
  function readMetrics(container) {
10453
10615
  const items = container.querySelectorAll(".aviala-scroll-picker-item");
10454
10616
  const first = items[0];
@@ -10470,6 +10632,9 @@ function scrollTopForIndex(container, metrics, index) {
10470
10632
  function fractionalIndexAtScrollTop(metrics, scrollTop) {
10471
10633
  return (scrollTop - metrics.originTop) / metrics.pitch;
10472
10634
  }
10635
+ function readRawIndex(container, metrics) {
10636
+ return Math.round(fractionalIndexAtScrollTop(metrics, container.scrollTop));
10637
+ }
10473
10638
  function ScrollPicker({ className, children, ...props }) {
10474
10639
  return /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("div", { className: cn("aviala-scroll-picker", className), ...props, children });
10475
10640
  }
@@ -10484,11 +10649,13 @@ function ScrollPickerColumn({
10484
10649
  getValueKey
10485
10650
  }) {
10486
10651
  const scrollRef = (0, import_react60.useRef)(null);
10652
+ const clipTrackRef = (0, import_react60.useRef)(null);
10487
10653
  const isUserScrollingRef = (0, import_react60.useRef)(false);
10488
10654
  const isProgrammaticScrollRef = (0, import_react60.useRef)(false);
10489
10655
  const smoothTargetValueRef = (0, import_react60.useRef)(null);
10490
10656
  const scrollEndTimerRef = (0, import_react60.useRef)(void 0);
10491
10657
  const wheelAccumRef = (0, import_react60.useRef)(0);
10658
+ const wheelAccumIdleTimerRef = (0, import_react60.useRef)(void 0);
10492
10659
  const selectedValueRef = (0, import_react60.useRef)(value);
10493
10660
  const reactId = (0, import_react60.useId)();
10494
10661
  selectedValueRef.current = value;
@@ -10503,36 +10670,43 @@ function ScrollPickerColumn({
10503
10670
  if (!loop) return [...values];
10504
10671
  return Array.from({ length: LOOP_SECTIONS2 }, () => values).flat();
10505
10672
  }, [loop, values]);
10506
- const scrollToIndex = (0, import_react60.useCallback)((index, behavior = "auto") => {
10673
+ const syncClipTrack = (0, import_react60.useCallback)(() => {
10507
10674
  const container = scrollRef.current;
10508
- if (!container) return;
10509
- const metrics = readMetrics(container);
10510
- if (!metrics) return;
10511
- isProgrammaticScrollRef.current = true;
10512
- container.scrollTo({ top: scrollTopForIndex(container, metrics, index), behavior });
10675
+ const track = clipTrackRef.current;
10676
+ if (!container || !track) return;
10677
+ const band = container.parentElement?.querySelector(
10678
+ ".aviala-scroll-picker-column__highlight"
10679
+ );
10680
+ const bandHeight = band?.offsetHeight || 26;
10681
+ const offset = container.scrollTop + (container.clientHeight - bandHeight) / 2;
10682
+ track.style.transform = `translate3d(0, ${-offset}px, 0)`;
10513
10683
  }, []);
10514
- const scrollToValue = (0, import_react60.useCallback)(
10515
- (nextValue, behavior = "smooth") => {
10516
- if (behavior === "smooth") {
10517
- smoothTargetValueRef.current = nextValue;
10684
+ const scrollToIndex = (0, import_react60.useCallback)(
10685
+ (index, behavior = "auto") => {
10686
+ const container = scrollRef.current;
10687
+ if (!container) return;
10688
+ const metrics = readMetrics(container);
10689
+ if (!metrics) return;
10690
+ isProgrammaticScrollRef.current = true;
10691
+ container.scrollTo({ top: scrollTopForIndex(container, metrics, index), behavior });
10692
+ if (behavior === "auto") {
10693
+ syncClipTrack();
10518
10694
  } else {
10519
- smoothTargetValueRef.current = null;
10695
+ requestAnimationFrame(syncClipTrack);
10520
10696
  }
10521
- const valueIndex = getIndex(nextValue);
10522
- const targetIndex = loop ? values.length * MIDDLE_SECTION2 + valueIndex : valueIndex;
10523
- scrollToIndex(targetIndex, behavior);
10524
10697
  },
10525
- [getIndex, loop, scrollToIndex, values.length]
10698
+ [syncClipTrack]
10526
10699
  );
10527
10700
  const settleScroll = (0, import_react60.useCallback)(() => {
10528
10701
  const container = scrollRef.current;
10529
- if (!container || isProgrammaticScrollRef.current) {
10530
- isProgrammaticScrollRef.current = false;
10702
+ if (!container) return;
10703
+ const wasProgrammatic = isProgrammaticScrollRef.current;
10704
+ isProgrammaticScrollRef.current = false;
10705
+ const metrics = readMetrics(container);
10706
+ if (!metrics || values.length === 0) {
10531
10707
  smoothTargetValueRef.current = null;
10532
10708
  return;
10533
10709
  }
10534
- const metrics = readMetrics(container);
10535
- if (!metrics || values.length === 0) return;
10536
10710
  const lastIndex = values.length - 1;
10537
10711
  const maxScroll = Math.max(container.scrollHeight - container.clientHeight, 0);
10538
10712
  let rawIndex = stickyRoundIndex(
@@ -10546,12 +10720,21 @@ function ScrollPickerColumn({
10546
10720
  rawIndex = 0;
10547
10721
  }
10548
10722
  }
10549
- const valueIndex = loop ? normalizeIndex2(rawIndex, values.length) : Math.min(Math.max(rawIndex, 0), lastIndex);
10723
+ const valueIndex = loop ? normalizeWheelIndex(rawIndex, values.length) : Math.min(Math.max(rawIndex, 0), lastIndex);
10550
10724
  const nextValue = values[valueIndex];
10551
- const targetIndex = loop ? values.length * MIDDLE_SECTION2 + valueIndex : valueIndex;
10725
+ const targetIndex = loop ? recenterLoopIndex(rawIndex, values.length, MIDDLE_SECTION2) : valueIndex;
10552
10726
  const targetTop = scrollTopForIndex(container, metrics, targetIndex);
10553
10727
  const section = loop ? Math.floor(rawIndex / values.length) : MIDDLE_SECTION2;
10554
10728
  const needsLoopRecenter = loop && section !== MIDDLE_SECTION2;
10729
+ if (wasProgrammatic) {
10730
+ if (needsLoopRecenter) {
10731
+ isProgrammaticScrollRef.current = true;
10732
+ container.scrollTop = targetTop;
10733
+ }
10734
+ smoothTargetValueRef.current = null;
10735
+ syncClipTrack();
10736
+ return;
10737
+ }
10555
10738
  const needsSnap = Math.abs(container.scrollTop - targetTop) > 1;
10556
10739
  if (needsLoopRecenter) {
10557
10740
  isProgrammaticScrollRef.current = true;
@@ -10569,15 +10752,17 @@ function ScrollPickerColumn({
10569
10752
  }
10570
10753
  isUserScrollingRef.current = false;
10571
10754
  wheelAccumRef.current = 0;
10572
- }, [loop, onChange, values]);
10755
+ syncClipTrack();
10756
+ }, [loop, onChange, syncClipTrack, values]);
10573
10757
  const handleScroll = (0, import_react60.useCallback)(() => {
10758
+ syncClipTrack();
10574
10759
  if (isProgrammaticScrollRef.current) return;
10575
10760
  isUserScrollingRef.current = true;
10576
10761
  if (scrollEndTimerRef.current) {
10577
10762
  clearTimeout(scrollEndTimerRef.current);
10578
10763
  }
10579
10764
  scrollEndTimerRef.current = setTimeout(settleScroll, WHEEL_SCROLL_END_MS);
10580
- }, [settleScroll]);
10765
+ }, [settleScroll, syncClipTrack]);
10581
10766
  (0, import_react60.useLayoutEffect)(() => {
10582
10767
  if (isUserScrollingRef.current) return;
10583
10768
  if (smoothTargetValueRef.current != null && Object.is(smoothTargetValueRef.current, value)) {
@@ -10588,11 +10773,20 @@ function ScrollPickerColumn({
10588
10773
  const metrics = readMetrics(container);
10589
10774
  if (!metrics) return;
10590
10775
  const valueIndex = getIndex(value);
10591
- const targetIndex = loop ? values.length * MIDDLE_SECTION2 + valueIndex : valueIndex;
10776
+ const currentRaw = readRawIndex(container, metrics);
10777
+ const currentValueIndex = loop ? normalizeWheelIndex(currentRaw, values.length) : Math.min(Math.max(currentRaw, 0), values.length - 1);
10778
+ if (currentValueIndex === valueIndex) {
10779
+ syncClipTrack();
10780
+ return;
10781
+ }
10782
+ const targetIndex = loop ? nearestLoopIndex(currentRaw, valueIndex, values.length, LOOP_SECTIONS2) : valueIndex;
10592
10783
  const targetTop = scrollTopForIndex(container, metrics, targetIndex);
10593
- if (Math.abs(container.scrollTop - targetTop) <= 1) return;
10784
+ if (Math.abs(container.scrollTop - targetTop) <= 1) {
10785
+ syncClipTrack();
10786
+ return;
10787
+ }
10594
10788
  scrollToIndex(targetIndex, "auto");
10595
- }, [getIndex, loop, scrollToIndex, value, values.length]);
10789
+ }, [getIndex, loop, scrollToIndex, syncClipTrack, value, values.length]);
10596
10790
  (0, import_react60.useEffect)(() => {
10597
10791
  const container = scrollRef.current;
10598
10792
  if (!container) return;
@@ -10615,23 +10809,52 @@ function ScrollPickerColumn({
10615
10809
  if (!container) return;
10616
10810
  const onWheel = (event) => {
10617
10811
  event.preventDefault();
10618
- if (isProgrammaticScrollRef.current || values.length === 0) return;
10812
+ if (values.length === 0) return;
10619
10813
  const metrics = readMetrics(container);
10620
10814
  if (!metrics) return;
10621
- wheelAccumRef.current += event.deltaY;
10815
+ if (wheelAccumIdleTimerRef.current) {
10816
+ clearTimeout(wheelAccumIdleTimerRef.current);
10817
+ }
10818
+ wheelAccumIdleTimerRef.current = setTimeout(() => {
10819
+ wheelAccumRef.current = 0;
10820
+ }, WHEEL_ACCUM_IDLE_MS);
10622
10821
  const threshold = metrics.pitch * WHEEL_STEP_THRESHOLD_FACTOR;
10623
- if (Math.abs(wheelAccumRef.current) < threshold) return;
10624
- const direction = wheelAccumRef.current > 0 ? 1 : -1;
10625
- wheelAccumRef.current = 0;
10626
- const currentIndex = getIndex(selectedValueRef.current);
10627
- const nextIndex = loop ? normalizeIndex2(currentIndex + direction, values.length) : Math.min(Math.max(currentIndex + direction, 0), values.length - 1);
10628
- if (nextIndex === currentIndex) return;
10822
+ const { nextAccum, steps } = consumeWheelSteps(
10823
+ wheelAccumRef.current,
10824
+ event.deltaY,
10825
+ threshold
10826
+ );
10827
+ wheelAccumRef.current = nextAccum;
10828
+ if (steps === 0) return;
10829
+ const currentRaw = readRawIndex(container, metrics);
10830
+ let targetRaw;
10831
+ if (loop) {
10832
+ if (Math.floor(currentRaw / values.length) !== MIDDLE_SECTION2) {
10833
+ isProgrammaticScrollRef.current = true;
10834
+ container.scrollTop = scrollTopForIndex(
10835
+ container,
10836
+ metrics,
10837
+ recenterLoopIndex(currentRaw, values.length, MIDDLE_SECTION2)
10838
+ );
10839
+ }
10840
+ targetRaw = stepLoopRawIndex(
10841
+ readRawIndex(container, metrics),
10842
+ steps,
10843
+ values.length,
10844
+ LOOP_SECTIONS2,
10845
+ MIDDLE_SECTION2
10846
+ ).rawIndex;
10847
+ } else {
10848
+ targetRaw = Math.min(Math.max(currentRaw + steps, 0), values.length - 1);
10849
+ if (targetRaw === currentRaw) return;
10850
+ }
10629
10851
  if (scrollEndTimerRef.current) {
10630
10852
  clearTimeout(scrollEndTimerRef.current);
10631
10853
  }
10632
- const nextValue = values[nextIndex];
10854
+ const nextValue = values[normalizeWheelIndex(targetRaw, values.length)];
10633
10855
  isUserScrollingRef.current = false;
10634
- scrollToValue(nextValue, "smooth");
10856
+ smoothTargetValueRef.current = nextValue;
10857
+ scrollToIndex(targetRaw, "smooth");
10635
10858
  if (!Object.is(nextValue, selectedValueRef.current)) {
10636
10859
  onChange(nextValue);
10637
10860
  }
@@ -10639,30 +10862,39 @@ function ScrollPickerColumn({
10639
10862
  container.addEventListener("wheel", onWheel, { passive: false });
10640
10863
  return () => {
10641
10864
  container.removeEventListener("wheel", onWheel);
10865
+ if (wheelAccumIdleTimerRef.current) {
10866
+ clearTimeout(wheelAccumIdleTimerRef.current);
10867
+ }
10642
10868
  };
10643
- }, [getIndex, loop, onChange, scrollToValue, values]);
10869
+ }, [loop, onChange, scrollToIndex, values]);
10644
10870
  const handleKeyDown = (event) => {
10645
- const currentIndex = getIndex(selectedValueRef.current);
10646
- let nextIndex = null;
10871
+ const container = scrollRef.current;
10872
+ if (!container || values.length === 0) return;
10873
+ const metrics = readMetrics(container);
10874
+ if (!metrics) return;
10875
+ const currentRaw = readRawIndex(container, metrics);
10876
+ let targetRaw = null;
10647
10877
  switch (event.key) {
10648
10878
  case "ArrowUp":
10649
- nextIndex = loop ? normalizeIndex2(currentIndex - 1, values.length) : Math.max(currentIndex - 1, 0);
10879
+ targetRaw = loop ? stepLoopRawIndex(currentRaw, -1, values.length, LOOP_SECTIONS2, MIDDLE_SECTION2).rawIndex : Math.max(currentRaw - 1, 0);
10650
10880
  break;
10651
10881
  case "ArrowDown":
10652
- nextIndex = loop ? normalizeIndex2(currentIndex + 1, values.length) : Math.min(currentIndex + 1, values.length - 1);
10882
+ targetRaw = loop ? stepLoopRawIndex(currentRaw, 1, values.length, LOOP_SECTIONS2, MIDDLE_SECTION2).rawIndex : Math.min(currentRaw + 1, values.length - 1);
10653
10883
  break;
10654
10884
  case "Home":
10655
- nextIndex = 0;
10885
+ targetRaw = loop ? nearestLoopIndex(currentRaw, 0, values.length, LOOP_SECTIONS2, -1) : 0;
10656
10886
  break;
10657
10887
  case "End":
10658
- nextIndex = values.length - 1;
10888
+ targetRaw = loop ? nearestLoopIndex(currentRaw, values.length - 1, values.length, LOOP_SECTIONS2, 1) : values.length - 1;
10659
10889
  break;
10660
10890
  default:
10661
10891
  return;
10662
10892
  }
10663
10893
  event.preventDefault();
10664
- const nextValue = values[nextIndex];
10665
- scrollToValue(nextValue, "smooth");
10894
+ const nextValue = values[normalizeWheelIndex(targetRaw, values.length)];
10895
+ isUserScrollingRef.current = false;
10896
+ smoothTargetValueRef.current = nextValue;
10897
+ scrollToIndex(targetRaw, "smooth");
10666
10898
  if (!Object.is(nextValue, selectedValueRef.current)) {
10667
10899
  onChange(nextValue);
10668
10900
  }
@@ -10682,17 +10914,20 @@ function ScrollPickerColumn({
10682
10914
  tabIndex: 0,
10683
10915
  onKeyDown: handleKeyDown,
10684
10916
  children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("ul", { className: "aviala-scroll-picker-column__list", children: repeatedValues.map((itemValue, index) => {
10685
- const isSelected = Object.is(itemValue, value);
10686
- const valueIndex = loop ? normalizeIndex2(index, values.length) : index;
10917
+ const isCommitted = Object.is(itemValue, value);
10918
+ const valueIndex = loop ? normalizeWheelIndex(index, values.length) : index;
10687
10919
  const optionId = `${reactId}-${loop ? index : valueIndex}`;
10688
10920
  const key = getValueKey?.(itemValue, index) ?? `${String(itemValue)}-${index}`;
10921
+ const useSelectedId = isCommitted && (!loop || Math.floor(index / values.length) === MIDDLE_SECTION2);
10689
10922
  return /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("li", { className: "contents", children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
10690
10923
  ScrollPickerItem,
10691
10924
  {
10692
- id: isSelected ? selectedOptionId : optionId,
10693
- selected: isSelected,
10925
+ id: useSelectedId ? selectedOptionId : optionId,
10926
+ selected: isCommitted,
10694
10927
  onSelect: () => {
10695
- scrollToValue(itemValue, "smooth");
10928
+ isUserScrollingRef.current = false;
10929
+ smoothTargetValueRef.current = itemValue;
10930
+ scrollToIndex(index, "smooth");
10696
10931
  if (!Object.is(itemValue, selectedValueRef.current)) {
10697
10932
  onChange(itemValue);
10698
10933
  }
@@ -10702,7 +10937,21 @@ function ScrollPickerColumn({
10702
10937
  ) }, key);
10703
10938
  }) })
10704
10939
  }
10705
- )
10940
+ ),
10941
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("div", { className: "aviala-scroll-picker-column__clip", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("div", { ref: clipTrackRef, className: "aviala-scroll-picker-column__clip-track", children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("div", { className: "aviala-scroll-picker-column__list", children: repeatedValues.map((itemValue, index) => {
10942
+ const key = getValueKey?.(itemValue, index) ?? `clip-${String(itemValue)}-${index}`;
10943
+ return /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
10944
+ "div",
10945
+ {
10946
+ className: cn(
10947
+ "aviala-scroll-picker-column__clip-item",
10948
+ typographyVariants({ level: "text" })
10949
+ ),
10950
+ children: formatValue ? formatValue(itemValue) : String(itemValue)
10951
+ },
10952
+ key
10953
+ );
10954
+ }) }) }) })
10706
10955
  ] }) });
10707
10956
  }
10708
10957
  function ScrollPickerItem({