{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "theme-customize",
  "title": "Theme Customize",
  "type": "registry:page",
  "files": [
    {
      "path": "packages/registry-svelte/blocks/theme-customize/ThemeCustomize.svelte",
      "content": "<script lang=\"ts\" module>\n  export interface ThemeCustomizeProps {\n    class?: string\n  }\n</script>\n\n<script lang=\"ts\">\n  import { onDestroy, onMount, tick, untrack } from 'svelte'\n  import type { Component } from 'svelte'\n  import { Check, Copy, Download, Monitor, Moon, Palette, Pipette, RotateCcw, Shuffle, Sun } from '@lucide/svelte'\n  import { Button } from '$lib/components/ui/button'\n  import { Slider } from '$lib/components/ui/slider'\n  import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '$lib/components/ui/sheet'\n  import {\n    CATEGORY_LABELS,\n    CATEGORY_ORDER,\n    FONTS,\n    PRESETS,\n    STORAGE_KEY,\n    THEMES,\n    TINTS,\n    type CategoryKey,\n    type FontKey,\n    type Mode,\n    type PresetKey,\n    type ThemePreset,\n    type TintKey,\n  } from './theme-data'\n  import { themeState } from './theme.svelte'\n\n  let { class: className }: ThemeCustomizeProps = $props()\n\n  let preset: PresetKey = $state('neutral')\n  let customColor = $state('#ec4899')\n  let font: FontKey = $state('sans')\n  let tint: TintKey = $state('pure')\n  let radius = $state(0.3)\n  let copied = $state(false)\n  let exported = $state(false)\n  let themeSearch = $state('')\n  let sheetOpen = $state(false)\n\n  const activeTheme = $derived<ThemePreset | null>(\n    // Radius isn't part of the preset signature any more (it stays\n    // user-controlled), so the match only checks the four token axes.\n    (THEMES.find(\n      (t) => t.mode === themeState.current && t.color === preset && t.tint === tint && t.font === font,\n    ) ?? null),\n  )\n\n  const filteredThemes = $derived.by(() => {\n    const q = themeSearch.trim().toLowerCase()\n    if (!q) return THEMES\n    return THEMES.filter((t) => t.label.toLowerCase().includes(q) || t.key.includes(q))\n  })\n\n  const themesByCategory = $derived.by<{ key: CategoryKey; label: string; items: ThemePreset[] }[]>(() =>\n    CATEGORY_ORDER.map((c) => ({\n      key: c,\n      label: CATEGORY_LABELS[c],\n      items: filteredThemes.filter((t) => t.category === c),\n    })).filter((g) => g.items.length > 0),\n  )\n\n  const modes: { value: Mode; label: string; icon: Component }[] = [\n    { value: 'light', label: 'Light', icon: Sun },\n    { value: 'dark', label: 'Dark', icon: Moon },\n    { value: 'system', label: 'System', icon: Monitor },\n  ]\n\n  function isDarkActive(): boolean {\n    if (typeof window === 'undefined') return false\n    return document.documentElement.classList.contains('dark')\n  }\n\n  function applyPreset(key: PresetKey) {\n    if (typeof window === 'undefined') return\n    const root = document.documentElement\n    if (key === 'custom') {\n      root.style.setProperty('--primary', customColor)\n      root.style.setProperty('--primary-foreground', 'oklch(0.985 0 0)')\n      root.style.setProperty('--ring', customColor)\n      return\n    }\n    const found = PRESETS.find((p) => p.key === key)\n    if (!found) return\n    const tokens = isDarkActive() ? found.dark : found.light\n    root.style.setProperty('--primary', tokens.primary)\n    root.style.setProperty('--primary-foreground', tokens.primaryForeground)\n    root.style.setProperty('--ring', tokens.ring)\n  }\n\n  function applyTint(key: TintKey) {\n    if (typeof window === 'undefined') return\n    const t = TINTS.find((x) => x.key === key)\n    if (!t) return\n    document.documentElement.style.setProperty('--background', isDarkActive() ? t.dark : t.light)\n  }\n\n  function applyFont(key: FontKey) {\n    if (typeof window === 'undefined') return\n    const f = FONTS.find((x) => x.key === key)\n    if (!f) return\n    document.documentElement.style.setProperty('--font-sans', f.stack)\n  }\n\n  function applyRadius(rem: number) {\n    if (typeof window === 'undefined') return\n    document.documentElement.style.setProperty('--radius', `${rem}rem`)\n  }\n\n  function save() {\n    try {\n      localStorage.setItem(\n        STORAGE_KEY,\n        JSON.stringify({ preset, customColor, font, tint, radius }),\n      )\n    } catch {\n      // ignore\n    }\n  }\n\n  function load() {\n    try {\n      const raw = localStorage.getItem(STORAGE_KEY)\n      if (!raw) return\n      const parsed = JSON.parse(raw)\n      if (parsed.preset) preset = parsed.preset\n      if (parsed.customColor) customColor = parsed.customColor\n      if (parsed.font) font = parsed.font\n      if (parsed.tint) tint = parsed.tint\n      if (typeof parsed.radius === 'number') radius = parsed.radius\n    } catch {\n      // ignore\n    }\n  }\n\n  function applyAll() {\n    applyPreset(preset)\n    applyTint(tint)\n    applyFont(font)\n    applyRadius(radius)\n  }\n\n  function applyTheme(t: ThemePreset) {\n    // Themes drive mode / color / tint / font. Radius is treated as a\n    // user-controlled value (default 0.3) and is not changed by presets.\n    themeState.set(t.mode)\n    preset = t.color\n    tint = t.tint\n    font = t.font\n  }\n\n  function applyRandomTheme() {\n    const candidates = THEMES.filter((t) => t.key !== activeTheme?.key)\n    const pick = candidates[Math.floor(Math.random() * candidates.length)]\n    if (pick) applyTheme(pick)\n  }\n\n  function reset() {\n    preset = 'neutral'\n    customColor = '#ec4899'\n    font = 'sans'\n    tint = 'pure'\n    radius = 0.3\n    const root = document.documentElement\n    ;['--primary', '--primary-foreground', '--ring', '--background', '--font-sans', '--radius'].forEach((p) =>\n      root.style.removeProperty(p),\n    )\n    try {\n      localStorage.removeItem(STORAGE_KEY)\n    } catch {\n      // ignore\n    }\n  }\n\n  function buildCss(): string {\n    const found = PRESETS.find((p) => p.key === preset) ?? PRESETS[0]\n    const isCustom = preset === 'custom'\n    const tokens = isDarkActive() ? found.dark : found.light\n    const t = TINTS.find((x) => x.key === tint) ?? TINTS[0]\n    const f = FONTS.find((x) => x.key === font) ?? FONTS[0]\n    const primary = isCustom ? customColor : tokens.primary\n    const primaryFg = isCustom ? 'oklch(0.985 0 0)' : tokens.primaryForeground\n    const ring = isCustom ? customColor : tokens.ring\n    return `:root {\n  --background: ${isDarkActive() ? t.dark : t.light};\n  --primary: ${primary};\n  --primary-foreground: ${primaryFg};\n  --ring: ${ring};\n  --radius: ${radius}rem;\n  --font-sans: ${f.stack};\n}`\n  }\n\n  async function copyCss() {\n    try {\n      await navigator.clipboard.writeText(buildCss())\n      copied = true\n      setTimeout(() => (copied = false), 1600)\n    } catch {\n      // ignore\n    }\n  }\n\n  function exportJson() {\n    const blob = new Blob(\n      [JSON.stringify({ preset, customColor, font, tint, radius, css: buildCss() }, null, 2)],\n      { type: 'application/json' },\n    )\n    const url = URL.createObjectURL(blob)\n    const a = document.createElement('a')\n    a.href = url\n    a.download = 'theme.json'\n    a.click()\n    URL.revokeObjectURL(url)\n    exported = true\n    setTimeout(() => (exported = false), 1600)\n  }\n\n  // Token watchers: apply + persist. save() is untracked so a persist never\n  // re-triggers the apply it just performed.\n  $effect(() => {\n    applyPreset(preset)\n    untrack(save)\n  })\n  $effect(() => {\n    applyFont(font)\n    untrack(save)\n  })\n  $effect(() => {\n    applyTint(tint)\n    untrack(save)\n  })\n  $effect(() => {\n    applyRadius(radius)\n    untrack(save)\n  })\n\n  $effect(() => {\n    // Re-apply color tokens once the mode flip has committed the dark class.\n    void themeState.current\n    tick().then(() => {\n      applyPreset(preset)\n      applyTint(tint)\n    })\n  })\n\n  // Smoothly fade color changes on the whole document. Toggled briefly\n  // during apply* so users don't see a hard snap between themes.\n  function pulseTransition() {\n    if (typeof document === 'undefined') return\n    const root = document.documentElement\n    root.style.transition = 'background-color 240ms ease, color 240ms ease'\n    window.setTimeout(() => {\n      root.style.transition = ''\n    }, 300)\n  }\n\n  $effect(() => {\n    void preset\n    void customColor\n    void font\n    void tint\n    void radius\n    void themeState.current\n    untrack(pulseTransition)\n  })\n\n  function onKeydown(e: KeyboardEvent) {\n    // Cmd/Ctrl + J -> toggle the customizer\n    if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'j') {\n      e.preventDefault()\n      sheetOpen = !sheetOpen\n    }\n  }\n\n  onMount(() => {\n    load()\n    themeState.apply()\n    applyAll()\n    if (typeof window !== 'undefined') window.addEventListener('keydown', onKeydown)\n  })\n\n  onDestroy(() => {\n    if (typeof window !== 'undefined') window.removeEventListener('keydown', onKeydown)\n  })\n\n  const activeSwatch = $derived(PRESETS.find((p) => p.key === activeTheme?.color)?.swatch)\n</script>\n\n<!-- Sheet renders only its children, so the slot marker lives on a layout-free wrapper. -->\n<div data-slot=\"theme-customize\" class={['contents', className]}>\n<Sheet bind:open={sheetOpen}>\n  <SheetTrigger>\n    {#snippet child({ props })}\n      <!-- class after the spread: trigger props carry no classes of their own. -->\n      <Button variant=\"outline\" size=\"sm\" {...props} class=\"gap-2\">\n        <Palette class=\"size-4\" />\n        <span class=\"hidden sm:inline\">Customize</span>\n        <kbd class=\"bg-muted text-muted-foreground hidden rounded px-1 py-0.5 text-xs font-medium sm:inline-block\"\n          >⌘J</kbd\n        >\n      </Button>\n    {/snippet}\n  </SheetTrigger>\n  <SheetContent side=\"right\" class=\"flex w-full flex-col gap-0 p-0 sm:max-w-md\">\n    <SheetHeader class=\"border-b px-6 py-4\">\n      <div class=\"flex items-center justify-between gap-3\">\n        <SheetTitle class=\"flex items-center gap-2 text-base\"><Palette class=\"size-4\" /> Customize</SheetTitle>\n        <!-- Active theme chip -->\n        {#if activeTheme}\n          <span\n            class=\"bg-muted text-foreground inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium\"\n          >\n            <span class=\"size-2 rounded-full shadow-2xs\" style:background={activeSwatch}></span>\n            {activeTheme.label}\n          </span>\n        {:else}\n          <span class=\"text-muted-foreground bg-muted/50 rounded-full px-2 py-0.5 text-xs font-medium\">Custom</span>\n        {/if}\n      </div>\n      <SheetDescription class=\"text-xs\">\n        Live-edit tokens. Changes write to <code class=\"font-mono\">:root</code> and persist locally.\n      </SheetDescription>\n      <!-- Quick actions -->\n      <div class=\"mt-2 flex gap-2\">\n        <Button variant=\"outline\" size=\"xs\" class=\"gap-1.5\" onclick={applyRandomTheme}>\n          <Shuffle class=\"size-3\" /> Surprise me\n        </Button>\n      </div>\n    </SheetHeader>\n    <div class=\"flex-1 overflow-y-auto px-6 py-5\">\n      <div class=\"space-y-6\">\n        <!-- Curated themes -->\n        <div>\n          <div class=\"mb-2 flex items-baseline justify-between\">\n            <span class=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">Themes</span>\n            <span class=\"text-muted-foreground text-xs\">\n              {filteredThemes.length}{#if themeSearch} / {THEMES.length}{/if} presets\n            </span>\n          </div>\n\n          <!-- Search -->\n          <div class=\"relative mb-3\">\n            <input\n              bind:value={themeSearch}\n              type=\"search\"\n              placeholder=\"Search themes...\"\n              class=\"border-input bg-background focus-visible:ring-ring h-8 w-full rounded-md border pr-2 pl-7 text-xs outline-none focus-visible:ring-2\"\n            />\n            <svg\n              viewBox=\"0 0 24 24\"\n              class=\"text-muted-foreground absolute top-1/2 left-2 size-3.5 -translate-y-1/2\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              aria-hidden=\"true\"\n            >\n              <circle cx=\"11\" cy=\"11\" r=\"7\" />\n              <path d=\"m21 21-4.3-4.3\" />\n            </svg>\n          </div>\n\n          <!-- Categorized groups -->\n          <div class=\"space-y-4\">\n            {#each themesByCategory as group (group.key)}\n              <div>\n                <div class=\"text-muted-foreground/70 mb-1.5 text-xs font-medium tracking-wider uppercase\">\n                  {group.label}\n                </div>\n                <div class=\"grid grid-cols-3 gap-2\">\n                  {#each group.items as t (t.key)}\n                    {@const tintOption = TINTS.find((x) => x.key === t.tint)}\n                    {@const colorSwatch = PRESETS.find((p) => p.key === t.color)?.swatch}\n                    {@const fontStack = FONTS.find((f) => f.key === t.font)?.stack}\n                    {@const isActive = activeTheme?.key === t.key}\n                    {@const previewBg = t.mode === 'dark' ? tintOption?.dark : tintOption?.light}\n                    {@const previewFg = t.mode === 'dark' ? 'oklch(0.985 0 0)' : 'oklch(0.205 0 0)'}\n                    <button\n                      type=\"button\"\n                      title={`${t.mode} · ${t.color} · ${t.font} · r=${t.radius}`}\n                      aria-pressed={isActive}\n                      class={[\n                        'group relative flex flex-col overflow-hidden rounded-lg border text-left transition-[box-shadow,scale]',\n                        isActive\n                          ? 'ring-ring ring-offset-background border-transparent ring-2 ring-offset-2'\n                          : 'hover:scale-[1.03] hover:shadow-md',\n                      ]}\n                      onclick={() => applyTheme(t)}\n                    >\n                      <!-- Mini-preview window -->\n                      <div\n                        class=\"relative h-14 px-2 py-1.5\"\n                        style:background={previewBg}\n                        style:color={previewFg}\n                      >\n                        <!-- Top row: dot + brand swatch dot -->\n                        <div class=\"flex items-center justify-between\">\n                          <div class=\"flex gap-0.5\">\n                            <span class=\"size-1.5 rounded-full opacity-40\" style:background=\"currentColor\"></span>\n                            <span class=\"size-1.5 rounded-full opacity-25\" style:background=\"currentColor\"></span>\n                            <span class=\"size-1.5 rounded-full opacity-15\" style:background=\"currentColor\"></span>\n                          </div>\n                          <span class=\"size-2 rounded-full shadow-2xs\" style:background={colorSwatch}></span>\n                        </div>\n                        <!-- Mini button + chip preview -->\n                        <div class=\"mt-2 flex items-center gap-1\">\n                          <span\n                            class=\"inline-block h-3 px-1.5 text-xs leading-3 font-semibold\"\n                            style:background={colorSwatch}\n                            style:color=\"oklch(0.985 0 0)\"\n                            style:border-radius={`${Math.min(t.radius, 0.5)}rem`}\n                            style:font-family={fontStack}>Aa</span\n                          >\n                          <span class=\"block h-1 flex-1 rounded-full opacity-30\" style:background=\"currentColor\"></span>\n                        </div>\n                        <!-- Lower text bar -->\n                        <div class=\"mt-1.5 flex gap-1\">\n                          <span class=\"block h-0.5 w-3 rounded-full opacity-20\" style:background=\"currentColor\"></span>\n                          <span class=\"block h-0.5 w-5 rounded-full opacity-15\" style:background=\"currentColor\"></span>\n                        </div>\n                      </div>\n                      <!-- Label strip -->\n                      <div class=\"bg-muted/40 flex items-center justify-between px-2 py-1\">\n                        <span class=\"text-xs leading-none font-medium\">{t.label}</span>\n                        {#if isActive}\n                          <Check class=\"text-primary size-3\" />\n                        {/if}\n                      </div>\n                    </button>\n                  {/each}\n                </div>\n              </div>\n            {/each}\n            {#if filteredThemes.length === 0}\n              <div class=\"text-muted-foreground py-6 text-center text-xs\">\n                No themes match \"<span class=\"font-mono\">{themeSearch}</span>\"\n              </div>\n            {/if}\n          </div>\n        </div>\n\n        <!-- Mode -->\n        <div>\n          <div class=\"text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase\">Mode</div>\n          <div class=\"bg-muted/50 grid grid-cols-3 gap-1 rounded-md p-1\">\n            {#each modes as m (m.value)}\n              <button\n                type=\"button\"\n                aria-pressed={themeState.current === m.value}\n                class={[\n                  'flex items-center justify-center gap-1.5 rounded px-2 py-1.5 text-xs font-medium transition-colors',\n                  themeState.current === m.value\n                    ? 'bg-background shadow-sm'\n                    : 'text-muted-foreground hover:text-foreground',\n                ]}\n                onclick={() => themeState.set(m.value)}\n              >\n                <m.icon class=\"size-3.5\" />\n                {m.label}\n              </button>\n            {/each}\n          </div>\n        </div>\n\n        <!-- Color -->\n        <div>\n          <div class=\"mb-2 flex items-baseline justify-between\">\n            <span class=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">Color</span>\n            <span class=\"text-muted-foreground text-xs\">{PRESETS.length} options</span>\n          </div>\n          <div class=\"grid grid-cols-10 gap-1.5\">\n            {#each PRESETS as p (p.key)}\n              <button\n                type=\"button\"\n                aria-label={p.label}\n                aria-pressed={preset === p.key}\n                title={p.label}\n                class={[\n                  'group ring-offset-background relative aspect-square rounded-md transition-transform',\n                  preset === p.key ? 'ring-ring ring-2 ring-offset-2' : 'hover:scale-110',\n                ]}\n                style:background={p.swatch}\n                onclick={() => (preset = p.key)}\n              >\n                {#if preset === p.key}\n                  <Check class=\"absolute inset-0 m-auto size-3 text-white mix-blend-difference\" />\n                {/if}\n              </button>\n            {/each}\n\n            <!-- Custom color slot -->\n            <label\n              class={[\n                'relative flex aspect-square cursor-pointer items-center justify-center rounded-md border transition-transform',\n                preset === 'custom'\n                  ? 'ring-ring ring-offset-background border-transparent ring-2 ring-offset-2'\n                  : 'border-dashed hover:scale-110',\n              ]}\n              style:background={preset === 'custom' ? customColor : undefined}\n              aria-label=\"Custom color\"\n              title=\"Custom color\"\n            >\n              <input\n                type=\"color\"\n                bind:value={customColor}\n                class=\"sr-only\"\n                oninput={() => (preset = 'custom')}\n              />\n              {#if preset !== 'custom'}\n                <Pipette class=\"text-muted-foreground size-3\" />\n              {:else}\n                <Check class=\"size-3 text-white mix-blend-difference\" />\n              {/if}\n            </label>\n          </div>\n        </div>\n\n        <!-- Surface tint -->\n        <div>\n          <div class=\"text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase\">Surface</div>\n          <div class=\"bg-muted/50 grid grid-cols-3 gap-1 rounded-md p-1\">\n            {#each TINTS as t (t.key)}\n              <button\n                type=\"button\"\n                aria-pressed={tint === t.key}\n                class={[\n                  'rounded px-2 py-1.5 text-xs font-medium transition-colors',\n                  tint === t.key ? 'bg-background shadow-sm' : 'text-muted-foreground hover:text-foreground',\n                ]}\n                onclick={() => (tint = t.key)}\n              >\n                {t.label}\n              </button>\n            {/each}\n          </div>\n        </div>\n\n        <!-- Font -->\n        <div>\n          <div class=\"text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase\">Font</div>\n          <div class=\"bg-muted/50 grid grid-cols-3 gap-1 rounded-md p-1\">\n            {#each FONTS as f (f.key)}\n              <button\n                type=\"button\"\n                aria-pressed={font === f.key}\n                class={[\n                  'rounded px-2 py-1.5 text-xs font-medium transition-colors',\n                  font === f.key ? 'bg-background shadow-sm' : 'text-muted-foreground hover:text-foreground',\n                ]}\n                style:font-family={f.stack}\n                onclick={() => (font = f.key)}\n              >\n                {f.label}\n              </button>\n            {/each}\n          </div>\n        </div>\n\n        <!-- Radius -->\n        <div>\n          <div class=\"mb-2 flex items-baseline justify-between\">\n            <span class=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">Radius</span>\n            <span class=\"text-muted-foreground font-mono text-xs\">{radius.toFixed(2)}rem</span>\n          </div>\n          <Slider bind:value={radius} min={0} max={1} step={0.05} />\n          <div class=\"text-muted-foreground mt-1 flex justify-between text-xs\">\n            <span>0</span>\n            <span>0.5</span>\n            <span>1</span>\n          </div>\n        </div>\n\n        <!-- Live preview -->\n        <div>\n          <div class=\"text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase\">Preview</div>\n          <div class=\"bg-card space-y-2.5 rounded-lg border p-3\">\n            <div class=\"flex items-center gap-2\">\n              <Button size=\"sm\" class=\"text-xs\">Primary</Button>\n              <Button size=\"sm\" variant=\"outline\" class=\"text-xs\">Outline</Button>\n              <span class=\"bg-primary/10 text-primary rounded-md px-2 py-0.5 text-xs font-medium\">Badge</span>\n            </div>\n            <div\n              class=\"ring-ring/40 bg-background border-input flex h-8 items-center rounded-md border px-2 text-xs focus-within:ring-2\"\n            >\n              Input sample\n            </div>\n            <p class=\"text-muted-foreground text-xs leading-relaxed\">The quick brown fox jumps over the lazy dog.</p>\n          </div>\n        </div>\n      </div>\n    </div>\n    <!-- Footer actions (sticky bottom) -->\n    <div class=\"bg-background/95 flex gap-2 border-t px-6 py-3 backdrop-blur\">\n      <Button variant=\"ghost\" size=\"sm\" class=\"flex-1 gap-1.5\" onclick={reset}>\n        <RotateCcw class=\"size-3.5\" /> Reset\n      </Button>\n      <Button aria-label=\"Download attachment\" variant=\"outline\" size=\"sm\" class=\"flex-1 gap-1.5\" onclick={exportJson}>\n        <Download class=\"size-3.5\" />\n        {exported ? 'Saved' : 'Export'}\n      </Button>\n      <Button variant=\"default\" size=\"sm\" class=\"flex-1 gap-1.5\" onclick={copyCss}>\n        {#if copied}\n          <Check class=\"size-3.5\" />\n        {:else}\n          <Copy class=\"size-3.5\" />\n        {/if}\n        {copied ? 'Copied' : 'Copy CSS'}\n      </Button>\n    </div>\n  </SheetContent>\n</Sheet>\n</div>\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/theme-customize/ThemeCustomize.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/theme-customize/theme-data.ts",
      "content": "/**\n * Token tables for the theme-customize block: color presets, curated themes,\n * fonts, and surface tints. Split out of ThemeCustomize.svelte so the block\n * entry stays under the single-file soft cap.\n */\n\nexport type Mode = 'light' | 'dark' | 'system'\nexport type PresetKey =\n  | 'neutral'\n  | 'zinc'\n  | 'slate'\n  | 'stone'\n  | 'rose'\n  | 'red'\n  | 'orange'\n  | 'amber'\n  | 'yellow'\n  | 'lime'\n  | 'emerald'\n  | 'teal'\n  | 'cyan'\n  | 'sky'\n  | 'blue'\n  | 'indigo'\n  | 'violet'\n  | 'purple'\n  | 'fuchsia'\n  | 'pink'\n  | 'custom'\nexport type ThemeKey =\n  | 'newyear'\n  | 'sunset'\n  | 'cyberpunk'\n  | 'forest'\n  | 'ocean'\n  | 'pastel'\n  | 'mono'\n  | 'brutalist'\n  | 'notebook'\n  | 'caffeine'\n  | 'midnight'\n  | 'mocha'\n  | 'retro'\n  | 'tangerine'\n  | 'quartz'\n  | 'cosmic'\n  | 'sage'\n  | 'marble'\n  | 'ember'\n  | 'amethyst'\n  | 'rainfall'\n  | 'peach'\n  | 'tropical'\n  | 'vintage'\nexport type FontKey = 'sans' | 'serif' | 'mono'\nexport type TintKey = 'pure' | 'cool' | 'warm'\nexport type CategoryKey = 'featured' | 'editorial' | 'bold' | 'cool' | 'soft'\n\nexport interface ThemePresetTokens {\n  key: PresetKey\n  label: string\n  swatch: string\n  light: { primary: string; primaryForeground: string; ring: string }\n  dark: { primary: string; primaryForeground: string; ring: string }\n}\n\nfunction mkPreset(\n  key: PresetKey,\n  label: string,\n  hue: number,\n  l = 0.6,\n  c = 0.2,\n  fg = 'oklch(0.985 0 0)',\n): ThemePresetTokens {\n  const swatch = `oklch(${l} ${c} ${hue})`\n  return {\n    key,\n    label,\n    swatch,\n    light: { primary: swatch, primaryForeground: fg, ring: swatch },\n    dark: { primary: swatch, primaryForeground: fg, ring: swatch },\n  }\n}\n\nexport const PRESETS: ThemePresetTokens[] = [\n  // Greyscale family\n  {\n    key: 'neutral',\n    label: 'Neutral',\n    swatch: 'oklch(0.205 0 0)',\n    light: { primary: 'oklch(0.205 0 0)', primaryForeground: 'oklch(0.985 0 0)', ring: 'oklch(0.708 0 0)' },\n    dark: { primary: 'oklch(0.922 0 0)', primaryForeground: 'oklch(0.205 0 0)', ring: 'oklch(0.556 0 0)' },\n  },\n  {\n    key: 'zinc',\n    label: 'Zinc',\n    swatch: 'oklch(0.21 0.006 285.885)',\n    light: {\n      primary: 'oklch(0.21 0.006 285.885)',\n      primaryForeground: 'oklch(0.985 0 0)',\n      ring: 'oklch(0.705 0.015 286.067)',\n    },\n    dark: {\n      primary: 'oklch(0.92 0.004 286.32)',\n      primaryForeground: 'oklch(0.21 0.006 285.885)',\n      ring: 'oklch(0.552 0.016 285.938)',\n    },\n  },\n  {\n    key: 'slate',\n    label: 'Slate',\n    swatch: 'oklch(0.21 0.034 264.665)',\n    light: {\n      primary: 'oklch(0.21 0.034 264.665)',\n      primaryForeground: 'oklch(0.985 0 0)',\n      ring: 'oklch(0.554 0.046 257.417)',\n    },\n    dark: {\n      primary: 'oklch(0.929 0.013 255.508)',\n      primaryForeground: 'oklch(0.21 0.034 264.665)',\n      ring: 'oklch(0.554 0.046 257.417)',\n    },\n  },\n  {\n    key: 'stone',\n    label: 'Stone',\n    swatch: 'oklch(0.216 0.006 56.043)',\n    light: {\n      primary: 'oklch(0.216 0.006 56.043)',\n      primaryForeground: 'oklch(0.985 0.001 106.423)',\n      ring: 'oklch(0.553 0.013 58.071)',\n    },\n    dark: {\n      primary: 'oklch(0.923 0.003 48.717)',\n      primaryForeground: 'oklch(0.216 0.006 56.043)',\n      ring: 'oklch(0.553 0.013 58.071)',\n    },\n  },\n  // Warm\n  mkPreset('rose', 'Rose', 16.439, 0.645, 0.246),\n  mkPreset('red', 'Red', 27.325, 0.637, 0.237),\n  mkPreset('orange', 'Orange', 47.604, 0.705, 0.213),\n  mkPreset('amber', 'Amber', 70.08, 0.769, 0.188),\n  mkPreset('yellow', 'Yellow', 95.277, 0.795, 0.184, 'oklch(0.286 0.066 53.813)'),\n  // Greens\n  mkPreset('lime', 'Lime', 130.85, 0.768, 0.233, 'oklch(0.274 0.072 132.109)'),\n  mkPreset('emerald', 'Emerald', 162.48, 0.696, 0.17),\n  mkPreset('teal', 'Teal', 184.704, 0.704, 0.14, 'oklch(0.984 0.014 180.72)'),\n  // Cool\n  mkPreset('cyan', 'Cyan', 221.723, 0.715, 0.143, 'oklch(0.302 0.056 229.695)'),\n  mkPreset('sky', 'Sky', 235.711, 0.685, 0.169),\n  mkPreset('blue', 'Blue', 264.376, 0.546, 0.245),\n  mkPreset('indigo', 'Indigo', 277.117, 0.511, 0.262),\n  // Purples\n  mkPreset('violet', 'Violet', 293.009, 0.541, 0.281),\n  mkPreset('purple', 'Purple', 303.9, 0.558, 0.288),\n  mkPreset('fuchsia', 'Fuchsia', 322.16, 0.667, 0.295),\n  mkPreset('pink', 'Pink', 354.308, 0.656, 0.241),\n]\n\nexport interface ThemePreset {\n  key: ThemeKey\n  label: string\n  category: CategoryKey\n  mode: Mode\n  color: PresetKey\n  tint: TintKey\n  font: FontKey\n  radius: number\n}\n\nexport const CATEGORY_LABELS: Record<CategoryKey, string> = {\n  featured: 'Featured',\n  editorial: 'Editorial',\n  bold: 'Bold',\n  cool: 'Cool',\n  soft: 'Soft',\n}\n\nexport const CATEGORY_ORDER: CategoryKey[] = ['featured', 'editorial', 'bold', 'cool', 'soft']\n\nexport const THEMES: ThemePreset[] = [\n  {\n    key: 'newyear',\n    label: 'New Year',\n    category: 'featured',\n    mode: 'dark',\n    color: 'amber',\n    tint: 'warm',\n    font: 'serif',\n    radius: 0.75,\n  },\n  {\n    key: 'sunset',\n    label: 'Sunset',\n    category: 'featured',\n    mode: 'light',\n    color: 'orange',\n    tint: 'warm',\n    font: 'sans',\n    radius: 0.625,\n  },\n  {\n    key: 'cyberpunk',\n    label: 'Cyberpunk',\n    category: 'featured',\n    mode: 'dark',\n    color: 'fuchsia',\n    tint: 'cool',\n    font: 'mono',\n    radius: 0.125,\n  },\n  {\n    key: 'forest',\n    label: 'Forest',\n    category: 'featured',\n    mode: 'light',\n    color: 'emerald',\n    tint: 'warm',\n    font: 'serif',\n    radius: 0.5,\n  },\n  {\n    key: 'ocean',\n    label: 'Ocean',\n    category: 'featured',\n    mode: 'dark',\n    color: 'cyan',\n    tint: 'cool',\n    font: 'sans',\n    radius: 0.5,\n  },\n  {\n    key: 'pastel',\n    label: 'Pastel',\n    category: 'featured',\n    mode: 'light',\n    color: 'rose',\n    tint: 'pure',\n    font: 'sans',\n    radius: 0.875,\n  },\n  {\n    key: 'mono',\n    label: 'Mono',\n    category: 'editorial',\n    mode: 'light',\n    color: 'neutral',\n    tint: 'pure',\n    font: 'mono',\n    radius: 0,\n  },\n  {\n    key: 'brutalist',\n    label: 'Brutalist',\n    category: 'editorial',\n    mode: 'light',\n    color: 'neutral',\n    tint: 'pure',\n    font: 'mono',\n    radius: 0,\n  },\n  {\n    key: 'notebook',\n    label: 'Notebook',\n    category: 'editorial',\n    mode: 'light',\n    color: 'stone',\n    tint: 'warm',\n    font: 'serif',\n    radius: 0.25,\n  },\n  {\n    key: 'vintage',\n    label: 'Vintage',\n    category: 'editorial',\n    mode: 'light',\n    color: 'amber',\n    tint: 'warm',\n    font: 'serif',\n    radius: 0.375,\n  },\n  {\n    key: 'caffeine',\n    label: 'Caffeine',\n    category: 'editorial',\n    mode: 'light',\n    color: 'orange',\n    tint: 'warm',\n    font: 'serif',\n    radius: 0.5,\n  },\n  {\n    key: 'marble',\n    label: 'Marble',\n    category: 'editorial',\n    mode: 'light',\n    color: 'stone',\n    tint: 'pure',\n    font: 'serif',\n    radius: 0.25,\n  },\n  {\n    key: 'retro',\n    label: 'Retro',\n    category: 'bold',\n    mode: 'dark',\n    color: 'lime',\n    tint: 'cool',\n    font: 'mono',\n    radius: 0,\n  },\n  {\n    key: 'tangerine',\n    label: 'Tangerine',\n    category: 'bold',\n    mode: 'light',\n    color: 'orange',\n    tint: 'pure',\n    font: 'sans',\n    radius: 0.75,\n  },\n  {\n    key: 'ember',\n    label: 'Ember',\n    category: 'bold',\n    mode: 'dark',\n    color: 'red',\n    tint: 'warm',\n    font: 'sans',\n    radius: 0.375,\n  },\n  {\n    key: 'tropical',\n    label: 'Tropical',\n    category: 'bold',\n    mode: 'light',\n    color: 'teal',\n    tint: 'cool',\n    font: 'sans',\n    radius: 0.625,\n  },\n  {\n    key: 'midnight',\n    label: 'Midnight',\n    category: 'cool',\n    mode: 'dark',\n    color: 'indigo',\n    tint: 'cool',\n    font: 'sans',\n    radius: 0.5,\n  },\n  {\n    key: 'cosmic',\n    label: 'Cosmic',\n    category: 'cool',\n    mode: 'dark',\n    color: 'violet',\n    tint: 'cool',\n    font: 'sans',\n    radius: 0.625,\n  },\n  {\n    key: 'rainfall',\n    label: 'Rainfall',\n    category: 'cool',\n    mode: 'dark',\n    color: 'sky',\n    tint: 'cool',\n    font: 'sans',\n    radius: 0.5,\n  },\n  {\n    key: 'amethyst',\n    label: 'Amethyst',\n    category: 'cool',\n    mode: 'dark',\n    color: 'purple',\n    tint: 'cool',\n    font: 'sans',\n    radius: 0.625,\n  },\n  {\n    key: 'sage',\n    label: 'Sage',\n    category: 'soft',\n    mode: 'light',\n    color: 'emerald',\n    tint: 'warm',\n    font: 'serif',\n    radius: 0.75,\n  },\n  {\n    key: 'peach',\n    label: 'Peach',\n    category: 'soft',\n    mode: 'light',\n    color: 'amber',\n    tint: 'warm',\n    font: 'sans',\n    radius: 0.875,\n  },\n  {\n    key: 'quartz',\n    label: 'Quartz',\n    category: 'soft',\n    mode: 'light',\n    color: 'violet',\n    tint: 'cool',\n    font: 'sans',\n    radius: 0.875,\n  },\n  {\n    key: 'mocha',\n    label: 'Mocha',\n    category: 'soft',\n    mode: 'light',\n    color: 'rose',\n    tint: 'warm',\n    font: 'serif',\n    radius: 0.5,\n  },\n]\n\nexport interface FontOption {\n  key: FontKey\n  label: string\n  stack: string\n}\n\nexport const FONTS: FontOption[] = [\n  { key: 'sans', label: 'Sans', stack: '\"DM Sans\", ui-sans-serif, system-ui, sans-serif' },\n  { key: 'serif', label: 'Serif', stack: '\"Source Serif Pro\", ui-serif, Georgia, serif' },\n  { key: 'mono', label: 'Mono', stack: '\"JetBrains Mono\", ui-monospace, monospace' },\n]\n\nexport interface TintOption {\n  key: TintKey\n  label: string\n  light: string\n  dark: string\n}\n\nexport const TINTS: TintOption[] = [\n  { key: 'pure', label: 'Pure', light: 'oklch(1 0 0)', dark: 'oklch(0.145 0 0)' },\n  { key: 'cool', label: 'Cool', light: 'oklch(0.99 0.005 240)', dark: 'oklch(0.16 0.008 250)' },\n  { key: 'warm', label: 'Warm', light: 'oklch(0.99 0.005 70)', dark: 'oklch(0.16 0.008 60)' },\n]\n\nexport const STORAGE_KEY = 'uipkge-theme-customize'\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/theme-customize/theme-data.ts"
    },
    {
      "path": "packages/registry-svelte/blocks/theme-customize/theme.svelte.ts",
      "content": "/**\n * Block-local theme mode state for theme-customize — the Svelte counterpart\n * of the Vue registry's `useTheme` composable. Cookie-backed (not\n * localStorage) so a server render can match the saved theme, and the only\n * writer of the `dark` class on `<html>` this block needs.\n */\nimport type { Mode } from './theme-data'\n\nconst COOKIE_KEY = 'uipkge-theme'\n\nfunction readCookie(): Mode {\n  if (typeof document === 'undefined') return 'system'\n  const m = document.cookie.match(/(?:^|; )uipkge-theme=([^;]+)/)\n  return (m ? decodeURIComponent(m[1]) : 'system') as Mode\n}\n\nfunction writeCookie(value: Mode) {\n  if (typeof document === 'undefined') return\n  document.cookie = `${COOKIE_KEY}=${encodeURIComponent(value)}; path=/; max-age=${60 * 60 * 24 * 365}; samesite=lax`\n}\n\nfunction applyMode(next: Mode) {\n  if (typeof window === 'undefined') return\n  const isDark = next === 'dark' || (next === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)\n  document.documentElement.classList.toggle('dark', isDark)\n}\n\n/**\n * A theme flip changes color, background, border and shadow on nearly every\n * element at once. Anything carrying `transition-colors` — cards, buttons,\n * nav items — then animates its own repaint, and the switch smears instead\n * of snapping. Kill transitions for the swap, force a reflow so the browser\n * commits the new colors with no transition in effect, then restore.\n */\nfunction withoutTransitions(swap: () => void) {\n  const style = document.createElement('style')\n  style.appendChild(document.createTextNode('*,*::before,*::after{transition:none !important}'))\n  document.head.appendChild(style)\n  swap()\n  // Reading offsetHeight flushes pending style changes.\n  void document.body.offsetHeight\n  requestAnimationFrame(() => {\n    requestAnimationFrame(() => style.remove())\n  })\n}\n\nclass ThemeState {\n  current = $state<Mode>(readCookie())\n\n  set(next: Mode) {\n    this.current = next\n    if (typeof window === 'undefined') return\n    writeCookie(next)\n    withoutTransitions(() => applyMode(next))\n  }\n\n  /** Apply the saved mode on mount (no transition kill — nothing to smear yet). */\n  apply() {\n    applyMode(this.current)\n  }\n}\n\nexport const themeState = new ThemeState()\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/theme-customize/theme.svelte.ts"
    },
    {
      "path": "packages/registry-svelte/blocks/theme-customize/+page.svelte",
      "content": "<script lang=\"ts\">\n  // Demo page wired by `npx shadcn-svelte add https://uipkge.dev/r/svelte/theme-customize.json`.\n  // Visit /theme-customize-demo after install.\n  import ThemeCustomize from '$lib/components/blocks/theme-customize/ThemeCustomize.svelte'\n</script>\n\n<div class=\"bg-background min-h-svh\">\n  <header class=\"border-border border-b px-8 py-4\">\n    <h1 class=\"text-muted-foreground font-mono text-sm tracking-widest uppercase\">theme-customize demo</h1>\n  </header>\n  <main class=\"p-8\">\n    <ThemeCustomize />\n  </main>\n</div>\n",
      "type": "registry:page",
      "target": "~/src/routes/theme-customize-demo/+page.svelte"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/svelte/button.json",
    "https://uipkge.dev/r/svelte/slider.json",
    "https://uipkge.dev/r/svelte/sheet.json"
  ],
  "description": "Compact theme customization sheet. Light/Dark/System mode (via a cookie-backed theme store), 20 curated color presets that overwrite `--primary` / `--primary-foreground` / `--ring`, and a radius slider bound to `--radius`. Persists to localStorage and exposes a one-click Copy CSS for the active token set.",
  "categories": [
    "layout",
    "dashboard"
  ]
}