{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "theme",
  "title": "Theme",
  "description": "Theme plugin: theme-library agnostic theme selection. Registers an Appearance card in account settings and a theme toggle in the user button. Pass `useTheme` from your theme library (e.g. next-themes) or static `setTheme`.",
  "dependencies": [
    "@better-auth-ui/react@latest",
    "@better-auth-ui/core@latest",
    "lucide-react"
  ],
  "registryDependencies": [
    "card",
    "dropdown-menu",
    "field",
    "radio-group",
    "tabs"
  ],
  "files": [
    {
      "path": "src/lib/auth/auth-plugin.ts",
      "content": "import type {\n  AuthPluginComponents,\n  AuthPlugin as AuthPluginPrimitive\n} from \"@better-auth-ui/react\"\n\ndeclare module \"@better-auth-ui/core\" {\n  /** Widens `useAuth().plugins` to the shadcn-typed `AuthPlugin`. */\n  interface AuthPluginRegister {\n    shadcn: AuthPlugin\n  }\n}\n\n/** Props the shadcn `<Auth>` router spreads onto plugin-contributed auth views. */\nexport type AuthViewProps = {\n  className?: string\n  socialLayout?: \"auto\" | \"horizontal\" | \"vertical\" | \"grid\"\n  socialPosition?: \"top\" | \"bottom\"\n}\n\n/** Props the shadcn `<Settings>` router spreads onto plugin-contributed settings views. */\nexport type SettingsViewProps = {\n  className?: string\n}\n\n/** Shadcn plugin type. Plugin authors import this from `@/lib/auth/auth-plugin`. */\nexport type AuthPlugin = AuthPluginPrimitive<\n  AuthPluginComponents,\n  AuthViewProps,\n  SettingsViewProps\n>\n",
      "type": "registry:lib",
      "target": "@lib/auth/auth-plugin.ts"
    },
    {
      "path": "src/lib/auth/theme-plugin.ts",
      "content": "import { createAuthPlugin } from \"@better-auth-ui/core\"\nimport {\n  themePlugin as coreThemePlugin,\n  type ThemeLocalization\n} from \"@better-auth-ui/core/plugins\"\n\nimport { Appearance } from \"@/components/auth/theme/appearance\"\nimport { ThemeToggleItem } from \"@/components/auth/theme/theme-toggle-item\"\n\n/**\n * Hook shape compatible with `next-themes`' `useTheme` and similar APIs. The\n * hook is invoked inside the plugin factory so consumers can register the\n * plugin in the same component as their `<ThemeProvider>` without an extra\n * inner component.\n */\nexport type UseThemeHook = () => {\n  theme?: string\n  setTheme: (theme: string) => void\n  themes?: string[]\n}\n\ntype CommonThemeOptions = {\n  /**\n   * Override the plugin's default localization strings.\n   * @remarks `ThemeLocalization`\n   */\n  localization?: Partial<ThemeLocalization>\n  /**\n   * Available theme options.\n   * @default [\"system\", \"light\", \"dark\"]\n   */\n  themes?: string[]\n}\n\nexport type ThemePluginOptions = CommonThemeOptions &\n  (\n    | {\n        /**\n         * A theme hook (e.g. next-themes' `useTheme`) called inside the\n         * plugin's slot components on every render. The hook owns the live\n         * theme value, so `theme`/`setTheme` are not accepted in this form.\n         */\n        useTheme: UseThemeHook\n        theme?: never\n        setTheme?: never\n      }\n    | {\n        /**\n         * Current theme value. Required when not using a hook so slot\n         * components can highlight the active option. Pass it from a\n         * stateful source (e.g. `useState`, Context) so updates flow\n         * through `<AuthProvider>` and re-render slot components.\n         * @remarks Do not memoize the static call (e.g. wrap\n         * `themePlugin({ theme, setTheme })` in `useMemo`): `theme` is\n         * captured at factory-creation time, so a memoized closure will\n         * keep returning the stale value and slot components will stop\n         * reflecting theme changes. Let the factory re-run each render.\n         */\n        theme: string\n        /** Setter that updates the value `theme` is read from. */\n        setTheme: (theme: string) => void\n        useTheme?: never\n      }\n  )\n\nexport const themePlugin = createAuthPlugin(\n  coreThemePlugin.id,\n  ({ useTheme, ...rest }: ThemePluginOptions) => {\n    // No-op `setTheme` baseline keeps core's required option satisfied on the\n    // hook branch (where the consumer doesn't pass a setter); on the static\n    // branch the spread overrides it with the consumer's real setter.\n    const base = coreThemePlugin({ setTheme: () => {}, ...rest })\n    return {\n      ...base,\n      // Slot components always call `plugin.useTheme()` — invoking the hook\n      // inside their render keeps it in scope of any `<ThemeProvider>` the\n      // consumer mounts. On the static branch the factory re-runs on every\n      // parent render, so the synthesized closure stays in sync with the\n      // consumer's `theme` state.\n      useTheme:\n        useTheme ??\n        (() => ({\n          theme: base.theme,\n          setTheme: base.setTheme,\n          themes: base.themes\n        })),\n      userMenuItems: [ThemeToggleItem],\n      accountCards: [Appearance]\n    }\n  }\n)\n",
      "type": "registry:lib",
      "target": "@lib/auth/theme-plugin.ts"
    },
    {
      "path": "src/components/auth/theme/appearance.tsx",
      "content": "\"use client\"\n\nimport {\n  ThemePreviewDark,\n  ThemePreviewLight,\n  ThemePreviewSystem,\n  useAuthPlugin\n} from \"@better-auth-ui/react\"\nimport { Monitor, Moon, Sun } from \"lucide-react\"\nimport { useEffect, useState } from \"react\"\n\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport {\n  Field,\n  FieldContent,\n  FieldLabel,\n  FieldTitle\n} from \"@/components/ui/field\"\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\"\nimport { themePlugin } from \"@/lib/auth/theme-plugin\"\nimport { cn } from \"@/lib/utils\"\n\nexport type AppearanceProps = {\n  className?: string\n}\n\n/**\n * Renders a theme selector card with visual theme previews.\n *\n * Displays a card containing radio buttons for selecting between system, light,\n * and dark themes. Each option shows a visual preview of the theme.\n *\n * @param className - Optional additional CSS class names for the card container.\n * @returns A JSX element containing the theme selector card.\n */\nexport function Appearance({ className }: AppearanceProps) {\n  const { useTheme, localization } = useAuthPlugin(themePlugin)\n  const { theme, setTheme, themes = [] } = useTheme()\n\n  const [isMounted, setIsMounted] = useState(false)\n  useEffect(() => setIsMounted(true), [])\n\n  return (\n    <div>\n      <h2 className=\"text-sm font-semibold mb-3\">{localization.appearance}</h2>\n\n      <Card className={cn(className)}>\n        <CardContent>\n          <Field>\n            <FieldLabel>{localization.theme}</FieldLabel>\n\n            <RadioGroup\n              value={isMounted ? theme : \"\"}\n              onValueChange={setTheme}\n              className=\"grid gap-3 grid-cols-2 sm:grid-cols-3\"\n              disabled={!isMounted || !theme}\n            >\n              {themes.includes(\"system\") && (\n                <FieldLabel htmlFor=\"system\">\n                  <Field orientation=\"horizontal\">\n                    <FieldContent className=\"gap-2\">\n                      <div className=\"flex items-center gap-2 justify-between\">\n                        <FieldTitle>\n                          <Monitor className=\"size-4 text-muted-foreground\" />\n\n                          {localization.system}\n                        </FieldTitle>\n\n                        <RadioGroupItem value=\"system\" id=\"system\" />\n                      </div>\n\n                      <ThemePreviewSystem className=\"w-full\" />\n                    </FieldContent>\n                  </Field>\n                </FieldLabel>\n              )}\n\n              {themes.includes(\"light\") && (\n                <FieldLabel htmlFor=\"light\">\n                  <Field orientation=\"horizontal\">\n                    <FieldContent className=\"gap-2\">\n                      <div className=\"flex items-center gap-2 justify-between\">\n                        <FieldTitle>\n                          <Sun className=\"size-4 text-muted-foreground\" />\n\n                          {localization.light}\n                        </FieldTitle>\n\n                        <RadioGroupItem value=\"light\" id=\"light\" />\n                      </div>\n\n                      <ThemePreviewLight className=\"w-full\" />\n                    </FieldContent>\n                  </Field>\n                </FieldLabel>\n              )}\n\n              {themes.includes(\"dark\") && (\n                <FieldLabel htmlFor=\"dark\">\n                  <Field orientation=\"horizontal\">\n                    <FieldContent className=\"gap-2\">\n                      <div className=\"flex items-center gap-2 justify-between\">\n                        <FieldTitle>\n                          <Moon className=\"size-4 text-muted-foreground\" />\n\n                          {localization.dark}\n                        </FieldTitle>\n\n                        <RadioGroupItem value=\"dark\" id=\"dark\" />\n                      </div>\n\n                      <ThemePreviewDark className=\"w-full\" />\n                    </FieldContent>\n                  </Field>\n                </FieldLabel>\n              )}\n            </RadioGroup>\n          </Field>\n        </CardContent>\n      </Card>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/theme/appearance.tsx"
    },
    {
      "path": "src/components/auth/theme/theme-toggle-item.tsx",
      "content": "\"use client\"\n\nimport { useAuthPlugin } from \"@better-auth-ui/react\"\nimport { Monitor, Moon, PaletteIcon, Sun } from \"lucide-react\"\nimport { useRef } from \"react\"\n\nimport { DropdownMenuItem } from \"@/components/ui/dropdown-menu\"\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\nimport { themePlugin } from \"@/lib/auth/theme-plugin\"\n\n/**\n * Theme toggle dropdown item used inside `UserButton`. Callers are responsible\n * for ensuring theming is configured before rendering this component.\n */\nexport function ThemeToggleItem() {\n  const { useTheme, localization } = useAuthPlugin(themePlugin)\n  const { theme, setTheme, themes = [] } = useTheme()\n  const tabsListRef = useRef<HTMLDivElement>(null)\n\n  // The TabsTriggers aren't part of the menu's roving focus group, so\n  // arrow-key navigation can't reach them on its own. When the wrapper\n  // menu item receives focus we delegate focus to the active TabsTrigger\n  // inside, letting the user switch themes with Left/Right arrows.\n  const focusActiveTab = () => {\n    const activeTab = tabsListRef.current?.querySelector<HTMLElement>(\n      '[role=\"tab\"][data-state=\"active\"]'\n    )\n    activeTab?.focus({ preventScroll: true })\n  }\n\n  // Up/Down on a TabsTrigger escapes back to the previous/next sibling\n  // menu item so users can keep navigating the menu with the arrow keys.\n  const handleTabsKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    if (event.key !== \"ArrowUp\" && event.key !== \"ArrowDown\") return\n\n    const target = event.target as HTMLElement\n    if (target.getAttribute(\"role\") !== \"tab\") return\n\n    const wrapper = target.closest<HTMLElement>('[role=\"menuitem\"]')\n    const content = wrapper?.closest<HTMLElement>(\n      '[data-slot=\"dropdown-menu-content\"]'\n    )\n    if (!wrapper || !content) return\n\n    const items = Array.from(\n      content.querySelectorAll<HTMLElement>(\n        '[role=\"menuitem\"]:not([aria-disabled=\"true\"])'\n      )\n    )\n    const currentIndex = items.indexOf(wrapper)\n    const nextIndex =\n      event.key === \"ArrowDown\" ? currentIndex + 1 : currentIndex - 1\n    const next = items[nextIndex]\n    if (!next) return\n\n    event.preventDefault()\n    next.focus()\n  }\n\n  return (\n    <DropdownMenuItem\n      onSelect={(e) => e.preventDefault()}\n      onFocus={(e) => {\n        // onFocus bubbles in React, so guard against re-entry from focus\n        // events fired by the inner TabsTrigger.\n        if (e.target === e.currentTarget) focusActiveTab()\n      }}\n    >\n      <PaletteIcon className=\"text-muted-foreground\" />\n\n      <span>{localization.theme}</span>\n\n      <Tabs\n        className=\"ml-auto\"\n        value={theme}\n        onValueChange={setTheme}\n        onKeyDown={handleTabsKeyDown}\n      >\n        <TabsList ref={tabsListRef} className=\"h-6!\">\n          {themes.includes(\"system\") && (\n            <TabsTrigger\n              value=\"system\"\n              className=\"size-5 p-0\"\n              aria-label={localization.system}\n            >\n              <Monitor className=\"size-3\" />\n            </TabsTrigger>\n          )}\n          {themes.includes(\"light\") && (\n            <TabsTrigger\n              value=\"light\"\n              className=\"size-5 p-0\"\n              aria-label={localization.light}\n            >\n              <Sun className=\"size-3\" />\n            </TabsTrigger>\n          )}\n          {themes.includes(\"dark\") && (\n            <TabsTrigger\n              value=\"dark\"\n              className=\"size-5 p-0\"\n              aria-label={localization.dark}\n            >\n              <Moon className=\"size-3\" />\n            </TabsTrigger>\n          )}\n        </TabsList>\n      </Tabs>\n    </DropdownMenuItem>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/theme/theme-toggle-item.tsx"
    }
  ],
  "type": "registry:component"
}