{
  "$schema": "https://blode.co/ui/schema/registry-item.json",
  "name": "use-install-prompt",
  "title": "Use Install Prompt",
  "author": "Matthew Blode",
  "description": "A hook that exposes Chromium's deferred install prompt.",
  "files": [
    {
      "path": "hooks/use-install-prompt.ts",
      "content": "\"use client\";\n\nimport { useCallback, useSyncExternalStore } from \"react\";\n\n/**\n * The Chromium-only event that offers a real one-tap install. It is\n * non-standard, so it is typed here rather than imported from lib.dom.\n */\ninterface BeforeInstallPromptEvent extends Event {\n  prompt: () => Promise<{ outcome: \"accepted\" | \"dismissed\" }>;\n  userChoice: Promise<{ outcome: \"accepted\" | \"dismissed\" }>;\n}\n\ntype InstallOutcome = \"accepted\" | \"dismissed\" | \"unavailable\";\n\nlet deferredEvent: BeforeInstallPromptEvent | null = null;\nlet installed = false;\nconst listeners = new Set<() => void>();\n\nconst emit = () => {\n  for (const listener of listeners) {\n    listener();\n  }\n};\n\n/*\n * Registered at module scope rather than on first subscribe, which is unusual\n * for this registry and deliberate: `beforeinstallprompt` fires shortly after\n * load, normally before any component that wants it has mounted. A listener\n * attached inside `subscribe` would miss the only time the event ever fires,\n * and the install button would never appear.\n */\nif (typeof window !== \"undefined\") {\n  window.addEventListener(\"beforeinstallprompt\", (event) => {\n    // Without this Chromium shows its own mini-infobar and the event is spent.\n    event.preventDefault();\n    deferredEvent = event as BeforeInstallPromptEvent;\n    emit();\n  });\n\n  window.addEventListener(\"appinstalled\", () => {\n    deferredEvent = null;\n    installed = true;\n    emit();\n  });\n}\n\nconst subscribe = (onStoreChange: () => void) => {\n  listeners.add(onStoreChange);\n  return () => {\n    listeners.delete(onStoreChange);\n  };\n};\n\nconst getCanInstall = () => deferredEvent !== null;\nconst getIsInstalled = () => installed;\nconst getFalse = () => false;\n\n/** True once the browser has offered an install prompt this page can replay. */\nconst useCanInstall = () => useSyncExternalStore(subscribe, getCanInstall, getFalse);\n\n/** True once this page has been installed during this session. */\nconst useIsInstalled = () => useSyncExternalStore(subscribe, getIsInstalled, getFalse);\n\n/**\n * Wraps Chromium's deferred install prompt.\n *\n * `canInstall` is false everywhere the event does not exist — every Apple\n * browser, Firefox, and any Chromium page that does not meet the install\n * criteria, which still require a service worker with a fetch handler. Treat\n * it as an enhancement over written instructions, never a replacement.\n */\nconst useInstallPrompt = () => {\n  const canInstall = useCanInstall();\n  const isInstalled = useIsInstalled();\n\n  const promptInstall = useCallback(async (): Promise<InstallOutcome> => {\n    const event = deferredEvent;\n\n    if (!event) {\n      return \"unavailable\";\n    }\n\n    // The event is single-use: whatever the outcome, it cannot be replayed.\n    deferredEvent = null;\n    emit();\n\n    await event.prompt();\n    const { outcome } = await event.userChoice;\n    return outcome;\n  }, []);\n\n  return { canInstall, isInstalled, promptInstall };\n};\n\nexport { useInstallPrompt };\nexport type { InstallOutcome };\n",
      "type": "registry:lib",
      "target": ""
    }
  ],
  "type": "registry:lib"
}
