# Select

> Keyboard-friendly listbox with a glowing popover and typeahead.

Category: Forms. Docs: https://neonkit.heyaadi.com/components/select

## Installation

```bash
npx shadcn@latest add https://neonkit.heyaadi.com/r/select.json
```

With the `@neonkit` namespace configured: `npx shadcn@latest add @neonkit/select`.

npm dependencies: `@floating-ui/react-dom`, `motion`. Also needs `lib/utils.ts` (`cn`) and `lib/neon.ts` (`neonVars`), which the CLI adds.

Builds on: [Icons](https://neonkit.heyaadi.com/components/icons.md). The CLI installs them too.

## Usage

```tsx
import { Select } from "@/components/ui/select"

<Select
  label="Region"
  defaultValue="fra1"
  options={[
    { value: "fra1", label: "Frankfurt" },
    { value: "iad1", label: "Washington, D.C." },
  ]}
/>
```

## Examples

### Region picker

Options can carry a second line. Disabled options are skipped by the keyboard.

```tsx
import { Select } from "@/components/ui/select";

const regions = [
  { value: "fra1", label: "Frankfurt", description: "eu-central-1 · 12 ms" },
  { value: "lhr1", label: "London", description: "eu-west-2 · 18 ms" },
  { value: "iad1", label: "Washington, D.C.", description: "us-east-1 · 84 ms" },
  { value: "sfo1", label: "San Francisco", description: "us-west-1 · 142 ms" },
  { value: "gru1", label: "São Paulo", description: "sa-east-1 · 196 ms" },
  { value: "hnd1", label: "Tokyo", description: "ap-northeast-1 · 228 ms" },
  { value: "syd1", label: "Sydney", description: "ap-southeast-2 · At capacity", disabled: true },
];

export default function SelectDemo() {
  return (
    <Select
      label="Region"
      description="Where your functions run. Pick the one closest to your database."
      options={regions}
      defaultValue="fra1"
      containerClassName="max-w-xs"
    />
  );
}
```

### In a form

Give it a name and the value submits with the form through a hidden input.

```tsx
"use client";

import * as React from "react";
import { GitBranch, GitPullRequest, Rocket, Tag } from "@/components/ui/icons";
import { Button } from "@/components/ui/button";
import { Select } from "@/components/ui/select";

const refs = [
  { value: "main", label: "main", icon: <GitBranch />, description: "Production · deployed 2 hours ago" },
  { value: "release/2.4", label: "release/2.4", icon: <GitBranch />, description: "Release candidate" },
  { value: "feat/otp-input", label: "feat/otp-input", icon: <GitPullRequest />, description: "#128 · Preview ready" },
  { value: "fix/select-typeahead", label: "fix/select-typeahead", icon: <GitPullRequest />, description: "#131 · Checks failing", disabled: true },
  { value: "v2.3.1", label: "v2.3.1", icon: <Tag />, description: "Tag · last stable" },
];

export default function SelectDeployBranch() {
  const [status, setStatus] = React.useState("");

  function deploy(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const branch = new FormData(event.currentTarget).get("branch");
    setStatus(`Queued ${branch} for production.`);
  }

  return (
    <form onSubmit={deploy} className="grid w-full max-w-sm gap-2">
      <div className="flex items-end gap-2">
        <Select
          name="branch"
          label="Deploy branch"
          options={refs}
          defaultValue="main"
          onValueChange={() => setStatus("")}
          containerClassName="flex-1"
        />
        <Button type="submit" variant="solid">
          <Rocket /> Deploy
        </Button>
      </div>
      <p aria-live="polite" className="h-5 text-xs leading-5 text-white/55">
        {status}
      </p>
    </form>
  );
}
```

### Sizes

```tsx
import { Select } from "@/components/ui/select";

const sorts = [
  { value: "updated", label: "Recently updated" },
  { value: "created", label: "Newest first" },
  { value: "stars", label: "Most stars" },
  { value: "name", label: "Name" },
];

export default function SelectSizes() {
  return (
    <div className="grid w-full max-w-xs gap-3">
      <Select size="sm" aria-label="Sort repositories" options={sorts} defaultValue="updated" />
      <Select size="md" aria-label="Sort repositories" options={sorts} defaultValue="created" />
      <Select size="lg" aria-label="Sort repositories" options={sorts} defaultValue="stars" />
    </div>
  );
}
```

### Error and disabled

```tsx
import { Select } from "@/components/ui/select";

const plans = [
  { value: "hobby", label: "Hobby" },
  { value: "pro", label: "Pro" },
  { value: "enterprise", label: "Enterprise" },
];

const timezones = [
  { value: "America/Los_Angeles", label: "Pacific Time", description: "Los Angeles · UTC−8" },
  { value: "America/New_York", label: "Eastern Time", description: "New York · UTC−5" },
  { value: "Europe/London", label: "Greenwich Mean Time", description: "London · UTC+0" },
  { value: "Europe/Berlin", label: "Central European Time", description: "Berlin · UTC+1" },
  { value: "Asia/Kolkata", label: "India Standard Time", description: "Kolkata · UTC+5:30" },
  { value: "Asia/Tokyo", label: "Japan Standard Time", description: "Tokyo · UTC+9" },
];

export default function SelectStates() {
  return (
    <div className="grid w-full max-w-xs gap-5">
      <Select
        label="Timezone"
        placeholder="Choose a timezone"
        options={timezones}
        error="Pick a timezone so the weekly report lands on Monday morning."
      />
      <Select
        label="Plan"
        options={plans}
        defaultValue="pro"
        description="Only workspace owners can change the plan."
        disabled
      />
    </div>
  );
}
```

## API reference

### Select

Also accepts native button attributes, and the ref points at the trigger. className and style go to the trigger.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `options` | `SelectOption[]` | — | The choices, in order. |
| `value` | `string` | — | Controlled value. |
| `defaultValue` | `string` | — | Initial value when uncontrolled. |
| `onValueChange` | `(value: string) => void` | — | Called when the user picks a different option. |
| `placeholder` | `ReactNode` | `"Select…"` | Shown while nothing is selected. |
| `name` | `string` | — | Submits the value with a surrounding form. A form reset puts back the starting value. |
| `required` | `boolean` | `false` | Stops an empty submit with the browser's own message, turns the trigger red and shows the neon dot on the label. |
| `label` | `ReactNode` | — | Visible label. Names both the trigger and the list. |
| `description` | `ReactNode` | — | Helper text under the trigger. |
| `error` | `ReactNode` | — | Red invalid state and message. Replaces description. Pass true for the state without a message. |
| `disabled` | `boolean` | `false` | Blocks opening and leaves the value out of form data. |
| `color` | `NeonColor` | `inherited` | Palette name or hex. Falls back to --neon-accent, then cyan. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Trigger height and option padding. |
| `containerClassName` | `string` | — | Classes for the outer block with the label, list and message. |

### SelectOption

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | — | Unique value, also the React key. |
| `label` | `string` | — | Shown in the list and the trigger. Typeahead matches against it. |
| `description` | `string` | — | Second line, shown in the list only. |
| `icon` | `ReactNode` | — | Shown before the label in the list and the trigger. |
| `disabled` | `boolean` | `false` | Visible but can't be picked. |

## Accessibility

- When the browser rejects the field on submit (required, type, pattern), it turns red until it's edited or the form resets. The browser shows its own message.
- Follows the WAI-ARIA select-only combobox pattern: focus stays on the trigger and aria-activedescendant tracks the highlighted option.
- Enter, Space and the arrow keys open the list. Arrows move, Home and End jump, Enter or Space picks, Escape and Tab close.
- Typing letters jumps to the first matching option. Pressing the same letter again cycles through matches.
- Without a visible label, pass aria-label. It names both the trigger and the list.
- The list renders in a portal, flips above the trigger near the bottom of the screen and is never clipped by overflow-hidden ancestors.

## Source

`components/ui/select.tsx`

```tsx
"use client";

import * as React from "react";
import { createPortal } from "react-dom";
import { autoUpdate, flip, offset, shift, size as fitToViewport, useFloating } from "@floating-ui/react-dom";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { Check, ChevronDown } from "@/components/ui/icons";

import { cn } from "@/lib/utils";
import { neonVars, type NeonColor } from "@/lib/neon";

const INVALID = "#ff4d5e";
const EASE_OUT = [0.23, 1, 0.32, 1] as const;

const subscribeNoop = () => () => {};

const useIsomorphicLayoutEffect =
  typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;

const sizes = {
  sm: {
    trigger: "h-8 gap-2 rounded-md px-2.5 text-xs [&_svg]:size-3.5",
    list: "rounded-lg",
    option: "gap-2 rounded-md px-2 py-1.5 text-xs [&_svg]:size-3.5",
    hint: "text-[11px]",
    label: "text-xs",
  },
  md: {
    trigger: "h-10 gap-2 rounded-lg px-3 text-sm [&_svg]:size-4",
    list: "rounded-xl",
    option: "gap-2.5 rounded-lg px-2.5 py-2 text-sm [&_svg]:size-4",
    hint: "text-xs",
    label: "text-sm",
  },
  lg: {
    trigger: "h-12 gap-2.5 rounded-xl px-4 text-base [&_svg]:size-[18px]",
    list: "rounded-xl",
    option: "gap-3 rounded-lg px-3 py-2.5 text-base [&_svg]:size-[18px]",
    hint: "text-sm",
    label: "text-sm",
  },
} as const;

export interface SelectOption {
  value: string;
  label: string;
  /** Second line under the label, inside the list only. */
  description?: string;
  icon?: React.ReactNode;
  disabled?: boolean;
}

/**
 * True once the browser rejects the field on submit (required, type, pattern), until it's
 * edited, its value changes or the form resets. The browser still shows its own message;
 * this keeps the field red after that bubble fades.
 */
function useRejected(ref: React.RefObject<HTMLInputElement | HTMLTextAreaElement | null>, value?: string) {
  const [rejected, setRejected] = React.useState<{ value?: string } | null>(null);
  const valueRef = React.useRef(value);
  React.useEffect(() => {
    valueRef.current = value;
  });
  React.useEffect(() => {
    const element = ref.current;
    if (!element) return;
    const form = element.form;
    const flag = () => setRejected({ value: valueRef.current });
    const clear = () => setRejected(null);
    element.addEventListener("invalid", flag);
    element.addEventListener("input", clear);
    form?.addEventListener("reset", clear);
    return () => {
      element.removeEventListener("invalid", flag);
      element.removeEventListener("input", clear);
      form?.removeEventListener("reset", clear);
    };
  }, [ref]);
  return rejected !== null && rejected.value === value;
}

export interface SelectProps
  extends Omit<
    React.ButtonHTMLAttributes<HTMLButtonElement>,
    "color" | "value" | "defaultValue" | "onChange" | "children" | "type"
  > {
  options: SelectOption[];
  value?: string;
  defaultValue?: string;
  onValueChange?: (value: string) => void;
  placeholder?: React.ReactNode;
  /** Palette name or any hex color. Inherits `--neon-accent` when omitted. */
  color?: NeonColor;
  size?: keyof typeof sizes;
  /** Submits the value under this name with a surrounding form. */
  name?: string;
  /** Blocks form submission until something is picked, with the browser's own message. */
  required?: boolean;
  label?: React.ReactNode;
  /** Helper text under the trigger. Hidden while `error` is set. */
  description?: React.ReactNode;
  /** Marks the select invalid and shows this message. Pass `true` for the red state alone. */
  error?: React.ReactNode;
  /** Classes for the trigger button. */
  className?: string;
  /** Classes for the outer block holding the label, trigger, list and message. */
  containerClassName?: string;
}

const Select = React.forwardRef<HTMLButtonElement, SelectProps>(
  (
    {
      options,
      value,
      defaultValue,
      onValueChange,
      placeholder = "Select…",
      color,
      size = "md",
      name,
      required = false,
      label,
      description,
      error,
      disabled,
      id: idProp,
      className,
      containerClassName,
      style,
      onClick,
      onKeyDown,
      onKeyUp,
      onMouseDown,
      onBlur,
      "aria-label": ariaLabel,
      "aria-labelledby": ariaLabelledBy,
      "aria-describedby": ariaDescribedBy,
      ...props
    },
    ref,
  ) => {
    const triggerRef = React.useRef<HTMLButtonElement>(null);
    const listRef = React.useRef<HTMLDivElement>(null);
    React.useImperativeHandle(ref, () => triggerRef.current as HTMLButtonElement);

    const autoId = React.useId();
    const id = idProp ?? autoId;
    const labelId = `${id}-label`;
    const listboxId = `${id}-listbox`;
    const optionId = (index: number) => `${id}-option-${index}`;

    const errored = Boolean(error);
    const message = errored ? (error === true ? null : error) : description;
    const messageId = message ? `${id}-${errored ? "error" : "description"}` : undefined;
    const s = sizes[size];
    const reduceMotion = useReducedMotion();

    const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
    const selectedValue = value !== undefined ? value : uncontrolledValue;

    // A form reset goes back to the value the select started with.
    const proxyRef = React.useRef<HTMLInputElement>(null);
    const rejected = useRejected(proxyRef, selectedValue ?? "");
    // A wrapping Field marks its control with aria-invalid; it lights the same red as `error`.
    const ariaInvalid = props["aria-invalid"];
    const invalid = errored || rejected || ariaInvalid === true || ariaInvalid === "true";
    const initialValue = React.useRef(selectedValue);
    const resetRef = React.useRef<() => void>(() => {});
    React.useEffect(() => {
      resetRef.current = () => {
        const initial = initialValue.current;
        if (initial === selectedValue) return;
        if (value === undefined) setUncontrolledValue(initial);
        if (initial !== undefined) onValueChange?.(initial);
      };
    });
    React.useEffect(() => {
      const form = proxyRef.current?.form;
      if (!form) return;
      const onReset = () => resetRef.current();
      form.addEventListener("reset", onReset);
      return () => form.removeEventListener("reset", onReset);
    }, []);
    const selectedIndex = options.findIndex((option) => option.value === selectedValue);
    const selected = selectedIndex >= 0 ? options[selectedIndex] : undefined;

    const [open, setOpen] = React.useState(false);
    // The list is portaled out of this subtree, so it gets the resolved color copied over.
    const [carriedNeon, setCarriedNeon] = React.useState<string>();
    const isClient = React.useSyncExternalStore(subscribeNoop, () => true, () => false);

    const { refs, floatingStyles, placement } = useFloating({
      placement: "bottom-start",
      strategy: "fixed",
      // Position with top/left; motion owns `transform` for the enter animation.
      transform: false,
      whileElementsMounted: autoUpdate,
      middleware: [
        offset(6),
        flip({ padding: 8 }),
        shift({ padding: 8 }),
        fitToViewport({
          padding: 8,
          apply({ rects, availableHeight, elements }) {
            Object.assign(elements.floating.style, {
              width: `${rects.reference.width}px`,
              maxHeight: `${Math.max(120, Math.min(256, availableHeight))}px`,
            });
          },
        }),
      ],
    });
    const above = placement.startsWith("top");
    const [activeIndex, setActiveIndex] = React.useState(-1);
    const isOpen = open && !disabled;

    const typeahead = React.useRef({ query: "", timer: 0 });
    // Only keyboard moves scroll the list; hover never yanks it under the cursor.
    const scrollToActive = React.useRef(false);

    React.useEffect(() => {
      const state = typeahead.current;
      return () => window.clearTimeout(state.timer);
    }, []);

    useIsomorphicLayoutEffect(() => {
      const list = listRef.current;
      if (!isOpen || !list || activeIndex < 0 || !scrollToActive.current) return;
      scrollToActive.current = false;
      const option = document.getElementById(`${id}-option-${activeIndex}`);
      if (!option) return;
      const top = option.offsetTop - 4;
      const bottom = option.offsetTop + option.offsetHeight + 4;
      if (top < list.scrollTop) list.scrollTop = top;
      else if (bottom > list.scrollTop + list.clientHeight) {
        list.scrollTop = bottom - list.clientHeight;
      }
    }, [isOpen, activeIndex, id]);

    function step(from: number, direction: 1 | -1) {
      for (let i = from; i >= 0 && i < options.length; i += direction) {
        if (!options[i].disabled) return i;
      }
      return -1;
    }

    function highlight(index: number) {
      if (index < 0) return;
      scrollToActive.current = true;
      setActiveIndex(index);
    }

    function openList(fallback: "first" | "last" = "first") {
      if (disabled) return;
      const index =
        selected && !selected.disabled
          ? selectedIndex
          : fallback === "first"
            ? step(0, 1)
            : step(options.length - 1, -1);
      scrollToActive.current = true;
      setActiveIndex(index);
      if (triggerRef.current) {
        setCarriedNeon(getComputedStyle(triggerRef.current).getPropertyValue("--neon").trim() || undefined);
      }
      setOpen(true);
    }

    function close() {
      setOpen(false);
      typeahead.current.query = "";
    }

    function commit(index: number) {
      const option = options[index];
      if (option && !option.disabled && option.value !== selectedValue) {
        if (value === undefined) setUncontrolledValue(option.value);
        onValueChange?.(option.value);
      }
      close();
      triggerRef.current?.focus();
    }

    function matchTypeahead(char: string) {
      const state = typeahead.current;
      window.clearTimeout(state.timer);
      state.timer = window.setTimeout(() => {
        state.query = "";
      }, 500);
      state.query += char.toLowerCase();

      // Pressing the same letter repeatedly cycles through options that start with it.
      const repeated = state.query.split("").every((c) => c === state.query[0]);
      const query = repeated ? state.query[0] : state.query;
      const from = isOpen ? activeIndex : selectedIndex;
      const offset = repeated ? 1 : 0;
      for (let n = 0; n < options.length; n++) {
        const index = (((from + offset + n) % options.length) + options.length) % options.length;
        const option = options[index];
        if (!option.disabled && option.label.toLowerCase().startsWith(query)) return index;
      }
      return -1;
    }

    function handleKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {
      onKeyDown?.(event);
      if (event.defaultPrevented || disabled) return;
      const { key } = event;
      const printable = key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;

      if (!isOpen) {
        if (key === "Enter" || key === " " || key === "ArrowDown") {
          event.preventDefault();
          openList("first");
        } else if (key === "ArrowUp") {
          event.preventDefault();
          openList("last");
        } else if (key === "Home" || key === "End") {
          event.preventDefault();
          highlight(key === "Home" ? step(0, 1) : step(options.length - 1, -1));
          setOpen(true);
        } else if (printable) {
          const index = matchTypeahead(key);
          if (index >= 0) {
            highlight(index);
            setOpen(true);
          }
        }
        return;
      }

      switch (key) {
        case "ArrowDown":
          event.preventDefault();
          highlight(step(activeIndex + 1, 1));
          break;
        case "ArrowUp":
          event.preventDefault();
          if (event.altKey) commit(activeIndex);
          else highlight(step(activeIndex - 1, -1));
          break;
        case "Home":
          event.preventDefault();
          highlight(step(0, 1));
          break;
        case "End":
          event.preventDefault();
          highlight(step(options.length - 1, -1));
          break;
        case "Enter":
          event.preventDefault();
          commit(activeIndex);
          break;
        case "Escape":
          event.preventDefault();
          event.stopPropagation();
          close();
          triggerRef.current?.focus();
          break;
        case "Tab":
          close();
          break;
        default:
          if (key === " " && !typeahead.current.query) {
            event.preventDefault();
            commit(activeIndex);
          } else if (printable) {
            event.preventDefault();
            highlight(matchTypeahead(key));
          }
      }
    }

    return (
      <div
        style={neonVars(color)}
        className={cn("[--neon:var(--neon-accent,34_211_238)]", "grid w-full gap-1.5", containerClassName)}
      >
        {label && (
          <label
            id={labelId}
            htmlFor={id}
            onClick={(event) => {
              // Focus the trigger like a native select label, without opening the list.
              event.preventDefault();
              triggerRef.current?.focus();
            }}
            className={cn("w-fit font-medium text-white/80", s.label)}
          >
            {label}
            {required && (
              <span
                aria-hidden
                title="Required"
                className="relative -top-px ml-1.5 inline-block size-[5px] rounded-full bg-[rgb(var(--neon))] align-middle shadow-[0_0_4px_rgb(var(--neon)/CALC(0.9*var(--neon-glow,1))),0_0_10px_rgb(var(--neon)/CALC(0.5*var(--neon-glow,1)))]"
              />
            )}
          </label>
        )}
        <div className="relative">
          <button
            ref={(node) => {
              triggerRef.current = node;
              refs.setReference(node);
            }}
            id={id}
            type="button"
            role="combobox"
            disabled={disabled}
            aria-haspopup="listbox"
            aria-expanded={isOpen}
            aria-controls={isOpen ? listboxId : undefined}
            aria-activedescendant={isOpen && activeIndex >= 0 ? optionId(activeIndex) : undefined}
            aria-label={ariaLabel}
            aria-labelledby={label ? labelId : ariaLabelledBy}
            aria-describedby={[ariaDescribedBy, messageId].filter(Boolean).join(" ") || undefined}
            aria-invalid={invalid || undefined}
            aria-required={required || undefined}
            data-state={isOpen ? "open" : "closed"}
            data-placeholder={selected ? undefined : ""}
            style={invalid ? neonVars(INVALID, style) : style}
            className={cn(
              "flex w-full cursor-pointer select-none items-center border border-white/10 bg-white/[0.03] text-left text-white/90 outline-none",
              "transition-[border-color,background-color,box-shadow] ease-[cubic-bezier(0.23,1,0.32,1)] duration-[150ms,150ms,320ms]",
              "focus-visible:border-[rgb(var(--neon)/0.8)] focus-visible:bg-[rgb(var(--neon)/0.04)] focus-visible:shadow-[0_0_0_1px_rgb(var(--neon)/0.2),0_0_20px_-4px_rgb(var(--neon)/CALC(0.5*var(--neon-glow,1))),inset_0_0_12px_-2px_rgb(var(--neon)/CALC(0.14*var(--neon-glow,1)))]",
              "disabled:cursor-not-allowed disabled:opacity-50",
              "[&_svg]:pointer-events-none [&_svg]:shrink-0",
              !isOpen && "hover:border-white/20",
              invalid && "border-[rgb(var(--neon)/0.55)]",
              invalid && !isOpen && "hover:border-[rgb(var(--neon)/0.75)]",
              isOpen &&
                "border-[rgb(var(--neon)/0.8)] bg-[rgb(var(--neon)/0.04)] shadow-[0_0_0_1px_rgb(var(--neon)/0.2),0_0_20px_-4px_rgb(var(--neon)/CALC(0.5*var(--neon-glow,1))),inset_0_0_12px_-2px_rgb(var(--neon)/CALC(0.14*var(--neon-glow,1)))]",
              s.trigger,
              className,
            )}
            {...props}
            onMouseDown={(event) => {
              onMouseDown?.(event);
              // Focus is set on click instead, so Safari (which never focuses buttons) behaves.
              if (event.button === 0) event.preventDefault();
            }}
            onClick={(event) => {
              onClick?.(event);
              if (event.defaultPrevented) return;
              triggerRef.current?.focus();
              if (isOpen) close();
              else openList("first");
            }}
            onKeyDown={handleKeyDown}
            onKeyUp={(event) => {
              onKeyUp?.(event);
              // Space is handled on keydown; cancel the native click it would fire on keyup.
              if (event.key === " ") event.preventDefault();
            }}
            onBlur={(event) => {
              onBlur?.(event);
              close();
            }}
          >
            {selected?.icon && (
              <span className="flex shrink-0 items-center text-white/50">{selected.icon}</span>
            )}
            <span className={cn("min-w-0 flex-1 truncate", !selected && "text-white/50")}>
              {selected ? selected.label : placeholder}
            </span>
            <ChevronDown
              aria-hidden
              className={cn(
                "text-white/40 transition-[transform,color] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none",
                isOpen && "rotate-180 text-[rgb(var(--neon))]",
              )}
            />
          </button>

          {/* Carries the value into forms. It sits under the trigger so the browser's
              "please select" bubble points at the right place, and hands focus on. */}
          <input
            ref={proxyRef}
            aria-hidden
            tabIndex={-1}
            name={name}
            value={selectedValue ?? ""}
            required={required}
            disabled={disabled}
            onChange={() => {}}
            onFocus={() => triggerRef.current?.focus()}
            className="pointer-events-none absolute inset-x-0 bottom-0 h-px w-full opacity-0"
          />

          {isClient &&
            createPortal(
              <AnimatePresence>
                {isOpen && (
                  <motion.div
                    ref={(node) => {
                      listRef.current = node;
                      refs.setFloating(node);
                    }}
                    id={listboxId}
                    role="listbox"
                    aria-labelledby={label ? labelId : ariaLabelledBy}
                    aria-label={label ? undefined : ariaLabel}
                    initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: above ? 4 : -4, scale: 0.98 }}
                    animate={{ opacity: 1, y: 0, scale: 1 }}
                    exit={reduceMotion ? { opacity: 0 } : { opacity: 0, y: above ? 4 : -4, scale: 0.98 }}
                    transition={{ duration: 0.16, ease: EASE_OUT }}
                    // Keep focus on the trigger so aria-activedescendant stays in charge.
                    onMouseDown={(event) => event.preventDefault()}
                    style={
                      {
                        ...floatingStyles,
                        ...(carriedNeon ? { "--neon": carriedNeon, "--neon-accent": carriedNeon } : null),
                      } as React.CSSProperties
                    }
                    className={cn(
                      "[--neon:var(--neon-accent,34_211_238)] z-50 overflow-y-auto overscroll-contain border border-white/10 bg-[#0a0b11] p-1",
                      "shadow-[0_18px_48px_-12px_rgb(0_0_0/0.85),0_0_0_1px_rgb(var(--neon)/0.06),0_0_28px_-10px_rgb(var(--neon)/CALC(0.45*var(--neon-glow,1)))]",
                      "[scrollbar-color:rgb(255_255_255/0.15)_transparent] [scrollbar-width:thin]",
                      above ? "origin-bottom" : "origin-top",
                      s.list,
                    )}
                  >
                    {options.map((option, index) => {
                      const active = index === activeIndex;
                      const isSelected = index === selectedIndex;
                      return (
                        <div
                          key={option.value}
                          id={optionId(index)}
                          role="option"
                          aria-selected={isSelected}
                          aria-disabled={option.disabled || undefined}
                          data-active={active || undefined}
                          onPointerMove={() => {
                            if (!option.disabled && !active) setActiveIndex(index);
                          }}
                          onClick={() => {
                            if (!option.disabled) commit(index);
                          }}
                          className={cn(
                            "flex cursor-pointer select-none items-center text-white/75 transition-colors duration-100",
                            s.option,
                            active && "bg-[rgb(var(--neon)/0.1)] text-white",
                            isSelected && "text-[rgb(var(--neon))]",
                            option.disabled && "cursor-not-allowed opacity-40",
                          )}
                        >
                          {option.icon && (
                            <span
                              className={cn(
                                "flex shrink-0 items-center",
                                isSelected ? "text-[rgb(var(--neon))]" : "text-white/50",
                              )}
                            >
                              {option.icon}
                            </span>
                          )}
                          <span className="flex min-w-0 flex-1 flex-col">
                            <span className="truncate">{option.label}</span>
                            {option.description && (
                              <span
                                className={cn(
                                  "truncate",
                                  s.hint,
                                  active ? "text-white/60" : "text-white/50",
                                )}
                              >
                                {option.description}
                              </span>
                            )}
                          </span>
                          <Check
                            aria-hidden
                            className={cn(
                              "shrink-0 text-[rgb(var(--neon))] [filter:drop-shadow(0_0_6px_rgb(var(--neon)/CALC(0.7*var(--neon-glow,1))))]",
                              !isSelected && "invisible",
                            )}
                          />
                        </div>
                      );
                    })}
                  </motion.div>
                )}
              </AnimatePresence>,
              document.body,
            )}
        </div>
        {message && (
          <p
            id={messageId}
            className={cn("text-xs leading-5", errored ? "text-[#ff4d5e]" : "text-white/55")}
          >
            {message}
          </p>
        )}
      </div>
    );
  },
);
Select.displayName = "Select";

export { Select };
```
