@lacspace/hooks 1.0.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/LICENSE ADDED
@@ -0,0 +1,51 @@
1
+ Lacspace Free Licence
2
+ Version 1.0, August 2026
3
+
4
+ Copyright (c) 2026 Lacspace
5
+
6
+ PREAMBLE
7
+
8
+ This software is published by Lacspace under the Lacspace Free Licence — a free,
9
+ permissive licence that lets you use this software for any purpose, including in
10
+ commercial products and services, at no cost. It grants the same freedoms as
11
+ common permissive open-source licences; the only condition is that this notice
12
+ travels with the software. The canonical, always-current text of this licence is
13
+ maintained at https://lacspace.com/licenses/lacspace-free-1.0
14
+
15
+ GRANT OF RIGHTS
16
+
17
+ Permission is hereby granted, free of charge, to any person or organisation
18
+ obtaining a copy of this software and its associated documentation and data files
19
+ (the "Software"), to deal in the Software without restriction, including without
20
+ limitation the rights to use, copy, modify, merge, publish, distribute,
21
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the
22
+ Software is furnished to do so, subject to the conditions below. These rights are
23
+ granted for any purpose, personal or commercial, and are perpetual, worldwide,
24
+ non-exclusive, and royalty-free.
25
+
26
+ CONDITIONS
27
+
28
+ The above copyright notice, this permission notice, and the name of this licence
29
+ ("Lacspace Free Licence") shall be included in all copies or substantial portions
30
+ of the Software.
31
+
32
+ TRADEMARKS
33
+
34
+ This licence does not grant permission to use the trade names, trademarks, service
35
+ marks, logos, or product names of Lacspace, except as required to reproduce the
36
+ notice above or to describe the origin of the Software in a truthful manner.
37
+
38
+ DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
42
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
43
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
44
+ AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
45
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
46
+
47
+ ---
48
+
49
+ The Lacspace Free Licence is a source-available, permissive licence and is not (as
50
+ of this version) an OSI-approved licence. In substance it grants the same freedoms
51
+ as the MIT Licence. Learn more at https://lacspace.com/licenses
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # @lacspace/hooks
2
+
3
+ **Essential, SSR-safe React hooks — everything you reach for, zero dependencies.**
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm i @lacspace/hooks
9
+ ```
10
+
11
+ `react` (`>=18`) is a **peer dependency** — you already have it in your app.
12
+
13
+ ## Usage
14
+
15
+ Persist state to `localStorage` (SSR-safe, syncs across tabs):
16
+
17
+ ```tsx
18
+ import { useLocalStorage } from "@lacspace/hooks";
19
+
20
+ function ThemeToggle() {
21
+ const [theme, setTheme] = useLocalStorage("theme", "light");
22
+ return (
23
+ <button onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}>
24
+ {theme}
25
+ </button>
26
+ );
27
+ }
28
+ ```
29
+
30
+ Debounce a search box:
31
+
32
+ ```tsx
33
+ import { useState } from "react";
34
+ import { useDebounce } from "@lacspace/hooks";
35
+
36
+ function Search() {
37
+ const [query, setQuery] = useState("");
38
+ const debounced = useDebounce(query, 300);
39
+ // ...run the query effect on `debounced`
40
+ return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
41
+ }
42
+ ```
43
+
44
+ Close a menu when clicking outside:
45
+
46
+ ```tsx
47
+ import { useRef } from "react";
48
+ import { useOnClickOutside } from "@lacspace/hooks";
49
+
50
+ function Menu({ onClose }: { onClose: () => void }) {
51
+ const ref = useRef<HTMLDivElement>(null);
52
+ useOnClickOutside(ref, onClose);
53
+ return <div ref={ref}>…</div>;
54
+ }
55
+ ```
56
+
57
+ React to a media query and copy to the clipboard:
58
+
59
+ ```tsx
60
+ import { useMediaQuery, useCopyToClipboard } from "@lacspace/hooks";
61
+
62
+ function Share({ url }: { url: string }) {
63
+ const isDark = useMediaQuery("(prefers-color-scheme: dark)");
64
+ const [copied, copy] = useCopyToClipboard();
65
+ return (
66
+ <button data-dark={isDark} onClick={() => copy(url)}>
67
+ {copied ? "Copied!" : "Copy link"}
68
+ </button>
69
+ );
70
+ }
71
+ ```
72
+
73
+ ## All hooks
74
+
75
+ | Hook | What it does |
76
+ | --- | --- |
77
+ | `useIsomorphicLayoutEffect` | `useLayoutEffect` on the client, `useEffect` on the server |
78
+ | `useIsMounted` | Stable getter for whether the component is still mounted |
79
+ | `useMountEffect` | Runs an effect once, on mount |
80
+ | `useUpdateEffect` | Like `useEffect` but skips the first run |
81
+ | `usePrevious` | The value from the previous render |
82
+ | `useLocalStorage` | JSON `localStorage` state — SSR-safe, cross-tab sync, `remove()` |
83
+ | `useSessionStorage` | Same contract, backed by `sessionStorage` |
84
+ | `useDebounce` | Debounced copy of a value |
85
+ | `useDebouncedCallback` | Debounced callback with `.cancel()` |
86
+ | `useThrottle` | Throttled copy of a value |
87
+ | `useToggle` | Boolean state with `toggle/on/off/set` |
88
+ | `useCounter` | Numeric state with `inc/dec/set/reset` |
89
+ | `useDisclosure` | Open/close state for modals, drawers, menus |
90
+ | `useInterval` | `setInterval` with a latest-callback ref; pause with `null` |
91
+ | `useTimeout` | `setTimeout` with a latest-callback ref; cancel with `null` |
92
+ | `useMediaQuery` | Tracks a CSS media query (SSR-safe) |
93
+ | `useWindowSize` | Current viewport `{ width, height }` (SSR-safe) |
94
+ | `useScrollPosition` | Current window scroll `{ x, y }` (SSR-safe) |
95
+ | `useEventListener` | Typed listener for `window`, `document`, or a ref/element |
96
+ | `useOnClickOutside` | Fires when a pointer event lands outside a ref |
97
+ | `useHover` | `[ref, hovered]` hover tracking |
98
+ | `useIntersectionObserver` | `[ref, isIntersecting, entry]` viewport observing |
99
+ | `useKeyPress` | `true` while a given key is held |
100
+ | `useCopyToClipboard` | `[copied, copy]` with a safe `execCommand` fallback |
101
+ | `useDocumentTitle` | Sets `document.title` while mounted |
102
+ | `useOnlineStatus` | Tracks online/offline (SSR-safe, `true` on server) |
103
+ | `useIdle` | `true` after N ms of no user activity |
104
+ | `useLockBodyScroll` | Locks body scroll while active, restores on cleanup |
105
+
106
+ ## Why it's tiny
107
+
108
+ - **Zero runtime dependencies** — nothing but React, which you already ship.
109
+ - **Tree-shakeable** — `sideEffects: false` and per-hook exports; bundle only what you import.
110
+ - **Isomorphic / SSR-safe** — no `window`, `document`, or storage access at module load or during render; safe under Next.js, Remix, and any server renderer.
111
+ - **Fully typed** — strict TypeScript with complete `.d.ts` for ESM and CJS.
112
+
113
+ ## Licensing
114
+
115
+ Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — MIT-equivalent freedoms. Use it in personal and commercial projects at no cost; just keep the notice. See the **[Lacspace Licence Centre](https://lacspace.com/licenses)**.
116
+
117
+ ---
118
+
119
+ **Part of the Lacspace ecosystem — zero-dependency, isomorphic TypeScript packages.**
120
+
121
+ [All packages ↗](https://lacspace.com/packages) · [npm org ↗](https://www.npmjs.com/org/lacspace) · [Licence Centre ↗](https://lacspace.com/licenses) · [GitHub ↗](https://github.com/lacspace/npm-packages)
122
+
123
+ <div align="center"><sub>Built with care by <a href="https://lacspace.com">Lacspace</a> · Lacspace Free Licence · <a href="https://github.com/lacspace/npm-packages">source</a></sub></div>
package/dist/index.cjs ADDED
@@ -0,0 +1,444 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ // src/index.ts
6
+ var isBrowser = typeof window !== "undefined";
7
+ var useIsomorphicLayoutEffect = isBrowser ? react.useLayoutEffect : react.useEffect;
8
+ function useIsMounted() {
9
+ const mounted = react.useRef(false);
10
+ react.useEffect(() => {
11
+ mounted.current = true;
12
+ return () => {
13
+ mounted.current = false;
14
+ };
15
+ }, []);
16
+ return react.useCallback(() => mounted.current, []);
17
+ }
18
+ function useMountEffect(effect) {
19
+ react.useEffect(effect, []);
20
+ }
21
+ function useUpdateEffect(effect, deps) {
22
+ const isFirst = react.useRef(true);
23
+ react.useEffect(() => {
24
+ if (isFirst.current) {
25
+ isFirst.current = false;
26
+ return;
27
+ }
28
+ return effect();
29
+ }, deps);
30
+ }
31
+ function usePrevious(value) {
32
+ const ref = react.useRef(void 0);
33
+ react.useEffect(() => {
34
+ ref.current = value;
35
+ }, [value]);
36
+ return ref.current;
37
+ }
38
+ function useStorage(storageArea, key, initialValue) {
39
+ const readValue = react.useCallback(() => {
40
+ if (!isBrowser) return initialValue;
41
+ try {
42
+ const raw = window[storageArea].getItem(key);
43
+ return raw === null ? initialValue : JSON.parse(raw);
44
+ } catch {
45
+ return initialValue;
46
+ }
47
+ }, [initialValue, key, storageArea]);
48
+ const [storedValue, setStoredValue] = react.useState(initialValue);
49
+ react.useEffect(() => {
50
+ setStoredValue(readValue());
51
+ }, [key]);
52
+ const setValue = react.useCallback(
53
+ (value) => {
54
+ setStoredValue((prev) => {
55
+ const next = value instanceof Function ? value(prev) : value;
56
+ if (isBrowser) {
57
+ try {
58
+ window[storageArea].setItem(key, JSON.stringify(next));
59
+ window.dispatchEvent(
60
+ new StorageEvent("storage", { key, newValue: JSON.stringify(next) })
61
+ );
62
+ } catch {
63
+ }
64
+ }
65
+ return next;
66
+ });
67
+ },
68
+ [key, storageArea]
69
+ );
70
+ const remove = react.useCallback(() => {
71
+ if (isBrowser) {
72
+ try {
73
+ window[storageArea].removeItem(key);
74
+ window.dispatchEvent(new StorageEvent("storage", { key, newValue: null }));
75
+ } catch {
76
+ }
77
+ }
78
+ setStoredValue(initialValue);
79
+ }, [initialValue, key, storageArea]);
80
+ react.useEffect(() => {
81
+ if (!isBrowser) return;
82
+ const onStorage = (e) => {
83
+ if (e.key !== null && e.key !== key) return;
84
+ setStoredValue(readValue());
85
+ };
86
+ window.addEventListener("storage", onStorage);
87
+ return () => window.removeEventListener("storage", onStorage);
88
+ }, [key, readValue]);
89
+ return [storedValue, setValue, remove];
90
+ }
91
+ function useLocalStorage(key, initialValue) {
92
+ return useStorage("localStorage", key, initialValue);
93
+ }
94
+ function useSessionStorage(key, initialValue) {
95
+ return useStorage("sessionStorage", key, initialValue);
96
+ }
97
+ function useDebounce(value, delayMs) {
98
+ const [debounced, setDebounced] = react.useState(value);
99
+ react.useEffect(() => {
100
+ const id = setTimeout(() => setDebounced(value), delayMs);
101
+ return () => clearTimeout(id);
102
+ }, [value, delayMs]);
103
+ return debounced;
104
+ }
105
+ function useDebouncedCallback(fn, delayMs) {
106
+ const fnRef = react.useRef(fn);
107
+ useIsomorphicLayoutEffect(() => {
108
+ fnRef.current = fn;
109
+ }, [fn]);
110
+ const timeout = react.useRef(null);
111
+ const cancel = react.useCallback(() => {
112
+ if (timeout.current !== null) {
113
+ clearTimeout(timeout.current);
114
+ timeout.current = null;
115
+ }
116
+ }, []);
117
+ react.useEffect(() => cancel, [cancel]);
118
+ const debounced = react.useCallback(
119
+ (...args) => {
120
+ if (timeout.current !== null) clearTimeout(timeout.current);
121
+ timeout.current = setTimeout(() => {
122
+ timeout.current = null;
123
+ fnRef.current(...args);
124
+ }, delayMs);
125
+ },
126
+ [delayMs]
127
+ );
128
+ debounced.cancel = cancel;
129
+ return debounced;
130
+ }
131
+ function useThrottle(value, ms) {
132
+ const [throttled, setThrottled] = react.useState(value);
133
+ const lastRan = react.useRef(isBrowser ? Date.now() : 0);
134
+ react.useEffect(() => {
135
+ const remaining = ms - (Date.now() - lastRan.current);
136
+ if (remaining <= 0) {
137
+ setThrottled(value);
138
+ lastRan.current = Date.now();
139
+ return;
140
+ }
141
+ const id = setTimeout(() => {
142
+ setThrottled(value);
143
+ lastRan.current = Date.now();
144
+ }, remaining);
145
+ return () => clearTimeout(id);
146
+ }, [value, ms]);
147
+ return throttled;
148
+ }
149
+ function useInterval(callback, delayMs) {
150
+ const saved = react.useRef(callback);
151
+ useIsomorphicLayoutEffect(() => {
152
+ saved.current = callback;
153
+ }, [callback]);
154
+ react.useEffect(() => {
155
+ if (delayMs === null) return;
156
+ const id = setInterval(() => saved.current(), delayMs);
157
+ return () => clearInterval(id);
158
+ }, [delayMs]);
159
+ }
160
+ function useTimeout(callback, delayMs) {
161
+ const saved = react.useRef(callback);
162
+ useIsomorphicLayoutEffect(() => {
163
+ saved.current = callback;
164
+ }, [callback]);
165
+ react.useEffect(() => {
166
+ if (delayMs === null) return;
167
+ const id = setTimeout(() => saved.current(), delayMs);
168
+ return () => clearTimeout(id);
169
+ }, [delayMs]);
170
+ }
171
+ function useToggle(initial = false) {
172
+ const [value, setValue] = react.useState(initial);
173
+ const toggle = react.useCallback(() => setValue((v) => !v), []);
174
+ const on = react.useCallback(() => setValue(true), []);
175
+ const off = react.useCallback(() => setValue(false), []);
176
+ const set = react.useCallback((v) => setValue(v), []);
177
+ return [value, { toggle, on, off, set }];
178
+ }
179
+ function useCounter(initial = 0) {
180
+ const [count, setCount] = react.useState(initial);
181
+ const inc = react.useCallback((step = 1) => setCount((c) => c + step), []);
182
+ const dec = react.useCallback((step = 1) => setCount((c) => c - step), []);
183
+ const set = react.useCallback((n) => setCount(n), []);
184
+ const reset = react.useCallback(() => setCount(initial), [initial]);
185
+ return { count, inc, dec, set, reset };
186
+ }
187
+ function useDisclosure(initial = false) {
188
+ const [isOpen, setOpen] = react.useState(initial);
189
+ const open = react.useCallback(() => setOpen(true), []);
190
+ const close = react.useCallback(() => setOpen(false), []);
191
+ const toggle = react.useCallback(() => setOpen((v) => !v), []);
192
+ return { isOpen, open, close, toggle };
193
+ }
194
+ function useMediaQuery(query) {
195
+ const [matches, setMatches] = react.useState(false);
196
+ react.useEffect(() => {
197
+ if (!isBrowser || typeof window.matchMedia !== "function") return;
198
+ const mql = window.matchMedia(query);
199
+ const onChange = () => setMatches(mql.matches);
200
+ onChange();
201
+ mql.addEventListener("change", onChange);
202
+ return () => mql.removeEventListener("change", onChange);
203
+ }, [query]);
204
+ return matches;
205
+ }
206
+ function useWindowSize() {
207
+ const [size, setSize] = react.useState({
208
+ width: 0,
209
+ height: 0
210
+ });
211
+ react.useEffect(() => {
212
+ if (!isBrowser) return;
213
+ const onResize = () => setSize({ width: window.innerWidth, height: window.innerHeight });
214
+ onResize();
215
+ window.addEventListener("resize", onResize);
216
+ return () => window.removeEventListener("resize", onResize);
217
+ }, []);
218
+ return size;
219
+ }
220
+ function useScrollPosition() {
221
+ const [pos, setPos] = react.useState({ x: 0, y: 0 });
222
+ react.useEffect(() => {
223
+ if (!isBrowser) return;
224
+ const onScroll = () => setPos({ x: window.scrollX, y: window.scrollY });
225
+ onScroll();
226
+ window.addEventListener("scroll", onScroll, { passive: true });
227
+ return () => window.removeEventListener("scroll", onScroll);
228
+ }, []);
229
+ return pos;
230
+ }
231
+ function useEventListener(type, handler, element, options) {
232
+ const savedHandler = react.useRef(handler);
233
+ useIsomorphicLayoutEffect(() => {
234
+ savedHandler.current = handler;
235
+ }, [handler]);
236
+ react.useEffect(() => {
237
+ if (!isBrowser) return;
238
+ const target = element == null ? window : "current" in element ? element.current : element;
239
+ if (target == null || typeof target.addEventListener !== "function") return;
240
+ const listener = (event) => savedHandler.current(event);
241
+ target.addEventListener(type, listener, options);
242
+ return () => target.removeEventListener(type, listener, options);
243
+ }, [type, element, options]);
244
+ }
245
+ function useOnClickOutside(ref, handler) {
246
+ const savedHandler = react.useRef(handler);
247
+ useIsomorphicLayoutEffect(() => {
248
+ savedHandler.current = handler;
249
+ }, [handler]);
250
+ react.useEffect(() => {
251
+ if (!isBrowser) return;
252
+ const listener = (event) => {
253
+ const el = ref.current;
254
+ const target = event.target;
255
+ if (el == null || target == null || el.contains(target)) return;
256
+ savedHandler.current(event);
257
+ };
258
+ document.addEventListener("mousedown", listener);
259
+ document.addEventListener("touchstart", listener, { passive: true });
260
+ return () => {
261
+ document.removeEventListener("mousedown", listener);
262
+ document.removeEventListener("touchstart", listener);
263
+ };
264
+ }, [ref]);
265
+ }
266
+ function useHover() {
267
+ const ref = react.useRef(null);
268
+ const [hovered, setHovered] = react.useState(false);
269
+ react.useEffect(() => {
270
+ const node = ref.current;
271
+ if (node == null) return;
272
+ const onEnter = () => setHovered(true);
273
+ const onLeave = () => setHovered(false);
274
+ node.addEventListener("mouseenter", onEnter);
275
+ node.addEventListener("mouseleave", onLeave);
276
+ return () => {
277
+ node.removeEventListener("mouseenter", onEnter);
278
+ node.removeEventListener("mouseleave", onLeave);
279
+ };
280
+ });
281
+ return [ref, hovered];
282
+ }
283
+ function useIntersectionObserver(options) {
284
+ const ref = react.useRef(null);
285
+ const [entry, setEntry] = react.useState(null);
286
+ const { root, rootMargin, threshold } = options ?? {};
287
+ react.useEffect(() => {
288
+ const node = ref.current;
289
+ if (!isBrowser || typeof IntersectionObserver !== "function" || node == null) {
290
+ return;
291
+ }
292
+ const observer = new IntersectionObserver(
293
+ (entries) => {
294
+ const first = entries[0];
295
+ if (first) setEntry(first);
296
+ },
297
+ { root, rootMargin, threshold }
298
+ );
299
+ observer.observe(node);
300
+ return () => observer.disconnect();
301
+ }, [root, rootMargin, JSON.stringify(threshold)]);
302
+ return [ref, (entry == null ? void 0 : entry.isIntersecting) ?? false, entry];
303
+ }
304
+ function useKeyPress(targetKey) {
305
+ const [pressed, setPressed] = react.useState(false);
306
+ react.useEffect(() => {
307
+ if (!isBrowser) return;
308
+ const down = (e) => {
309
+ if (e.key === targetKey) setPressed(true);
310
+ };
311
+ const up = (e) => {
312
+ if (e.key === targetKey) setPressed(false);
313
+ };
314
+ window.addEventListener("keydown", down);
315
+ window.addEventListener("keyup", up);
316
+ return () => {
317
+ window.removeEventListener("keydown", down);
318
+ window.removeEventListener("keyup", up);
319
+ };
320
+ }, [targetKey]);
321
+ return pressed;
322
+ }
323
+ function useCopyToClipboard() {
324
+ const [copied, setCopied] = react.useState(null);
325
+ const copy = react.useCallback(async (text) => {
326
+ if (!isBrowser) return false;
327
+ try {
328
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
329
+ await navigator.clipboard.writeText(text);
330
+ setCopied(text);
331
+ return true;
332
+ }
333
+ } catch {
334
+ }
335
+ try {
336
+ const el = document.createElement("textarea");
337
+ el.value = text;
338
+ el.setAttribute("readonly", "");
339
+ el.style.position = "absolute";
340
+ el.style.left = "-9999px";
341
+ document.body.appendChild(el);
342
+ el.select();
343
+ const ok = document.execCommand("copy");
344
+ document.body.removeChild(el);
345
+ if (ok) {
346
+ setCopied(text);
347
+ return true;
348
+ }
349
+ } catch {
350
+ }
351
+ setCopied(null);
352
+ return false;
353
+ }, []);
354
+ return [copied, copy];
355
+ }
356
+ function useDocumentTitle(title) {
357
+ useIsomorphicLayoutEffect(() => {
358
+ if (!isBrowser) return;
359
+ document.title = title;
360
+ }, [title]);
361
+ }
362
+ function useOnlineStatus() {
363
+ const [online, setOnline] = react.useState(true);
364
+ react.useEffect(() => {
365
+ if (!isBrowser) return;
366
+ const update = () => setOnline(navigator.onLine);
367
+ update();
368
+ window.addEventListener("online", update);
369
+ window.addEventListener("offline", update);
370
+ return () => {
371
+ window.removeEventListener("online", update);
372
+ window.removeEventListener("offline", update);
373
+ };
374
+ }, []);
375
+ return online;
376
+ }
377
+ function useIdle(ms) {
378
+ const [idle, setIdle] = react.useState(false);
379
+ react.useEffect(() => {
380
+ if (!isBrowser) return;
381
+ let timer;
382
+ const reset = () => {
383
+ setIdle(false);
384
+ clearTimeout(timer);
385
+ timer = setTimeout(() => setIdle(true), ms);
386
+ };
387
+ const events = [
388
+ "mousemove",
389
+ "mousedown",
390
+ "keydown",
391
+ "touchstart",
392
+ "scroll",
393
+ "wheel"
394
+ ];
395
+ events.forEach((e) => window.addEventListener(e, reset, { passive: true }));
396
+ reset();
397
+ return () => {
398
+ clearTimeout(timer);
399
+ events.forEach((e) => window.removeEventListener(e, reset));
400
+ };
401
+ }, [ms]);
402
+ return idle;
403
+ }
404
+ function useLockBodyScroll(locked = true) {
405
+ useIsomorphicLayoutEffect(() => {
406
+ if (!isBrowser || !locked) return;
407
+ const original = document.body.style.overflow;
408
+ document.body.style.overflow = "hidden";
409
+ return () => {
410
+ document.body.style.overflow = original;
411
+ };
412
+ }, [locked]);
413
+ }
414
+
415
+ exports.useCopyToClipboard = useCopyToClipboard;
416
+ exports.useCounter = useCounter;
417
+ exports.useDebounce = useDebounce;
418
+ exports.useDebouncedCallback = useDebouncedCallback;
419
+ exports.useDisclosure = useDisclosure;
420
+ exports.useDocumentTitle = useDocumentTitle;
421
+ exports.useEventListener = useEventListener;
422
+ exports.useHover = useHover;
423
+ exports.useIdle = useIdle;
424
+ exports.useIntersectionObserver = useIntersectionObserver;
425
+ exports.useInterval = useInterval;
426
+ exports.useIsMounted = useIsMounted;
427
+ exports.useIsomorphicLayoutEffect = useIsomorphicLayoutEffect;
428
+ exports.useKeyPress = useKeyPress;
429
+ exports.useLocalStorage = useLocalStorage;
430
+ exports.useLockBodyScroll = useLockBodyScroll;
431
+ exports.useMediaQuery = useMediaQuery;
432
+ exports.useMountEffect = useMountEffect;
433
+ exports.useOnClickOutside = useOnClickOutside;
434
+ exports.useOnlineStatus = useOnlineStatus;
435
+ exports.usePrevious = usePrevious;
436
+ exports.useScrollPosition = useScrollPosition;
437
+ exports.useSessionStorage = useSessionStorage;
438
+ exports.useThrottle = useThrottle;
439
+ exports.useTimeout = useTimeout;
440
+ exports.useToggle = useToggle;
441
+ exports.useUpdateEffect = useUpdateEffect;
442
+ exports.useWindowSize = useWindowSize;
443
+ //# sourceMappingURL=index.cjs.map
444
+ //# sourceMappingURL=index.cjs.map