xiod-ui 1.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/LICENSE +73 -0
- package/README.md +167 -0
- package/dist/components/accordion.d.ts +9 -0
- package/dist/components/accordion.js +46 -0
- package/dist/components/agent-steps.d.ts +47 -0
- package/dist/components/agent-steps.js +223 -0
- package/dist/components/alert-dialog.d.ts +21 -0
- package/dist/components/alert-dialog.js +74 -0
- package/dist/components/alert.d.ts +12 -0
- package/dist/components/alert.js +45 -0
- package/dist/components/aspect-ratio.d.ts +9 -0
- package/dist/components/aspect-ratio.js +22 -0
- package/dist/components/autocomplete.d.ts +35 -0
- package/dist/components/autocomplete.js +160 -0
- package/dist/components/avatar.d.ts +17 -0
- package/dist/components/avatar.js +65 -0
- package/dist/components/badge.d.ts +15 -0
- package/dist/components/badge.js +46 -0
- package/dist/components/breadcrumb.d.ts +12 -0
- package/dist/components/breadcrumb.js +75 -0
- package/dist/components/button-group.d.ts +14 -0
- package/dist/components/button-group.js +45 -0
- package/dist/components/button-split.d.ts +19 -0
- package/dist/components/button-split.js +73 -0
- package/dist/components/button.d.ts +15 -0
- package/dist/components/button.js +54 -0
- package/dist/components/calendar.d.ts +63 -0
- package/dist/components/calendar.js +425 -0
- package/dist/components/card.d.ts +17 -0
- package/dist/components/card.js +139 -0
- package/dist/components/carousel.d.ts +59 -0
- package/dist/components/carousel.js +503 -0
- package/dist/components/checkbox.d.ts +16 -0
- package/dist/components/checkbox.js +86 -0
- package/dist/components/circular-progress.d.ts +28 -0
- package/dist/components/circular-progress.js +127 -0
- package/dist/components/collapsible.d.ts +8 -0
- package/dist/components/collapsible.js +27 -0
- package/dist/components/color-picker.d.ts +159 -0
- package/dist/components/color-picker.js +1589 -0
- package/dist/components/combobox.d.ts +46 -0
- package/dist/components/combobox.js +237 -0
- package/dist/components/command.d.ts +22 -0
- package/dist/components/command.js +129 -0
- package/dist/components/context-menu.d.ts +38 -0
- package/dist/components/context-menu.js +164 -0
- package/dist/components/copy-to-clipboard.d.ts +32 -0
- package/dist/components/copy-to-clipboard.js +83 -0
- package/dist/components/corner-badge.d.ts +22 -0
- package/dist/components/corner-badge.js +84 -0
- package/dist/components/dashboard-grid.d.ts +83 -0
- package/dist/components/dashboard-grid.js +486 -0
- package/dist/components/date-picker.d.ts +29 -0
- package/dist/components/date-picker.js +261 -0
- package/dist/components/dialog.d.ts +26 -0
- package/dist/components/dialog.js +97 -0
- package/dist/components/dot-matrix.d.ts +32 -0
- package/dist/components/dot-matrix.js +707 -0
- package/dist/components/draggable.d.ts +42 -0
- package/dist/components/draggable.js +346 -0
- package/dist/components/drawer.d.ts +66 -0
- package/dist/components/drawer.js +304 -0
- package/dist/components/empty.d.ts +14 -0
- package/dist/components/empty.js +75 -0
- package/dist/components/field.d.ts +12 -0
- package/dist/components/field.js +44 -0
- package/dist/components/fieldset.d.ts +7 -0
- package/dist/components/fieldset.js +21 -0
- package/dist/components/file-upload.d.ts +61 -0
- package/dist/components/file-upload.js +482 -0
- package/dist/components/form.d.ts +6 -0
- package/dist/components/form.js +14 -0
- package/dist/components/frame.d.ts +10 -0
- package/dist/components/frame.js +47 -0
- package/dist/components/gauge.d.ts +27 -0
- package/dist/components/gauge.js +74 -0
- package/dist/components/grid.d.ts +26 -0
- package/dist/components/grid.js +204 -0
- package/dist/components/group.d.ts +19 -0
- package/dist/components/group.js +45 -0
- package/dist/components/input-group.d.ts +15 -0
- package/dist/components/input-group.js +61 -0
- package/dist/components/input-otp.d.ts +24 -0
- package/dist/components/input-otp.js +50 -0
- package/dist/components/input-payment.d.ts +131 -0
- package/dist/components/input-payment.js +1140 -0
- package/dist/components/input-phone.d.ts +70 -0
- package/dist/components/input-phone.js +657 -0
- package/dist/components/input-sensitive.d.ts +12 -0
- package/dist/components/input-sensitive.js +169 -0
- package/dist/components/input.d.ts +13 -0
- package/dist/components/input.js +26 -0
- package/dist/components/kbd.d.ts +6 -0
- package/dist/components/kbd.js +19 -0
- package/dist/components/kinetic-click.d.ts +48 -0
- package/dist/components/kinetic-click.js +1184 -0
- package/dist/components/label.d.ts +6 -0
- package/dist/components/label.js +18 -0
- package/dist/components/list-box.d.ts +20 -0
- package/dist/components/list-box.js +148 -0
- package/dist/components/loader.d.ts +13 -0
- package/dist/components/loader.js +430 -0
- package/dist/components/marker.d.ts +15 -0
- package/dist/components/marker.js +59 -0
- package/dist/components/menu.d.ts +42 -0
- package/dist/components/menu.js +186 -0
- package/dist/components/menubar.d.ts +40 -0
- package/dist/components/menubar.js +107 -0
- package/dist/components/message.d.ts +33 -0
- package/dist/components/message.js +112 -0
- package/dist/components/meter.d.ts +10 -0
- package/dist/components/meter.js +42 -0
- package/dist/components/morphic-toast.d.ts +61 -0
- package/dist/components/morphic-toast.js +916 -0
- package/dist/components/navigation-menu.d.ts +26 -0
- package/dist/components/navigation-menu.js +116 -0
- package/dist/components/number-field.d.ts +15 -0
- package/dist/components/number-field.js +86 -0
- package/dist/components/option-picker.d.ts +18 -0
- package/dist/components/option-picker.js +89 -0
- package/dist/components/orb.d.ts +38 -0
- package/dist/components/orb.js +1413 -0
- package/dist/components/pagination.d.ts +25 -0
- package/dist/components/pagination.js +147 -0
- package/dist/components/popover.d.ts +22 -0
- package/dist/components/popover.js +79 -0
- package/dist/components/preview-card.d.ts +16 -0
- package/dist/components/preview-card.js +60 -0
- package/dist/components/progress.d.ts +21 -0
- package/dist/components/progress.js +139 -0
- package/dist/components/radio.d.ts +16 -0
- package/dist/components/radio.js +68 -0
- package/dist/components/resizable.d.ts +58 -0
- package/dist/components/resizable.js +964 -0
- package/dist/components/ruler-picker.d.ts +27 -0
- package/dist/components/ruler-picker.js +253 -0
- package/dist/components/scroll-area.d.ts +13 -0
- package/dist/components/scroll-area.js +34 -0
- package/dist/components/scroll-bar.d.ts +13 -0
- package/dist/components/scroll-bar.js +47 -0
- package/dist/components/select.d.ts +29 -0
- package/dist/components/select.js +140 -0
- package/dist/components/separator.d.ts +6 -0
- package/dist/components/separator.js +15 -0
- package/dist/components/sidebar.d.ts +65 -0
- package/dist/components/sidebar.js +408 -0
- package/dist/components/skeleton.d.ts +5 -0
- package/dist/components/skeleton.js +12 -0
- package/dist/components/slider.d.ts +12 -0
- package/dist/components/slider.js +96 -0
- package/dist/components/sortable.d.ts +44 -0
- package/dist/components/sortable.js +327 -0
- package/dist/components/switch.d.ts +15 -0
- package/dist/components/switch.js +63 -0
- package/dist/components/table.d.ts +12 -0
- package/dist/components/table.js +65 -0
- package/dist/components/tabs.d.ts +12 -0
- package/dist/components/tabs.js +39 -0
- package/dist/components/text.d.ts +14 -0
- package/dist/components/text.js +69 -0
- package/dist/components/textarea.d.ts +9 -0
- package/dist/components/textarea.js +20 -0
- package/dist/components/theme-provider.d.ts +52 -0
- package/dist/components/theme-provider.js +148 -0
- package/dist/components/timeline.d.ts +34 -0
- package/dist/components/timeline.js +147 -0
- package/dist/components/toast.d.ts +17 -0
- package/dist/components/toast.js +161 -0
- package/dist/components/toggle-group.d.ts +14 -0
- package/dist/components/toggle-group.js +53 -0
- package/dist/components/toggle.d.ts +11 -0
- package/dist/components/toggle.js +36 -0
- package/dist/components/toolbar.d.ts +11 -0
- package/dist/components/toolbar.js +49 -0
- package/dist/components/tooltip.d.ts +17 -0
- package/dist/components/tooltip.js +56 -0
- package/dist/components/waveform.d.ts +61 -0
- package/dist/components/waveform.js +500 -0
- package/dist/components/wheel-picker.d.ts +44 -0
- package/dist/components/wheel-picker.js +771 -0
- package/dist/hooks/use-copy-to-clipboard.d.ts +9 -0
- package/dist/hooks/use-copy-to-clipboard.js +36 -0
- package/dist/hooks/use-media-query.d.ts +21 -0
- package/dist/hooks/use-media-query.js +58 -0
- package/dist/styles.css +202 -0
- package/dist/themes/afterglow.css +89 -0
- package/dist/themes/afterglow.scoped.css +90 -0
- package/dist/themes/blacklight.css +89 -0
- package/dist/themes/blacklight.scoped.css +90 -0
- package/dist/themes/cathode.css +89 -0
- package/dist/themes/cathode.scoped.css +90 -0
- package/dist/themes/cel.css +89 -0
- package/dist/themes/cel.scoped.css +90 -0
- package/dist/themes/cinder.css +89 -0
- package/dist/themes/cinder.scoped.css +90 -0
- package/dist/themes/clay.css +89 -0
- package/dist/themes/clay.scoped.css +90 -0
- package/dist/themes/cobalt.css +89 -0
- package/dist/themes/cobalt.scoped.css +90 -0
- package/dist/themes/crimson.css +89 -0
- package/dist/themes/crimson.scoped.css +90 -0
- package/dist/themes/ion.css +89 -0
- package/dist/themes/ion.scoped.css +90 -0
- package/dist/themes/iris.css +89 -0
- package/dist/themes/iris.scoped.css +90 -0
- package/dist/themes/lagoon.css +89 -0
- package/dist/themes/lagoon.scoped.css +90 -0
- package/dist/themes/mochi.css +89 -0
- package/dist/themes/mochi.scoped.css +90 -0
- package/dist/themes/riso.css +89 -0
- package/dist/themes/riso.scoped.css +90 -0
- package/dist/themes/roast.css +89 -0
- package/dist/themes/roast.scoped.css +90 -0
- package/dist/themes/sage.css +89 -0
- package/dist/themes/sage.scoped.css +90 -0
- package/dist/themes/sepia.css +89 -0
- package/dist/themes/sepia.scoped.css +90 -0
- package/dist/themes/sorbet.css +89 -0
- package/dist/themes/sorbet.scoped.css +90 -0
- package/dist/themes/taffy.css +89 -0
- package/dist/themes/taffy.scoped.css +90 -0
- package/dist/themes/toxin.css +89 -0
- package/dist/themes/toxin.scoped.css +90 -0
- package/package.json +140 -0
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "cn";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { cva } from "class-variance-authority";
|
|
5
|
+
import * as React$1 from "react";
|
|
6
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
//#region src/components/wheel-picker.tsx
|
|
8
|
+
const RESISTANCE = .3;
|
|
9
|
+
const MAX_VELOCITY = 30;
|
|
10
|
+
const easeOutCubic = (p) => (p - 1) ** 3 + 1;
|
|
11
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
|
|
12
|
+
const WheelPickerGroupContext = React$1.createContext(null);
|
|
13
|
+
function useWheelPickerGroup() {
|
|
14
|
+
return React$1.useContext(WheelPickerGroupContext);
|
|
15
|
+
}
|
|
16
|
+
function WheelPickerGroup({ className, children, ...props }) {
|
|
17
|
+
const [activeIndex, setActiveIndex] = useState(-1);
|
|
18
|
+
const counterRef = useRef(0);
|
|
19
|
+
const pickerRefsRef = useRef(/* @__PURE__ */ new Map());
|
|
20
|
+
const register = useCallback((existingIndex, ref) => {
|
|
21
|
+
const index = existingIndex !== null ? existingIndex : counterRef.current++;
|
|
22
|
+
pickerRefsRef.current.set(index, ref);
|
|
23
|
+
setActiveIndex((current) => current === -1 ? index : current);
|
|
24
|
+
return index;
|
|
25
|
+
}, []);
|
|
26
|
+
const getPickerRef = useCallback((index) => pickerRefsRef.current.get(index) ?? null, []);
|
|
27
|
+
const getPickerIndices = useCallback(() => Array.from(pickerRefsRef.current.keys()).toSorted((a, b) => a - b), []);
|
|
28
|
+
const value = useMemo(() => ({
|
|
29
|
+
activeIndex,
|
|
30
|
+
setActiveIndex,
|
|
31
|
+
register,
|
|
32
|
+
getPickerRef,
|
|
33
|
+
getPickerIndices
|
|
34
|
+
}), [
|
|
35
|
+
activeIndex,
|
|
36
|
+
register,
|
|
37
|
+
getPickerRef,
|
|
38
|
+
getPickerIndices
|
|
39
|
+
]);
|
|
40
|
+
return /* @__PURE__ */ jsx(WheelPickerGroupContext.Provider, {
|
|
41
|
+
value,
|
|
42
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
43
|
+
className: cn("relative flex items-stretch justify-center border border-border/80 bg-background/50 rounded-2xl p-[calc(--spacing(2)-1px)] select-none shadow-xs/5 max-w-full overflow-hidden before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", className),
|
|
44
|
+
"data-slot": "wheel-picker-group",
|
|
45
|
+
...props,
|
|
46
|
+
children
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const TYPEAHEAD_TIMEOUT_MS = 600;
|
|
51
|
+
function useTypeaheadSearch(options, { getTextValue, getCurrentIndex, onMatch }) {
|
|
52
|
+
const searchBufferRef = useRef("");
|
|
53
|
+
const timeoutRef = useRef(null);
|
|
54
|
+
const resetTypeahead = useCallback(() => {
|
|
55
|
+
searchBufferRef.current = "";
|
|
56
|
+
if (timeoutRef.current) {
|
|
57
|
+
clearTimeout(timeoutRef.current);
|
|
58
|
+
timeoutRef.current = null;
|
|
59
|
+
}
|
|
60
|
+
}, []);
|
|
61
|
+
return {
|
|
62
|
+
handleTypeaheadSearch: useCallback((char) => {
|
|
63
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
64
|
+
searchBufferRef.current += char.toLowerCase();
|
|
65
|
+
const searchTerm = searchBufferRef.current;
|
|
66
|
+
const normalizedSearch = searchTerm.length > 1 && Array.from(searchTerm).every((c) => c === searchTerm[0]) ? searchTerm[0] : searchTerm;
|
|
67
|
+
const currentIndex = getCurrentIndex();
|
|
68
|
+
const shouldCycle = normalizedSearch.length === 1;
|
|
69
|
+
let matchIndex = -1;
|
|
70
|
+
if (shouldCycle) for (let i = 1; i <= options.length; i++) {
|
|
71
|
+
const index = (currentIndex + i) % options.length;
|
|
72
|
+
if (getTextValue(options[index]).toLowerCase().startsWith(normalizedSearch)) {
|
|
73
|
+
matchIndex = index;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else matchIndex = options.findIndex((option) => {
|
|
78
|
+
return getTextValue(option).toLowerCase().startsWith(normalizedSearch);
|
|
79
|
+
});
|
|
80
|
+
if (matchIndex !== -1) onMatch(matchIndex);
|
|
81
|
+
timeoutRef.current = setTimeout(() => {
|
|
82
|
+
searchBufferRef.current = "";
|
|
83
|
+
timeoutRef.current = null;
|
|
84
|
+
}, TYPEAHEAD_TIMEOUT_MS);
|
|
85
|
+
}, [
|
|
86
|
+
options,
|
|
87
|
+
getTextValue,
|
|
88
|
+
getCurrentIndex,
|
|
89
|
+
onMatch
|
|
90
|
+
]),
|
|
91
|
+
resetTypeahead
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function useCallbackRef(callback) {
|
|
95
|
+
const callbackRef = useRef(callback);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
callbackRef.current = callback;
|
|
98
|
+
});
|
|
99
|
+
return useMemo(() => ((...args) => callbackRef.current?.(...args)), []);
|
|
100
|
+
}
|
|
101
|
+
function useControllableState({ prop, defaultProp, onChange = () => {} }) {
|
|
102
|
+
const [uncontrolledProp, setUncontrolledProp] = useState(defaultProp);
|
|
103
|
+
const isControlled = prop !== void 0;
|
|
104
|
+
const value = isControlled ? prop : uncontrolledProp;
|
|
105
|
+
const handleChange = useCallbackRef(onChange);
|
|
106
|
+
return [value, useCallback((nextValue) => {
|
|
107
|
+
if (isControlled) {
|
|
108
|
+
const val = typeof nextValue === "function" ? nextValue(prop) : nextValue;
|
|
109
|
+
if (val !== prop && val !== void 0) handleChange(val);
|
|
110
|
+
} else setUncontrolledProp(nextValue);
|
|
111
|
+
}, [
|
|
112
|
+
isControlled,
|
|
113
|
+
prop,
|
|
114
|
+
handleChange
|
|
115
|
+
])];
|
|
116
|
+
}
|
|
117
|
+
const wheelPickerVariants = cva("relative flex-1 cursor-default select-none focus:outline-none focus-visible:outline-none min-w-[70px]", {
|
|
118
|
+
variants: { size: {
|
|
119
|
+
sm: "h-[180px] rounded-lg",
|
|
120
|
+
md: "h-[220px] rounded-xl",
|
|
121
|
+
lg: "h-[260px] rounded-2xl"
|
|
122
|
+
} },
|
|
123
|
+
defaultVariants: { size: "md" }
|
|
124
|
+
});
|
|
125
|
+
function findNearestEnabledIndex(startIndex, direction, options, infinite) {
|
|
126
|
+
if (options.length === 0) return startIndex;
|
|
127
|
+
if (!options.some((opt) => !opt.disabled)) return startIndex;
|
|
128
|
+
const searchInDirection = (dir) => {
|
|
129
|
+
let currentIndex = startIndex;
|
|
130
|
+
let attempts = 0;
|
|
131
|
+
const maxAttempts = options.length;
|
|
132
|
+
while (attempts < maxAttempts) {
|
|
133
|
+
currentIndex = currentIndex + dir;
|
|
134
|
+
if (infinite) currentIndex = (currentIndex % options.length + options.length) % options.length;
|
|
135
|
+
else if (currentIndex < 0 || currentIndex >= options.length) return -1;
|
|
136
|
+
if (!options[currentIndex]?.disabled) return currentIndex;
|
|
137
|
+
attempts++;
|
|
138
|
+
}
|
|
139
|
+
return -1;
|
|
140
|
+
};
|
|
141
|
+
let nearestIndex = searchInDirection(direction);
|
|
142
|
+
if (nearestIndex === -1) nearestIndex = searchInDirection(direction * -1);
|
|
143
|
+
return nearestIndex === -1 ? startIndex : nearestIndex;
|
|
144
|
+
}
|
|
145
|
+
function WheelPicker({ className, options: optionsProp, value: valueProp, defaultValue, onValueChange, infinite = false, visibleCount = 20, dragSensitivity = 3, scrollSensitivity = 5, optionItemHeight, classNames, size = "md", ...props }) {
|
|
146
|
+
const itemHeight = optionItemHeight ?? (size === "sm" ? 28 : size === "lg" ? 44 : 36);
|
|
147
|
+
const firstEnabledValue = useMemo(() => {
|
|
148
|
+
return optionsProp.find((opt) => !opt.disabled)?.value ?? optionsProp[0]?.value;
|
|
149
|
+
}, [optionsProp]);
|
|
150
|
+
const [value = firstEnabledValue, setValue] = useControllableState({
|
|
151
|
+
prop: valueProp,
|
|
152
|
+
defaultProp: defaultValue,
|
|
153
|
+
onChange: onValueChange
|
|
154
|
+
});
|
|
155
|
+
const options = useMemo(() => {
|
|
156
|
+
if (!infinite || optionsProp.length === 0) return optionsProp;
|
|
157
|
+
const result = [];
|
|
158
|
+
const L = optionsProp.length;
|
|
159
|
+
const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
|
|
160
|
+
const lcm = L * visibleCount / gcd(L, visibleCount);
|
|
161
|
+
let targetLength = lcm;
|
|
162
|
+
const minLength = Math.ceil(visibleCount * 1.5);
|
|
163
|
+
if (targetLength < minLength) targetLength = Math.ceil(minLength / lcm) * lcm;
|
|
164
|
+
const k = targetLength / L;
|
|
165
|
+
for (let i = 0; i < k; i++) result.push(...optionsProp);
|
|
166
|
+
return result;
|
|
167
|
+
}, [
|
|
168
|
+
optionsProp,
|
|
169
|
+
infinite,
|
|
170
|
+
visibleCount
|
|
171
|
+
]);
|
|
172
|
+
const halfItemHeight = itemHeight * .5;
|
|
173
|
+
const itemAngle = 360 / visibleCount;
|
|
174
|
+
const radius = itemHeight / Math.tan(itemAngle * Math.PI / 180);
|
|
175
|
+
const containerHeight = Math.round(radius * 2 + itemHeight * .25);
|
|
176
|
+
const quarterCount = visibleCount >> 2;
|
|
177
|
+
const baseDeceleration = dragSensitivity * 12;
|
|
178
|
+
const snapBackDeceleration = 10;
|
|
179
|
+
const containerRef = useRef(null);
|
|
180
|
+
const wheelItemsRef = useRef(null);
|
|
181
|
+
const highlightListRef = useRef(null);
|
|
182
|
+
const scrollRef = useRef(0);
|
|
183
|
+
const moveId = useRef(0);
|
|
184
|
+
const draggingRef = useRef(false);
|
|
185
|
+
useRef(0);
|
|
186
|
+
const wheelSnapTimeoutRef = useRef(null);
|
|
187
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
188
|
+
const pickerIndexRef = useRef(-1);
|
|
189
|
+
const group = useWheelPickerGroup();
|
|
190
|
+
const containerRefCallback = useCallback((node) => {
|
|
191
|
+
containerRef.current = node;
|
|
192
|
+
if (node && group) {
|
|
193
|
+
const existingIndex = pickerIndexRef.current === -1 ? null : pickerIndexRef.current;
|
|
194
|
+
pickerIndexRef.current = group.register(existingIndex, node);
|
|
195
|
+
}
|
|
196
|
+
}, [group]);
|
|
197
|
+
const touchDataRef = useRef({
|
|
198
|
+
startY: 0,
|
|
199
|
+
yList: [],
|
|
200
|
+
touchScroll: 0,
|
|
201
|
+
isClick: true
|
|
202
|
+
});
|
|
203
|
+
const dragControllerRef = useRef(null);
|
|
204
|
+
const renderWheelItems = useMemo(() => {
|
|
205
|
+
const renderItem = (item, index, angle) => /* @__PURE__ */ jsx("li", {
|
|
206
|
+
className: cn("absolute left-0 w-full flex items-center justify-center text-muted-foreground/70 dark:text-muted-foreground/60 transition-[color] duration-150 text-sm font-medium tabular-nums select-none", item.disabled && "opacity-30 pointer-events-none", classNames?.optionItem),
|
|
207
|
+
"data-slot": "wheel-picker-option",
|
|
208
|
+
"data-index": index,
|
|
209
|
+
"data-disabled": item.disabled || void 0,
|
|
210
|
+
style: {
|
|
211
|
+
top: -halfItemHeight,
|
|
212
|
+
height: itemHeight,
|
|
213
|
+
lineHeight: `${itemHeight}px`,
|
|
214
|
+
transform: `rotateX(${angle}deg) translateZ(${radius}px)`,
|
|
215
|
+
backfaceVisibility: "hidden",
|
|
216
|
+
WebkitBackfaceVisibility: "hidden",
|
|
217
|
+
visibility: "hidden"
|
|
218
|
+
},
|
|
219
|
+
children: item.label
|
|
220
|
+
}, index);
|
|
221
|
+
const items = options.map((option, index) => renderItem(option, index, -itemAngle * index));
|
|
222
|
+
if (infinite) for (let i = 0; i < quarterCount; ++i) {
|
|
223
|
+
const prependIndex = -i - 1;
|
|
224
|
+
const appendIndex = i + options.length;
|
|
225
|
+
items.unshift(renderItem(options[options.length - i - 1], prependIndex, itemAngle * (i + 1)));
|
|
226
|
+
items.push(renderItem(options[i], appendIndex, -itemAngle * appendIndex));
|
|
227
|
+
}
|
|
228
|
+
return items;
|
|
229
|
+
}, [
|
|
230
|
+
itemHeight,
|
|
231
|
+
halfItemHeight,
|
|
232
|
+
infinite,
|
|
233
|
+
itemAngle,
|
|
234
|
+
options,
|
|
235
|
+
quarterCount,
|
|
236
|
+
radius,
|
|
237
|
+
classNames?.optionItem
|
|
238
|
+
]);
|
|
239
|
+
const renderHighlightItems = useMemo(() => {
|
|
240
|
+
const renderItem = (item, key) => /* @__PURE__ */ jsx("li", {
|
|
241
|
+
className: cn("w-full flex items-center justify-center text-foreground font-semibold tabular-nums select-none transition-colors duration-150", size === "sm" ? "text-sm" : size === "lg" ? "text-lg" : "text-base", item.disabled && "opacity-30 pointer-events-none", classNames?.highlightItem),
|
|
242
|
+
"data-slot": "wheel-picker-highlight-item",
|
|
243
|
+
"data-disabled": item.disabled || void 0,
|
|
244
|
+
style: { height: itemHeight },
|
|
245
|
+
children: item.label
|
|
246
|
+
}, key);
|
|
247
|
+
const items = options.map((option, index) => renderItem(option, index));
|
|
248
|
+
if (infinite && options.length > 0) {
|
|
249
|
+
const firstItem = options[0];
|
|
250
|
+
const lastItem = options[options.length - 1];
|
|
251
|
+
items.unshift(renderItem(lastItem, "infinite-start"));
|
|
252
|
+
items.push(renderItem(firstItem, "infinite-end"));
|
|
253
|
+
}
|
|
254
|
+
return items;
|
|
255
|
+
}, [
|
|
256
|
+
classNames?.highlightItem,
|
|
257
|
+
itemHeight,
|
|
258
|
+
infinite,
|
|
259
|
+
options,
|
|
260
|
+
size
|
|
261
|
+
]);
|
|
262
|
+
const wheelSegmentPositions = useMemo(() => {
|
|
263
|
+
let positionAlongWheel = 0;
|
|
264
|
+
const degToRad = Math.PI / 180;
|
|
265
|
+
const segmentRanges = [];
|
|
266
|
+
for (let i = quarterCount - 1; i >= -quarterCount + 1; --i) {
|
|
267
|
+
const angle = i * itemAngle;
|
|
268
|
+
const segmentLength = itemHeight * Math.cos(angle * degToRad);
|
|
269
|
+
const start = positionAlongWheel;
|
|
270
|
+
positionAlongWheel += segmentLength;
|
|
271
|
+
segmentRanges.push([start, positionAlongWheel]);
|
|
272
|
+
}
|
|
273
|
+
return segmentRanges;
|
|
274
|
+
}, [
|
|
275
|
+
itemAngle,
|
|
276
|
+
itemHeight,
|
|
277
|
+
quarterCount
|
|
278
|
+
]);
|
|
279
|
+
const normalizeScroll = useCallback((scroll) => {
|
|
280
|
+
if (options.length === 0) return 0;
|
|
281
|
+
return (scroll % options.length + options.length) % options.length;
|
|
282
|
+
}, [options.length]);
|
|
283
|
+
const scrollTo = useCallback((scroll) => {
|
|
284
|
+
const normalizedScroll = infinite ? normalizeScroll(scroll) : scroll;
|
|
285
|
+
if (wheelItemsRef.current) {
|
|
286
|
+
const transform = `translateZ(${-radius}px) rotateX(${itemAngle * normalizedScroll}deg)`;
|
|
287
|
+
wheelItemsRef.current.style.transform = transform;
|
|
288
|
+
const children = wheelItemsRef.current.childNodes;
|
|
289
|
+
for (let i = 0; i < children.length; i++) {
|
|
290
|
+
const li = children[i];
|
|
291
|
+
const distance = Math.abs(Number(li.dataset.index) - normalizedScroll);
|
|
292
|
+
li.style.visibility = distance > quarterCount ? "hidden" : "visible";
|
|
293
|
+
const baseOpacity = li.dataset.disabled ? .3 : 1;
|
|
294
|
+
const opacity = distance < .5 ? distance / .5 * baseOpacity : baseOpacity;
|
|
295
|
+
li.style.opacity = opacity.toString();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (highlightListRef.current) highlightListRef.current.style.transform = `translateY(${-normalizedScroll * itemHeight}px)`;
|
|
299
|
+
return normalizedScroll;
|
|
300
|
+
}, [
|
|
301
|
+
radius,
|
|
302
|
+
itemAngle,
|
|
303
|
+
quarterCount,
|
|
304
|
+
itemHeight,
|
|
305
|
+
infinite,
|
|
306
|
+
normalizeScroll
|
|
307
|
+
]);
|
|
308
|
+
const cancelAnimation = useCallback(() => {
|
|
309
|
+
cancelAnimationFrame(moveId.current);
|
|
310
|
+
}, []);
|
|
311
|
+
const animateScroll = useCallback((startScroll, endScroll, duration, onComplete) => {
|
|
312
|
+
if (startScroll === endScroll || duration === 0) {
|
|
313
|
+
scrollTo(startScroll);
|
|
314
|
+
onComplete?.();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const startTime = performance.now();
|
|
318
|
+
const totalDistance = endScroll - startScroll;
|
|
319
|
+
const tick = (currentTime) => {
|
|
320
|
+
const elapsed = (currentTime - startTime) / 1e3;
|
|
321
|
+
if (elapsed < duration) {
|
|
322
|
+
const progress = easeOutCubic(elapsed / duration);
|
|
323
|
+
scrollRef.current = scrollTo(startScroll + progress * totalDistance);
|
|
324
|
+
moveId.current = requestAnimationFrame(tick);
|
|
325
|
+
} else {
|
|
326
|
+
cancelAnimation();
|
|
327
|
+
scrollRef.current = scrollTo(endScroll);
|
|
328
|
+
onComplete?.();
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
requestAnimationFrame(tick);
|
|
332
|
+
}, [scrollTo, cancelAnimation]);
|
|
333
|
+
const scrollDirectionRef = useRef(1);
|
|
334
|
+
const selectByScroll = useCallbackRef((scroll, scrollDirection) => {
|
|
335
|
+
if (options.length === 0) return;
|
|
336
|
+
const normalized = Math.round(normalizeScroll(scroll));
|
|
337
|
+
const boundedScroll = infinite ? normalized : clamp(Math.round(scroll), 0, options.length - 1);
|
|
338
|
+
if (!infinite && boundedScroll !== scroll) return;
|
|
339
|
+
if (options[boundedScroll]?.disabled) {
|
|
340
|
+
const step = findNearestEnabledIndex(boundedScroll, scrollDirection ?? scrollDirectionRef.current, options, infinite) - scrollRef.current;
|
|
341
|
+
if (step !== 0) {
|
|
342
|
+
scrollByStep(step);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
scrollRef.current = scrollTo(boundedScroll);
|
|
347
|
+
const selected = options[scrollRef.current];
|
|
348
|
+
if (selected && !selected.disabled) setValue(selected.value);
|
|
349
|
+
});
|
|
350
|
+
const selectByValue = useCallbackRef((val) => {
|
|
351
|
+
if (options.length === 0 || draggingRef.current) return;
|
|
352
|
+
const currentIndex = Math.round(scrollRef.current);
|
|
353
|
+
let index = -1;
|
|
354
|
+
let minDistance = Number.MAX_VALUE;
|
|
355
|
+
for (let i = 0; i < options.length; i++) if (options[i].value === val) {
|
|
356
|
+
const distance = Math.abs(i - currentIndex);
|
|
357
|
+
if (distance < minDistance) {
|
|
358
|
+
minDistance = distance;
|
|
359
|
+
index = i;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (index === -1) return;
|
|
363
|
+
if (options[index]?.disabled) index = findNearestEnabledIndex(index, 1, options, infinite);
|
|
364
|
+
cancelAnimation();
|
|
365
|
+
selectByScroll(index);
|
|
366
|
+
});
|
|
367
|
+
const scrollByStep = useCallbackRef((step) => {
|
|
368
|
+
if (options.length === 0) return;
|
|
369
|
+
const startScroll = scrollRef.current;
|
|
370
|
+
let endScroll = startScroll + step;
|
|
371
|
+
if (infinite) endScroll = Math.round(endScroll);
|
|
372
|
+
else endScroll = clamp(Math.round(endScroll), 0, options.length - 1);
|
|
373
|
+
const distance = Math.abs(endScroll - startScroll);
|
|
374
|
+
if (distance === 0) return;
|
|
375
|
+
const direction = step > 0 ? 1 : -1;
|
|
376
|
+
scrollDirectionRef.current = direction;
|
|
377
|
+
const duration = Math.sqrt(distance / scrollSensitivity);
|
|
378
|
+
cancelAnimation();
|
|
379
|
+
animateScroll(startScroll, endScroll, duration, () => {
|
|
380
|
+
selectByScroll(scrollRef.current, direction);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
const handleWheelItemClick = useCallback((clientY) => {
|
|
384
|
+
const container = containerRef.current;
|
|
385
|
+
if (!container) return;
|
|
386
|
+
const { top } = container.getBoundingClientRect();
|
|
387
|
+
const clickOffsetY = clientY - top;
|
|
388
|
+
const clickedSegmentIndex = wheelSegmentPositions.findIndex(([start, end]) => clickOffsetY >= start && clickOffsetY <= end);
|
|
389
|
+
if (clickedSegmentIndex === -1) return;
|
|
390
|
+
const stepsToScroll = (quarterCount - clickedSegmentIndex - 1) * -1;
|
|
391
|
+
const targetIndex = scrollRef.current + stepsToScroll;
|
|
392
|
+
const normalizedIndex = infinite ? normalizeScroll(targetIndex) : Math.max(0, Math.min(targetIndex, options.length - 1));
|
|
393
|
+
if (options[normalizedIndex]?.disabled) return;
|
|
394
|
+
scrollByStep(stepsToScroll);
|
|
395
|
+
}, [
|
|
396
|
+
wheelSegmentPositions,
|
|
397
|
+
quarterCount,
|
|
398
|
+
infinite,
|
|
399
|
+
normalizeScroll,
|
|
400
|
+
options,
|
|
401
|
+
scrollByStep
|
|
402
|
+
]);
|
|
403
|
+
const updateScrollDuringDrag = useCallback((e) => {
|
|
404
|
+
if (options.length === 0) return;
|
|
405
|
+
const currentY = (e instanceof MouseEvent ? e.clientY : e.touches?.[0]?.clientY) || 0;
|
|
406
|
+
const touchData = touchDataRef.current;
|
|
407
|
+
if (touchData.isClick) {
|
|
408
|
+
if (Math.abs(currentY - touchData.startY) > 5) touchData.isClick = false;
|
|
409
|
+
}
|
|
410
|
+
touchData.yList.push([currentY, Date.now()]);
|
|
411
|
+
if (touchData.yList.length > 5) touchData.yList.shift();
|
|
412
|
+
const dragDelta = (touchData.startY - currentY) / itemHeight;
|
|
413
|
+
let nextScroll = scrollRef.current + dragDelta;
|
|
414
|
+
if (infinite) nextScroll = normalizeScroll(nextScroll);
|
|
415
|
+
else {
|
|
416
|
+
const maxIndex = options.length - 1;
|
|
417
|
+
if (nextScroll < 0) nextScroll *= RESISTANCE;
|
|
418
|
+
else if (nextScroll > maxIndex) nextScroll = maxIndex + (nextScroll - maxIndex) * RESISTANCE;
|
|
419
|
+
}
|
|
420
|
+
touchData.touchScroll = scrollTo(nextScroll);
|
|
421
|
+
}, [
|
|
422
|
+
options.length,
|
|
423
|
+
itemHeight,
|
|
424
|
+
infinite,
|
|
425
|
+
normalizeScroll,
|
|
426
|
+
scrollTo
|
|
427
|
+
]);
|
|
428
|
+
const decelerateAndAnimateScroll = useCallback((initialVelocity) => {
|
|
429
|
+
const currentScroll = scrollRef.current;
|
|
430
|
+
let targetScroll = currentScroll;
|
|
431
|
+
let duration = 0;
|
|
432
|
+
const direction = initialVelocity > 0 ? 1 : -1;
|
|
433
|
+
scrollDirectionRef.current = direction;
|
|
434
|
+
if (infinite) {
|
|
435
|
+
const signedDistance = initialVelocity * initialVelocity / (2 * baseDeceleration) * direction;
|
|
436
|
+
targetScroll = Math.round(currentScroll + signedDistance);
|
|
437
|
+
const inertiaDuration = Math.abs(initialVelocity / baseDeceleration);
|
|
438
|
+
const distanceToTarget = Math.abs(targetScroll - currentScroll);
|
|
439
|
+
const snapDuration = Math.sqrt(distanceToTarget / snapBackDeceleration);
|
|
440
|
+
duration = Math.max(inertiaDuration, snapDuration);
|
|
441
|
+
} else {
|
|
442
|
+
const predictedTarget = currentScroll + initialVelocity * initialVelocity / (2 * baseDeceleration) * direction;
|
|
443
|
+
if (predictedTarget < 0) {
|
|
444
|
+
targetScroll = 0;
|
|
445
|
+
const distance = Math.abs(currentScroll - targetScroll);
|
|
446
|
+
duration = Math.sqrt(distance / snapBackDeceleration);
|
|
447
|
+
} else if (predictedTarget > options.length - 1) {
|
|
448
|
+
targetScroll = options.length - 1;
|
|
449
|
+
const distance = Math.abs(currentScroll - targetScroll);
|
|
450
|
+
duration = Math.sqrt(distance / snapBackDeceleration);
|
|
451
|
+
} else {
|
|
452
|
+
targetScroll = Math.round(predictedTarget);
|
|
453
|
+
const inertiaDuration = Math.abs(initialVelocity / baseDeceleration);
|
|
454
|
+
const distanceToTarget = Math.abs(targetScroll - currentScroll);
|
|
455
|
+
const snapDuration = Math.sqrt(distanceToTarget / snapBackDeceleration);
|
|
456
|
+
duration = Math.max(inertiaDuration, snapDuration);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (targetScroll !== currentScroll) duration = clamp(duration, .15, .8);
|
|
460
|
+
else duration = 0;
|
|
461
|
+
animateScroll(currentScroll, targetScroll, duration, () => {
|
|
462
|
+
selectByScroll(scrollRef.current, direction);
|
|
463
|
+
});
|
|
464
|
+
}, [
|
|
465
|
+
baseDeceleration,
|
|
466
|
+
infinite,
|
|
467
|
+
options.length,
|
|
468
|
+
animateScroll,
|
|
469
|
+
selectByScroll
|
|
470
|
+
]);
|
|
471
|
+
const finalizeDragAndStartInertiaScroll = useCallback(() => {
|
|
472
|
+
try {
|
|
473
|
+
dragControllerRef.current?.abort();
|
|
474
|
+
dragControllerRef.current = null;
|
|
475
|
+
const touchData = touchDataRef.current;
|
|
476
|
+
if (touchData.isClick) {
|
|
477
|
+
handleWheelItemClick(touchData.startY);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const yList = touchData.yList;
|
|
481
|
+
let velocity = 0;
|
|
482
|
+
if (yList.length > 1) {
|
|
483
|
+
const len = yList.length;
|
|
484
|
+
const [startY, startTime] = yList[len - 2] ?? [0, 0];
|
|
485
|
+
const [endY, endTime] = yList[len - 1] ?? [0, 0];
|
|
486
|
+
const timeDiff = endTime - startTime;
|
|
487
|
+
if (timeDiff > 0) {
|
|
488
|
+
const velocityPerSecond = (startY - endY) / itemHeight * 1e3 / timeDiff;
|
|
489
|
+
velocity = Math.min(Math.abs(velocityPerSecond), MAX_VELOCITY) * (velocityPerSecond > 0 ? 1 : -1);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
scrollRef.current = touchData.touchScroll ?? scrollRef.current;
|
|
493
|
+
decelerateAndAnimateScroll(velocity);
|
|
494
|
+
} finally {
|
|
495
|
+
draggingRef.current = false;
|
|
496
|
+
}
|
|
497
|
+
}, [
|
|
498
|
+
handleWheelItemClick,
|
|
499
|
+
itemHeight,
|
|
500
|
+
decelerateAndAnimateScroll
|
|
501
|
+
]);
|
|
502
|
+
const handleDragMoveEvent = useCallback((event) => {
|
|
503
|
+
if (event.cancelable) event.preventDefault();
|
|
504
|
+
updateScrollDuringDrag(event);
|
|
505
|
+
}, [updateScrollDuringDrag]);
|
|
506
|
+
const handleDragEndEvent = useCallback((event) => {
|
|
507
|
+
if (options.length === 0) return;
|
|
508
|
+
if (event.cancelable) event.preventDefault();
|
|
509
|
+
finalizeDragAndStartInertiaScroll();
|
|
510
|
+
}, [options.length, finalizeDragAndStartInertiaScroll]);
|
|
511
|
+
const initiateDragGesture = useCallback((event) => {
|
|
512
|
+
draggingRef.current = true;
|
|
513
|
+
const controller = new AbortController();
|
|
514
|
+
const { signal } = controller;
|
|
515
|
+
dragControllerRef.current = controller;
|
|
516
|
+
const opts = {
|
|
517
|
+
signal,
|
|
518
|
+
passive: false
|
|
519
|
+
};
|
|
520
|
+
containerRef.current?.addEventListener("touchmove", handleDragMoveEvent, opts);
|
|
521
|
+
containerRef.current?.addEventListener("touchend", handleDragEndEvent, opts);
|
|
522
|
+
document.addEventListener("mousemove", handleDragMoveEvent, opts);
|
|
523
|
+
document.addEventListener("mouseup", handleDragEndEvent, opts);
|
|
524
|
+
window.addEventListener("blur", finalizeDragAndStartInertiaScroll, { signal });
|
|
525
|
+
const startY = (event instanceof MouseEvent ? event.clientY : event.touches?.[0]?.clientY) || 0;
|
|
526
|
+
const touchData = touchDataRef.current;
|
|
527
|
+
touchData.startY = startY;
|
|
528
|
+
touchData.yList = [[startY, Date.now()]];
|
|
529
|
+
touchData.touchScroll = scrollRef.current;
|
|
530
|
+
touchData.isClick = true;
|
|
531
|
+
cancelAnimation();
|
|
532
|
+
}, [
|
|
533
|
+
handleDragMoveEvent,
|
|
534
|
+
handleDragEndEvent,
|
|
535
|
+
finalizeDragAndStartInertiaScroll,
|
|
536
|
+
cancelAnimation
|
|
537
|
+
]);
|
|
538
|
+
const handleDragStartEvent = useCallback((e) => {
|
|
539
|
+
if (options.length === 0) return;
|
|
540
|
+
if (e instanceof MouseEvent && e.button !== 0) return;
|
|
541
|
+
if (e.cancelable) e.preventDefault();
|
|
542
|
+
initiateDragGesture(e);
|
|
543
|
+
}, [options.length, initiateDragGesture]);
|
|
544
|
+
const scrollByWheel = useCallback((event) => {
|
|
545
|
+
event.preventDefault();
|
|
546
|
+
cancelAnimation();
|
|
547
|
+
if (wheelSnapTimeoutRef.current) clearTimeout(wheelSnapTimeoutRef.current);
|
|
548
|
+
const delta = event.deltaY * .003 * scrollSensitivity;
|
|
549
|
+
let nextScroll = scrollRef.current + delta;
|
|
550
|
+
if (infinite) nextScroll = normalizeScroll(nextScroll);
|
|
551
|
+
else nextScroll = clamp(nextScroll, 0, options.length - 1);
|
|
552
|
+
scrollTo(nextScroll);
|
|
553
|
+
scrollRef.current = nextScroll;
|
|
554
|
+
const direction = event.deltaY > 0 ? 1 : -1;
|
|
555
|
+
scrollDirectionRef.current = direction;
|
|
556
|
+
wheelSnapTimeoutRef.current = setTimeout(() => {
|
|
557
|
+
const targetIndex = Math.round(scrollRef.current);
|
|
558
|
+
animateScroll(scrollRef.current, targetIndex, .15, () => {
|
|
559
|
+
selectByScroll(scrollRef.current, direction);
|
|
560
|
+
});
|
|
561
|
+
}, 150);
|
|
562
|
+
}, [
|
|
563
|
+
scrollSensitivity,
|
|
564
|
+
infinite,
|
|
565
|
+
options.length,
|
|
566
|
+
normalizeScroll,
|
|
567
|
+
scrollTo,
|
|
568
|
+
animateScroll,
|
|
569
|
+
selectByScroll,
|
|
570
|
+
cancelAnimation
|
|
571
|
+
]);
|
|
572
|
+
const handleWheelEvent = useCallback((event) => {
|
|
573
|
+
if (options.length === 0 || !containerRef.current) return;
|
|
574
|
+
if (containerRef.current.contains(event.target) && event.cancelable) {
|
|
575
|
+
event.preventDefault();
|
|
576
|
+
scrollByWheel(event);
|
|
577
|
+
}
|
|
578
|
+
}, [options.length, scrollByWheel]);
|
|
579
|
+
const navigateToPicker = useCallback((direction) => {
|
|
580
|
+
const pickerIndex = pickerIndexRef.current;
|
|
581
|
+
if (!group || pickerIndex === -1) return;
|
|
582
|
+
const sortedIndices = group.getPickerIndices();
|
|
583
|
+
const currentPos = sortedIndices.indexOf(pickerIndex);
|
|
584
|
+
if (currentPos === -1) return;
|
|
585
|
+
let targetPos;
|
|
586
|
+
if (direction === "prev") targetPos = currentPos > 0 ? currentPos - 1 : sortedIndices.length - 1;
|
|
587
|
+
else targetPos = currentPos < sortedIndices.length - 1 ? currentPos + 1 : 0;
|
|
588
|
+
const targetIndex = sortedIndices[targetPos];
|
|
589
|
+
const targetRef = group.getPickerRef(targetIndex);
|
|
590
|
+
if (targetRef) {
|
|
591
|
+
const currentRef = containerRef.current;
|
|
592
|
+
if (currentRef) currentRef.tabIndex = -1;
|
|
593
|
+
targetRef.tabIndex = 0;
|
|
594
|
+
group.setActiveIndex(targetIndex);
|
|
595
|
+
targetRef.focus();
|
|
596
|
+
}
|
|
597
|
+
}, [group]);
|
|
598
|
+
const enabledOptionsMap = useMemo(() => {
|
|
599
|
+
const map = /* @__PURE__ */ new Map();
|
|
600
|
+
const reverseMap = /* @__PURE__ */ new Map();
|
|
601
|
+
const enabled = [];
|
|
602
|
+
options.forEach((option, index) => {
|
|
603
|
+
if (!option.disabled) {
|
|
604
|
+
const enabledIndex = enabled.length;
|
|
605
|
+
map.set(enabledIndex, index);
|
|
606
|
+
reverseMap.set(index, enabledIndex);
|
|
607
|
+
enabled.push(option);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
return {
|
|
611
|
+
enabled,
|
|
612
|
+
map,
|
|
613
|
+
reverseMap
|
|
614
|
+
};
|
|
615
|
+
}, [options]);
|
|
616
|
+
const { handleTypeaheadSearch, resetTypeahead } = useTypeaheadSearch(enabledOptionsMap.enabled, {
|
|
617
|
+
getTextValue: (opt) => {
|
|
618
|
+
return opt.textValue ?? (typeof opt.label === "string" ? opt.label : String(opt.value));
|
|
619
|
+
},
|
|
620
|
+
getCurrentIndex: useCallback(() => {
|
|
621
|
+
const currentIndex = Math.round(scrollRef.current);
|
|
622
|
+
return enabledOptionsMap.reverseMap.get(currentIndex) ?? 0;
|
|
623
|
+
}, [enabledOptionsMap]),
|
|
624
|
+
onMatch: useCallback((enabledIndex) => {
|
|
625
|
+
const originalIndex = enabledOptionsMap.map.get(enabledIndex);
|
|
626
|
+
if (originalIndex !== void 0) {
|
|
627
|
+
const step = originalIndex - Math.round(scrollRef.current);
|
|
628
|
+
scrollByStep(step);
|
|
629
|
+
}
|
|
630
|
+
}, [enabledOptionsMap, scrollByStep])
|
|
631
|
+
});
|
|
632
|
+
const handleKeyDown = useCallback((event) => {
|
|
633
|
+
if (options.length === 0) return;
|
|
634
|
+
const handleVerticalNavigation = (direction) => {
|
|
635
|
+
event.preventDefault();
|
|
636
|
+
const currentIndex = Math.round(scrollRef.current);
|
|
637
|
+
let targetIndex = currentIndex + direction;
|
|
638
|
+
if (!infinite) {
|
|
639
|
+
if (direction === -1 && targetIndex < 0) return;
|
|
640
|
+
if (direction === 1 && targetIndex >= options.length) return;
|
|
641
|
+
}
|
|
642
|
+
const normalizedTarget = infinite ? (targetIndex % options.length + options.length) % options.length : targetIndex;
|
|
643
|
+
if (options[normalizedTarget]?.disabled) {
|
|
644
|
+
const nearestEnabled = findNearestEnabledIndex(normalizedTarget, direction, options, infinite);
|
|
645
|
+
targetIndex = currentIndex + direction + (nearestEnabled - normalizedTarget);
|
|
646
|
+
}
|
|
647
|
+
const step = targetIndex - currentIndex;
|
|
648
|
+
if (step !== 0) scrollByStep(step);
|
|
649
|
+
};
|
|
650
|
+
const handleHome = () => {
|
|
651
|
+
if (infinite) return;
|
|
652
|
+
event.preventDefault();
|
|
653
|
+
let targetIndex = 0;
|
|
654
|
+
if (options[0]?.disabled) targetIndex = findNearestEnabledIndex(0, 1, options, false);
|
|
655
|
+
const step = targetIndex - scrollRef.current;
|
|
656
|
+
if (step !== 0) scrollByStep(step);
|
|
657
|
+
};
|
|
658
|
+
const handleEnd = () => {
|
|
659
|
+
if (infinite) return;
|
|
660
|
+
event.preventDefault();
|
|
661
|
+
let targetIndex = options.length - 1;
|
|
662
|
+
if (options[targetIndex]?.disabled) targetIndex = findNearestEnabledIndex(targetIndex, -1, options, false);
|
|
663
|
+
const step = targetIndex - scrollRef.current;
|
|
664
|
+
if (step !== 0) scrollByStep(step);
|
|
665
|
+
};
|
|
666
|
+
const handler = {
|
|
667
|
+
ArrowUp: () => handleVerticalNavigation(-1),
|
|
668
|
+
ArrowDown: () => handleVerticalNavigation(1),
|
|
669
|
+
ArrowLeft: () => {
|
|
670
|
+
event.preventDefault();
|
|
671
|
+
navigateToPicker("prev");
|
|
672
|
+
},
|
|
673
|
+
ArrowRight: () => {
|
|
674
|
+
event.preventDefault();
|
|
675
|
+
navigateToPicker("next");
|
|
676
|
+
},
|
|
677
|
+
Home: handleHome,
|
|
678
|
+
End: handleEnd
|
|
679
|
+
}[event.key];
|
|
680
|
+
if (handler) handler();
|
|
681
|
+
else if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
682
|
+
event.preventDefault();
|
|
683
|
+
handleTypeaheadSearch(event.key);
|
|
684
|
+
}
|
|
685
|
+
}, [
|
|
686
|
+
options,
|
|
687
|
+
infinite,
|
|
688
|
+
navigateToPicker,
|
|
689
|
+
handleTypeaheadSearch,
|
|
690
|
+
scrollByStep
|
|
691
|
+
]);
|
|
692
|
+
const handleFocus = useCallback(() => {
|
|
693
|
+
setIsFocused(true);
|
|
694
|
+
const pickerIndex = pickerIndexRef.current;
|
|
695
|
+
if (group && pickerIndex !== -1) group.setActiveIndex(pickerIndex);
|
|
696
|
+
}, [group]);
|
|
697
|
+
const handleBlur = useCallback(() => {
|
|
698
|
+
setIsFocused(false);
|
|
699
|
+
resetTypeahead();
|
|
700
|
+
}, [resetTypeahead]);
|
|
701
|
+
useEffect(() => {
|
|
702
|
+
const container = containerRef.current;
|
|
703
|
+
if (!container) return;
|
|
704
|
+
const opts = { passive: false };
|
|
705
|
+
container.addEventListener("touchstart", handleDragStartEvent, opts);
|
|
706
|
+
container.addEventListener("wheel", handleWheelEvent, opts);
|
|
707
|
+
container.addEventListener("mousedown", handleDragStartEvent, opts);
|
|
708
|
+
return () => {
|
|
709
|
+
container.removeEventListener("touchstart", handleDragStartEvent);
|
|
710
|
+
container.removeEventListener("wheel", handleWheelEvent);
|
|
711
|
+
container.removeEventListener("mousedown", handleDragStartEvent);
|
|
712
|
+
if (dragControllerRef.current) dragControllerRef.current.abort();
|
|
713
|
+
if (wheelSnapTimeoutRef.current) clearTimeout(wheelSnapTimeoutRef.current);
|
|
714
|
+
};
|
|
715
|
+
}, [handleDragStartEvent, handleWheelEvent]);
|
|
716
|
+
useEffect(() => {
|
|
717
|
+
selectByValue(value);
|
|
718
|
+
}, [value, selectByValue]);
|
|
719
|
+
const pickerIndex = pickerIndexRef.current;
|
|
720
|
+
const activeIndex = group?.activeIndex ?? -1;
|
|
721
|
+
const tabIndex = group && pickerIndex !== -1 ? activeIndex === pickerIndex ? 0 : -1 : 0;
|
|
722
|
+
const selectedIndex = optionsProp.findIndex((option) => option.value === value);
|
|
723
|
+
const selectedOption = selectedIndex >= 0 ? optionsProp[selectedIndex] : void 0;
|
|
724
|
+
const selectedText = selectedOption ? selectedOption.textValue ?? (typeof selectedOption.label === "string" || typeof selectedOption.label === "number" ? String(selectedOption.label) : String(selectedOption.value)) : void 0;
|
|
725
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
726
|
+
ref: containerRefCallback,
|
|
727
|
+
className: cn(wheelPickerVariants({ size }), "relative flex-1 cursor-default select-none focus:outline-none focus-visible:outline-none [perspective:2000px] [transform-style:preserve-3d] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11", className),
|
|
728
|
+
tabIndex,
|
|
729
|
+
role: "spinbutton",
|
|
730
|
+
"aria-label": props["aria-label"] ?? "Wheel picker",
|
|
731
|
+
"aria-valuemin": optionsProp.length > 0 ? 0 : void 0,
|
|
732
|
+
"aria-valuemax": optionsProp.length > 0 ? optionsProp.length - 1 : void 0,
|
|
733
|
+
"aria-valuenow": selectedIndex >= 0 ? selectedIndex : void 0,
|
|
734
|
+
"aria-valuetext": selectedText,
|
|
735
|
+
onKeyDown: handleKeyDown,
|
|
736
|
+
onFocus: handleFocus,
|
|
737
|
+
onBlur: handleBlur,
|
|
738
|
+
style: { height: containerHeight },
|
|
739
|
+
"data-slot": "wheel-picker",
|
|
740
|
+
...props,
|
|
741
|
+
children: [
|
|
742
|
+
/* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-x-0 top-0 h-1/4 bg-gradient-to-b from-background to-transparent z-10" }),
|
|
743
|
+
/* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 h-1/4 bg-gradient-to-t from-background to-transparent z-10" }),
|
|
744
|
+
/* @__PURE__ */ jsx("ul", {
|
|
745
|
+
"aria-hidden": "true",
|
|
746
|
+
ref: wheelItemsRef,
|
|
747
|
+
className: "absolute top-1/2 left-0 block w-full h-0 m-0 p-0 list-none will-change-transform [transform-style:preserve-3d]",
|
|
748
|
+
"data-slot": "wheel-picker-cylinder",
|
|
749
|
+
children: renderWheelItems
|
|
750
|
+
}),
|
|
751
|
+
/* @__PURE__ */ jsx("div", {
|
|
752
|
+
className: cn("absolute top-1/2 -translate-y-1/2 w-full overflow-hidden border-y border-border/80 bg-accent/40 shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", isFocused && "border-primary/80 bg-accent/60 ring-2 ring-ring/30 dark:ring-ring/20", classNames?.highlightWrapper),
|
|
753
|
+
style: {
|
|
754
|
+
height: itemHeight,
|
|
755
|
+
lineHeight: `${itemHeight}px`
|
|
756
|
+
},
|
|
757
|
+
"data-slot": "wheel-picker-highlight-window",
|
|
758
|
+
children: /* @__PURE__ */ jsx("ul", {
|
|
759
|
+
"aria-hidden": "true",
|
|
760
|
+
ref: highlightListRef,
|
|
761
|
+
className: "absolute left-0 w-full m-0 p-0 list-none",
|
|
762
|
+
style: { top: infinite ? -itemHeight : void 0 },
|
|
763
|
+
"data-slot": "wheel-picker-highlight-list",
|
|
764
|
+
children: renderHighlightItems
|
|
765
|
+
})
|
|
766
|
+
})
|
|
767
|
+
]
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
//#endregion
|
|
771
|
+
export { WheelPicker, WheelPickerGroup, useWheelPickerGroup, wheelPickerVariants };
|