{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "wake-lock-toggle",
  "title": "Wake Lock Toggle",
  "description": "A toggle that stops the screen going dark while somebody is looking at it but not touching it, and — unlike every version written by hand — keeps on being true about whether the screen is actually being held awake. Reach for it wherever the page is being read rather than operated: a recipe followed with both hands busy, a barcode, QR code or boarding pass held up at a till or a gate, sheet music or guitar tabs on a stand, a presentation or kiosk view, a workout, cooking or interval timer counting down, an inspection or picking checklist walked through on a phone, turn-by-turn directions, a live scoreboard or auction, a long article or PDF being read, a dashboard left up on a wall display, a video call or a camera feed. Common asks it answers: \"keep screen awake react\", \"prevent screen from sleeping web\", \"screen wake lock react\", \"wake lock api react\", \"navigator.wakeLock react\", \"useWakeLock hook\", \"react-screen-wake-lock alternative\", \"NoSleep.js alternative\", \"stop phone screen turning off website\", \"keep display on pwa\", \"wake lock toggle shadcn\", \"shadcn keep screen on\", \"wake lock released when tab hidden\", \"wake lock stops working after switching tabs\", \"wakeLock request NotAllowedError\", \"navigator.wakeLock is undefined\", \"wake lock not working on http\", \"screen-wake-lock permissions policy iframe\", \"release wake lock on unmount\", \"画面が消えないようにする react\", \"スリープ防止 ウェブ\", \"スクリーンスリープ 無効化 react\", \"タブを戻すと画面が消えてしまう\". Official shadcn/ui has nothing of the kind, and the measurement is not close. Fetching all sixty-three registry entries today — sixty-two are fetchable, questionnaire alone 404s, and nine of the newer ones are only served on the new-york-v4 style track, so a probe of new-york and default silently misses them — and concatenating the component sources themselves rather than their JSON envelopes gives 223,287 bytes. In it, wakeLock, WakeLock, requestWakeLock, WakeLockSentinel, screen-wake-lock, keepAwake, NoSleep, visibilityState and visibilitychange are every one of them zero hits. Official has no component that reacts to the tab being hidden at all, so an agent asked for this writes it from scratch, and the version it writes has one specific bug in it. The bug is a single `const [isOn, setIsOn] = useState(false)` flipped inside the click handler, which makes the toggle a claim about the lock rather than a reading of it. It demos perfectly, because a demo never leaves the tab. Then the user glances at a message and comes back — and the browser releases a screen wake lock the moment the document stops being visible, silently, with no callback and nothing in the console. The toggle still reads \"Screen stays on\"; the phone in their hands starts dimming on schedule. Nothing in the UI ever admits it. That is the failure this component is built around: the lock has to be taken again on the way back, and the way back is `visibilitychange`. So the state here is two things, not one. `isEnabled` is what the user asked for and it survives the hidden stretches; `isActive` says whether a sentinel is being held right now, and it is written only by the sentinel’s own `release` event — the event the browser fires when the tab is hidden, the window is minimised, the battery gets low or the OS simply takes it back. On the element they appear as `aria-pressed` and `data-active`, so `aria-pressed=\"true\"` with `data-active=\"false\"` is a readable, honest state rather than a lie. Note what this does not need, because it is the exact opposite of the Fullscreen API and the difference is load-bearing: `wakeLock.request()` does not require a user gesture. It requires the document to be visible. That is what makes the re-acquire possible at all — there is no click to hang it on when somebody switches back to a tab — and it is also why the request fails in places a click would have got through. Asking while hidden is a guaranteed NotAllowedError, so it is not asked; and a NotAllowedError that arrives because the tab went away mid-request is a race, not a refusal, so it is not reported — a version that surfaces it puts an error in front of the user every time they switch tabs. A refusal that arrives while the page is visible is the real thing — an iframe that was not granted `allow=\"screen-wake-lock\"`, or a browser that has decided no — and there the setting goes back off and `onWakeLockError` fires, rather than retrying on a timer that would spin forever. TypeScript will not help you here and will in fact mislead you. Since TS 5.x, lib.dom.d.ts declares `readonly wakeLock: WakeLock` on Navigator — not optional — so `navigator.wakeLock.request(\"screen\")` type-checks cleanly and then throws `TypeError: Cannot read properties of undefined` at runtime wherever it is absent. This is a secure-context API, so that includes every page served over plain http: the staging box on an internal IP, the phone opening your dev server by LAN address. Detection is written by hand with an `in` check, it never reaches the render — so the server and the first client render agree and nothing hydrates twice — and where the API is missing the control stays in the tab order with `aria-disabled` and an accessible name that explains itself, rather than taking the `disabled` attribute that would remove it from the tab order so nobody ever hears why. The rest is the lifecycle nobody gets to on the first pass. Only one request is ever in flight, because a hidden/visible flap can fire two before the first settles and every sentinel but the last would be leaked, held with nothing left pointing at it. A sentinel that arrives after the user has switched the toggle off is released immediately instead of kept. Unmounting hands the lock back, because the sentinel is owned by the document rather than by the component: navigating from the recipe to the checkout inside a single-page app would otherwise leave the screen pinned awake with no control left to turn it off. The API: `WakeLockToggle` takes `defaultEnabled`, `onLabel`, `offLabel`, `unsupportedLabel`, `iconOnly`, `onEnabledChange` and `onWakeLockError` — named that way rather than `onChange` and `onError` because both of those are native DOM attributes React defines on every element, and a prop by either name collides the moment the options are spread onto the button. `useWakeLock()` returns `{ isEnabled, isActive, isSupported, error, enable, disable, toggle }` for a control you lay out yourself, and `isWakeLockSupported()` is there when you would rather hide your own. Within pulld it sits with the other components that watch what the browser is doing behind the app rather than what the user is doing in it: idle-timeout is the mirror image, ending a session when nobody is there, network-status reports a connection that changed without being asked, countdown and save-status are the things most often on screen while this is on, and fullscreen-button is the other half of a kiosk or presentation view. Distinct from a CSS or meta-tag trick: this is the browser’s own screen wake lock, and it lets the display dim on the browser’s terms the moment the setting is turned off. One file, one dependency (lucide-react for the two icons), every colour a shadcn token, so light and dark follow on their own.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/ui/wake-lock-toggle.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Lightbulb, LightbulbOff } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * The Screen Wake Lock entry point, or null where there isn't one.\n *\n * Hand-written because TypeScript will not help here and will in fact actively mislead. Since\n * TS 5.x, `lib.dom.d.ts` declares `readonly wakeLock: WakeLock` on `Navigator` — not optional —\n * so `navigator.wakeLock.request(\"screen\")` type-checks perfectly and then throws\n * `TypeError: Cannot read properties of undefined` at runtime on every browser that hasn't got\n * it, and on every page served over plain http, because this is a secure-context API and the\n * property is simply absent off HTTPS. A component that trusts the type breaks on localhost's\n * http sibling — the staging box on an internal IP — with an error that looks nothing like\n * \"unsupported\".\n */\nfunction wakeLockOf(): WakeLock | null {\n  if (typeof navigator === \"undefined\" || !(\"wakeLock\" in navigator)) return null\n  const api: WakeLock | undefined = navigator.wakeLock\n  return api && typeof api.request === \"function\" ? api : null\n}\n\n/** Whether the screen wake lock can be asked for here at all — browser support plus secure context. */\nexport function isWakeLockSupported(): boolean {\n  return wakeLockOf() !== null\n}\n\ninterface UseWakeLockOptions {\n  /** Ask for the lock as soon as the component mounts. */\n  defaultEnabled?: boolean\n  /**\n   * Called when the setting flips, including when a refusal switches it back off by itself.\n   *\n   * Not `onChange`: that is a native attribute of `<button>`, so a prop by that name would be\n   * spread onto the element as React's change handler as well as read here.\n   */\n  onEnabledChange?: (enabled: boolean) => void\n  /**\n   * Called when the browser refuses the lock while the page is visible.\n   *\n   * Not `onError`, for the same reason the one above is not `onChange`: `onError` is a native DOM\n   * attribute that React defines on every element, so a prop by that name collides with it the\n   * moment these options are spread onto the `<button>` — the compiler rejects the interface\n   * outright, and a version that silenced it would hand React's error handler to this callback and\n   * call it with a SyntheticEvent instead of an Error.\n   */\n  onWakeLockError?: (error: Error) => void\n}\n\ninterface UseWakeLockResult {\n  /**\n   * What the user asked for. This is the toggle's own state, and it survives the tab being\n   * hidden — the setting is still \"keep the screen on\" even in the minutes where no lock is held.\n   */\n  isEnabled: boolean\n  /**\n   * Whether a lock is being held *right now*.\n   *\n   * Separate from `isEnabled` on purpose, and the reason this component exists. Collapsing the two\n   * into one boolean is what makes every hand-rolled version lie: see the note on the visibility\n   * effect below. Read this one if you want to surface \"the screen will sleep after all\" — it goes\n   * false whenever the browser takes the lock back, whatever the reason.\n   */\n  isActive: boolean\n  /**\n   * Whether the API is here. Starts `true` so the server and the first client render agree, then\n   * settles on mount; read it to hide your own control rather than to pick a label.\n   */\n  isSupported: boolean\n  /** The last refusal, or null. Cleared as soon as a lock is obtained. */\n  error: Error | null\n  /** Ask to keep the screen on. */\n  enable: () => void\n  /** Let the screen sleep again. */\n  disable: () => void\n  /** Flip the setting. */\n  toggle: () => void\n}\n\n/**\n * The whole behaviour, for a control you lay out yourself.\n *\n * Note what this does *not* need, because it is the opposite of the Fullscreen API and the\n * difference is load-bearing: `wakeLock.request()` does not require a user gesture. It requires\n * the document to be **visible**. That is exactly what makes the re-acquire below possible —\n * there is no click to hang it on when somebody switches back to the tab — and it is also why the\n * request can fail from an effect that a click would have got through.\n */\nexport function useWakeLock({\n  defaultEnabled = false,\n  onEnabledChange,\n  onWakeLockError,\n}: UseWakeLockOptions = {}): UseWakeLockResult {\n  const [isEnabled, setIsEnabled] = React.useState(defaultEnabled)\n  const [isActive, setIsActive] = React.useState(false)\n  const [isSupported, setIsSupported] = React.useState(true)\n  const [error, setError] = React.useState<Error | null>(null)\n\n  const sentinelRef = React.useRef<WakeLockSentinel | null>(null)\n  // One request at a time. `request()` is async and a hidden/visible flap can call this twice\n  // before the first settles, which would leave a second sentinel held with nothing pointing at\n  // it — a lock that outlives the toggle and can never be released.\n  const pendingRef = React.useRef(false)\n  const enabledRef = React.useRef(isEnabled)\n  const mountedRef = React.useRef(true)\n\n  // Declared before everything else that reads it. Under StrictMode's deliberate mount/unmount/\n  // remount, the cleanup below runs and then the other effects run again; if this one came last,\n  // the second pass would see `mountedRef.current === false` and quietly refuse to take the lock,\n  // so the component would work in production and do nothing in development.\n  React.useEffect(() => {\n    mountedRef.current = true\n    return () => {\n      mountedRef.current = false\n    }\n  }, [])\n\n  const onEnabledChangeRef = React.useRef(onEnabledChange)\n  React.useEffect(() => {\n    onEnabledChangeRef.current = onEnabledChange\n  }, [onEnabledChange])\n\n  const onErrorRef = React.useRef(onWakeLockError)\n  React.useEffect(() => {\n    onErrorRef.current = onWakeLockError\n  }, [onWakeLockError])\n\n  const release = React.useCallback(async () => {\n    const sentinel = sentinelRef.current\n    sentinelRef.current = null\n    setIsActive(false)\n    if (!sentinel || sentinel.released) return\n    try {\n      await sentinel.release()\n    } catch {\n      // Already gone — the browser dropped it between the check and the call.\n    }\n  }, [])\n\n  const acquire = React.useCallback(async () => {\n    const api = wakeLockOf()\n    if (!api) {\n      // Not \"wait and see\": there is no API on this origin and there never will be during this\n      // page's life. Refusing the setting is the honest answer, because the alternative is a\n      // toggle stuck in the on position over a screen that dims on schedule.\n      setIsSupported(false)\n      setIsEnabled(false)\n      const failure = new Error(\n        \"Screen Wake Lock is unavailable here. It needs a supporting browser and a secure context (https, or localhost).\"\n      )\n      setError(failure)\n      onErrorRef.current?.(failure)\n      return\n    }\n    if (pendingRef.current) return\n    const held = sentinelRef.current\n    if (held && !held.released) return\n    // Asking while hidden is a guaranteed NotAllowedError. Skip it and let the visibility handler\n    // ask once we are back on screen.\n    if (typeof document === \"undefined\" || document.visibilityState !== \"visible\") return\n\n    pendingRef.current = true\n    try {\n      const sentinel = await api.request(\"screen\")\n      // The request is async, so the world may have moved on: the user can have switched the\n      // toggle off, or navigated away entirely, while it was in flight. Dropping it here is what\n      // keeps a lock from surviving the component that owns it.\n      if (!mountedRef.current || !enabledRef.current) {\n        void sentinel.release().catch(() => {})\n        return\n      }\n      sentinelRef.current = sentinel\n      setIsActive(true)\n      setError(null)\n      sentinel.addEventListener(\n        \"release\",\n        () => {\n          // The only writer of \"the lock is gone\", and it fires for reasons that never pass\n          // through this component: the tab was hidden, the window was minimised, the battery got\n          // low, the OS took it back. A sentinel is single-use — once released it stays released —\n          // so the reference is dropped and the next acquire starts a fresh one.\n          if (sentinelRef.current === sentinel) sentinelRef.current = null\n          setIsActive(false)\n        },\n        { once: true }\n      )\n    } catch (err) {\n      const failure = err instanceof Error ? err : new Error(String(err))\n      // A NotAllowedError raised because the tab went away mid-request is not a refusal, it is a\n      // race, and the visibility handler is already going to retry it. Reporting that one would\n      // put an error in front of the user every time they switched tabs.\n      if (typeof document !== \"undefined\" && document.visibilityState !== \"visible\") return\n      setIsActive(false)\n      setError(failure)\n      onErrorRef.current?.(failure)\n      // Refused while visible — a permissions policy on the iframe (`allow=\"screen-wake-lock\"` was\n      // not granted), or a browser that has decided no. Retrying on a timer would spin, so the\n      // setting goes back off and the button stops claiming something untrue.\n      setIsEnabled(false)\n    } finally {\n      pendingRef.current = false\n    }\n  }, [])\n\n  React.useEffect(() => {\n    setIsSupported(isWakeLockSupported())\n  }, [])\n\n  // Intent in, action out.\n  React.useEffect(() => {\n    enabledRef.current = isEnabled\n    if (isEnabled) void acquire()\n    else void release()\n  }, [acquire, isEnabled, release])\n\n  // The core of the whole component.\n  //\n  // The browser releases a screen wake lock whenever the document stops being visible, and it does\n  // it silently — no callback into your code, nothing in the console. Switch to another tab for\n  // ten seconds, come back, and a toggle that only tracked its own click still reads \"Screen stays\n  // on\" while the phone dims in the user's hands. The lock has to be taken again on the way back,\n  // and the way back is this event.\n  React.useEffect(() => {\n    if (!isEnabled || typeof document === \"undefined\") return\n    const onVisibilityChange = () => {\n      if (document.visibilityState === \"visible\") void acquire()\n    }\n    document.addEventListener(\"visibilitychange\", onVisibilityChange)\n    return () => document.removeEventListener(\"visibilitychange\", onVisibilityChange)\n  }, [acquire, isEnabled])\n\n  // Unmounting has to hand the lock back. Nothing else will: the sentinel is owned by the\n  // document, not by this component, so navigating from the recipe to the checkout inside a\n  // single-page app would otherwise leave the screen pinned awake with no control left to turn it\n  // off.\n  React.useEffect(() => {\n    return () => {\n      void release()\n    }\n  }, [release])\n\n  const reported = React.useRef(isEnabled)\n  React.useEffect(() => {\n    if (reported.current === isEnabled) return\n    reported.current = isEnabled\n    onEnabledChangeRef.current?.(isEnabled)\n  }, [isEnabled])\n\n  const enable = React.useCallback(() => setIsEnabled(true), [])\n  const disable = React.useCallback(() => setIsEnabled(false), [])\n  const toggle = React.useCallback(() => setIsEnabled((prev) => !prev), [])\n\n  return { isEnabled, isActive, isSupported, error, enable, disable, toggle }\n}\n\ninterface WakeLockToggleProps\n  extends Omit<React.ComponentPropsWithoutRef<\"button\">, \"children\">,\n    UseWakeLockOptions {\n  /** Label while the screen is being kept on. */\n  onLabel?: string\n  /** Label while the screen is free to sleep. */\n  offLabel?: string\n  /** Label where the API is missing. Kept as the accessible name so the control can explain itself. */\n  unsupportedLabel?: string\n  /** Drop the visible text and keep it as the accessible name. */\n  iconOnly?: boolean\n}\n\n/**\n * A toggle that stops the screen going dark, and keeps on being true about it.\n */\nexport function WakeLockToggle({\n  onLabel = \"Screen stays on\",\n  offLabel = \"Keep screen on\",\n  unsupportedLabel = \"Keeping the screen on isn't supported here\",\n  iconOnly = false,\n  defaultEnabled,\n  onEnabledChange,\n  onWakeLockError,\n  onClick,\n  className,\n  ...props\n}: WakeLockToggleProps) {\n  const { isEnabled, isActive, isSupported, toggle } = useWakeLock({\n    defaultEnabled,\n    onEnabledChange,\n    onWakeLockError,\n  })\n\n  function handleClick(event: React.MouseEvent<HTMLButtonElement>) {\n    onClick?.(event)\n    if (event.defaultPrevented) return\n    if (!isSupported) return\n    toggle()\n  }\n\n  const label = !isSupported ? unsupportedLabel : isEnabled ? onLabel : offLabel\n  const Icon = isEnabled ? Lightbulb : LightbulbOff\n\n  return (\n    <button\n      type=\"button\"\n      onClick={handleClick}\n      // `aria-pressed` carries the setting, because that is what the user operates and what\n      // persists across the hidden stretches where no lock is held. An icon swap is not something\n      // a screen reader reports, so it cannot be the only signal.\n      aria-pressed={isEnabled}\n      // `aria-disabled` rather than `disabled`: a real disabled button leaves the tab order, so a\n      // keyboard or screen-reader user never reaches it and never hears why it is not available.\n      // This one stays focusable and says so, and the click handler above is what refuses.\n      aria-disabled={isSupported ? undefined : true}\n      aria-label={iconOnly ? label : undefined}\n      data-state={isEnabled ? \"on\" : \"off\"}\n      // The honest one, for styling or a test: \"on\" with `data-active=\"false\"` is a setting whose\n      // lock is not currently held.\n      data-active={isActive ? \"true\" : \"false\"}\n      className={cn(\n        \"inline-flex h-9 items-center justify-center gap-2 rounded-md border border-input bg-transparent text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 disabled:pointer-events-none disabled:opacity-50\",\n        iconOnly ? \"w-9\" : \"px-4\",\n        className\n      )}\n      {...props}\n    >\n      <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n      {iconOnly ? null : label}\n    </button>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui",
  "docs": "Install any pulld component by name: add \"@pulld\": \"https://pulld.pages.dev/r/{name}.json\" to the registries block in components.json, then `npx shadcn add @pulld/<name>`. All 103 components: https://pulld.pages.dev/?utm_source=cli"
}
