{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "theme-switch",
  "title": "Theme Switch",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/theme-switch/theme-switch.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { useTheme } from 'next-themes'\nimport { ChevronDown, Monitor, Moon, Palette, Sparkles, Sun, type LucideIcon } from 'lucide-react'\nimport { SectionCard } from '@/components/ui/section-card'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} 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, LucideIcon> = {\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\nexport interface ThemeSwitchProps {\n  /** Controlled value. When omitted, falls back to next-themes' `theme`. */\n  value?: Theme\n  onValueChange?: (theme: Theme) => void\n  variant?: Variant\n  title?: string\n  description?: string\n  className?: string\n}\n\nconst ThemeSwitch = React.forwardRef<HTMLDivElement, ThemeSwitchProps>(\n  ({ value, onValueChange, variant = 'cards', title, description, className }, ref) => {\n    const { theme, setTheme } = useTheme()\n    const modelValue = (value ?? (theme as Theme) ?? 'system') as Theme\n\n    const options = VARIANT_OPTIONS[variant]\n    const activeIndex = React.useMemo(() => {\n      const i = options.indexOf(modelValue)\n      return i === -1 ? 0 : i\n    }, [options, modelValue])\n\n    const indicatorStyle: React.CSSProperties = {\n      width: `calc((100% - 4px) / ${options.length})`,\n      transform: `translateX(calc(${activeIndex} * 100%))`,\n    }\n\n    function set(t: Theme) {\n      if (onValueChange) onValueChange(t)\n      else setTheme(t)\n    }\n    function cycle() {\n      const next = options[(activeIndex + 1) % options.length]\n      if (next) set(next)\n    }\n\n    // Cards: full SectionCard with 3-button grid (default)\n    if (variant === 'cards') {\n      return (\n        <SectionCard\n          ref={ref}\n          title={title ?? 'Appearance'}\n          description={description ?? 'Choose your interface theme.'}\n          className={className}\n          headerAction={<Palette className=\"text-muted-foreground size-5\" />}\n        >\n          <div className=\"grid grid-cols-3 gap-2\">\n            {options.map((t) => {\n              const Icon = ICONS[t]\n              return (\n                <button\n                  type=\"button\"\n                  key={t}\n                  className={[\n                    'focus-visible:ring-ring rounded-md border p-3 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:outline-none',\n                    modelValue === t\n                      ? 'border-primary ring-primary bg-primary/5 ring-1'\n                      : 'border-border hover:bg-muted/50',\n                  ].join(' ')}\n                  onClick={() => set(t)}\n                >\n                  <Icon className=\"text-muted-foreground mb-2 size-4\" aria-hidden=\"true\" />\n                  <p className=\"text-xs font-medium\">{LABELS[t]}</p>\n                </button>\n              )\n            })}\n          </div>\n        </SectionCard>\n      )\n    }\n\n    // Icons: compact 3-icon segmented row, no labels\n    if (variant === 'icons') {\n      return (\n        <div\n          ref={ref}\n          role=\"radiogroup\"\n          aria-label={title ?? 'Theme'}\n          className={['border-border bg-card inline-flex items-center gap-0.5 rounded-md border p-0.5', className]\n            .filter(Boolean)\n            .join(' ')}\n        >\n          {options.map((t) => {\n            const Icon = ICONS[t]\n            return (\n              <button\n                type=\"button\"\n                key={t}\n                role=\"radio\"\n                aria-checked={modelValue === t}\n                aria-label={LABELS[t]}\n                className={[\n                  'focus-visible:ring-ring grid size-7 place-items-center rounded transition-colors focus-visible:ring-2 focus-visible:outline-none',\n                  modelValue === t\n                    ? 'bg-primary text-primary-foreground'\n                    : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n                ].join(' ')}\n                onClick={() => set(t)}\n              >\n                <Icon className=\"size-4\" aria-hidden=\"true\" />\n              </button>\n            )\n          })}\n        </div>\n      )\n    }\n\n    // Icon-only: header-grade icon button.\n    if (variant === 'icon-only') {\n      const Icon = ICONS[modelValue]\n      return (\n        <button\n          type=\"button\"\n          ref={ref as React.Ref<HTMLButtonElement>}\n          aria-label={LABELS[modelValue]}\n          className={[\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            className,\n          ]\n            .filter(Boolean)\n            .join(' ')}\n          onClick={cycle}\n        >\n          <Icon className=\"size-4\" aria-hidden=\"true\" />\n        </button>\n      )\n    }\n\n    // Dropdown: trigger button → menu of states\n    if (variant === 'dropdown') {\n      const TriggerIcon = ICONS[modelValue]\n      return (\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <button\n              type=\"button\"\n              className={[\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                className,\n              ]\n                .filter(Boolean)\n                .join(' ')}\n            >\n              <TriggerIcon className=\"size-4\" aria-hidden=\"true\" />\n              <span>{LABELS[modelValue]}</span>\n              <ChevronDown className=\"size-3 opacity-60\" aria-hidden=\"true\" />\n            </button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"end\" className=\"min-w-[140px]\">\n            {options.map((t) => {\n              const Icon = ICONS[t]\n              return (\n                <DropdownMenuItem key={t} onClick={() => set(t)}>\n                  <Icon className=\"mr-2 size-4\" aria-hidden=\"true\" />\n                  <span>{LABELS[t]}</span>\n                </DropdownMenuItem>\n              )\n            })}\n          </DropdownMenuContent>\n        </DropdownMenu>\n      )\n    }\n\n    // Pill / Pill-4: equal segments with sliding indicator\n    if (variant === 'pill' || variant === 'pill-4') {\n      return (\n        <div\n          ref={ref}\n          role=\"radiogroup\"\n          aria-label={title ?? 'Theme'}\n          className={['border-border bg-card relative inline-flex w-full max-w-md rounded-full border p-0.5', className]\n            .filter(Boolean)\n            .join(' ')}\n        >\n          <span\n            aria-hidden\n            className=\"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          {options.map((t) => {\n            const Icon = ICONS[t]\n            return (\n              <button\n                type=\"button\"\n                key={t}\n                role=\"radio\"\n                aria-checked={modelValue === t}\n                aria-label={LABELS[t]}\n                className={[\n                  '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                  modelValue === t ? 'text-primary-foreground' : 'text-muted-foreground hover:text-foreground',\n                ].join(' ')}\n                onClick={() => set(t)}\n              >\n                <Icon className=\"size-3.5\" aria-hidden=\"true\" />\n                <span>{LABELS[t]}</span>\n              </button>\n            )\n          })}\n        </div>\n      )\n    }\n\n    // Switch: iOS-style 2-state toggle with thumb that slides\n    return (\n      <button\n        type=\"button\"\n        ref={ref as React.Ref<HTMLButtonElement>}\n        role=\"switch\"\n        aria-checked={modelValue === 'dark'}\n        aria-label={LABELS[modelValue]}\n        className={[\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          className,\n        ]\n          .filter(Boolean)\n          .join(' ')}\n        onClick={() => set(modelValue === 'dark' ? 'light' : 'dark')}\n      >\n        <Sun\n          className={[\n            'absolute left-1.5 size-4 text-amber-500 transition-opacity',\n            modelValue === 'dark' ? 'opacity-30' : 'opacity-100',\n          ].join(' ')}\n          aria-hidden=\"true\"\n        />\n        <Moon\n          className={[\n            'absolute right-1.5 size-4 text-zinc-300 transition-opacity',\n            modelValue === 'light' ? 'opacity-30' : 'opacity-100',\n          ].join(' ')}\n          aria-hidden=\"true\"\n        />\n        <span\n          aria-hidden\n          className=\"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    )\n  },\n)\nThemeSwitch.displayName = 'ThemeSwitch'\n\nexport { ThemeSwitch }\n",
      "type": "registry:ui",
      "target": "~/components/ui/theme-switch/theme-switch.tsx"
    },
    {
      "path": "packages/registry-react/components/theme-switch/index.ts",
      "content": "export { ThemeSwitch, type ThemeSwitchProps } from './theme-switch'\n",
      "type": "registry:ui",
      "target": "~/components/ui/theme-switch/index.ts"
    }
  ],
  "dependencies": [
    "next-themes",
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/section-card.json",
    "https://uipkge.dev/r/react/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"
  ]
}