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

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

A button that finishes what it starts. Loading holds its width, success draws its own check, and a held delete fills twelve cells before anything is lost. shadcn's Button, 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`, `class-variance-authority@^0.7` (Base UI build); `class-variance-authority@^0.7`, `radix-ui@^1` (Radix build).
- Files: `components/ui/lumen/button.tsx`, `components/ui/lumen/button-client.tsx`; shared code: `lib/beautiful-ui/lumen/button.tsx`, `lib/beautiful-ui/lumen/glyphs.tsx`, `lib/beautiful-ui/lumen/sound.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: `Button`, `buttonVariants`, 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-sans`, `--lumen-radius-k`. Never add Tailwind colour classes inside the component.

```tsx
import { Button, buttonVariants } from "@/components/ui/lumen/button";
```

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `Button` | — | shadcn's Button: variants, sizes, the async states and hold to confirm. |
| `buttonVariants` | — | The classes alone, for links and other elements. |

## 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; `sound={false}` silences one instance.

## Match the original
- Read `components/ui/lumen/button.tsx`, `components/ui/lumen/button-client.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
- button, call to action, submit, save, delete, destructive action, hold to confirm, loading button, async action, link button, shadcn button, Lumen, glass
- Every action in a Lumen Halo interface: forms, dialogs, toolbars, hero calls to action
- A drop-in for shadcn's Button: same exports (Button, buttonVariants), variants, sizes and data-slot
- Async actions: state="loading" keeps the width and shows the 4-cell ticker, then "success" or "error"
- Destructive actions that must not happen by accident: holdToConfirm fills 12 cells over 1.2s and calls onConfirm

### Not when
- Analogue interfaces: use Analogue Button (analogue-button)
- Navigation inside prose: use variant="link", or buttonVariants on your router's Link

## Mistakes
- For the exact Lumen look, add the theme once: npx shadcn@latest add https://beautiful-ui.dev/r/lumen.json. Without it the button keeps Lumen's glass but takes your accent from --ring
- Pass type="submit" for form submit buttons: the default is "button"
- state="error" / "success" don't change the text: pass the new label yourself
- With holdToConfirm, onClick is never called: put the action in onConfirm
- Import from @/components/ui/lumen/button: it never replaces your own components/ui/button.tsx

## Usage

```tsx
import { Button, buttonVariants } from "@/components/ui/lumen/button";

<Button icon="plus" onClick={createWorkspace}>Create workspace</Button>
<Button variant="secondary" icon="copy">Duplicate</Button>
<Button type="submit" state={pending ? "loading" : "idle"}>Save changes</Button>

// Destructive, held to confirm:
<Button variant="destructive" icon="trash" holdToConfirm onConfirm={deleteWorkspace}>Hold to delete</Button>

// Hero call to action, as a Next.js link:
<Link href="/signup" className={buttonVariants({ size: "lg" })}>Start free</Link>
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `variant` | `"default" \| "secondary" \| "ghost" \| "destructive" \| "outline" \| "link"` | `"default"` | Default: white in dark, ink in light. Secondary: glass. Ghost: text until hovered. Destructive: coral glass. Outline: a glass ring without fill. Link: accent text, underlined on hover. |
| `size` | `"default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg"` | `"default"` | shadcn's sizes: 40 / 28 / 32 / 48px tall; icon sizes are round. 44px touch targets on coarse pointers without changing the look. |
| `state` | `"idle" \| "loading" \| "error" \| "success"` | `"idle"` | loading: aria-busy, clicks ignored, label kept for the width. error / success: coral or green skin led by ! or a check that draws in. Change the label yourself. |
| `icon` | `"plus" \| "grid" \| "more" \| "search" \| "filter" \| "copy" \| "arrow" \| "trash" \| "close" \| "check" \| ReactNode` |  | A Lumen icon or your own 1.1em icon before the label; replaced by ! and ✓ in error and success. |
| `trailingChip` | `boolean` |  | The round → chip at the end (hero calls to action); it nudges right on hover. |
| `fullWidth / wrap` | `boolean` |  | Fill the container (with trailingChip the label sits left, the chip right); wrap long labels instead of truncating (truncated labels get a title). |
| `disabled` | `boolean` |  | Stays focusable by default (aria-disabled) so a tooltip can explain why; never clicks or submits. |
| `focusableWhenDisabled` | `boolean` | `true` | false gives shadcn's native disabled button. |
| `holdToConfirm / onConfirm / confirmResetMs` | `boolean \| number / () => void / number` | `— / — / 2400` | Hold with pointer, Space or Enter; early release drains at 2.6×; Escape, blur, leave and cancel stop it. onClick is not called. |
| `labels` | `Partial<{ loading, holdHint, holding, confirmed }>` |  | The screen-reader loading text, the hold description, "Keep holding", "Deleted". |
| `motion` | `"full" \| "subtle" \| "off"` | `"full"` | subtle: 30% shorter on the calm curve. off: instant. Reduced motion: no sheen or bounce, 150ms fades. |
| `sound` | `boolean \| "subtle"` | `true` | Lumen cues through beautiful-ui-sound: a tap, destructive on the danger variant, quarter ticks and a success while holding, success and error from state. No audio code inside. |
| `asChild (Radix) / render (Base UI)` | `boolean / ReactElement` |  | Render as your own element, like shadcn's; the async states and hold need the full button. |
| `className / style / ref / …` | `as shadcn` |  | Everything shadcn's Button takes passes through; className merges last with cn. |
| `buttonVariants({ variant, size })` | `cva` |  | The classes alone, for links and other elements (server-safe). |

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

## Customising
- Motion: `motion="full"` (default) | `"subtle"` | `"off"`. prefers-reduced-motion is always respected.
- 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 |
|---|---|
| Tab | Focus the button (also when disabled, for its tooltip) |
| Enter / Space | Activate |
| Space or Enter, held | Fill a hold-to-confirm button; release early to cancel |
| Escape | Cancel a hold in progress |

## Motion inventory

| Interaction | What moves |
|---|---|
| Hover and press | Lift 1px with a deeper shadow; press sinks .5px and scales to .98 (transform .45s cubic-bezier(.34,1.5,.64,1); colour and shadow .3s) |
| Primary sheen | On hover a skewed light band sweeps across once (.9s) |
| Loading | The label fades (.2s) but keeps its space; four cells tick up in turn |
| Success | The check draws itself in over .5s |
| Hold to confirm | 12 cells fill over 1.2s; released early they drain at 2.6× |

## Accessibility contract (preserve when editing)
- A native <button> through Base UI or Radix, as shadcn builds it; type defaults to "button"
- disabled stays focusable by default (aria-disabled): it can show a tooltip, but never clicks or submits
- Loading sets aria-busy and reads "Loading" after the label; the ticker is hidden from assistive tech
- Hold-to-confirm is described ("Press and hold to confirm") and works with Space or Enter held
- Focus ring: a 2px gap then 2px of accent, over the button's own shadow; 44px touch targets on coarse pointers
- Every label colour reaches 4.5:1 in both themes, including the light error and success fills

## Install

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

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

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

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

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

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

## Source (Base UI build)

### components/ui/lumen/button.tsx

```tsx
/**
 * Button (Lumen Halo) v1.0.0 · Beautiful UI
 * Docs: https://beautiful-ui.dev/components/lumen-button · Agent prompt: https://beautiful-ui.dev/md/lumen-button.md
 * Licensed to the purchaser under the Beautiful UI license: https://beautiful-ui.dev/license
 */
import type * as React from "react";
import type { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { ButtonClient } from "@/components/ui/lumen/button-client";
import type { ButtonExtras } from "@/lib/beautiful-ui/lumen/button";

/*
 * Lumen Button (Base UI build). shadcn's Button in Lumen Halo: liquid glass in dark and
 * light, a sheen on the primary, a loading ticker that keeps the width, error and success that
 * lead with ! or a check drawing itself in, a round arrow chip for calls to action, and
 * hold-to-confirm for destructive actions. Same exports, variants, sizes and data-slot as shadcn;
 * everything Lumen adds is an optional prop. Server-safe like shadcn's: call buttonVariants()
 * anywhere; the interactive part lives in button-client.tsx.
 */

// Class names are written out in full: Tailwind only generates what it can read in the source.
const buttonVariants = cva(`lumen-button lumen-control`, {
  variants: {
    variant: {
      default: `lumen-button-default`,
      secondary: `lumen-button-secondary`,
      ghost: `lumen-button-ghost`,
      destructive: `lumen-button-destructive`,
      outline: `lumen-button-outline`,
      link: `lumen-button-link`,
    },
    size: {
      default: "",
      xs: `lumen-button-xs`,
      sm: `lumen-button-sm`,
      lg: `lumen-button-lg`,
      icon: `lumen-button-icon`,
      "icon-xs": `lumen-button-icon lumen-button-xs`,
      "icon-sm": `lumen-button-icon lumen-button-sm`,
      "icon-lg": `lumen-button-icon lumen-button-lg`,
    },
  },
  defaultVariants: { variant: "default", size: "default" },
});

function Button({
  className,
  variant = "default",
  size = "default",
  ...props
}: Omit<ButtonPrimitive.Props, "onClick"> &
  VariantProps<typeof buttonVariants> &
  ButtonExtras & {
    onClick?: ((event: React.MouseEvent<HTMLElement>) => void) | undefined;
  }) {
  return <ButtonClient {...props} variant={variant ?? "default"} size={size ?? "default"} className={cn(buttonVariants({ variant, size, className }))} />;
}

export { Button, buttonVariants };
```

### components/ui/lumen/button-client.tsx

```tsx
"use client";

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

import * as React from "react";
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { ButtonContent, splitExtras, useButtonBehaviour, type ButtonExtras, type ButtonSize, type ButtonVariantName } from "@/lib/beautiful-ui/lumen/button";

/*
 * Lumen Button, the interactive part (Base UI build): Base UI's Button with the Lumen behaviour.
 * Disabled stays focusable by default (so a tooltip can explain why), as Lumen designed it.
 */

function ButtonClient({
  className,
  variant = "default",
  size = "default",
  disabled,
  children,
  onClick,
  ...props
}: Omit<ButtonPrimitive.Props, "onClick"> & { variant?: ButtonVariantName | undefined; size?: ButtonSize | undefined } & ButtonExtras & { onClick?: ((event: React.MouseEvent<HTMLElement>) => void) | undefined }) {
  // <Button render={<Link href="/x">Docs</Link>} />: Base UI prefers the render element's children and
  // would drop the ticker, hold cells and hint, so they move into the button's content.
  const renderEl = (props as { render?: unknown }).render;
  if (React.isValidElement<{ children?: React.ReactNode }>(renderEl) && renderEl.props.children != null) {
    children = children ?? renderEl.props.children;
    // Rebuilt without a children key: an explicit children: undefined would still win Base UI's merge.
    const { children: _own, ...renderProps } = renderEl.props;
    (props as { render?: unknown }).render = React.createElement(renderEl.type, { ...renderProps, key: renderEl.key });
  }
  const [extras, rest] = splitExtras(props);
  const { focusableWhenDisabled = true } = extras;
  const own = rest as React.HTMLAttributes<HTMLElement> & { "data-sound"?: unknown; ref?: React.Ref<HTMLElement> | undefined };
  const { ref, attrs, events, state, interactive, hold, phase, labels, hintId, labelRef, label } = useButtonBehaviour({
    ...extras,
    variant,
    size,
    disabled: Boolean(disabled),
    onClick,
    handlers: own,
    ariaLabel: own["aria-label"],
    describedBy: own["aria-describedby"],
    theirSound: own["data-sound"],
    ref: own.ref,
  });
  const { ref: _ref, ...primitiveProps } = rest as ButtonPrimitive.Props & { ref?: React.Ref<HTMLElement> | undefined };
  return (
    <ButtonPrimitive
      {...primitiveProps}
      {...attrs}
      {...events}
      ref={ref}
      disabled={disabled}
      focusableWhenDisabled={focusableWhenDisabled}
      data-slot={(own as Record<string, unknown>)["data-slot"] ?? "button"}
      data-variant={variant}
      data-size={size}
      className={className}
    >
      <ButtonContent
        variant={variant}
        state={state}
        icon={extras.icon}
        trailingChip={extras.trailingChip}
        interactive={interactive}
        hold={hold}
        holdDone={phase === "done"}
        labels={labels}
        hintId={hintId}
        labelRef={labelRef}
      >
        {label(children)}
      </ButtonContent>
    </ButtonPrimitive>
  );
}

export { ButtonClient };
```

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

```tsx
"use client";

import * as React from "react";
import { LumenCheckGlyph, LumenErrorGlyph, LumenIcon, LumenTicker, isLumenIconName, type LumenIconName } from "@/lib/beautiful-ui/lumen/glyphs";
import { emitSound } from "@/lib/beautiful-ui/lumen/sound";

/*
 * Lumen Button behaviour, shared by the Base UI and Radix builds: the async states (loading keeps
 * the width, then error or success), hold-to-confirm (12 cells fill over 1.2s, drain when released
 * early), the primary's sheen, the trailing chip, the truncation title, and sound through
 * beautiful-ui-sound (attributes and beautiful-ui:sound events; no audio code here).
 */

export type ButtonVariantName = "default" | "secondary" | "ghost" | "destructive" | "outline" | "link";
export type ButtonSize = "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg";
export type ButtonState = "idle" | "loading" | "error" | "success";
export type ButtonMotion = "full" | "subtle" | "off";

/** Every string the button shows or announces. */
export interface ButtonLabels {
  /** Read after the label while loading (the ticker itself is hidden from assistive tech). */
  loading: string;
  /** The description of a hold-to-confirm button. */
  holdHint: string;
  /** Shown while a hold-to-confirm button is being held. */
  holding: string;
  /** Shown after a hold completes, until the button resets. */
  confirmed: string;
}

export const defaultButtonLabels: ButtonLabels = { loading: "Loading", holdHint: "Press and hold to confirm", holding: "Keep holding", confirmed: "Deleted" };

/** The designed hold: 1200ms to fill, draining at 2.6× speed, reset 2400ms after confirming. */
export const HOLD_MS = 1200;
export const HOLD_DRAIN_RATIO = 0.45 / 1.2;
export const HOLD_RESET_MS = 2400;

/** One frame of the hold meter (pure, for tests): toward 1 while held, back to 0 at 2.6× when released. */
export function stepHold(p: number, dir: 1 | -1, dtMs: number, holdMs = HOLD_MS): number {
  const span = Math.max(1, dir > 0 ? holdMs : holdMs * HOLD_DRAIN_RATIO);
  return Math.max(0, Math.min(1, p + (dir * Math.max(0, dtMs)) / span));
}

/** The holdToConfirm prop as a duration (0 = no hold). */
export function holdDuration(hold: boolean | number | undefined): number {
  if (hold === true) return HOLD_MS;
  if (typeof hold === "number" && Number.isFinite(hold) && hold > 0) return hold;
  return 0;
}


/** The Beautiful UI props every build adds to shadcn's Button (all optional). */
export interface ButtonExtras {
  /** "loading" (aria-busy, clicks ignored, width kept), "error", "success". Default "idle". */
  state?: ButtonState | undefined;
  /** A Lumen icon ("plus", "copy", "arrow", "trash" …) or your own 1.1em icon, before the label. Replaced by ! and ✓ in error and success. */
  icon?: LumenIconName | React.ReactNode | undefined;
  /** The round → chip inside the end of the button (hero calls to action). */
  trailingChip?: boolean | undefined;
  /** Fill the container; with trailingChip the label sits left and the chip right. */
  fullWidth?: boolean | undefined;
  /** Wrap long labels instead of truncating them (truncated labels get a title with the full text). */
  wrap?: boolean | undefined;
  /** Keep a disabled button in the tab order (aria-disabled) so its tooltip can explain why. Default true. */
  focusableWhenDisabled?: boolean | undefined;
  /** Hold to confirm (destructive actions): true for 1200ms, or a duration. onClick isn't called; onConfirm is. */
  holdToConfirm?: boolean | number | undefined;
  onConfirm?: (() => void) | undefined;
  /** How long the confirmed state shows before the button resets. Default 2400ms. */
  confirmResetMs?: number | undefined;
  labels?: Partial<ButtonLabels> | undefined;
  /** full, subtle (30% shorter, calm curve) or off (instant). Reduced motion: no sheen or bounce, 150ms fades. */
  motion?: ButtonMotion | undefined;
  /** false silences this button's cues; "subtle" plays them quieter. Needs beautiful-ui-sound. */
  sound?: boolean | "subtle" | undefined;
  /** Pin a state for docs and visual tests. */
  previewState?: "hover" | "focus" | "pressed" | undefined;
}

export const EXTRA_KEYS = [
  "state",
  "icon",
  "trailingChip",
  "fullWidth",
  "wrap",
  "focusableWhenDisabled",
  "holdToConfirm",
  "onConfirm",
  "confirmResetMs",
  "labels",
  "motion",
  "sound",
  "previewState",
] as const;

/** Splits the Beautiful UI extras from everything else (which goes to the primitive). */
export function splitExtras<P extends ButtonExtras>(props: P): [ButtonExtras, Omit<P, keyof ButtonExtras>] {
  const extras: Record<string, unknown> = {};
  const rest: Record<string, unknown> = {};
  for (const [k, v] of Object.entries(props)) {
    if ((EXTRA_KEYS as readonly string[]).includes(k)) extras[k] = v;
    else rest[k] = v;
  }
  return [extras as ButtonExtras, rest as Omit<P, keyof ButtonExtras>];
}

/** Points `ref` at `node`; returns the cleanup. */
function attachRef<T>(ref: React.Ref<T> | undefined, node: T | null): () => void {
  if (typeof ref === "function") {
    const cleanup = ref(node);
    return typeof cleanup === "function" ? cleanup : () => ref(null);
  }
  if (ref) {
    (ref as React.RefObject<T | null>).current = node;
    return () => {
      (ref as React.RefObject<T | null>).current = null;
    };
  }
  return () => {};
}

type Handlers = Partial<Record<"onPointerDown" | "onPointerUp" | "onPointerCancel" | "onPointerLeave" | "onPointerEnter" | "onFocus" | "onBlur" | "onKeyDown" | "onKeyUp" | "onContextMenu", ((e: never) => void) | undefined>>;

interface BehaviourInput extends ButtonExtras {
  variant: ButtonVariantName;
  size: ButtonSize;
  disabled?: boolean | undefined;
  onClick?: ((e: React.MouseEvent<HTMLElement>) => void) | undefined;
  handlers: Handlers;
  ariaLabel?: string | undefined;
  describedBy?: string | undefined;
  theirSound?: unknown;
  ref?: React.Ref<HTMLElement> | undefined;
}

type HoldPhase = "idle" | "holding" | "done";

/** Everything a build needs: attributes, handlers, the composed ref and what the content shows. */
export function useButtonBehaviour(input: BehaviourInput) {
  const { variant, disabled, state: stateProp = "idle", holdToConfirm, onConfirm, confirmResetMs = HOLD_RESET_MS, sound = true, motion, previewState, handlers } = input;
  const L = React.useMemo(() => ({ ...defaultButtonLabels, ...input.labels }), [input.labels]);
  const holdMs = holdDuration(holdToConfirm);
  const hold = holdMs > 0;
  const elRef = React.useRef<HTMLElement | null>(null);
  const theirRef = input.ref;
  const ref = React.useCallback(
    (node: HTMLElement | null) => {
      const a = attachRef(theirRef, node);
      const b = attachRef(elRef, node);
      return () => {
        a();
        b();
      };
    },
    [theirRef],
  );

  // Outcomes arriving through the state prop sound once (not on mount).
  const prevState = React.useRef(stateProp);
  React.useEffect(() => {
    const before = prevState.current;
    prevState.current = stateProp;
    if (before === stateProp) return;
    if (stateProp === "success") emitSound(elRef.current, "success");
    if (stateProp === "error") emitSound(elRef.current, "error");
  }, [stateProp]);

  // Hold to confirm.
  const [phase, setPhase] = React.useState<HoldPhase>("idle");
  const phaseRef = React.useRef<HoldPhase>("idle");
  const p = React.useRef(0);
  const dir = React.useRef<1 | -1>(1);
  const raf = React.useRef(0);
  const resetTimer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  const confirmRef = React.useRef(onConfirm);
  React.useEffect(() => {
    confirmRef.current = onConfirm;
  });
  const setP = (next: HoldPhase) => {
    phaseRef.current = next;
    setPhase(next);
  };
  const paint = (v: number) => elRef.current?.style.setProperty("--gch-p", String(v));
  const blocked = Boolean(disabled) || stateProp === "loading";
  const run = React.useCallback(() => {
    cancelAnimationFrame(raf.current);
    let last = performance.now();
    const step = (now: number) => {
      const prev = p.current;
      const next = stepHold(p.current, dir.current, now - last, holdMs);
      last = now;
      p.current = next;
      paint(next);
      // A quiet tick at each quarter while the hold fills.
      if (dir.current > 0 && next < 1 && Math.floor(next * 4) > Math.floor(prev * 4)) emitSound(elRef.current, "sliderTick", true);
      if (dir.current > 0 && next >= 1) {
        emitSound(elRef.current, "success");
        try {
          navigator.vibrate?.(12);
        } catch {}
        setP("done");
        confirmRef.current?.();
        clearTimeout(resetTimer.current);
        resetTimer.current = setTimeout(() => {
          p.current = 0;
          paint(0);
          setP("idle");
        }, confirmResetMs);
        return;
      }
      if ((dir.current > 0 && next < 1) || (dir.current < 0 && next > 0)) raf.current = requestAnimationFrame(step);
    };
    raf.current = requestAnimationFrame(step);
  }, [holdMs, confirmResetMs]);
  const holdStart = () => {
    if (!hold || blocked || phaseRef.current !== "idle") return;
    dir.current = 1;
    setP("holding");
    emitSound(elRef.current, variant === "destructive" ? "destructive" : "tap");
    run();
  };
  const holdEnd = () => {
    if (phaseRef.current !== "holding") return;
    dir.current = -1;
    setP("idle");
    run();
  };
  React.useEffect(() => {
    if (blocked && phaseRef.current === "holding") holdEnd();
    // eslint-disable-next-line -- (Beautiful UI's lint: react-hooks/exhaustive-deps)
  }, [blocked]);
  React.useEffect(() => {
    const el = elRef.current;
    return () => {
      cancelAnimationFrame(raf.current);
      clearTimeout(resetTimer.current);
      p.current = 0;
      el?.style.setProperty("--gch-p", "0");
      if (phaseRef.current !== "idle") {
        phaseRef.current = "idle";
        setPhase("idle");
      }
    };
  }, [holdMs]);

  // Truncated labels get the full text as a title.
  const labelRef = React.useRef<HTMLSpanElement>(null);
  const checkTruncation = () => {
    const el = labelRef.current;
    if (!el) return;
    if (el.scrollWidth > el.clientWidth + 1) el.title = el.textContent ?? "";
    else el.removeAttribute("title");
  };

  const hintId = React.useId();
  const state: ButtonState = stateProp !== "idle" ? stateProp : phase === "done" ? "success" : "idle";
  const interactive = state === "idle" && !disabled;
  const call = (name: keyof Handlers, e: unknown) => (handlers[name] as ((e: unknown) => void) | undefined)?.(e);

  const soundAttr = sound === false ? "off" : typeof input.theirSound === "string" ? input.theirSound : hold && !blocked ? "none" : variant === "destructive" ? "destructive" : undefined;
  const attrs = {
    "data-state": state === "idle" ? undefined : state,
    "data-pressed": phase === "holding" || previewState === "pressed" ? "" : undefined,
    "data-force": previewState,
    "data-chip": input.trailingChip ? "" : undefined,
    "data-full": input.fullWidth ? "" : undefined,
    "data-wrap": input.wrap ? "" : undefined,
    "data-hold": hold ? "" : undefined,
    "data-motion": motion && motion !== "full" ? motion : undefined,
    "data-sound": soundAttr,
    "data-sound-level": sound === "subtle" ? "subtle" : undefined,
    "aria-busy": state === "loading" ? true : undefined,
    "aria-label": state === "loading" && input.ariaLabel ? `${input.ariaLabel}, ${L.loading}` : input.ariaLabel,
    "aria-describedby": hold ? [input.describedBy, hintId].filter(Boolean).join(" ") : input.describedBy,
  };

  const events = {
    onClick: (e: React.MouseEvent<HTMLElement>) => {
      // aria-disabled and loading buttons stay focusable but never act or submit; hold buttons act on onConfirm.
      if (blocked || hold) {
        e.preventDefault();
        return;
      }
      input.onClick?.(e);
    },
    onPointerEnter: (e: React.PointerEvent<HTMLElement>) => {
      call("onPointerEnter", e);
      checkTruncation();
    },
    onFocus: (e: React.FocusEvent<HTMLElement>) => {
      call("onFocus", e);
      checkTruncation();
    },
    onPointerDown: (e: React.PointerEvent<HTMLElement>) => {
      call("onPointerDown", e);
      if (!hold || e.button !== 0) return;
      e.preventDefault();
      holdStart();
    },
    onPointerUp: (e: React.PointerEvent<HTMLElement>) => {
      call("onPointerUp", e);
      holdEnd();
    },
    onPointerLeave: (e: React.PointerEvent<HTMLElement>) => {
      call("onPointerLeave", e);
      holdEnd();
    },
    onPointerCancel: (e: React.PointerEvent<HTMLElement>) => {
      call("onPointerCancel", e);
      holdEnd();
    },
    onBlur: (e: React.FocusEvent<HTMLElement>) => {
      call("onBlur", e);
      holdEnd();
    },
    onContextMenu: (e: React.MouseEvent<HTMLElement>) => {
      call("onContextMenu", e);
      if (hold) e.preventDefault();
    },
    onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => {
      call("onKeyDown", e);
      if (!hold) return;
      // Cancelling always works; starting respects the caller's preventDefault.
      if (e.key === "Escape") holdEnd();
      else if (e.key === " " || e.key === "Enter") {
        const prevented = e.defaultPrevented;
        e.preventDefault();
        if (!prevented && !e.repeat) holdStart();
      }
    },
    onKeyUp: (e: React.KeyboardEvent<HTMLElement>) => {
      call("onKeyUp", e);
      if (hold && (e.key === " " || e.key === "Enter")) {
        e.preventDefault();
        holdEnd();
      }
    },
  };

  const label = (children: React.ReactNode) => (hold ? (phase === "done" ? L.confirmed : phase === "holding" ? L.holding : children) : children);
  return { ref, attrs, events, state, interactive, hold, phase, labels: L, hintId, labelRef, label };
}

/** The button's inside: sheen, hold cells, lead glyph, label, chip, and the loading ticker. */
export function ButtonContent({
  variant,
  state,
  icon,
  trailingChip,
  interactive,
  hold,
  holdDone,
  labels,
  hintId,
  labelRef,
  children,
}: {
  variant: ButtonVariantName;
  state: ButtonState;
  icon?: ButtonExtras["icon"] | undefined;
  trailingChip?: boolean | undefined;
  interactive: boolean;
  hold: boolean;
  holdDone: boolean;
  labels: ButtonLabels;
  hintId: string;
  labelRef: React.Ref<HTMLSpanElement>;
  children?: React.ReactNode | undefined;
}) {
  const lead = holdDone ? null : icon;
  const glyph = state === "error" ? <LumenErrorGlyph /> : state === "success" ? <LumenCheckGlyph /> : lead == null || lead === false ? null : isLumenIconName(lead) ? <LumenIcon name={lead} /> : lead;
  const hasText = children != null && children !== false && children !== "";
  return (
    <>
      {variant === "default" && interactive && (
        <span aria-hidden className={`lumen-button-shw`}>
          <span className={`lumen-button-sh`} />
        </span>
      )}
      {hold && (
        <span aria-hidden className={`lumen-button-hold`}>
          {Array.from({ length: 12 }, (_, i) => (
            <span key={i} style={{ "--i": i } as React.CSSProperties} />
          ))}
        </span>
      )}
      <span className={`lumen-button-l`}>
        {glyph && (
          <span className={`lumen-button-lead`} key={state}>
            {glyph}
          </span>
        )}
        {hasText && (
          <span className={`lumen-button-t`} ref={labelRef}>
            {children}
          </span>
        )}
        {trailingChip && (
          <span aria-hidden className={`lumen-button-chip`}>
            <LumenIcon name="arrow" />
          </span>
        )}
      </span>
      {state === "loading" && (
        <span className={`lumen-button-o`}>
          <LumenTicker />
          <span className={`lumen-sr`}> {labels.loading}</span>
        </span>
      )}
      {hold && (
        <span id={hintId} hidden>
          {labels.holdHint}
        </span>
      )}
    </>
  );
}
```

### 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/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"]'));
```

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

```css
@layer components {
  .lumen-button {
    --b-h: 40px;
    --b-fs: 14px;
    --b-px: 16px;
    --b-r: 20px;
    --b-g: 8px;
    --b-ifs: 16px;
    --b-py: 10px;
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    flex: none;
    max-width: 100%;
    height: var(--b-h);
    margin: 0;
    padding: 0 var(--b-px);
    border: none;
    border-radius: calc(var(--b-r) * var(--lumen-radius-k, 1));
    background: var(--b-bg);
    color: var(--b-c);
    box-shadow: var(--b-sh);
    transform: var(--b-tf,none);
    font: 500 var(--b-fs)/1 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    letter-spacing: -.006em;
    white-space: nowrap;
    text-decoration: none;
    text-align: center;
    vertical-align: middle;
    min-width: var(--b-h);
    cursor: pointer;
    outline: none;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
    user-select: none;
    -webkit-user-select: none;
    transition: transform var(--gc-t-tf) var(--gc-sp),box-shadow var(--gc-t-c) ease,background var(--gc-t-c) ease,color var(--gc-t-c) ease;
  }
  .lumen-button-sm {
    --b-h: 32px;
    --b-fs: 13px;
    --b-px: 12px;
    --b-r: 16px;
    --b-g: 6px;
    --b-ifs: 14px;
    --b-py: 7px;
  }
  .lumen-button-md {
    --b-h: 40px;
    --b-fs: 14px;
    --b-px: 16px;
    --b-r: 20px;
    --b-g: 8px;
    --b-ifs: 16px;
    --b-py: 10px;
  }
  .lumen-button-lg {
    --b-h: 48px;
    --b-fs: 15px;
    --b-px: 20px;
    --b-r: 24px;
    --b-g: 10px;
    --b-ifs: 18px;
    --b-py: 13px;
  }
  .lumen-button-icon {
    width: var(--b-h);
    padding: 0;
    border-radius: 50%;
    font-size: var(--b-ifs);
  }
  .lumen-button[data-chip] {
    padding-right: 5px;
  }
  .lumen-button[data-full] {
    width: 100%;
  }
  .lumen-button[data-full][data-chip] {
    justify-content: space-between;
    padding-left: calc(var(--b-px) + 2px);
  }
  .lumen-button[data-full][data-chip] .lumen-button-l {
    width: 100%;
    justify-content: space-between;
  }
  .lumen-button[data-wrap] {
    height: auto;
    min-height: var(--b-h);
    padding: var(--b-py) var(--b-px);
    white-space: normal;
    line-height: 1.35;
    border-radius: calc(calc(var(--b-r) - 4px) * var(--lumen-radius-k, 1));
  }
  .lumen-button[data-wrap] .lumen-button-t {
    overflow: visible;
  }
  .lumen-button:focus-visible,.lumen-button[data-force="focus"] {
    box-shadow: 0 0 0 2px var(--gc-gap), 0 0 0 4px var(--gc-focus), var(--b-sh);
  }
  .lumen-button:is([aria-disabled="true"],:disabled) {
    cursor: not-allowed;
  }
  .lumen-button[data-state="loading"] {
    cursor: progress;
  }
  .lumen-button-l {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--b-g);
    min-width: 0;
    max-width: 100%;
    overflow: hidden;
    transition: opacity var(--gc-t-fade) ease;
  }
  .lumen-button-icon .lumen-button-l {
    gap: 0;
    overflow: visible;
  }
  .lumen-button[data-state="loading"] .lumen-button-l {
    opacity: 0;
  }
  .lumen-button-t {
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
  }
  .lumen-button-o {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
  }
  .lumen-button-lead {
    display: flex;
    flex: none;
  }
  .lumen-button-chip {
    display: flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: calc(var(--b-h) - 10px);
    height: calc(var(--b-h) - 10px);
    margin-left: 2px;
    border-radius: 50%;
    background: var(--gc-chipBg);
    color: var(--gc-chipInk);
    font-size: calc(var(--b-fs) - 1px);
    transition: transform var(--gc-t-tf) var(--gc-sp);
  }
  .lumen-button-shw {
    position: absolute;
    inset: 0;
    border-radius: inherit;
    overflow: hidden;
    pointer-events: none;
  }
  .lumen-button-sh {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 40%;
    background: linear-gradient(100deg,transparent,var(--gc-sheen),transparent);
    transform: translateX(-110%) skewX(-18deg);
    transition: none;
  }
  .lumen-button-hold {
    position: absolute;
    inset: 4px;
    display: flex;
    gap: 2px;
    pointer-events: none;
  }
  .lumen-button-hold>span {
    flex: 1;
    border-radius: calc(4px * var(--lumen-radius-k, 1));
    background: var(--gc-bad);
    opacity: calc(clamp(0, var(--gch-p, 0) * 12 - var(--i), 1) * var(--gc-holdOp));
  }
  .lumen-button-hold>span:first-child {
    border-radius: calc(calc(var(--b-r) - 4px) * var(--lumen-radius-k, 1)) calc(4px * var(--lumen-radius-k, 1)) calc(4px * var(--lumen-radius-k, 1)) calc(calc(var(--b-r) - 4px) * var(--lumen-radius-k, 1));
  }
  .lumen-button-hold>span:last-child {
    border-radius: calc(4px * var(--lumen-radius-k, 1)) calc(calc(var(--b-r) - 4px) * var(--lumen-radius-k, 1)) calc(calc(var(--b-r) - 4px) * var(--lumen-radius-k, 1)) calc(4px * var(--lumen-radius-k, 1));
  }
  .lumen-button[data-state="success"] .lumen-button-hold {
    display: none;
  }
  .lumen-button[data-hold] {
    touch-action: none;
    -webkit-touch-callout: none;
  }
  @media (pointer:coarse) {
    .lumen-button::after {
      content: "";
      position: absolute;
      left: 50%;
      top: 50%;
      width: max(100%,44px);
      height: max(100%,44px);
      transform: translate(-50%,-50%);
    }
  }
  .lumen-button-default {
    --b-bg: var(--gc-primary-base-bg);
    --b-c: var(--gc-primary-base-c);
    --b-sh: var(--gc-primary-base-sh);
  }
  .lumen-button-default:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="hover"] {
    --b-bg: var(--gc-primary-hover-bg);
    --b-c: var(--gc-primary-hover-c);
    --b-sh: var(--gc-primary-hover-sh);
    --b-tf: translateY(-1px);
  }
  .lumen-button-default:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):active,.lumen-button-default:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="pressed"],.lumen-button-default[data-pressed] {
    --b-bg: var(--gc-primary-pressed-bg);
    --b-c: var(--gc-primary-pressed-c);
    --b-sh: var(--gc-primary-pressed-sh);
    --b-tf: translateY(.5px) scale(.98);
  }
  .lumen-button-default[data-state="error"] {
    --b-bg: var(--gc-primary-error-bg);
    --b-c: var(--gc-primary-error-c);
    --b-sh: var(--gc-primary-error-sh);
  }
  .lumen-button-default[data-state="success"] {
    --b-bg: var(--gc-primary-success-bg);
    --b-c: var(--gc-primary-success-c);
    --b-sh: var(--gc-primary-success-sh);
  }
  .lumen-button-default:is([aria-disabled="true"],:disabled) {
    --b-bg: var(--gc-primary-disabled-bg);
    --b-c: var(--gc-primary-disabled-c);
    --b-sh: var(--gc-primary-disabled-sh);
    --b-tf: none;
  }
  .lumen-button-secondary {
    --b-bg: var(--gc-secondary-base-bg);
    --b-c: var(--gc-secondary-base-c);
    --b-sh: var(--gc-secondary-base-sh);
  }
  .lumen-button-secondary:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="hover"] {
    --b-bg: var(--gc-secondary-hover-bg);
    --b-c: var(--gc-secondary-hover-c);
    --b-sh: var(--gc-secondary-hover-sh);
    --b-tf: translateY(-1px);
  }
  .lumen-button-secondary:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):active,.lumen-button-secondary:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="pressed"],.lumen-button-secondary[data-pressed] {
    --b-bg: var(--gc-secondary-pressed-bg);
    --b-c: var(--gc-secondary-pressed-c);
    --b-sh: var(--gc-secondary-pressed-sh);
    --b-tf: translateY(.5px) scale(.98);
  }
  .lumen-button-secondary[data-state="error"] {
    --b-bg: var(--gc-secondary-error-bg);
    --b-c: var(--gc-secondary-error-c);
    --b-sh: var(--gc-secondary-error-sh);
  }
  .lumen-button-secondary[data-state="success"] {
    --b-bg: var(--gc-secondary-success-bg);
    --b-c: var(--gc-secondary-success-c);
    --b-sh: var(--gc-secondary-success-sh);
  }
  .lumen-button-secondary:is([aria-disabled="true"],:disabled) {
    --b-bg: var(--gc-secondary-disabled-bg);
    --b-c: var(--gc-secondary-disabled-c);
    --b-sh: var(--gc-secondary-disabled-sh);
    --b-tf: none;
  }
  .lumen-button-ghost {
    --b-bg: var(--gc-quiet-base-bg);
    --b-c: var(--gc-quiet-base-c);
    --b-sh: var(--gc-quiet-base-sh);
  }
  .lumen-button-ghost:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="hover"] {
    --b-bg: var(--gc-quiet-hover-bg);
    --b-c: var(--gc-quiet-hover-c);
    --b-sh: var(--gc-quiet-hover-sh);
    --b-tf: none;
  }
  .lumen-button-ghost:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):active,.lumen-button-ghost:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="pressed"],.lumen-button-ghost[data-pressed] {
    --b-bg: var(--gc-quiet-pressed-bg);
    --b-c: var(--gc-quiet-pressed-c);
    --b-sh: var(--gc-quiet-pressed-sh);
    --b-tf: scale(.98);
  }
  .lumen-button-ghost[data-state="error"] {
    --b-bg: var(--gc-quiet-error-bg);
    --b-c: var(--gc-quiet-error-c);
    --b-sh: var(--gc-quiet-error-sh);
  }
  .lumen-button-ghost[data-state="success"] {
    --b-bg: var(--gc-quiet-success-bg);
    --b-c: var(--gc-quiet-success-c);
    --b-sh: var(--gc-quiet-success-sh);
  }
  .lumen-button-ghost:is([aria-disabled="true"],:disabled) {
    --b-bg: var(--gc-quiet-disabled-bg);
    --b-c: var(--gc-quiet-disabled-c);
    --b-sh: var(--gc-quiet-disabled-sh);
    --b-tf: none;
  }
  .lumen-button-destructive {
    --b-bg: var(--gc-destructive-base-bg);
    --b-c: var(--gc-destructive-base-c);
    --b-sh: var(--gc-destructive-base-sh);
  }
  .lumen-button-destructive:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="hover"] {
    --b-bg: var(--gc-destructive-hover-bg);
    --b-c: var(--gc-destructive-hover-c);
    --b-sh: var(--gc-destructive-hover-sh);
    --b-tf: translateY(-1px);
  }
  .lumen-button-destructive:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):active,.lumen-button-destructive:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="pressed"],.lumen-button-destructive[data-pressed] {
    --b-bg: var(--gc-destructive-pressed-bg);
    --b-c: var(--gc-destructive-pressed-c);
    --b-sh: var(--gc-destructive-pressed-sh);
    --b-tf: translateY(.5px) scale(.98);
  }
  .lumen-button-destructive[data-state="error"] {
    --b-bg: var(--gc-destructive-error-bg);
    --b-c: var(--gc-destructive-error-c);
    --b-sh: var(--gc-destructive-error-sh);
  }
  .lumen-button-destructive[data-state="success"] {
    --b-bg: var(--gc-destructive-success-bg);
    --b-c: var(--gc-destructive-success-c);
    --b-sh: var(--gc-destructive-success-sh);
  }
  .lumen-button-destructive:is([aria-disabled="true"],:disabled) {
    --b-bg: var(--gc-destructive-disabled-bg);
    --b-c: var(--gc-destructive-disabled-c);
    --b-sh: var(--gc-destructive-disabled-sh);
    --b-tf: none;
  }
  .lumen-button:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="hover"] .lumen-button-sh,.lumen-button:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="pressed"] .lumen-button-sh,.lumen-button:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):active .lumen-button-sh {
    transform: translateX(260%) skewX(-18deg);
  }
  .lumen-button:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="hover"] .lumen-button-chip {
    transform: translateX(2px);
  }
  .lumen-button-xs {
    --b-h: 28px;
    --b-fs: 12px;
    --b-px: 10px;
    --b-r: 14px;
    --b-g: 5px;
    --b-ifs: 13px;
    --b-py: 6px;
  }
  .lumen-button-outline {
    --b-bg: var(--gc-outline-base-bg);
    --b-c: var(--gc-outline-base-c);
    --b-sh: var(--gc-outline-base-sh);
  }
  .lumen-button-outline:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="hover"] {
    --b-bg: var(--gc-outline-hover-bg);
    --b-c: var(--gc-outline-hover-c);
    --b-sh: var(--gc-outline-hover-sh);
  }
  .lumen-button-outline:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):active,.lumen-button-outline:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="pressed"],.lumen-button-outline[data-pressed] {
    --b-bg: var(--gc-outline-pressed-bg);
    --b-c: var(--gc-outline-pressed-c);
    --b-sh: var(--gc-outline-pressed-sh);
    --b-tf: scale(.98);
  }
  .lumen-button-outline[data-state="error"] {
    --b-bg: var(--gc-outline-error-bg);
    --b-c: var(--gc-outline-error-c);
    --b-sh: var(--gc-outline-error-sh);
  }
  .lumen-button-outline[data-state="success"] {
    --b-bg: var(--gc-outline-success-bg);
    --b-c: var(--gc-outline-success-c);
    --b-sh: var(--gc-outline-success-sh);
  }
  .lumen-button-outline:is([aria-disabled="true"],:disabled) {
    --b-bg: var(--gc-outline-disabled-bg);
    --b-c: var(--gc-outline-disabled-c);
    --b-sh: var(--gc-outline-disabled-sh);
    --b-tf: none;
  }
  .lumen-button-link {
    --b-bg: var(--gc-link-base-bg);
    --b-c: var(--gc-link-base-c);
    --b-sh: var(--gc-link-base-sh);
  }
  .lumen-button-link:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="hover"] {
    --b-bg: var(--gc-link-hover-bg);
    --b-c: var(--gc-link-hover-c);
    --b-sh: var(--gc-link-hover-sh);
  }
  .lumen-button-link:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):active,.lumen-button-link:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="pressed"],.lumen-button-link[data-pressed] {
    --b-bg: var(--gc-link-pressed-bg);
    --b-c: var(--gc-link-pressed-c);
    --b-sh: var(--gc-link-pressed-sh);
    --b-tf: scale(.98);
  }
  .lumen-button-link[data-state="error"] {
    --b-bg: var(--gc-link-error-bg);
    --b-c: var(--gc-link-error-c);
    --b-sh: var(--gc-link-error-sh);
  }
  .lumen-button-link[data-state="success"] {
    --b-bg: var(--gc-link-success-bg);
    --b-c: var(--gc-link-success-c);
    --b-sh: var(--gc-link-success-sh);
  }
  .lumen-button-link:is([aria-disabled="true"],:disabled) {
    --b-bg: var(--gc-link-disabled-bg);
    --b-c: var(--gc-link-disabled-c);
    --b-sh: var(--gc-link-disabled-sh);
    --b-tf: none;
  }
  .lumen-button-link,.lumen-button-link .lumen-button-t {
    text-underline-offset: 3px;
  }
  .lumen-button-link .lumen-button-l,.lumen-button-link .lumen-button-t {
    padding-bottom: 5px;
    margin-bottom: -5px;
  }
  @media (hover:hover) {
    .lumen-button-default:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):hover {
      --b-bg: var(--gc-primary-hover-bg);
      --b-c: var(--gc-primary-hover-c);
      --b-sh: var(--gc-primary-hover-sh);
      --b-tf: translateY(-1px);
    }
    .lumen-button-secondary:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):hover {
      --b-bg: var(--gc-secondary-hover-bg);
      --b-c: var(--gc-secondary-hover-c);
      --b-sh: var(--gc-secondary-hover-sh);
      --b-tf: translateY(-1px);
    }
    .lumen-button-ghost:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):hover {
      --b-bg: var(--gc-quiet-hover-bg);
      --b-c: var(--gc-quiet-hover-c);
      --b-sh: var(--gc-quiet-hover-sh);
      --b-tf: none;
    }
    .lumen-button-destructive:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):hover {
      --b-bg: var(--gc-destructive-hover-bg);
      --b-c: var(--gc-destructive-hover-c);
      --b-sh: var(--gc-destructive-hover-sh);
      --b-tf: translateY(-1px);
    }
    .lumen-button[data-state="error"]:not(:is([aria-disabled="true"],:disabled)):hover {
      --b-tf: translateY(-1px);
    }
    .lumen-button:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):hover .lumen-button-sh {
      transform: translateX(260%) skewX(-18deg);
      transition: transform var(--gc-t-sheen) cubic-bezier(.22,1,.36,1);
    }
    .lumen-button:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):hover .lumen-button-chip {
      transform: translateX(2px);
    }
    .lumen-button-outline:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):hover {
      --b-bg: var(--gc-outline-hover-bg);
      --b-c: var(--gc-outline-hover-c);
      --b-sh: var(--gc-outline-hover-sh);
    }
    .lumen-button-link:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):hover {
      --b-bg: var(--gc-link-hover-bg);
      --b-c: var(--gc-link-hover-c);
      --b-sh: var(--gc-link-hover-sh);
    }
    .lumen-button-link:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):hover,.lumen-button-link:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"]):hover .lumen-button-t {
      text-decoration: underline;
    }
  }
  .lumen-button-link:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="hover"],.lumen-button-link:not(:is([aria-disabled="true"],:disabled)):not([data-state="loading"]):not([data-state="error"]):not([data-state="success"])[data-force="hover"] .lumen-button-t {
    text-decoration: underline;
  }
  :where(.lumen-button) svg:where(:not([class*="size-"]):not([style])) {
    width: var(--b-ifs,16px);
    height: var(--b-ifs,16px);
    flex: none;
  }
}
```

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.
