react-wheel-select 0.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vasil Rashkov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,575 @@
1
+ # react-wheel-select
2
+
3
+ <div align="center">
4
+
5
+ ![npm version](https://img.shields.io/npm/v/react-wheel-select)
6
+ ![bundle size](https://img.shields.io/bundlephobia/minzip/react-wheel-select)
7
+ ![license](https://img.shields.io/npm/l/react-wheel-select)
8
+ ![typescript](https://img.shields.io/badge/TypeScript-Ready-blue)
9
+
10
+ **A beautiful, accessible iOS-style wheel picker component for React**
11
+
12
+ [Live Demo](https://react-wheel-select.devapollo.com) ยท [Documentation](#-documentation) ยท [Examples](#-examples) ยท [Contributing](#-contributing)
13
+
14
+ </div>
15
+
16
+ ---
17
+
18
+ ## โœจ Features
19
+
20
+ - ๐ŸŽก **Smooth Wheel Scrolling** โ€” CSS scroll-snap powered picker with momentum scrolling
21
+ - โ™ฟ **Fully Accessible** โ€” ARIA compliant with full keyboard navigation
22
+ - ๐ŸŽจ **Highly Customizable** โ€” 30+ CSS variables for complete visual control
23
+ - ๐Ÿ“ฑ **Touch Optimized** โ€” Works beautifully on mobile devices
24
+ - ๐Ÿ”ง **TypeScript First** โ€” Complete type definitions with generics support
25
+ - ๐Ÿชถ **Lightweight** โ€” ~4KB minified + gzipped, zero dependencies
26
+ - ๐ŸŒ™ **Theme Support** โ€” Built-in dark/light modes with auto-detection
27
+ - ๐ŸŽฏ **Render Props** โ€” Custom trigger and option rendering
28
+ - ๐Ÿ“‹ **Form Compatible** โ€” Hidden native select for form submission
29
+ - ๐Ÿ”„ **Controlled & Uncontrolled** โ€” Works both ways with ref API
30
+
31
+ ---
32
+
33
+ ## ๐Ÿ“ฆ Installation
34
+
35
+ ```bash
36
+ # npm
37
+ npm install react-wheel-select
38
+
39
+ # yarn
40
+ yarn add react-wheel-select
41
+
42
+ # pnpm
43
+ pnpm add react-wheel-select
44
+
45
+ # bun
46
+ bun add react-wheel-select
47
+ ```
48
+
49
+ ---
50
+
51
+ ## ๐Ÿš€ Quick Start
52
+
53
+ ```tsx
54
+ import { useState } from 'react'
55
+ import { WheelSelect } from 'react-wheel-select'
56
+ import 'react-wheel-select/styles.css'
57
+
58
+ const fruits = [
59
+ { value: 'apple', label: 'Apple' },
60
+ { value: 'banana', label: 'Banana' },
61
+ { value: 'cherry', label: 'Cherry' },
62
+ { value: 'date', label: 'Date' },
63
+ { value: 'elderberry', label: 'Elderberry' },
64
+ ]
65
+
66
+ function App() {
67
+ const [fruit, setFruit] = useState('apple')
68
+
69
+ return (
70
+ <p>
71
+ I love to eat{' '}
72
+ <WheelSelect
73
+ options={fruits}
74
+ value={fruit}
75
+ onChange={setFruit}
76
+ />
77
+ </p>
78
+ )
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ ## ๐Ÿ“– Documentation
85
+
86
+ ### Table of Contents
87
+
88
+ - [Props Reference](#props-reference)
89
+ - [Theme Configuration](#theme-configuration)
90
+ - [Sizing Configuration](#sizing-configuration)
91
+ - [Behavior Configuration](#behavior-configuration)
92
+ - [Custom Icons](#custom-icons)
93
+ - [Accessibility](#accessibility)
94
+ - [Event Callbacks](#event-callbacks)
95
+ - [Imperative API](#imperative-api)
96
+ - [CSS Customization](#css-customization)
97
+ - [TypeScript](#typescript)
98
+
99
+ ---
100
+
101
+ ### Props Reference
102
+
103
+ | Prop | Type | Default | Description |
104
+ |------|------|---------|-------------|
105
+ | `options` | `WheelSelectOption[]` | **required** | Array of options to display |
106
+ | `value` | `string` | **required** | Currently selected value |
107
+ | `onChange` | `(value: string) => void` | **required** | Called when selection changes |
108
+ | `placeholder` | `string` | `'Select...'` | Placeholder when no value |
109
+ | `disabled` | `boolean` | `false` | Disable the component |
110
+ | `required` | `boolean` | `false` | Mark as required for forms |
111
+ | `name` | `string` | โ€” | Form field name |
112
+ | `id` | `string` | โ€” | Element ID |
113
+ | `className` | `string` | โ€” | Additional CSS class |
114
+ | `style` | `CSSProperties` | โ€” | Inline styles |
115
+ | `theme` | `WheelSelectTheme` | โ€” | Theme configuration |
116
+ | `sizing` | `WheelSelectSizing` | โ€” | Sizing configuration |
117
+ | `behavior` | `WheelSelectBehavior` | โ€” | Behavior configuration |
118
+ | `icons` | `WheelSelectIcons` | โ€” | Custom icons |
119
+ | `a11y` | `WheelSelectA11y` | โ€” | Accessibility options |
120
+ | `callbacks` | `WheelSelectCallbacks` | โ€” | Event callbacks |
121
+ | `renderTrigger` | `Function` | โ€” | Custom trigger renderer |
122
+ | `renderOption` | `Function` | โ€” | Custom option renderer |
123
+ | `zIndex` | `number` | `10001` | Overlay z-index |
124
+
125
+ #### Option Shape
126
+
127
+ ```typescript
128
+ interface WheelSelectOption<T extends string = string> {
129
+ value: T // Unique identifier
130
+ label: string // Display text
131
+ disabled?: boolean // Disable this option
132
+ data?: Record<string, unknown> // Custom data
133
+ }
134
+ ```
135
+
136
+ ---
137
+
138
+ ### Theme Configuration
139
+
140
+ Customize colors, typography, animations, and spacing:
141
+
142
+ ```tsx
143
+ <WheelSelect
144
+ theme={{
145
+ // Color scheme
146
+ colorScheme: 'dark', // 'dark' | 'light' | 'auto'
147
+
148
+ // Custom colors
149
+ colors: {
150
+ text: '#ffffff',
151
+ textMuted: '#888888',
152
+ activeBg: 'rgba(255, 255, 255, 0.15)',
153
+ hoverBg: 'rgba(255, 255, 255, 0.1)',
154
+ backdropBg: 'rgba(0, 0, 0, 0.6)',
155
+ focusRing: 'rgba(59, 130, 246, 0.5)',
156
+ },
157
+
158
+ // Border radius
159
+ borderRadius: 16,
160
+
161
+ // Typography
162
+ font: {
163
+ family: 'Inter, sans-serif',
164
+ size: 24,
165
+ weight: 600,
166
+ triggerSize: 18,
167
+ },
168
+
169
+ // Animations
170
+ animation: {
171
+ duration: 250,
172
+ easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
173
+ disabled: false, // Set true to disable all animations
174
+ },
175
+
176
+ // Spacing
177
+ spacing: {
178
+ triggerGap: 12,
179
+ triggerPadding: '10px 20px',
180
+ optionGap: 12,
181
+ optionPadding: '0 24px',
182
+ },
183
+ }}
184
+ />
185
+ ```
186
+
187
+ ---
188
+
189
+ ### Sizing Configuration
190
+
191
+ Control dimensions of the wheel and options:
192
+
193
+ ```tsx
194
+ <WheelSelect
195
+ sizing={{
196
+ wheelHeight: 400, // Height of the scroll container
197
+ wheelMinWidth: 280, // Minimum width
198
+ optionHeight: 64, // Height of each option
199
+ iconSize: 24, // Size of icons
200
+ }}
201
+ />
202
+ ```
203
+
204
+ ---
205
+
206
+ ### Behavior Configuration
207
+
208
+ Fine-tune interaction behavior:
209
+
210
+ ```tsx
211
+ <WheelSelect
212
+ behavior={{
213
+ closeOnOutsideClick: true, // Close when clicking backdrop
214
+ closeOnEscape: true, // Close on Escape key
215
+ closeOnSelect: true, // Close after selection
216
+ scrollDebounceMs: 50, // Scroll detection delay
217
+ keyboardNavigation: true, // Enable keyboard nav
218
+ focusTriggerOnClose: true, // Return focus after close
219
+ portalTarget: document.body, // Portal mount point
220
+ }}
221
+ />
222
+ ```
223
+
224
+ ---
225
+
226
+ ### Custom Icons
227
+
228
+ Replace default icons with your own:
229
+
230
+ ```tsx
231
+ import { ChevronDown, ArrowLeft } from 'lucide-react'
232
+
233
+ <WheelSelect
234
+ icons={{
235
+ chevron: <ChevronDown size={20} />,
236
+ arrow: <ArrowLeft size={20} />,
237
+ hideChevron: false, // Hide chevron icon
238
+ hideArrow: false, // Hide arrow on active item
239
+ }}
240
+ />
241
+ ```
242
+
243
+ ---
244
+
245
+ ### Accessibility
246
+
247
+ Full ARIA support with customizable labels:
248
+
249
+ ```tsx
250
+ <WheelSelect
251
+ a11y={{
252
+ triggerLabel: 'Select a fruit',
253
+ pickerLabel: 'Fruit options',
254
+ describedBy: 'fruit-helper-text',
255
+ }}
256
+ />
257
+ ```
258
+
259
+ **Keyboard Support:**
260
+
261
+ | Key | Action |
262
+ |-----|--------|
263
+ | `Enter` / `Space` | Open picker / Select option |
264
+ | `Escape` | Close picker |
265
+ | `โ†‘` / `โ†“` | Navigate options |
266
+ | `Home` | Jump to first option |
267
+ | `End` | Jump to last option |
268
+
269
+ ---
270
+
271
+ ### Event Callbacks
272
+
273
+ Subscribe to component events:
274
+
275
+ ```tsx
276
+ <WheelSelect
277
+ callbacks={{
278
+ onOpen: () => console.log('Picker opened'),
279
+ onClose: () => console.log('Picker closed'),
280
+ onChange: (value, option) => {
281
+ console.log('Selected:', value, option)
282
+ },
283
+ onActiveChange: (index, option) => {
284
+ console.log('Highlighted:', index, option)
285
+ },
286
+ onKeyDown: (event) => {
287
+ console.log('Key pressed:', event.key)
288
+ },
289
+ }}
290
+ />
291
+ ```
292
+
293
+ ---
294
+
295
+ ### Imperative API
296
+
297
+ Control the component programmatically using refs:
298
+
299
+ ```tsx
300
+ import { useRef } from 'react'
301
+ import { WheelSelect, WheelSelectRef } from 'react-wheel-select'
302
+
303
+ function App() {
304
+ const selectRef = useRef<WheelSelectRef>(null)
305
+
306
+ return (
307
+ <>
308
+ <WheelSelect ref={selectRef} {...props} />
309
+
310
+ <button onClick={() => selectRef.current?.open()}>
311
+ Open Picker
312
+ </button>
313
+
314
+ <button onClick={() => selectRef.current?.close()}>
315
+ Close Picker
316
+ </button>
317
+
318
+ <button onClick={() => selectRef.current?.scrollToIndex(5)}>
319
+ Scroll to Item 5
320
+ </button>
321
+ </>
322
+ )
323
+ }
324
+ ```
325
+
326
+ **Ref Methods:**
327
+
328
+ | Method | Description |
329
+ |--------|-------------|
330
+ | `open()` | Open the picker |
331
+ | `close()` | Close the picker |
332
+ | `toggle()` | Toggle open state |
333
+ | `focus()` | Focus the trigger |
334
+ | `isOpen()` | Get current open state |
335
+ | `scrollToIndex(n)` | Scroll to specific index |
336
+ | `getNativeSelect()` | Get native select element |
337
+
338
+ ---
339
+
340
+ ### CSS Customization
341
+
342
+ #### Using CSS Variables
343
+
344
+ Override any variable at the root or component level:
345
+
346
+ ```css
347
+ /* Global overrides */
348
+ :root {
349
+ --ws-color-active-bg: rgba(59, 130, 246, 0.2);
350
+ --ws-border-radius: 8px;
351
+ --ws-font-size: 20px;
352
+ }
353
+
354
+ /* Scoped overrides */
355
+ .my-custom-select {
356
+ --ws-color-text: #1a1a1a;
357
+ --ws-animation-duration: 300ms;
358
+ }
359
+ ```
360
+
361
+ #### Available CSS Variables
362
+
363
+ ```css
364
+ /* Colors */
365
+ --ws-color-text /* Text color */
366
+ --ws-color-text-muted /* Muted text color */
367
+ --ws-color-active-bg /* Active item background */
368
+ --ws-color-hover-bg /* Hover state background */
369
+ --ws-color-backdrop-bg /* Backdrop overlay color */
370
+ --ws-color-focus-ring /* Focus ring color */
371
+
372
+ /* Typography */
373
+ --ws-font-family /* Font family */
374
+ --ws-font-size /* Option font size */
375
+ --ws-font-weight /* Option font weight */
376
+ --ws-font-size-trigger /* Trigger font size */
377
+
378
+ /* Spacing */
379
+ --ws-border-radius /* Border radius */
380
+ --ws-trigger-gap /* Gap in trigger */
381
+ --ws-trigger-padding /* Trigger padding */
382
+ --ws-option-gap /* Gap in options */
383
+ --ws-option-padding /* Option padding */
384
+
385
+ /* Sizing */
386
+ --ws-wheel-height /* Wheel viewport height */
387
+ --ws-wheel-min-width /* Minimum wheel width */
388
+ --ws-option-height /* Option item height */
389
+ --ws-icon-size /* Icon dimensions */
390
+ --ws-spacer-height /* Top/bottom spacer */
391
+
392
+ /* Animation */
393
+ --ws-animation-duration /* Transition duration */
394
+ --ws-animation-easing /* Easing function */
395
+
396
+ /* Internal */
397
+ --ws-inactive-opacity /* Inactive items opacity */
398
+ --ws-hover-opacity /* Hover state opacity */
399
+ --ws-backdrop-blur /* Backdrop blur amount */
400
+ --ws-z-index /* Overlay z-index */
401
+ ```
402
+
403
+ #### Custom Class Names
404
+
405
+ Target specific elements:
406
+
407
+ ```css
408
+ .ws-root { } /* Root container */
409
+ .ws-trigger { } /* Trigger button */
410
+ .ws-trigger.ws-open { } /* Trigger when open */
411
+ .ws-trigger-text { } /* Trigger text */
412
+ .ws-chevron { } /* Chevron icon */
413
+ .ws-backdrop { } /* Fullscreen backdrop */
414
+ .ws-picker { } /* Picker container */
415
+ .ws-wheel { } /* Scrollable wheel */
416
+ .ws-spacer { } /* Top/bottom spacers */
417
+ .ws-option { } /* Option item */
418
+ .ws-option.ws-active { } /* Active/centered option */
419
+ .ws-option.ws-disabled { } /* Disabled option */
420
+ .ws-option-text { } /* Option label text */
421
+ .ws-arrow { } /* Active item arrow */
422
+ ```
423
+
424
+ ---
425
+
426
+ ### TypeScript
427
+
428
+ Full generic support for type-safe values:
429
+
430
+ ```tsx
431
+ // Define your value type
432
+ type Fruit = 'apple' | 'banana' | 'cherry'
433
+
434
+ // Options with typed values
435
+ const options: WheelSelectOption<Fruit>[] = [
436
+ { value: 'apple', label: 'Apple' },
437
+ { value: 'banana', label: 'Banana' },
438
+ { value: 'cherry', label: 'Cherry' },
439
+ ]
440
+
441
+ // Component with type inference
442
+ function App() {
443
+ const [fruit, setFruit] = useState<Fruit>('apple')
444
+
445
+ return (
446
+ <WheelSelect<Fruit>
447
+ options={options}
448
+ value={fruit}
449
+ onChange={setFruit} // Type-safe!
450
+ />
451
+ )
452
+ }
453
+ ```
454
+
455
+ ---
456
+
457
+ ## ๐ŸŽจ Examples
458
+
459
+ ### Inline Text Integration
460
+
461
+ ```tsx
462
+ <p className="sentence">
463
+ I want to{' '}
464
+ <WheelSelect
465
+ options={actions}
466
+ value={action}
467
+ onChange={setAction}
468
+ theme={{ font: { triggerSize: 'inherit' } }}
469
+ />
470
+ {' '}with my team.
471
+ </p>
472
+ ```
473
+
474
+ ### Custom Styled Trigger
475
+
476
+ ```tsx
477
+ <WheelSelect
478
+ renderTrigger={({ label, isOpen, onClick }) => (
479
+ <button
480
+ onClick={onClick}
481
+ className={`custom-trigger ${isOpen ? 'active' : ''}`}
482
+ >
483
+ {label}
484
+ <ChevronIcon />
485
+ </button>
486
+ )}
487
+ />
488
+ ```
489
+
490
+ ### Custom Option Rendering
491
+
492
+ ```tsx
493
+ <WheelSelect
494
+ renderOption={({ option, isActive, isSelected }) => (
495
+ <div className="custom-option">
496
+ <img src={option.data?.icon} alt="" />
497
+ <span>{option.label}</span>
498
+ {isSelected && <CheckIcon />}
499
+ </div>
500
+ )}
501
+ />
502
+ ```
503
+
504
+ ### With Form Integration
505
+
506
+ ```tsx
507
+ <form onSubmit={handleSubmit}>
508
+ <WheelSelect
509
+ name="country"
510
+ required
511
+ options={countries}
512
+ value={country}
513
+ onChange={setCountry}
514
+ />
515
+ <button type="submit">Submit</button>
516
+ </form>
517
+ ```
518
+
519
+ ### Disabled Options
520
+
521
+ ```tsx
522
+ const options = [
523
+ { value: 'free', label: 'Free Plan' },
524
+ { value: 'pro', label: 'Pro Plan' },
525
+ { value: 'enterprise', label: 'Enterprise', disabled: true },
526
+ ]
527
+ ```
528
+
529
+ ---
530
+
531
+ ## ๐ŸŒ Browser Support
532
+
533
+ - Chrome 88+
534
+ - Firefox 84+
535
+ - Safari 14+
536
+ - Edge 88+
537
+
538
+ Requires CSS `scroll-snap-type` and `backdrop-filter` support.
539
+
540
+ ---
541
+
542
+ ## ๐Ÿ“„ License
543
+
544
+ MIT ยฉ [Vasil Rashkov](https://github.com/vasilrashkov)
545
+
546
+ ---
547
+
548
+ ## ๐Ÿค Contributing
549
+
550
+ Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
551
+
552
+ 1. Fork the repository
553
+ 2. Create your feature branch (`git checkout -b feature/amazing`)
554
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
555
+ 4. Push to the branch (`git push origin feature/amazing`)
556
+ 5. Open a Pull Request
557
+
558
+ ---
559
+
560
+ ## ๐Ÿ’– Support
561
+
562
+ If you find this project useful, please consider:
563
+
564
+ - โญ Starring the repository
565
+ - ๐Ÿ› Reporting bugs
566
+ - ๐Ÿ’ก Suggesting features
567
+ - ๐Ÿ“– Improving documentation
568
+
569
+ ---
570
+
571
+ <div align="center">
572
+
573
+ Made with โค๏ธ for the React community
574
+
575
+ </div>
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ 'use strict';Object.defineProperty(exports,'__esModule',{value:true});var react=require('react'),reactDom=require('react-dom'),jsxRuntime=require('react/jsx-runtime');var Ee={colorScheme:"dark",colors:{text:"inherit",textMuted:"inherit",activeBg:"rgba(255, 255, 255, 0.12)",hoverBg:"rgba(255, 255, 255, 0.12)",backdropBg:"rgba(0, 0, 0, 0.5)",focusRing:"rgba(255, 255, 255, 0.5)"},borderRadius:12,font:{family:"inherit",size:28,weight:500,triggerSize:"inherit"},animation:{duration:200,easing:"ease",disabled:false},spacing:{triggerGap:16,triggerPadding:"8px 16px",optionGap:16,optionPadding:"0 20px"}},Oe={wheelHeight:320,wheelMinWidth:220,optionHeight:56,iconSize:20},We={closeOnOutsideClick:true,closeOnEscape:true,closeOnSelect:true,scrollDebounceMs:50,keyboardNavigation:true,portalTarget:null,focusTriggerOnClose:true},Pe=({size:i=20,className:a})=>jsxRuntime.jsxs("svg",{width:i,height:i,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:a,"aria-hidden":"true",children:[jsxRuntime.jsx("polyline",{points:"8 9 12 5 16 9"}),jsxRuntime.jsx("polyline",{points:"8 15 12 19 16 15"})]}),He=({size:i=20,className:a})=>jsxRuntime.jsxs("svg",{width:i,height:i,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:a,"aria-hidden":"true",children:[jsxRuntime.jsx("line",{x1:"19",y1:"12",x2:"5",y2:"12"}),jsxRuntime.jsx("polyline",{points:"12 19 5 12 12 5"})]}),D=(i,a="0")=>i===void 0?a:typeof i=="number"?`${i}px`:i,le=(i,a)=>{let r={...i};for(let c in a)a[c]!==void 0&&(typeof a[c]=="object"&&a[c]!==null&&!Array.isArray(a[c])?r[c]=le(i[c],a[c]):r[c]=a[c]);return r};function Me(i,a){let{options:r,value:c,onChange:q,placeholder:oe="Select...",disabled:V=false,required:R=false,name:te,id:v,className:A="",style:L,theme:j,sizing:H,behavior:M,icons:E,a11y:U,callbacks:k,renderTrigger:ie,renderOption:z,zIndex:re=10001}=i,s=react.useMemo(()=>le(Ee,j||{}),[j]),u=react.useMemo(()=>le(Oe,H||{}),[H]),g=react.useMemo(()=>le(We,M||{}),[M]),[b,x]=react.useState(false),[Y,O]=react.useState(()=>Math.max(0,r.findIndex(t=>t.value===c))),[W,ce]=react.useState(null),B=react.useRef(null),J=react.useRef(null),$=react.useRef(null),y=react.useRef([]),e=react.useRef(null),n=react.useRef(null),d=react.useRef(false),S=react.useMemo(()=>r.find(t=>t.value===c),[r,c]),T=S?.label??oe;react.useEffect(()=>{let t=r.findIndex(o=>o.value===c);t!==-1&&O(t);},[c,r]),react.useEffect(()=>()=>{n.current&&clearTimeout(n.current);},[]);let Q=react.useMemo(()=>{let t=typeof u.wheelHeight=="number"?u.wheelHeight:320,o=typeof u.optionHeight=="number"?u.optionHeight:56;return t/2-o/2},[u.wheelHeight,u.optionHeight]),P=react.useCallback(()=>{if(V)return;B.current&&ce(B.current.getBoundingClientRect());let t=r.findIndex(l=>l.value===c),o=t!==-1?t:0;e.current=o,O(o),x(true),k?.onOpen?.();},[V,r,c,k]),I=react.useCallback(()=>{x(false),g.focusTriggerOnClose&&B.current?.focus(),k?.onClose?.();},[g.focusTriggerOnClose,k]),X=react.useCallback(t=>{let o=r.find(l=>l.value===t);!o||o.disabled||(q(t),k?.onChange?.(t,o),J.current&&(J.current.value=t),g.closeOnSelect&&(x(false),g.focusTriggerOnClose&&B.current?.focus(),k?.onClose?.()));},[q,r,g.closeOnSelect,g.focusTriggerOnClose,k]);react.useEffect(()=>{if(b&&e.current!==null){let t=e.current;e.current=null,requestAnimationFrame(()=>{let o=$.current,l=y.current[t];if(o&&l){let f=o.clientHeight,h=l.clientHeight,Z=l.offsetTop-f/2+h/2;o.scrollTop=Z;}});}},[b]),react.useEffect(()=>{b&&$.current&&$.current.focus();},[b]);let ne=react.useCallback(()=>{let t=$.current;if(!t)return;let o=t.getBoundingClientRect(),l=o.top+o.height/2,f=0,h=1/0;y.current.forEach((Z,Te)=>{if(!Z)return;let be=Z.getBoundingClientRect(),Ce=be.top+be.height/2,fe=Math.abs(Ce-l);fe<h&&(h=fe,f=Te);}),O(f);let p=r[f];p&&k?.onActiveChange?.(f,p);},[r,k]),ve=react.useCallback(()=>{d.current=true,n.current&&clearTimeout(n.current),n.current=setTimeout(()=>{d.current=false,ne();},g.scrollDebounceMs);},[ne,g.scrollDebounceMs]),we=react.useCallback(t=>{if(g.keyboardNavigation)switch(k?.onKeyDown?.(t),t.key){case "Escape":g.closeOnEscape&&(t.preventDefault(),I());break;case "ArrowUp":t.preventDefault(),O(h=>{let p=h-1;for(;p>=0&&r[p]?.disabled;)p--;return p<0?h:(y.current[p]?.scrollIntoView({block:"center",behavior:"smooth"}),p)});break;case "ArrowDown":t.preventDefault(),O(h=>{let p=h+1;for(;p<r.length&&r[p]?.disabled;)p++;return p>=r.length?h:(y.current[p]?.scrollIntoView({block:"center",behavior:"smooth"}),p)});break;case "Enter":case " ":t.preventDefault();let o=r[Y];o&&!o.disabled&&X(o.value);break;case "Home":t.preventDefault();let l=r.findIndex(h=>!h.disabled);l!==-1&&(O(l),y.current[l]?.scrollIntoView({block:"center",behavior:"smooth"}));break;case "End":t.preventDefault();let f=r.findLastIndex(h=>!h.disabled);f!==-1&&(O(f),y.current[f]?.scrollIntoView({block:"center",behavior:"smooth"}));break}},[g.keyboardNavigation,g.closeOnEscape,r,Y,I,X,k]),ue=react.useCallback(t=>o=>{o.preventDefault(),o.stopPropagation(),n.current&&(clearTimeout(n.current),n.current=null);let l=r[t];l&&!l.disabled&&X(l.value);},[r,X]),ke=react.useCallback(t=>{g.closeOnOutsideClick&&t.target===t.currentTarget&&I();},[g.closeOnOutsideClick,I]),xe=react.useCallback(t=>{q(t.target.value);},[q]),ge=react.useCallback(t=>{y.current[t]?.scrollIntoView({block:"center",behavior:"smooth"});},[]);react.useImperativeHandle(a,()=>({open:P,close:I,toggle:()=>b?I():P(),focus:()=>B.current?.focus(),isOpen:()=>b,scrollToIndex:ge,getNativeSelect:()=>J.current}),[b,P,I,ge]);let he=react.useMemo(()=>({"--ws-color-text":s.colors.text,"--ws-color-text-muted":s.colors.textMuted,"--ws-color-active-bg":s.colors.activeBg,"--ws-color-hover-bg":s.colors.hoverBg,"--ws-color-backdrop-bg":s.colors.backdropBg,"--ws-color-focus-ring":s.colors.focusRing,"--ws-border-radius":D(s.borderRadius),"--ws-font-family":s.font.family,"--ws-font-size":D(s.font.size),"--ws-font-weight":String(s.font.weight),"--ws-font-size-trigger":D(s.font.triggerSize),"--ws-animation-duration":s.animation.disabled?"0ms":`${s.animation.duration}ms`,"--ws-animation-easing":s.animation.easing,"--ws-trigger-gap":D(s.spacing.triggerGap),"--ws-trigger-padding":s.spacing.triggerPadding,"--ws-option-gap":D(s.spacing.optionGap),"--ws-option-padding":s.spacing.optionPadding,"--ws-wheel-height":D(u.wheelHeight),"--ws-wheel-min-width":D(u.wheelMinWidth),"--ws-option-height":D(u.optionHeight),"--ws-icon-size":`${u.iconSize}px`,"--ws-spacer-height":`${Q}px`,"--ws-z-index":String(re)}),[s,u,Q,re]),ye=s.colorScheme==="auto"?"":s.colorScheme==="light"?"ws-light":"ws-dark",Se=()=>{if(!b||!W)return null;let t=g.portalTarget??document.body;return reactDom.createPortal(jsxRuntime.jsx("div",{className:"ws-backdrop",onClick:ke,role:"presentation",style:he,children:jsxRuntime.jsx("div",{className:"ws-picker",style:{left:W.left,top:W.top+W.height/2},role:"dialog","aria-modal":"true","aria-label":U?.pickerLabel??"Select an option",children:jsxRuntime.jsxs("div",{ref:$,className:"ws-wheel",role:"listbox",tabIndex:0,onKeyDown:we,onScroll:ve,"aria-activedescendant":`ws-option-${v??"default"}-${Y}`,"aria-describedby":U?.describedBy,children:[jsxRuntime.jsx("div",{className:"ws-spacer","aria-hidden":"true"}),r.map((o,l)=>{let f=l===Y,h=o.value===c;return z?jsxRuntime.jsx("div",{ref:p=>{y.current[l]=p;},id:`ws-option-${v??"default"}-${l}`,role:"option","aria-selected":h,"aria-disabled":o.disabled,className:`ws-option ${f?"ws-active":""} ${o.disabled?"ws-disabled":""}`,onClick:ue(l),children:z({option:o,index:l,isActive:f,isSelected:h})},o.value):jsxRuntime.jsxs("div",{ref:p=>{y.current[l]=p;},id:`ws-option-${v??"default"}-${l}`,role:"option","aria-selected":h,"aria-disabled":o.disabled,className:`ws-option ${f?"ws-active":""} ${o.disabled?"ws-disabled":""}`,onClick:ue(l),children:[jsxRuntime.jsx("span",{className:"ws-option-text",children:o.label}),f&&!E?.hideArrow&&(E?.arrow??jsxRuntime.jsx(He,{size:u.iconSize,className:"ws-arrow"}))]},o.value)}),jsxRuntime.jsx("div",{className:"ws-spacer","aria-hidden":"true"})]})})}),t)};return jsxRuntime.jsxs("span",{className:`ws-root ${ye} ${A}`,style:{...he,...L},children:[jsxRuntime.jsxs("select",{ref:J,name:te,id:v?`${v}-native`:void 0,value:c,onChange:xe,className:"ws-native-select",tabIndex:-1,"aria-hidden":"true",required:R,disabled:V,children:[!S&&jsxRuntime.jsx("option",{value:"",children:oe}),r.map(t=>jsxRuntime.jsx("option",{value:t.value,disabled:t.disabled,children:t.label},t.value))]}),ie?ie({value:c,label:T,isOpen:b,disabled:V,onClick:P}):jsxRuntime.jsxs("button",{ref:B,type:"button",id:v,className:`ws-trigger ${b?"ws-open":""}`,onClick:P,disabled:V,"aria-haspopup":"listbox","aria-expanded":b,"aria-label":U?.triggerLabel,"aria-describedby":U?.describedBy,style:{visibility:b?"hidden":"visible"},children:[jsxRuntime.jsx("span",{className:"ws-trigger-text",children:T}),!E?.hideChevron&&(E?.chevron??jsxRuntime.jsx(Pe,{size:u.iconSize,className:"ws-chevron"}))]}),Se()]})}var me=react.forwardRef(Me),Be=me;var Ae=()=>jsxRuntime.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:"base-select-chevron",children:[jsxRuntime.jsx("polyline",{points:"8 9 12 5 16 9"}),jsxRuntime.jsx("polyline",{points:"8 15 12 19 16 15"})]}),Le=()=>jsxRuntime.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:"base-select-arrow",children:[jsxRuntime.jsx("line",{x1:"19",y1:"12",x2:"5",y2:"12"}),jsxRuntime.jsx("polyline",{points:"12 19 5 12 12 5"})]});function ze({options:i,value:a,onChange:r,name:c,id:q,className:oe=""}){let[R,te]=react.useState(false),[v,A]=react.useState(()=>Math.max(0,i.findIndex(e=>e.value===a))),L=react.useRef(null),j=react.useRef(null),H=react.useRef(null),M=react.useRef([]),[E,U]=react.useState(null);react.useEffect(()=>{let e=i.findIndex(n=>n.value===a);e!==-1&&A(e);},[a,i]);let ie=i.find(e=>e.value===a)?.label??i[0]?.label??"",z=react.useRef(null),re=react.useCallback(()=>{L.current&&U(L.current.getBoundingClientRect());let e=i.findIndex(d=>d.value===a),n=e!==-1?e:0;z.current=n,A(n),te(true);},[i,a]),s=react.useCallback(()=>{te(false),L.current?.focus();},[]),u=react.useCallback(e=>{r(e),j.current&&(j.current.value=e),te(false),L.current?.focus();},[r]);react.useEffect(()=>{if(R&&z.current!==null){let e=z.current;z.current=null,requestAnimationFrame(()=>{let n=H.current,d=M.current[e];if(n&&d){let S=n.clientHeight,T=d.clientHeight,P=d.offsetTop-S/2+T/2;n.scrollTop=P;}});}},[R]);let g=react.useCallback(()=>{let e=H.current;if(!e)return;let n=e.getBoundingClientRect(),d=n.top+n.height/2,S=0,T=1/0;M.current.forEach((Q,P)=>{if(!Q)return;let I=Q.getBoundingClientRect(),X=I.top+I.height/2,ne=Math.abs(X-d);ne<T&&(T=ne,S=P);}),A(S);},[]),b=react.useRef(false),x=react.useRef(null),Y=react.useCallback(()=>{b.current=true,x.current&&clearTimeout(x.current),x.current=setTimeout(()=>{b.current=false,g();},50);},[g]),O=react.useCallback(e=>{switch(e.key){case "Escape":e.preventDefault(),s();break;case "ArrowUp":e.preventDefault(),A(n=>{let d=Math.max(0,n-1);return M.current[d]?.scrollIntoView({block:"center",behavior:"smooth"}),d});break;case "ArrowDown":e.preventDefault(),A(n=>{let d=Math.min(i.length-1,n+1);return M.current[d]?.scrollIntoView({block:"center",behavior:"smooth"}),d});break;case "Enter":e.preventDefault(),i[v]&&u(i[v].value);break}},[v,i,s,u]),W=react.useCallback(e=>{i[e]&&u(i[e].value);},[i,u]),ce=react.useCallback(e=>n=>{n.preventDefault(),n.stopPropagation(),x.current&&(clearTimeout(x.current),x.current=null),W(e);},[W]),B=react.useCallback(e=>{let d=e.target.closest(".base-select-option");if(d){let S=d.id,T=parseInt(S.replace("option-",""),10);!isNaN(T)&&i[T]&&W(T);}},[i,W]),J=react.useCallback(e=>{e.target===e.currentTarget&&s();},[s]);react.useEffect(()=>{R&&H.current&&H.current.focus();},[R]),react.useEffect(()=>()=>{x.current&&clearTimeout(x.current);},[]);let $=react.useCallback(e=>{r(e.target.value);},[r]),y=()=>!R||!E?null:reactDom.createPortal(jsxRuntime.jsx("div",{className:"base-select-backdrop",onClick:J,role:"presentation",children:jsxRuntime.jsx("div",{className:"base-select-picker",style:{left:E.left,top:E.top+E.height/2},role:"dialog","aria-modal":"true","aria-label":"Select an option",children:jsxRuntime.jsxs("div",{ref:H,className:"base-select-wheel",role:"listbox",tabIndex:0,onKeyDown:O,onScroll:Y,onClick:B,"aria-activedescendant":`option-${v}`,children:[jsxRuntime.jsx("div",{className:"base-select-spacer","aria-hidden":"true"}),i.map((e,n)=>jsxRuntime.jsxs("div",{ref:d=>{M.current[n]=d;},id:`option-${n}`,role:"option","aria-selected":n===v,className:`base-select-option ${n===v?"active":""}`,onClick:ce(n),children:[jsxRuntime.jsx("span",{className:"base-select-option-text",children:e.label}),n===v&&jsxRuntime.jsx(Le,{})]},e.value)),jsxRuntime.jsx("div",{className:"base-select-spacer","aria-hidden":"true"})]})})}),document.body);return jsxRuntime.jsxs("span",{className:`base-select-compat fallback ${oe}`,children:[jsxRuntime.jsx("select",{ref:j,name:c,id:q,value:a,onChange:$,className:"base-select-hidden",tabIndex:-1,"aria-hidden":"true",children:i.map(e=>jsxRuntime.jsx("option",{value:e.value,children:e.label},e.value))}),jsxRuntime.jsxs("button",{ref:L,type:"button",className:`base-select-trigger ${R?"open":""}`,onClick:re,"aria-haspopup":"listbox","aria-expanded":R,style:{visibility:R?"hidden":"visible"},children:[jsxRuntime.jsx("span",{className:"base-select-trigger-text",children:ie}),jsxRuntime.jsx(Ae,{})]}),y()]})}exports.BaseSelectCompat=ze;exports.WheelSelect=me;exports.default=Be;//# sourceMappingURL=index.cjs.map
2
+ //# sourceMappingURL=index.cjs.map