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
JavaScript
// 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