{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "theme-toggle",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["@base-ui/react/toggle", "lucide-react"],
  "devDependencies": [],
  "files": [
    {
      "path": "theme-toggle.tsx",
      "content": "\"use client\";\n\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\";\nimport { Moon } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type ThemeToggleSize = \"sm\" | \"md\" | \"lg\";\n\ntype ThemePreference = \"dark\" | \"light\" | \"system\";\n\nexport interface ThemeToggleProps {\n  \"aria-label\"?: string;\n  applyToDocument?: boolean;\n  className?: string;\n  defaultPressed?: boolean;\n  disabled?: boolean;\n  enableSystem?: boolean;\n  id?: string;\n  knobClassName?: string;\n  onPressedChange?: (pressed: boolean) => void;\n  persist?: boolean;\n  pressed?: boolean;\n  size?: ThemeToggleSize;\n  storageKey?: string;\n  trackClassName?: string;\n}\n\nconst DEFAULT_STORAGE_KEY = \"theme\";\nconst ICON_EASE = \"ease-[cubic-bezier(0.68,-0.55,0.265,1.55)]\";\nconst SUN_COLOR = \"#ffffff\";\nconst MOON_COLOR = \"#1a1838\";\n\nconst sizeConfig = {\n  sm: {\n    track: \"h-6 w-11\",\n    knob: \"h-4 w-4\",\n    knobOffset: \"left-[calc(100%-1.125rem)]\",\n    knobStart: \"left-0.5\",\n    icon: 12,\n    sunIcon: 12,\n  },\n  md: {\n    track: \"h-8 w-14\",\n    knob: \"h-6 w-6\",\n    knobOffset: \"left-[calc(100%-1.625rem)]\",\n    knobStart: \"left-0.5\",\n    icon: 16,\n    sunIcon: 16,\n  },\n  lg: {\n    track: \"h-10 w-[4.5rem]\",\n    knob: \"h-8 w-8\",\n    knobOffset: \"left-[calc(100%-2.125rem)]\",\n    knobStart: \"left-0.5\",\n    icon: 20,\n    sunIcon: 20,\n  },\n} as const;\n\ntype SizeConfig = (typeof sizeConfig)[ThemeToggleSize];\n\nfunction setRef<T>(ref: React.Ref<T> | undefined, value: T) {\n  if (typeof ref === \"function\") {\n    ref(value);\n    return;\n  }\n\n  if (ref) {\n    ref.current = value;\n  }\n}\n\nfunction readStoredPreference(storageKey: string): ThemePreference | null {\n  if (typeof window === \"undefined\") {\n    return null;\n  }\n\n  try {\n    const value = window.localStorage.getItem(storageKey);\n\n    if (value === \"dark\" || value === \"light\" || value === \"system\") {\n      return value;\n    }\n  } catch {\n    return null;\n  }\n\n  return null;\n}\n\nfunction writeStoredPreference(storageKey: string, pressed: boolean) {\n  if (typeof window === \"undefined\") {\n    return;\n  }\n\n  try {\n    window.localStorage.setItem(storageKey, pressed ? \"dark\" : \"light\");\n  } catch {\n    return;\n  }\n}\n\nfunction getSystemPrefersDark() {\n  if (typeof window === \"undefined\") {\n    return false;\n  }\n\n  return window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n}\n\nfunction documentHasDarkClass() {\n  if (typeof document === \"undefined\") {\n    return false;\n  }\n\n  return document.documentElement.classList.contains(\"dark\");\n}\n\nfunction resolveIsDark({\n  defaultPressed,\n  enableSystem,\n  stored,\n}: {\n  defaultPressed?: boolean;\n  enableSystem: boolean;\n  stored: ThemePreference | null;\n}) {\n  if (stored === \"dark\") {\n    return true;\n  }\n\n  if (stored === \"light\") {\n    return false;\n  }\n\n  if (stored === \"system\" || (stored === null && enableSystem)) {\n    return getSystemPrefersDark();\n  }\n\n  if (defaultPressed !== undefined) {\n    return defaultPressed;\n  }\n\n  return documentHasDarkClass();\n}\n\nfunction applyDocumentTheme(isDark: boolean) {\n  if (typeof document === \"undefined\") {\n    return;\n  }\n\n  const hasDark = documentHasDarkClass();\n\n  if (hasDark === isDark) {\n    return;\n  }\n\n  document.documentElement.classList.toggle(\"dark\", isDark);\n  document.documentElement.style.colorScheme = isDark ? \"dark\" : \"light\";\n}\n\nfunction ThemeSunIcon({\n  className,\n  size,\n}: {\n  className?: string;\n  size: number;\n}) {\n  return (\n    <svg\n      aria-hidden\n      className={cn(\"block shrink-0\", className)}\n      fill=\"none\"\n      height={size}\n      viewBox=\"0 0 24 24\"\n      width={size}\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <circle cx=\"12\" cy=\"12\" fill={SUN_COLOR} r=\"3.25\" />\n      <g stroke={SUN_COLOR} strokeLinecap=\"round\" strokeWidth=\"2\">\n        <path d=\"M12 2.5v2.5\" />\n        <path d=\"M12 19v2.5\" />\n        <path d=\"M4.22 4.22l1.77 1.77\" />\n        <path d=\"M18.01 18.01l1.77 1.77\" />\n        <path d=\"M2.5 12h2.5\" />\n        <path d=\"M19 12h2.5\" />\n        <path d=\"M4.22 19.78l1.77-1.77\" />\n        <path d=\"M18.01 5.99l1.77-1.77\" />\n      </g>\n    </svg>\n  );\n}\n\ntype ThemeToggleTrackProps = React.ComponentProps<\"button\"> & {\n  ariaLabel?: string;\n  config: SizeConfig;\n  disabled?: boolean;\n  isDark: boolean;\n  knobClassName?: string;\n  renderRef?: React.Ref<HTMLButtonElement>;\n  trackClassName?: string;\n};\n\nfunction ThemeToggleTrack({\n  ariaLabel = \"Toggle theme\",\n  className,\n  config,\n  disabled = false,\n  isDark,\n  knobClassName,\n  renderRef,\n  trackClassName,\n  ...buttonProps\n}: ThemeToggleTrackProps) {\n  return (\n    <button\n      {...buttonProps}\n      aria-label={ariaLabel}\n      className={cn(\n        \"relative cursor-pointer rounded-full border-2 transition-colors duration-700\",\n        ICON_EASE,\n        config.track,\n        isDark\n          ? \"border-[#2d2a4e] bg-[#1a1838]\"\n          : \"border-[#e8d5b7] bg-[#fef3c7]\",\n        disabled && \"pointer-events-none cursor-not-allowed opacity-50\",\n        trackClassName,\n        className\n      )}\n      disabled={disabled}\n      ref={renderRef}\n      type=\"button\"\n    >\n      <div\n        className={cn(\n          \"absolute top-1/2 grid -translate-y-1/2 place-items-center rounded-full transition-[left,background-color] duration-700\",\n          ICON_EASE,\n          config.knob,\n          isDark ? config.knobOffset : config.knobStart,\n          isDark ? \"bg-[#e8e6f0]\" : \"bg-[#ff9500]\",\n          knobClassName\n        )}\n      >\n        <ThemeSunIcon\n          className={cn(\n            \"col-start-1 row-start-1 origin-center transition-[transform,opacity] duration-500\",\n            ICON_EASE,\n            isDark\n              ? \"rotate-90 scale-50 opacity-0\"\n              : \"rotate-0 scale-100 opacity-100\"\n          )}\n          size={config.sunIcon}\n        />\n        <Moon\n          aria-hidden\n          className={cn(\n            \"col-start-1 row-start-1 block shrink-0 origin-center transition-[transform,opacity] duration-500\",\n            ICON_EASE,\n            isDark\n              ? \"rotate-0 scale-100 opacity-100\"\n              : \"-rotate-90 scale-50 opacity-0\"\n          )}\n          color={MOON_COLOR}\n          size={config.icon}\n          strokeWidth={2}\n        />\n      </div>\n    </button>\n  );\n}\n\nexport const ThemeToggle = React.forwardRef<\n  HTMLButtonElement,\n  ThemeToggleProps\n>(function ThemeToggle(\n  {\n    \"aria-label\": ariaLabel,\n    applyToDocument = true,\n    className,\n    defaultPressed,\n    disabled = false,\n    enableSystem = true,\n    id,\n    knobClassName,\n    onPressedChange,\n    persist = true,\n    pressed,\n    size = \"md\",\n    storageKey = DEFAULT_STORAGE_KEY,\n    trackClassName,\n  },\n  ref\n) {\n  const isControlled = pressed !== undefined;\n  const [visualPressed, setVisualPressed] = React.useState(\n    defaultPressed ?? false\n  );\n  const [hasResolvedTheme, setHasResolvedTheme] = React.useState(false);\n  const config = sizeConfig[size];\n  const followsSystemPreference = React.useRef(false);\n  const hasInitialized = React.useRef(false);\n  const onPressedChangeRef = React.useRef(onPressedChange);\n  const resolvedAriaLabel = ariaLabel ?? \"Toggle theme\";\n\n  onPressedChangeRef.current = onPressedChange;\n\n  const commitTheme = React.useCallback(\n    (\n      nextPressed: boolean,\n      options?: {\n        notify?: boolean;\n        persistPreference?: boolean;\n        updateDocument?: boolean;\n      }\n    ) => {\n      const shouldPersist = options?.persistPreference !== false && persist;\n      const shouldUpdateDocument =\n        options?.updateDocument !== false && applyToDocument;\n      const shouldNotify = options?.notify !== false;\n\n      if (shouldPersist) {\n        writeStoredPreference(storageKey, nextPressed);\n        followsSystemPreference.current = false;\n      }\n\n      if (shouldUpdateDocument) {\n        applyDocumentTheme(nextPressed);\n      }\n\n      if (shouldNotify) {\n        onPressedChangeRef.current?.(nextPressed);\n      }\n    },\n    [applyToDocument, persist, storageKey]\n  );\n\n  React.useLayoutEffect(() => {\n    if (hasInitialized.current) {\n      return;\n    }\n\n    hasInitialized.current = true;\n\n    const stored = persist ? readStoredPreference(storageKey) : null;\n    const initialPressed = isControlled\n      ? (pressed ?? false)\n      : resolveIsDark({\n          defaultPressed,\n          enableSystem,\n          stored,\n        });\n\n    followsSystemPreference.current =\n      !isControlled &&\n      persist &&\n      enableSystem &&\n      (stored === \"system\" || stored === null);\n\n    setVisualPressed(initialPressed);\n\n    if (!isControlled) {\n      commitTheme(initialPressed, {\n        notify: false,\n        persistPreference: false,\n      });\n    }\n\n    setHasResolvedTheme(true);\n  }, [\n    commitTheme,\n    defaultPressed,\n    enableSystem,\n    isControlled,\n    persist,\n    pressed,\n    storageKey,\n  ]);\n\n  React.useEffect(() => {\n    if (!isControlled) {\n      return;\n    }\n\n    setVisualPressed(pressed);\n\n    commitTheme(pressed, {\n      notify: false,\n      persistPreference: false,\n    });\n  }, [commitTheme, isControlled, pressed]);\n\n  React.useEffect(() => {\n    if (!(persist && enableSystem) || isControlled) {\n      return;\n    }\n\n    const mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\");\n\n    const handleSystemChange = () => {\n      if (!followsSystemPreference.current) {\n        return;\n      }\n\n      const nextPressed = mediaQuery.matches;\n      setVisualPressed(nextPressed);\n      commitTheme(nextPressed);\n    };\n\n    mediaQuery.addEventListener(\"change\", handleSystemChange);\n\n    return () => {\n      mediaQuery.removeEventListener(\"change\", handleSystemChange);\n    };\n  }, [commitTheme, enableSystem, isControlled, persist]);\n\n  React.useEffect(() => {\n    if (!persist || isControlled) {\n      return;\n    }\n\n    const handleStorage = (event: StorageEvent) => {\n      if (event.key !== storageKey) {\n        return;\n      }\n\n      const nextStored = event.newValue;\n\n      if (nextStored !== \"dark\" && nextStored !== \"light\") {\n        return;\n      }\n\n      const nextPressed = nextStored === \"dark\";\n      followsSystemPreference.current = false;\n      setVisualPressed(nextPressed);\n      commitTheme(nextPressed, { persistPreference: false });\n    };\n\n    window.addEventListener(\"storage\", handleStorage);\n\n    return () => {\n      window.removeEventListener(\"storage\", handleStorage);\n    };\n  }, [commitTheme, isControlled, persist, storageKey]);\n\n  const handlePressedChange = React.useCallback(\n    (nextPressed: boolean) => {\n      if (disabled || nextPressed === visualPressed) {\n        return;\n      }\n\n      setVisualPressed(nextPressed);\n      commitTheme(nextPressed);\n    },\n    [commitTheme, disabled, visualPressed]\n  );\n\n  if (!hasResolvedTheme) {\n    return (\n      <button\n        aria-label={resolvedAriaLabel}\n        className={cn(\n          \"relative rounded-full border-2 border-border bg-muted\",\n          config.track,\n          className\n        )}\n        disabled={disabled}\n        id={id}\n        ref={ref}\n        suppressHydrationWarning\n        type=\"button\"\n      />\n    );\n  }\n\n  return (\n    <TogglePrimitive\n      aria-label={resolvedAriaLabel}\n      disabled={disabled}\n      id={id}\n      onPressedChange={handlePressedChange}\n      pressed={visualPressed}\n      render={(renderProps) => {\n        const {\n          className: renderClassName,\n          ref: renderRef,\n          ...resolvedRenderProps\n        } = renderProps;\n\n        return (\n          <ThemeToggleTrack\n            {...resolvedRenderProps}\n            ariaLabel={resolvedAriaLabel}\n            className={cn(renderClassName, className)}\n            config={config}\n            disabled={disabled}\n            isDark={visualPressed}\n            knobClassName={knobClassName}\n            renderRef={(node) => {\n              setRef(renderRef, node);\n              setRef(ref, node);\n            }}\n            trackClassName={trackClassName}\n          />\n        );\n      }}\n    />\n  );\n});\n\nThemeToggle.displayName = \"ThemeToggle\";\n",
      "type": "registry:ui"
    }
  ],
  "title": "Theme Toggle",
  "description": "Animated light/dark switch with a sliding knob, sun and moon icons, and bouncy track transitions. Built with Base UI Toggle; toggles the document dark class."
}
