{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "select",
  "title": "Select",
  "description": "Select dropdown for React and shadcn that opens with the chosen option over its trigger, like a macOS pop-up button.",
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-overlay-core.json",
    "https://eased.dev/r/eased-floating.json",
    "https://eased.dev/r/eased-field.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/select/select.tsx",
      "content": "\"use client\";\n\nimport { Select as BaseSelect } from \"@base-ui/react/select\";\nimport { motion, type HTMLMotionProps } from \"motion/react\";\nimport { Fragment, useLayoutEffect, useRef, useState, type ReactNode, type RefObject } from \"react\";\nimport { InputGroup, type ControlSize } from \"../_primitives/field/field\";\nimport { FloatingLayer } from \"../_primitives/floating/floating\";\nimport {\n  groupClass,\n  groupLabelClass,\n  HighlightGroup,\n  itemClass,\n  ItemCheck,\n  ItemParts,\n  OVERLAY_ROLES,\n  panelSurface,\n  separatorClass,\n  withHighlight,\n} from \"../_primitives/overlay-core/overlay-core\";\n\n/**\n * A select on Base UI's Select, in the Eased Input's bordered shell. Like a macOS pop-up\n * button, the list opens with the selected row sitting over the trigger and grows from that\n * row; when there's no room (or on touch) it drops below the trigger like a listbox instead.\n * The highlight glides between rows on the snappy spring.\n */\n\nexport type SelectOption<V> = {\n  value: V;\n  /** The row's text, and what the trigger shows once it is chosen. */\n  label: string;\n  /** A 16px glyph, flag or colour dot before the label. */\n  icon?: ReactNode;\n  /** A second, muted line under the label. */\n  description?: ReactNode;\n  disabled?: boolean;\n};\n\nexport type SelectOptionGroup<V> = {\n  /** The group's heading. */\n  label: string;\n  items: SelectOption<V>[];\n};\n\ntype RootProps<V, M extends boolean | undefined> = Omit<\n  BaseSelect.Root.Props<V, M>,\n  \"items\" | \"children\" | \"itemToStringLabel\" | \"isItemEqualToValue\"\n>;\n\nexport type SelectProps<V, M extends boolean | undefined = false> = RootProps<V, M> & {\n  /** The options, flat or in groups. Leave out to compose `SelectItem` / `SelectGroup` children instead. */\n  items?: SelectOption<V>[] | SelectOptionGroup<V>[];\n  /** Rows composed by hand (`SelectItem`, `SelectGroup`, `SelectSeparator`). Ignored when `items` is set. */\n  children?: ReactNode;\n  /** Muted text in the trigger while nothing is chosen. */\n  placeholder?: ReactNode;\n  /** sm 32px, md 36px (default), lg 40px. */\n  size?: ControlSize;\n  /**\n   * Mark the select invalid outside a `Field` (inside one, the field's validity drives it):\n   * red border and one shake when it turns on.\n   */\n  invalid?: boolean;\n  /**\n   * Open with the selected row over the trigger (default true). Base UI falls back to a list\n   * below the trigger on touch and when there isn't room; false always drops below.\n   */\n  alignItemWithTrigger?: boolean;\n  /** Format the trigger's value, e.g. to add the chosen option's icon. Default: its label. */\n  renderValue?: (value: M extends true ? V[] : V | null) => ReactNode;\n  /** The trigger's accessible name when no `Field` label or `aria-labelledby` names it. */\n  \"aria-label\"?: string;\n  \"aria-labelledby\"?: string;\n  /** Portal target. Default `document.body`. */\n  container?: HTMLElement | null;\n  /** Stacking order of the popup. Default 50; raise it inside a higher layer (a dialog). */\n  zIndex?: number;\n  /** Classes for the bordered shell (set a width here; it fills its container by default). */\n  className?: string;\n  /** Classes for the popup. */\n  popupClassName?: string;\n};\n\n// The trigger covers the shell's padding so the whole box is one button.\nconst TRIGGER_INSET: Record<ControlSize, string> = { sm: \"-mx-2.5 px-2.5\", md: \"-mx-3 px-3\", lg: \"-mx-3.5 px-3.5\" };\n\n/*\n * The popup holds the scroll arrows (absolutely placed by Base UI), so the list inside it is the\n * scroller and carries the menu's three tracks (icon · label · trailing) that rows subgrid into.\n * In align mode the popup is wider than the trigger by the check track, so the trigger's box is\n * covered while the selected label sits on the trigger's value.\n */\nconst POPUP = `${panelSurface} relative flex flex-col overflow-hidden min-w-(--anchor-width) max-w-(--available-width) max-h-(--available-height) data-[side=none]:min-w-[calc(var(--anchor-width)+1.75rem)]`;\nconst LIST =\n  \"grid min-h-0 flex-1 grid-cols-[auto_minmax(0,1fr)_auto] content-start overflow-y-auto overscroll-contain p-1 scroll-py-6 outline-none [--pad:4px]\";\nconst ARROW =\n  \"absolute inset-x-0 z-10 flex h-6 cursor-default items-center justify-center text-text-muted from-surface-raised from-30% to-transparent transition-opacity duration-(--eased-duration-fast) ease-standard data-starting-style:opacity-0 data-ending-style:opacity-0 reduced:transition-none before:absolute before:inset-x-0 before:h-full\";\n\n/**\n * The select. Pass `items` for the common case, or compose rows as children:\n *\n *   <Select items={regions} defaultValue=\"eu-west\" aria-label=\"Region\" />\n */\nexport function Select<V, M extends boolean | undefined = false>({\n  items,\n  children,\n  placeholder,\n  size = \"md\",\n  invalid,\n  alignItemWithTrigger = true,\n  renderValue,\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledBy,\n  container,\n  zIndex,\n  className = \"\",\n  popupClassName = \"\",\n  disabled,\n  ...props\n}: SelectProps<V, M>) {\n  const anchor = useRef<HTMLDivElement>(null);\n  const trigger = useRef<HTMLButtonElement>(null);\n  const fieldInvalid = useDataInvalid(trigger);\n\n  return (\n    <BaseSelect.Root<V, M> {...props} disabled={disabled} items={items}>\n      <div ref={anchor} className={`flex w-full min-w-0 ${className}`}>\n        <InputGroup size={size} invalid={invalid ?? fieldInvalid} className=\"has-[[data-popup-open]]:not-data-invalid:border-(--eased-neutral-9)\">\n          <BaseSelect.Trigger\n            ref={trigger}\n            aria-label={ariaLabel}\n            aria-labelledby={ariaLabelledBy}\n            {...(invalid !== undefined ? { \"aria-invalid\": invalid } : null)}\n            className={`flex min-w-0 flex-1 cursor-default items-center gap-2 self-stretch rounded-[inherit] text-start outline-none select-none disabled:cursor-not-allowed ${TRIGGER_INSET[size]}`}\n          >\n            <BaseSelect.Value\n              placeholder={placeholder}\n              className=\"min-w-0 flex-1 truncate data-placeholder:text-text-muted\"\n            >\n              {renderValue as ((value: unknown) => ReactNode) | undefined}\n            </BaseSelect.Value>\n            <BaseSelect.Icon className=\"-me-0.5 inline-flex shrink-0 text-text-muted in-disabled:text-text-disabled\">\n              <ChevronUpDownIcon />\n            </BaseSelect.Icon>\n          </BaseSelect.Trigger>\n        </InputGroup>\n      </div>\n\n      <FloatingLayer\n        parts={BaseSelect}\n        motion={OVERLAY_ROLES.listbox}\n        side={OVERLAY_ROLES.listbox.side}\n        sideOffset={OVERLAY_ROLES.listbox.sideOffset}\n        align=\"start\"\n        container={container}\n        zIndex={zIndex}\n        positionerProps={{ alignItemWithTrigger, anchor }}\n        className={`${POPUP} ${popupClassName}`}\n      >\n        <BaseSelect.ScrollUpArrow className={`${ARROW} top-0 bg-linear-to-b data-[side=none]:before:-top-full`}>\n          <ChevronIcon up />\n        </BaseSelect.ScrollUpArrow>\n        {/* The list is a Motion element with `layoutScroll`, so the gliding highlight measures rows through the\n            list's scroll offset: it stays correct while the list scrolls under it (wheel, scroll arrows, typeahead\n            jumps, align mode's initial scroll). A render callback, not a component, so Base UI calls it as a plain function. */}\n        <BaseSelect.List render={(props) => <motion.div {...(props as HTMLMotionProps<\"div\">)} layoutScroll />} className={LIST}>\n          <HighlightGroup>{items ? <OptionRows items={items} /> : children}</HighlightGroup>\n        </BaseSelect.List>\n        <BaseSelect.ScrollDownArrow className={`${ARROW} bottom-0 bg-linear-to-t data-[side=none]:before:-bottom-full`}>\n          <ChevronIcon />\n        </BaseSelect.ScrollDownArrow>\n      </FloatingLayer>\n    </BaseSelect.Root>\n  );\n}\n\nfunction OptionRows<V>({ items }: { items: SelectOption<V>[] | SelectOptionGroup<V>[] }) {\n  return (\n    <>\n      {items.map((entry, i) =>\n        \"items\" in entry ? (\n          <Fragment key={`group:${entry.label}`}>\n            {i > 0 && <SelectSeparator />}\n            <SelectGroup label={entry.label}>\n              {entry.items.map((option) => (\n                <OptionRow key={String(option.value)} option={option} />\n              ))}\n            </SelectGroup>\n          </Fragment>\n        ) : (\n          <OptionRow key={String(entry.value)} option={entry} />\n        ),\n      )}\n    </>\n  );\n}\n\nfunction OptionRow<V>({ option }: { option: SelectOption<V> }) {\n  return (\n    <SelectItem value={option.value} label={option.label} icon={option.icon} description={option.description} disabled={option.disabled}>\n      {option.label}\n    </SelectItem>\n  );\n}\n\nexport type SelectItemProps = {\n  /** The value this row chooses. */\n  value: unknown;\n  /** The row's text. */\n  children: ReactNode;\n  /** The text typeahead matches when `children` isn't plain text. */\n  label?: string;\n  /** A 16px glyph, flag or colour dot before the label. */\n  icon?: ReactNode;\n  /** A second, muted line under the label. */\n  description?: ReactNode;\n  disabled?: boolean;\n};\n\n/**\n * One option. The check sits in the icon track and draws in when the row is chosen; labels\n * share one x whether or not a row has an `icon`.\n */\nexport function SelectItem({ value, children, label, icon, description, disabled }: SelectItemProps) {\n  return (\n    <BaseSelect.Item value={value} label={label} disabled={disabled} className={itemClass({ glide: true })} render={withHighlight}>\n      <ItemParts\n        icon={\n          <BaseSelect.ItemIndicator keepMounted render={<span />} className=\"inline-flex\">\n            <ItemCheck />\n          </BaseSelect.ItemIndicator>\n        }\n        description={description}\n      >\n        <span className=\"flex min-w-0 items-center gap-2\">\n          {icon !== undefined && (\n            <span aria-hidden className=\"inline-flex shrink-0 [&_svg]:size-4\">\n              {icon}\n            </span>\n          )}\n          {/* The text Base UI lines up with the trigger's value in align mode. */}\n          <BaseSelect.ItemText render={<span />} className=\"truncate\">\n            {children}\n          </BaseSelect.ItemText>\n        </span>\n      </ItemParts>\n    </BaseSelect.Item>\n  );\n}\n\n/** A labelled group of options that stays on the list's tracks. */\nexport function SelectGroup({ label, children }: { label: ReactNode; children: ReactNode }) {\n  return (\n    <BaseSelect.Group className={groupClass}>\n      <BaseSelect.GroupLabel className={groupLabelClass}>{label}</BaseSelect.GroupLabel>\n      {children}\n    </BaseSelect.Group>\n  );\n}\n\n/** A full-bleed rule between groups. */\nexport function SelectSeparator() {\n  return <BaseSelect.Separator className={separatorClass} />;\n}\n\n/**\n * Inside a Base UI Field the trigger carries `data-invalid`; the shell reads it from there so\n * it draws and shakes like an Input in the same form.\n */\nfunction useDataInvalid(ref: RefObject<HTMLElement | null>) {\n  const [invalid, setInvalid] = useState(false);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const read = () => setInvalid(el.hasAttribute(\"data-invalid\"));\n    read();\n    const mo = new MutationObserver(read);\n    mo.observe(el, { attributes: true, attributeFilter: [\"data-invalid\"] });\n    return () => mo.disconnect();\n  }, [ref]);\n  return invalid;\n}\n\nfunction ChevronUpDownIcon() {\n  return (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden className=\"size-4\">\n      <path d=\"m5.25 6.25 2.75-2.75 2.75 2.75M5.25 9.75 8 12.5l2.75-2.75\" stroke=\"currentColor\" strokeWidth={1.5} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\nfunction ChevronIcon({ up = false }: { up?: boolean }) {\n  return (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden className={`relative size-3.5 ${up ? \"rotate-180\" : \"\"}`}>\n      <path d=\"m4.5 6.5 3.5 3.5 3.5-3.5\" stroke=\"currentColor\" strokeWidth={1.5} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/select/select.tsx"
    }
  ],
  "meta": {
    "details": "A select that opens with the chosen row over its trigger and grows from that row, or drops 4px below on touch. The highlight glides on the snappy spring.",
    "industries": [
      "saas",
      "finance",
      "hr",
      "health"
    ],
    "releaseAt": "2026-10-21"
  },
  "docs": "Import Select from components/eased/select/select; pass items (flat or grouped) or compose SelectItem children, and wrap it in Field for a label. The Eased tokens were merged into your global CSS; nothing to import.",
  "type": "registry:ui"
}