{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "theme-customize",
  "title": "Theme Customize",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-react/blocks/theme-customize/ThemeCustomize.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { useTheme } from 'next-themes'\nimport { Check, Copy, Download, Monitor, Moon, Palette, Pipette, RotateCcw, Shuffle, Sun, type LucideIcon } from 'lucide-react'\nimport { Button } from '@/components/ui/button'\nimport { Slider } from '@/components/ui/slider'\nimport { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'\n\ntype Mode = 'light' | 'dark' | 'system'\ntype 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'\ntype 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'\ntype FontKey = 'sans' | 'serif' | 'mono'\ntype TintKey = 'pure' | 'cool' | 'warm'\n\ninterface Preset {\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(key: PresetKey, label: string, hue: number, l = 0.6, c = 0.2, fg = 'oklch(0.985 0 0)'): Preset {\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\nconst PRESETS: Preset[] = [\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\ntype CategoryKey = 'featured' | 'editorial' | 'bold' | 'cool' | 'soft'\n\ninterface ThemePreset {\n  key: ThemeKey\n  label: string\n  emoji: string\n  category: CategoryKey\n  mode: Mode\n  color: PresetKey\n  tint: TintKey\n  font: FontKey\n  radius: number\n}\n\nconst CATEGORY_LABELS: Record<CategoryKey, string> = {\n  featured: 'Featured',\n  editorial: 'Editorial',\n  bold: 'Bold',\n  cool: 'Cool',\n  soft: 'Soft',\n}\n\nconst THEMES: ThemePreset[] = [\n  { key: 'newyear', label: 'New Year', emoji: '✨', category: 'featured', mode: 'dark', color: 'amber', tint: 'warm', font: 'serif', radius: 0.75 },\n  { key: 'sunset', label: 'Sunset', emoji: '🌅', category: 'featured', mode: 'light', color: 'orange', tint: 'warm', font: 'sans', radius: 0.625 },\n  { key: 'cyberpunk', label: 'Cyberpunk', emoji: '⚡', category: 'featured', mode: 'dark', color: 'fuchsia', tint: 'cool', font: 'mono', radius: 0.125 },\n  { key: 'forest', label: 'Forest', emoji: '🌿', category: 'featured', mode: 'light', color: 'emerald', tint: 'warm', font: 'serif', radius: 0.5 },\n  { key: 'ocean', label: 'Ocean', emoji: '🌊', category: 'featured', mode: 'dark', color: 'cyan', tint: 'cool', font: 'sans', radius: 0.5 },\n  { key: 'pastel', label: 'Pastel', emoji: '🌸', category: 'featured', mode: 'light', color: 'rose', tint: 'pure', font: 'sans', radius: 0.875 },\n  { key: 'mono', label: 'Mono', emoji: '◼', category: 'editorial', mode: 'light', color: 'neutral', tint: 'pure', font: 'mono', radius: 0 },\n  { key: 'brutalist', label: 'Brutalist', emoji: '▢', category: 'editorial', mode: 'light', color: 'neutral', tint: 'pure', font: 'mono', radius: 0 },\n  { key: 'notebook', label: 'Notebook', emoji: '📓', category: 'editorial', mode: 'light', color: 'stone', tint: 'warm', font: 'serif', radius: 0.25 },\n  { key: 'vintage', label: 'Vintage', emoji: '📜', category: 'editorial', mode: 'light', color: 'amber', tint: 'warm', font: 'serif', radius: 0.375 },\n  { key: 'caffeine', label: 'Caffeine', emoji: '☕', category: 'editorial', mode: 'light', color: 'orange', tint: 'warm', font: 'serif', radius: 0.5 },\n  { key: 'marble', label: 'Marble', emoji: '🏛', category: 'editorial', mode: 'light', color: 'stone', tint: 'pure', font: 'serif', radius: 0.25 },\n  { key: 'retro', label: 'Retro', emoji: '🎮', category: 'bold', mode: 'dark', color: 'lime', tint: 'cool', font: 'mono', radius: 0 },\n  { key: 'tangerine', label: 'Tangerine', emoji: '🍊', category: 'bold', mode: 'light', color: 'orange', tint: 'pure', font: 'sans', radius: 0.75 },\n  { key: 'ember', label: 'Ember', emoji: '🔥', category: 'bold', mode: 'dark', color: 'red', tint: 'warm', font: 'sans', radius: 0.375 },\n  { key: 'tropical', label: 'Tropical', emoji: '🌴', category: 'bold', mode: 'light', color: 'teal', tint: 'cool', font: 'sans', radius: 0.625 },\n  { key: 'midnight', label: 'Midnight', emoji: '🌃', category: 'cool', mode: 'dark', color: 'indigo', tint: 'cool', font: 'sans', radius: 0.5 },\n  { key: 'cosmic', label: 'Cosmic', emoji: '🌌', category: 'cool', mode: 'dark', color: 'violet', tint: 'cool', font: 'sans', radius: 0.625 },\n  { key: 'rainfall', label: 'Rainfall', emoji: '🌧', category: 'cool', mode: 'dark', color: 'sky', tint: 'cool', font: 'sans', radius: 0.5 },\n  { key: 'amethyst', label: 'Amethyst', emoji: '💜', category: 'cool', mode: 'dark', color: 'purple', tint: 'cool', font: 'sans', radius: 0.625 },\n  { key: 'sage', label: 'Sage', emoji: '🌱', category: 'soft', mode: 'light', color: 'emerald', tint: 'warm', font: 'serif', radius: 0.75 },\n  { key: 'peach', label: 'Peach', emoji: '🍑', category: 'soft', mode: 'light', color: 'amber', tint: 'warm', font: 'sans', radius: 0.875 },\n  { key: 'quartz', label: 'Quartz', emoji: '💎', category: 'soft', mode: 'light', color: 'violet', tint: 'cool', font: 'sans', radius: 0.875 },\n  { key: 'mocha', label: 'Mocha', emoji: '🌹', category: 'soft', mode: 'light', color: 'rose', tint: 'warm', font: 'serif', radius: 0.5 },\n]\n\nconst FONTS: { key: FontKey; label: string; stack: string }[] = [\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\nconst TINTS: { key: TintKey; label: string; light: string; dark: string }[] = [\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\nconst STORAGE_KEY = 'uipkge-theme-customize'\n\nconst modes: { value: Mode; label: string; icon: LucideIcon }[] = [\n  { value: 'light', label: 'Light', icon: Sun },\n  { value: 'dark', label: 'Dark', icon: Moon },\n  { value: 'system', label: 'System', icon: Monitor },\n]\n\nfunction isDarkActive(): boolean {\n  if (typeof window === 'undefined') return false\n  return document.documentElement.classList.contains('dark')\n}\n\nexport function ThemeCustomize() {\n  const { theme, setTheme } = useTheme()\n  const mode = (theme as Mode) ?? 'system'\n\n  const [preset, setPreset] = React.useState<PresetKey>('neutral')\n  const [customColor, setCustomColor] = React.useState<string>('#ec4899')\n  const [font, setFont] = React.useState<FontKey>('sans')\n  const [tint, setTint] = React.useState<TintKey>('pure')\n  const [radius, setRadius] = React.useState(0.3)\n  const [copied, setCopied] = React.useState(false)\n  const [exported, setExported] = React.useState(false)\n  const [themeSearch, setThemeSearch] = React.useState('')\n  const [sheetOpen, setSheetOpen] = React.useState(false)\n\n  const activeTheme = React.useMemo<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    return (\n      THEMES.find((t) => t.mode === mode && t.color === preset && t.tint === tint && t.font === font) ?? null\n    )\n  }, [mode, preset, tint, font])\n\n  const filteredThemes = React.useMemo(() => {\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  }, [themeSearch])\n\n  const themesByCategory = React.useMemo<{ key: CategoryKey; label: string; items: ThemePreset[] }[]>(() => {\n    const order: CategoryKey[] = ['featured', 'editorial', 'bold', 'cool', 'soft']\n    return order\n      .map((c) => ({\n        key: c,\n        label: CATEGORY_LABELS[c],\n        items: filteredThemes.filter((t) => t.category === c),\n      }))\n      .filter((g) => g.items.length > 0)\n  }, [filteredThemes])\n\n  const applyPreset = React.useCallback(\n    (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', isDarkActive() ? 'oklch(0.985 0 0)' : '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    [customColor],\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  const save = React.useCallback(() => {\n    try {\n      localStorage.setItem(\n        STORAGE_KEY,\n        JSON.stringify({ preset, customColor, font, tint, radius }),\n      )\n    } catch {\n      // ignore\n    }\n  }, [preset, customColor, font, tint, radius])\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    setTheme(t.mode)\n    setPreset(t.color)\n    setTint(t.tint)\n    setFont(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    setPreset('neutral')\n    setCustomColor('#ec4899')\n    setFont('sans')\n    setTint('pure')\n    setRadius(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      setCopied(true)\n      setTimeout(() => setCopied(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    setExported(true)\n    setTimeout(() => setExported(false), 1600)\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  // Load persisted state + apply on mount; wire the Cmd/Ctrl+J shortcut.\n  React.useEffect(() => {\n    try {\n      const raw = localStorage.getItem(STORAGE_KEY)\n      if (raw) {\n        const parsed = JSON.parse(raw)\n        if (parsed.preset) setPreset(parsed.preset)\n        if (parsed.customColor) setCustomColor(parsed.customColor)\n        if (parsed.font) setFont(parsed.font)\n        if (parsed.tint) setTint(parsed.tint)\n        if (typeof parsed.radius === 'number') setRadius(parsed.radius)\n      }\n    } catch {\n      // ignore\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        setSheetOpen((v) => !v)\n      }\n    }\n    if (typeof window !== 'undefined') window.addEventListener('keydown', onKeydown)\n    return () => {\n      if (typeof window !== 'undefined') window.removeEventListener('keydown', onKeydown)\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  // watch([preset, customColor]) -> applyPreset + save\n  React.useEffect(() => {\n    applyPreset(preset)\n    save()\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [preset, customColor])\n\n  // watch(font) -> applyFont + save\n  React.useEffect(() => {\n    applyFont(font)\n    save()\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [font])\n\n  // watch(tint) -> applyTint + save\n  React.useEffect(() => {\n    applyTint(tint)\n    save()\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [tint])\n\n  // watch(radius) -> applyRadius + save\n  React.useEffect(() => {\n    applyRadius(radius)\n    save()\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [radius])\n\n  // watch(theme) -> re-apply preset + tint on next tick\n  React.useEffect(() => {\n    setTimeout(() => {\n      applyPreset(preset)\n      applyTint(tint)\n    }, 0)\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [theme])\n\n  // watch([preset, customColor, font, tint, radius, theme]) -> pulseTransition\n  React.useEffect(() => {\n    pulseTransition()\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [preset, customColor, font, tint, radius, theme])\n\n  return (\n    <Sheet open={sheetOpen} onOpenChange={setSheetOpen}>\n      <SheetTrigger asChild>\n        <Button variant=\"outline\" size=\"sm\" className=\"gap-2\">\n          <Palette className=\"size-4\" />\n          <span className=\"hidden sm:inline\">Customize</span>\n          <kbd className=\"bg-muted text-muted-foreground hidden rounded px-1 py-0.5 text-[10px] font-medium sm:inline-block\">\n            ⌘J\n          </kbd>\n        </Button>\n      </SheetTrigger>\n      <SheetContent side=\"right\" className=\"flex w-full flex-col gap-0 p-0 sm:max-w-md\">\n        <SheetHeader className=\"border-b px-6 py-4\">\n          <div className=\"flex items-center justify-between gap-3\">\n            <SheetTitle className=\"flex items-center gap-2 text-base\">\n              {' '}\n              <Palette className=\"size-4\" /> Customize{' '}\n            </SheetTitle>\n            {/* Active theme chip */}\n            {activeTheme ? (\n              <span className=\"bg-muted text-foreground inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium\">\n                <span className=\"text-[13px] leading-none\">{activeTheme.emoji}</span>\n                {activeTheme.label}\n              </span>\n            ) : (\n              <span className=\"text-muted-foreground bg-muted/50 rounded-full px-2 py-0.5 text-[11px] font-medium\">\n                Custom\n              </span>\n            )}\n          </div>\n          <SheetDescription className=\"text-xs\">\n            Live-edit tokens. Changes write to <code className=\"font-mono\">:root</code> and persist locally.\n          </SheetDescription>\n          {/* Quick actions */}\n          <div className=\"mt-2 flex gap-2\">\n            <Button variant=\"outline\" size=\"xs\" className=\"gap-1.5\" onClick={applyRandomTheme}>\n              <Shuffle className=\"size-3\" /> Surprise me\n            </Button>\n          </div>\n        </SheetHeader>\n        <div className=\"flex-1 overflow-y-auto px-6 py-5\">\n          <div className=\"space-y-6\">\n            {/* Curated themes */}\n            <div>\n              <div className=\"mb-2 flex items-baseline justify-between\">\n                <span className=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">Themes</span>\n                <span className=\"text-muted-foreground text-[10px]\">\n                  {filteredThemes.length}\n                  {themeSearch && <span> / {THEMES.length}</span>} presets\n                </span>\n              </div>\n\n              {/* Search */}\n              <div className=\"relative mb-3\">\n                <input\n                  value={themeSearch}\n                  onChange={(e) => setThemeSearch(e.target.value)}\n                  type=\"search\"\n                  placeholder=\"Search themes...\"\n                  className=\"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                  className=\"text-muted-foreground absolute top-1/2 left-2 size-3.5 -translate-y-1/2\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"2\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"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 className=\"space-y-4\">\n                {themesByCategory.map((group) => (\n                  <div key={group.key}>\n                    <div className=\"text-muted-foreground/70 mb-1.5 text-[10px] font-medium tracking-wider uppercase\">\n                      {group.label}\n                    </div>\n                    <div className=\"grid grid-cols-3 gap-2\">\n                      {group.items.map((t) => (\n                        <button\n                          key={t.key}\n                          type=\"button\"\n                          title={`${t.mode} · ${t.color} · ${t.font} · r=${t.radius}`}\n                          aria-pressed={activeTheme?.key === t.key}\n                          className={[\n                            'group relative flex flex-col overflow-hidden rounded-lg border text-left transition-all',\n                            activeTheme?.key === t.key\n                              ? 'ring-ring ring-offset-background border-transparent ring-2 ring-offset-2'\n                              : 'hover:scale-[1.03] hover:shadow-md',\n                          ].join(' ')}\n                          onClick={() => applyTheme(t)}\n                        >\n                          {/* Mini-preview window */}\n                          <div\n                            className=\"relative h-14 px-2 py-1.5\"\n                            style={{\n                              background: (TINTS.find((x) => x.key === t.tint) || ({} as Record<string, string>))[\n                                t.mode === 'dark' ? 'dark' : 'light'\n                              ],\n                              color: t.mode === 'dark' ? 'oklch(0.985 0 0)' : 'oklch(0.205 0 0)',\n                            }}\n                          >\n                            {/* Top row: dot + brand emoji */}\n                            <div className=\"flex items-center justify-between\">\n                              <div className=\"flex gap-0.5\">\n                                <span className=\"size-1.5 rounded-full opacity-40\" style={{ background: 'currentColor' }} />\n                                <span className=\"size-1.5 rounded-full opacity-25\" style={{ background: 'currentColor' }} />\n                                <span className=\"size-1.5 rounded-full opacity-15\" style={{ background: 'currentColor' }} />\n                              </div>\n                              <span className=\"text-[10px] leading-none\">{t.emoji}</span>\n                            </div>\n                            {/* Mini button + chip preview */}\n                            <div className=\"mt-2 flex items-center gap-1\">\n                              <span\n                                className=\"inline-block h-3 px-1.5 text-[8px] leading-3 font-semibold\"\n                                style={{\n                                  background: PRESETS.find((p) => p.key === t.color)?.swatch,\n                                  color: 'oklch(0.985 0 0)',\n                                  borderRadius: `${Math.min(t.radius, 0.5)}rem`,\n                                  fontFamily: FONTS.find((f) => f.key === t.font)?.stack,\n                                }}\n                              >\n                                Aa\n                              </span>\n                              <span className=\"block h-1 flex-1 rounded-full opacity-30\" style={{ background: 'currentColor' }} />\n                            </div>\n                            {/* Lower text bar */}\n                            <div className=\"mt-1.5 flex gap-1\">\n                              <span className=\"block h-0.5 w-3 rounded-full opacity-20\" style={{ background: 'currentColor' }} />\n                              <span className=\"block h-0.5 w-5 rounded-full opacity-15\" style={{ background: 'currentColor' }} />\n                            </div>\n                          </div>\n                          {/* Label strip */}\n                          <div className=\"bg-muted/40 flex items-center justify-between px-2 py-1\">\n                            <span className=\"text-[10px] leading-none font-medium\">{t.label}</span>\n                            {activeTheme?.key === t.key && <Check className=\"text-primary size-3\" />}\n                          </div>\n                        </button>\n                      ))}\n                    </div>\n                  </div>\n                ))}\n                {filteredThemes.length === 0 && (\n                  <div className=\"text-muted-foreground py-6 text-center text-xs\">\n                    No themes match \"<span className=\"font-mono\">{themeSearch}</span>\"\n                  </div>\n                )}\n              </div>\n            </div>\n\n            {/* Mode */}\n            <div>\n              <div className=\"text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase\">Mode</div>\n              <div className=\"bg-muted/50 grid grid-cols-3 gap-1 rounded-md p-1\">\n                {modes.map((m) => {\n                  const Icon = m.icon\n                  return (\n                    <button\n                      key={m.value}\n                      type=\"button\"\n                      aria-pressed={mode === m.value}\n                      className={[\n                        'flex items-center justify-center gap-1.5 rounded px-2 py-1.5 text-xs font-medium transition-colors',\n                        mode === m.value ? 'bg-background shadow-sm' : 'text-muted-foreground hover:text-foreground',\n                      ].join(' ')}\n                      onClick={() => setTheme(m.value)}\n                    >\n                      <Icon className=\"size-3.5\" />\n                      {m.label}\n                    </button>\n                  )\n                })}\n              </div>\n            </div>\n\n            {/* Color */}\n            <div>\n              <div className=\"mb-2 flex items-baseline justify-between\">\n                <span className=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">Color</span>\n                <span className=\"text-muted-foreground text-[10px]\">{PRESETS.length} options</span>\n              </div>\n              <div className=\"grid grid-cols-10 gap-1.5\">\n                {PRESETS.map((p) => (\n                  <button\n                    key={p.key}\n                    type=\"button\"\n                    aria-label={p.label}\n                    aria-pressed={preset === p.key}\n                    title={p.label}\n                    className={[\n                      'group ring-offset-background relative aspect-square rounded-md transition-all',\n                      preset === p.key ? 'ring-ring ring-2 ring-offset-2' : 'hover:scale-110',\n                    ].join(' ')}\n                    style={{ background: p.swatch }}\n                    onClick={() => setPreset(p.key)}\n                  >\n                    {preset === p.key && (\n                      <Check className=\"absolute inset-0 m-auto size-3 text-white mix-blend-difference\" />\n                    )}\n                  </button>\n                ))}\n\n                {/* Custom color slot */}\n                <label\n                  className={[\n                    'relative flex aspect-square cursor-pointer items-center justify-center rounded-md border transition-all',\n                    preset === 'custom'\n                      ? 'ring-ring ring-offset-background border-transparent ring-2 ring-offset-2'\n                      : 'border-dashed hover:scale-110',\n                  ].join(' ')}\n                  style={preset === 'custom' ? { background: customColor } : {}}\n                  aria-label=\"Custom color\"\n                  title=\"Custom color\"\n                >\n                  <input\n                    type=\"color\"\n                    value={customColor}\n                    className=\"sr-only\"\n                    onChange={(e) => {\n                      setCustomColor(e.target.value)\n                      setPreset('custom')\n                    }}\n                  />\n                  {preset !== 'custom' ? (\n                    <Pipette className=\"text-muted-foreground size-3\" />\n                  ) : (\n                    <Check className=\"size-3 text-white mix-blend-difference\" />\n                  )}\n                </label>\n              </div>\n            </div>\n\n            {/* Surface tint */}\n            <div>\n              <div className=\"text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase\">Surface</div>\n              <div className=\"bg-muted/50 grid grid-cols-3 gap-1 rounded-md p-1\">\n                {TINTS.map((t) => (\n                  <button\n                    key={t.key}\n                    type=\"button\"\n                    aria-pressed={tint === t.key}\n                    className={[\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                    ].join(' ')}\n                    onClick={() => setTint(t.key)}\n                  >\n                    {t.label}\n                  </button>\n                ))}\n              </div>\n            </div>\n\n            {/* Font */}\n            <div>\n              <div className=\"text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase\">Font</div>\n              <div className=\"bg-muted/50 grid grid-cols-3 gap-1 rounded-md p-1\">\n                {FONTS.map((f) => (\n                  <button\n                    key={f.key}\n                    type=\"button\"\n                    aria-pressed={font === f.key}\n                    className={[\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                    ].join(' ')}\n                    style={{ fontFamily: f.stack }}\n                    onClick={() => setFont(f.key)}\n                  >\n                    {f.label}\n                  </button>\n                ))}\n              </div>\n            </div>\n\n            {/* Radius */}\n            <div>\n              <div className=\"mb-2 flex items-baseline justify-between\">\n                <span className=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">Radius</span>\n                <span className=\"text-muted-foreground font-mono text-[10px]\">{radius.toFixed(2)}rem</span>\n              </div>\n              <Slider value={[radius]} min={0} max={1} step={0.05} onValueChange={([v]) => setRadius(v)} />\n              <div className=\"text-muted-foreground mt-1 flex justify-between text-[10px]\">\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 className=\"text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase\">Preview</div>\n              <div className=\"bg-card space-y-2.5 rounded-lg border p-3\">\n                <div className=\"flex items-center gap-2\">\n                  <Button size=\"sm\" className=\"text-xs\">\n                    Primary\n                  </Button>\n                  <Button size=\"sm\" variant=\"outline\" className=\"text-xs\">\n                    Outline\n                  </Button>\n                  <span className=\"bg-primary/10 text-primary rounded-md px-2 py-0.5 text-[10px] font-medium\">Badge</span>\n                </div>\n                <div className=\"ring-ring/40 bg-background border-input flex h-8 items-center rounded-md border px-2 text-xs focus-within:ring-2\">\n                  Input sample\n                </div>\n                <p className=\"text-muted-foreground text-[11px] leading-relaxed\">\n                  The quick brown fox jumps over the lazy dog.\n                </p>\n              </div>\n            </div>\n          </div>\n        </div>\n        {/* Footer actions (sticky bottom) */}\n        <div className=\"bg-background/95 flex gap-2 border-t px-6 py-3 backdrop-blur\">\n          <Button variant=\"ghost\" size=\"sm\" className=\"flex-1 gap-1.5\" onClick={reset}>\n            <RotateCcw className=\"size-3.5\" /> Reset\n          </Button>\n          <Button variant=\"outline\" size=\"sm\" className=\"flex-1 gap-1.5\" onClick={exportJson}>\n            <Download className=\"size-3.5\" />\n            {exported ? 'Saved' : 'Export'}\n          </Button>\n          <Button variant=\"default\" size=\"sm\" className=\"flex-1 gap-1.5\" onClick={copyCss}>\n            {copied ? <Check className=\"size-3.5\" /> : <Copy className=\"size-3.5\" />}\n            {copied ? 'Copied' : 'Copy CSS'}\n          </Button>\n        </div>\n      </SheetContent>\n    </Sheet>\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/ThemeCustomize.tsx"
    },
    {
      "path": "packages/registry-react/blocks/theme-customize/page.tsx",
      "content": "// Demo page wired by `npx shadcn@latest add https://uipkge.dev/r/react/theme-customize.json`.\n// Visit /theme-customize-demo after install.\nimport { ThemeCustomize } from '@/components/blocks/ThemeCustomize'\n\nexport default function Page() {\n  return (\n    <div className=\"bg-background min-h-svh\">\n      <header className=\"border-border border-b px-8 py-4\">\n        <h1 className=\"text-muted-foreground font-mono text-sm tracking-widest uppercase\">theme-customize demo</h1>\n      </header>\n      <main className=\"p-8\">\n        <ThemeCustomize />\n      </main>\n    </div>\n  )\n}",
      "type": "registry:block",
      "target": "~/app/theme-customize-demo/page.tsx"
    }
  ],
  "dependencies": [
    "lucide-react",
    "next-themes"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/button.json",
    "https://uipkge.dev/r/react/slider.json",
    "https://uipkge.dev/r/react/sheet.json",
    "https://uipkge.dev/r/react/use-theme.json"
  ],
  "description": "Compact theme customization popover. Light/Dark/System mode (via next-themes), six 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": [
    "dashboard"
  ]
}