{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "two-factor",
  "title": "Two-Factor",
  "description": "Two-factor plugin: the post-password challenge for authenticator, emailed, and backup codes, plus the settings card for enrollment and backup-code management.",
  "dependencies": [
    "@better-auth-ui/core@latest",
    "@better-auth-ui/react@latest",
    "@tanstack/react-form",
    "@tanstack/react-query",
    "cn",
    "date-fns",
    "input-otp",
    "lucide-react"
  ],
  "registryDependencies": [
    "alert-dialog",
    "button",
    "calendar",
    "card",
    "checkbox",
    "combobox",
    "dialog",
    "empty",
    "field",
    "https://better-auth-ui.com/r/radix-nova/sign-in.json",
    "https://better-auth-ui.com/r/radix-nova/username.json",
    "input",
    "input-group",
    "input-otp",
    "popover",
    "select",
    "skeleton",
    "slider",
    "sonner",
    "spinner",
    "switch",
    "tabs",
    "textarea"
  ],
  "files": [
    {
      "path": "examples/start-shadcn-example/src/components/auth/additional-field.tsx",
      "content": "\"use client\"\n\nimport {\n  type AdditionalField as AdditionalFieldConfig,\n  type AdditionalFieldFormValue,\n  getFormFieldErrors,\n  resolveInputType\n} from \"@better-auth-ui/core\"\nimport { useAuth, useCopyToClipboard } from \"@better-auth-ui/react\"\nimport { format } from \"date-fns\"\nimport { CalendarIcon, Check, ChevronDownIcon, Copy } from \"lucide-react\"\nimport { type ComponentType, useRef, useState } from \"react\"\nimport { toast } from \"sonner\"\n\nimport { buttonVariants } from \"@/components/ui/button\"\nimport { Calendar } from \"@/components/ui/calendar\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport {\n  Combobox,\n  ComboboxContent,\n  ComboboxEmpty,\n  ComboboxInput,\n  ComboboxItem,\n  ComboboxList\n} from \"@/components/ui/combobox\"\nimport {\n  Field,\n  FieldContent,\n  FieldError,\n  FieldLabel\n} from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput\n} from \"@/components/ui/input-group\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger\n} from \"@/components/ui/popover\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue\n} from \"@/components/ui/select\"\nimport { Slider } from \"@/components/ui/slider\"\nimport { Switch } from \"@/components/ui/switch\"\nimport { Textarea } from \"@/components/ui/textarea\"\nimport { cn } from \"cn\"\n\nexport type AdditionalFieldProps = {\n  name: string\n  field: AdditionalFieldConfig\n  value: AdditionalFieldFormValue\n  onBlur: () => void\n  onChange: (value: AdditionalFieldFormValue) => void\n  isInvalid?: boolean\n  errors?: unknown[]\n  isPending?: boolean\n  /** Complete suffix appended to labels for fields that are not required. */\n  optionalLabel?: string\n}\n\nfunction valueToString(value: AdditionalFieldFormValue) {\n  if (value == null) return \"\"\n  return value instanceof Date ? value.toISOString() : String(value)\n}\n\n/** Convert a `defaultValue` into a `Date` for the calendar. */\nfunction toDate(value: unknown): Date | undefined {\n  if (value instanceof Date) return value\n  if (typeof value === \"string\") {\n    const parsed = new Date(value)\n    return Number.isNaN(parsed.getTime()) ? undefined : parsed\n  }\n  return undefined\n}\n\n/** Format a Date as `HH:mm:ss` for an `<input type=\"time\">`. */\nfunction formatTime(date: Date) {\n  const pad = (n: number) => n.toString().padStart(2, \"0\")\n  return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`\n}\n\n/**\n * Icon-only copy button used as an `InputGroupAddon`. `getValue` is invoked\n * lazily on click so the button copies the input's *live* value rather than a\n * stale snapshot — important when paired with editable inputs.\n */\nfunction CopyButton({\n  getValue,\n  isDisabled\n}: {\n  getValue: () => string | undefined\n  isDisabled?: boolean\n}) {\n  const { localization } = useAuth()\n  const { copied, copy } = useCopyToClipboard({\n    onError: (error) => {\n      console.error(\"[Better Auth UI] Copy failed\", error)\n      toast.error(localization.errors.copyFailed)\n    }\n  })\n\n  async function handleCopy() {\n    const value = getValue()\n    if (!value) return\n\n    await copy(value)\n  }\n\n  return (\n    <InputGroupButton\n      aria-label={\n        copied\n          ? localization.settings.copiedToClipboard\n          : localization.settings.copyToClipboard\n      }\n      title={\n        copied\n          ? localization.settings.copiedToClipboard\n          : localization.settings.copyToClipboard\n      }\n      onClick={handleCopy}\n      disabled={isDisabled}\n    >\n      {copied ? <Check /> : <Copy />}\n    </InputGroupButton>\n  )\n}\n\n/** Renders a single additional user field via shadcn primitives. */\nexport function AdditionalField({\n  name,\n  field: configuredField,\n  value,\n  onBlur,\n  onChange,\n  isInvalid,\n  errors,\n  isPending,\n  optionalLabel\n}: AdditionalFieldProps) {\n  const field =\n    optionalLabel && !configuredField.required\n      ? {\n          ...configuredField,\n          label: (\n            <>\n              {configuredField.label}\n              {optionalLabel}\n            </>\n          )\n        }\n      : configuredField\n  const inputType = resolveInputType(field)\n  const fieldErrors = getFormFieldErrors(errors ?? [])\n\n  if (field.render) {\n    const FieldRenderer = field.render as ComponentType<AdditionalFieldProps>\n    return (\n      <FieldRenderer\n        name={name}\n        field={field}\n        value={value}\n        onBlur={onBlur}\n        onChange={onChange}\n        isInvalid={isInvalid}\n        errors={errors}\n        isPending={isPending}\n        optionalLabel={optionalLabel}\n      />\n    )\n  }\n\n  if (inputType === \"hidden\") {\n    return (\n      <input type=\"hidden\" name={name} value={valueToString(value)} readOnly />\n    )\n  }\n\n  if (inputType === \"textarea\") {\n    return (\n      <Field data-invalid={isInvalid}>\n        <FieldLabel htmlFor={name}>{field.label}</FieldLabel>\n\n        <Textarea\n          id={name}\n          name={name}\n          value={valueToString(value)}\n          onBlur={onBlur}\n          onChange={(event) => onChange(event.target.value || null)}\n          placeholder={field.placeholder}\n          required={field.required}\n          readOnly={field.readOnly}\n          disabled={isPending}\n          aria-invalid={isInvalid}\n        />\n\n        <FieldError errors={fieldErrors} />\n      </Field>\n    )\n  }\n\n  if (inputType === \"number\") {\n    const maxFractionDigits = field.formatOptions?.maximumFractionDigits\n\n    return (\n      <Field data-invalid={isInvalid}>\n        <FieldLabel htmlFor={name}>{field.label}</FieldLabel>\n\n        <Input\n          id={name}\n          name={name}\n          type=\"number\"\n          inputMode={maxFractionDigits ? \"decimal\" : \"numeric\"}\n          min={field.min}\n          max={field.max}\n          step={\n            field.step ??\n            (maxFractionDigits ? 1 / 10 ** maxFractionDigits : undefined)\n          }\n          value={typeof value === \"number\" ? value : \"\"}\n          onBlur={onBlur}\n          onChange={(event) =>\n            onChange(\n              event.target.value === \"\" ? null : event.target.valueAsNumber\n            )\n          }\n          placeholder={field.placeholder}\n          required={field.required}\n          readOnly={field.readOnly}\n          disabled={isPending}\n          aria-invalid={isInvalid}\n        />\n\n        <FieldError errors={fieldErrors} />\n      </Field>\n    )\n  }\n\n  if (inputType === \"slider\") {\n    return (\n      <SliderField\n        name={name}\n        field={field}\n        value={value}\n        onBlur={onBlur}\n        onChange={onChange}\n        isInvalid={isInvalid}\n        errors={errors}\n        isPending={isPending}\n      />\n    )\n  }\n\n  if (inputType === \"switch\") {\n    return (\n      <Field data-invalid={isInvalid} orientation=\"horizontal\">\n        <Switch\n          id={name}\n          name={name}\n          checked={value === true}\n          onBlur={onBlur}\n          onCheckedChange={onChange}\n          disabled={isPending || field.readOnly}\n          aria-invalid={isInvalid}\n        />\n\n        <FieldContent>\n          <FieldLabel htmlFor={name}>{field.label}</FieldLabel>\n        </FieldContent>\n        <FieldError errors={fieldErrors} />\n      </Field>\n    )\n  }\n\n  if (inputType === \"checkbox\") {\n    return (\n      <Field data-invalid={isInvalid} orientation=\"horizontal\">\n        <Checkbox\n          id={name}\n          name={name}\n          checked={value === true}\n          onBlur={onBlur}\n          onCheckedChange={(checked) => onChange(checked === true)}\n          required={field.required}\n          disabled={isPending || field.readOnly}\n          aria-invalid={isInvalid}\n        />\n\n        <FieldContent>\n          <FieldLabel htmlFor={name}>{field.label}</FieldLabel>\n        </FieldContent>\n        <FieldError errors={fieldErrors} />\n      </Field>\n    )\n  }\n\n  if (inputType === \"select\") {\n    return (\n      <Field data-invalid={isInvalid}>\n        <FieldLabel htmlFor={name}>{field.label}</FieldLabel>\n\n        <Select\n          name={name}\n          value={valueToString(value) || undefined}\n          onValueChange={(nextValue) => onChange(nextValue)}\n          required={field.required}\n          disabled={isPending || field.readOnly}\n        >\n          <SelectTrigger\n            id={name}\n            className=\"w-full\"\n            onBlur={onBlur}\n            aria-invalid={isInvalid}\n          >\n            <SelectValue placeholder={field.placeholder} />\n          </SelectTrigger>\n\n          <SelectContent>\n            {field.options?.map((option) => (\n              <SelectItem key={option.value} value={option.value}>\n                {option.label}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n\n        <FieldError errors={fieldErrors} />\n      </Field>\n    )\n  }\n\n  if (inputType === \"combobox\") {\n    const selectedOption = field.options?.find(\n      (option) => option.value === valueToString(value)\n    )\n\n    return (\n      <Field data-invalid={isInvalid}>\n        <FieldLabel htmlFor={name}>{field.label}</FieldLabel>\n\n        <Combobox\n          items={field.options ?? []}\n          name={name}\n          value={selectedOption ?? null}\n          onValueChange={(option) => onChange(option?.value ?? null)}\n          required={field.required}\n          disabled={isPending || field.readOnly}\n        >\n          <ComboboxInput\n            placeholder={field.placeholder}\n            id={name}\n            onBlur={onBlur}\n            aria-invalid={isInvalid}\n          />\n\n          <ComboboxContent>\n            <ComboboxEmpty>No items found.</ComboboxEmpty>\n\n            <ComboboxList>\n              {(option) => (\n                <ComboboxItem key={option.value} value={option}>\n                  {option.label}\n                </ComboboxItem>\n              )}\n            </ComboboxList>\n          </ComboboxContent>\n        </Combobox>\n\n        <FieldError errors={fieldErrors} />\n      </Field>\n    )\n  }\n\n  if (inputType === \"date\" || inputType === \"datetime\") {\n    return (\n      <DateInput\n        name={name}\n        field={field}\n        value={value}\n        onBlur={onBlur}\n        onChange={onChange}\n        isInvalid={isInvalid}\n        errors={errors}\n        isPending={isPending}\n      />\n    )\n  }\n\n  return (\n    <InputField\n      name={name}\n      field={field}\n      value={value}\n      onBlur={onBlur}\n      onChange={onChange}\n      isInvalid={isInvalid}\n      errors={errors}\n      isPending={isPending}\n    />\n  )\n}\n\nfunction InputField({\n  name,\n  field,\n  value,\n  onBlur,\n  onChange,\n  isInvalid,\n  errors,\n  isPending\n}: AdditionalFieldProps) {\n  const inputRef = useRef<HTMLInputElement>(null)\n  const fieldErrors = getFormFieldErrors(errors ?? [])\n\n  const hasPrefix = field.prefix != null\n  const hasSuffix = field.suffix != null || field.copyable\n\n  const isNumeric = field.type === \"number\"\n  const maxFractionDigits = field.formatOptions?.maximumFractionDigits\n  const nativeInputType = isNumeric ? \"number\" : undefined\n  const nativeInputMode = isNumeric\n    ? maxFractionDigits\n      ? \"decimal\"\n      : \"numeric\"\n    : undefined\n  const nativeStep = maxFractionDigits ? 1 / 10 ** maxFractionDigits : undefined\n\n  if (hasPrefix || hasSuffix) {\n    return (\n      <Field data-invalid={isInvalid}>\n        <FieldLabel htmlFor={name}>{field.label}</FieldLabel>\n\n        <InputGroup>\n          {hasPrefix && (\n            <InputGroupAddon align=\"inline-start\">\n              {field.prefix}\n            </InputGroupAddon>\n          )}\n\n          <InputGroupInput\n            ref={inputRef}\n            id={name}\n            name={name}\n            type={nativeInputType}\n            inputMode={nativeInputMode}\n            step={nativeStep}\n            value={valueToString(value)}\n            onBlur={onBlur}\n            onChange={(event) => onChange(event.target.value || null)}\n            placeholder={field.placeholder}\n            required={field.required}\n            readOnly={field.readOnly}\n            disabled={isPending}\n            aria-invalid={isInvalid}\n          />\n\n          {field.copyable ? (\n            <InputGroupAddon align=\"inline-end\">\n              <CopyButton\n                getValue={() => inputRef.current?.value}\n                isDisabled={isPending}\n              />\n            </InputGroupAddon>\n          ) : (\n            field.suffix != null && (\n              <InputGroupAddon align=\"inline-end\">\n                {field.suffix}\n              </InputGroupAddon>\n            )\n          )}\n        </InputGroup>\n\n        <FieldError errors={fieldErrors} />\n      </Field>\n    )\n  }\n\n  return (\n    <Field data-invalid={isInvalid}>\n      <FieldLabel htmlFor={name}>{field.label}</FieldLabel>\n\n      <Input\n        id={name}\n        name={name}\n        type={nativeInputType}\n        inputMode={nativeInputMode}\n        step={nativeStep}\n        value={valueToString(value)}\n        onBlur={onBlur}\n        onChange={(event) => onChange(event.target.value || null)}\n        placeholder={field.placeholder}\n        required={field.required}\n        readOnly={field.readOnly}\n        disabled={isPending}\n        aria-invalid={isInvalid}\n      />\n\n      <FieldError errors={fieldErrors} />\n    </Field>\n  )\n}\n\n/**\n * Slider field. Radix Slider doesn't render the current value, so we render\n * it next to the label and control the state to keep the displayed value in\n * sync. The selected value is submitted via the underlying Radix `name` prop.\n */\nfunction SliderField({\n  name,\n  field,\n  value,\n  onBlur,\n  onChange,\n  isInvalid,\n  errors,\n  isPending\n}: AdditionalFieldProps) {\n  const maxFractionDigits = field.formatOptions?.maximumFractionDigits\n  const min = field.min ?? 0\n  const max = field.max ?? 100\n  const step =\n    field.step ?? (maxFractionDigits ? 1 / 10 ** maxFractionDigits : 1)\n  const numericValue = typeof value === \"number\" ? value : min\n  const fieldErrors = getFormFieldErrors(errors ?? [])\n\n  const formatter = new Intl.NumberFormat(undefined, field.formatOptions)\n\n  return (\n    <Field data-invalid={isInvalid}>\n      <div className=\"flex items-center justify-between gap-2\">\n        <FieldLabel htmlFor={name}>{field.label}</FieldLabel>\n        <span className=\"text-sm text-muted-foreground tabular-nums\">\n          {formatter.format(numericValue)}\n        </span>\n      </div>\n\n      <Slider\n        id={name}\n        name={name}\n        value={[numericValue]}\n        onBlur={onBlur}\n        onValueChange={(nextValue) =>\n          onChange((Array.isArray(nextValue) ? nextValue[0] : nextValue) ?? min)\n        }\n        min={min}\n        max={max}\n        step={step}\n        disabled={isPending || field.readOnly}\n        aria-invalid={isInvalid}\n      />\n\n      <FieldError errors={fieldErrors} />\n    </Field>\n  )\n}\n\n/**\n * Date / datetime input. Composes `Popover` + `Calendar` for the date and\n * (optionally) `<input type=\"time\">` for the time. Submits the combined ISO\n * value via a hidden `<input>` so it shows up in `FormData`.\n */\nfunction DateInput({\n  name,\n  field,\n  value,\n  onBlur,\n  onChange,\n  isInvalid,\n  errors,\n  isPending\n}: AdditionalFieldProps) {\n  const { localization } = useAuth()\n  const inputType = resolveInputType(field)\n  const isDateTime = inputType === \"datetime\"\n  const fieldErrors = getFormFieldErrors(errors ?? [])\n\n  const date = toDate(value)\n  const [time, setTime] = useState<string>(\n    isDateTime && date ? formatTime(date) : \"\"\n  )\n  const [open, setOpen] = useState(false)\n\n  // Compose the hidden form value: ISO date for \"date\", ISO datetime for\n  // \"datetime\" (date + time).\n  return (\n    <Field data-invalid={isInvalid}>\n      <FieldLabel htmlFor={`${name}-date`}>{field.label}</FieldLabel>\n\n      <div className=\"relative flex gap-2\">\n        <Popover open={open} onOpenChange={setOpen}>\n          <PopoverTrigger\n            type=\"button\"\n            id={`${name}-date`}\n            data-empty={!date}\n            aria-invalid={isInvalid}\n            aria-required={field.required}\n            onBlur={onBlur}\n            disabled={isPending || field.readOnly}\n            className={cn(\n              buttonVariants({ variant: \"outline\" }),\n              \"flex-1 justify-between font-normal\",\n              \"data-[empty=true]:text-muted-foreground\"\n            )}\n          >\n            {date ? format(date, \"PPP\") : <span>{field.placeholder}</span>}\n\n            {isDateTime ? <ChevronDownIcon /> : <CalendarIcon />}\n          </PopoverTrigger>\n\n          <PopoverContent className=\"w-auto overflow-hidden p-0\" align=\"start\">\n            <Calendar\n              mode=\"single\"\n              selected={date}\n              defaultMonth={date}\n              captionLayout=\"dropdown\"\n              onSelect={(value) => {\n                if (!value) {\n                  onChange(null)\n                } else {\n                  const nextValue = new Date(value)\n                  if (isDateTime && time.trim()) {\n                    const [hours = \"0\", minutes = \"0\", seconds = \"0\"] =\n                      time.split(\":\")\n                    nextValue.setHours(\n                      Number(hours),\n                      Number(minutes),\n                      Number(seconds),\n                      0\n                    )\n                  } else {\n                    nextValue.setHours(0, 0, 0, 0)\n                  }\n                  onChange(nextValue)\n                }\n                if (!isDateTime) setOpen(false)\n              }}\n            />\n          </PopoverContent>\n        </Popover>\n\n        {isDateTime && (\n          <Field className=\"w-32\">\n            <FieldLabel htmlFor={`${name}-time`} className=\"sr-only\">\n              {localization.settings.time}\n            </FieldLabel>\n\n            <Input\n              type=\"time\"\n              id={`${name}-time`}\n              step=\"1\"\n              value={time}\n              onChange={(event) => {\n                const nextTime = event.target.value\n                setTime(nextTime)\n                if (!date) return\n                const nextValue = new Date(date)\n                const [hours = \"0\", minutes = \"0\", seconds = \"0\"] =\n                  nextTime.split(\":\")\n                nextValue.setHours(\n                  Number(hours),\n                  Number(minutes),\n                  Number(seconds),\n                  0\n                )\n                onChange(nextValue)\n              }}\n              disabled={isPending || field.readOnly}\n              className=\"appearance-none bg-background [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n            />\n          </Field>\n        )}\n      </div>\n\n      <FieldError errors={fieldErrors} />\n    </Field>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/additional-field.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/auth-form.tsx",
      "content": "\"use client\"\n\nimport {\n  type AdditionalField as AdditionalFieldConfig,\n  type AdditionalFieldFormValue,\n  DEFAULT_ADDITIONAL_FIELD_VALIDATION_DEBOUNCE_MS,\n  getFormFieldErrors,\n  normalizeAuthFormServerError,\n  validateAdditionalFieldRequired,\n  validateAdditionalFieldValue\n} from \"@better-auth-ui/core\"\nimport {\n  type AnyFormApi,\n  createFormHook,\n  createFormHookContexts\n} from \"@tanstack/react-form\"\nimport {\n  type ComponentProps,\n  type FormEvent,\n  type ReactNode,\n  useRef\n} from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Field,\n  FieldDescription,\n  FieldError,\n  FieldLabel\n} from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { AdditionalField, type AdditionalFieldProps } from \"./additional-field\"\n\nconst { fieldContext, formContext, useFieldContext, useFormContext } =\n  createFormHookContexts()\n\nconst DEFAULT_AUTH_FORM_SERVER_ERROR = \"Unable to submit this form. Try again.\"\n\nexport function focusFirstInvalidAuthFormControl(form: HTMLFormElement) {\n  requestAnimationFrame(() => {\n    form\n      .querySelector<HTMLElement>(\n        '[aria-invalid=\"true\"]:not([disabled]), :invalid:not([disabled])'\n      )\n      ?.focus()\n  })\n}\n\nfunction AuthFormFieldError() {\n  const field = useFieldContext<unknown>()\n  const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid\n\n  if (!isInvalid) return null\n\n  const errors = getFormFieldErrors(field.state.meta.errors)\n\n  return errors.length > 0 ? <FieldError errors={errors} /> : null\n}\n\nfunction AuthFormServerError() {\n  const form = useFormContext()\n\n  return (\n    <form.Subscribe selector={(state) => state.errorMap.onServer}>\n      {(error) => {\n        const formError =\n          error && typeof error === \"object\" && \"form\" in error\n            ? error.form\n            : error\n        const errors = getFormFieldErrors(formError ? [formError] : [])\n        return errors.length > 0 ? <FieldError errors={errors} /> : null\n      }}\n    </form.Subscribe>\n  )\n}\n\nexport function setAuthFormServerError(\n  form: AnyFormApi,\n  error: unknown,\n  fallbackMessage: string\n) {\n  const normalized = normalizeAuthFormServerError(error, fallbackMessage)\n  form.setErrorMap({\n    onServer: {\n      fields: normalized.fields ?? {},\n      form: normalized.form\n    }\n  })\n}\n\nexport function clearAuthFormServerError(form: AnyFormApi) {\n  form.setErrorMap({ onServer: { fields: {} } })\n}\n\nexport function clearAuthFormFieldServerError(\n  form: AnyFormApi,\n  fieldName: string\n) {\n  form.setErrorMap({ onServer: undefined })\n  if (!fieldName) return\n\n  const fieldMeta = form.getFieldMeta(fieldName as never)\n  if (!fieldMeta?.errorMap.onServer) return\n\n  form.setFieldMeta(fieldName as never, (current = fieldMeta) => ({\n    ...current,\n    errorMap: { ...current.errorMap, onServer: undefined },\n    errorSourceMap: { ...current.errorSourceMap, onServer: undefined }\n  }))\n}\n\nexport async function runAuthFormAction(\n  form: AnyFormApi,\n  action: () => Promise<unknown>,\n  serverErrorMessage = DEFAULT_AUTH_FORM_SERVER_ERROR\n) {\n  clearAuthFormServerError(form)\n  try {\n    await action()\n    return true\n  } catch (error) {\n    if (!form.state.errorMap.onServer) {\n      setAuthFormServerError(form, error, serverErrorMessage)\n    }\n    return false\n  }\n}\n\nexport async function submitAuthForm(\n  form: AnyFormApi,\n  serverErrorMessage = DEFAULT_AUTH_FORM_SERVER_ERROR\n) {\n  clearAuthFormServerError(form)\n  try {\n    await form.handleSubmit()\n    return form.state.isValid\n  } catch (error) {\n    if (!form.state.errorMap.onServer) {\n      setAuthFormServerError(form, error, serverErrorMessage)\n    }\n    return false\n  }\n}\n\ntype AuthFormRootProps = Omit<ComponentProps<\"form\">, \"onSubmit\"> & {\n  onBeforeSubmit?: () => void\n  serverErrorMessage?: string\n}\n\nfunction AuthFormRoot({\n  children,\n  onBeforeSubmit,\n  onInput,\n  serverErrorMessage = DEFAULT_AUTH_FORM_SERVER_ERROR,\n  ...props\n}: AuthFormRootProps) {\n  const form = useFormContext()\n  const submittingRef = useRef(false)\n\n  async function submit(event: FormEvent<HTMLFormElement>) {\n    event.preventDefault()\n    if (submittingRef.current || form.state.isSubmitting) return\n\n    const formElement = event.currentTarget\n    onBeforeSubmit?.()\n    submittingRef.current = true\n    try {\n      const isValid = await submitAuthForm(form, serverErrorMessage)\n      if (!isValid) focusFirstInvalidAuthFormControl(formElement)\n    } finally {\n      submittingRef.current = false\n    }\n  }\n\n  return (\n    <form\n      {...props}\n      onInvalid={(event) =>\n        focusFirstInvalidAuthFormControl(event.currentTarget)\n      }\n      onInput={(event) => {\n        const target = event.target\n        const fieldName =\n          target instanceof HTMLInputElement ||\n          target instanceof HTMLSelectElement ||\n          target instanceof HTMLTextAreaElement\n            ? target.name\n            : \"\"\n        clearAuthFormFieldServerError(form, fieldName)\n        onInput?.(event)\n      }}\n      onSubmit={submit}\n    >\n      {children}\n    </form>\n  )\n}\n\ntype AuthFormTextFieldProps = Omit<\n  ComponentProps<typeof Input>,\n  \"name\" | \"onBlur\" | \"onChange\" | \"value\"\n> & {\n  description?: ReactNode\n  label: ReactNode\n}\n\nfunction AuthFormTextField({\n  description,\n  id,\n  label,\n  ...props\n}: AuthFormTextFieldProps) {\n  const field = useFieldContext<string>()\n  const form = useFormContext()\n  const isInvalid = isAuthFormFieldInvalid(field.state.meta)\n  const inputId = id ?? field.name\n\n  return (\n    <Field data-invalid={isInvalid}>\n      <FieldLabel htmlFor={inputId}>{label}</FieldLabel>\n      <Input\n        {...props}\n        aria-busy={field.state.meta.isValidating || undefined}\n        aria-invalid={isInvalid}\n        id={inputId}\n        name={field.name}\n        onBlur={field.handleBlur}\n        onChange={(event) => {\n          clearAuthFormFieldServerError(form, field.name)\n          field.handleChange(event.target.value)\n        }}\n        value={field.state.value}\n      />\n      {description ? <FieldDescription>{description}</FieldDescription> : null}\n      <AuthFormFieldError />\n    </Field>\n  )\n}\n\nfunction AuthFormSubmitButton({\n  children,\n  disabled,\n  isPending,\n  ...props\n}: ComponentProps<typeof Button> & { isPending?: boolean }) {\n  const form = useFormContext()\n\n  return (\n    <form.Subscribe\n      selector={(state) => [state.isSubmitting, state.isValidating] as const}\n    >\n      {([isSubmitting, isValidating]) => (\n        <Button\n          {...props}\n          aria-busy={isPending || isSubmitting || undefined}\n          aria-disabled={\n            disabled || isPending || isSubmitting || isValidating || undefined\n          }\n          disabled={disabled || isPending || isSubmitting || isValidating}\n          type=\"submit\"\n        >\n          {isPending || isSubmitting ? (\n            <Spinner data-icon=\"inline-start\" />\n          ) : null}\n          {children}\n        </Button>\n      )}\n    </form.Subscribe>\n  )\n}\n\ntype AuthFormAdditionalFieldProps = Omit<\n  AdditionalFieldProps,\n  \"errors\" | \"isInvalid\" | \"name\" | \"onBlur\" | \"onChange\" | \"value\"\n>\n\nfunction AuthFormAdditionalField(props: AuthFormAdditionalFieldProps) {\n  const field = useFieldContext<AdditionalFieldFormValue>()\n  const form = useFormContext()\n  const isInvalid = isAuthFormFieldInvalid(field.state.meta)\n\n  return (\n    <AdditionalField\n      {...props}\n      errors={\n        isInvalid ? getFormFieldErrors(field.state.meta.errors) : undefined\n      }\n      isInvalid={isInvalid}\n      name={field.name}\n      onBlur={field.handleBlur}\n      onChange={(value) => {\n        clearAuthFormFieldServerError(form, field.name)\n        field.handleChange(value)\n      }}\n      value={field.state.value}\n    />\n  )\n}\n\nexport const {\n  useAppForm: useAuthForm,\n  withFieldGroup: withAuthFieldGroup,\n  withForm: withAuthForm\n} = createFormHook({\n  fieldComponents: {\n    AuthFormAdditionalField,\n    AuthFormFieldError,\n    AuthFormTextField\n  },\n  fieldContext,\n  formComponents: {\n    AuthFormRoot,\n    AuthFormServerError,\n    AuthFormSubmitButton\n  },\n  formContext\n})\n\nexport function isAuthFormFieldInvalid({\n  isTouched,\n  isValid\n}: {\n  isTouched: boolean\n  isValid: boolean\n}) {\n  return isTouched && !isValid\n}\n\nexport function getAuthAdditionalFieldValidators(\n  field: AdditionalFieldConfig,\n  requiredMessage: string\n) {\n  return {\n    onChange: ({ value }: { value: AdditionalFieldFormValue }) =>\n      validateAdditionalFieldRequired(field, value, requiredMessage),\n    onChangeAsync: field.validate\n      ? ({ value }: { value: AdditionalFieldFormValue }) =>\n          validateAdditionalFieldValue(field, value)\n      : undefined,\n    onChangeAsyncDebounceMs: field.validate\n      ? (field.validateDebounceMs ??\n        DEFAULT_ADDITIONAL_FIELD_VALIDATION_DEBOUNCE_MS)\n      : undefined\n  }\n}\n",
      "type": "registry:component",
      "target": "@components/auth/auth-form.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/otp-field.tsx",
      "content": "\"use client\"\n\nimport { REGEXP_ONLY_DIGITS } from \"input-otp\"\nimport { useId } from \"react\"\n\nimport { Field, FieldError, FieldLabel } from \"@/components/ui/field\"\nimport {\n  InputOTP,\n  InputOTPGroup,\n  InputOTPSlot\n} from \"@/components/ui/input-otp\"\nimport { cn } from \"cn\"\n\nexport type OtpFieldProps = {\n  /** Visible label rendered above the slots. */\n  label: string\n  /** Number of slots — keep in sync with the server's code length. */\n  length: number\n  value: string\n  onChange: (value: string) => void\n  onComplete?: (value: string) => void\n  autoFocus?: boolean\n  className?: string\n  errorMessage?: string\n  disabled?: boolean\n  name?: string\n}\n\n/** Strip everything the numeric slots can't hold — pasted codes often carry spaces or dashes. */\nfunction normalizeCode(value: string) {\n  return value.replace(/\\D/g, \"\")\n}\n\n/**\n * Labelled one-time-code input.\n *\n * Shared by every code-based flow (email OTP, two-factor challenge,\n * two-factor enrollment) so slot sizing, pasting, and error wiring behave the\n * same everywhere.\n *\n * @param label - Visible label, also used as the accessible name.\n * @param length - Number of code characters.\n * @param errorMessage - Rendered below the slots when set.\n */\nexport function OtpField({\n  autoFocus,\n  className,\n  disabled,\n  errorMessage,\n  label,\n  length,\n  name,\n  onChange,\n  onComplete,\n  value\n}: OtpFieldProps) {\n  const inputId = useId()\n\n  return (\n    <Field className={cn(className)} data-invalid={!!errorMessage}>\n      <FieldLabel htmlFor={inputId}>{label}</FieldLabel>\n\n      <InputOTP\n        aria-invalid={!!errorMessage}\n        aria-label={label}\n        autoComplete=\"one-time-code\"\n        autoFocus={autoFocus}\n        containerClassName=\"w-full justify-center\"\n        disabled={disabled}\n        id={inputId}\n        inputMode=\"numeric\"\n        maxLength={length}\n        name={name}\n        pasteTransformer={normalizeCode}\n        pattern={REGEXP_ONLY_DIGITS}\n        value={value}\n        onChange={(next) => onChange(normalizeCode(next))}\n        onComplete={(completedCode) =>\n          onComplete?.(normalizeCode(completedCode))\n        }\n      >\n        <InputOTPGroup>\n          {Array.from({ length }, (_, slotIndex) => (\n            <InputOTPSlot\n              index={slotIndex}\n              key={`otp-slot-${String(slotIndex + 1)}`}\n            />\n          ))}\n        </InputOTPGroup>\n      </InputOTP>\n\n      <FieldError>{errorMessage}</FieldError>\n    </Field>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/otp-field.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/two-factor/backup-codes.tsx",
      "content": "\"use client\"\n\nimport {\n  downloadTextFile,\n  formatBackupCodesText,\n  printTextFile\n} from \"@better-auth-ui/core\"\nimport {\n  useAuth,\n  useAuthPlugin,\n  useCopyToClipboard\n} from \"@better-auth-ui/react\"\nimport { Check, Copy, Download, Printer } from \"lucide-react\"\nimport { toast } from \"sonner\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { twoFactorPlugin } from \"@/lib/auth/two-factor-plugin\"\n\nexport type BackupCodesProps = {\n  codes: string[]\n}\n\n/**\n * One-time display of freshly generated backup codes.\n *\n * The codes are never persisted anywhere on the client. They live in the\n * calling component's state until the dialog closes, so the save actions\n * matter.\n *\n * @param codes - Backup codes returned by enable or regenerate.\n */\nexport function BackupCodes({ codes }: BackupCodesProps) {\n  const { localization } = useAuth()\n  const { localization: twoFactorLocalization } = useAuthPlugin(twoFactorPlugin)\n  const { copied, copy } = useCopyToClipboard({\n    onError: () => toast.error(twoFactorLocalization.backupCodesCopyFailed)\n  })\n  const getBackupCodesText = () =>\n    formatBackupCodesText(codes, twoFactorLocalization, window.location.origin)\n\n  const copyCodes = () => copy(getBackupCodesText())\n\n  return (\n    <div className=\"flex flex-col gap-3\">\n      <p className=\"text-muted-foreground text-sm\">\n        {twoFactorLocalization.backupCodesDescription}\n      </p>\n\n      <ul className=\"grid grid-cols-2 gap-2 rounded-md border bg-muted/40 p-4 font-mono text-sm\">\n        {codes.map((backupCode) => (\n          <li className=\"tracking-wide\" key={backupCode}>\n            {backupCode}\n          </li>\n        ))}\n      </ul>\n\n      <div className=\"flex flex-wrap gap-2\">\n        <Button\n          type=\"button\"\n          size=\"sm\"\n          variant=\"outline\"\n          aria-label={\n            copied\n              ? twoFactorLocalization.backupCodesCopied\n              : localization.settings.copyToClipboard\n          }\n          onClick={copyCodes}\n        >\n          {copied ? (\n            <Check data-icon=\"inline-start\" />\n          ) : (\n            <Copy data-icon=\"inline-start\" />\n          )}\n          {localization.settings.copyToClipboard}\n        </Button>\n\n        <Button\n          type=\"button\"\n          size=\"sm\"\n          variant=\"outline\"\n          onClick={() =>\n            downloadTextFile(getBackupCodesText(), \"backup-codes.txt\")\n          }\n        >\n          <Download data-icon=\"inline-start\" />\n          {twoFactorLocalization.downloadBackupCodes}\n        </Button>\n\n        <Button\n          type=\"button\"\n          size=\"sm\"\n          variant=\"outline\"\n          onClick={() => printTextFile(getBackupCodesText())}\n        >\n          <Printer data-icon=\"inline-start\" />\n          {twoFactorLocalization.printBackupCodes}\n        </Button>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/two-factor/backup-codes.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/two-factor/disable-two-factor-dialog.tsx",
      "content": "\"use client\"\n\nimport type { TwoFactorAuthClient } from \"@better-auth-ui/core/plugins/two-factor\"\nimport { useAuth, useAuthPlugin } from \"@better-auth-ui/react\"\nimport { useDisableTwoFactor } from \"@better-auth-ui/react/plugins/two-factor\"\nimport { ShieldAlert } from \"lucide-react\"\nimport { toast } from \"sonner\"\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 { Field, FieldError, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { twoFactorPlugin } from \"@/lib/auth/two-factor-plugin\"\nimport { useTwoFactorPasswordRequirement } from \"@/lib/auth/use-two-factor-password\"\nimport { useAuthForm } from \"../auth-form\"\n\nexport type DisableTwoFactorDialogProps = {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\n/**\n * Confirm turning two-factor off.\n *\n * @param open - Whether the dialog is open.\n * @param onOpenChange - Called when the dialog requests an open state change.\n */\nexport function DisableTwoFactorDialog({\n  open,\n  onOpenChange\n}: DisableTwoFactorDialogProps) {\n  const { authClient, localization } = useAuth()\n  const { localization: twoFactorLocalization } = useAuthPlugin(twoFactorPlugin)\n  const { isPending: isResolvingPasswordRequirement, requiresPassword } =\n    useTwoFactorPasswordRequirement()\n\n  const { mutateAsync: disableTwoFactor, isPending: isDisabling } =\n    useDisableTwoFactor(authClient as TwoFactorAuthClient, {\n      onSuccess: () => {\n        toast.success(twoFactorLocalization.twoFactorDisabled)\n        onOpenChange(false)\n      }\n    })\n\n  const isPending = isDisabling || isResolvingPasswordRequirement\n\n  const form = useAuthForm({\n    defaultValues: { password: \"\" },\n    onSubmit: async ({ value }) =>\n      await disableTwoFactor(\n        requiresPassword ? { password: value.password } : {}\n      )\n  })\n\n  return (\n    <AlertDialog open={open} onOpenChange={onOpenChange}>\n      <AlertDialogContent>\n        <form.AppForm>\n          <form.AuthFormRoot className=\"flex flex-col gap-6\">\n            <AlertDialogHeader>\n              <AlertDialogMedia className=\"bg-destructive/10 text-destructive dark:bg-destructive/20 dark:text-destructive\">\n                <ShieldAlert />\n              </AlertDialogMedia>\n\n              <AlertDialogTitle>\n                {twoFactorLocalization.disableTwoFactor}\n              </AlertDialogTitle>\n\n              <AlertDialogDescription>\n                {requiresPassword\n                  ? twoFactorLocalization.passwordConfirmation\n                  : twoFactorLocalization.twoFactorDescription}\n              </AlertDialogDescription>\n            </AlertDialogHeader>\n\n            {requiresPassword && (\n              <form.AppField name=\"password\">\n                {(field) => (\n                  <Field>\n                    <FieldLabel htmlFor=\"disable-two-factor-password\">\n                      {localization.auth.password}\n                    </FieldLabel>\n\n                    <Input\n                      id=\"disable-two-factor-password\"\n                      name={field.name}\n                      type=\"password\"\n                      autoComplete=\"current-password\"\n                      autoFocus\n                      required\n                      placeholder={localization.auth.passwordPlaceholder}\n                      disabled={isPending}\n                      value={field.state.value}\n                      onBlur={field.handleBlur}\n                      onChange={(event) =>\n                        field.handleChange(event.target.value)\n                      }\n                    />\n\n                    <FieldError />\n                  </Field>\n                )}\n              </form.AppField>\n            )}\n\n            <AlertDialogFooter>\n              <AlertDialogCancel disabled={isPending}>\n                {localization.settings.cancel}\n              </AlertDialogCancel>\n\n              <form.AuthFormSubmitButton\n                isPending={isPending}\n                variant=\"destructive\"\n                disabled={isPending}\n              >\n                {twoFactorLocalization.disableTwoFactor}\n              </form.AuthFormSubmitButton>\n            </AlertDialogFooter>\n          </form.AuthFormRoot>\n        </form.AppForm>\n      </AlertDialogContent>\n    </AlertDialog>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/two-factor/disable-two-factor-dialog.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/two-factor/enable-two-factor-dialog.tsx",
      "content": "\"use client\"\n\nimport { createQrCodeSvgData } from \"@better-auth-ui/core\"\nimport type {\n  TwoFactorAuthClient,\n  TwoFactorMethod\n} from \"@better-auth-ui/core/plugins/two-factor\"\nimport {\n  useAuth,\n  useAuthPlugin,\n  useCopyToClipboard\n} from \"@better-auth-ui/react\"\nimport {\n  useEnableTwoFactor,\n  useVerifyTotp\n} from \"@better-auth-ui/react/plugins/two-factor\"\nimport { Check, Copy, Mail, ShieldCheck, Smartphone } from \"lucide-react\"\nimport { useMemo, useState } from \"react\"\nimport { toast } from \"sonner\"\nimport { 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 {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput\n} from \"@/components/ui/input-group\"\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\nimport { twoFactorPlugin } from \"@/lib/auth/two-factor-plugin\"\nimport { useTwoFactorPasswordRequirement } from \"@/lib/auth/use-two-factor-password\"\nimport { submitAuthForm, useAuthForm } from \"../auth-form\"\nimport { OtpField } from \"../otp-field\"\nimport { BackupCodes } from \"./backup-codes\"\n\ntype EnrollmentStep = \"password\" | \"verify\" | \"backupCodes\"\n\nexport type EnableTwoFactorDialogProps = {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\n/**\n * Two-factor enrollment with authenticator-app and delivered-code methods.\n *\n * TOTP continues through QR verification and backup-code capture. OTP becomes\n * active as soon as Better Auth accepts the enrollment request.\n *\n * @param open - Whether the dialog is open.\n * @param onOpenChange - Called when the dialog requests an open state change.\n */\nexport function EnableTwoFactorDialog({\n  open,\n  onOpenChange\n}: EnableTwoFactorDialogProps) {\n  const { authClient, localization } = useAuth()\n  const {\n    codeLength,\n    enrollmentMethods,\n    localization: twoFactorLocalization\n  } = useAuthPlugin(twoFactorPlugin)\n  const { isPending: isResolvingPasswordRequirement, requiresPassword } =\n    useTwoFactorPasswordRequirement()\n\n  const twoFactorClient = authClient as TwoFactorAuthClient\n\n  const [step, setStep] = useState<EnrollmentStep>(\"password\")\n  const [method, setMethod] = useState<TwoFactorMethod>(\n    enrollmentMethods[0] ?? \"totp\"\n  )\n  const [totpUri, setTotpUri] = useState(\"\")\n  const [backupCodes, setBackupCodes] = useState<string[]>([])\n  const {\n    copied: setupKeyCopied,\n    copy: copySetupKeyValue,\n    reset: resetSetupKeyCopy\n  } = useCopyToClipboard({\n    onError: () => toast.error(twoFactorLocalization.setupKeyCopyFailed)\n  })\n\n  const qrCode = useMemo(\n    () => (totpUri ? createQrCodeSvgData(totpUri) : null),\n    [totpUri]\n  )\n\n  // Manual entry fallback for authenticator apps that can't scan. The URI is\n  // an `otpauth://` URL, so the secret is just a query parameter.\n  const setupKey = useMemo(() => {\n    if (!totpUri) return null\n\n    try {\n      return new URL(totpUri).searchParams.get(\"secret\")\n    } catch {\n      return null\n    }\n  }, [totpUri])\n\n  const copySetupKey = async () => {\n    if (!setupKey) return\n\n    await copySetupKeyValue(setupKey)\n  }\n\n  const {\n    mutateAsync: enableTwoFactor,\n    isPending: isEnabling,\n    reset: resetEnrollment\n  } = useEnableTwoFactor(twoFactorClient, {\n    onSuccess: (data) => {\n      if (data.method === \"otp\") {\n        toast.success(twoFactorLocalization.twoFactorEnabled)\n        handleOpenChange(false)\n        return\n      }\n\n      setTotpUri(data.totpURI)\n      setBackupCodes(data.backupCodes)\n      setStep(\"verify\")\n    }\n  })\n\n  const { mutateAsync: verifyTotp, isPending: isVerifying } = useVerifyTotp(\n    twoFactorClient,\n    {\n      onError: () => form.setFieldValue(\"code\", \"\"),\n      onSuccess: () => {\n        toast.success(twoFactorLocalization.twoFactorEnabled)\n        setStep(\"backupCodes\")\n      }\n    }\n  )\n\n  const isPending = isEnabling || isVerifying || isResolvingPasswordRequirement\n\n  const form = useAuthForm({\n    defaultValues: { code: \"\", password: \"\" },\n    onSubmit: async ({ value }) => {\n      if (step === \"backupCodes\") {\n        handleOpenChange(false)\n        return\n      }\n      if (step === \"verify\") {\n        await verifyCode(value.code)\n        return\n      }\n      await enableTwoFactor(\n        requiresPassword ? { method, password: value.password } : { method }\n      )\n    }\n  })\n\n  const verifyCode = async (completedCode: string) => {\n    if (isPending || step !== \"verify\" || completedCode.length !== codeLength) {\n      return\n    }\n\n    await verifyTotp({ code: completedCode })\n  }\n\n  const handleOpenChange = (nextOpen: boolean) => {\n    onOpenChange(nextOpen)\n\n    if (!nextOpen) {\n      setStep(\"password\")\n      setMethod(enrollmentMethods[0] ?? \"totp\")\n      setTotpUri(\"\")\n      setBackupCodes([])\n      form.reset()\n      resetSetupKeyCopy()\n      // Clears the resolved TOTP URI and backup codes from the mutation cache.\n      resetEnrollment()\n    }\n  }\n\n  const submitLabel =\n    step === \"backupCodes\"\n      ? twoFactorLocalization.done\n      : step === \"verify\"\n        ? twoFactorLocalization.verify\n        : twoFactorLocalization.enableTwoFactor\n\n  return (\n    <Dialog open={open} onOpenChange={handleOpenChange}>\n      <DialogContent>\n        <form.AppForm>\n          <form.AuthFormRoot className=\"flex flex-col gap-6\">\n            <DialogHeader>\n              <DialogTitle className=\"flex items-center gap-2\">\n                <ShieldCheck />\n                {twoFactorLocalization.twoFactor}\n              </DialogTitle>\n\n              <DialogDescription>\n                {step === \"password\" && requiresPassword\n                  ? twoFactorLocalization.passwordConfirmation\n                  : step === \"verify\"\n                    ? twoFactorLocalization.scanQrCode\n                    : twoFactorLocalization.twoFactorDescription}\n              </DialogDescription>\n            </DialogHeader>\n\n            {step === \"password\" && (\n              <div className=\"flex flex-col gap-4\">\n                {enrollmentMethods.length > 1 && (\n                  <Tabs\n                    value={method}\n                    onValueChange={(value) =>\n                      setMethod(value as TwoFactorMethod)\n                    }\n                  >\n                    <TabsList\n                      aria-label={twoFactorLocalization.chooseEnrollmentMethod}\n                      className=\"w-full\"\n                    >\n                      {enrollmentMethods.includes(\"totp\") && (\n                        <TabsTrigger value=\"totp\">\n                          <Smartphone\n                            aria-hidden=\"true\"\n                            className=\"text-muted-foreground\"\n                          />\n                          {twoFactorLocalization.authenticatorApp}\n                        </TabsTrigger>\n                      )}\n                      {enrollmentMethods.includes(\"otp\") && (\n                        <TabsTrigger value=\"otp\">\n                          <Mail\n                            aria-hidden=\"true\"\n                            className=\"text-muted-foreground\"\n                          />\n                          {twoFactorLocalization.deliveredCode}\n                        </TabsTrigger>\n                      )}\n                    </TabsList>\n                  </Tabs>\n                )}\n\n                <p className=\"text-muted-foreground text-sm\">\n                  {method === \"totp\"\n                    ? twoFactorLocalization.authenticatorAppDescription\n                    : twoFactorLocalization.deliveredCodeDescription}\n                </p>\n\n                {requiresPassword && (\n                  <form.AppField name=\"password\">\n                    {(field) => (\n                      <Field>\n                        <FieldLabel htmlFor=\"two-factor-password\">\n                          {localization.auth.password}\n                        </FieldLabel>\n\n                        <Input\n                          id=\"two-factor-password\"\n                          name={field.name}\n                          type=\"password\"\n                          autoComplete=\"current-password\"\n                          autoFocus\n                          required\n                          placeholder={localization.auth.passwordPlaceholder}\n                          disabled={isPending}\n                          value={field.state.value}\n                          onBlur={field.handleBlur}\n                          onChange={(event) =>\n                            field.handleChange(event.target.value)\n                          }\n                        />\n\n                        <FieldError />\n                      </Field>\n                    )}\n                  </form.AppField>\n                )}\n              </div>\n            )}\n\n            {step === \"verify\" && (\n              <div className=\"flex flex-col items-center gap-4\">\n                {qrCode && (\n                  <svg\n                    aria-hidden=\"true\"\n                    className=\"size-44 rounded-md border\"\n                    viewBox={`0 0 ${qrCode.size} ${qrCode.size}`}\n                  >\n                    <path\n                      fill=\"white\"\n                      d={`M0 0h${qrCode.size}v${qrCode.size}H0z`}\n                    />\n                    <path\n                      fill=\"black\"\n                      d={qrCode.path}\n                      shapeRendering=\"crispEdges\"\n                    />\n                  </svg>\n                )}\n\n                {setupKey && (\n                  <Field className=\"w-full gap-1\">\n                    <FieldLabel\n                      className=\"text-muted-foreground text-xs\"\n                      htmlFor=\"two-factor-setup-key\"\n                    >\n                      {twoFactorLocalization.setupKey}\n                    </FieldLabel>\n\n                    <InputGroup>\n                      <InputGroupInput\n                        className=\"font-mono text-xs\"\n                        id=\"two-factor-setup-key\"\n                        readOnly\n                        value={setupKey}\n                      />\n\n                      <InputGroupAddon align=\"inline-end\">\n                        <InputGroupButton\n                          aria-label={\n                            setupKeyCopied\n                              ? twoFactorLocalization.setupKeyCopied\n                              : localization.settings.copyToClipboard\n                          }\n                          onClick={copySetupKey}\n                          size=\"icon-xs\"\n                        >\n                          {setupKeyCopied ? <Check /> : <Copy />}\n                        </InputGroupButton>\n                      </InputGroupAddon>\n                    </InputGroup>\n                  </Field>\n                )}\n\n                <form.AppField name=\"code\">\n                  {(field) => (\n                    <OtpField\n                      autoFocus\n                      className=\"w-full\"\n                      disabled={isPending}\n                      label={twoFactorLocalization.authenticatorCode}\n                      length={codeLength}\n                      name={field.name}\n                      value={field.state.value}\n                      onChange={field.handleChange}\n                      onComplete={() => void submitAuthForm(form)}\n                    />\n                  )}\n                </form.AppField>\n              </div>\n            )}\n\n            {step === \"backupCodes\" && <BackupCodes codes={backupCodes} />}\n\n            <form.AuthFormServerError />\n\n            <DialogFooter>\n              {step !== \"backupCodes\" && (\n                <DialogClose\n                  className={buttonVariants({ variant: \"outline\" })}\n                  disabled={isPending}\n                  type=\"button\"\n                >\n                  {localization.settings.cancel}\n                </DialogClose>\n              )}\n\n              <form.Subscribe selector={(state) => state.values.code}>\n                {(code) => (\n                  <form.AuthFormSubmitButton\n                    isPending={isPending}\n                    disabled={\n                      isPending ||\n                      (step === \"verify\" && code.length !== codeLength)\n                    }\n                  >\n                    {submitLabel}\n                  </form.AuthFormSubmitButton>\n                )}\n              </form.Subscribe>\n            </DialogFooter>\n          </form.AuthFormRoot>\n        </form.AppForm>\n      </DialogContent>\n    </Dialog>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/two-factor/enable-two-factor-dialog.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/two-factor/regenerate-backup-codes-dialog.tsx",
      "content": "\"use client\"\n\nimport type { TwoFactorAuthClient } from \"@better-auth-ui/core/plugins/two-factor\"\nimport { useAuth, useAuthPlugin } from \"@better-auth-ui/react\"\nimport { useGenerateBackupCodes } from \"@better-auth-ui/react/plugins/two-factor\"\nimport { KeyRound } from \"lucide-react\"\nimport { useState } from \"react\"\nimport { toast } from \"sonner\"\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 { Field, FieldError, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { twoFactorPlugin } from \"@/lib/auth/two-factor-plugin\"\nimport { useTwoFactorPasswordRequirement } from \"@/lib/auth/use-two-factor-password\"\nimport { useAuthForm } from \"../auth-form\"\nimport { BackupCodes } from \"./backup-codes\"\n\nexport type RegenerateBackupCodesDialogProps = {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\n/**\n * Replace the existing backup codes with a fresh set.\n *\n * The new codes are shown once, in component state only — closing the dialog\n * is the point of no return, which is why the copy button sits right there.\n *\n * @param open - Whether the dialog is open.\n * @param onOpenChange - Called when the dialog requests an open state change.\n */\nexport function RegenerateBackupCodesDialog({\n  open,\n  onOpenChange\n}: RegenerateBackupCodesDialogProps) {\n  const { authClient, localization } = useAuth()\n  const { localization: twoFactorLocalization } = useAuthPlugin(twoFactorPlugin)\n  const { isPending: isResolvingPasswordRequirement, requiresPassword } =\n    useTwoFactorPasswordRequirement()\n\n  const [codes, setCodes] = useState<string[]>([])\n\n  const {\n    mutateAsync: generateBackupCodes,\n    isPending: isGenerating,\n    reset: resetGeneration\n  } = useGenerateBackupCodes(authClient as TwoFactorAuthClient, {\n    onSuccess: (data) => {\n      setCodes(data.backupCodes)\n      toast.success(twoFactorLocalization.backupCodesRegenerated)\n    }\n  })\n\n  const isPending = isGenerating || isResolvingPasswordRequirement\n\n  const form = useAuthForm({\n    defaultValues: { password: \"\" },\n    onSubmit: async ({ value }) => {\n      if (codes.length) {\n        handleOpenChange(false)\n        return\n      }\n      await generateBackupCodes(\n        requiresPassword ? { password: value.password } : {}\n      )\n    }\n  })\n\n  const handleOpenChange = (nextOpen: boolean) => {\n    onOpenChange(nextOpen)\n\n    if (!nextOpen) {\n      setCodes([])\n      form.reset()\n      // Clears the resolved backup codes from the mutation cache.\n      resetGeneration()\n    }\n  }\n\n  return (\n    <AlertDialog open={open} onOpenChange={handleOpenChange}>\n      <AlertDialogContent>\n        <form.AppForm>\n          <form.AuthFormRoot className=\"flex flex-col gap-6\">\n            <AlertDialogHeader>\n              <AlertDialogMedia>\n                <KeyRound />\n              </AlertDialogMedia>\n\n              <AlertDialogTitle>\n                {twoFactorLocalization.backupCodes}\n              </AlertDialogTitle>\n\n              <AlertDialogDescription>\n                {codes.length || !requiresPassword\n                  ? twoFactorLocalization.backupCodesDescription\n                  : twoFactorLocalization.passwordConfirmation}\n              </AlertDialogDescription>\n            </AlertDialogHeader>\n\n            {codes.length ? (\n              <BackupCodes codes={codes} />\n            ) : (\n              requiresPassword && (\n                <form.AppField name=\"password\">\n                  {(field) => (\n                    <Field>\n                      <FieldLabel htmlFor=\"regenerate-backup-codes-password\">\n                        {localization.auth.password}\n                      </FieldLabel>\n\n                      <Input\n                        id=\"regenerate-backup-codes-password\"\n                        name={field.name}\n                        type=\"password\"\n                        autoComplete=\"current-password\"\n                        autoFocus\n                        required\n                        placeholder={localization.auth.passwordPlaceholder}\n                        disabled={isPending}\n                        value={field.state.value}\n                        onBlur={field.handleBlur}\n                        onChange={(event) =>\n                          field.handleChange(event.target.value)\n                        }\n                      />\n\n                      <FieldError />\n                    </Field>\n                  )}\n                </form.AppField>\n              )\n            )}\n\n            <AlertDialogFooter>\n              {!codes.length && (\n                <AlertDialogCancel disabled={isPending}>\n                  {localization.settings.cancel}\n                </AlertDialogCancel>\n              )}\n\n              <form.AuthFormSubmitButton\n                isPending={isPending}\n                disabled={isPending}\n              >\n                {codes.length\n                  ? twoFactorLocalization.done\n                  : twoFactorLocalization.regenerateBackupCodes}\n              </form.AuthFormSubmitButton>\n            </AlertDialogFooter>\n          </form.AuthFormRoot>\n        </form.AppForm>\n      </AlertDialogContent>\n    </AlertDialog>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/two-factor/regenerate-backup-codes-dialog.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/two-factor/two-factor-challenge.tsx",
      "content": "\"use client\"\n\nimport { authQueryKeys, validateStringLength } from \"@better-auth-ui/core\"\nimport type { TwoFactorAuthClient } from \"@better-auth-ui/core/plugins/two-factor\"\nimport { useAuth, useAuthPlugin, useSession } from \"@better-auth-ui/react\"\nimport {\n  useSendTwoFactorOtp,\n  useVerifyBackupCode,\n  useVerifyTotp,\n  useVerifyTwoFactorOtp\n} from \"@better-auth-ui/react/plugins/two-factor\"\nimport { useQueryClient } from \"@tanstack/react-query\"\nimport { useEffect, useState } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle\n} from \"@/components/ui/card\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport {\n  Field,\n  FieldDescription,\n  FieldGroup,\n  FieldLabel\n} from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport {\n  clearTwoFactorMethods,\n  readTwoFactorMethods,\n  type TwoFactorMethod\n} from \"@/lib/auth/two-factor-methods\"\nimport { twoFactorPlugin } from \"@/lib/auth/two-factor-plugin\"\nimport {\n  RESEND_COOLDOWN_SECONDS,\n  useResendCooldown\n} from \"@/lib/auth/use-resend-cooldown\"\nimport { cn } from \"cn\"\nimport { runAuthFormAction, submitAuthForm, useAuthForm } from \"../auth-form\"\nimport { OtpField } from \"../otp-field\"\nimport { useIsHydrated } from \"../use-is-hydrated\"\n\n/** Challenge surfaces the view can render, in the order they are offered. */\ntype ChallengeMethod = TwoFactorMethod | \"backup\"\n\nexport type TwoFactorChallengeProps = {\n  className?: string\n}\n\n/**\n * Second-factor challenge that finishes a pending sign-in.\n *\n * Better Auth answers a password sign-in with `twoFactorRedirect` instead of\n * a session, and the shared sign-in continuation sends the browser here with\n * the enabled methods in session storage. Verifying is what creates the\n * session, after which the original `redirectTo` is resumed.\n *\n * @param className - Additional CSS classes applied to the card.\n */\nexport function TwoFactorChallenge({ className }: TwoFactorChallengeProps) {\n  const {\n    authClient,\n    basePaths,\n    localization,\n    navigate,\n    redirectTo,\n    viewPaths,\n    Link\n  } = useAuth()\n  const {\n    backupCodes: backupCodesEnabled,\n    codeLength,\n    localization: twoFactorLocalization,\n    trustDevice: trustDeviceEnabled\n  } = useAuthPlugin(twoFactorPlugin)\n\n  const twoFactorClient = authClient as TwoFactorAuthClient\n  const session = useSession(authClient)\n  const queryClient = useQueryClient()\n  const isHydrated = useIsHydrated()\n\n  const [methods, setMethods] = useState<TwoFactorMethod[]>(() =>\n    isHydrated ? readTwoFactorMethods() : [\"totp\", \"otp\"]\n  )\n  const [method, setMethod] = useState<ChallengeMethod>(\n    () => methods[0] ?? \"totp\"\n  )\n  const [otpRequested, setOtpRequested] = useState(false)\n  const { cooldown, isCoolingDown, startCooldown } = useResendCooldown()\n\n  useEffect(() => {\n    const stored = readTwoFactorMethods()\n    setMethods(stored)\n    setMethod(stored[0] ?? \"totp\")\n  }, [])\n\n  const onVerified = async () => {\n    clearTwoFactorMethods()\n    await queryClient.invalidateQueries({\n      queryKey: authQueryKeys.listSessions(session.data?.user.id)\n    })\n    navigate({ to: redirectTo })\n  }\n\n  const { mutateAsync: sendTwoFactorOtp, isPending: isSendingOtp } =\n    useSendTwoFactorOtp(twoFactorClient, {\n      onSuccess: () => {\n        setOtpRequested(true)\n        startCooldown(RESEND_COOLDOWN_SECONDS)\n      }\n    })\n\n  const { mutateAsync: verifyTotp, isPending: isVerifyingTotp } = useVerifyTotp(\n    twoFactorClient,\n    {\n      onError: () => form.setFieldValue(\"code\", \"\"),\n      onSuccess: onVerified\n    }\n  )\n\n  const { mutateAsync: verifyTwoFactorOtp, isPending: isVerifyingOtp } =\n    useVerifyTwoFactorOtp(twoFactorClient, {\n      onError: () => form.setFieldValue(\"code\", \"\"),\n      onSuccess: onVerified\n    })\n\n  const { mutateAsync: verifyBackupCode, isPending: isVerifyingBackupCode } =\n    useVerifyBackupCode(twoFactorClient, { onSuccess: onVerified })\n\n  const isPending =\n    isSendingOtp || isVerifyingTotp || isVerifyingOtp || isVerifyingBackupCode\n  const needsOtpRequest = method === \"otp\" && !otpRequested\n\n  const form = useAuthForm({\n    defaultValues: { backupCode: \"\", code: \"\", trustDevice: false },\n    onSubmit: async ({ value }) => {\n      const trust = trustDeviceEnabled ? { trustDevice: value.trustDevice } : {}\n      if (method === \"backup\") {\n        await verifyBackupCode({ code: value.backupCode.trim(), ...trust })\n        return\n      }\n      await verifyCode(value.code)\n    }\n  })\n\n  const switchMethod = (next: ChallengeMethod) => {\n    form.setFieldValue(\"code\", \"\")\n    form.setFieldValue(\"backupCode\", \"\")\n    setMethod(next)\n  }\n\n  const verifyCode = async (completedCode: string) => {\n    if (\n      isPending ||\n      needsOtpRequest ||\n      method === \"backup\" ||\n      completedCode.length !== codeLength\n    ) {\n      return\n    }\n\n    const trust = trustDeviceEnabled\n      ? { trustDevice: form.state.values.trustDevice }\n      : {}\n\n    if (method === \"otp\") {\n      await verifyTwoFactorOtp({ code: completedCode, ...trust })\n      return\n    }\n\n    await verifyTotp({ code: completedCode, ...trust })\n  }\n\n  const description =\n    method === \"backup\"\n      ? twoFactorLocalization.backupCodeDescription\n      : method === \"otp\"\n        ? twoFactorLocalization.emailedCodeDescription\n        : twoFactorLocalization.authenticatorCodeDescription\n\n  const alternatives: { key: ChallengeMethod; label: string }[] = [\n    ...(method !== \"totp\" && methods.includes(\"totp\")\n      ? [\n          {\n            key: \"totp\" as const,\n            label: twoFactorLocalization.useAuthenticator\n          }\n        ]\n      : []),\n    ...(method !== \"otp\" && methods.includes(\"otp\")\n      ? [{ key: \"otp\" as const, label: twoFactorLocalization.useEmailedCode }]\n      : []),\n    ...(method !== \"backup\" && backupCodesEnabled\n      ? [{ key: \"backup\" as const, label: twoFactorLocalization.useBackupCode }]\n      : [])\n  ]\n\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <CardHeader>\n        <CardTitle className=\"text-xl\">\n          {twoFactorLocalization.twoFactor}\n        </CardTitle>\n\n        <CardDescription>{description}</CardDescription>\n      </CardHeader>\n\n      <CardContent>\n        <form.AppForm>\n          <form.AuthFormRoot>\n            <FieldGroup>\n              {method === \"backup\" ? (\n                <form.AppField\n                  name=\"backupCode\"\n                  validators={{\n                    onChange: ({ value }) =>\n                      validateStringLength(value, {\n                        requiredMessage: localization.auth.fieldRequired,\n                        trim: true\n                      })\n                  }}\n                >\n                  {(field) => (\n                    <Field>\n                      <FieldLabel htmlFor=\"backupCode\">\n                        {twoFactorLocalization.backupCode}\n                      </FieldLabel>\n\n                      <Input\n                        id=\"backupCode\"\n                        name={field.name}\n                        autoComplete=\"one-time-code\"\n                        autoFocus\n                        required\n                        disabled={isPending}\n                        value={field.state.value}\n                        onBlur={field.handleBlur}\n                        onChange={(event) =>\n                          field.handleChange(event.target.value)\n                        }\n                      />\n                    </Field>\n                  )}\n                </form.AppField>\n              ) : (\n                <form.AppField name=\"code\">\n                  {(field) => (\n                    <OtpField\n                      autoFocus\n                      disabled={isPending || needsOtpRequest}\n                      label={\n                        method === \"otp\"\n                          ? twoFactorLocalization.emailedCode\n                          : twoFactorLocalization.authenticatorCode\n                      }\n                      length={codeLength}\n                      name={field.name}\n                      value={field.state.value}\n                      onChange={field.handleChange}\n                      onComplete={() => void submitAuthForm(form)}\n                    />\n                  )}\n                </form.AppField>\n              )}\n\n              {trustDeviceEnabled && (\n                <form.AppField name=\"trustDevice\">\n                  {(field) => (\n                    <Field orientation=\"horizontal\">\n                      <Checkbox\n                        id=\"trustDevice\"\n                        name={field.name}\n                        checked={field.state.value}\n                        disabled={isPending}\n                        onCheckedChange={(checked) =>\n                          field.handleChange(checked === true)\n                        }\n                      />\n\n                      <FieldLabel htmlFor=\"trustDevice\" className=\"font-normal\">\n                        {twoFactorLocalization.trustDevice}\n                      </FieldLabel>\n                    </Field>\n                  )}\n                </form.AppField>\n              )}\n\n              <form.AuthFormServerError />\n\n              <div className=\"flex flex-col gap-3\">\n                {needsOtpRequest ? (\n                  <Button\n                    type=\"button\"\n                    disabled={isSendingOtp}\n                    onClick={() =>\n                      void runAuthFormAction(form, () => sendTwoFactorOtp())\n                    }\n                  >\n                    {isSendingOtp && <Spinner />}\n\n                    {twoFactorLocalization.sendEmailCode}\n                  </Button>\n                ) : (\n                  <form.Subscribe selector={(state) => state.values.code}>\n                    {(code) => (\n                      <form.AuthFormSubmitButton\n                        isPending={isPending}\n                        disabled={\n                          isPending ||\n                          (method !== \"backup\" && code.length !== codeLength)\n                        }\n                      >\n                        {twoFactorLocalization.verify}\n                      </form.AuthFormSubmitButton>\n                    )}\n                  </form.Subscribe>\n                )}\n\n                {method === \"otp\" && otpRequested && (\n                  <Button\n                    type=\"button\"\n                    variant=\"outline\"\n                    disabled={isPending || isCoolingDown}\n                    onClick={() =>\n                      void runAuthFormAction(form, () => sendTwoFactorOtp())\n                    }\n                  >\n                    {isCoolingDown\n                      ? localization.auth.resendIn.replace(\n                          \"{{seconds}}\",\n                          String(cooldown)\n                        )\n                      : localization.auth.resend}\n                  </Button>\n                )}\n\n                {alternatives.map((alternative) => (\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    key={alternative.key}\n                    disabled={isPending}\n                    onClick={() => switchMethod(alternative.key)}\n                  >\n                    {alternative.label}\n                  </Button>\n                ))}\n              </div>\n            </FieldGroup>\n          </form.AuthFormRoot>\n        </form.AppForm>\n\n        <div className=\"flex flex-col gap-3 items-center w-full mt-4\">\n          <FieldDescription className=\"text-center\">\n            <Link\n              href={`${basePaths.auth}/${viewPaths.auth.signIn}`}\n              className=\"underline underline-offset-4\"\n            >\n              {twoFactorLocalization.backToSignIn}\n            </Link>\n          </FieldDescription>\n        </div>\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/two-factor/two-factor-challenge.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/two-factor/two-factor-settings.tsx",
      "content": "\"use client\"\n\nimport { useAuth, useAuthPlugin, useSession } from \"@better-auth-ui/react\"\nimport { ShieldCheck } from \"lucide-react\"\nimport { useState } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport {\n  Empty,\n  EmptyContent,\n  EmptyDescription,\n  EmptyHeader,\n  EmptyMedia,\n  EmptyTitle\n} from \"@/components/ui/empty\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { twoFactorPlugin } from \"@/lib/auth/two-factor-plugin\"\nimport { cn } from \"cn\"\nimport { DisableTwoFactorDialog } from \"./disable-two-factor-dialog\"\nimport { EnableTwoFactorDialog } from \"./enable-two-factor-dialog\"\nimport { RegenerateBackupCodesDialog } from \"./regenerate-backup-codes-dialog\"\n\nexport type TwoFactorSettingsProps = {\n  className?: string\n}\n\n/**\n * Security-settings card for enrolling in and managing two-factor auth.\n *\n * Reads `user.twoFactorEnabled` from the session — the field the Better Auth\n * two-factor plugin adds — so the card reflects enrollment without an extra\n * request.\n *\n * @param className - Additional CSS classes applied to the card.\n */\nexport function TwoFactorSettings({ className }: TwoFactorSettingsProps) {\n  const { authClient } = useAuth()\n  const {\n    backupCodes: backupCodesEnabled,\n    localization: twoFactorLocalization\n  } = useAuthPlugin(twoFactorPlugin)\n\n  const { data: session, isPending } = useSession(authClient)\n  const isEnabled = Boolean(\n    (session?.user as { twoFactorEnabled?: boolean } | undefined)\n      ?.twoFactorEnabled\n  )\n\n  const [enableOpen, setEnableOpen] = useState(false)\n  const [disableOpen, setDisableOpen] = useState(false)\n  const [regenerateOpen, setRegenerateOpen] = 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          {twoFactorLocalization.twoFactor}\n        </h2>\n\n        <Button\n          className=\"shrink-0\"\n          size=\"sm\"\n          variant={isEnabled ? \"destructive\" : \"default\"}\n          disabled={isPending}\n          onClick={() =>\n            isEnabled ? setDisableOpen(true) : setEnableOpen(true)\n          }\n        >\n          {isEnabled\n            ? twoFactorLocalization.disableTwoFactor\n            : twoFactorLocalization.enableTwoFactor}\n        </Button>\n      </div>\n\n      {!isPending && !isEnabled ? (\n        <Card className=\"p-0\">\n          <CardContent className=\"p-0\">\n            <Empty>\n              <EmptyHeader>\n                <EmptyMedia variant=\"icon\">\n                  <ShieldCheck />\n                </EmptyMedia>\n                <EmptyTitle>\n                  {twoFactorLocalization.twoFactorDisabled}\n                </EmptyTitle>\n                <EmptyDescription>\n                  {twoFactorLocalization.twoFactorDescription}\n                </EmptyDescription>\n              </EmptyHeader>\n              <EmptyContent>\n                <Button size=\"sm\" onClick={() => setEnableOpen(true)}>\n                  {twoFactorLocalization.enableTwoFactor}\n                </Button>\n              </EmptyContent>\n            </Empty>\n          </CardContent>\n        </Card>\n      ) : (\n        <Card>\n          <CardContent className=\"flex flex-col gap-4\">\n            {isPending ? (\n              <Skeleton className=\"h-5 w-48\" />\n            ) : (\n              <p className=\"text-sm font-medium\">\n                {twoFactorLocalization.twoFactorEnabled}\n              </p>\n            )}\n\n            <p className=\"text-muted-foreground text-sm\">\n              {twoFactorLocalization.twoFactorDescription}\n            </p>\n\n            {isEnabled && backupCodesEnabled && (\n              <Button\n                className=\"self-start\"\n                size=\"sm\"\n                variant=\"outline\"\n                onClick={() => setRegenerateOpen(true)}\n              >\n                {twoFactorLocalization.regenerateBackupCodes}\n              </Button>\n            )}\n          </CardContent>\n        </Card>\n      )}\n\n      <EnableTwoFactorDialog open={enableOpen} onOpenChange={setEnableOpen} />\n      <DisableTwoFactorDialog\n        open={disableOpen}\n        onOpenChange={setDisableOpen}\n      />\n      <RegenerateBackupCodesDialog\n        open={regenerateOpen}\n        onOpenChange={setRegenerateOpen}\n      />\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/two-factor/two-factor-settings.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/use-is-hydrated.ts",
      "content": "\"use client\"\n\nimport { useSyncExternalStore } from \"react\"\n\n/**\n * Returns `true` once the component is mounted on the client (hydrated) and\n * `false` while rendering on the server, so client-only reads (e.g.\n * `sessionStorage`) stay safe during SSR.\n *\n * @returns Whether the component has hydrated on the client.\n */\nexport function useIsHydrated() {\n  const subscribe = () => () => {}\n  return useSyncExternalStore(\n    subscribe,\n    () => true,\n    () => false\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/use-is-hydrated.ts"
    },
    {
      "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/two-factor-methods.ts",
      "content": "/**\n * Second-factor methods a user can complete the challenge with.\n *\n * `totp` and `otp` come from the server's `twoFactorMethods` list. Backup\n * codes are always available when the server has them enabled, so they are\n * never part of that list.\n */\nexport type TwoFactorMethod = \"totp\" | \"otp\"\n\nconst TWO_FACTOR_METHODS: TwoFactorMethod[] = [\"totp\", \"otp\"]\n\n/** Auth plugin id used by Better Auth UI's two-factor integration. */\nexport const TWO_FACTOR_PLUGIN_ID = \"twoFactor\"\n\n/**\n * `sessionStorage` key holding the methods reported by the sign-in response.\n *\n * Only the non-sensitive method names are stored, never a code, token, or the\n * two-factor cookie, which stays HTTP-only.\n */\nexport const TWO_FACTOR_METHODS_STORAGE_KEY =\n  \"better-auth-ui.two-factor-methods\"\n\ntype TwoFactorRedirect = {\n  twoFactorRedirect: true\n  twoFactorMethods?: unknown\n}\n\n/** Detect the redirect payload Better Auth returns before a second factor. */\nexport function isTwoFactorRedirect(data: unknown): data is TwoFactorRedirect {\n  return (\n    typeof data === \"object\" &&\n    data !== null &&\n    (data as { twoFactorRedirect?: unknown }).twoFactorRedirect === true\n  )\n}\n\n/** Narrow arbitrary method names to the challenge views this UI supports. */\nexport function parseTwoFactorMethods(methods?: unknown): TwoFactorMethod[] {\n  if (!Array.isArray(methods)) return []\n\n  return TWO_FACTOR_METHODS.filter((method) => methods.includes(method))\n}\n\n/** Persist the enabled method names without blocking sign-in on storage errors. */\nexport function storeTwoFactorMethods(methods?: unknown) {\n  if (typeof sessionStorage === \"undefined\") return\n\n  try {\n    sessionStorage.setItem(\n      TWO_FACTOR_METHODS_STORAGE_KEY,\n      JSON.stringify(parseTwoFactorMethods(methods))\n    )\n  } catch {\n    // The challenge falls back to every method when storage is unavailable.\n  }\n}\n\n/** Read the stored methods, falling back to every supported challenge. */\nexport function readTwoFactorMethods(): TwoFactorMethod[] {\n  if (typeof sessionStorage === \"undefined\") return TWO_FACTOR_METHODS\n\n  try {\n    const stored = sessionStorage.getItem(TWO_FACTOR_METHODS_STORAGE_KEY)\n    if (!stored) return TWO_FACTOR_METHODS\n\n    const methods = parseTwoFactorMethods(JSON.parse(stored))\n    return methods.length ? methods : TWO_FACTOR_METHODS\n  } catch {\n    return TWO_FACTOR_METHODS\n  }\n}\n\n/** Clear stored method hints after the challenge finishes or is abandoned. */\nexport function clearTwoFactorMethods() {\n  if (typeof sessionStorage === \"undefined\") return\n\n  try {\n    sessionStorage.removeItem(TWO_FACTOR_METHODS_STORAGE_KEY)\n  } catch {\n    // Stale method hints are harmless and must not block navigation.\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/auth/two-factor-methods.ts"
    },
    {
      "path": "examples/start-shadcn-example/src/lib/auth/two-factor-plugin.ts",
      "content": "import { createAuthPlugin } from \"@better-auth-ui/core\"\nimport {\n  twoFactorPlugin as coreTwoFactorPlugin,\n  type TwoFactorPluginOptions\n} from \"@better-auth-ui/core/plugins/two-factor\"\n\nimport { TwoFactorChallenge } from \"@/components/auth/two-factor/two-factor-challenge\"\nimport { TwoFactorSettings } from \"@/components/auth/two-factor/two-factor-settings\"\n\nexport const twoFactorPlugin = createAuthPlugin(\n  coreTwoFactorPlugin.id,\n  (options: TwoFactorPluginOptions = {}) => ({\n    ...coreTwoFactorPlugin(options),\n    securityCards: [TwoFactorSettings],\n    views: {\n      auth: { twoFactor: TwoFactorChallenge }\n    }\n  })\n)\n",
      "type": "registry:lib",
      "target": "@lib/auth/two-factor-plugin.ts"
    },
    {
      "path": "examples/start-shadcn-example/src/lib/auth/use-resend-cooldown.ts",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useState } from \"react\"\n\n/** Seconds a resend button stays disabled to keep users off the rate limit. */\nexport const RESEND_COOLDOWN_SECONDS = 60\n\n/**\n * Countdown state for \"resend code\" buttons.\n *\n * @param initialSeconds - Seconds to start at. Pass `0` when nothing has been\n *   sent yet, or the cooldown length when the flow arrives right after a send.\n */\nexport function useResendCooldown(initialSeconds = 0) {\n  const [cooldown, setCooldown] = useState(initialSeconds)\n\n  useEffect(() => {\n    if (cooldown <= 0) return\n\n    const interval = setInterval(() => {\n      setCooldown((current) => (current > 0 ? current - 1 : 0))\n    }, 1000)\n\n    return () => clearInterval(interval)\n  }, [cooldown])\n\n  // Stable so callers can start the cooldown from an effect without\n  // re-running it on every render.\n  const startCooldown = useCallback(\n    (seconds = RESEND_COOLDOWN_SECONDS) => setCooldown(seconds),\n    []\n  )\n\n  return {\n    cooldown,\n    isCoolingDown: cooldown > 0,\n    startCooldown\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/auth/use-resend-cooldown.ts"
    },
    {
      "path": "examples/start-shadcn-example/src/lib/auth/use-sign-in-continuation.ts",
      "content": "\"use client\"\n\nimport { useAuth } from \"@better-auth-ui/react\"\nimport { useCallback } from \"react\"\nimport {\n  isTwoFactorRedirect,\n  storeTwoFactorMethods,\n  TWO_FACTOR_PLUGIN_ID\n} from \"./two-factor-methods\"\n\n/**\n * Resolve what happens after a sign-in request succeeds.\n *\n * Better Auth withholds the session when a second factor is required and\n * answers with `{ twoFactorRedirect: true, twoFactorMethods }` instead, so no\n * sign-in strategy may navigate to `redirectTo` unconditionally. This hook is\n * the single place that decision lives — every password-based form calls it\n * from `onSuccess`.\n *\n * The enabled methods are stashed in session storage (names only, never a\n * code or token) and `redirectTo` rides along in the query string so the\n * challenge view can finish the original navigation.\n *\n * The two-factor plugin is looked up by its stable id, so sign-in forms stay\n * installable without either the two-factor components or a matching release\n * of `@better-auth-ui/core`.\n *\n * @returns A callback taking the resolved data of a sign-in mutation.\n */\nexport function useSignInContinuation() {\n  const { basePaths, navigate, plugins, redirectTo } = useAuth()\n\n  const twoFactorPath = plugins.find(\n    (plugin) => plugin.id === TWO_FACTOR_PLUGIN_ID\n  )?.viewPaths?.auth?.twoFactor\n\n  return useCallback(\n    (data: unknown) => {\n      if (twoFactorPath && isTwoFactorRedirect(data)) {\n        storeTwoFactorMethods(data.twoFactorMethods)\n\n        navigate({\n          to: `${basePaths.auth}/${twoFactorPath}?redirectTo=${encodeURIComponent(redirectTo)}`\n        })\n        return\n      }\n\n      navigate({ to: redirectTo })\n    },\n    [basePaths.auth, navigate, redirectTo, twoFactorPath]\n  )\n}\n",
      "type": "registry:lib",
      "target": "@lib/auth/use-sign-in-continuation.ts"
    },
    {
      "path": "examples/start-shadcn-example/src/lib/auth/use-two-factor-password.ts",
      "content": "\"use client\"\n\nimport { useAuth, useAuthPlugin, useListAccounts } from \"@better-auth-ui/react\"\n\nimport { twoFactorPlugin } from \"./two-factor-plugin\"\n\n/**\n * Whether two-factor management should ask for the account password.\n *\n * Better Auth's `allowPasswordless` option only waives the password for\n * accounts that have no credential account — a passkey-only user has no\n * password to type. Reading the linked accounts keeps the UI in step with\n * that rule instead of guessing from the option alone.\n */\nexport function useTwoFactorPasswordRequirement() {\n  const { authClient } = useAuth()\n  const { allowPasswordless } = useAuthPlugin(twoFactorPlugin)\n  const { data: accounts, isPending } = useListAccounts(authClient)\n\n  const hasCredentialAccount = accounts?.some(\n    (account) => account.providerId === \"credential\"\n  )\n\n  return {\n    isPending: allowPasswordless && isPending,\n    // Default to asking while the account list loads. Guessing \"no password\"\n    // early would submit an empty body for a credential user and fail the\n    // request; guessing \"password\" costs a passkey-only user nothing, because\n    // the field disappears as soon as the list resolves.\n    requiresPassword:\n      !allowPasswordless || isPending || Boolean(hasCredentialAccount)\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/auth/use-two-factor-password.ts"
    }
  ],
  "type": "registry:component"
}