{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "auth",
  "title": "Sign In / Sign Up",
  "description": "Sign in and sign up sections built on shadcn inputs: a split onboarding panel, a spotlight card and a multi-step flow with code verification, with an animated mode switch, inline validation, loading, success and error states.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "input",
    "label",
    "checkbox",
    "utils",
    "https://trytweenly.vercel.app/r/use-reduced-motion.json",
    "https://trytweenly.vercel.app/r/otp-input.json"
  ],
  "files": [
    {
      "path": "registry/new-york/auth/auth.tsx",
      "content": "\"use client\"\n\nimport {\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type ChangeEvent,\n  type CSSProperties,\n  type FormEvent,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\"\nimport {\n  AnimatePresence,\n  motion,\n  useAnimate,\n  useSpring,\n  type Transition,\n  type Variants,\n} from \"motion/react\"\nimport { ArrowLeft, ArrowRight, Check, Eye, EyeOff, LoaderCircle } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { useReducedMotion } from \"@/registry/new-york/hooks/use-reduced-motion\"\nimport { OtpInput } from \"@/registry/new-york/otp-input/otp-input\"\n\nexport type AuthVariant = \"split\" | \"card\" | \"steps\"\nexport type AuthMode = \"sign-in\" | \"sign-up\"\nexport type AuthProvider = \"google\" | \"github\" | \"apple\" | \"x\"\n/** A single string, or one string per mode. */\nexport type AuthText = string | Partial<Record<AuthMode, string>>\n\nexport interface AuthValues {\n  mode: AuthMode\n  email: string\n  password?: string\n  /** Split sign up. */\n  firstName?: string\n  /** Split sign up. */\n  lastName?: string\n  /** Full name (split joins first and last). */\n  name?: string\n  /** Sign in with \"Remember me\" checked. */\n  remember?: boolean\n  /** Card: the user asked for a magic link instead of a password. */\n  magicLink?: boolean\n  /** Steps: the verified one-time code. */\n  code?: string\n  /** Steps: the chosen role. */\n  role?: string\n}\n\nexport interface AuthSectionProps {\n  /** Layout: two-panel onboarding, centered card, or a multi-step flow. Default: \"split\" */\n  variant?: AuthVariant\n  /** Controlled mode. */\n  mode?: AuthMode\n  /** Initial mode when uncontrolled. Default: \"sign-up\" */\n  defaultMode?: AuthMode\n  /** Called when the user switches between sign in and sign up. */\n  onModeChange?: (mode: AuthMode) => void\n  /** Heading. A string, or one per mode. Default: per variant and mode, e.g. \"Sign Up Account\" */\n  title?: AuthText\n  /** Line under the heading. A string, or one per mode. Default: per variant and mode */\n  subtitle?: AuthText\n  /** Split: left panel headline. Default: \"Get Started with Us\" */\n  headline?: string\n  /** Split: left panel description. Default: \"Complete these easy steps to register your account.\" */\n  description?: string\n  /** Social sign-in buttons, in order. Pass [] to hide them. Default: [\"google\", \"github\"] */\n  providers?: AuthProvider[]\n  /** Receives the form values. Return a promise to show the loading state; reject to show its message. */\n  onSubmit?: (values: AuthValues) => Promise<void> | void\n  /** Called when a social button is pressed. */\n  onProviderClick?: (provider: AuthProvider) => void\n  /** Called when \"Forgot password?\" is pressed. */\n  onForgotPassword?: () => void\n  /** Renders \"Forgot password?\" as a link to this URL instead of a button. */\n  forgotPasswordHref?: string\n  /** Logo shown above the heading. Default: none */\n  logo?: ReactNode\n  /** Color of primary actions, links and focus rings. Default: \"#ff4d12\" */\n  accent?: string\n  /** Split: base color of the left panel. Default: \"#0f3d2e\" */\n  panelColor?: string\n  /** Split: color of the soft light in the left panel. Default: \"#2f8f73\" */\n  glowColor?: string\n  /** Corner radius of the outer container in px. Default: 24 */\n  radius?: number\n  /** Split: labels of the three onboarding steps. Default: [\"Sign up your account\", \"Set up your workspace\", \"Set up your profile\"] */\n  steps?: string[]\n  /** Show the remember-me checkbox when signing in. Default: true */\n  showRememberMe?: boolean\n  /** Card: offer a passwordless \"email me a link\" option. Default: true */\n  magicLink?: boolean\n  /** Steps: checks the emailed code. Default: accepts any complete code */\n  verifyCode?: (code: string, email: string) => Promise<boolean> | boolean\n  /** Steps: called when \"Resend code\" is pressed. */\n  onResendCode?: (email: string) => void\n  /** Steps: choices for the role picker. Default: [\"Designer\", \"Engineer\", \"Founder\", \"Other\"] */\n  roles?: string[]\n  /** Terms of service URL. Sign up shows \"By continuing you agree to…\" when this or privacyHref is set. */\n  termsHref?: string\n  /** Privacy policy URL. */\n  privacyHref?: string\n  className?: string\n}\n\ntype Bezier = [number, number, number, number]\nconst EASE_OUT: Bezier = [0.22, 1, 0.36, 1]\nconst EASE_IN_OUT: Bezier = [0.76, 0, 0.24, 1]\nconst SPRING: Transition = { type: \"spring\", stiffness: 450, damping: 34 }\nconst INSTANT: Transition = { duration: 0 }\n\nconst DEFAULT_PROVIDERS: AuthProvider[] = [\"google\", \"github\"]\nconst DEFAULT_STEPS = [\"Sign up your account\", \"Set up your workspace\", \"Set up your profile\"]\nconst DEFAULT_ROLES = [\"Designer\", \"Engineer\", \"Founder\", \"Other\"]\n\nconst COPY: Record<AuthVariant, Record<AuthMode, [string, string]>> = {\n  split: {\n    \"sign-up\": [\"Sign Up Account\", \"Enter your personal data to create your account.\"],\n    \"sign-in\": [\"Log In Account\", \"Enter your credentials to access your account.\"],\n  },\n  card: {\n    \"sign-up\": [\"Create an account\", \"Start building in less than a minute.\"],\n    \"sign-in\": [\"Welcome back\", \"Sign in to continue to your workspace.\"],\n  },\n  steps: {\n    \"sign-up\": [\"Create your account\", \"Enter your email to get started.\"],\n    \"sign-in\": [\"Sign in\", \"We'll email you a one-time code.\"],\n  },\n}\n\nconst PROVIDER_NAMES: Record<AuthProvider, string> = { google: \"Google\", github: \"GitHub\", apple: \"Apple\", x: \"X\" }\n\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\ntype Status = \"idle\" | \"loading\" | \"success\"\ntype FieldName = \"firstName\" | \"lastName\" | \"name\" | \"email\" | \"password\"\ntype Errors = Partial<Record<FieldName, string>>\n\nconst pick = (text: AuthText | undefined, mode: AuthMode, fallback: string) =>\n  (typeof text === \"string\" ? text : text?.[mode]) || fallback\n\nfunction validateField(name: FieldName, value: string, mode: AuthMode): string | undefined {\n  const v = value.trim()\n  switch (name) {\n    case \"email\":\n      if (!v) return \"Email is required.\"\n      return EMAIL_RE.test(v) ? undefined : \"Enter a valid email address.\"\n    case \"password\":\n      if (!value) return \"Password is required.\"\n      return mode === \"sign-up\" && value.length < 8 ? \"Must be at least 8 characters.\" : undefined\n    case \"firstName\":\n      return v ? undefined : \"First name is required.\"\n    case \"lastName\":\n      return v ? undefined : \"Last name is required.\"\n    case \"name\":\n      return v ? undefined : \"Name is required.\"\n  }\n}\n\nfunction errorMessage(err: unknown) {\n  if (err instanceof Error && err.message) return err.message\n  if (typeof err === \"string\" && err) return err\n  return \"Something went wrong. Please try again.\"\n}\n\n/* ------------------------------------------------------------------ */\n/* Small building blocks                                               */\n/* ------------------------------------------------------------------ */\n\nfunction ProviderIcon({ provider, className }: { provider: AuthProvider; className?: string }) {\n  const common = { viewBox: \"0 0 24 24\", className, \"aria-hidden\": true as const }\n  switch (provider) {\n    case \"google\":\n      return (\n        <svg {...common}>\n          <path fill=\"#4285F4\" d=\"M23.49 12.27c0-.79-.07-1.54-.19-2.27H12v4.51h6.47c-.29 1.48-1.14 2.73-2.4 3.58v3h3.86c2.26-2.09 3.56-5.17 3.56-8.82z\" />\n          <path fill=\"#34A853\" d=\"M12 24c3.24 0 5.95-1.08 7.93-2.91l-3.86-3c-1.08.72-2.45 1.16-4.07 1.16-3.13 0-5.78-2.11-6.73-4.96H1.29v3.09C3.26 21.3 7.31 24 12 24z\" />\n          <path fill=\"#FBBC05\" d=\"M5.27 14.29c-.25-.72-.38-1.49-.38-2.29s.14-1.57.38-2.29V6.62H1.29C.47 8.24 0 10.06 0 12s.47 3.76 1.29 5.38l3.98-3.09z\" />\n          <path fill=\"#EA4335\" d=\"M12 4.75c1.77 0 3.35.61 4.6 1.8l3.42-3.42C17.95 1.19 15.24 0 12 0 7.31 0 3.26 2.7 1.29 6.62l3.98 3.09c.95-2.85 3.6-4.96 6.73-4.96z\" />\n        </svg>\n      )\n    case \"github\":\n      return (\n        <svg {...common} fill=\"currentColor\">\n          <path d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\" />\n        </svg>\n      )\n    case \"apple\":\n      return (\n        <svg {...common} fill=\"currentColor\">\n          <path d=\"M16.365 1.43c0 1.14-.493 2.27-1.177 3.08-.744.9-1.99 1.57-2.987 1.57-.12 0-.23-.02-.3-.03-.01-.06-.04-.22-.04-.39 0-1.15.572-2.27 1.206-2.98.804-.94 2.142-1.64 3.248-1.68.03.13.05.28.05.43zm4.565 15.71c-.03.07-.463 1.58-1.518 3.12-.945 1.34-1.94 2.71-3.43 2.71-1.517 0-1.9-.88-3.63-.88-1.698 0-2.302.91-3.67.91-1.377 0-2.332-1.26-3.428-2.8-1.287-1.82-2.323-4.63-2.323-7.28 0-4.28 2.797-6.55 5.552-6.55 1.448 0 2.675.95 3.6.95.865 0 2.222-1.01 3.902-1.01.613 0 2.886.06 4.374 2.19-.13.09-2.383 1.37-2.383 4.19 0 3.26 2.854 4.42 2.955 4.45z\" />\n        </svg>\n      )\n    case \"x\":\n      return (\n        <svg {...common} fill=\"currentColor\">\n          <path d=\"M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z\" />\n        </svg>\n      )\n  }\n}\n\n/** Rises into place when scrolled into view; `order` staggers sibling blocks. */\nfunction Rise({ order = 0, reduced, className, children }: { order?: number; reduced: boolean; className?: string; children: ReactNode }) {\n  return (\n    <motion.div\n      className={className}\n      initial={reduced ? false : { opacity: 0, y: 14, filter: \"blur(6px)\" }}\n      whileInView={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n      viewport={{ once: true, amount: 0.2 }}\n      transition={reduced ? INSTANT : { duration: 0.7, ease: EASE_OUT, delay: 0.08 + order * 0.07 }}\n    >\n      {children}\n    </motion.div>\n  )\n}\n\n/** Height-animated show/hide. Overflow is only clipped while animating so focus rings stay visible. */\nfunction Collapse({ show, reduced, children, className }: { show: boolean; reduced: boolean; children: ReactNode; className?: string }) {\n  return (\n    <AnimatePresence initial={false}>\n      {show && (\n        <motion.div\n          key=\"collapse\"\n          className={cn(\"-mx-1 px-1\", className)}\n          initial={{ height: 0, opacity: 0, overflow: \"hidden\" }}\n          animate={{ height: \"auto\", opacity: 1, transitionEnd: { overflow: \"visible\" } }}\n          exit={{ height: 0, opacity: 0, overflow: \"hidden\" }}\n          transition={reduced ? INSTANT : { height: { duration: 0.5, ease: EASE_IN_OUT }, opacity: { duration: 0.3, ease: EASE_OUT } }}\n        >\n          {children}\n        </motion.div>\n      )}\n    </AnimatePresence>\n  )\n}\n\n/** Crossfades text in place; old and new overlap in one grid cell so nothing jumps. */\nfunction SwapText({ text, as = \"h2\", className, reduced }: { text: string; as?: \"h2\" | \"p\"; className?: string; reduced: boolean }) {\n  const Comp = as === \"h2\" ? motion.h2 : motion.p\n  return (\n    <div className=\"grid\">\n      <AnimatePresence initial={false}>\n        <Comp\n          key={text}\n          className={cn(\"[grid-area:1/1]\", className)}\n          initial={{ opacity: 0, y: 8 }}\n          animate={{ opacity: 1, y: 0 }}\n          exit={{ opacity: 0, y: -8 }}\n          transition={reduced ? INSTANT : { duration: 0.35, ease: EASE_OUT }}\n        >\n          {text}\n        </Comp>\n      </AnimatePresence>\n    </div>\n  )\n}\n\nfunction FieldMessage({ id, error, hint, reduced, hintClassName }: { id: string; error?: string; hint?: string; reduced: boolean; hintClassName?: string }) {\n  const t = reduced ? INSTANT : { duration: 0.28, ease: EASE_OUT }\n  return (\n    <div id={id} aria-live=\"polite\" className=\"text-xs\">\n      <AnimatePresence initial={false} mode=\"wait\">\n        {error ? (\n          <motion.p\n            key=\"error\"\n            className=\"pt-1.5 text-destructive\"\n            initial={{ opacity: 0, y: -4, height: hint ? \"auto\" : 0 }}\n            animate={{ opacity: 1, y: 0, height: \"auto\" }}\n            exit={{ opacity: 0, y: -4, height: hint ? \"auto\" : 0 }}\n            transition={t}\n          >\n            {error}\n          </motion.p>\n        ) : hint ? (\n          <motion.p\n            key=\"hint\"\n            className={cn(\"pt-1.5 text-muted-foreground\", hintClassName)}\n            initial={{ opacity: 0, y: 4 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: 4 }}\n            transition={t}\n          >\n            {hint}\n          </motion.p>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n\ninterface FieldProps {\n  id: string\n  label: string\n  error?: string\n  hint?: string\n  aside?: ReactNode\n  reduced: boolean\n  labelClassName?: string\n  hintClassName?: string\n  children: ReactNode\n}\n\nfunction Field({ id, label, error, hint, aside, reduced, labelClassName, hintClassName, children }: FieldProps) {\n  return (\n    <div className=\"group/field\">\n      <div className=\"mb-2 flex items-center justify-between gap-2\">\n        <Label\n          htmlFor={id}\n          className={cn(\"transition-colors duration-200 group-focus-within/field:text-[var(--auth-accent)]\", labelClassName)}\n        >\n          {label}\n        </Label>\n        {aside}\n      </div>\n      {children}\n      <FieldMessage id={`${id}-msg`} error={error} hint={hint} reduced={reduced} hintClassName={hintClassName} />\n    </div>\n  )\n}\n\nfunction PasswordInput({ reduced, className, ...props }: React.ComponentProps<\"input\"> & { reduced: boolean }) {\n  const [visible, setVisible] = useState(false)\n  return (\n    <div className=\"relative\">\n      <Input {...props} type={visible ? \"text\" : \"password\"} className={cn(\"pr-10\", className)} />\n      <button\n        type=\"button\"\n        onClick={() => setVisible((v) => !v)}\n        aria-label={visible ? \"Hide password\" : \"Show password\"}\n        aria-pressed={visible}\n        className=\"absolute inset-y-0 right-0 grid w-10 place-items-center rounded-r-lg text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-[var(--auth-accent)]\"\n      >\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          <motion.span\n            key={visible ? \"hide\" : \"show\"}\n            className=\"grid place-items-center\"\n            initial={{ opacity: 0, scale: 0.6, rotate: -30 }}\n            animate={{ opacity: 1, scale: 1, rotate: 0 }}\n            exit={{ opacity: 0, scale: 0.6, rotate: 30 }}\n            transition={reduced ? INSTANT : SPRING}\n          >\n            {visible ? <EyeOff className=\"size-4\" /> : <Eye className=\"size-4\" />}\n          </motion.span>\n        </AnimatePresence>\n      </button>\n    </div>\n  )\n}\n\nfunction SubmitButton({ status, reduced, className, style, children }: { status: Status; reduced: boolean; className?: string; style?: CSSProperties; children: ReactNode }) {\n  return (\n    <Button\n      type=\"submit\"\n      aria-busy={status === \"loading\" || undefined}\n      className={cn(\"relative h-10 w-full overflow-hidden rounded-lg text-sm font-medium\", className)}\n      style={style}\n    >\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        <motion.span\n          key={status}\n          className=\"flex items-center justify-center gap-2\"\n          initial={{ opacity: 0, y: status === \"idle\" ? -14 : 14, scale: 0.85 }}\n          animate={{ opacity: 1, y: 0, scale: 1 }}\n          exit={{ opacity: 0, y: -14, scale: 0.85 }}\n          transition={reduced ? INSTANT : SPRING}\n        >\n          {status === \"loading\" ? (\n            <LoaderCircle className={cn(\"size-4\", !reduced && \"animate-spin\")} aria-hidden=\"true\" />\n          ) : status === \"success\" ? (\n            <Check className=\"size-4\" strokeWidth={2.75} aria-hidden=\"true\" />\n          ) : (\n            children\n          )}\n        </motion.span>\n      </AnimatePresence>\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {status === \"loading\" ? \"Submitting\" : status === \"success\" ? \"Done\" : \"\"}\n      </span>\n    </Button>\n  )\n}\n\nfunction ProviderButtons({\n  providers,\n  onProviderClick,\n  iconOnly = false,\n  buttonClassName,\n}: {\n  providers: AuthProvider[]\n  onProviderClick?: (provider: AuthProvider) => void\n  iconOnly?: boolean\n  buttonClassName?: string\n}) {\n  const cols = iconOnly ? providers.length : Math.min(providers.length, 2)\n  return (\n    <div className=\"grid gap-2.5\" style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}>\n      {providers.map((p) => (\n        <Button\n          key={p}\n          type=\"button\"\n          variant=\"outline\"\n          aria-label={iconOnly ? `Continue with ${PROVIDER_NAMES[p]}` : undefined}\n          onClick={() => onProviderClick?.(p)}\n          className={cn(\"h-10 gap-2 rounded-lg transition-[transform,background-color] duration-150 active:scale-[0.97]\", buttonClassName)}\n        >\n          <ProviderIcon provider={p} className=\"size-4\" />\n          {!iconOnly && <span>{PROVIDER_NAMES[p]}</span>}\n        </Button>\n      ))}\n    </div>\n  )\n}\n\nfunction Divider({ label, className }: { label: string; className?: string }) {\n  return (\n    <div className={cn(\"flex items-center gap-3 text-xs text-muted-foreground\", className)}>\n      <span className=\"h-px flex-1 bg-border\" />\n      {label}\n      <span className=\"h-px flex-1 bg-border\" />\n    </div>\n  )\n}\n\nfunction Terms({ termsHref, privacyHref, className, linkClassName }: { termsHref?: string; privacyHref?: string; className?: string; linkClassName?: string }) {\n  if (!termsHref && !privacyHref) return null\n  const link = cn(\"font-medium text-foreground underline underline-offset-4 hover:text-[var(--auth-accent)]\", linkClassName)\n  return (\n    <p className={cn(\"text-center text-xs leading-relaxed text-muted-foreground\", className)}>\n      By continuing you agree to our{\" \"}\n      {termsHref && (\n        <a href={termsHref} className={link}>\n          Terms of Service\n        </a>\n      )}\n      {termsHref && privacyHref && \" and \"}\n      {privacyHref && (\n        <a href={privacyHref} className={link}>\n          Privacy Policy\n        </a>\n      )}\n      .\n    </p>\n  )\n}\n\nfunction ModeSwitch({ mode, onSwitch, className, linkClassName, signInLabel = \"Sign in\" }: { mode: AuthMode; onSwitch: (m: AuthMode) => void; className?: string; linkClassName?: string; signInLabel?: string }) {\n  const signUp = mode === \"sign-up\"\n  return (\n    <p className={cn(\"text-center text-sm text-muted-foreground\", className)}>\n      {signUp ? \"Already have an account? \" : \"Don't have an account? \"}\n      <button\n        type=\"button\"\n        onClick={() => onSwitch(signUp ? \"sign-in\" : \"sign-up\")}\n        className={cn(\"rounded-sm font-medium text-[var(--auth-accent)] underline-offset-4 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-[var(--auth-accent)]\", linkClassName)}\n      >\n        {signUp ? signInLabel : \"Sign up\"}\n      </button>\n    </p>\n  )\n}\n\nfunction useMode(mode: AuthMode | undefined, defaultMode: AuthMode, onModeChange?: (m: AuthMode) => void) {\n  const [inner, setInner] = useState(defaultMode)\n  const current = mode ?? inner\n  const set = (next: AuthMode) => {\n    if (next === current) return\n    if (mode === undefined) setInner(next)\n    onModeChange?.(next)\n  }\n  return [current, set] as const\n}\n\nfunction useShake(reduced: boolean) {\n  const [scope, animate] = useAnimate<HTMLDivElement>()\n  const shake = () => {\n    if (reduced || !scope.current) return\n    animate(scope.current, { x: [0, -10, 9, -6, 4, -2, 0] }, { duration: 0.5, ease: \"easeOut\" })\n  }\n  return [scope, shake] as const\n}\n\nconst ACCENT_FOCUS =\n  \"h-10 rounded-lg px-3 transition-[border-color,box-shadow,background-color] duration-200 focus-visible:border-[var(--auth-accent)] focus-visible:ring-[color-mix(in_oklab,var(--auth-accent)_25%,transparent)] focus-visible:shadow-[0_0_0_6px_color-mix(in_oklab,var(--auth-accent)_10%,transparent)]\"\n\n/* ------------------------------------------------------------------ */\n/* Credentials form (split + card)                                     */\n/* ------------------------------------------------------------------ */\n\ninterface CredentialsFormProps {\n  appearance: \"split\" | \"card\"\n  mode: AuthMode\n  onModeSwitch: (mode: AuthMode) => void\n  title: string\n  subtitle: string\n  logo?: ReactNode\n  providers: AuthProvider[]\n  onProviderClick?: (provider: AuthProvider) => void\n  onSubmit?: (values: AuthValues) => Promise<void> | void\n  onForgotPassword?: () => void\n  forgotPasswordHref?: string\n  showRememberMe: boolean\n  magicLink: boolean\n  termsHref?: string\n  privacyHref?: string\n  reduced: boolean\n  onSuccess?: () => void\n}\n\nfunction CredentialsForm({\n  appearance,\n  mode,\n  onModeSwitch,\n  title,\n  subtitle,\n  logo,\n  providers,\n  onProviderClick,\n  onSubmit,\n  onForgotPassword,\n  forgotPasswordHref,\n  showRememberMe,\n  magicLink,\n  termsHref,\n  privacyHref,\n  reduced,\n  onSuccess,\n}: CredentialsFormProps) {\n  const id = useId()\n  const [values, setValues] = useState<Record<FieldName, string>>({ firstName: \"\", lastName: \"\", name: \"\", email: \"\", password: \"\" })\n  const [remember, setRemember] = useState(true)\n  const [useLink, setUseLink] = useState(false)\n  const [errors, setErrors] = useState<Errors>({})\n  const [status, setStatus] = useState<Status>(\"idle\")\n  const [formError, setFormError] = useState<string | null>(null)\n  const [prevMode, setPrevMode] = useState(mode)\n  const [scope, shake] = useShake(reduced)\n  const runRef = useRef(0)\n\n  if (mode !== prevMode) {\n    setPrevMode(mode)\n    setErrors({})\n    setStatus(\"idle\")\n    setFormError(null)\n  }\n\n  const split = appearance === \"split\"\n  const signUp = mode === \"sign-up\"\n  const linkMode = !split && magicLink && useLink\n  const active: FieldName[] = [\n    ...(signUp ? (split ? ([\"firstName\", \"lastName\"] as const) : ([\"name\"] as const)) : []),\n    \"email\",\n    ...(linkMode ? [] : ([\"password\"] as const)),\n  ]\n\n  const inputCls = cn(\n    ACCENT_FOCUS,\n    split && \"border-white/10 bg-white/[0.05] text-white placeholder:text-white/30 dark:bg-white/[0.05]\"\n  )\n  const labelCls = split ? \"text-[13px] font-normal text-white/85\" : undefined\n  const hintCls = split ? \"text-white/45\" : undefined\n\n  const resetFeedback = () => {\n    if (status === \"success\") setStatus(\"idle\")\n    if (formError) setFormError(null)\n  }\n\n  const change = (name: FieldName) => (e: ChangeEvent<HTMLInputElement>) => {\n    const v = e.target.value\n    setValues((s) => ({ ...s, [name]: v }))\n    if (errors[name]) setErrors((er) => ({ ...er, [name]: validateField(name, v, mode) }))\n    resetFeedback()\n  }\n\n  const blur = (name: FieldName) => () => {\n    const v = values[name]\n    if (v) setErrors((er) => ({ ...er, [name]: validateField(name, v, mode) }))\n  }\n\n  const inputProps = (name: FieldName) => ({\n    id: `${id}-${name}`,\n    name,\n    value: values[name],\n    onChange: change(name),\n    onBlur: blur(name),\n    \"aria-invalid\": errors[name] ? true : undefined,\n    \"aria-describedby\": `${id}-${name}-msg`,\n    className: inputCls,\n  })\n\n  const switchMode = (next: AuthMode) => {\n    runRef.current++\n    onModeSwitch(next)\n  }\n\n  const submit = async (e: FormEvent<HTMLFormElement>) => {\n    e.preventDefault()\n    if (status === \"loading\") return\n    const next: Errors = {}\n    for (const n of active) {\n      const m = validateField(n, values[n], mode)\n      if (m) next[n] = m\n    }\n    setErrors(next)\n    setFormError(null)\n    const first = active.find((n) => next[n])\n    if (first) {\n      shake()\n      document.getElementById(`${id}-${first}`)?.focus()\n      return\n    }\n    const payload: AuthValues = { mode, email: values.email.trim() }\n    if (!linkMode) payload.password = values.password\n    if (signUp && split) {\n      payload.firstName = values.firstName.trim()\n      payload.lastName = values.lastName.trim()\n      payload.name = `${payload.firstName} ${payload.lastName}`\n    }\n    if (signUp && !split) payload.name = values.name.trim()\n    if (!signUp && showRememberMe && !linkMode) payload.remember = remember\n    if (linkMode) payload.magicLink = true\n\n    const run = ++runRef.current\n    setStatus(\"loading\")\n    try {\n      await onSubmit?.(payload)\n      if (runRef.current !== run) return\n      setStatus(\"success\")\n      onSuccess?.()\n    } catch (err) {\n      if (runRef.current !== run) return\n      setStatus(\"idle\")\n      setFormError(errorMessage(err))\n      shake()\n    }\n  }\n\n  const forgotCls = cn(\n    \"rounded-sm text-xs font-medium outline-none underline-offset-4 hover:underline focus-visible:ring-2 focus-visible:ring-[var(--auth-accent)]\",\n    split ? \"text-white/70 hover:text-white\" : \"text-[var(--auth-accent)]\"\n  )\n  const forgot = !signUp ? (\n    forgotPasswordHref ? (\n      <a href={forgotPasswordHref} className={forgotCls}>\n        Forgot password?\n      </a>\n    ) : (\n      <button type=\"button\" onClick={onForgotPassword} className={forgotCls}>\n        Forgot password?\n      </button>\n    )\n  ) : null\n\n  const submitLabel = linkMode ? \"Send magic link\" : split ? (signUp ? \"Sign Up\" : \"Log In\") : signUp ? \"Create account\" : \"Sign in\"\n\n  return (\n    <div className=\"flex w-full flex-col\">\n      <Rise order={0} reduced={reduced} className=\"flex flex-col items-center gap-1.5 text-center\">\n        {!split && logo && <div className=\"mb-3\">{logo}</div>}\n        <SwapText\n          text={title}\n          reduced={reduced}\n          className={cn(\"font-semibold tracking-tight\", split ? \"text-2xl text-white @[720px]:text-[1.75rem]\" : \"text-xl text-foreground\")}\n        />\n        <SwapText text={subtitle} as=\"p\" reduced={reduced} className={cn(\"text-sm\", split ? \"text-white/55\" : \"text-muted-foreground\")} />\n      </Rise>\n\n      {providers.length > 0 && (\n        <Rise order={1} reduced={reduced} className={cn(\"flex flex-col gap-5\", split ? \"mt-7\" : \"mt-6\")}>\n          <ProviderButtons\n            providers={providers}\n            onProviderClick={onProviderClick}\n            iconOnly={!split && providers.length > 2}\n            buttonClassName={split ? \"border-white/15 bg-transparent text-white hover:bg-white/[0.06] dark:bg-transparent dark:hover:bg-white/[0.06]\" : undefined}\n          />\n          <Divider label={split ? \"Or\" : \"or continue with email\"} className={split ? \"text-white/45 [&>span]:bg-white/10\" : undefined} />\n        </Rise>\n      )}\n\n      <Rise order={2} reduced={reduced} className={providers.length ? \"mt-5\" : \"mt-6\"}>\n      <div ref={scope}>\n      <form noValidate onSubmit={submit} className=\"flex flex-col\">\n        <Collapse show={signUp && split} reduced={reduced}>\n          <div className=\"grid grid-cols-2 gap-3 pb-4\">\n            <Field id={`${id}-firstName`} label=\"First Name\" error={errors.firstName} reduced={reduced} labelClassName={labelCls}>\n              <Input {...inputProps(\"firstName\")} autoComplete=\"given-name\" placeholder=\"eg. John\" />\n            </Field>\n            <Field id={`${id}-lastName`} label=\"Last Name\" error={errors.lastName} reduced={reduced} labelClassName={labelCls}>\n              <Input {...inputProps(\"lastName\")} autoComplete=\"family-name\" placeholder=\"eg. Francisco\" />\n            </Field>\n          </div>\n        </Collapse>\n        <Collapse show={signUp && !split} reduced={reduced}>\n          <div className=\"pb-4\">\n            <Field id={`${id}-name`} label=\"Name\" error={errors.name} reduced={reduced}>\n              <Input {...inputProps(\"name\")} autoComplete=\"name\" placeholder=\"Ada Lovelace\" />\n            </Field>\n          </div>\n        </Collapse>\n        <div className=\"pb-4\">\n          <Field id={`${id}-email`} label=\"Email\" error={errors.email} reduced={reduced} labelClassName={labelCls}>\n            <Input {...inputProps(\"email\")} type=\"email\" inputMode=\"email\" autoComplete=\"email\" placeholder={split ? \"eg. johnfrans@gmail.com\" : \"you@example.com\"} />\n          </Field>\n        </div>\n        <Collapse show={!linkMode} reduced={reduced}>\n          <div className=\"pb-4\">\n            <Field\n              id={`${id}-password`}\n              label=\"Password\"\n              error={errors.password}\n              hint={signUp ? \"Must be at least 8 characters.\" : undefined}\n              aside={forgot}\n              reduced={reduced}\n              labelClassName={labelCls}\n              hintClassName={hintCls}\n            >\n              <PasswordInput\n                {...inputProps(\"password\")}\n                reduced={reduced}\n                autoComplete={signUp ? \"new-password\" : \"current-password\"}\n                placeholder=\"Enter your password\"\n              />\n            </Field>\n          </div>\n        </Collapse>\n        <Collapse show={!signUp && showRememberMe && !linkMode} reduced={reduced}>\n          <div className=\"pb-4\">\n            <Label className={cn(\"w-fit cursor-pointer font-normal\", split ? \"text-white/75\" : \"text-muted-foreground\")}>\n              <Checkbox\n                checked={remember}\n                onCheckedChange={(checked) => setRemember(checked)}\n                className={cn(\n                  \"data-checked:border-[var(--auth-accent)] data-checked:bg-[var(--auth-accent)] data-checked:text-white dark:data-checked:bg-[var(--auth-accent)]\",\n                  split && \"border-white/20\"\n                )}\n              />\n              Remember me\n            </Label>\n          </div>\n        </Collapse>\n        <Collapse show={!!formError} reduced={reduced}>\n          <div className=\"pb-4\">\n            <p role=\"alert\" className=\"rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive\">\n              {formError}\n            </p>\n          </div>\n        </Collapse>\n        <SubmitButton\n          status={status}\n          reduced={reduced}\n          className={split ? \"bg-white text-black hover:bg-white/85\" : \"text-white hover:opacity-90\"}\n          style={split ? undefined : { backgroundColor: \"var(--auth-accent)\" }}\n        >\n          {submitLabel}\n        </SubmitButton>\n        <Collapse show={linkMode && status === \"success\"} reduced={reduced}>\n          <p className=\"pt-3 text-center text-sm text-muted-foreground\" role=\"status\">\n            Check your inbox. We sent a sign-in link to <span className=\"font-medium text-foreground\">{values.email.trim()}</span>.\n          </p>\n        </Collapse>\n      </form>\n      </div>\n      </Rise>\n\n      {!split && magicLink && (\n        <button\n          type=\"button\"\n          onClick={() => {\n            setUseLink((v) => !v)\n            setErrors((er) => ({ ...er, password: undefined }))\n            resetFeedback()\n          }}\n          className=\"mx-auto mt-3 rounded-sm text-xs font-medium text-muted-foreground outline-none underline-offset-4 hover:text-foreground hover:underline focus-visible:ring-2 focus-visible:ring-[var(--auth-accent)]\"\n        >\n          {useLink ? \"Use a password instead\" : \"Email me a sign-in link instead\"}\n        </button>\n      )}\n\n      <Rise order={3} reduced={reduced}>\n      <ModeSwitch\n        mode={mode}\n        onSwitch={switchMode}\n        signInLabel={split ? \"Log in\" : \"Sign in\"}\n        className={cn(\"mt-5\", split && \"text-white/55\")}\n        linkClassName={split ? \"text-white\" : undefined}\n      />\n      {signUp && (\n        <Terms termsHref={termsHref} privacyHref={privacyHref} className={cn(\"mt-3\", split && \"text-white/40\")} linkClassName={split ? \"text-white/80\" : undefined} />\n      )}\n      </Rise>\n    </div>\n  )\n}\n\n/* ------------------------------------------------------------------ */\n/* Split                                                              */\n/* ------------------------------------------------------------------ */\n\nfunction SplitPanel({\n  headline,\n  description,\n  steps,\n  active,\n  logo,\n  panelColor,\n  glowColor,\n  radius,\n  reduced,\n}: {\n  headline: string\n  description: string\n  steps: string[]\n  active: number\n  logo?: ReactNode\n  panelColor: string\n  glowColor: string\n  radius: number\n  reduced: boolean\n}) {\n  const id = useId()\n  return (\n    <div\n      className=\"relative isolate flex flex-col overflow-hidden p-5 text-white @[720px]:p-7\"\n      style={{\n        borderRadius: radius,\n        backgroundColor: `color-mix(in oklab, ${panelColor} 70%, #020604)`,\n      }}\n    >\n      <motion.div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute -top-[55%] left-1/2 -z-10 h-[90%] w-[120%] rounded-full opacity-25\"\n        style={{ x: \"-50%\", backgroundColor: glowColor }}\n        animate={reduced ? undefined : { y: [0, 18, 0], scale: [1, 1.06, 1] }}\n        transition={{ duration: 9, repeat: Infinity, ease: \"easeInOut\" }}\n      />\n      <motion.div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute -bottom-[30%] -left-[20%] -z-10 h-[55%] w-[60%] rounded-full opacity-15\"\n        style={{ backgroundColor: glowColor }}\n        animate={reduced ? undefined : { x: [0, 24, 0], y: [0, -12, 0] }}\n        transition={{ duration: 12, repeat: Infinity, ease: \"easeInOut\" }}\n      />\n\n      {logo && <div className=\"flex justify-center\">{logo}</div>}\n\n      <div className=\"flex flex-1 flex-col items-center justify-center gap-3 py-8 text-center @[720px]:py-12\">\n        <h3 className=\"max-w-[14ch] text-3xl leading-[1.05] font-semibold tracking-tight text-balance @[720px]:text-[2.6rem]\">{headline}</h3>\n        <p className=\"max-w-xs text-sm text-white/65\">{description}</p>\n      </div>\n\n      <ol aria-label=\"Registration steps\" className=\"grid grid-cols-3 gap-2 @[720px]:gap-2.5\">\n        {steps.slice(0, 3).map((label, i) => {\n          const isActive = i === active\n          const done = i < active\n          return (\n            <li\n              key={i}\n              aria-current={isActive ? \"step\" : undefined}\n              className=\"relative flex min-h-[6.5rem] flex-col justify-between gap-4 rounded-2xl border border-white/10 bg-white/[0.07] p-3 backdrop-blur-md @[720px]:min-h-[8rem] @[720px]:p-4\"\n            >\n              {isActive && (\n                <motion.span\n                  layoutId={`${id}-active`}\n                  aria-hidden=\"true\"\n                  className=\"absolute -inset-px bg-white shadow-lg\"\n                  style={{ borderRadius: 16 }}\n                  transition={reduced ? INSTANT : SPRING}\n                />\n              )}\n              <span\n                className={cn(\n                  \"relative grid size-6 place-items-center rounded-full text-xs font-semibold transition-colors duration-300 @[720px]:size-7\",\n                  isActive ? \"bg-black text-white\" : done ? \"bg-white text-black\" : \"bg-white/15 text-white\"\n                )}\n              >\n                {done ? <Check className=\"size-3.5\" strokeWidth={3} aria-label=\"Done\" /> : i + 1}\n              </span>\n              <span\n                className={cn(\n                  \"relative text-xs leading-snug font-medium transition-colors duration-300 @[720px]:text-sm\",\n                  isActive ? \"text-black\" : \"text-white/85\"\n                )}\n              >\n                {label}\n              </span>\n            </li>\n          )\n        })}\n      </ol>\n    </div>\n  )\n}\n\n/* ------------------------------------------------------------------ */\n/* Card                                                               */\n/* ------------------------------------------------------------------ */\n\nfunction TiltCard({ radius, accent, reduced, children }: { radius: number; accent: string; reduced: boolean; children: ReactNode }) {\n  const rx = useSpring(0, { stiffness: 400, damping: 32 })\n  const ry = useSpring(0, { stiffness: 400, damping: 32 })\n  const [hover, setHover] = useState(false)\n\n  return (\n    <div className=\"w-full max-w-[400px]\" style={{ perspective: 1200 }}>\n      <motion.div\n        className=\"relative border bg-card p-6 text-card-foreground shadow-sm @sm:p-8\"\n        style={{ rotateX: rx, rotateY: ry, borderRadius: radius }}\n        onPointerMove={(e) => {\n          if (e.pointerType !== \"mouse\") return\n          const r = e.currentTarget.getBoundingClientRect()\n          const px = (e.clientX - r.left) / r.width\n          const py = (e.clientY - r.top) / r.height\n          if (!reduced) {\n            rx.set((0.5 - py) * 4)\n            ry.set((px - 0.5) * 4)\n          }\n        }}\n        onPointerEnter={(e) => e.pointerType === \"mouse\" && setHover(true)}\n        onPointerLeave={() => {\n          setHover(false)\n          rx.set(0)\n          ry.set(0)\n        }}\n      >\n        <motion.div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute -inset-px rounded-[inherit] border\"\n          style={{ borderColor: accent }}\n          initial={false}\n          animate={{ opacity: hover ? 1 : 0 }}\n          transition={{ duration: 0.3 }}\n        />\n        <motion.div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 rounded-[inherit]\"\n          style={{ backgroundColor: `color-mix(in oklab, ${accent} 4%, transparent)` }}\n          initial={false}\n          animate={{ opacity: hover ? 1 : 0 }}\n          transition={{ duration: 0.3 }}\n        />\n        <div className=\"relative\">{children}</div>\n      </motion.div>\n    </div>\n  )\n}\n\n/* ------------------------------------------------------------------ */\n/* Steps                                                              */\n/* ------------------------------------------------------------------ */\n\ntype StepKey = \"email\" | \"verify\" | \"profile\" | \"done\"\nconst STEP_LABELS: Record<Exclude<StepKey, \"done\">, string> = { email: \"Email\", verify: \"Verify\", profile: \"Profile\" }\n\nconst slide: Variants = {\n  enter: (d: number) => ({ x: `${d * 35}%`, opacity: 0 }),\n  center: { x: 0, opacity: 1 },\n  exit: (d: number) => ({ x: `${d * -35}%`, opacity: 0 }),\n}\n\nfunction useHeight() {\n  const [el, setEl] = useState<HTMLDivElement | null>(null)\n  const [height, setHeight] = useState<number | \"auto\">(\"auto\")\n  useEffect(() => {\n    if (!el) return\n    const ro = new ResizeObserver(([entry]) => setHeight(entry.borderBoxSize?.[0]?.blockSize ?? entry.target.getBoundingClientRect().height))\n    ro.observe(el)\n    return () => ro.disconnect()\n  }, [el])\n  return [setEl, height] as const\n}\n\ninterface StepsAuthProps {\n  mode: AuthMode\n  onModeSwitch: (mode: AuthMode) => void\n  title: string\n  subtitle: string\n  logo?: ReactNode\n  providers: AuthProvider[]\n  onProviderClick?: (provider: AuthProvider) => void\n  onSubmit?: (values: AuthValues) => Promise<void> | void\n  verifyCode?: (code: string, email: string) => Promise<boolean> | boolean\n  onResendCode?: (email: string) => void\n  roles: string[]\n  radius: number\n  termsHref?: string\n  privacyHref?: string\n  reduced: boolean\n}\n\nfunction StepsAuth({\n  mode,\n  onModeSwitch,\n  title,\n  subtitle,\n  logo,\n  providers,\n  onProviderClick,\n  onSubmit,\n  verifyCode,\n  onResendCode,\n  roles,\n  radius,\n  termsHref,\n  privacyHref,\n  reduced,\n}: StepsAuthProps) {\n  const id = useId()\n  const [step, setStep] = useState<StepKey>(\"email\")\n  const [dir, setDir] = useState(1)\n  const [email, setEmail] = useState(\"\")\n  const [emailError, setEmailError] = useState<string>()\n  const [code, setCode] = useState(\"\")\n  const [name, setName] = useState(\"\")\n  const [nameError, setNameError] = useState<string>()\n  const [role, setRole] = useState(roles[0] ?? \"\")\n  const [status, setStatus] = useState<Status>(\"idle\")\n  const [formError, setFormError] = useState<string | null>(null)\n  const [resent, setResent] = useState(false)\n  const [otpKey, setOtpKey] = useState(0)\n  const [prevMode, setPrevMode] = useState(mode)\n  const [scope, shake] = useShake(reduced)\n  const [measure, height] = useHeight()\n  const bodyRef = useRef<HTMLDivElement>(null)\n  const movedRef = useRef(false)\n\n  const flow: Exclude<StepKey, \"done\">[] = mode === \"sign-up\" ? [\"email\", \"verify\", \"profile\"] : [\"email\", \"verify\"]\n  const index = step === \"done\" ? flow.length : flow.indexOf(step as Exclude<StepKey, \"done\">)\n\n  if (mode !== prevMode) {\n    setPrevMode(mode)\n    setStep(\"email\")\n    setStatus(\"idle\")\n    setFormError(null)\n    setEmailError(undefined)\n  }\n\n  // Move focus into the step that just slid in (never on first mount)\n  useEffect(() => {\n    if (!movedRef.current) return\n    const timer = setTimeout(() => {\n      const root = bodyRef.current?.querySelector<HTMLElement>(`[data-step=\"${step}\"]`)\n      root?.querySelector<HTMLElement>(\"[data-autofocus], input\")?.focus({ preventScroll: true })\n    }, reduced ? 0 : 120)\n    return () => clearTimeout(timer)\n  }, [step, reduced])\n\n  const go = (next: StepKey) => {\n    movedRef.current = true\n    const ni = next === \"done\" ? flow.length : flow.indexOf(next)\n    setDir(ni >= index ? 1 : -1)\n    setStatus(\"idle\")\n    setFormError(null)\n    setStep(next)\n  }\n\n  const submitEmail = (e: FormEvent<HTMLFormElement>) => {\n    e.preventDefault()\n    const err = validateField(\"email\", email, mode)\n    setEmailError(err)\n    if (err) {\n      shake()\n      document.getElementById(`${id}-email`)?.focus()\n      return\n    }\n    setCode(\"\")\n    setOtpKey((k) => k + 1)\n    go(\"verify\")\n  }\n\n  const verify = async (c: string) => {\n    setFormError(null)\n    let ok = false\n    try {\n      ok = verifyCode ? await verifyCode(c, email.trim()) : true\n    } catch {\n      ok = false\n    }\n    if (!ok) {\n      setFormError(\"That code didn't match. Try again.\")\n      return false\n    }\n    if (mode === \"sign-in\") {\n      try {\n        await onSubmit?.({ mode, email: email.trim(), code: c })\n      } catch (err) {\n        setFormError(errorMessage(err))\n        return false\n      }\n    }\n    setTimeout(() => go(mode === \"sign-up\" ? \"profile\" : \"done\"), reduced ? 300 : 1300)\n    return true\n  }\n\n  const submitProfile = async (e: FormEvent<HTMLFormElement>) => {\n    e.preventDefault()\n    if (status === \"loading\") return\n    const err = validateField(\"name\", name, mode)\n    setNameError(err)\n    setFormError(null)\n    if (err) {\n      shake()\n      document.getElementById(`${id}-name`)?.focus()\n      return\n    }\n    setStatus(\"loading\")\n    try {\n      await onSubmit?.({ mode, email: email.trim(), code, name: name.trim(), role })\n      setStatus(\"success\")\n      setTimeout(() => go(\"done\"), reduced ? 200 : 700)\n    } catch (error) {\n      setStatus(\"idle\")\n      setFormError(errorMessage(error))\n      shake()\n    }\n  }\n\n  const restart = () => {\n    setEmail(\"\")\n    setName(\"\")\n    setCode(\"\")\n    setRole(roles[0] ?? \"\")\n    go(\"email\")\n  }\n\n  const onRoleKey = (e: KeyboardEvent<HTMLDivElement>) => {\n    const keys = [\"ArrowRight\", \"ArrowDown\", \"ArrowLeft\", \"ArrowUp\"]\n    if (!keys.includes(e.key) || !roles.length) return\n    e.preventDefault()\n    const delta = e.key === \"ArrowRight\" || e.key === \"ArrowDown\" ? 1 : -1\n    const next = (roles.indexOf(role) + delta + roles.length) % roles.length\n    setRole(roles[next])\n    e.currentTarget.querySelectorAll<HTMLButtonElement>(\"[role=radio]\")[next]?.focus()\n  }\n\n  const t = (tr: Transition): Transition => (reduced ? INSTANT : tr)\n  const inputCls = ACCENT_FOCUS\n  const firstName = name.trim().split(/\\s+/)[0]\n\n  let body: ReactNode\n  if (step === \"email\") {\n    body = (\n      <form noValidate onSubmit={submitEmail} className=\"flex flex-col\">\n        <div className=\"flex flex-col gap-1.5\">\n          <SwapText text={title} reduced={reduced} className=\"text-xl font-semibold tracking-tight text-foreground\" />\n          <SwapText text={subtitle} as=\"p\" reduced={reduced} className=\"text-sm text-muted-foreground\" />\n        </div>\n        {providers.length > 0 && (\n          <div className=\"mt-6 flex flex-col gap-5\">\n            <ProviderButtons providers={providers} onProviderClick={onProviderClick} iconOnly={providers.length > 2} />\n            <Divider label=\"or\" />\n          </div>\n        )}\n        <div className={cn(\"pb-4\", providers.length ? \"mt-5\" : \"mt-6\")}>\n          <Field id={`${id}-email`} label=\"Email\" error={emailError} reduced={reduced}>\n            <Input\n              id={`${id}-email`}\n              name=\"email\"\n              type=\"email\"\n              inputMode=\"email\"\n              autoComplete=\"email\"\n              placeholder=\"you@example.com\"\n              value={email}\n              onChange={(e) => {\n                setEmail(e.target.value)\n                if (emailError) setEmailError(validateField(\"email\", e.target.value, mode))\n              }}\n              aria-invalid={emailError ? true : undefined}\n              aria-describedby={`${id}-email-msg`}\n              className={inputCls}\n            />\n          </Field>\n        </div>\n        <SubmitButton status=\"idle\" reduced={reduced} className=\"text-white hover:opacity-90\" style={{ backgroundColor: \"var(--auth-accent)\" }}>\n          Continue <ArrowRight className=\"size-4\" aria-hidden=\"true\" />\n        </SubmitButton>\n        <ModeSwitch mode={mode} onSwitch={onModeSwitch} className=\"mt-5\" />\n        {mode === \"sign-up\" && <Terms termsHref={termsHref} privacyHref={privacyHref} className=\"mt-3\" />}\n      </form>\n    )\n  } else if (step === \"verify\") {\n    body = (\n      <div className=\"flex flex-col items-center text-center\">\n        <button\n          type=\"button\"\n          onClick={() => go(\"email\")}\n          className=\"mb-4 inline-flex items-center gap-1.5 self-start rounded-sm text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-[var(--auth-accent)]\"\n        >\n          <ArrowLeft className=\"size-3.5\" aria-hidden=\"true\" /> Change email\n        </button>\n        <h2 className=\"text-xl font-semibold tracking-tight text-foreground\">Check your inbox</h2>\n        <p className=\"mt-1.5 text-sm text-muted-foreground\">\n          We sent a 6-digit code to <span className=\"font-medium break-all text-foreground\">{email.trim()}</span>\n        </p>\n        <div className=\"mt-6 origin-center @max-sm:scale-[0.85]\">\n          <OtpInput\n            key={otpKey}\n            length={6}\n            verify={verify}\n            onChange={(v) => {\n              if (v.length > code.length) setFormError(null)\n              setCode(v)\n            }}\n          />\n        </div>\n        <div className=\"w-full\">\n          <Collapse show={!!formError} reduced={reduced}>\n            <p role=\"alert\" className=\"pt-4 text-sm text-destructive\">\n              {formError}\n            </p>\n          </Collapse>\n        </div>\n        <p className=\"mt-5 text-xs text-muted-foreground\">\n          Didn&apos;t get it?{\" \"}\n          <button\n            type=\"button\"\n            onClick={() => {\n              onResendCode?.(email.trim())\n              setResent(true)\n              setTimeout(() => setResent(false), 2400)\n            }}\n            className=\"rounded-sm font-medium text-[var(--auth-accent)] outline-none underline-offset-4 hover:underline focus-visible:ring-2 focus-visible:ring-[var(--auth-accent)]\"\n          >\n            {resent ? \"Code sent\" : \"Resend code\"}\n          </button>\n        </p>\n      </div>\n    )\n  } else if (step === \"profile\") {\n    body = (\n      <form noValidate onSubmit={submitProfile} className=\"flex flex-col\">\n        <h2 className=\"text-xl font-semibold tracking-tight text-foreground\">Tell us about you</h2>\n        <p className=\"mt-1.5 text-sm text-muted-foreground\">This helps us tailor your workspace.</p>\n        <div className=\"mt-6 pb-4\">\n          <Field id={`${id}-name`} label=\"Full name\" error={nameError} reduced={reduced}>\n            <Input\n              id={`${id}-name`}\n              name=\"name\"\n              autoComplete=\"name\"\n              placeholder=\"Ada Lovelace\"\n              value={name}\n              onChange={(e) => {\n                setName(e.target.value)\n                if (nameError) setNameError(validateField(\"name\", e.target.value, mode))\n                if (formError) setFormError(null)\n              }}\n              aria-invalid={nameError ? true : undefined}\n              aria-describedby={`${id}-name-msg`}\n              className={inputCls}\n            />\n          </Field>\n        </div>\n        {roles.length > 0 && (\n          <div className=\"pb-4\">\n            <p id={`${id}-role`} className=\"mb-2 text-sm font-medium\">\n              Your role\n            </p>\n            <div role=\"radiogroup\" aria-labelledby={`${id}-role`} onKeyDown={onRoleKey} className=\"grid grid-cols-2 gap-1 rounded-xl bg-muted p-1 @sm:grid-cols-4\">\n              {roles.map((r) => {\n                const checked = r === role\n                return (\n                  <button\n                    key={r}\n                    type=\"button\"\n                    role=\"radio\"\n                    aria-checked={checked}\n                    tabIndex={checked ? 0 : -1}\n                    onClick={() => setRole(r)}\n                    className={cn(\n                      \"relative h-8 rounded-lg px-2 text-xs font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-[var(--auth-accent)]\",\n                      checked ? \"text-foreground\" : \"text-muted-foreground hover:text-foreground\"\n                    )}\n                  >\n                    {checked && (\n                      <motion.span\n                        layoutId={`${id}-role-pill`}\n                        aria-hidden=\"true\"\n                        className=\"absolute inset-0 bg-background shadow-sm\"\n                        style={{ borderRadius: 8 }}\n                        transition={t(SPRING)}\n                      />\n                    )}\n                    <span className=\"relative\">{r}</span>\n                  </button>\n                )\n              })}\n            </div>\n          </div>\n        )}\n        <Collapse show={!!formError} reduced={reduced}>\n          <div className=\"pb-4\">\n            <p role=\"alert\" className=\"rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive\">\n              {formError}\n            </p>\n          </div>\n        </Collapse>\n        <SubmitButton status={status} reduced={reduced} className=\"text-white hover:opacity-90\" style={{ backgroundColor: \"var(--auth-accent)\" }}>\n          Create account\n        </SubmitButton>\n      </form>\n    )\n  } else {\n    body = (\n      <div className=\"flex flex-col items-center py-4 text-center\">\n        <motion.div\n          className=\"grid size-16 place-items-center rounded-full text-white\"\n          style={{ backgroundColor: \"var(--auth-accent)\", boxShadow: \"0 0 0 8px color-mix(in oklab, var(--auth-accent) 15%, transparent)\" }}\n          initial={{ scale: 0.4, opacity: 0 }}\n          animate={{ scale: 1, opacity: 1 }}\n          transition={t({ type: \"spring\", stiffness: 420, damping: 30, delay: 0.1 })}\n        >\n          <svg viewBox=\"0 0 24 24\" className=\"size-7\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.6} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n            <motion.path d=\"M5 12.5 10 17.5 19 7\" initial={{ pathLength: 0 }} animate={{ pathLength: 1 }} transition={t({ duration: 0.4, ease: EASE_OUT, delay: 0.3 })} />\n          </svg>\n        </motion.div>\n        <h2 data-autofocus tabIndex={-1} className=\"mt-6 text-xl font-semibold tracking-tight text-foreground outline-none\">\n          You&apos;re all set\n        </h2>\n        <p className=\"mt-1.5 text-sm text-muted-foreground\">\n          {mode === \"sign-up\" ? `Welcome aboard${firstName ? `, ${firstName}` : \"\"}. Your workspace is ready.` : `Signed in as ${email.trim()}.`}\n        </p>\n        <Button type=\"button\" variant=\"outline\" onClick={restart} className=\"mt-6 h-9 rounded-lg\">\n          Start over\n        </Button>\n      </div>\n    )\n  }\n\n  return (\n    <div className=\"w-full max-w-md border bg-card p-6 text-card-foreground shadow-sm @sm:p-8\" style={{ borderRadius: radius }}>\n      <div className=\"mb-6\">\n        <div className=\"mb-4 flex items-center justify-between gap-3\">\n          {logo ?? <span />}\n          <span className=\"text-xs text-muted-foreground tabular-nums\" aria-live=\"polite\">\n            {step === \"done\" ? \"Complete\" : `Step ${index + 1} of ${flow.length}`}\n          </span>\n        </div>\n        <div\n          className=\"flex gap-1.5\"\n          role=\"progressbar\"\n          aria-label={mode === \"sign-up\" ? \"Sign-up progress\" : \"Sign-in progress\"}\n          aria-valuemin={0}\n          aria-valuemax={flow.length}\n          aria-valuenow={index}\n        >\n          {flow.map((k, i) => (\n            <div key={k} className=\"h-1 flex-1 overflow-hidden rounded-full bg-muted\">\n              <motion.div\n                className=\"h-full origin-left rounded-full\"\n                style={{ backgroundColor: \"var(--auth-accent)\" }}\n                initial={false}\n                animate={{ scaleX: i < index ? 1 : i === index ? 0.4 : 0 }}\n                transition={t({ duration: 0.5, ease: EASE_IN_OUT })}\n              />\n            </div>\n          ))}\n        </div>\n        <div className=\"mt-2 flex gap-1.5\">\n          {flow.map((k, i) => (\n            <span key={k} className={cn(\"flex-1 text-[11px] transition-colors duration-300\", i <= index ? \"text-foreground\" : \"text-muted-foreground\")}>\n              {STEP_LABELS[k]}\n            </span>\n          ))}\n        </div>\n      </div>\n\n      <motion.div\n        className=\"-mx-1 overflow-hidden px-1\"\n        initial={false}\n        animate={{ height: height === \"auto\" ? \"auto\" : height + 8 }}\n        transition={t({ duration: 0.45, ease: EASE_IN_OUT })}\n      >\n        <div ref={measure} className=\"relative pt-1 pb-1\">\n          <div ref={bodyRef}>\n            <div ref={scope}>\n              <AnimatePresence mode=\"popLayout\" initial={false} custom={dir}>\n                <motion.div\n                  key={step}\n                  data-step={step}\n                  custom={dir}\n                  variants={slide}\n                  initial=\"enter\"\n                  animate=\"center\"\n                  exit=\"exit\"\n                  transition={reduced ? INSTANT : { x: { type: \"spring\", stiffness: 420, damping: 36 }, opacity: { duration: 0.22 } }}\n                >\n                  {body}\n                </motion.div>\n              </AnimatePresence>\n            </div>\n          </div>\n        </div>\n      </motion.div>\n    </div>\n  )\n}\n\n/* ------------------------------------------------------------------ */\n/* AuthSection                                                         */\n/* ------------------------------------------------------------------ */\n\nexport function AuthSection({\n  variant = \"split\",\n  mode: modeProp,\n  defaultMode = \"sign-up\",\n  onModeChange,\n  title,\n  subtitle,\n  headline = \"Get Started with Us\",\n  description = \"Complete these easy steps to register your account.\",\n  providers = DEFAULT_PROVIDERS,\n  onSubmit,\n  onProviderClick,\n  onForgotPassword,\n  forgotPasswordHref,\n  logo,\n  accent = \"#ff4d12\",\n  panelColor = \"#0f3d2e\",\n  glowColor = \"#2f8f73\",\n  radius = 24,\n  steps = DEFAULT_STEPS,\n  showRememberMe = true,\n  magicLink = true,\n  verifyCode,\n  onResendCode,\n  roles = DEFAULT_ROLES,\n  termsHref,\n  privacyHref,\n  className,\n}: AuthSectionProps) {\n  const reduced = useReducedMotion()\n  const [mode, setMode] = useMode(modeProp, defaultMode, onModeChange)\n  const [progress, setProgress] = useState(0)\n  const [prevMode, setPrevMode] = useState(mode)\n  if (mode !== prevMode) {\n    setPrevMode(mode)\n    setProgress(0)\n  }\n\n  const [defTitle, defSubtitle] = COPY[variant][mode]\n  const heading = pick(title, mode, defTitle)\n  const sub = pick(subtitle, mode, defSubtitle)\n  const vars = { \"--auth-accent\": accent } as CSSProperties\n\n  const formProps = {\n    mode,\n    onModeSwitch: setMode,\n    title: heading,\n    subtitle: sub,\n    providers,\n    onProviderClick,\n    onSubmit,\n    onForgotPassword,\n    forgotPasswordHref,\n    showRememberMe,\n    termsHref,\n    privacyHref,\n    reduced,\n  }\n\n  if (variant === \"card\") {\n    return (\n      <div className={cn(\"@container grid w-full place-items-center\", className)} style={vars}>\n        <TiltCard radius={radius} accent={accent} reduced={reduced}>\n          <CredentialsForm {...formProps} appearance=\"card\" logo={logo} magicLink={magicLink} />\n        </TiltCard>\n      </div>\n    )\n  }\n\n  if (variant === \"steps\") {\n    return (\n      <div className={cn(\"@container grid w-full place-items-center\", className)} style={vars}>\n        <StepsAuth\n          mode={mode}\n          onModeSwitch={setMode}\n          title={heading}\n          subtitle={sub}\n          logo={logo}\n          providers={providers}\n          onProviderClick={onProviderClick}\n          onSubmit={onSubmit}\n          verifyCode={verifyCode}\n          onResendCode={onResendCode}\n          roles={roles}\n          radius={radius}\n          termsHref={termsHref}\n          privacyHref={privacyHref}\n          reduced={reduced}\n        />\n      </div>\n    )\n  }\n\n  const inner = Math.max(radius - 8, 0)\n  return (\n    <div className={cn(\"@container w-full\", className)} style={vars}>\n      <div\n        className=\"dark grid gap-2 bg-black p-2 text-foreground @[720px]:min-h-[560px] @[720px]:grid-cols-2\"\n        style={{ borderRadius: radius, colorScheme: \"dark\" }}\n      >\n        <SplitPanel\n          headline={headline}\n          description={description}\n          steps={steps}\n          active={progress}\n          logo={logo}\n          panelColor={panelColor}\n          glowColor={glowColor}\n          radius={inner}\n          reduced={reduced}\n        />\n        <div className=\"flex items-center justify-center px-4 py-8 @[720px]:px-10 @[720px]:py-10\">\n          <div className=\"w-full max-w-[380px]\">\n            <CredentialsForm {...formProps} appearance=\"split\" magicLink={false} onSuccess={() => setProgress(1)} />\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/auth",
  "type": "registry:component"
}