UNPKG

nextjs-app-hooks

Version:

A library of custom React hooks for simplified state management and streamlined component logic.

1,714 lines (1,702 loc) 126 kB
// src/hooks/useIsBrowser.ts import { useState, useEffect } from "react"; function useIsBrowser() { const [isBrowser, setIsBrowser] = useState(false); useEffect(() => { setIsBrowser(true); }, []); return isBrowser; } // src/hooks/useIsServer.ts import { useState as useState2, useEffect as useEffect2 } from "react"; function useIsServer() { const [isServer, setIsServer] = useState2(true); useEffect2(() => { setIsServer(false); }, []); return isServer; } // src/hooks/useBattery.ts import { useState as useState3, useEffect as useEffect3 } from "react"; function useBattery() { const [state, setState] = useState3({ isSupported: false, isLoading: true, error: null, battery: null }); useEffect3(() => { if (!navigator || !("getBattery" in navigator)) { setState({ isSupported: false, isLoading: false, error: null, battery: null }); return; } let batteryManager = null; let isComponentMounted = true; const updateBatteryState = (manager) => { if (!isComponentMounted) return; if (!manager) { setState((prev) => ({ ...prev, isLoading: false, error: new Error("Could not access battery information"), battery: null })); return; } setState({ isSupported: true, isLoading: false, error: null, battery: { charging: manager.charging, chargingTime: manager.chargingTime, dischargingTime: manager.dischargingTime, level: manager.level, connected: manager.charging || manager.level === 1 && manager.chargingTime === 0 } }); }; const setupBatteryListeners = (manager) => { if (!manager) return; manager.addEventListener( "chargingchange", () => updateBatteryState(manager) ); manager.addEventListener( "chargingtimechange", () => updateBatteryState(manager) ); manager.addEventListener( "dischargingtimechange", () => updateBatteryState(manager) ); manager.addEventListener( "levelchange", () => updateBatteryState(manager) ); batteryManager = manager; }; const getBatteryInfo = async () => { try { const manager = await navigator.getBattery(); if (isComponentMounted) { updateBatteryState(manager); setupBatteryListeners(manager); } } catch (error) { if (isComponentMounted) { setState({ isSupported: true, // API exists but there was an error isLoading: false, error: error instanceof Error ? error : new Error("Unknown battery error"), battery: null }); } } }; getBatteryInfo(); return () => { isComponentMounted = false; if (batteryManager) { batteryManager.removeEventListener( "chargingchange", updateBatteryState ); batteryManager.removeEventListener( "chargingtimechange", updateBatteryState ); batteryManager.removeEventListener( "dischargingtimechange", updateBatteryState ); batteryManager.removeEventListener("levelchange", updateBatteryState); } }; }, []); return state; } // src/hooks/useClickOutside.ts import { useEffect as useEffect4, useRef, useCallback } from "react"; import { useState as useState4 } from "react"; function useClickOutside(onClickOutside, options = {}) { const { enabled = true, ignoreRefs = [], mouseEvents = ["mousedown"], listenForTouchEvents = true, ignoreScrollbar = true } = options; const ref = useRef(null); const handleClickOutside = useCallback( (event) => { if (!enabled) return; const target = event.target; if (ignoreScrollbar && event instanceof MouseEvent) { if (event.clientX >= document.documentElement.clientWidth) { return; } } if (ref.current && ref.current.contains(target)) { return; } for (const ignoreRef of ignoreRefs) { if (ignoreRef.current && ignoreRef.current.contains(target)) { return; } } onClickOutside(event); }, [enabled, ignoreRefs, ignoreScrollbar, onClickOutside] ); useEffect4(() => { if (!enabled) return; mouseEvents.forEach((mouseEvent) => { document.addEventListener( mouseEvent, handleClickOutside ); }); if (listenForTouchEvents) { document.addEventListener( "touchstart", handleClickOutside ); } return () => { mouseEvents.forEach((mouseEvent) => { document.removeEventListener( mouseEvent, handleClickOutside ); }); if (listenForTouchEvents) { document.removeEventListener( "touchstart", handleClickOutside ); } }; }, [enabled, mouseEvents, listenForTouchEvents, handleClickOutside]); return ref; } function useClickOutsideState(initialState = false, options = {}) { const [isOpen, setIsOpen] = useState4(initialState); const ref = useClickOutside( () => { if (isOpen) setIsOpen(false); }, { ...options, enabled: options.enabled !== false && isOpen } ); return [ref, isOpen, setIsOpen]; } // src/hooks/useClipboard.ts import { useState as useState5, useCallback as useCallback2, useEffect as useEffect5 } from "react"; function useClipboard(options = {}) { const { resetDelay = 2e3, autoReset = true, initialValue = "", modernOnly = false, onCopySuccess, onCopyError } = options; const isBrowser = useIsBrowser(); const [value, setValue] = useState5(initialValue); const [status, setStatus] = useState5("idle"); const [error, setError] = useState5(null); const isModernSupported = isBrowser && typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function"; const isLegacySupported = isBrowser && typeof document !== "undefined" && typeof document.execCommand === "function"; const isSupported = isModernSupported || !modernOnly && isLegacySupported; useEffect5(() => { if (status !== "idle" && autoReset) { const timeoutId = setTimeout(() => { setStatus("idle"); }, resetDelay); return () => clearTimeout(timeoutId); } }, [status, resetDelay, autoReset]); const copyWithLegacyMethod = useCallback2((text) => { try { const textarea = document.createElement("textarea"); textarea.value = text; textarea.style.position = "absolute"; textarea.style.opacity = "0"; textarea.style.pointerEvents = "none"; textarea.style.zIndex = "-1"; document.body.appendChild(textarea); textarea.focus(); textarea.select(); const success = document.execCommand("copy"); document.body.removeChild(textarea); return success; } catch (err) { return false; } }, []); const copyToClipboard = useCallback2( async (text) => { if (!isBrowser) { setStatus("error"); const browserError = new Error( "Cannot copy to clipboard on the server" ); setError(browserError); onCopyError?.(browserError); return false; } const valueToCopy = text !== void 0 ? text : value; setValue(valueToCopy); try { let success = false; if (isModernSupported) { await navigator.clipboard.writeText(valueToCopy); success = true; } else if (!modernOnly && isLegacySupported) { success = copyWithLegacyMethod(valueToCopy); } else { throw new Error("Clipboard API not supported in this environment"); } if (success) { setStatus("copied"); setError(null); onCopySuccess?.(valueToCopy); return true; } else { throw new Error("Failed to copy to clipboard"); } } catch (err) { setStatus("error"); const copyError = err instanceof Error ? err : new Error(String(err)); setError(copyError); onCopyError?.(copyError); return false; } }, [ isBrowser, value, isModernSupported, modernOnly, isLegacySupported, copyWithLegacyMethod, onCopySuccess, onCopyError ] ); const reset = useCallback2(() => { setStatus("idle"); setError(null); }, []); return { copyToClipboard, value, status, isSuccess: status === "copied", isError: status === "error", reset, error, isSupported }; } // src/hooks/useDebounce.ts import { useEffect as useEffect6, useRef as useRef2, useState as useState6, useCallback as useCallback3 } from "react"; function useDebounce(fn, options = {}) { const { delay = 500, leading = false, maxWait, trackPending = false } = options; const timeoutRef = useRef2(null); const lastCalledRef = useRef2(0); const maxTimeoutRef = useRef2(null); const lastArgsRef = useRef2(null); const lastResultRef = useRef2(void 0); const [isPending, setIsPending] = useState6(false); const cancel = useCallback3(() => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } if (maxTimeoutRef.current) { clearTimeout(maxTimeoutRef.current); maxTimeoutRef.current = null; } if (trackPending && isPending) { setIsPending(false); } }, [isPending, trackPending]); const executeFunction = useCallback3( (...args) => { lastCalledRef.current = Date.now(); lastArgsRef.current = null; if (trackPending) { setIsPending(false); } try { const result = fn(...args); lastResultRef.current = result; return result; } catch (error) { lastResultRef.current = void 0; throw error; } }, [fn, trackPending] ); const flush = useCallback3(() => { if (timeoutRef.current || maxTimeoutRef.current) { if (lastArgsRef.current) { const result = executeFunction(...lastArgsRef.current); lastArgsRef.current = null; return result; } } return lastResultRef.current; }, [executeFunction]); useEffect6(() => { return () => cancel(); }, [cancel]); const debounced = useCallback3( (...args) => { lastArgsRef.current = args; const now = Date.now(); const timeSinceLastCall = now - lastCalledRef.current; if (trackPending && !isPending) { setIsPending(true); } if (timeoutRef.current) { clearTimeout(timeoutRef.current); } if (leading && (lastCalledRef.current === 0 || timeSinceLastCall >= delay)) { executeFunction(...args); } else { timeoutRef.current = setTimeout(() => { if (maxTimeoutRef.current) { clearTimeout(maxTimeoutRef.current); maxTimeoutRef.current = null; } executeFunction(...args); }, delay); if (maxWait && !maxTimeoutRef.current && timeSinceLastCall >= maxWait) { maxTimeoutRef.current = setTimeout(() => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } executeFunction(...args); }, maxWait - timeSinceLastCall); } } }, [delay, executeFunction, isPending, leading, maxWait, trackPending] ); return [debounced, cancel, isPending, flush]; } // src/hooks/useGeolocation.ts import { useState as useState7, useEffect as useEffect7, useCallback as useCallback4, useRef as useRef3 } from "react"; function useGeolocation(options = {}) { const { enableHighAccuracy = false, timeout = 1e4, maximumAge = 0, watchPosition: shouldWatch = false, autoRequest = true, onSuccess, onError, onPermissionChange } = options; const isBrowser = useIsBrowser(); const [position, setPosition] = useState7(null); const [error, setError] = useState7( null ); const [isLoading, setIsLoading] = useState7(false); const [isWatching, setIsWatching] = useState7(false); const [permissionState, setPermissionState] = useState7("unknown"); const watchIdRef = useRef3(null); const isSupported = isBrowser && typeof navigator !== "undefined" && "geolocation" in navigator; const formatPosition = useCallback4( (position2) => { return { latitude: position2.latitude, longitude: position2.longitude, accuracy: position2.accuracy, altitude: position2.altitude, altitudeAccuracy: position2.altitudeAccuracy, heading: position2.heading, speed: position2.speed, timestamp: position2.timestamp }; }, [] ); const handlePositionSuccess = useCallback4( (rawPosition) => { const formattedPosition = formatPosition(rawPosition); setPosition(formattedPosition); setError(null); setIsLoading(false); onSuccess?.(formattedPosition); }, [formatPosition, onSuccess] ); const handlePositionError = useCallback4( (positionError) => { setError(positionError); setIsLoading(false); onError?.(positionError); if (positionError instanceof GeolocationPositionError && positionError.code === 1 /* PERMISSION_DENIED */) { setPermissionState("denied"); onPermissionChange?.("denied"); } }, [onError, onPermissionChange] ); const getPosition = useCallback4(() => { return new Promise((resolve, reject) => { if (!isSupported) { const error2 = new Error( "Geolocation is not supported by this browser." ); setError(error2); reject(error2); return; } setIsLoading(true); setError(null); navigator.geolocation.getCurrentPosition( (rawPosition) => { const formattedPosition = { latitude: rawPosition.coords.latitude, longitude: rawPosition.coords.longitude, accuracy: rawPosition.coords.accuracy, altitude: rawPosition.coords.altitude, altitudeAccuracy: rawPosition.coords.altitudeAccuracy, heading: rawPosition.coords.heading, speed: rawPosition.coords.speed, timestamp: rawPosition.timestamp }; handlePositionSuccess(formattedPosition); resolve(formattedPosition); }, (positionError) => { handlePositionError(positionError); reject(positionError); }, { enableHighAccuracy, timeout, maximumAge } ); }); }, [ isSupported, enableHighAccuracy, timeout, maximumAge, handlePositionSuccess, handlePositionError ]); const watchPosition = useCallback4(() => { if (!isSupported) { const error2 = new Error("Geolocation is not supported by this browser."); setError(error2); return; } if (watchIdRef.current !== null) { navigator.geolocation.clearWatch(watchIdRef.current); } setIsLoading(true); setError(null); setIsWatching(true); watchIdRef.current = navigator.geolocation.watchPosition( (rawPosition) => { const formattedPosition = { latitude: rawPosition.coords.latitude, longitude: rawPosition.coords.longitude, accuracy: rawPosition.coords.accuracy, altitude: rawPosition.coords.altitude, altitudeAccuracy: rawPosition.coords.altitudeAccuracy, heading: rawPosition.coords.heading, speed: rawPosition.coords.speed, timestamp: rawPosition.timestamp }; handlePositionSuccess(formattedPosition); }, handlePositionError, { enableHighAccuracy, timeout, maximumAge } ); }, [ isSupported, enableHighAccuracy, timeout, maximumAge, handlePositionSuccess, handlePositionError ]); const stopWatching = useCallback4(() => { if (!isSupported || watchIdRef.current === null) return; navigator.geolocation.clearWatch(watchIdRef.current); watchIdRef.current = null; setIsWatching(false); }, [isSupported]); const checkPermissionState = useCallback4(async () => { if (!isBrowser) return "unknown"; if (navigator.permissions && navigator.permissions.query) { try { const permissionStatus = await navigator.permissions.query({ name: "geolocation" }); const state = permissionStatus.state; setPermissionState(state); onPermissionChange?.(state); permissionStatus.addEventListener("change", () => { const newState = permissionStatus.state; setPermissionState(newState); onPermissionChange?.(newState); }); return state; } catch (err) { console.warn("Permissions API error:", err); } } if (position) { setPermissionState("granted"); return "granted"; } else if (error instanceof GeolocationPositionError && error.code === 1 /* PERMISSION_DENIED */) { setPermissionState("denied"); return "denied"; } return "prompt"; }, [isBrowser, position, error, onPermissionChange]); useEffect7(() => { if (isSupported) { checkPermissionState(); } }, [isSupported, checkPermissionState]); useEffect7(() => { if (!isSupported || !autoRequest) return; if (shouldWatch) { watchPosition(); } else { getPosition().catch(() => { }); } return () => { if (watchIdRef.current !== null) { navigator.geolocation.clearWatch(watchIdRef.current); watchIdRef.current = null; setIsWatching(false); } }; }, [isSupported, autoRequest, shouldWatch, watchPosition, getPosition]); return { position, error, isLoading, isWatching, permissionState, getPosition, watchPosition, stopWatching, isSupported }; } // src/hooks/useHover.ts import { useState as useState8, useRef as useRef4, useEffect as useEffect8, useCallback as useCallback5 } from "react"; function useHover(options = {}) { const { enterDelay = 0, leaveDelay = 0, enabled: initialEnabled = true, supportTouch = false, onHoverStart, onHoverEnd, onEnabledChange } = options; const isBrowser = useIsBrowser(); const [isHovered, setIsHovered] = useState8(false); const [isEnabled, setIsEnabled] = useState8(initialEnabled); const hoverRef = useRef4(null); const enterTimeoutRef = useRef4(null); const leaveTimeoutRef = useRef4(null); const isTouchDevice = useRef4(false); const clearTimeouts = useCallback5(() => { if (enterTimeoutRef.current) { clearTimeout(enterTimeoutRef.current); enterTimeoutRef.current = null; } if (leaveTimeoutRef.current) { clearTimeout(leaveTimeoutRef.current); leaveTimeoutRef.current = null; } }, []); const handleEnter = useCallback5( (event) => { if (event.type.startsWith("mouse") && isTouchDevice.current) { return; } if (event.type.startsWith("touch")) { isTouchDevice.current = true; } if (leaveTimeoutRef.current) { clearTimeout(leaveTimeoutRef.current); leaveTimeoutRef.current = null; } if (enterDelay > 0) { enterTimeoutRef.current = setTimeout(() => { setIsHovered(true); onHoverStart?.(event); }, enterDelay); } else { setIsHovered(true); onHoverStart?.(event); } }, [enterDelay, onHoverStart] ); const handleLeave = useCallback5( (event) => { if (event.type.startsWith("mouse") && isTouchDevice.current) { return; } if (enterTimeoutRef.current) { clearTimeout(enterTimeoutRef.current); enterTimeoutRef.current = null; } if (leaveDelay > 0) { leaveTimeoutRef.current = setTimeout(() => { setIsHovered(false); onHoverEnd?.(event); }, leaveDelay); } else { setIsHovered(false); onHoverEnd?.(event); } }, [leaveDelay, onHoverEnd] ); const enable = useCallback5(() => { if (!isEnabled) { setIsEnabled(true); onEnabledChange?.(true); } }, [isEnabled, onEnabledChange]); const disable = useCallback5(() => { if (isEnabled) { setIsEnabled(false); setIsHovered(false); clearTimeouts(); onEnabledChange?.(false); } }, [isEnabled, clearTimeouts, onEnabledChange]); useEffect8(() => { const element = hoverRef.current; if (!isBrowser || !element || !isEnabled) { return; } if (typeof window !== "undefined") { isTouchDevice.current = "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; } element.addEventListener("mouseenter", handleEnter); element.addEventListener("mouseleave", handleLeave); if (supportTouch) { element.addEventListener("touchstart", handleEnter, { passive: true }); element.addEventListener("touchend", handleLeave, { passive: true }); const handleTouchMove = (event) => { const touch = event.touches[0]; if (touch && element) { const rect = element.getBoundingClientRect(); if (touch.clientX < rect.left || touch.clientX > rect.right || touch.clientY < rect.top || touch.clientY > rect.bottom) { handleLeave(event); } } }; document.addEventListener("touchmove", handleTouchMove, { passive: true }); return () => { element.removeEventListener("mouseenter", handleEnter); element.removeEventListener("mouseleave", handleLeave); element.removeEventListener("touchstart", handleEnter); element.removeEventListener("touchend", handleLeave); document.removeEventListener( "touchmove", handleTouchMove ); clearTimeouts(); }; } return () => { element.removeEventListener("mouseenter", handleEnter); element.removeEventListener("mouseleave", handleLeave); clearTimeouts(); }; }, [ isBrowser, isEnabled, supportTouch, handleEnter, handleLeave, clearTimeouts ]); useEffect8(() => { if (initialEnabled !== isEnabled) { setIsEnabled(initialEnabled); } }, [initialEnabled, isEnabled]); useEffect8(() => { return () => { clearTimeouts(); }; }, [clearTimeouts]); return { hoverRef, isHovered, enable, disable, isEnabled }; } // src/hooks/useIdle.ts import { useState as useState9, useEffect as useEffect9, useCallback as useCallback6, useRef as useRef5 } from "react"; function useIdle(options = {}) { const { idleTime = 6e4, // Default 1 minute initialState = false, events = [ "mousemove", "mousedown", "resize", "keydown", "touchstart", "wheel" ], minimumMovement = 10, trackInBackground = true, onIdle, onActive, enabled: initialEnabled = true, idleOnVisibilityHidden = false, syncWithStorage = false, storageKey = "user-idle-state", emitEvents = false, eventName = "user-idle-state-change", pollingInterval = 1e3 } = options; const isBrowser = useIsBrowser(); const [isIdle, setIsIdle] = useState9(initialState); const [isEnabled, setIsEnabled] = useState9(initialEnabled); const [idleTimeTracked, setIdleTimeTracked] = useState9(0); const [remainingTimeValue, setRemainingTimeValue] = useState9(idleTime); const lastActiveRef = useRef5(Date.now()); const timerRef = useRef5(null); const pollingIntervalRef = useRef5(null); const lastMousePositionRef = useRef5({ x: 0, y: 0 }); const lastEventHandlerRef = useRef5(() => { }); const initTimer = useCallback6(() => { if (!isBrowser || !isEnabled) return; if (timerRef.current) { clearTimeout(timerRef.current); } timerRef.current = setTimeout(() => { if (!document.hidden || trackInBackground) { setIsIdle(true); onIdle?.(); if (syncWithStorage) { try { localStorage.setItem( storageKey, JSON.stringify({ isIdle: true, timestamp: Date.now() }) ); } catch (e) { console.warn("Failed to sync idle state to localStorage", e); } } if (emitEvents && typeof window !== "undefined") { const idleEvent = new CustomEvent(eventName, { detail: { isIdle: true, idleTime: Date.now() - lastActiveRef.current, timestamp: Date.now() } }); window.dispatchEvent(idleEvent); } } }, idleTime); return () => { if (timerRef.current) { clearTimeout(timerRef.current); } }; }, [ isBrowser, isEnabled, idleTime, trackInBackground, onIdle, syncWithStorage, storageKey, emitEvents, eventName ]); const startPolling = useCallback6(() => { if (!isBrowser || !isEnabled) return; if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); } pollingIntervalRef.current = setInterval(() => { const now = Date.now(); const elapsed = now - lastActiveRef.current; const remaining = Math.max(0, idleTime - elapsed); setIdleTimeTracked(elapsed); setRemainingTimeValue(remaining); }, pollingInterval); return () => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } }; }, [isBrowser, isEnabled, idleTime, pollingInterval]); const handleActivity = useCallback6( (event) => { if (!isEnabled) return; let shouldReset = true; if (event && event.type === "mousemove" && minimumMovement > 0) { const mouseEvent = event; const { x, y } = lastMousePositionRef.current; const deltaX = Math.abs(mouseEvent.clientX - x); const deltaY = Math.abs(mouseEvent.clientY - y); const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); shouldReset = distance >= minimumMovement; lastMousePositionRef.current = { x: mouseEvent.clientX, y: mouseEvent.clientY }; } if (shouldReset) { const wasIdle = isIdle; const now = Date.now(); lastActiveRef.current = now; if (wasIdle) { setIsIdle(false); onActive?.(); if (syncWithStorage) { try { localStorage.setItem( storageKey, JSON.stringify({ isIdle: false, timestamp: now }) ); } catch (e) { console.warn("Failed to sync idle state to localStorage", e); } } if (emitEvents && typeof window !== "undefined") { const activeEvent = new CustomEvent(eventName, { detail: { isIdle: false, idleTime: 0, timestamp: now } }); window.dispatchEvent(activeEvent); } } initTimer(); } }, [ isEnabled, isIdle, minimumMovement, onActive, initTimer, syncWithStorage, storageKey, emitEvents, eventName ] ); const handleVisibilityChange = useCallback6(() => { if (!isEnabled) return; if (document.hidden && idleOnVisibilityHidden) { setIsIdle(true); onIdle?.(); if (syncWithStorage) { try { localStorage.setItem( storageKey, JSON.stringify({ isIdle: true, timestamp: Date.now() }) ); } catch (e) { console.warn("Failed to sync idle state to localStorage", e); } } } else if (!document.hidden && isIdle) { handleActivity(); } }, [ isEnabled, idleOnVisibilityHidden, isIdle, onIdle, syncWithStorage, storageKey, handleActivity ]); const handleStorageChange = useCallback6( (e) => { if (!isEnabled || !syncWithStorage || e.key !== storageKey) return; try { const data = e.newValue ? JSON.parse(e.newValue) : null; if (data && typeof data.isIdle === "boolean") { setIsIdle(data.isIdle); if (data.isIdle) { onIdle?.(); } else { lastActiveRef.current = data.timestamp || Date.now(); onActive?.(); initTimer(); } } } catch (err) { console.warn("Failed to parse idle state from localStorage", err); } }, [isEnabled, syncWithStorage, storageKey, onIdle, onActive, initTimer] ); const setIdleManually = useCallback6(() => { setIsIdle(true); onIdle?.(); if (syncWithStorage) { try { localStorage.setItem( storageKey, JSON.stringify({ isIdle: true, timestamp: Date.now() }) ); } catch (e) { console.warn("Failed to sync idle state to localStorage", e); } } if (emitEvents && typeof window !== "undefined") { const idleEvent = new CustomEvent(eventName, { detail: { isIdle: true, idleTime: Date.now() - lastActiveRef.current, timestamp: Date.now() } }); window.dispatchEvent(idleEvent); } }, [onIdle, syncWithStorage, storageKey, emitEvents, eventName]); const setActiveManually = useCallback6(() => { lastActiveRef.current = Date.now(); setIsIdle(false); onActive?.(); initTimer(); if (syncWithStorage) { try { localStorage.setItem( storageKey, JSON.stringify({ isIdle: false, timestamp: Date.now() }) ); } catch (e) { console.warn("Failed to sync idle state to localStorage", e); } } if (emitEvents && typeof window !== "undefined") { const activeEvent = new CustomEvent(eventName, { detail: { isIdle: false, idleTime: 0, timestamp: Date.now() } }); window.dispatchEvent(activeEvent); } }, [onActive, initTimer, syncWithStorage, storageKey, emitEvents, eventName]); const reset = useCallback6(() => { if (isIdle) { setActiveManually(); } else { lastActiveRef.current = Date.now(); initTimer(); } }, [isIdle, setActiveManually, initTimer]); const enable = useCallback6(() => { if (!isEnabled) { setIsEnabled(true); lastActiveRef.current = Date.now(); initTimer(); startPolling(); } }, [isEnabled, initTimer, startPolling]); const disable = useCallback6(() => { if (isEnabled) { setIsEnabled(false); if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } setIsIdle(false); } }, [isEnabled]); useEffect9(() => { if (!isBrowser) return; if (syncWithStorage) { try { const storedState = localStorage.getItem(storageKey); if (storedState) { const { isIdle: storedIsIdle, timestamp } = JSON.parse(storedState); if (typeof storedIsIdle === "boolean") { setIsIdle(storedIsIdle); if (!storedIsIdle && timestamp) { lastActiveRef.current = timestamp; } } } } catch (e) { console.warn("Failed to load idle state from localStorage", e); } } lastEventHandlerRef.current = handleActivity; if (isEnabled) { lastActiveRef.current = Date.now(); initTimer(); startPolling(); } return () => { if (timerRef.current) { clearTimeout(timerRef.current); } if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); } }; }, [ isBrowser, isEnabled, initTimer, startPolling, syncWithStorage, storageKey, handleActivity ]); useEffect9(() => { if (!isBrowser || !isEnabled) return; const eventHandler = (event) => { lastEventHandlerRef.current(event); }; events.forEach((eventName2) => { if (eventName2 === "visibilitychange") { document.addEventListener( eventName2, handleVisibilityChange ); } else { window.addEventListener(eventName2, eventHandler, { passive: true }); } }); if (syncWithStorage) { window.addEventListener("storage", handleStorageChange); } return () => { events.forEach((eventName2) => { if (eventName2 === "visibilitychange") { document.removeEventListener( eventName2, handleVisibilityChange ); } else { window.removeEventListener(eventName2, eventHandler); } }); if (syncWithStorage) { window.removeEventListener("storage", handleStorageChange); } }; }, [ isBrowser, isEnabled, events, handleVisibilityChange, syncWithStorage, handleStorageChange ]); useEffect9(() => { if (initialEnabled !== isEnabled) { if (initialEnabled) { enable(); } else { disable(); } } }, [initialEnabled, isEnabled, enable, disable]); const idlePercentage = Math.min( 100, Math.floor(idleTimeTracked / idleTime * 100) ); return { isIdle, idleTime: idleTimeTracked, remainingTime: remainingTimeValue, lastActive: lastActiveRef.current, idlePercentage, setIdle: setIdleManually, setActive: setActiveManually, reset, enable, disable, isEnabled }; } // src/hooks/useIntersectionObserver.ts import { useState as useState10, useEffect as useEffect10, useRef as useRef6, useCallback as useCallback7 } from "react"; function useIntersectionObserver(options = {}) { const { root = null, rootMargin = "0px", threshold = 0, onEnter, onExit, triggerOnce = false, skip = false, initialInView = false, delay = 0, trackVisibilityChanges = false, historySize = 10, useIsIntersecting = true } = options; const isBrowser = useIsBrowser(); const [isInView, setIsInView] = useState10(initialInView); const [intersectionRatio, setIntersectionRatio] = useState10(0); const [entry, setEntry] = useState10(null); const [entryHistory, setEntryHistory] = useState10( [] ); const [isConnected, setIsConnected] = useState10(false); const [enteredAt, setEnteredAt] = useState10(null); const [exitedAt, setExitedAt] = useState10(null); const [inViewDuration, setInViewDuration] = useState10(0); const ref = useRef6(null); const observerRef = useRef6(null); const skipRef = useRef6(skip); const wasInViewRef = useRef6(initialInView); const timerId = useRef6(null); const durationTimerId = useRef6(null); const disconnect = useCallback7(() => { if (observerRef.current) { observerRef.current.disconnect(); setIsConnected(false); if (durationTimerId.current !== null) { cancelAnimationFrame(durationTimerId.current); durationTimerId.current = null; } } }, []); const updateInViewDuration = useCallback7(() => { if (isInView && enteredAt !== null) { setInViewDuration(Date.now() - enteredAt); durationTimerId.current = requestAnimationFrame(updateInViewDuration); } else { setInViewDuration(0); } }, [isInView, enteredAt]); const handleIntersection = useCallback7( (entries) => { if (skipRef.current) return; const entry2 = entries[0]; const inThreshold = useIsIntersecting ? entry2.isIntersecting : entry2.intersectionRatio > 0; setEntry(entry2); setIntersectionRatio(entry2.intersectionRatio); if (trackVisibilityChanges) { setEntryHistory((prevEntries) => { const newEntries = [entry2, ...prevEntries]; return newEntries.slice(0, historySize); }); } if (inThreshold && !wasInViewRef.current) { wasInViewRef.current = true; setIsInView(true); setEnteredAt(Date.now()); if (durationTimerId.current === null) { durationTimerId.current = requestAnimationFrame(updateInViewDuration); } onEnter?.(entry2); if (triggerOnce) { disconnect(); } } if (!inThreshold && wasInViewRef.current) { wasInViewRef.current = false; setIsInView(false); setExitedAt(Date.now()); if (durationTimerId.current !== null) { cancelAnimationFrame(durationTimerId.current); durationTimerId.current = null; setInViewDuration(0); } onExit?.(entry2); } }, [ disconnect, onEnter, onExit, triggerOnce, trackVisibilityChanges, historySize, useIsIntersecting, updateInViewDuration ] ); const observe = useCallback7(() => { if (!isBrowser || !ref.current || skipRef.current) return; observerRef.current = new IntersectionObserver(handleIntersection, { root, rootMargin, threshold }); observerRef.current.observe(ref.current); setIsConnected(true); return () => disconnect(); }, [isBrowser, root, rootMargin, threshold, handleIntersection, disconnect]); const reset = useCallback7(() => { wasInViewRef.current = initialInView; setIsInView(initialInView); setIntersectionRatio(0); setEntry(null); setEntryHistory([]); setEnteredAt(null); setExitedAt(null); setInViewDuration(0); if (durationTimerId.current !== null) { cancelAnimationFrame(durationTimerId.current); durationTimerId.current = null; } disconnect(); observe(); }, [initialInView, disconnect, observe]); useEffect10(() => { skipRef.current = skip; if (skip && observerRef.current) { disconnect(); } else if (!skip && ref.current && !observerRef.current) { observe(); } }, [skip, disconnect, observe]); useEffect10(() => { if (delay > 0) { timerId.current = setTimeout(observe, delay); return () => { if (timerId.current !== null) { clearTimeout(timerId.current); } disconnect(); }; } else { observe(); return () => disconnect(); } }, [delay, observe, disconnect]); return { ref, isInView, intersectionRatio, entry, entryHistory, reset, disconnect, observe, isConnected, enteredAt, exitedAt, inViewDuration }; } // src/hooks/useHasRendered.ts import { useRef as useRef7 } from "react"; function useHasRendered() { const hasRenderedRef = useRef7(false); if (!hasRenderedRef.current) { hasRenderedRef.current = true; return false; } return true; } // src/hooks/useLockBodyScroll.ts import { useEffect as useEffect11, useState as useState11, useCallback as useCallback8 } from "react"; function useLockBodyScroll(initialLocked = false, options = {}) { const { disabled = false, targetElement = null, preserveScrollPosition = true, reserveScrollbarGap = true } = options; const isBrowser = useIsBrowser(); const [isLocked, setIsLocked] = useState11(initialLocked); const [originalStyles, setOriginalStyles] = useState11( {} ); const [scrollbarWidth, setScrollbarWidth] = useState11(0); const measureScrollbarWidth = useCallback8(() => { if (!isBrowser) return 0; const outer = document.createElement("div"); outer.style.visibility = "hidden"; outer.style.overflow = "scroll"; document.body.appendChild(outer); const inner = document.createElement("div"); outer.appendChild(inner); const width = outer.offsetWidth - inner.offsetWidth; outer.parentNode?.removeChild(outer); setScrollbarWidth(width); return width; }, [isBrowser]); const lock = useCallback8(() => { if (!isBrowser || disabled) return; const target = targetElement || document.body; const originalOverflow = target.style.overflow; const originalPaddingRight = target.style.paddingRight; const originalPosition = target.style.position; const originalTop = target.style.top; const originalScrollY = window.scrollY; setOriginalStyles({ overflow: originalOverflow, paddingRight: originalPaddingRight, position: originalPosition, top: originalTop, scrollY: originalScrollY.toString() }); const sbWidth = reserveScrollbarGap ? scrollbarWidth || measureScrollbarWidth() : 0; const paddingRight = parseInt(window.getComputedStyle(target).paddingRight, 10) || 0; target.style.overflow = "hidden"; if (reserveScrollbarGap) { target.style.paddingRight = `${paddingRight + sbWidth}px`; } if (preserveScrollPosition) { target.style.position = "fixed"; target.style.top = `-${originalScrollY}px`; target.style.width = "100%"; } setIsLocked(true); }, [ isBrowser, disabled, targetElement, preserveScrollPosition, reserveScrollbarGap, scrollbarWidth, measureScrollbarWidth ]); const unlock = useCallback8(() => { if (!isBrowser || disabled) return; const target = targetElement || document.body; target.style.overflow = originalStyles.overflow || ""; target.style.paddingRight = originalStyles.paddingRight || ""; target.style.position = originalStyles.position || ""; target.style.top = originalStyles.top || ""; if (preserveScrollPosition && originalStyles.scrollY) { window.scrollTo(0, parseInt(originalStyles.scrollY, 10)); } setIsLocked(false); }, [ isBrowser, disabled, targetElement, originalStyles, preserveScrollPosition ]); const toggle = useCallback8(() => { if (isLocked) { unlock(); } else { lock(); } }, [isLocked, lock, unlock]); useEffect11(() => { if (initialLocked && !disabled) { lock(); } return () => { if (isLocked) { unlock(); } }; }, [initialLocked, disabled, lock, unlock, isLocked]); useEffect11(() => { if (isBrowser && reserveScrollbarGap && scrollbarWidth === 0) { measureScrollbarWidth(); } }, [isBrowser, reserveScrollbarGap, scrollbarWidth, measureScrollbarWidth]); return { isLocked, lock, unlock, toggle }; } // src/hooks/useLongPress.ts import { useState as useState12, useRef as useRef8, useCallback as useCallback9, useEffect as useEffect12 } from "react"; function useLongPress(callback, options = {}) { const isBrowser = useIsBrowser(); const { delay = 500, disabled = false, preventDefault = true, stopPropagation = false, detectMouse = true, moveTolerance = 10, vibrate = false, vibrationPattern = [100], continuousLongPress = false, continuousLongPressInterval = 100, onPressStart, onPressMove, onLongPress, onLongPressEnd, onPressCancel, onClick } = options; const [isPressed, setIsPressed] = useState12(false); const [isLongPressed, setIsLongPressed] = useState12(false); const [position, setPosition] = useState12(null); const [movement, setMovement] = useState12(null); const timerRef = useRef8(null); const continuousTimerRef = useRef8(null); const initialPositionRef = useRef8(null); const currentPositionRef = useRef8(null); const isLongPressedRef = useRef8(false); const clearTimers = useCallback9(() => { if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } if (continuousTimerRef.current) { clearInterval(continuousTimerRef.current); continuousTimerRef.current = null; } }, []); const reset = useCallback9(() => { setIsPressed(false); setIsLongPressed(false); setPosition(null); setMovement(null); initialPositionRef.current = null; currentPositionRef.current = null; isLongPressedRef.current = false; clearTimers(); }, [clearTimers]); const calculateMovement = useCallback9( (initial, current) => { const deltaX = current.x - initial.x; const deltaY = current.y - initial.y; const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); let direction = null; if (distance > 5) { if (Math.abs(deltaX) > Math.abs(deltaY)) { direction = deltaX > 0 ? "right" : "left"; } else { direction = deltaY > 0 ? "down" : "up"; } } return { distance, deltaX, deltaY, direction }; }, [] ); const triggerVibration = useCallback9(() => { if (isBrowser && vibrate && "vibrate" in navigator) { try { navigator.vibrate(vibrationPattern); } catch (e) { console.error("Vibration API error:", e); } } }, [isBrowser, vibrate, vibrationPattern]); const startLongPressTimer = useCallback9(() => { if (disabled || !initialPositionRef.current) return; clearTimers(); timerRef.current = setTimeout(() => { if (initialPositionRef.current && currentPositionRef.current) { const moveData = calculateMovement( initialPositionRef.current, currentPositionRef.current ); setIsLongPressed(true); isLongPressedRef.current = true; setMovement(moveData); triggerVibration(); if (callback) callback({}); if (onLongPress) onLongPress(currentPositionRef.current, moveData); if (continuousLongPress) { continuousTimerRef.current = setInterval(() => { if (currentPositionRef.current) { const currentMoveData = calculateMovement( initialPositionRef.current, currentPositionRef.current ); if (onLongPress) onLongPress(currentPositionRef.current, currentMoveData); } }, continuousLongPressInterval); } } }, delay); }, [ disabled, delay, callback, onLongPress, calculateMovement, triggerVibration, continuousLongPress, continuousLongPressInterval ]); const triggerLongPress = useCallback9(() => { if (disabled) return; const position2 = currentPositionRef.current || { x: 0, y: 0 }; setIsPressed(true); setIsLongPressed(true); isLongPressedRef.current = true; setPosition(position2); const moveData = movement || { distance: 0, deltaX: 0, deltaY: 0, direction: null }; setMovement(moveData); triggerVibration(); if (callback) callback({}); if (onLongPress) onLongPress(position2, moveData); }, [disabled, callback, onLongPress, triggerVibration, movement]); const cancelLongPress = useCallback9(() => { if (isLongPressedRef.current && onPressCancel) { onPressCancel(); } reset(); }, [onPressCancel, reset]); const getEventPosition = useCallback9( (e) => { if ("touches" in e) { return { x: e.touches[0]?.clientX || 0, y: e.touches[0]?.clientY || 0 }; } return { x: e.clientX, y: e.clientY }; }, [] ); const handlePressStart = useCallback9( (e) => { if (disabled) return; if (preventDefault) e.preventDefault(); if (stopPropagation) e.stopPropagation(); const pos = getEventPosition(e); setIsPressed(true); setIsLongPressed(false); setPosition(pos); setMovement(null); isLongPressedRef.current = false; initialPositionRef.current = pos; currentPositionRef.current = pos; if (onPressStart) onPressStart(pos); startLongPressTimer(); }, [ disabled, preventDefault, stopPropagation, getEventPosition, onPressStart, startLongPressTimer ] ); const handlePressMove = useCallback9( (e) => { if (!isPressed || disabled) return; if (preventDefault) e.preventDefault(); if (stopPropagation) e.stopPropagation(); const currentPos = getEventPosition(e); currentPositionRef.current = currentPos; setPosition(currentPos); if (initialPositionRef.current) { const moveData = calculateMovement( initialPositionRef.current, currentPos ); setMovement(moveData); if (moveData.distance > moveTolerance) { clearTimers(); if (!isLongPressedRef.current) { if (onPressCancel) onPressCancel(); } } if (onPressMove) onPressMove(moveData); } }, [ isPressed, disabled, preventDefault, stopPropagation, getEventPosition, calculateMovement, moveTolerance, onPressMove, onPressCancel, clearTimers ] ); const handlePressEnd = useCallback9( (e) => { if (!isPressed) return; if (preventDefault) e.preventDefault(); if (stopPropagation) e.stopPropagation(); const wasLongPress = isLongPressedRef.current; cons