{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "theme-switch",
  "title": "Theme Switch",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/theme-switch/ThemeSwitch.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { ChevronDown, Monitor, Moon, Palette, Sparkles, Sun } from 'lucide-vue-next'\nimport { SectionCard } from '@/components/ui/section-card'\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'\n\ntype Theme = 'light' | 'dark' | 'system' | 'black'\ntype Variant = 'cards' | 'icons' | 'icon-only' | 'dropdown' | 'pill' | 'pill-4' | 'switch'\n\nconst ICONS: Record<Theme, any> = {\n  light: Sun,\n  dark: Moon,\n  system: Monitor,\n  black: Sparkles,\n}\n\nconst LABELS: Record<Theme, string> = {\n  light: 'Light',\n  dark: 'Dark',\n  system: 'System',\n  black: 'Black',\n}\n\nconst VARIANT_OPTIONS: Record<Variant, Theme[]> = {\n  cards: ['light', 'dark', 'system'],\n  icons: ['light', 'dark', 'system'],\n  'icon-only': ['light', 'dark'],\n  dropdown: ['light', 'dark', 'system'],\n  pill: ['light', 'dark', 'system'],\n  'pill-4': ['system', 'light', 'dark', 'black'],\n  switch: ['light', 'dark'],\n}\n\nconst props = withDefaults(\n  defineProps<{\n    modelValue: Theme\n    variant?: Variant\n    title?: string\n    description?: string\n    class?: string\n  }>(),\n  { variant: 'cards' },\n)\n\nconst emit = defineEmits<{ 'update:modelValue': [Theme] }>()\n\nconst options = computed(() => VARIANT_OPTIONS[props.variant])\nconst activeIndex = computed(() => {\n  const i = options.value.indexOf(props.modelValue)\n  return i === -1 ? 0 : i\n})\n\nconst indicatorStyle = computed(() => ({\n  width: `calc((100% - 4px) / ${options.value.length})`,\n  transform: `translateX(calc(${activeIndex.value} * 100%))`,\n}))\n\nfunction set(t: Theme) {\n  emit('update:modelValue', t)\n}\nfunction cycle() {\n  const next = options.value[(activeIndex.value + 1) % options.value.length]\n  if (next) emit('update:modelValue', next)\n}\n</script>\n\n<template>\n  <!-- Cards: full SectionCard with 3-button grid (default) -->\n  <SectionCard\n    v-if=\"variant === 'cards'\"\n    :title=\"title ?? 'Appearance'\"\n    :description=\"description ?? 'Choose your interface theme.'\"\n    :class=\"$props.class\"\n  >\n    <template #header-action>\n      <Palette class=\"text-muted-foreground size-5\" />\n    </template>\n    <div class=\"grid grid-cols-3 gap-2\">\n      <button\n        type=\"button\"\n        v-for=\"t in options\"\n        :key=\"t\"\n        class=\"focus-visible:ring-ring rounded-md border p-3 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:outline-none\"\n        :class=\"\n          modelValue === t ? 'border-primary ring-primary bg-primary/5 ring-1' : 'border-border hover:bg-muted/50'\n        \"\n        @click=\"set(t)\"\n      >\n        <component :is=\"ICONS[t]\" class=\"text-muted-foreground mb-2 size-4\" aria-hidden=\"true\" />\n        <p class=\"text-xs font-medium\">{{ LABELS[t] }}</p>\n      </button>\n    </div>\n  </SectionCard>\n\n  <!-- Icons: compact 3-icon segmented row, no labels -->\n  <div\n    v-else-if=\"variant === 'icons'\"\n    role=\"radiogroup\"\n    :aria-label=\"title ?? 'Theme'\"\n    :class=\"['border-border bg-card inline-flex items-center gap-0.5 rounded-md border p-0.5', $props.class]\"\n  >\n    <button\n      type=\"button\"\n      v-for=\"t in options\"\n      :key=\"t\"\n      role=\"radio\"\n      :aria-checked=\"modelValue === t\"\n      :aria-label=\"LABELS[t]\"\n      class=\"focus-visible:ring-ring grid size-7 place-items-center rounded transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n      :class=\"\n        modelValue === t\n          ? 'bg-primary text-primary-foreground'\n          : 'text-muted-foreground hover:bg-muted hover:text-foreground'\n      \"\n      @click=\"set(t)\"\n    >\n      <component :is=\"ICONS[t]\" class=\"size-4\" aria-hidden=\"true\" />\n    </button>\n  </div>\n\n  <!-- Icon-only: header-grade icon button. No border, ghost background,\n       rounded-lg to match adjacent header affordances (notification\n       bell, profile avatar). The previous `rounded-full border bg-card`\n       coin-shape stood out from typical icon-button row layouts; if you\n       need that look, pass `class=\"rounded-full border border-border bg-card\"`\n       and it'll override.\n\n       Dropped the rotate+fade Transition that the previous version\n       wrapped the icon in. `mode=\"out-in\"` with opacity-0 on both the\n       enter-from and leave-to states left a frame where the button was\n       empty -- visible as a flicker that occasionally rendered as \"no\n       icon at all\" on slower devices. A direct swap reads cleaner. -->\n  <button\n    type=\"button\"\n    v-else-if=\"variant === 'icon-only'\"\n    :aria-label=\"LABELS[modelValue]\"\n    :class=\"[\n      'text-muted-foreground hover:text-foreground hover:bg-accent focus-visible:ring-ring inline-flex size-8 items-center justify-center rounded-lg transition-colors focus-visible:ring-2 focus-visible:outline-none',\n      $props.class,\n    ]\"\n    @click=\"cycle\"\n  >\n    <component :is=\"ICONS[modelValue]\" class=\"size-4\" aria-hidden=\"true\" />\n  </button>\n\n  <!-- Dropdown: trigger button → menu of states -->\n  <DropdownMenu v-else-if=\"variant === 'dropdown'\">\n    <DropdownMenuTrigger as-child>\n      <button\n        type=\"button\"\n        :class=\"[\n          'border-border bg-card hover:bg-muted focus-visible:ring-ring inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm transition focus-visible:ring-2 focus-visible:outline-none',\n          $props.class,\n        ]\"\n      >\n        <component :is=\"ICONS[modelValue]\" class=\"size-4\" aria-hidden=\"true\" />\n        <span>{{ LABELS[modelValue] }}</span>\n        <ChevronDown class=\"size-3 opacity-60\" aria-hidden=\"true\" />\n      </button>\n    </DropdownMenuTrigger>\n    <DropdownMenuContent align=\"end\" class=\"min-w-[140px]\">\n      <DropdownMenuItem v-for=\"t in options\" :key=\"t\" @click=\"set(t)\">\n        <component :is=\"ICONS[t]\" class=\"mr-2 size-4\" aria-hidden=\"true\" />\n        <span>{{ LABELS[t] }}</span>\n      </DropdownMenuItem>\n    </DropdownMenuContent>\n  </DropdownMenu>\n\n  <!-- Pill / Pill-4: equal segments with sliding indicator -->\n  <div\n    v-else-if=\"variant === 'pill' || variant === 'pill-4'\"\n    role=\"radiogroup\"\n    :aria-label=\"title ?? 'Theme'\"\n    :class=\"['border-border bg-card relative inline-flex w-full max-w-md rounded-full border p-0.5', $props.class]\"\n  >\n    <span\n      aria-hidden\n      class=\"bg-primary pointer-events-none absolute top-0.5 bottom-0.5 left-0.5 rounded-full transition-transform duration-300 ease-out\"\n      :style=\"indicatorStyle\"\n    />\n    <button\n      type=\"button\"\n      v-for=\"t in options\"\n      :key=\"t\"\n      role=\"radio\"\n      :aria-checked=\"modelValue === t\"\n      :aria-label=\"LABELS[t]\"\n      class=\"focus-visible:ring-ring relative z-[1] inline-flex h-7 flex-1 items-center justify-center gap-1.5 rounded-full px-3 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n      :class=\"modelValue === t ? 'text-primary-foreground' : 'text-muted-foreground hover:text-foreground'\"\n      @click=\"set(t)\"\n    >\n      <component :is=\"ICONS[t]\" class=\"size-3.5\" aria-hidden=\"true\" />\n      <span>{{ LABELS[t] }}</span>\n    </button>\n  </div>\n\n  <!-- Switch: iOS-style 2-state toggle with thumb that slides -->\n  <button\n    type=\"button\"\n    v-else-if=\"variant === 'switch'\"\n    role=\"switch\"\n    :aria-checked=\"modelValue === 'dark'\"\n    :aria-label=\"LABELS[modelValue]\"\n    :class=\"[\n      'border-border focus-visible:ring-ring relative inline-flex h-8 w-16 items-center rounded-full border transition-colors focus-visible:ring-2 focus-visible:outline-none',\n      modelValue === 'dark' ? 'bg-zinc-900' : 'bg-amber-100',\n      $props.class,\n    ]\"\n    @click=\"set(modelValue === 'dark' ? 'light' : 'dark')\"\n  >\n    <Sun\n      class=\"absolute left-1.5 size-4 text-amber-500 transition-opacity\"\n      :class=\"modelValue === 'dark' ? 'opacity-30' : 'opacity-100'\"\n      aria-hidden=\"true\"\n    />\n    <Moon\n      class=\"absolute right-1.5 size-4 text-zinc-300 transition-opacity\"\n      :class=\"modelValue === 'light' ? 'opacity-30' : 'opacity-100'\"\n      aria-hidden=\"true\"\n    />\n    <span\n      aria-hidden\n      class=\"bg-card border-border absolute size-6 rounded-full border shadow transition-transform duration-300 ease-out\"\n      :style=\"{ transform: `translateX(${modelValue === 'dark' ? '36px' : '4px'})` }\"\n    />\n  </button>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/theme-switch/ThemeSwitch.vue"
    },
    {
      "path": "packages/registry-vue/components/theme-switch/index.ts",
      "content": "export { default as ThemeSwitch } from './ThemeSwitch.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/theme-switch/index.ts"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/section-card.json",
    "https://uipkge.dev/r/vue/dropdown-menu.json"
  ],
  "description": "Light / dark / system theme toggle — drop in the header. Seven visual variants: `cards`, `icons`, `icon-only`, `dropdown`, `pill`, `pill-4`, and `switch`. Persists choice to `localStorage` and respects `prefers-color-scheme` for `system`.",
  "categories": [
    "action"
  ]
}