{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "openlmis-form-fields",
  "title": "Form Fields",
  "description": "Thirteen TanStack Form controls with stacked, row and inline layouts, accessible hints and errors, password reveal, searchable options, tags, images and dates, compact switch rows and radio cards or tiles.",
  "dependencies": [
    "@base-ui/react@^1",
    "@tanstack/react-form@^1",
    "lucide-react@^1",
    "zod@^4"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "combobox",
    "field",
    "input",
    "input-group",
    "popover",
    "radio-group",
    "select",
    "skeleton",
    "switch",
    "textarea",
    "tooltip",
    "utils",
    "https://registry.soldevelo.com/r/openlmis-combobox-filter.json",
    "https://registry.soldevelo.com/r/openlmis-date-picker.json",
    "https://registry.soldevelo.com/r/openlmis-settings-list.json"
  ],
  "files": [
    {
      "path": "registry/components/openlmis/form-fields/form-context.ts",
      "content": "import { createFormHookContexts } from \"@tanstack/react-form\"\n\nexport const { fieldContext, formContext, useFieldContext } =\n  createFormHookContexts()",
      "type": "registry:lib",
      "target": "components/openlmis/form-fields/form-context.ts"
    },
    {
      "path": "registry/components/openlmis/form-fields/form-fields.tsx",
      "content": "\"use client\"\n\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react/combobox\"\nimport { cn } from \"@/lib/utils\"\nimport {\n  EyeIcon,\n  EyeOffIcon,\n  InfoIcon,\n  Trash2Icon,\n  UploadIcon,\n  XIcon,\n} from \"lucide-react\"\nimport {\n  type ComponentProps,\n  type ReactNode,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Combobox,\n  ComboboxChip,\n  ComboboxChips,\n  ComboboxChipsInput,\n  ComboboxContent,\n  ComboboxEmpty,\n  ComboboxInput,\n  ComboboxItem,\n  ComboboxList,\n  ComboboxValue,\n  useComboboxAnchor,\n} from \"@/components/ui/combobox\"\nimport {\n  Field,\n  FieldContent,\n  FieldDescription,\n  FieldError,\n  FieldLabel,\n  FieldLegend,\n  FieldSet,\n  FieldTitle,\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  PopoverDescription,\n  PopoverHeader,\n  PopoverTitle,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { Switch } from \"@/components/ui/switch\"\nimport { Textarea } from \"@/components/ui/textarea\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { narrowOptions } from \"@/components/openlmis/combobox-filter/narrow-options\"\nimport { DatePicker } from \"@/components/openlmis/date-picker/date-picker\"\nimport { SettingsRowFrame } from \"@/components/blocks/openlmis/settings-list\"\n\nimport { useFieldContext } from \"@/components/openlmis/form-fields/form-context\"\nimport { useAboutLabel, useDateMessages, useFormatError } from \"@/components/openlmis/form-fields/form-messages\"\nimport { addTag, type TagRefusal, tagSuggestions } from \"@/components/openlmis/form-fields/tags\"\n\n/** `row` for a `SettingsList` row; `inline` keeps the label for screen readers only, e.g. in a table. */\ntype FieldLayout = \"stacked\" | \"row\" | \"inline\"\n\ntype FieldProps = {\n  label: ReactNode\n  description?: ReactNode\n  required?: boolean\n  disabled?: boolean\n  layout?: FieldLayout\n}\n\ntype FieldFrameProps = FieldProps & {\n  /** Beside the label in a row, such as a status badge. */\n  badge?: ReactNode\n  /** After the badge in a row, or at the end of a stacked label's line, such as a Reset or a link. */\n  action?: ReactNode\n  state: ReturnType<typeof useFieldErrors>\n  /** `end` lines a row's error up with a value set at the end, such as an image preview. */\n  errorAlign?: \"end\"\n  /** The control, which takes the field's name as its id. */\n  children: ReactNode\n}\n\n/** The field's errors as display text, whether there are any, and what the control is described by. */\nfunction useFieldErrors(\n  description?: ReactNode,\n  extraDescribedBy?: string,\n  badge?: ReactNode\n) {\n  const field = useFieldContext<unknown>()\n  const formatError = useFormatError()\n  const errors = field.state.meta.errors.map((error: unknown) => ({\n    message:\n      typeof error === \"string\"\n        ? formatError(error)\n        : error && typeof error === \"object\" && \"message\" in error\n          ? formatError(String(error.message))\n          : undefined,\n  }))\n  const isInvalid = errors.length > 0\n  const descriptionId = `${field.name}-description`\n  const errorId = `${field.name}-error`\n  const badgeId = `${field.name}-badge`\n  // Read out with the control, so a screen reader hears the hint and, after a submit, the error.\n  const describedBy =\n    [\n      badge ? badgeId : \"\",\n      description ? descriptionId : \"\",\n      extraDescribedBy ?? \"\",\n      isInvalid ? errorId : \"\",\n    ]\n      .filter(Boolean)\n      .join(\" \") || undefined\n  return { errors, isInvalid, descriptionId, errorId, badgeId, describedBy }\n}\n\n/** A label's text with the required mark, for any label, including a skeleton's. */\nexport function FieldLabelText({\n  label,\n  required,\n}: Pick<FieldProps, \"label\" | \"required\">) {\n  return (\n    <span>\n      {label}\n      {required && (\n        <span aria-hidden=\"true\" className=\"ms-0.5 text-destructive\">\n          *\n        </span>\n      )}\n    </span>\n  )\n}\n\n/** For screen readers only; not a direct `Field` child, whose `sr-only` rule would size it to its text. */\nfunction HiddenFromView({ children }: { children: ReactNode }) {\n  return (\n    <div className=\"contents\">\n      <div className=\"sr-only\">{children}</div>\n    </div>\n  )\n}\n\nfunction RowExtras({\n  badge,\n  badgeId,\n  action,\n}: Pick<FieldFrameProps, \"badge\" | \"action\"> & { badgeId: string }) {\n  return (\n    <>\n      {badge && <span id={badgeId}>{badge}</span>}\n      {action}\n    </>\n  )\n}\n\n/** Label, control, description and error, laid out as the field's `layout` asks. */\nfunction FieldFrame({\n  layout = \"stacked\",\n  label,\n  badge,\n  action,\n  description,\n  required,\n  disabled,\n  state: { errors, isInvalid, descriptionId, errorId, badgeId },\n  errorAlign,\n  children,\n}: FieldFrameProps) {\n  const field = useFieldContext<unknown>()\n  const labelText = <FieldLabelText label={label} required={required} />\n  const descriptionNode = description && (\n    <FieldDescription id={descriptionId}>{description}</FieldDescription>\n  )\n  const details = (\n    <>\n      {layout === \"inline\" ? (\n        <HiddenFromView>{descriptionNode}</HiddenFromView>\n      ) : (\n        descriptionNode\n      )}\n      {isInvalid && <FieldError errors={errors} id={errorId} />}\n    </>\n  )\n\n  if (layout === \"row\") {\n    return (\n      <Field\n        data-disabled={disabled}\n        data-invalid={isInvalid}\n        className=\"gap-1\"\n      >\n        <SettingsRowFrame\n          badge={<RowExtras action={action} badge={badge} badgeId={badgeId} />}\n          description={descriptionNode}\n          label={\n            <FieldLabel htmlFor={field.name} id={`${field.name}-label`}>\n              <span className=\"font-normal\">{labelText}</span>\n            </FieldLabel>\n          }\n          value=\"control\"\n        >\n          {children}\n          {isInvalid && (\n            <div className={errorAlign === \"end\" ? \"text-end\" : undefined}>\n              <FieldError errors={errors} id={errorId} />\n            </div>\n          )}\n        </SettingsRowFrame>\n      </Field>\n    )\n  }\n  if (layout === \"inline\") {\n    return (\n      <Field\n        data-disabled={disabled}\n        data-invalid={isInvalid}\n        className=\"gap-1\"\n      >\n        <HiddenFromView>\n          <label htmlFor={field.name} id={`${field.name}-label`}>\n            {labelText}\n          </label>\n        </HiddenFromView>\n        {children}\n        {details}\n      </Field>\n    )\n  }\n  const stackedLabel = (\n    <FieldLabel htmlFor={field.name} id={`${field.name}-label`}>\n      {labelText}\n    </FieldLabel>\n  )\n  return (\n    <Field data-disabled={disabled} data-invalid={isInvalid} className=\"gap-1\">\n      {action ? (\n        // Beside the label on screen, after the input in tab order, and wrapping when cramped.\n        <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n          <div className=\"shrink-0\">{stackedLabel}</div>\n          <div className=\"order-last basis-full\">{children}</div>\n          <div className=\"ms-auto\">{action}</div>\n        </div>\n      ) : (\n        <>\n          {stackedLabel}\n          {children}\n        </>\n      )}\n      {details}\n    </Field>\n  )\n}\n\ntype TextFieldProps = FieldProps &\n  Pick<FieldFrameProps, \"badge\"> & {\n    type?: \"text\" | \"email\" | \"tel\" | \"time\"\n    autoComplete?: string\n    placeholder?: string\n    maxLength?: number\n    /** `ltr` for codes and phone numbers; `auto` for free text that may be in another script. */\n    dir?: \"ltr\" | \"auto\"\n    inputMode?: \"numeric\" | \"decimal\"\n  }\n\nexport function TextField({\n  label,\n  layout,\n  badge,\n  description,\n  required,\n  disabled,\n  type = \"text\",\n  autoComplete,\n  placeholder,\n  maxLength,\n  dir,\n  inputMode,\n}: TextFieldProps) {\n  const field = useFieldContext<string>()\n  const state = useFieldErrors(description, undefined, badge)\n  const { isInvalid, describedBy: ariaDescribedBy } = state\n\n  return (\n    <FieldFrame\n      badge={badge}\n      description={description}\n      disabled={disabled}\n      label={label}\n      layout={layout}\n      required={required}\n      state={state}\n    >\n      <Input\n        aria-describedby={ariaDescribedBy}\n        aria-invalid={isInvalid}\n        aria-required={required}\n        autoComplete={autoComplete}\n        dir={dir}\n        disabled={disabled}\n        id={field.name}\n        inputMode={inputMode}\n        maxLength={maxLength}\n        name={field.name}\n        onBlur={field.handleBlur}\n        onChange={(event) => field.handleChange(event.target.value)}\n        placeholder={placeholder}\n        type={type}\n        value={field.state.value}\n      />\n    </FieldFrame>\n  )\n}\n\nexport function NumberField(props: FieldProps) {\n  return (\n    <TextField {...props} autoComplete=\"off\" dir=\"ltr\" inputMode=\"numeric\" />\n  )\n}\n\nexport function DecimalField(props: FieldProps) {\n  return (\n    <TextField {...props} autoComplete=\"off\" dir=\"ltr\" inputMode=\"decimal\" />\n  )\n}\n\ntype TextareaFieldProps = FieldProps & {\n  placeholder?: string\n  dir?: \"auto\"\n}\n\nexport function TextareaField({\n  label,\n  layout,\n  description,\n  required,\n  disabled,\n  placeholder,\n  dir,\n}: TextareaFieldProps) {\n  const field = useFieldContext<string>()\n  const state = useFieldErrors(description)\n  const { isInvalid, describedBy: ariaDescribedBy } = state\n\n  return (\n    <FieldFrame\n      description={description}\n      disabled={disabled}\n      label={label}\n      layout={layout}\n      required={required}\n      state={state}\n    >\n      <Textarea\n        dir={dir}\n        aria-describedby={ariaDescribedBy}\n        aria-invalid={isInvalid}\n        aria-required={required}\n        disabled={disabled}\n        id={field.name}\n        name={field.name}\n        onBlur={field.handleBlur}\n        onChange={(event) => field.handleChange(event.target.value)}\n        placeholder={placeholder}\n        value={field.state.value}\n      />\n    </FieldFrame>\n  )\n}\n\ntype PasswordFieldProps = FieldProps &\n  Pick<FieldFrameProps, \"action\"> & {\n    autoComplete?: \"new-password\" | \"current-password\"\n    placeholder?: string\n    /** Names the button that reveals the password, for screen readers. */\n    showLabel?: string\n    hideLabel?: string\n    describedBy?: string\n  } & SharedVisibility\n\n/** Both or neither, for fields that show and hide together, such as a password and its confirmation. */\ntype SharedVisibility =\n  | { visible?: never; onVisibleChange?: never }\n  | { visible: boolean; onVisibleChange: (visible: boolean) => void }\n\nexport function PasswordField({\n  label,\n  layout,\n  description,\n  required,\n  disabled,\n  autoComplete = \"new-password\",\n  placeholder,\n  showLabel = \"Show Password\",\n  hideLabel = \"Hide Password\",\n  describedBy,\n  visible: sharedVisible,\n  onVisibleChange,\n  action,\n}: PasswordFieldProps) {\n  const field = useFieldContext<string>()\n  const state = useFieldErrors(description, describedBy)\n  const { isInvalid, describedBy: ariaDescribedBy } = state\n  const [ownVisible, setOwnVisible] = useState(false)\n  const visible = sharedVisible ?? ownVisible\n\n  return (\n    <FieldFrame\n      action={action}\n      description={description}\n      disabled={disabled}\n      label={label}\n      layout={layout}\n      required={required}\n      state={state}\n    >\n      <InputGroup>\n        <InputGroupInput\n          aria-describedby={ariaDescribedBy}\n          aria-invalid={isInvalid}\n          aria-required={required}\n          autoComplete={autoComplete}\n          disabled={disabled}\n          id={field.name}\n          name={field.name}\n          onBlur={field.handleBlur}\n          onChange={(event) => field.handleChange(event.target.value)}\n          placeholder={placeholder}\n          type={visible ? \"text\" : \"password\"}\n          value={field.state.value}\n        />\n        <InputGroupAddon align=\"inline-end\">\n          <InputGroupButton\n            aria-label={visible ? hideLabel : showLabel}\n            disabled={disabled}\n            onClick={() => (onVisibleChange ?? setOwnVisible)(!visible)}\n            size=\"icon-xs\"\n            type=\"button\"\n            variant=\"ghost\"\n          >\n            {visible ? <EyeOffIcon /> : <EyeIcon />}\n          </InputGroupButton>\n        </InputGroupAddon>\n      </InputGroup>\n    </FieldFrame>\n  )\n}\n\ntype ChoiceCardProps = {\n  /** The control's id, so a click anywhere on the card reaches it. */\n  htmlFor?: string\n  label: ReactNode\n  /** Text goes in a description line; any other node, such as a skeleton, is placed as it is. */\n  description?: ReactNode\n  disabled?: boolean\n  /** The switch or radio, or a placeholder while loading. */\n  children: ReactNode\n  media?: ReactNode\n}\n\n/** The card around one choice: label and description at the start, the control at the end. */\nexport function ChoiceCard({\n  htmlFor,\n  label,\n  description,\n  disabled,\n  children,\n  media,\n}: ChoiceCardProps) {\n  return (\n    <FieldLabel htmlFor={htmlFor}>\n      <Field data-disabled={disabled} orientation=\"horizontal\">\n        {media}\n        <FieldContent>\n          <FieldTitle>{label}</FieldTitle>\n          {typeof description === \"string\" ? (\n            <FieldDescription>{description}</FieldDescription>\n          ) : (\n            description\n          )}\n        </FieldContent>\n        {children}\n      </Field>\n    </FieldLabel>\n  )\n}\n\n/** A small option: its media across the top, the label and the control below. */\nfunction ChoiceTile({\n  htmlFor,\n  label,\n  disabled,\n  children,\n  media,\n}: Omit<ChoiceCardProps, \"description\">) {\n  return (\n    <FieldLabel htmlFor={htmlFor}>\n      <Field data-disabled={disabled}>\n        {media}\n        <div className=\"flex items-center justify-between gap-2\">\n          <FieldTitle>{label}</FieldTitle>\n          {children}\n        </div>\n      </Field>\n    </FieldLabel>\n  )\n}\n\ntype SwitchFieldProps = Omit<FieldProps, \"required\" | \"label\"> &\n  Pick<FieldFrameProps, \"action\"> & { label: string }\n\n/** An info button that shows a field's description in a popover. */\nfunction FieldAbout({\n  label,\n  description,\n}: {\n  label: string\n  description: ReactNode\n}) {\n  const aboutLabel = useAboutLabel()\n  return (\n    <Popover>\n      <PopoverTrigger\n        render={\n          <Button\n            aria-label={aboutLabel(label)}\n            // A dialog opening on this form focuses the field itself, not its info button.\n            data-no-initial-focus=\"\"\n            size=\"icon-xs\"\n            type=\"button\"\n            variant=\"ghost\"\n          />\n        }\n      >\n        <InfoIcon />\n      </PopoverTrigger>\n      <PopoverContent align=\"start\">\n        <PopoverHeader>\n          <PopoverTitle>{label}</PopoverTitle>\n          <PopoverDescription>{description}</PopoverDescription>\n        </PopoverHeader>\n      </PopoverContent>\n    </Popover>\n  )\n}\n\n/** A yes/no setting: label and info button at the start, the switch at the end, or the switch alone in a cell. */\nexport function SwitchField({\n  label,\n  description,\n  disabled,\n  layout,\n  action,\n}: SwitchFieldProps) {\n  const field = useFieldContext<boolean>()\n  const { descriptionId, badgeId, describedBy } = useFieldErrors(description)\n  const control = (\n    <Switch\n      aria-describedby={layout === \"inline\" ? undefined : describedBy}\n      checked={field.state.value}\n      disabled={disabled}\n      id={field.name}\n      name={field.name}\n      onBlur={field.handleBlur}\n      onCheckedChange={(checked) => field.handleChange(checked)}\n    />\n  )\n\n  if (layout === \"row\") {\n    return (\n      <Field data-disabled={disabled}>\n        <SettingsRowFrame\n          badge={<RowExtras action={action} badgeId={badgeId} />}\n          description={\n            description && (\n              <FieldDescription id={descriptionId}>\n                {description}\n              </FieldDescription>\n            )\n          }\n          label={\n            <FieldLabel htmlFor={field.name}>\n              <span className=\"font-normal\">{label}</span>\n            </FieldLabel>\n          }\n        >\n          {control}\n        </SettingsRowFrame>\n      </Field>\n    )\n  }\n  if (layout === \"inline\") {\n    return (\n      <Field data-disabled={disabled} orientation=\"horizontal\">\n        <HiddenFromView>\n          <label htmlFor={field.name}>{label}</label>\n        </HiddenFromView>\n        {control}\n      </Field>\n    )\n  }\n  return (\n    <Field data-disabled={disabled} orientation=\"horizontal\">\n      <div className=\"flex min-h-8 min-w-0 flex-1 items-center gap-1\">\n        <FieldLabel htmlFor={field.name}>{label}</FieldLabel>\n        {description && (\n          <>\n            <FieldAbout description={description} label={label} />\n            <span className=\"sr-only\" id={descriptionId}>\n              {description}\n            </span>\n          </>\n        )}\n      </div>\n      {control}\n    </Field>\n  )\n}\n\ntype IconActionProps = Omit<\n  ComponentProps<typeof Button>,\n  \"children\" | \"size\" | \"type\"\n> & {\n  label: string\n  icon: ReactNode\n}\n\n/** An icon button named by `label`, which its tooltip shows too. */\nfunction IconAction({ label, icon, ...props }: IconActionProps) {\n  return (\n    <Tooltip>\n      <TooltipTrigger\n        render={\n          <Button aria-label={label} size=\"icon-xs\" type=\"button\" {...props} />\n        }\n      >\n        {icon}\n      </TooltipTrigger>\n      <TooltipContent>{label}</TooltipContent>\n    </Tooltip>\n  )\n}\n\ntype ImageFieldProps = Omit<FieldProps, \"layout\" | \"required\"> & {\n  accept: string\n  previewUrl: string\n  previewAlt: string\n  chooseLabel: string\n  removeLabel: string\n  canRemove: boolean\n}\n\nexport function ImageField({\n  label,\n  description,\n  disabled,\n  accept,\n  previewUrl,\n  previewAlt,\n  chooseLabel,\n  removeLabel,\n  canRemove,\n}: ImageFieldProps) {\n  const field = useFieldContext<File | null | undefined>()\n  const input = useRef<HTMLInputElement>(null)\n  const choose = useRef<HTMLButtonElement>(null)\n  const state = useFieldErrors(description)\n\n  const actions = (\n    <span className=\"flex items-center gap-1\">\n      <IconAction\n        aria-describedby={state.describedBy}\n        aria-invalid={state.isInvalid}\n        disabled={disabled}\n        icon={<UploadIcon />}\n        label={chooseLabel}\n        onClick={() => input.current?.click()}\n        ref={choose}\n        variant=\"outline\"\n      />\n      {canRemove && (\n        <IconAction\n          disabled={disabled}\n          icon={<Trash2Icon />}\n          label={removeLabel}\n          onClick={() => {\n            field.handleChange(null)\n            choose.current?.focus()\n          }}\n          variant=\"destructive\"\n        />\n      )}\n    </span>\n  )\n\n  return (\n    <FieldFrame\n      action={actions}\n      description={description}\n      disabled={disabled}\n      errorAlign=\"end\"\n      label={label}\n      layout=\"row\"\n      state={state}\n    >\n      <div className=\"flex justify-end\">\n        {/* oxlint-disable-next-line next/no-img-element -- items are framework-neutral, and the preview is a local object URL */}\n        <img\n          alt={previewAlt}\n          className=\"h-10 max-w-32 object-contain\"\n          src={previewUrl}\n        />\n        <input\n          accept={accept}\n          aria-describedby={state.describedBy}\n          aria-invalid={state.isInvalid}\n          className=\"sr-only\"\n          disabled={disabled}\n          id={field.name}\n          name={field.name}\n          onChange={(event) => {\n            const file = event.target.files?.[0]\n            if (file) field.handleChange(file)\n            event.target.value = \"\"\n          }}\n          ref={input}\n          tabIndex={-1}\n          type=\"file\"\n        />\n      </div>\n    </FieldFrame>\n  )\n}\n\nexport type RadioGroupFieldOption = {\n  value: string\n  label: ReactNode\n  description?: ReactNode\n  media?: ReactNode\n}\n\ntype RadioGroupFieldProps = {\n  /** Names the group; shown above the options. */\n  label: ReactNode\n  options: readonly RadioGroupFieldOption[]\n  disabled?: boolean\n  /** `row` puts the cards side by side once there is room. */\n  columns?: \"row\"\n  /** `tile` puts the media above the label, in a grid of small options such as colours. */\n  variant?: \"card\" | \"tile\"\n}\n\n/** One choice from a few, each drawn as a card, or as a small tile. */\nexport function RadioGroupField({\n  label,\n  options,\n  disabled,\n  columns,\n  variant = \"card\",\n}: RadioGroupFieldProps) {\n  const field = useFieldContext<string>()\n\n  return (\n    <FieldSet className=\"@container/choices\">\n      <FieldLegend id={`${field.name}-legend`} variant=\"label\">\n        {label}\n      </FieldLegend>\n      <RadioGroup\n        aria-labelledby={`${field.name}-legend`}\n        className={cn(\n          variant === \"tile\" &&\n            \"grid-cols-3 @sm/choices:grid-cols-4 @xl/choices:grid-cols-6\",\n          variant === \"card\" &&\n            columns === \"row\" &&\n            \"@sm/choices:auto-cols-fr @sm/choices:grid-flow-col\"\n        )}\n        disabled={disabled}\n        name={field.name}\n        onBlur={field.handleBlur}\n        onValueChange={(value) => field.handleChange(String(value))}\n        value={field.state.value}\n      >\n        {options.map((option) => {\n          const id = `${field.name}-${option.value}`\n          if (variant === \"tile\") {\n            return (\n              <ChoiceTile\n                disabled={disabled}\n                htmlFor={id}\n                key={option.value}\n                label={option.label}\n                media={option.media}\n              >\n                <RadioGroupItem id={id} value={option.value} />\n              </ChoiceTile>\n            )\n          }\n          return (\n            <ChoiceCard\n              description={option.description}\n              disabled={disabled}\n              htmlFor={id}\n              key={option.value}\n              label={option.label}\n              media={option.media}\n            >\n              <RadioGroupItem id={id} value={option.value} />\n            </ChoiceCard>\n          )\n        })}\n      </RadioGroup>\n    </FieldSet>\n  )\n}\n\nexport type SelectFieldItem = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\ntype SelectFieldProps = FieldProps &\n  Pick<FieldFrameProps, \"action\"> & {\n    items: readonly SelectFieldItem[]\n  }\n\n/** One of a short, fixed list; the field's value is the item's `value`. */\nexport function SelectField({\n  label,\n  description,\n  required,\n  disabled,\n  layout,\n  items,\n  action,\n}: SelectFieldProps) {\n  const field = useFieldContext<string>()\n  const state = useFieldErrors(description)\n  const { isInvalid, describedBy: ariaDescribedBy } = state\n\n  return (\n    <FieldFrame\n      action={action}\n      description={description}\n      disabled={disabled}\n      label={label}\n      layout={layout}\n      required={required}\n      state={state}\n    >\n      <Select\n        disabled={disabled}\n        items={items}\n        onValueChange={(value) => value !== null && field.handleChange(value)}\n        value={field.state.value}\n      >\n        <SelectTrigger\n          aria-describedby={ariaDescribedBy}\n          aria-invalid={isInvalid}\n          aria-required={required}\n          id={field.name}\n          className=\"w-full\"\n          onBlur={field.handleBlur}\n        >\n          <SelectValue />\n        </SelectTrigger>\n        <SelectContent alignItemWithTrigger={false}>\n          {items.map((item) => (\n            <SelectItem\n              disabled={item.disabled}\n              key={item.value}\n              value={item.value}\n            >\n              {item.label}\n            </SelectItem>\n          ))}\n        </SelectContent>\n      </Select>\n    </FieldFrame>\n  )\n}\n\nexport type ComboboxFieldItem = {\n  value: string\n  label: string\n  description?: string\n}\n\ntype ComboboxFieldProps = FieldProps & {\n  items: readonly ComboboxFieldItem[]\n  placeholder?: string\n  emptyMessage?: ReactNode\n  /** Names the button that empties the field, for screen readers. */\n  clearLabel?: string\n  /** Most matches rendered at once; past it a hint asks the user to search. */\n  limit?: number\n}\n\n/** Picks one item by typing to filter; the field's value is the item's `value`, or null for none. */\nexport function ComboboxField({\n  label,\n  layout,\n  description,\n  required,\n  disabled,\n  items,\n  placeholder,\n  emptyMessage = \"No matches.\",\n  clearLabel = \"Clear\",\n  limit = 50,\n}: ComboboxFieldProps) {\n  const field = useFieldContext<string | null>()\n  const [query, setQuery] = useState(\"\")\n  const { items: listed, hint } = narrowOptions(items, { query, limit })\n  const state = useFieldErrors(description)\n  const { isInvalid, describedBy: ariaDescribedBy } = state\n  const selected = useMemo(\n    () => items.find((item) => item.value === field.state.value) ?? null,\n    [items, field.state.value]\n  )\n\n  return (\n    <FieldFrame\n      description={description}\n      disabled={disabled}\n      label={label}\n      layout={layout}\n      required={required}\n      state={state}\n    >\n      <Combobox\n        disabled={disabled}\n        isItemEqualToValue={(item, value) => item.value === value.value}\n        filter={null}\n        itemToStringLabel={(item) => item.label}\n        items={listed}\n        // Only typing counts as a search, not the selected label filling the input.\n        onInputValueChange={(text, details) =>\n          setQuery(details.reason === \"input-change\" ? text : \"\")\n        }\n        onValueChange={(item, details) => {\n          // Base UI clears the field on Escape once the list is closed; let Escape close the dialog instead.\n          if (details.reason === \"escape-key\" && !item)\n            return details.allowPropagation()\n          field.handleChange(item?.value ?? null)\n        }}\n        value={selected}\n      >\n        <ComboboxInput\n          aria-describedby={ariaDescribedBy}\n          aria-invalid={isInvalid}\n          aria-required={required}\n          className=\"w-full\"\n          disabled={disabled}\n          id={field.name}\n          onBlur={field.handleBlur}\n          placeholder={placeholder}\n        >\n          {/* Its own clear button, since the stock one has no accessible name. */}\n          {selected !== null && !disabled && (\n            <InputGroupAddon align=\"inline-end\">\n              <ComboboxPrimitive.Clear\n                aria-label={clearLabel}\n                // The slot hides the stock dropdown trigger while a value can be cleared.\n                data-slot=\"combobox-clear\"\n                render={<InputGroupButton size=\"icon-xs\" variant=\"ghost\" />}\n              >\n                <XIcon />\n              </ComboboxPrimitive.Clear>\n            </InputGroupAddon>\n          )}\n        </ComboboxInput>\n        <ComboboxContent className=\"flex flex-col\">\n          <ComboboxPrimitive.Status className=\"text-xs text-muted-foreground\">\n            {hint && <span className=\"block px-3 pt-2\">{hint}</span>}\n          </ComboboxPrimitive.Status>\n          <ComboboxEmpty>{emptyMessage}</ComboboxEmpty>\n          <ComboboxList>\n            {(item: ComboboxFieldItem) => (\n              <ComboboxItem key={item.value} value={item}>\n                <span className=\"min-w-0 truncate\" dir=\"auto\">\n                  {item.label}\n                </span>\n                {item.description && (\n                  <span className=\"ms-auto shrink-0 text-xs text-muted-foreground\">\n                    {item.description}\n                  </span>\n                )}\n              </ComboboxItem>\n            )}\n          </ComboboxList>\n        </ComboboxContent>\n      </Combobox>\n    </FieldFrame>\n  )\n}\n\ntype MultiComboboxFieldProps = FieldProps & {\n  items: readonly ComboboxFieldItem[]\n  placeholder?: string\n  emptyMessage: ReactNode\n  removeLabel: (label: string) => string\n  /** Called with what the user types; the caller narrows `items` and they are listed as given. */\n  onSearch?: (text: string) => void\n  /** With `onSearch`, how many items match in all when `items` holds only some of them. */\n  total?: number\n  /** Most matches rendered at once; past it a hint asks the user to search. */\n  limit?: number\n}\n\nexport function MultiComboboxField({\n  label,\n  layout,\n  description,\n  required,\n  disabled,\n  items,\n  placeholder,\n  emptyMessage,\n  removeLabel,\n  onSearch,\n  total,\n  limit = 50,\n}: MultiComboboxFieldProps) {\n  const field = useFieldContext<string[]>()\n  const state = useFieldErrors(description)\n  const { isInvalid, describedBy: ariaDescribedBy } = state\n  const anchor = useComboboxAnchor()\n  const [picked, setPicked] = useState<readonly ComboboxFieldItem[]>([])\n  // A value missing from `items` keeps a chip named by its value, so the next change cannot drop it.\n  const selected = useMemo(() => {\n    const known = new Map(\n      [...picked, ...items].map((item) => [item.value, item])\n    )\n    return field.state.value.map(\n      (value) => known.get(value) ?? { value, label: value }\n    )\n  }, [items, picked, field.state.value])\n  const [query, setQuery] = useState(\"\")\n  const { items: listed, hint } = narrowOptions(items, {\n    query,\n    limit,\n    searched: Boolean(onSearch),\n    total: onSearch ? total : undefined,\n  })\n\n  return (\n    <FieldFrame\n      description={description}\n      disabled={disabled}\n      label={label}\n      layout={layout}\n      required={required}\n      state={state}\n    >\n      <Combobox\n        disabled={disabled}\n        isItemEqualToValue={(item, value) => item.value === value.value}\n        itemToStringLabel={(item) => item.label}\n        filter={null}\n        items={listed}\n        multiple\n        onInputValueChange={(text, details) => {\n          const typed = details.reason === \"input-change\" ? text : \"\"\n          setQuery(typed)\n          onSearch?.(typed)\n        }}\n        onOpenChange={(_, details) => {\n          if (onSearch && details.reason === \"item-press\") details.cancel()\n        }}\n        onValueChange={(chosen, details) => {\n          if (details.reason === \"escape-key\" && chosen.length === 0)\n            return details.allowPropagation()\n          setPicked(chosen)\n          field.handleChange(chosen.map((item) => item.value))\n        }}\n        value={selected}\n      >\n        <ComboboxChips ref={anchor}>\n          <ComboboxValue>\n            {(values: ComboboxFieldItem[]) => (\n              <>\n                {values.map((item) => (\n                  <NamedChip\n                    key={item.value}\n                    removeLabel={removeLabel(item.label)}\n                  >\n                    <span dir=\"auto\">{item.label}</span>\n                  </NamedChip>\n                ))}\n                <ComboboxChipsInput\n                  aria-describedby={ariaDescribedBy}\n                  aria-invalid={isInvalid}\n                  aria-required={required}\n                  disabled={disabled}\n                  id={field.name}\n                  onBlur={field.handleBlur}\n                  placeholder={values.length === 0 ? placeholder : undefined}\n                />\n              </>\n            )}\n          </ComboboxValue>\n        </ComboboxChips>\n        <ComboboxContent anchor={anchor} className=\"flex flex-col\">\n          <ComboboxPrimitive.Status className=\"text-xs text-muted-foreground\">\n            {hint && <span className=\"block px-3 pt-2\">{hint}</span>}\n          </ComboboxPrimitive.Status>\n          <ComboboxEmpty>{emptyMessage}</ComboboxEmpty>\n          <ComboboxList>\n            {(item: ComboboxFieldItem) => (\n              <ComboboxItem key={item.value} value={item}>\n                <span dir=\"auto\">{item.label}</span>\n              </ComboboxItem>\n            )}\n          </ComboboxList>\n        </ComboboxContent>\n      </Combobox>\n    </FieldFrame>\n  )\n}\n\n/** A stock chip with its own remove button, since the stock one has no accessible name. */\nfunction NamedChip({\n  removeLabel,\n  children,\n}: {\n  removeLabel: string\n  children: ReactNode\n}) {\n  return (\n    <ComboboxChip showRemove={false}>\n      {children}\n      <ComboboxPrimitive.ChipRemove\n        aria-label={removeLabel}\n        className=\"-ms-1\"\n        data-slot=\"combobox-chip-remove\"\n        render={<Button size=\"icon-xs\" variant=\"ghost\" />}\n      >\n        <XIcon className=\"pointer-events-none\" />\n      </ComboboxPrimitive.ChipRemove>\n    </ComboboxChip>\n  )\n}\n\ntype TagsFieldProps = FieldProps & {\n  suggestions: readonly string[]\n  placeholder?: string\n  minLength?: number\n  maxLength?: number\n  removeLabel: (tag: string) => string\n  refusedMessage: (reason: TagRefusal) => string\n}\n\nconst isTagSeparator = (key: string) => key === \",\" || key === \"،\"\n\n/** Enter, Tab or leaving the box takes the highlighted suggestion or the typed text; a comma, the typed text. */\nexport function TagsField({\n  label,\n  layout,\n  description,\n  required,\n  disabled,\n  suggestions,\n  placeholder,\n  minLength,\n  maxLength,\n  removeLabel,\n  refusedMessage,\n}: TagsFieldProps) {\n  const field = useFieldContext<string[]>()\n  const refusedId = `${field.name}-refused`\n  const [text, setText] = useState(\"\")\n  const [open, setOpen] = useState(false)\n  const [refusal, setRefusal] = useState<TagRefusal>()\n  const highlighted = useRef<string | undefined>(undefined)\n  const state = useFieldErrors(description, refusal ? refusedId : undefined)\n  const { isInvalid, describedBy: ariaDescribedBy } = state\n  const anchor = useComboboxAnchor()\n  const tags = field.state.value\n  const items = useMemo(\n    () => tagSuggestions(suggestions, tags, text),\n    [suggestions, tags, text]\n  )\n  const listOpen = open && items.length > 0\n  const suggested = () =>\n    listOpen && highlighted.current && items.includes(highlighted.current)\n      ? highlighted.current\n      : undefined\n\n  const add = (next: string) => {\n    const result = addTag(tags, next, { min: minLength, max: maxLength })\n    if (!result) return\n    if (\"refused\" in result) {\n      setRefusal(result.refused)\n      return\n    }\n    field.handleChange(result.tags)\n    setText(\"\")\n  }\n\n  return (\n    <FieldFrame\n      description={description}\n      disabled={disabled}\n      label={label}\n      layout={layout}\n      required={required}\n      state={state}\n    >\n      <Combobox\n        autoHighlight\n        disabled={disabled}\n        filter={null}\n        inputValue={text}\n        items={items}\n        multiple\n        onInputValueChange={(value, details) => {\n          // Base UI empties the box on Enter and on closing; refused text stays, as in legacy.\n          if (details.reason === \"input-clear\") return\n          setText(value)\n          setRefusal(undefined)\n        }}\n        onItemHighlighted={(item) => {\n          highlighted.current = item\n        }}\n        onOpenChange={setOpen}\n        onValueChange={(next) => {\n          field.handleChange(next)\n          setText(\"\")\n          setRefusal(undefined)\n        }}\n        open={listOpen}\n        value={tags}\n      >\n        <ComboboxChips ref={anchor}>\n          <ComboboxValue>\n            {(values: string[]) => (\n              <>\n                {values.map((tag) => (\n                  <NamedChip key={tag} removeLabel={removeLabel(tag)}>\n                    <span className=\"truncate\" dir=\"auto\" title={tag}>\n                      {tag}\n                    </span>\n                  </NamedChip>\n                ))}\n                <ComboboxChipsInput\n                  aria-describedby={ariaDescribedBy}\n                  aria-invalid={isInvalid || refusal !== undefined}\n                  aria-required={required}\n                  disabled={disabled}\n                  id={field.name}\n                  onBlur={(event) => {\n                    // A click on a suggestion moves focus into the list, and picks it there.\n                    const clicked = event.relatedTarget?.closest(\n                      '[data-slot=\"combobox-content\"]'\n                    )\n                    if (!clicked) add(suggested() ?? text)\n                    field.handleBlur()\n                  }}\n                  onKeyDown={(event) => {\n                    const separator = isTagSeparator(event.key)\n                    if (\n                      !separator &&\n                      (event.key !== \"Enter\" || suggested() || !text.trim())\n                    )\n                      return\n                    event.preventDefault()\n                    add(text)\n                  }}\n                  placeholder={values.length === 0 ? placeholder : undefined}\n                />\n              </>\n            )}\n          </ComboboxValue>\n        </ComboboxChips>\n        <ComboboxContent anchor={anchor}>\n          <ComboboxList>\n            {(item: string) => (\n              <ComboboxItem key={item} value={item}>\n                <span dir=\"auto\">{item}</span>\n              </ComboboxItem>\n            )}\n          </ComboboxList>\n        </ComboboxContent>\n      </Combobox>\n      {refusal && (\n        <FieldError\n          errors={[{ message: refusedMessage(refusal) }]}\n          id={refusedId}\n        />\n      )}\n    </FieldFrame>\n  )\n}\n\ntype DateFieldProps = FieldProps & {\n  placeholder: string\n  clearLabel?: string\n  /** The first and last days that can be picked, as `yyyy-MM-dd`. */\n  earliest?: string\n  latest?: string\n}\n\n/** A `yyyy-MM-dd` date from a calendar, or an empty string for none. */\nexport function DateField({\n  label,\n  layout,\n  description,\n  required,\n  disabled,\n  placeholder,\n  clearLabel,\n  earliest,\n  latest,\n}: DateFieldProps) {\n  const field = useFieldContext<string>()\n  const state = useFieldErrors(description)\n  const { requiredLabel, dateLanguage } = useDateMessages()\n\n  return (\n    <FieldFrame\n      description={description}\n      disabled={disabled}\n      label={label}\n      layout={layout}\n      required={required}\n      state={state}\n    >\n      <DatePicker\n        clearLabel={clearLabel}\n        dateLanguage={dateLanguage}\n        describedBy={state.describedBy}\n        disabled={disabled}\n        earliest={earliest}\n        id={field.name}\n        invalid={state.isInvalid}\n        labelledBy={`${field.name}-label`}\n        latest={latest}\n        onBlur={field.handleBlur}\n        onValueChange={field.handleChange}\n        placeholder={placeholder}\n        required={required}\n        requiredLabel={requiredLabel}\n        value={field.state.value}\n      />\n    </FieldFrame>\n  )\n}\n\n/** One line of small text: its line height with a bar inside, so it takes the room the text will. */\nexport function SkeletonLine({\n  width = \"medium\",\n}: {\n  width?: \"short\" | \"medium\"\n}) {\n  return (\n    <div className=\"flex h-4 items-center\">\n      <Skeleton className={width === \"short\" ? \"h-3 w-32\" : \"h-3 w-56\"} />\n    </div>\n  )\n}\n\n/** A field's real label over a placeholder input, for a form whose values are still loading. */\nexport function FieldSkeleton({\n  label,\n  required = false,\n}: Pick<FieldProps, \"label\" | \"required\">) {\n  return (\n    <Field className=\"gap-1\">\n      <FieldLabel>\n        <FieldLabelText label={label} required={required} />\n      </FieldLabel>\n      <Skeleton className=\"h-8 w-full\" />\n    </Field>\n  )\n}\n\n/** A `ChoiceCard` whose value is still loading; pass `description` when it is known already. */\nexport function ChoiceCardSkeleton({\n  label,\n  description,\n  control = \"switch\",\n}: {\n  label: ReactNode\n  description?: string\n  control?: \"switch\" | \"radio\"\n}) {\n  return (\n    <ChoiceCard description={description ?? <SkeletonLine />} label={label}>\n      {/* Plain elements, since Skeleton owns its corner radius and both controls are round. */}\n      {control === \"radio\" ? (\n        <div className=\"size-4 shrink-0 animate-pulse rounded-full bg-muted\" />\n      ) : (\n        <div className=\"h-4.5 w-8 shrink-0 animate-pulse rounded-full bg-muted\" />\n      )}\n    </ChoiceCard>\n  )\n}",
      "type": "registry:component",
      "target": "components/openlmis/form-fields/form-fields.tsx"
    },
    {
      "path": "registry/components/openlmis/form-fields/form-messages.tsx",
      "content": "\"use client\"\n\nimport { createContext, type ReactNode, use, useMemo } from \"react\"\n\ntype FormMessages = {\n  formatError: (message: string) => string\n  aboutLabel: (label: string) => string\n  requiredLabel: string\n  dateLanguage: string\n}\n\nconst defaultAboutLabel = (label: string) => `About ${label}`\n\nconst FormMessagesContext = createContext<FormMessages>({\n  formatError: (message) => message,\n  aboutLabel: defaultAboutLabel,\n  requiredLabel: \"Required\",\n  dateLanguage: \"en-US\",\n})\n\ntype FormMessagesProviderProps = {\n  /** Turns a validation message into display text, e.g. by translating a message key. */\n  formatError: (message: string) => string\n  /** Names the info button that shows a field's description. */\n  aboutLabel?: (label: string) => string\n  /** Read out with a field that must be filled in but cannot say so itself, such as a date. */\n  requiredLabel?: string\n  /** The language dates are shown in. */\n  dateLanguage?: string\n  children: ReactNode\n}\n\n/** Optional: without it, messages show exactly as written, in English. */\nexport function FormMessagesProvider({\n  formatError,\n  aboutLabel = defaultAboutLabel,\n  requiredLabel = \"Required\",\n  dateLanguage = \"en-US\",\n  children,\n}: FormMessagesProviderProps) {\n  const messages = useMemo(\n    () => ({ formatError, aboutLabel, requiredLabel, dateLanguage }),\n    [formatError, aboutLabel, requiredLabel, dateLanguage]\n  )\n  return <FormMessagesContext value={messages}>{children}</FormMessagesContext>\n}\n\nexport function useFormatError() {\n  return use(FormMessagesContext).formatError\n}\n\nexport function useAboutLabel() {\n  return use(FormMessagesContext).aboutLabel\n}\n\nexport function useDateMessages() {\n  const { requiredLabel, dateLanguage } = use(FormMessagesContext)\n  return { requiredLabel, dateLanguage }\n}",
      "type": "registry:component",
      "target": "components/openlmis/form-fields/form-messages.tsx"
    },
    {
      "path": "registry/components/openlmis/form-fields/form.tsx",
      "content": "import { createFormHook } from \"@tanstack/react-form\"\n\nimport { fieldContext, formContext } from \"@/components/openlmis/form-fields/form-context\"\nimport {\n  ComboboxField,\n  DateField,\n  DecimalField,\n  ImageField,\n  MultiComboboxField,\n  NumberField,\n  PasswordField,\n  RadioGroupField,\n  SelectField,\n  SwitchField,\n  TagsField,\n  TextareaField,\n  TextField,\n} from \"@/components/openlmis/form-fields/form-fields\"\n\n/** `useForm` with the field components attached, used as `<form.AppField>{(field) => <field.TextField />}`. */\nexport const { useAppForm, withForm } = createFormHook({\n  fieldContext,\n  formContext,\n  fieldComponents: {\n    TextField,\n    NumberField,\n    DecimalField,\n    TextareaField,\n    PasswordField,\n    SwitchField,\n    RadioGroupField,\n    ComboboxField,\n    MultiComboboxField,\n    TagsField,\n    SelectField,\n    ImageField,\n    DateField,\n  },\n  formComponents: {},\n})",
      "type": "registry:component",
      "target": "components/openlmis/form-fields/form.tsx"
    },
    {
      "path": "registry/components/openlmis/form-fields/tags.ts",
      "content": "export type TagRefusal = \"too-short\" | \"too-long\" | \"duplicate\"\n\ntype TagLimits = { min?: number | undefined; max?: number | undefined }\n\nconst sameTag = (a: string, b: string) =>\n  a.localeCompare(b, undefined, { sensitivity: \"accent\" }) === 0\n\n/** The tags with `text` added, the reason it can't be, or null when there is nothing to add. */\nexport function addTag(\n  tags: readonly string[],\n  text: string,\n  { min = 1, max = Number.POSITIVE_INFINITY }: TagLimits = {}\n): { tags: string[] } | { refused: TagRefusal } | null {\n  const tag = text.trim()\n  if (!tag) return null\n  if (tag.length < min) return { refused: \"too-short\" }\n  if (tag.length > max) return { refused: \"too-long\" }\n  if (tags.some((existing) => sameTag(existing, tag)))\n    return { refused: \"duplicate\" }\n  return { tags: [...tags, tag] }\n}\n\n/** The suggestions containing `text` that aren't added yet, both ignoring case; none until something is typed. */\nexport function tagSuggestions(\n  suggestions: readonly string[],\n  tags: readonly string[],\n  text: string\n) {\n  const term = text.trim().toLocaleLowerCase()\n  if (!term) return []\n  return suggestions.filter(\n    (suggestion) =>\n      suggestion.toLocaleLowerCase().includes(term) &&\n      !tags.some((tag) => sameTag(tag, suggestion))\n  )\n}",
      "type": "registry:lib",
      "target": "components/openlmis/form-fields/tags.ts"
    }
  ],
  "cssVars": {
    "light": {
      "destructive": "oklch(0.577 0.245 27.325)",
      "destructive-foreground": "oklch(0.985 0 0)",
      "success": "oklch(0.596 0.145 163.225)",
      "success-foreground": "oklch(0.985 0 0)",
      "warning": "oklch(0.666 0.179 58.318)",
      "warning-foreground": "oklch(0.985 0 0)",
      "info": "oklch(0.55 0.13 237)",
      "info-foreground": "oklch(0.985 0 0)"
    },
    "dark": {
      "destructive": "oklch(0.704 0.191 22.216)",
      "destructive-foreground": "oklch(0.21 0.04 25)",
      "success": "oklch(0.696 0.17 162.48)",
      "success-foreground": "oklch(0.21 0.04 163)",
      "warning": "oklch(0.769 0.188 70.08)",
      "warning-foreground": "oklch(0.22 0.04 60)",
      "info": "oklch(0.7 0.14 237)",
      "info-foreground": "oklch(0.2 0.04 240)"
    }
  },
  "meta": {
    "project": "openlmis",
    "height": "1677px"
  },
  "docs": "Requires a `base-*` style in components.json (the `shadcn init` default). Legacy `new-york`/`radix-*` styles install Radix primitives, which reject the `render` prop this item uses.",
  "categories": [
    "forms"
  ],
  "type": "registry:component"
}
