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

- id: `lumen-tabs` · version 1.0.0 · component · free
- category: Navigation
- build: Base UI (this item also ships a Radix build)
- install (this build): `npx shadcn@latest add https://beautiful-ui.dev/r/lumen-tabs.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-tabs
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

Tabs with a sense of place: a glass thumb glides to the tab you pick and the panel rises in, or, as page tabs, a lens and a filament of light follow you along the row. shadcn's Tabs, 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/tabs.tsx`; shared code: `lib/beautiful-ui/lumen/indicator.ts`, `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: `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/lumen/tabs";
```

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `Tabs` | `tabs` | State and orientation. |
| `TabsList` | `tabs-list` | The pill track or the page-tab row, with the gliding indicator. |
| `TabsTrigger` | `tabs-trigger` | One tab. |
| `TabsContent` | `tabs-content` | One panel. |

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.
- This item: select, pitched up the Lumen scale by the tab's position.

## Match the original
- Read `components/ui/lumen/tabs.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
- tabs, tab bar, segmented tabs, page tabs, section switcher, views, shadcn tabs, Lumen
- Switching views of the same thing: Overview, Funnels, Retention
- Page sections with counts: variant="line" with <span data-slot="tabs-count">
- A drop-in for shadcn's Tabs: same exports, parts, data-slots and tabsListVariants

### Not when
- Navigating between pages: use links
- Picking a value for a form: use Lumen Toggle Group or Radio Group

## Mistakes
- Give every TabsTrigger a value that matches its TabsContent
- Don't put the count in the label text: use <span data-slot="tabs-count">, so it lights with the tab
- Import from @/components/ui/lumen/tabs: it never replaces your own

## Usage

```tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/lumen/tabs";

<Tabs defaultValue="overview">
  <TabsList>
    <TabsTrigger value="overview">Overview</TabsTrigger>
    <TabsTrigger value="funnels">Funnels</TabsTrigger>
  </TabsList>
  <TabsContent value="overview">…</TabsContent>
  <TabsContent value="funnels">…</TabsContent>
</Tabs>
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `Tabs value / defaultValue / onValueChange` | `string / string / (value) => void` |  | Controlled or not, as shadcn. |
| `Tabs orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Vertical stacks the list beside the panels; the indicator follows both ways. |
| `TabsList variant` | `"default" \| "line"` | `"default"` | default: the glass pill track and gliding thumb. line: page tabs with a lens and a filament. |
| `TabsList size` | `"sm" \| "default" \| "lg"` | `"default"` | Beautiful UI: 26, 30 or 40px tabs (36 and 40 on phones). |
| `tabs-count` | `<span data-slot="tabs-count">` |  | A mono count chip that lights with its tab. |
| `Tabs sound` | `boolean` | `true` | Beautiful UI: false silences; otherwise select, pitched by the tab's position. |

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

## 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 |
|---|---|
| Arrow keys | Move between tabs |
| Home / End | First or last tab |
| Tab | Into the panel |

## Motion inventory

| Interaction | What moves |
|---|---|
| Switch | The thumb or lens glides to the new tab (.6s, cubic-bezier(.22,1,.36,1)); the filament rides under the lens |
| Panel | Rises 4px and fades in (.4s) |
| Press | An inactive tab presses to .96 on a spring |
| Sound | select, pitched up the Lumen scale by the tab's position |

## Accessibility contract (preserve when editing)
- role=tablist, tab and tabpanel with arrow-key navigation, from Base UI or Radix
- The gliding thumb and the lens are decoration (aria-hidden); the active tab is aria-selected
- Panels take focus with a visible ring
- Reduced motion: the thumb jumps, the panel fades without rising

## Install

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

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

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

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

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

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

## Source (Base UI build)

### components/ui/lumen/tabs.tsx

```tsx
"use client";

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

import * as React from "react";
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { useActiveIndicator } from "@/lib/beautiful-ui/lumen/indicator";
import { emitSound, useMergedRef } from "@/lib/beautiful-ui/lumen/sound";

/*
 * Lumen Tabs (Base UI build). shadcn's Tabs in Lumen Halo. The default list is a glass pill track
 * whose thumb glides to the active tab on a spring; variant="line" is page tabs, a glass lens behind
 * the active tab with a filament of light under it. Counts go in <span data-slot="tabs-count">.
 * Switching plays Lumen's select cue, pitched by the tab's position (beautiful-ui-sound). Same exports,
 * parts and data-slots as shadcn; size is Beautiful UI's.
 */

function Tabs({ className, orientation = "horizontal", onValueChange, sound = true, ref, ...props }: TabsPrimitive.Root.Props & { sound?: boolean }) {
  const own = React.useRef<HTMLDivElement>(null);
  const merged = useMergedRef(own, ref);
  return (
    <TabsPrimitive.Root
      ref={merged}
      data-slot="tabs"
      data-orientation={orientation}
      data-sound={sound ? undefined : "off"}
      orientation={orientation}
      className={cn(`lumen-tabs lumen-control`, className)}
      onValueChange={(value, details) => {
        onValueChange?.(value, details);
        // Silent for the automatic first selection and for changes you cancel.
        if (details?.reason === "initial") return;
        requestAnimationFrame(() => {
          if (details?.isCanceled) return;
          const tabs = [...(own.current?.querySelectorAll<HTMLElement>('[data-slot="tabs-trigger"]') ?? [])];
          const at = tabs.findIndex((t) => t.getAttribute("aria-selected") === "true");
          if (at >= 0) emitSound(tabs[at] ?? null, "select", false, at);
        });
      }}
      {...props}
    />
  );
}

const tabsListVariants = cva(`lumen-tabs-list`, {
  variants: { variant: { default: "", line: "" } },
  defaultVariants: { variant: "default" },
});

function TabsList({
  className,
  variant = "default",
  size = "default",
  children,
  ref,
  ...props
}: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants> & { size?: "sm" | "default" | "lg" }) {
  const own = React.useRef<HTMLDivElement>(null);
  const merged = useMergedRef(own, ref);
  useActiveIndicator(own);
  return (
    <TabsPrimitive.List ref={merged} data-slot="tabs-list" data-variant={variant} data-size={size} className={cn(tabsListVariants({ variant }), className)} {...props}>
      <span aria-hidden className={`lumen-tabs-indicator`} />
      {children}
    </TabsPrimitive.List>
  );
}

function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
  return <TabsPrimitive.Tab data-slot="tabs-trigger" data-sound="none" className={cn(`lumen-tabs-trigger`, className)} {...props} />;
}

function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
  return <TabsPrimitive.Panel data-slot="tabs-content" className={cn(`lumen-tabs-content`, className)} {...props} />;
}

export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
```

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

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

```css
@layer components {
  .lumen-tabs {
    display: flex;
    flex-direction: column;
    gap: 14px;
    min-width: 0;
    font-family: var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
  }
  .lumen-tabs[data-orientation="vertical"] {
    flex-direction: row;
  }
  .lumen-tabs-list {
    --p-h: 30px;
    --p-px: 13px;
    --p-fs: 13px;
    --p-pad: 4px;
    position: relative;
    display: inline-flex;
    align-items: stretch;
    box-sizing: border-box;
    width: max-content;
    max-width: 100%;
    min-width: 0;
  }
  .lumen-tabs-list[data-size="sm"] {
    --p-h: 26px;
    --p-px: 10px;
    --p-fs: 12px;
  }
  .lumen-tabs-list[data-size="lg"] {
    --p-h: 40px;
    --p-px: 16px;
    --p-fs: 14px;
  }
  .lumen-tabs[data-orientation="vertical"]>.lumen-tabs-list {
    flex-direction: column;
    width: auto;
  }
  .lumen-tabs-indicator {
    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));
    pointer-events: none;
    opacity: 0;
    transition: transform calc(0.6s * var(--gcp-k)) cubic-bezier(.22,1,.36,1),width calc(0.6s * var(--gcp-k)) cubic-bezier(.22,1,.36,1),height calc(0.6s * var(--gcp-k)) cubic-bezier(.22,1,.36,1),opacity min(0.3s, var(--gcp-cm)) ease;
  }
  .lumen-tabs-list[data-ind-ready] .lumen-tabs-indicator {
    opacity: 1;
  }
  .lumen-tabs-list[data-ind-still] .lumen-tabs-indicator {
    transition: none;
  }
  .lumen-tabs-list[data-variant="default"] {
    gap: 0;
    padding: var(--p-pad);
    border-radius: calc(calc(var(--p-h) / 2 + var(--p-pad)) * var(--lumen-radius-k, 1));
    background: var(--gc-p-track);
    box-shadow: var(--gc-p-trackSh);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .lumen-tabs-list[data-variant="default"]::-webkit-scrollbar {
    display: none;
  }
  .lumen-tabs-list[data-variant="default"] .lumen-tabs-indicator {
    border-radius: calc(calc(var(--p-h) / 2) * var(--lumen-radius-k, 1));
    background: var(--gc-p-thumb);
    box-shadow: var(--gc-p-thumbSh);
  }
  .lumen-tabs-trigger {
    position: relative;
    z-index: 1;
    display: inline-flex;
    flex: 1 0 auto;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-width: 0;
    height: var(--p-h);
    box-sizing: border-box;
    margin: 0;
    padding: 0 var(--p-px);
    border: none;
    border-radius: calc(calc(var(--p-h) / 2) * var(--lumen-radius-k, 1));
    background: transparent;
    box-shadow: none;
    color: var(--gc-p-m62);
    font: 500 var(--p-fs)/normal var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    letter-spacing: normal;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    cursor: pointer;
    outline: none;
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
    transition: color min(0.4s, var(--gcp-cm)) ease,background min(0.25s, var(--gcp-cm)) ease,transform calc(0.35s * var(--gcp-k)) var(--gc-sp);
  }
  .lumen-tabs-trigger>svg {
    width: 14px;
    height: 14px;
    flex: none;
  }
  @media (hover:hover) {
    .lumen-tabs-trigger:not(:is([data-active],[data-state="active"])):not(:is([data-disabled],:disabled)):hover {
      background: var(--gc-p-hov);
      color: var(--gc-ink);
    }
  }
  .lumen-tabs-trigger:not(:is([data-active],[data-state="active"])):not(:is([data-disabled],:disabled)):active {
    transform: scale(.96);
  }
  .lumen-tabs-trigger:is([data-active],[data-state="active"]) {
    color: var(--gc-p-thumbInk);
  }
  .lumen-tabs-trigger:is([data-disabled],:disabled) {
    color: var(--gc-p-m30);
    cursor: not-allowed;
  }
  .lumen-tabs-trigger:focus-visible {
    box-shadow: 0 0 0 2px var(--gc-gap), 0 0 0 4px var(--gc-p-focus), 0 0 #0000;
  }
  .lumen-tabs-list[data-variant="line"] {
    --p-h: 36px;
    --p-px: 14px;
    --p-fs: 14px;
    gap: 2px;
    margin: -4px -4px 0;
    padding: 4px 4px 10px;
    background: linear-gradient(var(--gc-hair),var(--gc-hair)) no-repeat 4px 100% / calc(100% - 8px) 1px;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .lumen-tabs-list[data-variant="line"][data-size="sm"] {
    --p-h: 30px;
    --p-px: 11px;
    --p-fs: 13px;
  }
  .lumen-tabs-list[data-variant="line"][data-size="lg"] {
    --p-h: 42px;
    --p-px: 16px;
    --p-fs: 15px;
  }
  .lumen-tabs-list[data-variant="line"]::-webkit-scrollbar {
    display: none;
  }
  .lumen-tabs-list[data-variant="line"] .lumen-tabs-indicator {
    border-radius: calc(12px * var(--lumen-radius-k, 1));
    background: var(--gc-p-lens);
    box-shadow: var(--gc-p-lensSh);
  }
  .lumen-tabs-list[data-variant="line"] .lumen-tabs-indicator::after {
    content: "";
    position: absolute;
    left: 12px;
    right: 12px;
    bottom: -10px;
    height: 1.5px;
    border-radius: calc(1px * var(--lumen-radius-k, 1));
    background: linear-gradient(90deg,transparent,var(--gc-p-acc),transparent);
  }
  .lumen-tabs-list[data-variant="line"] .lumen-tabs-trigger {
    border-radius: calc(12px * var(--lumen-radius-k, 1));
    color: var(--gc-p-m60);
  }
  .lumen-tabs-list[data-variant="line"] .lumen-tabs-trigger:is([data-active],[data-state="active"]) {
    color: var(--gc-ink);
  }
  .lumen-tabs-list[data-variant="line"] .lumen-tabs-trigger:is([data-disabled],:disabled) {
    color: var(--gc-p-m30);
  }
  .lumen-tabs-list[data-variant="default"]:not([data-ind-ready]) .lumen-tabs-trigger:is([data-active],[data-state="active"]) {
    background: var(--gc-p-thumb);
    box-shadow: var(--gc-p-thumbSh);
  }
  .lumen-tabs-list[data-variant="line"]:not([data-ind-ready]) .lumen-tabs-trigger:is([data-active],[data-state="active"]) {
    background: var(--gc-p-lens);
    box-shadow: var(--gc-p-lensSh);
  }
  .lumen-tabs-trigger>[data-slot="tabs-count"] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    box-sizing: border-box;
    padding: 0 5px;
    border-radius: calc(5px * var(--lumen-radius-k, 1));
    background: var(--gc-kbd);
    color: var(--gc-p-m55);
    font: 500 10.5px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
    transition: background min(0.35s, var(--gcp-cm)) ease,color min(0.35s, var(--gcp-cm)) ease;
  }
  .lumen-tabs-trigger:is([data-active],[data-state="active"])>[data-slot="tabs-count"] {
    background: color-mix(in srgb, var(--gc-p-acc) 16%, transparent);
    color: var(--gc-p-acc-t);
  }
  .lumen-tabs-content {
    min-width: 0;
    outline: none;
    animation: lumen-tab-in calc(.4s * var(--gcp-k)) cubic-bezier(.22,1,.36,1) both;
  }
  .lumen-tabs-content:focus-visible {
    border-radius: calc(12px * var(--lumen-radius-k, 1));
    box-shadow: 0 0 0 2px var(--gc-gap), 0 0 0 4px var(--gc-p-focus), 0 0 #0000;
  }
  @keyframes lumen-tab-in {
    from {
      opacity: 0;
      transform: translateY(4px);
    }
  }
  @media (max-width: 699px) {
    .lumen-tabs-list[data-variant="default"]:not([data-size="lg"]) {
      --p-h: 36px;
    }
    .lumen-tabs-list[data-variant="line"]:not([data-size="lg"]) {
      --p-h: 40px;
    }
  }
}
```

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.
