{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "native-select",
  "title": "Native Select",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "The browser's own select, styled to match the input: three sizes, a placeholder, groups, a leading icon, invalid and disabled states and a loading spinner, so phones keep their picker and forms keep working.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/native-select.tsx",
      "content": "// Ballmac UI: Native Select. https://ui.ballmac.com/components/native-select\n// Based on shadcn/ui's Input (MIT, Copyright (c) 2023 shadcn) for the look, wrapped around the browser's own select.\n\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { ChevronDown, Loader2 } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst selectVariants = cva(\n  \"peer w-full min-w-0 appearance-none rounded-md border border-input bg-background text-foreground shadow-xs outline-none transition-[color,border-color,box-shadow] duration-150 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 motion-reduce:transition-none\",\n  {\n    variants: {\n      size: {\n        sm: \"h-[var(--bm-control-h-sm,calc(var(--spacing)*8))] ps-2.5 text-[13px]\",\n        default: \"h-[var(--bm-control-h,calc(var(--spacing)*9))] ps-3 text-sm\",\n        lg: \"h-[var(--bm-control-h-lg,calc(var(--spacing)*11))] ps-4 text-[15px]\",\n      },\n      // a list box has no arrow, so it needs no room for one\n      listbox: {\n        false: \"cursor-pointer pe-9\",\n        true: \"h-auto py-1 pe-2\",\n      },\n    },\n    defaultVariants: { size: \"default\", listbox: false },\n  }\n)\n\ntype NativeSelectProps = Omit<React.ComponentProps<\"select\">, \"size\"> &\n  Pick<VariantProps<typeof selectVariants>, \"size\"> & {\n    /** Called with the chosen value, as well as the usual `onChange`. */\n    onValueChange?: (value: string) => void\n    /** Drawn before the text, such as an icon. Keep it decorative. */\n    startIcon?: React.ReactNode\n    /** Shows a spinner in place of the arrow and marks the field busy, while the options load. The field stays usable. */\n    loading?: boolean\n    /** Text of a first, empty choice that asks for a selection. It is not offered once something is chosen. */\n    placeholder?: string\n    /** Lines to show for a `multiple` select. */\n    rows?: number\n  }\n\n/**\n * The browser's own select, dressed to match the rest of the form. Because it is the real element, it keeps what people expect: the phone's picker,\n * type to jump, form submission, autofill and every screen reader. It takes the same sizes and states as the input.\n */\nfunction NativeSelect({ className, size = \"default\", onValueChange, onChange, startIcon, loading = false, placeholder, rows, multiple, children, defaultValue, value, style, ...props }: NativeSelectProps) {\n  const listbox = !!multiple\n  // with a placeholder, an untouched select starts on it\n  const initial = value === undefined && defaultValue === undefined && placeholder ? \"\" : defaultValue\n  return (\n    <div data-slot=\"native-select\" data-size={size} className={cn(\"relative w-full min-w-0\", className)}>\n      {startIcon && !listbox ? (\n        <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-y-0 start-3 flex items-center text-muted-foreground peer-disabled:opacity-50 [&_svg]:size-4\">\n          {startIcon}\n        </span>\n      ) : null}\n      <select\n        data-slot=\"native-select-field\"\n        multiple={multiple}\n        size={listbox ? (rows ?? 4) : undefined}\n        aria-busy={loading || undefined}\n        value={value}\n        defaultValue={initial}\n        onChange={(e) => {\n          onChange?.(e)\n          onValueChange?.(multiple ? Array.from(e.target.selectedOptions, (o) => o.value).join(\",\") : e.target.value)\n        }}\n        className={cn(selectVariants({ size, listbox }), startIcon && !listbox && \"ps-9\", \"invalid:text-muted-foreground [&:has(option[value='']:checked)]:text-muted-foreground\")}\n        style={style}\n        {...props}\n      >\n        {placeholder ? (\n          <option value=\"\" disabled={props.required}>\n            {placeholder}\n          </option>\n        ) : null}\n        {children}\n      </select>\n      {!listbox ? (\n        <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-y-0 end-3 flex items-center text-muted-foreground peer-disabled:opacity-50\">\n          {loading ? <Loader2 className=\"size-4 animate-spin motion-reduce:animate-none\" /> : <ChevronDown className=\"size-4\" />}\n        </span>\n      ) : null}\n    </div>\n  )\n}\n\n/** One choice. The browser draws it, so only what a native option can do applies: text, a value and `disabled`. */\nfunction NativeSelectOption({ className, ...props }: React.ComponentProps<\"option\">) {\n  return <option data-slot=\"native-select-option\" className={cn(\"bg-popover text-popover-foreground\", className)} {...props} />\n}\n\n/** A heading over a set of choices. */\nfunction NativeSelectGroup({ className, ...props }: React.ComponentProps<\"optgroup\">) {\n  return <optgroup data-slot=\"native-select-group\" className={cn(\"bg-popover font-medium text-muted-foreground\", className)} {...props} />\n}\n\nexport { NativeSelect, NativeSelectGroup, NativeSelectOption, selectVariants, type NativeSelectProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/native-select.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "select",
      "native",
      "dropdown",
      "form",
      "mobile",
      "picker",
      "optgroup"
    ],
    "version": "1.0.0",
    "updated": "2026-10-19",
    "tests": true,
    "ai": {
      "summary": "<NativeSelect name placeholder> with <NativeSelectOption value> children and <NativeSelectGroup label> for groups. It is a real select: use value and onValueChange, or leave it uncontrolled in a form.",
      "whenToUse": [
        "Forms where a short list of choices should use the device's own picker (phones, tablets)",
        "Long lists such as countries or time zones where type-to-jump matters",
        "Anywhere accessibility and autofill should be exactly the browser's"
      ],
      "whenNotToUse": [
        "Options with icons, descriptions or search; use select or combobox",
        "Choosing many items with chips; use multi-select",
        "A handful of choices that deserve to be seen at once; use radio-group or segmented-control"
      ],
      "composesWith": [
        "label",
        "input",
        "field"
      ],
      "a11y": [
        {
          "keys": "Tab / Shift+Tab",
          "action": "Moves to and from the field"
        },
        {
          "keys": "Space / Alt+Down / Enter",
          "action": "Opens the browser's list; arrow keys and typing choose"
        },
        {
          "keys": "Screen readers",
          "action": "It is a real select, so name, value, options and groups are read by the browser; give it a label"
        },
        {
          "keys": "aria-invalid and aria-describedby",
          "action": "Set them with an error message, as on an input"
        }
      ],
      "customization": [
        "size: sm | default | lg, to match input heights",
        "placeholder for a first empty choice (required keeps it from being chosen again)",
        "startIcon, loading, rows for a multiple select",
        "onValueChange as well as onChange"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading options",
        "example": "native-select-loading",
        "trigger": "loading",
        "note": "A spinner takes the arrow's place and the field is marked busy; it can still be opened, and the options appear when they arrive."
      },
      {
        "id": "disabled",
        "title": "Disabled",
        "example": "native-select-disabled",
        "trigger": "disabled",
        "note": "Dimmed and unavailable to the pointer and keyboard, and left out of the form's data, as a native select is."
      }
    ],
    "source": {
      "name": "shadcn/ui Input",
      "url": "https://github.com/shadcn-ui/ui",
      "license": "MIT",
      "copyright": "Copyright (c) 2023 shadcn",
      "modified": true
    },
    "examples": [
      "native-select-demo",
      "native-select-groups",
      "native-select-sizes",
      "native-select-form",
      "native-select-loading",
      "native-select-disabled"
    ],
    "url": "https://ui.ballmac.com/components/native-select"
  },
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}