{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "theme-script",
  "type": "registry:lib",
  "title": "No-flash theme bootstrap script",
  "description": "@interlace/ui — the inline `<head>` script that applies the stored theme before first paint. Without it every page load flashes the default theme.",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "foundation",
    "util"
  ],
  "dependencies": [],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme-tokens.json"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/lib/theme-script.ts",
      "target": "lib/theme-script.ts",
      "type": "registry:lib",
      "content": "import { DEFAULT_THEME, THEMES } from '@/lib/theme-tokens';\n\n// @interlace/theme-script v1.0.1 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/theme-script\n// What changed since: https://ds.interlace.tools/c/theme-script#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\n/**\n * @interlace/ui — the no-flash theme bootstrap.\n *\n * A theme applied after hydration is a WHITE FLASH on every page load: the\n * document paints `:root` (Interlace · light) first, React mounts, an effect\n * reads localStorage, and only then does the page repaint into the dark or\n * re-branded palette the user actually chose. On a fast connection that is\n * one frame of the wrong colours; on a slow one it is half a second of a\n * blinding white page. It is the single most visible failure mode of a theme\n * system, and no amount of correctness in the hook fixes it — by the time\n * any React code runs, the wrong paint has already happened.\n *\n * The only fix is to write the DOM attributes BEFORE first paint, which means\n * a synchronous, blocking, inline `<script>` in `<head>`. Hence a string\n * constant rather than a component: it has to be inlined by the host\n * document, and the host is the only thing that can put it there.\n *\n * ─── How consumers use it ─────────────────────────────────────────\n *\n * Next.js App Router (`app/layout.tsx`) — server component, no 'use client':\n *\n * ```tsx\n * import { THEME_SCRIPT } from '@interlace/ui/theme-script';\n *\n * <html lang=\"en\" suppressHydrationWarning>\n *   <head>\n *     <script dangerouslySetInnerHTML={{ __html: THEME_SCRIPT }} />\n *   </head>\n * ```\n *\n * `suppressHydrationWarning` on `<html>` is required and is the same thing\n * next-themes asks for: the script deliberately mutates the element React is\n * about to hydrate, so the server markup and the client DOM differ by\n * design. It suppresses the warning for that one element's attributes only.\n *\n * Vite / plain HTML: paste the string into `<head>` as an inline script, or\n * emit it at build time. It has no imports and no dependencies.\n *\n * ─── What it deliberately does NOT do ─────────────────────────────\n *\n * No feature detection beyond what it needs, no polyfills, no error\n * reporting. It is wrapped in one `try` and fails silently, because a\n * blocking head script that throws is a worse outcome than a page in the\n * default theme: Safari in private mode throws on `localStorage` access,\n * and a broken page for those users is not a trade worth making for a\n * console message nobody reads.\n */\n\n/** `localStorage` key holding the chosen theme name. */\nexport const THEME_STORAGE_KEY = 'interlace-theme';\n\n/**\n * `localStorage` key holding the chosen colour scheme: `'light'`, `'dark'`,\n * or absent. ABSENT IS MEANINGFUL — it is \"the user has expressed no\n * preference\", which is what makes `prefers-color-scheme` authoritative.\n * Storing the string `'system'` would work too, but then a user who has\n * never touched the switcher and a user who explicitly chose \"system\" are\n * indistinguishable from the OS's point of view, and a future default\n * change silently overrides the second one.\n */\nexport const SCHEME_STORAGE_KEY = 'interlace-scheme';\n\n/**\n * Names the bootstrap will accept out of `localStorage`, minus the default\n * (which is written as no attribute at all — `:root` already IS that theme).\n *\n * Validating against the registry matters: `localStorage` is user-writable\n * and survives forever, so a theme that shipped once and was later removed\n * would otherwise keep writing `data-theme=\"…\"` for a selector no stylesheet\n * defines — a page that silently renders half-default, half-nothing.\n */\nconst NON_DEFAULT_THEMES = THEMES.filter((t) => t.name !== DEFAULT_THEME).map(\n  (t) => t.name,\n);\n\n/**\n * The bootstrap itself — an IIFE, minified by hand because it ships as\n * literal bytes in every page's `<head>` and no bundler will ever see it.\n *\n * Derived from the registry (`THEMES`) and the storage keys above, so adding\n * a theme cannot leave the bootstrap behind.\n *\n * What it does, in order:\n *   1. read the stored theme; write `data-theme` only when it is a\n *      registered NON-default theme (the default is `:root`, so writing it\n *      would be noise);\n *   2. read the stored scheme; when absent or corrupt, fall back to\n *      `prefers-color-scheme` — the OS preference is the correct default,\n *      not `light`;\n *   3. toggle the `.dark` class (shadcn / next-themes canon — the selector\n *      every consumer and the Storybook decorator already write);\n *   4. set `style.color-scheme`, so form controls, scrollbars and the\n *      canvas the browser paints BEFORE any CSS also match. Without it the\n *      page is dark and the scrollbar is white.\n */\n/**\n * JSON for embedding inside a `<script>` element.\n *\n * `JSON.stringify` alone is not enough (CodeQL js/bad-code-sanitization): a\n * value containing `</script>` closes the tag from inside a string literal,\n * and `<!--` opens an HTML comment that swallows the rest of the script.\n * Escaping `<` as `\\u003c` is inert in JS and closes both. Every value here is\n * a build-time constant today — this keeps that safe if one ever becomes a\n * prop, which is precisely when nobody would think to re-check it.\n */\nconst jsonForScript = (value: unknown): string =>\n  JSON.stringify(value).replace(/</g, '\\\\u003c');\n\nexport const THEME_SCRIPT = `(function(){try{var d=document.documentElement,l=window.localStorage;var t=l.getItem(${jsonForScript(\n  THEME_STORAGE_KEY,\n)});if(${jsonForScript(\n  NON_DEFAULT_THEMES,\n)}.indexOf(t)>-1){d.setAttribute('data-theme',t)}else{d.removeAttribute('data-theme')}var s=l.getItem(${jsonForScript(\n  SCHEME_STORAGE_KEY,\n)});if(s!=='light'&&s!=='dark'){s=window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}d.classList.toggle('dark',s==='dark');d.style.colorScheme=s}catch(e){}})();`;\n"
    }
  ],
  "meta": {
    "tier": "util",
    "client": false,
    "minViewport": null,
    "loading": false,
    "version": "1.0.1",
    "since": "1.0.0"
  },
  "docs": "## @interlace/theme-script\n\nInstalled to `lib/theme-script.ts`.\n\n```tsx\nimport { /* … */ } from '@/lib/theme-script';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/theme-script"
}
