{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "theme-customize",
  "title": "Theme Customize",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/theme-customize/ThemeCustomize.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, onMounted, onUnmounted, ref, watch } from 'vue'\nimport { Check, Copy, Download, Monitor, Moon, Palette, Pipette, RotateCcw, Shuffle, Sun } from 'lucide-vue-next'\nimport { Button } from '@/components/ui/button'\nimport { Slider } from '@/components/ui/slider'\nimport { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'\nimport { useTheme } from '@/composables/useTheme'\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  {\n    key: 'newyear',\n    label: 'New Year',\n    emoji: '✨',\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    emoji: '🌅',\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    emoji: '⚡',\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    emoji: '🌿',\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    emoji: '🌊',\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    emoji: '🌸',\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    emoji: '◼',\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    emoji: '▢',\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    emoji: '📓',\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    emoji: '📜',\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    emoji: '☕',\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    emoji: '🏛',\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    emoji: '🎮',\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    emoji: '🍊',\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    emoji: '🔥',\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    emoji: '🌴',\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    emoji: '🌃',\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    emoji: '🌌',\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    emoji: '🌧',\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    emoji: '💜',\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    emoji: '🌱',\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    emoji: '🍑',\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    emoji: '💎',\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    emoji: '🌹',\n    category: 'soft',\n    mode: 'light',\n    color: 'rose',\n    tint: 'warm',\n    font: 'serif',\n    radius: 0.5,\n  },\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 { theme, setTheme } = useTheme()\nconst preset = ref<PresetKey>('neutral')\nconst customColor = ref<string>('#ec4899')\nconst font = ref<FontKey>('sans')\nconst tint = ref<TintKey>('pure')\nconst radius = ref(0.3)\nconst copied = ref(false)\nconst exported = ref(false)\nconst themeSearch = ref('')\nconst sheetOpen = ref(false)\n\nconst activeTheme = computed<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(\n      (t) => t.mode === theme.value && t.color === preset.value && t.tint === tint.value && t.font === font.value,\n    ) ?? null\n  )\n})\n\nconst filteredThemes = computed(() => {\n  const q = themeSearch.value.trim().toLowerCase()\n  if (!q) return THEMES\n  return THEMES.filter((t) => t.label.toLowerCase().includes(q) || t.key.includes(q))\n})\n\nconst themesByCategory = computed<{ 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.value.filter((t) => t.category === c),\n    }))\n    .filter((g) => g.items.length > 0)\n})\n\nconst modes: { value: Mode; label: string; icon: typeof Sun }[] = [\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\nfunction applyPreset(key: PresetKey) {\n  if (typeof window === 'undefined') return\n  const root = document.documentElement\n  if (key === 'custom') {\n    root.style.setProperty('--primary', customColor.value)\n    root.style.setProperty('--primary-foreground', isDarkActive() ? 'oklch(0.985 0 0)' : 'oklch(0.985 0 0)')\n    root.style.setProperty('--ring', customColor.value)\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\nfunction 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\nfunction 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\nfunction applyRadius(rem: number) {\n  if (typeof window === 'undefined') return\n  document.documentElement.style.setProperty('--radius', `${rem}rem`)\n}\n\nfunction save() {\n  try {\n    localStorage.setItem(\n      STORAGE_KEY,\n      JSON.stringify({\n        preset: preset.value,\n        customColor: customColor.value,\n        font: font.value,\n        tint: tint.value,\n        radius: radius.value,\n      }),\n    )\n  } catch {\n    // ignore\n  }\n}\n\nfunction 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.value = parsed.preset\n    if (parsed.customColor) customColor.value = parsed.customColor\n    if (parsed.font) font.value = parsed.font\n    if (parsed.tint) tint.value = parsed.tint\n    if (typeof parsed.radius === 'number') radius.value = parsed.radius\n  } catch {\n    // ignore\n  }\n}\n\nfunction applyAll() {\n  applyPreset(preset.value)\n  applyTint(tint.value)\n  applyFont(font.value)\n  applyRadius(radius.value)\n}\n\nfunction 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  preset.value = t.color\n  tint.value = t.tint\n  font.value = t.font\n}\n\nfunction applyRandomTheme() {\n  const candidates = THEMES.filter((t) => t.key !== activeTheme.value?.key)\n  const pick = candidates[Math.floor(Math.random() * candidates.length)]\n  if (pick) applyTheme(pick)\n}\n\nfunction reset() {\n  preset.value = 'neutral'\n  customColor.value = '#ec4899'\n  font.value = 'sans'\n  tint.value = 'pure'\n  radius.value = 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\nfunction buildCss(): string {\n  const found = PRESETS.find((p) => p.key === preset.value) ?? PRESETS[0]\n  const isCustom = preset.value === 'custom'\n  const tokens = isDarkActive() ? found.dark : found.light\n  const t = TINTS.find((x) => x.key === tint.value) ?? TINTS[0]\n  const f = FONTS.find((x) => x.key === font.value) ?? FONTS[0]\n  const primary = isCustom ? customColor.value : tokens.primary\n  const primaryFg = isCustom ? 'oklch(0.985 0 0)' : tokens.primaryForeground\n  const ring = isCustom ? customColor.value : 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.value}rem;\n  --font-sans: ${f.stack};\n}`\n}\n\nasync function copyCss() {\n  try {\n    await navigator.clipboard.writeText(buildCss())\n    copied.value = true\n    setTimeout(() => (copied.value = false), 1600)\n  } catch {\n    // ignore\n  }\n}\n\nfunction exportJson() {\n  const blob = new Blob(\n    [\n      JSON.stringify(\n        {\n          preset: preset.value,\n          customColor: customColor.value,\n          font: font.value,\n          tint: tint.value,\n          radius: radius.value,\n          css: buildCss(),\n        },\n        null,\n        2,\n      ),\n    ],\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.value = true\n  setTimeout(() => (exported.value = false), 1600)\n}\n\nwatch([preset, customColor], () => {\n  applyPreset(preset.value)\n  save()\n})\nwatch(font, (k) => {\n  applyFont(k)\n  save()\n})\nwatch(tint, (k) => {\n  applyTint(k)\n  save()\n})\nwatch(radius, (r) => {\n  applyRadius(r)\n  save()\n})\n\nwatch(theme, () => {\n  setTimeout(() => {\n    applyPreset(preset.value)\n    applyTint(tint.value)\n  }, 0)\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.\nfunction 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\nwatch([preset, customColor, font, tint, radius, theme], pulseTransition)\n\nfunction 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.value = !sheetOpen.value\n  }\n}\n\nonMounted(() => {\n  load()\n  applyAll()\n  if (typeof window !== 'undefined') window.addEventListener('keydown', onKeydown)\n})\n\nonUnmounted(() => {\n  if (typeof window !== 'undefined') window.removeEventListener('keydown', onKeydown)\n})\n\nconst sliderModel = computed({\n  get: () => [radius.value],\n  set: ([v]) => {\n    radius.value = v\n  },\n})\n</script>\n\n<template>\n  <Sheet v-model:open=\"sheetOpen\">\n    <SheetTrigger as-child>\n      <Button variant=\"outline\" size=\"sm\" 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-[10px] font-medium sm:inline-block\"\n          >⌘J</kbd\n        >\n      </Button>\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          <span\n            v-if=\"activeTheme\"\n            class=\"bg-muted text-foreground inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium\"\n          >\n            <span class=\"text-[13px] leading-none\">{{ activeTheme.emoji }}</span>\n            {{ activeTheme.label }}\n          </span>\n          <span v-else class=\"text-muted-foreground bg-muted/50 rounded-full px-2 py-0.5 text-[11px] font-medium\">\n            Custom\n          </span>\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\" @click=\"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-[10px]\">\n                {{ filteredThemes.length }}<span v-if=\"themeSearch\"> / {{ THEMES.length }}</span> presets\n              </span>\n            </div>\n\n            <!-- Search -->\n            <div class=\"relative mb-3\">\n              <input\n                v-model=\"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              <div v-for=\"group in themesByCategory\" :key=\"group.key\">\n                <div class=\"text-muted-foreground/70 mb-1.5 text-[10px] font-medium tracking-wider uppercase\">\n                  {{ group.label }}\n                </div>\n                <div class=\"grid grid-cols-3 gap-2\">\n                  <button\n                    v-for=\"t in group.items\"\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                    :class=\"[\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                    ]\"\n                    @click=\"applyTheme(t)\"\n                  >\n                    <!-- Mini-preview window -->\n                    <div\n                      class=\"relative h-14 px-2 py-1.5\"\n                      :style=\"{\n                        background: (TINTS.find((x) => x.key === t.tint) || {})[t.mode === 'dark' ? 'dark' : 'light'],\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 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' }\" />\n                          <span class=\"size-1.5 rounded-full opacity-25\" :style=\"{ background: 'currentColor' }\" />\n                          <span class=\"size-1.5 rounded-full opacity-15\" :style=\"{ background: 'currentColor' }\" />\n                        </div>\n                        <span class=\"text-[10px] leading-none\">{{ t.emoji }}</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-[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                          >Aa</span\n                        >\n                        <span\n                          class=\"block h-1 flex-1 rounded-full opacity-30\"\n                          :style=\"{ background: 'currentColor' }\"\n                        />\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' }\" />\n                        <span class=\"block h-0.5 w-5 rounded-full opacity-15\" :style=\"{ background: 'currentColor' }\" />\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-[10px] leading-none font-medium\">{{ t.label }}</span>\n                      <Check v-if=\"activeTheme?.key === t.key\" class=\"text-primary size-3\" />\n                    </div>\n                  </button>\n                </div>\n              </div>\n              <div v-if=\"filteredThemes.length === 0\" class=\"text-muted-foreground py-6 text-center text-xs\">\n                No themes match \"<span class=\"font-mono\">{{ themeSearch }}</span\n                >\"\n              </div>\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              <button\n                v-for=\"m in modes\"\n                :key=\"m.value\"\n                type=\"button\"\n                :aria-pressed=\"theme === 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                  theme === m.value ? 'bg-background shadow-sm' : 'text-muted-foreground hover:text-foreground',\n                ]\"\n                @click=\"setTheme(m.value)\"\n              >\n                <component :is=\"m.icon\" class=\"size-3.5\" />\n                {{ m.label }}\n              </button>\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-[10px]\">{{ PRESETS.length }} options</span>\n            </div>\n            <div class=\"grid grid-cols-10 gap-1.5\">\n              <button\n                v-for=\"p in PRESETS\"\n                :key=\"p.key\"\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-all',\n                  preset === p.key ? 'ring-ring ring-2 ring-offset-2' : 'hover:scale-110',\n                ]\"\n                :style=\"{ background: p.swatch }\"\n                @click=\"preset = p.key\"\n              >\n                <Check v-if=\"preset === p.key\" class=\"absolute inset-0 m-auto size-3 text-white mix-blend-difference\" />\n              </button>\n\n              <!-- Custom color slot -->\n              <label\n                :class=\"[\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                ]\"\n                :style=\"preset === 'custom' ? { background: customColor } : {}\"\n                :aria-label=\"'Custom color'\"\n                :title=\"'Custom color'\"\n              >\n                <input type=\"color\" v-model=\"customColor\" class=\"sr-only\" @input=\"preset = 'custom'\" />\n                <Pipette v-if=\"preset !== 'custom'\" class=\"text-muted-foreground size-3\" />\n                <Check v-else class=\"size-3 text-white mix-blend-difference\" />\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              <button\n                v-for=\"t in TINTS\"\n                :key=\"t.key\"\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                @click=\"tint = t.key\"\n              >\n                {{ t.label }}\n              </button>\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              <button\n                v-for=\"f in FONTS\"\n                :key=\"f.key\"\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=\"{ fontFamily: f.stack }\"\n                @click=\"font = f.key\"\n              >\n                {{ f.label }}\n              </button>\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-[10px]\">{{ radius.toFixed(2) }}rem</span>\n            </div>\n            <Slider v-model=\"sliderModel\" :min=\"0\" :max=\"1\" :step=\"0.05\" />\n            <div class=\"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 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-[10px] 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-[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 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\" @click=\"reset\">\n          <RotateCcw class=\"size-3.5\" /> Reset\n        </Button>\n        <Button variant=\"outline\" size=\"sm\" class=\"flex-1 gap-1.5\" @click=\"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\" @click=\"copyCss\">\n          <Check v-if=\"copied\" class=\"size-3.5\" />\n          <Copy v-else class=\"size-3.5\" />\n          {{ copied ? 'Copied' : 'Copy CSS' }}\n        </Button>\n      </div>\n    </SheetContent>\n  </Sheet>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/ThemeCustomize.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/slider.json",
    "https://uipkge.dev/r/vue/sheet.json",
    "https://uipkge.dev/r/vue/use-theme.json"
  ],
  "description": "Compact theme customization popover. Light/Dark/System mode (via the useTheme cookie), 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"
  ]
}