@versini/ui-hooks 5.1.0 → 5.2.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/dist/index.js CHANGED
@@ -1,34 +1,830 @@
1
- import { useClickOutside as r } from "./hooks/useClickOutside.js";
2
- import { useHaptic as s } from "./hooks/useHaptic.js";
3
- import { getHotkeyHandler as p, shouldFireEvent as f, useHotkeys as m } from "./hooks/useHotkeys.js";
4
- import { useInterval as i } from "./hooks/useInterval.js";
5
- import { useInViewport as n } from "./hooks/useInViewport.js";
6
- import { useIsMounted as d } from "./hooks/useIsMounted.js";
7
- import { useLocalStorage as H } from "./hooks/useLocalStorage.js";
8
- import { useMergeRefs as V } from "./hooks/useMergeRefs.js";
9
- import { useResizeObserver as k } from "./hooks/useResizeObserver.js";
10
- import { useUncontrolled as w } from "./hooks/useUncontrolled.js";
11
- import { useUniqueId as S } from "./hooks/useUniqueId.js";
12
- import { useViewportSize as M } from "./hooks/useViewportSize.js";
13
- import { useVisualViewportSize as R } from "./hooks/useVisualViewportSize.js";
14
1
  /*!
15
- @versini/ui-hooks v5.1.0
2
+ @versini/ui-hooks v5.2.0
16
3
  © 2025 gizmette.com
17
4
  */
18
- export {
19
- p as getHotkeyHandler,
20
- f as shouldFireEvent,
21
- r as useClickOutside,
22
- s as useHaptic,
23
- m as useHotkeys,
24
- n as useInViewport,
25
- i as useInterval,
26
- d as useIsMounted,
27
- H as useLocalStorage,
28
- V as useMergeRefs,
29
- k as useResizeObserver,
30
- w as useUncontrolled,
31
- S as useUniqueId,
32
- M as useViewportSize,
33
- R as useVisualViewportSize
5
+ try {
6
+ if (!window.__VERSINI_UI_HOOKS__) {
7
+ window.__VERSINI_UI_HOOKS__ = {
8
+ version: "5.2.0",
9
+ buildTime: "11/04/2025 03:42 PM EST",
10
+ homepage: "https://github.com/aversini/ui-components",
11
+ license: "MIT",
12
+ };
13
+ }
14
+ } catch (error) {
15
+ // nothing to declare officer
16
+ }
17
+
18
+ import { useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
19
+
20
+ ;// CONCATENATED MODULE: external "react"
21
+
22
+ ;// CONCATENATED MODULE: ./src/hooks/useClickOutside.tsx
23
+
24
+ const DEFAULT_EVENTS = [
25
+ "mousedown",
26
+ "touchstart"
27
+ ];
28
+ /**
29
+ * Custom hooks that triggers a callback when a click is detected
30
+ * outside the target element.
31
+ *
32
+ * @param handler - Function to be called when clicked outside
33
+ * @param events - Array of events to listen to
34
+ * @param nodes - Array of nodes to check against
35
+ * @returns Ref to be attached to the target element
36
+ *
37
+ * @example
38
+ * const ref = useClickOutside(() => {
39
+ * console.log('Clicked outside!');
40
+ * });
41
+ * <div ref={ref}>Click me!</div>
42
+ *
43
+ */ function useClickOutside(handler, events, nodes) {
44
+ const ref = useRef(null);
45
+ useEffect(()=>{
46
+ const listener = (event)=>{
47
+ /* v8 ignore next 1 */ const target = event ? event.target : undefined;
48
+ if (Array.isArray(nodes)) {
49
+ /* v8 ignore next 2 */ const shouldIgnore = !document.body.contains(target) && target.tagName !== "HTML";
50
+ const shouldTrigger = nodes.every((node)=>!!node && !event.composedPath().includes(node));
51
+ shouldTrigger && !shouldIgnore && handler();
52
+ } else if (ref.current && !ref.current.contains(target)) {
53
+ handler();
54
+ }
55
+ };
56
+ (events || DEFAULT_EVENTS).forEach((fn)=>document.addEventListener(fn, listener));
57
+ return ()=>{
58
+ (events || DEFAULT_EVENTS).forEach((fn)=>document.removeEventListener(fn, listener));
59
+ };
60
+ }, [
61
+ handler,
62
+ nodes,
63
+ events
64
+ ]);
65
+ return ref;
66
+ }
67
+
68
+ ;// CONCATENATED MODULE: ./src/hooks/useHaptic.ts
69
+
70
+ const HAPTIC_DURATION_MS = 50;
71
+ const HAPTIC_INTERVAL_MS = 120;
72
+ /**
73
+ * Singleton state for the haptic element shared across all hook instances. This
74
+ * ensures only one DOM element is created regardless of how many components use
75
+ * the hook.
76
+ */ let sharedLabelElement = null;
77
+ let refCount = 0;
78
+ /**
79
+ * Creates the shared haptic element if it doesn't exist. This function is
80
+ * idempotent - calling it multiple times is safe. Checks DOM directly to handle
81
+ * React Strict Mode double-mounting.
82
+ */ const ensureHapticElement = ()=>{
83
+ /* c8 ignore next 3 */ if (typeof window === "undefined") {
84
+ return;
85
+ }
86
+ // First check: do we have valid references that exist in the DOM?
87
+ if (sharedLabelElement && document.body.contains(sharedLabelElement)) {
88
+ return;
89
+ }
90
+ /**
91
+ * Second check: does an element already exist in the DOM from a previous
92
+ * mount?
93
+ */ const existingLabel = document.querySelector('label[data-haptic-singleton="true"]');
94
+ if (existingLabel) {
95
+ sharedLabelElement = existingLabel;
96
+ return;
97
+ }
98
+ // Clear stale references.
99
+ sharedLabelElement = null;
100
+ // Create new elements.
101
+ const input = document.createElement("input");
102
+ input.type = "checkbox";
103
+ input.setAttribute("switch", "");
104
+ input.style.display = "none";
105
+ input.setAttribute("aria-hidden", "true");
106
+ input.dataset.hapticSingleton = "true";
107
+ const label = document.createElement("label");
108
+ label.style.display = "none";
109
+ label.setAttribute("aria-hidden", "true");
110
+ label.dataset.hapticSingleton = "true";
111
+ label.appendChild(input);
112
+ document.body.appendChild(label);
113
+ sharedLabelElement = label;
34
114
  };
115
+ /**
116
+ * Removes the shared haptic element from the DOM and clears references. Only
117
+ * called when the last component using the hook unmounts.
118
+ */ const cleanupHapticElement = ()=>{
119
+ if (sharedLabelElement && document.body && document.body.contains(sharedLabelElement)) {
120
+ document.body.removeChild(sharedLabelElement);
121
+ }
122
+ sharedLabelElement = null;
123
+ };
124
+ /**
125
+ * Custom hook providing imperative haptic feedback for mobile devices. Uses
126
+ * navigator.vibrate when available, falls back to iOS switch element trick for
127
+ * Safari on iOS devices that don't support the Vibration API.
128
+ *
129
+ * This hook uses a singleton pattern - only one haptic element is created in
130
+ * the DOM regardless of how many components use this hook. The element is
131
+ * automatically cleaned up when the last component unmounts.
132
+ *
133
+ * @example
134
+ * ```tsx
135
+ * const { haptic } = useHaptic();
136
+ *
137
+ * // Trigger a single haptic pulse
138
+ * haptic(1);
139
+ *
140
+ * // Trigger two rapid haptic pulses
141
+ * haptic(2);
142
+ * ```
143
+ *
144
+ */ const useHaptic = ()=>{
145
+ const timeoutsRef = useRef(new Set());
146
+ useEffect(()=>{
147
+ // Increment reference count and create element if needed.
148
+ refCount++;
149
+ try {
150
+ ensureHapticElement();
151
+ } catch (error) {
152
+ /**
153
+ * If element creation fails, we need to decrement refCount immediately since
154
+ * the cleanup function won't be registered. This prevents refCount from
155
+ * getting out of sync.
156
+ */ refCount--;
157
+ throw error;
158
+ }
159
+ return ()=>{
160
+ /**
161
+ * Cleanup function: clear all pending timeouts to prevent haptics from
162
+ * firing after unmount, and decrement the reference count. Only remove the
163
+ * DOM element when the last component unmounts.
164
+ */ for (const timeoutId of timeoutsRef.current){
165
+ clearTimeout(timeoutId);
166
+ }
167
+ timeoutsRef.current.clear();
168
+ refCount--;
169
+ if (refCount === 0) {
170
+ cleanupHapticElement();
171
+ }
172
+ };
173
+ }, []);
174
+ /**
175
+ * Triggers a single haptic pulse using either the Vibration API or the switch
176
+ * element trick.
177
+ */ const triggerSingleHaptic = useCallback(()=>{
178
+ try {
179
+ if (navigator?.vibrate) {
180
+ navigator.vibrate(HAPTIC_DURATION_MS);
181
+ return;
182
+ }
183
+ sharedLabelElement?.click();
184
+ } catch {
185
+ // Silently fail if haptics are not supported.
186
+ }
187
+ }, []);
188
+ /**
189
+ * Triggers haptic feedback with the specified number of pulses.
190
+ *
191
+ * @param count - Number of haptic pulses to trigger (default: 1). For count > 1,
192
+ * pulses are triggered in rapid succession with a 120ms interval between each
193
+ * pulse.
194
+ *
195
+ * @example
196
+ * ```tsx
197
+ * const { haptic } = useHaptic();
198
+ *
199
+ * // Single haptic
200
+ * haptic(1);
201
+ *
202
+ * // Two rapid haptics
203
+ * haptic(2);
204
+ *
205
+ * // Three rapid haptics
206
+ * haptic(3);
207
+ * ```
208
+ *
209
+ */ const haptic = useCallback((count = 1)=>{
210
+ /* c8 ignore next 3 */ if (typeof window === "undefined") {
211
+ return;
212
+ }
213
+ if (count < 1) {
214
+ return;
215
+ }
216
+ if (navigator?.vibrate && count > 1) {
217
+ /**
218
+ * For multi-haptic patterns with navigator.vibrate, create an array pattern
219
+ * like [50, 120, 50, 120, 50] for better timing control.
220
+ */ const pattern = [];
221
+ for(let i = 0; i < count; i++){
222
+ pattern.push(HAPTIC_DURATION_MS);
223
+ if (i < count - 1) {
224
+ pattern.push(HAPTIC_INTERVAL_MS - HAPTIC_DURATION_MS);
225
+ }
226
+ }
227
+ navigator.vibrate(pattern);
228
+ return;
229
+ }
230
+ // For switch element or single vibration, trigger sequentially.
231
+ for(let i = 0; i < count; i++){
232
+ const timeoutId = setTimeout(()=>{
233
+ triggerSingleHaptic();
234
+ timeoutsRef.current.delete(timeoutId);
235
+ }, i * HAPTIC_INTERVAL_MS);
236
+ timeoutsRef.current.add(timeoutId);
237
+ }
238
+ }, [
239
+ triggerSingleHaptic
240
+ ]);
241
+ return {
242
+ haptic
243
+ };
244
+ };
245
+
246
+ ;// CONCATENATED MODULE: ./src/hooks/utilities.ts
247
+ function parseHotkey(hotkey) {
248
+ const keys = hotkey.toLowerCase().split("+").map((part)=>part.trim());
249
+ const modifiers = {
250
+ alt: keys.includes("alt"),
251
+ ctrl: keys.includes("ctrl"),
252
+ meta: keys.includes("meta"),
253
+ mod: keys.includes("mod"),
254
+ shift: keys.includes("shift")
255
+ };
256
+ const reservedKeys = [
257
+ "alt",
258
+ "ctrl",
259
+ "meta",
260
+ "shift",
261
+ "mod"
262
+ ];
263
+ const freeKey = keys.find((key)=>!reservedKeys.includes(key));
264
+ return {
265
+ ...modifiers,
266
+ key: freeKey
267
+ };
268
+ }
269
+ function isExactHotkey(hotkey, event) {
270
+ const { alt, ctrl, meta, mod, shift, key } = hotkey;
271
+ const { altKey, ctrlKey, metaKey, shiftKey, key: pressedKey } = event;
272
+ if (alt !== altKey) {
273
+ return false;
274
+ }
275
+ if (mod) {
276
+ if (!ctrlKey && !metaKey) {
277
+ return false;
278
+ }
279
+ } else {
280
+ if (ctrl !== ctrlKey) {
281
+ return false;
282
+ }
283
+ if (meta !== metaKey) {
284
+ return false;
285
+ }
286
+ }
287
+ if (shift !== shiftKey) {
288
+ return false;
289
+ }
290
+ if (key && (pressedKey.toLowerCase() === key.toLowerCase() || event.code.replace("Key", "").toLowerCase() === key.toLowerCase())) {
291
+ return true;
292
+ }
293
+ return false;
294
+ }
295
+ function getHotkeyMatcher(hotkey) {
296
+ return (event)=>isExactHotkey(parseHotkey(hotkey), event);
297
+ }
298
+ function getHotkeyHandler(hotkeys) {
299
+ return (event)=>{
300
+ const _event = "nativeEvent" in event ? event.nativeEvent : event;
301
+ hotkeys.forEach(([hotkey, handler, options = {
302
+ preventDefault: true
303
+ }])=>{
304
+ if (getHotkeyMatcher(hotkey)(_event)) {
305
+ if (options.preventDefault) {
306
+ event.preventDefault();
307
+ }
308
+ handler(_event);
309
+ }
310
+ });
311
+ };
312
+ }
313
+
314
+ ;// CONCATENATED MODULE: ./src/hooks/useHotkeys.ts
315
+
316
+
317
+
318
+ function shouldFireEvent(event, tagsToIgnore, triggerOnContentEditable = false) {
319
+ if (event.target instanceof HTMLElement) {
320
+ if (triggerOnContentEditable) {
321
+ return !tagsToIgnore.includes(event.target.tagName);
322
+ }
323
+ return !event.target.isContentEditable && !tagsToIgnore.includes(event.target.tagName);
324
+ }
325
+ return true;
326
+ }
327
+ function useHotkeys(hotkeys, tagsToIgnore = [
328
+ "INPUT",
329
+ "TEXTAREA",
330
+ "SELECT"
331
+ ], triggerOnContentEditable = false) {
332
+ useEffect(()=>{
333
+ const keydownListener = (event)=>{
334
+ hotkeys.forEach(([hotkey, handler, options = {
335
+ preventDefault: true
336
+ }])=>{
337
+ if (getHotkeyMatcher(hotkey)(event) && shouldFireEvent(event, tagsToIgnore, triggerOnContentEditable)) {
338
+ if (options.preventDefault) {
339
+ event.preventDefault();
340
+ }
341
+ handler(event);
342
+ }
343
+ });
344
+ };
345
+ document.documentElement.addEventListener("keydown", keydownListener);
346
+ return ()=>document.documentElement.removeEventListener("keydown", keydownListener);
347
+ }, [
348
+ hotkeys,
349
+ tagsToIgnore,
350
+ triggerOnContentEditable
351
+ ]);
352
+ }
353
+
354
+ ;// CONCATENATED MODULE: ./src/hooks/useInterval.ts
355
+
356
+ /**
357
+ * Custom hook to call a function within a given interval.
358
+ *
359
+ * @param fn Callback function to be executed at each interval
360
+ * @param interval Interval time in milliseconds
361
+ * @returns An object containing start, stop, and active state
362
+ *
363
+ * @example
364
+ * const { start, stop, active } = useInterval(() => {
365
+ * console.log("Interval executed");
366
+ * }, 1000);
367
+ * start(); // To start the interval
368
+ * stop(); // To stop the interval
369
+ * console.log(active); // To check if the interval is active
370
+ *
371
+ */ function useInterval(fn, interval) {
372
+ const [active, setActive] = useState(false);
373
+ const intervalRef = useRef(null);
374
+ const fnRef = useRef(null);
375
+ const start = useCallback(()=>{
376
+ setActive((old)=>{
377
+ if (!old && (!intervalRef.current || intervalRef.current === -1)) {
378
+ intervalRef.current = window.setInterval(fnRef.current, interval);
379
+ }
380
+ return true;
381
+ });
382
+ }, [
383
+ interval
384
+ ]);
385
+ const stop = useCallback(()=>{
386
+ setActive(false);
387
+ window.clearInterval(intervalRef.current || -1);
388
+ intervalRef.current = -1;
389
+ }, []);
390
+ useEffect(()=>{
391
+ fnRef.current = fn;
392
+ active && start();
393
+ return stop;
394
+ }, [
395
+ fn,
396
+ active,
397
+ start,
398
+ stop
399
+ ]);
400
+ return {
401
+ start,
402
+ stop,
403
+ active
404
+ };
405
+ }
406
+
407
+ ;// CONCATENATED MODULE: ./src/hooks/useInViewport.ts
408
+
409
+ /**
410
+ * Hook that checks if an element is visible in the viewport.
411
+ * @returns
412
+ * ref: React ref object to attach to the element you want to monitor.
413
+ * inViewport: Boolean indicating if the element is in the viewport.
414
+ */ /* v8 ignore next 24 */ function useInViewport() {
415
+ const observer = useRef(null);
416
+ const [inViewport, setInViewport] = useState(false);
417
+ const ref = useCallback((node)=>{
418
+ if (typeof IntersectionObserver !== "undefined") {
419
+ if (node && !observer.current) {
420
+ observer.current = new IntersectionObserver((entries)=>setInViewport(entries.some((entry)=>entry.isIntersecting)));
421
+ } else {
422
+ observer.current?.disconnect();
423
+ }
424
+ if (node) {
425
+ observer.current?.observe(node);
426
+ } else {
427
+ setInViewport(false);
428
+ }
429
+ }
430
+ }, []);
431
+ return {
432
+ ref,
433
+ inViewport
434
+ };
435
+ }
436
+
437
+ ;// CONCATENATED MODULE: ./src/hooks/useIsMounted.ts
438
+
439
+ /**
440
+ * Custom hook that returns a function indicating whether the component
441
+ * is mounted or not.
442
+ *
443
+ * @returns A function that returns a boolean value indicating whether
444
+ * the component is mounted.
445
+ *
446
+ * @example
447
+ * const isMounted = useIsMounted();
448
+ * console.log(isMounted()); // true
449
+ *
450
+ */ function useIsMounted() {
451
+ const isMounted = useRef(false);
452
+ useEffect(()=>{
453
+ isMounted.current = true;
454
+ return ()=>{
455
+ isMounted.current = false;
456
+ };
457
+ }, []);
458
+ return useCallback(()=>isMounted.current, []);
459
+ }
460
+
461
+ ;// CONCATENATED MODULE: ./src/hooks/useLocalStorage.ts
462
+
463
+ function dispatchStorageEvent(key, newValue) {
464
+ window.dispatchEvent(new StorageEvent("storage", {
465
+ key,
466
+ newValue
467
+ }));
468
+ }
469
+ const setLocalStorageItem = (key, value)=>{
470
+ const stringifiedValue = JSON.stringify(typeof value === "function" ? value() : value);
471
+ window.localStorage.setItem(key, stringifiedValue);
472
+ dispatchStorageEvent(key, stringifiedValue);
473
+ };
474
+ const removeLocalStorageItem = (key)=>{
475
+ window.localStorage.removeItem(key);
476
+ dispatchStorageEvent(key, null);
477
+ };
478
+ const getLocalStorageItem = (key)=>{
479
+ return window.localStorage.getItem(key);
480
+ };
481
+ const useLocalStorageSubscribe = (callback)=>{
482
+ window.addEventListener("storage", callback);
483
+ return ()=>window.removeEventListener("storage", callback);
484
+ };
485
+ /**
486
+ *
487
+ * @example
488
+ * import { useLocalStorage } from '@versini/ui-hooks';
489
+ * const [value, setValue, resetValue, removeValue] = useLocalStorage({
490
+ * key: 'gpt-model',
491
+ * initialValue: 'gpt-3',
492
+ * });
493
+ *
494
+ * setValue('gpt-4'); ==> "gpt-4"
495
+ * setValue((current) => (current === 'gpt-3' ? 'gpt-4' : 'gpt-3'));
496
+ * resetValue(); ==> "gpt-3"
497
+ * removeValue(); ==> null
498
+ */ function useLocalStorage({ key, initialValue }) {
499
+ const getSnapshot = ()=>getLocalStorageItem(key);
500
+ const store = useSyncExternalStore(useLocalStorageSubscribe, getSnapshot);
501
+ const setValue = useCallback((v)=>{
502
+ try {
503
+ const nextState = typeof v === "function" ? v(JSON.parse(store)) : v;
504
+ if (nextState === undefined || nextState === null) {
505
+ removeLocalStorageItem(key);
506
+ } else {
507
+ setLocalStorageItem(key, nextState);
508
+ }
509
+ /* v8 ignore next 3 */ } catch (e) {
510
+ console.warn(e);
511
+ }
512
+ }, [
513
+ key,
514
+ store
515
+ ]);
516
+ const resetValue = useCallback(()=>{
517
+ setValue(initialValue);
518
+ }, [
519
+ initialValue,
520
+ setValue
521
+ ]);
522
+ const removeValue = useCallback(()=>{
523
+ setValue(null);
524
+ }, [
525
+ setValue
526
+ ]);
527
+ useEffect(()=>{
528
+ try {
529
+ if (getLocalStorageItem(key) === null && typeof initialValue !== "undefined") {
530
+ setLocalStorageItem(key, initialValue);
531
+ }
532
+ /* v8 ignore next 3 */ } catch (e) {
533
+ console.warn(e);
534
+ }
535
+ }, [
536
+ key,
537
+ initialValue
538
+ ]);
539
+ return [
540
+ store ? JSON.parse(store) : null,
541
+ setValue,
542
+ resetValue,
543
+ removeValue
544
+ ];
545
+ }
546
+
547
+ ;// CONCATENATED MODULE: ./src/hooks/useMergeRefs.ts
548
+ /**
549
+ * React utility to merge refs.
550
+ *
551
+ * When developing low level UI components, it is common to have to use a local
552
+ * ref but also support an external one using React.forwardRef. Natively, React
553
+ * does not offer a way to set two refs inside the ref property.
554
+ *
555
+ * @param Array of refs (object, function, etc.)
556
+ *
557
+ * @example
558
+ *
559
+ * const Example = React.forwardRef(function Example(props, ref) {
560
+ * const localRef = React.useRef();
561
+ * const mergedRefs = useMergeRefs([localRef, ref]);
562
+ *
563
+ * return <div ref={mergedRefs} />;
564
+ * });
565
+ *
566
+ */
567
+ function useMergeRefs(refs) {
568
+ // biome-ignore lint/correctness/useExhaustiveDependencies: refs array is used as dependency but spread for proper comparison
569
+ return useMemo(()=>{
570
+ if (refs.every((ref)=>ref == null)) {
571
+ return ()=>{};
572
+ }
573
+ return (value)=>{
574
+ refs.forEach((ref)=>{
575
+ if (typeof ref === "function") {
576
+ ref(value);
577
+ } else if (ref != null) {
578
+ ref.current = value;
579
+ }
580
+ });
581
+ };
582
+ }, [
583
+ ...refs
584
+ ]);
585
+ }
586
+
587
+ ;// CONCATENATED MODULE: ./src/hooks/useResizeObserver.ts
588
+
589
+
590
+ const defaultState = {
591
+ x: 0,
592
+ y: 0,
593
+ width: 0,
594
+ height: 0,
595
+ top: 0,
596
+ left: 0,
597
+ bottom: 0,
598
+ right: 0
599
+ };
600
+ /**
601
+ * A custom hook that uses the ResizeObserver API to track the size changes of a DOM element.
602
+ *
603
+ * @template T - The type of the DOM element being observed.
604
+ * @param {ResizeObserverOptions} [options] - The options to configure the ResizeObserver.
605
+ * @returns {[React.RefObject<T>, ObserverRect]} - A tuple containing the ref object and
606
+ * the observed rectangle.
607
+ * @example
608
+ *
609
+ * const [rightElementRef, rect] = useResizeObserver<HTMLDivElement>();
610
+ * <div ref={componentRef}>
611
+ * Observed: <code>{JSON.stringify(rect)}</code>
612
+ * </div>
613
+ */ function useResizeObserver(options) {
614
+ const isMounted = useIsMounted();
615
+ const frameID = useRef(0);
616
+ const ref = useRef(null);
617
+ const [rect, setRect] = useState(defaultState);
618
+ const observer = useMemo(()=>{
619
+ /* c8 ignore next 3 */ if (typeof ResizeObserver === "undefined") {
620
+ return null;
621
+ }
622
+ return new ResizeObserver((entries)=>{
623
+ const entry = entries[0];
624
+ if (entry) {
625
+ cancelAnimationFrame(frameID.current);
626
+ frameID.current = requestAnimationFrame(()=>{
627
+ if (ref.current && isMounted()) {
628
+ setRect(entry.contentRect);
629
+ }
630
+ });
631
+ }
632
+ });
633
+ }, [
634
+ isMounted
635
+ ]);
636
+ useEffect(()=>{
637
+ /* c8 ignore next 3 */ if (ref.current) {
638
+ observer?.observe(ref.current, options);
639
+ }
640
+ return ()=>{
641
+ observer?.disconnect();
642
+ /* c8 ignore next 3 */ if (frameID.current) {
643
+ cancelAnimationFrame(frameID.current);
644
+ }
645
+ };
646
+ }, [
647
+ observer,
648
+ options
649
+ ]);
650
+ return [
651
+ ref,
652
+ rect
653
+ ];
654
+ }
655
+
656
+ ;// CONCATENATED MODULE: ./src/hooks/useUncontrolled.ts
657
+
658
+ function useUncontrolled({ value, defaultValue, finalValue, onChange = ()=>{}, initialControlledDelay = 0 }) {
659
+ const [initialDelayDone, setInitialDelayDone] = useState(false);
660
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue !== undefined ? defaultValue : finalValue);
661
+ const handleUncontrolledChange = (val)=>{
662
+ setUncontrolledValue(val);
663
+ onChange?.(val);
664
+ };
665
+ useEffect(()=>{
666
+ (async ()=>{
667
+ /**
668
+ * If initialControlledDelay is provided, wait for the delay.
669
+ */ if (value !== undefined) {
670
+ /* c8 ignore start */ if (!initialDelayDone && initialControlledDelay > 0) {
671
+ await new Promise((resolve)=>setTimeout(resolve, initialControlledDelay));
672
+ setInitialDelayDone(true);
673
+ }
674
+ /* c8 ignore end */ }
675
+ })();
676
+ }, [
677
+ value,
678
+ initialControlledDelay,
679
+ initialDelayDone
680
+ ]);
681
+ /**
682
+ * If value is provided, return the controlled value.
683
+ * If there is a delay, we need to wait for the delay: we need to first send
684
+ * back a value of an empty string, then after the delay
685
+ * we can send the actual value.
686
+ */ if (value !== undefined) {
687
+ if (!initialDelayDone && initialControlledDelay > 0) {
688
+ return [
689
+ "",
690
+ onChange,
691
+ true
692
+ ];
693
+ } else {
694
+ return [
695
+ value,
696
+ onChange,
697
+ true
698
+ ];
699
+ }
700
+ }
701
+ /**
702
+ * If value is not provided, return the uncontrolled value.
703
+ */ return [
704
+ uncontrolledValue,
705
+ handleUncontrolledChange,
706
+ false
707
+ ];
708
+ }
709
+
710
+ ;// CONCATENATED MODULE: ./src/hooks/useUniqueId.ts
711
+
712
+ function useUniqueId(options) {
713
+ const generatedId = useId();
714
+ if (!options) {
715
+ return generatedId;
716
+ }
717
+ if (typeof options === "number" || typeof options === "string") {
718
+ return `${options}${generatedId}`;
719
+ }
720
+ if (typeof options === "object") {
721
+ const { id, prefix = "" } = options;
722
+ if (typeof id === "number" || typeof id === "string") {
723
+ return `${prefix}${id}`;
724
+ }
725
+ return `${prefix}${generatedId}`;
726
+ }
727
+ }
728
+
729
+ ;// CONCATENATED MODULE: ./src/hooks/useViewportSize.tsx
730
+
731
+ function useWindowEvent(type, listener) {
732
+ useEffect(()=>{
733
+ window.addEventListener(type, listener, {
734
+ passive: true
735
+ });
736
+ return ()=>window.removeEventListener(type, listener);
737
+ }, [
738
+ type,
739
+ listener
740
+ ]);
741
+ }
742
+ /**
743
+ * Custom hook that returns the current viewport size. It will update
744
+ * when the window is resized or the orientation changes.
745
+ *
746
+ * @returns The current viewport size
747
+ *
748
+ * @example
749
+ * const { width, height } = useViewportSize();
750
+ */ function useViewportSize() {
751
+ const [windowSize, setWindowSize] = useState({
752
+ width: 0,
753
+ height: 0
754
+ });
755
+ const setSize = useCallback(()=>{
756
+ setWindowSize({
757
+ width: window.innerWidth || 0,
758
+ height: window.innerHeight || 0
759
+ });
760
+ }, []);
761
+ useWindowEvent("resize", setSize);
762
+ useWindowEvent("orientationchange", setSize);
763
+ useEffect(setSize, []);
764
+ return windowSize;
765
+ }
766
+
767
+ ;// CONCATENATED MODULE: ./src/hooks/useVisualViewportSize.tsx
768
+ /* v8 ignore start */
769
+ /**
770
+ * Custom hook that returns the current visual viewport size. It will update
771
+ * when the window is resized (zoom, virtual keyboard displayed, etc.) or
772
+ * the orientation changes.
773
+ *
774
+ * @returns The current visual viewport size
775
+ *
776
+ * @example
777
+ * const { width, height } = useVisualViewportSize();
778
+ */ function useVisualViewportSize() {
779
+ const [windowSize, setWindowSize] = useState({
780
+ width: 0,
781
+ height: 0
782
+ });
783
+ // Define setSize function once and never recreate it
784
+ const setSize = useCallback(()=>{
785
+ setWindowSize({
786
+ width: window?.visualViewport?.width || window.innerWidth || 0,
787
+ height: window?.visualViewport?.height || window.innerHeight || 0
788
+ });
789
+ }, []); // Empty dependency array is correct here
790
+ useEffect(()=>{
791
+ // Initial size setup
792
+ setSize();
793
+ // Set up event listeners
794
+ if (window.visualViewport) {
795
+ window.visualViewport.addEventListener("resize", setSize, {
796
+ passive: true
797
+ });
798
+ window.visualViewport.addEventListener("orientationchange", setSize, {
799
+ passive: true
800
+ });
801
+ }
802
+ // Cleanup
803
+ return ()=>{
804
+ if (window.visualViewport) {
805
+ window.visualViewport.removeEventListener("resize", setSize);
806
+ window.visualViewport.removeEventListener("orientationchange", setSize);
807
+ }
808
+ };
809
+ }, [
810
+ setSize
811
+ ]);
812
+ return windowSize;
813
+ } /* v8 ignore end */
814
+
815
+ ;// CONCATENATED MODULE: ./src/hooks/index.ts
816
+
817
+
818
+
819
+
820
+
821
+
822
+
823
+
824
+
825
+
826
+
827
+
828
+
829
+
830
+ export { getHotkeyHandler, shouldFireEvent, useClickOutside, useHaptic, useHotkeys, useInViewport, useInterval, useIsMounted, useLocalStorage, useMergeRefs, useResizeObserver, useUncontrolled, useUniqueId, useViewportSize, useVisualViewportSize };