@yewdiel21/motion-kit 0.16.0 → 0.18.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.
Files changed (158) hide show
  1. package/README.md +44 -0
  2. package/dist/auto/index.d.ts +23 -3
  3. package/dist/auto/index.js +43 -3
  4. package/dist/auto/index.js.map +1 -1
  5. package/dist/{chunk-RBEKR5UZ.js → chunk-AWDEKDEJ.js} +2 -2
  6. package/dist/{chunk-NFZIX3IA.js → chunk-EDMXWUW3.js} +2 -2
  7. package/dist/{chunk-SDQ776LG.js → chunk-Q7IW4FV3.js} +3941 -43
  8. package/dist/chunk-Q7IW4FV3.js.map +1 -0
  9. package/dist/css/components/badge-button.css +16 -0
  10. package/dist/css/components/bento-grid.css +13 -0
  11. package/dist/css/components/drilldown-menu.css +134 -0
  12. package/dist/css/components/equalizer-slider.css +26 -0
  13. package/dist/css/components/handwriting-text.css +15 -0
  14. package/dist/css/components/magnetize-button.css +18 -0
  15. package/dist/css/components/morphing-page-dots.css +13 -0
  16. package/dist/css/components/not-found.css +22 -0
  17. package/dist/css/components/notification-stack.css +42 -0
  18. package/dist/css/components/particle-button.css +14 -0
  19. package/dist/css/components/radio-group.css +29 -0
  20. package/dist/css/components/rating-scale.css +23 -0
  21. package/dist/css/components/scroll-preview-dock.css +18 -0
  22. package/dist/css/components/select.css +16 -0
  23. package/dist/css/components/signature-pad.css +131 -0
  24. package/dist/css/components/stepper.css +36 -0
  25. package/dist/css/components/sticky-header.css +10 -0
  26. package/dist/css/components/temperature-slider.css +29 -0
  27. package/dist/css/components/voice-message-bubble.css +67 -0
  28. package/dist/css/components/waveform-player.css +51 -0
  29. package/dist/css/components/wheel-picker.css +18 -0
  30. package/dist/css/tokens.css +4 -0
  31. package/dist/elements/index.d.ts +292 -4
  32. package/dist/elements/index.js +856 -4
  33. package/dist/elements/index.js.map +1 -1
  34. package/dist/{reveal-DRpNO4KP.d.ts → engine-t8d7brjn.d.ts} +534 -1
  35. package/dist/index.css +713 -0
  36. package/dist/index.d.ts +23 -23
  37. package/dist/index.js +43 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/react/index.d.ts +151 -7
  40. package/dist/react/index.js +893 -3
  41. package/dist/react/index.js.map +1 -1
  42. package/dist/{reduced-DQwVXQYb.d.ts → reduced-dPsZYqtI.d.ts} +1 -1
  43. package/dist/{store-Da3zSh8Z.d.ts → store-6hZNKiee.d.ts} +1 -1
  44. package/package.json +110 -110
  45. package/registry.json +244 -0
  46. package/src/auto/auto-init.ts +339 -293
  47. package/src/components/badge-button/README.md +28 -0
  48. package/src/components/badge-button/badge-button.css +15 -0
  49. package/src/components/badge-button/element.ts +16 -0
  50. package/src/components/badge-button/engine.ts +85 -0
  51. package/src/components/badge-button/react.tsx +31 -0
  52. package/src/components/bento-grid/README.md +26 -0
  53. package/src/components/bento-grid/bento-grid.css +12 -0
  54. package/src/components/bento-grid/element.ts +22 -0
  55. package/src/components/bento-grid/engine.ts +153 -0
  56. package/src/components/bento-grid/react.tsx +25 -0
  57. package/src/components/drilldown-menu/README.md +163 -0
  58. package/src/components/drilldown-menu/drilldown-menu.css +133 -0
  59. package/src/components/drilldown-menu/element.ts +59 -0
  60. package/src/components/drilldown-menu/engine.ts +378 -0
  61. package/src/components/drilldown-menu/react.tsx +76 -0
  62. package/src/components/equalizer-slider/README.md +43 -0
  63. package/src/components/equalizer-slider/element.ts +41 -0
  64. package/src/components/equalizer-slider/engine.ts +32 -0
  65. package/src/components/equalizer-slider/equalizer-slider.css +26 -0
  66. package/src/components/equalizer-slider/react.tsx +60 -0
  67. package/src/components/equalizer-slider/shared.ts +161 -0
  68. package/src/components/handwriting-text/README.md +29 -0
  69. package/src/components/handwriting-text/element.ts +14 -0
  70. package/src/components/handwriting-text/engine.ts +108 -0
  71. package/src/components/handwriting-text/glyphs.ts +71 -0
  72. package/src/components/handwriting-text/handwriting-text.css +14 -0
  73. package/src/components/handwriting-text/react.tsx +23 -0
  74. package/src/components/magnetize-button/README.md +26 -0
  75. package/src/components/magnetize-button/element.ts +13 -0
  76. package/src/components/magnetize-button/engine.ts +84 -0
  77. package/src/components/magnetize-button/magnetize-button.css +17 -0
  78. package/src/components/magnetize-button/react.tsx +22 -0
  79. package/src/components/morphing-page-dots/README.md +28 -0
  80. package/src/components/morphing-page-dots/element.ts +16 -0
  81. package/src/components/morphing-page-dots/engine.ts +89 -0
  82. package/src/components/morphing-page-dots/morphing-page-dots.css +12 -0
  83. package/src/components/morphing-page-dots/react.tsx +32 -0
  84. package/src/components/not-found/README.md +26 -0
  85. package/src/components/not-found/element.ts +15 -0
  86. package/src/components/not-found/engine.ts +57 -0
  87. package/src/components/not-found/not-found.css +21 -0
  88. package/src/components/not-found/react.tsx +23 -0
  89. package/src/components/notification-stack/README.md +24 -0
  90. package/src/components/notification-stack/element.ts +26 -0
  91. package/src/components/notification-stack/engine.ts +329 -0
  92. package/src/components/notification-stack/notification-stack.css +41 -0
  93. package/src/components/notification-stack/react.tsx +28 -0
  94. package/src/components/particle-button/README.md +26 -0
  95. package/src/components/particle-button/element.ts +13 -0
  96. package/src/components/particle-button/engine.ts +52 -0
  97. package/src/components/particle-button/particle-button.css +13 -0
  98. package/src/components/particle-button/react.tsx +22 -0
  99. package/src/components/radio-group/README.md +43 -0
  100. package/src/components/radio-group/element.ts +41 -0
  101. package/src/components/radio-group/engine.ts +11 -0
  102. package/src/components/radio-group/radio-group.css +28 -0
  103. package/src/components/radio-group/react.tsx +60 -0
  104. package/src/components/radio-group/shared.ts +118 -0
  105. package/src/components/rating-scale/README.md +43 -0
  106. package/src/components/rating-scale/element.ts +41 -0
  107. package/src/components/rating-scale/engine.ts +63 -0
  108. package/src/components/rating-scale/rating-scale.css +22 -0
  109. package/src/components/rating-scale/react.tsx +60 -0
  110. package/src/components/scroll-preview-dock/README.md +24 -0
  111. package/src/components/scroll-preview-dock/element.ts +20 -0
  112. package/src/components/scroll-preview-dock/engine.ts +228 -0
  113. package/src/components/scroll-preview-dock/react.tsx +26 -0
  114. package/src/components/scroll-preview-dock/scroll-preview-dock.css +17 -0
  115. package/src/components/select/README.md +23 -9
  116. package/src/components/select/select.css +16 -0
  117. package/src/components/signature-pad/README.md +97 -0
  118. package/src/components/signature-pad/element.ts +57 -0
  119. package/src/components/signature-pad/engine.ts +550 -0
  120. package/src/components/signature-pad/react.tsx +56 -0
  121. package/src/components/signature-pad/signature-pad.css +130 -0
  122. package/src/components/stepper/README.md +46 -0
  123. package/src/components/stepper/element.ts +42 -0
  124. package/src/components/stepper/engine.ts +103 -0
  125. package/src/components/stepper/react.tsx +60 -0
  126. package/src/components/stepper/stepper.css +35 -0
  127. package/src/components/sticky-header/README.md +30 -0
  128. package/src/components/sticky-header/element.ts +19 -0
  129. package/src/components/sticky-header/engine.ts +95 -0
  130. package/src/components/sticky-header/react.tsx +25 -0
  131. package/src/components/sticky-header/sticky-header.css +9 -0
  132. package/src/components/temperature-slider/README.md +42 -0
  133. package/src/components/temperature-slider/element.ts +53 -0
  134. package/src/components/temperature-slider/engine.ts +44 -0
  135. package/src/components/temperature-slider/react.tsx +66 -0
  136. package/src/components/temperature-slider/temperature-slider.css +29 -0
  137. package/src/components/voice-message-bubble/README.md +31 -0
  138. package/src/components/voice-message-bubble/element.ts +20 -0
  139. package/src/components/voice-message-bubble/engine.ts +79 -0
  140. package/src/components/voice-message-bubble/react.tsx +27 -0
  141. package/src/components/voice-message-bubble/voice-message-bubble.css +14 -0
  142. package/src/components/waveform-player/README.md +30 -0
  143. package/src/components/waveform-player/element.ts +21 -0
  144. package/src/components/waveform-player/engine.ts +344 -0
  145. package/src/components/waveform-player/react.tsx +27 -0
  146. package/src/components/waveform-player/waveform-player.css +50 -0
  147. package/src/components/wheel-picker/README.md +35 -0
  148. package/src/components/wheel-picker/element.ts +25 -0
  149. package/src/components/wheel-picker/engine.ts +239 -0
  150. package/src/components/wheel-picker/react.tsx +32 -0
  151. package/src/components/wheel-picker/wheel-picker.css +17 -0
  152. package/src/css/tokens.css +4 -0
  153. package/src/elements/index.ts +318 -258
  154. package/src/index.ts +21 -0
  155. package/src/react/index.ts +192 -171
  156. package/dist/chunk-SDQ776LG.js.map +0 -1
  157. /package/dist/{chunk-RBEKR5UZ.js.map → chunk-AWDEKDEJ.js.map} +0 -0
  158. /package/dist/{chunk-NFZIX3IA.js.map → chunk-EDMXWUW3.js.map} +0 -0
@@ -0,0 +1,239 @@
1
+ import { createScope } from '../../core/scope';
2
+ import { reduced } from '../../core/reduced';
3
+ import { inert, type Instance } from '../../core/types';
4
+ import { gsap, EASE, registerMotion } from '../../core/gsap';
5
+
6
+ export interface WheelPickerItem { value: string; label: string; disabled?: boolean }
7
+ export type WheelPickerEntry = string | WheelPickerItem;
8
+ export interface WheelPickerOptions {
9
+ items?: WheelPickerEntry[];
10
+ value?: string;
11
+ name?: string;
12
+ label?: string;
13
+ disabled?: boolean;
14
+ /** Odd number of visible rows, 3…9 (default 5). */
15
+ visibleItems?: number;
16
+ /** Height per row in px, at least 44 (default 44). */
17
+ itemHeight?: number;
18
+ onInput?: (value: string) => void;
19
+ onChange?: (value: string) => void;
20
+ }
21
+ export interface WheelPickerInstance extends Instance {
22
+ setValue(value: string, notify?: boolean): void;
23
+ setItems(items: WheelPickerEntry[], notify?: boolean): void;
24
+ readonly value: string;
25
+ }
26
+
27
+ let sequence = 0;
28
+ function normalize(items: WheelPickerEntry[]) {
29
+ const seen = new Set<string>();
30
+ return items.flatMap(item => {
31
+ const row = typeof item === 'string' ? { value: item, label: item } : item;
32
+ if (!row || typeof row !== 'object' || row.value == null) return [];
33
+ const value = String(row.value);
34
+ if (seen.has(value)) return [];
35
+ seen.add(value);
36
+ return [{ value, label: String(row.label ?? value), disabled: Boolean(row.disabled) }];
37
+ });
38
+ }
39
+
40
+ /** Native scrolling and inertia with a centred snap row, roving selection and a real form value. */
41
+ export function createWheelPicker(root: HTMLElement | null, options: WheelPickerOptions = {}): WheelPickerInstance {
42
+ if (!root) return inert({ setValue() {}, setItems() {}, value: '' });
43
+ registerMotion();
44
+ const scope = createScope(), uid = `mk-wheel-${++sequence}`;
45
+ let items = normalize(Array.isArray(options.items) ? options.items : []);
46
+ const rawRows = Number.isFinite(options.visibleItems) ? Math.round(options.visibleItems!) : 5;
47
+ const rows = Math.min(9, Math.max(3, rawRows % 2 ? rawRows : rawRows + 1));
48
+ const height = Number.isFinite(options.itemHeight) ? Math.max(44, Math.min(96, options.itemHeight!)) : 44;
49
+ const disabled = Boolean(options.disabled);
50
+ const originalStyle = root.getAttribute('style');
51
+ root.style.setProperty('--mk-wheel-row', `${height}px`);
52
+ root.style.setProperty('--mk-wheel-rows', String(rows));
53
+ scope.add(() => { if (originalStyle === null) root.removeAttribute('style'); else root.setAttribute('style', originalStyle); });
54
+ scope.attr(root, 'data-mk-ready', ''); scope.attr(root, 'data-mk-disabled', disabled ? '' : null);
55
+ const label = document.createElement('span'); label.id = `${uid}-label`; label.dataset.mkWheelLabel = ''; label.textContent = options.label ?? root.getAttribute('aria-label') ?? 'Seleccionar';
56
+ const frame = document.createElement('div'); frame.dataset.mkWheelFrame = '';
57
+ const highlight = document.createElement('span'); highlight.dataset.mkWheelHighlight = ''; highlight.setAttribute('aria-hidden', 'true');
58
+ const viewport = document.createElement('div'); viewport.dataset.mkWheelViewport = ''; viewport.setAttribute('role', 'listbox'); viewport.setAttribute('aria-labelledby', label.id); viewport.setAttribute('aria-orientation', 'vertical'); viewport.setAttribute('aria-disabled', String(disabled)); viewport.tabIndex = disabled ? -1 : 0;
59
+ const list = document.createElement('div'); list.dataset.mkWheelList = ''; viewport.append(list); frame.append(highlight, viewport);
60
+ const input = document.createElement('input'); input.type = 'hidden'; if (options.name) input.name = options.name; input.disabled = disabled;
61
+ const status = document.createElement('span'); status.className = 'mk-sr-only'; status.setAttribute('role', 'status');
62
+ root.append(label, frame, input, status);
63
+ scope.add(() => { label.remove(); frame.remove(); input.remove(); status.remove(); });
64
+ const enabled = () => items.flatMap((item, index) => item.disabled ? [] : [index]);
65
+ const find = (value: string | undefined) => items.findIndex(item => value !== undefined && item.value === String(value) && !item.disabled);
66
+ let current = find(options.value); if (current < 0) current = enabled()[0] ?? -1;
67
+ const initial = items[current]?.value ?? '';
68
+ let committed = initial;
69
+ let nodes: HTMLDivElement[] = [];
70
+ let timer: ReturnType<typeof setTimeout> | undefined;
71
+ let typing: ReturnType<typeof setTimeout> | undefined;
72
+ let prefix = '';
73
+ let programmatic: number | null = null;
74
+ let glide: gsap.core.Tween | null = null;
75
+ let feedback: gsap.core.Tween | null = null;
76
+ let dragging: { id: number; y: number; top: number; moved: boolean } | null = null;
77
+ let suppressClick = false;
78
+ scope.add(() => { clearTimeout(timer); clearTimeout(typing); glide?.kill(); feedback?.kill(); });
79
+
80
+ // The drum's radius is half the frame, so 90° lands on its edge and each row keeps
81
+ // its arc length: rows compress, narrow and recede towards the rims like iOS.
82
+ const radius = height * rows / 2, step = height / radius, camera = radius * 3;
83
+ const paint = () => {
84
+ const offset = viewport.scrollTop / height, flat = reduced();
85
+ nodes.forEach((node, index) => {
86
+ const distance = index - offset;
87
+ // Project each row onto a cylinder. The ordinary list keeps native scroll,
88
+ // touch inertia and option semantics; Y/Z and orientation form the wheel.
89
+ const angle = distance * step, hidden = Math.abs(angle) >= Math.PI / 2;
90
+ const depth = radius * (Math.cos(angle) - 1);
91
+ // Depth narrows and shrinks the row; its centre stays on the drum's silhouette
92
+ // so the rims meet the frame instead of floating inside it.
93
+ const cameraScale = camera / (camera - depth);
94
+ const shiftY = (radius * Math.sin(angle) - distance * height) / cameraScale;
95
+ node.style.transform = flat || hidden ? '' : `perspective(${camera.toFixed(0)}px) translate3d(0, ${shiftY.toFixed(3)}px, ${depth.toFixed(3)}px) rotateX(${(-angle * 180 / Math.PI).toFixed(3)}deg)`;
96
+ node.style.opacity = String((items[index].disabled ? .32 : 1) * (flat ? 1 : hidden ? 0 : Math.pow(Math.cos(angle), 1.6)));
97
+ node.style.visibility = !flat && hidden ? 'hidden' : '';
98
+ node.style.pointerEvents = !flat && Math.abs(angle) > 1.2 ? 'none' : '';
99
+ node.setAttribute('aria-selected', String(index === current));
100
+ });
101
+ input.value = items[current]?.value ?? '';
102
+ if (current >= 0) viewport.setAttribute('aria-activedescendant', `${uid}-${current}`);
103
+ else viewport.removeAttribute('aria-activedescendant');
104
+ };
105
+ const announce = (kind: 'input' | 'change') => {
106
+ const value = items[current]?.value ?? '';
107
+ if (kind === 'input') options.onInput?.(value);
108
+ else {
109
+ options.onChange?.(value); status.textContent = items[current]?.label ?? '';
110
+ feedback?.kill();
111
+ if (!reduced()) feedback = gsap.fromTo(highlight, { scaleX: .94, scaleY: .82 }, { scaleX: 1, scaleY: 1, duration: .5, ease: EASE.click, clearProps: 'transform' });
112
+ }
113
+ input.dispatchEvent(new Event(kind, { bubbles: true }));
114
+ root.dispatchEvent(new CustomEvent(`mk:${kind}`, { bubbles: true, detail: { value, item: current >= 0 ? { ...items[current] } : null } }));
115
+ };
116
+ const align = (animate: boolean) => {
117
+ glide?.kill(); glide = null;
118
+ const top = Math.max(0, current) * height;
119
+ if (animate && !reduced()) {
120
+ programmatic = top;
121
+ const proxy = { top: viewport.scrollTop };
122
+ glide = gsap.to(proxy, { top, duration: .55, ease: EASE.swatch,
123
+ onUpdate: () => { viewport.scrollTop = proxy.top; paint(); },
124
+ onComplete: () => { glide = null; programmatic = null; viewport.scrollTop = top; paint(); },
125
+ });
126
+ } else { programmatic = null; viewport.scrollTop = top; paint(); }
127
+ };
128
+ const select = (index: number, notify: boolean, animate = true) => {
129
+ if (scope.disposed || index < 0 || index >= items.length || items[index].disabled) return;
130
+ const previous = items[current]?.value ?? '';
131
+ current = index; paint(); align(animate);
132
+ const value = items[current].value;
133
+ committed = value;
134
+ if (notify && value !== previous) { announce('input'); announce('change'); }
135
+ };
136
+ const nearest = () => {
137
+ const at = viewport.scrollTop / height;
138
+ return enabled().reduce((best, index) => best < 0 || Math.abs(index - at) < Math.abs(best - at) ? index : best, -1);
139
+ };
140
+ const settle = () => {
141
+ if (scope.disposed || dragging) return;
142
+ if (glide?.isActive()) return;
143
+ clearTimeout(timer);
144
+ if (programmatic !== null) {
145
+ // Keep controlled changes from notifying as native scrolling passes intermediate rows.
146
+ programmatic = null; viewport.scrollTop = Math.max(0, current) * height; paint(); return;
147
+ }
148
+ const index = nearest(); if (index < 0) return;
149
+ current = index; align(false); paint();
150
+ const value = items[current].value;
151
+ if (value !== committed) { committed = value; announce('change'); }
152
+ };
153
+ const build = () => {
154
+ list.replaceChildren();
155
+ nodes = items.map((item, index) => {
156
+ const node = document.createElement('div'); node.id = `${uid}-${index}`; node.dataset.mkWheelOption = String(index); node.setAttribute('role', 'option'); node.setAttribute('aria-disabled', String(item.disabled)); node.textContent = item.label; list.append(node); return node;
157
+ });
158
+ if (!items.length) { const empty = document.createElement('span'); empty.dataset.mkWheelEmpty = ''; empty.textContent = 'Sin opciones'; list.append(empty); }
159
+ align(false); paint();
160
+ };
161
+ const interrupt = () => {
162
+ glide?.kill(); glide = null;
163
+ programmatic = null;
164
+ };
165
+ scope.on(viewport, 'pointerdown', event => {
166
+ interrupt();
167
+ const pointer = event as PointerEvent;
168
+ if (disabled || pointer.pointerType === 'touch' || pointer.button !== 0) return;
169
+ dragging = { id: pointer.pointerId, y: pointer.clientY, top: viewport.scrollTop, moved: false };
170
+ viewport.focus({ preventScroll: true });
171
+ });
172
+ scope.on(window, 'pointermove', event => {
173
+ const pointer = event as PointerEvent;
174
+ if (!dragging || pointer.pointerId !== dragging.id) return;
175
+ const delta = pointer.clientY - dragging.y;
176
+ if (Math.abs(delta) > 4 && !dragging.moved) {
177
+ dragging.moved = true; viewport.style.scrollSnapType = 'none';
178
+ try { viewport.setPointerCapture(pointer.pointerId); } catch { /* Synthetic events have no pointer capture. */ }
179
+ }
180
+ if (!dragging.moved) return;
181
+ event.preventDefault();
182
+ viewport.scrollTop = Math.max(0, Math.min(Math.max(0, (items.length - 1) * height), dragging.top - delta));
183
+ });
184
+ const release = (event: Event) => {
185
+ if (!dragging || dragging.id !== (event as PointerEvent).pointerId) return;
186
+ suppressClick = dragging.moved; dragging = null; viewport.style.removeProperty('scroll-snap-type');
187
+ if (suppressClick) { settle(); clearTimeout(timer); timer = setTimeout(() => { suppressClick = false; }, 0); }
188
+ };
189
+ scope.on(window, 'pointerup', release); scope.on(window, 'pointercancel', release); scope.on(viewport, 'lostpointercapture', release);
190
+ scope.on(viewport, 'wheel', interrupt, { passive: true });
191
+ scope.on(viewport, 'scroll', () => {
192
+ if (disabled) return;
193
+ if (programmatic === null) {
194
+ const index = nearest();
195
+ if (index >= 0 && index !== current) { current = index; paint(); announce('input'); }
196
+ }
197
+ paint(); clearTimeout(timer); timer = setTimeout(settle, 100);
198
+ }, { passive: true });
199
+ scope.on(viewport, 'scrollend', settle);
200
+ scope.on(viewport, 'click', event => {
201
+ if (disabled) return;
202
+ if (suppressClick) { event.preventDefault(); suppressClick = false; return; }
203
+ const row = (event.target as Element).closest<HTMLElement>('[data-mk-wheel-option]');
204
+ if (!row || !list.contains(row)) return;
205
+ select(Number(row.dataset.mkWheelOption), true); viewport.focus({ preventScroll: true });
206
+ });
207
+ scope.on(viewport, 'keydown', event => {
208
+ if (disabled) return;
209
+ const key = (event as KeyboardEvent).key, choices = enabled(), at = choices.indexOf(current);
210
+ if (key === 'Enter' || key === ' ') { event.preventDefault(); return; }
211
+ const move = key === 'ArrowDown' ? 1 : key === 'ArrowUp' ? -1 : key === 'PageDown' ? rows - 1 : key === 'PageUp' ? 1 - rows : 0;
212
+ let next = key === 'Home' ? choices[0] : key === 'End' ? choices.at(-1) : move ? choices[Math.min(choices.length - 1, Math.max(0, at + move))] : undefined;
213
+ if (next === undefined && key.length === 1 && !(event as KeyboardEvent).ctrlKey && !(event as KeyboardEvent).metaKey) {
214
+ prefix += key.toLocaleLowerCase(); clearTimeout(typing); typing = setTimeout(() => { prefix = ''; }, 600);
215
+ next = choices.find(index => items[index].label.toLocaleLowerCase().startsWith(prefix));
216
+ }
217
+ if (next === undefined) return;
218
+ event.preventDefault(); interrupt(); select(next, true);
219
+ });
220
+ const form = root.closest('form');
221
+ if (form) scope.on(form, 'reset', event => queueMicrotask(() => {
222
+ if (scope.disposed || event.defaultPrevented) return;
223
+ const index = find(initial); select(index >= 0 ? index : enabled()[0] ?? -1, true, false); paint();
224
+ }));
225
+ build(); input.defaultValue = initial;
226
+ return {
227
+ setValue(value, notify = false) { const next = find(value); if (next !== current) select(next, notify); },
228
+ setItems(next, notify = false) {
229
+ if (scope.disposed) return;
230
+ clearTimeout(timer); programmatic = null;
231
+ const before = items[current]?.value ?? ''; items = normalize(Array.isArray(next) ? next : []);
232
+ current = find(before); if (current < 0) current = enabled()[0] ?? -1;
233
+ build(); committed = items[current]?.value ?? '';
234
+ if (notify && committed !== before) { announce('input'); announce('change'); }
235
+ },
236
+ get value() { return items[current]?.value ?? ''; },
237
+ destroy: () => scope.dispose(),
238
+ };
239
+ }
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+ import { useReducer, useRef, type HTMLAttributes } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import { createWheelPicker, type WheelPickerInstance, type WheelPickerOptions } from './engine';
5
+
6
+ export interface WheelPickerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'onInput' | 'defaultValue' | 'children' | 'dangerouslySetInnerHTML'>, WheelPickerOptions {
7
+ defaultValue?: string;
8
+ instanceRef?: { current: WheelPickerInstance | null };
9
+ }
10
+ export function WheelPicker({ items, value, defaultValue, name, label, disabled, visibleItems, itemHeight, onInput, onChange, instanceRef, className, ...props }: WheelPickerProps) {
11
+ const ref = useRef<HTMLDivElement>(null), instance = useRef<WheelPickerInstance | null>(null);
12
+ const changed = useLatest(onChange), input = useLatest(onInput), latestItems = useLatest(items);
13
+ const controlled = useLatest(value);
14
+ const [, refresh] = useReducer((revision: number) => revision + 1, 0);
15
+ const last = useRef(value ?? defaultValue);
16
+ useIsomorphicLayoutEffect(() => {
17
+ const created = createWheelPicker(ref.current, { items: latestItems.current, value: value ?? last.current, name, label, disabled, visibleItems, itemHeight,
18
+ onInput: next => { last.current = next; input.current?.(next); }, onChange: next => { last.current = next; changed.current?.(next); if (controlled.current !== undefined) refresh(); } });
19
+ instance.current = created;
20
+ return () => { last.current = created.value; created.destroy(); instance.current = null; };
21
+ }, [name, label, disabled, visibleItems, itemHeight]);
22
+ const key = JSON.stringify(items ?? []);
23
+ useIsomorphicLayoutEffect(() => { instance.current?.setItems(items ?? []); }, [key]);
24
+ useIsomorphicLayoutEffect(() => { if (value !== undefined) instance.current?.setValue(value); });
25
+ useIsomorphicLayoutEffect(() => {
26
+ if (instanceRef) instanceRef.current = instance.current;
27
+ return () => { if (instanceRef) instanceRef.current = null; };
28
+ }, [instanceRef, name, label, disabled, visibleItems, itemHeight]);
29
+ const domProps: HTMLAttributes<HTMLDivElement> = { ...props };
30
+ delete domProps.children; delete domProps.dangerouslySetInnerHTML;
31
+ return <div {...domProps} ref={ref} className={['mk-wheel-picker', className].filter(Boolean).join(' ')} />;
32
+ }
@@ -0,0 +1,17 @@
1
+ .mk-wheel-picker, mk-wheel-picker { --mk-wheel-row: 44px; --mk-wheel-rows: 5; display: grid; gap: .75rem; min-width: 0; color: var(--mk-text); font-family: var(--mk-font); }
2
+ [data-mk-wheel-label] { color: var(--mk-muted); font-size: .8125rem; text-align: center; }
3
+ [data-mk-wheel-frame] { position: relative; isolation: isolate; min-width: 0; overflow: hidden; border-radius: var(--mk-radius-xl); background: var(--mk-surface-1); }
4
+ [data-mk-wheel-highlight] { position: absolute; top: calc(50% - var(--mk-wheel-row) / 2); right: .5rem; left: .5rem; height: var(--mk-wheel-row); border-radius: var(--mk-radius-md); background: var(--mk-surface-3); box-shadow: inset 0 1px color-mix(in srgb, var(--mk-text) 5%, transparent), inset 0 -1px color-mix(in srgb, var(--mk-text) 4%, transparent); pointer-events: none; z-index: -1; }
5
+ [data-mk-wheel-viewport] { height: calc(var(--mk-wheel-row) * var(--mk-wheel-rows)); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; scroll-snap-type: y mandatory; scrollbar-width: none; -webkit-tap-highlight-color: transparent; mask-image: linear-gradient(transparent, #000 16%, #000 84%, transparent); }
6
+ [data-mk-wheel-viewport]::-webkit-scrollbar { display: none; }
7
+ [data-mk-wheel-viewport]:focus { outline: none; }
8
+ [data-mk-wheel-frame]:has([data-mk-wheel-viewport]:focus-visible) { outline: 2px solid var(--mk-accent); outline-offset: 3px; border-radius: 24px; }
9
+ [data-mk-wheel-list] { padding-block: calc(var(--mk-wheel-row) * (var(--mk-wheel-rows) - 1) / 2); }
10
+ [data-mk-wheel-option] { display: grid; place-items: center; height: var(--mk-wheel-row); min-height: 44px; padding-inline: 1rem; color: var(--mk-text); font-size: 1.35rem; font-variant-numeric: tabular-nums; font-weight: 450; scroll-snap-align: center; cursor: pointer; user-select: none; transform-origin: center; backface-visibility: hidden; transition: color .18s ease; }
11
+ [data-mk-wheel-option][aria-selected='true'] { color: var(--mk-text); font-weight: 650; }
12
+ [data-mk-wheel-option]:not([aria-disabled='true']):hover { color: var(--mk-accent); }
13
+ [data-mk-wheel-option][aria-disabled='true'] { color: var(--mk-muted); cursor: not-allowed; }
14
+ [data-mk-wheel-empty] { display: block; text-align: center; color: var(--mk-muted); font-size: .875rem; }
15
+ [data-mk-disabled] [data-mk-wheel-viewport] { overflow: hidden; opacity: .5; }
16
+ @media (prefers-reduced-motion: reduce) { [data-mk-wheel-option] { transition: none; transform: none !important; } }
17
+ @media (forced-colors: active) { [data-mk-wheel-highlight] { border: 2px solid Highlight; } }
@@ -15,6 +15,10 @@
15
15
  --mk-ease-nav-move: cubic-bezier(0.38, 0.49, 0, 1.2);
16
16
  --mk-ease-swatch: cubic-bezier(0.38, 0.49, 0, 1.5);
17
17
  --mk-ease-swatch-light: cubic-bezier(0.11, 1, 0, 1);
18
+ --mk-ease-click: cubic-bezier(0.37, 1.42, 0.37, 1);
19
+ --mk-ease-playful: cubic-bezier(0.38, 0.49, 0, 2);
20
+ --mk-ease-pop: cubic-bezier(0.1, 0.8, 0, 1.1);
21
+ --mk-ease-wipe: cubic-bezier(0.38, 0.49, 0, 1);
18
22
  --mk-ease-slot: cubic-bezier(0.34, 1.56, 0.64, 1);
19
23
  /* Glass, sheet and control curves */
20
24
  --mk-ease-glass: cubic-bezier(0.25, 1, 0.5, 1);