{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "oauth-provider",
  "title": "OAuth Provider",
  "description": "OAuth Provider plugin: authorization screens, connected applications, and personal or organization OAuth client management.",
  "dependencies": [
    "@better-auth-ui/core@latest",
    "@better-auth-ui/react@latest",
    "@tanstack/react-form",
    "@tanstack/react-query",
    "better-auth",
    "cn",
    "date-fns",
    "lucide-react"
  ],
  "registryDependencies": [
    "alert-dialog",
    "avatar",
    "badge",
    "button",
    "calendar",
    "card",
    "checkbox",
    "combobox",
    "dialog",
    "empty",
    "field",
    "https://better-auth-ui.com/r/radix-nova/sign-up.json",
    "https://better-auth-ui.com/r/radix-nova/user-avatar.json",
    "input",
    "input-group",
    "item",
    "popover",
    "select",
    "separator",
    "skeleton",
    "slider",
    "sonner",
    "spinner",
    "switch",
    "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/last-login-method/last-used-badge.tsx",
      "content": "\"use client\"\n\nimport { useLastLoginMethod } from \"@better-auth-ui/react\"\n\nimport { Badge } from \"@/components/ui/badge\"\n\nexport type LastUsedBadgeProps = {\n  /** Login method IDs that should display the indicator. */\n  method: string | string[]\n  /** Use the shorter label in constrained layouts. */\n  compact?: boolean\n  /** Float the compact indicator over the top-right edge of its container. */\n  floating?: boolean\n}\n\n/**\n * Displays an indicator when one of the supplied method IDs matches Better\n * Auth's stored last login method.\n */\nexport function LastUsedBadge({\n  method,\n  compact,\n  floating\n}: LastUsedBadgeProps) {\n  const { method: lastLoginMethod, localization } = useLastLoginMethod()\n  const methods = Array.isArray(method) ? method : [method]\n\n  if (!lastLoginMethod || !methods.includes(lastLoginMethod)) return null\n\n  return (\n    <Badge\n      className={\n        floating\n          ? \"pointer-events-none absolute top-0 right-0 z-10 translate-x-1/4 -translate-y-1/2 shadow-sm\"\n          : undefined\n      }\n      variant=\"secondary\"\n    >\n      {compact || floating ? localization.lastUsedShort : localization.lastUsed}\n    </Badge>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/last-login-method/last-used-badge.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/oauth-provider/authorized-application-skeleton.tsx",
      "content": "\"use client\"\n\nimport { Item, ItemContent, ItemMedia } from \"@/components/ui/item\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\n\nexport function AuthorizedApplicationSkeleton() {\n  return (\n    <Item>\n      <ItemMedia>\n        <Skeleton className=\"size-10 shrink-0 rounded-md\" />\n      </ItemMedia>\n      <ItemContent>\n        <Skeleton className=\"h-4 w-32\" />\n        <Skeleton className=\"h-3 w-40\" />\n\n        <div className=\"flex gap-1.5\">\n          <Skeleton className=\"h-5 w-20 rounded-full\" />\n          <Skeleton className=\"h-5 w-24 rounded-full\" />\n        </div>\n      </ItemContent>\n    </Item>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/authorized-application-skeleton.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/oauth-provider/authorized-application.tsx",
      "content": "\"use client\"\n\nimport type { OAuthProviderAuthClient } from \"@better-auth-ui/core/plugins/oauth-provider\"\nimport {\n  type AuthorizedOAuthApplication,\n  resolveOAuthScopeMetadata,\n  sanitizeOAuthClientUrl\n} from \"@better-auth-ui/core/plugins/oauth-provider\"\nimport { useAuth, useAuthPlugin } from \"@better-auth-ui/react\"\nimport { usePublicOAuthClient } from \"@better-auth-ui/react/plugins/oauth-provider\"\nimport { ShieldCheck, ChevronRight, Check } from \"lucide-react\"\nimport { useState } from \"react\"\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Item,\n  ItemActions,\n  ItemContent,\n  ItemDescription,\n  ItemMedia,\n  ItemTitle\n} from \"@/components/ui/item\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { oauthProviderPlugin } from \"@/lib/auth/oauth-provider-plugin\"\nimport { RemoveAuthorizationDialog } from \"./remove-authorization-dialog\"\n\nexport type AuthorizedApplicationProps = {\n  /** @remarks `AuthorizedOAuthApplication` */\n  application: AuthorizedOAuthApplication\n}\n\n/**\n * A single authorized application row.\n *\n * Each row loads its own public client metadata so one slow or missing\n * application never blocks the rest of the card.\n */\nexport function AuthorizedApplication({\n  application\n}: AuthorizedApplicationProps) {\n  const { authClient } = useAuth()\n  const { localization, scopeMetadata } = useAuthPlugin(oauthProviderPlugin)\n  const [removeOpen, setRemoveOpen] = useState(false)\n\n  const publicClient = usePublicOAuthClient(\n    authClient as OAuthProviderAuthClient,\n    application.clientId\n  )\n\n  const client = publicClient.data\n  const clientName = client?.client_name || application.clientId\n  const logoUrl = sanitizeOAuthClientUrl(client?.logo_uri)\n  const websiteUrl = sanitizeOAuthClientUrl(client?.client_uri)\n\n  return (\n    <Item className=\"grid grid-cols-[auto_minmax(0,1fr)] items-start gap-x-3 gap-y-4 p-4 sm:grid-cols-[auto_minmax(0,1fr)_auto]\">\n      <ItemMedia variant=\"image\">\n        {publicClient.isPending ? (\n          <Skeleton className=\"size-10 shrink-0 rounded-md\" />\n        ) : (\n          <Avatar className=\"size-10 shrink-0 rounded-md\">\n            <AvatarImage\n              alt={clientName}\n              referrerPolicy=\"no-referrer\"\n              src={logoUrl}\n            />\n            <AvatarFallback className=\"rounded-md\">\n              <ShieldCheck className=\"size-4.5\" />\n            </AvatarFallback>\n          </Avatar>\n        )}\n      </ItemMedia>\n      <ItemContent className=\"min-w-0\">\n        {publicClient.isPending ? (\n          <Skeleton className=\"h-4 w-32\" />\n        ) : (\n          <ItemTitle>{clientName}</ItemTitle>\n        )}\n\n        {websiteUrl ? (\n          <ItemDescription>\n            <a\n              className=\"truncate text-xs text-muted-foreground underline-offset-4 hover:underline\"\n              href={websiteUrl}\n              rel=\"noreferrer\"\n              target=\"_blank\"\n            >\n              {websiteUrl}\n            </a>\n          </ItemDescription>\n        ) : null}\n\n        {application.updatedAt ? (\n          <ItemDescription>\n            {`${localization.lastAuthorized} ${application.updatedAt.toLocaleDateString(\n              undefined,\n              { dateStyle: \"medium\" }\n            )}`}\n          </ItemDescription>\n        ) : null}\n      </ItemContent>\n      <ItemActions className=\"col-start-2 sm:col-start-3 sm:row-start-1\">\n        <Button size=\"sm\" variant=\"outline\" onClick={() => setRemoveOpen(true)}>\n          {localization.removeAuthorization}\n        </Button>\n\n        <RemoveAuthorizationDialog\n          application={application}\n          clientName={clientName}\n          open={removeOpen}\n          onOpenChange={setRemoveOpen}\n        />\n      </ItemActions>\n      {application.scopes.length > 0 ? (\n        <details className=\"group/permissions col-span-full sm:col-start-2 sm:col-span-2\">\n          <summary className=\"flex w-fit cursor-pointer list-none items-center gap-2 rounded-sm text-sm text-muted-foreground focus-visible:outline-2 focus-visible:outline-ring [&::-webkit-details-marker]:hidden\">\n            <ChevronRight\n              aria-hidden=\"true\"\n              className=\"size-4 shrink-0 group-open/permissions:rotate-90\"\n            />\n            {localization.permissions}{\" \"}\n            <span>({application.scopes.length})</span>\n          </summary>\n          <ul className=\"mt-3 grid gap-3 rounded-lg bg-muted/50 p-4\">\n            {application.scopes.map((scope) => {\n              const details = resolveOAuthScopeMetadata(scopeMetadata, scope, {\n                clientId: application.clientId,\n                requestedScopes: application.scopes\n              })\n              return (\n                <li key={scope} className=\"flex gap-3\">\n                  <Check\n                    aria-hidden=\"true\"\n                    className=\"mt-0.5 size-4 shrink-0 text-muted-foreground\"\n                  />\n                  <div className=\"grid min-w-0 gap-1\">\n                    <p className=\"text-sm font-medium break-words\">\n                      {details.label}\n                    </p>\n                    {details.description ? (\n                      <p className=\"text-sm leading-5 text-muted-foreground\">\n                        {details.description}\n                      </p>\n                    ) : null}\n                  </div>\n                </li>\n              )\n            })}\n          </ul>\n        </details>\n      ) : null}\n    </Item>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/authorized-application.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/oauth-provider/authorized-applications-empty.tsx",
      "content": "\"use client\"\n\nimport { useAuthPlugin } from \"@better-auth-ui/react\"\nimport { ShieldCheck } from \"lucide-react\"\n\nimport {\n  Empty,\n  EmptyDescription,\n  EmptyHeader,\n  EmptyMedia,\n  EmptyTitle\n} from \"@/components/ui/empty\"\nimport { oauthProviderPlugin } from \"@/lib/auth/oauth-provider-plugin\"\n\nexport function AuthorizedApplicationsEmpty() {\n  const { localization } = useAuthPlugin(oauthProviderPlugin)\n\n  return (\n    <Empty>\n      <EmptyHeader>\n        <EmptyMedia variant=\"icon\">\n          <ShieldCheck />\n        </EmptyMedia>\n        <EmptyTitle>{localization.noConnectedApplications}</EmptyTitle>\n        <EmptyDescription>\n          {localization.connectedApplicationsDescription}\n        </EmptyDescription>\n      </EmptyHeader>\n    </Empty>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/authorized-applications-empty.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/oauth-provider/authorized-applications.tsx",
      "content": "\"use client\"\n\nimport {\n  groupOAuthConsents,\n  type OAuthProviderAuthClient\n} from \"@better-auth-ui/core/plugins/oauth-provider\"\nimport { useAuth, useAuthPlugin } from \"@better-auth-ui/react\"\nimport { useListOAuthConsents } from \"@better-auth-ui/react/plugins/oauth-provider\"\nimport { Fragment } from \"react\"\n\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport { ItemGroup, ItemSeparator } from \"@/components/ui/item\"\nimport { oauthProviderPlugin } from \"@/lib/auth/oauth-provider-plugin\"\nimport { cn } from \"cn\"\nimport { AuthorizedApplication } from \"./authorized-application\"\nimport { AuthorizedApplicationSkeleton } from \"./authorized-application-skeleton\"\nimport { AuthorizedApplicationsEmpty } from \"./authorized-applications-empty\"\n\nexport type AuthorizedApplicationsProps = {\n  className?: string\n}\n\n/**\n * Security card listing the OAuth applications this account has authorized.\n *\n * It manages consent records, not sign-in sessions and not live access\n * tokens. Better Auth can store more than one consent per client, so records\n * are grouped by client ID and rendered as a single application.\n */\nexport function AuthorizedApplications({\n  className\n}: AuthorizedApplicationsProps) {\n  const { authClient } = useAuth()\n  const { localization } = useAuthPlugin(oauthProviderPlugin)\n\n  const { data: consents, isPending } = useListOAuthConsents(\n    authClient as OAuthProviderAuthClient\n  )\n\n  const applications = groupOAuthConsents(consents)\n\n  return (\n    <div className={cn(\"flex flex-col gap-3\", className)}>\n      <h2 className=\"truncate text-sm font-semibold\">\n        {localization.connectedApplications}\n      </h2>\n\n      <Card className=\"p-0\">\n        <CardContent className=\"p-0\">\n          {isPending ? (\n            <AuthorizedApplicationSkeleton />\n          ) : !applications.length ? (\n            <AuthorizedApplicationsEmpty />\n          ) : (\n            <ItemGroup className=\"gap-0\">\n              {applications.map((application, index) => (\n                <Fragment key={application.clientId}>\n                  {index > 0 && <ItemSeparator />}\n                  <AuthorizedApplication application={application} />\n                </Fragment>\n              ))}\n            </ItemGroup>\n          )}\n        </CardContent>\n      </Card>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/authorized-applications.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/oauth-provider/oauth-clients.tsx",
      "content": "\"use client\"\n\nimport {\n  validateAbsoluteUrlList,\n  validateStringLength\n} from \"@better-auth-ui/core\"\nimport {\n  createBetterAuthOAuthClientManager,\n  type ManagedOAuthClient,\n  type OAuthClientInput,\n  type OAuthClientManager,\n  type OAuthClientOwner,\n  type OAuthProviderAuthClient\n} from \"@better-auth-ui/core/plugins/oauth-provider\"\nimport {\n  useAuth,\n  useAuthPlugin,\n  useCopyToClipboard,\n  useSession\n} from \"@better-auth-ui/react\"\nimport {\n  useCreateOAuthClient,\n  useDeleteOAuthClient,\n  useOAuthClients,\n  useRotateOAuthClientSecret,\n  useSetOAuthClientDisabled,\n  useUpdateOAuthClient\n} from \"@better-auth-ui/react/plugins/oauth-provider\"\nimport {\n  Check,\n  Code2,\n  Copy,\n  Pencil,\n  Plus,\n  RotateCcwKey,\n  Trash2\n} from \"lucide-react\"\nimport { useMemo, useState } from \"react\"\nimport { toast } from \"sonner\"\nimport {\n  AlertDialog,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle\n} from \"@/components/ui/alert-dialog\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button, buttonVariants } from \"@/components/ui/button\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle\n} from \"@/components/ui/dialog\"\nimport {\n  Field,\n  FieldDescription,\n  FieldGroup,\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  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue\n} from \"@/components/ui/select\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { Switch } from \"@/components/ui/switch\"\nimport { Textarea } from \"@/components/ui/textarea\"\nimport { oauthProviderPlugin } from \"@/lib/auth/oauth-provider-plugin\"\nimport { cn } from \"cn\"\nimport { isAuthFormFieldInvalid, useAuthForm } from \"../auth-form\"\n\ntype ClientAction =\n  | { kind: \"delete\"; client: ManagedOAuthClient }\n  | { kind: \"rotate\"; client: ManagedOAuthClient }\n\ntype OAuthClientFormValues = {\n  applicationType: \"native\" | \"web\"\n  clientName: string\n  clientUri: string\n  logoUri: string\n  redirectUris: string\n  scope: string\n}\n\nexport type OAuthClientsProps = {\n  manager: OAuthClientManager\n  owner: OAuthClientOwner\n  ownerKey?: string\n  className?: string\n}\n\nconst uniqueLines = (value: string) =>\n  Array.from(\n    new Set(\n      value\n        .split(/\\r?\\n/)\n        .map((entry) => entry.trim())\n        .filter(Boolean)\n    )\n  )\n\nconst getOAuthClientFormValues = (\n  client?: ManagedOAuthClient\n): OAuthClientFormValues => ({\n  applicationType: client?.application_type === \"native\" ? \"native\" : \"web\",\n  clientName: client?.client_name ?? \"\",\n  clientUri: client?.client_uri ?? \"\",\n  logoUri: client?.logo_uri ?? \"\",\n  redirectUris: client?.redirect_uris.join(\"\\n\") ?? \"\",\n  scope: client?.scope ?? \"\"\n})\n\nexport function OAuthClients({\n  manager,\n  owner,\n  ownerKey,\n  className\n}: OAuthClientsProps) {\n  const { localization } = useAuth()\n  const { localization: oauthLocalization } = useAuthPlugin(oauthProviderPlugin)\n  const clients = useOAuthClients(manager, owner, ownerKey)\n  const createClient = useCreateOAuthClient(manager, owner, ownerKey)\n  const updateClient = useUpdateOAuthClient(manager, owner, ownerKey)\n  const deleteClient = useDeleteOAuthClient(manager, owner, ownerKey)\n  const rotateSecret = useRotateOAuthClientSecret(manager, owner, ownerKey)\n  const setDisabled = useSetOAuthClientDisabled(manager, owner, ownerKey)\n  const [editorOpen, setEditorOpen] = useState(false)\n  const [editingClient, setEditingClient] = useState<ManagedOAuthClient>()\n  const [action, setAction] = useState<ClientAction>()\n  const [secret, setSecret] = useState<ManagedOAuthClient>()\n  const { copied, copy, reset } = useCopyToClipboard({\n    onError: (error) => {\n      console.error(\"[Better Auth UI] Copy failed\", error)\n      toast.error(localization.errors.copyFailed)\n    }\n  })\n  const form = useAuthForm({\n    defaultValues: getOAuthClientFormValues(),\n    onSubmit: async ({ value }) => {\n      const input: OAuthClientInput = {\n        client_name: value.clientName.trim(),\n        application_type: value.applicationType,\n        redirect_uris: uniqueLines(value.redirectUris),\n        client_uri: value.clientUri.trim() || undefined,\n        logo_uri: value.logoUri.trim() || undefined,\n        scope: value.scope.trim() || undefined\n      }\n\n      try {\n        if (editingClient) {\n          await updateClient.mutateAsync({\n            clientId: editingClient.client_id,\n            update: input\n          })\n          setEditorOpen(false)\n          return\n        }\n\n        const client = await createClient.mutateAsync(input)\n        setEditorOpen(false)\n        setSecret(client)\n      } catch {\n        // The mutation keeps its error state for the host application.\n      }\n    }\n  })\n\n  const openCreate = () => {\n    setEditingClient(undefined)\n    form.reset(getOAuthClientFormValues())\n    setEditorOpen(true)\n  }\n\n  const confirmAction = () => {\n    if (!action) return\n\n    if (action.kind === \"delete\") {\n      deleteClient.mutate(action.client.client_id, {\n        onSuccess: () => setAction(undefined)\n      })\n      return\n    }\n\n    rotateSecret.mutate(action.client.client_id, {\n      onSuccess: (client) => {\n        setAction(undefined)\n        setSecret(client)\n      }\n    })\n  }\n\n  return (\n    <div className={cn(\"flex flex-col gap-3\", className)}>\n      <div className=\"flex items-end justify-between gap-3\">\n        <div className=\"flex min-w-0 flex-col gap-1\">\n          <h2 className=\"truncate text-sm font-semibold\">\n            {oauthLocalization.oauthClients}\n          </h2>\n          <p className=\"text-sm text-muted-foreground\">\n            {oauthLocalization.oauthClientsDescription}\n          </p>\n        </div>\n        <Button\n          className=\"shrink-0\"\n          size=\"sm\"\n          disabled={clients.isPending}\n          onClick={openCreate}\n        >\n          <Plus />\n          {oauthLocalization.createClient}\n        </Button>\n      </div>\n\n      <Card className=\"p-0\">\n        <CardContent className=\"flex flex-col gap-0 p-0\">\n          {clients.isPending ? (\n            <div className=\"flex min-h-28 items-center justify-center\">\n              <Spinner />\n              <span className=\"sr-only\">{oauthLocalization.oauthClients}</span>\n            </div>\n          ) : clients.data?.length ? (\n            clients.data.map((client) => (\n              <div\n                key={client.client_id}\n                className=\"flex flex-col gap-3 border-b border-dashed p-4 last:border-0 sm:flex-row sm:items-start sm:justify-between\"\n              >\n                <div className=\"flex min-w-0 gap-3\">\n                  <div className=\"mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted\">\n                    <Code2 className=\"size-4\" />\n                  </div>\n                  <div className=\"min-w-0\">\n                    <div className=\"flex flex-wrap items-center gap-2\">\n                      <p className=\"truncate text-sm font-medium\">\n                        {client.client_name || oauthLocalization.application}\n                      </p>\n                      <Badge\n                        variant={client.disabled ? \"destructive\" : \"secondary\"}\n                      >\n                        {client.disabled\n                          ? oauthLocalization.disabled\n                          : oauthLocalization.enabled}\n                      </Badge>\n                    </div>\n                    <p className=\"mt-1 truncate font-mono text-xs text-muted-foreground\">\n                      {client.client_id}\n                    </p>\n                    <p className=\"mt-1 text-xs text-muted-foreground\">\n                      {client.redirect_uris.length}{\" \"}\n                      {oauthLocalization.redirectUrls.toLowerCase()}\n                    </p>\n                  </div>\n                </div>\n\n                <div className=\"flex shrink-0 flex-wrap items-center gap-1\">\n                  {manager.setDisabled && (\n                    <Switch\n                      aria-label={\n                        client.disabled\n                          ? oauthLocalization.disabled\n                          : oauthLocalization.enabled\n                      }\n                      checked={!client.disabled}\n                      disabled={setDisabled.isPending}\n                      onCheckedChange={(enabled) =>\n                        setDisabled.mutate({\n                          clientId: client.client_id,\n                          disabled: !enabled\n                        })\n                      }\n                    />\n                  )}\n                  <Button\n                    size=\"icon-sm\"\n                    variant=\"ghost\"\n                    aria-label={oauthLocalization.editClient}\n                    onClick={() => {\n                      setEditingClient(client)\n                      form.reset(getOAuthClientFormValues(client))\n                      setEditorOpen(true)\n                    }}\n                  >\n                    <Pencil />\n                  </Button>\n                  <Button\n                    size=\"icon-sm\"\n                    variant=\"ghost\"\n                    aria-label={oauthLocalization.rotateSecret}\n                    onClick={() => setAction({ kind: \"rotate\", client })}\n                  >\n                    <RotateCcwKey />\n                  </Button>\n                  <Button\n                    size=\"icon-sm\"\n                    variant=\"ghost\"\n                    aria-label={oauthLocalization.deleteClient}\n                    onClick={() => setAction({ kind: \"delete\", client })}\n                  >\n                    <Trash2 />\n                  </Button>\n                </div>\n              </div>\n            ))\n          ) : (\n            <div className=\"flex min-h-40 flex-col items-center justify-center gap-3 p-6 text-center\">\n              <div className=\"flex size-10 items-center justify-center rounded-xl bg-muted\">\n                <Code2 />\n              </div>\n              <div className=\"flex max-w-sm flex-col gap-1\">\n                <p className=\"text-sm font-medium\">\n                  {oauthLocalization.noOAuthClients}\n                </p>\n                <p className=\"text-sm text-muted-foreground\">\n                  {oauthLocalization.noOAuthClientsDescription}\n                </p>\n              </div>\n              <Button size=\"sm\" variant=\"outline\" onClick={openCreate}>\n                {oauthLocalization.createClient}\n              </Button>\n            </div>\n          )}\n        </CardContent>\n      </Card>\n\n      <Dialog open={editorOpen} onOpenChange={setEditorOpen}>\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                  {editingClient ? <Pencil /> : <Plus />}\n                  {editingClient\n                    ? oauthLocalization.editClient\n                    : oauthLocalization.createClient}\n                </DialogTitle>\n                <DialogDescription>\n                  {oauthLocalization.oauthClientsDescription}\n                </DialogDescription>\n              </DialogHeader>\n              <FieldGroup>\n                <form.AppField\n                  name=\"clientName\"\n                  validators={{\n                    onChange: ({ value }) =>\n                      validateStringLength(value, {\n                        requiredMessage: localization.auth.fieldRequired,\n                        trim: true\n                      })\n                  }}\n                >\n                  {(field) => (\n                    <Field\n                      data-invalid={isAuthFormFieldInvalid(field.state.meta)}\n                    >\n                      <FieldLabel htmlFor=\"oauth-client-name\">\n                        {oauthLocalization.clientName}\n                      </FieldLabel>\n                      <Input\n                        autoFocus\n                        id=\"oauth-client-name\"\n                        name={field.name}\n                        onBlur={field.handleBlur}\n                        onChange={(event) =>\n                          field.handleChange(event.target.value)\n                        }\n                        required\n                        value={field.state.value}\n                        aria-invalid={isAuthFormFieldInvalid(field.state.meta)}\n                      />\n                      <field.AuthFormFieldError />\n                    </Field>\n                  )}\n                </form.AppField>\n                <form.Field name=\"applicationType\">\n                  {(field) => (\n                    <Field>\n                      <FieldLabel htmlFor=\"oauth-application-type\">\n                        {oauthLocalization.applicationType}\n                      </FieldLabel>\n                      <Select\n                        name={field.name}\n                        onValueChange={(value) =>\n                          field.handleChange(value as \"native\" | \"web\")\n                        }\n                        value={field.state.value}\n                      >\n                        <SelectTrigger\n                          id=\"oauth-application-type\"\n                          className=\"w-full\"\n                        >\n                          <SelectValue />\n                        </SelectTrigger>\n                        <SelectContent>\n                          <SelectItem value=\"web\">\n                            {oauthLocalization.webApplication}\n                          </SelectItem>\n                          <SelectItem value=\"native\">\n                            {oauthLocalization.nativeApplication}\n                          </SelectItem>\n                        </SelectContent>\n                      </Select>\n                    </Field>\n                  )}\n                </form.Field>\n                <form.AppField\n                  name=\"redirectUris\"\n                  validators={{\n                    onChange: ({ value }) =>\n                      validateAbsoluteUrlList(value, {\n                        invalidMessage: oauthLocalization.invalidUrl,\n                        requiredMessage: localization.auth.fieldRequired\n                      })\n                  }}\n                >\n                  {(field) => (\n                    <Field\n                      data-invalid={isAuthFormFieldInvalid(field.state.meta)}\n                    >\n                      <FieldLabel htmlFor=\"oauth-redirect-uris\">\n                        {oauthLocalization.redirectUrls}\n                      </FieldLabel>\n                      <Textarea\n                        id=\"oauth-redirect-uris\"\n                        name={field.name}\n                        onBlur={field.handleBlur}\n                        onChange={(event) =>\n                          field.handleChange(event.target.value)\n                        }\n                        required\n                        rows={3}\n                        value={field.state.value}\n                        aria-invalid={isAuthFormFieldInvalid(field.state.meta)}\n                      />\n                      <FieldDescription>\n                        {oauthLocalization.redirectUrlsDescription}\n                      </FieldDescription>\n                      <field.AuthFormFieldError />\n                    </Field>\n                  )}\n                </form.AppField>\n                {(\n                  [\n                    [\n                      \"clientUri\",\n                      \"oauth-client-uri\",\n                      oauthLocalization.applicationUrl,\n                      \"url\"\n                    ],\n                    [\n                      \"logoUri\",\n                      \"oauth-logo-uri\",\n                      oauthLocalization.logoUrl,\n                      \"url\"\n                    ],\n                    [\"scope\", \"oauth-scopes\", oauthLocalization.scopes, \"text\"]\n                  ] as const\n                ).map(([name, id, label, type]) => (\n                  <form.Field key={name} name={name}>\n                    {(field) => (\n                      <Field>\n                        <FieldLabel htmlFor={id}>{label}</FieldLabel>\n                        <Input\n                          id={id}\n                          name={field.name}\n                          onBlur={field.handleBlur}\n                          onChange={(event) =>\n                            field.handleChange(event.target.value)\n                          }\n                          placeholder={\n                            name === \"scope\"\n                              ? \"openid profile email\"\n                              : undefined\n                          }\n                          type={type}\n                          value={field.state.value}\n                        />\n                      </Field>\n                    )}\n                  </form.Field>\n                ))}\n              </FieldGroup>\n              <DialogFooter>\n                <DialogClose\n                  type=\"button\"\n                  className={buttonVariants({ variant: \"outline\" })}\n                >\n                  {oauthLocalization.cancel}\n                </DialogClose>\n                <form.AuthFormSubmitButton>\n                  {editingClient\n                    ? oauthLocalization.saveChanges\n                    : oauthLocalization.createClient}\n                </form.AuthFormSubmitButton>\n              </DialogFooter>\n            </form.AuthFormRoot>\n          </form.AppForm>\n        </DialogContent>\n      </Dialog>\n\n      <AlertDialog\n        open={Boolean(action)}\n        onOpenChange={(open) => !open && setAction(undefined)}\n      >\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogTitle>\n              {action?.kind === \"delete\"\n                ? oauthLocalization.deleteClientTitle\n                : oauthLocalization.rotateSecretTitle}\n            </AlertDialogTitle>\n            <AlertDialogDescription>\n              {action?.kind === \"delete\"\n                ? oauthLocalization.deleteClientDescription\n                : oauthLocalization.rotateSecretDescription}\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel>{oauthLocalization.cancel}</AlertDialogCancel>\n            <Button\n              type=\"button\"\n              variant={action?.kind === \"delete\" ? \"destructive\" : \"default\"}\n              disabled={deleteClient.isPending || rotateSecret.isPending}\n              onClick={confirmAction}\n            >\n              {(deleteClient.isPending || rotateSecret.isPending) && (\n                <Spinner />\n              )}\n              {action?.kind === \"delete\"\n                ? oauthLocalization.deleteClient\n                : oauthLocalization.rotateSecret}\n            </Button>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n\n      <Dialog\n        open={Boolean(secret)}\n        onOpenChange={(open) => {\n          if (!open) {\n            setSecret(undefined)\n            reset()\n          }\n        }}\n      >\n        <DialogContent showCloseButton={false}>\n          <DialogHeader>\n            <DialogTitle className=\"flex items-center gap-2\">\n              <Code2 />\n              {secret?.client_name || oauthLocalization.clientSecret}\n            </DialogTitle>\n            <DialogDescription>\n              {oauthLocalization.clientSecretWarning}\n            </DialogDescription>\n          </DialogHeader>\n          <FieldGroup>\n            <Field>\n              <FieldLabel htmlFor=\"new-oauth-client-id\">\n                {oauthLocalization.clientId}\n              </FieldLabel>\n              <Input\n                id=\"new-oauth-client-id\"\n                value={secret?.client_id ?? \"\"}\n                readOnly\n                className=\"font-mono text-xs\"\n              />\n            </Field>\n            <Field>\n              <FieldLabel htmlFor=\"new-oauth-client-secret\">\n                {oauthLocalization.clientSecret}\n              </FieldLabel>\n              <InputGroup>\n                <InputGroupInput\n                  id=\"new-oauth-client-secret\"\n                  value={secret?.client_secret ?? \"\"}\n                  readOnly\n                  className=\"font-mono text-xs\"\n                />\n                <InputGroupAddon align=\"inline-end\">\n                  <InputGroupButton\n                    size=\"icon-xs\"\n                    aria-label={\n                      copied\n                        ? localization.settings.copiedToClipboard\n                        : localization.settings.copyToClipboard\n                    }\n                    onClick={() =>\n                      secret?.client_secret && copy(secret.client_secret)\n                    }\n                  >\n                    {copied ? <Check /> : <Copy />}\n                  </InputGroupButton>\n                </InputGroupAddon>\n              </InputGroup>\n            </Field>\n          </FieldGroup>\n          <DialogFooter>\n            <Button\n              onClick={() => {\n                setSecret(undefined)\n                reset()\n              }}\n            >\n              {oauthLocalization.continue}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </div>\n  )\n}\n\nexport function UserOAuthClients(\n  props: Omit<OAuthClientsProps, \"manager\" | \"owner\" | \"ownerKey\">\n) {\n  const { authClient } = useAuth<OAuthProviderAuthClient>()\n  const { clientManager } = useAuthPlugin(oauthProviderPlugin)\n  const { data: session } = useSession(authClient)\n  const defaultManager = useMemo(\n    () => createBetterAuthOAuthClientManager(authClient),\n    [authClient]\n  )\n\n  return (\n    <OAuthClients\n      {...props}\n      manager={clientManager ?? defaultManager}\n      owner={{ type: \"user\" }}\n      ownerKey={session?.user.id}\n    />\n  )\n}\n\nexport function OrganizationOAuthClients({\n  organizationId,\n  organizationSlug,\n  ...props\n}: Omit<OAuthClientsProps, \"manager\" | \"owner\" | \"ownerKey\"> & {\n  organizationId: string\n  organizationSlug: string\n}) {\n  const { organizationClientManager } = useAuthPlugin(oauthProviderPlugin)\n\n  if (!organizationClientManager) return null\n\n  return (\n    <OAuthClients\n      {...props}\n      manager={organizationClientManager}\n      owner={{ type: \"organization\", organizationId, organizationSlug }}\n      ownerKey={`organization:${organizationId}:${organizationSlug}`}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/oauth-clients.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/oauth-provider/oauth-consent.tsx",
      "content": "\"use client\"\n\nimport {\n  getOAuthAuthorizationDestination,\n  type OAuthProviderAuthClient,\n  parseOAuthAuthorizationRequest,\n  resolveOAuthScopeMetadata,\n  sanitizeOAuthClientUrl\n} from \"@better-auth-ui/core/plugins/oauth-provider\"\nimport { useAuth, useAuthPlugin, useSession } from \"@better-auth-ui/react\"\nimport {\n  useOAuthConsent,\n  usePublicOAuthClient\n} from \"@better-auth-ui/react/plugins/oauth-provider\"\nimport {\n  AppWindow,\n  Check,\n  Ellipsis,\n  Link as LinkIcon,\n  Shield\n} from \"lucide-react\"\nimport { useSyncExternalStore } from \"react\"\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle\n} from \"@/components/ui/card\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { oauthProviderPlugin } from \"@/lib/auth/oauth-provider-plugin\"\nimport { cn } from \"cn\"\nimport { UserAvatar } from \"../user/user-avatar\"\n\nexport type OAuthConsentProps = {\n  className?: string\n  /** The complete signed query. Pass it from your router for same-page navigation. */\n  oauthQuery?: string\n}\n\nconst subscribeToQuery = (onChange: () => void) => {\n  window.addEventListener(\"popstate\", onChange)\n  return () => window.removeEventListener(\"popstate\", onChange)\n}\n\nconst interpolateClient = (template: string, clientName: string) =>\n  template.replace(\"{{client}}\", clientName)\n\nexport function OAuthConsent({ className, oauthQuery }: OAuthConsentProps) {\n  const { authClient } = useAuth()\n  const { localization, scopeMetadata } = useAuthPlugin(oauthProviderPlugin)\n  const oauthClient = authClient as OAuthProviderAuthClient\n  const { data: session, isPending: isSessionPending } = useSession(oauthClient)\n  const browserQuery = useSyncExternalStore(\n    subscribeToQuery,\n    () => window.location.search,\n    () => undefined\n  )\n  const query = oauthQuery ?? browserQuery\n  const request =\n    query === undefined ? undefined : parseOAuthAuthorizationRequest(query)\n  const publicClient = usePublicOAuthClient(oauthClient, request?.clientId, {\n    enabled: Boolean(session && request?.clientId),\n    oauthQuery: query,\n    retry: false,\n    staleTime: 0,\n    gcTime: 0\n  })\n  const consent = useOAuthConsent(oauthClient)\n  const client =\n    !publicClient.isFetching && !publicClient.isError\n      ? publicClient.data\n      : undefined\n  const destination =\n    client && query ? getOAuthAuthorizationDestination(query) : undefined\n  const clientName = client?.client_name || localization.application\n  const logoUrl = sanitizeOAuthClientUrl(client?.logo_uri)\n  const policyUrl = sanitizeOAuthClientUrl(client?.policy_uri)\n  const termsUrl = sanitizeOAuthClientUrl(client?.tos_uri)\n  const requestResolved = request !== undefined\n  const invalidRequest =\n    requestResolved &&\n    (!request.clientId ||\n      (!isSessionPending && !session) ||\n      publicClient.isError ||\n      (!publicClient.isPending &&\n        !publicClient.isFetching &&\n        session &&\n        !client))\n  const canRespond = Boolean(\n    request?.clientId && session && client && !consent.isPending\n  )\n\n  if (invalidRequest) {\n    return (\n      <Card className={cn(\"w-full max-w-md\", className)}>\n        <CardHeader>\n          <CardTitle role=\"heading\" aria-level={2} className=\"text-xl\">\n            {localization.invalidRequest}\n          </CardTitle>\n          <CardDescription>\n            {localization.invalidRequestDescription}\n          </CardDescription>\n        </CardHeader>\n      </Card>\n    )\n  }\n\n  return (\n    <Card\n      className={cn(\n        \"max-h-[calc(100dvh-2rem)] w-full max-w-lg gap-0 py-0\",\n        className\n      )}\n    >\n      <div\n        className=\"min-h-0 overflow-y-auto overscroll-contain focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\"\n        role=\"region\"\n        aria-label={interpolateClient(localization.authorize, clientName)}\n        tabIndex={0}\n      >\n        <CardHeader className=\"gap-5 p-6\">\n          <div className=\"flex items-center justify-center gap-5\">\n            {client ? (\n              <Avatar className=\"size-16\">\n                <AvatarImage\n                  alt={clientName}\n                  referrerPolicy=\"no-referrer\"\n                  src={logoUrl}\n                />\n                <AvatarFallback>\n                  <AppWindow className=\"size-7\" />\n                </AvatarFallback>\n              </Avatar>\n            ) : (\n              <Skeleton className=\"size-16 rounded-full\" />\n            )}\n            <Ellipsis\n              aria-hidden=\"true\"\n              className=\"size-5 text-muted-foreground\"\n            />\n            <UserAvatar\n              className=\"size-16\"\n              isPending={isSessionPending}\n              user={session?.user}\n            />\n          </div>\n          <div className=\"grid justify-items-center gap-1 text-center\">\n            <CardTitle\n              role=\"heading\"\n              aria-level={2}\n              className=\"max-w-full break-words text-xl font-semibold\"\n            >\n              {client ? clientName : <Skeleton className=\"h-6 w-36\" />}\n            </CardTitle>\n            <CardDescription>\n              {localization.authorizationRequest}\n            </CardDescription>\n            <div className=\"mt-2 flex max-w-full flex-wrap justify-center gap-x-1 text-sm text-muted-foreground\">\n              <span>{localization.signedInAs}</span>\n              {session ? (\n                <span className=\"break-all font-medium text-foreground\">\n                  {session.user.name || session.user.email}\n                </span>\n              ) : (\n                <Skeleton className=\"h-4 w-32\" />\n              )}\n            </div>\n          </div>\n        </CardHeader>\n\n        <CardContent className=\"px-4 pb-6 sm:px-6\">\n          <div className=\"flex flex-col gap-5 rounded-lg bg-muted/50 p-5\">\n            <div className=\"grid gap-3\">\n              <p className=\"text-sm font-medium\">\n                {interpolateClient(\n                  localization.requestedPermissions,\n                  clientName\n                )}\n              </p>\n\n              {client && request ? (\n                <ul className=\"grid gap-3\">\n                  {request.scopes.map((scope) => {\n                    const metadata = resolveOAuthScopeMetadata(\n                      scopeMetadata,\n                      scope,\n                      {\n                        clientId: request.clientId,\n                        requestedScopes: request.scopes\n                      }\n                    )\n\n                    return (\n                      <li className=\"flex gap-3\" key={scope}>\n                        <Check\n                          aria-hidden=\"true\"\n                          className=\"mt-0.5 size-5 shrink-0 text-muted-foreground\"\n                        />\n                        <div className=\"grid min-w-0 gap-1\">\n                          <p className=\"text-base leading-6 break-words\">\n                            {metadata.label}\n                          </p>\n                          {metadata.description ? (\n                            <p className=\"text-sm leading-5 text-muted-foreground\">\n                              {metadata.description}\n                            </p>\n                          ) : null}\n                        </div>\n                      </li>\n                    )\n                  })}\n                </ul>\n              ) : (\n                <div className=\"flex gap-3\">\n                  <Skeleton className=\"mt-0.5 size-4 shrink-0 rounded-full\" />\n                  <div className=\"grid flex-1 gap-2\">\n                    <Skeleton className=\"h-4 w-32\" />\n                    <Skeleton className=\"h-3 w-full max-w-64\" />\n                  </div>\n                </div>\n              )}\n            </div>\n\n            <Separator />\n\n            {destination ? (\n              <div className=\"flex gap-3 text-sm leading-5 text-muted-foreground\">\n                <LinkIcon\n                  aria-hidden=\"true\"\n                  className=\"mt-0.5 size-4 shrink-0\"\n                />\n                <p>\n                  {localization.redirectTo}{\" \"}\n                  <span className=\"block break-all font-medium text-foreground\">\n                    {destination}\n                  </span>\n                </p>\n              </div>\n            ) : null}\n            {policyUrl || termsUrl ? (\n              <div className=\"flex flex-wrap gap-x-4 gap-y-2 text-sm\">\n                {policyUrl ? (\n                  <a\n                    className=\"text-primary underline underline-offset-4\"\n                    href={policyUrl}\n                    rel=\"noreferrer\"\n                    target=\"_blank\"\n                  >\n                    {localization.privacyPolicy}\n                  </a>\n                ) : null}\n                {termsUrl ? (\n                  <a\n                    className=\"text-primary underline underline-offset-4\"\n                    href={termsUrl}\n                    rel=\"noreferrer\"\n                    target=\"_blank\"\n                  >\n                    {localization.termsOfService}\n                  </a>\n                ) : null}\n              </div>\n            ) : null}\n            <div className=\"flex gap-3 text-sm leading-5 text-muted-foreground\">\n              <Shield aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0\" />\n              <p>{localization.applicationInformation}</p>\n            </div>\n          </div>\n        </CardContent>\n      </div>\n\n      <CardFooter className=\"grid shrink-0 grid-cols-2 gap-3 p-4 sm:p-6\">\n        <Button\n          className=\"h-11\"\n          disabled={!canRespond}\n          variant=\"secondary\"\n          onClick={() => consent.mutate({ accept: false, oauth_query: query })}\n        >\n          {consent.isPending && consent.variables?.accept === false ? (\n            <Spinner />\n          ) : null}\n          {localization.cancel}\n        </Button>\n        <Button\n          className=\"h-11\"\n          disabled={!canRespond}\n          onClick={() => consent.mutate({ accept: true, oauth_query: query })}\n        >\n          {consent.isPending && consent.variables?.accept === true ? (\n            <Spinner />\n          ) : null}\n          {localization.allow}\n        </Button>\n      </CardFooter>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/oauth-consent.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/oauth-provider/oauth-select-account.tsx",
      "content": "\"use client\"\n\nimport type { ListDeviceSession } from \"@better-auth-ui/core/plugins/multi-session\"\nimport type { OAuthProviderMultiSessionAuthClient } from \"@better-auth-ui/core/plugins/oauth-provider\"\nimport {\n  type OAuthAuthorizationRequest,\n  parseOAuthAuthorizationRequest,\n  sanitizeOAuthClientUrl\n} from \"@better-auth-ui/core/plugins/oauth-provider\"\nimport { useAuth, useAuthPlugin, useSession } from \"@better-auth-ui/react\"\nimport {\n  useListDeviceSessions,\n  useSetActiveSession\n} from \"@better-auth-ui/react/plugins/multi-session\"\nimport {\n  useOAuthContinue,\n  usePublicOAuthClient\n} from \"@better-auth-ui/react/plugins/oauth-provider\"\nimport { ShieldCheck, ChevronRight } from \"lucide-react\"\nimport { useEffect, useState } from \"react\"\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle\n} from \"@/components/ui/card\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { oauthProviderPlugin } from \"@/lib/auth/oauth-provider-plugin\"\nimport { cn } from \"cn\"\nimport { UserAvatar } from \"../user/user-avatar\"\n\nexport type OAuthSelectAccountProps = {\n  className?: string\n}\n\nconst interpolateClient = (template: string, clientName: string) =>\n  template.replace(\"{{client}}\", clientName)\n\n/**\n * Account chooser for a signed OAuth authorization request.\n *\n * Switching accounts has to land before Better Auth resumes the request, so\n * picking a different session calls `multiSession.setActive()` first and only\n * then `oauth2.continue({ selected: true })`. Picking the account that is\n * already active skips the switch entirely.\n *\n * This screen deliberately has no sign-out or revoke actions — session\n * management belongs in security settings.\n */\nexport function OAuthSelectAccount({ className }: OAuthSelectAccountProps) {\n  const { authClient } = useAuth()\n  const { localization } = useAuthPlugin(oauthProviderPlugin)\n  const oauthClient = authClient as OAuthProviderMultiSessionAuthClient\n\n  const { data: session, isPending: isSessionPending } = useSession(oauthClient)\n  const [request, setRequest] = useState<OAuthAuthorizationRequest>()\n  const [pendingSessionId, setPendingSessionId] = useState<string>()\n\n  useEffect(() => {\n    setRequest(parseOAuthAuthorizationRequest(window.location.search))\n  }, [])\n\n  const publicClient = usePublicOAuthClient(oauthClient, request?.clientId, {\n    enabled: Boolean(session && request?.clientId)\n  })\n  const { data: deviceSessions, isPending: isDeviceSessionsPending } =\n    useListDeviceSessions(oauthClient)\n\n  const client = publicClient.data\n  const clientName = client?.client_name || localization.application\n  const logoUrl = sanitizeOAuthClientUrl(client?.logo_uri)\n\n  const setActiveSession = useSetActiveSession(oauthClient)\n  const oauthContinue = useOAuthContinue(oauthClient)\n\n  const requestResolved = request !== undefined\n  const invalidRequest =\n    requestResolved &&\n    (!request.clientId ||\n      (!isSessionPending && !session) ||\n      publicClient.isError ||\n      (!publicClient.isPending && session && !client))\n\n  const selectAccount = async (\n    deviceSession: ListDeviceSession<OAuthProviderMultiSessionAuthClient>\n  ) => {\n    setPendingSessionId(deviceSession.session.id)\n\n    try {\n      if (deviceSession.session.id !== session?.session.id) {\n        await setActiveSession.mutateAsync({\n          sessionToken: deviceSession.session.token\n        })\n      }\n\n      await oauthContinue.mutateAsync({ selected: true })\n    } catch {\n      // The error toaster surfaces the failure; re-enable the rows so the\n      // user can pick again.\n      setPendingSessionId(undefined)\n    }\n  }\n\n  if (invalidRequest) {\n    return (\n      <Card className={cn(\"w-full max-w-md\", className)}>\n        <CardHeader>\n          <CardTitle className=\"text-xl\">\n            {localization.invalidRequest}\n          </CardTitle>\n          <CardDescription>\n            {localization.invalidRequestDescription}\n          </CardDescription>\n        </CardHeader>\n      </Card>\n    )\n  }\n\n  const isBusy = pendingSessionId !== undefined\n\n  return (\n    <Card\n      className={cn(\n        \"max-h-[calc(100dvh-2rem)] w-full max-w-lg gap-0 overflow-y-auto py-0\",\n        className\n      )}\n    >\n      <CardHeader className=\"grid justify-items-center gap-5 p-6 text-center\">\n        <div className=\"grid justify-items-center gap-3\">\n          {client ? (\n            <Avatar className=\"size-16\">\n              <AvatarImage\n                alt={clientName}\n                referrerPolicy=\"no-referrer\"\n                src={logoUrl}\n              />\n              <AvatarFallback>\n                <ShieldCheck className=\"size-7\" />\n              </AvatarFallback>\n            </Avatar>\n          ) : (\n            <Skeleton className=\"size-16 rounded-full\" />\n          )}\n          {client ? (\n            <p className=\"max-w-full break-words text-base font-medium\">\n              {clientName}\n            </p>\n          ) : (\n            <Skeleton className=\"h-5 w-36\" />\n          )}\n        </div>\n        <div className=\"grid gap-1\">\n          <CardTitle className=\"text-xl font-semibold\">\n            {localization.selectAccount}\n          </CardTitle>\n          <CardDescription>\n            {interpolateClient(\n              localization.selectAccountDescription,\n              clientName\n            )}\n          </CardDescription>\n        </div>\n      </CardHeader>\n      <CardContent className=\"px-4 pb-6 sm:px-6\">\n        {isDeviceSessionsPending ? (\n          <div className=\"flex items-center gap-3 rounded-lg bg-muted/50 p-4\">\n            <UserAvatar isPending />\n            <div className=\"grid flex-1 gap-2\">\n              <Skeleton className=\"h-4 w-28\" />\n              <Skeleton className=\"h-3 w-40\" />\n            </div>\n          </div>\n        ) : !deviceSessions?.length ? (\n          <div className=\"grid justify-items-center gap-1 py-6 text-center\">\n            <p className=\"text-sm font-semibold\">{localization.noAccounts}</p>\n            <p className=\"text-sm text-muted-foreground\">\n              {interpolateClient(\n                localization.noAccountsDescription,\n                clientName\n              )}\n            </p>\n          </div>\n        ) : (\n          <ul className=\"divide-y divide-border overflow-hidden rounded-lg bg-muted/50\">\n            {deviceSessions.map((deviceSession) => {\n              const isCurrent = deviceSession.session.id === session?.session.id\n              const isSelecting = pendingSessionId === deviceSession.session.id\n              return (\n                <li key={deviceSession.session.id}>\n                  <Button\n                    className=\"h-auto min-h-20 w-full justify-start gap-3 rounded-none p-4 text-left whitespace-normal\"\n                    variant=\"ghost\"\n                    disabled={isBusy || !client || isSessionPending}\n                    aria-label={`${localization.continue}: ${deviceSession.user.name || deviceSession.user.email} (${deviceSession.user.email})`}\n                    onClick={() => selectAccount(deviceSession)}\n                  >\n                    <UserAvatar\n                      className=\"size-10 shrink-0\"\n                      user={deviceSession.user}\n                    />\n                    <span className=\"grid min-w-0 flex-1 gap-0.5\">\n                      <span className=\"break-words text-base font-medium\">\n                        {deviceSession.user.name || deviceSession.user.email}\n                      </span>\n                      {deviceSession.user.name ? (\n                        <span className=\"break-all text-sm text-muted-foreground\">\n                          {deviceSession.user.email}\n                        </span>\n                      ) : null}\n                    </span>\n                    {isCurrent ? (\n                      <Badge className=\"shrink-0\" variant=\"secondary\">\n                        {localization.currentAccount}\n                      </Badge>\n                    ) : null}\n                    {isSelecting ? (\n                      <Spinner />\n                    ) : (\n                      <ChevronRight\n                        aria-hidden=\"true\"\n                        className=\"size-4 shrink-0 text-muted-foreground\"\n                      />\n                    )}\n                  </Button>\n                </li>\n              )\n            })}\n          </ul>\n        )}\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/oauth-select-account.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/oauth-provider/oauth-sign-up.tsx",
      "content": "\"use client\"\n\nimport type { OAuthProviderAuthClient } from \"@better-auth-ui/core/plugins/oauth-provider\"\nimport {\n  hasOAuthPrompt,\n  type OAuthAuthorizationRequest,\n  parseOAuthAuthorizationRequest\n} from \"@better-auth-ui/core/plugins/oauth-provider\"\nimport { useAuth, useAuthPlugin } from \"@better-auth-ui/react\"\nimport {\n  useOAuthContinue,\n  usePublicOAuthClient\n} from \"@better-auth-ui/react/plugins/oauth-provider\"\nimport { useEffect, useState } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle\n} from \"@/components/ui/card\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { oauthProviderPlugin } from \"@/lib/auth/oauth-provider-plugin\"\nimport { cn } from \"cn\"\nimport type { SocialLayout } from \"../provider-buttons\"\nimport { SignUp } from \"../sign-up\"\n\nexport type OAuthSignUpProps = {\n  className?: string\n  socialLayout?: SocialLayout\n  socialPosition?: \"top\" | \"bottom\"\n}\n\nconst interpolateClient = (template: string, clientName: string) =>\n  template.replace(\"{{client}}\", clientName)\n\n/**\n * Sign-up view that resumes a signed OAuth authorization request.\n *\n * When Better Auth sends the user here with `prompt=create`, the ordinary\n * sign-up form still does the account creation. Only once that succeeds and\n * leaves a usable session does this call `oauth2.continue({ created: true })`\n * so Better Auth can finish the authorization it started.\n *\n * Without `prompt=create` this is just the normal sign-up view.\n */\nexport function OAuthSignUp({\n  className,\n  socialLayout,\n  socialPosition\n}: OAuthSignUpProps) {\n  const { authClient } = useAuth()\n  const { localization } = useAuthPlugin(oauthProviderPlugin)\n  const oauthClient = authClient as OAuthProviderAuthClient\n\n  const [request, setRequest] = useState<OAuthAuthorizationRequest>()\n  const [isCreated, setIsCreated] = useState(false)\n\n  useEffect(() => {\n    setRequest(parseOAuthAuthorizationRequest(window.location.search))\n  }, [])\n\n  const isOAuthSignUp = Boolean(request && hasOAuthPrompt(request, \"create\"))\n\n  const publicClient = usePublicOAuthClient(oauthClient, request?.clientId, {\n    enabled: isOAuthSignUp\n  })\n  const clientName = publicClient.data?.client_name || localization.application\n\n  const oauthContinue = useOAuthContinue(oauthClient)\n\n  // The account already exists at this point, so retrying continuation is the\n  // only sensible recovery — never send the user back through the form.\n  if (isCreated) {\n    return (\n      <Card className={cn(\"w-full max-w-sm\", className)}>\n        <CardHeader>\n          <CardTitle className=\"text-xl font-semibold\">\n            {localization.accountCreated}\n          </CardTitle>\n\n          <CardDescription>\n            {interpolateClient(\n              oauthContinue.isError\n                ? localization.continueFailed\n                : localization.continuing,\n              clientName\n            )}\n          </CardDescription>\n        </CardHeader>\n\n        {oauthContinue.isError && (\n          <CardFooter>\n            <Button\n              className=\"w-full\"\n              disabled={oauthContinue.isPending}\n              onClick={() => oauthContinue.mutate({ created: true })}\n            >\n              {oauthContinue.isPending && <Spinner />}\n\n              {localization.tryAgain}\n            </Button>\n          </CardFooter>\n        )}\n      </Card>\n    )\n  }\n\n  return (\n    <SignUp\n      className={className}\n      socialLayout={socialLayout}\n      socialPosition={socialPosition}\n      onSignUpSuccess={\n        isOAuthSignUp\n          ? () => {\n              setIsCreated(true)\n              oauthContinue.mutate({ created: true })\n            }\n          : undefined\n      }\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/oauth-sign-up.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/oauth-provider/remove-authorization-dialog.tsx",
      "content": "\"use client\"\n\nimport type {\n  AuthorizedOAuthApplication,\n  OAuthProviderAuthClient\n} from \"@better-auth-ui/core/plugins/oauth-provider\"\nimport { useAuth, useAuthPlugin } from \"@better-auth-ui/react\"\nimport { useDeleteOAuthConsent } from \"@better-auth-ui/react/plugins/oauth-provider\"\nimport { ShieldOff } from \"lucide-react\"\nimport { useState } from \"react\"\n\nimport {\n  AlertDialog,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogMedia,\n  AlertDialogTitle\n} from \"@/components/ui/alert-dialog\"\nimport { Button } from \"@/components/ui/button\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { oauthProviderPlugin } from \"@/lib/auth/oauth-provider-plugin\"\n\nexport type RemoveAuthorizationDialogProps = {\n  /** @remarks `AuthorizedOAuthApplication` */\n  application: AuthorizedOAuthApplication\n  clientName: string\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\n/**\n * Confirmation for removing every consent record tied to one OAuth client.\n *\n * The copy is deliberate: Better Auth's consent deletion removes the stored\n * approval, so the application must ask again — it does not revoke access or\n * refresh tokens that were already issued.\n */\nexport function RemoveAuthorizationDialog({\n  application,\n  clientName,\n  open,\n  onOpenChange\n}: RemoveAuthorizationDialogProps) {\n  const { authClient, localization } = useAuth()\n  const { localization: oauthLocalization } = useAuthPlugin(oauthProviderPlugin)\n  const [isRemoving, setIsRemoving] = useState(false)\n\n  const { mutateAsync: deleteConsent } = useDeleteOAuthConsent(\n    authClient as OAuthProviderAuthClient\n  )\n\n  const removeAuthorization = async () => {\n    setIsRemoving(true)\n\n    try {\n      // Sequential so a mid-list failure leaves a predictable server state\n      // that the refetched list reflects accurately.\n      for (const id of application.consentIds) {\n        await deleteConsent({ id })\n      }\n\n      onOpenChange(false)\n    } catch {\n      // The error toaster reports the failure; the dialog stays open so the\n      // remaining records can be retried.\n    } finally {\n      setIsRemoving(false)\n    }\n  }\n\n  return (\n    <AlertDialog open={open} onOpenChange={onOpenChange}>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogMedia>\n            <ShieldOff />\n          </AlertDialogMedia>\n\n          <AlertDialogTitle>\n            {oauthLocalization.removeAuthorizationTitle}\n          </AlertDialogTitle>\n\n          <AlertDialogDescription>\n            {oauthLocalization.removeAuthorizationDescription}\n          </AlertDialogDescription>\n        </AlertDialogHeader>\n\n        <p className=\"text-sm font-medium\">{clientName}</p>\n\n        <AlertDialogFooter>\n          <AlertDialogCancel disabled={isRemoving}>\n            {localization.settings.cancel}\n          </AlertDialogCancel>\n\n          <Button\n            type=\"button\"\n            variant=\"destructive\"\n            disabled={isRemoving}\n            onClick={removeAuthorization}\n          >\n            {isRemoving && <Spinner />}\n\n            {oauthLocalization.remove}\n          </Button>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/remove-authorization-dialog.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/password-strength-meter.tsx",
      "content": "\"use client\"\n\nimport {\n  evaluatePasswordStrength,\n  type PasswordStrengthLevel\n} from \"@better-auth-ui/core\"\nimport { useAuth } from \"@better-auth-ui/react\"\n\nimport { cn } from \"cn\"\n\n/** Fixed segment identities, so the bars keep their own React keys. */\nconst STRENGTH_SEGMENTS = [1, 2, 3, 4] as const\n\ntype FilledLevel = Exclude<PasswordStrengthLevel, \"empty\">\n\nconst segmentColors: Record<FilledLevel, string> = {\n  weak: \"bg-destructive\",\n  fair: \"bg-amber-500\",\n  good: \"bg-sky-500\",\n  strong: \"bg-emerald-500\"\n}\n\nexport type PasswordStrengthMeterProps = {\n  /** The password as typed. Renders nothing while it is empty. */\n  password: string\n  className?: string\n}\n\n/**\n * Four-segment strength hint shown while someone picks a new password.\n *\n * Renders nothing when `emailAndPassword.strengthMeter` is off or the field is\n * empty. The score never gates submission: your server rules stay the\n * authority on what is acceptable.\n */\nexport function PasswordStrengthMeter({\n  password,\n  className\n}: PasswordStrengthMeterProps) {\n  const { emailAndPassword, localization } = useAuth()\n\n  if (!emailAndPassword?.strengthMeter) return null\n\n  const { score, level } = evaluatePasswordStrength(password, {\n    minLength: emailAndPassword.minPasswordLength\n  })\n\n  if (level === \"empty\") return null\n\n  const levelLabels: Record<FilledLevel, string> = {\n    weak: localization.auth.passwordWeak,\n    fair: localization.auth.passwordFair,\n    good: localization.auth.passwordGood,\n    strong: localization.auth.passwordStrong\n  }\n\n  return (\n    <div className={cn(\"flex flex-col gap-1.5\", className)}>\n      {/* Decorative: the live region below is what gets announced. */}\n      <div aria-hidden=\"true\" className=\"flex gap-1\">\n        {STRENGTH_SEGMENTS.map((segment) => (\n          <span\n            key={segment}\n            className={cn(\n              \"h-1 flex-1 rounded-full bg-muted transition-colors\",\n              segment <= score && segmentColors[level]\n            )}\n          />\n        ))}\n      </div>\n\n      <p aria-live=\"polite\" className=\"text-muted-foreground text-xs\">\n        {localization.auth.passwordStrength}:{\" \"}\n        <span className=\"font-medium text-foreground\">\n          {levelLabels[level]}\n        </span>\n      </p>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/password-strength-meter.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/provider-button.tsx",
      "content": "\"use client\"\n\nimport {\n  type AuthSocialProvider,\n  type AuthView,\n  authMutationKeys,\n  getProviderId,\n  getProviderName,\n  type OAuthPopupAuthClient\n} from \"@better-auth-ui/core\"\nimport {\n  renderProviderIcon,\n  useAuth,\n  useFetchOptions,\n  useSignInOAuthPopup,\n  useSignInSocial\n} from \"@better-auth-ui/react\"\nimport { useIsMutating } from \"@tanstack/react-query\"\nimport type { ComponentProps } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { cn } from \"cn\"\nimport { LastUsedBadge } from \"./last-login-method/last-used-badge\"\n\nexport type ProviderButtonProps = {\n  provider: AuthSocialProvider\n  display?: \"full\" | \"name\" | \"icon\"\n  view?: AuthView\n} & Omit<ComponentProps<typeof Button>, \"onClick\" | \"children\" | \"disabled\">\n\n/**\n * Social provider sign-in button.\n *\n * @param provider - Provider to sign in with.\n * @param display - `\"full\"` (e.g. \"Continue with Google\"), `\"name\"` (just the provider name), or `\"icon\"` (icon only).\n */\nexport function ProviderButton({\n  provider,\n  display = \"full\",\n  view = \"signIn\",\n  variant = \"outline\",\n  className,\n  ...props\n}: ProviderButtonProps) {\n  const {\n    authClient,\n    baseURL,\n    localization,\n    navigate,\n    redirectTo,\n    socialSignInMode\n  } = useAuth()\n\n  const callbackURL = `${baseURL}${redirectTo}`\n  const { fetchOptions, resetFetchOptions } = useFetchOptions()\n\n  const { mutate: signInSocial, isPending: signInSocialPending } =\n    useSignInSocial(authClient, { onError: resetFetchOptions })\n  const { mutate: signInPopup, isPending: signInPopupPending } =\n    useSignInOAuthPopup(authClient as OAuthPopupAuthClient, {\n      onError: resetFetchOptions\n    })\n\n  const providerId = getProviderId(provider)\n  const providerIcon = renderProviderIcon(provider)\n\n  const signInMutating = useIsMutating({\n    mutationKey: authMutationKeys.signIn.all\n  })\n  const signUpMutating = useIsMutating({\n    mutationKey: authMutationKeys.signUp.all\n  })\n  const isPending = signInMutating + signUpMutating > 0\n\n  const handleSignIn = () => {\n    if (socialSignInMode === \"popup\") {\n      signInPopup(\n        {\n          provider: providerId,\n          callbackURL,\n          requestSignUp: view === \"signUp\"\n        },\n        { onSuccess: () => navigate({ to: redirectTo }) }\n      )\n      return\n    }\n\n    signInSocial({ provider: providerId, callbackURL, fetchOptions })\n  }\n\n  return (\n    <Button\n      type=\"button\"\n      variant={variant}\n      disabled={isPending}\n      onClick={handleSignIn}\n      className={cn(\"relative overflow-visible\", className)}\n      {...props}\n    >\n      {signInSocialPending || signInPopupPending ? <Spinner /> : providerIcon}\n\n      {display === \"full\"\n        ? localization.auth.continueWith.replace(\n            \"{{provider}}\",\n            getProviderName(provider)\n          )\n        : display === \"name\"\n          ? getProviderName(provider)\n          : null}\n\n      {display === \"icon\" && (\n        <span className=\"sr-only\">{getProviderName(provider)}</span>\n      )}\n\n      {view !== \"signUp\" && <LastUsedBadge method={providerId} floating />}\n    </Button>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/provider-button.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/provider-buttons.tsx",
      "content": "\"use client\"\n\nimport { type AuthView, getProviderId } from \"@better-auth-ui/core\"\nimport { useAuth } from \"@better-auth-ui/react\"\nimport { useMemo } from \"react\"\n\nimport { cn } from \"cn\"\nimport { ProviderButton } from \"./provider-button\"\n\nexport type ProviderButtonsProps = {\n  socialLayout?: SocialLayout\n  view?: AuthView\n}\n\nexport type SocialLayout = \"auto\" | \"horizontal\" | \"vertical\" | \"grid\"\n\n/**\n * Render sign-in buttons for configured social providers. Each button owns its own sign-in mutation\n * and reads the shared sign-in pending state from React Query.\n *\n * @param socialLayout - Preferred layout for the provider buttons; `\"auto\"` chooses based on the number of providers.\n */\nexport function ProviderButtons({\n  socialLayout = \"auto\",\n  view = \"signIn\"\n}: ProviderButtonsProps) {\n  const { socialProviders } = useAuth()\n\n  const resolvedSocialLayout = useMemo(() => {\n    if (socialLayout === \"auto\") {\n      if (socialProviders?.length && socialProviders.length >= 4) {\n        return \"horizontal\"\n      }\n\n      return \"vertical\"\n    }\n\n    return socialLayout\n  }, [socialLayout, socialProviders?.length])\n\n  return (\n    <div\n      className={cn(\n        \"gap-3\",\n        resolvedSocialLayout === \"grid\" && \"grid grid-cols-2\",\n        resolvedSocialLayout === \"vertical\" && \"flex flex-col\",\n        resolvedSocialLayout === \"horizontal\" && \"flex flex-row flex-wrap\"\n      )}\n    >\n      {socialProviders?.map((provider) => (\n        <ProviderButton\n          key={getProviderId(provider)}\n          provider={provider}\n          view={view}\n          display={\n            resolvedSocialLayout === \"vertical\"\n              ? \"full\"\n              : resolvedSocialLayout === \"grid\"\n                ? \"name\"\n                : \"icon\"\n          }\n          className={cn(resolvedSocialLayout === \"horizontal\" && \"flex-1\")}\n        />\n      ))}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/provider-buttons.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/sign-up.tsx",
      "content": "\"use client\"\n\nimport {\n  authMutationKeys,\n  getAdditionalFieldDefaultValues,\n  getAdditionalFieldSubmitValues,\n  getAuthCallbackURL,\n  getAuthLinkURL,\n  isPasswordCompromisedError,\n  validateEmailAddress,\n  validateMatchingValue,\n  validateStringLength\n} from \"@better-auth-ui/core\"\nimport {\n  AuthPrompts,\n  useAuth,\n  useFetchOptions,\n  useSignUpEmail\n} from \"@better-auth-ui/react\"\nimport { useIsMutating } from \"@tanstack/react-query\"\nimport { Eye, EyeOff } from \"lucide-react\"\nimport { useMemo, useState } from \"react\"\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\"\nimport {\n  Field,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLabel,\n  FieldSeparator\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 { cn } from \"cn\"\nimport {\n  getAuthAdditionalFieldValidators,\n  isAuthFormFieldInvalid,\n  useAuthForm\n} from \"./auth-form\"\nimport { PasswordStrengthMeter } from \"./password-strength-meter\"\nimport { ProviderButtons, type SocialLayout } from \"./provider-buttons\"\n\nexport type SignUpProps = {\n  className?: string\n  socialLayout?: SocialLayout\n  socialPosition?: \"top\" | \"bottom\"\n  /**\n   * Runs instead of the post-sign-up redirect, but only when the sign-up\n   * created an immediately usable session. Email verification still takes\n   * priority, and social sign-ups are unaffected.\n   */\n  onSignUpSuccess?: () => void\n}\n\n/**\n * Renders a sign-up form with name, email, and password fields, optional social provider buttons, and submission handling.\n *\n * Submits credentials to the configured auth client and handles the response:\n * - If email verification is required, shows a notification and navigates to sign-in\n * - On success, refreshes the session and navigates to the configured redirect path\n * - On failure, displays error toasts\n * - Manages a pending state while the request is in-flight\n *\n * @param className - Additional CSS classes applied to the outer container\n * @param socialLayout - Social layout to apply to the component\n * @param socialPosition - Social position to apply to the component\n * @param onSignUpSuccess - Replaces the post-sign-up redirect when the new account is immediately usable\n * @returns The sign-up form React element.\n */\nexport function SignUp({\n  className,\n  socialLayout,\n  socialPosition = \"bottom\",\n  onSignUpSuccess\n}: SignUpProps) {\n  const {\n    additionalFields,\n    authClient,\n    basePaths,\n    baseURL,\n    emailAndPassword,\n    localization,\n    plugins,\n    redirectTo,\n    socialProviders,\n    viewPaths,\n    navigate,\n    Link\n  } = useAuth()\n\n  const { fetchOptions, resetFetchOptions } = useFetchOptions()\n\n  const { mutateAsync: signUpEmail } = useSignUpEmail(authClient, {\n    onError: (error) => {\n      // The haveIBeenPwned plugin rejects on the password itself,\n      // so it belongs against the field rather than in a toast.\n      if (isPasswordCompromisedError(error)) {\n        setIsCompromised(true)\n      }\n\n      form.setFieldValue(\"password\", \"\")\n      form.setFieldValue(\"confirmPassword\", \"\")\n      resetFetchOptions()\n    },\n    onSuccess: (_data, { email }) => {\n      if (emailAndPassword?.requireEmailVerification) {\n        sessionStorage.setItem(\"better-auth-ui.verify-email\", email)\n        navigate({\n          to: getAuthLinkURL(\n            `${basePaths.auth}/${viewPaths.auth.verifyEmail}`,\n            redirectTo\n          )\n        })\n      } else if (onSignUpSuccess) {\n        onSignUpSuccess()\n      } else {\n        navigate({ to: redirectTo })\n      }\n    }\n  })\n\n  const signInMutating = useIsMutating({\n    mutationKey: authMutationKeys.signIn.all\n  })\n  const signUpMutating = useIsMutating({\n    mutationKey: authMutationKeys.signUp.all\n  })\n  const isPending = signInMutating + signUpMutating > 0\n\n  const Captcha = plugins.find(\n    (plugin) => plugin.captchaComponent\n  )?.captchaComponent\n\n  const [isPasswordVisible, setIsPasswordVisible] = useState(false)\n  const [isConfirmPasswordVisible, setIsConfirmPasswordVisible] =\n    useState(false)\n\n  const [isCompromised, setIsCompromised] = useState(false)\n  const signUpFields = useMemo(\n    () => additionalFields?.filter((field) => field.signUp) ?? [],\n    [additionalFields]\n  )\n  const form = useAuthForm({\n    defaultValues: {\n      additionalFields: getAdditionalFieldDefaultValues(signUpFields),\n      confirmPassword: \"\",\n      email: \"\",\n      name: \"\",\n      password: \"\"\n    },\n    onSubmit: async ({ value }) => {\n      try {\n        await signUpEmail({\n          name: emailAndPassword?.name === false ? \"\" : value.name,\n          email: value.email.trim(),\n          password: value.password,\n          ...getAdditionalFieldSubmitValues(\n            signUpFields,\n            value.additionalFields\n          ),\n          callbackURL: getAuthCallbackURL(baseURL, redirectTo),\n          fetchOptions\n        })\n      } catch {\n        // The mutation reports the error through its configured handler.\n      }\n    }\n  })\n\n  const showSeparator =\n    emailAndPassword?.enabled && socialProviders && socialProviders.length > 0\n\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <AuthPrompts view=\"signUp\" />\n      <CardHeader>\n        <CardTitle className=\"text-xl font-semibold\">\n          {localization.auth.signUp}\n        </CardTitle>\n      </CardHeader>\n\n      <CardContent>\n        <div className=\"flex flex-col gap-6\">\n          {socialPosition === \"top\" && (\n            <>\n              {socialProviders && socialProviders.length > 0 && (\n                <ProviderButtons socialLayout={socialLayout} view=\"signUp\" />\n              )}\n\n              {showSeparator && (\n                <FieldSeparator className=\"*:data-[slot=field-separator-content]:bg-card text-xs flex items-center\">\n                  {localization.auth.or}\n                </FieldSeparator>\n              )}\n            </>\n          )}\n\n          {emailAndPassword?.enabled && (\n            <form.AppForm>\n              <form.AuthFormRoot>\n                <FieldGroup>\n                  {emailAndPassword.name !== false && (\n                    <form.AppField\n                      name=\"name\"\n                      validators={{\n                        onChange: ({ value }) =>\n                          validateStringLength(value, {\n                            requiredMessage: localization.auth.fieldRequired,\n                            trim: true\n                          })\n                      }}\n                    >\n                      {(field) => {\n                        const isInvalid = isAuthFormFieldInvalid(\n                          field.state.meta\n                        )\n\n                        return (\n                          <Field data-invalid={isInvalid}>\n                            <FieldLabel htmlFor=\"name\">\n                              {localization.auth.name}\n                            </FieldLabel>\n\n                            <Input\n                              id=\"name\"\n                              name={field.name}\n                              type=\"text\"\n                              autoComplete=\"name\"\n                              placeholder={localization.auth.namePlaceholder}\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                              aria-invalid={isInvalid}\n                            />\n\n                            <field.AuthFormFieldError />\n                          </Field>\n                        )\n                      }}\n                    </form.AppField>\n                  )}\n\n                  <form.AppField\n                    name=\"email\"\n                    validators={{\n                      onChange: ({ value }) =>\n                        validateEmailAddress(value, {\n                          invalidMessage: localization.auth.invalidEmail,\n                          requiredMessage: localization.auth.fieldRequired\n                        })\n                    }}\n                  >\n                    {(field) => {\n                      const isInvalid = isAuthFormFieldInvalid(field.state.meta)\n\n                      return (\n                        <Field data-invalid={isInvalid}>\n                          <FieldLabel htmlFor=\"email\">\n                            {localization.auth.email}\n                          </FieldLabel>\n\n                          <Input\n                            id=\"email\"\n                            name={field.name}\n                            type=\"email\"\n                            autoComplete=\"email\"\n                            placeholder={localization.auth.emailPlaceholder}\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                            aria-invalid={isInvalid}\n                          />\n\n                          <field.AuthFormFieldError />\n                        </Field>\n                      )\n                    }}\n                  </form.AppField>\n\n                  {signUpFields.map(\n                    (configuredField) =>\n                      configuredField.signUp === \"above\" && (\n                        <form.AppField\n                          key={configuredField.name}\n                          name={`additionalFields.${configuredField.name}`}\n                          validators={getAuthAdditionalFieldValidators(\n                            configuredField,\n                            localization.auth.fieldRequired\n                          )}\n                        >\n                          {(field) => (\n                            <field.AuthFormAdditionalField\n                              field={configuredField}\n                              isPending={isPending}\n                              optionalLabel={localization.auth.optional}\n                            />\n                          )}\n                        </form.AppField>\n                      )\n                  )}\n\n                  <form.AppField\n                    name=\"password\"\n                    validators={{\n                      onChange: ({ value }) =>\n                        validateStringLength(value, {\n                          maxLength: emailAndPassword?.maxPasswordLength,\n                          maxLengthMessage: localization.auth.tooLong.replace(\n                            \"{{max}}\",\n                            String(emailAndPassword?.maxPasswordLength)\n                          ),\n                          minLength: emailAndPassword?.minPasswordLength,\n                          minLengthMessage: localization.auth.tooShort.replace(\n                            \"{{min}}\",\n                            String(emailAndPassword?.minPasswordLength)\n                          ),\n                          requiredMessage: localization.auth.fieldRequired\n                        })\n                    }}\n                  >\n                    {(field) => {\n                      const isInvalid =\n                        isAuthFormFieldInvalid(field.state.meta) ||\n                        isCompromised\n\n                      return (\n                        <Field data-invalid={isInvalid}>\n                          <FieldLabel htmlFor=\"password\">\n                            {localization.auth.password}\n                          </FieldLabel>\n\n                          <InputGroup>\n                            <InputGroupInput\n                              id=\"password\"\n                              name={field.name}\n                              type={isPasswordVisible ? \"text\" : \"password\"}\n                              autoComplete=\"new-password\"\n                              value={field.state.value}\n                              onBlur={field.handleBlur}\n                              onChange={(e) => {\n                                field.handleChange(e.target.value)\n                                setIsCompromised(false)\n                              }}\n                              placeholder={\n                                localization.auth.passwordPlaceholder\n                              }\n                              required\n                              minLength={emailAndPassword?.minPasswordLength}\n                              maxLength={emailAndPassword?.maxPasswordLength}\n                              disabled={isPending}\n                              aria-invalid={isInvalid}\n                            />\n\n                            <InputGroupAddon align=\"inline-end\">\n                              <InputGroupButton\n                                size=\"icon-xs\"\n                                aria-label={\n                                  isPasswordVisible\n                                    ? localization.auth.hidePassword\n                                    : localization.auth.showPassword\n                                }\n                                title={\n                                  isPasswordVisible\n                                    ? localization.auth.hidePassword\n                                    : localization.auth.showPassword\n                                }\n                                onClick={() => {\n                                  setIsPasswordVisible((visible) => !visible)\n                                }}\n                              >\n                                {isPasswordVisible ? <EyeOff /> : <Eye />}\n                              </InputGroupButton>\n                            </InputGroupAddon>\n                          </InputGroup>\n\n                          {isCompromised ? (\n                            <FieldError>\n                              {localization.auth.passwordCompromised}\n                            </FieldError>\n                          ) : (\n                            <field.AuthFormFieldError />\n                          )}\n\n                          <PasswordStrengthMeter password={field.state.value} />\n                        </Field>\n                      )\n                    }}\n                  </form.AppField>\n\n                  {emailAndPassword?.confirmPassword && (\n                    <form.AppField\n                      name=\"confirmPassword\"\n                      validators={{\n                        onChangeListenTo: [\"password\"],\n                        onChange: ({ fieldApi, value }) =>\n                          validateStringLength(value, {\n                            maxLength: emailAndPassword?.maxPasswordLength,\n                            maxLengthMessage: localization.auth.tooLong.replace(\n                              \"{{max}}\",\n                              String(emailAndPassword?.maxPasswordLength)\n                            ),\n                            minLength: emailAndPassword?.minPasswordLength,\n                            minLengthMessage:\n                              localization.auth.tooShort.replace(\n                                \"{{min}}\",\n                                String(emailAndPassword?.minPasswordLength)\n                              ),\n                            requiredMessage: localization.auth.fieldRequired\n                          }) ??\n                          validateMatchingValue(\n                            value,\n                            fieldApi.form.getFieldValue(\"password\"),\n                            localization.auth.passwordsDoNotMatch\n                          )\n                      }}\n                    >\n                      {(field) => {\n                        const isInvalid = isAuthFormFieldInvalid(\n                          field.state.meta\n                        )\n\n                        return (\n                          <Field data-invalid={isInvalid}>\n                            <FieldLabel htmlFor=\"confirmPassword\">\n                              {localization.auth.confirmPassword}\n                            </FieldLabel>\n\n                            <InputGroup>\n                              <InputGroupInput\n                                id=\"confirmPassword\"\n                                name={field.name}\n                                type={\n                                  isConfirmPasswordVisible ? \"text\" : \"password\"\n                                }\n                                autoComplete=\"new-password\"\n                                value={field.state.value}\n                                onBlur={field.handleBlur}\n                                onChange={(e) =>\n                                  field.handleChange(e.target.value)\n                                }\n                                placeholder={\n                                  localization.auth.confirmPasswordPlaceholder\n                                }\n                                required\n                                minLength={emailAndPassword?.minPasswordLength}\n                                maxLength={emailAndPassword?.maxPasswordLength}\n                                disabled={isPending}\n                                aria-invalid={isInvalid}\n                              />\n\n                              <InputGroupAddon align=\"inline-end\">\n                                <InputGroupButton\n                                  size=\"icon-xs\"\n                                  aria-label={\n                                    isConfirmPasswordVisible\n                                      ? localization.auth.hidePassword\n                                      : localization.auth.showPassword\n                                  }\n                                  title={\n                                    isConfirmPasswordVisible\n                                      ? localization.auth.hidePassword\n                                      : localization.auth.showPassword\n                                  }\n                                  onClick={() =>\n                                    setIsConfirmPasswordVisible(\n                                      (visible) => !visible\n                                    )\n                                  }\n                                >\n                                  {isConfirmPasswordVisible ? (\n                                    <EyeOff />\n                                  ) : (\n                                    <Eye />\n                                  )}\n                                </InputGroupButton>\n                              </InputGroupAddon>\n                            </InputGroup>\n\n                            <field.AuthFormFieldError />\n                          </Field>\n                        )\n                      }}\n                    </form.AppField>\n                  )}\n\n                  {signUpFields.map(\n                    (configuredField) =>\n                      configuredField.signUp !== \"above\" && (\n                        <form.AppField\n                          key={configuredField.name}\n                          name={`additionalFields.${configuredField.name}`}\n                          validators={getAuthAdditionalFieldValidators(\n                            configuredField,\n                            localization.auth.fieldRequired\n                          )}\n                        >\n                          {(field) => (\n                            <field.AuthFormAdditionalField\n                              field={configuredField}\n                              isPending={isPending}\n                              optionalLabel={localization.auth.optional}\n                            />\n                          )}\n                        </form.AppField>\n                      )\n                  )}\n\n                  {Captcha && (\n                    <div className=\"flex justify-center\">{Captcha}</div>\n                  )}\n\n                  <div className=\"flex flex-col gap-3\">\n                    <form.AuthFormSubmitButton disabled={isPending}>\n                      {localization.auth.signUp}\n                    </form.AuthFormSubmitButton>\n\n                    {plugins.flatMap((plugin) =>\n                      (plugin.authButtons ?? []).map((AuthButton, index) => (\n                        <AuthButton\n                          key={`${plugin.id}-${index.toString()}`}\n                          view=\"signUp\"\n                        />\n                      ))\n                    )}\n                  </div>\n                </FieldGroup>\n              </form.AuthFormRoot>\n            </form.AppForm>\n          )}\n\n          {socialPosition === \"bottom\" && (\n            <>\n              {showSeparator && (\n                <FieldSeparator className=\"*:data-[slot=field-separator-content]:bg-card text-xs flex items-center\">\n                  {localization.auth.or}\n                </FieldSeparator>\n              )}\n\n              {socialProviders && socialProviders.length > 0 && (\n                <ProviderButtons socialLayout={socialLayout} view=\"signUp\" />\n              )}\n            </>\n          )}\n        </div>\n\n        {emailAndPassword?.enabled && (\n          <div className=\"flex flex-col gap-3 items-center w-full mt-4\">\n            <FieldDescription className=\"text-center\">\n              {localization.auth.alreadyHaveAnAccount}{\" \"}\n              <Link\n                href={getAuthLinkURL(\n                  `${basePaths.auth}/${viewPaths.auth.signIn}`,\n                  redirectTo\n                )}\n                className=\"underline underline-offset-4\"\n              >\n                {localization.auth.signIn}\n              </Link>\n            </FieldDescription>\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/sign-up.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/user/user-avatar.tsx",
      "content": "\"use client\"\n\nimport type { UsernameAuthClient } from \"@better-auth-ui/core/plugins/username\"\nimport { useAuth, useSession } from \"@better-auth-ui/react\"\nimport type { User } from \"better-auth\"\nimport { User2 } from \"lucide-react\"\nimport type { ReactNode } from \"react\"\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { cn } from \"cn\"\n\nexport type UserAvatarProps = {\n  className?: string\n  fallback?: ReactNode\n  isPending?: boolean\n  /** @remarks `User` */\n  user?: User & { username?: string | null; displayUsername?: string | null }\n}\n\n/**\n * Display a user's avatar using session information or an explicit user prop.\n *\n * Renders a circular avatar that shows the user's image when available, a fallback node if provided, or the user's first two initials; while the session is loading (or when `isPending` is true) and no `user` prop is supplied, renders a skeleton placeholder.\n *\n * @param className - Additional CSS classes applied to the avatar container\n * @param user - Optional user object to display instead of the session user\n * @param isPending - When true, treat the component as loading and show the skeleton if no `user` is provided\n * @param fallback - Node to render inside the avatar fallback area before initials or the default icon\n * @returns The avatar element to render (JSX)\n */\nexport function UserAvatar({\n  className,\n  user,\n  isPending,\n  fallback\n}: UserAvatarProps) {\n  const { authClient } = useAuth<UsernameAuthClient>()\n  const { data: session, isPending: sessionPending } = useSession(authClient, {\n    enabled: !user && !isPending\n  })\n\n  if ((isPending || sessionPending) && !user) {\n    return <Skeleton className={cn(\"size-8 rounded-full\", className)} />\n  }\n\n  const resolvedUser = user ?? session?.user\n\n  const initials = (\n    resolvedUser?.username ||\n    resolvedUser?.name ||\n    resolvedUser?.email\n  )\n    ?.slice(0, 2)\n    .toUpperCase()\n\n  return (\n    <Avatar\n      className={cn(\n        \"size-8 bg-muted text-foreground text-sm rounded-full\",\n        className\n      )}\n    >\n      <AvatarImage\n        src={resolvedUser?.image ?? undefined}\n        alt={\n          resolvedUser?.displayUsername ||\n          resolvedUser?.name ||\n          resolvedUser?.email\n        }\n      />\n\n      <AvatarFallback className=\"text-muted-foreground!\">\n        {fallback || initials || <User2 className=\"size-4\" />}\n      </AvatarFallback>\n    </Avatar>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/user/user-avatar.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/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/oauth-provider-plugin.ts",
      "content": "import { createAuthPlugin } from \"@better-auth-ui/core\"\nimport {\n  oauthProviderPlugin as coreOAuthProviderPlugin,\n  type OAuthProviderPluginOptions\n} from \"@better-auth-ui/core/plugins/oauth-provider\"\nimport { Code2 } from \"lucide-react\"\nimport { createElement } from \"react\"\n\nimport { AuthorizedApplications } from \"@/components/auth/oauth-provider/authorized-applications\"\nimport {\n  OrganizationOAuthClients,\n  UserOAuthClients\n} from \"@/components/auth/oauth-provider/oauth-clients\"\nimport { OAuthConsent } from \"@/components/auth/oauth-provider/oauth-consent\"\nimport { OAuthSelectAccount } from \"@/components/auth/oauth-provider/oauth-select-account\"\nimport { OAuthSignUp } from \"@/components/auth/oauth-provider/oauth-sign-up\"\n\nconst clientManagementLabel = (label: string) =>\n  createElement(\n    \"span\",\n    { className: \"flex items-center gap-1\" },\n    createElement(Code2),\n    label\n  )\n\nexport const oauthProviderPlugin = createAuthPlugin(\n  coreOAuthProviderPlugin.id,\n  (options: OAuthProviderPluginOptions = {}) => {\n    const core = coreOAuthProviderPlugin(options)\n\n    return {\n      ...core,\n      views: {\n        auth: {\n          oauthConsent: OAuthConsent,\n          // A route of its own rather than an override of the built-in\n          // `signUp` view — ordinary sign-up stays untouched.\n          oauthSignUp: OAuthSignUp,\n          oauthSelectAccount: OAuthSelectAccount\n        }\n      },\n      ...(core.showConnectedApplications\n        ? { securityCards: [AuthorizedApplications] }\n        : {}),\n      ...(core.clientManagement\n        ? {\n            settingsTabs: [\n              {\n                view: \"oauthClients\" as const,\n                label: clientManagementLabel(core.localization.oauthClients),\n                component: UserOAuthClients\n              }\n            ]\n          }\n        : {}),\n      ...(core.organizationClientManager\n        ? {\n            organizationTabs: [\n              {\n                id: \"oauthClients\",\n                path: options.clientManagementPath ?? \"oauth-clients\",\n                label: clientManagementLabel(core.localization.oauthClients),\n                component: OrganizationOAuthClients\n              }\n            ]\n          }\n        : {})\n    }\n  }\n)\n",
      "type": "registry:lib",
      "target": "@lib/auth/oauth-provider-plugin.ts"
    }
  ],
  "type": "registry:component"
}