npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@classytic/react-presence

v0.2.0

Published

React 19 hooks for user presence detection - tab visibility, window focus, online status, idle detection, page leave, wake lock, network info, heartbeat, and devtools detection

Readme

@classytic/react-presence

React 19 hooks for user presence detection. 13 hooks, context provider with granular selectors, tree-shakeable.

Requires react >= 19.0.0.

npm install @classytic/react-presence

Hooks

| Hook | Purpose | | ---------------------- | -------------------------------------------------------------------------------------- | | usePresence | Combined presence -- composes tab, focus, online, idle, network into one status | | useTabVisibility | Tab visible/hidden via Page Visibility API | | useWindowFocus | Window focus/blur | | useOnlineStatus | Browser online/offline | | useIdleDetection | User inactivity with prompt stage, cross-tab sync, sleep detection | | usePageLeave | Page leave detection (beforeunload, pagehide, mobile visibility) | | useWakeLock | Screen Wake Lock API -- prevent screen dimming | | useNetworkInfo | Network Information API -- effective type, downlink, RTT | | useDevToolsDetection | DevTools open detection via window size heuristic | | useHeartbeat | Periodic server pings with auto-pause on presence status | | useMultiDisplay | Detects multiple connected monitors via Window Management API | | useDeviceChange | Detects media hardware connections/disconnections (mics, cameras, headphones) | | useScreenShareGuard | Validates active screen shares to prevent cheating (e.g. blocking full-screen sharing) | | useFullscreenGuard | Enforce Fullscreen mode across browsers for focused/proctored apps | | useClipboardGuard | Block or detect copy, cut, paste, drag, and drop events to prevent cheating | | useBattery | Track battery level and discharging state to warn users before unexpected power loss | | usePermissions | Pre-emptively query OS-level browser permissions (camera, microphone, clipboard) |

Quick start

Combined hook

import { usePresence } from "@classytic/react-presence";

function App() {
  const { status, state, resetAll } = usePresence({
    idleTimeout: 30_000,
    promptTimeout: 10_000,
    onStatusChange: (status) => console.log(status),
  });

  // status: 'active' | 'prompted' | 'idle' | 'away' | 'offline'
  return <div>{status}</div>;
}

Context provider (multi-component)

import {
  PresenceProvider,
  usePresenceStatus,
  useIsTabVisible,
} from "@classytic/react-presence";

function App() {
  return (
    <PresenceProvider idleTimeout={60_000}>
      <StatusBadge />
    </PresenceProvider>
  );
}

// Only re-renders when status changes
function StatusBadge() {
  const status = usePresenceStatus();
  return <span>{status}</span>;
}

Individual hooks

import { useTabVisibility, useIdleDetection } from "@classytic/react-presence";

const { isVisible, hiddenDuration, resetDuration } = useTabVisibility({
  onHidden: () => log("tab hidden"),
});

const { isIdle, isPrompted, getRemainingTime } = useIdleDetection({
  idleTimeout: 60_000,
  promptTimeout: 10_000,
  onPrompt: () => showModal("Still there?"),
});

API reference

Status priority

offline > away > idle > prompted > active

| Status | Condition | | ---------- | ------------------------------------------------------------------- | | offline | Browser offline | | away | Tab hidden or window unfocused | | idle | No user activity for idleTimeout ms | | prompted | No activity for idleTimeout - promptTimeout ms (pre-idle warning) | | active | None of the above |


usePresence(options?): UsePresenceReturn

Composes useTabVisibility, useWindowFocus, useOnlineStatus, useIdleDetection, and optionally useNetworkInfo.

// Options
interface UsePresenceOptions {
  idleTimeout?: number; // Default: 60000
  promptTimeout?: number; // Pre-idle warning window (ms)
  idleEvents?: string[]; // Default: mousemove, mousedown, keydown, touchstart, scroll, wheel
  trackIdle?: boolean; // Default: true
  trackNetwork?: boolean; // Default: false
  crossTab?: boolean | CrossTabOptions;
  detectSleep?: boolean | SleepDetectorOptions;
  timers?: TimerFunctions; // Custom timers (e.g. worker timers)
  onStatusChange?: (status: PresenceStatus, state: PresenceState) => void;
  onTabVisibilityChange?: (state: TabVisibilityState) => void;
  onWindowFocusChange?: (state: WindowFocusState) => void;
  onOnlineStatusChange?: (state: OnlineState) => void;
  onIdleChange?: (state: IdleState) => void;
  onPrompt?: () => void;
  onNetworkChange?: (state: NetworkInfoState) => void;
  onSleep?: (event: SleepEvent) => void;
}

// Return
interface UsePresenceReturn {
  status: PresenceStatus;
  state: PresenceState;
  tabVisibility: UseTabVisibilityReturn;
  windowFocus: UseWindowFocusReturn;
  onlineStatus: UseOnlineStatusReturn;
  idle: UseIdleDetectionReturn;
  network?: NetworkInfoState;
  resetAll: () => void;
  getRemainingTime: () => number; // ms until idle
  getElapsedTime: () => number; // ms since last activity
  activate: () => void; // reset from prompted/idle to active
}

useTabVisibility(options?): UseTabVisibilityReturn

Tracks tab visibility via document.visibilityState. Live hiddenDuration counter updates every 1s while hidden.

// Options
interface UseTabVisibilityOptions {
  onVisible?: () => void;
  onHidden?: () => void;
  onChange?: (state: TabVisibilityState) => void;
}

// Return
interface UseTabVisibilityReturn {
  isVisible: boolean;
  visibilityState: "visible" | "hidden";
  lastChange: number; // timestamp
  hiddenDuration: number; // cumulative ms hidden
  resetDuration: () => void; // reset counter (safe to call while hidden)
}

useWindowFocus(options?): UseWindowFocusReturn

Tracks window focus/blur. Live blurDuration counter updates every 1s while blurred.

// Options
interface UseWindowFocusOptions {
  onFocus?: () => void;
  onBlur?: () => void;
  onChange?: (state: WindowFocusState) => void;
}

// Return
interface UseWindowFocusReturn {
  isFocused: boolean;
  lastChange: number;
  blurDuration: number; // cumulative ms blurred
  resetDuration: () => void; // safe to call while blurred
}

useOnlineStatus(options?): UseOnlineStatusReturn

Tracks navigator.onLine via online/offline events.

// Options
interface UseOnlineStatusOptions {
  onOnline?: () => void;
  onOffline?: () => void;
  onChange?: (state: OnlineState) => void;
}

// Return
interface UseOnlineStatusReturn {
  isOnline: boolean;
  lastChange: number;
  wasOffline: boolean; // true if ever went offline this session
  resetOfflineFlag: () => void;
}

useIdleDetection(options?): UseIdleDetectionReturn

Detects user inactivity. State machine: active -> prompted -> idle -> active.

Features: prompt stage, cross-tab sync, sleep/hibernate detection, worker timer support, imperative time getters.

// Options
interface UseIdleDetectionOptions {
  idleTimeout?: number; // Default: 60000
  promptTimeout?: number; // ms before idle to trigger prompt
  events?: string[]; // Default: mousemove, mousedown, keydown, touchstart, scroll, wheel
  startOnMount?: boolean; // Default: true
  timers?: TimerFunctions; // Worker timers for background accuracy
  crossTab?: boolean | CrossTabOptions;
  detectSleep?: boolean | SleepDetectorOptions;
  onIdle?: () => void;
  onActive?: () => void;
  onPrompt?: () => void;
  onChange?: (state: IdleState) => void;
  onSleep?: (event: SleepEvent) => void;
}

// Return
interface UseIdleDetectionReturn {
  isIdle: boolean;
  isPrompted: boolean;
  idleTime: number; // ms since last activity (live, updates every 1s)
  lastActivity: number; // timestamp
  isRunning: boolean;
  markActive: () => void; // reset to active
  activate: () => void; // alias for markActive
  start: () => void;
  stop: () => void;
  getRemainingTime: () => number; // imperative, ms until idle
  getElapsedTime: () => number; // imperative, ms since last activity
  getActiveTime: () => number; // imperative, total active ms
  crossTab?: CrossTabState; // { isLeader, tabCount, lastActivityAcrossTabs }
}

usePageLeave(options?): UsePageLeaveReturn

bfcache-compatible leave detection. Uses beforeunload, pagehide, visibility change (mobile). No deprecated unload event.

// Options
interface UsePageLeaveOptions {
  preventLeave?: boolean | (() => boolean); // triggers browser "Leave site?" dialog
  beaconUrl?: string; // send beacon on leave
  beaconData?: () => BodyInit;
  onBeforeLeave?: () => void;
  onLeave?: () => void;
  onChange?: (state: PageLeaveState) => void;
}

// Return
interface UsePageLeaveReturn {
  isLeaving: boolean;
  leaveAttempts: number;
  lastLeaveAttempt: number;
  enablePrevention: () => void; // dynamically enable leave prevention (sets override)
  disablePrevention: () => void; // dynamically disable (sets override)
  clearOverride: () => void; // clear override, return to prop-controlled behavior
}

Override behavior:

  • enablePrevention() / disablePrevention() set an override that persists across re-renders
  • Boolean prop changes (truefalse) clear any active override
  • Function props can be inline (new reference each render) without clearing override
  • clearOverride() explicitly returns to prop-controlled behavior

useWakeLock(options?): UseWakeLockReturn

Screen Wake Lock API. Auto re-acquires on tab return (browser releases on tab hide).

// Options
interface UseWakeLockOptions {
  autoReacquire?: boolean; // Default: true
  onLock?: () => void;
  onRelease?: () => void;
  onError?: (error: Error) => void;
}

// Return
interface UseWakeLockReturn {
  isSupported: boolean;
  isLocked: boolean;
  error: string | null;
  request: () => Promise<void>;
  release: () => Promise<void>;
}

useNetworkInfo(options?): UseNetworkInfoReturn

Network Information API (Chromium only, graceful no-op fallback).

// Options
interface UseNetworkInfoOptions {
  enabled?: boolean; // Default: true. Set false to skip subscription.
  onChange?: (state: NetworkInfoState) => void;
  onSlow?: () => void; // connection became slow-2g or 2g
  onRecover?: () => void; // connection recovered from slow
}

// Return
interface UseNetworkInfoReturn {
  isSupported: boolean;
  effectiveType: "slow-2g" | "2g" | "3g" | "4g" | null;
  downlink: number | null; // Mbps
  rtt: number | null; // ms
  saveData: boolean;
  isSlowConnection: boolean; // true if slow-2g or 2g
}

useDevToolsDetection(options?): UseDevToolsDetectionReturn

Detects DevTools open state via outerWidth - innerWidth > threshold.

Limitation: This is a best-effort heuristic. Cannot detect undocked DevTools (separate window) and may false-positive on narrow browser windows. Use for analytics/UX hints, not security.

// Options
interface UseDevToolsDetectionOptions {
  checkInterval?: number; // Default: 1000
  threshold?: number; // Default: 160 (px)
  onOpen?: () => void;
  onClose?: () => void;
  onChange?: (state: DevToolsState) => void;
}

// Return
interface UseDevToolsDetectionReturn {
  isOpen: boolean;
}

useHeartbeat(options): UseHeartbeatReturn

Periodic server pings. Auto-pauses when presence status is idle/away/offline.

// Options (onPing is required)
interface UseHeartbeatOptions {
  onPing: () => Promise<void>;
  onPingError?: (error: Error) => void;
  interval?: number; // Default: 30000
  jitter?: number; // 0-1 range, e.g. 0.2 = ±20% interval variance
  startOnMount?: boolean; // Default: true
  autoPause?: boolean; // Default: true
  pauseOnStatus?: PresenceStatus[]; // Default: ['idle', 'away', 'offline']
  presenceStatus?: PresenceStatus; // pass from usePresence
}

// Jitter prevents "thundering herd" when many clients reconnect simultaneously
useHeartbeat({ onPing, interval: 30000, jitter: 0.2 }); // 24-36s intervals

// Return
interface UseHeartbeatReturn {
  isActive: boolean;
  lastPing: number;
  pingCount: number;
  lastPingFailed: boolean;
  start: () => void;
  stop: () => void;
  ping: () => Promise<void>; // manual ping
}

useMultiDisplay(options?): UseMultiDisplayReturn

Detects connected external monitors.

Detailed Mode: Uses the Window Management API (window.getScreenDetails()) which requires a browser permission prompt. If detailedMode is false, it uses a passive check (window.screen.isExtended) which does not prompt for permission but only provides a boolean state.

// Options
interface UseMultiDisplayOptions {
  detailedMode?: boolean; // If true, prompts for permission to get exact screenCount
  onDisplayConnected?: () => void;
  onDisplayDisconnected?: () => void;
  onChange?: (state: MultiDisplayState) => void;
}

// Return
interface UseMultiDisplayReturn {
  isExtended: boolean; // True if multiple displays are connected
  screenCount: number | null; // Only populated if detailedMode was requested and granted
  isSupported: boolean; // True on Chrome/Edge 100+
  requestDetailedMode: () => Promise<boolean>; // Manually request screen details and permission
}

useDeviceChange(options?): UseDeviceChangeReturn

Detects when a user plugs in or unplugs a media device (headphones, microphone, webcam) via the devicechange event listener on navigator.mediaDevices.

// Options
interface UseDeviceChangeOptions {
  onChange?: (state: DeviceChangeState) => void;
  onFirstChange?: () => void;
}

// Return
interface UseDeviceChangeReturn {
  hasChanged: boolean; // True once a device is added/removed
  changeCount: number; // Number of times devices were changed
  lastChangedAt: Date | null;
  isSupported: boolean;
  reset: () => void; // Resets hasChanged and changeCount
}

useScreenShareGuard(options?): UseScreenShareGuardReturn

Monitors an active screen share MediaStreamTrack to enforce proctoring rules, such as preventing a user from sharing just a specific window instead of their entire screen.

// Options
interface UseScreenShareGuardOptions {
  /**
   * Allowed surface types. Defaults to `["monitor"]` (entire screen only).
   * Set to `["monitor", "window"]` to also allow window sharing.
   */
  allowedSurfaces?: DisplaySurfaceType[];
}

// Return
interface UseScreenShareGuardReturn {
  /**
   * Request screen share with entire-screen preference.
   * Resolves with the MediaStream if valid, or throws with a user-friendly
   * message if the user selected a disallowed surface type.
   */
  requestScreenShare: (
    constraints?: DisplayMediaStreamOptions,
  ) => Promise<MediaStream>;
  /** Validate an existing screen share track (e.g. after reconnect) */
  validateTrack: (track: MediaStreamTrack) => ScreenShareValidation;
}

useFullscreenGuard(options?): UseFullscreenGuardReturn

Monitors whether the current app is rendering in full screen mode, allowing you to force focus visually for tests and proctored environments. Also provides methods for triggering or exiting fullscreen manually.

// Options
interface FullscreenGuardOptions {
  onEnter?: () => void;
  onExit?: () => void;
  onChange?: (state: FullscreenGuardState) => void;
  onError?: (error: Error) => void;
}

// Return
interface UseFullscreenGuardReturn {
  isFullscreen: boolean;
  isSupported: boolean;
  requestFullscreen: () => Promise<void>;
  exitFullscreen: () => Promise<void>;
  toggleFullscreen: () => Promise<void>;
}

useClipboardGuard(options?): UseClipboardGuardReturn

Blocks attempts to copy, cut, paste, drag, and drop anywhere inside an application globally, or scoped to a specific target Ref. Helpful for preventing external cheating.

// Options
interface ClipboardGuardOptions {
  blockCopy?: boolean; // Default: true
  blockCut?: boolean; // Default: true
  blockPaste?: boolean; // Default: true
  blockDragDrop?: boolean; // Default: true
  targetRef?: React.RefObject<HTMLElement | null>;
  onViolation?: (type: "copy" | "cut" | "paste" | "drag" | "drop") => void;
}

// Return
interface UseClipboardGuardReturn {
  violationCount: number;
  lastViolationAt: Date | null;
  lastViolationType: "copy" | "cut" | "paste" | "drag" | "drop" | null;
  reset: () => void;
}

useBattery(options?): UseBatteryReturn

Leverages the navigator.getBattery() API. Often necessary for long-running processes (like AI interviews) where losing power means disconnecting unexpectedly.

// Options
interface BatteryOptions {
  onChange?: (state: BatteryState) => void;
  onLowBattery?: (level: number) => void;
  lowThreshold?: number; // Default: 0.2 (20%)
}

// Return
interface UseBatteryReturn {
  isSupported: boolean;
  fetched: boolean;
  level: number | null; // e.g. 0.85 (85%)
  charging: boolean | null;
  dischargingTime: number | null; // Seconds until dead
  chargingTime: number | null; // Seconds until full
  refresh: () => Promise<void>;
}

usePermissions(options): UsePermissionsReturn

Proactively query the hardware/browser permission state without unnecessarily popping up a prompt (getUserMedia typically requests permissions, while navigator.permissions just checks their current state passively).

// Options
interface PermissionsOptions {
  name:
    | "camera"
    | "microphone"
    | "clipboard-read"
    | "clipboard-write"
    | "display-capture"
    | PermissionName;
  onChange?: (state: PermissionsState) => void;
}

// Return
interface UsePermissionsReturn {
  state: PermissionState | "unsupported" | "prompt"; // 'granted', 'denied', 'prompt'
  isSupported: boolean;
  refresh: () => Promise<void>;
}

Context provider and selectors

PresenceProvider wraps usePresence and exposes granular selectors via useSyncExternalStore. Components only re-render when their selected slice changes.

<PresenceProvider idleTimeout={60_000} trackNetwork>
  {children}
</PresenceProvider>

PresenceProvider accepts all UsePresenceOptions as props.

Selector hooks

All require PresenceProvider ancestor. Throw if used outside.

// Full context
usePresenceContext(): UsePresenceReturn

// State slice selectors
usePresenceStatus(): PresenceStatus
useTabVisibilityState(): TabVisibilityState
useWindowFocusState(): WindowFocusState
useOnlineStatusState(): OnlineState
useIdleState(): IdleState
useNetworkInfoState(): NetworkInfoState | undefined

// Boolean selectors (most granular)
useIsTabVisible(): boolean
useIsWindowFocused(): boolean
useIsOnline(): boolean
useIsIdle(): boolean
useIsPrompted(): boolean
useIsSlowConnection(): boolean

Worker timers

Secondary entry point for unthrottled timers in background tabs. Uses inline Web Worker via Blob URL.

import { createWorkerTimers } from "@classytic/react-presence/timers";

const timers = createWorkerTimers();

// Pass to hooks
useIdleDetection({ timers, idleTimeout: 60_000 });
usePresence({ timers, idleTimeout: 60_000 });

// Cleanup when done
timers.dispose();

Debugging

import { enableDebug, disableDebug } from "@classytic/react-presence";

enableDebug(); // or localStorage.setItem('DEBUG_REACT_PRESENCE', 'true')

Types

All types are exported from the main entry point:

import type {
  PresenceStatus, // 'active' | 'prompted' | 'idle' | 'away' | 'offline'
  PresenceState,
  TabVisibilityState,
  WindowFocusState,
  OnlineState,
  IdleState,
  PageLeaveState,
  WakeLockState,
  NetworkInfoState,
  HeartbeatState,
  DevToolsState,
  TimerFunctions,
  CrossTabState,
  SleepEvent,
} from "@classytic/react-presence";

License

MIT