Components

Getting started

Components144

More

A choice from a list that is never the browser's dropdown: the pill grows into its list and folds back as the label morphs.

Used forFormsTable toolbarsSettingsMapping screens

Install with the shadcn CLI

$ npx shadcn@latest add https://hairlineui.com/r/select.json

Templates

Need the whole page?

43 Next.js landing pages, each designed from scratch. Try any of them live; one is $39, all of them $99, and 5 are free.

All 43 templates

Where it’s used

  • Forms

    Country, role or timezone with a label and hint.

  • Table toolbars

    Sort by and rows per page.

  • Settings

    Language and theme choices.

  • Mapping screens

    Picking a field for each column.

What it handles for you

A styled select has to handle typeahead, scrolling lists, collision with the viewport and clipping parents. This one does all four.

Instead of wiring upRadix Selectnative <select>portal and collision logic
  1. One surface

    The pill grows into the list and folds back; nothing pops over it.

  2. Typeahead

    Typing a few letters finds an option; arrows glide a highlight.

  3. Never clipped

    A portal on the body that carries the theme tokens with it.

  4. Field like the rest

    Label, hint and error from field.tsx.

Overview

Native selects ignore the theme and look different on every system; most custom ones pop a panel over the trigger. Here the pill itself grows into the list from the edge it lines up with, opens upward when there is no room, and folds back as its label morphs to the choice. It lives in a portal, so no card or scroller clips it.

States

closed
A pill with the chosen label and a chevron.
open
The pill grows into its list (420ms, size and corners), upward if there is no room.
pick
The list folds back and the label morphs; the chosen option carries a drawn check.

Usage

usage.tsx
import { Select } from "@/components/ui/select";<Select label="Timezone" defaultValue="europe-london" options={[  { value: "europe-london", label: "London" },  { value: "america-new-york", label: "New York" },]} />

Works with

Props

PropTypeDescription
options{ value, label, description? }[]The list.
value / defaultValuestring | nullChosen value.
onValueChange(value: string) => voidCalled on choice.
labelstringShown above; accessible name.
hideLabelbooleandefault falseKeep the name, hide the text.
hint / errorReactNodeAs in Input.
placeholder / align / width / mono…Passed to the pill.

Accessibility and motion

  • Escape folds it back; scrolling the page closes it.
  • Reduced motion: it opens and closes in place.

Requirements

React 19, Tailwind CSS v4 and shadcn/ui theme variables (any style, light and dark). Icons from lucide-react.

The CLI also installs Text morph.

Source

components/select.tsx
"use client";import * as React from "react";import { Field, useFieldIds } from "./field";import { PillSelect, type PillOption, type PillSelectProps } from "./pill-select";/* ───────────────────────────────────────────────────────── * SELECT: a choice from a list, never the browser's dropdown * *   open     the pill grows into its list (size and corners ease) *            from the edge it lines up with; upward when there's *            no room below; long lists scroll *   pick     the list folds back into the pill and the label *            morphs to the choice, which carries a drawn check *   keys     arrows glide a highlight, typing finds an option, *            Enter picks, Escape folds it back * * The list lives in a portal, so no toolbar, card or scroller * clips it. Labelled and hinted like every other field. * ───────────────────────────────────────────────────────── */export type SelectOption = PillOption;export interface SelectProps extends Omit<PillSelectProps, "label" | "value" | "onValueChange"> {  /** Shown above; also the accessible name. */  label: string;  /** Hide the label visually (it stays the accessible name). */  hideLabel?: boolean;  hint?: React.ReactNode;  error?: React.ReactNode;  value?: string | null;  defaultValue?: string | null;  onValueChange?: (value: string) => void;  id?: string;}export function Select({ label, hideLabel, hint, error, value, defaultValue = null, onValueChange, id, className = "", block = true, size = "md", ...props }: SelectProps) {  const ids = useFieldIds(id);  const [own, setOwn] = React.useState<string | null>(defaultValue);  const current = value !== undefined ? value : own;  return (    <Field id={ids.id} label={hideLabel ? undefined : label} hint={hint} error={error} className={className}>      <PillSelect        {...props}        block={block}        size={size}        label={label}        value={current}        onValueChange={(v) => {          if (value === undefined) setOwn(v);          onValueChange?.(v);        }}      />    </Field>  );}export default Select;
components/field.tsx
"use client";import * as React from "react";import { EASE_OUT, useInteractionReducedMotion } from "./ui-interaction-motion";/* ───────────────────────────────────────────────────────── * FIELD: the column every text control sits in * *   label    above, with an optional aside on the right (a count) *   hint     under the control while all is well *   error    takes the hint's place: the hint folds away, the *            error rises in out of a blur, the control shakes once * * Shared by input, textarea, password, search and select so the * whole family has one surface, one focus ring and one error. * ───────────────────────────────────────────────────────── */export interface FieldProps {  id: string;  label?: React.ReactNode;  /** Right side of the label row, e.g. a character count. */  aside?: React.ReactNode;  hint?: React.ReactNode;  error?: React.ReactNode;  required?: boolean;  className?: string;  children: React.ReactNode;}export type FieldSize = "sm" | "md";/** The surface of a text control: a hairline that darkens on hover and turns to the ring on focus. */export function fieldSurface({ invalid, disabled, round = true }: { invalid?: boolean; disabled?: boolean; round?: boolean }) {  const ring = invalid    ? "shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--color-red-500)_55%,transparent)] focus-within:shadow-[inset_0_0_0_1px_var(--color-red-500),0_0_0_3px_color-mix(in_oklab,var(--color-red-500)_14%,transparent)]"    : "shadow-[inset_0_0_0_1px_var(--border)] [&:not(:focus-within)]:hover:shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--foreground)_24%,var(--border))] focus-within:shadow-[inset_0_0_0_1px_var(--ring),0_0_0_3px_color-mix(in_oklab,var(--ring)_16%,transparent)]";  return `relative flex w-full items-center bg-background text-foreground transition-[box-shadow,background-color,opacity] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)] ${round ? "rounded-full" : "rounded-[14px]"} ${ring} ${    disabled ? "pointer-events-none opacity-50" : ""  }`;}/** ids for the control's label, hint and error. */export function useFieldIds(id?: string) {  const auto = React.useId();  const base = id ?? auto;  return { id: base, hint: `${base}-hint`, error: `${base}-error` };}/** One short shake whenever `on` turns true (an error arrives). */export function useShake<T extends HTMLElement>(on: boolean) {  const ref = React.useRef<T>(null);  const reduced = useInteractionReducedMotion();  React.useEffect(() => {    if (!on || reduced) return;    ref.current?.animate(      [{ transform: "none" }, { transform: "translateX(-4px)" }, { transform: "translateX(4px)" }, { transform: "translateX(-2px)" }, { transform: "none" }],      { duration: 380, easing: "ease-out" },    );  }, [on, reduced]);  return ref;}/** Sets a native input's value so React's onChange still fires (clear buttons, pickers). */export function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string) {  const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;  Object.getOwnPropertyDescriptor(proto, "value")?.set?.call(el, value);  el.dispatchEvent(new Event("input", { bubbles: true }));}/** A line under the control that folds open and closed; keeps its last words while closing. */function Line({ show, children, id, tone, reduced }: { show: boolean; children: React.ReactNode; id: string; tone: string; reduced: boolean }) {  const [kept, setKept] = React.useState(children);  if (show && children !== kept) setKept(children);  return (    <div      className="grid"      style={{ gridTemplateRows: show ? "1fr" : "0fr", transition: reduced ? "none" : `grid-template-rows 320ms ${EASE_OUT}` }}    >      <div className="min-h-0 overflow-hidden">        <p          id={id}          className={`pt-1.5 pl-3 text-[12.5px] leading-snug ${tone}`}          style={{            opacity: show ? 1 : 0,            filter: show ? "blur(0px)" : "blur(4px)",            transform: show ? "none" : "translateY(-4px)",            transition: reduced ? "none" : show ? `opacity 280ms ${EASE_OUT} 60ms, filter 280ms ${EASE_OUT} 60ms, transform 380ms ${EASE_OUT}` : `opacity 140ms ${EASE_OUT}, filter 140ms ${EASE_OUT}, transform 200ms ${EASE_OUT}`,          }}        >          {show ? children : kept}        </p>      </div>    </div>  );}export function Field({ id, label, aside, hint, error, required, className = "", children }: FieldProps) {  const reduced = useInteractionReducedMotion();  const ids = { hint: `${id}-hint`, error: `${id}-error` };  return (    <div className={`flex w-full flex-col ${className}`}>      {(label || aside) && (        <div className="flex items-baseline justify-between gap-3 px-3 pb-1.5">          {label && (            <label htmlFor={id} className="text-[13px] font-medium text-foreground">              {label}              {required && (                <span aria-hidden="true" className="ml-0.5 text-muted-foreground">                  *                </span>              )}            </label>          )}          {aside && <span className="text-[12px] tabular-nums text-muted-foreground">{aside}</span>}        </div>      )}      {children}      <Line show={!error && !!hint} id={ids.hint} tone="text-muted-foreground" reduced={reduced}>        {hint}      </Line>      <Line show={!!error} id={ids.error} tone="text-red-600 dark:text-red-400" reduced={reduced}>        {error}      </Line>    </div>  );}export default Field;
components/pill-select.tsx
"use client";import * as React from "react";import { createPortal } from "react-dom";import { TextMorph } from "./text-morph";/* ───────────────────────────────────────────────────────── * PILL SELECT: a select that's the same surface as its pill * *   open      the pill grows into its list (size and corners *             ease) from the edge it's lined up with, while its *             label fades into the options; it opens upward when *             there's no room below, and long lists scroll *   pick      the list folds back into the pill and the label *             morphs to the new choice; the current option *             carries a drawn check *   keys      arrows move a highlight that glides between rows, *             Home and End jump, typing a few letters finds an *             option, Enter picks, Escape folds it back * * The list lives in a portal on the page's body, placed over * the pill, so a clipping toolbar, fold or scroller never cuts * it off; it carries the theme's colours with it. Scrolling * the page closes it. * * Used wherever a component needs a choice from a list; never * the browser's own dropdown, which can't follow the theme. * ───────────────────────────────────────────────────────── */export interface PillOption {  value: string;  label: string;  /** A second line in the list, e.g. a sample value. */  description?: string;}export interface PillSelectProps {  options: PillOption[];  value: string | null | undefined;  onValueChange: (value: string) => void;  /** Accessible name, e.g. "Column for Email". */  label: string;  /** Shown while nothing is chosen. */  placeholder?: string;  /** What the pill shows for a choice; the option's label by default. */  display?: (option: PillOption | undefined) => string;  /** The pill's contents, when it needs more than text (an icon that turns). */  renderValue?: (option: PillOption | undefined, open: boolean) => React.ReactNode;  /** Monospace labels, for code-like values (event names, column headers). */  mono?: boolean;  /** Which edge of the pill the list lines up with. */  align?: "start" | "end";  /** The list's width; never narrower than the pill. */  width?: number;  /** Fill the width it's given. */  block?: boolean;  /** Dims the pill's text, e.g. for "Don't import". */  muted?: boolean;  disabled?: boolean;  /** "sm" is the 28px pill for toolbars; "md" is 36px, the height of the other form fields. */  size?: "sm" | "md";  className?: string;}type Phase = "closed" | "opening" | "open" | "closing";const EASE = "cubic-bezier(0.16,1,0.3,1)";const DURATION = 420;const MAX_LIST = 280;const PILL_SIZE = { sm: "h-7 px-2.5 text-[12px]", md: "h-9 pl-4 pr-3.5 text-[13.5px]" };// The theme travels with the list, so it matches the pill wherever the pill's colours were set.const TOKENS = ["--background", "--foreground", "--popover", "--popover-foreground", "--border", "--accent", "--accent-foreground", "--primary", "--primary-foreground", "--muted", "--muted-foreground", "--ring"];const FOCUS = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40";const reducedQuery = "(prefers-reduced-motion: reduce)";const subscribeReduced = (onChange: () => void) => {  const query = window.matchMedia(reducedQuery);  query.addEventListener("change", onChange);  return () => query.removeEventListener("change", onChange);};const useReducedMotion = () =>  React.useSyncExternalStore(subscribeReduced, () => window.matchMedia(reducedQuery).matches, () => false);function Chevron({ up, reduced }: { up: boolean; reduced: boolean }) {  return (    <svg viewBox="0 0 16 16" fill="none" aria-hidden="true" className="size-3 shrink-0 text-muted-foreground" style={{ transform: up ? "rotate(180deg)" : "none", transition: reduced ? "none" : `transform 320ms ${EASE}` }}>      <path d="M4.5 6.25 8 9.75l3.5-3.5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />    </svg>  );}export function PillSelect({  options,  value,  onValueChange,  label,  placeholder = "Choose",  display,  renderValue,  mono = false,  align = "start",  width = 240,  block = false,  muted = false,  disabled = false,  size = "sm",  className = "",}: PillSelectProps) {  const id = React.useId();  const reduced = useReducedMotion();  const [phase, setPhase] = React.useState<Phase>("closed");  const [active, setActive] = React.useState(0);  const [pill, setPill] = React.useState({ width: 0, height: 28 });  const [panelHeight, setPanelHeight] = React.useState(0);  const [fit, setFit] = React.useState({ up: false, width, shift: 0, x: 0, y: 0, theme: {} as Record<string, string> });  const [highlight, setHighlight] = React.useState({ top: 0, height: 0, ready: false });  const [quiet, setQuiet] = React.useState(false);  const wrapRef = React.useRef<HTMLDivElement>(null);  const triggerRef = React.useRef<HTMLButtonElement>(null);  const panelRef = React.useRef<HTMLDivElement>(null);  const surfaceRef = React.useRef<HTMLDivElement>(null);  const listRef = React.useRef<HTMLDivElement>(null);  const rowRefs = React.useRef<(HTMLDivElement | null)[]>([]);  const timer = React.useRef(0);  const typed = React.useRef({ text: "", at: 0 });  const selected = options.find((o) => o.value === value);  const expanded = phase === "open";  const mounted = phase !== "closed";  const [prevPhase, setPrevPhase] = React.useState(phase);  if (phase !== prevPhase) {    setPrevPhase(phase);    if (phase === "closed") {      setHighlight({ top: 0, height: 0, ready: false });    }  }  const text = display ? display(selected) : (selected?.label ?? placeholder);  const measureFit = () => {    const wrap = wrapRef.current;    if (!wrap) return;    const rect = wrap.getBoundingClientRect();    const vw = document.documentElement.clientWidth;    const w = Math.min(Math.max(width, rect.width), vw - 24);    // Lined up with the pill's edge, nudged back on screen.    const shift = align === "start" ? Math.min(0, vw - 12 - (rect.left + w)) : Math.max(0, 12 - (rect.right - w));    const estimate = Math.min(MAX_LIST, options.length * (options.some((o) => o.description) ? 46 : 32)) + 12;    const vh = window.innerHeight;    const below = vh - 8 - rect.top;    const above = rect.bottom - 8;    const up = below < estimate + 8 && above > below;    // Fixed coordinates of the pill's corner the list grows from.    const x = align === "start" ? rect.left : vw - rect.right;    const y = up ? vh - rect.bottom : rect.top;    const cs = getComputedStyle(wrap);    const theme: Record<string, string> = { colorScheme: cs.colorScheme, fontFamily: cs.fontFamily };    for (const t of TOKENS) {      const v = cs.getPropertyValue(t);      if (v) theme[t] = v;    }    setFit({ up, width: w, shift, x, y, theme });  };  const open = () => {    if (disabled || phase === "open" || phase === "opening") return;    window.clearTimeout(timer.current);    measureFit();    setActive(Math.max(0, options.findIndex((o) => o.value === value)));    setPhase(reduced ? "open" : "opening");  };  const close = React.useCallback(    (refocus = true) => {      window.clearTimeout(timer.current);      if (refocus) triggerRef.current?.focus({ preventScroll: true });      if (reduced) return setPhase("closed");      setPhase("closing");      timer.current = window.setTimeout(() => setPhase("closed"), DURATION);    },    [reduced],  );  React.useEffect(() => {    if (phase !== "opening") return;    let inner = 0;    const outer = requestAnimationFrame(() => (inner = requestAnimationFrame(() => setPhase("open"))));    return () => {      cancelAnimationFrame(outer);      cancelAnimationFrame(inner);    };  }, [phase]);  React.useEffect(() => () => window.clearTimeout(timer.current), []);  React.useLayoutEffect(() => {    const trigger = triggerRef.current;    if (!trigger) return;    const measure = () => setPill((p) => (p.width === trigger.offsetWidth && p.height === trigger.offsetHeight ? p : { width: trigger.offsetWidth, height: trigger.offsetHeight }));    measure();    const observer = new ResizeObserver(measure);    observer.observe(trigger);    return () => observer.disconnect();  }, []);  React.useLayoutEffect(() => {    const panel = panelRef.current;    if (panel) setPanelHeight(panel.offsetHeight);  }, [mounted, options]);  // The highlight glides to the active row, and the list scrolls to keep it in view.  React.useLayoutEffect(() => {    const row = rowRefs.current[active];    const list = listRef.current;    if (!row || !list) return;    setHighlight((h) => (h.top === row.offsetTop && h.height === row.offsetHeight && h.ready === (h.ready || expanded) ? h : { top: row.offsetTop, height: row.offsetHeight, ready: h.ready || expanded }));    const scroller = list.parentElement;    if (scroller && scroller.scrollHeight > scroller.clientHeight) {      if (row.offsetTop < scroller.scrollTop) scroller.scrollTop = row.offsetTop - 4;      else if (row.offsetTop + row.offsetHeight > scroller.scrollTop + scroller.clientHeight) scroller.scrollTop = row.offsetTop + row.offsetHeight - scroller.clientHeight + 4;    }  }, [active, expanded, mounted]);  React.useEffect(() => {    if (expanded) listRef.current?.focus({ preventScroll: true });  }, [expanded]);  React.useEffect(() => {    if (!expanded) return;    const inside = (n: Node) => Boolean(wrapRef.current?.contains(n) || surfaceRef.current?.contains(n));    const onDown = (e: PointerEvent) => {      if (!inside(e.target as Node)) close(false);    };    // The list sits where the pill was; if the page moves under it, it goes at once rather than fold in the wrong place.    const gone = () => {      window.clearTimeout(timer.current);      setPhase("closed");    };    const onScroll = (e: Event) => {      if (!surfaceRef.current?.contains(e.target as Node)) gone();    };    const onResize = gone;    document.addEventListener("pointerdown", onDown);    window.addEventListener("scroll", onScroll, true);    window.addEventListener("resize", onResize);    return () => {      document.removeEventListener("pointerdown", onDown);      window.removeEventListener("scroll", onScroll, true);      window.removeEventListener("resize", onResize);    };  }, [expanded, close]);  const choose = (o: PillOption) => {    close();    if (o.value !== value) onValueChange(o.value);  };  const onListKey = (e: React.KeyboardEvent) => {    const last = options.length - 1;    const move = (to: number) => {      e.preventDefault();      setActive(Math.max(0, Math.min(last, to)));    };    if (e.key === "ArrowDown") move(active === last ? 0 : active + 1);    else if (e.key === "ArrowUp") move(active === 0 ? last : active - 1);    else if (e.key === "Home") move(0);    else if (e.key === "End") move(last);    else if (e.key === "PageDown") move(active + 6);    else if (e.key === "PageUp") move(active - 6);    else if (e.key === "Enter" || e.key === " ") {      e.preventDefault();      if (options[active]) choose(options[active]);    } else if (e.key === "Tab") close(false);    else if (e.key.length === 1 && /\S/.test(e.key)) {      // A few letters typed quickly find the option that starts with them.      const now = performance.now();      typed.current = { text: (now - typed.current.at < 700 ? typed.current.text : "") + e.key.toLowerCase(), at: now };      const hit = options.findIndex((o) => o.label.toLowerCase().startsWith(typed.current.text));      if (hit !== -1) setActive(hit);    }  };  const w = expanded ? fit.width : pill.width;  const h = expanded ? panelHeight : pill.height;  const listId = `${id}-list`;  const pillText = renderValue ? null : (    <>      <span className={`min-w-0 flex-1 truncate text-left ${mono ? "font-mono text-[11.5px]" : ""} ${muted || !selected ? "text-muted-foreground" : "text-foreground"}`}>        {block ? text : <TextMorph>{text}</TextMorph>}      </span>      <Chevron up={expanded} reduced={reduced} />    </>  );  return (    <div      ref={wrapRef}      className={`relative ${block ? "flex w-full min-w-0" : "inline-flex"} ${className}`}      onKeyDown={(e) => {        setQuiet(false);        if (e.key === "Escape" && expanded) {          e.preventDefault();          e.stopPropagation();          close();        }      }}      onBlur={(e) => {        const to = e.relatedTarget as Node | null;        if (expanded && document.hasFocus() && !(to && (wrapRef.current?.contains(to) || surfaceRef.current?.contains(to)))) close(false);      }}    >      <button        ref={triggerRef}        type="button"        aria-haspopup="listbox"        aria-expanded={expanded}        aria-controls={mounted ? listId : undefined}        aria-label={`${label}: ${selected?.label ?? placeholder}`}        disabled={disabled}        onClick={() => (expanded ? close() : open())}        onKeyDown={(e) => {          if (e.key === "ArrowDown" || e.key === "ArrowUp") {            e.preventDefault();            open();          }        }}        onPointerDown={() => setQuiet(true)}        className={`inline-flex ${PILL_SIZE[size]} min-w-0 cursor-pointer items-center gap-1.5 rounded-full font-medium whitespace-nowrap shadow-[inset_0_0_0_1px_var(--border)] transition-colors duration-150 hover:bg-accent disabled:cursor-default disabled:opacity-50 disabled:hover:bg-transparent ${block ? "w-full" : ""} ${quiet ? "outline-none" : FOCUS}`}      >        {renderValue ? renderValue(selected, expanded) : pillText}      </button>      {mounted &&        createPortal(        <div          ref={surfaceRef}          onKeyDown={(e) => {            if (e.key === "Escape") {              e.preventDefault();              e.stopPropagation();              close();            }          }}          onPointerDown={(e) => {            if (!(e.target as HTMLElement).closest("[data-scroll]")) e.preventDefault();            setQuiet(true);          }}          className="fixed z-[60] overflow-hidden bg-popover text-popover-foreground"          style={{            ...fit.theme,            [fit.up ? "bottom" : "top"]: fit.y,            [align === "start" ? "left" : "right"]: fit.x + (expanded ? (align === "start" ? fit.shift : -fit.shift) : 0),            width: w,            height: h,            borderRadius: expanded ? 16 : pill.height / 2,            // The pill's own hairline, the whole way: it is the same surface.            boxShadow: "inset 0 0 0 1px var(--border)",            transition: reduced ? "none" : `width ${DURATION}ms ${EASE}, height ${DURATION}ms ${EASE}, left ${DURATION}ms ${EASE}, right ${DURATION}ms ${EASE}, border-radius ${DURATION}ms ${EASE}`,          }}        >          {/* The pill's label, fading out as the surface grows and back in as it folds. */}          <span            aria-hidden="true"            className={`pointer-events-none absolute inline-flex ${PILL_SIZE[size]} items-center gap-1.5 font-medium whitespace-nowrap ${fit.up ? "bottom-0" : "top-0"} ${align === "start" ? "left-0" : "right-0"}`}            style={{ width: pill.width, opacity: expanded ? 0 : 1, transition: reduced ? "none" : `opacity ${expanded ? 120 : 260}ms ${EASE} ${expanded ? 0 : 120}ms` }}          >            {renderValue ? renderValue(selected, expanded) : pillText}          </span>          <div            ref={panelRef}            className={`absolute p-1 ${fit.up ? "bottom-0" : "top-0"} ${align === "start" ? "left-0" : "right-0"}`}            style={{ width: fit.width, opacity: expanded ? 1 : 0, transition: reduced ? "none" : `opacity ${expanded ? 260 : 120}ms ${EASE} ${expanded ? 90 : 0}ms` }}          >            <div data-scroll="" className="overflow-y-auto overscroll-contain" style={{ maxHeight: MAX_LIST }}>              <div                ref={listRef}                id={listId}                role="listbox"                tabIndex={-1}                aria-label={label}                aria-activedescendant={`${id}-o${active}`}                onKeyDown={onListKey}                className="relative outline-none"              >                <span                  aria-hidden="true"                  className="pointer-events-none absolute inset-x-0 top-0 rounded-[12px] bg-accent"                  style={{                    height: highlight.height,                    transform: `translateY(${highlight.top}px)`,                    opacity: highlight.ready ? 1 : 0,                    transition: highlight.ready && !reduced ? `transform 260ms ${EASE}, height 260ms ${EASE}, opacity 200ms` : "none",                  }}                />                {options.map((o, i) => {                  const on = o.value === value;                  return (                    <div                      key={o.value}                      ref={(el) => {                        rowRefs.current[i] = el;                      }}                      id={`${id}-o${i}`}                      role="option"                      aria-selected={on}                      onPointerMove={() => setActive(i)}                      onClick={() => choose(o)}                      className="relative flex cursor-pointer items-start gap-2 rounded-[12px] px-2.5 py-1.5"                    >                      <span className="min-w-0 flex-1">                        <span className={`block truncate ${mono ? "font-mono text-[12px]" : "text-[12.5px] font-medium"}`}>{o.label}</span>                        {o.description && <span className="mt-0.5 block truncate text-[11.5px] text-muted-foreground">{o.description}</span>}                      </span>                      <svg viewBox="0 0 16 16" fill="none" aria-hidden="true" className="mt-0.5 size-3.5 shrink-0">                        <path                          d="M3.5 8.5 6.5 11.5 12.5 4.5"                          stroke="currentColor"                          strokeWidth="1.8"                          strokeLinecap="round"                          strokeLinejoin="round"                          pathLength={1}                          strokeDasharray={1}                          style={{ strokeDashoffset: on && expanded ? 0 : 1, transition: reduced ? "none" : `stroke-dashoffset 380ms ${EASE} 160ms` }}                        />                      </svg>                    </div>                  );                })}              </div>            </div>          </div>        </div>,          document.body,        )}    </div>  );}

Registry item: /r/select.json