{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "additional-field",
  "title": "Additional Field",
  "description": "Renders a configured additional user field on the sign-up form and user profile, with support for string, number, boolean, date, datetime, select, combobox, slider, and hidden types.",
  "dependencies": [
    "@better-auth-ui/react@latest",
    "@better-auth-ui/core@latest",
    "better-auth",
    "date-fns",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "calendar",
    "checkbox",
    "combobox",
    "field",
    "input",
    "input-group",
    "popover",
    "select",
    "slider",
    "switch",
    "textarea"
  ],
  "files": [
    {
      "path": "src/components/auth/additional-field.tsx",
      "content": "\"use client\"\n\nimport {\n  type AdditionalField as AdditionalFieldConfig,\n  resolveInputType\n} from \"@better-auth-ui/core\"\nimport { useAuth } 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 \"@/lib/utils\"\n\nexport type AdditionalFieldProps = {\n  name: string\n  field: AdditionalFieldConfig\n  isPending?: boolean\n  /** Complete suffix appended to labels for fields that are not required. */\n  optionalLabel?: string\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, setCopied] = useState(false)\n\n  async function handleCopy() {\n    const value = getValue()\n    if (!value) return\n\n    try {\n      await navigator.clipboard.writeText(value)\n      setCopied(true)\n      setTimeout(() => setCopied(false), 1500)\n    } catch (error) {\n      toast.error(error instanceof Error ? error.message : String(error))\n    }\n  }\n\n  return (\n    <InputGroupButton\n      aria-label={localization.settings.copyToClipboard}\n      title={localization.settings.copyToClipboard}\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  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\n  if (field.render) {\n    const FieldRenderer = field.render as ComponentType<AdditionalFieldProps>\n    return (\n      <FieldRenderer\n        name={name}\n        field={field}\n        isPending={isPending}\n        optionalLabel={optionalLabel}\n      />\n    )\n  }\n\n  if (inputType === \"hidden\") {\n    return (\n      <input\n        type=\"hidden\"\n        name={name}\n        value={\n          field.defaultValue == null\n            ? \"\"\n            : field.defaultValue instanceof Date\n              ? field.defaultValue.toISOString()\n              : String(field.defaultValue)\n        }\n      />\n    )\n  }\n\n  if (inputType === \"textarea\") {\n    return (\n      <Field>\n        <FieldLabel htmlFor={name}>{field.label}</FieldLabel>\n\n        <Textarea\n          id={name}\n          name={name}\n          defaultValue={\n            field.defaultValue == null ? undefined : String(field.defaultValue)\n          }\n          placeholder={field.placeholder}\n          required={field.required}\n          readOnly={field.readOnly}\n          disabled={isPending}\n        />\n\n        <FieldError />\n      </Field>\n    )\n  }\n\n  if (inputType === \"number\") {\n    const maxFractionDigits = field.formatOptions?.maximumFractionDigits\n\n    return (\n      <Field>\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          defaultValue={\n            field.defaultValue == null\n              ? undefined\n              : typeof field.defaultValue === \"number\"\n                ? field.defaultValue\n                : String(field.defaultValue)\n          }\n          placeholder={field.placeholder}\n          required={field.required}\n          readOnly={field.readOnly}\n          disabled={isPending}\n        />\n\n        <FieldError />\n      </Field>\n    )\n  }\n\n  if (inputType === \"slider\") {\n    return <SliderField name={name} field={field} isPending={isPending} />\n  }\n\n  if (inputType === \"switch\") {\n    return (\n      <Field orientation=\"horizontal\">\n        <Switch\n          id={name}\n          name={name}\n          defaultChecked={\n            field.defaultValue === true || field.defaultValue === \"true\"\n          }\n          disabled={isPending || field.readOnly}\n        />\n\n        <FieldContent>\n          <FieldLabel htmlFor={name}>{field.label}</FieldLabel>\n        </FieldContent>\n      </Field>\n    )\n  }\n\n  if (inputType === \"checkbox\") {\n    return (\n      <Field orientation=\"horizontal\">\n        <Checkbox\n          id={name}\n          name={name}\n          defaultChecked={\n            field.defaultValue === true || field.defaultValue === \"true\"\n          }\n          required={field.required}\n          disabled={isPending || field.readOnly}\n        />\n\n        <FieldContent>\n          <FieldLabel htmlFor={name}>{field.label}</FieldLabel>\n        </FieldContent>\n      </Field>\n    )\n  }\n\n  if (inputType === \"select\") {\n    return (\n      <Field>\n        <FieldLabel htmlFor={name}>{field.label}</FieldLabel>\n\n        <Select\n          name={name}\n          defaultValue={\n            field.defaultValue != null ? String(field.defaultValue) : undefined\n          }\n          required={field.required}\n          disabled={isPending || field.readOnly}\n        >\n          <SelectTrigger id={name} className=\"w-full\">\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 />\n      </Field>\n    )\n  }\n\n  if (inputType === \"combobox\") {\n    return (\n      <Field>\n        <FieldLabel htmlFor={name}>{field.label}</FieldLabel>\n\n        <Combobox\n          items={field.options ?? []}\n          name={name}\n          defaultValue={\n            field.defaultValue != null ? String(field.defaultValue) : undefined\n          }\n          required={field.required}\n          disabled={isPending || field.readOnly}\n        >\n          <ComboboxInput placeholder={field.placeholder} id={name} />\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 />\n      </Field>\n    )\n  }\n\n  if (inputType === \"date\" || inputType === \"datetime\") {\n    return <DateInput name={name} field={field} isPending={isPending} />\n  }\n\n  return <InputField name={name} field={field} isPending={isPending} />\n}\n\nfunction InputField({ name, field, isPending }: AdditionalFieldProps) {\n  const inputRef = useRef<HTMLInputElement>(null)\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>\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            defaultValue={\n              field.defaultValue == null\n                ? undefined\n                : String(field.defaultValue)\n            }\n            placeholder={field.placeholder}\n            required={field.required}\n            readOnly={field.readOnly}\n            disabled={isPending}\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 />\n      </Field>\n    )\n  }\n\n  return (\n    <Field>\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        defaultValue={\n          field.defaultValue == null ? undefined : String(field.defaultValue)\n        }\n        placeholder={field.placeholder}\n        required={field.required}\n        readOnly={field.readOnly}\n        disabled={isPending}\n      />\n\n      <FieldError />\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({ name, field, isPending }: 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 initial =\n    typeof field.defaultValue === \"number\"\n      ? field.defaultValue\n      : field.defaultValue != null && !Number.isNaN(Number(field.defaultValue))\n        ? Number(field.defaultValue)\n        : min\n\n  const [value, setValue] = useState<number>(initial)\n\n  const formatter = new Intl.NumberFormat(undefined, field.formatOptions)\n\n  return (\n    <Field>\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(value)}\n        </span>\n      </div>\n\n      <Slider\n        id={name}\n        name={name}\n        value={[value]}\n        onValueChange={(v) => setValue((Array.isArray(v) ? v[0] : v) ?? min)}\n        min={min}\n        max={max}\n        step={step}\n        disabled={isPending || field.readOnly}\n      />\n\n      <FieldError />\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({ name, field, isPending }: AdditionalFieldProps) {\n  const { localization } = useAuth()\n  const inputType = resolveInputType(field)\n  const isDateTime = inputType === \"datetime\"\n\n  const [date, setDate] = useState<Date | undefined>(toDate(field.defaultValue))\n  const [time, setTime] = useState<string>(\n    isDateTime && date ? formatTime(date) : \"\"\n  )\n  const [open, setOpen] = useState(false)\n  const [error, setError] = useState<string>()\n\n  // Compose the hidden form value: ISO date for \"date\", ISO datetime for\n  // \"datetime\" (date + time).\n  let formValue = \"\"\n  if (date) {\n    if (isDateTime && time && time.trim() !== \"\") {\n      const [h = \"0\", m = \"0\", s = \"0\"] = time.split(\":\")\n      const combined = new Date(date)\n      combined.setHours(Number(h), Number(m), Number(s), 0)\n      formValue = combined.toISOString()\n    } else {\n      // Anchor to local midnight then serialize as ISO so the downstream\n      // `parseAdditionalFieldValue` parses the same calendar day regardless\n      // of timezone (a bare \"YYYY-MM-DD\" would be parsed as UTC midnight).\n      // Datetime fields with a blank time also fall through here, defaulting\n      // the time to local midnight since the parsed value is always a `Date`.\n      const localMidnight = new Date(date)\n      localMidnight.setHours(0, 0, 0, 0)\n      formValue = localMidnight.toISOString()\n    }\n  }\n\n  return (\n    <Field data-invalid={!!error}>\n      <FieldLabel htmlFor={`${name}-date`}>{field.label}</FieldLabel>\n\n      <div className=\"relative flex gap-2\">\n        {/* Visually-hidden input so required constraint validation fires on submit.\n            onInvalid suppresses the native browser balloon and routes the message\n            through the styled <FieldError> below — matching the pattern used by\n            the Name / Email / Password fields in the sign-up form. */}\n        <input\n          aria-label={typeof field.label === \"string\" ? field.label : name}\n          type=\"text\"\n          name={name}\n          value={formValue}\n          onChange={() => {}}\n          required={field.required}\n          tabIndex={-1}\n          className=\"sr-only\"\n          onInvalid={(e) => {\n            e.preventDefault()\n            setError((e.target as HTMLInputElement).validationMessage)\n          }}\n        />\n        <Popover open={open} onOpenChange={setOpen}>\n          <PopoverTrigger\n            type=\"button\"\n            id={`${name}-date`}\n            data-empty={!date}\n            aria-invalid={!!error}\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                setDate(value)\n                if (value) setError(undefined)\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={(e) => setTime(e.target.value)}\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>{error}</FieldError>\n    </Field>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/additional-field.tsx"
    }
  ],
  "type": "registry:component"
}