{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "passkey",
  "title": "Passkey",
  "description": "Passkey plugin: WebAuthn sign-in button and passkey management card. Registers a passkey sign-in button on the sign-in form and a Passkeys security card in account settings.",
  "dependencies": [
    "@better-auth-ui/react@latest",
    "@better-auth-ui/core@latest",
    "@better-auth/passkey",
    "lucide-react"
  ],
  "registryDependencies": [
    "alert-dialog",
    "button",
    "card",
    "dialog",
    "empty",
    "field",
    "input",
    "item",
    "separator",
    "skeleton",
    "spinner"
  ],
  "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/passkey-plugin.ts",
      "content": "import { createAuthPlugin } from \"@better-auth-ui/core\"\nimport {\n  passkeyPlugin as corePasskeyPlugin,\n  type PasskeyPluginOptions\n} from \"@better-auth-ui/core/plugins\"\n\nimport { PasskeyButton } from \"@/components/auth/passkey/passkey-button\"\nimport { Passkeys } from \"@/components/auth/passkey/passkeys\"\n\nexport const passkeyPlugin = createAuthPlugin(\n  corePasskeyPlugin.id,\n  (options: PasskeyPluginOptions = {}) => ({\n    ...corePasskeyPlugin(options),\n    authButtons: [PasskeyButton],\n    securityCards: [Passkeys]\n  })\n)\n",
      "type": "registry:lib",
      "target": "@lib/auth/passkey-plugin.ts"
    },
    {
      "path": "src/components/auth/passkey/passkey-button.tsx",
      "content": "\"use client\"\n\nimport { type AuthView, authMutationKeys } from \"@better-auth-ui/core\"\nimport {\n  type PasskeyAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useSignInPasskey\n} from \"@better-auth-ui/react\"\nimport { useIsMutating } from \"@tanstack/react-query\"\nimport { Fingerprint } from \"lucide-react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { passkeyPlugin } from \"@/lib/auth/passkey-plugin\"\nimport { cn } from \"@/lib/utils\"\n\nexport type PasskeyButtonProps = {\n  /** @remarks `AuthView` */\n  view?: AuthView\n}\n\n/**\n * \"Continue with Passkey\" button rendered alongside the password sign-in form.\n *\n * Hidden on the sign-up view where passkey sign-in isn't applicable.\n *\n * @param view - Current auth view. Hides the button on `\"signUp\"`.\n */\nexport function PasskeyButton({ view }: PasskeyButtonProps) {\n  const { authClient, localization, redirectTo, navigate } = useAuth()\n  const { localization: passkeyLocalization } = useAuthPlugin(passkeyPlugin)\n\n  const { mutate: signInPasskey, isPending: passkeyPending } = useSignInPasskey(\n    authClient as PasskeyAuthClient,\n    {\n      onSuccess: () => navigate({ to: redirectTo })\n    }\n  )\n\n  const signInMutating = useIsMutating({\n    mutationKey: authMutationKeys.signIn.all\n  })\n  const signUpMutating = useIsMutating({\n    mutationKey: authMutationKeys.signUp.all\n  })\n  const isPending = signInMutating + signUpMutating > 0\n\n  // Passkey sign-in isn't relevant on the sign-up flow.\n  if (view === \"signUp\") return null\n\n  return (\n    <Button\n      type=\"button\"\n      variant=\"outline\"\n      disabled={isPending}\n      className={cn(\"w-full\", isPending && \"pointer-events-none opacity-50\")}\n      onClick={() => signInPasskey()}\n    >\n      {passkeyPending ? <Spinner /> : <Fingerprint />}\n      {localization.auth.continueWith.replace(\n        \"{{provider}}\",\n        passkeyLocalization.passkey\n      )}\n    </Button>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/passkey/passkey-button.tsx"
    },
    {
      "path": "src/components/auth/passkey/add-passkey-dialog.tsx",
      "content": "\"use client\"\n\nimport {\n  type PasskeyAuthClient,\n  useAddPasskey,\n  useAuth,\n  useAuthPlugin\n} from \"@better-auth-ui/react\"\nimport { Fingerprint } from \"lucide-react\"\nimport type { SyntheticEvent } from \"react\"\nimport { Button, buttonVariants } from \"@/components/ui/button\"\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle\n} from \"@/components/ui/dialog\"\nimport { Field, FieldError, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { passkeyPlugin } from \"@/lib/auth/passkey-plugin\"\n\nexport type AddPasskeyDialogProps = {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\nexport function AddPasskeyDialog({\n  open,\n  onOpenChange\n}: AddPasskeyDialogProps) {\n  const { authClient, localization } = useAuth()\n  const { localization: passkeyLocalization } = useAuthPlugin(passkeyPlugin)\n\n  const { mutate: addPasskey, isPending: isAdding } = useAddPasskey(\n    authClient as PasskeyAuthClient\n  )\n\n  const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {\n    e.preventDefault()\n\n    const formData = new FormData(e.target as HTMLFormElement)\n    const name = (formData.get(\"name\") as string)?.trim()\n\n    addPasskey(name ? { name } : undefined, {\n      onSuccess: () => onOpenChange(false)\n    })\n  }\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent>\n        <form onSubmit={handleSubmit} className=\"flex flex-col gap-6\">\n          <DialogHeader>\n            <DialogTitle>\n              <Fingerprint />\n              {passkeyLocalization.addPasskey}\n            </DialogTitle>\n\n            <DialogDescription>\n              {passkeyLocalization.passkeysDescription}\n            </DialogDescription>\n          </DialogHeader>\n\n          <Field>\n            <FieldLabel htmlFor=\"passkey-name\">\n              {passkeyLocalization.name}\n            </FieldLabel>\n\n            <Input\n              id=\"passkey-name\"\n              name=\"name\"\n              autoFocus\n              placeholder={localization.settings.optional}\n              disabled={isAdding}\n            />\n\n            <FieldError />\n          </Field>\n\n          <DialogFooter>\n            <DialogClose\n              className={buttonVariants({ variant: \"outline\" })}\n              disabled={isAdding}\n              type=\"button\"\n            >\n              {localization.settings.cancel}\n            </DialogClose>\n\n            <Button type=\"submit\" disabled={isAdding}>\n              {isAdding && <Spinner />}\n\n              {passkeyLocalization.addPasskey}\n            </Button>\n          </DialogFooter>\n        </form>\n      </DialogContent>\n    </Dialog>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/passkey/add-passkey-dialog.tsx"
    },
    {
      "path": "src/components/auth/passkey/delete-passkey-dialog.tsx",
      "content": "\"use client\"\n\nimport {\n  type PasskeyAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useDeletePasskey\n} from \"@better-auth-ui/react\"\nimport { Fingerprint } from \"lucide-react\"\n\nimport {\n  AlertDialog,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogMedia,\n  AlertDialogTitle\n} from \"@/components/ui/alert-dialog\"\nimport { Button } from \"@/components/ui/button\"\nimport { Field, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { passkeyPlugin } from \"@/lib/auth/passkey-plugin\"\n\nexport type ListedPasskey = {\n  id: string\n  name?: string | null\n  createdAt: Date\n}\n\nexport type DeletePasskeyDialogProps = {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n  passkey: ListedPasskey\n}\n\nexport function DeletePasskeyDialog({\n  open,\n  onOpenChange,\n  passkey\n}: DeletePasskeyDialogProps) {\n  const { authClient, localization } = useAuth()\n  const { localization: passkeyLocalization } = useAuthPlugin(passkeyPlugin)\n\n  const passkeyName = passkey.name || passkeyLocalization.passkey\n  const previewId = `delete-passkey-preview-${passkey.id}`\n\n  const { mutate: deletePasskey, isPending: isDeleting } = useDeletePasskey(\n    authClient as PasskeyAuthClient,\n    {\n      onSuccess: () => onOpenChange(false)\n    }\n  )\n\n  return (\n    <AlertDialog open={open} onOpenChange={onOpenChange}>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogMedia>\n            <Fingerprint />\n          </AlertDialogMedia>\n\n          <AlertDialogTitle>\n            {passkeyLocalization.deletePasskeyTitle}\n          </AlertDialogTitle>\n\n          <AlertDialogDescription>\n            {passkeyLocalization.deletePasskeyWarning}\n          </AlertDialogDescription>\n        </AlertDialogHeader>\n\n        <Field>\n          <FieldLabel htmlFor={previewId}>\n            {passkey.name || passkeyLocalization.passkey}\n          </FieldLabel>\n\n          <Input id={previewId} value={passkeyName} readOnly disabled />\n        </Field>\n\n        <AlertDialogFooter>\n          <AlertDialogCancel disabled={isDeleting}>\n            {localization.settings.cancel}\n          </AlertDialogCancel>\n\n          <Button\n            type=\"button\"\n            variant=\"destructive\"\n            disabled={isDeleting}\n            onClick={() => deletePasskey({ id: passkey.id })}\n          >\n            {isDeleting && <Spinner />}\n\n            {passkeyLocalization.deletePasskeyTitle}\n          </Button>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/passkey/delete-passkey-dialog.tsx"
    },
    {
      "path": "src/components/auth/passkey/passkeys-empty.tsx",
      "content": "\"use client\"\n\nimport { useAuthPlugin } from \"@better-auth-ui/react\"\nimport { Fingerprint } from \"lucide-react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Empty,\n  EmptyContent,\n  EmptyDescription,\n  EmptyHeader,\n  EmptyMedia,\n  EmptyTitle\n} from \"@/components/ui/empty\"\nimport { passkeyPlugin } from \"@/lib/auth/passkey-plugin\"\n\nexport type PasskeysEmptyProps = {\n  onAddPress: () => void\n}\n\nexport function PasskeysEmpty({ onAddPress }: PasskeysEmptyProps) {\n  const { localization: passkeyLocalization } = useAuthPlugin(passkeyPlugin)\n\n  return (\n    <Empty>\n      <EmptyHeader>\n        <EmptyMedia variant=\"icon\">\n          <Fingerprint />\n        </EmptyMedia>\n        <EmptyTitle>{passkeyLocalization.noPasskeys}</EmptyTitle>\n        <EmptyDescription>\n          {passkeyLocalization.passkeysDescription}\n        </EmptyDescription>\n      </EmptyHeader>\n      <EmptyContent>\n        <Button size=\"sm\" onClick={onAddPress}>\n          {passkeyLocalization.addPasskey}\n        </Button>\n      </EmptyContent>\n    </Empty>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/passkey/passkeys-empty.tsx"
    },
    {
      "path": "src/components/auth/passkey/passkey-skeleton.tsx",
      "content": "\"use client\"\n\nimport { Item, ItemContent, ItemMedia } from \"@/components/ui/item\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\n\nexport function PasskeySkeleton() {\n  return (\n    <Item>\n      <ItemMedia>\n        <Skeleton className=\"size-10 rounded-md\" />\n      </ItemMedia>\n      <ItemContent>\n        <Skeleton className=\"h-4 w-28\" />\n        <Skeleton className=\"h-3 w-32\" />\n      </ItemContent>\n    </Item>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/passkey/passkey-skeleton.tsx"
    },
    {
      "path": "src/components/auth/passkey/passkeys.tsx",
      "content": "\"use client\"\n\nimport {\n  type PasskeyAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useListPasskeys\n} from \"@better-auth-ui/react\"\nimport { Fragment, useState } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport { ItemGroup, ItemSeparator } from \"@/components/ui/item\"\nimport { passkeyPlugin } from \"@/lib/auth/passkey-plugin\"\nimport { cn } from \"@/lib/utils\"\n\nimport { AddPasskeyDialog } from \"./add-passkey-dialog\"\nimport { Passkey } from \"./passkey\"\nimport { PasskeySkeleton } from \"./passkey-skeleton\"\nimport { PasskeysEmpty } from \"./passkeys-empty\"\n\nexport type PasskeysProps = {\n  className?: string\n}\n\nexport function Passkeys({ className }: PasskeysProps) {\n  const { authClient } = useAuth()\n  const { localization: passkeyLocalization } = useAuthPlugin(passkeyPlugin)\n\n  const { data: passkeys, isPending } = useListPasskeys(\n    authClient as PasskeyAuthClient\n  )\n\n  const [addOpen, setAddOpen] = useState(false)\n\n  return (\n    <div className={cn(\"flex flex-col gap-3\", className)}>\n      <div className=\"flex items-end justify-between gap-3\">\n        <h2 className=\"truncate text-sm font-semibold\">\n          {passkeyLocalization.passkeys}\n        </h2>\n\n        <Button\n          className=\"shrink-0\"\n          size=\"sm\"\n          disabled={isPending}\n          onClick={() => setAddOpen(true)}\n        >\n          {passkeyLocalization.addPasskey}\n        </Button>\n      </div>\n\n      <Card className=\"p-0\">\n        <CardContent className=\"p-0\">\n          {isPending ? (\n            <PasskeySkeleton />\n          ) : !passkeys?.length ? (\n            <PasskeysEmpty onAddPress={() => setAddOpen(true)} />\n          ) : (\n            <ItemGroup className=\"gap-0\">\n              {passkeys.map((passkey, index) => (\n                <Fragment key={passkey.id}>\n                  {index > 0 && <ItemSeparator />}\n                  <Passkey passkey={passkey} />\n                </Fragment>\n              ))}\n            </ItemGroup>\n          )}\n        </CardContent>\n      </Card>\n\n      <AddPasskeyDialog open={addOpen} onOpenChange={setAddOpen} />\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/passkey/passkeys.tsx"
    },
    {
      "path": "src/components/auth/passkey/passkey.tsx",
      "content": "\"use client\"\n\nimport { useAuth, useAuthPlugin } from \"@better-auth-ui/react\"\nimport { Fingerprint, X } from \"lucide-react\"\nimport { useState } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Item,\n  ItemActions,\n  ItemContent,\n  ItemDescription,\n  ItemMedia,\n  ItemTitle\n} from \"@/components/ui/item\"\nimport { passkeyPlugin } from \"@/lib/auth/passkey-plugin\"\n\nimport {\n  DeletePasskeyDialog,\n  type ListedPasskey\n} from \"./delete-passkey-dialog\"\n\nexport type PasskeyProps = {\n  passkey: ListedPasskey\n}\n\nexport function Passkey({ passkey }: PasskeyProps) {\n  const { localization } = useAuth()\n  const { localization: passkeyLocalization } = useAuthPlugin(passkeyPlugin)\n  const [deleteOpen, setDeleteOpen] = useState(false)\n\n  const passkeyName = passkey.name || passkeyLocalization.passkey\n\n  return (\n    <Item>\n      <ItemMedia variant=\"icon\">\n        <Fingerprint />\n      </ItemMedia>\n      <ItemContent>\n        <ItemTitle>{passkeyName}</ItemTitle>\n        <ItemDescription>\n          {new Date(passkey.createdAt).toLocaleString(undefined, {\n            dateStyle: \"medium\",\n            timeStyle: \"short\"\n          })}\n        </ItemDescription>\n      </ItemContent>\n      <ItemActions>\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          onClick={() => setDeleteOpen(true)}\n          aria-label={passkeyLocalization.deletePasskey.replace(\n            \"{{name}}\",\n            passkeyName\n          )}\n        >\n          <X />\n\n          {localization.settings.delete}\n        </Button>\n\n        <DeletePasskeyDialog\n          open={deleteOpen}\n          onOpenChange={setDeleteOpen}\n          passkey={passkey}\n        />\n      </ItemActions>\n    </Item>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/passkey/passkey.tsx"
    }
  ],
  "type": "registry:component"
}