{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "select",
  "title": "SelectField",
  "description": "Select and multi-select field for BuzzForm v2.",
  "registryDependencies": [
    "@buzzform/init",
    "combobox",
    "input-group",
    "field"
  ],
  "files": [
    {
      "path": "registry/shadcn/fields/select.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport type { SelectField as SelectFieldDef } from \"@buildnbuzz/form-react\";\nimport { useDataField, useFieldOptions } from \"@buildnbuzz/form-react\";\nimport type { NormalizedOption } from \"@buildnbuzz/form-react\";\nimport {\n  Combobox,\n  ComboboxContent,\n  ComboboxEmpty,\n  ComboboxInput,\n  ComboboxItem,\n  ComboboxList,\n  ComboboxChips,\n  ComboboxChip,\n  ComboboxChipsInput,\n  ComboboxValue,\n} from \"@/components/ui/combobox\";\nimport {\n  Field,\n  FieldContent,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLabel,\n} from \"@/components/ui/field\";\nimport { cn } from \"@/lib/utils\";\n\n// ---------------------------------------------------------------------------\n// UI options\n// ---------------------------------------------------------------------------\n\ninterface SelectUi {\n  /** Show a clear button when a value is selected. Defaults to `true` when not required. */\n  isClearable?: boolean;\n  /** className applied to `<FieldGroup>`. */\n  className?: string;\n  /** Inline width applied to `<FieldGroup>`. */\n  width?: string | number;\n  /** Controls required asterisk visibility. */\n  asterisk?: boolean;\n  /** Text and label overrides. */\n  labels?: {\n    /** Message shown when no options match the search query. */\n    empty?: React.ReactNode;\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Option item renderer\n// ---------------------------------------------------------------------------\n\nfunction OptionContent({\n  opt,\n  hideDescription,\n}: {\n  opt: NormalizedOption;\n  hideDescription?: boolean;\n}) {\n  const optDescription = opt.ui?.description as React.ReactNode;\n  const showDescription = optDescription && !hideDescription;\n\n  return (\n    <span\n      className={cn(\n        \"flex items-center gap-2 flex-1 min-w-0\",\n        showDescription && \"py-0.5\",\n      )}\n    >\n      <span className=\"flex-1 min-w-0\">\n        <span className=\"truncate block\">{opt.label}</span>\n        {showDescription && (\n          <span className=\"text-xs text-muted-foreground truncate block mt-0.5\">\n            {optDescription}\n          </span>\n        )}\n      </span>\n    </span>\n  );\n}\n\n// ---------------------------------------------------------------------------\n// SelectField\n// ---------------------------------------------------------------------------\n\nexport function SelectField() {\n  const {\n    fieldApi,\n    field,\n    isDisabled,\n    isReadOnly,\n    isRequired,\n    label,\n    placeholder,\n    description,\n    errors,\n    isInvalid,\n    descriptionId,\n    errorId,\n    ariaDescribedBy,\n  } = useDataField<SelectFieldDef>();\n\n  const { options, isLoading } = useFieldOptions(field.options);\n\n  const ui = field.ui as SelectUi | undefined;\n  const isClearable = ui?.isClearable ?? !isRequired;\n  const hasMany = !!field.hasMany;\n\n  const widthStyle = ui?.width\n    ? {\n        width: typeof ui.width === \"number\" ? `${ui.width}px` : ui.width,\n        flex: \"0 1 auto\",\n      }\n    : undefined;\n\n  const value = fieldApi.state.value;\n\n  const selectedValues = React.useMemo(() => {\n    if (hasMany) {\n      const vals = (Array.isArray(value) ? value : []) as string[];\n      return vals\n        .map((v) => options.find((o) => o.value === v))\n        .filter(Boolean) as NormalizedOption[];\n    }\n    return value\n      ? (options.find((o) => o.value === (value as string)) ?? null)\n      : null;\n  }, [value, options, hasMany]);\n\n  const handleValueChange = (\n    val: NormalizedOption[] | NormalizedOption | null,\n  ) => {\n    if (isReadOnly) return;\n    if (hasMany && Array.isArray(val)) {\n      if (field.maxSelected !== undefined && val.length > field.maxSelected)\n        return;\n      fieldApi.handleChange(val.map((o) => o.value));\n    } else {\n      fieldApi.handleChange(\n        ((val as NormalizedOption | null)?.value ?? undefined) as string,\n      );\n    }\n  };\n\n  return (\n    <FieldGroup\n      data-field={fieldApi.name}\n      className={ui?.className}\n      style={widthStyle}\n    >\n      <Field data-invalid={isInvalid} data-disabled={isDisabled}>\n        {label && (\n          <FieldLabel htmlFor={fieldApi.name} className=\"gap-1 items-baseline\">\n            {label}\n            {isRequired && ui?.asterisk !== false && (\n              <span className=\"text-destructive\">*</span>\n            )}\n          </FieldLabel>\n        )}\n\n        <FieldContent>\n          <Combobox\n            items={options}\n            multiple={hasMany}\n            value={selectedValues}\n            onValueChange={handleValueChange}\n            disabled={isDisabled || isReadOnly || isLoading}\n            itemToStringValue={(opt) =>\n              typeof opt.label === \"string\" ? opt.label : opt.value\n            }\n          >\n            {hasMany ? (\n              <ComboboxChips\n                aria-invalid={isInvalid}\n                aria-describedby={ariaDescribedBy}\n                className={cn(\n                  \"w-full transition-all duration-200\",\n                  isLoading && \"animate-pulse bg-muted/40\",\n                )}\n              >\n                <ComboboxValue>\n                  {(values: NormalizedOption[]) => (\n                    <React.Fragment>\n                      {values.map((opt) => (\n                        <ComboboxChip key={opt.value}>\n                          {opt.label}\n                        </ComboboxChip>\n                      ))}\n                      <ComboboxChipsInput\n                        disabled={isDisabled || isReadOnly || isLoading}\n                        placeholder={\n                          isLoading\n                            ? \"...\"\n                            : values.length === 0\n                              ? placeholder\n                              : \"\"\n                        }\n                      />\n                    </React.Fragment>\n                  )}\n                </ComboboxValue>\n              </ComboboxChips>\n            ) : (\n              <ComboboxInput\n                id={fieldApi.name}\n                disabled={isDisabled || isReadOnly || isLoading}\n                placeholder={isLoading ? \"...\" : placeholder}\n                aria-invalid={isInvalid}\n                aria-describedby={ariaDescribedBy}\n                showClear={isClearable && !!selectedValues}\n                className={cn(\n                  \"w-full transition-all duration-200\",\n                  isLoading && \"animate-pulse bg-muted/40\",\n                )}\n              />\n            )}\n\n            <ComboboxContent>\n              <ComboboxEmpty>\n                {ui?.labels?.empty ?? \"No results found.\"}\n              </ComboboxEmpty>\n              <ComboboxList>\n                {(opt: NormalizedOption) => (\n                  <ComboboxItem\n                    key={opt.value}\n                    value={opt}\n                    disabled={opt.disabled}\n                  >\n                    <OptionContent opt={opt} />\n                  </ComboboxItem>\n                )}\n              </ComboboxList>\n            </ComboboxContent>\n          </Combobox>\n        </FieldContent>\n\n        {description && !isInvalid && (\n          <FieldDescription id={descriptionId}>{description}</FieldDescription>\n        )}\n        {isInvalid && <FieldError id={errorId} errors={errors} />}\n      </Field>\n    </FieldGroup>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/buzzform/fields/select.tsx"
    }
  ],
  "type": "registry:ui"
}