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

- id: `lumen-toggle` · 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-toggle.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-toggle
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

A switch that looks like an option: a glass chip that lifts under your cursor, presses in, and lights with the accent (icon and all) when it's on. shadcn's Toggle, 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/toggle.tsx`; shared code: `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: `Toggle`, `toggleVariants`, 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 { Toggle } from "@/components/ui/lumen/toggle";
```

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `Toggle` | `toggle` | The glass chip. |
| `toggleVariants` | `toggle` | The classes alone, as shadcn. |

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

## Match the original
- Read `components/ui/lumen/toggle.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
- toggle, toggle button, filter chip, view option, pressed button, shadcn toggle, Lumen
- View options on a chart or list: Compare, Annotations, Forecast
- Filter chips
- A drop-in for shadcn's Toggle: same export, variants and sizes

### Not when
- A setting that persists: use Lumen Switch
- One of several choices: use Lumen Toggle Group

## Mistakes
- Icon-only toggles need an aria-label
- Import from @/components/ui/lumen/toggle: it never replaces your own

## Usage

```tsx
import { Toggle } from "@/components/ui/lumen/toggle";

<Toggle pressed={compare} onPressedChange={setCompare}>Compare</Toggle>
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `pressed / defaultPressed / onPressedChange` | `boolean / boolean / (pressed) => void` |  | Controlled or not, as shadcn. |
| `variant` | `"default" \| "outline"` | `"default"` | default: the glass chip. outline: the ring without the fill. |
| `size` | `"sm" \| "default" \| "lg"` | `"default"` | 26, 30 or 36px (40 on phones). |
| `sound` | `boolean` | `true` | Beautiful UI: false silences; otherwise toggle-on and toggle-off. |

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

## 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 |
|---|---|
| Space / Enter | Toggle |

## Motion inventory

| Interaction | What moves |
|---|---|
| Hover | Lifts 1px; the ring brightens |
| Press | Scales to .97 on a spring |
| On | Tints with the accent; the icon lights |

## Accessibility contract (preserve when editing)
- A button with aria-pressed, from Base UI or Radix
- A 44px touch target on every size
- The pressed state shows in the tint and the icon, and in aria-pressed

## Install

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

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

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

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

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

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

## Source (Base UI build)

### components/ui/lumen/toggle.tsx

```tsx
"use client";

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

import * as React from "react";
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { emitSound, useMergedRef } from "@/lib/beautiful-ui/lumen/sound";

/*
 * Lumen Toggle (Base UI build). shadcn's Toggle in Lumen Halo: the glass filter chip. It lifts
 * on hover, presses in, and when on it tints with the accent (its icon lights). 40px tall on
 * phones with a 44px touch target. Plays toggle-on / toggle-off through beautiful-ui-sound. Same exports,
 * variants and sizes as shadcn.
 */

const toggleVariants = cva(`lumen-toggle lumen-control`, {
  variants: {
    variant: { default: "", outline: `lumen-toggle-outline` },
    size: { default: "", sm: "", lg: "" },
  },
  defaultVariants: { variant: "default", size: "default" },
});

function Toggle({
  className,
  variant = "default",
  size = "default",
  sound = true,
  onPressedChange,
  ref,
  ...props
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants> & { sound?: boolean }) {
  const own = React.useRef<HTMLButtonElement>(null);
  const merged = useMergedRef(own, ref);
  return (
    <TogglePrimitive
      ref={merged}
      data-slot="toggle"
      data-size={size}
      data-sound={sound ? "none" : "off"}
      className={cn(toggleVariants({ variant, size, className }))}
      onPressedChange={(pressed, details) => {
        onPressedChange?.(pressed, details);
        if (!details?.isCanceled) emitSound(own.current, pressed ? "toggleOn" : "toggleOff");
      }}
      {...props}
    />
  );
}

export { Toggle, toggleVariants };
```

### 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-toggle {
    --t-h: 30px;
    --t-px: 11px;
    --t-fs: 12.5px;
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    box-sizing: border-box;
    flex: none;
    max-width: 100%;
    height: var(--t-h);
    min-width: var(--t-h);
    margin: 0;
    padding: 0 var(--t-px);
    border: none;
    border-radius: calc(10px * var(--lumen-radius-k, 1));
    background: var(--gc-glassB);
    --c-ring: var(--gc-glassR);
    box-shadow: inset 0 0 0 1px var(--c-ring);
    color: var(--gc-m78);
    font: 500 var(--t-fs)/1 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    letter-spacing: normal;
    white-space: nowrap;
    cursor: pointer;
    outline: none;
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
    vertical-align: middle;
    -webkit-font-smoothing: antialiased;
    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,opacity var(--gc-t-c) ease;
  }
  .lumen-toggle[data-size="sm"] {
    --t-h: 26px;
    --t-px: 9px;
    --t-fs: 12px;
    border-radius: calc(8px * var(--lumen-radius-k, 1));
  }
  .lumen-toggle[data-size="lg"] {
    --t-h: 36px;
    --t-px: 14px;
    --t-fs: 13.5px;
    border-radius: calc(12px * var(--lumen-radius-k, 1));
  }
  .lumen-toggle>svg {
    width: 14px;
    height: 14px;
    flex: none;
    color: var(--gc-lead);
    transition: color var(--gc-t-c) ease;
  }
  .lumen-toggle-outline {
    background: transparent;
  }
  @media (hover:hover) {
    .lumen-toggle:not(:is([data-pressed],[data-state="on"],[aria-pressed="true"])):not(:is([data-disabled],:disabled)):hover {
      background: var(--gc-chipHover);
      --c-ring: var(--gc-wrH);
      color: var(--gc-ink);
      transform: translateY(-1px);
    }
    .lumen-toggle:is([data-pressed],[data-state="on"],[aria-pressed="true"]):not(:is([data-disabled],:disabled)):hover {
      transform: translateY(-1px);
    }
  }
  .lumen-toggle:not(:is([data-disabled],:disabled)):active {
    transform: scale(.97);
  }
  .lumen-toggle:is([data-pressed],[data-state="on"],[aria-pressed="true"]) {
    background: color-mix(in srgb, var(--gc-acc) 13%, transparent);
    --c-ring: color-mix(in srgb, var(--gc-acc) 40%, transparent);
    color: var(--gc-acc-tint);
  }
  .lumen-toggle:is([data-pressed],[data-state="on"],[aria-pressed="true"])>svg {
    color: var(--gc-acc);
  }
  .lumen-toggle:focus-visible {
    box-shadow: 0 0 0 2px var(--gc-gap), 0 0 0 4px var(--gc-p-focus), inset 0 0 0 1px var(--c-ring);
  }
  .lumen-toggle:is([data-disabled],:disabled) {
    opacity: .4;
    cursor: not-allowed;
  }
  .lumen-toggle[aria-invalid="true"] {
    --c-ring: color-mix(in srgb, var(--gc-bad) 60%, transparent);
  }
  .lumen-toggle::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: max(100%,44px);
    height: max(100%,44px);
    transform: translate(-50%,-50%);
  }
  @media (max-width: 699px) {
    .lumen-toggle:is([data-size],:not([data-size])) {
      --t-h: 40px;
      --t-px: 14px;
      --t-fs: 14px;
      border-radius: calc(12px * var(--lumen-radius-k, 1));
    }
  }
}
```

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.
