{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "author": "DesignX ", "name": "auth-drawer", "type": "registry:component", "title": "Auth Drawer", "description": "A floating sign-in sheet with providers, one-time codes, passkeys and wallets.", "dependencies": [ "@base-ui/react@^1.8.0", "lucide-react@^0.577.0", "motion@^14.0.0", "react-use-measure@^2.1.7" ], "registryDependencies": [ "@dx/input-otp", "@dx/moving-border", "@dx/utils" ], "files": [ { "path": "dx/auth-drawer.tsx", "type": "registry:component", "target": "components/dx/auth-drawer.tsx", "content": "import * as React from \"react\";\nimport { Drawer as DrawerPrimitive } from \"@base-ui/react/drawer\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { ArrowRightIcon, ChevronLeftIcon, FingerprintIcon, WalletIcon, XIcon } from \"lucide-react\";\nimport useMeasure from \"react-use-measure\";\n\nimport { cn } from \"@/lib/utils\";\nimport { InputOTP, InputOTPSlot } from \"@/components/ui/input-otp\";\nimport { MovingBorder } from \"@/components/dx/moving-border\";\n\n/**\n * AuthDrawera floating sign-in sheet. Social providers, an\n * Email / Phone / Passkey switch, one-time-code verification and a wallet\n * list, with the sheet's height morphing between steps.\n */\n\ntype AuthMethod = \"email\" | \"phone\" | \"passkey\";\ntype AuthOption = { id: string; label: string; icon: React.ReactNode; badge?: string };\ntype View = \"signin\" | \"otp\" | \"passkey\" | \"wallet\";\n\ntype AuthDrawerProps = {\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Element that opens the drawer. */\n trigger?: React.ReactElement;\n title?: string;\n providers?: AuthOption[];\n wallets?: AuthOption[];\n methods?: AuthMethod[];\n onProvider?: (id: string) => void;\n onWallet?: (id: string) => void;\n /** Called before the code step; throw to stay on the form. */\n onSendCode?: (payload: { method: \"email\" | \"phone\"; value: string }) => void | Promise;\n /** Return false to reject the code. */\n onVerify?: (payload: { method: \"email\" | \"phone\"; value: string; code: string }) => boolean | Promise;\n /** Return false if the passkey prompt failed. */\n onPasskey?: () => boolean | Promise;\n /** Fired after a code or passkey succeeds; the drawer closes itself. */\n onSuccess?: (payload: { method: AuthMethod; value?: string }) => void;\n className?: string;\n};\n\nconst METHOD_LABEL: Record = { email: \"Email\", phone: \"Phone\", passkey: \"Passkey\" };\nconst PATTERN = {\n email: /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/,\n phone: /^\\+?[\\d\\s\\-()]{10,}$/,\n};\n\nconst ease = [0.25, 1, 0.5, 1] as const;\n\nfunction IconButton({ className, ...props }: React.ComponentProps<\"button\">) {\n return (\n \n );\n}\n\nfunction Header({ title, onBack }: { title: string; onBack?: () => void }) {\n return (\n
\n {onBack ? (\n \n \n \n ) : null}\n \n {title}\n \n {onBack && }\n
\n );\n}\n\nconst pill =\n \"focus-ring flex h-12 w-full items-center justify-center gap-2 rounded-full text-base font-semibold transition-[transform,background-color,opacity] duration-200 active:scale-[0.97] disabled:pointer-events-none disabled:opacity-50\";\n\nfunction AuthDrawer({\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n trigger,\n title = \"Sign in\",\n providers = [],\n wallets = [],\n methods = [\"email\", \"phone\", \"passkey\"],\n onProvider,\n onWallet,\n onSendCode,\n onVerify,\n onPasskey,\n onSuccess,\n className,\n}: AuthDrawerProps) {\n const [innerOpen, setInnerOpen] = React.useState(defaultOpen);\n const open = openProp ?? innerOpen;\n const [view, setView] = React.useState(\"signin\");\n const [method, setMethod] = React.useState(methods[0] ?? \"email\");\n const [value, setValue] = React.useState(\"\");\n const [code, setCode] = React.useState(\"\");\n const [status, setStatus] = React.useState<\"idle\" | \"busy\" | \"error\">(\"idle\");\n const [ref, bounds] = useMeasure();\n const pillId = React.useId();\n\n const setOpen = (o: boolean) => {\n if (openProp === undefined) setInnerOpen(o);\n onOpenChange?.(o);\n if (!o) {\n window.setTimeout(() => {\n setView(\"signin\");\n setCode(\"\");\n setStatus(\"idle\");\n }, 300);\n }\n };\n\n const go = (v: View) => {\n setStatus(\"idle\");\n setCode(\"\");\n setView(v);\n };\n\n const valid = method === \"passkey\" || PATTERN[method].test(value.trim());\n\n const succeed = (m: AuthMethod, v?: string) => {\n onSuccess?.({ method: m, value: v });\n setOpen(false);\n };\n\n const submit = async () => {\n if (!valid || status === \"busy\") return;\n if (method === \"passkey\") {\n go(\"passkey\");\n setStatus(\"busy\");\n const ok = onPasskey ? await onPasskey() : true;\n if (ok) succeed(\"passkey\");\n else setStatus(\"error\");\n return;\n }\n setStatus(\"busy\");\n try {\n await onSendCode?.({ method, value: value.trim() });\n go(\"otp\");\n } catch {\n setStatus(\"error\");\n }\n };\n\n const verify = async (c: string) => {\n if (method === \"passkey\" || c.length < 6) return;\n setStatus(\"busy\");\n const ok = onVerify ? await onVerify({ method, value: value.trim(), code: c }) : true;\n if (ok) succeed(method, value.trim());\n else {\n setStatus(\"error\");\n setCode(\"\");\n }\n };\n\n let content: React.ReactNode;\n if (view === \"signin\") {\n content = (\n
\n
\n
\n {providers.length > 0 && (\n
\n {providers.map((p) => (\n onProvider?.(p.id)}\n className=\"state-layer focus-ring flex h-12 flex-1 items-center justify-center rounded-xl bg-container-high transition-transform active:scale-95 [&_svg]:size-4.5\"\n >\n {p.icon}\n \n ))}\n
\n )}\n {methods.length > 1 && (\n
\n {methods.map((m) => (\n {\n setMethod(m);\n setValue(\"\");\n setStatus(\"idle\");\n }}\n className={cn(\n \"focus-ring relative flex flex-1 items-center justify-center rounded-xl text-sm font-semibold transition-colors\",\n method === m ? \"text-foreground\" : \"text-muted-foreground hover:text-foreground\",\n )}\n >\n {method === m && (\n \n )}\n {METHOD_LABEL[m]}\n \n ))}\n
\n )}\n {\n e.preventDefault();\n void submit();\n }}\n className={cn(\n \"flex h-12 items-center gap-3 overflow-hidden rounded-2xl bg-container-high pr-1 pl-4 ring-inset transition-shadow focus-within:ring-1 focus-within:ring-foreground/25\",\n status === \"error\" && \"ring-1 ring-destructive\",\n )}\n >\n {method === \"passkey\" ? (\n \n \n Continue with a passkey\n \n ) : (\n {\n setValue(e.target.value);\n if (status === \"error\") setStatus(\"idle\");\n }}\n className=\"min-w-0 flex-1 bg-transparent text-base font-medium outline-none placeholder:text-muted-foreground/70\"\n />\n )}\n \n \n \n \n
\n {wallets.length > 0 ? (\n
\n
\n \n Or\n \n
\n \n
\n ) : (\n
\n )}\n
\n );\n } else if (view === \"otp\") {\n content = (\n
\n
go(\"signin\")} />\n
\n
\n

Enter the code sent to

\n

{value}

\n
\n \n {\n setCode(c);\n if (status === \"error\") setStatus(\"idle\");\n }}\n onComplete={(c: string) => void verify(c)}\n aria-label=\"One-time code\"\n containerClassName=\"flex justify-between gap-2\"\n >\n {Array.from({ length: 6 }, (_, i) => (\n \n ))}\n \n \n

\n {status === \"error\" ? \"That code didn't work. Try again.\" : status === \"busy\" ? \"Verifying…\" : \"\"}\n

\n void verify(code)}\n className={cn(pill, \"bg-success text-white\")}\n >\n Verify code\n \n
\n
\n );\n } else if (view === \"passkey\") {\n content = (\n
\n
go(\"signin\")} />\n
\n \n \n \n
\n

{status === \"error\" ? \"Passkey failed\" : \"Waiting for passkey\"}

\n

\n {status === \"error\" ? \"The prompt was dismissed or timed out.\" : \"Follow the prompts from your device to continue.\"}\n

\n
\n (status === \"error\" ? void submit() : go(\"signin\"))}\n className={cn(pill, status === \"error\" ? \"bg-primary text-primary-foreground\" : \"bg-container-high\")}\n >\n {status === \"error\" ? \"Try again\" : \"Cancel\"}\n \n
\n
\n );\n } else {\n content = (\n
\n
go(\"signin\")} />\n
    \n {wallets.map((w) => (\n
  • \n onWallet?.(w.id)}\n className=\"state-layer focus-ring flex h-15 w-full items-center justify-between gap-3 rounded-2xl bg-container-high px-4 text-left\"\n >\n \n {w.label}\n {w.badge && (\n \n {w.badge}\n \n )}\n \n {w.icon}\n \n
  • \n ))}\n
\n
\n );\n }\n\n return (\n \n {trigger && }\n \n \n \n \n \n \n \n \n
\n \n \n {content}\n \n \n
\n
\n \n
\n
\n
\n );\n}\n\nexport { AuthDrawer };\nexport type { AuthDrawerProps, AuthMethod, AuthOption };\n" } ], "categories": [ "motion" ], "meta": { "group": "dx", "docs": "https://dxuireact.com/docs/components/auth-drawer" } }