@arolariu/components 1.0.0 → 1.1.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 (218) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/EXAMPLES.md +2510 -0
  3. package/dist/components/ui/alert-dialog.d.ts +4 -16
  4. package/dist/components/ui/alert-dialog.d.ts.map +1 -1
  5. package/dist/components/ui/alert-dialog.js +18 -14
  6. package/dist/components/ui/alert-dialog.js.map +1 -1
  7. package/dist/components/ui/avatar.d.ts +3 -12
  8. package/dist/components/ui/avatar.d.ts.map +1 -1
  9. package/dist/components/ui/avatar.js +18 -15
  10. package/dist/components/ui/avatar.js.map +1 -1
  11. package/dist/components/ui/button-group.d.ts +1 -1
  12. package/dist/components/ui/button-group.d.ts.map +1 -1
  13. package/dist/components/ui/calendar.d.ts +1 -4
  14. package/dist/components/ui/calendar.d.ts.map +1 -1
  15. package/dist/components/ui/calendar.js +7 -7
  16. package/dist/components/ui/calendar.js.map +1 -1
  17. package/dist/components/ui/carousel.d.ts.map +1 -1
  18. package/dist/components/ui/carousel.js.map +1 -1
  19. package/dist/components/ui/chart.d.ts.map +1 -1
  20. package/dist/components/ui/chart.js +125 -59
  21. package/dist/components/ui/chart.js.map +1 -1
  22. package/dist/components/ui/checkbox-group.d.ts +2 -6
  23. package/dist/components/ui/checkbox-group.d.ts.map +1 -1
  24. package/dist/components/ui/checkbox-group.js +8 -7
  25. package/dist/components/ui/checkbox-group.js.map +1 -1
  26. package/dist/components/ui/checkbox.d.ts +3 -1
  27. package/dist/components/ui/checkbox.d.ts.map +1 -1
  28. package/dist/components/ui/checkbox.js +4 -1
  29. package/dist/components/ui/checkbox.js.map +1 -1
  30. package/dist/components/ui/collapsible.d.ts.map +1 -1
  31. package/dist/components/ui/collapsible.js.map +1 -1
  32. package/dist/components/ui/combobox.d.ts +335 -0
  33. package/dist/components/ui/combobox.d.ts.map +1 -0
  34. package/dist/components/ui/combobox.js +206 -0
  35. package/dist/components/ui/combobox.js.map +1 -0
  36. package/dist/components/ui/combobox.module.js +23 -0
  37. package/dist/components/ui/combobox.module.js.map +1 -0
  38. package/dist/components/ui/combobox_module.css +142 -0
  39. package/dist/components/ui/combobox_module.css.map +1 -0
  40. package/dist/components/ui/command.d.ts.map +1 -1
  41. package/dist/components/ui/command.js +25 -16
  42. package/dist/components/ui/command.js.map +1 -1
  43. package/dist/components/ui/context-menu.d.ts.map +1 -1
  44. package/dist/components/ui/context-menu.js.map +1 -1
  45. package/dist/components/ui/drawer.d.ts.map +1 -1
  46. package/dist/components/ui/drawer.js.map +1 -1
  47. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  48. package/dist/components/ui/dropdown-menu.js.map +1 -1
  49. package/dist/components/ui/dropdrawer.d.ts +10 -16
  50. package/dist/components/ui/dropdrawer.d.ts.map +1 -1
  51. package/dist/components/ui/dropdrawer.js +28 -20
  52. package/dist/components/ui/dropdrawer.js.map +1 -1
  53. package/dist/components/ui/item.d.ts +1 -1
  54. package/dist/components/ui/item.d.ts.map +1 -1
  55. package/dist/components/ui/menubar.d.ts +11 -13
  56. package/dist/components/ui/menubar.d.ts.map +1 -1
  57. package/dist/components/ui/menubar.js.map +1 -1
  58. package/dist/components/ui/meter.d.ts +8 -24
  59. package/dist/components/ui/meter.d.ts.map +1 -1
  60. package/dist/components/ui/meter.js +23 -19
  61. package/dist/components/ui/meter.js.map +1 -1
  62. package/dist/components/ui/navigation-menu.d.ts +3 -12
  63. package/dist/components/ui/navigation-menu.d.ts.map +1 -1
  64. package/dist/components/ui/navigation-menu.js +14 -11
  65. package/dist/components/ui/navigation-menu.js.map +1 -1
  66. package/dist/components/ui/number-field.d.ts +6 -12
  67. package/dist/components/ui/number-field.d.ts.map +1 -1
  68. package/dist/components/ui/number-field.js.map +1 -1
  69. package/dist/components/ui/progress.d.ts +1 -4
  70. package/dist/components/ui/progress.d.ts.map +1 -1
  71. package/dist/components/ui/progress.js +10 -9
  72. package/dist/components/ui/progress.js.map +1 -1
  73. package/dist/components/ui/radio-group.d.ts +2 -4
  74. package/dist/components/ui/radio-group.d.ts.map +1 -1
  75. package/dist/components/ui/radio-group.js.map +1 -1
  76. package/dist/components/ui/resizable.d.ts +3 -3
  77. package/dist/components/ui/resizable.d.ts.map +1 -1
  78. package/dist/components/ui/resizable.js.map +1 -1
  79. package/dist/components/ui/scratcher.d.ts +1 -1
  80. package/dist/components/ui/scratcher.d.ts.map +1 -1
  81. package/dist/components/ui/scratcher.js +5 -4
  82. package/dist/components/ui/scratcher.js.map +1 -1
  83. package/dist/components/ui/scroll-area.d.ts +2 -4
  84. package/dist/components/ui/scroll-area.d.ts.map +1 -1
  85. package/dist/components/ui/scroll-area.js.map +1 -1
  86. package/dist/components/ui/separator.d.ts +1 -4
  87. package/dist/components/ui/separator.d.ts.map +1 -1
  88. package/dist/components/ui/separator.js +9 -8
  89. package/dist/components/ui/separator.js.map +1 -1
  90. package/dist/components/ui/sheet.d.ts.map +1 -1
  91. package/dist/components/ui/sheet.js.map +1 -1
  92. package/dist/components/ui/sidebar.d.ts +1 -1
  93. package/dist/components/ui/sidebar.d.ts.map +1 -1
  94. package/dist/components/ui/sidebar.js.map +1 -1
  95. package/dist/components/ui/sonner.d.ts +5 -4
  96. package/dist/components/ui/sonner.d.ts.map +1 -1
  97. package/dist/components/ui/sonner.js +7 -6
  98. package/dist/components/ui/sonner.js.map +1 -1
  99. package/dist/components/ui/toggle-group.d.ts +2 -8
  100. package/dist/components/ui/toggle-group.d.ts.map +1 -1
  101. package/dist/components/ui/toggle-group.js +12 -10
  102. package/dist/components/ui/toggle-group.js.map +1 -1
  103. package/dist/components/ui/toolbar.d.ts +10 -30
  104. package/dist/components/ui/toolbar.d.ts.map +1 -1
  105. package/dist/components/ui/toolbar.js +28 -23
  106. package/dist/components/ui/toolbar.js.map +1 -1
  107. package/dist/hooks/useClipboard.d.ts +77 -0
  108. package/dist/hooks/useClipboard.d.ts.map +1 -0
  109. package/dist/hooks/useClipboard.js +42 -0
  110. package/dist/hooks/useClipboard.js.map +1 -0
  111. package/dist/hooks/useControllableState.d.ts +54 -0
  112. package/dist/hooks/useControllableState.d.ts.map +1 -0
  113. package/dist/hooks/useControllableState.js +29 -0
  114. package/dist/hooks/useControllableState.js.map +1 -0
  115. package/dist/hooks/useDebounce.d.ts +33 -0
  116. package/dist/hooks/useDebounce.d.ts.map +1 -0
  117. package/dist/hooks/useDebounce.js +20 -0
  118. package/dist/hooks/useDebounce.js.map +1 -0
  119. package/dist/hooks/useEventCallback.d.ts +34 -0
  120. package/dist/hooks/useEventCallback.d.ts.map +1 -0
  121. package/dist/hooks/useEventCallback.js +12 -0
  122. package/dist/hooks/useEventCallback.js.map +1 -0
  123. package/dist/hooks/useId.d.ts +30 -0
  124. package/dist/hooks/useId.d.ts.map +1 -0
  125. package/dist/hooks/useId.js +9 -0
  126. package/dist/hooks/useId.js.map +1 -0
  127. package/dist/hooks/useIntersectionObserver.d.ts +51 -0
  128. package/dist/hooks/useIntersectionObserver.d.ts.map +1 -0
  129. package/dist/hooks/useIntersectionObserver.js +25 -0
  130. package/dist/hooks/useIntersectionObserver.js.map +1 -0
  131. package/dist/hooks/useInterval.d.ts +55 -0
  132. package/dist/hooks/useInterval.d.ts.map +1 -0
  133. package/dist/hooks/useInterval.js +24 -0
  134. package/dist/hooks/useInterval.js.map +1 -0
  135. package/dist/hooks/useLocalStorage.d.ts +43 -0
  136. package/dist/hooks/useLocalStorage.d.ts.map +1 -0
  137. package/dist/hooks/useLocalStorage.js +53 -0
  138. package/dist/hooks/useLocalStorage.js.map +1 -0
  139. package/dist/hooks/useMergedRefs.d.ts +27 -0
  140. package/dist/hooks/useMergedRefs.d.ts.map +1 -0
  141. package/dist/hooks/useMergedRefs.js +11 -0
  142. package/dist/hooks/useMergedRefs.js.map +1 -0
  143. package/dist/hooks/useOnClickOutside.d.ts +32 -0
  144. package/dist/hooks/useOnClickOutside.d.ts.map +1 -0
  145. package/dist/hooks/useOnClickOutside.js +23 -0
  146. package/dist/hooks/useOnClickOutside.js.map +1 -0
  147. package/dist/hooks/usePrevious.d.ts +33 -0
  148. package/dist/hooks/usePrevious.d.ts.map +1 -0
  149. package/dist/hooks/usePrevious.js +14 -0
  150. package/dist/hooks/usePrevious.js.map +1 -0
  151. package/dist/hooks/useThrottle.d.ts +37 -0
  152. package/dist/hooks/useThrottle.d.ts.map +1 -0
  153. package/dist/hooks/useThrottle.js +34 -0
  154. package/dist/hooks/useThrottle.js.map +1 -0
  155. package/dist/hooks/useTimeout.d.ts +28 -0
  156. package/dist/hooks/useTimeout.d.ts.map +1 -0
  157. package/dist/hooks/useTimeout.js +24 -0
  158. package/dist/hooks/useTimeout.js.map +1 -0
  159. package/dist/index.d.ts +14 -0
  160. package/dist/index.d.ts.map +1 -1
  161. package/dist/index.js +14 -0
  162. package/dist/lib/utilities.d.ts +2 -3
  163. package/dist/lib/utilities.d.ts.map +1 -1
  164. package/dist/lib/utilities.js.map +1 -1
  165. package/dist/motion/tokens.js +5 -5
  166. package/dist/motion/tokens.js.map +1 -1
  167. package/dist/rslib-runtime.js +39 -0
  168. package/dist/rslib-runtime.js.map +1 -0
  169. package/package.json +82 -3
  170. package/src/components/ui/alert-dialog.tsx +15 -8
  171. package/src/components/ui/avatar.tsx +9 -6
  172. package/src/components/ui/calendar.tsx +7 -13
  173. package/src/components/ui/carousel.tsx +2 -0
  174. package/src/components/ui/chart.tsx +63 -60
  175. package/src/components/ui/checkbox-group.tsx +4 -5
  176. package/src/components/ui/checkbox.tsx +10 -2
  177. package/src/components/ui/collapsible.tsx +1 -0
  178. package/src/components/ui/combobox.module.css +158 -0
  179. package/src/components/ui/combobox.tsx +569 -0
  180. package/src/components/ui/command.tsx +31 -15
  181. package/src/components/ui/context-menu.tsx +3 -0
  182. package/src/components/ui/drawer.tsx +2 -0
  183. package/src/components/ui/dropdown-menu.tsx +3 -0
  184. package/src/components/ui/dropdrawer.tsx +80 -62
  185. package/src/components/ui/menubar.tsx +9 -10
  186. package/src/components/ui/meter.tsx +16 -17
  187. package/src/components/ui/navigation-menu.tsx +41 -33
  188. package/src/components/ui/number-field.tsx +6 -13
  189. package/src/components/ui/progress.tsx +3 -2
  190. package/src/components/ui/radio-group.tsx +2 -5
  191. package/src/components/ui/resizable.tsx +2 -2
  192. package/src/components/ui/scratcher.tsx +6 -10
  193. package/src/components/ui/scroll-area.tsx +2 -5
  194. package/src/components/ui/separator.tsx +4 -3
  195. package/src/components/ui/sheet.tsx +3 -0
  196. package/src/components/ui/sidebar.tsx +1 -0
  197. package/src/components/ui/sonner.tsx +20 -12
  198. package/src/components/ui/toggle-group.tsx +6 -4
  199. package/src/components/ui/toolbar.tsx +20 -21
  200. package/src/hooks/useClipboard.tsx +137 -0
  201. package/src/hooks/useControllableState.tsx +81 -0
  202. package/src/hooks/useDebounce.tsx +50 -0
  203. package/src/hooks/useEventCallback.tsx +47 -0
  204. package/src/hooks/useId.tsx +36 -0
  205. package/src/hooks/useIntersectionObserver.tsx +81 -0
  206. package/src/hooks/useInterval.tsx +80 -0
  207. package/src/hooks/useLocalStorage.tsx +111 -0
  208. package/src/hooks/useMergedRefs.tsx +48 -0
  209. package/src/hooks/useOnClickOutside.tsx +55 -0
  210. package/src/hooks/usePrevious.tsx +44 -0
  211. package/src/hooks/useThrottle.tsx +78 -0
  212. package/src/hooks/useTimeout.tsx +51 -0
  213. package/src/index.ts +23 -0
  214. package/src/lib/utilities.ts +4 -4
  215. package/src/motion/tokens.ts +4 -4
  216. package/src/stories/DesignPrinciples.mdx +48 -0
  217. package/src/stories/GettingStarted.mdx +92 -0
  218. package/src/stories/Welcome.mdx +44 -0
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ /**
6
+ * Debounces a value, delaying updates until after the specified delay has elapsed.
7
+ *
8
+ * @remarks
9
+ * This hook returns a debounced version of the provided value that only updates
10
+ * after the value has stopped changing for the specified delay. Useful for optimizing
11
+ * performance in scenarios like search inputs, where you want to avoid triggering
12
+ * expensive operations on every keystroke.
13
+ *
14
+ * The debounce timer resets on every value change and cleans up automatically on unmount.
15
+ *
16
+ * @typeParam T - The type of the value being debounced.
17
+ * @param value - The value to debounce.
18
+ * @param delay - The delay in milliseconds before the debounced value updates.
19
+ * @returns The debounced value.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * function SearchInput() {
24
+ * const [searchTerm, setSearchTerm] = useState("");
25
+ * const debouncedSearchTerm = useDebounce(searchTerm, 500);
26
+ *
27
+ * useEffect(() => {
28
+ * // Expensive search operation
29
+ * performSearch(debouncedSearchTerm);
30
+ * }, [debouncedSearchTerm]);
31
+ *
32
+ * return <input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />;
33
+ * }
34
+ * ```
35
+ */
36
+ export function useDebounce<T>(value: T, delay: number): T {
37
+ const [debouncedValue, setDebouncedValue] = React.useState<T>(value);
38
+
39
+ React.useEffect(() => {
40
+ const timeoutId = globalThis.setTimeout(() => {
41
+ setDebouncedValue(value);
42
+ }, delay);
43
+
44
+ return () => {
45
+ globalThis.clearTimeout(timeoutId);
46
+ };
47
+ }, [value, delay]);
48
+
49
+ return debouncedValue;
50
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ /**
6
+ * Creates a stable callback reference that always calls the latest version of the provided function.
7
+ *
8
+ * @remarks
9
+ * Unlike `useCallback`, this hook returns a stable function reference that never changes,
10
+ * but always invokes the most recent version of the callback. This is useful when you need
11
+ * to pass callbacks to optimized child components or effects without triggering re-renders
12
+ * when dependencies change.
13
+ *
14
+ * The returned function is safe to use in dependency arrays because its identity never changes.
15
+ *
16
+ * @typeParam Args - The tuple type of the callback's arguments.
17
+ * @typeParam Return - The return type of the callback.
18
+ * @param callback - The function to wrap with a stable reference.
19
+ * @returns A stable function reference that invokes the latest callback.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * function SearchInput({onSearch}) {
24
+ * const [query, setQuery] = useState("");
25
+ * // stableOnSearch never changes identity, but always calls the latest onSearch
26
+ * const stableOnSearch = useEventCallback(onSearch);
27
+ *
28
+ * useEffect(() => {
29
+ * const timer = setTimeout(() => stableOnSearch(query), 500);
30
+ * return () => clearTimeout(timer);
31
+ * }, [query, stableOnSearch]); // Safe to include in deps
32
+ *
33
+ * return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
34
+ * }
35
+ * ```
36
+ */
37
+ export function useEventCallback<Args extends unknown[], Return>(callback: (...args: Args) => Return): (...args: Args) => Return {
38
+ const callbackRef = React.useRef(callback);
39
+
40
+ React.useLayoutEffect(() => {
41
+ callbackRef.current = callback;
42
+ });
43
+
44
+ return React.useCallback((...args: Args) => {
45
+ return callbackRef.current(...args);
46
+ }, []);
47
+ }
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ /**
6
+ * Generates a unique, stable identifier that is safe for server-side rendering.
7
+ *
8
+ * @remarks
9
+ * This hook wraps React's `useId` and optionally prepends a custom prefix.
10
+ * The generated ID remains stable across re-renders and matches between server
11
+ * and client, making it ideal for associating form labels with inputs or
12
+ * managing accessible ARIA relationships.
13
+ *
14
+ * @param prefix - Optional string to prepend to the generated ID.
15
+ * @returns A unique identifier string.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * function FormField({label}) {
20
+ * const id = useId("field");
21
+ *
22
+ * return (
23
+ * <div>
24
+ * <label htmlFor={id}>{label}</label>
25
+ * <input id={id} type="text" />
26
+ * </div>
27
+ * );
28
+ * }
29
+ * ```
30
+ *
31
+ * @see {@link https://react.dev/reference/react/useId | React useId}
32
+ */
33
+ export function useId(prefix?: string): string {
34
+ const reactId = React.useId();
35
+ return prefix ? `${prefix}-${reactId}` : reactId;
36
+ }
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ /**
6
+ * Observes element visibility using the Intersection Observer API.
7
+ *
8
+ * @remarks
9
+ * This hook creates an IntersectionObserver that watches the provided element
10
+ * reference and returns the latest `IntersectionObserverEntry`. It's useful
11
+ * for implementing lazy loading, infinite scroll, animations on scroll, and
12
+ * tracking element visibility.
13
+ *
14
+ * The observer automatically disconnects when the component unmounts or when
15
+ * the element reference changes. The hook is SSR-safe and returns `null` when
16
+ * running on the server or when the observer is not yet initialized.
17
+ *
18
+ * @param ref - A React ref object pointing to the element to observe.
19
+ * @param options - Optional IntersectionObserver configuration (threshold, root, rootMargin).
20
+ * @returns The latest IntersectionObserverEntry or null if not intersecting yet.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * function LazyImage({src, alt}: {src: string; alt: string}) {
25
+ * const imageRef = useRef<HTMLImageElement>(null);
26
+ * const entry = useIntersectionObserver(imageRef, {threshold: 0.1});
27
+ *
28
+ * return (
29
+ * <img
30
+ * ref={imageRef}
31
+ * src={entry?.isIntersecting ? src : undefined}
32
+ * alt={alt}
33
+ * />
34
+ * );
35
+ * }
36
+ * ```
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * function AnimateOnScroll({children}: {children: React.ReactNode}) {
41
+ * const ref = useRef<HTMLDivElement>(null);
42
+ * const entry = useIntersectionObserver(ref, {threshold: 0.5});
43
+ * const isVisible = entry?.isIntersecting ?? false;
44
+ *
45
+ * return (
46
+ * <div ref={ref} className={isVisible ? "fade-in" : "hidden"}>
47
+ * {children}
48
+ * </div>
49
+ * );
50
+ * }
51
+ * ```
52
+ */
53
+ export function useIntersectionObserver(
54
+ ref: React.RefObject<Element | null>,
55
+ options?: IntersectionObserverInit,
56
+ ): IntersectionObserverEntry | null {
57
+ const [entry, setEntry] = React.useState<IntersectionObserverEntry | null>(null);
58
+
59
+ React.useEffect(() => {
60
+ const element = ref.current;
61
+
62
+ // SSR safety: IntersectionObserver is not available on server
63
+ if (typeof globalThis.IntersectionObserver === "undefined" || !element) {
64
+ return;
65
+ }
66
+
67
+ const observer = new globalThis.IntersectionObserver(([observerEntry]) => {
68
+ if (observerEntry) {
69
+ setEntry(observerEntry);
70
+ }
71
+ }, options);
72
+
73
+ observer.observe(element);
74
+
75
+ return () => {
76
+ observer.disconnect();
77
+ };
78
+ }, [ref, options?.threshold, options?.root, options?.rootMargin]);
79
+
80
+ return entry;
81
+ }
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ /**
6
+ * Executes a callback function at specified intervals with automatic cleanup.
7
+ *
8
+ * @remarks
9
+ * This hook provides a declarative interface for `setInterval` that automatically
10
+ * handles cleanup on unmount and ensures the latest callback is always invoked
11
+ * (preventing stale closures). Setting the delay to `null` pauses the interval,
12
+ * which is useful for implementing play/pause functionality.
13
+ *
14
+ * Unlike raw `setInterval`, this hook guarantees that the interval is cleared
15
+ * when the component unmounts or when the delay changes, preventing memory leaks
16
+ * and unexpected behavior.
17
+ *
18
+ * @param callback - The function to execute at each interval.
19
+ * @param delay - The interval delay in milliseconds, or `null` to pause the interval.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * function Timer() {
24
+ * const [count, setCount] = useState(0);
25
+ *
26
+ * useInterval(() => {
27
+ * setCount((c) => c + 1);
28
+ * }, 1000);
29
+ *
30
+ * return <div>Count: {count}</div>;
31
+ * }
32
+ * ```
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * function PausableTimer() {
37
+ * const [count, setCount] = useState(0);
38
+ * const [isRunning, setIsRunning] = useState(true);
39
+ *
40
+ * useInterval(
41
+ * () => {
42
+ * setCount((c) => c + 1);
43
+ * },
44
+ * isRunning ? 1000 : null,
45
+ * );
46
+ *
47
+ * return (
48
+ * <div>
49
+ * <div>Count: {count}</div>
50
+ * <button onClick={() => setIsRunning(!isRunning)}>
51
+ * {isRunning ? "Pause" : "Resume"}
52
+ * </button>
53
+ * </div>
54
+ * );
55
+ * }
56
+ * ```
57
+ */
58
+ export function useInterval(callback: () => void, delay: number | null): void {
59
+ const savedCallback = React.useRef(callback);
60
+
61
+ // Update ref to latest callback on every render to avoid stale closures
62
+ React.useEffect(() => {
63
+ savedCallback.current = callback;
64
+ }, [callback]);
65
+
66
+ React.useEffect(() => {
67
+ // Don't schedule if delay is null
68
+ if (delay === null) {
69
+ return;
70
+ }
71
+
72
+ const intervalId = globalThis.setInterval(() => {
73
+ savedCallback.current();
74
+ }, delay);
75
+
76
+ return () => {
77
+ globalThis.clearInterval(intervalId);
78
+ };
79
+ }, [delay]);
80
+ }
@@ -0,0 +1,111 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ /**
6
+ * Persists state to localStorage with SSR safety and JSON serialization.
7
+ *
8
+ * @remarks
9
+ * This hook synchronizes state with localStorage, allowing data to persist
10
+ * across page refreshes and browser sessions. It is SSR-safe and returns the
11
+ * initial value on the server until hydration completes. The hook also syncs
12
+ * state across tabs/windows via the `storage` event and handles JSON parse
13
+ * errors gracefully by falling back to the initial value.
14
+ *
15
+ * @typeParam T - The type of the value being stored.
16
+ * @param key - The localStorage key to store the value under.
17
+ * @param initialValue - The default value to use if no value is found in localStorage.
18
+ * @returns A tuple containing the current value and a setter function.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * function UserSettings() {
23
+ * const [theme, setTheme] = useLocalStorage("theme", "light");
24
+ *
25
+ * return (
26
+ * <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
27
+ * Toggle theme (current: {theme})
28
+ * </button>
29
+ * );
30
+ * }
31
+ * ```
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * function ShoppingCart() {
36
+ * const [cart, setCart] = useLocalStorage<Product[]>("cart", []);
37
+ *
38
+ * return (
39
+ * <button onClick={() => setCart((prev) => [...prev, newProduct])}>
40
+ * Add to cart ({cart.length} items)
41
+ * </button>
42
+ * );
43
+ * }
44
+ * ```
45
+ */
46
+ export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void] {
47
+ const [storedValue, setStoredValue] = React.useState<T>(() => {
48
+ // SSR safety: return initial value on server
49
+ if (typeof globalThis.window === "undefined") {
50
+ return initialValue;
51
+ }
52
+
53
+ try {
54
+ const item = globalThis.window.localStorage.getItem(key);
55
+
56
+ return item !== null ? (JSON.parse(item) as T) : initialValue;
57
+ } catch (error) {
58
+ console.error(`Error reading localStorage key "${key}":`, error);
59
+
60
+ return initialValue;
61
+ }
62
+ });
63
+
64
+ const setValue = React.useCallback(
65
+ (value: T | ((prev: T) => T)) => {
66
+ try {
67
+ setStoredValue((currentValue) => {
68
+ const valueToStore = value instanceof Function ? value(currentValue) : value;
69
+
70
+ if (typeof globalThis.window !== "undefined") {
71
+ globalThis.window.localStorage.setItem(key, JSON.stringify(valueToStore));
72
+ }
73
+
74
+ return valueToStore;
75
+ });
76
+ } catch (error) {
77
+ console.error(`Error setting localStorage key "${key}":`, error);
78
+ }
79
+ },
80
+ [key],
81
+ );
82
+
83
+ React.useEffect(() => {
84
+ // SSR safety: window is not available on server
85
+ if (typeof globalThis.window === "undefined") {
86
+ return;
87
+ }
88
+
89
+ const handleStorageChange = (event: StorageEvent) => {
90
+ if (event.key !== key || event.storageArea !== globalThis.window.localStorage) {
91
+ return;
92
+ }
93
+
94
+ try {
95
+ const newValue = event.newValue !== null ? (JSON.parse(event.newValue) as T) : initialValue;
96
+
97
+ setStoredValue(newValue);
98
+ } catch (error) {
99
+ console.error(`Error parsing storage event for key "${key}":`, error);
100
+ }
101
+ };
102
+
103
+ globalThis.window.addEventListener("storage", handleStorageChange);
104
+
105
+ return () => {
106
+ globalThis.window.removeEventListener("storage", handleStorageChange);
107
+ };
108
+ }, [key, initialValue]);
109
+
110
+ return [storedValue, setValue];
111
+ }
@@ -0,0 +1,48 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ /**
6
+ * Merges multiple refs into a single callback ref.
7
+ *
8
+ * @remarks
9
+ * This hook is essential when you need to attach multiple refs to the same element,
10
+ * such as combining a forwarded ref with an internal ref for measurements or
11
+ * imperative operations. All provided refs will receive the same element instance.
12
+ *
13
+ * Supports all ref types: callback refs, mutable ref objects, and `null`/`undefined`.
14
+ *
15
+ * @typeParam T - The type of the element being referenced.
16
+ * @param refs - An array of refs to merge. Can include callback refs, ref objects, or undefined.
17
+ * @returns A callback ref that updates all provided refs.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * const MyComponent = React.forwardRef<HTMLDivElement, Props>((props, forwardedRef) => {
22
+ * const internalRef = useRef<HTMLDivElement>(null);
23
+ * const mergedRef = useMergedRefs(forwardedRef, internalRef);
24
+ *
25
+ * return <div ref={mergedRef}>Content</div>;
26
+ * });
27
+ * ```
28
+ */
29
+ export function useMergedRefs<T>(...refs: Array<React.Ref<T> | undefined>): React.RefCallback<T> {
30
+ return React.useCallback(
31
+ (element: T | null) => {
32
+ for (const ref of refs) {
33
+ if (!ref) {
34
+ continue;
35
+ }
36
+
37
+ if (typeof ref === "function") {
38
+ ref(element);
39
+ } else {
40
+ // Mutable ref object
41
+ (ref as React.MutableRefObject<T | null>).current = element;
42
+ }
43
+ }
44
+ },
45
+ // eslint-disable-next-line react-hooks/exhaustive-deps
46
+ refs,
47
+ );
48
+ }
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ /**
6
+ * Detects clicks or touch events outside a referenced element.
7
+ *
8
+ * @remarks
9
+ * This hook is commonly used for implementing dropdown menus, modals, and popovers
10
+ * that should close when the user interacts outside their boundaries. It listens
11
+ * to both mouse and touch events to ensure broad device compatibility.
12
+ *
13
+ * The event listeners are automatically cleaned up when the component unmounts.
14
+ *
15
+ * @param ref - A ref object pointing to the element to monitor.
16
+ * @param handler - Callback invoked when a click or touch occurs outside the element.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * function Dropdown() {
21
+ * const [isOpen, setIsOpen] = useState(false);
22
+ * const dropdownRef = useRef<HTMLDivElement>(null);
23
+ *
24
+ * useOnClickOutside(dropdownRef, () => setIsOpen(false));
25
+ *
26
+ * return (
27
+ * <div ref={dropdownRef}>
28
+ * {isOpen && <DropdownMenu />}
29
+ * </div>
30
+ * );
31
+ * }
32
+ * ```
33
+ */
34
+ export function useOnClickOutside(ref: React.RefObject<HTMLElement | null>, handler: (event: MouseEvent | TouchEvent) => void): void {
35
+ React.useEffect(() => {
36
+ const listener = (event: MouseEvent | TouchEvent) => {
37
+ const element = ref.current;
38
+
39
+ // Do nothing if clicking ref's element or descendent elements
40
+ if (!element || element.contains(event.target as Node)) {
41
+ return;
42
+ }
43
+
44
+ handler(event);
45
+ };
46
+
47
+ globalThis.document.addEventListener("mousedown", listener);
48
+ globalThis.document.addEventListener("touchstart", listener);
49
+
50
+ return () => {
51
+ globalThis.document.removeEventListener("mousedown", listener);
52
+ globalThis.document.removeEventListener("touchstart", listener);
53
+ };
54
+ }, [ref, handler]);
55
+ }
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ /**
6
+ * Tracks and returns the previous value of a state or prop.
7
+ *
8
+ * @remarks
9
+ * This hook stores the value from the previous render cycle, allowing you to compare
10
+ * current and previous values. On the initial render, it returns `undefined` since
11
+ * there is no previous value yet.
12
+ *
13
+ * Useful for detecting changes, implementing undo functionality, or creating
14
+ * animations based on value transitions.
15
+ *
16
+ * @typeParam T - The type of the value being tracked.
17
+ * @param value - The current value to track.
18
+ * @returns The value from the previous render, or `undefined` on the first render.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * function Counter() {
23
+ * const [count, setCount] = useState(0);
24
+ * const previousCount = usePrevious(count);
25
+ *
26
+ * return (
27
+ * <div>
28
+ * <p>Current: {count}</p>
29
+ * <p>Previous: {previousCount ?? "N/A"}</p>
30
+ * <button onClick={() => setCount(count + 1)}>Increment</button>
31
+ * </div>
32
+ * );
33
+ * }
34
+ * ```
35
+ */
36
+ export function usePrevious<T>(value: T): T | undefined {
37
+ const ref = React.useRef<T | undefined>(undefined);
38
+
39
+ React.useEffect(() => {
40
+ ref.current = value;
41
+ }, [value]);
42
+
43
+ return ref.current;
44
+ }
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ /**
6
+ * Throttles a callback function, limiting how often it can be invoked.
7
+ *
8
+ * @remarks
9
+ * This hook returns a throttled version of the provided callback that can only
10
+ * be executed once per specified interval. Subsequent calls within the interval
11
+ * are ignored. Useful for rate-limiting expensive operations triggered by high-frequency
12
+ * events like scrolling, resizing, or mouse movement.
13
+ *
14
+ * Unlike debouncing, throttling ensures the callback is invoked at regular intervals
15
+ * during continuous events, providing more predictable execution timing.
16
+ *
17
+ * @typeParam Args - The tuple type of the callback's arguments.
18
+ * @param callback - The function to throttle.
19
+ * @param delay - The minimum interval in milliseconds between invocations.
20
+ * @returns A throttled version of the callback.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * function ScrollTracker() {
25
+ * const [scrollPos, setScrollPos] = useState(0);
26
+ *
27
+ * const handleScroll = useThrottle(() => {
28
+ * setScrollPos(window.scrollY);
29
+ * }, 200);
30
+ *
31
+ * useEffect(() => {
32
+ * window.addEventListener("scroll", handleScroll);
33
+ * return () => window.removeEventListener("scroll", handleScroll);
34
+ * }, [handleScroll]);
35
+ *
36
+ * return <p>Scroll position: {scrollPos}</p>;
37
+ * }
38
+ * ```
39
+ */
40
+ export function useThrottle<Args extends unknown[]>(callback: (...args: Args) => void, delay: number): (...args: Args) => void {
41
+ const lastRunRef = React.useRef(0);
42
+ const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
43
+ const callbackRef = React.useRef(callback);
44
+
45
+ React.useEffect(() => {
46
+ callbackRef.current = callback;
47
+ }, [callback]);
48
+
49
+ React.useEffect(() => {
50
+ return () => {
51
+ if (timeoutRef.current) {
52
+ globalThis.clearTimeout(timeoutRef.current);
53
+ }
54
+ };
55
+ }, []);
56
+
57
+ return React.useCallback(
58
+ (...args: Args) => {
59
+ const now = Date.now();
60
+ const timeSinceLastRun = now - lastRunRef.current;
61
+
62
+ if (timeSinceLastRun >= delay) {
63
+ callbackRef.current(...args);
64
+ lastRunRef.current = now;
65
+ } else {
66
+ if (timeoutRef.current) {
67
+ globalThis.clearTimeout(timeoutRef.current);
68
+ }
69
+
70
+ timeoutRef.current = globalThis.setTimeout(() => {
71
+ callbackRef.current(...args);
72
+ lastRunRef.current = Date.now();
73
+ }, delay - timeSinceLastRun);
74
+ }
75
+ },
76
+ [delay],
77
+ );
78
+ }
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ /**
6
+ * Executes a callback after a specified delay with automatic cleanup.
7
+ *
8
+ * @remarks
9
+ * This hook wraps `setTimeout` and automatically clears the timeout when the component
10
+ * unmounts or when the delay changes. Setting `delay` to `null` disables the timeout.
11
+ *
12
+ * The timeout is reset whenever the `callback` or `delay` changes, ensuring the most
13
+ * recent callback is always executed.
14
+ *
15
+ * @param callback - The function to execute after the delay.
16
+ * @param delay - The delay in milliseconds, or `null` to disable the timeout.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * function DelayedMessage() {
21
+ * const [visible, setVisible] = useState(false);
22
+ *
23
+ * useTimeout(() => {
24
+ * setVisible(true);
25
+ * }, 3000);
26
+ *
27
+ * return visible ? <p>Message appeared!</p> : <p>Waiting...</p>;
28
+ * }
29
+ * ```
30
+ */
31
+ export function useTimeout(callback: () => void, delay: number | null): void {
32
+ const savedCallback = React.useRef(callback);
33
+
34
+ React.useLayoutEffect(() => {
35
+ savedCallback.current = callback;
36
+ }, [callback]);
37
+
38
+ React.useEffect(() => {
39
+ if (delay === null) {
40
+ return;
41
+ }
42
+
43
+ const timeoutId = globalThis.setTimeout(() => {
44
+ savedCallback.current();
45
+ }, delay);
46
+
47
+ return () => {
48
+ globalThis.clearTimeout(timeoutId);
49
+ };
50
+ }, [delay]);
51
+ }