@tarsis/toolkit 0.6.5 → 0.7.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 (78) hide show
  1. package/dist/Container-BVX2MW1U.cjs +138 -0
  2. package/dist/Container-BirkN1fA.js +119 -0
  3. package/dist/SelectBase-BC6WKZVF.cjs +448 -0
  4. package/dist/SelectBase-DPcXvMTa.js +399 -0
  5. package/dist/Slot-SOe-b2n6.cjs +77 -0
  6. package/dist/Slot-z71j7q57.js +65 -0
  7. package/dist/animation-BFpILbqb.js +102 -0
  8. package/dist/animation-BauloIgQ.cjs +119 -0
  9. package/dist/assets-BMqH4phf.cjs +52 -0
  10. package/dist/assets-huTvlamy.js +29 -0
  11. package/dist/audio/fail.mp3 +0 -0
  12. package/dist/audio/fail.ogg +0 -0
  13. package/dist/audio/hover.mp3 +0 -0
  14. package/dist/audio/hover.ogg +0 -0
  15. package/dist/audio/lock/fail.mp3 +0 -0
  16. package/dist/audio/lock/fail.ogg +0 -0
  17. package/dist/audio/lock/hover.mp3 +0 -0
  18. package/dist/audio/lock/hover.ogg +0 -0
  19. package/dist/audio/lock/prev-next.mp3 +0 -0
  20. package/dist/audio/lock/prev-next.ogg +0 -0
  21. package/dist/audio/lock/select.mp3 +0 -0
  22. package/dist/audio/lock/select.ogg +0 -0
  23. package/dist/audio/lock/success.mp3 +0 -0
  24. package/dist/audio/lock/success.ogg +0 -0
  25. package/dist/audio/prev-next.mp3 +0 -0
  26. package/dist/audio/prev-next.ogg +0 -0
  27. package/dist/audio/select.mp3 +0 -0
  28. package/dist/audio/select.ogg +0 -0
  29. package/dist/audio/success.mp3 +0 -0
  30. package/dist/audio/success.ogg +0 -0
  31. package/dist/chunk-CKQMccvm.cjs +28 -0
  32. package/dist/fonts/orbitron/orbitron-black.fnt +426 -0
  33. package/dist/fonts/orbitron/orbitron-black.png +0 -0
  34. package/dist/fonts/orbitron-black.fnt +426 -0
  35. package/dist/fonts/orbitron-black.png +0 -0
  36. package/dist/gl-B0NhVYRl.cjs +177 -0
  37. package/dist/gl-BipoEx9s.js +171 -0
  38. package/dist/hooks.cjs +661 -24
  39. package/dist/hooks.d.ts +72 -0
  40. package/dist/hooks.js +635 -1
  41. package/dist/index.cjs +26708 -384
  42. package/dist/index.d.ts +913 -27
  43. package/dist/index.js +26282 -3
  44. package/dist/layout.cjs +5 -0
  45. package/dist/layout.d.ts +45 -0
  46. package/dist/layout.js +2 -0
  47. package/dist/primitives.cjs +13 -0
  48. package/dist/primitives.d.ts +178 -0
  49. package/dist/primitives.js +3 -0
  50. package/dist/server.cjs +25 -0
  51. package/dist/server.d.ts +70 -0
  52. package/dist/server.js +2 -0
  53. package/dist/styles.css +3872 -2798
  54. package/dist/tokens-B2AxRYyF.js +434 -0
  55. package/dist/tokens-DlMougUi.cjs +469 -0
  56. package/dist/tokens.cjs +12 -0
  57. package/dist/tokens.d.ts +435 -0
  58. package/dist/tokens.js +3 -0
  59. package/dist/useMergeRefs-BM2-gSLn.js +16 -0
  60. package/dist/useMergeRefs-C_l6omwU.cjs +28 -0
  61. package/dist/utils-BGgmkNY4.cjs +330 -0
  62. package/dist/utils-Dw5El_3G.js +222 -0
  63. package/dist/utils.cjs +44 -38
  64. package/dist/utils.d.ts +75 -0
  65. package/dist/utils.js +3 -1
  66. package/dist/values-BTw18-W5.js +138 -0
  67. package/dist/values-BqSJ0h9o.cjs +275 -0
  68. package/package.json +88 -36
  69. package/dist/gl-Bp3e3vph.js +0 -3258
  70. package/dist/gl-Duf2UKsB.cjs +0 -3262
  71. package/dist/index-BcIzOPR7.cjs +0 -116866
  72. package/dist/index-BjG_vCX_.js +0 -3910
  73. package/dist/index-ZBjz1bHI.cjs +0 -3912
  74. package/dist/index-ss50SEnC.js +0 -116503
  75. package/dist/svg-BT_esDTZ.cjs +0 -236
  76. package/dist/svg-CQLdTbLk.js +0 -205
  77. package/dist/useWindowReady-6kIdYolB.cjs +0 -9317
  78. package/dist/useWindowReady-tUs-ONyG.js +0 -9224
package/dist/hooks.js CHANGED
@@ -1 +1,635 @@
1
- export { u as useAnimatedText, a as useBowser, b as useDebounce, d as useEffectEvent, e as useInterval, f as useLiveRef, g as useMatchMedia, h as useOklch, i as useOutsideClick, k as usePreviousRender, j as usePreviousState, l as useRaf, m as useThrottle, n as useTimeout, c as useUniversalLayoutEffect, o as useWindowReady } from './useWindowReady-tUs-ONyG.js';
1
+ import { n as useMergeRefs, t as mergeRefs } from "./useMergeRefs-BM2-gSLn.js";
2
+ import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
3
+ import gsap from "gsap";
4
+ import { animate, useMotionValue } from "motion/react";
5
+ import Bowser from "bowser";
6
+ //#region src/hooks/usePrevious.ts
7
+ var usePreviousState = (value) => {
8
+ const previousRef = useRef(void 0);
9
+ const currentRef = useRef(void 0);
10
+ useEffect(() => {
11
+ if (value !== currentRef.current) {
12
+ previousRef.current = currentRef.current;
13
+ currentRef.current = value;
14
+ }
15
+ }, [value]);
16
+ return previousRef.current;
17
+ };
18
+ var usePreviousRender = (value) => {
19
+ const previousRef = useRef(void 0);
20
+ useEffect(() => {
21
+ previousRef.current = value;
22
+ }, [value]);
23
+ return previousRef.current;
24
+ };
25
+ //#endregion
26
+ //#region src/hooks/useAnimatedText/useAnimatedText.ts
27
+ function useAnimatedText(text, delimiter) {
28
+ const resolvedDelimiter = delimiter === "word" ? " " : "";
29
+ const animatedCursor = useMotionValue(0);
30
+ const [cursor, setCursor] = useState(0);
31
+ const prevText = usePreviousRender(text);
32
+ const isSameText = text.startsWith(prevText ?? "");
33
+ const mountedRef = useRef(true);
34
+ useEffect(() => {
35
+ mountedRef.current = true;
36
+ return () => {
37
+ mountedRef.current = false;
38
+ };
39
+ }, []);
40
+ useEffect(() => {
41
+ if (!isSameText && mountedRef.current) {
42
+ animatedCursor.jump(0);
43
+ requestAnimationFrame(() => {
44
+ if (mountedRef.current) setCursor(0);
45
+ });
46
+ }
47
+ }, [isSameText, animatedCursor]);
48
+ useEffect(() => {
49
+ if (!mountedRef.current) return;
50
+ const controls = animate(animatedCursor, text.split(resolvedDelimiter).length, {
51
+ duration: 3,
52
+ ease: "easeOut",
53
+ onUpdate(latest) {
54
+ if (mountedRef.current) setCursor(Math.floor(latest));
55
+ }
56
+ });
57
+ return () => {
58
+ controls.stop();
59
+ };
60
+ }, [
61
+ animatedCursor,
62
+ isSameText,
63
+ text,
64
+ resolvedDelimiter
65
+ ]);
66
+ return text.split(resolvedDelimiter).slice(0, cursor).join(resolvedDelimiter);
67
+ }
68
+ //#endregion
69
+ //#region src/hooks/useControllableState.ts
70
+ function useControllableState(controlledValue, defaultValue, onChange) {
71
+ const isControlled = controlledValue !== void 0;
72
+ const isControlledRef = useRef(isControlled);
73
+ const [internalValue, setInternalValue] = useState(defaultValue);
74
+ useEffect(() => {
75
+ if (process.env.NODE_ENV !== "production") {
76
+ if (isControlledRef.current !== isControlled) console.warn("Component switched between controlled and uncontrolled. Decide between using a controlled or uncontrolled value for the lifetime of the component.");
77
+ }
78
+ isControlledRef.current = isControlled;
79
+ });
80
+ return [isControlled ? controlledValue : internalValue, useCallback((next) => {
81
+ const resolve = (prev) => typeof next === "function" ? next(prev) : next;
82
+ if (!isControlledRef.current) setInternalValue((prev) => {
83
+ const nextValue = resolve(prev);
84
+ onChange?.(nextValue);
85
+ return nextValue;
86
+ });
87
+ else onChange?.(resolve(controlledValue));
88
+ }, [onChange, controlledValue])];
89
+ }
90
+ //#endregion
91
+ //#region src/hooks/useBowser.ts
92
+ var useBowser = () => {
93
+ const [browser] = useState(() => typeof window === "undefined" ? null : Bowser.parse(window.navigator.userAgent));
94
+ const [parser] = useState(() => typeof window === "undefined" ? null : Bowser.getParser(window.navigator.userAgent));
95
+ return useMemo(() => {
96
+ if (!parser || !browser) return null;
97
+ const browserName = parser.getBrowser().name;
98
+ const isDesktop = browser.platform.type === "desktop";
99
+ return {
100
+ isMobile: browser.platform.type === "mobile",
101
+ isDesktop,
102
+ isIOS: browser.os.name?.toLowerCase() === "ios",
103
+ isMacOS: browser.platform.type?.toLocaleLowerCase() === "desktop" && browser.platform.vendor?.toLowerCase() === "apple" && browser.os.name?.toLocaleLowerCase() === "macos",
104
+ isSafari: browserName ? Boolean(browserName.toLowerCase().match(/safari/)) : false,
105
+ isFirefox: browserName ? Boolean(browserName.toLowerCase().match(/firefox/)) : false,
106
+ isChrome: browserName ? Boolean(browserName.toLowerCase().match(/chrome/)) : false
107
+ };
108
+ }, [browser, parser]);
109
+ };
110
+ //#endregion
111
+ //#region src/hooks/useLiveRef.ts
112
+ var useLiveRef = (value) => {
113
+ const ref = useRef(value);
114
+ ref.current = value;
115
+ return ref;
116
+ };
117
+ //#endregion
118
+ //#region src/hooks/useDebounce.ts
119
+ /**
120
+ * Custom hook that debounces a callback function to delay its execution until after
121
+ * a specified wait time has elapsed since the last time it was invoked.
122
+ *
123
+ * Debouncing ensures that the callback is only executed once after a series of rapid calls,
124
+ * waiting for a pause in the calls before executing.
125
+ *
126
+ * @param callback - The function to debounce
127
+ * @param delay - The delay time (in milliseconds) to wait before executing the callback
128
+ * @param options - Optional configuration object
129
+ * @param options.maxWait - Maximum time before function must be invoked
130
+ * @param options.leading - If true, invoke on the leading edge
131
+ * @returns A debounced version of the callback function with a `cancel` method
132
+ *
133
+ * @example
134
+ * ```tsx
135
+ * const debouncedSearch = useDebounce((query: string) => {
136
+ * console.log('Searching for:', query)
137
+ * }, 300)
138
+ *
139
+ * return <input onChange={(e) => debouncedSearch(e.target.value)} />
140
+ * ```
141
+ *
142
+ * @example
143
+ * ```tsx
144
+ * // With maxWait and leading options
145
+ * const debouncedSearch = useDebounce(
146
+ * (query: string) => console.log('Searching for:', query),
147
+ * 300,
148
+ * { maxWait: 1000, leading: true }
149
+ * )
150
+ * ```
151
+ *
152
+ * @example
153
+ * ```tsx
154
+ * // Cancel pending debounced call
155
+ * const debouncedSearch = useDebounce((query: string) => {
156
+ * console.log('Searching for:', query)
157
+ * }, 300)
158
+ *
159
+ * debouncedSearch('test')
160
+ * debouncedSearch.cancel() // Cancels the pending call
161
+ * ```
162
+ */
163
+ var useDebounce = (callback, delay, { maxWait, leading = false } = {}) => {
164
+ const timeoutRef = useRef(null);
165
+ const maxTimeoutMetadataRef = useRef(null);
166
+ const callbackRef = useLiveRef(callback);
167
+ const clearTimers = useCallback(() => {
168
+ if (timeoutRef.current) {
169
+ clearTimeout(timeoutRef.current);
170
+ timeoutRef.current = null;
171
+ }
172
+ if (maxTimeoutMetadataRef.current) {
173
+ clearTimeout(maxTimeoutMetadataRef.current.timeout);
174
+ maxTimeoutMetadataRef.current = null;
175
+ }
176
+ }, []);
177
+ const cancel = useCallback(() => {
178
+ clearTimers();
179
+ }, [clearTimers]);
180
+ const invokeCallback = useCallback((args) => {
181
+ clearTimers();
182
+ callbackRef.current(...args);
183
+ }, [clearTimers, callbackRef]);
184
+ useEffect(() => {
185
+ return clearTimers;
186
+ }, [clearTimers]);
187
+ const debounced = useCallback((...args) => {
188
+ const isNewCycle = timeoutRef.current === null;
189
+ if (leading && isNewCycle) callbackRef.current(...args);
190
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
191
+ if (maxWait && !maxTimeoutMetadataRef.current) maxTimeoutMetadataRef.current = {
192
+ timeout: setTimeout(() => {
193
+ if (maxTimeoutMetadataRef.current) invokeCallback(maxTimeoutMetadataRef.current.args);
194
+ }, maxWait),
195
+ args
196
+ };
197
+ else if (maxTimeoutMetadataRef.current) maxTimeoutMetadataRef.current.args = args;
198
+ timeoutRef.current = setTimeout(() => {
199
+ invokeCallback(args);
200
+ }, delay);
201
+ }, [
202
+ delay,
203
+ maxWait,
204
+ leading,
205
+ invokeCallback,
206
+ callbackRef
207
+ ]);
208
+ return Object.assign(debounced, { cancel });
209
+ };
210
+ //#endregion
211
+ //#region src/hooks/useEffectEvent.ts
212
+ /**
213
+ * Universal layout effect that uses useLayoutEffect in browser environments
214
+ * and falls back to useEffect in SSR environments
215
+ */
216
+ var useUniversalLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
217
+ /**
218
+ * Use `toString()` to prevent bundlers from trying to `import { useInsertionEffect } from 'react';`
219
+ * @see https://github.com/radix-ui/primitives/blob/main/packages/react/id/src/id.tsx
220
+ */
221
+ var useInsertionEffect = React["useInsertionEffect".toString()] || useUniversalLayoutEffect;
222
+ function useEffectEvent(callback) {
223
+ const ref = React.useRef(() => {
224
+ throw new Error("Cannot call an event handler while rendering.");
225
+ });
226
+ useInsertionEffect(() => {
227
+ ref.current = callback;
228
+ }, [callback]);
229
+ return React.useCallback((...args) => {
230
+ return ref.current?.(...args);
231
+ }, []);
232
+ }
233
+ //#endregion
234
+ //#region src/hooks/useInterval.ts
235
+ var DEFAULT_DEPENDENCIES$1 = [];
236
+ /**
237
+ * Custom hook that manages an interval with automatic cleanup.
238
+ * Provides a reliable way to handle intervals that are properly cleaned up
239
+ * on component unmount or when dependencies change.
240
+ *
241
+ * @param callback - The function to execute on each interval
242
+ * @param delay - The delay in milliseconds (null to clear the interval)
243
+ * @param deps - Optional dependency array. If provided, interval restarts when deps change.
244
+ * If not provided, callback updates via ref without restarting interval.
245
+ *
246
+ * @example
247
+ * ```tsx
248
+ * // Runs continuously, callback updates via ref
249
+ * useInterval(() => {
250
+ * console.log('This runs every second')
251
+ * }, 1000)
252
+ *
253
+ * // Restarts interval when count changes
254
+ * useInterval(() => {
255
+ * console.log('Count:', count)
256
+ * }, 1000, [count])
257
+ * ```
258
+ */
259
+ var useInterval = (callback, delay = null, deps = DEFAULT_DEPENDENCIES$1) => {
260
+ const intervalRef = useRef(null);
261
+ const callbackRef = useRef(callback);
262
+ useEffect(() => {
263
+ callbackRef.current = callback;
264
+ }, [callback]);
265
+ useEffect(() => {
266
+ if (delay === null) return;
267
+ intervalRef.current = setInterval(() => {
268
+ callbackRef.current();
269
+ }, delay);
270
+ return () => {
271
+ if (intervalRef.current) {
272
+ clearInterval(intervalRef.current);
273
+ intervalRef.current = null;
274
+ }
275
+ };
276
+ }, [delay, ...deps]);
277
+ };
278
+ //#endregion
279
+ //#region src/hooks/useMatchMedia.ts
280
+ var hasMatchMedia = () => {
281
+ return typeof window?.matchMedia === "function";
282
+ };
283
+ var useMatchMedia = (query) => {
284
+ const [matches, setMatches] = useState(() => {
285
+ if (!hasMatchMedia()) return null;
286
+ try {
287
+ return window.matchMedia(query).matches;
288
+ } catch {
289
+ return null;
290
+ }
291
+ });
292
+ const matchMedia = useMemo(() => {
293
+ if (!hasMatchMedia()) return null;
294
+ try {
295
+ return window.matchMedia(query);
296
+ } catch {
297
+ return null;
298
+ }
299
+ }, [query]);
300
+ const handleChange = useCallback((event) => {
301
+ setMatches(event.matches);
302
+ }, []);
303
+ useEffect(() => {
304
+ if (!matchMedia) return;
305
+ requestAnimationFrame(() => {
306
+ setMatches(matchMedia.matches);
307
+ });
308
+ if (matchMedia.addEventListener) {
309
+ matchMedia.addEventListener("change", handleChange);
310
+ return () => matchMedia.removeEventListener("change", handleChange);
311
+ }
312
+ const legacyHandler = (event) => {
313
+ handleChange(event);
314
+ };
315
+ matchMedia.addListener(legacyHandler);
316
+ return () => matchMedia.removeListener(legacyHandler);
317
+ }, [matchMedia, handleChange]);
318
+ return matches;
319
+ };
320
+ //#endregion
321
+ //#region src/hooks/useOklch.ts
322
+ var globalCounterRef = { current: 0 };
323
+ var useOklch = () => {
324
+ const hasSetP3Ref = useRef(false);
325
+ useEffect(() => {
326
+ if (typeof window === "undefined") return;
327
+ globalCounterRef.current += 1;
328
+ hasSetP3Ref.current = true;
329
+ document.body.dataset.p3 = "false";
330
+ return () => {
331
+ globalCounterRef.current -= 1;
332
+ if (globalCounterRef.current < 1 && hasSetP3Ref.current) {
333
+ document.body.dataset.p3 = void 0;
334
+ hasSetP3Ref.current = false;
335
+ }
336
+ };
337
+ }, []);
338
+ };
339
+ //#endregion
340
+ //#region src/hooks/useOutsideClick.ts
341
+ /**
342
+ * Custom hook that detects clicks outside of a specified element and triggers a callback.
343
+ *
344
+ * Useful for implementing dropdown menus, modals, or any component that should close
345
+ * when the user clicks outside of it.
346
+ *
347
+ * @param ref - React ref object pointing to the element to monitor
348
+ * @param callback - Function to call when a click occurs outside the element
349
+ * @param isActive - Whether the outside click detection should be active (default: true)
350
+ *
351
+ * @example
352
+ * ```tsx
353
+ * const ref = useRef<HTMLDivElement>(null)
354
+ * const [isOpen, setIsOpen] = useState(false)
355
+ *
356
+ * useOutsideClick(ref, () => setIsOpen(false), isOpen)
357
+ *
358
+ * return <div ref={ref}>...</div>
359
+ * ```
360
+ */
361
+ var useOutsideClick = (ref, callback, isActive = true) => {
362
+ const liveCallbackRef = useLiveRef(callback);
363
+ useEffect(() => {
364
+ if (!isActive) return;
365
+ const handleClick = (event) => {
366
+ if (ref.current && !ref.current.contains(event.target)) liveCallbackRef.current();
367
+ };
368
+ document.addEventListener("pointerdown", handleClick, true);
369
+ return () => {
370
+ document.removeEventListener("pointerdown", handleClick, true);
371
+ };
372
+ }, [
373
+ ref,
374
+ isActive,
375
+ liveCallbackRef
376
+ ]);
377
+ };
378
+ //#endregion
379
+ //#region src/hooks/useRaf.ts
380
+ /**
381
+ * Custom hook that provides a requestAnimationFrame-based callback execution.
382
+ * Useful for performance-critical animations and continuous updates that should
383
+ * be synchronized with the browser's refresh rate.
384
+ *
385
+ * The callback will be executed on the next animation frame, and can be
386
+ * controlled with an enabled flag for conditional execution.
387
+ *
388
+ * @param callback - The function to execute on each animation frame
389
+ * @param enabled - Whether the animation loop should be active (default: true)
390
+ * @returns A function to manually trigger the animation frame callback
391
+ *
392
+ * @example
393
+ * ```tsx
394
+ * const animate = useRaf(() => {
395
+ * // Animation logic here
396
+ * console.log('Animation frame')
397
+ * }, isAnimating)
398
+ *
399
+ * // Manually trigger
400
+ * animate()
401
+ * ```
402
+ */
403
+ var useRaf = (callback, enabled = true) => {
404
+ const rafIdRef = useRef(null);
405
+ const isRunningRef = useRef(false);
406
+ const enabledRef = useLiveRef(enabled);
407
+ const cleanup = useCallback(() => {
408
+ if (rafIdRef.current) {
409
+ cancelAnimationFrame(rafIdRef.current);
410
+ rafIdRef.current = null;
411
+ }
412
+ isRunningRef.current = false;
413
+ }, []);
414
+ const animate = useCallback(() => {
415
+ if (!enabledRef.current) return;
416
+ cleanup();
417
+ isRunningRef.current = true;
418
+ const loop = () => {
419
+ if (!enabledRef.current || !isRunningRef.current) {
420
+ isRunningRef.current = false;
421
+ return;
422
+ }
423
+ callback();
424
+ rafIdRef.current = requestAnimationFrame(loop);
425
+ };
426
+ rafIdRef.current = requestAnimationFrame(loop);
427
+ }, [
428
+ callback,
429
+ cleanup,
430
+ enabledRef
431
+ ]);
432
+ useEffect(() => {
433
+ if (enabled) animate();
434
+ else cleanup();
435
+ return cleanup;
436
+ }, [
437
+ enabled,
438
+ animate,
439
+ cleanup
440
+ ]);
441
+ return animate;
442
+ };
443
+ //#endregion
444
+ //#region src/hooks/useThrottle.ts
445
+ /**
446
+ * Custom hook that throttles a callback function to limit how often it can be invoked.
447
+ *
448
+ * Throttling ensures that the callback is executed at most once per specified time interval,
449
+ * regardless of how many times the throttled function is called.
450
+ *
451
+ * @param callback - The function to throttle
452
+ * @param delay - The minimum time interval (in milliseconds) between executions
453
+ * @returns A throttled version of the callback function
454
+ *
455
+ * @example
456
+ * ```tsx
457
+ * const throttledClick = useThrottle(() => {
458
+ * console.log('Button clicked')
459
+ * }, 500)
460
+ *
461
+ * return <button onClick={throttledClick}>Click me</button>
462
+ * ```
463
+ */
464
+ var useThrottle = (callback, delay) => {
465
+ const lastExecuted = useRef(0);
466
+ const timeoutRef = useRef(null);
467
+ useEffect(() => {
468
+ return () => {
469
+ if (timeoutRef.current) {
470
+ clearTimeout(timeoutRef.current);
471
+ timeoutRef.current = null;
472
+ }
473
+ };
474
+ }, [callback, delay]);
475
+ return useCallback((...args) => {
476
+ const now = Date.now();
477
+ if (now - lastExecuted.current >= delay) {
478
+ callback(...args);
479
+ lastExecuted.current = now;
480
+ } else {
481
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
482
+ timeoutRef.current = setTimeout(() => {
483
+ callback(...args);
484
+ lastExecuted.current = Date.now();
485
+ timeoutRef.current = null;
486
+ }, delay - (now - lastExecuted.current));
487
+ }
488
+ }, [callback, delay]);
489
+ };
490
+ //#endregion
491
+ //#region src/hooks/useTimeout.ts
492
+ var DEFAULT_DEPENDENCIES = [];
493
+ /**
494
+ * Custom hook that manages a timeout with automatic cleanup.
495
+ * Provides a reliable way to handle timeouts that are properly cleaned up
496
+ * on component unmount or when dependencies change.
497
+ *
498
+ * @param callback - The function to execute after the delay
499
+ * @param delay - The delay in milliseconds (null to clear the timeout)
500
+ * @param deps - Optional dependency array. If provided, timeout restarts when deps change.
501
+ * If not provided, callback updates via ref without restarting timeout.
502
+ *
503
+ * @example
504
+ * ```tsx
505
+ * // Runs once, callback updates via ref
506
+ * useTimeout(() => {
507
+ * console.log('This runs after 1 second')
508
+ * }, 1000)
509
+ *
510
+ * // Restarts timeout when count changes
511
+ * useTimeout(() => {
512
+ * console.log('Count:', count)
513
+ * }, 1000, [count])
514
+ * ```
515
+ */
516
+ var useTimeout = (callback, delay = null, deps = DEFAULT_DEPENDENCIES) => {
517
+ const timeoutRef = useRef(null);
518
+ const callbackRef = useRef(callback);
519
+ useEffect(() => {
520
+ callbackRef.current = callback;
521
+ }, [callback]);
522
+ useEffect(() => {
523
+ if (delay === null) return;
524
+ timeoutRef.current = setTimeout(() => {
525
+ callbackRef.current();
526
+ }, delay);
527
+ return () => {
528
+ if (timeoutRef.current) {
529
+ clearTimeout(timeoutRef.current);
530
+ timeoutRef.current = null;
531
+ }
532
+ };
533
+ }, [delay, ...deps]);
534
+ };
535
+ //#endregion
536
+ //#region src/hooks/useWindowReady.tsx
537
+ var useWindowReady = () => {
538
+ const [ready, setReady] = useState(false);
539
+ useEffect(() => {
540
+ const timer = setTimeout(() => setReady(true), 0);
541
+ return () => clearTimeout(timer);
542
+ }, []);
543
+ return ready;
544
+ };
545
+ //#endregion
546
+ //#region src/hooks/useReducedMotion.ts
547
+ var QUERY = "(prefers-reduced-motion: reduce)";
548
+ function useReducedMotion() {
549
+ const [reduced, setReduced] = useState(() => {
550
+ if (typeof window === "undefined") return false;
551
+ return window.matchMedia(QUERY).matches;
552
+ });
553
+ useEffect(() => {
554
+ const mql = window.matchMedia(QUERY);
555
+ const handler = (e) => setReduced(e.matches);
556
+ mql.addEventListener("change", handler);
557
+ return () => mql.removeEventListener("change", handler);
558
+ }, []);
559
+ return reduced;
560
+ }
561
+ //#endregion
562
+ //#region src/hooks/useGsapContext.ts
563
+ /**
564
+ * Creates a GSAP context scoped to the given ref.
565
+ * All animations created inside the callback are automatically
566
+ * reverted on unmount — preventing DOM leak bugs.
567
+ */
568
+ function useGsapContext(scope, callback, deps = []) {
569
+ const ctxRef = useRef(null);
570
+ useEffect(() => {
571
+ if (!scope.current) return;
572
+ const ctx = gsap.context((context) => {
573
+ const typedContext = context;
574
+ ctxRef.current = typedContext;
575
+ const cleanup = callback(typedContext);
576
+ if (typeof cleanup === "function") typedContext.add(cleanup);
577
+ }, scope.current);
578
+ ctxRef.current = ctx;
579
+ return () => {
580
+ ctx.revert();
581
+ ctxRef.current = null;
582
+ };
583
+ }, [scope, ...deps]);
584
+ return ctxRef;
585
+ }
586
+ //#endregion
587
+ //#region src/hooks/useFormField.ts
588
+ function useFormField(options = {}) {
589
+ const { id: externalId, hasError = false, hasHint = false } = options;
590
+ const autoId = useId();
591
+ const fieldId = externalId ?? autoId;
592
+ const ids = useMemo(() => ({
593
+ fieldId,
594
+ labelId: `${fieldId}-label`,
595
+ errorId: `${fieldId}-error`,
596
+ hintId: `${fieldId}-hint`,
597
+ descriptionId: `${fieldId}-description`
598
+ }), [fieldId]);
599
+ const describedBy = useMemo(() => {
600
+ const parts = [];
601
+ if (hasError) parts.push(ids.errorId);
602
+ if (hasHint && !hasError) parts.push(ids.hintId);
603
+ return parts.length > 0 ? parts.join(" ") : void 0;
604
+ }, [
605
+ hasError,
606
+ hasHint,
607
+ ids.errorId,
608
+ ids.hintId
609
+ ]);
610
+ return useMemo(() => ({
611
+ ids,
612
+ labelProps: {
613
+ htmlFor: fieldId,
614
+ id: ids.labelId
615
+ },
616
+ fieldProps: {
617
+ id: fieldId,
618
+ "aria-describedby": describedBy,
619
+ "aria-invalid": hasError || void 0,
620
+ "aria-errormessage": hasError ? ids.errorId : void 0
621
+ },
622
+ errorProps: {
623
+ id: ids.errorId,
624
+ role: "alert"
625
+ },
626
+ hintProps: { id: ids.hintId }
627
+ }), [
628
+ ids,
629
+ fieldId,
630
+ describedBy,
631
+ hasError
632
+ ]);
633
+ }
634
+ //#endregion
635
+ export { mergeRefs, useAnimatedText, useBowser, useControllableState, useDebounce, useEffectEvent, useFormField, useGsapContext, useInterval, useLiveRef, useMatchMedia, useMergeRefs, useOklch, useOutsideClick, usePreviousRender, usePreviousState, useRaf, useReducedMotion, useThrottle, useTimeout, useUniversalLayoutEffect, useWindowReady };