{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A light/dark/system theme switch with sun-moon, pill, switch and orb visual treatments.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { Monitor, Moon, Sun } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type KeyboardEvent,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nexport const THEME_TOGGLE_VARIANTS = [\n  \"sun-moon\",\n  \"pill\",\n  \"switch\",\n  \"orb\",\n] as const;\n\nexport type ThemeToggleVariant = (typeof THEME_TOGGLE_VARIANTS)[number];\nexport type ThemeToggleValue = \"light\" | \"dark\" | \"system\";\n\nexport type ThemeToggleProps = {\n  /** Additional CSS classes */\n  className?: string;\n  /** Default theme for uncontrolled usage */\n  defaultTheme?: ThemeToggleValue;\n  /** Accessible label describing the control */\n  label?: string;\n  /** Called whenever the chosen theme changes */\n  onThemeChange?: (theme: ThemeToggleValue) => void;\n  /** Whether to include a third \"system\" option */\n  showSystem?: boolean;\n  /** Size of the control */\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Controlled theme value */\n  theme?: ThemeToggleValue;\n  /** Visual treatment of the control */\n  variant?: ThemeToggleVariant;\n};\n\n/* -------------------------------------------------------------------------- */\n/* Motion tokens                                                              */\n/* -------------------------------------------------------------------------- */\n\nconst SPRING = { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\nconst MORPH_SPRING = { bounce: 0.1, duration: 0.35, type: \"spring\" as const };\nconst KNOB_SPRING = { bounce: 0.1, duration: 0.3, type: \"spring\" as const };\nconst INSTANT = { duration: 0 };\nconst EASE_OUT: [number, number, number, number] = [0.23, 1, 0.32, 1];\nconst FADE = { duration: 0.3, ease: EASE_OUT };\nconst TRAVEL = { duration: 0.4, ease: EASE_OUT };\n\n/* -------------------------------------------------------------------------- */\n/* Sizing                                                                     */\n/* -------------------------------------------------------------------------- */\n\ntype SizeConfig = {\n  glyph: number;\n  icon: string;\n  knob: number;\n  orb: number;\n  pad: number;\n  segment: string;\n  text: string;\n};\n\nconst SIZES: Record<\"sm\" | \"md\" | \"lg\", SizeConfig> = {\n  lg: {\n    glyph: 74,\n    icon: \"size-5\",\n    knob: 44,\n    orb: 104,\n    pad: 5,\n    segment: \"h-11 px-5\",\n    text: \"text-sm\",\n  },\n  md: {\n    glyph: 58,\n    icon: \"size-4\",\n    knob: 34,\n    orb: 82,\n    pad: 4,\n    segment: \"h-10 px-4\",\n    text: \"text-sm\",\n  },\n  sm: {\n    glyph: 42,\n    icon: \"size-3.5\",\n    knob: 26,\n    orb: 58,\n    pad: 3,\n    segment: \"h-8 px-3\",\n    text: \"text-xs\",\n  },\n};\n\ntype OptionEntry = {\n  icon: LucideIcon;\n  label: string;\n  value: ThemeToggleValue;\n};\n\nconst THEME_OPTIONS: OptionEntry[] = [\n  { icon: Sun, label: \"Light\", value: \"light\" },\n  { icon: Moon, label: \"Dark\", value: \"dark\" },\n  { icon: Monitor, label: \"System\", value: \"system\" },\n];\n\n/* -------------------------------------------------------------------------- */\n/* Variant 1 — sun-moon: a single disc carved by a travelling mask circle      */\n/* -------------------------------------------------------------------------- */\n\nconst VIEWBOX = 24;\nconst CENTER = 12;\nconst RAY_ANGLES = [0, 45, 90, 135, 180, 225, 270, 315];\nconst RAY_INNER = 7.3;\nconst RAY_OUTER = 10.3;\nconst RAY_STAGGER = 0.025;\nconst RAY_WIDTH = 1.7;\nconst RAY_RETRACTED_SCALE = 0.12;\n\n/** Where the carving circle sits: off-canvas (sun), overlapping (crescent), or a\n *  huge disc whose left edge lands exactly on the centre line (half-lit system). */\nconst MASK_STATE: Record<\n  ThemeToggleValue,\n  { cx: number; cy: number; r: number }\n> = {\n  dark: { cx: 17.6, cy: 8, r: 8.6 },\n  light: { cx: 38, cy: 4, r: 9 },\n  system: { cx: 52, cy: 12, r: 40 },\n};\n\nconst DISC_RADIUS: Record<ThemeToggleValue, number> = {\n  dark: 8.2,\n  light: 5,\n  system: 7,\n};\n\nconst RAY_ROTATION: Record<ThemeToggleValue, number> = {\n  dark: 60,\n  light: 0,\n  system: 30,\n};\n\nconst GLYPH_COLOR: Record<ThemeToggleValue, string> = {\n  dark: \"text-indigo-400\",\n  light: \"text-amber-500\",\n  system: \"text-foreground\",\n};\n\nconst rayTarget = (angle: number, value: ThemeToggleValue) => {\n  if (value === \"light\") {\n    return { opacity: 1, scale: 1 };\n  }\n  if (value === \"dark\") {\n    return { opacity: 0, scale: RAY_RETRACTED_SCALE };\n  }\n  // \"system\" keeps only the rays on the lit half of the disc.\n  const isLit = angle > 90 && angle < 270;\n  return {\n    opacity: isLit ? 1 : 0,\n    scale: isLit ? 1 : RAY_RETRACTED_SCALE,\n  };\n};\n\ntype SunMoonGlyphProps = {\n  canAnimate: boolean;\n  maskId: string;\n  size: number;\n  value: ThemeToggleValue;\n};\n\nconst SunMoonGlyph = ({\n  canAnimate,\n  maskId,\n  size,\n  value,\n}: SunMoonGlyphProps) => {\n  const mask = MASK_STATE[value];\n  const morph = canAnimate ? MORPH_SPRING : INSTANT;\n\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className={cn(\"transition-colors duration-300\", GLYPH_COLOR[value])}\n      height={size}\n      viewBox={`0 0 ${VIEWBOX} ${VIEWBOX}`}\n      width={size}\n    >\n      <mask id={maskId}>\n        <rect fill=\"white\" height={VIEWBOX} width={VIEWBOX} x={0} y={0} />\n        <motion.circle\n          animate={mask}\n          cx={mask.cx}\n          cy={mask.cy}\n          fill=\"black\"\n          initial={false}\n          r={mask.r}\n          transition={morph}\n        />\n      </mask>\n\n      <motion.circle\n        animate={{ r: DISC_RADIUS[value] }}\n        cx={CENTER}\n        cy={CENTER}\n        fill=\"currentColor\"\n        initial={false}\n        mask={`url(#${maskId})`}\n        r={DISC_RADIUS[value]}\n        transition={morph}\n      />\n\n      <motion.g\n        animate={{ rotate: RAY_ROTATION[value] }}\n        initial={false}\n        style={{ transformOrigin: `${CENTER}px ${CENTER}px` }}\n        transition={morph}\n      >\n        {RAY_ANGLES.map((angle, index) => {\n          const radians = (angle * Math.PI) / 180;\n          return (\n            <motion.line\n              animate={rayTarget(angle, value)}\n              initial={false}\n              key={angle}\n              stroke=\"currentColor\"\n              strokeLinecap=\"round\"\n              strokeWidth={RAY_WIDTH}\n              style={{ transformOrigin: `${CENTER}px ${CENTER}px` }}\n              transition={\n                canAnimate ? { ...SPRING, delay: index * RAY_STAGGER } : INSTANT\n              }\n              x1={CENTER + Math.cos(radians) * RAY_INNER}\n              x2={CENTER + Math.cos(radians) * RAY_OUTER}\n              y1={CENTER + Math.sin(radians) * RAY_INNER}\n              y2={CENTER + Math.sin(radians) * RAY_OUTER}\n            />\n          );\n        })}\n      </motion.g>\n    </svg>\n  );\n};\n\n/* -------------------------------------------------------------------------- */\n/* Variant 2 — pill: segmented control with a shared-layout thumb              */\n/* -------------------------------------------------------------------------- */\n\nconst ACTIVE_ICON_COLOR: Record<ThemeToggleValue, string> = {\n  dark: \"text-indigo-400\",\n  light: \"text-amber-500\",\n  system: \"text-foreground\",\n};\n\ntype PillToggleProps = {\n  canAnimate: boolean;\n  groupLabel: string;\n  iconClassName: string;\n  onKeyDown: (event: KeyboardEvent<HTMLButtonElement>, index: number) => void;\n  onSelect: (value: ThemeToggleValue) => void;\n  options: OptionEntry[];\n  registerRef: (index: number, el: HTMLButtonElement | null) => void;\n  segmentClassName: string;\n  selectedIndex: number;\n  textClassName: string;\n  thumbId: string;\n};\n\nconst PillToggle = ({\n  canAnimate,\n  groupLabel,\n  iconClassName,\n  onKeyDown,\n  onSelect,\n  options,\n  registerRef,\n  segmentClassName,\n  selectedIndex,\n  textClassName,\n  thumbId,\n}: PillToggleProps) => (\n  <div\n    aria-label={groupLabel}\n    className=\"relative inline-flex items-center gap-1 rounded-full border border-border bg-muted p-1 shadow-[inset_0_1px_3px_rgba(0,0,0,0.08)]\"\n    role=\"radiogroup\"\n  >\n    {options.map((option, index) => {\n      const active = index === selectedIndex;\n      const Icon = option.icon;\n      return (\n        <button\n          aria-checked={active}\n          className={cn(\n            \"relative flex cursor-pointer items-center justify-center gap-2 rounded-full font-medium transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n            active\n              ? \"text-foreground\"\n              : \"text-muted-foreground hover:text-foreground/70\",\n            segmentClassName,\n            textClassName\n          )}\n          key={option.value}\n          onClick={() => onSelect(option.value)}\n          onKeyDown={(event) => onKeyDown(event, index)}\n          ref={(el) => {\n            registerRef(index, el);\n          }}\n          role=\"radio\"\n          tabIndex={active ? 0 : -1}\n          type=\"button\"\n        >\n          {active ? (\n            <motion.span\n              aria-hidden=\"true\"\n              className=\"absolute inset-0 rounded-full bg-background shadow-[0_1px_2px_rgba(0,0,0,0.16),0_8px_18px_-8px_rgba(0,0,0,0.45)] ring-1 ring-foreground/10\"\n              layoutId={thumbId}\n              transition={canAnimate ? SPRING : INSTANT}\n            />\n          ) : null}\n          <Icon\n            className={cn(\n              \"relative z-10 transition-colors duration-200\",\n              iconClassName,\n              active ? ACTIVE_ICON_COLOR[option.value] : \"\"\n            )}\n          />\n          <span className=\"relative z-10\">{option.label}</span>\n        </button>\n      );\n    })}\n  </div>\n);\n\n/* -------------------------------------------------------------------------- */\n/* Variant 3 — switch: sky-to-night track with a lit knob                      */\n/* -------------------------------------------------------------------------- */\n\nconst TRACK_LAYERS: Record<ThemeToggleValue, string> = {\n  dark: \"linear-gradient(180deg,#1e293b 0%,#0f172a 55%,#020617 100%)\",\n  light: \"linear-gradient(180deg,#7dd3fc 0%,#38bdf8 55%,#0284c7 100%)\",\n  system:\n    \"linear-gradient(100deg,#38bdf8 0%,#38bdf8 34%,#1e293b 64%,#020617 100%)\",\n};\n\nconst TRACK_LAYER_ORDER: ThemeToggleValue[] = [\"light\", \"dark\", \"system\"];\n\nconst STAR_POSITIONS = [\n  { left: 0.14, size: 2, top: 0.26 },\n  { left: 0.29, size: 1.5, top: 0.64 },\n  { left: 0.45, size: 2.5, top: 0.2 },\n  { left: 0.58, size: 1.5, top: 0.72 },\n  { left: 0.72, size: 2, top: 0.36 },\n  { left: 0.86, size: 1.5, top: 0.58 },\n];\n\nconst KNOB_ICON: Record<ThemeToggleValue, LucideIcon> = {\n  dark: Moon,\n  light: Sun,\n  system: Monitor,\n};\n\nconst KNOB_ICON_COLOR: Record<ThemeToggleValue, string> = {\n  dark: \"text-slate-700\",\n  light: \"text-amber-500\",\n  system: \"text-slate-600\",\n};\n\nconst starOpacityFor = (value: ThemeToggleValue) => {\n  if (value === \"dark\") {\n    return 1;\n  }\n  return value === \"system\" ? 0.55 : 0;\n};\n\ntype SwitchVisualProps = {\n  canAnimate: boolean;\n  iconClassName: string;\n  index: number;\n  knob: number;\n  pad: number;\n  trackHeight: number;\n  trackWidth: number;\n  value: ThemeToggleValue;\n};\n\nconst SwitchVisual = ({\n  canAnimate,\n  iconClassName,\n  index,\n  knob,\n  pad,\n  trackHeight,\n  trackWidth,\n  value,\n}: SwitchVisualProps) => {\n  const Icon = KNOB_ICON[value];\n  const starOpacity = starOpacityFor(value);\n\n  return (\n    <span\n      aria-hidden=\"true\"\n      className=\"relative block overflow-hidden rounded-full\"\n      style={{ height: trackHeight, width: trackWidth }}\n    >\n      {TRACK_LAYER_ORDER.map((layer) => (\n        <motion.span\n          animate={{ opacity: layer === value ? 1 : 0 }}\n          className=\"absolute inset-0\"\n          initial={false}\n          key={layer}\n          style={{ background: TRACK_LAYERS[layer] }}\n          transition={canAnimate ? FADE : INSTANT}\n        />\n      ))}\n\n      {STAR_POSITIONS.map((star, starIndex) => (\n        <motion.span\n          animate={{ opacity: starOpacity, scale: starOpacity > 0 ? 1 : 0.4 }}\n          className=\"absolute rounded-full bg-white\"\n          initial={false}\n          key={`${star.left}-${star.top}`}\n          style={{\n            height: star.size,\n            left: star.left * trackWidth,\n            top: star.top * trackHeight,\n            width: star.size,\n          }}\n          transition={\n            canAnimate ? { ...SPRING, delay: starIndex * RAY_STAGGER } : INSTANT\n          }\n        />\n      ))}\n\n      <span className=\"pointer-events-none absolute inset-0 rounded-full shadow-[inset_0_2px_6px_rgba(0,0,0,0.5),inset_0_-1px_0_rgba(255,255,255,0.2)]\" />\n\n      <motion.span\n        animate={{ x: index * knob }}\n        className=\"absolute flex items-center justify-center rounded-full bg-gradient-to-b from-white to-slate-200 shadow-[0_3px_8px_rgba(0,0,0,0.4),inset_0_-2px_4px_rgba(0,0,0,0.12),inset_0_2px_3px_rgba(255,255,255,0.95)]\"\n        initial={false}\n        style={{ height: knob, left: pad, top: pad, width: knob }}\n        transition={canAnimate ? KNOB_SPRING : INSTANT}\n      >\n        <span className=\"pointer-events-none absolute top-[12%] left-[18%] h-1/4 w-1/3 rounded-full bg-white/95 blur-[2px]\" />\n        <AnimatePresence initial={false} mode=\"wait\">\n          <motion.span\n            animate={{ opacity: 1, rotate: 0, scale: 1 }}\n            className=\"relative\"\n            exit={\n              canAnimate\n                ? { opacity: 0, rotate: -120, scale: 0.4 }\n                : { opacity: 0, transition: INSTANT }\n            }\n            initial={\n              canAnimate ? { opacity: 0, rotate: 120, scale: 0.4 } : false\n            }\n            key={value}\n            transition={canAnimate ? SPRING : INSTANT}\n          >\n            <Icon className={cn(iconClassName, KNOB_ICON_COLOR[value])} />\n          </motion.span>\n        </AnimatePresence>\n      </motion.span>\n    </span>\n  );\n};\n\n/* -------------------------------------------------------------------------- */\n/* Variant 4 — orb: a lit sphere with a travelling specular highlight */\n/* -------------------------------------------------------------------------- */\n\nconst ORB_BODY: Record<ThemeToggleValue, string> = {\n  dark: \"radial-gradient(circle at 32% 26%, #a8b6cd 0%, #64748b 20%, #334155 52%, #0f172a 82%, #020617 100%)\",\n  light:\n    \"radial-gradient(circle at 34% 28%, #fffbeb 0%, #fde68a 24%, #fbbf24 54%, #ea580c 86%, #9a3412 100%)\",\n  system:\n    \"radial-gradient(circle at 50% 24%, #fef3c7 0%, #fbbf24 30%, #64748b 56%, #1e293b 84%, #020617 100%)\",\n};\n\nconst ORB_LAYER_ORDER: ThemeToggleValue[] = [\"light\", \"dark\", \"system\"];\n\nconst SPECULAR: Record<\n  ThemeToggleValue,\n  { opacity: number; x: number; y: number }\n> = {\n  dark: { opacity: 0.3, x: 0.58, y: 0.2 },\n  light: { opacity: 0.92, x: 0.17, y: 0.11 },\n  system: { opacity: 0.62, x: 0.38, y: 0.09 },\n};\n\nconst CRATERS = [\n  { left: 0.5, size: 0.2, top: 0.3 },\n  { left: 0.28, size: 0.13, top: 0.58 },\n  { left: 0.6, size: 0.1, top: 0.66 },\n];\n\ntype OrbVisualProps = {\n  canAnimate: boolean;\n  size: number;\n  value: ThemeToggleValue;\n};\n\nconst OrbVisual = ({ canAnimate, size, value }: OrbVisualProps) => {\n  const specular = SPECULAR[value];\n  const travel = canAnimate ? TRAVEL : INSTANT;\n\n  return (\n    <span\n      aria-hidden=\"true\"\n      className=\"pointer-events-none relative block\"\n      style={{ height: size, width: size }}\n    >\n      <span className=\"absolute inset-0 overflow-hidden rounded-full\">\n        {ORB_LAYER_ORDER.map((layer) => (\n          <motion.span\n            animate={{ opacity: layer === value ? 1 : 0 }}\n            className=\"absolute inset-0\"\n            initial={false}\n            key={layer}\n            style={{ background: ORB_BODY[layer] }}\n            transition={canAnimate ? FADE : INSTANT}\n          />\n        ))}\n\n        {CRATERS.map((crater) => (\n          <motion.span\n            animate={{ opacity: value === \"dark\" ? 0.32 : 0 }}\n            className=\"absolute rounded-full bg-slate-950\"\n            initial={false}\n            key={`${crater.left}-${crater.top}`}\n            style={{\n              filter: \"blur(2px)\",\n              height: size * crater.size,\n              left: size * crater.left,\n              top: size * crater.top,\n              width: size * crater.size,\n            }}\n            transition={canAnimate ? FADE : INSTANT}\n          />\n        ))}\n\n        <motion.span\n          animate={{\n            opacity: specular.opacity,\n            x: size * specular.x,\n            y: size * specular.y,\n          }}\n          className=\"absolute top-0 left-0 rounded-[50%] bg-white\"\n          initial={false}\n          style={{\n            filter: `blur(${Math.round(size * 0.06)}px)`,\n            height: size * 0.24,\n            width: size * 0.32,\n          }}\n          transition={travel}\n        />\n\n        <span className=\"absolute inset-0 rounded-full shadow-[inset_-10px_-14px_28px_rgba(0,0,0,0.5),inset_8px_10px_22px_rgba(255,255,255,0.16)]\" />\n        <span className=\"absolute inset-0 rounded-full ring-1 ring-white/20 ring-inset\" />\n      </span>\n    </span>\n  );\n};\n\n/* -------------------------------------------------------------------------- */\n/* Shared trigger for the two \"one shape\" variants                            */\n/* -------------------------------------------------------------------------- */\n\ntype CycleButtonProps = {\n  ariaLabel: string;\n  ariaPressed?: boolean;\n  children: ReactNode;\n  className?: string;\n  onKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;\n  onSelect: () => void;\n};\n\nconst CycleButton = ({\n  ariaLabel,\n  ariaPressed,\n  children,\n  className,\n  onKeyDown,\n  onSelect,\n}: CycleButtonProps) => (\n  <button\n    aria-label={ariaLabel}\n    aria-pressed={ariaPressed}\n    className={cn(\n      \"inline-flex cursor-pointer items-center justify-center rounded-full transition-transform duration-150 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-4 focus-visible:ring-offset-background active:scale-95 motion-reduce:transition-none motion-reduce:active:scale-100\",\n      className\n    )}\n    onClick={onSelect}\n    onKeyDown={onKeyDown}\n    type=\"button\"\n  >\n    {children}\n  </button>\n);\n\n/* -------------------------------------------------------------------------- */\n/* Root                                                                       */\n/* -------------------------------------------------------------------------- */\n\nconst ThemeToggle = ({\n  className,\n  defaultTheme = \"system\",\n  label,\n  onThemeChange,\n  showSystem = true,\n  size = \"md\",\n  theme: controlledTheme,\n  variant = \"sun-moon\",\n}: ThemeToggleProps) => {\n  const shouldReduceMotion = Boolean(useReducedMotion());\n  const reactId = useId();\n  const uid = reactId.replace(/:/g, \"\");\n  const maskId = `theme-toggle-mask-${uid}`;\n  const thumbId = `theme-toggle-thumb-${uid}`;\n  const optionRefs = useRef<(HTMLButtonElement | null)[]>([]);\n\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n  const canAnimate = mounted && !shouldReduceMotion;\n\n  const [internalTheme, setInternalTheme] =\n    useState<ThemeToggleValue>(defaultTheme);\n  const isControlled = controlledTheme !== undefined;\n  const theme = isControlled ? controlledTheme : internalTheme;\n\n  const selectTheme = useCallback(\n    (next: ThemeToggleValue) => {\n      if (!isControlled) {\n        setInternalTheme(next);\n      }\n      onThemeChange?.(next);\n    },\n    [isControlled, onThemeChange]\n  );\n\n  const sizeConfig = SIZES[size];\n  const options = useMemo(\n    () => (showSystem ? THEME_OPTIONS : THEME_OPTIONS.slice(0, 2)),\n    [showSystem]\n  );\n  const selectedIndex = Math.max(\n    0,\n    options.findIndex((option) => option.value === theme)\n  );\n  const selected = options[selectedIndex];\n  const nextOption = options[(selectedIndex + 1) % options.length];\n  const isDark = theme === \"dark\";\n\n  const step = useCallback(\n    (delta: number) => {\n      const nextIndex =\n        (selectedIndex + delta + options.length) % options.length;\n      selectTheme(options[nextIndex].value);\n      return nextIndex;\n    },\n    [options, selectedIndex, selectTheme]\n  );\n\n  const handleCycleKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLButtonElement>) => {\n      if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") {\n        event.preventDefault();\n        step(1);\n        return;\n      }\n      if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") {\n        event.preventDefault();\n        step(-1);\n      }\n    },\n    [step]\n  );\n\n  const handleOptionKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLButtonElement>, index: number) => {\n      let nextIndex = index;\n      switch (event.key) {\n        case \"ArrowRight\":\n        case \"ArrowDown\":\n          nextIndex = (index + 1) % options.length;\n          break;\n        case \"ArrowLeft\":\n        case \"ArrowUp\":\n          nextIndex = (index - 1 + options.length) % options.length;\n          break;\n        case \"Home\":\n          nextIndex = 0;\n          break;\n        case \"End\":\n          nextIndex = options.length - 1;\n          break;\n        default:\n          return;\n      }\n      event.preventDefault();\n      selectTheme(options[nextIndex].value);\n      optionRefs.current[nextIndex]?.focus();\n    },\n    [options, selectTheme]\n  );\n\n  const registerRef = useCallback(\n    (index: number, el: HTMLButtonElement | null) => {\n      optionRefs.current[index] = el;\n    },\n    []\n  );\n\n  const groupLabel = label ?? \"Theme\";\n  const prefix = label ? `${label}: ` : \"\";\n  const cycleLabel = `${prefix}${selected.label} theme. Activate to switch to ${nextOption.label}.`;\n  const binaryPressed = showSystem ? undefined : isDark;\n\n  const trackWidth = sizeConfig.knob * options.length + sizeConfig.pad * 2;\n  const trackHeight = sizeConfig.knob + sizeConfig.pad * 2;\n\n  const renderVariant = () => {\n    if (variant === \"pill\") {\n      return (\n        <PillToggle\n          canAnimate={canAnimate}\n          groupLabel={groupLabel}\n          iconClassName={sizeConfig.icon}\n          onKeyDown={handleOptionKeyDown}\n          onSelect={selectTheme}\n          options={options}\n          registerRef={registerRef}\n          segmentClassName={sizeConfig.segment}\n          selectedIndex={selectedIndex}\n          textClassName={sizeConfig.text}\n          thumbId={thumbId}\n        />\n      );\n    }\n\n    if (variant === \"switch\") {\n      const visual = (\n        <SwitchVisual\n          canAnimate={canAnimate}\n          iconClassName={sizeConfig.icon}\n          index={selectedIndex}\n          knob={sizeConfig.knob}\n          pad={sizeConfig.pad}\n          trackHeight={trackHeight}\n          trackWidth={trackWidth}\n          value={theme}\n        />\n      );\n\n      if (!showSystem) {\n        return (\n          <button\n            aria-label={cycleLabel}\n            aria-pressed={isDark}\n            className=\"inline-flex cursor-pointer rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n            onClick={() => selectTheme(isDark ? \"light\" : \"dark\")}\n            type=\"button\"\n          >\n            {visual}\n          </button>\n        );\n      }\n\n      return (\n        <div\n          aria-label={groupLabel}\n          className=\"relative inline-block rounded-full\"\n          role=\"radiogroup\"\n        >\n          {visual}\n          <div\n            className=\"absolute inset-0 flex\"\n            style={{ padding: sizeConfig.pad }}\n          >\n            {options.map((option, index) => (\n              <button\n                aria-checked={index === selectedIndex}\n                aria-label={option.label}\n                className=\"cursor-pointer rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-1 focus-visible:ring-offset-transparent\"\n                key={option.value}\n                onClick={() => selectTheme(option.value)}\n                onKeyDown={(event) => handleOptionKeyDown(event, index)}\n                ref={(el) => {\n                  registerRef(index, el);\n                }}\n                role=\"radio\"\n                style={{ width: sizeConfig.knob }}\n                tabIndex={index === selectedIndex ? 0 : -1}\n                type=\"button\"\n              />\n            ))}\n          </div>\n        </div>\n      );\n    }\n\n    if (variant === \"orb\") {\n      return (\n        <CycleButton\n          ariaLabel={cycleLabel}\n          ariaPressed={binaryPressed}\n          className=\"p-1\"\n          onKeyDown={handleCycleKeyDown}\n          onSelect={() => step(1)}\n        >\n          <OrbVisual\n            canAnimate={canAnimate}\n            size={sizeConfig.orb}\n            value={theme}\n          />\n        </CycleButton>\n      );\n    }\n\n    return (\n      <CycleButton\n        ariaLabel={cycleLabel}\n        ariaPressed={binaryPressed}\n        className=\"p-1\"\n        onKeyDown={handleCycleKeyDown}\n        onSelect={() => step(1)}\n      >\n        <SunMoonGlyph\n          canAnimate={canAnimate}\n          maskId={maskId}\n          size={sizeConfig.glyph}\n          value={theme}\n        />\n      </CycleButton>\n    );\n  };\n\n  return (\n    <div className={cn(\"inline-flex flex-col items-center gap-2\", className)}>\n      {label ? (\n        <span className=\"font-medium text-foreground text-sm\">{label}</span>\n      ) : null}\n      {renderVariant()}\n      <span aria-live=\"polite\" className=\"sr-only\">\n        {`${selected.label} theme selected`}\n      </span>\n    </div>\n  );\n};\n\nexport default ThemeToggle;\n","path":"index.tsx","target":"components/smoothui/theme-toggle/index.tsx","type":"registry:ui"}],"name":"theme-toggle","registryDependencies":[],"title":"Theme Toggle","type":"registry:ui"}