{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "theme-toggle-block",
  "type": "registry:block",
  "title": "Theme toggle",
  "description": "Controlled theme controls with a three-value switch, binary switch, cycle button, and dropdown.",
  "dependencies": [
    "lucide-react@^1.27.0"
  ],
  "registryDependencies": [
    "http://127.0.0.1:3000/r/button.json",
    "http://127.0.0.1:3000/r/core.json",
    "http://127.0.0.1:3000/r/dropdown-menu.json",
    "http://127.0.0.1:3000/r/switch.json"
  ],
  "files": [
    {
      "path": "src/registry/blocks/control-ui/theme-toggle.tsx",
      "target": "@components/control-ui/blocks/theme-toggle.tsx",
      "type": "registry:block",
      "content": "\"use client\";\n\nimport { CheckIcon, ChevronDownIcon, MonitorIcon, MoonIcon, SunIcon } from \"lucide-react\";\nimport type { ComponentProps, ComponentType, CSSProperties } from \"react\";\nimport { useId } from \"react\";\nimport { Button } from \"@/components/control-ui/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuTrigger,\n} from \"@/components/control-ui/ui/dropdown-menu\";\nimport { Switch } from \"@/components/control-ui/ui/switch\";\n\nexport type ThemeMode = \"light\" | \"dark\" | \"system\";\n\nexport type ThemeToggleOption = {\n  value: ThemeMode;\n  label: string;\n  icon: ComponentType<{ className?: string; \"aria-hidden\"?: boolean }>;\n  disabled?: boolean;\n};\n\ntype ThemeControlProps = {\n  value: ThemeMode;\n  onValueChange: (value: ThemeMode) => void;\n  options?: readonly ThemeToggleOption[];\n};\n\nexport type ThemeToggleProps = Omit<ComponentProps<typeof Button>, \"value\" | \"onChange\"> &\n  ThemeControlProps & {\n    showLabel?: boolean;\n    showLabels?: boolean;\n  };\n\nexport type ThemeSwitchProps = Omit<ComponentProps<typeof Switch>, \"checked\" | \"defaultChecked\" | \"onCheckedChange\" | \"value\"> & {\n  value: ThemeMode;\n  onValueChange: (value: ThemeMode) => void;\n  onValue?: ThemeMode;\n  offValue?: ThemeMode;\n};\n\nexport type ThemeSegmentedSwitchProps = Omit<ComponentProps<\"div\">, \"onChange\"> &\n  ThemeControlProps & {\n    name?: string;\n    showLabels?: boolean;\n  };\n\nexport type ThemeDropdownProps = Omit<ComponentProps<typeof DropdownMenuTrigger>, \"children\" | \"value\" | \"onChange\"> &\n  ThemeControlProps & {\n    label?: string;\n  };\n\nconst defaultThemeOptions: readonly ThemeToggleOption[] = [\n  { value: \"light\", label: \"Light\", icon: SunIcon },\n  { value: \"dark\", label: \"Dark\", icon: MoonIcon },\n  { value: \"system\", label: \"System\", icon: MonitorIcon },\n];\n\nconst defaultSwitchCheckedIcon = <MoonIcon />;\nconst defaultSwitchUncheckedIcon = <SunIcon />;\n\nfunction classes(...values: Array<string | undefined>) {\n  return values.filter(Boolean).join(\" \");\n}\n\nfunction enabledOptions(options: readonly ThemeToggleOption[]) {\n  return options.filter((option) => !option.disabled);\n}\n\nfunction controlOptions(options: readonly ThemeToggleOption[]) {\n  return options.length > 0 ? options : defaultThemeOptions;\n}\n\nfunction currentThemeOption(value: ThemeMode, options: readonly ThemeToggleOption[]) {\n  return (\n    options.find((option) => option.value === value) ??\n    defaultThemeOptions.find((option) => option.value === value) ??\n    defaultThemeOptions[2]\n  );\n}\n\nfunction effectiveThemeValue(value: ThemeMode, options: readonly ThemeToggleOption[]) {\n  if (options.some((option) => option.value === value)) return value;\n  return enabledOptions(options)[0]?.value ?? options[0]?.value ?? \"system\";\n}\n\nfunction nextThemeValue(value: ThemeMode, options: readonly ThemeToggleOption[]) {\n  const available = enabledOptions(options);\n  if (available.length === 0) return value;\n\n  const currentIndex = available.findIndex((option) => option.value === value);\n  return available[(currentIndex + 1) % available.length]?.value ?? available[0].value;\n}\n\nexport function ThemeSwitch({\n  value,\n  onValueChange,\n  onValue = \"dark\",\n  offValue = \"light\",\n  checkedIcon,\n  uncheckedIcon,\n  \"aria-label\": ariaLabel,\n  ...props\n}: ThemeSwitchProps) {\n  return (\n    <Switch\n      checked={value === onValue}\n      checkedIcon={checkedIcon ?? defaultSwitchCheckedIcon}\n      uncheckedIcon={uncheckedIcon ?? defaultSwitchUncheckedIcon}\n      onCheckedChange={(checked) => onValueChange(checked ? onValue : offValue)}\n      aria-label={ariaLabel ?? \"Dark mode\"}\n      {...props}\n    />\n  );\n}\n\nexport function ThemeSegmentedSwitch({\n  value,\n  onValueChange,\n  options = defaultThemeOptions,\n  showLabels = false,\n  name,\n  className,\n  style,\n  \"aria-label\": ariaLabel = \"Theme\",\n  ...props\n}: ThemeSegmentedSwitchProps) {\n  const generatedName = useId();\n  const choices = controlOptions(options);\n  const currentValue = effectiveThemeValue(value, choices);\n  const activeIndex = Math.max(\n    0,\n    choices.findIndex((option) => option.value === currentValue),\n  );\n  const indicatorStyle = {\n    width: `calc((100% - 0.25rem) / ${choices.length})`,\n    transform: `translateX(${activeIndex * 100}%)`,\n  } satisfies CSSProperties;\n\n  return (\n    <div\n      role=\"radiogroup\"\n      aria-label={ariaLabel}\n      className={classes(\n        \"relative isolate inline-flex h-7 w-fit shrink-0 rounded-full border border-border bg-foreground/8 p-0.5 text-muted-foreground shadow-inner\",\n        className,\n      )}\n      style={style}\n      {...props}\n    >\n      <span\n        aria-hidden\n        className=\"pointer-events-none absolute inset-y-0.5 left-0.5 rounded-full bg-background shadow-sm transition-transform duration-[var(--duration-base)] ease-[var(--ease-emphasized)]\"\n        style={indicatorStyle}\n      />\n      {choices.map((option) => {\n        const selected = option.value === currentValue;\n        const Icon = option.icon;\n\n        return (\n          <label\n            key={option.value}\n            data-selected={selected ? \"true\" : undefined}\n            data-disabled={option.disabled ? \"true\" : undefined}\n            className={classes(\n              \"relative z-[1] inline-flex h-6 cursor-pointer items-center justify-center gap-1.5 rounded-full px-1.5 text-xs font-medium outline-none transition-[color,scale] duration-[var(--duration-fast)] ease-[var(--ease-standard)] hover:text-foreground has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-foreground/25 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-45 data-[selected=true]:text-foreground active:scale-95\",\n              showLabels ? \"min-w-20\" : \"w-7\",\n            )}\n          >\n            <input\n              type=\"radio\"\n              name={name ?? generatedName}\n              value={option.value}\n              checked={selected}\n              disabled={option.disabled}\n              aria-label={option.label}\n              className=\"sr-only\"\n              onChange={(event) => {\n                if (event.currentTarget.checked) onValueChange(option.value);\n              }}\n            />\n            <Icon className=\"size-3.5\" aria-hidden />\n            {showLabels ? <span>{option.label}</span> : null}\n          </label>\n        );\n      })}\n    </div>\n  );\n}\n\nexport function ThemeToggle({\n  value,\n  onValueChange,\n  options = defaultThemeOptions,\n  showLabel,\n  showLabels,\n  className,\n  onClick,\n  \"aria-label\": ariaLabel,\n  ...props\n}: ThemeToggleProps) {\n  const current = currentThemeOption(value, options);\n  const Icon = current.icon;\n  const visibleLabel = showLabel ?? showLabels ?? false;\n\n  return (\n    <Button\n      variant=\"surface\"\n      size=\"sm\"\n      className={classes(\"gap-2\", className)}\n      aria-label={ariaLabel ?? `Theme: ${current.label}`}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) onValueChange(nextThemeValue(value, options));\n      }}\n      {...props}\n    >\n      <Icon className=\"size-3.5\" aria-hidden />\n      {visibleLabel ? <span>{current.label}</span> : null}\n    </Button>\n  );\n}\n\nexport function ThemeDropdown({\n  value,\n  onValueChange,\n  options = defaultThemeOptions,\n  label = \"Theme\",\n  className,\n  \"aria-label\": ariaLabel,\n  ...props\n}: ThemeDropdownProps) {\n  const current = currentThemeOption(value, options);\n  const CurrentIcon = current.icon;\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger aria-label={ariaLabel ?? label} className={classes(\"min-w-36 gap-2\", className)} {...props}>\n        <CurrentIcon className=\"size-3.5\" aria-hidden />\n        <span className=\"min-w-0 truncate\">{current.label}</span>\n        <ChevronDownIcon className=\"size-3 text-muted-foreground\" aria-hidden />\n      </DropdownMenuTrigger>\n      <DropdownMenuContent className=\"min-w-40\">\n        <DropdownMenuLabel>{label}</DropdownMenuLabel>\n        {options.map((option) => {\n          const selected = option.value === value;\n          const Icon = option.icon;\n\n          return (\n            <DropdownMenuItem key={option.value} disabled={option.disabled} onClick={() => onValueChange(option.value)}>\n              <Icon className=\"size-3.5\" aria-hidden />\n              <span className=\"min-w-0 flex-1 truncate\">{option.label}</span>\n              {selected ? <CheckIcon className=\"size-3.5\" aria-hidden /> : <span className=\"size-3.5\" aria-hidden />}\n            </DropdownMenuItem>\n          );\n        })}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
    }
  ],
  "meta": {}
}
