{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "multi-session",
  "title": "Multi Session",
  "description": "Multi-session plugin: Enable multiple concurrent sessions with account switching. Registers account management cards in account settings and a switch account submenu in the user button.",
  "dependencies": [
    "@better-auth-ui/core@latest",
    "@better-auth-ui/react@latest",
    "better-auth",
    "cn",
    "lucide-react"
  ],
  "registryDependencies": [
    "avatar",
    "button",
    "card",
    "dropdown-menu",
    "https://better-auth-ui.com/r/radix-nova/additional-field.json",
    "https://better-auth-ui.com/r/radix-nova/user-view.json",
    "item",
    "skeleton",
    "sonner",
    "spinner"
  ],
  "files": [
    {
      "path": "examples/start-shadcn-example/src/components/auth/multi-session/manage-account.tsx",
      "content": "\"use client\"\n\nimport type {\n  ListDeviceSession,\n  MultiSessionAuthClient\n} from \"@better-auth-ui/core/plugins/multi-session\"\nimport { useAuth, useAuthPlugin, useSession } from \"@better-auth-ui/react\"\nimport {\n  useRevokeMultiSession,\n  useSetActiveSession\n} from \"@better-auth-ui/react/plugins/multi-session\"\nimport { ArrowLeftRight, LogOut, MoreHorizontal } from \"lucide-react\"\nimport { toast } from \"sonner\"\nimport { UserView } from \"@/components/auth/user/user-view\"\nimport { Button, buttonVariants } from \"@/components/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger\n} from \"@/components/ui/dropdown-menu\"\nimport { Item, ItemActions } from \"@/components/ui/item\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { multiSessionPlugin } from \"@/lib/auth/multi-session-plugin\"\nimport { cn } from \"cn\"\n\nexport type ManageAccountProps = {\n  deviceSession?: ListDeviceSession | null\n  isPending?: boolean\n}\n\n/**\n * Render a single account row with user info and switch/revoke controls.\n *\n * Shows the user's avatar and info. For the active session, shows a sign-out button.\n * For non-active sessions, shows a dropdown menu with switch and sign-out options.\n *\n * @param deviceSession - The device session object containing session and user data\n * @param isPending - Whether the device session is pending\n * @returns A JSX element containing the account row\n */\nexport function ManageAccount({\n  deviceSession,\n  isPending\n}: ManageAccountProps) {\n  const { authClient, localization } = useAuth<MultiSessionAuthClient>()\n  const { localization: multiSessionLocalization } =\n    useAuthPlugin(multiSessionPlugin)\n  const { data: session } = useSession(authClient)\n\n  const { mutate: setActiveSession, isPending: isSwitching } =\n    useSetActiveSession(authClient, {\n      onSuccess: () => window.scrollTo({ top: 0 })\n    })\n\n  const { mutate: revokeSession, isPending: isRevoking } =\n    useRevokeMultiSession(authClient, {\n      onSuccess: () => toast.success(localization.settings.revokeSessionSuccess)\n    })\n\n  const isActive = deviceSession?.session.userId === session?.session.userId\n  const isBusy = isSwitching || isRevoking\n\n  return (\n    <Item>\n      <UserView\n        className=\"flex-1\"\n        user={deviceSession?.user}\n        isPending={isPending}\n      />\n      <ItemActions>\n        {deviceSession && isActive && (\n          <Button\n            className=\"shrink-0\"\n            variant=\"outline\"\n            size=\"sm\"\n            onClick={() =>\n              revokeSession({ sessionToken: deviceSession.session.token })\n            }\n            disabled={isBusy}\n          >\n            {isRevoking ? <Spinner /> : <LogOut />}\n            {localization.auth.signOut}\n          </Button>\n        )}\n\n        {deviceSession && !isActive && (\n          <DropdownMenu>\n            <DropdownMenuTrigger\n              className={cn(\n                buttonVariants({ variant: \"ghost\", size: \"icon-sm\" }),\n                \"shrink-0\"\n              )}\n              disabled={isBusy}\n            >\n              <MoreHorizontal />\n            </DropdownMenuTrigger>\n\n            <DropdownMenuContent align=\"end\" className=\"min-w-fit\">\n              <DropdownMenuItem\n                onClick={() =>\n                  setActiveSession({\n                    sessionToken: deviceSession.session.token\n                  })\n                }\n              >\n                <ArrowLeftRight className=\"text-muted-foreground\" />\n                {multiSessionLocalization.switchAccount}\n              </DropdownMenuItem>\n\n              <DropdownMenuItem\n                onClick={() =>\n                  revokeSession({\n                    sessionToken: deviceSession.session.token\n                  })\n                }\n              >\n                <LogOut className=\"text-muted-foreground\" />\n                {localization.auth.signOut}\n              </DropdownMenuItem>\n            </DropdownMenuContent>\n          </DropdownMenu>\n        )}\n      </ItemActions>\n    </Item>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/multi-session/manage-account.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/multi-session/manage-accounts.tsx",
      "content": "\"use client\"\n\nimport type { MultiSessionAuthClient } from \"@better-auth-ui/core/plugins/multi-session\"\nimport { useAuth, useAuthPlugin, useSession } from \"@better-auth-ui/react\"\nimport { useListDeviceSessions } from \"@better-auth-ui/react/plugins/multi-session\"\nimport { Fragment } from \"react\"\n\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport { ItemGroup, ItemSeparator } from \"@/components/ui/item\"\nimport { multiSessionPlugin } from \"@/lib/auth/multi-session-plugin\"\nimport { cn } from \"cn\"\nimport { ManageAccount } from \"./manage-account\"\n\nexport type ManageAccountsProps = {\n  className?: string\n}\n\n/**\n * Render a card that lists and manages all device sessions for the current user.\n *\n * Shows each session with user information and actions to switch to or revoke a session.\n * When device session data is loading, a pending placeholder row is displayed.\n *\n * @returns A JSX element containing the accounts management card\n */\nexport function ManageAccounts({ className }: ManageAccountsProps) {\n  const { authClient } = useAuth<MultiSessionAuthClient>()\n  const { localization: multiSessionLocalization } =\n    useAuthPlugin(multiSessionPlugin)\n  const { data: session } = useSession(authClient)\n\n  const { data: deviceSessions, isPending } = useListDeviceSessions(authClient)\n\n  const otherSessions = deviceSessions?.filter(\n    (deviceSession) => deviceSession.session.id !== session?.session.id\n  )\n\n  const allRows = [\n    {\n      key: session?.session.id ?? \"current\",\n      deviceSession: !isPending ? session : null,\n      isPending\n    },\n    ...(otherSessions?.map((deviceSession) => ({\n      key: deviceSession.session.id,\n      deviceSession,\n      isPending: false\n    })) ?? [])\n  ]\n\n  return (\n    <div>\n      <h2 className=\"text-sm font-semibold mb-3\">\n        {multiSessionLocalization.manageAccounts}\n      </h2>\n\n      <Card className={cn(\"p-0\", className)}>\n        <CardContent className=\"p-0\">\n          <ItemGroup className=\"gap-0\">\n            {allRows.map((row, index) => (\n              <Fragment key={row.key}>\n                {index > 0 && <ItemSeparator />}\n                <ManageAccount\n                  deviceSession={row.deviceSession}\n                  isPending={row.isPending}\n                />\n              </Fragment>\n            ))}\n          </ItemGroup>\n        </CardContent>\n      </Card>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/multi-session/manage-accounts.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/multi-session/switch-account-submenu-content.tsx",
      "content": "\"use client\"\n\nimport type { MultiSessionAuthClient } from \"@better-auth-ui/core/plugins/multi-session\"\nimport { useAuth, useAuthPlugin, useSession } from \"@better-auth-ui/react\"\nimport { useListDeviceSessions } from \"@better-auth-ui/react/plugins/multi-session\"\nimport { Check, CirclePlus } from \"lucide-react\"\nimport { UserView } from \"@/components/auth/user/user-view\"\nimport {\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuSubContent\n} from \"@/components/ui/dropdown-menu\"\nimport { multiSessionPlugin } from \"@/lib/auth/multi-session-plugin\"\nimport { SwitchAccountSubmenuItem } from \"./switch-account-submenu-item\"\n\n/**\n * Render the submenu content for switching between multiple authenticated sessions.\n *\n * Shows the current session with a checkmark, lists other device sessions that can be activated,\n * and provides an option to add a new account. This component should be rendered inside a\n * DropdownMenuSub to defer the useListDeviceSessions query until the submenu is opened.\n *\n * @returns The switch account submenu content as a JSX element\n */\nexport function SwitchAccountSubmenuContent() {\n  const { authClient, basePaths, viewPaths, navigate } =\n    useAuth<MultiSessionAuthClient>()\n  const { localization: multiSessionLocalization } =\n    useAuthPlugin(multiSessionPlugin)\n  const { data: session } = useSession(authClient)\n  const { data: deviceSessions, isPending } = useListDeviceSessions(authClient)\n\n  return (\n    <DropdownMenuSubContent className=\"min-w-48 md:min-w-56 max-w-[48svw]\">\n      <DropdownMenuItem>\n        <UserView isPending={isPending} />\n\n        {!isPending && <Check className=\"ml-auto\" />}\n      </DropdownMenuItem>\n\n      {deviceSessions\n        ?.filter(\n          (deviceSession) => deviceSession.session.id !== session?.session.id\n        )\n        .map((deviceSession) => (\n          <SwitchAccountSubmenuItem\n            key={deviceSession.session.id}\n            deviceSession={deviceSession}\n          />\n        ))}\n\n      <DropdownMenuSeparator />\n\n      <DropdownMenuItem\n        onClick={() =>\n          navigate({ to: `${basePaths.auth}/${viewPaths.auth.signIn}` })\n        }\n      >\n        <CirclePlus className=\"text-muted-foreground\" />\n\n        {multiSessionLocalization.addAccount}\n      </DropdownMenuItem>\n    </DropdownMenuSubContent>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/multi-session/switch-account-submenu-content.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/multi-session/switch-account-submenu-item.tsx",
      "content": "\"use client\"\n\nimport type {\n  ListDeviceSession,\n  MultiSessionAuthClient\n} from \"@better-auth-ui/core/plugins/multi-session\"\nimport { useAuth } from \"@better-auth-ui/react\"\nimport { useSetActiveSession } from \"@better-auth-ui/react/plugins/multi-session\"\nimport { UserView } from \"@/components/auth/user/user-view\"\nimport { DropdownMenuItem } from \"@/components/ui/dropdown-menu\"\nimport { Spinner } from \"@/components/ui/spinner\"\n\nexport type SwitchAccountSubmenuItemProps = {\n  deviceSession: ListDeviceSession\n}\n\n/**\n * Render a dropdown menu item for switching to a different authenticated session.\n *\n * @param deviceSession - The device session to display and switch to when selected\n * @returns The switch account dropdown menu item as a JSX element\n */\nexport function SwitchAccountSubmenuItem({\n  deviceSession\n}: SwitchAccountSubmenuItemProps) {\n  const { authClient } = useAuth<MultiSessionAuthClient>()\n  const { mutate: setActiveSession, isPending } = useSetActiveSession(\n    authClient,\n    {\n      onSuccess: () => window.scrollTo({ top: 0 })\n    }\n  )\n\n  return (\n    <DropdownMenuItem\n      disabled={isPending}\n      onClick={() =>\n        setActiveSession({ sessionToken: deviceSession.session.token })\n      }\n    >\n      <UserView user={deviceSession.user} />\n\n      {isPending && <Spinner className=\"ml-auto size-4\" />}\n    </DropdownMenuItem>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/multi-session/switch-account-submenu-item.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/multi-session/switch-account-submenu.tsx",
      "content": "\"use client\"\n\nimport { useAuth, useAuthPlugin, useSession } from \"@better-auth-ui/react\"\nimport { ArrowLeftRight } from \"lucide-react\"\n\nimport {\n  DropdownMenuSub,\n  DropdownMenuSubTrigger\n} from \"@/components/ui/dropdown-menu\"\nimport { multiSessionPlugin } from \"@/lib/auth/multi-session-plugin\"\nimport { SwitchAccountSubmenuContent } from \"./switch-account-submenu-content\"\n\nexport type SwitchAccountSubmenuProps = {\n  className?: string\n}\n\n/**\n * Render a submenu trigger for switching between multiple authenticated sessions.\n *\n * This component renders as a dropdown menu item that opens a submenu containing\n * the switch account menu. It should be rendered inside the UserButton dropdown\n * as a userMenuItem from the multiSessionPlugin.\n *\n * @param className - Optional additional CSS class names\n * @returns The switch account submenu as a JSX element\n */\nexport function SwitchAccountSubmenu({ className }: SwitchAccountSubmenuProps) {\n  const { authClient } = useAuth()\n  const { data: session } = useSession(authClient)\n  const { localization: multiSessionLocalization } =\n    useAuthPlugin(multiSessionPlugin)\n\n  if (!session) {\n    return null\n  }\n\n  return (\n    <DropdownMenuSub>\n      <DropdownMenuSubTrigger className={className}>\n        <ArrowLeftRight className=\"text-muted-foreground\" />\n\n        {multiSessionLocalization.switchAccount}\n      </DropdownMenuSubTrigger>\n\n      <SwitchAccountSubmenuContent />\n    </DropdownMenuSub>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/multi-session/switch-account-submenu.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/user/user-avatar.tsx",
      "content": "\"use client\"\n\nimport type { UsernameAuthClient } from \"@better-auth-ui/core/plugins/username\"\nimport { useAuth, useSession } from \"@better-auth-ui/react\"\nimport type { User } from \"better-auth\"\nimport { User2 } from \"lucide-react\"\nimport type { ReactNode } from \"react\"\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { cn } from \"cn\"\n\nexport type UserAvatarProps = {\n  className?: string\n  fallback?: ReactNode\n  isPending?: boolean\n  /** @remarks `User` */\n  user?: User & { username?: string | null; displayUsername?: string | null }\n}\n\n/**\n * Display a user's avatar using session information or an explicit user prop.\n *\n * Renders a circular avatar that shows the user's image when available, a fallback node if provided, or the user's first two initials; while the session is loading (or when `isPending` is true) and no `user` prop is supplied, renders a skeleton placeholder.\n *\n * @param className - Additional CSS classes applied to the avatar container\n * @param user - Optional user object to display instead of the session user\n * @param isPending - When true, treat the component as loading and show the skeleton if no `user` is provided\n * @param fallback - Node to render inside the avatar fallback area before initials or the default icon\n * @returns The avatar element to render (JSX)\n */\nexport function UserAvatar({\n  className,\n  user,\n  isPending,\n  fallback\n}: UserAvatarProps) {\n  const { authClient } = useAuth<UsernameAuthClient>()\n  const { data: session, isPending: sessionPending } = useSession(authClient, {\n    enabled: !user && !isPending\n  })\n\n  if ((isPending || sessionPending) && !user) {\n    return <Skeleton className={cn(\"size-8 rounded-full\", className)} />\n  }\n\n  const resolvedUser = user ?? session?.user\n\n  const initials = (\n    resolvedUser?.username ||\n    resolvedUser?.name ||\n    resolvedUser?.email\n  )\n    ?.slice(0, 2)\n    .toUpperCase()\n\n  return (\n    <Avatar\n      className={cn(\n        \"size-8 bg-muted text-foreground text-sm rounded-full\",\n        className\n      )}\n    >\n      <AvatarImage\n        src={resolvedUser?.image ?? undefined}\n        alt={\n          resolvedUser?.displayUsername ||\n          resolvedUser?.name ||\n          resolvedUser?.email\n        }\n      />\n\n      <AvatarFallback className=\"text-muted-foreground!\">\n        {fallback || initials || <User2 className=\"size-4\" />}\n      </AvatarFallback>\n    </Avatar>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/user/user-avatar.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/user/user-view.tsx",
      "content": "\"use client\"\n\nimport type { UsernameAuthClient } from \"@better-auth-ui/core/plugins/username\"\nimport { useAuth, useSession } from \"@better-auth-ui/react\"\nimport type { User } from \"better-auth\"\n\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { cn } from \"cn\"\nimport { UserAvatar } from \"./user-avatar\"\n\nexport type UserViewProps = {\n  className?: string\n  isPending?: boolean\n  /**\n   * When true, the subtitle line (email when name/username is shown) is hidden.\n   * @default false\n   */\n  hideSubtitle?: boolean\n  /** @remarks `User` */\n  user?: Partial<User> & {\n    username?: string | null\n    displayUsername?: string | null\n  }\n}\n\n/**\n * Render a compact user item with an avatar, a primary label (display username, name, or email), and an optional subtitle (email).\n *\n * @param isPending - If true and no `user` prop is provided, renders a loading skeleton instead of user details\n * @param className - Additional CSS classes applied to the outer container\n * @param hideSubtitle - When true, omits the muted subtitle row under the primary label\n * @param user - Optional user object to display; when omitted the current session user is used\n * @returns A React element showing the user's avatar with their identifying information\n */\nexport function UserView({\n  className,\n  isPending,\n  hideSubtitle = false,\n  user\n}: UserViewProps) {\n  const { authClient } = useAuth<UsernameAuthClient>()\n  const { data: session, isPending: sessionPending } = useSession(authClient, {\n    enabled: !user && !isPending\n  })\n\n  const resolvedUser = user ?? session?.user\n\n  if ((isPending || sessionPending) && !user) {\n    return (\n      <div className={cn(\"flex items-center gap-2 min-w-0\", className)}>\n        <UserAvatar isPending />\n\n        <div className=\"grid flex-1 gap-1 text-left text-sm\">\n          <Skeleton className=\"h-4 w-24\" />\n\n          {!hideSubtitle && <Skeleton className=\"h-3 w-32\" />}\n        </div>\n      </div>\n    )\n  }\n\n  return (\n    <div className={cn(\"flex items-center gap-2 min-w-0\", className)}>\n      <UserAvatar user={resolvedUser as User | undefined} />\n\n      <div className=\"grid min-w-0 flex-1 text-left text-sm leading-tight\">\n        <span className=\"truncate font-medium text-foreground\">\n          {resolvedUser?.displayUsername ||\n            resolvedUser?.name ||\n            resolvedUser?.email}\n        </span>\n\n        {!hideSubtitle &&\n          (resolvedUser?.displayUsername || resolvedUser?.name) && (\n            <span className=\"text-muted-foreground truncate text-xs\">\n              {resolvedUser?.email}\n            </span>\n          )}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/user/user-view.tsx"
    },
    {
      "path": "examples/start-shadcn-example/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": "examples/start-shadcn-example/src/lib/auth/multi-session-plugin.ts",
      "content": "import { createAuthPlugin } from \"@better-auth-ui/core\"\nimport {\n  multiSessionPlugin as coreMultiSessionPlugin,\n  type MultiSessionPluginOptions\n} from \"@better-auth-ui/core/plugins/multi-session\"\n\nimport { ManageAccounts } from \"@/components/auth/multi-session/manage-accounts\"\nimport { SwitchAccountSubmenu } from \"@/components/auth/multi-session/switch-account-submenu\"\n\nexport const multiSessionPlugin = createAuthPlugin(\n  coreMultiSessionPlugin.id,\n  (options: MultiSessionPluginOptions = {}) => ({\n    ...coreMultiSessionPlugin(options),\n    accountCards: [ManageAccounts],\n    userMenuItems: [SwitchAccountSubmenu]\n  })\n)\n",
      "type": "registry:lib",
      "target": "@lib/auth/multi-session-plugin.ts"
    }
  ],
  "type": "registry:component"
}