# Fader

> A mixing-desk channel fader with a scale and an LED meter.

Category: Instruments. Docs: https://neonkit.heyaadi.com/components/fader

## Installation

```bash
npx shadcn@latest add https://neonkit.heyaadi.com/r/fader.json
```

With the `@neonkit` namespace configured: `npx shadcn@latest add @neonkit/fader`.

npm dependencies: none. Also needs `lib/utils.ts` (`cn`) and `lib/neon.ts` (`neonVars`), which the CLI adds.

## Usage

```tsx
import { Fader } from "@/components/ui/fader"

<Fader label="Vox" defaultValue={70} level={0.6} />
```

## Examples

### Mixing desk

Six channels and a main bus. Each fader runs −60 to +10 dB on a printed scale, and level drives the LED meter beside it from a fake 120 BPM groove. Double-click a cap to return it to 0 dB.

```tsx
"use client";

import * as React from "react";
import { Fader } from "@/components/ui/fader";
import { Knob } from "@/components/ui/knob";

const scale = [
  { value: 10, label: "+10" },
  { value: 5, label: "+5" },
  { value: 0, label: "0" },
  { value: -5, label: "−5" },
  { value: -10, label: "−10" },
  { value: -20, label: "−20" },
  { value: -30, label: "−30" },
  { value: -40, label: "−40" },
  { value: -60, label: "−∞" },
];

const decibels = (v: number) =>
  v <= -60 ? "−∞ dB" : `${v > 0 ? "+" : v < 0 ? "−" : ""}${Math.abs(v).toFixed(1)} dB`;
const pan = (v: number) => (v === 0 ? "C" : v < 0 ? `L${-v}` : `R${v}`);

// A fake 120 BPM groove: each channel's amplitude (0–1) at time t, in seconds.
const sources: Record<string, (t: number) => number> = {
  kick: (t) => 0.9 * Math.exp(-((t * 2) % 1) * 9),
  snare: (t) => {
    const p = ((t * 2) % 2) - 1;
    return p >= 0 ? 0.75 * Math.exp(-p * 6) : 0.03;
  },
  bass: (t) => {
    const eighth = Math.floor(t * 4) % 8;
    return [1, 1, 0, 1, 1, 0, 1, 1][eighth] ? 0.55 * (0.6 + 0.4 * Math.exp(-((t * 4) % 1) * 4)) : 0.05;
  },
  keys: (t) => 0.45 * (0.35 + 0.65 * Math.exp(-((t / 2) % 1) * 1.6)),
  vox: (t) => ((t / 3) % 1 < 0.66 ? 0.5 + 0.15 * Math.sin(t * 11) + 0.08 * Math.sin(t * 3.7) : 0.02),
  fx: (t) => 0.3 + 0.08 * Math.sin(t * 1.3) + 0.05 * Math.sin(t * 5.1),
};

const channels = [
  { id: "kick", name: "Kick", gain: -4, pan: 0 },
  { id: "snare", name: "Snare", gain: -6, pan: -4 },
  { id: "bass", name: "Bass", gain: -3, pan: 0 },
  { id: "keys", name: "Keys", gain: -9, pan: 18 },
  { id: "vox", name: "Vox", gain: -2, pan: 0 },
  { id: "fx", name: "FX", gain: -14, pan: -12 },
];

const toMeter = (amplitude: number) => {
  const db = 20 * Math.log10(Math.max(amplitude, 1e-4));
  return Math.min(1, Math.max(0, (db + 48) / 48));
};
const gainOf = (db: number) => (db <= -60 ? 0 : Math.pow(10, db / 20));

export default function FaderDemo() {
  const [gains, setGains] = React.useState<Record<string, number>>(() =>
    Object.fromEntries([...channels.map((c) => [c.id, c.gain]), ["main", 0]]),
  );
  const [levels, setLevels] = React.useState<Record<string, number>>({});
  const gainsRef = React.useRef(gains);

  React.useEffect(() => {
    gainsRef.current = gains;
  }, [gains]);

  const rootRef = React.useRef<HTMLDivElement>(null);

  // The meters only run while the desk is on screen.
  React.useEffect(() => {
    const node = rootRef.current;
    if (!node) return;
    const start = performance.now();
    let id = 0;
    const stop = () => {
      window.clearInterval(id);
      id = 0;
    };
    const run = () => {
      if (id) return;
      id = window.setInterval(tick, 50);
    };
    const tick = () => {
      const t = (performance.now() - start) / 1000;
      const g = gainsRef.current;
      const next: Record<string, number> = {};
      let sum = 0;
      for (const c of channels) {
        const amplitude = sources[c.id](t) * gainOf(g[c.id]);
        sum += amplitude * amplitude;
        next[c.id] = toMeter(amplitude);
      }
      next.main = toMeter(Math.sqrt(sum) * 0.7 * gainOf(g.main));
      setLevels(next);
    };
    const io = new IntersectionObserver(([entry]) => (entry.isIntersecting ? run() : stop()));
    io.observe(node);
    return () => {
      io.disconnect();
      stop();
    };
  }, []);

  const setGain = (id: string) => (value: number) => setGains((g) => ({ ...g, [id]: value }));

  return (
    <div
      ref={rootRef}
      className="w-full max-w-[700px] overflow-x-auto rounded-2xl border border-white/[0.07] bg-[linear-gradient(180deg,#141720,#0b0d12)] shadow-[inset_0_1px_0_rgb(255_255_255/0.07),0_30px_60px_-30px_rgb(0_0_0/0.9)]"
    >
      <div className="flex min-w-max">
        {channels.map((c, i) => (
          <div
            key={c.id}
            className="flex flex-1 flex-col items-center gap-4 border-r border-white/[0.05] px-3 pb-5 pt-4"
          >
            <span className="text-[10px] font-medium tabular-nums text-white/50">{String(i + 1).padStart(2, "0")}</span>
            <Knob
              size="sm"
              aria-label={`${c.name} pan`}
              min={-50}
              max={50}
              defaultValue={c.pan}
              detents={[0]}
              formatValue={pan}
            />
            <Fader
              label={c.name}
              min={-60}
              max={10}
              step={0.5}
              ticks={scale}
              value={gains[c.id]}
              defaultValue={0}
              onValueChange={setGain(c.id)}
              level={levels[c.id] ?? 0}
              formatValue={decibels}
              showValue
              size="sm"
              className="h-64"
            />
          </div>
        ))}
        <div className="flex flex-1 flex-col items-center gap-4 border-l border-white/[0.08] bg-white/[0.02] px-4 pb-5 pt-4">
          <span className="text-[10px] font-semibold uppercase tracking-[0.16em] text-white/50">Main</span>
          <div className="h-14" aria-hidden />
          <Fader
            label="Stereo"
            min={-60}
            max={10}
            step={0.5}
            ticks={scale}
            value={gains.main}
            defaultValue={0}
            onValueChange={setGain("main")}
            level={levels.main ?? 0}
            formatValue={decibels}
            showValue
            size="sm"
            className="h-64"
          />
        </div>
      </div>
    </div>
  );
}
```

### Graphic EQ

Ticks with a null label print the line without a number. Controlled values let presets move every band at once.

```tsx
"use client";

import * as React from "react";
import { Button } from "@/components/ui/button";
import { Fader } from "@/components/ui/fader";

const bands = ["31", "63", "125", "250", "500", "1k", "2k", "4k", "8k", "16k"];

const presets: Record<string, number[]> = {
  Flat: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  Vocal: [-6, -4, -2, 0, 2, 4, 5, 4, 2, 0],
  "Bass boost": [8, 7, 5, 2, 0, 0, 0, 0, 1, 2],
};

const scale = [
  { value: 12, label: null },
  { value: 6, label: null },
  { value: 0, label: null },
  { value: -6, label: null },
  { value: -12, label: null },
];

const gain = (v: number) => `${v > 0 ? "+" : v < 0 ? "−" : ""}${Math.abs(v)} dB`;

export default function FaderEqualizer() {
  const [gains, setGains] = React.useState(presets.Vocal);

  return (
    <div className="w-full max-w-[560px] rounded-xl border border-white/10 bg-white/[0.03] p-5">
      <div className="mb-4 flex flex-wrap items-center justify-between gap-3">
        <span className="text-sm font-medium text-white/90">Graphic EQ</span>
        <div className="flex gap-1.5">
          {Object.keys(presets).map((name) => (
            <Button key={name} size="sm" variant="ghost" onClick={() => setGains(presets[name])}>
              {name}
            </Button>
          ))}
        </div>
      </div>
      <div className="flex justify-between overflow-x-auto">
        {bands.map((band, i) => (
          <Fader
            key={band}
            size="sm"
            label={band}
            aria-label={`${band} Hz`}
            min={-12}
            max={12}
            step={0.5}
            defaultValue={0}
            value={gains[i]}
            onValueChange={(value) => setGains((current) => current.map((g, j) => (j === i ? value : g)))}
            ticks={scale}
            formatValue={gain}
            className="h-44"
          />
        ))}
      </div>
    </div>
  );
}
```

### Crossfader

orientation="horizontal" lays the strip on its side; the scale prints below and the meter, if any, above.

```tsx
"use client";

import * as React from "react";
import { Fader } from "@/components/ui/fader";

const blend = (v: number) => (v === 0 ? "Center" : v < 0 ? `${-v}% A` : `${v}% B`);

export default function FaderHorizontal() {
  const [position, setPosition] = React.useState(-30);
  const a = 50 - position;
  const b = 50 + position;

  return (
    <div className="w-full max-w-sm rounded-xl border border-white/10 bg-white/[0.03] px-6 pb-5 pt-4">
      <div className="mb-3 flex items-center justify-between text-xs">
        <span className="text-white/55">
          Deck A <span className="tabular-nums text-white/90">{a}%</span>
        </span>
        <span className="text-white/55">
          <span className="tabular-nums text-white/90">{b}%</span> Deck B
        </span>
      </div>
      <Fader
        orientation="horizontal"
        aria-label="Crossfader"
        min={-50}
        max={50}
        value={position}
        defaultValue={0}
        onValueChange={setPosition}
        ticks={[
          { value: -50, label: "A" },
          { value: -25, label: null },
          { value: 0, label: null },
          { value: 25, label: null },
          { value: 50, label: "B" },
        ]}
        formatValue={blend}
        className="w-full"
      />
    </div>
  );
}
```

### Sizes, colors and disabled

The meter's top segments stay amber and red whatever the color.

```tsx
"use client";

import { Fader } from "@/components/ui/fader";

export default function FaderColors() {
  return (
    <div className="flex flex-wrap items-end justify-center gap-x-3 gap-y-8 sm:gap-8">
      <Fader size="sm" label="Drums" color="amber" defaultValue={70} level={0.62} ticks={6} className="h-52" />
      <Fader size="md" label="Synth" color="magenta" defaultValue={55} level={0.48} ticks={6} className="h-52" />
      <Fader size="lg" label="Pads" color="green" defaultValue={40} level={0.3} ticks={6} className="h-52" />
      <Fader size="md" label="Muted" defaultValue={25} level={0} ticks={6} disabled className="h-52" />
    </div>
  );
}
```

## API reference

### Fader

className and style go on the outer element, where a height (or a width when horizontal) sets the travel. The ref and every other span attribute go on the cap, which has role="slider".

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number` | — | Controlled value. |
| `defaultValue` | `number` | `min` | Initial value when uncontrolled, and where a double-click on the cap returns to. |
| `onValueChange` | `(value: number) => void` | — | Called on every change while dragging or pressing keys. |
| `onValueCommit` | `(value: number) => void` | — | Called once the cap is let go or a key is released. |
| `min` | `number` | `0` | Lowest value. |
| `max` | `number` | `100` | Highest value. |
| `step` | `number` | `1` | Values snap to multiples of step from min. |
| `orientation` | `"vertical" \| "horizontal"` | `"vertical"` | Direction of travel. Up and right increase the value. |
| `ticks` | `number \| (number \| { value: number; label?: ReactNode \| null })[]` | `11` | A count of evenly spaced marks, or exact marks. A null or empty label prints the line only; 0 hides the scale. |
| `level` | `number` | — | Signal from 0 to 1. Shows an LED meter beside the track when set. |
| `label` | `ReactNode` | — | Engraved legend under the strip. Names the slider unless aria-label is set. |
| `showValue` | `boolean` | `false` | Prints the formatted value under the label. |
| `formatValue` | `(value: number) => string` | — | Formats the printed value and aria-valuetext. |
| `name` | `string` | — | Submits the value with a form through a hidden input. |
| `color` | `NeonColor` | `inherited` | Palette name or hex. Lights the cap's line and the meter's lower segments. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Scales the cap, slot, scale and meter. |
| `disabled` | `boolean` | `false` | Darkens the line and meter, blocks input and removes the cap from the tab order. |

## Accessibility

- The cap has role="slider" with aria-orientation, aria-valuemin, aria-valuemax, aria-valuenow, and aria-valuetext when formatValue is set.
- Arrow keys move by one step, Shift+Arrow and Page Up/Down by a tenth of the range, Home and End jump to the ends.
- Grabbing the cap never makes it jump; pressing the slot glides the cap there and keeps dragging. Hold Shift while dragging for fine control.
- The LED meter is decorative and hidden from screen readers. Announce levels elsewhere if they matter.

## Source

`components/ui/fader.tsx`

```tsx
"use client";

import * as React from "react";

import { cn } from "@/lib/utils";
import { neonVars, type NeonColor } from "@/lib/neon";

// Everything inside the strip is sized in em, so one font size scales the hardware.
const sizes = {
  sm: { root: "text-[12px]", label: "text-[10px]", value: "text-[11px]" },
  md: { root: "text-[14px]", label: "text-[10px]", value: "text-xs" },
  lg: { root: "text-[16px]", label: "text-[11px]", value: "text-[13px]" },
} as const;

const CORE = "color-mix(in srgb, rgb(var(--neon)) 30%, white)";
const METER_SEGMENTS = 18;
const AMBER = "251 191 36";
const RED = "255 77 94";

const settleClass = {
  none: "",
  ease: "transition-transform duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none",
  glide: "transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none",
} as const;

function clamp(value: number, lower: number, upper: number) {
  return Math.min(upper, Math.max(lower, value));
}

function decimalsOf(value: number) {
  return (String(value).split(".")[1] ?? "").length;
}

export type FaderTick = number | { value: number; label?: React.ReactNode | null };

export interface FaderProps
  extends Omit<React.HTMLAttributes<HTMLSpanElement>, "color" | "defaultValue" | "onChange" | "children"> {
  /** Palette name or any hex color. Inherits `--neon-accent` when omitted. */
  color?: NeonColor;
  size?: keyof typeof sizes;
  value?: number;
  defaultValue?: number;
  onValueChange?: (value: number) => void;
  /** Fires once the cap is let go or a key is released. */
  onValueCommit?: (value: number) => void;
  min?: number;
  max?: number;
  step?: number;
  orientation?: "vertical" | "horizontal";
  /** Printed scale: a count of evenly spaced marks, or the exact marks with optional labels. */
  ticks?: number | FaderTick[];
  /** Signal level from 0 to 1. Shows an LED meter beside the track when set. */
  level?: number;
  /** Engraved legend under the strip. Also names the control. */
  label?: React.ReactNode;
  /** Prints the value under the label. */
  showValue?: boolean;
  /** Formats the printed value and the value read to screen readers. */
  formatValue?: (value: number) => string;
  /** Submits the value under this name with a surrounding form. */
  name?: string;
  disabled?: boolean;
}

type Settle = keyof typeof settleClass;

const Fader = React.forwardRef<HTMLSpanElement, FaderProps>(
  (
    {
      className,
      style,
      color,
      size = "md",
      value: valueProp,
      defaultValue,
      onValueChange,
      onValueCommit,
      min = 0,
      max = 100,
      step = 1,
      orientation = "vertical",
      ticks = 11,
      level,
      label,
      showValue = false,
      formatValue,
      name,
      disabled = false,
      id: idProp,
      "aria-label": ariaLabel,
      "aria-labelledby": ariaLabelledBy,
      onKeyDown,
      onKeyUp,
      onDoubleClick,
      ...props
    },
    ref,
  ) => {
    const autoId = React.useId();
    const labelId = `${idProp ?? autoId}-label`;
    const capRef = React.useRef<HTMLSpanElement>(null);
    const railRef = React.useRef<HTMLSpanElement>(null);
    React.useImperativeHandle(ref, () => capRef.current as HTMLSpanElement);

    const [uncontrolled, setUncontrolled] = React.useState(() => clamp(defaultValue ?? min, min, max));
    const isControlled = valueProp !== undefined;
    const value = clamp(isControlled ? valueProp : uncontrolled, min, max);

    const [dragging, setDragging] = React.useState(false);
    const [settle, setSettle] = React.useState<Settle>("none");
    const drag = React.useRef<{ offset: number; last: number; raw: number; start: number } | null>(null);
    const keyChanged = React.useRef(false);

    const vertical = orientation === "vertical";
    const s = sizes[size];
    const range = max - min || 1;
    const decimals = Math.max(decimalsOf(step), decimalsOf(min));
    const fraction = (v: number) => (clamp(v, min, max) - min) / range;
    // Position along the rail from its start (top or left), as a fraction.
    const along = (v: number) => (vertical ? 1 - fraction(v) : fraction(v));

    function snapToStep(raw: number) {
      return clamp(Number((Math.round((raw - min) / step) * step + min).toFixed(decimals)), min, max);
    }

    function setValue(next: number, mode: Settle) {
      if (next === value) return false;
      if (!isControlled) setUncontrolled(next);
      setSettle(mode);
      onValueChange?.(next);
      return true;
    }

    function railMetrics() {
      const rect = railRef.current?.getBoundingClientRect();
      if (!rect) return null;
      const length = vertical ? rect.height : rect.width;
      return length > 0 ? { start: vertical ? rect.top : rect.left, length } : null;
    }

    function valueAt(position: number) {
      const rail = railMetrics();
      if (!rail) return value;
      const t = clamp((position - rail.start) / rail.length, 0, 1);
      return min + (vertical ? 1 - t : t) * range;
    }

    function handlePointerDown(event: React.PointerEvent<HTMLSpanElement>) {
      if (disabled || event.button !== 0) return;
      event.preventDefault();
      const position = vertical ? event.clientY : event.clientX;
      const rail = railMetrics();
      const capPosition = rail ? rail.start + along(value) * rail.length : position;
      const onCap = capRef.current?.contains(event.target as Node) ?? false;
      // Grabbing the cap never makes it jump; pressing the slot glides it there.
      const offset = onCap ? position - capPosition : 0;
      let raw = value;
      if (!onCap) {
        raw = valueAt(position);
        setValue(snapToStep(raw), "glide");
      }
      // Capture on the cap itself when it was grabbed, so a double-click still lands on it.
      (onCap && capRef.current ? capRef.current : event.currentTarget).setPointerCapture(event.pointerId);
      drag.current = { offset, last: position, raw, start: value };
      setDragging(true);
      capRef.current?.focus({ preventScroll: true });
    }

    function handlePointerMove(event: React.PointerEvent<HTMLSpanElement>) {
      const d = drag.current;
      const rail = railMetrics();
      if (!d || !rail) return;
      const position = vertical ? event.clientY : event.clientX;
      if (event.shiftKey) {
        // Fine: a fifth of the travel, and re-anchor so letting go of Shift doesn't jump.
        const delta = ((position - d.last) / rail.length) * range * 0.2;
        d.raw = clamp(d.raw + (vertical ? -delta : delta), min, max);
        d.offset = position - (rail.start + along(d.raw) * rail.length);
      } else {
        d.raw = valueAt(position - d.offset);
      }
      d.last = position;
      setValue(snapToStep(d.raw), "none");
    }

    function endDrag() {
      const d = drag.current;
      if (!d) return;
      drag.current = null;
      setDragging(false);
      if (value !== d.start) onValueCommit?.(value);
    }

    function handleKeyDown(event: React.KeyboardEvent<HTMLSpanElement>) {
      onKeyDown?.(event);
      if (disabled || event.defaultPrevented) return;
      const big = Math.max(step, Math.round(range / 10 / step) * step);
      const arrow = event.shiftKey ? big : step;
      const delta: Record<string, number> = {
        ArrowUp: arrow,
        ArrowRight: arrow,
        ArrowDown: -arrow,
        ArrowLeft: -arrow,
        PageUp: big,
        PageDown: -big,
        Home: -Infinity,
        End: Infinity,
      };
      if (!(event.key in delta)) return;
      event.preventDefault();
      if (setValue(snapToStep(clamp(value + delta[event.key], min, max)), "ease")) keyChanged.current = true;
    }

    function handleKeyUp(event: React.KeyboardEvent<HTMLSpanElement>) {
      onKeyUp?.(event);
      if (!keyChanged.current) return;
      keyChanged.current = false;
      onValueCommit?.(value);
    }

    const text = formatValue ? formatValue(value) : String(value);
    const lit = !disabled;

    const marks = (() => {
      if (Array.isArray(ticks)) {
        return ticks.map((tick) => {
          const t = typeof tick === "number" ? { value: tick } : tick;
          // An explicit null or empty label prints the line without a number.
          const label = t.label === undefined ? String(t.value) : t.label || null;
          return { value: t.value, label, major: true };
        });
      }
      const count = Math.max(0, Math.floor(ticks));
      if (count < 2) return [];
      const every = count > 6 ? 2 : 1;
      return Array.from({ length: count }, (_, i) => {
        const v = Number((min + (i / (count - 1)) * range).toFixed(decimals));
        const major = i % every === 0 || i === count - 1;
        return { value: v, label: major ? String(v) : null, major };
      });
    })();

    const segments = level === undefined ? 0 : METER_SEGMENTS;
    const litSegments = clamp(level ?? 0, 0, 1) * segments;

    // Half the cap's length: the rail and scale are inset by this so the cap never overhangs.
    const half = vertical ? "inset-y-[0.8em]" : "inset-x-[0.8em]";

    return (
      <div
        style={neonVars(color, style)}
        className={cn(
          "[--neon:var(--neon-accent,34_211_238)]",
          "inline-flex select-none flex-col items-center gap-2.5",
          vertical ? "h-60" : "w-60",
          disabled && "opacity-50",
          className,
        )}
      >
        <div
          className={cn(
            "flex min-h-0 min-w-0 flex-1",
            vertical ? "flex-row items-stretch" : "w-full flex-col items-stretch",
            s.root,
          )}
        >
          {marks.some((mark) => mark.label !== null) && (
            <span
              aria-hidden
              className={cn(
                "relative shrink-0 text-[0.62em] leading-none tabular-nums text-white/50",
                vertical ? "order-first mr-[0.5em] w-[2.6em]" : "order-last mt-[0.55em] h-[1em]",
              )}
            >
              <span className={cn("absolute", vertical ? "inset-x-0 inset-y-[1.3em]" : "inset-y-0 inset-x-[1.3em]")}>
                {marks.map((mark, i) =>
                  mark.label === null ? null : (
                    <span
                      key={i}
                      className={cn(
                        "absolute whitespace-nowrap",
                        vertical ? "right-0 -translate-y-1/2" : "top-0 -translate-x-1/2",
                      )}
                      style={vertical ? { top: `${along(mark.value) * 100}%` } : { left: `${along(mark.value) * 100}%` }}
                    >
                      {mark.label}
                    </span>
                  ),
                )}
              </span>
            </span>
          )}

          <span
            className={cn(
              "relative touch-none",
              vertical ? "w-[2.9em] self-stretch" : "h-[2.9em] w-full",
              disabled ? "cursor-not-allowed" : "cursor-pointer",
            )}
            onPointerDown={handlePointerDown}
            onPointerMove={handlePointerMove}
            onPointerUp={endDrag}
            onPointerCancel={endDrag}
          >
            {/* Printed scale lines; the slot and cap sit on top of them */}
            <span aria-hidden className={cn("pointer-events-none absolute", vertical ? `inset-x-0 ${half}` : `inset-y-0 ${half}`)}>
              {marks.map((mark, i) => (
                <span
                  key={i}
                  className={cn(
                    "absolute",
                    mark.major ? "bg-white/[0.22]" : "bg-white/[0.11]",
                    vertical
                      ? cn("h-px -translate-y-1/2", mark.major ? "inset-x-[0.15em]" : "inset-x-[0.6em]")
                      : cn("w-px -translate-x-1/2", mark.major ? "inset-y-[0.15em]" : "inset-y-[0.6em]"),
                  )}
                  style={vertical ? { top: `${along(mark.value) * 100}%` } : { left: `${along(mark.value) * 100}%` }}
                />
              ))}
            </span>

            {/* The slot */}
            <span
              aria-hidden
              className={cn(
                "pointer-events-none absolute rounded-full bg-[#020304] shadow-[inset_0_0.1em_0.2em_rgb(0_0_0/0.95),0_1px_0_rgb(255_255_255/0.07),0_0_0_1px_rgb(0_0_0/0.5)]",
                vertical
                  ? "inset-y-[0.35em] left-1/2 w-[0.32em] -translate-x-1/2"
                  : "inset-x-[0.35em] top-1/2 h-[0.32em] -translate-y-1/2",
              )}
            />

            <span ref={railRef} className={cn("pointer-events-none absolute", vertical ? `inset-x-0 ${half}` : `inset-y-0 ${half}`)}>
              <span
                className={cn("absolute inset-0", settleClass[settle])}
                style={{
                  transform: vertical
                    ? `translateY(${along(value) * 100}%)`
                    : `translateX(${along(value) * 100}%)`,
                }}
              >
                {/* Light from the cap's line falling on the panel */}
                <span
                  aria-hidden
                  className={cn(
                    "absolute rounded-full bg-[radial-gradient(closest-side,rgb(var(--neon)/CALC(0.24*var(--neon-glow,1))),rgb(var(--neon)/CALC(0.06*var(--neon-glow,1)))_55%,transparent)] transition-opacity duration-200",
                    vertical
                      ? "left-1/2 top-0 h-[3.4em] w-[6em] -translate-x-1/2 -translate-y-1/2"
                      : "left-0 top-1/2 h-[6em] w-[3.4em] -translate-x-1/2 -translate-y-1/2",
                    lit ? (dragging ? "opacity-100" : "opacity-70") : "opacity-0",
                  )}
                />
                <span
                  ref={capRef}
                  id={idProp}
                  role="slider"
                  tabIndex={disabled ? -1 : 0}
                  aria-label={ariaLabel}
                  aria-labelledby={ariaLabelledBy ?? (label && !ariaLabel ? labelId : undefined)}
                  aria-valuemin={min}
                  aria-valuemax={max}
                  aria-valuenow={value}
                  aria-valuetext={formatValue ? text : undefined}
                  aria-orientation={orientation}
                  aria-disabled={disabled || undefined}
                  data-state={dragging ? "dragging" : undefined}
                  data-orientation={orientation}
                  {...props}
                  onKeyDown={handleKeyDown}
                  onKeyUp={handleKeyUp}
                  onDoubleClick={(event) => {
                    onDoubleClick?.(event);
                    if (disabled || event.defaultPrevented || defaultValue === undefined) return;
                    const next = snapToStep(clamp(defaultValue, min, max));
                    if (setValue(next, "glide")) onValueCommit?.(next);
                  }}
                  className={cn(
                    "pointer-events-auto absolute block rounded-[0.22em] border border-black/80 outline-none",
                    "focus-visible:ring-2 focus-visible:ring-[rgb(var(--neon)/0.6)] focus-visible:ring-offset-2 focus-visible:ring-offset-[#05060a]",
                    "shadow-[0_0.45em_0.7em_-0.1em_rgb(0_0_0/0.85),0_0.12em_0.2em_rgb(0_0_0/0.7),inset_0_1px_0_rgb(255_255_255/0.2)]",
                    vertical
                      ? "left-1/2 top-0 h-[1.6em] w-[2.9em] -translate-x-1/2 -translate-y-1/2 bg-[linear-gradient(180deg,#4b515e,#2a2e37_40%,#15171c_50%,#1d2027_60%,#2b2f39)]"
                      : "left-0 top-1/2 h-[2.9em] w-[1.6em] -translate-x-1/2 -translate-y-1/2 bg-[linear-gradient(90deg,#4b515e,#2a2e37_40%,#15171c_50%,#1d2027_60%,#2b2f39)]",
                    disabled ? "cursor-not-allowed" : dragging ? "cursor-grabbing" : "cursor-grab",
                  )}
                >
                  {/* Grip ribs on both slopes of the cap */}
                  {(["start", "end"] as const).map((side) => (
                    <span
                      key={side}
                      aria-hidden
                      className={cn(
                        "absolute",
                        vertical
                          ? cn(
                              "inset-x-[10%] h-[28%] bg-[repeating-linear-gradient(180deg,rgb(0_0_0/0.5)_0_1px,rgb(255_255_255/0.07)_1px_2px,transparent_2px_3.5px)]",
                              side === "start" ? "top-[9%]" : "bottom-[9%]",
                            )
                          : cn(
                              "inset-y-[10%] w-[28%] bg-[repeating-linear-gradient(90deg,rgb(0_0_0/0.5)_0_1px,rgb(255_255_255/0.07)_1px_2px,transparent_2px_3.5px)]",
                              side === "start" ? "left-[9%]" : "right-[9%]",
                            ),
                      )}
                    />
                  ))}
                  {/* The lit line */}
                  <span
                    aria-hidden
                    className={cn(
                      "absolute rounded-full transition-[background-color,box-shadow] duration-200",
                      vertical
                        ? "inset-x-[7%] top-1/2 h-[0.13em] min-h-[1.5px] -translate-y-1/2"
                        : "inset-y-[7%] left-1/2 w-[0.13em] min-w-[1.5px] -translate-x-1/2",
                      lit
                        ? dragging
                          ? "shadow-[0_0_0.2em_rgb(var(--neon)/var(--neon-glow,1)),0_0_0.6em_rgb(var(--neon)/CALC(0.9*var(--neon-glow,1)))]"
                          : "shadow-[0_0_0.15em_rgb(var(--neon)/var(--neon-glow,1)),0_0_0.45em_rgb(var(--neon)/CALC(0.7*var(--neon-glow,1)))]"
                        : "bg-[rgb(var(--neon)/0.16)]",
                    )}
                    style={lit ? { backgroundColor: CORE } : undefined}
                  />
                </span>
              </span>
            </span>
          </span>

          {segments > 0 && (
            <span
              aria-hidden
              data-meter=""
              className={cn(
                "relative flex shrink-0 gap-[0.14em]",
                vertical ? "ml-[0.55em] w-[0.42em] flex-col-reverse py-[0.35em]" : "order-first mb-[0.55em] h-[0.42em] flex-row px-[0.35em]",
              )}
            >
              {Array.from({ length: segments }, (_, i) => {
                const zone = i >= segments - 2 ? RED : i >= segments - 5 ? AMBER : "var(--neon)";
                const on = lit && litSegments > i;
                return (
                  <span
                    key={i}
                    data-lit={on ? "" : undefined}
                    style={{ "--seg": zone } as React.CSSProperties}
                    className="relative flex-1 rounded-[1px] bg-[rgb(var(--seg)/0.13)]"
                  >
                    <span
                      className={cn(
                        "absolute inset-0 rounded-[1px] bg-[rgb(var(--seg))] shadow-[0_0_0.35em_rgb(var(--seg)/CALC(0.75*var(--neon-glow,1)))]",
                        on ? "opacity-100" : "opacity-0 transition-opacity duration-300 ease-out",
                      )}
                    />
                  </span>
                );
              })}
            </span>
          )}
        </div>

        {label && (
          <span
            id={labelId}
            onClick={() => {
              if (!disabled) capRef.current?.focus();
            }}
            className={cn("font-medium uppercase leading-none tracking-[0.16em] text-white/55", s.label)}
          >
            {label}
          </span>
        )}
        {showValue && (
          <span
            aria-hidden
            className={cn(
              "leading-none tabular-nums transition-[color,text-shadow] duration-150",
              dragging ? "text-[rgb(var(--neon))] [text-shadow:0_0_8px_rgb(var(--neon)/CALC(0.6*var(--neon-glow,1)))]" : "text-white/80",
              s.value,
            )}
          >
            {text}
          </span>
        )}
        {name && <input type="hidden" name={name} value={value} disabled={disabled} />}
      </div>
    );
  },
);
Fader.displayName = "Fader";

export { Fader };
```
