react-hook-toolkit 3.0.5 → 5.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.
@@ -0,0 +1,190 @@
1
+ import { MutableRefObject } from 'react';
2
+ interface UseCounterOptions {
3
+ min?: number;
4
+ max?: number;
5
+ step?: number;
6
+ }
7
+ interface UseCounterReturn {
8
+ count: number;
9
+ increment: () => void;
10
+ decrement: () => void;
11
+ reset: () => void;
12
+ set: (value: number) => void;
13
+ }
14
+ /**
15
+ * Manages a numeric counter with optional min/max bounds and a configurable step.
16
+ */
17
+ export declare function useCounter(initialValue?: number, options?: UseCounterOptions): UseCounterReturn;
18
+ interface UseMapReturn<K, V> {
19
+ map: Map<K, V>;
20
+ set: (key: K, value: V) => void;
21
+ get: (key: K) => V | undefined;
22
+ has: (key: K) => boolean;
23
+ delete: (key: K) => void;
24
+ clear: () => void;
25
+ size: number;
26
+ }
27
+ /**
28
+ * Manages a Map data structure as React state with stable action methods.
29
+ */
30
+ export declare function useMap<K, V>(initialEntries?: Iterable<[K, V]>): UseMapReturn<K, V>;
31
+ interface UseSetReturn<T> {
32
+ set: Set<T>;
33
+ add: (value: T) => void;
34
+ has: (value: T) => boolean;
35
+ delete: (value: T) => void;
36
+ toggle: (value: T) => void;
37
+ clear: () => void;
38
+ size: number;
39
+ }
40
+ /**
41
+ * Manages a Set data structure as React state with stable action methods.
42
+ */
43
+ export declare function useSet<T>(initialValues?: Iterable<T>): UseSetReturn<T>;
44
+ interface UseQueueReturn<T> {
45
+ queue: T[];
46
+ enqueue: (item: T) => void;
47
+ dequeue: () => T | undefined;
48
+ peek: () => T | undefined;
49
+ clear: () => void;
50
+ size: number;
51
+ isEmpty: boolean;
52
+ }
53
+ /**
54
+ * Manages a FIFO queue as React state.
55
+ */
56
+ export declare function useQueue<T>(initialItems?: T[]): UseQueueReturn<T>;
57
+ interface UseStackReturn<T> {
58
+ stack: T[];
59
+ push: (item: T) => void;
60
+ pop: () => T | undefined;
61
+ peek: () => T | undefined;
62
+ clear: () => void;
63
+ size: number;
64
+ isEmpty: boolean;
65
+ }
66
+ /**
67
+ * Manages a LIFO stack as React state.
68
+ */
69
+ export declare function useStack<T>(initialItems?: T[]): UseStackReturn<T>;
70
+ interface UseFocusReturn {
71
+ ref: MutableRefObject<HTMLElement | null>;
72
+ isFocused: boolean;
73
+ focus: () => void;
74
+ blur: () => void;
75
+ }
76
+ /**
77
+ * Tracks focus state of a DOM element via a ref.
78
+ */
79
+ export declare function useFocus(): UseFocusReturn;
80
+ /**
81
+ * Tracks whether a DOM element is being hovered via a ref.
82
+ * Returns a [ref, isHovered] tuple.
83
+ */
84
+ export declare function useHover<T extends HTMLElement = HTMLElement>(): [MutableRefObject<T | null>, boolean];
85
+ interface UseIntersectionObserverOptions extends IntersectionObserverInit {
86
+ freezeOnceVisible?: boolean;
87
+ }
88
+ /**
89
+ * Observes whether a ref'd element is intersecting the viewport (or a given root).
90
+ * When freezeOnceVisible is true the entry is frozen after first intersection.
91
+ */
92
+ export declare function useIntersectionObserver(elementRef: MutableRefObject<Element | null>, options?: UseIntersectionObserverOptions): IntersectionObserverEntry | undefined;
93
+ interface DOMRect {
94
+ x: number;
95
+ y: number;
96
+ width: number;
97
+ height: number;
98
+ top: number;
99
+ right: number;
100
+ bottom: number;
101
+ left: number;
102
+ }
103
+ interface UseMeasureReturn {
104
+ ref: MutableRefObject<HTMLElement | null>;
105
+ rect: DOMRect;
106
+ }
107
+ /**
108
+ * Measures the bounding rect of a DOM element, updating on resize.
109
+ */
110
+ export declare function useMeasure(): UseMeasureReturn;
111
+ interface NetworkState {
112
+ isOnline: boolean;
113
+ effectiveType: string | null;
114
+ downlink: number | null;
115
+ rtt: number | null;
116
+ saveData: boolean | null;
117
+ type: string | null;
118
+ }
119
+ /**
120
+ * Returns detailed network connection information including online status,
121
+ * effective connection type, downlink speed, and RTT.
122
+ */
123
+ export declare function useNetwork(): NetworkState;
124
+ /**
125
+ * Reads and writes the URL hash (fragment) as state.
126
+ * Returns [hash, setHash] where hash does not include the leading '#'.
127
+ */
128
+ export declare function useHash(): [string, (hash: string) => void];
129
+ /**
130
+ * Returns a ref that always holds the most recent value.
131
+ * Useful for reading the latest prop/state inside a callback without
132
+ * adding it to the dependency array.
133
+ */
134
+ export declare function useLatest<T>(value: T): MutableRefObject<T>;
135
+ /**
136
+ * Returns a stable callback reference that always delegates to the latest
137
+ * version of the provided function. Safe to pass to event listeners without
138
+ * causing unnecessary re-subscriptions.
139
+ */
140
+ export declare function useEventCallback<T extends (...args: any[]) => any>(fn: T): T;
141
+ /**
142
+ * A drop-in replacement for useState that suppresses state updates after the
143
+ * component has unmounted, preventing "Can't perform a React state update on an
144
+ * unmounted component" warnings.
145
+ */
146
+ export declare function useSafeState<T>(initialState: T | (() => T)): [T, React.Dispatch<React.SetStateAction<T>>];
147
+ /**
148
+ * Identical to useState but batches updates via requestAnimationFrame,
149
+ * reducing the number of renders for rapidly changing values (e.g. mouse
150
+ * position, scroll events).
151
+ */
152
+ export declare function useRafState<T>(initialState: T | (() => T)): [T, React.Dispatch<React.SetStateAction<T>>];
153
+ interface UseTitleOptions {
154
+ restoreOnUnmount?: boolean;
155
+ }
156
+ /**
157
+ * Sets the document title reactively. Optionally restores the previous title
158
+ * on unmount.
159
+ */
160
+ export declare function useTitle(title: string, options?: UseTitleOptions): void;
161
+ /**
162
+ * Dynamically updates the page favicon by href. Useful for notifications or
163
+ * theme changes (e.g. switching between light/dark favicon).
164
+ */
165
+ export declare function useFavicon(href: string): void;
166
+ /**
167
+ * Debug hook that logs which prop/state changes triggered the last render.
168
+ * Only active in development; no-ops in production.
169
+ * Pass a label and the component's props/state object.
170
+ */
171
+ export declare function useWhyDidYouUpdate(name: string, props: Record<string, any>): void;
172
+ interface UseFullscreenReturn {
173
+ ref: MutableRefObject<HTMLElement | null>;
174
+ isFullscreen: boolean;
175
+ enter: () => Promise<void>;
176
+ exit: () => Promise<void>;
177
+ toggle: () => Promise<void>;
178
+ isSupported: boolean;
179
+ }
180
+ /**
181
+ * Manages the Fullscreen API for a specific element (or the whole document when
182
+ * no ref target is provided). Tracks fullscreen state reactively.
183
+ */
184
+ export declare function useFullscreen(): UseFullscreenReturn;
185
+ /**
186
+ * Logs the component name and any values passed on every render.
187
+ * Only logs in development mode.
188
+ */
189
+ export declare function useLogger(name: string, ...values: any[]): void;
190
+ export {};
@@ -0,0 +1,408 @@
1
+ import { useState, useEffect, useCallback, useRef, useLayoutEffect } from 'react';
2
+ /**
3
+ * Manages a numeric counter with optional min/max bounds and a configurable step.
4
+ */
5
+ export function useCounter(initialValue = 0, options = {}) {
6
+ const { min = -Infinity, max = Infinity, step = 1 } = options;
7
+ const [count, setCount] = useState(() => Math.min(Math.max(initialValue, min), max));
8
+ const increment = useCallback(() => setCount(prev => Math.min(prev + step, max)), [max, step]);
9
+ const decrement = useCallback(() => setCount(prev => Math.max(prev - step, min)), [min, step]);
10
+ const reset = useCallback(() => setCount(Math.min(Math.max(initialValue, min), max)), [initialValue, min, max]);
11
+ const set = useCallback((value) => setCount(Math.min(Math.max(value, min), max)), [min, max]);
12
+ return { count, increment, decrement, reset, set };
13
+ }
14
+ /**
15
+ * Manages a Map data structure as React state with stable action methods.
16
+ */
17
+ export function useMap(initialEntries = []) {
18
+ const [map, setMap] = useState(() => new Map(initialEntries));
19
+ const set = useCallback((key, value) => {
20
+ setMap(prev => new Map(prev).set(key, value));
21
+ }, []);
22
+ const deleteKey = useCallback((key) => {
23
+ setMap(prev => {
24
+ const next = new Map(prev);
25
+ next.delete(key);
26
+ return next;
27
+ });
28
+ }, []);
29
+ const clear = useCallback(() => setMap(new Map()), []);
30
+ const get = useCallback((key) => map.get(key), [map]);
31
+ const has = useCallback((key) => map.has(key), [map]);
32
+ return { map, set, get, has, delete: deleteKey, clear, size: map.size };
33
+ }
34
+ /**
35
+ * Manages a Set data structure as React state with stable action methods.
36
+ */
37
+ export function useSet(initialValues = []) {
38
+ const [set, setSet] = useState(() => new Set(initialValues));
39
+ const add = useCallback((value) => {
40
+ setSet(prev => new Set(prev).add(value));
41
+ }, []);
42
+ const deleteValue = useCallback((value) => {
43
+ setSet(prev => {
44
+ const next = new Set(prev);
45
+ next.delete(value);
46
+ return next;
47
+ });
48
+ }, []);
49
+ const toggle = useCallback((value) => {
50
+ setSet(prev => {
51
+ const next = new Set(prev);
52
+ if (next.has(value))
53
+ next.delete(value);
54
+ else
55
+ next.add(value);
56
+ return next;
57
+ });
58
+ }, []);
59
+ const clear = useCallback(() => setSet(new Set()), []);
60
+ const has = useCallback((value) => set.has(value), [set]);
61
+ return { set, add, has, delete: deleteValue, toggle, clear, size: set.size };
62
+ }
63
+ /**
64
+ * Manages a FIFO queue as React state.
65
+ */
66
+ export function useQueue(initialItems = []) {
67
+ const [queue, setQueue] = useState(initialItems);
68
+ const enqueue = useCallback((item) => {
69
+ setQueue(prev => [...prev, item]);
70
+ }, []);
71
+ const dequeue = useCallback(() => {
72
+ let removed;
73
+ setQueue(prev => {
74
+ if (prev.length === 0)
75
+ return prev;
76
+ [removed] = prev;
77
+ return prev.slice(1);
78
+ });
79
+ return removed;
80
+ }, []);
81
+ const peek = useCallback(() => queue[0], [queue]);
82
+ const clear = useCallback(() => setQueue([]), []);
83
+ return { queue, enqueue, dequeue, peek, clear, size: queue.length, isEmpty: queue.length === 0 };
84
+ }
85
+ /**
86
+ * Manages a LIFO stack as React state.
87
+ */
88
+ export function useStack(initialItems = []) {
89
+ const [stack, setStack] = useState(initialItems);
90
+ const push = useCallback((item) => {
91
+ setStack(prev => [...prev, item]);
92
+ }, []);
93
+ const pop = useCallback(() => {
94
+ let removed;
95
+ setStack(prev => {
96
+ if (prev.length === 0)
97
+ return prev;
98
+ removed = prev[prev.length - 1];
99
+ return prev.slice(0, -1);
100
+ });
101
+ return removed;
102
+ }, []);
103
+ const peek = useCallback(() => stack[stack.length - 1], [stack]);
104
+ const clear = useCallback(() => setStack([]), []);
105
+ return { stack, push, pop, peek, clear, size: stack.length, isEmpty: stack.length === 0 };
106
+ }
107
+ /**
108
+ * Tracks focus state of a DOM element via a ref.
109
+ */
110
+ export function useFocus() {
111
+ const ref = useRef(null);
112
+ const [isFocused, setIsFocused] = useState(false);
113
+ useEffect(() => {
114
+ const el = ref.current;
115
+ if (!el)
116
+ return;
117
+ const onFocus = () => setIsFocused(true);
118
+ const onBlur = () => setIsFocused(false);
119
+ el.addEventListener('focus', onFocus);
120
+ el.addEventListener('blur', onBlur);
121
+ return () => {
122
+ el.removeEventListener('focus', onFocus);
123
+ el.removeEventListener('blur', onBlur);
124
+ };
125
+ });
126
+ const focus = useCallback(() => { var _a; return (_a = ref.current) === null || _a === void 0 ? void 0 : _a.focus(); }, []);
127
+ const blur = useCallback(() => { var _a; return (_a = ref.current) === null || _a === void 0 ? void 0 : _a.blur(); }, []);
128
+ return { ref, isFocused, focus, blur };
129
+ }
130
+ // ─── useHover ─────────────────────────────────────────────────────────────────
131
+ /**
132
+ * Tracks whether a DOM element is being hovered via a ref.
133
+ * Returns a [ref, isHovered] tuple.
134
+ */
135
+ export function useHover() {
136
+ const ref = useRef(null);
137
+ const [isHovered, setIsHovered] = useState(false);
138
+ useEffect(() => {
139
+ const el = ref.current;
140
+ if (!el)
141
+ return;
142
+ const onEnter = () => setIsHovered(true);
143
+ const onLeave = () => setIsHovered(false);
144
+ el.addEventListener('mouseenter', onEnter);
145
+ el.addEventListener('mouseleave', onLeave);
146
+ return () => {
147
+ el.removeEventListener('mouseenter', onEnter);
148
+ el.removeEventListener('mouseleave', onLeave);
149
+ };
150
+ });
151
+ return [ref, isHovered];
152
+ }
153
+ /**
154
+ * Observes whether a ref'd element is intersecting the viewport (or a given root).
155
+ * When freezeOnceVisible is true the entry is frozen after first intersection.
156
+ */
157
+ export function useIntersectionObserver(elementRef, options = {}) {
158
+ const { threshold = 0, root = null, rootMargin = '0%', freezeOnceVisible = false } = options;
159
+ const [entry, setEntry] = useState();
160
+ const frozen = (entry === null || entry === void 0 ? void 0 : entry.isIntersecting) && freezeOnceVisible;
161
+ useEffect(() => {
162
+ const el = elementRef === null || elementRef === void 0 ? void 0 : elementRef.current;
163
+ if (frozen || !el || !('IntersectionObserver' in window))
164
+ return;
165
+ const observer = new IntersectionObserver(([newEntry]) => setEntry(newEntry), { threshold, root, rootMargin });
166
+ observer.observe(el);
167
+ return () => observer.disconnect();
168
+ }, [elementRef, threshold, root, rootMargin, frozen]);
169
+ return entry;
170
+ }
171
+ const defaultRect = { x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0 };
172
+ /**
173
+ * Measures the bounding rect of a DOM element, updating on resize.
174
+ */
175
+ export function useMeasure() {
176
+ const ref = useRef(null);
177
+ const [rect, setRect] = useState(defaultRect);
178
+ useLayoutEffect(() => {
179
+ const el = ref.current;
180
+ if (!el)
181
+ return;
182
+ const update = () => {
183
+ const r = el.getBoundingClientRect();
184
+ setRect({ x: r.x, y: r.y, width: r.width, height: r.height, top: r.top, right: r.right, bottom: r.bottom, left: r.left });
185
+ };
186
+ update();
187
+ const observer = new ResizeObserver(update);
188
+ observer.observe(el);
189
+ return () => observer.disconnect();
190
+ }, []);
191
+ return { ref, rect };
192
+ }
193
+ /**
194
+ * Returns detailed network connection information including online status,
195
+ * effective connection type, downlink speed, and RTT.
196
+ */
197
+ export function useNetwork() {
198
+ const getState = () => {
199
+ var _a, _b, _c, _d, _e;
200
+ const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
201
+ return {
202
+ isOnline: navigator.onLine,
203
+ effectiveType: (_a = conn === null || conn === void 0 ? void 0 : conn.effectiveType) !== null && _a !== void 0 ? _a : null,
204
+ downlink: (_b = conn === null || conn === void 0 ? void 0 : conn.downlink) !== null && _b !== void 0 ? _b : null,
205
+ rtt: (_c = conn === null || conn === void 0 ? void 0 : conn.rtt) !== null && _c !== void 0 ? _c : null,
206
+ saveData: (_d = conn === null || conn === void 0 ? void 0 : conn.saveData) !== null && _d !== void 0 ? _d : null,
207
+ type: (_e = conn === null || conn === void 0 ? void 0 : conn.type) !== null && _e !== void 0 ? _e : null,
208
+ };
209
+ };
210
+ const [state, setState] = useState(getState);
211
+ useEffect(() => {
212
+ const update = () => setState(getState());
213
+ window.addEventListener('online', update);
214
+ window.addEventListener('offline', update);
215
+ const conn = navigator.connection;
216
+ conn === null || conn === void 0 ? void 0 : conn.addEventListener('change', update);
217
+ return () => {
218
+ window.removeEventListener('online', update);
219
+ window.removeEventListener('offline', update);
220
+ conn === null || conn === void 0 ? void 0 : conn.removeEventListener('change', update);
221
+ };
222
+ }, []);
223
+ return state;
224
+ }
225
+ // ─── useHash ──────────────────────────────────────────────────────────────────
226
+ /**
227
+ * Reads and writes the URL hash (fragment) as state.
228
+ * Returns [hash, setHash] where hash does not include the leading '#'.
229
+ */
230
+ export function useHash() {
231
+ const [hash, setHashState] = useState(() => typeof window !== 'undefined' ? window.location.hash.slice(1) : '');
232
+ useEffect(() => {
233
+ const onHashChange = () => setHashState(window.location.hash.slice(1));
234
+ window.addEventListener('hashchange', onHashChange);
235
+ return () => window.removeEventListener('hashchange', onHashChange);
236
+ }, []);
237
+ const setHash = useCallback((newHash) => {
238
+ const normalized = newHash.startsWith('#') ? newHash : `#${newHash}`;
239
+ window.location.hash = normalized;
240
+ }, []);
241
+ return [hash, setHash];
242
+ }
243
+ // ─── useLatest ────────────────────────────────────────────────────────────────
244
+ /**
245
+ * Returns a ref that always holds the most recent value.
246
+ * Useful for reading the latest prop/state inside a callback without
247
+ * adding it to the dependency array.
248
+ */
249
+ export function useLatest(value) {
250
+ const ref = useRef(value);
251
+ ref.current = value;
252
+ return ref;
253
+ }
254
+ // ─── useEventCallback ─────────────────────────────────────────────────────────
255
+ /**
256
+ * Returns a stable callback reference that always delegates to the latest
257
+ * version of the provided function. Safe to pass to event listeners without
258
+ * causing unnecessary re-subscriptions.
259
+ */
260
+ export function useEventCallback(fn) {
261
+ const ref = useRef(fn);
262
+ useLayoutEffect(() => {
263
+ ref.current = fn;
264
+ });
265
+ return useCallback((...args) => ref.current(...args), []);
266
+ }
267
+ // ─── useSafeState ─────────────────────────────────────────────────────────────
268
+ /**
269
+ * A drop-in replacement for useState that suppresses state updates after the
270
+ * component has unmounted, preventing "Can't perform a React state update on an
271
+ * unmounted component" warnings.
272
+ */
273
+ export function useSafeState(initialState) {
274
+ const isMounted = useRef(false);
275
+ const [state, setState] = useState(initialState);
276
+ useEffect(() => {
277
+ isMounted.current = true;
278
+ return () => { isMounted.current = false; };
279
+ }, []);
280
+ const safeSetState = useCallback((value) => {
281
+ if (isMounted.current)
282
+ setState(value);
283
+ }, []);
284
+ return [state, safeSetState];
285
+ }
286
+ // ─── useRafState ──────────────────────────────────────────────────────────────
287
+ /**
288
+ * Identical to useState but batches updates via requestAnimationFrame,
289
+ * reducing the number of renders for rapidly changing values (e.g. mouse
290
+ * position, scroll events).
291
+ */
292
+ export function useRafState(initialState) {
293
+ const frameRef = useRef(0);
294
+ const [state, setState] = useState(initialState);
295
+ const setRafState = useCallback((value) => {
296
+ cancelAnimationFrame(frameRef.current);
297
+ frameRef.current = requestAnimationFrame(() => setState(value));
298
+ }, []);
299
+ useEffect(() => () => cancelAnimationFrame(frameRef.current), []);
300
+ return [state, setRafState];
301
+ }
302
+ /**
303
+ * Sets the document title reactively. Optionally restores the previous title
304
+ * on unmount.
305
+ */
306
+ export function useTitle(title, options = {}) {
307
+ const { restoreOnUnmount = false } = options;
308
+ const originalTitle = useRef(typeof document !== 'undefined' ? document.title : '');
309
+ useEffect(() => {
310
+ if (typeof document === 'undefined')
311
+ return;
312
+ document.title = title;
313
+ return () => {
314
+ if (restoreOnUnmount) {
315
+ document.title = originalTitle.current;
316
+ }
317
+ };
318
+ }, [title, restoreOnUnmount]);
319
+ }
320
+ // ─── useFavicon ───────────────────────────────────────────────────────────────
321
+ /**
322
+ * Dynamically updates the page favicon by href. Useful for notifications or
323
+ * theme changes (e.g. switching between light/dark favicon).
324
+ */
325
+ export function useFavicon(href) {
326
+ useEffect(() => {
327
+ if (typeof document === 'undefined' || !href)
328
+ return;
329
+ let link = document.querySelector('link[rel~="icon"]');
330
+ if (!link) {
331
+ link = document.createElement('link');
332
+ link.rel = 'icon';
333
+ document.head.appendChild(link);
334
+ }
335
+ link.href = href;
336
+ }, [href]);
337
+ }
338
+ // ─── useWhyDidYouUpdate ───────────────────────────────────────────────────────
339
+ /**
340
+ * Debug hook that logs which prop/state changes triggered the last render.
341
+ * Only active in development; no-ops in production.
342
+ * Pass a label and the component's props/state object.
343
+ */
344
+ export function useWhyDidYouUpdate(name, props) {
345
+ const previousProps = useRef({});
346
+ useEffect(() => {
347
+ if (process.env.NODE_ENV !== 'production') {
348
+ const allKeys = Object.keys(Object.assign(Object.assign({}, previousProps.current), props));
349
+ const changes = {};
350
+ allKeys.forEach(key => {
351
+ if (previousProps.current[key] !== props[key]) {
352
+ changes[key] = { from: previousProps.current[key], to: props[key] };
353
+ }
354
+ });
355
+ if (Object.keys(changes).length > 0) {
356
+ console.log(`[useWhyDidYouUpdate] ${name}`, changes);
357
+ }
358
+ }
359
+ previousProps.current = props;
360
+ });
361
+ }
362
+ /**
363
+ * Manages the Fullscreen API for a specific element (or the whole document when
364
+ * no ref target is provided). Tracks fullscreen state reactively.
365
+ */
366
+ export function useFullscreen() {
367
+ const ref = useRef(null);
368
+ const [isFullscreen, setIsFullscreen] = useState(false);
369
+ const isSupported = typeof document !== 'undefined' && 'fullscreenEnabled' in document;
370
+ useEffect(() => {
371
+ const onChange = () => setIsFullscreen(!!document.fullscreenElement);
372
+ document.addEventListener('fullscreenchange', onChange);
373
+ return () => document.removeEventListener('fullscreenchange', onChange);
374
+ }, []);
375
+ const enter = useCallback(async () => {
376
+ var _a, _b;
377
+ const el = (_a = ref.current) !== null && _a !== void 0 ? _a : document.documentElement;
378
+ if (!document.fullscreenElement) {
379
+ await ((_b = el.requestFullscreen) === null || _b === void 0 ? void 0 : _b.call(el));
380
+ }
381
+ }, []);
382
+ const exit = useCallback(async () => {
383
+ var _a;
384
+ if (document.fullscreenElement) {
385
+ await ((_a = document.exitFullscreen) === null || _a === void 0 ? void 0 : _a.call(document));
386
+ }
387
+ }, []);
388
+ const toggle = useCallback(async () => {
389
+ var _a, _b, _c, _d;
390
+ if (document.fullscreenElement)
391
+ await ((_a = document.exitFullscreen) === null || _a === void 0 ? void 0 : _a.call(document));
392
+ else
393
+ await ((_d = (_c = ((_b = ref.current) !== null && _b !== void 0 ? _b : document.documentElement)).requestFullscreen) === null || _d === void 0 ? void 0 : _d.call(_c));
394
+ }, []);
395
+ return { ref, isFullscreen, enter, exit, toggle, isSupported };
396
+ }
397
+ // ─── useLogger ────────────────────────────────────────────────────────────────
398
+ /**
399
+ * Logs the component name and any values passed on every render.
400
+ * Only logs in development mode.
401
+ */
402
+ export function useLogger(name, ...values) {
403
+ useEffect(() => {
404
+ if (process.env.NODE_ENV !== 'production') {
405
+ console.log(`[${name}]`, ...values);
406
+ }
407
+ });
408
+ }