# Select (Lumen Halo): prompt.md (v1.0.0)

- id: `lumen-select` · version 1.0.0 · component · free
- category: Selection
- build: Base UI (this item also ships a Radix build)
- install (this build): `npx shadcn@latest add https://beautiful-ui.dev/r/lumen-select.json`
- npm dependencies: @base-ui/react@^1
- registry dependencies: utils, https://beautiful-ui.dev/r/lumen-foundation.json
- docs: https://beautiful-ui.dev/components/lumen-select
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

Choose one from a list: the trigger is Lumen's recessed well with the focus filament, and the options sit in smoked glass where a lens glides to the one you're on. shadcn's Select, cut from Lumen glass.

## Build it
- Stack: React 19 (`ref` is a plain prop), TypeScript, Tailwind CSS v4 utilities, a shadcn-initialised project with the `@/*` alias.
- Packages: `@base-ui/react@^1` (Base UI build); `radix-ui@^1` (Radix build).
- Files: `components/ui/lumen/select.tsx`; shared code: `lib/beautiful-ui/lumen/portal.ts`, `lib/beautiful-ui/lumen/glyphs.tsx`, `lib/beautiful-ui/lumen/indicator.ts`, `lib/beautiful-ui/lumen/sound.ts`, `lib/beautiful-ui/lumen/field-label.ts`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `lumen-foundation`.
- Builds: separate Base UI and Radix files. Install the one that matches the project (see Install): a free item's bare URL installs the Base UI build of it and its dependencies.
- Exports to keep: `Select`, `SelectTrigger / SelectValue`, `SelectContent`, `SelectItem / Label / Group / Separator`, and every exported type.
- CSS: the install merges this item's rules (the registry `css` field) into your global stylesheet, in `@layer components`, and adds the lumen foundation (tokens, keyframes, motion levels) once. Nothing to import by hand.
- Re-running `add` (or `--overwrite`) re-applies those rules: put overrides in your own CSS, never in the installed rules.
- Tokens: retheme with the `--lumen-*` custom properties (`--lumen-accent`, `--lumen-accent-text`, `--lumen-bad`, `--lumen-bad-text`, `--lumen-focus`, `--lumen-good`, `--lumen-good-text`, `--lumen-hairline`, `--lumen-ink`, `--lumen-muted-ink`, `--lumen-series-1`, `--lumen-series-2`, `--lumen-series-3`, `--lumen-series-4`, `--lumen-series-5`, `--lumen-series-6`, `--lumen-warn`, `--lumen-warn-text`); this item's CSS also reads `--lumen-font-mono`, `--lumen-font-sans`, `--lumen-radius-k`. Never add Tailwind colour classes inside the component.

```tsx
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/lumen/select";
```

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `Select` | — | Root. |
| `SelectTrigger / SelectValue` | `select-trigger` | The well. |
| `SelectContent` | `select-content` | The glass list. |
| `SelectItem / Label / Group / Separator` | `select-item` | Options. |

Style a part with `[data-slot="<slot>"]` selectors or its `className`; keep the attributes when editing.

## Sound
- Keep every `data-slot` and `data-sound` attribute: the sound layer reads them.
- Installing this item adds no audio. Nothing plays until the app mounts `GlassSoundProvider` once (install: `npx shadcn@latest add https://beautiful-ui.dev/r/glass-sound.json`, import from `@/components/beautiful-ui/glass-sound`); `GlassSoundToggle` is its mute control. Without a provider the audio engine never loads.

## Match the original
- Read `components/ui/lumen/select.tsx` as the reference implementation before changing or recreating anything, and match it: sizes, colours per theme, motion timings, copy and behaviour.
- If you deviate (a prop you can't honour, a style you changed, a dependency you swapped), say so in your reply, part by part.
- Keep the accessibility contract, the keyboard map and the motion levels listed below.

## Use it when
- select, dropdown select, picker, choose option, form select, shadcn select, Lumen
- One value from a known list in a form: a region, a plan, a period

### Not when
- Long lists you type to filter: use Combobox
- Phones where the native picker is best: use Native Select

## Mistakes
- Don't use it for fewer than four options: use Radio Group

## Usage

```tsx
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/lumen/select";

<Select defaultValue="30d">
  <SelectTrigger><SelectValue /></SelectTrigger>
  <SelectContent>
    <SelectItem value="7d">Last 7 days</SelectItem>
    <SelectItem value="30d">Last 30 days</SelectItem>
  </SelectContent>
</Select>
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `SelectTrigger size` | `"sm" \| "default"` | `"default"` | 32px or 40px (44px on phones). |
| `SelectContent alignItemWithTrigger` | `boolean` | `true` | Base UI: open over the trigger with the chosen item aligned. |

Full docs: https://beautiful-ui.dev/components/lumen-select

## Customising
- Colours: set the `--lumen-*` tokens on `:root`, or on a container with the `lumen-scope` class to retheme one area. Add the `lumen-inherit` class to follow your shadcn palette instead (`--chart-N`, `--destructive`).
- Dark mode follows the `.dark` class on an ancestor (the shadcn and next-themes convention).
- Update later by re-running the install with `--overwrite` (review the diff if you edited it). Changelog: https://beautiful-ui.dev/r/changelog.json

## Keyboard

| Keys | Action |
|---|---|
| ↑ ↓ | Move; the lens follows |
| Enter | Choose |
| Escape | Close |

## Motion inventory

| Interaction | What moves |
|---|---|
| Open | The chevron turns; the list grows |
| Highlight | The lens glides |

## Accessibility contract (preserve when editing)
- A listbox with arrow keys and typeahead
- Pair it with a Label

## Install

Base UI project (a base-* style in components.json):

```bash
npx shadcn@latest add https://beautiful-ui.dev/r/lumen-select.json
```

Radix project (a radix-*, new-york or default style):

```bash
npx shadcn@latest add https://beautiful-ui.dev/r/radix-nova/lumen-select.json
```

Or add the `@beautiful-ui` registry to components.json and run `npx shadcn@latest add @beautiful-ui/lumen-select`: the CLI picks the build from your style.

## Credits
- Built on shadcn/ui (https://ui.shadcn.com)

## Source (Base UI build)

### components/ui/lumen/select.tsx

```tsx
"use client";

/**
 * Select (Lumen Halo) v1.0.0 · Beautiful UI
 * Docs: https://beautiful-ui.dev/components/lumen-select · Agent prompt: https://beautiful-ui.dev/md/lumen-select.md
 * Licensed to the purchaser under the Beautiful UI license: https://beautiful-ui.dev/license
 */

import * as React from "react";
import { Select as SelectPrimitive } from "@base-ui/react/select";
import { cn } from "@/lib/utils";
import { useLumenPortal } from "@/lib/beautiful-ui/lumen/portal";
import { LumenCheckGlyph } from "@/lib/beautiful-ui/lumen/glyphs";
import { useActiveIndicator } from "@/lib/beautiful-ui/lumen/indicator";
import { useFieldLabelId } from "@/lib/beautiful-ui/lumen/field-label";
import { useMergedRef } from "@/lib/beautiful-ui/lumen/sound";

/*
 * Lumen Select (Base UI build). shadcn's Select in Lumen Halo: the trigger is Lumen's recessed well
 * (with the focus filament), and the options sit in the menu's smoked glass, where a lens of light
 * glides to the highlighted one and the chosen one lights a check. Same exports, sizes, placement
 * props and data-slots as shadcn.
 */

const Chevron = ({ up = false }: { up?: boolean }) => (
  <svg aria-hidden viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className={`lumen-select-chevron`}>
    <path d={up ? "M4 10l4-4 4 4" : "M4 6l4 4 4-4"} />
  </svg>
);

const Select = SelectPrimitive.Root;

function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
  return <SelectPrimitive.Group data-slot="select-group" className={cn(`lumen-select-group`, className)} {...props} />;
}

function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
  return <SelectPrimitive.Value data-slot="select-value" className={cn(`lumen-select-value`, className)} {...props} />;
}

function SelectTrigger({ className, size = "default", children, ...props }: SelectPrimitive.Trigger.Props & { size?: "sm" | "default" }) {
  // Inside a Field, the FieldLabel names the trigger (a button, so <label> alone doesn't).
  const fieldLabelId = useFieldLabelId();
  return (
    <SelectPrimitive.Trigger data-slot="select-trigger" data-size={size} className={cn(`lumen-select-trigger lumen-control`, className)} aria-labelledby={props["aria-label"] ? undefined : fieldLabelId} {...props}>
      {children}
      <SelectPrimitive.Icon render={<span className={`lumen-select-icon`} />}>
        <Chevron />
      </SelectPrimitive.Icon>
    </SelectPrimitive.Trigger>
  );
}

function SelectContent({
  className,
  children,
  side = "bottom",
  sideOffset = 6,
  align = "center",
  alignOffset = 0,
  alignItemWithTrigger = true,
  ref,
  ...props
}: SelectPrimitive.Popup.Props & Pick<SelectPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger">) {
  const portal = useLumenPortal();
  // A popup's element exists only while open: a callback ref lets the lens attach each time.
  const [own, setOwn] = React.useState<HTMLDivElement | null>(null);
  const merged = useMergedRef(setOwn, ref as React.Ref<HTMLDivElement>);
  useActiveIndicator(own, "[data-highlighted]");
  return (
    <SelectPrimitive.Portal container={portal}>
      <SelectPrimitive.Positioner side={side} sideOffset={sideOffset} align={align} alignOffset={alignOffset} alignItemWithTrigger={alignItemWithTrigger} className={`lumen-positioner`}>
        <SelectPrimitive.Popup ref={merged} data-slot="select-content" data-align-trigger={alignItemWithTrigger} className={cn(`lumen-menu lumen-select-content lumen-control`, className)} {...props}>
          <span aria-hidden className={`lumen-menu-lens`} />
          <SelectScrollUpButton />
          <SelectPrimitive.List>{children}</SelectPrimitive.List>
          <SelectScrollDownButton />
        </SelectPrimitive.Popup>
      </SelectPrimitive.Positioner>
    </SelectPrimitive.Portal>
  );
}

function SelectLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props) {
  return <SelectPrimitive.GroupLabel data-slot="select-label" className={cn(`lumen-menu-label`, className)} {...props} />;
}

function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props) {
  return (
    <SelectPrimitive.Item data-slot="select-item" className={cn(`lumen-menu-item lumen-menu-choice`, className)} {...props}>
      <SelectPrimitive.ItemText className={`lumen-select-item-text`}>{children}</SelectPrimitive.ItemText>
      <SelectPrimitive.ItemIndicator render={<span className={`lumen-menu-indicator`} />}>
        <LumenCheckGlyph />
      </SelectPrimitive.ItemIndicator>
    </SelectPrimitive.Item>
  );
}

function SelectSeparator({ className, ...props }: SelectPrimitive.Separator.Props) {
  return <SelectPrimitive.Separator data-slot="select-separator" className={cn(`lumen-menu-separator`, className)} {...props} />;
}

function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
  return (
    <SelectPrimitive.ScrollUpArrow data-slot="select-scroll-up-button" className={cn(`lumen-select-scroll`, className)} {...props}>
      <Chevron up />
    </SelectPrimitive.ScrollUpArrow>
  );
}

function SelectScrollDownButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
  return (
    <SelectPrimitive.ScrollDownArrow data-slot="select-scroll-down-button" className={cn(`lumen-select-scroll`, className)} {...props}>
      <Chevron />
    </SelectPrimitive.ScrollDownArrow>
  );
}

export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue };
```

### lib/beautiful-ui/lumen/portal.ts

```tsx
"use client";

import * as React from "react";

/*
 * Where Lumen overlays (dialogs, sheets, menus, popovers, select) portal to. Unset, they portal to
 * document.body, as shadcn's do. Provide an element to keep them inside a themed or scoped part of
 * the page: a preview, a shadow root, an embedded widget.
 *
 *   <LumenPortalContext value={element}>…</LumenPortalContext>
 */
export const LumenPortalContext = React.createContext<HTMLElement | null>(null);

/** The container for an overlay's Portal: the provided element, else undefined (document.body). */
export function useLumenPortal(): HTMLElement | undefined {
  return React.useContext(LumenPortalContext) ?? undefined;
}
```

### lib/beautiful-ui/lumen/glyphs.tsx

```tsx
import type * as React from "react";

/*
 * Lumen's glyphs for its shadcn-built controls: the 4-cell loading ticker, the check that draws
 * itself in, the ! glyph, and the cell-drawn icon set. The same drawings as the glass family.
 */

const SVG = { width: "1.1em", height: "1.1em", viewBox: "0 0 16 16", fill: "none", "aria-hidden": true, focusable: false } as const;
const BOX: React.CSSProperties = { display: "block", flex: "none" };
const ST = { stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", fill: "none" } as const;
const cell = (x: number, y: number, s = 3) => <rect key={`${x}-${y}`} x={x} y={y} width={s} height={s} rx={0.9} fill="currentColor" />;

/** The 4-cell loading ticker in currentColor (.32em cells, .16em gaps). */
export function LumenTicker({ className }: { className?: string }) {
  return (
    <span aria-hidden className={className ? `lumen-tick ${className}` : `lumen-tick`}>
      <span />
      <span />
      <span />
      <span />
    </span>
  );
}

/** The check that draws itself in (stroke-dashoffset 16 → 0). Remount it (key) to replay. */
export function LumenCheckGlyph() {
  return (
    <svg {...SVG} className={`lumen-check`} style={BOX}>
      <path d="M3.5 8.4l2.9 2.9 6.1-6.6" {...ST} strokeWidth={1.9} />
    </svg>
  );
}

/** The ! glyph, cropped tight so its gap to a label matches the other glyphs. */
export function LumenErrorGlyph() {
  return (
    <svg {...SVG} width=".36em" viewBox="6.2 0 3.6 16" style={{ ...BOX, margin: "0 1px 0 -1px" }}>
      <rect x={6.9} y={2.6} width={2.2} height={7} rx={1.1} fill="currentColor" />
      <rect x={6.9} y={11.2} width={2.2} height={2.2} rx={0.8} fill="currentColor" />
    </svg>
  );
}

export const LUMEN_ICON_NAMES = ["plus", "grid", "more", "search", "filter", "copy", "arrow", "trash", "close", "check"] as const;
export type LumenIconName = (typeof LUMEN_ICON_NAMES)[number];
export const isLumenIconName = (v: unknown): v is LumenIconName => typeof v === "string" && (LUMEN_ICON_NAMES as readonly string[]).includes(v);

/** One icon from the Lumen set, 1.1em square in currentColor: plus, grid and more are cells; the rest are 1.6px strokes. */
export function LumenIcon({ name }: { name: LumenIconName }) {
  const p = { ...SVG, style: BOX };
  switch (name) {
    case "plus":
      return <svg {...p}>{[cell(6.5, 2.5), cell(2.5, 6.5), cell(6.5, 6.5), cell(10.5, 6.5), cell(6.5, 10.5)]}</svg>;
    case "more":
      return <svg {...p}>{[cell(2, 6.5), cell(6.5, 6.5), cell(11, 6.5)]}</svg>;
    case "grid":
      return <svg {...p}>{[cell(3, 3, 4), cell(9, 3, 4), cell(3, 9, 4), cell(9, 9, 4)]}</svg>;
    case "search":
      return (
        <svg {...p}>
          <circle cx={7} cy={7} r={4.25} {...ST} />
          <path d="M10.3 10.3L13.5 13.5" {...ST} />
        </svg>
      );
    case "copy":
      return (
        <svg {...p}>
          <rect x={5.5} y={5.5} width={8} height={8} rx={2} {...ST} />
          <path d="M10.5 5.5V4a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 4v5A1.5 1.5 0 0 0 4 10.5h1.5" {...ST} />
        </svg>
      );
    case "trash":
      return (
        <svg {...p}>
          <path d="M2.8 4.5h10.4M6.4 4.5V3h3.2v1.5M4.3 4.5l.6 8.5h6.2l.6-8.5" {...ST} />
        </svg>
      );
    case "arrow":
      return (
        <svg {...p}>
          <path d="M3 8h10M9 4l4 4-4 4" {...ST} />
        </svg>
      );
    case "close":
      return (
        <svg {...p}>
          <path d="M4.5 4.5l7 7M11.5 4.5l-7 7" {...ST} />
        </svg>
      );
    case "filter":
      return (
        <svg {...p}>
          <path d="M2.5 4.5h11M4.5 8h7M6.5 11.5h3" {...ST} />
        </svg>
      );
    case "check":
      return <LumenCheckGlyph />;
  }
}
```

### lib/beautiful-ui/lumen/indicator.ts

```tsx
"use client";

import * as React from "react";

/*
 * Where the active item sits inside a list, as CSS variables on the list: --ind-x, --ind-y, --ind-w,
 * --ind-h (px, relative to the list's padding box). Lumen's sliding thumbs, lenses and filaments read
 * them, so the same markup animates on Base UI and Radix alike. `data-ind-still` is set for the first
 * measurement and on resize, so the indicator appears in place instead of sliding in from 0.
 */

const ACTIVE = '[aria-selected="true"],[data-active],[data-state="active"],[aria-pressed="true"],[data-pressed],[data-state="on"]';

// Measure before paint on the client (no frame without the thumb); a plain effect on the server.
const useIsoLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;

// The item's box within the list. Offsets ignore transforms (a menu opening on a scale), but they are
// relative to the nearest positioned ancestor (a pagination or navigation <li>), so walk up to the list.
function offsetWithin(el: HTMLElement, list: HTMLElement) {
  let x = 0;
  let y = 0;
  for (let n: HTMLElement | null = el; n && n !== list; ) {
    x += n.offsetLeft;
    y += n.offsetTop;
    const parent = n.offsetParent as HTMLElement | null;
    if (!parent || parent === list || !list.contains(parent)) break;
    x += parent.clientLeft;
    y += parent.clientTop;
    n = parent;
  }
  return { x, y };
}

/**
 * `target` is a ref for lists that are always mounted (tabs, toggle groups), or the element itself
 * for popups (menus, select, combobox): a popup's element only exists while it is open, so pass it
 * through a callback ref (useState) and the indicator attaches each time it opens.
 */
export function useActiveIndicator(target: React.RefObject<HTMLElement | null> | HTMLElement | null, active = ACTIVE, enabled = true) {
  useIsoLayoutEffect(() => {
    const list = target && "current" in target ? target.current : target;
    if (!list || !enabled) return;
    let frame = 0;
    let measured = false;
    const place = (still: boolean) => {
      frame = 0;
      const el = list.querySelector<HTMLElement>(active);
      if (!el) {
        list.removeAttribute("data-ind-ready");
        return;
      }
      if (still || !measured) list.setAttribute("data-ind-still", "");
      const { x, y } = offsetWithin(el, list);
      list.style.setProperty("--ind-x", `${x}px`);
      list.style.setProperty("--ind-y", `${y}px`);
      list.style.setProperty("--ind-w", `${el.offsetWidth}px`);
      list.style.setProperty("--ind-h", `${el.offsetHeight}px`);
      list.setAttribute("data-ind-ready", "");
      measured = true;
      // Let the still position paint, then allow the next change to glide.
      requestAnimationFrame(() => requestAnimationFrame(() => list.removeAttribute("data-ind-still")));
    };
    const schedule = (still: boolean) => {
      if (frame) cancelAnimationFrame(frame);
      frame = requestAnimationFrame(() => place(still));
    };
    place(true);
    const ro = new ResizeObserver(() => schedule(true));
    const watchChildren = () => {
      ro.observe(list);
      for (const child of Array.from(list.children)) ro.observe(child);
    };
    watchChildren();
    // Selection changes glide; tabs added, removed or reordered, or a direction flip, re-place it.
    const mo = new MutationObserver((records) => {
      const structural = records.some((r) => r.type === "childList" || r.attributeName === "dir");
      if (structural) watchChildren();
      schedule(structural);
    });
    mo.observe(list, { subtree: true, childList: true, attributes: true, attributeFilter: ["aria-selected", "aria-checked", "aria-current", "data-active", "data-state", "aria-pressed", "data-pressed", "data-highlighted", "data-popup-open", "aria-expanded", "dir"] });
    const html = document.documentElement;
    const dirMo = new MutationObserver(() => schedule(true));
    // A direction flip anywhere above (a wrapper, the page) moves equal-sized items without resizing them.
    dirMo.observe(html, { attributes: true, subtree: true, attributeFilter: ["dir"] });
    return () => {
      mo.disconnect();
      dirMo.disconnect();
      ro.disconnect();
      if (frame) cancelAnimationFrame(frame);
    };
  }, [target, active, enabled]);
}
```

### lib/beautiful-ui/lumen/sound.ts

```tsx
"use client";

import * as React from "react";

/**
 * Asks the page's sound layer (beautiful-ui-sound) to play `cue`. Silent when nothing listens, and inside
 * anything marked data-sound="off". `index` pitches select cues by position. No audio code ships in
 * the components themselves.
 */
export function emitSound(el: Element | null, cue: string, force?: boolean, index?: number) {
  if (!el || typeof CustomEvent === "undefined" || el.closest('[data-sound="off"]')) return;
  el.dispatchEvent(new CustomEvent("beautiful-ui:sound", { bubbles: true, detail: { cue, force, index } }));
}

/**
 * Plays the cue `pick` returns whenever `value` changes (never on mount): an error when a field
 * turns invalid, a success when a check passes.
 */
export function useCueOnChange<T>(ref: React.RefObject<Element | null>, value: T, pick: (next: T, prev: T) => string | null) {
  const prev = React.useRef(value);
  const pickRef = React.useRef(pick);
  React.useEffect(() => {
    pickRef.current = pick;
  });
  React.useEffect(() => {
    if (Object.is(prev.current, value)) return;
    const before = prev.current;
    prev.current = value;
    const cue = pickRef.current(value, before);
    if (cue) emitSound(ref.current, cue);
  }, [ref, value]);
}

/** Points one ref (callback or object) at `node`; returns how to let go of it. */
function assign<T>(r: React.Ref<T> | undefined, node: T | null): () => void {
  if (typeof r === "function") {
    const cleanup = r(node);
    return typeof cleanup === "function" ? cleanup : () => r(null);
  }
  if (r) {
    (r as React.RefObject<T | null>).current = node;
    return () => ((r as React.RefObject<T | null>).current = null);
  }
  return () => {};
}

/**
 * One stable callback ref for the component's own ref and the caller's. It changes only when one of
 * them does, so a caller's callback ref isn't detached and reattached on every render, and it
 * honours React 19 ref cleanups.
 */
export function useMergedRef<T>(own: React.Ref<T> | undefined, theirs: React.Ref<T> | undefined): React.RefCallback<T> {
  return React.useCallback(
    (node: T | null) => {
      const release = [assign(own, node), assign(theirs, node)];
      return () => release.forEach((f) => f());
    },
    [own, theirs],
  );
}

/** True when `el` sits in an input group whose status part speaks for it (one cue per change). */
export const statusSpeaksFor = (el: Element | null) => Boolean(el?.closest('[data-slot="input-group"]')?.querySelector('[data-slot="input-group-status"]'));
```

### lib/beautiful-ui/lumen/field-label.ts

```tsx
"use client";

import * as React from "react";

/*
 * The id a Lumen Field gives its label. A control that is a button rather than an input (Select's
 * trigger, a combobox) isn't named by a <label>, so it points at this id with aria-labelledby.
 */
export const FieldLabelContext = React.createContext<string | undefined>(undefined);

/** The id of the enclosing Field's label, or undefined outside a Field. */
export function useFieldLabelId() {
  return React.useContext(FieldLabelContext);
}
```

### CSS (the registry `css` / `cssVars`, merged into the global stylesheet by the shadcn CLI)

```css
:root {
  --lo-scrim: rgba(20,24,40,.18);
  --lo-panel: linear-gradient(180deg,rgba(255,255,255,.97),rgba(247,248,251,.96));
  --lo-panelSh: 0 0 0 1px rgba(20,24,40,.08),0 1px 2px rgba(20,24,40,.06),0 24px 60px -20px rgba(20,24,40,.35);
  --lo-rim: inset 0 1px 0 #fff;
}
.dark, [data-theme="dark"] {
  --lo-scrim: rgba(0,0,0,.5);
  --lo-panel: linear-gradient(180deg,rgba(38,40,46,.96),rgba(18,19,23,.97));
  --lo-panelSh: 0 0 0 1px rgba(0,0,0,.8),0 30px 80px -24px rgba(0,0,0,.95);
  --lo-rim: inset 0 0 0 1px rgba(255,255,255,.08),inset 0 1px 0 rgba(255,255,255,.12);
}
@layer components {
  @keyframes lumen-grow {
    from {
      opacity: 0;
      transform: scale(.96) translateY(var(--lf-y,-4px));
    }
  }
  @keyframes lumen-shrink {
    to {
      opacity: 0;
      transform: scale(.98);
    }
  }
  .lumen-positioner {
    isolation: isolate;
    z-index: 50;
  }
  .lumen-menu {
    z-index: 50;
    box-sizing: border-box;
    border-radius: calc(14px * var(--lumen-radius-k, 1));
    background: var(--lo-panel);
    box-shadow: var(--lo-rim),var(--lo-panelSh);
    color: var(--gc-ink);
    font: 400 13.5px/1.5 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    outline: none;
    transform-origin: var(--transform-origin,var(--radix-popover-content-transform-origin,var(--radix-hover-card-content-transform-origin,var(--radix-dropdown-menu-content-transform-origin,var(--radix-context-menu-content-transform-origin,var(--radix-menubar-content-transform-origin,var(--radix-select-content-transform-origin,center)))))));
    position: relative;
    min-width: 180px;
    max-height: var(--available-height,var(--radix-dropdown-menu-content-available-height,70vh));
    overflow-x: hidden;
    overflow-y: auto;
    padding: 4px;
    font-size: 13.5px;
  }
  .lumen-menu[data-side="top"] {
    --lf-y: 4px;
  }
  .lumen-menu:is([data-side="left"],[data-side="right"],[data-side="inline-start"],[data-side="inline-end"]) {
    --lf-y: 0px;
  }
  .lumen-menu:is([data-open],[data-state="open"]) {
    animation: lumen-grow calc(.35s * var(--gcp-k)) var(--gc-sp) both,lumen-fade-in var(--gc-t-fade) ease both;
  }
  .lumen-menu:is([data-closed],[data-state="closed"]) {
    animation: lumen-shrink calc(.14s * var(--gcp-k)) ease both,lumen-fade-out var(--gc-t-fade) ease both;
  }
  @keyframes lumen-fade-in {
    from {
      opacity: 0;
    }
  }
  @keyframes lumen-fade-out {
    to {
      opacity: 0;
    }
  }
  .lumen-menu-lens {
    position: absolute;
    left: 0;
    top: 0;
    width: var(--ind-w,0);
    height: var(--ind-h,0);
    transform: translate(var(--ind-x,0),var(--ind-y,0));
    border-radius: calc(9px * var(--lumen-radius-k, 1));
    background: color-mix(in srgb,var(--gc-ink) 8%,transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb,var(--gc-ink) 6%,transparent);
    opacity: 0;
    pointer-events: none;
    transition: transform calc(0.35s * var(--gcp-k)) cubic-bezier(.22,1,.36,1),height calc(0.35s * var(--gcp-k)) cubic-bezier(.22,1,.36,1),width calc(0.35s * var(--gcp-k)) cubic-bezier(.22,1,.36,1),opacity min(0.15s, var(--gcp-cm)) ease;
  }
  :is(.lumen-menu,.lumen-select-viewport)[data-ind-ready]>.lumen-menu-lens {
    opacity: 1;
  }
  :is(.lumen-menu,.lumen-select-viewport)[data-ind-still]>.lumen-menu-lens {
    transition: opacity min(0.15s, var(--gcp-cm)) ease;
  }
  .lumen-menu-item {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 32px;
    box-sizing: border-box;
    padding: 6px 10px;
    border-radius: calc(9px * var(--lumen-radius-k, 1));
    color: var(--gc-ink);
    cursor: default;
    outline: none;
    user-select: none;
  }
  .lumen-menu-item[data-inset] {
    padding-inline-start: 30px;
  }
  .lumen-menu-item>svg {
    width: 15px;
    height: 15px;
    flex: none;
    color: var(--gc-sec);
    pointer-events: none;
  }
  .lumen-menu-item[data-variant="destructive"],.lumen-menu-item[data-variant="destructive"]>svg {
    color: var(--gc-bad-ui);
  }
  .lumen-menu-item:is([data-disabled],[aria-disabled="true"]) {
    opacity: .45;
    pointer-events: none;
  }
  .lumen-menu-item:is([data-popup-open],[data-state="open"]) {
    background: color-mix(in srgb,var(--gc-ink) 6%,transparent);
  }
  .lumen-menu-choice {
    padding-inline-end: 32px;
  }
  .lumen-menu-indicator {
    position: absolute;
    inset-inline-end: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 14px;
    height: 14px;
    color: var(--gc-acc-ui);
    pointer-events: none;
  }
  .lumen-menu-indicator svg {
    width: 14px;
    height: 14px;
  }
  .lumen-menu-dot {
    display: block;
    width: 7px;
    height: 7px;
    border-radius: calc(2px * var(--lumen-radius-k, 1));
    background: var(--gc-acc-ui);
    box-shadow: 0 0 8px color-mix(in srgb,var(--gc-acc-ui) 60%,transparent);
  }
  .lumen-menu-label {
    padding: 6px 10px 4px;
    font: 500 10.5px/1.3 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--gc-sec);
  }
  .lumen-menu-label[data-inset] {
    padding-inline-start: 30px;
  }
  .lumen-menu-separator {
    height: 1px;
    margin: 4px -4px;
    background: var(--gc-hair);
  }
  .lumen-menu-shortcut {
    margin-inline-start: auto;
    padding-inline-start: 16px;
    font: 500 11px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    letter-spacing: .06em;
    color: var(--gc-sec);
  }
  .lumen-menu-chevron {
    width: 14px;
    height: 14px;
    margin-inline-start: auto;
    color: var(--gc-sec);
  }
  [dir="rtl"] .lumen-menu-chevron {
    transform: scaleX(-1);
  }
  .lumen-select-trigger {
    box-sizing: border-box;
    width: fit-content;
    min-width: 140px;
    margin: 0;
    border: none;
    outline: none;
    --lf-ring: var(--gc-wr);
    --lf-halo: 0 0 0 0 transparent;
    background-color: var(--lf-bg,var(--gc-well));
    box-shadow: inset 0 0 0 1px var(--lf-ring),var(--gc-wellIn),var(--lf-halo);
    background-image: linear-gradient(90deg,transparent,var(--lf-fil,var(--gc-acc)),transparent);
    background-repeat: no-repeat;
    background-position: 50% 100%;
    background-size: 0% 1px;
    font-family: var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    font-weight: 400;
    font-size: var(--lf-fs,14px);
    letter-spacing: normal;
    color: var(--gc-ink);
    caret-color: var(--gc-acc);
    -webkit-appearance: none;
    appearance: none;
    transition: box-shadow var(--gc-t-c) ease,background-size var(--gc-t-fil) cubic-bezier(.22,1,.36,1),background-color var(--gc-t-c) ease;
    display: inline-flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    height: 40px;
    padding: 0 10px 0 12px;
    border-radius: calc(12px * var(--lumen-radius-k, 1));
    text-align: start;
    white-space: nowrap;
    cursor: default;
    user-select: none;
  }
  .lumen-select-trigger::placeholder {
    color: var(--gc-ph);
    opacity: 1;
  }
  @media (hover:hover) {
    .lumen-select-trigger:hover:not(:disabled) {
      --lf-ring: var(--gc-wrH);
    }
  }
  .lumen-select-trigger:focus,.lumen-select-trigger[data-force="focus"] {
    --lf-ring: color-mix(in srgb, var(--gc-acc) 65%, transparent);
    --lf-halo: 0 0 0 4px color-mix(in srgb, var(--gc-acc) 12%, transparent);
    background-size: calc(100% - 28px) 1px;
  }
  .lumen-select-trigger[aria-invalid="true"] {
    --lf-ring: color-mix(in srgb, var(--gc-bad) 60%, transparent);
    --lf-halo: 0 0 0 4px color-mix(in srgb, var(--gc-bad) 8%, transparent);
    --lf-fil: var(--gc-bad);
    background-size: calc(100% - 28px) 1px;
    animation: lumen-shake-a calc(.38s * var(--gcp-k, 1)) cubic-bezier(.22,1,.36,1);
  }
  .lumen-select-trigger:disabled {
    --lf-ring: var(--gc-wr);
    --lf-bg: var(--gc-wellD);
    opacity: .5;
    cursor: not-allowed;
    -webkit-text-fill-color: var(--gc-ink);
  }
  .lumen-select-trigger:-webkit-autofill {
    -webkit-text-fill-color: var(--gc-ink);
    transition: background-color 100000s 0s;
  }
  .lumen-select-trigger[data-size="sm"] {
    height: 32px;
    min-width: 0;
    padding: 0 8px 0 10px;
    border-radius: calc(10px * var(--lumen-radius-k, 1));
    font-size: 13px;
  }
  .lumen-select-trigger:is([data-popup-open],[data-state="open"]) {
    --lf-ring: color-mix(in srgb, var(--gc-acc) 65%, transparent);
    --lf-halo: 0 0 0 4px color-mix(in srgb, var(--gc-acc) 12%, transparent);
    background-size: calc(100% - 28px) 1px;
  }
  .lumen-select-trigger:is([data-placeholder],[data-placeholder=""]) .lumen-select-value,.lumen-select-trigger[data-placeholder]>span:first-child {
    color: var(--gc-ph);
  }
  .lumen-select-value {
    display: flex;
    flex: 1;
    align-items: center;
    gap: 6px;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .lumen-select-icon {
    display: inline-flex;
    color: var(--gc-sec);
  }
  .lumen-select-chevron {
    width: 15px;
    height: 15px;
    transition: transform calc(0.35s * var(--gcp-k)) cubic-bezier(.22,1,.36,1);
  }
  .lumen-select-trigger:is([data-popup-open],[data-state="open"]) .lumen-select-chevron {
    transform: rotate(180deg);
  }
  .lumen-select-content {
    min-width: max(var(--anchor-width,var(--radix-select-trigger-width,0px)),160px);
  }
  .lumen-select-content[data-align-trigger="true"] {
    animation: none;
  }
  .lumen-select-viewport {
    position: relative;
  }
  .lumen-select-item-text {
    display: flex;
    flex: 1;
    gap: 8px;
    white-space: nowrap;
  }
  .lumen-select-group {
    padding: 2px 0;
  }
  .lumen-select-scroll {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 24px;
    color: var(--gc-sec);
    cursor: default;
  }
  @media (max-width: 699px) {
    .lumen-menu-item {
      min-height: 40px;
    }
    .lumen-select-trigger {
      height: 44px;
      --lf-fs: 16px;
    }
  }
}
```

The lumen foundation (the tokens listed above, keyframes and motion levels) installs once with the first component; its CSS is public at https://beautiful-ui.dev/r/lumen-foundation.json.
