{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "notification-permission",
  "title": "Notification Permission",
  "description": "A button that asks for browser notification permission at a moment you choose, and refuses to ask when asking cannot work. Reach for it wherever a notification is the point rather than the decoration: a chat, support inbox or team channel that needs to reach somebody who has tabbed away, an alert or on-call dashboard, a long export, build, render or import that finishes minutes later, a CI run or deploy a developer is waiting on, a calendar or meeting reminder, a reminder, habit or medication app, a delivery or order-status page, a live auction or bid about to close, the other player's turn, a price or stock watch, and the settings screen where somebody goes looking for the switch afterwards. Common asks it answers: \"notification permission react\", \"Notification.requestPermission react\", \"ask for notification permission button\", \"browser notification permission component\", \"request notification permission shadcn\", \"useNotificationPermission hook\", \"web notifications react hook\", \"enable notifications button react\", \"notification permission denied forever\", \"can't ask for notification permission again\", \"Notification.requestPermission does nothing\", \"requestPermission resolves denied immediately\", \"how to reset notification permission\", \"notification permission three states\", \"default granted denied notification\", \"detect when user allows notifications\", \"listen for notification permission change\", \"navigator.permissions.query notifications\", \"new Notification does not show anything\", \"Notification constructor silently fails\", \"notification not showing but no error\", \"requestPermission callback vs promise\", \"notification permission in iframe\", \"notification permission ios safari\", \"notifications only work in installed pwa\", \"showNotification service worker react\", \"two enable notification buttons out of sync\", \"should i ask for notification permission on page load\". shadcn/ui ships nothing that touches notifications: measured against the live catalogue on 2026-10-28 (65 items indexed, the 62 fetchable ones concatenated to 223,287 bytes of source), Notification, requestPermission, navigator.permissions, PermissionStatus, permission, serviceWorker, showNotification and isSecureContext are all zero hits. pulld has nothing either — the single occurrence of the word anywhere in the registry is toast's aria-label=\"Notifications\", which is a string and not an API. It is the asking half of the family that pulld install-prompt and geolocation-button belong to, and the loud counterpart to pulld tab-title-badge, which tells a reader something is waiting without asking anyone for permission at all; on iOS, where notifications are reachable only from an installed app, install-prompt is the route to get there first. The reason to install this rather than write the four lines is that the four lines spend a thing you only get once, and the platform gives almost no sign of it. Measured in Chrome 152 rather than remembered. One: on an origin that has already been blocked, Notification.requestPermission() resolved \"denied\" in 0.3 milliseconds, with no prompt and without needing a user gesture to do it, then did the same three more times in under a tenth of a millisecond each. There is no retry and no second chance — the call is a lookup wearing the costume of a request — so a \"Try again\" button is decoration, and the only honest interface for that state says the browser will not ask again and points at site settings. This component refuses the press itself rather than handing it to an API that will answer instantly and identically forever. Two: new Notification(\"hello\") on a blocked origin does not throw. It constructs, returns an object, displays nothing, and fires an error event instead — measured: onerror fired within 600ms while onshow and onclose did not. So the natural `try { new Notification(t) } catch {}` reports success to a caller whose user is seeing nothing, permanently. notify() here checks the permission before constructing anything and reports from the event, never from the absence of an exception; when neither event arrives inside a second it answers \"unknown\" rather than claiming success it did not observe. Three: a sandboxed or cross-origin embed reads exactly like a refusal. Measured in an iframe with sandbox=\"allow-scripts\": location.origin is the string \"null\", Notification is still a function, permission reads \"denied\", and requestPermission() resolves \"denied\" with no prompt — identical to somebody who pressed Block, and the opposite advice. Telling that user to fix their site settings sends them hunting for a switch that does not exist, so denied carries a reason, \"blocked\" or \"embedded\", and the copy follows it. Four: Notification.permission is an accessor on the constructor (measured: a getter, no value), so it is never stale and will never tell React it changed. Reading it during render is a hydration mismatch as well, so the first paint is deterministic and an effect corrects it — the button renders nothing until it knows, rather than flashing an offer at somebody who accepted months ago. The live value arrives through navigator.permissions.query({ name: \"notifications\" }), which hands back a real PermissionStatus with a change event, which is what turns \"change it in your settings\" into advice that visibly works instead of a button that stays dead until a reload. A descriptor a browser does not recognise rejects with a TypeError rather than answering, so not knowing is handled as an ordinary answer and every path works without it. Five: requestPermission has two signatures and must be called once. The promise form is what runs today (measured: arity 0, returns a real Promise), but the callback form is older and is the only one some browsers shipped, where the call returns undefined and `await undefined` coerces to \"not granted\" — throwing away an answer the person gave. The obvious repair, await the promise and call again with a callback if it was undefined, asks twice: two prompts for one press, the second spending the chance the first was still waiting on. The deprecated callback is an optional trailing argument of the promise form, so one call serves both. Concurrent presses collapse too — the permission is one fact about the origin, not a property of a button, so a request in flight is shared and two controls on one page settle together. The API is the hook and the button. useNotificationPermission() returns status (\"default\" | \"granted\" | \"denied\" | \"unsupported\"), canAsk, deniedReason, isRequesting, isResolved, request() and notify(); request() resolves \"granted\", \"denied\", \"dismissed\" — a prompt closed without an answer stores nothing and really can be asked again, which is the one denial worth offering a retry for — or \"unavailable\". notify(title, options) prefers an existing service-worker registration where there is one and registers none of its own, falling back to the constructor. NotificationPermissionButton takes label, requestingLabel, grantedLabel, deniedLabel, unsupportedLabel, hideWhenGranted, iconOnly, hideMessage, per-situation messages, containerClassName and onOutcome. isNotificationSupported and isOpaqueOrigin are exported for the parts worth overriding. Accessible by construction: the message region is mounted empty from the first render rather than inserted when something happens, because a live region that arrives already holding its text is not reliably announced; the refusal is aria-disabled rather than disabled, so the control keeps its place in the tab order and the one explanation of why notifications are off reaches the people who most need it; the button carries aria-busy while the prompt is open, icons are aria-hidden, icon-only keeps the label as the accessible name, and a caller's own aria-describedby is merged rather than overwritten. Styled with shadcn tokens (input, accent, ring, muted-foreground) so it follows light and dark, className merges rather than fights, server-rendering safe, one file, no notification library.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/ui/notification-permission.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Bell, BellOff, BellRing, Loader2 } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * Whether the Notification API exists in this context.\n *\n * `\"Notification\" in window` rather than `typeof Notification`, because the second one is a\n * reference to a global that genuinely is not declared everywhere this file can be imported —\n * a server render, a worker, a test harness — and `typeof` on an undeclared identifier is the only\n * form that does not throw. The `in` check reads the same and is honest about what it asks.\n *\n * Note what it does not promise. It returning true is entirely compatible with every request being\n * refused without a prompt: measured in a sandboxed frame below, and true of any origin the person\n * has already blocked. Support is the first question, not the whole question.\n */\nexport function isNotificationSupported(): boolean {\n  if (typeof window === \"undefined\") return false\n  if (!(\"Notification\" in window)) return false\n  const api = (window as Window & { Notification?: typeof Notification }).Notification\n  return typeof api === \"function\" && typeof api.requestPermission === \"function\"\n}\n\n/**\n * Whether this document has an opaque origin, where the answer is \"denied\" and always was.\n *\n * Measured, in Chrome 152, from a page whose own permission was `denied`: a `<iframe sandbox=\"allow-scripts\">`\n * reports `location.origin === \"null\"`, `Notification` is still a function, `Notification.permission`\n * reads `\"denied\"`, and `requestPermission()` resolves `\"denied\"` with no prompt. So an embed gets\n * the identical reading to a person who pressed Block — and the advice that fits one is wrong for\n * the other. Telling an embedded widget's user to go and unblock notifications in their site\n * settings sends them looking for a switch that is not there, for an origin that has no settings.\n *\n * The string is `\"null\"` and not `null`: `location.origin` is serialised, and the opaque origin\n * serialises to those four characters. Comparing against the value `null` is the version of this\n * check that compiles, runs, and is never once true.\n */\nexport function isOpaqueOrigin(): boolean {\n  if (typeof location === \"undefined\") return false\n  return location.origin === \"null\"\n}\n\n/** Whether this document is inside a frame at all. Best-effort: a cross-origin parent can throw. */\nfunction isFramed(): boolean {\n  if (typeof window === \"undefined\") return false\n  try {\n    return window.self !== window.top\n  } catch {\n    // A cross-origin parent makes `window.top` unreadable, which is itself the answer.\n    return true\n  }\n}\n\n/**\n * The stored permission, read fresh.\n *\n * `Notification.permission` is an accessor, not a value — measured: its property descriptor on the\n * constructor has a getter and no `value`. That means it is never stale, and it also means nothing\n * will ever tell React that it changed. Every read in this file goes through here, and the state\n * that drives rendering is pushed by the subscription below rather than sampled during render.\n */\nfunction readPermission(): NotificationPermission | null {\n  if (!isNotificationSupported()) return null\n  return window.Notification.permission\n}\n\n/**\n * Reads the live permission object, or null where that cannot be asked.\n *\n * Wrapped in `try` because the Permissions API rejects rather than answers for a descriptor it does\n * not recognise — measured: `navigator.permissions.query({ name: \"not-a-real-permission\" })` throws\n * a `TypeError`, it does not resolve to anything. Browsers differ on which names they accept, so a\n * rejection here means \"this browser will not say\", which is an ordinary answer. Every path below\n * works without it; what it buys is the `change` event, and the `change` event is what makes the\n * dead end recoverable.\n */\nasync function queryNotificationPermission(): Promise<PermissionStatus | null> {\n  if (typeof navigator === \"undefined\" || !(\"permissions\" in navigator)) return null\n  const permissions: Permissions | undefined = navigator.permissions\n  if (!permissions || typeof permissions.query !== \"function\") return null\n  try {\n    return await permissions.query({ name: \"notifications\" as PermissionName })\n  } catch {\n    return null\n  }\n}\n\n/**\n * Everything mounted right now that wants to hear about a change.\n *\n * Module scope on purpose. The notification permission is one fact about the origin, not a property\n * of a button — measured: `Notification.permission` and the Permissions API agreed, and two\n * `requestPermission()` calls issued in the same tick both resolved with the same answer. So a page\n * with a button in the header and another in a settings panel has two views of one value, and the\n * one that is not pressed has to update too. Without this it keeps offering to enable notifications\n * that are already on.\n */\nconst listeners = new Set<() => void>()\n\nfunction emit(): void {\n  for (const listener of listeners) listener()\n}\n\n/**\n * The one request in flight, shared by every instance.\n *\n * Two buttons pressed in the same moment must not become two prompts, and the second press of one\n * button must not either. Reusing the promise is better than refusing the second call: both callers\n * get the real answer, rather than one of them getting a lie about being busy.\n */\nlet inFlight: Promise<NotificationRequestOutcome> | null = null\n\n/** What there is to show, and which of the four interfaces to show. */\nexport type NotificationPermissionStatus =\n  /** No Notification API here. Nothing to offer and nothing the person can do about it. */\n  | \"unsupported\"\n  /** Nothing stored. A press will put the browser's prompt in front of them — the one chance. */\n  | \"default\"\n  /** Allowed. */\n  | \"granted\"\n  /** Refused, and the browser will not ask again. See `canAsk`. */\n  | \"denied\"\n\n/** How a request ended. */\nexport type NotificationRequestOutcome =\n  | \"granted\"\n  /** Stored as a refusal. This is the end of the road for this origin. */\n  | \"denied\"\n  /** The prompt was closed without an answer, so nothing was stored and asking again still works. */\n  | \"dismissed\"\n  /** Never asked: unsupported, or already settled. */\n  | \"unavailable\"\n\n/** How a `notify()` call ended. */\nexport type NotifyOutcome =\n  | \"shown\"\n  /** Not attempted, because the permission is not `granted`. Nothing was constructed. */\n  | \"not-permitted\"\n  /** Attempted and the platform reported a failure. */\n  | \"failed\"\n  /** Attempted, and the platform said nothing either way inside the wait. */\n  | \"unknown\"\n  | \"unsupported\"\n\n/** Why `denied` is on screen, where that can be told apart. */\nexport type DeniedReason =\n  /** The ordinary one: a stored refusal for this origin, changeable only in site settings. */\n  | \"blocked\"\n  /** An opaque-origin embed, which reads `denied` whatever the top-level page's answer is. */\n  | \"embedded\"\n\nexport interface UseNotificationPermissionOptions {\n  /** Called once the browser's prompt has been answered, or closed. */\n  onOutcome?: (outcome: NotificationRequestOutcome) => void\n}\n\nexport interface NotificationPermissionState {\n  /** Which of the four situations this is. `\"default\"` until the first effect has run. */\n  status: NotificationPermissionStatus\n  /**\n   * Whether a press can still produce a prompt.\n   *\n   * This is the reason the component exists. Measured, on an origin whose permission was already\n   * `denied`: `Notification.requestPermission()` resolved with `\"denied\"` in **0.3 ms**, showed no\n   * prompt, and needed no user gesture to do it — then did the same thing three more times in a\n   * row, in under a tenth of a millisecond each. There is no retry, no backoff and no second\n   * chance; the call is a lookup wearing the costume of a request. A \"Try again\" button wired to\n   * it is a control that cannot work, and the only honest interface for that state says so and\n   * points at the browser's own settings.\n   */\n  canAsk: boolean\n  /** Set when `status` is `\"denied\"`, to tell a real refusal from an embed that never could ask. */\n  deniedReason: DeniedReason | null\n  /** Whether a prompt is open right now. */\n  isRequesting: boolean\n  /** False until the first effect has read the real permission. The button waits for it. */\n  isResolved: boolean\n  /**\n   * Ask. Call it straight from the click handler, with nothing awaited in front of it.\n   *\n   * Resolves `\"unavailable\"` without touching the API when there is nothing to ask.\n   */\n  request: () => Promise<NotificationRequestOutcome>\n  /** Show one notification, if and only if that is permitted. See the implementation note. */\n  notify: (title: string, options?: NotificationOptions) => Promise<NotifyOutcome>\n}\n\n/**\n * The permission, the one call that changes it, and the guard around using it.\n *\n * Four states and not three, because \"this browser cannot\" and \"this person said no\" are different\n * situations with different right answers, and the unsupported case is the one that gets dropped —\n * it never produces an error to render, so a component built from error states alone shows a live,\n * hopeful button on the one platform where it can never do anything.\n */\nexport function useNotificationPermission(\n  options: UseNotificationPermissionOptions = {}\n): NotificationPermissionState {\n  const { onOutcome } = options\n\n  // Deterministic on the server and on the first client render alike, then corrected by the effect\n  // below. Reading the real permission during render — or in a `useState` initialiser, which is the\n  // same bug with a hat on — is a hydration mismatch on every page that server-renders.\n  const [status, setStatus] = React.useState<NotificationPermissionStatus>(\"default\")\n  const [deniedReason, setDeniedReason] = React.useState<DeniedReason | null>(null)\n  const [isRequesting, setIsRequesting] = React.useState(false)\n  const [isResolved, setIsResolved] = React.useState(false)\n\n  const onOutcomeRef = React.useRef(onOutcome)\n  onOutcomeRef.current = onOutcome\n\n  const mountedRef = React.useRef(true)\n  // Declared before anything that reads it: under StrictMode the cleanup runs and the effects run\n  // again, and a later declaration would leave the second pass looking at `false`.\n  React.useEffect(() => {\n    mountedRef.current = true\n    return () => {\n      mountedRef.current = false\n    }\n  }, [])\n\n  React.useEffect(() => {\n    function read() {\n      const permission = readPermission()\n      const next: NotificationPermissionStatus = permission === null ? \"unsupported\" : permission\n      setStatus((previous) => (previous === next ? previous : next))\n      setDeniedReason(\n        next !== \"denied\" ? null : isOpaqueOrigin() || isFramed() ? \"embedded\" : \"blocked\"\n      )\n      setIsResolved(true)\n    }\n\n    // Subscribed before the first read, so an answer arriving from another instance in between is\n    // not dropped into the gap. Reading first and subscribing second is the ordinary version of\n    // this bug.\n    listeners.add(read)\n    read()\n    return () => {\n      listeners.delete(read)\n    }\n  }, [])\n\n  /**\n   * Keeps the dead end honest.\n   *\n   * A person sent to site settings by the `denied` copy comes back to a page that is still open,\n   * and the browser fires `change` on the status object the moment they flip the switch — measured:\n   * the object handed back for `{ name: \"notifications\" }` is a real `PermissionStatus` with an\n   * `onchange` slot. Handling it is what turns \"change it in your settings\" into advice that\n   * visibly works. Ignoring it leaves the button dead until a reload, which is the point at which\n   * people decide the site is broken.\n   */\n  React.useEffect(() => {\n    let status: PermissionStatus | null = null\n    let cancelled = false\n\n    const handleChange = () => {\n      // Deliberately re-read `Notification.permission` rather than trusting `status.state`. The two\n      // agreed when measured, and this is the one that the rest of the file acts on, so a browser\n      // where they ever disagree should move the UI to the value that governs behaviour.\n      emit()\n    }\n\n    void queryNotificationPermission().then((result) => {\n      if (cancelled || !result) return\n      status = result\n      result.addEventListener(\"change\", handleChange)\n    })\n\n    return () => {\n      cancelled = true\n      status?.removeEventListener(\"change\", handleChange)\n    }\n  }, [])\n\n  const request = React.useCallback(async (): Promise<NotificationRequestOutcome> => {\n    if (!isNotificationSupported()) return \"unavailable\"\n    // Short-circuited here rather than left to the browser, so that nothing in this component ever\n    // depends on the 0.3 ms no-op described on `canAsk`. A press that cannot produce a prompt is\n    // refused by the component, visibly, instead of being sent to an API that will answer\n    // instantly and identically forever.\n    if (window.Notification.permission !== \"default\") return \"unavailable\"\n\n    if (inFlight) return inFlight\n\n    setIsRequesting(true)\n    const run = (async (): Promise<NotificationRequestOutcome> => {\n      try {\n        // Called with nothing awaited in front of it, and that is why this function does its\n        // checks against synchronous reads only. Browsers honour the prompt while the click that\n        // led here is still the active user gesture, so an `await` first — asking the Permissions\n        // API whether to bother, reading a flag from storage — is what turns a working button into\n        // a prompt that never appears.\n        //\n        // The promise form is the one in use (measured: `requestPermission.length` is 0 and the\n        // call returns a real `Promise`), but the callback form is older than the promise and is\n        // the only form some browsers ever shipped, where the call returns `undefined` and\n        // `await undefined` is `undefined` — which coerces to \"not granted\" and throws away an\n        // answer the person actually gave.\n        //\n        // Both forms are served by **one** call, and that is the whole subtlety. The obvious\n        // repair — await the promise, and if it was undefined call again with a callback — asks\n        // twice, which on the browsers it is meant to rescue means two prompts for one press, and\n        // the second one spends the single chance that the first was still waiting on. The IDL has\n        // carried the callback as an optional trailing argument since the promise was added\n        // (`requestPermission(optional NotificationPermissionCallback deprecatedCallback)`), so\n        // passing it is legal on both and whichever form this browser implements answers. The\n        // guard is for the browser that honours both: first answer wins, the other is dropped.\n        const answer: NotificationPermission = await new Promise<NotificationPermission>(\n          (resolve) => {\n            // No guard against being answered twice, because `resolve` already is one: a browser\n            // that honours both signatures calls this with the same answer twice and the second\n            // call is a no-op. A flag here would read as though it were protecting something.\n            const deliver = resolve\n            const returned: Promise<NotificationPermission> | undefined =\n              window.Notification.requestPermission(deliver)\n            // Deliberately not raced against a timeout. A prompt left open is not a failure — it\n            // is somebody deciding — and cutting it off would report a refusal nobody made.\n            if (returned && typeof returned.then === \"function\") {\n              returned.then(deliver, () => deliver(window.Notification.permission))\n            }\n          }\n        )\n\n        // The resolved value *is* the resulting permission, so `\"default\"` means the prompt was\n        // closed without an answer: nothing was stored, and asking again is a real offer rather\n        // than the no-op that `\"denied\"` would make it.\n        return answer === \"granted\" ? \"granted\" : answer === \"denied\" ? \"denied\" : \"dismissed\"\n      } catch {\n        return \"unavailable\"\n      }\n    })()\n\n    inFlight = run\n    const outcome = await run\n    inFlight = null\n\n    // Published to every instance before this one's own state is touched, so two buttons on one\n    // page settle together.\n    emit()\n    if (mountedRef.current) setIsRequesting(false)\n    onOutcomeRef.current?.(outcome)\n    return outcome\n  }, [])\n\n  /**\n   * Shows a notification, and reports what actually happened.\n   *\n   * The guard is not politeness, it is the whole method. Measured, on a `denied` origin:\n   * `new Notification(\"probe\")` **does not throw**. It constructs, returns an object, and nothing\n   * appears anywhere. So the natural-looking version —\n   *\n   * ```ts\n   * try { new Notification(title); return true } catch { return false }\n   * ```\n   *\n   * — returns `true` to a caller whose user is seeing nothing at all, and will keep returning it\n   * forever. What the platform does instead is fire an `error` event on the notification object:\n   * measured, `onerror` fired for the blocked probe within 600 ms, while `onshow` and `onclose`\n   * did not. Both facts are used below — the permission is checked before anything is constructed,\n   * and the event, not the absence of an exception, is what \"shown\" means.\n   *\n   * The service-worker route is preferred where a registration already exists, because\n   * `ServiceWorkerRegistration.prototype.showNotification` is the form that survives platforms\n   * where the constructor is unavailable (measured present here). Nothing is registered on the\n   * caller's behalf: if there is no service worker, the constructor is used, and if there is no\n   * constructor either, that is reported rather than guessed at.\n   */\n  const notify = React.useCallback(\n    async (title: string, notificationOptions?: NotificationOptions): Promise<NotifyOutcome> => {\n      if (!isNotificationSupported()) return \"unsupported\"\n      if (window.Notification.permission !== \"granted\") return \"not-permitted\"\n\n      const registration =\n        typeof navigator !== \"undefined\" && \"serviceWorker\" in navigator\n          ? await navigator.serviceWorker.getRegistration().catch(() => undefined)\n          : undefined\n\n      if (registration && typeof registration.showNotification === \"function\") {\n        try {\n          await registration.showNotification(title, notificationOptions)\n          return \"shown\"\n        } catch {\n          return \"failed\"\n        }\n      }\n\n      try {\n        const notification = new window.Notification(title, notificationOptions)\n        return await new Promise<NotifyOutcome>((resolve) => {\n          // Bounded, because a platform that fires neither event must not leave the caller's\n          // promise pending for the life of the page. What it resolves with then is `\"unknown\"`\n          // and not `\"shown\"` — reporting success from the absence of evidence is the exact bug\n          // this method is written to avoid, and a caller that wants to treat silence as success\n          // can, knowingly.\n          const timer = setTimeout(() => resolve(\"unknown\"), 1000)\n          const settle = (outcome: NotifyOutcome) => () => {\n            clearTimeout(timer)\n            resolve(outcome)\n          }\n          notification.addEventListener(\"show\", settle(\"shown\"))\n          notification.addEventListener(\"error\", settle(\"failed\"))\n        })\n      } catch {\n        return \"failed\"\n      }\n    },\n    []\n  )\n\n  return {\n    status,\n    canAsk: status === \"default\",\n    deniedReason,\n    isRequesting,\n    isResolved,\n    request,\n    notify,\n  }\n}\n\nconst DEFAULT_MESSAGES: Record<NotificationPermissionStatus | DeniedReason, string> = {\n  unsupported: \"This browser can't show notifications.\",\n  default: \"\",\n  granted: \"Notifications are on.\",\n  denied: \"\",\n  blocked:\n    \"Notifications are blocked for this site. Turn them back on in your browser's site settings — this page can't ask again.\",\n  embedded: \"Notifications aren't available inside this embed.\",\n}\n\nconst BUTTON_CLASS =\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\"\n\nexport interface NotificationPermissionButtonProps\n  extends Omit<React.ComponentPropsWithoutRef<\"button\">, \"children\">,\n    UseNotificationPermissionOptions {\n  /** Resting label, shown when a press will produce the prompt. */\n  label?: string\n  /** While the browser's prompt is open. */\n  requestingLabel?: string\n  /** Once allowed. */\n  grantedLabel?: string\n  /** Once refused. Not an action — see `canAsk`. */\n  deniedLabel?: string\n  /** Where notifications do not exist at all. */\n  unsupportedLabel?: string\n  /** Render nothing once the permission is granted, instead of showing the settled state. */\n  hideWhenGranted?: boolean\n  /** Drop the visible text and keep it as the accessible name. */\n  iconOnly?: boolean\n  /** Hide the message under the button. It stays in the live region either way. */\n  hideMessage?: boolean\n  /** Per-situation wording, merged over the defaults. */\n  messages?: Partial<Record<NotificationPermissionStatus | DeniedReason, string>>\n  /** Class for the wrapper. `className` goes to the button. */\n  containerClassName?: string\n}\n\n/**\n * A button that asks for notification permission at a moment you choose, and never twice.\n *\n * It renders nothing until the first effect has read the real permission, so a page that\n * server-renders does not flash an offer at somebody who accepted months ago. After that it is one\n * of four things, and only one of them is a button that does anything.\n *\n * Mount it where the value is obvious — beside the thing that will send the notifications, after\n * the person has done something that implies they want them — and not on load. That placement is\n * the entire product decision here, and it is the caller's: the component's job is to make the\n * cost of getting it wrong visible, by refusing to pretend a refusal can be undone.\n *\n * Every colour is a shadcn token, so it follows light and dark. For a control of your own design,\n * take `useNotificationPermission` and leave this one out; all of the behaviour is in the hook.\n */\nexport function NotificationPermissionButton({\n  label = \"Enable notifications\",\n  requestingLabel = \"Waiting for permission…\",\n  grantedLabel = \"Notifications on\",\n  deniedLabel = \"Notifications blocked\",\n  unsupportedLabel = \"Notifications unavailable\",\n  hideWhenGranted = false,\n  iconOnly = false,\n  hideMessage = false,\n  messages,\n  containerClassName,\n  onOutcome,\n  onClick,\n  className,\n  ...props\n}: NotificationPermissionButtonProps) {\n  const { status, canAsk, deniedReason, isRequesting, isResolved, request } =\n    useNotificationPermission({ onOutcome })\n\n  // Referenced by id from the button, so it needs one that survives hydration.\n  const messageId = React.useId()\n\n  if (!isResolved) return null\n  if (status === \"granted\" && hideWhenGranted) return null\n\n  const actionable = canAsk && !isRequesting\n\n  function handleClick(event: React.MouseEvent<HTMLButtonElement>) {\n    onClick?.(event)\n    if (event.defaultPrevented || !actionable) return\n    // Not awaited, and nothing is awaited before it. See the note inside `request`.\n    void request()\n  }\n\n  const visibleLabel = isRequesting\n    ? requestingLabel\n    : status === \"granted\"\n      ? grantedLabel\n      : status === \"denied\"\n        ? deniedLabel\n        : status === \"unsupported\"\n          ? unsupportedLabel\n          : label\n\n  // The denied reason is more specific than the status, so it wins where there is one.\n  const messageKey: NotificationPermissionStatus | DeniedReason =\n    status === \"denied\" && deniedReason ? deniedReason : status\n  const message = messages?.[messageKey] ?? DEFAULT_MESSAGES[messageKey]\n\n  const Icon = isRequesting\n    ? Loader2\n    : status === \"granted\"\n      ? BellRing\n      : status === \"default\"\n        ? Bell\n        : BellOff\n\n  // The caller's own description first, the reason last: the reason is the newer and more urgent of\n  // the two, and reading it last keeps it from being buried under a static hint.\n  const describedBy = [props[\"aria-describedby\"], message ? messageId : null]\n    .filter(Boolean)\n    .join(\" \")\n\n  return (\n    <div className={cn(\"flex flex-col items-start gap-2\", containerClassName)}>\n      <button\n        type=\"button\"\n        onClick={handleClick}\n        // `aria-disabled` rather than `disabled`, so the control keeps its place in the tab order\n        // and can still be reached and read. A real `disabled` button is skipped entirely, which\n        // delivers the one explanation of why notifications are off to everybody except the people\n        // who most need it. The click handler above is what refuses.\n        aria-disabled={actionable ? undefined : true}\n        aria-busy={isRequesting || undefined}\n        aria-label={iconOnly ? visibleLabel : undefined}\n        data-status={status}\n        data-denied-reason={deniedReason ?? undefined}\n        className={cn(BUTTON_CLASS, iconOnly ? \"w-9\" : \"px-4\", className)}\n        {...props}\n        // After the spread, and the only attribute here that is: this one has to be the merge of\n        // the caller's value and ours, so letting `{...props}` have the last word on it is the one\n        // ordering that loses information. A control inside a settings form passing its own hint —\n        // the ordinary thing to do — would otherwise drop the single line that says why\n        // notifications cannot be turned on.\n        aria-describedby={describedBy || undefined}\n      >\n        <Icon className={cn(\"h-4 w-4\", isRequesting && \"animate-spin\")} aria-hidden=\"true\" />\n        {iconOnly ? null : visibleLabel}\n      </button>\n      {/*\n        Mounted from the start and left empty, never conditionally rendered. A live region inserted\n        into the document already holding its text is not reliably announced — the region has to\n        exist for the browser to notice the text changing inside it — so the version that only\n        appears when something goes wrong is silent for exactly the users depending on it.\n      */}\n      <p\n        id={messageId}\n        role=\"status\"\n        aria-live=\"polite\"\n        className={cn(\"text-sm text-muted-foreground\", (hideMessage || !message) && \"sr-only\")}\n      >\n        {message}\n      </p>\n    </div>\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 140 components: https://pulld.pages.dev/?utm_source=cli"
}
